《.NET 5.0 背锅案》第7集-大结局:捉拿真凶 StackExchange.Redis.Extensions 归案
- 第1集:验证 .NET 5.0 正式版 docker 镜像问题
- 第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore
- 第3集-剧情反转:EnyimMemcachedCore 无罪,.NET 5.0 继续背锅
- 第4集:一个.NET,两手准备,一个issue,加倍关注
- 第5集-案情突破:都是我们的错,让 .NET 5.0 背锅
- 第6集-案发现场回顾:故障情况下 Kubernetes 的部署表现
- 第7集-大结局:捉拿真凶 StackExchange.Redis.Extensions 归案
随着第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错误:
- 使用 Lazy 造成无法预热连接池
- 没有使用锁或其他方式避免重复创建连接
- 没有使用 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 归案的更多相关文章
- 《.NET 5.0 背锅案》第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore
在第1集的剧情中,主角是".NET 5.0 正式版 docker 镜像",它有幸入选第1位嫌疑对象,不是因为它的嫌疑最大,而是它的验证方法最简单,只需要再进行一次发布即可.我们在周 ...
- 《.NET 5.0 背锅案》第5集-案情大转弯:都是我们的错,让 .NET 5.0 背锅
第1集:验证 .NET 5.0 正式版 docker 镜像问题 第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore 第3集-剧情反转:EnyimMemcachedCo ...
- 《.NET 5.0 背锅案》第6集-案发现场回顾:故障情况下 Kubernetes 的部署表现
第1集:验证 .NET 5.0 正式版 docker 镜像问题 第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore 第3集-剧情反转:EnyimMemcachedCo ...
- 《.NET 5.0 背锅案》第3集-剧情反转:EnyimMemcachedCore 无罪,.NET 5.0 继续背锅
今天晚上基于第2集中改进版的 EnyimMemcachedCore 进行了发布,发布过程中故障重现,最大的嫌犯 EnyimMemcachedCore 被证明无罪,暂时委屈 .NET 5.0 继续背锅. ...
- 《.NET 5.0 背锅案》第4集:一个.NET,两手准备,一个issue,加倍关注
第1集:验证 .NET 5.0 正式版 docker 镜像问题 第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore 第3集-剧情反转:EnyimMemcachedCo ...
- 《.NET 5.0 背锅案》第1集:验证 .NET 5.0 正式版 docker 镜像问题
今天我们分析了博客站点的2次故障(故障一.故障二),发现一个巧合的地方,.NET 5.0 正式版的 docker 镜像是在11月10日提前发布上线的. 而在11月10日下午4点左右,由于 CI 服务器 ...
- 【故障公告】Memcached 的“惹祸”,不知在为谁背锅
在 .NET 5.0 背锅 . Memcached 的惹祸 .缓存雪崩之后,我们没有找到问题的真正原因,我们知道没有找到根源的故障总是会再次光临的,不是在这周就是在下周,也许就在双11前后. 就在今天 ...
- Redis 5.0.7 讲解,单机、集群模式搭建
Redis 5.0.7 讲解,单机.集群模式搭建 一.Redis 介绍 不管你是从事 Python.Java.Go.PHP.Ruby等等... Redis都应该是一个比较熟悉的中间件.而大部分经常写业 ...
- Memcached 的惹祸,.NET 5.0 的背锅
抱歉,拖到现在才写这篇为 .NET 5.0 洗白的博文(之前的博文),不好意思,又错了,不是洗白,是还 .NET 5.0 的清白. 抱歉,就在今天上午写这篇博客的过程中,由于一个bug被迫在访问高峰发 ...
随机推荐
- docker设置http访问
1 编辑配置文件 vim /etc/docker/daemon.json { "registry-mirrors": ["https://a4fyjv0u.mirr ...
- Nginx常见错误解决办法
报错: nginx: [error] CreateFile() "C:\mytools\nginx-1.8.1/logs/nginx.pid" failed (2: The sys ...
- tp5伪静态--宝塔
location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; break; } }
- Qt导入CMakeLists.txt后无法调试
问题: Qt导入CMakeLists.txt后无法单步调试 解决方法: 在CMakeLists.txt后加入一句: SET(CMAKE_BUILD_TYPE DEBUG)
- 使用ML.NET模型生成器来完成图片性别识别
什么是ML.NET? ML.NET 使你能够在联机或脱机场景中将机器学习添加到 .NET 应用程序中. 借助此功能,可以使用应用程序的可用数据进行自动预测. 机器学习应用程序利用数据中的模式来进行预测 ...
- 如何在 vue 项目里正确地引用 jquery
转载 2016年11月13日 使用vue-cli构建的vue项目,webpack的配置文件是分散在很多地方的,而我们需要修改的是build/webpack.base.conf.js,修改两处的代码 / ...
- B. Once Again... 解析(思維、DP、LIS、矩陣冪)
Codeforce 582 B. Once Again... 解析(思維.DP.LIS.矩陣冪) 今天我們來看看CF582B 題目連結 題目 給你一個長度為\(n\)的數列\(a\),求\(a\)循環 ...
- Java学习的第十九天
1.今天学了接口只能有抽象的常量和方法,接口为interface 承接接口是implements 接口的使用 接口中的方法必须是抽象的,没有构造方法 2.今天没有问题 3.明天学习第六章综合实例 ...
- CF295C Greg and Friends
首先 我们考虑每次船来回运人时都可以看成一种dp状态 又因为人的体重只有50kg和100kg两种, 所以我们可以开一个三维数组dp[i][j][k],第1维表示在出发岸50kg有i个,第2维表示在出发 ...
- iNeuOS工业互联平台,WEB组态(iNeuView)增加工程视图导入、导出功能,及优化和修复,发布:v3.2.1版本
目 录 1. 概述... 2 2. 平台演示... 2 3. 导出组态工程文件... 2 4. 导入组态工程文件... 3 1. 概述 iNe ...