DevDevデブ!!

プログラミングのこととか書きます。多分。。。

ぼくと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があります。

github.com

当初はgo-cacheをそのまま利用しようと考えていたのですが、go-cacheはシャーディングの仕組みが無く、ロックの待ち時間でレスポンスが悪化する懸念がありました。

(シャーディングの実装自体はあるんですが、experimentalになっています)

そこで、bigcacheの実装を参考に、シャーディングを自前実装しました。

github.com

実装は公開できませんが、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排除へと動いているという話でした。