memcached 賛歌
(jchri.st)- キャッシュはデータベース負荷を減らすために導入されるが、Redisのように使いやすいツールは、時間がたつと永続ストレージのように依存されやすい
- 問題はRedisの永続化機能そのものではなく、最初は揮発性キャッシュとして導入されたコンポーネントがアプリケーションの中核状態と結びついていく運用フローにある
- memcachedは公式の定義からして分散メモリオブジェクトキャッシュシステムであり、ディスク保存を前提としないため、ステートレスなキャッシュワークロードとして扱いやすい
- 複数のmemcachedインスタンスはサーバー側ではなくクライアント側がURL一覧とキーのハッシュで振り分け、障害ノードはハッシャーから外された後、のちに再接続が試みられる
- 「データベースが遅い」という理由でキャッシュを先に追加するより、まず遅いクエリと不足しているインデックスを確認すべき
Redisがキャッシュからストレージへ変わる瞬間
- インフラを運用していると「キャッシュが必要だ」という要求はよく出てきて、慣れていて機能も多いRedisが真っ先に思い浮かびやすい
- RedisのホームページではAIアプリ向けリアルタイムコンテキストエンジンのRedis Irisが前面に出ているが、Redisが収益を上げなければならない会社であることを考えれば理解できる方向性ではある
- Redisをデプロイして接続文字列を渡せば、最初は信頼できるキャッシュのように動作する
数カ月後に起きる問題
- 時間がたつと
cache.set("key", "value")はINSERT INTO table VALUES ('key', 'value')よりはるかに単純なので、人々はRedisを次のように扱い始める- 常に存在するコンポーネントであり、データを保持する場所。事実上のデータベース
- REmote DIctionary Server を揮発性キャッシュではなく永続ストレージとして認識するようになる
- あなたも、運用チームの同僚もこの事実に気づいておらず、キャッシュは揮発性であることを前提にしていると思っているため、アラートシステムもこれを検知できない
- アップグレード、ノード移行、あるいはRAID0サーバーのHDDトレイが抜ける事故など、Redisに何か手を加えたときに初めて問題が表面化する
- 核心の問題はRedisに永続化機能がないことではなく、キャッシュとして導入されたRedisを人々がキャッシュとして扱わないという前提のずれにある
- 遅れて依存関係に気づいた頃には、Redisはアプリケーションに深く組み込まれていて外しづらく、結局は「ペット」のように保守し監視しなければならなくなる
memcachedがキャッシュの役割により直結している理由
- memcached は「無料のオープンソース、高性能、分散メモリオブジェクトキャッシュシステム」であり、データベース負荷を減らして動的Webアプリケーションを高速化するための汎用キャッシュである
- Djangoのようにプラガブルキャッシュをサポートするフレームワークでは、キャッシュバックエンドを切り替えられる
- Redisより機能が少なくてもmemcachedを選ぶ理由として、運用特性がシンプルである点が挙げられる
- ダウンタイム対応が容易: クライアントライブラリは接続例外を無視することが多く、単純な
getはサーバーが落ちていてもデフォルト値やNoneを返せる - memcachedにはクラスタリング機能が組み込まれていないが、そのことがかえってクラスタリングをやりやすくしている
- クライアントライブラリに複数のURLを設定すると、キーのハッシュで対象インスタンスを選ぶ
- クライアント呼び出しがインスタンスダウンを検知すると、ハッシャーからノードを除外し、一定時間後に自動で再接続を試みる
- 永続化の負担が構造的に小さい: memcachedはディスクに保存しないため、ステートレスなワークロードとして任意の場所にスケジューリングしやすい
- ダウンタイム対応が容易: クライアントライブラリは接続例外を無視することが多く、単純な
- Redisでも似た運用方式を作ることはできるが、memcachedのアーキテクチャはこの方向により近く、キャッシュとして扱うのが直感的である
- memcachedは比較的シンプルなアプリケーションで、約64MBのキャッシュサイズのインスタンスを数十個動かしてもオーバーヘッドがほとんどないことも選ばれる理由になる
- 多くの「データベースが遅い」問題は、実際には遅いクエリや不足しているインデックスから始まるので、キャッシュ追加とあわせてクエリ最適化も見るべきである
- memcachedの設計判断に興味があるなら、memcached blog に興味深い記事が多くあり、そのひとつが5月に公開された 「そのレスポンスが返ってくるまで実際どれくらいかかるのか? (How Long Does That Response Take… For Real?)」 である
1件のコメント
Hacker Newsの意見
Redisは優れた技術だが、永続的なデータ構造と揮発性キャッシュという異なる2つの役割を同時にうまく果たそうとして苦労していると思う。
実際のRedisでもこの2つはうまく混ざっておらず、永続性はグローバルに有効化または無効化する形になっている。
純粋なキャッシュにはmemcachedか同等のものを使い、スコアボードのようなデータ構造が必要なときだけ永続性を有効にしたRedisを使うだろう。
$WORKではどちらも導入しておらず、低速な処理向けのキャッシュ層はファイルシステムと、キー・バリュー・ストアのように使うDBテーブルの両方にデータを置いている。
DBはthundering herdの調整に役立ち、同じサーバーからの読み取りはファイルシステムだけを参照し、別サーバーからの読み取りはDBを一度見たうえでファイルシステムに保持する。
ファイルシステム層をmemcachedに置き換えることもできるが、今のところ非常によく機能している。
Redisのほうが機能は明らかに多く、antirezも魅力的で驚くほど謙虚な人物だったので、Redisの人気が高まった理由は理解できる。
それでも自分にとってmemcachedは常に退屈な技術を選べの極致だった。
プラットフォームエンジニアとしては両方サポートできるが、開発者がRedisの高度な機能である永続化、レプリケーション、クラスタリングを使い始めたら、その選択の欠点を本当に理解しているか確認したくなる。
こうした解決策を提案するたびに、キャッシュは必ず専用ストアにあるべきだと感じている未熟な人たちと、エンジニアリングの現場で数え切れないほど衝突してきた。
memcacheだからといって、こうした問題を避けられるわけではまったくない。
2000年代半ばにmemcacheを使うスケーラブルなシステムを扱ったが、開発者たちは記事でRedisの例として挙げられていたのとまったく同じ落とし穴にはまっていた。
memcacheで分散システムの法則を回避しようとし、キャッシュ依存症のせいでmemcacheが稼働している前提でサーバー群のサイズを決めた結果、障害が起きると突然DDoSのように吹き上がった。
あるホストがTPSの高いキーを失うと、他のすべてのホストがそのキーを再投入しようとして依存サービスを叩く書き込み増幅も起きたし、ホットキーがホットホストを生み、memcachedをサービスデーモンと同居させたことで正体不明のCPUスパイクにもつながった。
古いDNSエントリの粘着性のせいで、memcache呼び出しがブラックホールに吸い込まれることもあった。
どれもmemcacheをより適切に使えば避けられたが、乱用の誘惑が強すぎた。
筆者が言及していたRedis/Valkeyの問題は、プロダクションでほぼ全部見たことがある気がする。
Valkeyにメモリポリシーがなく、メモリを全部食い尽くしてappend-only fileの書き込みエラーを起こした障害もあったし、単純にディスクが満杯でAOF書き込みに失敗したケースもあった。
Redisが生きていて動作中で、しかも全ユーザーデータで埋まっていることを完全に前提にしていて、スローパスへ戻る仕組みがないために500エラーになったこともある。
ソート済みセットや他のデータ構造を創造的に使い、その集合が絶対に退避されないことに依存していた例もあった。
現場でそうした観察をしていても、Redisより先にmemcacheを勧めるのはやはり難しいと思う。
memcacheに向いたキャッシュ配置になるようアプリを設計するのは厄介なことがあり、十分に大きなチームがmemcacheを使えば、結局Redisが必要になる道を見つける可能性が非常に高い。
そうなると2つのキャッシュ技術を維持することになる。
キャッシュ用に設定したRedisインスタンスは他の目的には使えず、キャッシュ用インスタンスには退避が必要で、非キャッシュ用インスタンスには退避があってはならない。
結局、設定の異なる2つ目のRedisが必要になる。
正直、アプリをmemcache向きのキャッシュ配置に設計するのは、Redis向きのキャッシュ配置に設計するのと同じことだ。
こうしたアプリケーションキャッシュのパターンは同一で、取得して、なければ計算して設定するという方式になる。
var value = cache.lookup( keyname, () => db.query(...), TimeSpan.FromMinutes(5) // or CacheOptions );こうすれば、キャッシュミス時にすぐフォールバック経路へ進んだり、挿入したりできる。
memcacheであまり語られないもう1つの特徴は、すべての操作が設計上**O(1)**であることだ。
著者たちが意識的に選んだ設計なので制約はあるが、単純な操作でランダムに停止するようなことが起きないよう保証している。
一方Redisはシングルスレッドコア設計なので、任意の複雑さの操作を実行でき、開発者はそれを使って賢くなった気分になるかもしれないが、その操作が終わるまで他のすべてが待たされる。
オープンソースプロジェクトや長期保守されるプログラムでは、こういうことはよく起きる。
コードベースが大きくなると、当初の計画になかったものまで結局サポートし始める。
機能が増えるとユーザーも増え、ある人は昔の機能だけを使い、ある人は新機能を受け入れ、結局ある特定の値が事実上のデフォルトになって、もはや選択肢には見えなくなる。
Redisを例にすると、AOFを無効にすれば揮発性のインメモリキャッシュとして動作するが、ほとんどの人はそう考えていない。
だから機能が少なくてシンプルなほうがよいという理屈が出てきて、この文脈ではMemcachedがそうした 拘束衣アプローチ の例になる。
大きなチームでは完全に理にかなっているが、オープンソースプロジェクトは資金や貢献を継続的に得るために定期的なアップデートが必要なので、内在する緊張関係がある。
ときには、あるニッチ領域に特化したフォークや派生プロジェクトにつながる。
個人的には正解はなく、文脈次第だと思う。
コミュニケーション自体もタダではないからだ。
開発者たちはそれをまるで全く分かっていないように見える。
MemcachedをRedisに置き換えて、同じものを期待したからだと思う。
それでも素晴らしいキャッシュではある。
ここ数年かなりFlaskの仕事をしてきたし、フルタイムではないが小さなeコマース事業の技術スタックの一部として使ってきた。
MongoEngine、SQLAlchemy、Celery、Google/eBay/Shopify向けのPythonスタックではあらゆる地雷や妙な挙動を経験したが、Redisではそういうことはなかった。
たぶんRedisを永続ストレージだと思っている人に管理者権限を与えないようにしていたからかもしれないが、正直Redisは 非常に堅牢でよく設計された技術 だと言いたい。
APIは極めてシンプルで、少し変わったことをしなければならないときにも、毎回合理的でよく考えられた方法がある。
タギングのような無効化システムは使えるかもしれない。
キャッシュシステムでできる変わったこととは何なのか、単にデータをキャッシュする以外に人々がキャッシュで何をしているのか純粋に気になる。
memcachedは好きだが、Redisを揮発性キャッシュとして設定しておいて人々が 永続データストア のように扱うのだとしたら、それはRedisのせいではない。
memcachedも永続的ではないという点で、この比較は特に奇妙だ。
別途そう言われない限り、新しい開発者がそう仮定するのも無理はない。
Memcachedは登場当時、キャッシュの救世主だった。
2003年にBrad FitzpatrickがLiveJournalのために作ったという点もよい。
ユーザーフィードの各投稿ごとにアクセス制限が異なる可能性があり、そのおかげで投稿やページ全体をキャッシュできた。
Ruby on Railsとともに何年も使い、ページは速くなり、とにかくうまく動いた。
欠点であり速度面での利点でもあったのは、キャッシュがディスクではなく メモリ に保存されることだった。
キャッシュすべきデータが広範で大規模なサイトなら、ホスティングコストが高くなり得る。
そういう場合には、Solid Cacheが私にとっての救世主だった。
今取り組んでいるプロジェクトには100GBを超えるキャッシュがあり、PostgreSQLのディスクに保存され、インデックスで高速に参照され、Railsが自動的に期限切れを処理して該当行を削除する。
キャッシュ規模がもっと小さく、すでにRedisを使っているなら、おそらくそのままRedisを使うと思う。
ただし速度が最優先なら、MemcachedとRedisをベンチマークしてみる。
memcachedが一時的なものであることと、人々がそれを永続的なもののように使うかどうかは別問題だ。
キャッシュヒット率が99.9%のように見えて常に存在するなら、いずれ誰かがその挙動に依存するコードを書くことになる。
開発モードでは、クライアントライブラリが10%くらいnullを返すようにできたら役に立つかもしれない。
memcachedは単純な キー・バリューキャッシュ の処理ではRedisよりばかげているほど速い。
スレッドがあり、1つの仕事を非常にうまくこなすよう高度に最適化されている。
一方Redisは、あらゆるデータ構造とシングルスレッド性などを備えた、任意共有のPythonヒープのようなものに近い。
NotionではRedisを複数の用途に使っているが、実際のキャッシュはmemcachedに任せている。
読み取り1回あたり平均 300マイクロ秒 対 350マイクロ秒 くらいだ。
シングルスレッドである点もそれほど重要ではなく、CPUボトルネックではなくレスポンシブI/Oだからだ。
より多くのCPUコアを使えるようにはなるが、負荷がそれほど高くないなら、シングルスレッドのmemcachedのほうがマルチスレッドよりCPUを少なく使う。