ぼくとRedisの一年戦争
WAR IS OVER.....
じゃあないんだよ!絶賛続行中です
この記事はWanoグループアドベントカレンダーの19日目の記事です。
今回はWano入社から1年以上に渡って続いているRedisとの格闘について記したいと思います。
Redisとは
Redisは、データ構造サーバーを実装するオープンソースソフトウェアプロジェクトである。 いわゆるNoSQLデータベースの一つであり、Redis Labsがスポンサーとなって開発されている。 ネットワーク接続されたインメモリデータベースでかつキー・バリュー型データベースであり、オプションとして永続性を持つ。
Wikipediaより引用
はい。レスポンスがごっつ早いKVSです。いろんな用途あると思いますが、うちでは広告配信データを配置するデータベースとして使ってました。
Elasticacheで。
上記の概要には書かれてないですが、Redisはシングルスレッドで動作します。(ここ重要)
バトルその1 容量が足りないの巻
広告impressionに際して発生するあるデータ(毎回ではない)をRedisに載せてたんですが、メモリ容量が簡単に枯渇するので、当該データの保存先をDynamoDBに移しました。
Elasticacheのスケールアップで対応しようとしたら金がいくらあっても足らないので。
(スケールアップすると、メモリ倍々だけど利益ByeByeなので)
このとき、広告配信サーバはngx_mrubyで実装されてたんですが、DynamoDBのクライアントを自作する必要があったのが地味に大変でした。DynamoDBのapi自体はシンプルなんですけど、awsの認証を通すのがね。。。
あと、現在は知らないけど、当時はkeep_aliveが有効なmruby用のhttpclientがmattnさんのlibcurlのラッパーしかなかったような。
ちなみに、Elasticacheは限界超えてメモリを使うと、突然死しますね。(しました)
バトルその2 毎回コネクション貼り直してた
これはただのチョンボなんですけどね。。。
Elasticacheのcloudwatch見てたら、なーんか接続コネクション数がアホほど多いので、何かと思ったら毎回
client = Redis.new("アドレス")
とかやってんのね(ノ∀`)アチャー
Redis側新規コネクション確立コストって結構馬鹿にならないので、もろにパフォーマンスに影響出てました。
対処方法は簡単で、ngx_mrubyの初期化処理でコネクションを貼って、それを使い回すだけ。nginxはシングルスレッドモデルなので、コネクションプールは不要
バトルその3 mruby-redisがブロッキングする
はい。ngx_mrubyがmruby-redisを使ってredisと通信している間、ブロッキングするため、処理が止まります。
ダメじゃん
実装前に確認しとけっつー話なんですけどね。(実装したの私じゃないんですが)
ツワモノならここでコントリビュートチャンスとなるところなのかもしれませんが、私はC言語書けません(大学で習いはした)ので、golangで書き直しました。(nginxからリバースプロキシ)
バトルその4 計算量の大きい命令を投げている
golangで書き直して普通にパフォーマンス上がったんですが(どれくらい上がったのか記録に残してない。。。)、cloudwatchを確認すると、妙にElasticacheのCPU利用率が上がるタイミングがあるので確認したら、かなり計算量の大きい命令を頻繁に投げてました。
HGETALLとかSMEMBERSとかですね。
Time complexity: O(N) where N is the size of the hash.
なわけですよ。
要素数が小さいうちは問題にならないけど、うちの場合はN=3000ぐらいあったので、ちょーっと無視できない負荷になってましたね。
で、冒頭で述べた通り、Redisはシングルスレッドで動作するので、クライアントから受け付けた命令は逐次実行されるんですよね。計算量の大きい命令を受けると、自ずと他のクライアントへのレスポンス速度は悪化しますよと。
このとき、要素数Nを小さくするというのは難しかったので、golangのプロセス内にキャッシュを持つようにしました。(以降プロセスキャッシュと呼びます)
golang用のキャッシュライブラリとしてはgo-cacheがあります。
当初はgo-cacheをそのまま利用しようと考えていたのですが、go-cacheはシャーディングの仕組みが無く、ロックの待ち時間でレスポンスが悪化する懸念がありました。
(シャーディングの実装自体はあるんですが、experimentalになっています)
そこで、bigcacheの実装を参考に、シャーディングを自前実装しました。
実装は公開できませんが、go-cacheのインスタンスが1シャードとなるようにして、あとはbigcacheのシャーディングの仕組みそのまんまって感じです。
(何故bigcacheをそのまま使わなかったのかは覚えていない。。。)
今のところ、おかしな挙動を見せることなく動いています。
プロセスキャッシュを利用することによって、HGETALLの発行頻度を抑えることができたので、ElasticacheのCPU利用率も低減することができました。
なお、全サーバのキャッシュが同時にexpireして、同時期に一斉にHGETALLを発行するとElasticacheが爆発してしまうので、expire設定は固定値 + 乱数にしました。
バトルその5 数の暴力
バトルその4を終えた時点でかなりシステムとして安定を見せていたのですが、それでも数の暴力(広告リクエスト増)には勝てません。
この問題は現在も継続中で、芸能人のスキャンダル(山口メンバーとか)があったりすると、配信先メディアのアクセス数が爆発し、こちらへの広告リクエストも爆発し、システムが爆発する、的な流れになります。
この問題を解決しようとすると、
- Redisへのアクセスを分散させて、負荷を散らす
- Redis以外の激つよkvsに変更する(AerospikeとかAerospikeとか、Aerospikeとかね)
- 広告配信ロジック中から参照するデータを全て個々の配信サーバに持たせて、メモリに載せる
とかになるでしょうか。
なお、シングルスレッドで動くため、Elasticacheのインスタンスサイズを上げてもあまり効果は期待できません。
(ググってみると、アドテクの有名な会社はたいていアクセス爆発->Redis爆発->アーキテクチャ改善の流れをたどってますよね)
うちの場合は、金が無い、人がいないなので1番、2番は難しいです。
(Redisをカウンター的に使ってる処理があるので、水平分散がちょっとむずかしい)
AWSがマネージドAerospikeとか出さないかと期待してるんですけどね。
恐らくやるとしたら3番です。
うまくいけば、来年のアドベントカレンダーに顛末が書けるかもしれません。
まとめ
Redisと格闘の末、Redis排除へと動いているという話でした。