みなさんさようなら、インフラ部の @h3_poteto です。
今日は昇竜拳の話をします。
↑昇竜拳
クラウドワークス本体のアプリはRailsで作られているのですが、その中にちょいちょい非同期処理が載せられています。 メール送ったり、Elasticsearchとの同期処理だったり、重いスカウト処理だったり。
DelayedJobだと限界が見えてきた
その非同期処理として、今までは、DelayedJobを使っていたんですが、ジョブが多くなり、キューが多くなり、ワーカーが多くなるにつれて、どんどん重くなってきました。 ActiveRecordを介してDBにキューを貯めるという方式がどうにもキツイ。
DelayedJobは本当に気軽に非同期処理が出来て良いのですが、キューにRDBを使うところがすごくイケてないです(だから気軽にできるんですけど)。
エンキューが多くなればそれだけ書き込みも、また処理完了後の削除数も多くなってしまうので、どんどんDBが断片化していってしまうんです。
そうすると、やがてDelayedJobのワーカーがジョブを探すために find
するクエリすら重くなってきます。
非同期に任せるジョブを増やして、その結果DelayedJobのワーカーを増やせば増やすほど、find
のクエリ数は増えていき、結果としてRDBに負荷をかけ続けます。
これではどうにもスケールしない。
Sidekiqにするか?
理想的には、キューはRDBではないものを使いたいです。キューというのはSQLを組まないと探せないほど複雑なデータ構造はしていないのが一般的であるし、頻繁に find
かけたいじゃないですか。
Railsの非同期処理としては、他にResque、Sidekiqなどが有名ですね。 どちらもRedisをバックエンドに使ってキューの管理をしてくれます。
前述のようなDB高負荷問題もあり、一度Sidekiq導入も考えたのですが、「とりあえずDelayedJobとの共存」という制約もありました(現状DelayedJobで動いているものを一気に移すのはキツイ、1つずつ順々に移すためにひとまず同居したい)。
ただ、同居させるには、Sidekiq側の .delay
メソッドを封じておく必要があります(ここがDelayedJobと被る)。
Delayed extensions · mperham/sidekiq Wiki · GitHub
そこで迷っている間に、shoryuken というやつを見つけてきました。
Sidekiqにだいぶ近いのですが、バックエンドにAWS SQSを使うという特徴があります。 キューのシステムとしては、Redisより堅牢そう。
そして、shoryukenには .delay
メソッドみたいにDelayedJobと被るメソッドは存在しませんでした。
というわけでshoryukenを導入してみた
細かい設定が違うのですが、イメージとしてはだいたいSidekiqと同じです。 作者もかなりSidekiqを意識しているようなので。
aws: access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %> secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %> region: <%= ENV['AWS_REGION'] %> sqs_endpoint: <%= ENV['AWS_SQS_ENDPOINT'] %> receive_message: attribute_names: - ApproximateReceiveCount - SentTimestamp concurrency: 25 delay: 25 queues: - [high_priority, 6] - [default, 2] - [low_priority, 1]
こんな config/shoryuken.yml
を作って、
$ bundle exec shoryuken -R -C config/shoryuken.yml
どーん!
SQSにキューを貯めるので、RDBへの負荷がかかることはもちろんなくなりました。 これでどんどんワーカーを増やせる!
細かい設定とか動作
すごく雑に説明したんですが、shoryukenの細かい設定については以下にまとめました。
環境変数読み込みつらい問題
上記の例だと、AWSの設定を全て環境変数経由としていました。shoryukenのワーカーは shoryuken.yml
を読みこませるので、もちろんこれでAWS関連の設定が取得できるのですが、Rails側からAWS SQSにエンキューする際に、なんでこれで上手くかというと、(内部で使われている)aws-sdk
が環境変数経由で設定読み込みしてくれるからです。
なのでこれ、環境変数の変数名変えたりすると動かなくなります。
アプリ全体的に環境変数で設定を読みこませる形で設計していればなにも問題ないのですが、クラウドワークスの場合はほとんど環境変数を使っていません。 ここだけ環境変数にするというのはなかなかいただけない。
そんな議論はgithub上でもされていて、 github.com
解決方法が提示されています。
Shoryuken::EnvironmentLoader.load(config_file: "config/shoryuken.yml")
こんなのを config/initializers/shoryuken.rb
とかに書いておくと、shoryuken.yml
のAWS設定を読み込み、aws-sdk
がリクエストを投げるときに使ってくれます。
こうしておけば、環境変数を使わずに、
aws: access_key_id: "aws_access_key_id" secret_access_key: "aws_secret_access_key" region: "ap-northeast-1" sqs_endpoint: "https://sqs.ap-northeast-1.amazonaws.com"
こういう shoryuken.yml
の書き方をしておいても問題なく動作します。
開発環境でどうしようか問題
私一人であれば、別にAWS SQSに開発用のキューを作ればいいのですが、エンジニアがたくさんいる今の開発チームではちょっと現実的ではなさそうでした。 お金が無尽蔵にあれば、SQSに開発メンバー全員分のキューを作っておけばいいかもしれないけれど、それを管理するのはめんどくさいなぁ……。
というわけで、fake_sqsというものがあります。
ローカルでSQS みたいな ものを立ち上げてくれます。
厳密に言うとAWS SQSとはちょっとだけ違う部分があったりしますが、とりあえず開発環境で使う分には困らなそうです。
gemで入るので、Gemfileに書いておくだけで開発メンバー全員の環境に入ります。べんり。
$ bundle exec fake_sqs
で起動できます。
ただ、fake_sqsはメモリ上にキューを作っているので、プロセスが終了するとキューが消えます。もちろん中身のメッセージも消えます。 なので、どこかでキューを作ってやる必要があります。
# fake_sqsを使うとき以外は不要です # 適切に、Rails.env.production? みたいな判定を追加することをおすすめします sqs_client = Aws::SQS::Client.new( endpoint: "http://localhost:4568", secret_access_key: "secret access key", access_key_id: "access key id", region: "region" ) queues = sqsl_client.list_queues if queues.successful? # queues.queue_urlsは # "http://0.0.0.0:4568/default" # というstringが返ってくるので、比較のためにpathを抜き出す unless queues.queue_urls.map{|q| URI.parse(q).path }.include?("/default") sqs_client.create_queue(queue_name: "default") end end
こんなのを、config/initializers/sqs.rb
に配置するとRailsの起動時にfake_sqsのキューをチェックして、必要であればキューを作ります。
まだまだこの先が大変
ひとまず一部の処理をshoryukenで実装することはできました。 やはりキューがRDBでないというだけで、かなり改善はされます。
ただ、まだDelayedJobで動いている処理が大量にあるので、順次これを移していきたいです。
DelayedJobは気軽に非同期処理にエンキューできるため、結構みんな気軽にエンキューしています。 ですが、それをshoryukenに移すとなると難しくて、例えば、トランザクション内でエンキューするような場合が良い例です。
DelayedJobはActiveRecordをバックエンドにするため、トランザクション内でエンキューした場合は、そのトランザクションがコミットされるタイミングで、DelayedJobのキューもコミットされエンキューされます。 これをSidekiqやshoryukenに移す場合には、まずトランザクション内のエンキューをトランザクション外に変更する必要があります(トランザクション内でraiseした時にエンキューされてたらヤバイからね!)。
トランザクションということは、モデルのコールバックも含まれます。
つまり、「after_save
で非同期処理にエンキュー」みたいなのはつい実装したくなる処理だと思うのですが、これそのままでは移せないってことです。
そういう処理を1つずつ移植していかなければ……。
おわりに
そんなわけでまだまだ未完成な昇竜拳しか打てませんでしたが、これからどんどん移行していこうと思っています。