《.NET 5.0 背锅案》第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore
在第1集的剧情中,主角是“.NET 5.0 正式版 docker 镜像”,它有幸入选第1位嫌疑对象,不是因为它的嫌疑最大,而是它的验证方法最简单,只需要再进行一次发布即可。我们在周五晚上(11月13日)进行了发布验证,发布后没有出现故障,docker 镜像的嫌疑指数下降,但这不能100%证明它的清白,因为可能是因为周五晚上的并发量不够触发故障。
在这一集中,主角是 memcached 客户端 EnyimMemcachedCore,它是到目前为止我们发现的最大嫌疑对象,它是我们从 .NET Framework 版的 EnyimMemcached 迁移到 .NET Core 的,修修补补、补补修修了好几年,最大的改动是实现异步化(async/await)。
在 review EnyimMemcachedCore 源代码的过程中,在 Enyim.Caching.Memcached.MemcachedNode.InternalPoolImpl 的 AcquireAsync() 方法中发现下面这行代码:
retval.Reset();
这行代码的作用是从 EnyimMemcachedCore 维护的 socket pool 中拿到空闲 socket 连接之后,对该连接进行重置,就比如你在餐厅等到一个空闲位置后,就坐之前服务员会将餐桌清理干净。
当看到这个方法的方法名之后没有 Async 之后,我们当时头脑中就嗡的一声,怎么会,怎么会,这个地方怎么会没有调用异步方法?在我们对 EnyimMemcached 进行异步化改造时竟然漏掉了这个地方,罪过罪过,这个错值得关禁闭1个月。
继续追查案件,看看 Reset() 方法的实现代码
public void Reset()
{
// discard any buffered data
_inputStream.Flush();
int available = _socket.Available;
if (available > 0)
{
if (_logger.IsEnabled(LogLevel.Warning))
_logger.LogWarning(
"Socket bound to {0} has {1} unread data! This is probably a bug in the code. InstanceID was {2}.",
_socket.RemoteEndPoint, available, this.InstanceId);
byte[] data = new byte[available];
this.Read(data, 0, available);
if (_logger.IsEnabled(LogLevel.Warning))
_logger.LogWarning(Encoding.ASCII.GetString(data));
}
}
从表面上看,这个方法的实现意图简单明了,就是清除 Socket NetworkStream 的内部缓冲数据,如果清除之后,Socket 中还有数据,就读取这些数据并在日志中记录。
F12查看一下 NetworkStream.Flush 注释
// Summary:
// Flushes data from the stream. This method is reserved for future use.
public override void Flush();
不看不知道,一看吓一跳,这个方法竟然是个摆设,什么也没实现,真的是这样吗?github 上查看 .NET 5.0 的源代码确认一下,找到 NetworkStream.cs#L743
// Flushes data from the stream. This is meaningless for us, so it does nothing.
public override void Flush()
{
}
public override Task FlushAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
千真万确,而且这是5年前就已经发生的事实

微软为什么放这样一个摆设?这个摆设会带来副作用吗?在 NetworkStream.Flush 的帮助文档中找到了答案:
The Flush method implements the Stream.Flush method; however, because NetworkStream is not buffered, it has no effect on network streams. Calling the Flush method does not throw an exception.
原来 NetworkSteam 是一个与众不同的 Stream,它没有 buffered 数据,Flush 操作对它本来就没有意义,所以这是一个无伤大雅的摆设。
既然 NetworkStream.Flush 是摆设,那 EnyimMemcachedCore 的 Reset 方法中调用 NetworkStream.Flush 也是摆设,也是无伤大雅,这里是否调用异步方法没有影响,坑不在这里。
继续追查,来看后面读取 _socket.Available 的代码
byte[] data = new byte[available];
this.Read(data, 0, available);
以及对应的 Read 方法实现代码
public void Read(byte[] buffer, int offset, int count)
{
this.CheckDisposed();
int read = 0;
int shouldRead = count;
while (read < count)
{
try
{
int currentRead = _inputStream.Read(buffer, offset, shouldRead);
if (currentRead == count)
break;
if (currentRead < 1)
throw new IOException("The socket seems to be disconnected");
read += currentRead;
offset += currentRead;
shouldRead -= currentRead;
}
catch (Exception ex)
{
if (ex is IOException || ex is SocketException)
{
_isAlive = false;
}
throw;
}
}
}
坑在这里!Read 方法中通过 NetworkStream.Read 同步方法从 socket 读取数据会阻塞线程,如果有很多线程在进行这个操作,会造成线程池中的线程不够用,在高并发场景下足以致命。
我们来设想一下掉进这个大坑里的情形。正常情况下,EnyimMemcachedCore 维护着一个 socket 连接池,处理请求的线程通过连接池中的某个 socket 连接与 memacahed 服务器传输数据,当发生某种异常情况造成客户端很多连接突然断开,很多没有完成数据传输的连接被放回 socket pool,这些连接被后续的线程拿到,由于连接是客户端突发断开的,拿到 socket 连接的线程发现 socket 中还有数据,就在 Reset 方法中读掉这些数据,对于 tcp 连接,如果想重用它,必须这么干(参考NetworkStream doesn't flush data)。在高并发的场景下,当有大量的线程忙于这个,如果是异步方法,需要读取 socket 数据的线程线程会被释放出来,不会带来大的影响,而如果是同步方法,大量线程阻塞在这里等待读取 socket 数据,线程池中的线程就不够用,而对于 web 应用,每1个请求都需要1个线程来处理,线程不够用就会造成请求排队等分配线程,从用户侧看就是打开页面缓慢。
从目前来看,我们遇到的四次故障(一、二、三、四)很可能就掉进了这个大坑,但现在不能确认它就是罪魁祸首,需要进一步验证。
上个周末我们已经实现了异步的 ResetAsync 并发布上线
public async Task ResetAsync()
{
int available = _socket.Available;
if (available > 0)
{
if (_logger.IsEnabled(LogLevel.Warning))
{
_logger.LogWarning(
"Socket bound to {0} has {1} unread data! This is probably a bug in the code. InstanceID was {2}.",
_socket.RemoteEndPoint, available, this.InstanceId);
}
var data = new byte[available];
await ReadAsync(data, 0, available);
}
}
今天晚上8点左右会进行一次发布操作,看是否还会出现故障。
对于这个验证工作,需要至少5次工作日晚上的发布验证。
留下的疑问:这个坑埋在园码中多年,为什么最近才多次掉进去?而且恰恰是在我们将博客系统升级到 .NET 5.0,是什么样的巧合造成让 .NET 5.0 背锅的尴尬?
《.NET 5.0 背锅案》第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore的更多相关文章
- 《.NET 5.0 背锅案》第7集-大结局:捉拿真凶 StackExchange.Redis.Extensions 归案
第1集:验证 .NET 5.0 正式版 docker 镜像问题 第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore 第3集-剧情反转:EnyimMemcachedCo ...
- 《.NET 5.0 背锅案》第4集:一个.NET,两手准备,一个issue,加倍关注
第1集:验证 .NET 5.0 正式版 docker 镜像问题 第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore 第3集-剧情反转:EnyimMemcachedCo ...
- 《.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 背锅案》第1集:验证 .NET 5.0 正式版 docker 镜像问题
今天我们分析了博客站点的2次故障(故障一.故障二),发现一个巧合的地方,.NET 5.0 正式版的 docker 镜像是在11月10日提前发布上线的. 而在11月10日下午4点左右,由于 CI 服务器 ...
- 通过tarball形式安装HBASE Cluster(CDH5.0.2)——如何配置分布式集群中的zookeeper
集群安装总览参见这里 Zookeeper的配置 1,/etc/profile中加入zk的路径设置,见上面背景说明. 2,进入~/zk/conf目录,复制zoo_sample.cfg为zoo.cfg v ...
- 【故障公告】Memcached 的“惹祸”,不知在为谁背锅
在 .NET 5.0 背锅 . Memcached 的惹祸 .缓存雪崩之后,我们没有找到问题的真正原因,我们知道没有找到根源的故障总是会再次光临的,不是在这周就是在下周,也许就在双11前后. 就在今天 ...
- Memcached 的惹祸,.NET 5.0 的背锅
抱歉,拖到现在才写这篇为 .NET 5.0 洗白的博文(之前的博文),不好意思,又错了,不是洗白,是还 .NET 5.0 的清白. 抱歉,就在今天上午写这篇博客的过程中,由于一个bug被迫在访问高峰发 ...
随机推荐
- ubuntu 19.10 中防火墙iptables配置
$sudo which iptables /usr/sbin/iptables说明有安装 如果没有安装,那么使用sudo apt-get install iptables 安装. 刚装机,是这个样 ...
- 用 C 语言游戏编程开发!果然最担心的事又发生了!
30了.我要怎么办,老了.人就像一头小毛驴,方向都是牵着的人定的. 这个项目从去年开始的,一个手机游戏,当时接这个项目的时候其实没有太多考虑,我一向都喜欢打肿脸充胖子的,好面子,人家找上门来,不能不给 ...
- 【贪心算法】HDU 5747 Aaronson
题目大意 vjudge链接 给你一个n,m,求解满足等式x0+2x1+4x2+...+2mxm=n的x0~xm的最小和(xi为非负整数) 数据范围 0≤n,m≤109 思路 n和m都在int范围内,所 ...
- poj2411 Mondriaan's Dream (轮廓线dp、状压dp)
Mondriaan's Dream Time Limit: 3000MS Memory Limit: 65536K Total Submissions: 17203 Accepted: 991 ...
- 浅谈DevOps
DevOps: Development和Operations的组合,是一种软件开发方法,涉及软件在整个开发生命周期中的持续开发,持续测试,持续集成,持续部署和持续监控. 可以把DevOps看作系统开发 ...
- C#/WPF 使用的Task线程程序缺依旧响应缓慢问题
问题:C#/WPF 使用的Task线程程序缺依旧响应缓慢问题 摘要:相信很多做WPF的开发者在请求接口的时候会去采用开一个线程Task去请求接口.避免UI卡主等等.但有一个问题是但开的线程比较多的情况 ...
- origin添加两个Y轴
1. 选中X和两个Y 2. 点击Double Y 3. 关掉gap to Symbol,否则Line+Symbol这种显示方式可能显示不出线条
- Paraview教程
快速入门 https://www.youtube.com/watch?time_continue=1017&v=Y1RATo2swM8 Cyprien Rusu系列 Paraview Vide ...
- 正则表达式小应用之对xml格式字符串每个字段加双引号
通过Python对接口进行自动化后需要把xml格式的报文放到LoadRunner上进行压力测试,在pyCharm控制台打印出报文后,把报文放到LoadRunner中做了格式调整后,每个字段需要添加双引 ...
- 企业级数据大屏设计如何实现,div+html+echarts
大屏是什么? 大屏设计是最近比较流行的概念,一般按照功能来分有几种: 1. 可交互的触摸屏,大多运用在互动教学课程或者报告演示现场,用户可结合交互操作来阐述具体内容.设计师需要对交互形式和传达内容作统 ...