Upgrade to Pro — share decks privately, control downloads, hide ads and more …

決済システムで学ぶレジリエントなサービスのいろは / The guide of resilient service learned with payment systems

E228752b0ba3d85d485083c7fc6c3622?s=47 hainet
December 06, 2021

決済システムで学ぶレジリエントなサービスのいろは / The guide of resilient service learned with payment systems

2021年12月3日にオンラインで開催されたSpring Fest 2021に登壇したときに使用した資料です。
決済システムの要件を参考にレジリエントなサービスを開発するために導入している工夫をご紹介しています。

▼Spring Fest 2021公式ページ▼
https://springfest2021.springframework.jp/

▼講演アーカイブ▼
https://www.youtube.com/watch?v=9-yDaFlGTxE

E228752b0ba3d85d485083c7fc6c3622?s=128

hainet

December 06, 2021
Tweet

Transcript

  1. 決済システムで学ぶ レジリエントなサービスのいろは Spring Fest 2021 December 3 2021 Haine Takano

    (@hainet50b), SB Payment Service, haine.takano@g.softbank.co.jp 1
  2. SBペイメントサービス株式会社 アプリケーション開発者 髙野 はいね( @hainet50b) 自己紹介 決済システム(Java, Spring Boot)の開発に従事 2016年にSBペイメントサービス株式会社に入社

    主な業務 ・決済システム開発 ・加盟店様の決済システム導入時の技術サポート 2
  3. ソフトバンク携帯ユーザー向けの 「ソフトバンクカード」のカード発行・ 運営をしています。 ソフトバンクカードは、 Visa加盟店 で利用できるプリペイドカードです。 ご利用金額に応じて Tポイントが貯 まります。 カード発行業務

    決済代行 EC運営事業者さま向けにオンライン決済 事業を運営しています。豊富な決済手段 をまとめてご提供しています。 カード加盟店業務 Visa、Mastercard、UnionPay(銀聯)のメン バーシップライセンスを保有しており、各ブラ ンドのアクワイアラ(クレジットカード加盟店 契約会社)としての加盟店審査や管理事 業、端末決済サービスを提供しています。 ソフトバンクと共同で、ソフトバンク 携帯ユーザー向けの通話料合算 請求「ソフトバンクまとめて支払い」 の開発・運営をしています。 キャリア決済 EC/ネット店舗 実店舗/訪問販売 SBペイメントサービスの事業内容 3
  4. ソフトバンク携帯ユーザー向けの 「ソフトバンクカード」のカード発行・ 運営をしています。 ソフトバンクカードは、 Visa加盟店 で利用できるプリペイドカードです。 ご利用金額に応じて Tポイントが貯 まります。 カード発行業務

    決済代行 EC運営事業者さま向けにオンライン決済 事業を運営しています。豊富な決済手段 をまとめてご提供しています。 カード加盟店業務 Visa、Mastercard、UnionPay(銀聯)のメン バーシップライセンスを保有しており、各ブラ ンドのアクワイアラー(クレジットカード加盟 店契約会社)としての加盟店審査や管理事 業、端末決済サービスを提供しています。 ソフトバンクと共同で、ソフトバンク 携帯ユーザー向けの通話料合算 請求「ソフトバンクまとめて支払い」 の開発・運営をしています。 キャリア決済 EC/ネット店舗 実店舗/訪問販売 SBペイメントサービスの事業内容 4
  5. 加盟店 決済機関 通販サイト ゲーム 教育 不動産 その他 電子書籍/動画 チケット クレジット

    携帯キャリア決済 コンビニ支払い プリペイドカード 口座振替 ポイント支払い アカウント連携決済 Webシステム 決済代行サービス 5
  6. 加盟店 決済機関 通販サイト ゲーム 教育 不動産 その他 電子書籍/動画 チケット クレジット

    携帯キャリア決済 コンビニ支払い プリペイドカード 口座振替 ポイント支払い アカウント連携決済 決済代行サービス Tanzu Application Service ・・・ 6
  7. 加盟店 決済機関 通販サイト ゲーム 教育 不動産 その他 電子書籍/動画 チケット クレジット

    携帯キャリア決済 コンビニ支払い プリペイドカード 口座振替 ポイント支払い アカウント連携決済 決済代行サービス Tanzu Application Service ・・・ 7 参考:https://www.slideshare.net/JunyaSuzuki1/cloudnative2-sfa4
  8. 加盟店 決済機関 通販サイト ゲーム 教育 不動産 その他 電子書籍/動画 チケット クレジット

    携帯キャリア決済 コンビニ支払い プリペイドカード 口座振替 ポイント支払い アカウント連携決済 決済代行サービス Tanzu Application Service ・・・ 8 プラットフォームや開発体制ではなく 運用や実装に着目してお話いたします。
  9. 決済システムに 求められる要件とは? 9

  10. 決済システムに求められる要件とは? 1. 1件でも多く成立させる 2. 不整合を何としても避ける 3. 決済を特定できるようにする ※お金だけ引き落とされて商品が提供されない状態 ※逆にお金が引き落とされずに商品が提供されてしまう状態 10

  11. 1. 1件でも多く成立させる 2. 不整合を何としても避ける 3. 決済を特定できるようにする ※お金だけ引き落とされて商品が提供されない状態 ※逆にお金が引き落とされずに商品が提供されてしまう状態 決済システムに求められる要件とは? -

    リトライ - 非同期 11
  12. 1. 1件でも多く成立させる 2. 不整合を何としても避ける 3. 決済を特定できるようにする 決済システムに求められる要件とは? 12 ※お金だけ引き落とされて商品が提供されない状態 ※逆にお金が引き落とされずに商品が提供されてしまう状態

    - タイムアウト - 冪等
  13. ※お金だけ引き落とされて商品が提供されない状態 ※逆にお金が引き落とされずに商品が提供されてしまう状態 1. 1件でも多く成立させる 2. 不整合を何としても避ける 3. 決済を特定できるようにする 決済システムに求められる要件とは? -

    ログ - 分散トレーシング 13
  14. このような要件を満たす レジリエントなサービスを開発するために 弊社が取り入れた工夫をご紹介します すべての方に当てはまるものではないかもしれませんが 参考にしていただけますと幸いです 14

  15. お品書き No 手法 対応するライブラリ 想定される効果 1 タイムアウト Spring Web -

    滞留による全断を防ぐ 2 リトライ Spring Retry - 1件でも多く決済を成立させる 3 冪等 - - リトライ可能にする 4 分散トレーシング Spring Cloud Sleuth / Zipkin - 処理を追跡する - 処理時間の傾向を把握する 5 非同期 Spring Cloud Stream - パフォーマンスを向上させる 6 キャッシュ Spring Cache - 障害ポイントを減らす 7 サーキットブレーカー Resilience4j - 障害の拡大を防ぐ 8 バルクヘッド Resilience4j - 対向先の障害を防ぐ この資料が見返して役に立つ「いろは」になりましたら幸いです 🙇 15
  16. 1-1. タイムアウトを設定する 16

  17. 決済機関 17 1-1. タイムアウトを設定する ECサイト 決済システム 1500 ms 1000 ms

    通常時はスムーズにリクエストが流れていますが...
  18. 決済機関 18 1-1. タイムアウトを設定する ECサイト 決済システム 60500 ms 60000 ms

    決済機関が障害で遅延することはよくあります。
  19. 決済機関 19 1-1. タイムアウトを設定する ECサイト 決済システム 放っておくとリクエストが徐々に滞留して...

  20. 決済機関 20 1-1. タイムアウトを設定する ECサイト 決済システム Webスレッドが枯渇してサービスが全断します。 新規のリクエストを受け付けられません ><

  21. 決済機関 21 1-1. タイムアウトを設定する ECサイト 決済システム 適切なタイムアウトを設定すると... ダメそうなので諦めます! 60000 ms

    15500 ms 15000 ms
  22. 決済機関 22 1-1. タイムアウトを設定する ECサイト 決済システム 何とか持ち堪えます。 60000 ms 15000

    ms 15500 ms 滞留はしたけれどちょっとだけ!
  23. タイムアウト設定を長めにすると、 - 対向先の遅延の度合いによっては滞留が発生してしまう。 - 偶然発生した1件の遅延を救えることがある。 タイムアウト設定を短めにすると、 - 対向先の状況に関わらず安定したパフォーマンスを発揮する。 - あと100ms待てば決済を成立させられた...かもしれない。

    業界によって設定のさじ加減は変わってくるかと思います。 弊社では1件でも多く決済を成立させるために長めにすることが多いです。 23 1-1. タイムアウトを設定する
  24. Spring Web RestTemplateのタイムアウト設定はデフォルトで無限です。 Bean定義する際には以下のようにして必ずタイムアウトを設定します。 24 1-1. タイムアウトを設定する @Bean public RestTemplate

    restTemplate(RestTemplateBuilder builder) { return builder .setConnectTimeout(Duration.ofMillis(2_000)) .setReadTimeout(Duration.ofMillis(15_000)) .build(); }
  25. 1-2. タイムアウトを実施した その後に全力を注ぐ 25

  26. 決済機関 26 1-2. タイムアウトを実施したその後に全力を注ぐ ECサイト 決済システム タイムアウトするとお客様にエラーを返します。 ごめんなさい。決済できませんでした 🙇

  27. 決済機関 27 1-2. タイムアウトを実施したその後に全力を注ぐ ECサイト 決済システム しかし実は決済が成立しているかもしれません。 商品は提供しないようにします。 遅くなりましたが、 代金を引き落としておきます。

    このような不整合は何としても避けなければなりません。
  28. 1-2. タイムアウトを実施したその後に全力を注ぐ このような状況には大きく2つの対策方法があります。 1. 取消を使う 決済機関にはエンドユーザーに明細が残らない 障害取消という機能があり、こちらをよく使います。 2. 参照を使う 不整合疑いのトランザクションを一旦キューに保持します。

    時間をおいてから定期的に状態を参照して、 取引が存在しないか失敗していた場合はクローズ、 取引が成功してしまっていた場合は取消を行います。 こちらに全力を注ぐイメージを 次のスライドで説明します。
  29. 決済機関 29 1-2. タイムアウトを実施したその後に全力を注ぐ 決済システム 課金リクエストでタイムアウトを検知したら... ①タイムアウト

  30. 決済機関 30 1-2. タイムアウトを実施したその後に全力を注ぐ 決済システム 同一トランザクション内で取消を投げます。 ①タイムアウト ②取消

  31. 決済機関 31 1-2. タイムアウトを実施したその後に全力を注ぐ 決済システム 取消にも失敗したら... ①タイムアウト ②取消

  32. 決済機関 32 1-2. タイムアウトを実施したその後に全力を注ぐ 決済システム キューに格納して、時間をおいて投げ直します! ①タイムアウト ②取消 ③キューに格納 ④取消(2回目)

    決済 サブシステム 後ほど取り上げます。
  33. 決済機関 33 1-2. タイムアウトを実施したその後に全力を注ぐ 決済システム それでも失敗したら... ①タイムアウト ②取消 ③キューに格納 ④取消(2回目)

    決済 サブシステム
  34. 決済機関 34 1-2. タイムアウトを実施したその後に全力を注ぐ 決済システム キューに格納し直して、n回やり直します!!! ①タイムアウト ②取消 ③キューに格納 ④取消(2回目)

    決済 サブシステム ⑤キューに格納(以下繰り返し) ※さらに一定回数を超えた取引はアラートで検知して手運用に回ります。  タイムアウトは自社の判断なので責任を持ちます。
  35. 2. リトライで1件でも多く救う 35

  36. 36 2. リトライで1件でも多く救う 外部決済機関との通信では リトライできるエラーとそうでないエラーがあります。 エラーの種類 リトライの可否 Connection Reset /

    Refused ◦ Connect Timeout ◦ Unknown Host ◦ Read Timeout × HTTPステータスコード400系 △(多くの場合で必要ありません) HTTPステータスコード500系 × 通信に関するエラーについては救うことができます。 その他は2重課金の恐れがあるためリトライできません。 いずれもTCP接続に関するエラーで リクエストが到達していないことが 保証されているためリトライできる クラウド上にアプリケーションを 配置するようになってから 発生することが増えた
  37. 37 2. リトライで1件でも多く救う RestTemplateではタイムアウト時にResourceAccessExceptionが発生します。 この例外が持つメッセージでタイムアウトの種別を判別することができます。 public String lookup() { try

    { return restTemplate.getForObject("/lookup", String.class); } catch (ResourceAccessException e) { final String message = e.getMessage(); if (message != null && message.contains("Read timed out")) { // Handle read timeout. throw new RuntimeException(e); } else if (message != null && message.contains("connect timed out")) { // Let's retry! throw new RetryableException(e); } else { // Handle other timeout. throw new RuntimeException(e); } } } Java 17では “connect timed out”の”c”が大文字になって “Connect timed out”となっていました。 文字列の検査は危険な実装ですので注意が必要です。
  38. 38 2. リトライで1件でも多く救う Spring Retry spring-retryをアプリケーションの依存に追加することで 簡単にリトライを実装できます。 <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId>

    </dependency>
  39. 39 2. リトライで1件でも多く救う @SpringBootApplication @EnableRetry public class SpringFest2021Application { //

    omit } @EnableRetryを付与することで リトライ機能が有効になります。 @Retryable( // リトライ対象とする例外を指定します。 // 抽象化した例外を作成すると見通しが良くなります。 value = RetryableException.class, // 処理が実行される最大数です。 // 3の場合は最低1回の処理と最大2回のリトライが実施されます。 maxAttempts = 3, // リトライの間隔を指定します。 // サンプルの場合は1回目のリトライは1秒後に、 // 2回目のリトライは2秒後に実施されます。 backoff = @Backoff(delay = 1_000, multiplier = 2.0) ) public String lookup() { // omit } @Retryableで設定を指定します。
  40. 3. 冪等を導入して リトライしやすいシステムにする 40

  41. 決済機関 41 3. 冪等を導入してリトライしやすいシステムにする 決済システム 課金は2重課金の恐れがあるためリトライできませんでした。 ①課金タイムアウト ②課金リトライ 2重課金の恐れがある 💸

  42. 決済機関 42 3. 冪等を導入してリトライしやすいシステムにする 決済システム 参照ならば問題なくリトライすることができます。 ①参照タイムアウト ②参照リトライ 何回やっても同じ!

  43. 43 3. 冪等を導入してリトライしやすいシステムにする 参照がリトライできるのは冪等という性質を持っているためです。 冪等とは何回やっても状態が変わらない ような性質のことをいいます。          ※例えばHTTP RESTのGET, PUT,

    PATCH, DELETEは冪等の性質を持っています。 本当にリトライしたい(=冪等であって欲しい)のは課金リクエストです。 この課金リクエストに冪等の性質を持ち込む工夫をご紹介いたします。 【参考】 冪等についてはStripe社やSquare社など多数の採用実績があります。 Stripe: https://stripe.com/docs/api/idempotent_requests Square: https://developer.squareup.com/docs/working-with-apis/idempotency
  44. 44 App 3. 冪等を導入してリトライしやすいシステムにする ①Appへのリクエストに  冪等キーを要求する。 ①課金インターフェースに冪等キーを要求します。 冪等キーは以下の性質を持つ ・利用者が任意に決める ・冪等にしたい単位ごとに

     毎回必ず変更をしなければならない  例:課金ならば一つの課金ごとに    一つの冪等キーを発行する
  45. 45 App 3. 冪等を導入してリトライしやすいシステムにする ②冪等キーとAppへのリクエスト内容の組み合わせを保管します。 ②以下の組み合わせを保管する。 - 冪等キー - HTTPメソッド

    - エンドポイント - HTTPリクエストボディ idempotency_key: 9c4d5af5-4114-4fd6-ac94-160fee09de41 POST /authorize { "transaction_id": "spring-fest-2021-0001", "amount": 1980 } 保管する組み合わせの例
  46. 46 App 3. 冪等を導入してリトライしやすいシステムにする ③冪等キーとAppからのレスポンス内容の組み合わせを保管します。 ③以下の組み合わせを保管する。 - 冪等キー - HTTPステータスコード

    - HTTPレスポンスボディ idempotency_key: 9c4d5af5-4114-4fd6-ac94-160fee09de41 200 OK { "result": "ok" } 保管する組み合わせの例 ①~③の情報を用いると冪等な振る舞いを持つ APIを作ることができます。
  47. App A 47 App B 3. 冪等を導入してリトライしやすいシステムにする 適用パターン1:2回リクエストをしてしまった ①1回目のリクエスト ②1回目のリクエストに

    対応する処理結果を返却する ③2回目のリクエスト ④②とまったく同じ レスポンスを返却する App Bは何度でも同じレスポンスを返却できます👍 処理は実行しない
  48. App A 48 App B 3. 冪等を導入してリトライしやすいシステムにする 適用パターン2:リクエストがタイムアウトしてしまった ①1回目のリクエストが タイムアウトした

    ②2回目のリクエスト ③①で返却するはずだった レスポンスを返却する App Aは結果が返ってくるまで何度でもリトライできます👍 バックエンドで 処理が実行されている 可能性がある 処理は実行しない
  49. App A 49 App B 3. 冪等を導入してリトライしやすいシステムにする 適用パターン3:「短時間で」2回リクエストをしてしまった ①1回目のリクエスト ②2回目のリクエスト

    ③1回目のリクエストが 処理中のためエラーを返却する ④1回目のリクエストに 対応する処理結果を返却する App Bは処理中は同一のリクエストを受け付けないようにできます👍 処理は実行しない ネットワークの問題による 意図しない再送など NGとするか 冪等に忠実に待たせるか 2択を考えたが 滞留を避けるため NGにすることにした
  50. 4-1. 分散トレーシングで 処理の流れを追う 50

  51. 51 App A 4-1. 分散トレーシングで処理の流れを追う App B App C Request

    X 複数のアプリケーションで構成されるシステムにリクエストを送信すると...
  52. 52 App A 4-1. 分散トレーシングで処理の流れを追う App B App C Request

    X Log A Log B Log C それぞれのアプリケーションはリクエストに対応するログを出力します。 タイムスタンプを参考にして Log A -> B -> Cの順で見れば 処理の流れを追うことができる。
  53. 53 App A 4-1. 分散トレーシングで処理の流れを追う App B App C Request

    X Log A Log B Log C リクエストが複数になると対応関係を把握することは難しくなります。 Request Y Log A Log B Log C とあるLog Aから どのLog Bにつながっているのか分からない
  54. 54 App A 4-1. 分散トレーシングで処理の流れを追う App B App C Request

    X Log A-X Log B-X Log C-X そこでリクエストごとに固有の通し番号を付与すれば解決する、 という手法が分散トレーシングです。 Request Y Log A-Y Log B-Y Log C-Y
  55. 55 4-1. 分散トレーシングで処理の流れを追う Spring Cloud Sleuth spring-cloud-starter-sleuthをアプリケーションの依存関係に追加すると TraceIDとSpanIDをSLF4JのMDCに追加してくれます。 <dependency> <groupId>org.springframework.cloud</groupId>

    <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> ログの例 3件のログに対して それぞれTraceIDが付与されている。
  56. 56 4-1. 分散トレーシングで処理の流れを追う App A App B Spring Cloud SleuthではB3という形式でTraceIDを伝播させています。

    Spring Cloud Sleuthを適切に設定することで、 RestTemplateやRabbitTemplate、KafkaTemplateなど 様々なプロトコルに対応したB3ヘッダを付与してくれます。 これによってSpring Cloud Sleuthを採用したアプリケーション間では 実装者が意識することなくTraceIDを引き継ぐことができます。 TraceID Spring Cloud Sleuth Spring Cloud Sleuth RestTemplate, RabbitTemplate, etc...
  57. 57 4-1. 分散トレーシングで処理の流れを追う Zipkin 弊社では分散トレーシングにZipkinを採用しています。 spring-cloud-sleuth-zipkinをアプリケーションの依存関係に追加すると トレースデータをZipkinサーバーに送信してくれるようになります。 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin</artifactId>

    </dependency> データストア App TraceID / SpanID Spanの開始終了時刻など HTTP
  58. 58 4-1. 分散トレーシングで処理の流れを追う Trace Span アプリケーション名 Span Nameと処理時間 ZipkinダッシュボードでTraceをビジュアルで把握することができます。 Traceあたり100以上のSpanになることもあり、

    増やせばよいというものでもないと感じています。
  59. 59 4-1. 分散トレーシングで処理の流れを追う Servlet FilterやRestTemplateで自動でSpanを切ってくれますが、 実装で任意のポイントでSpanを切ることもできます。 @NewSpan("span-name") public void spanByAnnotation()

    { // do something } アノテーションで指定する場合 Tracerで指定する場合 private final Tracer tracer; public SpringFest2021Service(Tracer tracer) { this.tracer = tracer; } Span newSpan = tracer.nextSpan().name("span-name"); try (Tracer.SpanInScope ws = tracer.withSpan(newSpan.start())) { // do something } finally { newSpan.end(); } SQLの実行や、 ファイルの読み書きに付与することが多いです。
  60. 4-1. 分散トレーシングで処理の流れを追う App トレースデータのサンプリングレートを設定することができます。 spring: sleuth: sampler: probability: 1.0 #

    割合で設定する。 # rate: 10 # 秒間件数で設定する。 0 〜 100 %の割合で送信 or 秒間n件だけ送信 処理の傾向を掴めば十分である場合には 低めに設定します。 弊社では例外的な遅延も検知したいため probability 1.0 (100%)で運用しています。 開発環境ではZipkinに負荷をかけないために rate 10 (秒間10件まで)で運用しています。
  61. 4-1. 分散トレーシングで処理の流れを追う ZipkinのデータストアはMySQL, Cassandra, Elasticsearchがサポートされており、 弊社ではElasticsearchを採用しています。 Zipkinダッシュボードとは別にKibanaでトレースデータを可視化しており、 Spanの処理時間の平均やパーセンタイルを可視化することで 障害調査や長期的な性能劣化を把握することに役立てています。 決済機関通信に関するZipkinダッシュボード

  62. 4-2. 分散トレーシングに 頼り過ぎない 62

  63. 63 業務 システム 通信 システム Request X 4-2. 分散トレーシングに頼り過ぎない 決済機関

    業務ログ ・トランザクションID ・TraceID 通信ログ ・TraceID 例:HTTPステータス500 決済機関からエラーレスポンスを受領したら、 そのTraceIDを元にトランザクションIDを特定すればよい...と思っていました。 社内の業務システムで使う トランザクションIDもTraceIDから 逆引きをすれば特定できる!
  64. 64 業務 システム 通信 システム Request X 4-2. 分散トレーシングに頼り過ぎない 決済機関

    通信ログ ・TraceID 例:HTTPステータス500 しかし複数のエラー原因が出てくると雲行きが怪しくなり ... Request Y 例:Connect Timeout 前提:通信システムは 抽象化したエラーレスポンスを返却する。 業務ログ ・トランザクションID ・TraceID
  65. 65 業務 システム 通信 システム Request X 4-2. 分散トレーシングに頼り過ぎない 決済機関

    ネットワーク障害などで 同時にエラーが100件を超えて出たあたりで調査が破綻しました。 Request Y Request Z ・・・ 通信システムのログだけで以下のような調査を実施しなければならない。 ・Read TimeoutとHTTPステータス500のトランザクションIDを一覧にしたい ・エンドポイント単位のエラーを業務単位のエラーにマッピングしたい
  66. 66 業務 システム 通信 システム Request 4-2. 分散トレーシングに頼り過ぎない たとえ分散トレーシングを導入したとしても、 トランザクションIDは各システムのログに出力するようにしました。

    トランザクションID 通信システムに自由項目を設けた。 自由項目で受け取った業務 IDをログに出力する。 業務ログ ・トランザクションID ・TraceID 通信ログ ・トランザクションID ・TraceID
  67. 67 業務 システム 通信 システム Request 4-2. 分散トレーシングに頼り過ぎない たとえ分散トレーシングを導入したとしても、 トランザクションIDは各システムのログに出力するようにしました。

    トランザクションID 通信システムに自由項目を設けた。 自由項目で受け取った業務 IDをログに出力する。 業務ログ ・トランザクションID ・TraceID 業務ログ ・トランザクションID ・TraceID 「この方法をシステムの数珠つなぎが100連になってもやりますか」 と聞かれると、「確かにそれは難しい」という回答になります。 これはTagやBaggageという機能で達成することができます。 開発時に「分散トレーシングに依存しすぎないようにしよう」と 2つのシステムだったこともあり採用しました。 (後から振り返ってみればTagで良かったとも感じています) これから開発する場合はTagとBaggageも検討してみてください。
  68. 5. 後回しでいいものは 非同期にする 68

  69. 69 5. 後回しでいいものは非同期にする 弊社では通信に関わるログをセキュリティ要件のため 標準出力やファイルではなくRDBに保管しています。 決済 システム 決済機関 RDB 決済機関

    リクエスト 決済機関 レスポンス 業務 リクエスト 業務 レスポンス INSERT 加盟店様のセールなどで 大量トランザクションが発生した場合に パフォーマンスに影響してしまう。
  70. 70 5. 後回しでいいものは非同期にする ログ書き込みのような「必要だけれど今でなくてもいい」処理は MQ(Message Queue)に格納して非同期で実施させることができます。 決済 システム 決済機関 RDB

    決済機関 リクエスト 決済機関 レスポンス 業務 リクエスト 業務 レスポンス Queueing ログ投入 システム
  71. 71 5. 後回しでいいものは非同期にする Spring Cloud Stream & RabbitMQ Spring Cloud

    Streamでメッセージブローカーに簡単に接続できます。 また弊社ではメッセージブローカーに OSSのRabbitMQを採用しています。 spring-cloud-stream-binder-rabbitをアプリケーションの依存に追加することで、 RabbitMQに簡単に接続する実装を行えるようになります。 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-binder-rabbit</artifactId> </dependency>
  72. 72 5. 後回しでいいものは非同期にする application.ymlでRabbitMQの接続先とグループ名を定義します。 spring: rabbitmq: host: localhost port: 5672

    username: guest password: guest cloud: stream: bindings: log-in-0: group: log-queue グループ名の定義が RabbitMQのExchange名やQueue名に影響します。
  73. 73 5. 後回しでいいものは非同期にする 以下のようなBeanを定義をすることでConsumerを実装できます。 @Bean public Consumer<String> log() { return

    log -> { System.out.println("Received: " + log); }; }
  74. 74 5. 後回しでいいものは非同期にする StreamBridgeを使うことでProducerを実装できます。 Spring Webのハンドラメソッドなど好きなタイミングで起動させられます。 private final StreamBridge streamBridge;

    public SpringFest2021GymService(StreamBridge streamBridge) { this.streamBridge = streamBridge; } public void log() { streamBridge.send("log-in-0", "Hello Spring Fest 2021!"); } RabbitMQでのExchangeを指定する ※Spring Cloud Stream 2.x系であった宣言的な記述は3.x系で非推奨となりました。
  75. 75 5. 後回しでいいものは非同期にする 性能が安定しない場合にも非同期を活用できます。 決済機関 ECサイトA 通知システム 1000 ms ECサイトB

    ECサイトC 1500 ms 3000 ms 3500 ms 60000 ms 60500 ms 複数の加盟店様がいるため 決済機関に対して性能を保証できない エンドユーザーの処理完了が通知される。
  76. 通知受信 システム 76 5. 後回しでいいものは非同期にする MQに格納することで決済機関に素早くレスポンスを返却できます。 決済機関 ECサイトA 通知 システム

    1000 ms ECサイトB ECサイトC 50 ms 3000 ms 60000 ms 50 ms 50 ms 加盟店様によっては 流量や性能をコントロールできる 決済機関には常に一定の性能で レスポンスを返却できる
  77. 通知受信 システム 77 5. 後回しでいいものは非同期にする MQに格納することで決済機関に素早くレスポンスを返却できます。 決済機関 ECサイトA 通知 システム

    1000 ms ECサイトB ECサイトC 50 ms 3000 ms 60000 ms 50 ms 50 ms 加盟店様によっては 流量や性能をコントロールできる 決済機関には常に一定の性能で レスポンスを返却できる 決済機関には即時OKと返しています。 これはタイムアウトと同様に 自社で責任を持つ行為になりますので、 通知システムのリトライの仕組みは 徹底的に設計する必要があります。 「同一の通知が複数送信されることがあります」 などサービス仕様まで踏み込むことも考えられます。
  78. 6. 更新頻度が低いデータは キャッシュで持つ 78

  79. 79 6. 更新頻度が低いデータはキャッシュで持つ 決済システムのとあるシーケンスにて... 決済システム ECサイト RDB 決済機関 課金リクエスト 加盟店情報

    読み取り 鍵交換 課金
  80. 80 6. 更新頻度が低いデータはキャッシュで持つ もしかしたら必要のないリクエストがあるかもしれません。 決済システム ECサイトA RDB 決済機関 課金リクエスト 加盟店情報

    読み取り 鍵交換 課金 マスタ情報の更新頻度は とても低かったりしないか 鍵の有効期限は 実は1週間ほどあったりしないか
  81. 81 6. 更新頻度が低いデータはキャッシュで持つ Spring Cache spring-boot-starter-cacheをアプリケーションの依存に追加すると 特定のメソッドの戻り値をキャッシュする機能を使えるようになります。 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId>

    </dependency>
  82. 82 6. 更新頻度が低いデータはキャッシュで持つ @EnableCaching public class SpringFest2021GymApplication { // omit

    } @Cacheable("people") public Person findById(String id) { // omit } @EnableCachingを付与することで キャッシュ機能が有効になる キャッシュしたい値を返却するメソッドに @Cacheableを付与することで2回目以降は キャッシュされた結果が返却される 「引数ごとに」キャッシュされるため それを前提としたメソッドを設計する必要がある
  83. 83 6. 更新頻度が低いデータはキャッシュで持つ Caffeine Spring Cacheはメソッドの引数をキーにキャッシュされることや 対象の機能をメソッドに切り出さなければならず使いづらい場面もありました。 キャッシュライブラリのCaffeineを直接使用することがあります。 <dependency> <groupId>com.github.ben-manes.caffeine</groupId>

    <artifactId>caffeine</artifactId> </dependency>
  84. 84 6. 更新頻度が低いデータはキャッシュで持つ @Service public class PeopleService { private final

    Cache<String, String> cache = Caffeine.newBuilder() .maximumSize(3) .expireAfterWrite(Duration.ofMinutes(60)) .build(); public Person findById(String id) { return cache.get(id, i -> { // omit }); } } この場合インメモリで持つ実装のため 必ずサイズ上限とTTLを設けておく。
  85. 85 6. 更新頻度が低いデータはキャッシュで持つ 障害ポイントとなる外部システムへの接続を減らすことができました。 決済システム ECサイトA RDB 決済機関 課金リクエスト 加盟店情報

    読み取り 鍵交換 課金 能動的にキャッシュを 更新/破棄するアクセスポイントは用意する
  86. 7. サーキットブレーカーで 被害の拡大を防ぐ 86

  87. 7. サーキットブレーカーで被害の拡大を防ぐ 決済機関のような外部システムはコントロールすることができません。 ときには障害の影響で自社システムも滞留が発生する可能性があります。 決済機関 決済機関 システム 決済機関の障害の影響で 決済機関システムの Webスレッドが

    滞留して応答できなくなることも ...
  88. 7. サーキットブレーカーで被害の拡大を防ぐ サーキットブレーカーを使用することで、 特定の処理について一定時間ごとのエラー率やエラー数を検査して 問題があれば即時折り返す実装をすることができます。 決済機関 決済機関 システム 決済機関 決済機関

    システム 決済機関の障害を検知したら ... 通信を行わずに 即時折り返してくれます。
  89. 7. サーキットブレーカーで被害の拡大を防ぐ 決済機関A ECサイトX ※決済手段 A, Bを利用 決済機関A システム 決済機関

    GW 1 決済機関B 決済機関B システム 決済機関 GW 2 決済機関C 決済機関C システム フロント システム ECサイトY ※決済手段 Cを利用 すべての通信は同期通信を前提とします。 サーキットブレーカーがなかった場合
  90. 7. サーキットブレーカーで被害の拡大を防ぐ 決済機関A ECサイトX ※決済手段 A, Bを利用 決済機関A システム 決済機関

    GW 1 決済機関B 決済機関B システム 決済機関 GW 2 決済機関C 決済機関C システム フロント システム ECサイトY ※決済手段 Cを利用 決済機関Bに問題が発生すると ... サーキットブレーカーがなかった場合
  91. 7. サーキットブレーカーで被害の拡大を防ぐ 決済機関A ECサイトX ※決済手段 A, Bを利用 決済機関A システム 決済機関

    GW 1 決済機関B 決済機関B システム 決済機関 GW 2 決済機関C 決済機関C システム フロント システム ECサイトY ※決済手段 Cを利用 決済機関Bシステムの Webスレッドが埋め尽くされて 滞留します。 サーキットブレーカーがなかった場合
  92. 7. サーキットブレーカーで被害の拡大を防ぐ 決済機関A ECサイトX ※決済手段 A, Bを利用 決済機関A システム 決済機関

    GW 1 決済機関B 決済機関B システム 決済機関 GW 2 決済機関C 決済機関C システム フロント システム ECサイトY ※決済手段 Cを利用 決済機関Bの障害に引きづられて 決済機関GW1も滞留します。 決済機関Bとは関係がないはずの 決済機関Aも利用できなくなります。 サーキットブレーカーがなかった場合
  93. 7. サーキットブレーカーで被害の拡大を防ぐ 決済機関A ECサイトX ※決済手段 A, Bを利用 決済機関A システム 決済機関

    GW 1 決済機関B 決済機関B システム 決済機関 GW 2 決済機関C 決済機関C システム フロント システム ECサイトY ※決済手段 Cを利用 決済手段Bにまったく関係ない ECサイトYも決済できなくなります。 サーキットブレーカーがなかった場合
  94. 7. サーキットブレーカーで被害の拡大を防ぐ 決済機関A ECサイトX ※決済手段 A, Bを利用 決済機関A システム 決済機関

    GW 1 決済機関B 決済機関B システム 決済機関 GW 2 決済機関C 決済機関C システム フロント システム ECサイトY ※決済手段 Cを利用 レスポンスタイムが早いため 決済はできないが滞留することはない サーキットブレーカーを入れた場合
  95. 7. サーキットブレーカーで被害の拡大を防ぐ Resilience4j サーキットブレーカーだけでなくこのあと紹介するバルクヘッドや、 リトライ、レートリミット機能などを実装できるライブラリです。 <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId> <version>1.7.1</version> </dependency>

    resilience4j-spring-boot2をアプリケーションの依存に追加することで Resilience4jのサーキットブレーカー機能を利用できます。
  96. 7. サーキットブレーカーで被害の拡大を防ぐ resilience4j: circuitbreaker: instances: default: # CLOSE failureRateThreshold: 100

    # サーキットブレーカーが OPEN状態となるエラー率 slidingWindowType: TIME_BASED slidingWindowSize: 120 # エラー率を検査する時間幅(秒) minimumNumberOfCalls: 10 # サーキットブレーカーが OPEN状態となるために必要な最低処理数 # OPEN waitDurationInOpenState: 30_000 # OPEN状態からHALF_OPEN状態に移行するまでの待機時間 # HALF_OPEN permittedNumberOfCallsInHalfOpenState: 10 # HALF_OPEN状態での検査処理数 # Exceptions # サーキットブレーカーの検査対象外とする例外クラス ignoreExceptions: - jp.sbps.springfest2021.exception.BusinessException サーキットブレーカーの設定はapplication.ymlに記述します。
  97. 7. サーキットブレーカーで被害の拡大を防ぐ サーキットブレーカーは3つの状態を遷移します。 CLOSE OPEN HALF_ OPEN

  98. 7. サーキットブレーカーで被害の拡大を防ぐ サーキットブレーカーは3つの状態を遷移します。 CLOSE OPEN HALF_ OPEN CLOSE 通常状態です。 特に制限なく処理を実行します。

    一定のエラーを検知することで OPEN状態に移行します。
  99. 7. サーキットブレーカーで被害の拡大を防ぐ サーキットブレーカーは3つの状態を遷移します。 CLOSE OPEN HALF_ OPEN OPEN 何らかの異常が発生している状態です。 処理を実行せずに折り返します。

    一定の時間が経過すると HALF_OPEN状態に移行します。
  100. 7. サーキットブレーカーで被害の拡大を防ぐ サーキットブレーカーは3つの状態を遷移します。 CLOSE OPEN HALF_ OPEN HALF_OPEN OPEN状態から移行します。 限られた回数だけ処理を実行して

    問題なければCLOSE状態に、 問題があればOPEN状態に移行します。
  101. 7. サーキットブレーカーで被害の拡大を防ぐ resilience4j: circuitbreaker: instances: default: # CLOSE failureRateThreshold: 100

    # サーキットブレーカーが OPEN状態となるエラー率 slidingWindowType: TIME_BASED slidingWindowSize: 120 # エラー率を検査する時間幅(秒) minimumNumberOfCalls: 10 # サーキットブレーカーが OPEN状態となるために必要な最低処理数 # OPEN waitDurationInOpenState: 30_000 # OPEN状態からHALF_OPEN状態に移行するまでの待機時間 # HALF_OPEN permittedNumberOfCallsInHalfOpenState: 10 # HALF_OPEN状態での検査処理数 # Exceptions # サーキットブレーカーの検査対象外とする例外クラス ignoreExceptions: - jp.sbps.springfest2021.exception.BusinessException 再掲 決済システムでは1件でも 正常な取引ができるものがあれば通したいので エラー率が100%になるまでOPEN状態にしません。 サーキットブレーカーの設定はapplication.ymlに記述します。
  102. 7. サーキットブレーカーで被害の拡大を防ぐ @CircuitBreaker(name = "default") public void exchange() { //

    do something } アノテーションで実装する場合 private final CircuitBreaker circuitBreaker; public SpringFest2021Service(CircuitBreakerRegistry registry) { this.circuitBreaker = registry.circuitBreaker("default"); } public void exchange() { Supplier<String> decoratedSupplier = CircuitBreaker.decorateSupplier( circuitBreaker, service::exchange ); // Use decoratedSupplier as you like. } 手続き的に実装する場合
  103. 7. サーキットブレーカーで被害の拡大を防ぐ 決済機関A ECサイトX ※決済手段 A, Bを利用 決済機関A システム 決済機関

    GW 1 決済機関B 決済機関B システム 決済機関 GW 2 決済機関C 決済機関C システム フロント システム ECサイトY ※決済手段 Cを利用 レスポンスタイムが早いため 決済はできないが滞留することはない サーキットブレーカーを入れた場合 決済機関に通信するネットワークの問題で レスポンスタイム遅延が発生し 全件タイムアウトになったことがありました。 このときはサーキットブレーカーが OPEN状態となり即時返却状態となりました。 15分ほどHALF_OPENと行き来した後に 問題が収まってCLOSE状態となりました。 OPEN CLOSE 取扱量 レスポンスタイム OPENとHALF_OPENを行き来している
  104. 8. バルクヘッドで 対向システムを守る 104

  105. 105 8. バルクヘッドで対向システムを守る 決済機関 ECサイト 決済システム 1 TPS 1 TPS

    通常時は決済機関と1TPSで通信をしている状態を考えます。
  106. 106 8. バルクヘッドで対向システムを守る 決済機関 ECサイト 決済システム 100 TPS ところが加盟店のスパイクで突然100TPSで通信をしてしまうと 決済機関の障害を引き起こしてしまうことがあります。

    100 TPS
  107. 107 8. バルクヘッドで対向システムを守る 決済機関 決済システム 多くの場合は決済機関との契約時に同時接続数を取り決めます。 それを超えるリクエストを送信してしまうと性能が保証されません。 20 TPS 20TPS程度が望ましいです。

    平均レスポンスタイムは 1秒程度です。 かしこまりました。 それでは同時接続数を 20本とさせていただきます。
  108. 108 8. バルクヘッドで対向システムを守る Resilience4j Resilience4jでは同時接続数を制御するバルクヘッド機能も利用できます。 resilience4j-spring-boot2をアプリケーションの依存に追加することで Resilience4jのバルクヘッド機能を利用できます。 <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId>

    <version>1.7.1</version> </dependency> 再掲
  109. 109 8. バルクヘッドで対向システムを守る resilience4j: bulkhead: instances: default: maxConcurrentCalls: 20 #

    処理を同時に呼び出せる数 maxWaitDuration: 0 # バルクヘッドに到達したときに諦めるまでの時間 バルクヘッドの設定はapplication.ymlに記述します。 バルクヘッドに到達する状況では 待っても解消の見込みが薄いため即時エラー返却としている。
  110. 110 8. バルクヘッドで対向システムを守る @Bulkhead(name = "default") public void exchange() {

    // do something } アノテーションで実装する場合 手続き的に実装する場合 private final Bulkhead bulkhead; public SpringFest2021Service(BulkheadRegistry registry) { this.bulkhead = registry.bulkhead("default"); } public void exchange() { Supplier<String> decoratedSupplier = Bulkhead.decorateSupplier( bulkhead, service::exchange ); // Use decoratedSupplier as you like. }
  111. 111 まとめ No 手法 対応するライブラリ 想定される効果 1 タイムアウト Spring Web

    - 滞留による全断を防ぐ 2 リトライ Spring Retry - 1件でも多く決済を成立させる 3 冪等 - - リトライ可能にする 4 分散トレーシング Spring Cloud Sleuth / Zipkin - 処理を追跡する - 処理時間の傾向を把握する 5 非同期 Spring Cloud Stream - パフォーマンスを向上させる 6 キャッシュ Spring Cache - 障害ポイントを減らす 7 サーキットブレーカー Resilience4j - 障害の拡大を防ぐ 8 バルクヘッド Resilience4j - 対向先の障害を防ぐ
  112. 112 We are hiring! ご視聴いただきましてありがとうございました。 SBペイメントサービスではエンジニアを募集しています。 ご興味がある方は  @hainet50b までご連絡ください。