解Bug之路-串包Bug

笔者很热衷于解决Bug,同时比较擅长(网络/协议)部分,所以经常被唤去解决一些网络IO方面的Bug。现在就挑一个案例出来,写出分析思路,以飨读者,希望读者在以后的工作中能够少踩点坑。

串包Bug现场

前置故障Redis超时

由于某个系统大量的hget、hset操作将Redis拖垮,通过监控发现Redis的CPU和IO有大量的尖刺,CPU示意图下图所示:



CPU达到了100%,导致很多Redis请求处理不及时,其它业务系统都频繁爆出readTimeOut。此时,紧急将这个做大量hget、hset的系统kill,过了一段时间,Redis的CPU恢复平稳。

一波未平,一波又起

就在我们以为事件平息的时候,线上爆出登录后的用户名称不正确。同时错误日志里面也有大量的Redis返回不正确的报错。尤为奇葩的是,系统获取一个已经存在的key,例如get User123456Name,返回的竟然是redis的成功返回OK。示意图如下:

Jedis.sendCommand:get User123456Name
Jedis.return:OK
or
Jedis.sendCommand:get User123456Name
Jedis.return:user789

我们发现此情况时,联系op将Redis集群的所有Key紧急delete,当时监控示意图:



当重启后,我们再去线上观察的时候,发现错误依然存在,神奇的是,这种错误发生的频率会随着时间的增加而递减。到最后刷个10分钟页面才会出现这种错,示意图如下所示:



既然如此,那只能祭出重启大法,把出错的业务系统全部重启了一遍。

重启之后,线上恢复正常,一切Okay。

Bug复盘

此次Bug是由Redis本身Server负载太高超时引起的。Bug的现象是通过Jedis去取对应的Key值,得不到预期的结果,简而言之包乱了,串包了。

缩小Bug范围

首先:Redis是全球久经考验的系统,这样的串包不应该是Redis的问题。

第二:Redis刷新了key后Bug依然存在,而业务系统重启了之后Okay。

第三:笔者在错误日志中发现一个现象,A系统只可能打印出属于A系统的json串结构(redis存的是json)。

很明显,是业务系统的问题,如果是Redis本身的问题,那么在很大概率上A系统会接收到B系统的json串结构。

业务系统问题定位

业务系统用的是Jedis,这同样也是一个久经考验的库,出现此问题的可能性不大。那么问题肯定是出在运用Jedis的姿势上。

于是笔者找到了下面一段代码:

public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
JedisClient jedisClient = jedisPool.getResource();
try{
return method.invoke(jedisClient,args);
} catch(Exception e){
logger.error("invoke redis error",e);
throw e;
}finally {
if(jedisClient != null){
// 问题处在下面这句
jedisPool.returnResource(jedisClient);
}
}
}

当时我就觉得很奇怪,笔者自己写的,阅读过的连接池的代码都没有将抛异常的连接放回池里。就以Druid为例,如果是网络IO等fatal级别的异常,直接抛弃连接。这里把jedisClient连接返回去感觉就是出问题的关键。

Bug推理

笔者意识到,之所以串包可能是由于jedisClient里面可能有残余的数据,导致读取的时候读取到此数据,从而造成串包的现象。

串包原因

正常情况下的redis交互

先上Jedis源码

public String get(final String key) {
checkIsInMulti();
client.sendCommand(Protocol.Command.GET, key);
return client.getBulkReply();
}

Jedis本身用的是Bio,上述源码的过程示意图如下:

出错的业务系统的redis交互



由于Redis本身在高负载状态,导致没能及时相应command请求,从而导致readTimeOut异常。

复用这个出错链接导致出错

在Redis响应了上一个command后,把数据传到了对应command的socket,进而被inputream给buffer起来。而这个command由于超时失败了。



这样,inputStream里面就有个上个命令留下来的数据。

下一次业务操作在此拿到这个连接的时候,就会出现下面的情况。



再下面的命令get user789Key会拿到get user456Key的结果,依次类推,则出现串包的现象。

串包过程图



上图中相同颜色的矩形对应的数据是一致的。但是由于Redis超时,导致数据串了。

为什么get操作返回OK

上图很明显的解释了为什么一个get操作会返回OK的现象。因为其上一个操作是set操作,它返回的OK被get操作读取到,于是就有了这种现象。

为什么会随着时间的收敛而频率降低

因为在调用Redis出错后,业务系统有一层拦截器会拦截到业务层的出错,同时给这个JedisClient的错误个数+1,当错误个数>3的时候,会将其从池中踢掉。这样这种串了的连接会越来越少,导致Bug原来越难以出现。

在每次调用之前清理下inputstream可行否

不行,因为Redis可能在你清理inputstream后,你下次读取前把数据给传回来。

怎么避免这种现象?

抛出这种IO异常的连接直接给扔掉,不要放到池子里面。

怎么从协议层面避免这种现象

对每次发送的命令都加一个随机的packetId,然后结果返回回来的时候将这个packetId带回来。在客户端每次接收到数据的时候,获取包中的packetId和之前发出的packetId相比较,如下代码所示:

if(oldPacketId != packetIdFromData){
throw new RuntimeException("串包");
}

总结

至少在笔者遇到的场景中,出现IO异常的连接都必须被抛掉废弃,因为你永远不知道在你复用的那一刻,socket或者inputstream的buffer中到底有没有上一次命令遗留的数据。

当然如果刻意的去构造协议,能够通过packetId之类的手段把收发状态重新调整为一致也是可以的,这无疑增加了很高的复杂度。所以废弃连接重建是最简单有效的方法。

公众号

关注笔者公众号,获取更多干货文章:

解Bug之路-串包Bug的更多相关文章

  1. 解Bug之路-TCP粘包Bug

    解Bug之路-TCP粘包Bug - 无毁的湖光-Al的个人空间 - 开源中国 https://my.oschina.net/alchemystar/blog/880659 解Bug之路-TCP粘包Bu ...

  2. 解Bug之路-Nginx 502 Bad Gateway

    解Bug之路-Nginx 502 Bad Gateway 前言 事实证明,读过Linux内核源码确实有很大的好处,尤其在处理问题的时刻.当你看到报错的那一瞬间,就能把现象/原因/以及解决方案一股脑的在 ...

  3. 解Bug之路-记一次对端机器宕机后的tcp行为

    解Bug之路-记一次对端机器宕机后的tcp行为 前言 机器一般过质保之后,就会因为各种各样的问题而宕机.而这一次的宕机,让笔者观察到了平常观察不到的tcp在对端宕机情况下的行为.经过详细跟踪分析原因之 ...

  4. 解Bug之路-记一次线上请求偶尔变慢的排查

    解Bug之路-记一次线上请求偶尔变慢的排查 前言 最近解决了个比较棘手的问题,由于排查过程挺有意思,于是就以此为素材写出了本篇文章. Bug现场 这是一个偶发的性能问题.在每天几百万比交易请求中,平均 ...

  5. 解Bug之路-NAT引发的性能瓶颈

    解Bug之路-NAT引发的性能瓶颈 笔者最近解决了一个非常曲折的问题,从抓包开始一路排查到不同内核版本间的细微差异,最后才完美解释了所有的现象.在这里将整个过程写成博文记录下来,希望能够对读者有所帮助 ...

  6. 解Bug之路-ZooKeeper集群拒绝服务

    解Bug之路-ZooKeeper集群拒绝服务 前言 ZooKeeper作为dubbo的注册中心,可谓是重中之重,线上ZK的任何风吹草动都会牵动心弦.最近笔者就碰到线上ZK Leader宕机后,选主无法 ...

  7. 解Bug之路-记一次中间件导致的慢SQL排查过程

    解Bug之路-记一次中间件导致的慢SQL排查过程 前言 最近发现线上出现一个奇葩的问题,这问题让笔者定位了好长时间,期间排查问题的过程还是挺有意思的,正好博客也好久不更新了,就以此为素材写出了本篇文章 ...

  8. 解Bug之路-记一次存储故障的排查过程

    解Bug之路-记一次存储故障的排查过程 高可用真是一丝细节都不得马虎.平时跑的好好的系统,在相应硬件出现故障时就会引发出潜在的Bug.偏偏这些故障在应用层的表现稀奇古怪,很难让人联想到是硬件出了问题, ...

  9. 解Bug之路-记一次JVM堆外内存泄露Bug的查找

    解Bug之路-记一次JVM堆外内存泄露Bug的查找 前言 JVM的堆外内存泄露的定位一直是个比较棘手的问题.此次的Bug查找从堆内内存的泄露反推出堆外内存,同时对物理内存的使用做了定量的分析,从而实锤 ...

随机推荐

  1. 简单认识Adam优化器

    转载地址 https://www.jianshu.com/p/aebcaf8af76e 基于随机梯度下降(SGD)的优化算法在科研和工程的很多领域里都是极其核心的.很多理论或工程问题都可以转化为对目标 ...

  2. X86汇编——计算斐波那契数列程序(详细注释和流程图说明)

    X86汇编实现斐波那契数列 程序说明: 输入斐波那契数列的项数, 然后依次输出斐波那契数列, 输入的项数小于256且为数字, 计算的项数不能超过2^16次方, 输入失败是 不会回显数字 因为存结果是A ...

  3. 金题大战Vol.0 C、树上的等差数列

    金题大战Vol.0 C.树上的等差数列 题目描述 给定一棵包含\(N\)个节点的无根树,节点编号\(1-N\).其中每个节点都具有一个权值,第\(i\)个节点的权值是\(A_i\). 小\(Hi\)希 ...

  4. 【Azure DevOps系列】什么是Azure DevOps

    DevOps DevOps是一种重视"软件开发人员(Dev)"和"IT运维技术人员(Ops)"之间沟通合作的文化,它促进开发和运营团队之间的协作,以自动化和可重 ...

  5. java基础-04:标识符与关键字

    在Java中,标识符是代表你对程序中某个方法或变量赋予的一个名称,而这个名称不能是关键字 关键字:

  6. 在 Linux 中查找和删除重复文件

    原文链接:https://www.linuxprobe.com/linux-FSlint.html FSlint同时具有GUI和CLI模式.因此,对于新手来说,这是一个用户友好的工具.FSlint不仅 ...

  7. JavaGUI之Swing简单入门示例

    简介 AWT(译:抽象窗口工具包),是Java的平台独立的窗口系统,图形和用户界面器件工具包. Swing 是为了解决 AWT 存在的问题而以 AWT 为基础新开发的包(在使用Swing时也常会用到j ...

  8. Spring编程式注解使用不当导致其他事务无法正常提交

    1.事故背景 原本在使用的是注解式事务,后面因为需要在事务中增加异步推送机制,所以需要将推送机制放到事务之外,修改后发现系统经常出现事务长时间无法提交导致回滚. 2.排查流程 (1)一开始重启应用是能 ...

  9. 一台主机的最大TCP连接数是多少?

    在没接触过这个问题之前,自然会想到服务器端连接数是由服务器端口号限制的.但这其实是一个很严重的误解,要解决这个问题,必须理解socket的连接过程. 以python为例,tcp服务端socket需要经 ...

  10. 阿里巴巴Java开发手册1.4.0

    链接:https://pan.baidu.com/s/16kKzcRcu20SMDdydMm9ZUw 提取码:p9ef