【書き起こし】メルペイスマート払いにおけるマイクロサービス化の軌跡 – 吉田 拓矢【Merpay Tech Fest 2021】

Merpay Tech Fest 2021は、事業との関わりから技術への興味を深め、プロダクトやサービスを支えるエンジニアリングを知れるお祭りで、2021年7月26日(月)からの5日間、開催しました。セッションでは、事業を支える組織・技術・課題などへの試行錯誤やアプローチを紹介していきました。

この記事は、「メルペイスマート払いにおけるマイクロサービス化の軌跡」の書き起こしです。

吉田拓矢氏:それでは、始めていきたいと思います。次のセッションでは「メルペイスマート払いにおけるマイクロサービス化の軌跡」というセッションタイトルで吉田が発表させていただきます。

私は2019年7月にメルペイにジョインし、Credit Designチームのバックエンドエンジニアとして、メルペイスマート払いに関連するマイクロサービス化や、新機能の開発運用といった業務に従事してきました。

本日のアジェンダはこちらのようになっています。まず最初に、メルペイスマート払いについて簡単にご説明したあと、初期のシステム構成、それをどのようにマイクロサービス化していったかという話、最後にまとめというような構成になっています。

メルペイスマート払いについて

それでは最初に、メルペイスマート払いについて簡単にご説明したいと思います。

メルペイでは、2019年2月に提供を開始したiD決済を皮切りに、これまでさまざまな決済方法を提供してきました。今回は、その中でも2019年4月に提供が開始されたメルペイスマート払い、2020年7月に新たに追加された定額払いに関してお話させていただきます。

まず、メルペイスマート払いに関してですが、メルペイスマート払いとは、メルカリにおける過去の利用実績をもとにお客さまごとの利用限度を決定し、枠内で購入した代金を翌月にまとめて支払うことができるサービスになっております。支払いには売上金やポイントはもちろん、お客さまが設定した日に自動で銀行口座から引き落としをして支払う自動引き落としと呼ばれる支払い方法や、コンビニ払いなど、多様な支払い手段を提供しています。このサービスは、メルペイのミッションでもある「信用を創造して、なめらかな社会を創る」のうえでも、中核のサービスの1つになっております。属性情報だけではなく、メルカリの取引実績によるユニークなデータをもとに約束を守っていただけるか否かを判断し、新たな信用を生み出すサービスになっています。

また、2020年7月には、お金が理由で諦めるということをなくす財布をコンセプトに、使いすぎや、払い終わらないということをなくし、お客さまにより安心してお買い物を楽しんでもらうために定額払いをリリースしました。こちらのサービスは、メルペイスマート払いの購入代金を月々に分けて支払うことができるサービスになっております。

初期のシステム構成

それでは、そんなメルペイスマート払いにおいて、リリース当初、何があったのかをお話したいと思います。

リリース当初、メルペイスマート払いはおもに、ビジネス観点でのハードデッドラインがありました。当初は、メルペイのマイクロサービスアーキテクチャーに則り独立したマイクロサービスの開発を進めていましたが、開発工数はそのデッドラインには到底間に合うものではありませんでした。

そこで生まれたプロジェクトが「Ultra-C」です。これは、メルペイスマート払いの前身に当たる、2017年6月に試験運用を開始したメルカリ月イチ払いのコード資産を流用し、決済や支払いなどの大部分の機能を実現し、追加要件となるユーザーの状態や利用限度額の管理を行う、新たなマイクロサービスを相互に連携させることでサービスを実現しました。

その外観がこちらになります。メルカリ月イチ払いのコード資産は、メルカリ創業初期から開発されてきたメルカリAPIというモノリス上に実装されていたため、そちらに新たに開発した smartpay service を、相互の同期通信またはメッセージキューを介した非同期な通信で連携させるシステム構成になっていました。それでは次に、このアーキテクチャーでどのように機能を実現していたかを見ていきたいと思います。

まずは、決済データのマイグレーションです。これは、マイクロサービス側で利用限度枠を管理するために smartpay service 側でもお客さまの決済データを持ち、信用から決定した枠のうちお客さまが残りいくら使うことができるかを判定するために必要なデータになっていました。マイグレーションを実行するトリガーはいくつかあり、それはメルカリAPI側と smartpay service 側からと両方向のユースケースがあり、メルカリAPIからデータを送信してマイグレーションする方法と、 smartpay service 側からデータを取得してマイグレーションを行うという2つのユースケースがありました。またこのとき、マイグレーションするデータは先ほども述べたように、利用限度額の計算を用途とするため、まだ支払われてない決済に関してのみマイグレーションするようになっていました。つまり、マイグレーション時に支払い済みになっている決済のデータに関しては、まだ、メルカリAPIのMySQLとスマートペイの Spanner に関しては同期が取れていない状態になっていました。

メルカリの決済とメルペイの決済でフローが異なるのですが、こちらはメルカリ決済とその決済のキャンセルを図示したものになります。まず、メルカリAPIが起点となって smartpay service の決済やキャンセルのAPIを呼び出し、その結果をSpannerに書き込みます。そして、そのSpannerへの書き込みが完了すると、メルカリAPIは自身のMySQLに経過を書き込むという二重書き込みを行っています。

次に、こちらはメルペイの決済やそのキャンセルに関して図示したものになります。メルカリでの決済とは逆の方向でスマートペイからメッセージキューを介して、決済データを smartpay service とメルカリAPIに二重書き込みしています。

次に、決済した商品を翌月お支払いいただく際のフローを図示したものになります。こちらもメルカリAPIが起点となっていて、まずは、お客さまの売上金やポイントを管理している balance service からお支払い分の金額を消費する必要があります。そこで呼ばれるのが、1の売上金ポイントを消費するというものです。 payment service を介してこの処理は実行され、その実行結果をもとにお支払いが完了した場合、2の支払いという smartpay service のAPIを呼び出して支払い結果を反映します。そして最後に、自身のメルカリAPIのMySQLに対しても同じような結果を書き込んで支払いが完了とみなしていました。

このような初期のシステム構成では、次のような課題がありました。まず1つ目に、データの二重書き込みによる整合性担保の難しさです。決済や支払いに関するデータを同期非同期の通信でメルカリAPIのMySQLとスマートペイのSpannerに二重化込みしているため、システム障害やネットワーク障害によってその整合性を担保し続けるのはとても難しいことでした。

2つ目に、モノリスとマイクロサービスにまたがる機能の分散や重複です。決済やキャンセル、支払いといったコアな機能はメルカリAPI側をマスターとして動いていましたが、その他ユーザーの状態管理や利用限度額の管理、それに付随する設定の管理は smartpay service で動いているといった機能の分散がありました。また、決済やキャンセルではメルカリメルペイで双方向のユースケースがあるため、一部ロジックは重複して実装されていました。

そして3つ目に、モノリスの複雑性です。コードを流用しているメルカリ月イチ払いは2017年6月に運用が開始されており、コードにはそれなりに歴史的な負債がたまっている状態でした。また、特殊な対応等で既に役目を終えて実は動いてないコードや、特殊な対応によって手動でいじられた特殊なデータなどパターンが豊富になっていました。また、これらが要因で次のような問題がありました。まず、運用コストが高いことです。データの整合性は日々のリコンサイル作業で確認していましたが、整合性が失われている際の要因調査や修正等、システムの複雑性が高いため困難を極めました。また、運用コストの話に近いのですがマニュアルオペレーションが多いことです。データの不整合が発生した際の温かみのある手作業は、原因調査や実施の際の確認などやることが多いだけではなく精神を削る作業になるため、できればやりたくない作業になります。

最後に、開発の難易度が高いことです。これはおもに、モノリスとマイクロサービスの機能が分散重複しているため全体像の理解が難しい点や、モノリスの実装自体の複雑性が要因で開発コストが高くなっていました。

それでは、初期のシステムにおける課題にもあった、二重書き込みにおける整合性に関して少し例を見てみたいと思います。例えばメルカリ決済では、1の決済の処理がネットワーク障害により、 smartpay service 側の処理は成功したが、メルカリAPIでは失敗となってしまうケースや、1の決済は成功したが、3のMySQLへの書き込みに失敗したケースなどがあります。これらはどちらも smartpay service 側では成功として決済データが残りますが、メルカリAPI側では失敗になるのでデータの不整合が発生してしまいます。

次に、支払いに関する障害パターンです。例えば1の売上金・ポイントの消費で payment service、balance service 側は成功したが、メルカリAPIはエラーを受け取ってしまったケース。これは、売上金やポイントは消費されていますが、メルカリAPI、スマートペイで管理されている決済に関しては支払いが済んでいない状態になってしまいます。また、2の支払いのケースも同様です。こちらも同様に売上金ポイントは消費されてしまっていますが、支払い済みになっていない状態になっています。4のMySQLの書き込みで失敗したケースでは、メルペイ側のシステム、つまり payment service や balance service 、 smartpay service と呼ばれるシステムのデータ上は正しい状態になっていますが、メルカリAPIのデータだけ支払い済みにできていないためデータ不整合となってしまいます。このように、初期のシステム構成では異常ケースにおける問題があるため、マイクロサービス化は急務でした。

マイクロサービス化

それでは、実際にどのようにマイクロサービス化を進めたかをお話していきたいと思います。

メルペイスマート払いのマイクロサービス化では、次のような方針で進めました。まずは、フェーズ分けに関することです。移行先のマイクロサービスを開発しきったうえで向き先を変えていくような方針もありますが、今回はまず、決済、キャンセル、支払いといった機能ごとにフェーズを分けて検討しました。また、機能ごとのフェーズ内でもロジックを移行するフェーズと、データを移行するフェーズに細分化しました。これは、マイクロサービス化による影響を小さくすることでリスクヘッジするとともに、設計や開発における複雑性を下げることができました。また、移行後のロジックで動くデータをある時点から作成された新しいデータや、設定した条件を満たしているノイズの少ないデータとすることで、想定された条件下に絞ってロジックを組むことができるため設計コストを下げることができました。仮に、ロジックとデータの移行を同じフェーズで行い、新旧問わないデータを移行後ロジックで動かした場合、考慮すべきパターンが増すばかりではなく、障害発生時にそれがロジック起因のものなのか、データ起因のものなのかを判断して対応を分けていく必要があります。一方、今回の方針では、データは正という仮定のもとに対応することができるため、初期対応のスピード感が変わってきます。そして最後に、データ移行の際に移行後ロジックで動くように調整します。これは、外れ値のような異常なデータを標準化して移行するだけではなく、ロジックにケースを足し込んでいくようなイメージもあります。ロジック移行のフェーズでちゃんと動いているということを確認できるため、そこに対してあるパターンも動くようにというケースを追加で実装していくのは、実装初期にあらゆるケースを網羅的に考慮して実装に落とし込んでいくことと比較して、難易度を大きく下げることができます。それでは次からは、実際に行ったメルペイスマート払いのマイクロサービス化における各フェーズをご紹介していきたいと思います。

まずは、決済データのマイグレーションを行いました。これは、初期のシステム構成でもお話したあるトリガーにより、マイグレーションを実行する際の対象データの範囲を広げたものになります。オンデマンドで実行されるマイグレーションでは、支払いが済んでいない決済を対象にマイグレーションを行っていましたが、このフェーズでは支払い済みのものも含めメルカリAPIにあって、 smartpay service 側にないすべてのデータを対象にマイグレーションを実施しました。こちらは、メルカリAPI側が持っている決済データをお客さま単位で smartpay service にリクエストし、 smartpay service 側で持っている、持っていないを判断して必要なデータを Spanner に書き込むようなバッチおよびAPIを開発し、バッチを数週間かけて実行することでデータを順次移行していきました。

次に、決済キャンセル機能の移行です。メルカリ決済のマイクロサービス化では、今までメルカリAPIが直接 smartpay service を呼び出して決済データを連携していたのですが、新たに payment service と呼ばれるペイメントプラットフォームチームで開発されているお金や決済トランザクションの流れを管理するマイクロサービスを経由して、 smartpay service に決済をつくるというふうになっています。それでは、初期のシステム構成における障害パターンをこのマイクロサービス化によってどのように改善したかを見ていきたいと思います。

例えば、2の payment service から smartpay service への決済呼び出しでエラーとなるパターンです。こちらの場合は、 payment service がエラーを検知すると、 smartpay service に対してキャンセルを呼び出してロールバックを試行することで、 smartpay service でだけ決済ができてしまうようなデータ不正を防ぐことができます。

また、1のメルカリAPIから payment service への決済呼び出しや、4のメルカリAPIのMySQLへの書き込みでエラーが発生したケースも同様です。メルカリAPIのほうにリコンサイル用のバッチが既に存在しており、そちらが定期的にメルカリAPIと payment service の整合性をチェックしています。仮に、 payment service や smartpay service などのメルペイ側では決済が成功しており、メルカリAPI側では失敗となっていた場合、このリコンサイルバッチが不整合を検出し、 payment service を経由して smartpay service などのダウンストリームサービスに対してもロールバックを試行して整合を自動で復旧することができます。このように、決済キャンセルのマイクロサービス化では、システム移行と同時に今までの障害パターンなどを克服し、より剛健なシステムになるよう改善を入れながら進めてきました。一方で、すべてのデータがまだ移しきれていないため、この段階ではメルカリAPIのMySQLと、 smartpay service の Spanner への二重書き込みは継続している状態です。

次に、支払いのマイクロサービス化です。支払いのマイクロサービス化では、ロジックの移行、そして、今までの支払いデータ移行の順でフェーズを分けて進めました。まずは、ロジックの移行です。こちらは、 smartpay service 側にほとんど新機能開発に近い形で新しくロジックの組み直しを行いました。これはTCCパターンとイベント駆動型のアーキテクチャーを採用してできており、まず、1でメルカリAPIから smartpay service の支払いのAPIを呼び出します。このとき、メルカリAPIはほぼゲートウェイという立ち位置で動いており、特殊なロジックはほぼ持っていない状態でクライアントから受け取ったリクエストをそのまま smartpay service に流すようなつくりになっています。そして、 smartpay service はTCCパターンに則り、まずは支払いをTryします。Tryによって、 balance service にあるお客さまの売上金やポイントは支払い分だけ仮押さえされます。そして、トライの結果を自身の Spanner に書き込んで処理は終了です。

次に、イベント駆動の部分になります。Tryの成功は、 payment service からメッセージキューを介して smartpay service に知らされるので、そのメッセージをトリガーに Confirm を行い、 balance service で仮押さえしていた売上金やポイントを確定させます。そして、確定させたデータを自身の Spanner にも書き込んで次の処理に進みます。 Confirm が成功した結果、お支払いは完了したことになるので、その情報をメルカリAPIのほうに伝播させる必要があります。これは、メッセージキューを介して非同期の通信で行い、お客さまの支払いが成功したことを通知し、メルカリAPIはその結果をMySQLに書き込みます。支払いのマイクロサービス化ではこのような仕組みを導入しましたが、初期のシステム構成における障害パターンがどのように改善されたかを見ていきたいと思います。

例えば、Try後の結果を smartpay service の Spanner に書き込むところでエラーになったときです。この場合、お客さまの売上金やポイントは仮押さえられた状態になってしまいますが、次の処理フェーズでこの不整合を自動復旧することができます。

smartpay service ではTry後の処理に関わらず、Tryの成功をメッセージで受け取ることができるため、Tryは成功しているが自身のSpanner上ではトライは成功していない、または、行っていない状態にロールバックされています。 smartpay service では、その状態を検知して支払いをキャンセルし、取り消すことでロールバックを行い、お客さまの仮押さえしていた売上金やポイントをもとに戻すことができます。

また、ConfirmやConfirm後のSpannerへの書き込みでエラーが発生したケースを考えたいと思います。この場合も、 balance service でお客さまの売上金やポイントは支払い分消費が確定した状態、もしくは仮押さえられたままの状態になりますが、一方で、 smartpay service 側ではまだ支払いは完了していないことになっています。

こちらは、Tryが成功した場合は必ずConfirmが行えるという前提のもとに、メッセージキューに対してNackを返して再送を依頼し、トランザクションが成功するまでやり直すことで結果整合が取れる形になっています。

最後に、支払いデータのマイグレーションです。支払いデータには、支払い手段ごとや、すべての支払いが支払い済み、もしくは一部の決済が支払い済みというように複数のパターンが存在するため、パターンごとにマイグレーションのやり方が異なりました。今回の進め方だと、支払い手段等のパターンによってマイグレーションするバッチを分割し、それぞれ依存のないように開発を進めてデータをマイグレーションさせました。また、マイグレーション状態をMySQLに持たせることで、マイグレーションが完了した決済の支払いから順次、移行後のロジックで支払いが行えるようにしました。これまでに決済データ、決済機能、支払い機能、支払いデータの順でマイクロサービス化を行ってきました。これで機能やデータの移行はあらかた済んだ状態で、残りクライアントのデータ参照を smartpay service のほうに向き変えていくことで、メルカリAPIとの依存を完全に断ってマイクロサービス化は完了となりますが、ここで定額払いが登場します。

定額払いは、このようなシステム構成になっていました。定額払いは、新規のマイクロサービスではなく、既存の smartpay service 上にメインのロジックを実装することで動いています。また、新たにお客さまが行った決済をもとに債務を管理する debt service や、これまでメルカリAPI上で動いていた督促を行うデマンドサービスなど、新たなマイクロサービスと連携することでメルカリAPIとの依存がほとんどないシステム構成になっています。この状態を実装すると、if定額払い – elseifスマート払い(翌月払い)のように、お客さまの状態によって処理が完全に分岐するような形になり、一部機能は共通化しつつも、ほとんどの機能が分岐するような形で実装されました。これは単純に、実装の複雑性を上げるだけでなく、if文が多く入り込んだ見通しの悪い実装になってしまっていたため、マイクロサービス化ではこちらにも追従する必要がありました。ただし、この定額払いのリリースでは、マイクロサービス化の観点で悪い点だけではなく良い点もありました。それは、もともとマイクロサービス化の中で、クライアントの参照をスマートペイに向ける移行が必要があったのですが、定額払いではクライアントの実装も一新され、システム上の理想に近い形でメルカリAPIではなく、メルペイAPIを経由して smartpay service や debt service から各種データを取得して画面を描画するようになっていました。また、この切り替えは smartpay service が持っているフラグ1つで管理されていたため、マイクロサービス化では、これらを流用して定額払いのロジックへの移行やデータの参照を smartpay service に切り替えることを行いました。

これを債務データのマイグレーションと言っています。債務データのマイグレーションでは、スマートペイ上に存在している決済データを debt service 等にバッチを使って移行すると同時に、移行状態を持っているステータスを更新することで行っています。移行状態のステータスが完了に更新されると同時に、クライアントは今までのメルカリAPIを経由した参照から、メルペイAPIを経由した smartpay service への参照に切り変わり、決済やキャンセル、支払いといった機能も定額払いで実装されたロジックで動くようになるため、 smartpay service 上でのロジックの共通化が行えました。

まとめ

最後に、まとめになります。

メルペイスマート払いではリリース当初、モノリスとマイクロサービスにデータの二重書き込みを行い、相互に連携させることでサービスを実現していました。これは、データの二重書き込みによる整合性担保やシステムの複雑性を上げることにつながっており、非常に運用が難しいシステムになっていました。マイクロサービス化では、機能やロジック、データの移行をフェーズに分けることで推進し、マイクロサービス時に初期システムの対障害性が低い問題等、さまざまな問題を改善しながら新たな仕組みを使って解決していきました。

これで発表を以上とさせていただきます。ご清聴ありがとうございました。