決済処理のマイクロサービス化における3つの課題

斎藤祐一郎氏(以下、斎藤):マイクロサービス化の話題が盛り上がって、数年が経ちました。みなさまは、さまざまな論文や海外での大規模な導入事例に触れながら、導入の検討、及び実際に導入を進められていらっしゃるはずです。

そんな最新の事情に関心のあるアクティブなソフトウェアエンジニアのみなさんにお越しいただけたことを光栄に思います。ありがとうございます。今日は私たちがいままさに開発を進めているマイクロサービスの事例について、お話していきます。

メルカリでは、メルペイという金融サービスを鋭意開発中です。これはメルカリのアプリのなかに新しいサービスを組み込む、大きなプロジェクトになります。そのなかでメルペイの残高をメルカリの電子決済時に利用するための仕組みの開発が、必要になってきました。

一方で、いままでのメルカリは引き続き提供されます。いまできることはそのままに、さらに電子決済に対応していく必要があります。そのために、私たちは決済処理をマイクロサービス化することを決断いたしました。

お腹が痛そうな人がいますね。まさに僕もいまお腹が痛いんですけども(笑)。

(会場笑)

はい、これは私です(笑)。

(会場笑)

ありがとうございます。他にもお金の決済処理と聞いただけでお腹が痛くなる方は、多くいらっしゃると思います。今回、決済処理のマイクロサービス化にあたり、3つの課題と解決方法をお話いたします。

まず(1つ目)は現状把握です。組織の拡大に伴う開発のスケーラビリティの確保が、喫緊の課題となっていました。加えて、モノリシックで複雑に絡み合ったコードが、機能追加を妨げていました。

2つ目は分散トランザクションです。それぞれのマイクロサービスが独立して動くため、データベースのトランザクションのように失敗したら直ちにロールバックというわけにはいきません。そこで、技術の面でどう立ち向かうかを考えねばなりません。

3つ目は移行です。新しいシステムの開発をする時は、例外なく発生する事項です。古いシステムから新しいシステムへ、齟齬なくデータを維持できなければなりません。

ソースコードは古びた温泉旅館のように

それでも決済処理のマイクロサービス化を進めるのは、なぜなのでしょうか? 組織とシステムを疎結合にすることで、関心を分離し、責務をはっきりさせ、保守性を向上させる、それが目的です。

拡大し、コミュニケーションを取るのが難しくなる開発組織のなかで、それぞれのシステムが疎に結合するマイクロサービスアーキテクチャは、私たちにとっても良い仕組みでした。そして、私たちがGo Boldになめらかな決済サービスを展開すべく、決済処理をマイクロサービス化させるのは必然とも言えるのです。

その前に、ご紹介が遅れました。私、メルカリのグループ会社であるメルペイに勤めているバックエンドソフトウェアエンジニア、「koemu」こと、斎藤 祐一郎と申します。

メルカリには2016年1月に入社し、これまでUS/JP版のメルカリの決済、配送、不正対策の開発に携わってまいりました。そして今年から、メルペイで決済基盤の開発に携わっています。

では、なぜマイクロサービス化に踏み切ったかをお話していきます。メルカリのバックエンドシステムは、モノリシックなアプリケーションです。そして、多くのお客さまにご利用いただいているなかで、さまざまな機能追加を行ってきました。

さらに、昨年にソースコードを国ごとにフォークしていく前は、JP版ばかりでなくUS版、UK版のソースコードも、折り重なって実装されていました。それはまるで古びた温泉旅館のようです。

サービスも成長しソフトウェアエンジニアが増えるなかで、このままでは開発業務はスケールしません。保守性も低下していきます。

なお、決済以外の話は他の方もされています。私のセッションでは、とくにメルカリ内での決済処理について触れていきます。

巨大な条件処理

決済処理の改変は多くの方が怖がるでしょう。私も怖いです。なぜなら、ミスをすると会社の収入がなくなってしまうからです。大損害ですね。

また、開発にあたっては、決済ネットワーク事業者さまの仕様とメルカリのサービスの仕様、どちらにも精通している必要があり、たくさんの業務知識を必要としています。こうなると、触れる人がどんどん減っていきます。会社のサービスは大きくなっているのに触れる人が限られていることは、組織運営上良くないことです。

そして、拡張に拡張を重ねた決済処理は保守性が低下しており、これ以上の拡張の余地はほぼありませんでした。この度電子決済サービスを立ち上げるにあたり、これが大きなネックとなっていました。

では、温泉旅館の状態とは具体的にどんなもんか、一例をご紹介していきます。それは決済種別に、手段別に分岐していく、巨大なswitch文です。とりあえず、成長を追うごとに、コードを見ていきましょう。

これらは疑似コードで、実際は長いコードです。これをもとに、みなさんの頭のなかでイメージを膨らませていただければ幸いです。

はい。カード決済の頃は、こんな感じだったのでしょう。ネットワーク事業者を通じて決済ができたら、売り手と買い手の交流を成立させればいいのです。あまり難しくはありません。

次に、コンビニ決済が登場しました。

各種コンビニエンスストアやATMで、微妙にデータの出力値具合が違ってきます。また、決済から入金までのタイミングが、数分ないし数日単位で開きます。このタイムラグを考慮した処理が必要となりそうです。すなわち、買い手から入金されるまでは、売り手は配送手配を始められないようになっているのです。決済のための処理が少しずつ難しくなってきました。

続いて登場したのが、日本ではお馴染みの携帯電話キャリア決済です。

お客さまが何事もなく携帯キャリアの決済ページでお金をお支払いいただければいいのですが、もし通信が途切れて決済画面から離脱してしまったら、その後の処理はどうなるでしょうか?

リファクタリングではなく、マイクロサービスを選んだ理由

さあ、この後処理を追加してっと、やあ、さらに難しくなってまいりました。そして、メルカリでは月イチ払いという支払い手段を、今年に入ってから一部のお客さまにご提供を始めました。

これは一定の与信枠のなかで商品を購入することができ、後でコンビニなどで代金を支払うことができる仕組みです。

さて、与信枠の評価などなどしてから購入ができるようになるのですが、この間にも一筋縄ではいきません。それでも僕らは業務分析を続けました。実はこれ以外にも、USとUKの決済処理もあるんですが、話が長くなるだけなので省略します。

はい。(あり得)ないですよね? でも、これ。もうこの時点でない! ここにさらにメルペイの支払いってcase文を追加する。やりたいですか? みなさん。ないですね。僕は絶対やりたくありません。遠慮しときます。

改めて、私たちが抱えている問題点を整理していきます。

まず、現状の決済処理の構造は、このままでは拡張性に乏しい状況です。しかし、これだけの理由では、マイクロサービス化の理由にはなりません。なぜなら、プログラムの構造が悪いだけなら、リファクタリングを行うことで構造を整理できるからです。

ここで、私がはじめにお話したことを思い出してください。私たちには、開発組織の拡大に伴う開発プロジェクトの運営の問題がありました。そうです。人が増えるとコミュニケーションコストが上がり、コードを書くことに力が注ぎづらくなります。

そこで、決済処理をマイクロサービス化していくことで、他のシステムとは疎な結合をすることができます。同時に開発組織の構造も疎にすることができるため、他の開発への影響も少なくできることが期待できます。組織の問題は、リファクタリングだけでは解決できない問題だったのです。

分散システムならではの問題に直面した

そして、3つの結果を得ることができました。

1つ目は保守性の向上です。多態性も想定したコードに書き換えることで、コードの見通しが良くなります。テストコードもこのタイミングで充実させることで、リファクタリングも恐れず行うことができます。実際、開発中にリファクタリングも行っていますが、テストが充実していることで、問題を即時に発見できるようになっています。

2つ目は、責務が分離できました。モノリシック、かつ、コード上でも密に結合していた決済処理が、マイクロサービスというかたちで疎に結合することができるようになります。

3つ目は、決済手段を容易に増やせるようになったことです。もとのメルカリの決済処理から切り離したことから、メルペイ側の判断で柔軟に決済処理を追加することができるようになりました。

これらの作業を通じて、より多くの仲間が決済処理について理解を深めることができるようになりました。それはまるで伊勢神宮の式年遷宮のようです。

さて、話は変わります。マイクロサービス化にあたって、避けることのできない分散トランザクションのお話です。「マイクロサービス化で、開発組織の拡大の問題とモノリシックでレガシーなコードを整理できる。あー、良かったー!」、そう思った日は私にもありました。

しかし、喜ぶには早かったようです。

どのようなシステムでも、何事もなく処理が進めばそれでいいのです。しかし、残念なことに、エラーは起こります。残念。とくにマイクロサービスをはじめとした計算機が分かれる分散型のシステムでは、各々が独立した原因で問題が発生することが、容易に想像できます。ここで一度、シンプルなクレジットカードの決済を見てみましょう。

はい。モノリシックなシステムでは、データベースのトランザクションを使うことで、問題が発生した時はロールバックをしてしまえば、問題がない状態まで容易に復元が可能でした。

決済処理とポイント処理のどちらかが失敗する状況もあり得る

では、マイクロサービス化する、すなわち分散システムとなるとどうなるでしょうか? 1つ目の問題、分散システムですので当然ですが、処理の結果はそれぞれのシステムで独立です。ステートレスなマイクロサービスなら問題がないのですが、決済処理のようなステートフルな処理だったらどうなるでしょうか?

分散型の決済処理を導入すると……“payment->consume()”、決済実行ですね、決済実行は読み出し元の状態にかかわらず実行されます。

前提として、メルカリのポイントデータは、これまでメルカリのデータベースにありました。決済処理が分散処理化されると、カード決済とともにポイント処理も独立した処理になります。

そこで片方だけが失敗した場合、一体どうなるでしょうか? 正しく決済のエラーをハンドリングできず、例えばカード決済の処理は成功しているのに、ポイントの決済の引き落としが失敗するような事象が発生する可能性があるのです。これではダメですね。

ここで、私たちは次の解決策を用いました。1つ目は、呼び出し側でトランザクションの段階を分けることにしました。

クレジットカードの決済の仕組みをご存知の方ならお馴染みですね。私たちのシステムもそれに近いです。まずは支払いの枠を押さえます。支払いの枠を適切に押さえたら、そのうえで呼び出し側の処理、すなわち購入に関わる処理を進めていきます。最後に、問題がなければ確定していきます。

また、この処理は冪等性を担保しています。すなわち購入処理が途中で失敗して、改めてお客さまが購入処理を行った場合でも、二重に課金されることはありません。古くからある分散トランザクションの解決方法です。

通信経路での障害、決済サービスの処理エラー、ネットワーク事業者サイドのエラーなど

さて、もう1つの問題を見てみましょう。決済のマイクロサービスであるペイメントサービス、こちらに中途半端な障害が起こったらどうなるでしょうか? はい、またソースコードになります。

決済実行である“payment->consume()”は、別のシステムであるペイメントサービスをネットワーク越しに呼び出しています。ここ重要です。ネットワーク越しに呼び出しています。

「ネットワーク越し」っていうのは、うちの会社のシステム構成図をご存知の方がいらっしゃるかもしれませんが、いまのモノリスのシステムがさくらインターネットさんにあります。各種マイクロサービスはまた別のところ(Google Cloud Platform)にあります。通信を、物理的に離れたところと行う必要があるんですね。

それぞれのシステムは同じネットワークじゃないという前提でシステムを組んでる、ということをちょっと留意していただければと思います。

ここでペイメントサービスが次の状態になったら、どうなるでしょうか? 通信経路に障害が起きていたら、つまり先ほど申し上げましたデータセンターが別という想定も十分に入れたうえで、なにかが起こった場合ということです。

あと、ペイメントサービスの処理が追い付いていなかったら。そうですね、ペイメントサービスのアーキテクチャはスケーラブルなかたちを担保してはいるのですが、それでも耐えられないということは、決済ですから十分に想定をする必要があります。

そして、ネットワーク事業者さま側で処理が遅延していたら。ネットワーク事業者さま、要するに決済を実際に、カードのデータとかやりとりするところですね。当社の側に仮に問題がなくても、ネットワーク事業者さまのほうでもし詰まった場合でも、お客さまにはなんかしら、中途半端な状態で返すというわけにはいきません。ダメならダメ、OKならOKという状態を担保する必要があります。そうしないとお金が、決済がズレてしまいます。

以上のことがあっても、実際、正常に終了はするかもしれません。ここまでお話して、みなさま勘がいい方ばかりだと思うんでご理解いただけるかと思うんですが、お客さまの決済の体験は悪化してしまう可能性があります。いやー、やっぱりこれは良くないですね。

ですので、処理の一貫性を担保するために、呼び出されるペイメントサービスにも工夫をしています。それがステートマシンと私たちが呼んでいるものです。

ステートマシンはリクエストごとの状態を管理しながら、処理を進めることができます。

ステートマシンには3つ特徴があります。まず、一時的な失敗の場合は、その状態となる処理だけ再試行を何度でもすることができます。次に、復帰不能な処理が発生した場合は、補償トランザクションを実行して処理を巻き戻すことができます。実はこのテストを書くのが、一番難しい作業の1つになっています。

新旧のシステムを連携的に並行稼動させて移行

また、プログラミング言語はGoを使っていますので、Goを用いている特性を活かして、並列処理を柔軟に行うことができます。したがって、計算機の性能が間に合う限り、並列度を高められ、可用性を担保することができます。データベースもCloud Spannerを使っていまして、併せて可用性を担保しています。

はい、閑話休題、システムの移行には、旧システムと新システム、どのようにつなぐか、この問題が発生します。私たちの場合、会計システムとどうやってつなぐか、連携するかが課題になりました。システムは、いきなりは全部は移行しません。並行稼動期間が存在します。その際に会計の集計システムにつながるデータが、変わりなく保存されている必要があります。

このようなことも丁寧にやる必要があります。「忘れてませんかー?」「大丈夫です、忘れてません」。

私たちはシンプルに会計の記録を並行書き込みする、この方法を取りました。バラバラに取りにいくことは、現行の会計システムに負担をかけることになるからです。

ペイメントサービスで決済した場合は、ネットワーク事業者からの結果をペイメントサービスに一度記録して、同じ情報を現在のシステムにも並列で書き込みます。この連携はPub/Subを使って行っています。

また、現在のシステムで決済した場合は、引き続き現在のシステムでも書き込みを行っていきます。これで完全に移行するまでは、現在のシステムにすべてのデータが集まっていることになります。

はい。ここまで、メルカリの決済処理をマイクロサービス化するにあたり、いままさに取り組んでいることをご紹介いたしました。いままさになので、このことをお話するのに少し私は躊躇しました。しかし、知見を出すということに、それが自らにノウハウを定着させることだと信じ、お話させていただくことを決めました。

では、これまでにお話した3つの点、組織的な問題の解決、分散トランザクション、そして、移行について、まとめに入っていきます。

まず1つ目、メルカリのバックエンドシステムは、まさに1つのモノリスでした。

それを決済処理をマイクロサービス化するということで、組織とシステムを疎に結び付けることができ、保守性を高めることができるのです。

具体的には、責務の分割、柔軟な拡張、そして、属人性の排除です。

一人ひとりがこれでBe Professonalに働けるようになっていくはずです。これはさまざまなマイクロサービス開発で言われていることと、とくに変わりはありません。

マイクロサービス化のこれから

しかし、マイクロサービスありきでマイクロサービス化を選択したのではありません。開発組織の肥大化を抜本的に解決すべく、組織とシステムを疎に結合し、開発速度を高く保つことが、私たちには求められていたからこその選択だったからです。リファクタリングだけでは解決することが難しいと判断したんです。

2つ目、分散トランザクションと立ち向かうために、私たちは2つの仕組みを構築しています。

1つ目は冪等性を担保した処理、2つ目はステートマシンを用いた再試行処理と補償トランザクションの処理です。実際、私たちの開発のほとんどは分散トランザクションと立ち向かっていくことに費やされている、と言っても過言ではありません。

3つ目、移行期間中は現行と新システムで並行書き込みを実施します。これにより、会計システムへの影響を最小化することができます。

はい。改めてお伝えするのですが、このシステム、まだ完成はしていません。そして、完成してもみなさまの目の前には、たぶんほぼ見えないと思います。そんななかでも、「信用を創造して、なめらかな社会を創る」というメルペイのミッション、これを達成すべく、基盤のシステムとなれるよう、いまもAll for Oneで一生懸命作っています。

ちなみに、メルカリのAll for Oneは「みんなで仲良く」ではありません。Oneのところにポイントが置かれていて、「1つの結果、1つの目標に対して、全員が一丸となって立ち向かっていく」という意味があります。

後世、もしメルカリがもっとおもしろい施策、それがあるとしたならば、もしより多くの決済をさばいていてもビクともしていなければ、それは「マイクロサービス化がうまくいっているのかな」、そんなことを思い出していただければうれしいです。

(スライドを指して)一部の素材は、みなさまお馴染みのいらすとやさんから拝借いたしました。

(会場笑)

はい。質問がありましたら、Ask the Speakerコーナー、大きなトラックへ向かい、さらに向こうですね、あちらのほうに座って……。この後、この講演が終わって5分か10分後に行ってますので、もしよろしければ質問にいらしてください。お待ちしております。この度はご清聴ありがとうございました。以上です。

(会場拍手)