《.NET 5.0 背鍋案》第7集-大結局:捉拿真兇 StackExchange.Redis.Extensions 歸案

隨着第5集的播出,隨着案情的突破,《.NET 5.0 背鍋案》演變爲《博客園技術團隊甩鍋記》,拍片不成卻自曝家醜,此次對咱們是一次深入的教訓。html

在此次甩鍋丟醜過程當中,咱們過於自信,咱們的博客系統身經百戰,咱們使用的開源 redis 客戶端 StackExchange.Redis 更是身經千戰,雖然 .NET 3.1 版與 .NET 5.0 版相差100多個 commit,但都是業務代碼,咱們沒能耐寫出這麼大的 bug,惟一不是頗有信心就是咱們維護的 memcached 客戶端 EnyimMemcachedCore,當確認 EnyimMemcachedCore 無罪後,咱們信心滿滿地讓剛出道的 .NET 5.0 繼續背鍋,結果甩鍋不成反丟醜。git

當劇情由「鍋兒甩甩」發展爲「本身的鍋本身背」,咱們已無路可退。望着那看不到邊的100多個commit(gitlab compare不支持顯示這麼多的commit),咱們依然抑制不住甩鍋的衝動,再次驗證了那句話——「惡習難改」,咱們將甩鍋的目光瞄向了 redis 客戶端,這段時間博客系統中非業務層面代碼的最大變化就是引入了 redis 緩存,並打算逐步用 redis 取代 memcached,以前一直沒有懷疑 redis 緩存部分,是由於不出故障的 .NET Core 3.1 版與出故障的 .NET 5.0 版都使用了 redis 緩存。github

如今 redis 客戶端榮幸地入選爲咱們的首選甩鍋對象,即便不懷疑它,也要給它找找茬。咱們的目光首先鎖定 StackExchange.Redis,當看到它身上的 Star 4.5k,迅速地移開了目光,這是大佬,這是前輩,此鍋怎麼也不能甩給它,否則又會鬧出大笑話。就在這時,大佬身旁的助理 ——StackExchange.Redis.Extensions —— 讓咱們眼前一亮,Star 386——甩鍋的好對象,並且咱們的代碼中都是經過這個助理和大佬 StackExchange.Redis 打交道的。redis

public class BlogPostService : IBlogPostService
{
    private readonly IRedisDatabase _redis;
    // ...
}

這時,咱們忽然想到一句俗話「助理強,則大佬強」,立馬意識到以前咱們直覺地認爲「大佬強,則助理不會差」是個誤區,首先應該懷疑的是助理,而不是大佬。進一步分析發現 StackExchange.Redis.Extensions 助理是咱們當前知道的博客系統中高併發戰鬥經驗最少的,它最應該成爲嫌疑犯,而不是甩鍋的對象,雖然從外表看(Extensions命名)它應該不會作出帶來高併發問題這麼出格的事情。docker

當即以閃電般的速度趕到助理所在的城市 github ,潛入 StackExchange.Redis.Extensions 倉庫偵查。緩存

經過 IRedisDatabase 接口找到對應的實現類 RedisDatabase,發現了下面的代碼:多線程

public IDatabase Database
{
    get
    {
        var db = connectionPoolManager.GetConnection().GetDatabase(dbNumber);

        if (!string.IsNullOrWhiteSpace(keyPrefix))
            return db.WithKeyPrefix(keyPrefix);

        return db;
    }
}

StackExchange.Redis.Extensions 在本身管理着 redis 鏈接池,這但是高併發事故(尤爲是程序啓動時)最容易發生的高危地段啊,這須要很強很強的助理啊,Extensions 助理能搞定嗎?這時電腦屏幕上「出現了」滿屏的問號???併發

繼續追查,看看 GetConnection 方法的實現 RedisCacheConnectionPoolManager.GetConnection:異步

public IConnectionMultiplexer GetConnection()
{
    this.EmitConnections();

    var loadedLazies = this.connections.Where(lazy => lazy.IsValueCreated);

    if (loadedLazies.Count() == this.connections.Count)
        return (ConnectionMultiplexer)this.connections.OrderBy(x => x.Value.TotalOutstanding()).First().Value;

    return (ConnectionMultiplexer)this.connections.First(lazy => !lazy.IsValueCreated).Value;
}

這裏居然用了 Lazy<T>,這樣會形成啓動時沒法對鏈接池進行預熱,會加重高併發問題。tcp

繼續追查,看看更關鍵的 EmitConnections 方法實現:

private void EmitConnections()
{
    if (connections.Count >= this.redisConfiguration.PoolSize)
        return;

    for (var i = 0; i < this.redisConfiguration.PoolSize; i++)
    {
        this.EmitConnection();
    }
}

這裏沒有用鎖,程序啓動後,併發請求一進來,會有不少線程重複地建立鏈接,假如 PoolSize 是50,若是剛啓動時有100個併發請求進來,就會試圖建立5000個鏈接,這是個大問題,但實際狀況沒這麼糟糕,因爲使用了前面提到的 Lazy ,不會當即建立鏈接,因此不會帶來大的的併發問題。

繼續追,看看更更關鍵的 EmitConnection 方法:

private void EmitConnection()
{
    this.connections.Add(new Lazy<StateAwareConnection>(() =>
    {
        this.logger.LogDebug("Creating new Redis connection.");

        var multiplexer = ConnectionMultiplexer.Connect(redisConfiguration.ConfigurationOptions);

        if (this.redisConfiguration.ProfilingSessionProvider != null)
            multiplexer.RegisterProfiler(this.redisConfiguration.ProfilingSessionProvider);

        return new StateAwareConnection(multiplexer, logger);
    }));
}

當咱們看到 ConnectionMultiplexer.Connect 使用的是同步方法時,根據咱們在 EnyimMemcachedCore 遇到過的血的教訓,咱們知道真兇找到了!

這個地方使用同步方法,在程序啓動時,在鏈接池創建好以前,大量的併發請求進來,同步方法會阻塞線程,加上建立 tcp 鏈接是個耗時操做,這時會消耗不少線程,形成耗盡線程池中的線程緊缺,從而引起咱們在背鍋案中遇到的故障。若是改成異步方法,好比這裏改成 ConnectionMultiplexer.ConnectAsync,在進行建立 tcp 鏈接的IO操做時會釋放當前線程,因此不會出現前述的問題。若是必定要使用同步方法,有一個緩解方法就是在預熱階段(程序啓動時請求進來以前)建立好鏈接池。

StackExchange.Redis.Extensions 這個助理,扛着 StackExchange.Redis 的大旗,卻犯了3錯誤:

  1. 使用 Lazy 形成沒法預熱鏈接池
  2. 沒有使用鎖或其餘方式避免重複建立鏈接
  3. 沒有使用 StackExchange.Redis 的異步方法 ConnectionMultiplexer.ConnectAsync

而第3個錯誤是最致命的,也是 .NET 5.0 背鍋案的罪魁禍首。

昨天下午,咱們將真兇 StackExchange.Redis.Extensions 捉拿歸案,並對其進行改造,改造代碼見 https://github.com/imperugo/StackExchange.Redis.Extensions/pull/356

昨天晚上,咱們發佈了升級到 StackExchange.Redis.Extensions 改造版的博客系統,發佈過程當中穩穩的、妥妥的,發佈後一切正常。

今天,咱們發佈了《.NET 5.0 背鍋案》第7集,宣佈結案。

結案感言:

  • 咱們的錯,咱們會好好反思,吸引教訓。博客園技術團隊也是剛剛從單兵做戰階段邁向團隊協做規模做戰階段,咱們有不少不少東西須要學習,請你們諒解咱們在學習過程當中所犯的錯誤。
  • 助理強,則大佬強;生態強,則 .NET 強。僅僅有強大的 C# ,強大的 Visual Studio,強大的 runtime,強大的基礎類庫是不夠的,還須要勇於分享問題,不怕 .NET 被黑被背鍋的社區。.NET 的將來不是咱們但願出來的,是咱們實際使用出來的,是咱們踩坑踩出來的。
相關文章
相關標籤/搜索