1734207182
2024-12-12 12:26:00
Mattermost では、最近次の規模に拡張することができました。 10万人のユーザー。しかし、私たちはそこで止まりたくなく、さらに前進したかったのです。この投稿では、その取り組みがどのようにして Redis をアーキテクチャに導入することになったのか、そして Redis を大規模に実行することで得た教訓について詳しく説明します。
これまで、私たちはすべてのキャッシュ ニーズに単純なメモリ内 LRU キャッシュを使用してきました。これはこれまでのところうまく機能していますが、さらに拡張しようと努めるにつれて、いくつかの拡張のボトルネックに遭遇し始めました。
私たちが直面した主な問題は、キャッシュの無効化に関するものでした。メモリ内キャッシュ アーキテクチャにより、Mattermost クラスター内のすべてのアプリ ノードは独自のキャッシュを持ちます。また、何かが無効になる場合は、すべてのノードで無効になる必要があります。これは、後でキャッシュ エントリを再設定するために、すべてのノードが個別に独自の DB 呼び出しを行う必要があることを意味します。これにより、クラスター内にノードを追加するほど問題が悪化するというスケーリングの問題が発生しました。 Redis を外部キャッシュとして導入すると、すべてのノードが Redis にアクセスするため、DB 呼び出しは 1 つだけで済むため、この問題は解決されます。
Redis の導入により、その強力な pub-sub メカニズムを使用して、非常に大規模なクラスター ブロードキャストを効率的に実行できるようになりました。
しかし、Redis の実装は、LRU キャッシュを Redis キャッシュに交換するほど単純ではありませんでした。素朴に、そうなると思っていました。それははるかに難しいことが判明しました。このプロセスで学んだ教訓のいくつかを見てみましょう。
ライブラリの選択は重要です 📚
私たちが選んだのは、 https://github.com/redis/rueidis より人気のあるライブラリの代わりに https://github.com/redis/go-redis。
主な理由は、rueidis がデフォルトでさらに多くのことを行うのに対し、go-redis は多くの面倒な作業をユーザーに依存しているためです。例えば:
- 同時クエリの自動パイプライン化
- クライアント側キャッシュ
これらは、ライブラリでサポートされているものの一部です。さらに、ライブラリは、次のような追加のノブを公開することで、パフォーマンスに合わせてより調整されています。 MaxFlushDelay。それでは次の点に移ります。
バッチ処理はオプションではありません 🍞
Redis には という機能があります。 パイプライン化 これにより、1 回の呼び出しで複数のコマンドを送信できるようになります。これは Redis を効率的に使用したい場合に必須であり、私たちが選択した理由の 1 つです。 rueidis の代わりに go-redis。後者の場合、パイプラインを自分で手動で構築する必要がありますが、 rueidis クエリが別のゴルーチンからのものである場合、クエリを自動的にパイプライン化します。
これにより、ラウンドトリップ時間 (RTT) が削減されるだけでなく、Redis 自体のシステムコールのオーバーヘッドも削減されます。
たとえば、次のシステムコールのオーバーヘッドを考えてみましょう。 X そして実際のユーザー空間の動作は次のようになります Y 単一の Redis コマンドの場合。これは純粋にサーバー側であり、クライアントからのネットワーク遅延は無視します。それで処理するには n Redis が実行する必要があるコマンド n(X + Y) 仕事の量。しかし今なら、それらが n コマンドはパイプライン化されており、syscall を 1 つ実行するだけで済むため、合計の作業量は次のようになります。 X + nYこれは、CPU 使用率の削減に多大な影響を与える可能性があります。
両方の長所を得るためにクライアント側のキャッシュを使用します 💺
発信リクエストをバッチ処理したとしても、ネットワークへのアクセスには無視できないコストがかかります。幸いなことに、Redis ではそのようなことが可能です 特徴。クライアント側のキャッシュとして使用する少量のメモリを割り当てると、アプリケーションの遅延の短縮に大きな影響を与える可能性があります。ただし、キャッシュの最適なサイズを決定するには注意が必要です。小さすぎると、十分な改善が得られない可能性があります。また、サイズが大きすぎると、キャッシュを無効にしようとする際に余分なネットワーク オーバーヘッドが発生します。最終的には接続ごとに 32MiB を使用しました。アプリケーションを分析して、最も使用されているキャッシュ オブジェクトを特定し、適切な数を割り出します。
GET/DELETE によるタイトなループがある場合は、MGET/MDELETE を使用します ➰
一部の API 呼び出しでは、正当な理由もなくパフォーマンスが一貫して低下していました。これを理解するまでにかなりの時間がかかりましたが、最終的には Redis の使用が原因であることがわかりました GETタイトなループ内のコマンド。私たちのシナリオは次のようなものでした。
var itemsToQueryFromDB []string
var foundItems []Struct
for _, item := range items {
// call redis GET(item)
// if not found
// append to itemsToQueryFromDB slice
// else
// append to foundItems slice
}
// query DB(itemsToQueryFromDB)
// append to foundItems slice
return foundItems
これはメモリ内キャッシュでは正常に機能しましたが、Redis では悲惨なほど悪かったです。各通話のネットワーク遅延が項目ごとに累積され続けるためです。そして、アイテムが多ければ多いほど、返品にかかる時間も長くなります。
を使用して修正しました MGET 次のように、1 回の呼び出しですべての要素を取得します。
var itemsToQueryFromDB []string
var foundItems []Struct
// call redis MGET(items)
// find items not found in cache, and prepare itemsToQueryFromDB slice
// query DB(itemsToQueryFromDB)
// append to foundItems slice
return foundItems
同じロジックを適用できます DELETE同じように。
大量の場合は注意してください SCAN 電話📈
いくつかのキャッシュがあり、キャッシュ全体を反復する必要があるいくつかのオブジェクトを削除する必要がありました。たとえば、セッション キャッシュはセッション ID によってキー設定されますが、userID によってすべてのセッション オブジェクトを削除するには、すべてのセッション オブジェクトを反復して、userID と等しい ID を持つオブジェクトを見つけて削除する必要があります。それは次のようなものでした:
var toDelete []string
err := sessionCache.Scan(func(keys []string) error {
sessions, errs := sessionCache.GetMulti(keys)
for i, err := range errs {
// error handling
if sessions[i].UserId == userID { // userID is passed from a higher-level function
toDelete = append(toDelete, keys[i])
}
}
})
// error handling
err = sessionCache.RemoveMulti(toDelete)
// error handling
これにより、単に大量の SCAN 呼び出しが原因で、Redis にかなりの CPU 負荷が発生しました。バッチサイズを試してみましたが、大きな違いはありませんでした。もちろん、1000 を超える巨大なバッチ サイズは理想的ではありません。
最終的に、この特性を持つキャッシュの一部については、メモリ内キャッシュを使用し続ける必要がありました。
pub-sub を使用している間はメッセージを自分で送信しないようにしてください 🖇️
pub-sub を使用するときは、Redis がパブリッシャーであっても、すべてのメッセージをすべてのサブスクライバーに送信することに注意してください。したがって、ノードがパブリッシャーとサブスクライバーの両方であり、パブリッシャー ノードが発行されたばかりのイベントを受信しないようにするシナリオがある場合は、イベントにメタデータを追加して、どのノードからのイベントであるかを示す必要があります。送信元ノードからのものであれば無視します。
終わりの言葉
言うまでもないことですが、ソフトウェアを使用するには、それがどのように機能するのか、またソフトウェアを製品と最適に統合する方法を深く理解する必要があります。すべてのソフトウェアには独自のワークロードと要件があります。あなたの考えは私たちのものとは異なるかもしれませんが、この演習からいくつかの重要なポイントを得ることができれば幸いです。
Redis は優れたソフトウェアであり、効率的に使用すると非常にうまく機能します。さらに調査を進めながら、コードベースの他の領域にも Redis の使用を拡張することも検討しています。
これに関してコメントやご意見がございましたら、コミュニティ サーバー (次のアドレス) までお気軽にご連絡ください。 https://community.mattermost.com。私のユーザーIDは @agniva.de-sarker。
#Redis #を大規模に実行することから学んだ教訓