PayPayの分散システムの裏側

Harsh Prasad氏:みなさん、こんばんは。PayPayのペイメントコアシステムを担当している、プラサド・ハーシュと申します。今日お話しさせていただきたいことは、ペイメントマイクロサービスとして提供しているバックエンド・フロントエンドのどこに課題があるか、どんなことを改善しないといけないかを説明させていただきます。

簡単に自己紹介をすると、2013年にインドのIIT(インド工科大学)を卒業して、Yahoo! JAPANに2014年に入社しました。趣味は撮影と旅行とハイキングで、肉大好きな人です。

今日話したいこととして、まずは、なぜマイクロサービスを選択したか。そしてペイメントプラットフォームアーキテクチャの説明もさせていただきたいと思ってます。あとは、現在分散システムを使ってるので、そのシステムの課題と、整合性の問題をどのように強化するか、大量のリクエストが来ても正確な処理をするために、どのように対応すればいいか。こういったことを話していきたいと思ってます。

なぜマイクロサービスを採用したのか?

まず、「なぜマイクロサービスなのか?」についてです。マイクロサービスがいくつもあると、失敗する箇所も増えていきます。また、エラー処理や管理がひどくなるとも言われています。

ですが、マイクロサービスの場合、大規模のService-Oriented Applicationsシステムを分散サービスとして提供すれば、そういった課題はなくなります。

逆に、決済処理のそれぞれの部分を独立して動かすこともできますし、開発スピードとリリースも早くなるので、一部のマイクロサービスだけをすぐに更新して、ほかのところに影響がない状況を確保することもできます。

あとは、マイクロサービスが古くなったりあまり使いたくなくなったときには、すぐ捨てることも可能です。各マイクロサービスの役割と開発者の役割が分散するので、マイクロサービスアーキテクチャのほうが快適だと感じています。

現在、決済だけをちゃんと同期させたいと思ってるんですが、決済に関係ないとこが決済に影響しないように、別のマイクロサービスで動かすことも可能です。

また、いろいろなビジネスコンテキストに応じて開発が可能です。新しいマイクロサービスをどんどん追加して、新しい機能もどんどん提供できるようになっています。こうしたマイクロサービス開発を行いながら、いろいろな分野について知っている人と、ビジネスコンテキストを理解している人で、Smart, Cross Functional Teamが誕生しました。

PayPayのペイメントプラットフォームの構成

続いて、PayPayのペイメントプラットフォームアーキテクチャについてお話しします。

こちらの画像のように、いろいろなコンポーネントがありますが、15個以上のマイクロサービスはペイメントのプラットフォームアーキテクチャの中に入っています。

それぞれ説明すると15分では足りないので、簡単に説明したいと思ってます。

まず、トランザクションシステムというマイクロサービスが先頭にあります。これは、決済のリクエストを受け取るコンポーネントです。トランザクションシステムの役割は、取引番号の発番をしたり、二重決済の監視やエラー処理、冪等性(べきとうせい)の管理などを担っています。

あとキャッシャーというコンポーネントは、一応ステートは持ってないコンポーネントです。この決済やこの取引は進んでいいのかを判断しています。ユーザーの履歴やほかのところもすべて確認して、「この取引で進めていい」という判断をしたら、ペイメントシステムに決済のリクエストを流してウォレットからお金を動かしたり、銀行やクレジットカードシステム、外部のウォレットなど、さまざまなサービスと連携して、ちゃんと決済が完了できるようにしてます。

決済に直接関係がないところは、すべてメッセージキュー経由で情報を得て自分のビューを作るなど、それぞれの役割を果たしてます。

例えばリードビューというコンポーネントは、決済履歴を作るコンポーネントです。現在、それぞれのマイクロサービスは自分のデータベースを持ってます。そのため、決済に関する情報がいろいろなマイクロサービスに分散しているので、それを全部収集・集計してユーザーに表示しなければいけません。

そのために、リードビューというシステムが、さまざまなマイクロサービスからイベントを収集して、ユーザーに見せたい情報を集計して、DBに書き込んでます。

同じくマーチャント、振込システムも直接DBから情報を引っ張ってくるのではなく、Kafkaのメッセージキューのイベントを使っています。過去のマーチャントに対して、いくら振り込むべきか、いつ振り込むべきか、独立して動くようにできてます。

一平さんが説明したキャッシュバックシステムと同じく、決済が走ったらそれぞれのイベントを収集して、何をいくら付与したらいいか、全部判断できます。

これを全部、ペイメントがプラットフォームを通して提供すれば、その上でいくつもの新しいフィーチャーをどんどん追加しても、ペイメントのAPIだけを叩いて新しい機能の追加ができます。

例えば銀行からのチャージも、ペイメントとしては何も修正せずに新しいフィーチャーとして提供したら、「この銀行からチャージしたい」にCDだけ出せば、全部うまく動くと思います。

分散システムの課題

ですが、いろんな分散システムを使っていて、課題もいくつか存在しています。一番大きな課題は、データの整合性の担保です。マイクロサービスAでは情報は正しく整理されているものの、マイクロサービスBはそれ知らないとか、エラーが発生してもちゃんと処理できないといった問題があります。

あと、1つの決済に対して数十個のマイクロサービスを通して決済を完了しないといけないんですが、E2Eテストだと正常ケースはけっこう速めに、簡単にできるんです。ですが異常のテストは難しくて、いろいろなコンポーネントで異常が発生する可能性があるのか、テストの自動化はけっこう大変です。

マイクロサービスのデザインパターンとして、イベントソーシングで非同期のメッセージを収集して、それでビューを作ったり、ほかの処理をしています。イベントソーシングのリライアビリティについてはKafkaを使ってるんですが、Kafkaでメッセージがなくなったり、Kafkaにバックできなかったり。そういったところを検知していかなければ、不整合が起こってしまいます。

整合性の強化

非同期処理でもいろいろなことが発生します。例えばペイアウト、マーチャントで振込しているところも非同期で全部やっているのですが、それがちゃんとマーチャントに振込できたかどうか、その正確性も確認しなければいけません。そのため、整合性の強化が必要になってきました。

まずデータの整合性を強化するために、必ずペイメントシステムとして持っていなければいけないのは冪等性です。

ペイメントシステムの中にはいろいろなマイクロサービスがありますが、すべてのマイクロサービスが相互に突合を行っています。突合はreconciliationとも言いますが、マイクロサービスBにあるステートとマイクロサービスAにあるステートが一致しているかどうかを必ずチェックしなければ、いつか絶対に不整合が出てしまいます。

そのため、ペイメントシステムとしても、リアルタイムの突合を行っています。また、1日とか半日のバッチで定期で突合をやっていて、情報の状態がずれてないことを確認しています。ずれていたら補償トランザクション、compensationですべての情報のステートと状態が正しくなるようにしています。

compensationについてですが、いろいろなマイクロサービスが他のマイクロサービスや外部のサービスと連携をしていますが、その際に、タイムアウトが発生する可能性があります。外部サービスの不具合によってもタイムアウトが発生する可能性があるので、その際にすべて手動で対応するのは難しいです。そのため、各マイクロサービスがこの補償トランザクション、compensationを持って、次の層までこのcompensationの取引を渡せるような仕組みを持ってます。

ペイメントシステムやトランザクションシステムに何かあったら、トランザクションシステムが補償トランザクションを使って、ペイメントシステムまで渡す役割を持ってます。ペイメントシステムが受け取ったらトランザクションシステムの役割が終わって、ペイメントシステムがそれをウォレットまで渡します。また、エクスターナルサービス、外部のサービスにも渡す役割を持ってます。

こうした仕組みによって簡単に、それぞれのマイクロサービスが補償トランザクションを提供しておけば、自動で不整合をなくすことが可能です。

大量のリクエストを正確に処理する

現在、ビジネス側でもいろいろな企画を行っていますが、キャンペーンを行った際などに、大量のリクエストがいきなり発生することがあります。それらの突発的なリクエストに対して、いちいちシステムが安定しているか、システムが耐えられるかどうかを確認するのは難しいので。そのため、いきなり大量のリクエストが来てもシステムが正確に処理できるような仕組みを作りたいと考えています。

もちろんロードバランサーやCDCR(Cross Data Center Replication)などは必須です。複数データセンターとアベイラビリティゾーンに同じ情報を同期しています。これのもっとも重要でおすすめなのが、Fail Fastやサーキットブレーカーを使うことです。

サーキットブレーカーは、決済が失敗したりタイムアウトしたり、そういった異常を各マイクロサービスが自分で検知するようにしています。マイクロサービスBは今は反応が悪い、という場合はすぐに折り返して、リクエストがすぐに失敗するようにしています。

システム全体が死んでしまうと、決済も何も、ほかのことができなくなってしまうので、すぐに折り返すことは重要です。1つのマイクロサービスが使えなくなったとしても、そのマイクロサービスにもうリクエストを送らないようにすることで、相手のマイクロサービス側に復活する余裕が生まれます。そのため、サーキットブレーカーを必ず、ほかのマイクロサービスと接続してるところに入れています。

あとは、決済に必要なものだけ同期して、それ以外の処理はすべて非同期にしています。ユーザーの数値や補償トランザクションでリカバリーしたことを周知するものも、非同期でやっています。ペイアウトもユーザーのビューやペイメントでできることを作るときも、すべて非同期でやっています。

そして、決済は高速で実行できなければいけません。今この瞬間に各ユーザーがレジをしていて決済が5秒かかってしまうと嫌になりますし、後ろの人も待たせてしまいます。そのため、決済自体を早く終わらせなければいけません。

現在、ユーザーの履歴を作ったり、ほかのところを見せるために、DBにインデックスを追加しています。ですが、インデックスが入ったら各取引にも影響があるので、冪等性の必要があるインデックスだけに追加して、それ以外のインデックスはread用の別のスレーブのデータベースに追加したほうが良いです。これなら決済を早く終わらせることができるので、ユーザーも楽しくPayPayを使えるのではないかと思います。

そして冪等性があれば、クライアント側でリトライしてもらってぜんぜん問題ありません。クライアントで結果が返って来ないからあとでリトライすれば、失敗になっているか成功になっているかがわかります。クライアント側には少し負担がかかりますが、リトライしてもらったほうがいいと思います。

そして処理できない場合は、補償トランザクションやcompensationが必ず走るんですが、「お金が銀行にもないしPayPayにもない」といったことがあるとユーザーも困ってしまうので、必ずユーザーへ通知を行っています。

以上です。ありがとうございました。

(会場拍手)