UDN-企业互联网技术人气社区

板块导航

浏览  : 1128
回复  : 0

[讨论交流] 使用JavaAgent修复飞行中的飞机引擎

[复制链接]
呵呵燕的头像 楼主
发表于 2016-11-10 17:19:00 | 显示全部楼层 |阅读模式
  事情是这样的,我司支付网段有3台zookeeper,因为部署年份比较早使用的是老的虚拟机,最近ops要淘汰老的虚拟机,所以要将这几台zookeeper迁移到新的虚拟机上。为了下文描述方便就称之为zk1, zk2, zk3吧。又因为支付网段比较特殊,有一些特殊的网络配置,所以跟ops商量采用了不更换hostname和ip的做法:先将老的虚拟机停掉,然后建新的虚拟机,将老的hostname和ip换到新的上面。

  迁移之前我看了下集群状态,当时zk1和zk3是follower,zk2是leader。follower的迁移一般没什么问题,所以在白天找个时间通知了一下支付的同学就很快搞定了。所以现在zk1, zk3都切到新的虚拟机上了。但是迁移zk2(leader)的时候我犹豫了,因为各种各样的原因之前出现过重启leader导致zkclient无法重连的问题(zkclient本身就特别重,bug也不少)。所以迁移就暂时搁置下来,等到ops将支付网段内所有的虚拟机都切换完成后,又盯住不放了。没办法只有趁着一个夜黑风高的凌晨进行leader切换。我本来切换的步骤是这样的:

  1. 重启zk2(leader),观察leader是不是漂走,并让业务线观察应用是否正常

  2. 停止zk2

  3. 通知ops停止zk2老虚拟机,建新虚拟机

  4. 在新虚拟机上起zk2

  执行操作1之后,观察leader漂向了zk3,业务回复说正常,观察日志也正常。然后ops就将老zk2的虚机改名改ip(但并没有停机,也没有stop zk的实例)。当时想想应该也没啥问题,就在新的虚拟机上执行重建脚本,新的zk2启动了。然后使用srVR命令观察一下各个zk节点的状态,发现zk2一直不正常,好像没起来的样子。于是进入新zk2的日志,发现一直打印如下的日志:

  Have smaller server identifier, so dropping the connection: (3, 2)

  Notification: 1 (message format version), 2 (n.leader), 0x0 (n.zxid), 0x1 (n.round), LOOKING (n.state), 2 (n.sid), 0x0 (n.peerEpoch) LOOKING (my state)

  第一行日志是,zk2连接zk3的时候,发现zk3的sid大于自己的,主动放弃连接。而第二条日志就是zk2给自己投了一票(这就是毛遂自荐了)。

  看样子是新起来的zk2一直处于LOOKING状态,然后发出投票请求,但是一直没获得回应。为了验证这个想法,就在zk1, zk2, zk3上都开了tcpdump。发现zk2确实主动连接了zk1, zk3,并且发送了数据,但是不知道为啥zk1, zk3只有ACK回去,但是没有发数据回去。这个现象说明zk1, zk3用于leader选举的监听端口还是在监听,但是应用层面(zookeeper)已经没有accept。使用ss -l命令看了一下,也验证了这个看法:

  State Recv-Q Send-Q Local Address:Port Peer Address:Port

  LISTEN 50 50 192.168.14.189:30881 *:*

  Rec-Q有大量未处理数据,这些都是位于协议栈里的accept队列里,而应用层并没有调用accept从里取,所以一直堆积在这里。这个时候没有办法了,只有看下leader选举端口监听的地方的代码了(为了节约篇幅就不贴完整的了,完整版可以去github上看):

2.webp.jpg


  就是在一个线程的run方法里,先开启监听(最多重试3次),然后在一个循环里调用accept,没接收一个连接交给receiveConnection处理。看起来也没有什么问题,而且在代码里每接到一个连接还会打印日志,但是我在日志里一直没找到。这个时候唯一的结论就只有这个监听线程由于某种原因退出了。赶快使用jstack观察一下,确实zk1, zk3里都没有这个监听线程了。

  大哥,搞什么飞机啊。这个监听线程可是用来leader选举用的啊,leader就是zk集群的总统,所有协调都要依靠它来做,现在选举机制等于失效了。所以新节点也无法加入。还有一个危险是如果老总统突然病逝,那还选不出新总统出来,最后只有整个集群玩完。

  赶快又扒拉了一下这块代码,发现各个地方异常都处理了,只有调用receiveConnection的时候有这样的情况:



  也就是出现UnresolvedAddressException异常并没有处理,而是直接抛出了。这个异常抛出后因为run里又没有捕获,就成了未处理异常,导致run方法退出,监听线程也就挂了,但是挂之前它并没有关闭监听的Socket,所以我们观察到监听端口还在的情况。这个异常名字就说得很清楚了,无法解析出给定的地址。当我们切换虚拟机的时候,ops将老的hostname和ip注销,因为要加在新的上面。这就导致了这个异常抛出。

  好吧,问题找到了。现在选举线程没了,新节点无法加入,这个集群只有两个不稳定节点了。只要有风吹草动肯定就玩完,想着后面挂着的那么多关键服务背心就冒冷汗。想来想去,也只有重启整个zk集群了,想想又要凌晨操作,现在年纪大了,凌晨起来一次基本上就别想再睡了。那么作为一个自认为还算靠谱的开发人员还有没有其他办法呢。

  答案是肯定的,可能有的同学已经看了下QTracer字节码插桩部分的代码,其实现原理是使用java agent的机制,将一个agent加载到目标进程,然后拿到Instrumention API,使用这个API就可以在运行时对字节码进行修改,以达到插桩的目的。那么既然我们可以向目标进程注入代码,那是不是说明我将有机会将选举的监听线程重启呢?答案是不一定。

  因为你虽然可以注入代码,或许可以自己起一个监听端口,但是如何将监听到的数据交给目标进程里呢?其实就是如何将接收到的连接传递给上面图1代码中的receiveConnection呢?那么肯定要拿到目标进程对应的实例。所以现在的方向就是如何在我注入的代码里访问到监听线程这个实例了。

  虽然平时我痛恨静态全局的东西,痛恨单例模式,但是这个时候我好想好想zookeeper里谁给弄一个单例模式什么的,这样我就可以找到入口进入啊。

  功夫不负有心人(我呸),还真被我找到了。Zookeeper里大量使用了jmx,将一些组件通过MBean的方式暴露出去,这样方便观察和监控(虽然并没有什么卵用)。在这里我发现他们将一些我需要的实例注册进去了,而这里使用了单例:

  MBeanRegistry.getInstance()

  而这其中就注册了这么一个QuorumBean,通过七拐八扭终于达到了目标实例。然后我就在注入的代码里new了一个Thread,将监听线程的run传入进去(监听线程已经退出了,是叫不起来的,只有这样了)。

  编译打包上传执行,终于看到起来的监听线程了,然后小心翼翼的启动zk2,ok,新节点加入了。集群恢复正常,不用再重启整个集群了。

  我已经将agent代码放到github上,点击原文链接可以看到。至于java agent的原理什么的,网上搜一下有很多文章,在这里就不做详细介绍了。

原文作者:余昭辉  来源:开发者头条
1.webp.jpg

相关帖子

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部