【数据存储系列】手牵手学习全链路测试
大促之前全链路压测原理篇
能力目标
- 全链路压测思想以及实现
- 全链路跟踪的原理和实现
- 常用的链路压测方案
- 中间件如何来进行实现
- SpringBoot starter 如何编写
1. 全链路压测的意义
上图是 2012 年淘宝核心业务应用关系的拓扑图,还不包含了其他的非核心业务应用,所谓的核心业务就是和交易相关的,和钱相关的业务,这张图大家可能看不清楚,看不清楚才是正常的,因为当时的阿里应用数量之多、应用间关系之混乱靠人工确实已经无法理清楚了。
在真实的业务场景种,每个系统的压力都比较大,而系统之间是有相互依赖关系的,单机压测没有考虑到依赖环节压力都比较大的情况,会引入一个不确定的误差。这就好比,我们要生产一个仪表,每一个零件都经过了严密的测试,最终把零件组装成一个仪表,仪表的工作状态会是什么样的并不清楚。
2. 链路压测方案刨析
2.1 线下压测
顾名思义就是在测试环境进行压测,且是针对一些重点项目这种测试手段,因为测试环境硬件资源以及压测数据与线上差别太大并且服务间依赖关系错综复杂,测试环境很难模拟且不够稳定,压测出来的数据指标参考价值不大,难以用测试环境得出的结果推导生产真实容量。
2.2 预生产环境压测
这个一般是将生成环境的硬件以及软件同步复制到与生产环境一份,然后对服务内部的外部调用接口进行拦截,然后进行压测这样可以评估出来生产环境的真实容量以及达到压测的目的,但是成本非常高,需要将生产环境的硬件完全的复制一份,并未维护成本非常高,部署的时候需要同步的在预生产环境进行部署,以及压测代码的更改。
2.3 引流压测
随着业务量的不断增长,考虑到线下测试结果的准确性,开始尝试生产压测,这种压测手段,我们称之为引流压测。事实上没有真正的模拟放大压力进行测试,而是一种通过缩小在线服务集群数的方式来放大单机处理量。比如一个业务系统的集群有100个节点,将其中90个节点模拟下线或转发流量到剩余的10个节点上实施压测。
引流压测的弊端在于,DB承受压力不变,上下游系统的压力不变。压测结果仅能代表单个应用的性能,但往往无法识别链路和架构级的隐患,而且在引流过程中倘若出现异常或突如其来的业务高峰,很容易造成生产故障。
2.4 全链路压测
随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要一些可以帮助理解系统行为、用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题,但是他的缺点也很明显就是需要的技术难度很高,需要克服流量染色,数据隔离,日志隔离,风险熔断等技术难题,因位在生产环境压测,所以控制不好风险也是非常高的。
所以,在复杂的微服务架构系统中,几乎每一个前端请求都会形成一个复杂的分布式服务调用链路。一个请求完整调用链可能如下图所示:
2.5 四种压测方案对比
压测效果 | 技术难度 | 机器成本 | 维护成本 | 风险 | |
---|---|---|---|---|---|
线下压测 | 差 | 低 | 低 | 低 | 无 |
预生产压测 | 好 | 低 | 高 | 高 | 中 |
引流压测 | 差 | 中 | 无 | 低 | 高 |
全链路压测 | 好 | 高 | 无 | 低 | 高 |
3. 全链路压测概述
3.1 什么是全链路压测
基于实际的生产业务场景、生产环境,模拟海量的用户请求和数据对整个业务链(通常是核心业务链)进行压力测试,并持续调优的过程。
3.2 解决什么问题
解决在业务场景越发复杂化、海量数据冲击下系统整个业务链的可用性、服务能力的瓶颈,以及容量规划等问题。
3.2.1 精确的容量规划
3.2.1.1 为什么需要容量规划
什么时候增减机器、保障系统稳定性、节约成本
容量规划的目的在于让每一个业务系统能够清晰地知道:什么时候该加机器、什么时候应该减机器?双11等大促场景需要准备多少机器,既能保障系统稳定性、又能节约成本
3.2.1.2 容量规划四步走
- 业务流量预估阶段:通过历史数据分析未来某一个时间点业务的访问量会有多大
- 系统容量评估阶段:初步计算每一个系统需要分配多少机器
- 容量的精调阶段:通过全链路压测来模拟大促时刻的用户行为,在验证站点能力的同时对整个站点的容量水位进行精细调整
- 流量控制阶段:对系统配置限流阈值等系统保护措施,防止实际的业务流量超过预估业务流量的情况下,系统无法提供正常服务流量控制阶段:对系统配置限流阈值等系统保护措施,防止实际的业务流量超过预估业务流量的情况下,系统无法提供正常服务
3.2.2 进行全链路的性能监控
全链路性能监控 从整体维度到局部维度展示各项指标,将跨应用的所有调用链性能信息集中展现,可方便度量整体和局部性能,并且方便找到故障产生的源头,生产上可极大缩短故障排除时间。
- 保证系统稳定性:可能提前预估系统存在的各种问题,提前模拟高并发场景,有备无患。
- 请求链路追踪,故障快速定位:可以通过调用链结合业务日志快速定位错误信息。
- 精准的容量评估:能够定位到最需要扩容的服务,帮助公司用最低的成本满足业务的性能要求
- 真实的性能验证:能够在生成环境以最真实的环境来验证系统的真实性能。
- 数据分析,优化链路:可以得到用户的行为路径,汇总分析应用在很多业务场景。
3.3 如何展开全链路压测
3.3.1 业务模型梳理
- 首先应该将核心业务和非核心业务进行拆分,确认流量高峰针对的是哪些业务场景和模块,针对性的进行扩容准备。
- 梳理出对外的接口:使用MOCK(模拟)方式做挡板。
- 千万不要污染正常数据:认真梳理数据处理的每一个环节,确保 mock 数据的处理结果不会写入到正常库里面
3.3.2 数据模型构建
- 数据的真实性和可用性:可以从生产环境完全移植一份当量的数据包,作为压测的基础数据,然后基于基础数据,通过分析历史数据增长趋势,预估当前可能的数据量
- 数据隔离:千万千万不要污染正常数据:认真梳理数据处理的每一个环节,可以考虑通过压测数据隔离处理,落入影子库,mock 对象等手段,来防止数据污染
3.3.3 压测工具选型
使用分布式压测的手段来进行用户请求模拟,目前有很多的开源工具可以提供分布式压测的方式,比如JMeter、nGrinder、Locust等。
4. 业务模块介绍
现在我们对整体的业务进行介绍以及演示
5. 全链路整体架构
上面介绍了为什么需要全链路压测,下面来看下全链路压测的整体架构。
整体架构如下主要是对压测客户端的压测数据染色,全链路中间件识别出染色数据,并将正常数据和压测数据区分开,进行数据隔离,这里主要涉及到mysql数据库,RabbitMQ,Redis,还需要处理因为hystrix线程池不能通过ThreadLocal传递染色表示的问题。
5.1 需要应对的问题
5.1.1 业务问题
如何开展全链路压测?在说这个问题前,我们先考虑下,全链路压测有哪些问题比较难解决。
涉及的系统太多,牵扯的开发人员太多
在压测过程中,做一个全链路的压测一般会涉及到大量的系统,在整个压测过程中,光各个产品的人员协调就是一个比较大的工程,牵扯到太多的产品经理和开发人员,如果公司对全链路压测早期没有足够的重视,那么这个压测工作是非常难开展的。
模拟的测试数据和访问流量不真实
在压测过程中经常会遇到压测后得到的数据不准确的问题,这就使得压测出的数据参考性不强,为什么会产生这样的问题?主要就是因为压测的环境可能和生成环境存在误差、参数存在不一样的地方、测试数据存在不一样的地方这些因素综合起来导致测试结果的不可信。
压测生产数据未隔离,影响生产环境
在全链路压测过程中,压测数据可能会影响到生产环境的真实数据,举个例子,电商系统在生产环境进行全链路压测的时候可能会有很多压测模拟用户去下单,如果不做处理,直接下单的话会导致系统一下子会产生很多废订单,从而影响到库存和生产订单数据,影响到日常的正常运营。
5.1.2 技术问题
5.1.2.1 探针的性能消耗
APM组件服务的影响应该做到足够小。
服务调用埋点本身会带来性能损耗,这就需要调用跟踪的低损耗,实际中还会通过配置采样率的方式,选择一部分请求去分析请求路径。在一些高度优化过的服务,即使一点点损耗也会很容易察觉到,而且有可能迫使在线服务的部署团队不得不将跟踪系统关停。
5.1.2.2 代码的侵入性
即也作为业务组件,应当尽可能少入侵或者无入侵其他业务系统,对于使用方透明,减少开发人员的负担。
对于应用的程序员来说,是不需要知道有跟踪系统这回事的。如果一个跟踪系统想生效,就必须需要依赖应用的开发者主动配合,那么这个跟踪系统也太脆弱了,往往由于跟踪系统在应用中植入代码的bug或疏忽导致应用出问题,这样才是无法满足对跟踪系统“无所不在的部署”这个需求。
5.1.2.3 可扩展性
一个优秀的调用跟踪系统必须支持分布式部署,具备良好的可扩展性。能够支持的组件越多当然越好。或者提供便捷的插件开发API,对于一些没有监控到的组件,应用开发者也可以自行扩展。
数据的分析
数据的分析要快 ,分析的维度尽可能多。跟踪系统能提供足够快的信息反馈,就可以对生产环境下的异常状况做出快速反应。分析的全面,能够避免二次开发。
5.2 全链路压测核心技术
上面从总体架构层面分析了全链路压测的核心,下面就分析下全链路压测用到的核心技术点
5.2.1 全链路流量染色
做到微服务和中间件的染色标志的穿透
通过压测平台对输出的压力请求打上标识,在订单系统中提取压测标识,确保完整的程序上下文都持有该标识,并且能够穿透微服务以及各种中间件,比如 MQ,hystrix,Fegin等。
5.2.2 全链路服务监控
需要能够实时监控服务的运行状况以及分析服务的调用链,我们采用skywalking进行服务监控和压测分析
5.2.3 全链路日志隔离
做到日志隔离,防止污染生产日志
当订单系统向磁盘或外设输出日志时,若流量是被标记的压测流量,则将日志隔离输出,避免影响生产日志。
5.2.4 全链路风险熔断
流量控制,防止流量超载,导致集群不可用
当订单系统访问会员系统时,通过RPC协议延续压测标识到会员系统,两个系统之间服务通讯将会有白黑名单开关来控制流量流入许可。该方案设计可以一定程度上避免下游系统出现瓶颈或不支持压测所带来的风险,这里可以采用Sentinel来实现风险熔断。
5.3 全链路数据隔离
对各种存储服务以及中间件做到数据隔离,方式数据污染
5.3.1 数据库隔离
当会员系统访问数据库时,在持久化层同样会根据压测标识进行路由访问压测数据表。数据隔离的手段有多种,比如影子库、影子表,或者影子数据,三种方案的仿真度会有一定的差异,他们的对比如下。
隔离性 | 兼容性 | 安全级别 | 技术难度 | |
---|---|---|---|---|
影子库 | 高 | 高 | 高 | 高 |
影子表 | 中 | 低 | 中 | 中 |
影子数据 | 低 | 低 | 低 | 低 |
5.3.2 消息队列隔离
当我们生产的消息扔到MQ之后,接着让消费者进行消费,这个没有问题,压测的数据不能够直接扔到MQ中的,因为它会被正常的消费者消费到的,要做好数据隔离,方案有队列隔离,消息隔离,他们对比如下。
隔离性 | 兼容性 | 安全级别 | 技术难度 | |
---|---|---|---|---|
队列隔离 | 高 | 好 | 高 | 高 |
消息隔离 | 低 | 低 | 低 | 中 |
5.3.3 Redis 隔离
通过 key 值来区分,压测流量的 key 值加统一后缀,通过改造RedisTemplate来实现key的路由。
6. 框架实现
6.1 流量染色方案
上面分析了从整体分析了全链路压测用的的核心技术,下面就来实现第一个流量染色。
6.1.1 流量识别
要想压测的流量和数据不影响线上真实的生产数据,就需要线上的集群能识别出压测的流量,只要能识别出压测请求的流量,那么流量触发的读写操作就很好统一去做隔离了。
全链路压测发起的都是Http的请求,只需要要请求头上添加统一的压测请求头。
通过在请求协议中添加压测请求的标识,在不同服务的相互调用时,一路透传下去,这样每一个服务都能识别出压测的请求流量,这样做的好处是与业务完全的解耦,只需要应用框架进行感知,对业务方代码无侵入。
6.1.2 MVC接收数据
客户端传递过来的数据可以通过获取Header的方式获取到,并将其设置进当前的ThreadLocal,交给后面的方法使用。
6.1.2.1 MVC拦截器实现
1 | /** |
6.1.2.2 Tomcat线程复用问题
tomcat默认使用线程池来管理线程,一个请求过来,如果线程池里面有空闲的线程,那么会在线程池里面取一个线程来处理该请求,一旦该线程当前在处理请求,其他请求就不会被分配到该线程上,直到该请求处理完成。请求处理完成后,会将该线程重新加入线程池,因为是通过线程池复用线程,就会如果线程内部的ThreadLocal没有清除就会出现问题,需要新的请求进来的时候,清除ThreadLocal。
6.1.3 Fegin传递传递染色标识
我们项目的微服务是使用Fegin来实现远程调用的,跨微服务传递染色标识是通过MVC拦截器获取到请求Header的染色标识,并放进ThreadLocal中,然后交给Fegin拦截器在发送请求之前从ThreadLocal中获取到染色标识,并放进Fegin构建请求的Header中,实现微服务之间的火炬传递。
6.1.3.1 代码实现
1 | public class WormholeFeignRequestInterceptor implements RequestInterceptor { |
6.1.4 Hystrix传递染色标识
6.1.4.1 Hystrix隔离技术
Hystrix 实现资源隔离,主要有两种技术:
信号量
信号量的资源隔离只是起到一个开关的作用,比如,服务 A 的信号量大小为 10,那么就是说它同时只允许有 10 个 tomcat 线程来访问服务 A,其它的请求都会被拒绝,从而达到资源隔离和限流保护的作用。
线程池
线程池隔离技术,是用 Hystrix 自己的线程去执行调用;而信号量隔离技术,是直接让 tomcat 线程去调用依赖服务。信号量隔离,只是一道关卡,信号量有多少,就允许多少个 tomcat 线程通过它,然后去执行。
7.1.4.2 Hystrix穿透
如果使用线程池模式,那么存在一个ThreadLocal变量跨线程传递的问题,即在主线程的ThreadLocal变量,无法在线程池中使用,不过Hystrix内部提供了解决方案。
封装Callable任务
1 | public final class DelegatingWormholeContextCallable<V> implements Callable<V> { |
实现Hystrix的并发策略类
因为Hystrix默认的并发策略不支持ThreadLocal传递,我们可以自定义并发策略类继承HystrixConcurrencyStrategy
1 | public class ThreadLocalAwareStrategy extends HystrixConcurrencyStrategy { |
Hystrix注入新并发策略并进行刷新
1 | public class HystrixThreadLocalConfiguration { |
6.2 数据隔离方案
6.2.1 JDBC数据源隔离
数据隔离需要对DB,Redis,RabbitMQ进行数据隔离
通过实现Spring动态数据源AbstractRoutingDataSource
,通过ThreadLocal
识别出来压测数据,如果是压测数据就路由到影子库,如果是正常流量则路由到主库,通过流量识别的改造,各个服务都已经能够识别出压测的请求流量了。
6.2.1.1 代码实现
数据源路由Key持有对象
根据路由Key将选择将操作路由给那个数据源
1 | /** |
动态数据源实现类
根据路由Key实现数据源的切换
1 | /** |
6.2.2 Redis 数据源隔离
同时通过ThreadLocal
识别出来压测数据,自定义Redis的主键的序列化方式,如果是压测数据则在主键后面加上后缀,这样就可以通过不同主键将Redis数据进行隔离。
6.2.2.1 实现key序列化
1 | public class KeyStringRedisSerializer extends StringRedisSerializer { |
6.2.2.2 配置序列化器
1 | /** |
6.2.3 RabbitMQ 数据隔离
6.2.3.1 自动创建影子队列
因为SpringAMQP中的
中的关键方法是私有的,无法通过继承的方式进行实现对以配置好的队列进行扩展,所以需要自定义该类,来实现对自动创建影子队列,并和交换器进行绑定
代码实现
改造RabbitListenerAnnotationBeanPostProcessor
类来实现创建MQ影子队列以及将影子Key绑定到影子队列。
1 | public class WormholeRabbitListenerAnnotationBeanPostProcessor extends RabbitListenerAnnotationBeanPostProcessor { |
6.2.3.2 传递染色标识
因为MQ是异步通讯,为了传递染色标识,会在发送MQ的时候将染色标识传递过来,MQ接收到之后放进当前线程的ThreadLocal
里面,这个需要扩展Spring的SimpleRabbitListenerContainerFactory
来实现
代码实现
1 | public class WormholeSimpleRabbitListenerContainerFactory extends SimpleRabbitListenerContainerFactory { |
6.2.3.3 发送MQ消息处理
同上,需要传递染色标识,就通过继承RabbitTemplate
重写convertAndSend
方法来实现传递染色标识。
1 | public class ShadowRabbitTemplate extends RabbitTemplate { |
6.3 接口隔离方法
6.3.1 Mock 第三方接口
对于第三方数据接口需要进行隔离,比如短信接口,正常的数据需要发送短信,对于压测数据则不能直接调用接口发送短信,并且需要能够识别出来压测数据,并进行MOCK接口调用。
6.3.1.1 核心类实现
1 | @Aspect |
6.3.1.2 使用方式
在具体方法上面加上注解就可以使用了
1 | @Override |
6.4 零侵入方案
如果开发的中间件需要各个微服务大量改造,对开发人员来说就是一个灾难,所以这里采用零侵入的springboot starter 来解决
6.4.1 自动装配
使用微服务得
@Conditional
来完成配置得自动装配,这里用MVC得配置来演示自动装配,其他得都是类似这样可以最大限度的优化代码并提高很高的可扩展性。
1 | /** |
6.4.1.1 Conditional 简介
@Conditional表示仅当所有指定条件都匹配时,组件才有资格注册 。 该@Conditional注释可以在以下任一方式使用:
- 作为任何@Bean方法的方法级注释
- 作为任何类的直接或间接注释的类型级别注释 @Component,包括@Configuration类
- 作为元注释,目的是组成自定义构造型注释
6.4.1.2 Conditional派生注解
@Conditional派生了很多注解,下面给个表格列举一下派生注解的用法
@Conditional派生注解 | 作用(都是判断是否符合指定的条件) |
---|---|
@ConditionalOnJava | 系统的java版本是否符合要求 |
@ConditionalOnBean | 有指定的Bean类 |
@ConditionalOnMissingBean | 没有指定的bean类 |
@ConditionalOnExpression | 符合指定的SpEL表达式 |
@ConditionalOnClass | 有指定的类 |
@ConditionalOnMissingClass | 没有指定的类 |
@ConditionalOnSingleCandidate | 容器只有一个指定的bean,或者这个bean是首选bean |
@ConditionalOnProperty | 指定的property属性有指定的值 |
@ConditionalOnResource | 路径下存在指定的资源 |
@ConditionalOnWebApplication | 系统环境是web环境 |
@ConditionalOnNotWebApplication | 系统环境不是web环境 |
@ConditionalOnjndi | JNDI存在指定的项 |
6.4.2 SpringBoot starter
和自动装配一样,Spring Boot Starter的目的也是简化配置,而Spring Boot Starter解决的是依赖管理配置复杂的问题,有了它,当我需要构建一个Web应用程序时,不必再遍历所有的依赖包,一个一个地添加到项目的依赖管理中,而是只需要一个配置spring-boot-starter-web
。
6.4.2.1 使用规范
在 Spring Boot starter 开发规范中,项目中会有一个空的名为 xxx-spring-boot-starter 的项目,这个项目主要靠 pom.xml 将所有需要的依赖引入进来。同时项目还会有一个 xxx-spring-boot-autoconfigure 项目,这个项目主要写带 @Configuration 注解的配置类,在这个类或者类中带 @Bean 的方法上。
6.4.2.2 项目使用
在 xxx-spring-boot-starter的项目下的resources文件夹下面新建一个META-INF文件,并在下面创建spring.factories文件,将我们的自动配置类配置进去
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
大促之前全链路压监控篇
1. skywalking服务监控
1.1 skywalking简介
Skywalking 是一个APM系统,即应用性能监控系统,为微服务架构和云原生架构系统设计。它通过探针自动收集所需的指标,并进行分布式追踪。通过这些调用链路以及指标,Skywalking APM会感知应用间关系和服务间关系,并进行相应的指标统计。目前支持链路追踪和监控应用组件如下,基本涵盖主流框架和容器,如国产PRC Dubbo和motan等,国际化的spring boot,spring cloud都支持了
SkyWalking是分布式系统的应用程序性能监视工具,专为微服务、云原生架构和基于容器(Docker、K8S、Mesos)架构而设计
SkyWalking是观察性分析平台和应用性能管理系统。提供分布式追踪、服务网格遥测分析、度量聚合和可视化一体化解决方案
1.1.1 SkyWalking组件
- Skywalking Agent: 采集
tracing
(调用链数据)和metric
(指标)信息并上报,上报通过HTTP或者gRPC方式发送数据到Skywalking Collector - Skywalking Collector : 链路数据收集器,对agent传过来的
tracing
和metric
数据进行整合分析通过Analysis Core
模块处理并落入相关的数据存储中,同时会通过Query Core
模块进行二次统计和监控告警 - Storage: Skywalking的存储,支持以
ElasticSearch
、Mysql
、TiDB
、H2
等作为存储介质进行数据存储 - UI: Web可视化平台,用来展示落地的数据,目前官方采纳了RocketBot作为SkyWalking的主UI
1.2 配置SkyWalking
1.2.1 下载SkyWalking
下载SkyWalking的压缩包,解压后将压缩包里面的agent文件夹放进本地磁盘,探针包含整个目录,请不要改变目录结构。
1.2.2 Agent配置
通过了解配置,可以对一个组件功能有一个大致的了解,解压开skywalking的压缩包,在agent/config文件夹中可以看到agent的配置文件,从skywalking支持环境变量配置加载,在启动的时候优先读取环境变量中的相关配置。
skywalking配置名称 | 描述 |
---|---|
agent.namespace | 跨进程链路中的header,不同的namespace会导致跨进程的链路中断 |
agent.service_name | 一个服务(项目)的唯一标识,这个字段决定了在sw的UI上的关于service的展示名称 |
agent.sample_n_per_3_secs | 客户端采样率,0或者负数标识禁用,默认-1 |
agent.authentication | 与collector进行通信的安全认证,需要同collector中配置相同 |
agent.ignore_suffix | 忽略特定请求后缀的trace |
collecttor.backend_service | agent需要同collector进行数据传输的IP和端口 |
logging.level | agent记录日志级别 |
skywalking agent使用javaagent无侵入式的配合collector实现对分布式系统的追踪和相关数据的上下文传递。
1.2.3 配置探针
配置SpringBoot启动参数,需要填写如下的运行参数,代码放在后面,需要的自己粘贴。
1 | -javaagent:D:/data/skywalking/agent/skywalking-agent.jar |
- javaagent:复制的agent目录下探针的jar包路径
- skywalking.agent.service_name:需要在skywalking显示的服务名称
- skywalking.collector.backend_service:skywalking服务端地址默认是11800
2. Arthas服务诊断
2.1 Arthas是什么
Arthas
是Alibaba开源的Java诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪Java代码;实时监控JVM状态。
Arthas
支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab
自动补全功能,进一步方便进行问题的定位和诊断。
2.2 Arthas能做什么
当你遇到以下类似问题而束手无策时,
Arthas
可以帮助你解决:
Arthas
采用命令行交互模式,同时提供丰富的 Tab
自动补全功能,进一步方便进行问题的定位和诊断。
2.3 安装使用
2.3.1 快速安装
下载
arthas-boot.jar
,然后用java -jar
的方式启动
1 | curl -O https://arthas.aliyun.com/arthas-boot.jar |
2.4 启动arthas
在命令行下面执行(使用和目标进程一致的用户启动,否则可能attach失败):
1 | java -jar arthas-boot.jar |
- 执行该程序的用户需要和目标进程具有相同的权限。比如以
admin
用户来执行:sudo su admin && java -jar arthas-boot.jar
或sudo -u admin -EH java -jar arthas-boot.jar
。 - 如果attach不上目标进程,可以查看
~/logs/arthas/
目录下的日志。 - 如果下载速度比较慢,可以使用aliyun的镜像:
java -jar arthas-boot.jar --repo-mirror aliyun --use-http
java -jar arthas-boot.jar -h
打印更多参数信息。
2.5 选择java进程
在打印的java进程中选择一个需要
attach
的进程,我们在这里选择4
这个order-service的java进程
2.6 命令列表
2.7 线程命令
2.7.1 dashboard指令
当前系统的实时数据面板,按 ctrl+c 退出。
当运行在服务时,会显示当前服务的实时信息,如HTTP请求的qps, rt, 错误数, 线程池信息等等。
2.7.1.1 参数说明
参数名称 | 参数说明 |
---|---|
[i:] | 刷新实时数据的时间间隔 (ms),默认5000ms |
[n:] | 刷新实时数据的次数 |
2.7.1.2 使用
在Arthas中输入
dashboard
命令会得到如下界面
1 | dashboard |
2.7.1.3 线程参数解释
- ID: Java级别的线程ID,注意这个ID不能跟jstack中的nativeID一一对应。
- NAME: 线程名
- GROUP: 线程组名
- PRIORITY: 线程优先级, 1~10之间的数字,越大表示优先级越高
- STATE: 线程的状态
- CPU%: 线程的cpu使用率。比如采样间隔1000ms,某个线程的增量cpu时间为100ms,则cpu使用率=100/1000=10%
- DELTA_TIME: 上次采样之后线程运行增量CPU时间,数据格式为
秒
- TIME: 线程运行总CPU时间,数据格式为
分:秒
- INTERRUPTED: 线程当前的中断位状态
- DAEMON: 是否是daemon线程
2.7.2 thread相关指令
查看当前线程信息,查看线程的堆栈
2.7.2.1 参数说明
参数名称 | 参数说明 |
---|---|
id | 线程id |
[n:] | 指定最忙的前N个线程并打印堆栈 |
[b] | 找出当前阻塞其他线程的线程 |
[i <value> ] |
指定cpu使用率统计的采样间隔,单位为毫秒,默认值为200 |
[–all] | 显示所有匹配的线程 |
2.7.2.2 cpu使用率如何统计
这里的cpu使用率与linux 命令top -H -p <pid>
的线程%CPU
类似,一段采样间隔时间内,当前JVM里各个线程的增量cpu时间与采样间隔时间的比例。
2.7.2.3 工作原理
首先第一次采样,获取所有线程的CPU时间(调用的是
java.lang.management.ThreadMXBean#getThreadCpuTime()
及sun.management.HotspotThreadMBean.getInternalThreadCpuTimes()
接口)然后睡眠等待一个间隔时间(默认为200ms,可以通过
-i
指定间隔时间)再次第二次采样,获取所有线程的CPU时间,对比两次采样数据,计算出每个线程的增量CPU时间
线程CPU使用率 = 线程增量CPU时间 / 采样间隔时间 * 100%
注意: 这个统计也会产生一定的开销(JDK这个接口本身开销比较大),因此会看到as的线程占用一定的百分比,为了降低统计自身的开销带来的影响,可以把采样间隔拉长一些,比如5000毫秒。
2.7.2.4 JVM内部线程
Java 8之后支持获取JVM内部线程CPU时间,这些线程只有名称和CPU时间,没有ID及状态等信息(显示ID为-1)。 通过内部线程可以观测到JVM活动,如GC、JIT编译等占用CPU情况,方便了解JVM整体运行状况。
- 当JVM 堆(heap)/元数据(metaspace)空间不足或OOM时,可以看到GC线程的CPU占用率明显高于其他的线程。
- 当执行
trace/watch/tt/redefine
等命令后,可以看到JIT线程活动变得更频繁。因为JVM热更新class字节码时清除了此class相关的JIT编译结果,需要重新编译。
JVM内部线程包括下面几种:
- JIT编译线程: 如
C1 CompilerThread0
,C2 CompilerThread0
- GC线程: 如
GC Thread0
,G1 Young RemSet Sampling
- 其它内部线程: 如
VM Periodic Task Thread
,VM Thread
,Service Thread
2.7.2.5 显示线程列表
输入thread 列出来线程列表,默认按照CPU增量时间降序排列,只显示第一页数据。
1 | thread |
2.7.2.6 显示所有线程
显示所有匹配线程信息,有时需要获取全部JVM的线程数据进行分析。
1 | thread -all |
2.7.2.7 打印线程堆栈
通过
thread id
命令, 显示指定线程的运行堆栈,我们看下139
这个线程的堆栈信息
1 | thread 139 |
这样可以打印出来线程139的堆栈以及调用关系
2.7.2.8 排查死锁
找出当前阻塞其他线程的线程
有时候我们发现应用卡住了, 通常是由于某个线程拿住了某个锁, 并且其他线程都在等待这把锁造成的。 为了排查这类问题, arthas提供了thread -b
, 一键找出那个罪魁祸首。
1 | thread -b |
发现当前没有发现阻塞其他线程的线程
2.8 虚拟机命令
2.8.1 jvm相关指令
查看当前JVM信息
2.8.1.1 查看JVM信息
jvm 命令可以打印出当前微服务jvm相关的参数
1 | jvm |
Thread相关
参数解释
- COUNT: JVM当前活跃的线程数
- DAEMON-COUNT: JVM当前活跃的守护线程数
- PEAK-COUNT: 从JVM启动开始曾经活着的最大线程数
- STARTED-COUNT: 从JVM启动开始总共启动过的线程次数
- DEADLOCK-COUNT: JVM当前死锁的线程数
2.8.2 JVM选项指令
查看,更新VM诊断相关的参数
2.8.2.1 查看相关选项
通过该命令可以看到当前jvm的一些参数选项
1 | vmoption |
2.8.2.2 查看指定选项
这里我们查看是否打开了
PrintGCDetails
1 | vmoption PrintGCDetails |
2.8.2.3 更新选项
我们可以使用arthas对jvm的选项进行更新,这里我们开启
PrintGCDetails
1 | vmoption PrintGCDetails true |
2.9 类相关命令
2.9.1 搜索相关类
查看JVM已加载的类信息,SC是“Search-Class” 的简写,这个命令能搜索出所有已经加载到 JVM 中的 Class 信息
2.9.1.1 参数说明
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
[d] | 输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的ClassLoader等详细信息。 如果一个类被多个ClassLoader所加载,则会出现多次 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[f] | 输出当前类的成员变量信息(需要配合参数-d一起使用) |
[x:] | 指定输出静态变量时属性的遍历深度,默认为 0,即直接使用 toString 输出 |
[c:] |
指定class的 ClassLoader 的 hashcode |
[classLoaderClass:] |
指定执行表达式的 ClassLoader 的 class name |
[n:] |
具有详细信息的匹配类的最大数量(默认为100) |
class-pattern支持全限定名,如com.黑马.test.AAA,也支持com/黑马/test/AAA这样的格式,这样,我们从异常堆栈里面把类名拷贝过来的时候,不需要在手动把
/
替换为.
啦。
sc 默认开启了子类匹配功能,也就是说所有当前类的子类也会被搜索出来,想要精确的匹配,请打开
options disable-sub-class true
开关
2.9.2.2 模糊搜索
该命令可以搜索
com.heima.fulllink
下面的包以及子包的类
1 | sc com.heima.fulllink.* |
2.9.2.3 打印类的详细信息
可以使用 -d 参数打印 类的详细信息
1 | sc -d com.heima.fulllink.module.po.OrderPO |
2.9.2.4 打印出类的Field信息
该命令可以打印类中字段信息,可以看到各种字段的介绍
1 | sc -d -f com.heima.fulllink.module.po.OrderPO |
2.9.2 搜索相关方法
sm是“Search-Method” 的简写,这个命令能搜索出所有已经加载了 Class 信息的方法信息。
sm
命令只能看到由当前类所声明 (declaring) 的方法,父类则无法看到。
2.9.2.1 参数说明
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
[d] | 展示每个方法的详细信息 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[c:] |
指定class的 ClassLoader 的 hashcode |
[classLoaderClass:] |
指定执行表达式的 ClassLoader 的 class name |
[n:] |
具有详细信息的匹配类的最大数量(默认为100) |
2.9.2.2 查看类的方法
查看
com.heima.fullink.service.impl.AsyncOrderServiceImpl
类的相关方法
1 | sm com.heima.fullink.service.impl.AsyncOrderServiceImpl |
可以看到该方法有两个方法actualPlaceOrder以及processOrder
2.9.2.3 查看方法信息信息
通过
-d
可以看到方法的详细信息
1 | sm -d com.heima.fullink.service.impl.AsyncOrderServiceImpl |
我们发现通过-d参数可以看到方法的详细信息
2.9.2.4 查看具体方法信息
我们可以在类名后面加入加入方法名称就可以查看具体的方法详情
1 | sm -d com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder |
2.9.3 反编译
jad
命令将 JVM 中实际运行的 class 的 byte code 反编译成 java 代码,便于你理解业务逻辑;
- 在 Arthas Console 上,反编译出来的源码是带语法高亮的,阅读更方便
- 当然,反编译出来的 java 代码可能会存在语法错误,但不影响你进行阅读理解
2.9.3.1 参数说明
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
[c:] |
类所属 ClassLoader 的 hashcode |
[classLoaderClass:] |
指定执行表达式的 ClassLoader 的 class name |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
2.9.3.2 反编译类
可以将jvm内存中的类进行反编译
1 | jad com.heima.fullink.service.impl.AsyncOrderServiceImpl |
反编译后信息比较全面,有类加载等信息
2.9.3.3 只显示源代码
默认情况下,反编译结果里会带有
ClassLoader
信息,通过--source-only
选项,可以只打印源代码。
1 | jad --source-only com.heima.fullink.service.impl.AsyncOrderServiceImpl |
我们发现通过
--source-only
后就没有了类加载等信息
2.9.3.4 反编译指定的方法
有时候我们可以反编译类中的指定方法
1 | jad --source-only com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder |
这样只会反编译对应的方法的代码
2.9.3.5 不显示行号
有时候我们不需要打印行号,可以使用
--lineNumber
参数来设置是否显示行号,默认值为true,显示指定为false则不打印行号
1 | jad --source-only com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder --lineNumber false |
通过这种方式我们可以不显示具体行号
2.10 监控相关命令
2.10.1 方法执行监控
monitor
命令是一个非实时返回命令,对匹配class-pattern
/method-pattern
/condition-express
的类、方法的调用进行监控。
实时返回命令是输入之后立即返回,而非实时返回的命令,则是不断的等待目标 Java 进程返回信息,直到用户输入 Ctrl+C
为止。
服务端是以任务的形式在后台跑任务,植入的代码随着任务的中止而不会被执行,所以任务关闭后,不会对原有性能产生太大影响,而且原则上,任何Arthas命令不会引起原有业务逻辑的改变。
2.10.1.1 监控的维度说明
监控项 | 说明 |
---|---|
timestamp | 时间戳 |
class | Java类 |
method | 方法(构造方法、普通方法) |
total | 调用次数 |
success | 成功次数 |
fail | 失败次数 |
rt | 平均RT |
fail-rate | 失败率 |
2.10.1.2 参数说明
方法拥有一个命名参数
[c:]
,意思是统计周期(cycle of output),拥有一个整型的参数值
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
condition-express | 条件表达式 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[c:] |
统计周期,默认值为120秒 |
[b] | 在方法调用之前计算condition-express |
2.10.1.3 打印方法调用
1 | monitor -c 20 com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder |
这样每隔20秒打印一次
actualPlaceOrder
方法的调用信息
2.10.2 查看方法参数
watch
让你能方便的观察到指定方法的调用情况。能观察到的范围为:返回值
、抛出异常
、入参
,通过编写 OGNL 表达式进行对应变量的查看。
2.10.2.1 参数说明
watch 的参数比较多,主要是因为它能在 4 个不同的场景观察对象
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
express | 观察表达式 |
condition-express | 条件表达式 |
[b] | 在方法调用之前观察 |
[e] | 在方法异常之后观察 |
[s] | 在方法返回之后观察 |
[f] | 在方法结束之后(正常返回和异常返回)观察 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[x:] | 指定输出结果的属性遍历深度,默认为 1 |
2.10.2.2 观察表达式
这里重点要说明的是观察表达式,观察表达式的构成主要由 ognl 表达式组成,所以你可以这样写"{params,returnObj}"
,只要是一个合法的 ognl 表达式,都能被正常支持。
2.10.2.3 使用说明
- watch 命令定义了4个观察事件点,即
-b
方法调用前,-e
方法异常后,-s
方法返回后,-f
方法结束后 - 4个观察事件点
-b
、-e
、-s
默认关闭,-f
默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出 - 这里要注意
方法入参
和方法出参
的区别,有可能在中间被修改导致前后不一致,除了-b
事件点params
代表方法入参外,其余事件都代表方法出参 - 当使用
-b
时,由于观察事件点是在方法调用前,此时返回值或异常均不存在
2.10.2.4 方法调用前
通过该命令我们可以查看到方法调用前的
actualPlaceOrder
方法第一个参数的入参信息
1 | watch com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder "{params[0],returnObj}" -b |
我们看到入参是对象,但是看不到对象的具体数据,我们可以使用
-x
表示遍历深度,可以调整来打印具体的参数和结果内容,默认值是1
1 | watch com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder "{params[0],returnObj}" -x 2 -b |
我们将遍历的深度设置为两层,这样我们就可以看到对象里面的属性
2.10.2.5 方法调用后
通过该命令我们可以查看到方法调用后的
actualPlaceOrder
方法第一个参数的入参信息和返回值
1 | watch com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder "{params[0],returnObj}" -x 2 |
我们发现在方法返回调用后监控参数获取到了方法的返回结果
2.10.2.6 方法调用前后
有时候我们需要查看方法执行前后的结果信息,我们可以使用如下的形式查看
1 | watch com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder "{params[0],returnObj}" -x 2 -b -s -n 2 |
这样我们可以查看方法的执行前后的参数信息
- 参数里
-n 2
,表示只执行两次 - 这里输出结果中,第一次输出的是方法调用前的观察表达式的结果,第二次输出的是方法返回后的表达式的结果
- 结果的输出顺序和事件发生的先后顺序一致,和命令中
-s -b
的顺序无关
2.10.2.7 条件表达式
查看参数还支持查看满足特定条件后才会打印参数
1 | watch com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder "{params[0],returnObj}" "params[0].amount>100" -x 2 |
只有满足
amount
参数大于100的数据才会打印参数信息
2.10.2.8 查看耗时慢的参数
有时候我们需要查看因为那些参数导致服务速度慢,可以使用如下的形式
1 | watch com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder "{params[0],returnObj}" "#cost>50" -x 2 |
这样只有方法调用耗时大于50ms的方法才会打印参数信息
2.10.3 链路追踪
方法内部调用路径,并输出方法路径上的每个节点上耗时
trace
命令能主动搜索 class-pattern
/method-pattern
对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。
2.10.3.1 参数说明
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
condition-express | 条件表达式 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[n:] |
命令执行次数 |
#cost |
方法执行耗时 |
2.10.3.2 跟踪方法
能够跟踪方法的调用链信息,并且可以打印方法调用耗时
1 | trace com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder |
2.10.3.3 限制次数
如果方法调用的次数很多,那么可以用
-n
参数指定捕捉结果的次数。比如下面的例子里,捕捉到一次调用就退出命令。
1 | trace com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder -n 1 |
2.10.3.4 根据耗时过滤
有时候我们只要查看耗时时间长的调用的方法,我们可以使用如下方式
1 | trace com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder '#cost > 80' -n 1 |
只会展示耗时大于80ms的调用路径,有助于在排查问题的时候,只关注异常情况
2.10.4 调用路径
输出当前方法被调用的调用路径
很多时候我们都知道一个方法被执行,但这个方法被执行的路径非常多,或者你根本就不知道这个方法是从那里被执行了,此时你需要的是 stack 命令。
2.10.4.1 参数说明
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-patter | 方法名表达式匹配 |
condition-express | 条件表达式 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[n:] |
执行次数限制 |
2.10.4.2 查看方法调用
1 | stack com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder |
这样就可以看到
actualPlaceOrder
是从什么地方调用过来的
2.10.4.3 根据条件过滤
有时候我们只需要看特定条件的的调用路径,可以使用如下形式
1 | stack com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder "params[0].amount>100" -n 1 |
我们只需要打印金额大于100的调用链,并且只需要打印一次
2.10.4.4 根据耗时过滤
有时候我们只需要过滤耗时长的调用路径,可以使用如下的形式
1 | stack com.heima.fullink.service.impl.AsyncOrderServiceImpl actualPlaceOrder '#cost > 80' -n 1 |
只打印耗时大于80ms的方法调用路径
2.10.5 生成火焰图
profiler
命令支持生成应用热点的火焰图。本质上是通过不断的采样,然后把收集到的采样结果生成火焰图。
2.10.5.1 参数说明
参数名称 | 参数说明 |
---|---|
action | 要执行的操作 |
actionArg | 属性名模式 |
[i:] | 采样间隔(单位:ns)(默认值:10’000’000,即10 ms) |
[f:] | 将输出转储到指定路径 |
[d:] | 运行评测指定秒 |
[e:] | 要跟踪哪个事件(cpu, alloc, lock, cache-misses等),默认是cpu |
2.10.5.2 启动profile
默认情况下,生成的是cpu的火焰图,即event为
cpu
。可以用--event
参数来指定
1 | profiler start |
进行服务压测
2.10.5.3 查看profiler状态
可以查看当前profiler在采样哪种
event
和采样时间。
1 | profiler status |
2.10.5.4 停止profiler
1 | profiler stop |
停止后会自动生成svg格式的文件
默认情况下,生成的结果保存到应用的工作目录
下的arthas-output
目录。可以通过 --file
参数来指定输出结果路径。比如:
1 | profiler stop --file /tmp/output.svg |
2.10.5.5 生成html格式结果
默认情况下,结果文件是
svg
格式,如果想生成html
格式,可以用--format
参数指定:
1 | profiler stop --format html |
或者在
--file
参数里用文件名指名格式。比如--file /tmp/result.html
2.10.5.6 查看结果
通过浏览器查看arthas-output下面的profiler结果,默认情况下,arthas使用3658端口,但是一般不允许外部IP访问,我们可以下载到本地查看
3. 生产环境部署服务
3.1 环境服务列表
需要在虚拟机或者linux服务器启动运行环境
服务 | ip | 端口 | 备注 |
---|---|---|---|
mysql | 172.18.0.10 | 3306 | 数据库服务 |
rabbitMQ | 172.18.0.20 | 5672,5672 | RabbitMQ消息服务 |
redis | 172.18.0.30 | 6379 | Redis缓存服务 |
nacos | 172.18.0.40 | 8848 | 微服务注册中心 |
skywalking | 172.18.0.50 | 1234,11800,12800 | 链路追踪APM服务端 |
skywalking-ui | 172.18.0.60 | 8080 | 链路追踪APM服务UI端 |
3.2 应用服务列表
应用服务可以单独部署或者在idea中启动
服务 | ip | 端口 | 备注 |
---|---|---|---|
order-service | 192.168.64.177 | 8001 | 订单服务 |
account-service | 192.168.64.177 | 8002 | 账户服务 |
storage-service | 192.168.64.177 | 8003 | 数据存储服务 |
notice-service | 192.168.64.177 | 8004 | 通知服务 |
3.3 docker-compose 编排环境
我们的docker-compose只对环境进行了搭建,具体微服务在本地运行或者在容器运行都可以。
1 | version: '2' |
3.4 初始化数据
初始化用户数据以及产品数据
将feign,hystrix,ribbon等统一配置配置到nacos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46# 配置超时时间
feign:
hystrix:
enabled: true #开启熔断
httpclient:
enabled: true
hystrix:
threadpool:
default:
coreSize: 50
maxQueueSize: 1500
queueSizeRejectionThreshold: 1000
command:
default:
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 10000
ReadTimeout: 50000
3.5 准备工作
3.5.1 安装Agent
将部署在本地的agent复制到
centos
主机中
3.5.2 打包微服务
将微服务打包并上传到centos中
打包后并上传到centos中去
3.5.3 配置启动脚本
3.5.3.1 编写启动脚本
在启动脚本中加入对于jvm内存的限制以及对
skywalking
的配置
1 | vi start.sh |
3.5.3.2 创建一个log目录
1 | mkdir logs |
3.5.3.3 配置sh权限
1 | chmod 755 start.sh |
3.5.3.4 完整结构如下
3.5.4 配置数据库访问权限
需要配置数据库的外网访问权限
1 | GRANT ALL PRIVILEGES ON fulllink.* TO 'root'@'%' IDENTIFIED BY 'root'; |
3.6 启动微服务
3.6.1 执行启动脚本
1 | # 启动微服务集群 |
3.6.2 nacos查看注册列表
访问
http://116.62.213.90:9105/nacos
进入nacos注册列表
我们发现服务都已经注册了
3.6.3 测试接口
使用postman测试服务接口是否正常,我们访问发现是是可以正常访问的
3.6.4 查看skyworking
4. 压测调优
4.1 jmeter配置
配置好压测数据,并且配置压测线程数1000 进行10轮压测
4.2 第一轮压测
4.2.1 链路分析优化
我们找到一个调用时长1S左右的链路,分析发现在存储服务调用时,耗时较长,但是数据库调用耗时并不长,基本说明是存储服务的连接池耗尽导致调用过长。
4.2.1.1 数据库连接池优化
调整存储服务的连接池,由原来的最大10 改为100
1 | initialSize: 10 |
4.2.2 查看JVM内存
我们发现jvm的中的老年代并没有太大的变化,说明我们的JVM不需要怎么优化
并且我们发现我们的eden区每隔一段时间就会垃圾回收一次,并且很少有晋升到老年代的情况
4.3 第二轮压测
结果已经由原来的服务内部的耗时 变为了fegin的耗时,这种情况下可以考虑使用fegin的连接池优化或者新增节点
4.3.1 观察消费节点
发现消费速度很慢,产生了大量消息堆积
检查
storage-service
的actualPlaceOrder
端点信息
发现平均响应时间在200ms左右
检查断点链路/storage/order/actualPlaceOrder
发现是事务提交慢造成的,这个时候就需要优化mysql服务器了
5. Skywalking 使用(自学)
5.1 Skywalking 模块栏目
Skywalking web UI 主要包括如下几个大的功能模块:
- 仪表盘:查看被监控服务的运行状态
- 拓扑图:以拓扑图的方式展现服务直接的关系,并以此为入口查看相关信息
- 追踪:以接口列表的方式展现,追踪接口内部调用过程
- 性能剖析:单独端点进行采样分析,并可查看堆栈信息
- 告警:触发告警的告警列表,包括实例,请求超时等。
- 自动刷新:刷新当前数据内容。
5.2 仪表盘
- 第一栏:不同内容主题的监控面板,应用/数据库/容器等
- 第二栏:操作,包括编辑/导出当前数据/倒入展示数据/不同服务端点筛选展示
- 第三栏:不同纬度展示,服务/实例/端点
9.3 展示栏
9.3.1 Global全局维度
- 第一栏:Global、Server、Instance、Endpoint不同展示面板,可以调整内部内容
- Services load:服务每分钟请求数
- Slow Services:慢响应服务,单位ms
- Un-Health services(Apdex):Apdex性能指标,1为满分。
- Global Response Latency:百分比响应延时,不同百分比的延时时间,单位ms
- Global Heatmap:服务响应时间热力分布图,根据时间段内不同响应时间的数量显示颜色深度
- 底部栏:展示数据的时间区间,点击可以调整。
5.3.2 Service服务维度
- Service Apdex(数字):当前服务的评分
- Service Apdex(折线图):不同时间的Apdex评分
- Successful Rate(数字):请求成功率
- Successful Rate(折线图):不同时间的请求成功率
- Servce Load(数字):每分钟请求数
- Servce Load(折线图):不同时间的每分钟请求数
- Service Avg Response Times:平均响应延时,单位ms
- Global Response Time Percentile:百分比响应延时
- Servce Instances Load:每个服务实例的每分钟请求数
- Show Service Instance:每个服务实例的最大延时
- Service Instance Successful Rate:每个服务实例的请求成功率
5.3.3 Instance实例维度
- Service Instance Load:当前实例的每分钟请求数
- Service Instance Successful Rate:当前实例的请求成功率
- Service Instance Latency:当前实例的响应延时
- JVM CPU:jvm占用CPU的百分比
- JVM Memory:JVM内存占用大小,单位m
- JVM GC Time:JVM垃圾回收时间,包含YGC和OGC
- JVM GC Count:JVM垃圾回收次数,包含YGC和OGC
- CLR XX:类似JVM虚拟机,这里用不上就不做解释了
5.3.4 Endpoint端点(API)维度
- Endpoint Load in Current Service:每个端点的每分钟请求数
- Slow Endpoints in Current Service:每个端点的最慢请求时间,单位ms
- Successful Rate in Current Service:每个端点的请求成功率
- Endpoint Load:当前端点每个时间段的请求数据
- Endpoint Avg Response Time:当前端点每个时间段的请求行响应时间
- Endpoint Response Time Percentile:当前端点每个时间段的响应时间占比
- Endpoint Successful Rate:当前端点每个时间段的请求成功率
5.4 拓扑图
- 1:选择不同的服务关联拓扑
- 2:查看单个服务相关内容
- 3:服务间连接情况
- 4:分组展示服务拓扑
5.5 追踪
- 左侧:api接口列表,红色-异常请求,蓝色-正常请求
- 右侧:api追踪列表,api请求连接各端点的先后顺序和时间
5.6 性能剖析
- 服务:需要分析的服务
- 端点:链路监控中端点的名称,可以再链路追踪中查看端点名称
- 监控时间:采集数据的开始时间
- 监控持续时间:监控采集多长时间
- 起始监控时间:多少秒后进行采集
- 监控间隔:多少秒采集一次
- 最大采集数:最大采集多少样本
查看监控结果
6. Java Agent 技术
我们上面讲到的skywalking以及Arthas都用到了
Java Agent
字节码技术
6.1 什么是Java Agent
笼统地来讲,Java Agent 是一个统称,该功能是 Java 虚拟机提供的一整套后门。通过这套后门可以对虚拟机方方面面进行监控与分析。甚至干预虚拟机的运行。
Java Agent 又叫做 Java 探针,Java Agent 是在 JDK1.5 引入的,是一种可以动态修改 Java 字节码的技术。Java 类编译之后形成字节码被 JVM 执行,在 JVM 在执行这些字节码之前获取这些字节码信息,并且通过字节码转换器对这些字节码进行修改,来完成一些额外的功能,这种就是 Java Agent 技术。
从用户使用层面来看,Java Agent 一般通过在应用启动参数中添加 -javaagent 参数添加 ClassFileTransformer 字节码转换器。 在 Java 虚拟机启动时,执 行main() 函数之前,Java 虚拟机会先找到 -javaagent 命令指定 jar 包,然后执行 premain-class 中的 premain() 方法。用一句概括其功能的话就是:main() 函数之前的一个拦截器。
6.2 Java Agent能做什么
从上面提到的字节码转换器的两种执行方式来看可以实现如下功能:
- Java Agent 能够在加载 Java 字节码之前进行拦截并对字节码进行修改;
- 在 Jvm 运行期间修改已经加载的字节码;
因此,通过以上两点即可实现在一些框架或是技术的采集点进行字节码修改,对应用进行监控(比如通过JVM CPU Profiler 从CPU、Memory、Thread、Classes、GC等多个方面对程序进行动态分析),或是对执行指定方法或接口时做一些额外操作,比如打印日志、打印方法执行时间、采集方法的入参和结果等;
基于前面对 Java Agent 大致机制的描述,我们不难猜到,能够干预 Java JVM 虚拟机的运行,那么就可以解决不限于如下的问题:
- 使用 JVMTI 对 class 文件加密:有时一些涉及到关键技术的 class 文件或者 jar 包我们不希望对外暴露,因而需要进行加密。使用一些常规的手段(例如使用混淆器或者自定义类加载器)来对 class 文件进行加密很容易被反编译。反编译后的代码虽然增加了阅读的难度,但花费一些功夫也是可以读懂的。使用 JVMTI 我们可以将解密的代码封装成 .dll, 或 .so 文件。这些文件想要反编译就很麻烦了,另外还能加壳。解密代码不能被破解,从而也就保护了我们想要加密的 class 文件。
- 使用 JVMTI 实现应用性能监控(APM) 在微服务大行其道的环境下,分布式系统的逻辑结构变得越来越复杂。这给系统性能分析和问题定位带来了非常大的挑战。基于JVMTI的APM能够解决分布式架构和微服务带来的监控和运维上的挑战。APM通过汇聚业务系统各处理环节的实时数据,分析业务系统各事务处理的交易路径和处理时间,实现对应用的全链路性能监测。开源的Skywalking、Pinpoint,、ZipKin、 Hawkular, 商业的 AppDynamics、OneAPM、Google Dapper等都是个中好手。
6.3 Instrument
Javaagent是java命令的一个参数。参数 javaagent 可以用于指定一个 jar 包,并且对该 java 包有2个要求:
- 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。
- Premain-Class 指定的那个类必须实现 premain() 方法。
6.3.1 premain方法
premain 方法,从字面上理解,就是运行在 main 函数之前的的类。当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行-javaagent
所指定 jar 包内 Premain-Class 这个类的 premain 方法 。
6.4 Javaagent命令
在命令行输入
java
可以看到相应的参数,其中有 和 java agent相关的:
1 | -agentlib:<libname>[=<选项>] 加载本机代理库 <libname>, 例如 -agentlib:hprof |
在上面-javaagent
参数中提到了参阅java.lang.instrument
,这是在rt.jar
中定义的一个包,该路径下有两个重要的类:
该包提供了一些工具帮助开发人员在 Java 程序运行时,动态修改系统中的 Class 类型。其中,使用该软件包的一个关键组件就是 Javaagent。从名字上看,似乎是个 Java 代理之类的,而实际上,他的功能更像是一个Class 类型的转换器,他可以在运行时接受重新外部请求,对Class类型进行修改。
从本质上讲,Java Agent 是一个遵循一组严格约定的常规 Java 类。 上面说到 javaagent命令要求指定的类中必须要有premain()方法,并且对premain方法的签名也有要求,签名必须满足以下两种格式:
1 | public static void premain(String agentArgs, Instrumentation inst) |
JVM 会优先加载 带 Instrumentation
签名的方法,加载成功忽略第二种,如果第一种没有,则加载第二种方法。这个逻辑在sun.instrument.InstrumentationImpl 类中:
6.5 Instrumentation定义
1 | public interface Instrumentation { |
6.6 使用 javaagent 需要几个步骤
- 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine-Classes 和 Can-Retransform-Classes 选项。
- 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
- 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
- 使用参数 -javaagent: jar包路径 启动要代理的方法。
在执行以上步骤后,JVM 会先执行 premain 方法,大部分类加载都会通过该方法,注意:是大部分,不是所有。当然,遗漏的主要是系统类,因为很多系统类先于 agent 执行,而用户类的加载肯定是会被拦截的。也就是说,这个方法是在 main 方法启动前拦截大部分类的加载活动,既然可以拦截类的加载,那么就可以去做重写类这样的操作,结合第三方的字节码编译工具,比如ASM,javassist,cglib等等来改写实现类。
通过上面的步骤我们用代码实现来实现。实现 javaagent 你需要搭建两个工程,一个工程是用来承载 javaagent类,单独的打成jar包;一个工程是javaagent需要去代理的类。即javaagent会在这个工程中的main方法启动之前去做一些事情。
6.7 入门案例
6.7.1 代码结构
1 | ├─com |
6.7.2 HelloWorldAgent代理类
1 | public class HelloWorldAgent { |
上面就是我实现的一个类,实现了带Instrumentation参数的premain()方法。调用addTransformer()方法对启动时所有的类进行拦截。
6.7.3 MANIFREST文件
打包项目后 resources 目录下会自动在
META-INF
目录下创建文件:MANIFREST.MF:
1 | Manifest-Version: 1.0 |
说一下MANIFREST.MF文件的作用,这里如果你不去手动指定的话,直接 打包,默认会在打包的文件中生成一个MANIFREST.MF文件:
1 | Manifest-Version: 1.0 |
这是默认的文件,包含当前的一些版本信息,当前工程的启动类,它还有别的参数允许你做更多的事情。
6.7.4 参数含义
- Premain-Class :包含 premain 方法的类(类的全路径名)
- Agent-Class :包含 agentmain 方法的类(类的全路径名)
- Boot-Class-Path :设置引导类加载器搜索的路径列表。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。(可选)
- Can-Redefine-Classes :true表示能重定义此代理所需的类,默认值为 false(可选)
- Can-Retransform-Classes :true 表示能重转换此代理所需的类,默认值为 false (可选)
- Can-Set-Native-Method-Prefix: true表示能设置此代理所需的本机方法前缀,默认值为 false(可选)
即在该文件中主要定义了程序运行相关的配置信息,程序运行前会先检测该文件中的配置项。
一个java程序中-javaagent
参数的个数是没有限制的,所以可以添加任意多个javaagent。所有的java agent会按照你定义的顺序执行,例如:
1 | java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar |
程序执行的顺序将会是:
MyAgent1.premain -> MyAgent2.premain -> MyProgram.main
6.7.5 maven 编译配置
说回上面的 javaagent工程,接下来将该工程打成jar包,在打包的时候发现打完包之后的 MANIFREST.MF文件被默认配置替换掉了。所以需要手动将上面我的配置文件替换到jar包中的文件,会比较麻烦,maven提供了对应的打包插件。
1 | <plugin> |
用这种插件的方式也可以自动生成该文件。
6.7.6 测试类编写
agent代码就写完了,下面再重新开一个工程,你只需要写一个带 main 方法的类即可:
1 | public class JavaAgentTest { |
6.7.7 运行测试
6.7.7.1 Idea测试
如果你用的是idea,那么你可以点击菜单: run-debug configuration,然后将你的代理类包 指定在 启动参数中即可:
6.7.7.2 命令方式运行
另一种方式是不用 编译器,采用命令行的方法。与上面大致相同,将 上面的测试类编译成 class文件,然后 运行该类即可:
1 | #将该类编译成class文件 |
6.7.8 运行结果
使用上面两种方式都可以运行,输出结果如下:
1 | Hi, This is a agent! |
上面的输出结果我们能够发现
- 执行main方法之前会加载所有的类,包括系统类和自定义类;
- 在ClassFileTransformer中会去拦截系统类和自己实现的类对象;
- 如果你有对某些类对象进行改写,那么在拦截的时候抓住该类使用字节码编译工具即可实现。