Powered by

Spring Batch でサブスク1億件のクレカ決済処理【JJUG CCC 2021 Fall登壇】

article-069_top.jpg

アプリケーション構造やデータ分散で工夫した点や、サブスクリプション型のクレジットカードお支払い運用についてご紹介します。

※本稿は2021年11月21日開催の「JJUG CCC 2021 Fall」の登壇内容を加筆修正し掲載しています。

鈴木 隆志  Takashi Suzuki

GMOペイメントゲートウェイ株式会社
マネージャー/アプリケーションエンジニア/採用

2012年にGMOペイメントゲートウェイに入社。決済サービスの開発・運用から新規サービス立ち上げまで担当

サブスクについて

article-069_thumb01.jpg

まず「サブスクについて」。これは「サブスクリプション」の略で、定期的なお支払いのことを指しています。具体的には動画の配信サービスであったり、雑誌を含めた定期購読、月会費や月額利用料といったところも広義のサブスクといえるでしょう。
今回は、単純なサブスクリプションだけでなく、こういったすべての定期的で継続的なお支払いに関する処理のことを説明します。

当社のサービスについて

article-069_thumb02.jpg

当社のサービスについて簡単に説明いたします。
通常皆さんがインターネットのショッピングサイトであったり、フリマ、ゲームへの課金、動画配信といったところで何らかの課金をした場合、ショップさんから裏の決済事業者、よく使われるのはクレジットカード会社さんであったり、キャリア決済であれば各キャリアに直接リクエストが飛んでいることが想像されると思います。しかし、実態は間に決済代行という会社が入ることがほとんどです。その名の通り決済を代行している会社で、まさに当社はこの位置でお仕事をさせていただいています。
英語圏ではPSP、「Payment Service Provider」と呼ばれていたり、SaaSの一つでもあるのですが、最近ではプラットフォームではなくPaymentのほうのas a Service、「PaaS」という呼ばれ方をしています。

決済代行会社がここに存在するメリットは、左側のショップさんに対する側面が大きいです。ショップさんは決済事業者すべて1つ1つと契約することなく、当社と一本契約すればご希望の決済サービスをお使いになれること、それから売上金・手数料もすべて当社の窓口一本で精算やお支払いを済ませることができるようになります。
システム面においても当社のインターフェースの仕様に沿ってつないでいただければ、ご希望の決済手段が使えるサービスを提供しております。

上に書かれている数字は、当社の実績です。(2021年9月時点)
まずショップさんについては約12万店舗のお客様にご利用いただいており、件数、金額は記載の通りとなっています。8兆円というのは規模感が想像しにくいと思いますが、国内のショッピングモール最大手の1.5から2倍と想像してください。また特徴的なのは、常にこのあたりの数字(特に営業利益)を年間25パーセント以上成長させるという目標でやっており、事実、決済金額の8兆円というのは、昨年までは6兆円台でした。
そういった形で毎年毎年伸ばしてきているというのも一つ特徴になっています。
25パーセント成長というのは、大体3年ぐらい経つと倍ぐらいになる数字です。したがって3年前はだいたいこの半分ぐらいだったということになります。

定期的な決済の裏側

article-069_thumb03.jpg

ここからはサブスクの定期的なお支払いの裏側が実際どうなっているのかを、シーケンス図を用いて説明します。

いくつかパターンがあります。上図は一番分かりやすい例です。
ショップさんから決済リクエストを一件一件Web APIで飛ばしてもらいます。同時50リクエストあるお客様もいらっしゃいますが、このような形で一件一件リクエストを飛ばしてもらいます。
最近はこの形をとることが増えています。やはり一件一件やりますので、エラー時のハンドリングであったり、リトライというのはショップさんの方で制御できるということと、通常の継続課金は1カ月サイクルで回すものではありますが、こちらについてはショップさんの任意のタイミングで決済を出せるというのが大きなメリットになっていると思います。
したがって、最近の大手テック企業さんなどは、みなさん大体この形をとられることが多いと思います。
補足説明ですが、右下の方に「確定ファイル伝送」と書いています。実はクレジットカードの仕組みの一つで、リアルタイムのリクエスト、「オーソリ・与信リクエスト」というのを当社からクレジットカード会社に出していますが、これだけでは確定しないのです。
大体は翌日に確定ファイルというのを送って初めて、皆さんのクレジットカードから課金が成立するということになります。

これが一番分かりやすいパターンです。特に当社は継続的なお支払いに関して何かすることなく、大量のリクエストを安定的にさばくというところをお手伝いできていると思います。

パターン2

article-069_thumb04.jpg

続けてパターン2です。
これは小規模なお客様、だいたい課金の件数が数千から1万いかないぐらいの例です。場合によっては月30件やそういった件数のお客様がご利用になるパターンです。ショップさんが当社の提供している管理画面からファイルを手動でアップロードします。
このパターンは1カ月サイクルになっていますので、締め切りがあります。毎月どちらかを選べますが、5日か10日の締め切りまでに間に合うようにショップさんの運用の方がファイルを作ってアップロードするという形。
アップロードして締め切った後は、先ほどお見せしたのと同じような形のシーケンスになります。結果は数日後に管理画面から、またダウンロードできるような形で渡して、この結果をもってショップさんの方で実際課金が成功したかどうかを確認するというような流れになっています。

パターン3

article-069_thumb05.jpg

続いて3番目。
先ほどとほとんど変わらない物になっていますが、ファイルを手動でアップロードするという部分を自動化できるようにもしています。
こちらはSFTPでやりとりをしています。1個前の「パターン2」同様に締め切りがあるものになります。件数上限がございませんので、本当に多いお客様ですと200万件近いものを毎月毎月上げてきているような形になっています。

パターン4

article-069_thumb06.jpg

続けて4番目。
これは先ほどのファイルのSFTPでアップロードする物の発展した形になります。事前にショップさんの方でこの会員の方に毎月幾らという定義を全員にやっておいていただきます。その定義に基づいて自動でリクエストを作って、ファイルをアップロードする相当のことをしています。
以降は先程のパターン2パターン3と同じような形で課金をしていきます。

パターン5

article-069_thumb07.jpg

こちらもパターン2、3に近いのですが、特徴的なのが即時であるということです。
これまで1番目以外で説明したものは、1カ月サイクルになっていますので毎月の締め切りがありますが、このパターンは締め切りはなくファイルをアップロードしておよそ数時間以内にはすべて処理を終え、結果をお戻しすることができるようになっています。
最近はやはり1カ月サイクルではなかなかショップさんの課金のサイクルに合わないこともあり、このような即時型の物も提供しております。ただし件数上限を設けており、およそ数万件という規模のお客さんにご利用いただくことが多いです。

article-069_thumb08.jpg

パターン4に戻りますが、1カ月サイクルの物については一つ特徴がございます。Loopで囲っている当社からクレジット会社に送信している「オーソリ・与信リクエスト」。これを一定の条件を満たせば無くてもいいようにしています。「フロアリミット運用」といいまして、一定の条件を満たせば毎月必ず与信をとる必要なく、確定ファイルを翌日に送るということが可能になっています。
なぜ説明したかといいますと、パターン5の即時型はフロアリミット運用をすることはできません。そのあたりはそれぞれ特徴が違っています。

このようにサブスクというか継続的なお支払いの仕方一つをとっても、いろんなパターンを用意しております。小規模なショップさんから大規模なところまで、すべての細かいニーズに対応できるような形でいろんなパターンを用意しております。

洗替処理パターン

article-069_thumb09.jpg

最後に補足になりますが、皆さんサブスク決済等でカード登録をしていると思いますが、例えば有効期限が切れた、プランを変えてゴールドカードになったなど、クレジットカードの情報が変わります。変わった後は様々なネットサイトやサブスクに登録しているクレジットカード情報を変えないと引き落としができない、というような案内をもらうことがあると思います。
一方一部のサイトだと、自動で新しい情報に切り替わっていることがあるのではないでしょうか。実は当社のような会社が、毎月クレジットカード会社さんに今のカード番号をすべてお送りして、必要な情報をいただいてわれわれが充当しております。
当社では「洗替(洗い替え)」と呼んでおりまして、毎月このようにすべてのカードのリフレッシュをかけています。これもサブスク型というか、お支払いを継続する上で非常に重要な要素になっており、更新漏れで課金できないといったことを事前に防げるようにしています。

当社のBatchアプリ

ここからは先程説明した定期的なお支払いの裏側。実際当社のシステムでどのようなアプリケーションでやっているかを説明いたします。

バッチjar

article-069_thumb10.jpg

上の図が当社のBatchアプリケーションの構成になっています。非常にシンプルで一つの大きなBatchのjarファイルというものを用意してあるだけで、構成はSpring bootのもので構成しています。Spring BatchとSpring Web JPそれからSpring MVCも使っており、通常コマンドラインであったりBatch起動時に自動であげるというやり方が主流だと思うのですが、常時稼働スケジュールや、リカバリのために手動での起動というのを考慮して、全てHTTPでBatchを起動する形をとっています。

先程もあったように様々なパターンがあり、Batchもかなり多くの種類を用意していますので、すべて一つのSpring bootのjarファイルの中に押し込んであります。一つ特徴的なのがSpring Batch標準のJobRepository機能は使っていません。主にこのJobRepository機能はリトライであったり、リランのために使われることが多いと思いますが、当社の場合このBatchの前にもともとあった物があって、その仕組み自体がかなりリランの考慮された設計になっていましたので、使わないという判断をしてこのJobRepository機能は完全無効化しております。

またコンテナは使用していません。オンプレミスの環境でOSの上でjarファイルを置いて起動して常時稼動している状態で、Batchのリクエストが来るのを待ち受けている形をとっています。先程のパターンであったようにショップさんはファイルを24時間365日いつ上げても動き出せるような形を取るために、このようにしております。

パターン1

article-069_thumb12.jpg

続けてこのBatchアプリケーション自体、実は作ったのは3年前の2018年です。2018年当時はまだあまりこのSpring batchに関する参考資料が無かったため、なかなか構築するのに苦労しましたが、自分たちのアプリケーションに合う形ということでSpring Batchを使っていくつかのアプリケーションのパターンを決めて、それに沿って構築していくという形を取りました。

先ほどの説明にあったように、かなりのショップさんが連なっています。数千ぐらいあるイメージでいただければと思います。数千あるショップも多いところは200万件、300万件のデータを持っていますし、少ないところでは100件ぐらいといった形。いろいろなパターンがあります。これらを効率よく処理していくために、やはりショップ単位にスレッドを分けて並列で処理するという形で、将来的なスケールができるようにしています。スケールの仕組みなどは後述いたします。

当社の場合、まずジョブのパターンを四つほどに分けて、必ずこの四つのどれかの形をとるようにしています。
一つ目が、入力は特になく最終的な出力はデータベースへのステータスの反映をする形。一番シンプルなものです。Spring Batch、Chunk形式のReader/Processor/Writerの物もありますけれども、極力それを使わなくても済むものはTaskletでやってしまえば良いという整理で、このようなものはTaskletで分けて処理しています。

パターン2

article-069_thumb12.jpg

続けて次のパターンです。これは入力がデータベースから大量のレコードを取って、そこからファイルに出力していくというようなパターンを想定しています。ショップさんのレコードが100万ぐらいもうすでにある状態から、そのレコードをファイルに出力するような場合はこの形をとっていて、これもChunk形式は取らず本当に単純なTaskletでやっています。
ただ書き込み時は、JdbcTemplateのRowCallbackHandlerを使いながらFetchしながら書き出すというような形を必ず取るようにしております。

パターン3

article-069_thumb13.jpg

次のパターン。ここからChunkを使います。書き込み時にデータベースに大量のレコードを書き込むないしアップデートするという形は、Chunk形式を必ず取るようにしています。書き込み時はこちらも同じように JdbcTemplateのbatchUpdate機能を使って、必ず一括更新するような形をとっています。

パターン4

article-069_thumb14.jpg

最後のパターンです。1個前のものはファイルからデータベースへ、だったのですが、今回は違うテーブルから違うテーブルへの書き込みやインサートというのを想定したパターンで、書き込みのライター部分は同じです。batchUpdateで一括で反映させますが、読み込むところは必ずJdbcPagingItemReaderを使ってFetchしながら読んでいき、その単位でChunkを回すという形をとるようにしています。

月間処理時間の結果

article-069_thumb15.jpg

数十個のジョブがありますけれども、すべてこのどれか四つのパターンに必ず当てはまるように設計・構築をして、2018年のSpring bootのBatchに置き換える前にはトータルで月間大体100時間弱ぐらいがかかり、ひどいジョブでは24時間つきぬけるようなこともあったわけですが、このアプリケーションに置き換えたことで、およそ月間14時間ぐらいまでにおさめることができるようになっております。
ただし、こちらは3年前の数字ですので、今は大体これの倍ぐらいの数にはなっているなと思います。ただスレッド数をチューニングするなどして、この下の処理時間というのは一定時間内をキープできるようにしております。

3年間の性能関連のトラブル

article-069_thumb16.jpg

今回この登壇のお話を頂いた時にいろいろ内容を考えたのですが、当社のBatchアプリケーションは3年間性能関連のトラブルはゼロでやっているという実績がございますので、このお話をご紹介できればと思った次第です。

article-069_thumb17.jpg

実際これはヒープのメモリの状況です。もっとも処理が集中する5日、10日前後のところをお見せしていますけれども、非常に安定した稼働ができていることをお示しできるかなと思います。

Tips

では最後、ここからは実際にSpring Batchのアプリケーションを作った際にいくつか工夫したことや気をつけていたことなどをご紹介いたします。

起動はHTTP

article-069_thumb18.jpg

まずは起動方法です。これはSpringのWeb MVCを使われている方はすぐお分りになると思いますが、下のようにRestControllerでコントローラーを定義しておいて、外部からcurlなどで叩けるようにしています。
具体的には下のSampleBatchResponseのSampleJobControllerというところでリクエストを受け付けて、JobLauncherをキックするという形をとっています。
こうしておくことのメリットは、色々なスケジューラや他のサブアプリケーションからも単発ジョブをキックしたりということが可能になりますし、ショップさんもどうしても締め切りに間に合わずにアップロードを忘れてしまうということがあるわけです。そのような場合の単純なリランであったりということは、コマンドラインのcurlで単発キックするというような形を取れるかなと思っています。

起動オプション

article-069_thumb19.jpg

続いて、起動オプションで必ずどのショップさんをやるか、ないしどのショップさんを除外するかというオプションを、必ずすべてのBatchジョブに適応させています。これは先に説明した通り、どうしても遅れてしまったり大量のデータがあるお客様だけ後で回すとか、そういったことを可能にしています。幸いにも除外のオプションはこれまで使った事はありませんが、どうしてもここだけもう一回締め切った後にやり直したいというご要望を頂いたときは、ピンポイントで、そこのショップさんを指定しBatchを動かせるようにしています。

すべてのジョブは必ずこれを実装するような形。実装上もすべてここの部分は共通化してabstractクラスに押し込んで使えるような形にしています。
下に書いてありますとおり、例えばcreate job parameterというメソッドです。

article-069_thumb20.jpg

こういったところでジョブパラメーターとして裏で全部自動設定するような形を取っていますし、パーティションの話になりますが、パーティションの中でもジョブパラメーターを必ずとる。これは実際SamplePartitionerになっていますがabstract形式にして、すべてのジョブのPartitionerはこの形をとるようにしています。
実際パーティションが呼ばれたときには、対象のショップなのか除外のショップなのかというのは、引数を見ながら処理対象のショップの一覧を作っています。その中で指定されたものだけを作るのか、すべてなのか、除外指定されたものを一覧にするのかといった形で、後続の処理対象を絞っています。パーティションは後述します。

Taskletで大量のDBレコードをファイルに書き込み

article-069_thumb21.jpg

続けて先のパターンにあったようにいくつかパターンを用意している中で、Taskletでデータベースに大量に書き込むという場合です。この場合はJdbcTemplateのRowcallbackhandlerを使っています。これは特にSpring Batchは関係ない話で、記載の例のようにTaskletからこのcreateFileを呼ばれることを想定していますが、呼ばれた後は単純にRowcallbackhandlerで書いていくという形をとっています。

JdbcTemplateはStepScopeにしてステップごとにフェッチサイズを割り当て

article-069_thumb22.jpg

このJdbcTemplateをインジェクションせずに渡しています。渡しているのはBatchのConfigurationクラスの方で実はインスタンス生成して、その際にFetchサイズを設定してからTaskletに渡すようにしています。
このFetchサイズというのは、必ず外部から変更可能な形にしています。当社の場合環境変数で持たせていますので、環境ごとにFetchサイズを変えながらチューニングできるような形をとっています。こうしておくことでBatch一つが大きなjarファイルになります。JdbcTemplateはステップごとに固有のスコープにしないとシングルトンになってしまって、あるジョブはFetchサイズ100だけど、あるものは300という形で、動き出すたびにFetchサイズが変わってしまいます。このような形で必ずステップごとにスコープを切って渡すような形を取っております。

下が実際のTaskletのサンプルです。JdbcTemplateはインスタンス生成時に渡されるような形をとっています。どのショップを処理するべきなのかというのは、先程お見せしたPartitionerでstepExecutionContextに渡していますので、このような形でStepExecutionContextを使って受け取りをしています。

PartitionerとTaskExecutorでマルチスレッド処理

article-069_thumb23.jpg

続けてPartitionerで分解するのは良いのですが、実際にマルチスレッドを裏で動かすというのは具体的にはSpringのThreadPoolTaskExecutorでやっています。
これはPartitionerのクラスです。実際Partitionerでやっていることというのは、単純に処理対象のショップさんの一覧を作るだけです。それをExecutionContextに詰めて、マップに格納しておしまいになります。

article-069_thumb24.jpg

実際にスレッドごとに分けるのは、ThreadPoolTaskExecutorに任せています。こちらもBatchのConfigurationクラスで外部からcorePoolSizeでそれぞれ渡せるようにしておき、起動時にTaskExecutorをきかせて大体どのジョブも5から10、物によっては3などにチューニングをして、負荷や最適な処理時間を見ながら、変えつつチューニングできるようにしています。
これはすべてのジョブ単位にcorePoolSizeを変えるような形式を取っています。一番下のメソッドにあるようにExecutorというところをThreadPoolExecutorに置き換えてcorePoolSizeを設定するということをやっておけば、後はPartitionerに全部渡してもPartitionerから先はcorePoolSizeで指定した分のスレッドで処理が進んでいきます。

article-069_thumb25.jpg

これが絵にしたものです。こちらの絵自体はSpring BatchのDocsのほうにある絵なのですが。パーティションの絵はあったのですが、そことThreadPoolTaskExecutorのセットの絵はなかったので、補足いたします。
パーティションで分けた物をexecuteでステップを分けていますが、赤線で囲っている部分はcorePoolSizeで指定した部分をマルチスレッドにしております。
Partitionerはいろんな使い方があると思いますが、当社の場合は単純に一覧を作るだけです。Spring Batchの標準でグリッドサイズというのを渡されるようになっていますが、私たちの使い方であればグリッドサイズは全く使いません。ただ単純に一覧だけ作り、あとはTaskExecutorに任せるという形をとっています。

ChunkでファイルからDB一括インサート

article-069_thumb26.jpg

続いてChunkの場合をいくつか見ていきます。
ChunkWriterでテーブルにインサート、ないしアップデートをかけていくというパターンを見ていきますと、こちらも必ずcorePoolSizeで外部から渡すようにしているのと、加えてChunkサイズというのも必ずジョブ別に渡すようにしています。Chunkサイズイコール、テーブルへのコミット件数です。
何件アップデートしてコミットするかという単位と同等として扱っており、このChunkサイズを外部から渡してChunkサイズ自体は一番下にあるsampleChunkStepにあるようにChunkの引数としてChunkスレッドを渡しています。
その結果Writerに渡ってくるときは、このChunkサイズ分のリストの件数でWriterに渡されますので、そこから先はバルクインサートするだけとなっています。

これまでにデータベースの書き込み部分は主にJdbcTemplateを使っているという話はしてきていますけれども、最初はJPA(Spring Data JPA)を使っておりました。しかし性能がなかなか出ないという問題にぶつかり、最終的にはJdbcTemplateを使って解消しています。
したがってJPAも使い分けかなと思っています。1行単位のセレクトであったり更新というのはSpring Batchアプリケーション中でもJPAを使っていますが、何かを一括でセレクトする、アップデートするという場合は、必ずJdbcTemplateを使うようにしています。

article-069_thumb27.jpg

こちらWriterのサンプルです。ステップが始まる前に各々インスタンスを生成しています。実際このバルクインサートというのがWriter処理の中で呼ばれますが、JdbcTemplateをセットで渡しています。何らかの経緯があってステップ単位でインスタンスを分けたのかなと思いますが、現状このような形をとっています。

article-069_thumb28.jpg

これはバルクインサートの中身になっています。書き方は標準的なJdbcTemplateのBatchアップデートの物になっていますが、まずBatchPreparedStatementSetterを用意しておき、ここのBatchサイズというのはChunkで渡ってくるリストのサイズを渡すことで、100なら100でChunkも100単位で回すし、このパッチアップデートも100件単位のコミットという形がとれるようになっています。

ファイルのReaderクラスを便利に

article-069_thumb29.jpg

ここは補足的に、ファイルから読み込む処理は多々ありますけれども、標準のファイルリーダーがSpring Batchには用意はされています。私達はどうしても運用上ログファイルを追いかける場面が多く、しかも100万件、200万件あるようなショップさんの処理が多いというというときに、動いているのか止まっているのか分からないという問題があります。途中の経過状況を見たいときに、標準のファイルリーダーを出すことが難しかったので、このような形で独自のファイルリーダーを用意しています。処理件数を都度アップしながら、1万件単位にログを出して「今何件処理しています」というのを出すようにしています。

大量データをテーブルから取得して別のテーブルへ

article-069_thumb30.jpg

では最後のパターンです。大量のテーブルをリーダーにする場合の書き方を紹介します。大量のデータをテーブルから読み込む場合、当社ではJavaのSpring Batch標準のJdbcリーダーを用意しています。ただFetchサイズ自体は、これまで説明したのと同じようにConfigurationクラスで外部から渡し、ここの例では300としていますがリーダークラスにconstructorsで渡しています。
コンストラクタ内でFetchサイズを持たせておいて、実際はbeforeStepの中でJdbcItemReaderにFetchサイズを渡すという形です。これで300と決めたら300単位でChunkerもありますし、データベースも300件Fetchしながら読むということが可能になります。

最後に

最後の方はかなり実際のコードを見ながらご紹介いたしました。馴染みがない方にはなかなか難しかったかと思いますが、これまで説明してきましたように、このアプリケーションコースで3年間、月間約1億に近い以上の数の処理を問題なくさばけております。
また当社の特徴でもあります、件数の増加、毎年の25パーセントの成長を続けても耐えうる構成にできているかなと思います。私たちは一緒に処理件数を25パーセント成長させていく仲間を募集しています。ご興味がある方はぜひよろしくお願いします。

features03_mainv.png

エンジニアイベント書き起こし記事一覧はこちら

recruit_btn.png

採用情報はこちら

feature01_btn.png

パートナーインタビュー記事一覧はこちら

※本コンテンツ内容の著作権は、GMOペイメントゲートウェイ株式会社に属します。

SSL GMOグローバルサインのサイトシール