作者简介
宋通,携程框架研发资深工程师,参与过分布式消息系统等多个中间件及框架产品的设计与研发,对分布式系统设计及程序性能优化有持续的兴趣。
一般情况下,在携程我们是不建议研发同学直接从办公网络访问生产环境服务器的。这样做,除了安全方面的原因外,更重要的就是要维护生产环境机器运行环境的统一性。但这样也给故障排除增加了一些复杂性,比如在排障过程中可能会遇到以下场景:
1. 明明我的 pom 里写的依赖某中间件版本是 A,本地运行也没问题,为啥到生产环境跑起来就感觉像依赖了版本 B?
2. 程序报了连接数不够的异常,生产环境想看看系统参数,还要联系网站运营同学帮忙,要么还要东奔西走申请各种服务器权限?
为了解决这个问题,同时为了能更快地进行故障排除,我们研发了一款中间件 —— VI。
VI 的全称是 Validate Internal,直译过来是“内部验证”。看到这个名称就会有同学问了,内部是哪里的内部?验证是要验证什么?
简单来讲,“内部”是指应用程序的内部,包括应用程序所处的环境、用到的框架等静态依赖,以及CPU、内存使用情况等运行时状态;而“验证”则是指对程序的静态依赖、运行时状态进行实时监控,以辅助应用 Owner 来验证当前应用状态是否符合预期。
因此, 对应用程序而言,VI 可以实时采集程序及容器状态,并通过友好的可视化界面进行展示。
这段描述会让不少同学想到比较常见的 Metrics 中间件。有很多 Metrics 中间件可以以 Json 等相对友好的形式,将用户自定义的一些 Metrics 数据暴露出来;除此之外还有 Java 自带的 JMX(Java Management Extensions),也可以通过注册自定义的 MBean 来获取各种程序内部信息。那么,VI 相对于这些 Metrics 中间件的优势在哪里呢?
1. 强大的数据自定义展示功能 : VI 提供了一套默认的数据展示模板,用户只需以简单的 api 调用将待展示的数据交给 VI,就可以在本机的 VI 界面上看到友好的展示和交互:
2. 完备的容器信息采集 :就我们常见的运行环境而言,业务代码的运行,自上而下会用到很多容器:Spring IOC,Tomcat,JVM,Docker等。常见的 Metrics 中间件多数只提供了基本的 Metrics 框架,很少包含具体的数据采集模块。
但是,这些容器的状态往往会对业务逻辑的运行产生很大的影响。每个用户都增加完整的数据采集模块没有必要,但排障时再想添加又已经晚了,而且普通用户也并没有权限登录生产环境去查看这些数据。对于这类情况,VI 可以以更加友好的方式,提供非常完备的支持:
3. 一站式自助排障体验 :对同一个应用而言,不同团队的关注点是不同的。产品经理关注用户体验,业务开发关注业务逻辑,框架关注稳定性和性能,运维关注容器各项指标。所以,一旦有用户反馈程序有问题,那么我们的排障之路就会很漫长。
基于友好的交互界面和完备的基本信息采集,VI 在一定程度上有助于缓解这个问题。VI 可以帮助你 从应用的角度而不是从“理论上”的角度 来看应用程序运行时真实感受到的环境、框架、容器的方方面面,而且不需要基础团队的支持、不需要申请各种权限,也不需要为了查看不同的数据在各种工具间转来转去,显著地节约了问题定位耗时,减少因排障速度而增加的业务损失。
4. 发布系统支持 :完善的发布流程生命周期控制,可以很大程度上减少因发布而引发的生产故障。在这一点上,VI 通过自定义点火/健康检测组件,为发布流程的生命周期控制提供了必要的支持。
除了可以自定义业务点火逻辑,各种常见的公共框架组件也集成了 VI 的点火组件。通过与发布系统集成,从流程上控制了点火/健康检测失败的应用
不会在生产环境提供服务。
虽然 VI 可以暴露很多底层细节情况,但 VI 本身与我们常见的监控系统(例如CAT) 还是有很大区别的,主要表现在以下方面:
1. 历史追溯 :VI 是无法追溯历史监控数据的,用户只能从 VI 获取到实时的监控数据。之所无法追溯历史数据,是因为 VI 的设计目标之一是帮助用户快速排除 当前 正在发生的故障,并不关心历史曾经出现的问题。而且考虑到有些监控数据的获取代价较高,且业务正常时并无太大参考价值,所以 VI 被设计为只有当用户存在访问行为时,才开始采集数据,当用户访问行为结束后即关闭数据采集。
2. 告警 :告警功能几乎是监控系统的标配,但 VI 并不具备这样的能力,原因是 VI 并未在后台持续采集数据,因而并没有衡量系统指标变化的能力,所以也无法针对指标变化提供告警功能。
3. 数据处理/分析能力 :VI 并没有中央节点来处理/分析应用的全局数据,因而无法从宏观上对应用健康情况进行评价,而这一能力是完善的监控系统需要考虑到的。
因此,虽然 VI 可以实时采集很多监控数据,但 VI 的设计目标并不是成为一个监控系统,而是帮助用户快速定位/解决问题的工具。
接下来的内容会向大家简单介绍下 VI 两个基本功能(交互设计/在线调试)的设计细节,供大家参考。
VI 的使用方式非常简单,常见的Web 应用只需要添加 Maven 依赖,应用启动后即可通过VI Portal 或直接 IP 访问特定 URL 来使用 VI。那么 VI 是怎么实现只增加一个Jar 包依赖就可以提供页面交互呢?是否所有类型的应用都可以这么简单地使用 VI 的页面交互?
1. 依赖:VI 接入最简单的方式就是只添加 Maven 依赖,而无需进行配置和额外代码编写。这种接入方式条件就是需要使用servlet 3.0及以上版本,默认已被 Tomcat 7 及以上版本所支持。
对大多数业务应用场景来说,这个条件是非常容易达成的。但如果应用并没有使用 Web 容器,就需要根据实际情况,增加 vi-server 依赖或者 vi-netty 依赖。其中,vi-server 依赖内嵌了 jetty,本质也是通过启动一个内嵌的 web 容器来实现界面交互;vi-netty 则使用了 netty 自带的 http handler,为应用程序监听的端口增加了 http 协议处理能力,以启用 VI 界面交互。
2. 入口:VI 的交互界面本身并未跳出J2EE 的规范,因此其入口也仅根据运行环境不同略有变化,这部分对用户是基本透明的。入口的基本思路是,在应用程序生命周期尽可能早的地方,进行且只进行一次初始化;尽可能利用常见框架来带动完成 VI 的初始化动作。
3. 路由:VI 路由动作分为两块,静态资源路由和数据路由。其中,静态资源路由负责将 http request 请求的静态资源,从 jar 包的 resources 中加载并 response 给请求方;数据路由则有点像 dispatcher 的角色,根据不同的 api 路径,找到对应的 component,从中获取数据并反馈给请求方。
4. 组件:VI 的内容生产者。根据需求,组件大致可以分为控制型组件和数据型组件两种类型。其中,控制型组件是会影响到程序状态的,比如点火组件如果点火失败,程序是不会对外提供服务的,这类组件可能并不是由外界请求触发,而是在 VI 整体初始化的时候就被触发了。另一类数据型组件则不会影响到程序状态,数据型组件只被动地收集数据,而且只有在真正使用 VI 时才会开始收集,以期尽可能减少对应用程序的影响。
从上述描述可以看出,VI 交互设计主要遵循了几个基本原则:
1. 尽量避免侵入业务程序:这个也是目前各种中间件都尽量遵循的原则之一。这样做有几个好处,接口越少,用户的学习成本就越低;接口越简单,滥用和误用的可能性就会越少;业务代码无侵入,就意味着中间件代码与业务代码耦合性会很低,排障难度也会变小。
2. 模块化/插件化:VI 允许业务自定义点火逻辑,它自身的点火逻辑也是通过同样的机制来实现的;VI 允许业务自定义 metrics,它本身也通过 metrics 组件提供出很多常见的 metrics 出来。因此,对 VI 来说,很多功能,只是定义了一层 SPI,用户可以自己去实现这套 SPI,甚至 VI 本身很多功能也按照同样的逻辑来实现的。这么做的好处是灵活、层次结构分明,便于维护和扩展。
3. 尽可能少的资源消耗:对应用程序来说,VI 算是辅助类型的组件,因此,VI 不应该对应用程序的稳定性产生不好的影响。考虑到这一点,VI 只有在有用户打开界面时才实时开启数据采集功能,且对整体内存消耗做了严格的限制,尽最大的努力以减少 VI 本身对应用程序资源的消耗。
我们有时候会遇到这种问题,同一份代码,在测试环境运行得很好,一旦部署到生产环境就会偶尔出现意外的问题,排除掉代码和运行环境的问题后,一般会猜想是否上下游的某些数据交互存在问题。但不巧的是,可能对应的数据处理逻辑事先并未考虑到监控埋点。这种时候我们可能不得不修改代码,增加监控埋点,重新打包发布,再验证先前的猜想,而猜想验证失败后,很可能又要从头再来一遍。
VI 的在线调试功能可以极大地降低这种情况下的排障负担。
所谓的在线调试并不是真的给应用加个断点把应用阻塞住,而仅仅是给业务代码的某个类某一行加个标签。VI 会在标签生效后,动态修改对应类的字节码,并促使 JVM 重新加载该类。VI 修改的类字节码部分非常简单且可靠,没有任何变更操作,只采集了对应标签的上下文信息,通过交互界面展示给用户,告诉用户断点位置的上下文情况。
所以,这个功能的难点在于如何在程序运行时动态修改字节码。
JDK 的 java/lang/instrument 包提供了这种功能。JDK 对这个包的说明是这样的:
Provides services that allowJava programming language agents to instrument programs running on the JVM. Themechanism for instrumentation is modification of the byte-codes of methods.
也就是说这个包,通过提供字节码修改的功能,为Java Agent 提供了操纵运行时程序的能力。
如果有了解过 Java Agent,应该知道通常我们要为 JVM 增加 Agent,是通过增加JVM 参数来实现的,VI 在线调试的第一版就是这样做的。这样做有个问题,需要每个希望使用在线调试功能的同学都修改自己的 JVM 参数;或者需要 OPS 同学帮忙统一添加 JVM 参数,无论哪种方式都不符合 VI 尽可能简化接入方式的目标。所以 VI 在随后的版本,改为使用反射的方式,通过调用 HotSpotVirtualMachine的 loadAgent 方法,来实现动态加载 JavaAgent 的能力。
自此,VI 的在线调试功能就变得很好用了,无痛接入,界面友好,动态启用。用户不必通过重复猜想-埋点-打包上线-验证猜想的过程,就可以获取想要的信息,极大地缩短了此类问题的排障时间。
做程序员久了,谁没写过几个 BUG,有 BUG 不可怕,找不到 BUG 才最可怕,因为定位问题太过耗时而影响到用户更可怕。
这么可怕的事情,携程VI 希望能尽最大努力帮到程序猿们。 如果大家对 VI 有什么产品建议,欢迎在评论区留言交流。
【推荐阅读】