内部のRBKmoney支払い-支払いプラットフォームのロジック


こんにちは、Habr! この投稿で始まったRBK.money支払いプラットフォームの内部に関するサイクルを公開し続けます。 今日は、論理処理スキーム、特定のマイクロサービスとそれらの相互関係、各ビジネスロジックを処理するサービスが論理的に分離される方法、処理コアが支払いカードの番号とプラットフォーム内での支払い方法について何も知らない理由について説明します。 また、もう少し詳しく、高負荷を処理するために高可用性とスケーリングを提供する方法のトピックを明らかにします。


概要ロジックと一般的なアプローチ


一般に、支払いを担当する処理のその部分の基本要素のスキームは次のようになります。



論理的に自分の内部で、責任範囲を3つのドメインに分割します。



各ゾーン内には、ビジネスロジックの処理の各部分を実行するマイクロサービスがあります。 入力でRPCコールを受信し、出力で、組み込みアルゴリズムに従って処理されたデータを生成します。このアルゴリズムは、チェーンに沿った他のマイクロサービスのコールとしても実行されます。


スケーラビリティを確保するために、可能な限り少ない場所に状態を保存しようとします。 図のステートレスサービスには、永続リポジトリとの接続はなく、それぞれステートフルが接続されています。 一般に、永続的な状態のストレージにはいくつかの制限されたサービスを使用します-処理の主要部分では、これらは関連サービス用のRiak KVクラスターです-PostgreSQL、Kafkaを使用するキューの非同期処理用です。


高可用性を確保するために、通常3〜5の複数のコピーでサービスを展開します。


ステートレスサービスのスケーリングは簡単です。異なる仮想マシンで必要なインスタンスの数を増やすだけで、Consulに登録され、コンソールDNSを介して解決し、他のサービスからの呼び出しを受信し、受信したデータを処理してさらに送信します。


ステートフルサービス、またはそれはメインのものであり、図ではMachinegunとして示されており、アクセスしやすいインターフェイスを実装しています(分散アーキテクチャはErlang Distributionに基づいています)。ConsulKVを介した同期は、キューイングと分散ロックを保証するために使用されます。 これは短い、詳細な説明は別の投稿になります。


Riakは、すぐにアクセスできる永続的なマスターレスストレージを提供します。どのような方法でも準備することはなく、設定はほぼデフォルトです。 現在の負荷プロファイルでは、クラスター内の5つのノードが別々のホストにデプロイされています。 重要な注意-インデックスと大きなデータサンプルを実際には使用せず、特定のキーを使用します。


KVスキームを実装するには費用がかかりすぎる場合は、オンラインパーツからMachinegunを使用して障害が発生した場合に必要なイベントをいつでもアップロードできるため、PostgeSQLデータベースをレプリケーションまたはシングルモードソリューションで使用します。


図のマイクロサービスの色分けは、それらが記述されている言語を示しています-薄緑色-これらはJavaアプリケーション、薄青色-Erlangです。


すべてのサービスはDockerコンテナで動作します。これはCIビルドアーティファクトであり、ローカルのDockerレジストリにあります。 構成がプライベートGithubリポジトリにあるSaltStack本番環境にサービスをデプロイします。


開発者はこのリポジトリの変更を独自に要求し、サービスの要件を記述します。コンテナに割り当てられたメモリのサイズ、環境変数などに転送される必要なバージョンとパラメータを示します。 さらに、承認された従業員による変更の要求を手動で確認した後(DevOps、サポート、および情報セキュリティがあります)、CDは新しいバージョンのコンテナーのインスタンスを製品環境のホストに自動的にロールアップします。



また、各サービスは、Elasticsearchが理解できる形式でログを書き込みます。 ログファイルはFilebeatによって取得され、Elasticsearchクラスターに書き込まれます。 したがって、開発者は製品環境にアクセスできないという事実にもかかわらず、彼らは常にデバッグし、サービスに何が起こるかを見る機会があります。


外の世界との相互作用



プラットフォームの状態の変更は、パブリックAPIの対応するメソッドの呼び出しによってのみ発生します。 従来のWebアプリケーションとサーバー側のコンテンツ生成は使用していません。実際、UIとして表示されるのは、公開API上のJSビューのみです。 原則として、プラットフォームでのアクションは、使用するコンソールからのcurl呼び出しのチェーンを使用して実行できます。 特に、統合テスト(JSでライブラリとして作成しました)を記述するために、CIで各アセンブリ中にすべてのパブリックメソッドをチェックします。


また、このようなアプローチは、プラットフォームとの外部統合のすべての問題を解決し、エンドユーザーが支払いデータを入力するという美しい形式の単一のプロトコルと、サーバー間対話のみを使用したサードパーティの処理との直接統合のためのホストツーホストの両方を取得できるようにします。


統合テストでの完全なカバレッジに加えて、ステージング更新アプローチを使用します。分散アーキテクチャでは、これを行うのは非常に簡単です。たとえば、1回のパスで各グループから1つのサービスのみを展開し、その後ログとグラフの一時停止と分析を行います。


これにより、金曜日の夜を含むほぼ24時間展開が可能になります。何も実行できないことやすぐにロールバックすることを恐れず、誰も気付かないまで変更を加えた単純なコミットを元に戻します。


プラットフォーム登録とパブリックAPI



パブリックメソッドを呼び出す前に、クライアントを承認および認証する必要があります。 クライアントがプラットフォームに表示されるためには、エンドユーザーとのすべてのやり取りを処理し、パスワードの登録、入力、リセット、セキュリティ制御、その他のバインディングのためのインターフェースを提供するサービスが必要です。


ここでは、自転車を発明しませんでしたが、Redhat- Keycloakのオープンソースソリューションを単に統合しました 。 弊社とのやり取りを開始する前に、プラットフォームに登録する必要があります。これは実際、Keycloakを通じて行われます。


サービスでの認証に成功すると、クライアントはJWTを受け取ります。 後で承認に使用します。Keycloak側では、JWTに単純なjson構造として埋め込まれ、サービスの秘密キーで署名されるロールを記述する任意のフィールドを指定できます。


JWTの機能の1つは、この構造がサーバーの秘密キーによって署名されていることです。したがって、ロールと他のオブジェクトのリストを承認するために、承認サービスにアクセスする必要はなく、プロセスは完全に分離されます。 起動時のCAPIサービスは、Keycloakパブリックキーを読み取り、それを使用してパブリックAPIメソッドへの呼び出しを許可します。


キー失効スキームを思いついたので、ストーリーは独立しており、独自の投稿に値します。


したがって、JWTを受け取りました。これを認証に使用できます。 ここでは、CAPIおよびCAPI-DSSとして示されているダイアグラムで、次の機能を実装するマイクロサービスの共通APIのグループが機能します。



このようなサービスは多数あり、非常にシンプルでオークであり、状態を保存しません。そのため、線形パフォーマンススケーリングでは、必要な量の空き容量でサービスを展開するだけです。


PCI-DSSおよびオープンカードデータ



図からわかるように、このような2つのサービスグル​​ープがあります-メインは共通APIであり、オープンカード会員データを持たないすべてのデータストリームを処理します。2つ目は、これらのカードで直接動作するPCI-DSS共通APIです。 内部ではまったく同じですが、物理的に分離し、異なる鉄片に配置しました。


これは、カードデータを保存および処理する場所の数を最小限に抑え、このデータおよびPCI-DSS認定エリアの漏洩のリスクを減らすために行われます。 そして、これはかなり時間がかかり、費用のかかるプロセスです-支払い会社として、私たちは毎年MPS規格に準拠するために有料の認証を受ける必要があり、それに関係するサーバーとサービスが少ないほど、このプロセスを完了するのはより速く簡単になります。 さて、セキュリティ上、これは最もポジティブな方法で反映されます。


請求とトークン化



そのため、支払いを開始し、支払人のカードからお金を償却します。


このリクエストは、パブリックAPIのメソッドへの一連の呼び出しの形で行われたと想像してください。これは、オンラインストアに行って商品のバスケットを収集し、[購入]をクリックし、支払いにカードの詳細を入力した後、支払者として開始されましたフォームをクリックし、「支払い」ボタンをクリックしました。


私たちはお金を償却するためのさまざまなビジネスプロセスを提供していますが、最も興味深いのは買掛金を使用するプロセスです。 プラットフォームでは、支払いの請求書、または支払いのコンテナとなる請求書を作成できます。


1つの請求書内で、1つずつ支払いを試みることができます。つまり、次の支払いが成功するまで支払いを作成できます。 たとえば、さまざまなカード、ウォレット、その他の支払い方法から請求書の支払いを試みることができます。 カードの1つにお金がない場合は、別のカードを試すことができます。


これは、コンバージョンとユーザーエクスペリエンスにプラスの効果をもたらします。


請求書ステートマシン



プラットフォーム内では、このチェーンは次のルートに沿った相互作用に変わります。



curl -X POST \ https://api.rbk.money/v2/processing/invoices \ -H 'Authorization: Bearer {JWT}' \ -H 'Content-Type: application/json; charset=utf-8' \ -H 'X-Request-ID: 1554417367' \ -H 'cache-control: no-cache' \ -d '{ "shopID": "TEST", "dueDate": "2019-03-28T17:41:32.569Z", "amount": 6000, "currency": "RUB", "product": "Order num 12345", "description": "Delicious meals", "cart": [ { "price": 5000, "product": "Sandwich", "quantity": 1, "taxMode": { "rate": "10%", "type": "InvoiceLineTaxVAT" } }, { "price": 1000, "product": "Cola", "quantity": 1, "taxMode": { "rate": "18%", "type": "InvoiceLineTaxVAT" } } ], "metadata": { "order_id": "Internal order num 13123298761" } }' 

リクエストは、共通APIグループのアーランアプリケーションの1つでバランスが取られ、有効性を確認し、ベンダーサービスに行き、そこでkey等性キーを受け取り、リフトに転送し、Hellgateサービスグル​​ープにリクエストを送信しました。 Hellgateインスタンスはビジネスチェックを実行しました。たとえば、このJWTの所有者が原則的にブロックされていないことを確認し、請求書を作成し、一般的にプラットフォームと対話して請求書の作成を開始しました。


Hellgateは私たちの処理の中核であると言うことができます、それはビジネスエンティティで動作し、支払いを開始する方法を知っている人、この支払いが実際のお金の請求に変わるために蹴る必要がある人、この支払いの経路を計算する方法、それを取り消すように言われるべきだからです貸借対照表に反映され、手数料およびその他の拘束力を計算します。


通常、状態も保存せず、簡単に拡張できます。 しかし、何らかの理由でネットワークの分割やHellgateの障害が発生した場合、請求書を紛失したり、カードから二重のお金を請求したりすることは望みません。 このデータを永続的に保存する必要があります。


3番目のマイクロサービス、つまりMachinegunが登場します。 HellgateはMachinegunに呼び出しを送信し、クエリパラメーターの形式のペイロードで「オートマトンを作成」します。 Machinegunは同時要求を整理し、Hellgateを使用して、パラメーターから最初のイベントInvoiceCreatedを作成します。 その後、それ自体がRiakとキューに書き込みます。 その後、成功した応答がチェーン内の最初のクエリに逆の順序で返されます。


要するに、Machinegunは、現在のバージョンのプラットフォームであるRiak上の、他のDBMSよりもタイマーが長いDBMSです。 これは、独立したマシンを管理できるインターフェースを提供し、ency等性と記録順序の保証を提供します。 複数のHGが突然そのような要求でそれに来た場合、イベントが順番通りにマシンに書き込まれることを許可しないのはMGです。


オートマトンはプラットフォーム内の一意のエンティティであり、識別子、イベントのリスト形式のデータセット、およびタイマーで構成されます。 オートマトンの最終状態は、対応する状態への遷移を開始するすべてのイベントの処理から計算されます。 このアプローチを使用して、ビジネスエンティティを処理し、それらを有限状態マシンとして説明します。 実際、販売者が作成したすべての請求書とその支払いは、状態間の遷移の独自のロジックを備えた有限状態マシンです。


Machinegunでタイマーを操作するためのインターフェイスを使用すると、「15年以内にこのマシンの処理を継続したい」という形式のリクエストを、記録用のイベントとともに別のサービスから受信できます。 このような保留中のタスクは、組み込みタイマーに実装されています。 実際には、これらは非常に頻繁に使用されます-銀行への定期的な呼び出し、長時間の非アクティブによる支払いを伴う自動アクションなど。


ところで、Machinegunのソースコードは、 公開リポジトリの Apache 2.0ライセンスで公開されています このサービスがコミュニティに役立つことを願っています。


Machinegunの作業の詳細な説明と、一般に配布システムを準備する方法については、別の大きな投稿にまとめられているので、ここで詳しく説明することはしません。


外部クライアントの認可のニュアンス



保存に成功すると、HellgateはデータをCAPIに返し、バイナリトリフト構造を美しくデザインされたJSONに変換し、マーチャントバックエンドに送信できる状態にします。


 { "invoice": { "amount": 6000, "cart": [ { "cost": 5000, "price": 5000, "product": "Sandwich", "quantity": 1, "taxMode": { "rate": "10%", "type": "InvoiceLineTaxVAT" } }, { "cost": 1000, "price": 1000, "product": "Cola", "quantity": 1, "taxMode": { "rate": "18%", "type": "InvoiceLineTaxVAT" } } ], "createdAt": "2019-04-04T23:00:31.565518Z", "currency": "RUB", "description": "Delicious meals", "dueDate": "2019-04-05T00:00:30.889000Z", "id": "18xtygvzFaa", "metadata": { "order_id": "Internal order num 13123298761" }, "product": "Order num 12345", "shopID": "TEST", "status": "unpaid" }, "invoiceAccessToken": { "payload": "{JWT}" } } 

ブラウザで支払人にコンテンツを送信して支払いプロセスを開始できるように思えますが、ここでは、すべてのマーチャントがクライアント側で独立して承認を実装する準備ができているわけではないので、自分で実装しました。 アプローチは、CAPIが別のJWTを生成することで、カードトークン化プロセスを開始し、特定の請求書を管理して、返された請求書構造に追加できるようにします。


同様のJWT内で説明されているロールの例:


  "resource_access": { "common-api": { "roles": [ "invoices.18xtygvzFaa.payments:read", "invoices.18xtygvzFaa.payments:write", "invoices.18xtygvzFaa:read", "payment_resources:write" ] } } 

このJWTの使用試行回数は制限されており、有効期間も設定されているため、支払人のブラウザで公開できます。 傍受されたとしても、攻撃者ができる最大のことは、誰かの請求書の代金を支払うか、データを読み取ることです。 さらに、決済マシンはオープンカードデータでは動作しないため、攻撃者が見ることができる最大値は、タイプ4242 42** **** 4242マスクされたカード番号、支払い金額、およびオプションで商品のバスケットです。


作成された請求書とその請求書へのアクセスキーにより、支払いビジネスプロセスを開始できます。 請求書IDとそのJWTを支払人のブラウザに渡し、JSアプリケーションに制御を渡します。


Checkout JSアプリケーションは、支払者としてあなたと対話するためのインターフェースを実装します-支払データ入力フォームを描画し、支払を開始し、最終ステータスを受け取り、面白いまたは悲しいポイントを表示します。


トークン化とカードデータ



ただし、Checkoutはカードデータでは機能しません。 前述のように、機密データをできるだけ少ない場所にカード会員データの形式で保存する必要があります。 これを行うには、トークン化を実装します。


これが、Tokenizer JSライブラリーの出番です。 入力フィールドにカードを入力して[Pay]をクリックすると、このデータがインターセプトされ、 createPaymentResource()メソッドを呼び出して非同期的に処理のために送信されます。


このリクエストは個々のCAPI-DSSアプリケーションのバランスが取れており、請求書JWTを確認してデータを検証し、カードデータストレージサービスに些細なことを送信するだけでリクエストを承認します。 図では、CDS-Card Data Storageとして示されています。


このサービスの主な目的:



途中で、このサービスは、キーを暗号化するためのキーを生成する、これらのキーを安全に入力する、データを再暗号化する、支払い後のCVVの消去を制御するなど、重要なタスクを解決しますが、これはこの投稿の範囲外です。


足元で自分を撃つ可能性からの保護がなかったわけではありません。 バックエンドからのリクエストを許可するように設計されたプライベートJWTがWeb上でクライアントのブラウザに公開される可能性はゼロではありません。 これを防ぐために、保護機能が組み込まれています。createPaymentResource()メソッドを呼び出すことができるのは、請求書の認証キーのみです。 プライベートJWTプラットフォームを使用しようとすると、HTTP / 401エラーが返されます。


トークナイゼーションリクエストの完了後、トークナイザーは受信したトークンをCheckoutに返し、この作業を終了します。


支払機のビジネスプロセス



Checkoutは支払いプロセスを開始します。つまり、 createPayment()メソッドを呼び出し、以前に受け取ったカードトークンを引数として渡し、ポーリングイベントのプロセスを開始します。実際には、 getInvoiceEvents() APIメソッドを1秒に1回呼び出します。


CAPIを介したこれらの要求はHellgateに分類され、カードデータを使用せずに支払いビジネスプロセスの実装が開始されます。



受信したトークンのプロトコルアダプターはCDSに移動し、復号化されたカードデータを受信し、銀行固有のプロトコルに転送し、一般に承認を取得します-指定された金額が支払人の口座で凍結されていることを取得銀行から確認します。


この時点で、銀行からカードからの引き落としに関するメッセージが記載されたSMSを受け取りますが、実際には、実際には資金は口座で凍結されているだけです。


アダプターはHGに承認の成功を通知し、CVVコードがCDSサービスから削除されます。これで、対話フェーズが終了します。 経営陣はHGに戻ります。



支払いビジネスプロセスのマーチャントによるcreatePayment()呼び出しで指定された支払いプロセスに応じて、HGは外部APIから認証取得メソッドへの呼び出し、つまり、カードからのお金の引き出しの確認、またはマーチャントがスキームを選択した場合、直ちにそれを行いますシングルステージ支払い。


原則として、ほとんどの商人は一段階の支払いを使用しますが、承認の時点で、引き落とされた合計金額がまだわからないビジネスカテゴリがあります。 これは、旅行業界で1つの金額のツアーを予約するときによく発生します。予約を確認した後、その金額は指定され、最初に承認された金額と異なる場合があります。


確認の量は排他的に承認の量以下であるという事実にもかかわらず、ここには落とし穴があります。 カードがリンクされている銀行口座の通貨とは異なる通貨でカードから製品またはサービスの料金を支払うことを想像してください。


承認時に、承認日の為替レートに基づいてアカウントでブロックされた金額。 支払いは数日間「承認済み」の状態になる可能性があるため(鉄道省は最大期間の推奨事項があり、現在は3日間です)、承認の取得は、承認された日のレートで実行されます。


したがって、あなたは通貨リスクを負います。これは、特に通貨市場のボラティリティが高い状況では、あなたにとって有利なこととあなたにとって不利になることがあります。


承認を取得するために、プロトコルアダプタとの通信プロセスが受信と同じプロセスで行われ、成功した場合、HGはShumway内のアカウント投稿プランを適用し、支払いを「支払い済み」ステータスに移行します。 現時点では、支払いシステムとして、取引の参加者に金銭的義務を負っています。


また、支払いプロセスを含むインボイスマシンの状態の変化はMachinegunのHellgateによって記録され、データの永続性を確保し、新しいイベントでインボイスを充実させることも注目に値します。


支払い機とUIの状態の同期



プラットフォーム内で支払いのバックグラウンドプロセスが行われている間、Checkoutは処理を注ぎ、イベントを要求します。 特定のイベントを受信すると、彼は現在の支払いの状態を人が理解できる形式で描画します-プリローダーを描画し、「支払いが正常に処理されました」または「支払いを受信できませんでした」画面を表示するか、ブラウザを発行銀行のページにリダイレクトして3D-Secureパスワードを入力します;


失敗した場合、Checkoutは別の支払い方法を選択するか再試行するよう提案します。したがって、請求書の一部として新しい支払いを開始します。


イベントのポーリングを伴うこのようなスキームにより、ブラウザーのタブを閉じた後でも状態を復元できます-繰り返し起動した場合、Checkoutは現在のイベントのリストを受け取り、3D-Secureコードを入力するか、支払いが既に正常に完了したことを示すなど、ユーザーとの対話の現在のシナリオを描画します


オフラインゾーンでのイベントの複製



マシン制御インターフェイスと同時に、Machinegunは、イベントのフローをプラットフォームのオンラインではない他のタスクを担当するサービスにオーバーフローさせるサービスを実装します。


決勝戦のキューブローカーとして、Kafkaに決めましたが、以前はMachinegun自体を使用してこの機能を実装していました。 一般に、このサービスは、イベントの保証された順序付きストリームの保存、または他のコンシューマーへの要求に応じたイベントの特定のリストの発行です。


また、最初にイベント重複排除スキームを実装し、同じイベントが2回レプリケートされないことを保証しましたが、このアプローチを生成したRiakの負荷により破棄されました-結局、インデックス検索は最良のものではありませんKVストレージ対応。 現在、各サービスコンシューマは、イベントの重複排除を個別に担当しています。


一般に、Machinegunによるイベントのレプリケーションは、データがKafkaに保存されたことを確認することで終了し、消費者はすでにKafkaトピックに接続され、関心のあるイベントのリストをダウンロードします。


典型的なオフラインゾーンアプリケーションテンプレート


たとえば、Dudoserサービスは、支払いが成功したことを通知するメールを送信します。 起動時に、成功した支払いのイベントのリストをポンプで送り出し、そこから住所と金額に関する情報を取得し、ローカルのPostgreSQLインスタンスに保存して、ビジネスロジックのさらなる処理に使用します。


他のすべての同様のサービスは同じロジックに従って動作します。たとえば、Magistaサービスは販売者の個人アカウントで請求書と支払いを検索し、Hookerサービスはバックエンドに非同期コールバックを送信します。処理APIに直接。


このアプローチにより、処理の負担を解き放ち、最大のリソースを割り当て、支払い処理の高速性と可用性を提供し、高い変換を実現できます。 「ビジネス顧客が過去1年間の支払いに関する統計を見たい」などの大量のクエリは、オンライン処理部分の現在の負荷に影響を与えないサービスによって処理されるため、支払人や商人としての顧客には影響しません。


おそらく、投稿を長くしすぎないように、これで停止します。 将来の記事では、Machinegun、Bender、CAPI、およびHellgateを例として使用し、ロードされた分散システムで変更、保証、順序のアトミック性を確保することの微妙な違いについて確実に説明します。


さて、次回ソルトスタックについて¯\_(ツ)_/¯



Source: https://habr.com/ru/post/J447440/


All Articles