Slide 1

Slide 1 text

RailsのAPIを普通に動かしたい話 2021 / 05 / 28 ~ 事業成長を加速させたエンジニアリングのウラ側 ~ @terry_i_ MedPeer, inc. - Engineer

Slide 2

Slide 2 text

Agenda 自己紹介 導入 背景 起きたこと 原因と解決 まとめ

Slide 3

Slide 3 text

誰やお前

Slide 4

Slide 4 text

福本 晃之 Teruhisa Fukumoto MedPeer Web Developer (2019/10~) f-teruhisa @terry_i_

Slide 5

Slide 5 text

Twitter #medpeer

Slide 6

Slide 6 text

導入

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

特定保健指導 プラットフォーム開発

Slide 9

Slide 9 text

関連技術 / アーキテクチャ 『特定保健指導"フィッツプラス"事業を支えるモノリシック Rails + VIPER Swift アーキテクチャ』: https://tech.medpeer.co.jp/entry/2020/06/22/121153

Slide 10

Slide 10 text

フィッツプラスのRails API ● モノリシックRailsを中心とした構成 ● 同じRails上に異なるスキーマのAPIが複数存在する ● 各APIスキーマのコンテキストが微妙に異なる ○ スマホ/Webアプリ, 社内/外向け, toB/toC etc...

Slide 11

Slide 11 text

そこでタイトルの話題ですよ

Slide 12

Slide 12 text

RailsのAPIを普通に動かしたい話

Slide 13

Slide 13 text

“普通” is 何?? ● スキーマ定義通りにレスポンスを返すこと ● エラー検知できること、通常動作は検知しないこと ● テストが可能なこと、テストしやすいこと

Slide 14

Slide 14 text

普通が意外と難しい ※APIを開発している現場のイメージです

Slide 15

Slide 15 text

● API開発でまんまとハマった罠 ● 最低限気をつけといたほうがよさそうな話 ● やった工夫とか小さいTipsとか 今日する話「普通を目指すために」 ≠API開発のベストプラクティスの話

Slide 16

Slide 16 text

起きたこと

Slide 17

Slide 17 text

今回取り上げるサービスのAPI スマホアプリ ⇔ サーバー ⇔ 自社のRails(★)

Slide 18

Slide 18 text

1日の食事写真をスマホからアップロード スマホアプリ ⇔ サーバー ⇔ 自社のRails(★)

Slide 19

Slide 19 text

リリース前 テストもバッチリ書いてるし staging環境での動作検証も問題ない。 通知されたエラーも全部潰したし、 勝ちゲーや!!俺はエライ!!

Slide 20

Slide 20 text

リリース後に問題が...

Slide 21

Slide 21 text

同じエラーの通知が100回鳴る

Slide 22

Slide 22 text

ユニークなはずのレコードが2つ作られる

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

なんでや

Slide 25

Slide 25 text

サンプルコード ※プロダクトコードから改変しています

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

● トランザクション ● 並列度の高いAPIリクエスト ● ActiveJobによる非同期処理 この辺の複合的な要因

Slide 29

Slide 29 text

ちょっとずつ見ていきます

Slide 30

Slide 30 text

原因と解決

Slide 31

Slide 31 text

1. トランザクション 2. 並列度の高いAPIリクエスト 3. ActiveJobによる非同期処理 原因

Slide 32

Slide 32 text

1. トランザクション 2. 並列度の高いAPIリクエスト 3. ActiveJobによる非同期処理 原因

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

あっ(凡ミス)

Slide 35

Slide 35 text

find_or_create_by 自体がトランザクションを張る トランザクション内でカジュアルに使う󰢃 https://railsguides.jp/active_record_querying.html#find-or-create-by

Slide 36

Slide 36 text

解決するなら...?? ● find_or_create_byしない(簡単) ● トランザクションをネスト(require: true)させる ○ トランザクションの状態を読み解きづらくあんまやりたくない ● エラー時にトランザクションを抜けてリトライさせる ● (create_withとかでINSERT時にlock外したりしてもOKぽいけど、コード上で ロック状態を読み解かせるのはツライ気がしている) 参考になるやつ『Rails APIドキュメント: Active Recordのトランザクション(翻訳)』 : https://techracho.bpsinc.jp/hachi8833/2020_11_30/101160

Slide 37

Slide 37 text

1. トランザクション 2. 並列度の高いAPIリクエスト 3. ActiveJobによる非同期処理 原因

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

トランザクションのミスだけなら エラーになって処理されないだけでは...??

Slide 40

Slide 40 text

ログを漁ってみた結果... ※リクエスト元の管理画面のAPI連携ログの画面

Slide 41

Slide 41 text

ログを漁ってみた結果 ※リクエスト元の管理画面のAPI連携ログの画面 同じエンドポイントのAPIに対して 中身の違う複数のリクエストが同時に来てた ※同じ日の食事が複数投稿/更新されたら、溜めてJobで一気に送ってたみたい

Slide 42

Slide 42 text

APIリクエストの”並列度”を考えてなかった ● そもそも同時にAPIを呼ばれる考えがなかった ● どのようにAPIが叩かれるかを把握してなかった ● 並列度が高い状態でのトランザクションの扱い...?? ○ 何がいつロックされるのかよくわからん

Slide 43

Slide 43 text

● 検索(find)と更新(create / update)の分ける ○ 検索はトランザクション外で実行 ● APIの処理全体を明示的にリトライさせる ○ リトライ時に他のトランザクションのCOMMITを拾い、 DBから検索できるように 解決した際の方針

Slide 44

Slide 44 text

原因 1. トランザクション 2. 並列度の高いAPIリクエスト 3. ActiveJobによる非同期処理

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

S3への画像アップロードを Jobに渡している

Slide 47

Slide 47 text

Jobはこんな感じ

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

詳細(今回は割愛) https://zenn.dev/t_fukumoto/articles/fe3598ea0a128d93c8cd

Slide 50

Slide 50 text

起きたこと(再掲) ActiveJob::DeserializationErrorが起きまくった

Slide 51

Slide 51 text

● Jobの引数に配列を渡していた ○ タイミングによってGlobalIDのシリアライズに失敗するっぽい ● (エラーで)死んだJobが残る ○ 失敗してもJob自体が破棄されない ○ Sidekiqが律儀にリトライしようとする 原因 参考になるやつ『Rails: Active Jobスタイルガイド(翻訳)』 : https://techracho.bpsinc.jp/hachi8833/2020_09_30/96694

Slide 52

Slide 52 text

はい リトライで盛られていたので、 実際には100回起きたわけじゃなさそう

Slide 53

Slide 53 text

Jobに渡すときのポイント ● 引数に単一のインスタンスを渡す ● JobではSidekiqのリトライ機能を使う(sidekiq6系~) ● discard_onでエラーを指定し明示的にJobを破棄 ● トランザクション内でJobは呼ばない ○ 今回はやらなかったけど、やりがちなので気をつける

Slide 54

Slide 54 text

1. トランザクション 2. 並列度の高いAPIリクエスト 3. ActiveJobによる非同期処理 原因

Slide 55

Slide 55 text

🎉めでたし🎉 ※その後Jobで再度リクエストし直してもらい事なきを得る (重複データはRakeで滅尽🔪)

Slide 56

Slide 56 text

おまけ(FIXME)

Slide 57

Slide 57 text

並列度の高いRspecのテスト....?? 引用『RSpec での悲観ロックのテスト』 : https://tech.actindi.net/2015/11/09/3656032930

Slide 58

Slide 58 text

まとめ

Slide 59

Slide 59 text

● API開発は奥行きを意識する(トランザクション, 並列度) ● APIを叩く側のコンテキストにも関心を持つ ● 検索と更新 / 作成を分け、うまくリトライさせる ● Jobは実行タイミングなど通常の処理との違いを理解する まとめ

Slide 60

Slide 60 text

Thank you!! @terry_i_ MedPeer, inc. - Engineer