第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.InternalPoolImplAcquireAsync() 方法中发现下面这行代码

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的更多相关文章

  1. 《.NET 5.0 背锅案》第7集-大结局:捉拿真凶 StackExchange.Redis.Extensions 归案

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

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

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

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

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

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

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

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

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

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

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

  7. 通过tarball形式安装HBASE Cluster(CDH5.0.2)——如何配置分布式集群中的zookeeper

    集群安装总览参见这里 Zookeeper的配置 1,/etc/profile中加入zk的路径设置,见上面背景说明. 2,进入~/zk/conf目录,复制zoo_sample.cfg为zoo.cfg v ...

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

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

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

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

随机推荐

  1. C# 中 System.Index 结构体和 Hat 运算符(^)的全新用法

    翻译自 John Demetriou 2019年2月17日 的文章 <C# 8 – Introducing Index Struct And A Brand New Usage For The ...

  2. 空间视频和GIS

    摘要. GIS的空间数据基本单位表示通常是根据 点,线和面.但是,另一种类型的空间数据正在变得越来越频繁 捕获的是视频,但在GIS中却被很大程度上忽略了.数字录像时 是现代社会中常见的一种媒介,包含多 ...

  3. Rust之路(0)

    Rust--一个2012年出现,2015年推出1.0版本的"年轻"语言.在 2016 至 2018 年的 stack overflow 开发人员调查中,被评比为 "最受欢 ...

  4. pytest文档33-Hooks函数获取用例执行结果(pytest_runtest_makereport)

    前言 pytest提供的很多钩子(Hooks)方法方便我们对测试用例框架进行二次开发,可以根据自己的需求进行改造. 先学习下pytest_runtest_makereport这个钩子方法,可以更清晰的 ...

  5. 【矩阵乘优化DP】涂色游戏

    题目大意 用 \(p\) 种颜色填 \(n\times m\) 的画板,要求任意相邻两列的颜色数都不少于 \(q\) ,求方案数. 数据范围 \(1\leq n\leq 100,1\leq m\leq ...

  6. spring boot:用cookie保存i18n信息避免每次请求时传递参数(spring boot 2.3.3)

    一,用cookie保存i18n信息的优点? 当开发一个web项目(非api站)时,如果把i18n的选择信息保存到cookie, 则不需要在每次发送请求时都传递所选择语言的参数, 也不需要增加heade ...

  7. Spring Boot 整合多点套路,少走点弯路~

    持续原创输出,点击上方蓝字关注我 个人原创博客+1,点击前往,查看更多 目录 前言 Spring Boot 版本 找到自动配置类 注意@Conditionalxxx注解 注意EnableConfigu ...

  8. Hadoop框架:DataNode工作机制详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.工作机制 1.基础描述 DataNode上数据块以文件形式存储在磁盘上,包括两个文件,一个是数据本身,一个是数据块元数据包括长度.校验.时 ...

  9. Docker学习笔记之-在虚拟机VM上安装CentOS 7.8

    虚拟机VM版本:VMware Workstation Pro 16 中文虚拟机软件专业版 到官网下载即可,或者也可以通过下边链接下载 下载地址: http://www.epinv.com/post/1 ...

  10. javaSE、javaEE、Android知识点总结

    曾今上学时候的一些学习总结,如有错误请大家指出,共同学习. 1. 什么是WebView? WebView是一个使用WebKit引擎的浏览器组件,用来加载网页. 2. WebView中加载网页的两种方式 ...