本文导读:
[1] 疫情当前
[2] 应用异常监控
[3] Redis客户端异常分析
[4] Redis客户端问题引导分析
[5] 站在Redis客户端视角分析
[6] 站在Redis服务端视角分析
[7] 资源池生产配置合理性分析
[8] 本文总结
[1] 疫情当前
为响应国家抗击疫情的号召,全国有过亿的企业职员选择了远程办公,IT科技大厂们也纷纷开启了VPN模式,保障企业运营。
既然这样,我们该怎么做呢?苦逼的程序猿们只能加油干!来张图看看老板们担心的是什么?
不好意思,大BOSS们首先担心的可不是员工身心健康,而是工作效率的提升哦~
但是,据业内人士预估,新冠肺炎疫情很有可能激发国内企业信息化建设提速。
对于企业而言,值得期待的是,『远程办公』让企业看到办公形式的更多可能性,有助于企业未来办公形式的新尝试。
更为重要的是,企业可以此为契机,提升企业自身信息化建设,增强团队凝聚力与协同性,在『危机』中平稳运行,甚至是发现机会。
笔者也不例外,本周已依照公司要求,开启了远程办公模式,本周的感受来说,工作效率上肯定会收到一些影响。但因我们一季度目标明确,所以每天可以按部就班按计划如期进行。
而且,也因为疫情的影响,七大姑八大姨都被憋在家里了,本公司某端的服务 DAU 最近一段时间逆袭不断上涨,付费会员收入也随之增长不少,对于我们来说算是个好消息。
在家远程办公,不给国家添乱就好哇🤩!
接下来,我们继续聊聊线上环境遇到的一个问题以及分析过程。
[2] 应用异常监控
这不,项目中有一个Redis客户端的异常在疫情期间,出现在了你的面前,虽然该异常是偶发,有必要仔细分析下该异常出现的原由。
具体异常信息如下所示:
大家看截图展示的异常信息,是不是很想问,这个异常显示怎么这么「友好」?
没错,是通过一款非常好用的实时异常监控工具:Sentry来监控到的,这款工具在我们的项目中已经接入并使用了很长一段时间了,对异常的监控非常到位。
比如针对发生的异常,将具体访问的整个URL、客户端上报的信息、设备型号等信息作为TAGS收集上来,尽情的展示给你,让你尽快结合这些信息快速定位问题。
该服务部署在k8s容器环境下,在截图中TAGS中,也能够看到 server_name 代表的是Pod的hostname,这样便能快速知道是哪个Pod出现的问题,进入容器平台直接进入到Pod内部进一步详细分析。
强烈推荐大家项目中接入Sentry
,因为它不但有很好用的异常治理平台,更为重要的是Sentry支持跨语言客户端,比如支持Java、Andriod、C 、Python、Go等大部分语言,现成的客户端易于接入和使用。
我想只要你的服务不卡死,如果出现问题,项目里输出的日志中总会有一些 ERROR 级别的日志出现的,那么此时就交给Sentry,它会及时向你发出告警(邮件…)通知你。
[3] Redis客户端异常分析
本项目中使用的Jedis(Redis的Java客户端),提示异常信息 JedisConnectionException Unexpected end of stream
,在使用Redis过程中我还很少遇到这个问题,既然遇到了,这是不是缘分啊 😃
其实异常栈中已经给出了详细的调用过程,在哪里出现的问题,顺藤摸瓜根据这个堆栈去查找线索。
如何找到更为详细的堆栈?别担心,在上图中点击下 raw
会出现完整的异常堆栈的文本信息,也方便复制拷贝出来分析。
如下所示:
redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.
at redis.clients.util.RedisInputStream.ensureFill(RedisInputStream.java:199)
at redis.clients.util.RedisInputStream.readByte(RedisInputStream.java:40)
at redis.clients.jedis.Protocol.process(Protocol.java:151)
at redis.clients.jedis.Protocol.read(Protocol.java:215)
at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:340)
at redis.clients.jedis.Connection.getStatusCodeReply(Connection.java:239)
at redis.clients.jedis.BinaryJedis.auth(BinaryJedis.java:2139)
at redis.clients.jedis.JedisFactory.makeObject(JedisFactory.java:108)
at org.apache.commons.pool2.impl.GenericObjectPool.create(GenericObjectPool.java:888)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:432)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:361)
...
根据以上信息,发现是调用到 BinaryJedis.auth
验证Redis密码时出错的,而且有 GenericObjectPool.borrowObject
表示借用对象的方法,GenericObjectPool是Apache开源项目的线程池,在很多开源项目中都能看到它的身影。
说明是在伸手向资源池索要对象时,在资源池里没有拿到对象,那就只能创建一个,调用了 GenericObjectPool.create
,调用具体实现方法 JedisFactory.makeObject
创建Jedis对象时出错的。
哦?这么一看,简单一想猜测下,在创建新的对象时验证密码时,可能因网络不稳定,Redis-Server没有正常返回异常信息导致的。
[4] Redis客户端问题引导分析
在上文中,我们在异常堆栈中发现使用了线程池,如果不使用资源池管理这些对象,会发生什么情况?
如下所示,每次使用Redis连接都会在客户端重新创建Jedis对象,创建Jedis对象后,连接Redis Server,这个过程会建立TCP连接(三次握手),完成操作后,断开TCP连接(四次挥手),当遇到并发量稍大的请求,就会吃不消了,消耗资源的同时,无法满足应用性能上的要求。
如果使用了线程池,如下图所示的样子:
按需在资源池中初始化一定数量的对象,当有客户端请求到达时,从资源池里获取对象,对象使用完成,再将对象丢回到资源池里,给其他客户端使用。
这就是所谓的 「池化技术」,相信在你的项目中一定会用到的,比如数据库连接池、应用服务器的线程池等等。
池化技术的优势就是能够复用池中的对象,比如上述图示中,避免了分配内存和创建堆中对象的开销;避免了因对象重复创建,进而能避免了TCP连接的建立和断开的资源开销;避免了释放内存和销毁堆中对象的开销,进而减少垃圾收集器的负担;避免内存抖动,不必重复初始化对象状态。
当然,我们也可以自己来实现,但是如果想写出比较完善的对象池的资源管理功能,也需要花费不少的精力,考虑的细节也是非常多的。
站在巨人的肩膀上,在前文中提到的Jedis内部是由 Apache Common Pool2 开源工具包来实现的,很多开源项目中应用也是很广泛的。
而Jedis客户端的很多参数都是来源于Apache Common Pool2的底层实现过程所需要的参数。
这也是Jedis或者说一些Redis客户端给用户使用简单的原因,但是简单的同时,我们也要根据不同场景去合理配置好连接池的参数,不合理的配置加上不合理的功能使用,可能会引起很多的问题。
在回归到前文的最开始的异常,这些异常跟什么有关系呢?
从图示中,我们能知道客户端使用了线程池,可能跟线程池有关系;创建对象时,auth 验证密码时出现了问题,而验证密码前已经发起了 connect 连接了,说明连接到了Redis Server,所以 Redis Server 也脱离不了干系的。
跟 Redis Client 有关系,我们就要进一步分析客户端的参数,连接池的参数是否合理。
跟 Redis Server 有关系,就要结合问题分析下服务端的参数,相关配置参数是否合理。
[5] 站在Redis客户端视角分析
既然讲到了Redis客户端,首先想到的是从客户端配置的参数入手。
直接从参数入手,不如我们可以先接着对异常栈的分析,从对象资源池入手去分析,看看这个对象池到底是怎样管理的?
1、资源池对象管理