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

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

hainet50b
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

hainet50b

December 06, 2021
Tweet

More Decks by hainet50b

Other Decks in Programming

Transcript

  1. 決済システムで学ぶ
    レジリエントなサービスのいろは
    Spring Fest 2021
    December 3 2021
    Haine Takano (@hainet50b), SB Payment Service, [email protected]
    1

    View Slide

  2. SBペイメントサービス株式会社
    アプリケーション開発者
    髙野 はいね( @hainet50b)
    自己紹介
    決済システム(Java, Spring Boot)の開発に従事
    2016年にSBペイメントサービス株式会社に入社
    主な業務
    ・決済システム開発
    ・加盟店様の決済システム導入時の技術サポート
    2

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. 加盟店 決済機関
    通販サイト
    ゲーム
    教育
    不動産
    その他
    電子書籍/動画
    チケット
    クレジット
    携帯キャリア決済
    コンビニ支払い
    プリペイドカード
    口座振替
    ポイント支払い
    アカウント連携決済
    決済代行サービス
    Tanzu
    Application Service
    ・・・
    8
    プラットフォームや開発体制ではなく
    運用や実装に着目してお話いたします。

    View Slide

  9. 決済システムに
    求められる要件とは?
    9

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  16. 1-1. タイムアウトを設定する
    16

    View Slide

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

    View Slide

  18. 決済機関
    18
    1-1. タイムアウトを設定する
    ECサイト 決済システム
    60500 ms 60000 ms
    決済機関が障害で遅延することはよくあります。

    View Slide

  19. 決済機関
    19
    1-1. タイムアウトを設定する
    ECサイト 決済システム
    放っておくとリクエストが徐々に滞留して...

    View Slide

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

    View Slide

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

    View Slide

  22. 決済機関
    22
    1-1. タイムアウトを設定する
    ECサイト 決済システム
    何とか持ち堪えます。
    60000 ms
    15000 ms
    15500 ms
    滞留はしたけれどちょっとだけ!

    View Slide

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

    View Slide

  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();
    }

    View Slide

  25. 1-2. タイムアウトを実施した
    その後に全力を注ぐ
    25

    View Slide

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

    View Slide

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

    View Slide

  28. 1-2. タイムアウトを実施したその後に全力を注ぐ
    このような状況には大きく2つの対策方法があります。
    1. 取消を使う
    決済機関にはエンドユーザーに明細が残らない
    障害取消という機能があり、こちらをよく使います。
    2. 参照を使う
    不整合疑いのトランザクションを一旦キューに保持します。
    時間をおいてから定期的に状態を参照して、
    取引が存在しないか失敗していた場合はクローズ、
    取引が成功してしまっていた場合は取消を行います。
    こちらに全力を注ぐイメージを
    次のスライドで説明します。

    View Slide

  29. 決済機関
    29
    1-2. タイムアウトを実施したその後に全力を注ぐ
    決済システム
    課金リクエストでタイムアウトを検知したら...
    ①タイムアウト

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  34. 決済機関
    34
    1-2. タイムアウトを実施したその後に全力を注ぐ
    決済システム
    キューに格納し直して、n回やり直します!!!
    ①タイムアウト
    ②取消
    ③キューに格納 ④取消(2回目)
    決済
    サブシステム
    ⑤キューに格納(以下繰り返し)
    ※さらに一定回数を超えた取引はアラートで検知して手運用に回ります。
     タイムアウトは自社の判断なので責任を持ちます。

    View Slide

  35. 2. リトライで1件でも多く救う
    35

    View Slide

  36. 36
    2. リトライで1件でも多く救う
    外部決済機関との通信では
    リトライできるエラーとそうでないエラーがあります。
    エラーの種類 リトライの可否
    Connection Reset / Refused ○
    Connect Timeout ○
    Unknown Host ○
    Read Timeout ×
    HTTPステータスコード400系 △(多くの場合で必要ありません)
    HTTPステータスコード500系 ×
    通信に関するエラーについては救うことができます。
    その他は2重課金の恐れがあるためリトライできません。
    いずれもTCP接続に関するエラーで
    リクエストが到達していないことが
    保証されているためリトライできる
    クラウド上にアプリケーションを
    配置するようになってから
    発生することが増えた

    View Slide

  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”となっていました。
    文字列の検査は危険な実装ですので注意が必要です。

    View Slide

  38. 38
    2. リトライで1件でも多く救う
    Spring Retry
    spring-retryをアプリケーションの依存に追加することで
    簡単にリトライを実装できます。

    org.springframework.retry
    spring-retry

    View Slide

  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で設定を指定します。

    View Slide

  40. 3. 冪等を導入して
    リトライしやすいシステムにする
    40

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  44. 44
    App
    3. 冪等を導入してリトライしやすいシステムにする
    ①Appへのリクエストに
     冪等キーを要求する。
    ①課金インターフェースに冪等キーを要求します。
    冪等キーは以下の性質を持つ
    ・利用者が任意に決める
    ・冪等にしたい単位ごとに
     毎回必ず変更をしなければならない
     例:課金ならば一つの課金ごとに
       一つの冪等キーを発行する

    View Slide

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

    View Slide

  46. 46
    App
    3. 冪等を導入してリトライしやすいシステムにする
    ③冪等キーとAppからのレスポンス内容の組み合わせを保管します。
    ③以下の組み合わせを保管する。
    - 冪等キー
    - HTTPステータスコード
    - HTTPレスポンスボディ
    idempotency_key: 9c4d5af5-4114-4fd6-ac94-160fee09de41
    200 OK
    {
    "result": "ok"
    }
    保管する組み合わせの例
    ①~③の情報を用いると冪等な振る舞いを持つ
    APIを作ることができます。

    View Slide

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

    View Slide

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

    View Slide

  49. App A
    49
    App B
    3. 冪等を導入してリトライしやすいシステムにする
    適用パターン3:「短時間で」2回リクエストをしてしまった
    ①1回目のリクエスト
    ②2回目のリクエスト
    ③1回目のリクエストが
    処理中のためエラーを返却する
    ④1回目のリクエストに
    対応する処理結果を返却する
    App Bは処理中は同一のリクエストを受け付けないようにできます👍
    処理は実行しない
    ネットワークの問題による
    意図しない再送など
    NGとするか
    冪等に忠実に待たせるか
    2択を考えたが
    滞留を避けるため
    NGにすることにした

    View Slide

  50. 4-1. 分散トレーシングで
    処理の流れを追う
    50

    View Slide

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

    View Slide

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

    View Slide

  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につながっているのか分からない

    View Slide

  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

    View Slide

  55. 55
    4-1. 分散トレーシングで処理の流れを追う
    Spring Cloud Sleuth
    spring-cloud-starter-sleuthをアプリケーションの依存関係に追加すると
    TraceIDとSpanIDをSLF4JのMDCに追加してくれます。

    org.springframework.cloud
    spring-cloud-starter-sleuth

    ログの例
    3件のログに対して
    それぞれTraceIDが付与されている。

    View Slide

  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...

    View Slide

  57. 57
    4-1. 分散トレーシングで処理の流れを追う
    Zipkin
    弊社では分散トレーシングにZipkinを採用しています。
    spring-cloud-sleuth-zipkinをアプリケーションの依存関係に追加すると
    トレースデータをZipkinサーバーに送信してくれるようになります。

    org.springframework.cloud
    spring-cloud-sleuth-zipkin

    データストア
    App TraceID / SpanID
    Spanの開始終了時刻など
    HTTP

    View Slide

  58. 58
    4-1. 分散トレーシングで処理の流れを追う
    Trace
    Span
    アプリケーション名 Span Nameと処理時間
    ZipkinダッシュボードでTraceをビジュアルで把握することができます。
    Traceあたり100以上のSpanになることもあり、
    増やせばよいというものでもないと感じています。

    View Slide

  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の実行や、
    ファイルの読み書きに付与することが多いです。

    View Slide

  60. 4-1. 分散トレーシングで処理の流れを追う
    App
    トレースデータのサンプリングレートを設定することができます。
    spring:
    sleuth:
    sampler:
    probability: 1.0 # 割合で設定する。
    # rate: 10 # 秒間件数で設定する。
    0 〜 100 %の割合で送信
    or
    秒間n件だけ送信
    処理の傾向を掴めば十分である場合には
    低めに設定します。
    弊社では例外的な遅延も検知したいため
    probability 1.0 (100%)で運用しています。
    開発環境ではZipkinに負荷をかけないために
    rate 10 (秒間10件まで)で運用しています。

    View Slide

  61. 4-1. 分散トレーシングで処理の流れを追う
    ZipkinのデータストアはMySQL, Cassandra, Elasticsearchがサポートされており、
    弊社ではElasticsearchを採用しています。
    Zipkinダッシュボードとは別にKibanaでトレースデータを可視化しており、
    Spanの処理時間の平均やパーセンタイルを可視化することで
    障害調査や長期的な性能劣化を把握することに役立てています。
    決済機関通信に関するZipkinダッシュボード

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  68. 5. 後回しでいいものは
    非同期にする
    68

    View Slide

  69. 69
    5. 後回しでいいものは非同期にする
    弊社では通信に関わるログをセキュリティ要件のため
    標準出力やファイルではなくRDBに保管しています。
    決済
    システム 決済機関
    RDB
    決済機関
    リクエスト
    決済機関
    レスポンス
    業務
    リクエスト
    業務
    レスポンス
    INSERT 加盟店様のセールなどで
    大量トランザクションが発生した場合に
    パフォーマンスに影響してしまう。

    View Slide

  70. 70
    5. 後回しでいいものは非同期にする
    ログ書き込みのような「必要だけれど今でなくてもいい」処理は
    MQ(Message Queue)に格納して非同期で実施させることができます。
    決済
    システム 決済機関
    RDB
    決済機関
    リクエスト
    決済機関
    レスポンス
    業務
    リクエスト
    業務
    レスポンス
    Queueing
    ログ投入
    システム

    View Slide

  71. 71
    5. 後回しでいいものは非同期にする
    Spring Cloud Stream & RabbitMQ
    Spring Cloud Streamでメッセージブローカーに簡単に接続できます。
    また弊社ではメッセージブローカーに
    OSSのRabbitMQを採用しています。
    spring-cloud-stream-binder-rabbitをアプリケーションの依存に追加することで、
    RabbitMQに簡単に接続する実装を行えるようになります。

    org.springframework.cloud
    spring-cloud-stream-binder-rabbit

    View Slide

  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名に影響します。

    View Slide

  73. 73
    5. 後回しでいいものは非同期にする
    以下のようなBeanを定義をすることでConsumerを実装できます。
    @Bean
    public Consumer log() {
    return log -> {
    System.out.println("Received: " + log);
    };
    }

    View Slide

  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系で非推奨となりました。

    View Slide

  75. 75
    5. 後回しでいいものは非同期にする
    性能が安定しない場合にも非同期を活用できます。
    決済機関
    ECサイトA
    通知システム
    1000 ms
    ECサイトB
    ECサイトC
    1500 ms
    3000 ms 3500 ms
    60000 ms 60500 ms
    複数の加盟店様がいるため
    決済機関に対して性能を保証できない
    エンドユーザーの処理完了が通知される。

    View Slide

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

    View Slide

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

    View Slide

  78. 6. 更新頻度が低いデータは
    キャッシュで持つ
    78

    View Slide

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

    View Slide

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

    View Slide

  81. 81
    6. 更新頻度が低いデータはキャッシュで持つ
    Spring Cache
    spring-boot-starter-cacheをアプリケーションの依存に追加すると
    特定のメソッドの戻り値をキャッシュする機能を使えるようになります。

    org.springframework.boot
    spring-boot-starter-cache

    View Slide

  82. 82
    6. 更新頻度が低いデータはキャッシュで持つ
    @EnableCaching
    public class SpringFest2021GymApplication {
    // omit
    }
    @Cacheable("people")
    public Person findById(String id) {
    // omit
    }
    @EnableCachingを付与することで
    キャッシュ機能が有効になる
    キャッシュしたい値を返却するメソッドに
    @Cacheableを付与することで2回目以降は
    キャッシュされた結果が返却される
    「引数ごとに」キャッシュされるため
    それを前提としたメソッドを設計する必要がある

    View Slide

  83. 83
    6. 更新頻度が低いデータはキャッシュで持つ
    Caffeine
    Spring Cacheはメソッドの引数をキーにキャッシュされることや
    対象の機能をメソッドに切り出さなければならず使いづらい場面もありました。
    キャッシュライブラリのCaffeineを直接使用することがあります。

    com.github.ben-manes.caffeine
    caffeine

    View Slide

  84. 84
    6. 更新頻度が低いデータはキャッシュで持つ
    @Service
    public class PeopleService {
    private final Cache cache = Caffeine.newBuilder()
    .maximumSize(3)
    .expireAfterWrite(Duration.ofMinutes(60))
    .build();
    public Person findById(String id) {
    return cache.get(id, i -> {
    // omit
    });
    }
    }
    この場合インメモリで持つ実装のため
    必ずサイズ上限とTTLを設けておく。

    View Slide

  85. 85
    6. 更新頻度が低いデータはキャッシュで持つ
    障害ポイントとなる外部システムへの接続を減らすことができました。
    決済システム
    ECサイトA RDB 決済機関
    課金リクエスト
    加盟店情報
    読み取り
    鍵交換
    課金
    能動的にキャッシュを
    更新/破棄するアクセスポイントは用意する

    View Slide

  86. 7. サーキットブレーカーで
    被害の拡大を防ぐ
    86

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  94. 7. サーキットブレーカーで被害の拡大を防ぐ
    決済機関A
    ECサイトX
    ※決済手段
    A, Bを利用
    決済機関A
    システム
    決済機関
    GW 1
    決済機関B
    決済機関B
    システム
    決済機関
    GW 2
    決済機関C
    決済機関C
    システム
    フロント
    システム
    ECサイトY
    ※決済手段
    Cを利用
    レスポンスタイムが早いため
    決済はできないが滞留することはない
    サーキットブレーカーを入れた場合

    View Slide

  95. 7. サーキットブレーカーで被害の拡大を防ぐ
    Resilience4j
    サーキットブレーカーだけでなくこのあと紹介するバルクヘッドや、
    リトライ、レートリミット機能などを実装できるライブラリです。

    io.github.resilience4j
    resilience4j-spring-boot2
    1.7.1

    resilience4j-spring-boot2をアプリケーションの依存に追加することで
    Resilience4jのサーキットブレーカー機能を利用できます。

    View Slide

  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に記述します。

    View Slide

  97. 7. サーキットブレーカーで被害の拡大を防ぐ
    サーキットブレーカーは3つの状態を遷移します。
    CLOSE
    OPEN
    HALF_
    OPEN

    View Slide

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

    View Slide

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

    View Slide

  100. 7. サーキットブレーカーで被害の拡大を防ぐ
    サーキットブレーカーは3つの状態を遷移します。
    CLOSE
    OPEN
    HALF_
    OPEN
    HALF_OPEN
    OPEN状態から移行します。
    限られた回数だけ処理を実行して
    問題なければCLOSE状態に、
    問題があればOPEN状態に移行します。

    View Slide

  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に記述します。

    View Slide

  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 decoratedSupplier = CircuitBreaker.decorateSupplier(
    circuitBreaker,
    service::exchange
    );
    // Use decoratedSupplier as you like.
    }
    手続き的に実装する場合

    View Slide

  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を行き来している

    View Slide

  104. 8. バルクヘッドで
    対向システムを守る
    104

    View Slide

  105. 105
    8. バルクヘッドで対向システムを守る
    決済機関
    ECサイト 決済システム
    1 TPS 1 TPS
    通常時は決済機関と1TPSで通信をしている状態を考えます。

    View Slide

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

    View Slide

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

    View Slide

  108. 108
    8. バルクヘッドで対向システムを守る
    Resilience4j
    Resilience4jでは同時接続数を制御するバルクヘッド機能も利用できます。
    resilience4j-spring-boot2をアプリケーションの依存に追加することで
    Resilience4jのバルクヘッド機能を利用できます。

    io.github.resilience4j
    resilience4j-spring-boot2
    1.7.1

    再掲

    View Slide

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

    View Slide

  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 decoratedSupplier = Bulkhead.decorateSupplier(
    bulkhead,
    service::exchange
    );
    // Use decoratedSupplier as you like.
    }

    View Slide

  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 - 対向先の障害を防ぐ

    View Slide

  112. 112
    We are hiring!
    ご視聴いただきましてありがとうございました。
    SBペイメントサービスではエンジニアを募集しています。
    ご興味がある方は  @hainet50b までご連絡ください。

    View Slide