聂振宇,2013年加入去哪儿网技术团队,目前在平台事业部/基础架构部,对并发编程,构建高并发系统很感兴趣。

引子

一直以来,调试都是在线应用的痛点。 曾经在微博上流传着这么一个程序员才懂的笑话,NASA 要发射一个新型火箭,火箭发射升空后发现不行,NASA 把火箭拖回来加了两行 log,再次发射,发现又不行,又加了两行 log 发射,发现又不行... 当然这只是一个笑话,但这样的场景在我们的实际开发中却屡见不鲜,多少次我们解决故障的时间就在不断地加 log,发布,加 log,发布的过程中溜走。

背景

几年前,为了解决大家调试应用的痛点,我们实现了在线 Debug 的功能取名叫云 Debug,提供一个 Web 界面在代码打上断点,触发时将断点处的应用快照(包括静态变量、成员变量、本地变量、调用栈)展示给用户。当时大家都感觉这是个非常牛逼,振奋人心的功能... 几年过去了,云 Debug 几乎无人问津... 面对用户用脚投票的结果,我们反思了自己做的不好的地方: 要使用云 Debug,用户系统需要依赖新版本的公共组件;需要在配置中心添加一个配置文件并重启系统;修复 bug 得升级依赖重新发布;需要在日志格式里添加特殊 pattern;结果展示时效依赖于内部 Qtrace 系统;触发断点需要在调用入口加上特殊参数(比如 http 请求加上一个参数)... 种种不足让我们自认为牛逼的功能变得无人问津,所以现在我们发布了新的在线 Debug,解决了上述的所有问题。不管老系统还是新系统,不需要任何依赖,不需要重启应用,即插即用,自动升级,对应用完全透明,给大家不一样的 Debug 体验。

介绍

先来简单介绍一下在线 Debug 的整体结构。 在线 Debug 是 Qunar 生产问题诊断工具 Bistoury 的一个功能,Bistoury 结构图如下:

其中 UI 负责接收用户的请求并转发给用户系统对应的 Proxy。Proxy 负责接收 UI 的请求并对命令做一定的修改,比如去应用中心获取用户系统的 pid 等信息并加入到命令参数(简化操作并优化用户体验,实际上这些信息可以通过用户提供参数来传递),然后将最终命令发送给相应的 Agent。 Agent 运行在用户机器上,负责与用户系统通信并初始化 Debug 模块。它通过 NG 获取一台 Proxy 地址并使用 netty 连接 Proxy。每一条命令的传递流向为 UI->Proxy->Agent-> 用户系统,结果以相反的方向传递,最后在 UI 上进行展示。

原理

Java 字节码里面包含了源文件的代码行号信息,可以通过断点行号找到对应字节码的位置,然后使用 Java asm 框架修改相应位置字节码,插入断点触发和获取快照的逻辑,达到 Debug 的效果。 下面两段代码表现了打断点前后程序代码的差异。

  1. userSystem.preDo();

  2. userSystem.do();

  3. userSystem.afterDo();

  1. userSystem.preDo();

  2. if (hasBreakpoint()) {

  3.    captureSnapshot();

  4. }

  5. userSystem.do();

  6. userSystem.afterDo();

运行时Instrument

刚刚说明了在线 Debug 的原理,但用户系统是如何接受修改字节码的请求并执行,又是如何返回结果的呢?这里用到了 Java 运行时 Instrument 的技术。 JDK1.6 版本以后,JDK 的 Instrument 包支持在运行时,也就是程序启动后加载一个外部 Agent jar 包的能力,这让我们实现无侵入式的在线 Debug 成为了可能。下面两幅图说明了 Instrument 前后的系统变化:

Agent 系统刚启动的时候,它与用户系统的唯一关系仅仅是启动在同一台机器上。 首先它会调用 VirtualMachine.attach(pid) 方法,获取用户系统的 jvm 对象; 然后调用 jvm 对象 loadAgent 方法,在用户 jvm 里加载 Instrument jar 包;Instrument jar里面指定类的 Agentmain 方法被调用,获取用来操作字节码的 Instrumentation 对象并加载 attach jar 包(attach jar 包是一些 jar 包的集合),并且调用 attach jar 的初始化;attach jar 进行在线 Debug 的初始化操作并创建一个 netty server,监听 Agent 发来的请求以及返回响应。

BistouryClassLoader

装载 attach jar 的时候,我们会碰到几个问题:一个是 attach jar 里面有很多 jar 包(比如 guava),这些 jar 包并不一定和用户系统所使用的版本一致,不一致的 jar 包版本可能有版本问题;一个是 Agent 会进行升级,Agent 使用和依赖的 jar 包可能也会变化;还有一个问题是用户系统可能非常稳定,甚至一年都没有重启过,而我们的Agent 可能在这一年中升级了几十个版本,每个版本都需要在用户系统里面加载一大堆类,已加载类的回收条件很严格,这些不再需要的类又该如何处理。 为了解决这些问题,我们将 attach jar 所使用的 Classloader 和应用 Classloader 以及 Instrument jar 所在的分离开来,使用一个专用的 BistouryClassLoader 来加载 attach jar。 在 BistouryClassLoader 里,我们重写了 loadClass 方法,对于所有非 jdk 系统类,与系统默认 ClassLoader 的实现方式相反,我们优先使用 BistouryClassLoader 自己而不是父 ClassLoader 加载。 BistouryClassLoader 的引用被保留在 Instrument jar 包里面,在需要时与它加载的类一起被回收;而 Instrument jar 作为一个非常轻的无依赖的 jar 包残留于用户系统中,由于它需要在 Debug 时被各个类所使用,我们使用 BootstrapClassLoader 来加载它。

好了,现在我们解决了动态 attach jar 回收与升级的问题。 不过这里还有一个需要注意的点,使用 BistouryClassLoader 来加载 attach jar 需要解决一个问题,就是前面所说的修改程序代码,比如 hasBreakpoint 函数,这个函数被 BistouryClassLoader 加载,调用点却是在用户系统 ClassLoader。 在 Instrument jar 里我们定义了一个 BistourySpys 类,里面持有了一些 Method 对象,像 hasBreakpoint 这些方法都被这个 spy 对象持有。 那么在程序代码里面,它调用的是 BistourySpys.hasBreakpoint 方法,由 BistourySpys 去调用真正的 hasBreakpoint 方法。

序列化问题

在线 Debug 不能影响程序的正常运行,那么我们需要在 CaptureSnapshot 的时候把程序当时的快照 dump 出来,这就涉及到对象的序列化问题。 这里的序列化有几个特点: 一是任何类都可能被序列化,序列化时我们不清楚会在程序中序列化什么类型的对象; 二是我们没有反序列化所需要的类型,但我们也不需要反序列化成对象,展示清晰易懂即可; 三是不能调用诸如对象的 getter 方法,有些对象的 getter 方法具有副作用,会影响应用运行; 四是必须要处理循环引用的情况。 基于以上特点,决定将对象序列化为 json 数据展示给用户。 但 json 序列化同样有一定的问题,被广泛使用的几个 json 库中,fastjson 会调用 getter 方法,jackson 和 gson 需要在对象上添加注解才会检测循环引用,避免栈溢出。 对于这个问题,我们采用修改 jackson 源码的方式,在 attach jar 中打包一个修改了源码的类,使用这一个类来覆盖 jackson 中的同名类。 需要注意的是,这和我们平常在程序中写一个类直接就覆盖掉依赖 jar 包中同名类情况不一样,attach jar 中各个 jar 包是平等的,需要有一个机制确保类加载的优先级。

MagicClassLoader

在这里,我们再一次编写了一个特殊的 ClassLoader,取名为 MagicClassLoader,用来解决同名类加载优先级的问题。 MagciClassLoader 重写了 LoadClass 方法,它只负责加载 Bistoury-magic-classes.jar 里面的类。加载其它类时采用 Java ClassLoader 默认的实现,委托给它的父 classloader BistouryClassLoader(我们重写的类同样会依赖很多其它的类,遇到其它类时会使用加载当前 Class 的 ClassLoader 也就是 MagicClassLoader进行加载)。 这样实现的结果是,BistouryClassLoader 在加载 Bistoury-magic-classes.jar 中类时,会委托给 MagicClassLoader 加载,形成两个 ClassLoader 之间的双向委托。 这种设计还带来了一个额外的好处,那就是当我们发现使用的某些 jar 包有重要 bug 而没有修复时,不用再等待维护者,自己可以提前动手进行修复。

结束语

本文介绍了 Qunar 无侵入式在线 Debug 的设计与实现,欢迎 Qunar 的各位同事使用,也欢迎各位指正和拍砖。

Qunar黑科技-无侵入在线Debug的实现相关推荐

  1. cad求和插件_黑科技 | 无BIM建模下平面CAD自动生成门窗表

    如果你接到的施工图既不是用天正出的,也不是用revit出的,还得统计门窗表,那么你需要读完这篇文章. 为了能够让自己和所有底层同行们从这项无脑又烧脑的机械劳动中解脱,C君近期利用茶余饭后的时间开发了一 ...

  2. 唯一一种能走路的:“黑科技”。,曝诈网

    所谓黑科技的含义, 就是现实中看起来不可能的存在的科技.(曝诈网) 黑科技无科学依据,但很厉害又挂着科技名义,用起来和魔法一样的东西. 它像玩游戏的外挂一样,它像科幻片一样丰富,它和谍战片一样精彩.对 ...

  3. 用python让excel飞起来 pdf_讯飞智能键盘K710 一款无网络实力依然在线的黑科技产品...

    科大讯飞一直在人工智能领域创新有佳,产品不断.前一阵子了解科大讯飞发布一款新的智能硬件--讯飞智能键盘K710,居然是键盘和智能语音的神仙组合,究竟是怎样的黑科技,我迫不及待入手看看.下面,就看看这款 ...

  4. 可穿戴在线展首日巡礼:剖析产业痛点 直击黑科技新品

    OFweek可穿戴设备网讯 7月20日,"2016中国可穿戴在线展会"正式拉开帷幕,吸引了欧司朗.赛微微电子.大联大控股.汇顶科技.敦泰.日图科技等产业链上游企业和埃微.握奇.欧德 ...

  5. android beam苹果,安卓多年黑科技 苹果终于蹒跚追上_苹果 iPhone X _手机评测-中关村在线...

    安卓多年黑科技 苹果终于蹒跚追上 用"Android系统多年的黑科技,iOS现在终于用上了"这一句话来形容iOS 11.3支持刷公交卡这个行为一点也不为过.是的,早在三四年前多数A ...

  6. 缺氧游戏黑科技计算机,《缺氧》不用bug黑科技debug长期生存技巧详解

    原标题:<缺氧>不用bug黑科技debug长期生存技巧详解 缺氧游戏中很多玩家喜欢用各种bug及黑科技,当然,也有玩家不用照常可以活很久,今天给大家带来的是"黑_永兰一" ...

  7. 一个神奇的测试_这4个在线黑科技工具拥有神奇的魔法,值得收藏!

    本期神器妹分享4个超实用在线黑科技工具,其典型特点就是无需安装任何软件,打开网址就可以使用,另外就是个个都有其独到之处,用起来也很爽.下面来详细介绍这4个工具:2.万能命令 这是一个神奇的在线工具效率 ...

  8. 无人车飞速狂飙,黑科技如何为其加油打气?

    科技快速发展的年代,我们越来越不能轻易地"预测未来",因为总有那么多意料之外在等着我们. 几年前,我们还认为无人车仅是作为科幻电影里的炫酷桥段,然而现在,公园.封闭园区.大学校园等 ...

  9. 小米台灯底座接口很松_小米黑科技,AirPods和小米10 Pro伴侣,ZMI无线充蓝牙音箱体验...

    这年头,没有无线充都不能算旗舰机吧?说的不是OPPO Find X2 Pro哈.小米雷军曾经周末做过一个小调查:高端旗舰是否需要高速无线充电?11万人投票,8.5万人认为应该有,用不用另说. 不止手机 ...

  10. AI 降噪、多平台支持,在线课程背后的黑科技大公开!

    作者 | 伍杏玲 出品 | CSDN(ID:CSDNnews) 互联网让知识变得触手可及,当程序员想系统学习某一项技术时,相信很多人会采用便捷的在线听课.在线教育不再受限于地域和时间,打开手机便能随时 ...

最新文章

  1. android系统的测试方法,运行测试  |  Android 开源项目  |  Android Open Source Project...
  2. 寻找数组变化:树形结构,分治模型
  3. nodejs部署神器pm2的使用体验
  4. 计算机辅助教育会议,子会议1:学习科学、计算机辅助合作学习、智慧教育
  5. php 实现类,php 获取页面中指定内容的实现类
  6. jvm线程分析命令_JVM:如何分析线程转储
  7. C++:听说C++很难学?该怎么学习C++?
  8. Xcode的编译/运行结果保存的路径
  9. android nexus 6尺寸,谷歌Nexus 6详细配置曝光 原生Android大杀器
  10. 3.1、如何通过ISP(FlyMcu串口)下载程序(附CH340驱动及FlyMcu安装包)
  11. mix2线刷开发板救砖_小米MIX2线刷刷机教程_小米MIX2第三方rom包_线刷救砖教程
  12. HTML 和 CSS 重构网页 (Steam主页)
  13. wchar_t和char,WCHAR和CHAR的区别和互相转化
  14. Android的屏幕多样性支持
  15. c语言单片机温度调节系统设计,基于单片机的温度控制系统的设计
  16. 东北大学acm暑期夏令营第七天
  17. centos7.2安装五笔输入法的方法
  18. 如何在 IDEA 中创建并部署 JavaWeb 程序
  19. java改变鼠标图片_Java 将鼠标改为图片的两种方法
  20. Nwafu-OJ-1431 Problem b C语言实习题五——6.用指针实现子字符串提取

热门文章

  1. Android中UI线程与后台线程交互设计的5种方法
  2. 第六章 函数逼近-强化学习理论学习与代码实现(强化学习导论第二版)
  3. MFC字符串CString分割函数 简洁 C++
  4. day3--numpy
  5. windows下Dos命令行设置代理
  6. ios 判断手机角度_iPhone那么贵,为什么电池还那么小呢?安卓手机电池都那么大了!...
  7. java课时,java学习笔记_课时一
  8. 数仓系列 | Flink 窗口的应用与实现
  9. 跳槽到新公司,我直接让项目的性能提升了一半。。。
  10. VLC框架总结(二)VLC源码及各modules功能介绍