はじめに
こんにちは。エンジニアアルバイトの高木です。大学ではフランス文学を学んでいます。よろしくお願いします。
今回は PIXTA サービス内の換金処理で頻発していたタイムアウトエラーをどのように解決したかを書きたいと思います。
換金処理に発生していた問題
PIXTA では素材が購入された際に、「クレジット」と呼ばれるサービス内ポイントがクリエイターに還元されます。一定額のクレジットを貯めることで、クリエイターは PIXTA のサイト上から現地の通貨に換金できるようになります。
一方で、クリエイターによってはクレジットの換金処理に要する時間が非常に長く、時にはタイムアウトエラーが発生して現金に換金できない…という問い合わせがたびたびありました。
実際のコードを調査していくと、以下のことが原因そうだというのが判明しました。
# controller.rb # コードはブログに記事を載せるためのイメージです # 換金処理実行(時間のかかる処理) @exchange.generate!(params) # 換金処理が終わったらリダイレクト redirect_to exchange_complete_path
クリエイターからの申請に応じてリアルタイムで処理を返す形で実装されていますが、その処理(@exchange.generate!)が重いことが現在発生している問題の原因でした。 この問題の解決策は2つ考えられます。
- 重いクエリを根本から直す
- 重い処理を切り離してキューとバッチによる非同期・遅延実行の形に直す
重いクエリを走らせている箇所がお金に関わるところであったため、クエリの修正にはかなり注意が必要であったことに加え、時間的猶予があまりなかったということから後者の対応方法を選びました。また今回の現象が起こらない程度の一般のクリエイターにはリアルタイムで換金できなくなるというマイナスの変更になるという懸念もありましたが、今回発生した現象の対象となるユーザーが増えていくことを考慮したうえでの決断です。
対応案の検討
Rails アプリケーションにおいて処理を遅延実行させるための実装方針が2通りありました。
- delayed_jobなどバックグラウンド実行用のgemを使う
- Amazon Simple Queue Service(SQS)を使う
それぞれのpros/consを書いていきます。
- 1のpros
- OSSライブラリとして提供されていれば無料で利用できる
- 導入が簡単
- 1のcons
- バージョンの問題がある
- メタプロを駆使して書かれているプロダクションコードとの相性が悪く、エラーが発生してしまう(キューをDBに入れるところまではいけたけど、キューの取り出しから実行のところがうまくいかない)
- 2のpros
- 換金申請からSQSに投げるところまでの処理は、メタプロが絡まない(バッチでの実際の換金処理にメタプロの影響を閉じ込められる)
- 2のcons
- 有料(送信するキューの総量に応じた従量課金制)
- 導入に時間がかかる
時間の制約もあり、1のconsの修正に時間をかけたくなかったためSQSを使用する2の方法で実装することにしました。
対応
このようなリアルタイムの同期処理をSQSによる非同期の遅延実行に移行するためにあたり、様々な対応が必要となりました。
キューのメッセージの量
SQS の料金形態上、クリエイターからの申請のたびに即メッセージを送信するのはコストがかさみます。しかし、クリエイター視点でいうと換金処理の申請前後で自身のクレジット額がなるべく早く反映されてほしいはずです。例えば1日1回しか換金できないという修正にするとクリエイターは遅いと感じるでしょう。これらを踏まえて1時間に1回申請を受け付けるように仕様を変えました。処理実行頻度
キューのメッセージを受け取った後(換金申請後)、修正前と同様にすぐに換金処理を実行するという実装をSQSを使ったバッチでの実装ですると、サーバーへの負荷が大きくなります。今までの換金申請の頻度はまばらで1日数件しかないこともありました。これらを考慮し1時間に1回バッチ処理として実行することにしました。バッチ処理中にエラーが起きた場合のキュー内メッセージ処理
バッチ処理の実行結果によらず、必ずキューからメッセージを削除するようにしました。メッセージは明示的に削除しないかぎりキューの中に残り続けるため、何かしらのエラーでメッセージが削除されずに溜まり続けることで後続メッセージが実行されなくなるためです。
これらを踏まえてざっくりと以下のような処理の流れになりました。
# controller.rb # パラメーターをメッセージとしてSQSに送る AWS::SQS.new.client.send_message(queue_url: “sqs_url”, message_body: params.to_json) # リダイレクト redirect_to complete_path
# batch.rb # 1時間に1度実行するバッチ処理 # 換金処理実行(時間のかかる処理) generate_exchange! # messageを消す delete_message
結果
今までタイムアウトエラーが多発していた申請処理を、SQSへメッセージを飛ばすだけの処理に切り離し、実際の換金処理は定期実行されるバッチ処理に逃がすことでクリエイターが換金処理を申請してから完了するまでの待機時間を60秒から29ミリ秒まで減らすことに成功しました。
クリエイターからは今回の仕様変更に関する問い合わせが数件かありましたが、大きなエンバグなどの報告がなく稼働できています。
まとめ
SQSのキューに処理を逃して遅延実行することでタイムアウトエラーは消え、速度改善を果たすことができました。しかし、もともと換金処理に不満のなかったユーザーには、今回の仕様変更で換金処理を非同期実行するようになることでタイムラグが発生したように見えてしまいます。今回はそれを踏まえても修正の必要があると判断しましたが、修正による影響とユーザビリティのバランス感覚、トレードオフが大事だなーと感じました。