随着第5集的播出,随着案情的突破,《.NET 5.0 背锅案》演变为《博客园技术团队甩锅记》,拍片不成却自曝家丑,这次对我们是一次深刻的教训。

在这次甩锅丢丑过程中,我们过于自信,我们的博客系统身经百战,我们使用的开源 redis 客户端 StackExchange.Redis 更是身经千战,虽然 .NET 3.1 版与 .NET 5.0 版相差100多个 commit,但都是业务代码,我们没能耐写出这么大的 bug,唯一不是很有信心就是我们维护的 memcached 客户端 EnyimMemcachedCore,当确认 EnyimMemcachedCore 无罪后,我们信心满满地让刚出道的 .NET 5.0 继续背锅,结果甩锅不成反丢丑。

当剧情由“锅儿甩甩”发展为“自己的锅自己背”,我们已无路可退。望着那看不到边的100多个commit(gitlab compare不支持显示这么多的commit),我们依然抑制不住甩锅的冲动,再次验证了那句话——“恶习难改”,我们将甩锅的目光瞄向了 redis 客户端,这段时间博客系统中非业务层面代码的最大变化就是引入了 redis 缓存,并打算逐步用 redis 取代 memcached,之前一直没有怀疑 redis 缓存部分,是因为不出故障的 .NET Core 3.1 版与出故障的 .NET 5.0 版都使用了 redis 缓存。

现在 redis 客户端荣幸地入选为我们的首选甩锅对象,即使不怀疑它,也要给它找找茬。我们的目光首先锁定 StackExchange.Redis,当看到它身上的 Star 4.5k,迅速地移开了目光,这是大佬,这是前辈,此锅怎么也不能甩给它,不然又会闹出大笑话。就在这时,大佬身旁的助理 ——StackExchange.Redis.Extensions —— 让我们眼前一亮,Star 386——甩锅的好对象,而且我们的代码中都是通过这个助理和大佬 StackExchange.Redis 打交道的。

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

这时,我们突然想到一句俗话“助理强,则大佬强”,立马意识到之前我们直觉地认为“大佬强,则助理不会差”是个误区,首先应该怀疑的是助理,而不是大佬。进一步分析发现 StackExchange.Redis.Extensions 助理是我们当前知道的博客系统中高并发战斗经验最少的,它最应该成为嫌疑犯,而不是甩锅的对象,虽然从外表看(Extensions命名)它应该不会做出带来高并发问题这么出格的事情。

立即以闪电般的速度赶到助理所在的城市 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>,这样会造成启动时无法对连接池进行预热,会加剧高并发问题。

继续追查,看看更关键的 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/cnblogs/StackExchange.Redis.Extensions/pull/1

昨天晚上,我们发布了升级到 StackExchange.Redis.Extensions 改造版的博客系统,发布过程中稳稳的、妥妥的,发布后一切正常。

今天,我们发布了《.NET 5.0 背锅案》第7集,宣布结案。

结案感言:

  • 我们的错,我们会好好反思,吸引教训。博客园技术团队也是刚刚从单兵作战阶段迈向团队协作规模作战阶段,我们有很多很多东西需要学习,请大家谅解我们在学习过程中所犯的错误。
  • 助理强,则大佬强;生态强,则 .NET 强。仅仅有强大的 C# ,强大的 Visual Studio,强大的 runtime,强大的基础类库是不够的,还需要敢于分享问题,不怕 .NET 被黑被背锅的社区。.NET 的未来不是我们希望出来的,是我们实际使用出来的,是我们踩坑踩出来的。

《.NET 5.0 背锅案》第7集-大结局:捉拿真凶 StackExchange.Redis.Extensions 归案的更多相关文章

  1. 《.NET 5.0 背锅案》第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore

    在第1集的剧情中,主角是".NET 5.0 正式版 docker 镜像",它有幸入选第1位嫌疑对象,不是因为它的嫌疑最大,而是它的验证方法最简单,只需要再进行一次发布即可.我们在周 ...

  2. 《.NET 5.0 背锅案》第5集-案情大转弯:都是我们的错,让 .NET 5.0 背锅

    第1集:验证 .NET 5.0 正式版 docker 镜像问题 第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore 第3集-剧情反转:EnyimMemcachedCo ...

  3. 《.NET 5.0 背锅案》第6集-案发现场回顾:故障情况下 Kubernetes 的部署表现

    第1集:验证 .NET 5.0 正式版 docker 镜像问题 第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore 第3集-剧情反转:EnyimMemcachedCo ...

  4. 《.NET 5.0 背锅案》第3集-剧情反转:EnyimMemcachedCore 无罪,.NET 5.0 继续背锅

    今天晚上基于第2集中改进版的 EnyimMemcachedCore 进行了发布,发布过程中故障重现,最大的嫌犯 EnyimMemcachedCore 被证明无罪,暂时委屈 .NET 5.0 继续背锅. ...

  5. 《.NET 5.0 背锅案》第4集:一个.NET,两手准备,一个issue,加倍关注

    第1集:验证 .NET 5.0 正式版 docker 镜像问题 第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore 第3集-剧情反转:EnyimMemcachedCo ...

  6. 《.NET 5.0 背锅案》第1集:验证 .NET 5.0 正式版 docker 镜像问题

    今天我们分析了博客站点的2次故障(故障一.故障二),发现一个巧合的地方,.NET 5.0 正式版的 docker 镜像是在11月10日提前发布上线的. 而在11月10日下午4点左右,由于 CI 服务器 ...

  7. 【故障公告】Memcached 的“惹祸”,不知在为谁背锅

    在 .NET 5.0 背锅 . Memcached 的惹祸 .缓存雪崩之后,我们没有找到问题的真正原因,我们知道没有找到根源的故障总是会再次光临的,不是在这周就是在下周,也许就在双11前后. 就在今天 ...

  8. Redis 5.0.7 讲解,单机、集群模式搭建

    Redis 5.0.7 讲解,单机.集群模式搭建 一.Redis 介绍 不管你是从事 Python.Java.Go.PHP.Ruby等等... Redis都应该是一个比较熟悉的中间件.而大部分经常写业 ...

  9. Memcached 的惹祸,.NET 5.0 的背锅

    抱歉,拖到现在才写这篇为 .NET 5.0 洗白的博文(之前的博文),不好意思,又错了,不是洗白,是还 .NET 5.0 的清白. 抱歉,就在今天上午写这篇博客的过程中,由于一个bug被迫在访问高峰发 ...

随机推荐

  1. Jenkins集成appium自动化测试(Windows篇)

    一,引入问题 自动化测试脚本绝大部分用于回归测试,这就需要制定执行策略,如每天.代码更新后.项目上线前定时执行,才能达到最好的效果,这时就需要进行Jenkins集成. 不像web UI自动化测试可以使 ...

  2. 扫描仪扫描文件处理-ABBYY生成小体积黑白二值化PDF

    禁止所有预处理选项: PDF保存选项: 保存提示(选择"保存为仅图像PDF"):

  3. python GDAL 读写shp文件

    gdal包用于处理栅格数据,ogr用于处理矢量数据. 1 #!C:\Program Files\pythonxy\python\python.exe 2 #-*- coding:gb2312 -*- ...

  4. 记录Spring Boot 2.3.4.RELEASE版注解方式实现AOP和通知的执行顺序

    1.advice 按照以下的顺序执行 输出结果:(正常和异常) 说明:Spring boot 2.3.4.RELEASE 版本使用的AOP是spring-aop-5.2.9.RELEASE,AOP的通 ...

  5. 闭包 - Js函数笔记

    闭包 当函数被保存到外部时,将会生成闭包 闭包会导致原有作用域链不释放,造成内存泄漏 类似的代码就叫闭包 闭包的运行作用域 代码 a被执行,b被定义并保存出来 a结束,b被执行时,a的执行期上下文指向 ...

  6. 解谜:为何用了9-Patch背景图后自带Padding属性?

    本次分享的主题源于笔者在实际开发中遇到的问题. 具体现象为:当普通的9-Patch图用作TextView的backGround属性后,整个TextView便有了一定的Padding值.但笔者并没有给定 ...

  7. idea2019注册码,亲测可用(暂时不可用)!

    原文链接:https://www.jianshu.com/p/702deab2447c 注册码: MNQ043JMTU-eyJsaWNlbnNlSWQiOiJNTlEwNDNKTVRVIiwibGlj ...

  8. A. Peter and Snow Blower 解析(思維、幾何)

    Codeforce 613 A. Peter and Snow Blower 解析(思維.幾何) 今天我們來看看CF613A 題目連結 題目 給你一個點\(P\)和\(n\)個點形成的多邊形(照順或逆 ...

  9. python获取响应某个字段值的三种方法

    近期将要对两个接口进行测试,第一个接口的响应值是第二个接口的查询条件.为了一劳永逸,打算写个自动化测试框架.因为请求和响应都是xml格式的,遇到的问题就是怎么获取xml响应的某一个值.尝试了很多博客的 ...

  10. LoRa联盟的简介

    LoRa联盟成立于2015年3月,从成立开始,LoRaWAN规范就在不断更新,从1.0.0版本已更新至1.0.2版本,目前能公开下载的事2016年7月完成的1.0.2版本,可以看到该规范的主要作者包括 ...