去哪儿一站式 Java 应用诊断解决方案 - Bistoury
作者介绍
·聂振宇:2013年加入去哪儿网,一直从事中间件相关的开发。
·谢磊:2018年加入去哪儿网,从事 bistoury、qtrace 等中间件开发,对 java 字节码插桩、问题排查工具、服务可观测性有丰富的实践经验。
一是 arthas 更像是一个工具,而不像一个产品。如果要使用它,首先要登录相关机器,然后在机器上下载 arthas,再执行一些命令来运行。这整个流程里,下载可能出现问题,运行 arthas 也需要具有目标进程相应的权限,还需要先看看对应进程id等等...这些确实只是一些小问题,但也可以选择让这些问题不存在,让整个使用过程更加流畅。 二是 arthas 缺少 web 界面。命令行界面用起来确实很酷,但不可否认在相当一部分情况下 web 界面更直观更友好,很多需要查文档的情况在 web 界面下都可以直接操作,降低了使用门槛。 三是 arthas 所有功能都针对单台机器,实际上很多时候我们需要考虑和观察整个应用的运行情况,需要一个应用级的视角。 四是 arthas 是一个独立的工具。对于一个开源工具,这不是一个缺点,但如果能和公司内部的应用中心、发布系统等做一些适配的话,在使用上会更方便,也能够做出一些独立工具做不到的功能。
用户系统就是待诊断的正在运行的应用。 agent 和待诊断系统部署在同一台机器上,接收来自 proxy 的命令,并根据其类型直接执行一部分命令,还要负责另一部分命令与用户系统之间的交互。 proxy 负责维护与 agent 之间的长期 netty 连接,并以 websocket 的方式维护与 ui 在命令执行期间的连接,它将 ui 传来的命令发送给具体的一个或多个 agent,同时对相应的连接进行管理。 ui 则提供图形化和命令行界面,接收用户请求并发送给 proxy,并将最终结果展示给用户。 注册中心负责 proxy 的注册,ui 通过注册中心获取 proxy 地址。 负载均衡器负责 agent 到 proxy 的负载均衡,agent 在每次启动时通过负载均衡器获取单个 proxy 地址,并与之建立长期连接。 应用中心提供应用和机器的相关信息给 bistoury,用于简化操作,并提供一些特殊功能需要的信息。 其它:bistoury 还会访问公司内代码仓库等系统,但这些都是具体功能所需要,并不在 bistoury 整体的系统设计上。
不过字节码插桩和运行时 instrumentation 在网上有大量文章,这里不再进行具体说明。
下图所示的是 bistoury agent 动态 attach 后,应用内部的 ClassLoader 结构图,其中 BistouryClassLoader 是一个 bistoury 专有的 ClassLoader。
为什么要使用一个专有的 BistouryClassLoader 呢,这是因为 attach jar 中包含的各个 jar 包在用户系统中也可能存在,如果版本不一致很可能会出现问题;bistoury agent 会进行升级,它的功能实现代码、依赖的jar包都可能变化,需要对它们的影响范围做一个限制;用户系统可能非常稳定,甚至一年都没有重启过,而 agent 可能在这一年中升级了几十个版本,每个版本都需要在用户系统里面加载一大堆类,这些 jar 包和类都需要进行卸载。
配合从 agent 加载到 BootstrapClassLoader 中的 instrument jar,每次 agent 升级或卸载时,做完清理工作后将 instrument jar 中的 ClassLoader 引用重置,就可以将整个 BistouryClassLoader 和里面的 attach jar 回收。
首先来说一说这个问题的场景。在 bistoury 的开发过程中,为了满足需求,发现需要对 arthas 和 jackson 的源码的进行少量修改。可以选择的解决方案有自己 fork 一个分支,针对 jackson 也可以选择不使用序列化框架自己写一个。但要修改的代码比较少,笔者不想大动干戈也不想长期维护 fork 分支,只想要简单依赖 jar 包就好,于是就有了 MagicClassLoader 的出现。
MagicClassLoader 作用是 bistoury 可以指定一些类,把这些类委托给MagicClassLoader 加载,MagicClassLoader 会优先加载 Bistoury-magic-classes.jar 中的类文件。这样的话,只需要把需要修改源码的少量几个类放入 Bistoury-magic-classes.jar,就可以达到修改 jar 包中源代码的目的。
曾经在微博上流传着这么一个程序员才懂的笑话,NASA 要发射一个新型火箭,火箭发射升空后发现不行,NASA 把火箭拖回来加了两行 log,再次发射,发现又不行,又加了两行 log 发射,发现又不行...
当然这只是一个笑话,但这样的场景在我们的实际开发中却屡见不鲜,多少次我们解决故障的时间就在不断地加 log,发布,加 log,发布的过程中溜走。
Arthas 的 watch 命令让我们可以观察函数的入参、返回值、异常等等,然而似乎每次 watch 都需要看看文档里参数该如何设置,面对函数中的本地变量也是无能为力,特别是行数较多的方法,方法内部的情况还是难以明了,想象一下面对上百行的方法,你需要脑补出其中各个本地变量值的情形,这个时候,我们需要的是 ide 的 debug 功能。
Bistoury 的在线 debug 功能正是针对这个场景而生,它模拟了 ide 的调试体验,在功能上和远程调试,或者说你在 ide 上 debug 本地代码几乎一致。你在代码某一行打一个断点或条件断点,断点触发就能看到本地变量、成员变量、静态变量以及调用栈;与 idea 远程 debug 不同的是,它不需要在系统启动就带上调试相关参数,对应用完全透明,同时在断点触发时不会暂停整个系统,而是只打印断点处快照信息,打印后继续执行代码逻辑,完美符合我们对在线应用的 debug 需求。
protected ModelAndView detailView(String viewName, String code) {Application app = applicationManager.getAppByCode(code, From.master);applicationManager.checkOwner(app);return createView(viewName).addObject("app", app);}
userSystem.preDo();userSystem.do();userSystem.afterDo();
userSystem.preDo();if (hitBreakPoint()) {captureSnapshot();}userSystem.do;userSystem.afterDo();`
函数的入参、返回值、异常、静态变量等信息我们通过 arthas 也可以获取,bistoury 更进一步的是获取到了本地变量的信息。
这里涉及到两个问题:断点设置在源码处,如何对应字节码里的位置;这个位置有哪些本地变量,它们的名字和值如何获取。
通过查阅 java 虚拟机规范,我们可以发现,java 类字节码里用来表示方法的 method_info 结构有一个 code 属性,code 属性的属性表里有一个叫做 LineNumberTable,这里用 java 代码来近似描述 LineNumberTable 的一部分结构:
class LineNumberTable {LineNumber[] lineNumbers;}class LineNumber {short start_pc; // 方法body字节码数组的索引short line_number; // 源文件的行号}
同样是在 code 属性的属性表中,我们还可以找到一个名为 LocalVariableTable 的属性,还是用 java 代码来对其中一部分结构进行描述:
class LocalVariableTable {Varible varible;}class Varible {short start_pc; // 变量在body字节码数组的起始索引short length; // 变量在body字节码数组存在的长度short name_index; // 变量名的索引short descriptor_index; // 变量类型的索引short index; // 变量在局部变量表的索引}
结合前面的 LineNumberTable 信息,就可以获取断点处有哪些本地变量,达到获取断点处本地变量信息的目的。
可以看到,传统的机器 cpu 使用率监控给出的信息量太少,它能够帮助发现问题,但在解决问题时作用不大。
Bistoury 的线程级 cpu 监控正是为解决各种 cpu 问题而生,让此类问题的解决难以置信的简单。
Linux 线程 id 和 jvm 线程的关联与我们日常运维的操作类似,都是通过 jstack 获取线程号来进行关联,同时在 jstack 中,我们还可以获取到具体的线程栈和锁等各种信息。
最后将每分钟的信息持久化,通过界面将数据展示出来,这也就是 bistoury 的线程级 cpu 监控。
以上就是本次分享的所有内容啦!
最后,给大家带来一些岗位招聘信息。
你与驼厂只差一份简历的距离
快扫码投递吧
点击“阅读原文”,进入项目GitHub地址