大型案件対策をした話

こんにちは、SREの岩崎です。1QのSRE施策で大型案件対策をしたので、今回はその話について書きたいと思います。

CAMPFIREでは定期的に大型案件と呼ばれる規模の大きいプロジェクトがオープンします。大型案件は公開と同時に支援が殺到するケースが多く、数分間で数百万もの支援が入るケースも珍しくありません。

この時に注意しなければいけないのが、高負荷によるシステム障害です。実際に昨年末の大型案件では、公開後の数分間に渡り504エラーが発生し支援できない状態となってしまいました。

非日常的な負荷が原因とはいえ、大型案件がオープンするたびに障害の危険性がある状況は決して好ましいものではありません。また、度々障害が起こればサイトの信頼性にも関わってきます。大型案件時の負荷対策はチームにとって喫緊の課題でした。

エラー原因の調査

エラーの解消にあたり、まず行ったのがエラーの確認と原因の分析です。上記の大型案件のケースでは、「公開後の数分間に渡り支援時に504エラーが発生し支援できない」という報告がありました。この時の504エラーはCDNとして利用していたCloudFrontの504(Gateway Timeout)エラーだったのですが、なぜタイムアウトしたのかもう少し詳しく探っていく必要がありました。

単純にインフラ側のタイムアウト設定値が短すぎることが原因なら値を調整すれば良いのですが、もしアプリケーション側のどこかでタイムアウトしているとしたらその原因を見つけて潰さなくてはなりません。その場合、原因が決済会社とのAPI通信なのか、自サービス内の処理なのかについても切り分ける必要があります。

調査を進めていくと、どうやら支援数を更新する処理でデッドロックが発生していることがわかりました。

CAMPFIREではカウンターキャッシュの仕組みを使ってプロジェクトやリターンの支援数と支援金額を更新しているのですが、大型案件時のように短時間で大量の支援が殺到すると、同じレコードに対する更新処理が頻発しデッドロックが起こっていました。

デッドロックが発生しないようにコミット後に更新する、カウントをDBではなくRedisなどの外部に持つといった解決策が考えられましたが、より万全を期すために今回は後者を選択しました。

カウンターキャッシュのRedis実装

実装にあたり、Redis::Objectsというgemを利用しました。projects と project_rewards という二つのテーブルで支援数と支援金額を更新する必要があったため、RedisCounterをConcernとして切り出しそれぞれのクラスでincludeする手法を取りました。

・app/models/concerns/redis_counter.rb

# frozen_string_literal: true

module RedisCounter
  extend ActiveSupport::Concern
  
  included do
    include Redis::Objects
    
    counter :redis_backer_count
    counter :redis_backer_total

    def backer_count
      redis_backer_count.value
    end

    def backer_total
      redis_backer_total.value
    end
  end
end

・app/models/project.rb

# frozen_string_literal: true

class Project < ApplicationRecord
  include RedisCounter

  has_many :project_rewards, dependent: :destroy
end

・app/models/project_reward.rb

# frozen_string_literal: true

class ProjectReward < ApplicationRecord
  include RedisCounter

  belongs_to :project
end

あとは支援時のインクリメントを既存のカウンターキャッシュではなくRedis上で行うようにすれば完成です。実際のコードはもっと複雑ですが、大まかなイメージは掴んでいただけたと思います。

最後に

今のところ、この実装がリリースされてからは一度も大型案件でのエラーは起こっていません。まだ安心はできませんが、ひとまず成功と言って良いのではないでしょうか。

このようにSREではソフトウェアエンジニアリングを用いて課題を解決します。ただ、今回のケースはかなりアプリケーション寄りなのでSRE本来の意味合いとは少し異なるかもしれません。しかし、それならそれで良いと思っています。

SREがGoogleのベストプラクティスであるように、それを参考にしつつもCAMPFIRE独自のSREを作っていけば良いと思っています。ポイントはいかにSREを忠実に再現するかではなく、自分自身の環境に落とし込んで再考できるかではないでしょうか。



この記事が気に入ったらサポートをしてみませんか?