《.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被迫在访问高峰发 ...
随机推荐
- 扫描仪扫描文件处理-Photoshop批处理无响应问题
问题描述:Photoshop批处理时候卡死.卡住.无响应问题(出现在处理60M及以上TIFF文件的时候) 解决办法: 调整系统虚拟内存见<扫描-Photoshop批处理内存不足问题解决> ...
- 【9】进大厂必须掌握的面试题-DevOps面试
Q1.DevOps和Agile之间的根本区别是什么? 下表中列出了两者之间的差异. 特征 DevOps--开发运维 Agile--敏捷 敏捷 开发和运营中的敏捷性 只有发展才能敏捷 流程/实践 涉及C ...
- spring boot: 用redis的消息订阅功能更新应用内的caffeine本地缓存(spring boot 2.3.2)
一,为什么要更新caffeine缓存? 1,caffeine缓存的优点和缺点 生产环境中,caffeine缓存是我们在应用中使用的本地缓存, 它的优势在于存在于应用内,访问速度最快,通常都不到1ms就 ...
- centos8平台redis5的主从同步搭建及sentinel哨兵配置
一,规划三台redis的ip:一主二从 redismaster01: 172.18.1.1 主 redisslave01: 172.18.1.2 从 redisslave02: 172.18.1.3 ...
- DateDiff() 方法语法 T-SQL语法
表达式DateDiff(timeinterval,date1,date2 [, firstdayofweek [, firstweekofyear]]) 允许数据类型: timeinterval 表示 ...
- 第三十六章 Linux常用性能检测的指令
作为一个Linux运维人员,介绍下常用的性能检测指令! 一.uptime 命令返回的信息: 19:08:17 //系统当前时间 up 127 days, 3:00 ...
- MySQL死锁系列-线上死锁问题排查思路
前言 MySQL 死锁异常是我们经常会遇到的线上异常类别,一旦线上业务日间复杂,各种业务操作之间往往会产生锁冲突,有些会导致死锁异常.这种死锁异常一般要在特定时间特定数据和特定业务操作才会复现,并且分 ...
- Qlik Sense学习笔记之Mashup开发(一)
date: 2018-12-21 12:33:29 updated: 2018-12-21 12:33:29 Qlik Sense学习笔记之Mashup开发(一) 1.基于Qlik Sense API ...
- MapReduce工作原理详解
文章概览: 1.MapReduce简介 2.MapReduce有哪些角色?各自的作用是什么? 3.MapReduce程序执行流程 4.MapReduce工作原理 5.MapReduce中Shuffle ...
- 理解import声明 与 export声明
import的两种使用方法 import "mod"; // 引入一个模块 import v from "mod"; // 把模块默认的导出值放入变量 v im ...