2024.10.10
将来は卵1パックの価格が2倍に? 多くの日本人が知らない世界の新潮流、「動物福祉」とは
RailsのAPIを普通に動かしたい話 (全1記事)
リンクをコピー
記事をブックマーク
福本晃之氏(以下、福本):では、今日は「RailsのAPIを普通に動かしたい話」というテーマで進めていきます。よろしくお願いします。(スライドを指して)アジェンダはこちらです。今日はこんな感じで進めていきます。
最初に自己紹介をします。福本です。ちょうど1年半くらい前の2019年10月にメドピアに入って、それからずっとバックエンドの開発を中心に進めている感じです。
まず、僕はメドピアの中で、予防医療の観点から食事に関わるサービスの開発に携わっています。メドピアにはFitsPlusというグループ会社があり、そこで開発をしています。今日は詳細はあまり話しませんが、特定保健指導という領域があり、そこのプラットフォームの開発に携わっています。
詳細をテックフローに書きましたが、(スライドを指して)このようなアーキテクチャです。
ポイントとしては、一番真ん中にモノリシックのでかいRailsがあって、同じRailsアプリケーション上に、スキーマの違うアプリや、それ向けのAPIが複数存在しています。さらに、各APIスキーマのコンテキストが微妙に異なる点もポイントです。
続いてタイトルの「RailsのAPIを普通に動かしたい話」についてお話します。
「”普通”is何ですか?」と思われるでしょうが、僕は「ちゃんとスキーマ定義どおりにレスポンスを返す」または「エラー検知ができること」だと考えています。逆に言うと、エラーじゃないものは検知しないとか。ほかに、テストがちゃんとできることを普通だと捉えています。
しかし僕は、この普通が意外と難しいんじゃないかと勝手に思っていて。
今日は普通を目指す話をします。ベストプラクティスのようなかっこいい話ではなく、「最低限こういうことを守っていこう」とか、「こういうのやりがち」という話を共有できたらなと思っています。
まず最初に社外のスマホアプリからバックエンドのサーバーに通信して、サーバー経由で自社のRailsにAPI通信でリクエストがくるものについてお話します。(スライドを指して)実際にはこういう構成になっているサービスなのですが、「なんでこういう構成になってんだ」というツッコミはちょっと効くのでやめてほしいです(笑)。
1日の食事の写真をスマホのアプリからアップロードすると、その写真がアプリのサーバーに届きます。その後、うちのRailsにAPI通信で写真などのデータが連携される、という構成です。
リリース前に、あるエンジニアが「テストも書いているし、staging検証も動作検証もイケたぞ」「通知されたエラーも全部潰したし、これはイケる!」と調子に乗っていたやつがいたんですけど、「リリース後に問題が……」となりました。もう完全にフリですね。
同じエラーの通知が100回鳴りました。うちはRollbarを使っているんですけど、僕は(スライドを指して)この「100th」という回数の文字を初めて見て「すみません」みたいな感じで(笑)。さらに、ユニークなはずのレコードが2つ作られるすごく香ばしい状態になりました。
この時の僕の表情がスライドのような感じです。これ全部僕の顔です。
「なんでや」という感じですが、「その時はこんな感じのコードを書いていました」というものをお見せします。実際のコードよりかなり簡単に書いていますが。こんな感じで、パッチのAPIでパラメータのIDから既存のリソースを取ってきて、アソシエーションをfind_or_create_byして、そのパラメータの値で更新をし、終わった後にJobを投げる感じの処理です。
この処理の緑色で囲んだ部分の複合的な要因で、さっきのような感じになっていました。
原因と解決については(スライドを指して)この3つですが、1つずつ見ていきます。
トランザクションです。スライドの緑色で囲んだ部分はトランザクションを張っているんですけど、見た瞬間に「おい」となる人もたぶんいると思います。僕も後から見て「おい」となったんですが、もう完全に凡ミスです。
ということで、find_or_create_by自体がトランザクションを張るので、find_or_create_byはトランザクションの中であまりカジュアルに使わない方がいい。
トランザクション、難しくないですか? 僕は「トランザクションナニモワカラナイ」という人なんですけど(笑)。find_or_create_byをしないことが一番簡単で、あと、トランザクションをrequire: trueさせると、いちおうネストして中で抜けられる。さらに、トランザクションを抜けて、ちゃんとリトライさせましょう、ということです。
また、create_withして、INSERTする時にlockを外してもいいと思います。でも、さっきのトランザクションのネストやテーブルのlockのところは、コード上で「こういう状態を想定しています」と(コードの読者に)伝えるのが無理な気がしたので。いろいろ試してみましたが、やめました。いろいろ手は打ちましたが、単純にfind_or_create_byしない方向で考えています。
2つ目は、並列度の高いAPIリクエストです。ここは、トランザクションをミスっただけなら、エラーになって処理されないだけなのではないかと思います。100回も怒られなくていいんじゃないかと思いました、この通信されてきたログをいろいろあさってみた結果、同じエンドポイントのAPIに対して、中身(Body)が違う複数のリクエストが、ほぼ同時に来ていました。
あとで把握したところ、同じ1日の食事、例えば朝昼晩の3つ写真が送られてきたら、それを溜めて一気に送っていたらしいんです。
ただ、こういうことに気づいて、そもそも同時にAPIが呼ばれるという考えが僕になく、どういうふうにAPIが叩かれるのかという事情も把握していなかった。さらに、「これって同時に叩かれたらどうなるのか」というトランザクションの扱いも、あまりわかっていなかったことが反省点としてあります。
解決の方針としては、「検索と更新は分ける」という感じです。検索はトランザクション外で実行。たぶん、APIの処理全体を明示的にリトライさせたほうがいい。リトライする時に、ほかのトランザクションでコミットされた結果を拾えるようにしたほうがいいと思います。
3つ目は、ActiveJobによる非同期処理です。これは、若干APIにかかわらずというところですが、最後にこのトランザクションが抜けたあとに謎のJobの処理が入っていました。
これはS3への画像で、先ほどの食事写真の画像アップロードをJobで渡しているんですね。(スライドを指して)具体的にはこんな感じですが、詳細は割愛します。
直接うちのRailsに写真や画像データが届いていない以上、受け取り元のサーバーから、S3のbucketにアクセスできるkeyをもらい、そのkeyをもとにS3のAPIを叩いて、いったんうちのサーバーに画像をダウンロードします。そして、サーバーにダウンロードした画像をうちのS3 bucketに上げなおす処理が必要なので、それをやっています。
詳細は、別途個人的にZennに書いたので、興味のある人は見てください。
(スライドを指して)もう一回置きましたが、Jobの部分はDeserializationErrorが起きまくっていました。
原因としては、Jobの引数に配列を渡していたのが、タイミングによってはGlobaliDのシリアライズに配列を渡すと失敗するらしいんです。たぶん、うまくシリアライズできなかったためにエラーが起きていました。
そして、エラーが起きたら、そのあと死にJobが残りました。Jobが破棄されないことと、Sidekiqが律儀にリトライしようとしたことが原因でした。
僕の気持ちがスライドの右側です。リトライで盛られていたので、たぶん実際は100回も起きていたわけではありませんが、こういうのが起きちゃいました、っていう感じでした。
Jobに渡す時のポイントとしては、引数に単一のインスタンスを渡すことと、Jobごとリトライするので、Sidekiqのリトライ機能を使ったほうがいいです。ただし、Sidekiq6系以上でないと使えません。僕が直した時はまだSidekiq6系ではなかったので、使えませんでした。
あと、DeserializationErrorが起きたら、そのJobはもうどうしようもないので、discard_onで指定して、明示的にJobを破棄したほうがいいです。さらに今回はやらなかったんですけど、トランザクションの中でJobを呼ぶと実行タイミングがズレてしまうのでうまくいかない。そこも気をつけたほうがいいと思います。
原因の3つを追ったら、ことなきを得ました。ピッタリ止みました。めでたしめでたし、という感じです。おまけですが、テスト。並列テストのところは、正直ちょっとうまいこと書けていないので、なんかうまいことやっている事例などがあったら教えてください、という感じです。
それでは最後にまとめます。API開発では、トランザクションや並列度など、奥行きを意識しましょう。あと、APIを叩く時の、叩く側の事情にも関心を持つ、うまくリトライさせる。さらに、検索と更新/作成は分ける。Jobの実行タイミングがあるので、それと通常の処理との違いを理解する。以上、「普通」の話をしました。ありがとうございました。
司会者:ありがとうございました。質問がきています。「トランザクション処理に失敗して、結局データは壊れてしまったんですか?」
福本:おっしゃるとおりです。2つずつ作られていたので、あとから直しました。Rakeタスクで重複データを滅尽しました。
司会者:無事復旧しましたか?
福本:無事復旧しました。Rakeでやった理由としては、ちゃんとRSpecを書きたかったからです。例えば2つ消されるとか、逆に消されないとかはやりたくなくって、ちゃんとテストで消えるかどうかを確かめたかった。なので、いわゆるConsoleで削除はしませんでした。
司会者:ほかに「API連携は難しいと思いますが、インターフェイスのやり取りなど工夫されていたことはありますか?」という質問です。
福本:かっこつけて言うと、いわゆるスキーマ駆動開発じゃないんですが、ローカルだとSwaggerでOpenAPIをやっていまして。
司会者:かっこつけて言ってくださいよ(笑)。
福本:(笑)。アーキテクチャを見せたほうが早いですね。Swaggerを使っていて、いわゆるOpenAPIのドキュメントを使っています。あとは、テストをcommitteeで書いていて、レスポンスをどう返すかの担保もしています。モックもサーバーも、apisproutという、たぶんちょっとマイナーなものを起動させて、「こうリクエストにリトライしたらいいんだね」とか「こういうふう返ってくるんだね」ということがわかるようにはしています。ただ、そのapisproutが、悪さをするというか(笑)。
例えば、アクセスするコントローラーで、よくAPIだとapi/v1/なんかでディレクトリを切ると思いますが、そのapi/v1をなぜかパスする。リクエストすると(apisprout側で)ignoreされて、2回つけないといけないんです(笑)。api/v1/api/v1/users/1みたいなをやらないといけない。ちょっとどうしよっかな、みたいな感じでしたね(笑)。そういうことがありつつも、工夫はしています。
司会者:ちょうど時間になったので、発表ありがとうございました。
関連タグ:
2024.11.13
週3日働いて年収2,000万稼ぐ元印刷屋のおじさん 好きなことだけして楽に稼ぐ3つのパターン
2024.11.11
自分の「本質的な才能」が見つかる一番簡単な質問 他者から「すごい」と思われても意外と気づかないのが才能
2024.11.13
“退職者が出た時の会社の対応”を従業員は見ている 離職防止策の前に見つめ直したい、部下との向き合い方
2024.11.12
自分の人生にプラスに働く「イライラ」は才能 自分の強みや才能につながる“良いイライラ”を見分けるポイント
2023.03.21
民間宇宙開発で高まる「飛行機とロケットの衝突」の危機...どうやって回避する?
2024.11.11
気づいたら借金、倒産して身ぐるみを剥がされる経営者 起業に「立派な動機」を求められる恐ろしさ
2024.11.11
「退職代行」を使われた管理職の本音と葛藤 メディアで話題、利用者が右肩上がり…企業が置かれている現状とは
2024.11.18
20名の会社でGoogleの採用を真似するのはもったいない 人手不足の時代における「脱能力主義」のヒント
2024.11.12
先週まで元気だったのに、突然辞める「びっくり退職」 退職代行サービスの影響も?上司と部下の“すれ違い”が起きる原因
2024.11.14
よってたかってハイリスクのビジネスモデルに仕立て上げるステークホルダー 「社会的理由」が求められる時代の起業戦略