StackExchange.RedisのConnectionMultiplexer.Connectで作ったコネクションは使いまわそう
Redisを使っている部分(非.NET Core)で高負荷時に下記のようなメッセージが出るというので調査した時のメモです。
“No connection is available to service this operation: SET test; IOCP: (Busy=0,Free=1000,Min=2,Max=1000), WORKER: (Busy=24,Free=32743,Min=2,Max=32767), Local-CPU: n/a
ググってみると、StackExchange.Redisのバージョンを下げると問題が解消したとか、タイムアウトを長くしたら解消したとかいうものが出てくるのですが、他のライブラリとのバージョンの競合でバージョンは下げられないし、タイムアウトは対症療法でしかないということで、コードを確認していきます。
そもそもRedisからデータを取得するだけなのにやけに遅いんですよね。
調べてみると、こんな共通のコードをいたるところから呼び出していました。
public class RedisClient { public async Task GetAsync(string key) { using (var redis = await ConnectionMultiplexer.ConnectAsync(this.ConnectionString)) { var db = _redis.GetDatabase(); var result = await db.StringGetAsync(key); return (result.IsNull) ? default(T) : JsonConvert.DeserializeObject(result); } } }
Redisからデータを取得する際に毎回コネクションを作っているので、接続を作るのが遅いし、おそらくクライアント側の資源を使いつくしているせいで冒頭のメッセージが出ているっぽい。このあたりの方針は、StackExchange.RedisのREADMEに記載があって、HttpClientなんかと同じように、StackExchange.RedisのConnectionMultiplexer.Connectメソッドで作られたコネクションは、複数個所から共有して呼び出されることを想定しているようです。
単純にやるなら、Connectionをstaticとして保持しちゃう、ちゃんとやるならFactoryを導入してConnectionの生存期間を制御できるようにする。ってところでしょうか。
public class RedisClient { private static ConnectionMultiplexer _redis; private static ConnectionMultiplexer GetClient() { return _redis ?? (_redis = ConnectionMultiplexer.Connect(configuration.ConnectionString)); } public async Task GetAsync(string key) { var db = GetClient().GetDatabase(); var result = await db.StringGetAsync(key); return (result.IsNull) ? default(T) : JsonConvert.DeserializeObject(result); } }
.NET Core からは、IDistributedCache インターフェイスを使うとこの辺りは面倒見てくれるようになりますよね。
https://docs.microsoft.com/ja-jp/aspnet/core/performance/caching/distributed?view=aspnetcore-3.1