Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

お品書き 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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

55 4-1. 分散トレーシングで処理の流れを追う Spring Cloud Sleuth spring-cloud-starter-sleuthをアプリケーションの依存関係に追加すると TraceIDとSpanIDをSLF4JのMDCに追加してくれます。 org.springframework.cloud spring-cloud-starter-sleuth ログの例 3件のログに対して それぞれTraceIDが付与されている。

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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を設けておく。

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

7. サーキットブレーカーで被害の拡大を防ぐ Resilience4j サーキットブレーカーだけでなくこのあと紹介するバルクヘッドや、 リトライ、レートリミット機能などを実装できるライブラリです。 io.github.resilience4j resilience4j-spring-boot2 1.7.1 resilience4j-spring-boot2をアプリケーションの依存に追加することで Resilience4jのサーキットブレーカー機能を利用できます。

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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. } 手続き的に実装する場合

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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