《.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被迫在访问高峰发 ...
随机推荐
- xpath教程-通过ID和Class检索 转
通过ID和Class检索 必备知识点 在html中,id是唯一的 在html中,class是可以多处引用的 工具 Python3版本 lxml库[优点是解析快] HTML代码块[从网络中获取或者自 ...
- mongodb安装及使用
安装命令: sudo apt-get install mongodb 开始认证,创建用户: 编辑配置文件: sudo vim /etc/mongodb.conf 11行中的 bind_ip值 修改成为 ...
- Ambari仓库安装教程
Ambari仓库安装教程 如果用户需要后续使用Ambari server进行安装Hadoop则必须搭建一个内部的yum源,否则直接下载速度将会很慢,当然该服务仅要搭建一个即可,可以一直使用. 一.Ce ...
- mac 搭建 Robot Framework
前提介绍,我的mac上python2和python3是都要有的,然后大家可以看看我其他的文章,这些文章虽然很多都是连接,是别人的博客或者资料,但都是自己试过没有问题的,只是比较懒然后就没有自己写. r ...
- linux mount 挂载提示 mount: you must specify the filesystem type
解决方法: mkfs.ext3 /dev/vdv mount -t ext3 /dev/vdv /usr1
- 1. HttpRunner介绍及环境准备
介绍 HttpRunner 是一款面向 HTTP(S) 协议的通用测试框架 只需编写维护一份 YAML/JSON脚本,即可实现自动化测试.性能测试.线上监控.持续集成等多种测试需求 官方文档: htt ...
- 安装Mysql,开发权限,以及复制数据库
官网下载 https://downloads.mysql.com/archives/community/ 解压后安装,管理员身份打开cmd,转到mysql的bin目录,mysqld --ins ...
- Java集合(类)框架(二)
1.Set集合 1.1 HashSet集合 HashSet底层为哈希码 不是数组,因此没有下标的概念,也就不能根据下标来查询某个元素 存放元素无序,不可重复 1.1.1 声明 Set<Strin ...
- python socket 二进制
问题 在工作中经常会用到socket传输数据,例如客户端给服务器发送数据(双方约定了数据格式),在交测之前,自己用python写个接受数据的server,解析下拼成的数据格式是否正确.用python写 ...
- nodejs中连接mongodb数据库
const mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/blog', { useNewUrlParser ...