全链路压测的整体架构设计,以及 5 种实现方案(流量染色、数据隔离、接口隔离、零侵入、服务监控)
今日这篇文章接上文,今天我们继续聊聊全链路压测的具体架构设计以及 5 种实现方案流量染色方案、数据隔离方案、接口隔离方案、零侵入方案、服务监控方案。
看不懂的小伙伴,可以先看这个:
业务模块介绍
现在我们对整体的业务进行介绍以及演示
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.1.2.4 数据的分析
数据的分析要快 ,分析的维度尽可能多 **。跟踪系统能提供足够快的信息反馈,就可以对生产环境下的异常状况做出快速反应。** 分析的全面,能够避免二次开发。
5.2 全链路压测核心技术
上面从总体架构层面分析了全链路压测的核心,下面就分析下全链路压测用到的核心技术点
5.2.1 全链路流量染色
做到微服务和中间件的染色标志的穿透
通过压测平台对输出的压力请求打上标识,在订单系统中提取压测标识,确保完整的程序上下文都持有该标识,并且能够穿透微服务以及各种中间件,比如 MQ,hystrix,Fegin 等。
5.2.2 全链路服务监控
需要能够实时监控服务的运行状况以及分析服务的调用链,我们采用 skywalking 进行服务监控和压测分析
5.2.3 全链路日志隔离
做到日志隔离,防止污染生产日志
当订单系统向磁盘或外设输出日志时,若流量是被标记的压测流量,则将日志隔离输出,避免影响生产日志。
5.2.4 全链路风险熔断
流量控制,防止流量超载,导致集群不可用
当订单系统访问会员系统时,通过 RPC 协议延续压测标识到会员系统,两个系统之间服务通讯将会有白黑名单开关来控制流量流入许可。该方案设计可以一定程度上避免下游系统出现瓶颈或不支持压测所带来的风险,这里可以采用 Sentinel 来实现风险熔断。
5.3 全链路数据隔离
对各种存储服务以及中间件做到数据隔离,方式数据污染
2.3.1 数据库隔离
当会员系统访问数据库时,在持久化层同样会根据压测标识进行路由访问压测数据表。数据隔离的手段有多种,比如影子库、影子表,或者影子数据,三种方案的仿真度会有一定的差异,他们的对比如下。
隔离性 | 兼容性 | 安全级别 | 技术难度 | |
---|---|---|---|---|
影子库 | 高 | 高 | 高 | 高 |
影子表 | 中 | 低 | 中 | 中 |
影子数据 | 低 | 低 | 低 | 低 |
5.3.2 消息队列隔离
当我们生产的消息扔到 MQ 之后,接着让消费者进行消费,这个没有问题,压测的数据不能够直接扔到 MQ 中的,因为它会被正常的消费者消费到的,要做好数据隔离,方案有队列隔离,消息隔离,他们对比如下。
隔离性 | 兼容性 | 安全级别 | 技术难度 | |
---|---|---|---|---|
队列隔离 | 高 | 好 | 高 | 高 |
消息隔离 | 低 | 低 | 低 | 中 |
5.3.3 Redis 隔离
通过 key 值来区分,压测流量的 key 值加统一后缀,通过改造 RedisTemplate 来实现 key 的路由。
框架实现
6.1 流量染色方案
上面分析了从整体分析了全链路压测用的的核心技术,下面就来实现第一个流量染色。
6.1.1 流量识别
要想压测的流量和数据不影响线上真实的生产数据,就需要线上的集群能识别出压测的流量,只要能识别出压测请求的流量,那么流量触发的读写操作就很好统一去做隔离了。
全链路压测发起的都是 Http 的请求,只需要要请求头上添加统一的压测请求头。
通过在请求协议中添加压测请求的标识,在不同服务的相互调用时,一路透传下去,这样每一个服务都能识别出压测的请求流量,这样做的好处是与业务完全的解耦,只需要应用框架进行感知,对业务方代码无侵入。
6.1.2 MVC 接收数据
客户端传递过来的数据可以通过获取 Header 的方式获取到,并将其设置进当前的 ThreadLocal,交给后面的方法使用。
6.1.2.1 MVC 拦截器实现
/**
* 链路跟踪Request设置值
*/
public class MvcWormholeWebInterceptor implements WebRequestInterceptor {
@Override
public void preHandle(WebRequest webRequest) {
//失效上下文,解决Tomcat线程复用问题
WormholeContextHolder.invalidContext();
String wormholeValue = webRequest.getHeader(WormholeContextHolder.WORMHOLE_REQUEST_MARK);
if (StringUtils.isNotEmpty(wormholeValue)) {
WormholeContextHolder.setContext(new WormholeContext(wormholeValue));
}
}
@Override
public void postHandle(WebRequest webRequest, ModelMap modelMap) throws Exception {
}
@Override
public void afterCompletion(WebRequest webRequest, Exception e) throws Exception {
}
}
6.1.2.2 Tomcat 线程复用问题
tomcat 默认使用线程池来管理线程,一个请求过来,如果线程池里面有空闲的线程,那么会在线程池里面取一个线程来处理该请求,一旦该线程当前在处理请求,其他请求就不会被分配到该线程上,直到该请求处理完成。请求处理完成后,会将该线程重新加入线程池,因为是通过线程池复用线程,就会如果线程内部的 ThreadLocal 没有清除就会出现问题,需要新的请求进来的时候,清除 ThreadLocal。
6.1.3 Fegin 传递传递染色标识
我们项目的微服务是使用 Fegin 来实现远程调用的,跨微服务传递染色标识是通过 MVC 拦截器获取到请求 Header 的染色标识,并放进 ThreadLocal 中,然后交给 Fegin 拦截器在发送请求之前从 ThreadLocal 中获取到染色标识,并放进 Fegin 构建请求的 Header 中,实现微服务之间的火炬传递。
6.1.3.1 代码实现
public class WormholeFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
WormholeContext wormholeContext = WormholeContextHolder.getContext();
if (null != wormholeContext) {
requestTemplate.header(WormholeContextHolder.WORMHOLE_REQUEST_MARK, wormholeContext.toString());
}
}
}
6.1.4 Hystrix 传递染色标识
6.1.4.1 Hystrix 隔离技术
Hystrix 实现资源隔离,主要有两种技术:
信号量
信号量的资源隔离只是起到一个开关的作用,比如,服务 A 的信号量大小为 10,那么就是说它同时只允许有 10 个 tomcat 线程来访问服务 A,其它的请求都会被拒绝,从而达到资源隔离和限流保护的作用。
线程池
线程池隔离技术,是用 Hystrix 自己的线程去执行调用;而信号量隔离技术,是直接让 tomcat 线程去调用依赖服务。信号量隔离,只是一道关卡,信号量有多少,就允许多少个 tomcat 线程通过它,然后去执行。
6.1.4.2 Hystrix 穿透
如果使用线程池模式,那么存在一个 ThreadLocal 变量跨线程传递的问题,即在主线程的 ThreadLocal 变量,无法在线程池中使用,不过 Hystrix 内部提供了解决方案。
封装 Callable 任务
public final class DelegatingWormholeContextCallable<V> implements Callable<V> {
private final Callable<V> delegate;
// 用户信息上下文(根据项目实际情况定义ThreadLocal上下文)
private WormholeContext orginWormholeContext;
public DelegatingWormholeContextCallable(Callable<V> delegate,
WormholeContext wormholeContext) {
this.delegate = delegate;
this.orginWormholeContext = wormholeContext;
}
public V call() throws Exception {
//防止线程复用销毁ThreadLocal的数据
WormholeContextHolder.invalidContext();
// 将当前的用户上下文设置进Hystrix线程的TreadLocal中
WormholeContextHolder.setContext(orginWormholeContext);
try {
return delegate.call();
} finally {
// 执行完毕,记得清理ThreadLocal资源
WormholeContextHolder.invalidContext();
}
}
public static <V> Callable<V> create(Callable<V> delegate,
WormholeContext wormholeContext) {
return new DelegatingWormholeContextCallable<V>(delegate, wormholeContext);
}
}
实现 Hystrix 的并发策略类
因为 Hystrix 默认的并发策略不支持 ThreadLocal 传递,我们可以自定义并发策略类继承 HystrixConcurrencyStrategy
public class ThreadLocalAwareStrategy extends HystrixConcurrencyStrategy {
// 最简单的方式就是引入现有的并发策略,进行功能扩展
private final HystrixConcurrencyStrategy existingConcurrencyStrategy;
public ThreadLocalAwareStrategy(
HystrixConcurrencyStrategy existingConcurrencyStrategy) {
this.existingConcurrencyStrategy = existingConcurrencyStrategy;
}
@Override
public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy.getBlockingQueue(maxQueueSize)
: super.getBlockingQueue(maxQueueSize);
}
@Override
public <T> HystrixRequestVariable<T> getRequestVariable(
HystrixRequestVariableLifecycle<T> rv) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy.getRequestVariable(rv)
: super.getRequestVariable(rv);
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
HystrixProperty<Integer> corePoolSize,
HystrixProperty<Integer> maximumPoolSize,
HystrixProperty<Integer> keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy.getThreadPool(threadPoolKey, corePoolSize,
maximumPoolSize, keepAliveTime, unit, workQueue)
: super.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize,
keepAliveTime, unit, workQueue);
}
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy
.wrapCallable(new DelegatingWormholeContextCallable<>(callable, WormholeContextHolder.getContext()))
: super.wrapCallable(new DelegatingWormholeContextCallable<T>(callable, WormholeContextHolder.getContext()));
}
}
Hystrix 注入新并发策略并进行刷新
public class HystrixThreadLocalConfiguration {
@Autowired(required = false)
private HystrixConcurrencyStrategy existingConcurrencyStrategy;
@PostConstruct
public void init() {
// Keeps references of existing Hystrix plugins.
HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance()
.getEventNotifier();
HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance()
.getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance()
.getPropertiesStrategy();
HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance()
.getCommandExecutionHook();
HystrixPlugins.reset();
HystrixPlugins.getInstance().registerConcurrencyStrategy(new ThreadLocalAwareStrategy(existingConcurrencyStrategy));
HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
}
}
6.2 数据隔离方案
6.2.1 JDBC 数据源隔离
数据隔离需要对 DB,Redis,RabbitMQ 进行数据隔离
通过实现 Spring 动态数据源 AbstractRoutingDataSource
, 通过 ThreadLocal
识别出来压测数据,如果是压测数据就路由到影子库,如果是正常流量则路由到主库,通过流量识别的改造,各个服务都已经能够识别出压测的请求流量了。
6.2.1.1 代码实现
数据源路由 Key 持有对象
根据路由 Key 将选择将操作路由给那个数据源
/**
* 动态数据源上下文
*/
public class DynamicDataSourceContextHolder {
public static final String PRIMARY_DB = "primary";
public static final String SHADOW_DB = "shadow";
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
/**
* 将 master 数据源的 key作为默认数据源的 key
*/
@Override
protected String initialValue() {
return PRIMARY_DB;
}
};
/**
* 数据源的 key集合,用于切换时判断数据源是否存在
*/
public static List<Object> dataSourceKeys = new ArrayList<>();
/**
* 切换数据源
*
* @param key
*/
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
/**
* 获取数据源
*
* @return
*/
public static String getDataSourceKey() {
return contextHolder.get();
}
/**
* 重置数据源
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}
/**
* 判断是否包含数据源
*
* @param key 数据源key
* @return
*/
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
/**
* 添加数据源keys
*
* @param keys
* @return
*/
public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
return dataSourceKeys.addAll(keys);
}
}
动态数据源实现类
根据路由 Key 实现数据源的切换
/**
* 动态数据源实现类
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 如果不希望数据源在启动配置时就加载好,可以定制这个方法,从任何你希望的地方读取并返回数据源
* 比如从数据库、文件、外部接口等读取数据源信息,并最终返回一个DataSource实现类对象即可
*/
@Override
protected DataSource determineTargetDataSource() {
//获取当前的上下文
WormholeContext wormholeContext = WormholeContextHolder.getContext();
//如果不为空使用影子库
if (null != wormholeContext) {
DynamicDataSourceContextHolder.setDataSourceKey(DynamicDataSourceContextHolder.SHADOW_DB);
} else {
//为空则使用主数据源
DynamicDataSourceContextHolder.setDataSourceKey(DynamicDataSourceContextHolder.PRIMARY_DB);
}
return super.determineTargetDataSource();
}
/**
* 如果希望所有数据源在启动配置时就加载好,这里通过设置数据源Key值来切换数据,定制这个方法
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}
6.2.2 Redis 数据源隔离
同时通过 ThreadLocal
识别出来压测数据,自定义 Redis 的主键的序列化方式,如果是压测数据则在主键后面加上后缀,这样就可以通过不同主键将 Redis 数据进行隔离。
6.2.2.1 实现 key 序列化
public class KeyStringRedisSerializer extends StringRedisSerializer {
@Resource
private WormholeIsolationConfiguration isolationConfiguration;
public byte[] serialize(@Nullable String redisKey) {
WormholeContext wormholeContext = WormholeContextHolder.getContext();
if (null != wormholeContext) {
redisKey = isolationConfiguration.generateIsolationKey(redisKey);
}
return super.serialize(redisKey);
}
}
6.2.2.2 配置序列化器
/**
* Redis 配置类
*/
@Configuration
@ConditionalOnClass({RedisTemplate.class, RedisOperations.class, RedisConnectionFactory.class})
public class WormholeRedisAutoConfiguration {
@Bean
public KeyStringRedisSerializer keyStringRedisSerializer() {
return new KeyStringRedisSerializer();
}
@Bean("redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate template = new RedisTemplate();
//使用fastjson序列化
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(keyStringRedisSerializer());
template.setHashKeySerializer(keyStringRedisSerializer());
template.setConnectionFactory(factory);
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setKeySerializer(keyStringRedisSerializer());
template.setHashKeySerializer(keyStringRedisSerializer());
template.setConnectionFactory(factory);
return template;
}
}
6.2.3 RabbitMQ 数据隔离
6.2.3.1 自动创建影子队列
因为 SpringAMQP 中的
中的关键方法是私有的,无法通过继承的方式进行实现对以配置好的队列进行扩展,所以需要自定义该类,来实现对自动创建影子队列,并和交换器进行绑定
代码实现
改造 RabbitListenerAnnotationBeanPostProcessor
类来实现创建 MQ 影子队列以及将影子 Key 绑定到影子队列。
public class WormholeRabbitListenerAnnotationBeanPostProcessor extends RabbitListenerAnnotationBeanPostProcessor {
@Resource
private WormholeIsolationConfiguration wormholeIsolationConfiguration;
/**
* routingKey 前置处理器
*
* @param queueName
* @param routingKey
* @return
*/
@Override
public String preProcessingRoutingKey(String queueName, String routingKey) {
//如果是影子队列就将routingKey转换为 影子routingKey
if (wormholeIsolationConfiguration.checkIsolation(queueName) && !wormholeIsolationConfiguration.checkIsolation(routingKey)) {
return wormholeIsolationConfiguration.generateIsolationKey(routingKey);
}
return routingKey;
}
/**
* 处理队列问题,如果来了一个队列就生成一个shadow的队列
*
* @param queues
* @return
*/
@Override
public List<String> handelQueues(List<String> queues) {
List<String> isolationQueues = new ArrayList<>();
if (null != queues && !queues.isEmpty()) {
for (String queue : queues) {
//添加shadow队列
isolationQueues.add(wormholeIsolationConfiguration.generateIsolationKey(queue));
}
queues.addAll(isolationQueues);
}
return queues;
}
}
6.2.3.2 传递染色标识
因为 MQ 是异步通讯,为了传递染色标识,会在发送 MQ 的时候将染色标识传递过来,MQ 接收到之后放进当前线程的 ThreadLocal
里面,这个需要扩展 Spring 的 SimpleRabbitListenerContainerFactory
来实现
代码实现
public class WormholeSimpleRabbitListenerContainerFactory extends SimpleRabbitListenerContainerFactory {
@Override
protected SimpleMessageListenerContainer createContainerInstance() {
SimpleMessageListenerContainer simpleMessageListenerContainer = new SimpleMessageListenerContainer();
simpleMessageListenerContainer.setAfterReceivePostProcessors(message -> {
//防止线程复用 销毁ThreadLocal
WormholeContextHolder.invalidContext();
//获取消息属性标识
String wormholeRequestContext = message.getMessageProperties().getHeader(WormholeContextHolder.WORMHOLE_REQUEST_MARK);
if (StringUtils.isNotEmpty(wormholeRequestContext)) {
WormholeContextHolder.setContext(wormholeRequestContext);
}
return message;
});
return simpleMessageListenerContainer;
}
}
6.2.3.3 发送 MQ 消息处理
同上,需要传递染色标识,就通过继承 RabbitTemplate
重写 convertAndSend
方法来实现传递染色标识。
public class ShadowRabbitTemplate extends RabbitTemplate {
public ShadowRabbitTemplate(ConnectionFactory connectionFactory) {
super(connectionFactory);
}
@Autowired
private WormholeIsolationConfiguration isolationConfiguration;
@Override
public void send(final String exchange, final String routingKey,
final Message message, @Nullable final CorrelationData correlationData)
throws AmqpException {
WormholeContext wormholeContext = WormholeContextHolder.getContext();
if (null == wormholeContext) {
super.send(exchange, routingKey, message, correlationData);
} else {
message.getMessageProperties().setHeader(WormholeContextHolder.WORMHOLE_REQUEST_MARK, wormholeContext.toString());
//生成Rabbit 隔离Key
String wormholeRoutingKey = isolationConfiguration.generateIsolationKey(routingKey);
//调用父类进行发送
super.send(exchange, wormholeRoutingKey, message, correlationData);
}
}
}
6.3 接口隔离方法
6.3.1 Mock 第三方接口
对于第三方数据接口需要进行隔离,比如短信接口,正常的数据需要发送短信,对于压测数据则不能直接调用接口发送短信,并且需要能够识别出来压测数据,并进行 MOCK 接口调用。
6.3.1.1 核心类实现
@Aspect
public class WormholeMockSection {
/**
* 切点 拦截@WormholeMock的注解
*/
@Pointcut("@annotation(com.heima.wormhole.component.mock.annotation.WormholeMock)")
public void pointCut() {
}
/**
* 环绕通知
*
* @param point
* @return
* @throws Throwable
*/
@Around("pointCut()")
public Object section(ProceedingJoinPoint point) throws Throwable {
WormholeContext wormholeContext = WormholeContextHolder.getContext();
Object[] parameter = point.getArgs();
//如果没有wormholeContext 就执行正常方法
if (null == wormholeContext) {
return point.proceed(parameter);
}
//如果存在就执行MOCK方法
WormholeMock wormholeMock = WormholeMockUtils.getMethodAnnotation(point, WormholeMock.class);
if (null != wormholeMock) {
//获取到 Mock 回调类
WormholeMockCallback wormholeMockCallback = WormholeMockUtils.getWormholeMockCallback(wormholeMock);
if (null != wormholeMockCallback) {
return wormholeMockCallback.handelMockData(parameter);
}
}
return null;
}
}
6.3.1.2 使用方式
在具体方法上面加上注解就可以使用了
@Override
//加入注解进行MOCK测试拦截 设置最大耗时
@WormholeMock(maxDelayTime = 10, minDelayTime = 2)
public boolean send(NotifyVO notifyVO) {
logger.info("开始发送短信通知.....");
try {
//模拟发送短信耗时
Thread.sleep(5);
} catch (InterruptedException e) {
}
return true;
}
6.4 零侵入方案
如果开发的中间件需要各个微服务大量改造,对开发人员来说就是一个灾难,所以这里采用零侵入的 springboot starter 来解决
6.4.1 自动装配
使用微服务得
@Conditional
来完成配置得自动装配,这里用 MVC 得配置来演示自动装配,其他得都是类似这样可以最大限度的优化代码并提高很高的可扩展性。
/**
* MVC 自动装配
*/
@Configuration
//当DispatcherServlet存在时该配置类才会被执行到
@ConditionalOnClass(org.springframework.web.servlet.DispatcherServlet.class)
public class WormholeMVCAutoConfiguration {
@ConditionalOnClass
@Bean
public WormholeMVCConfiguration wormholeMVCConfiguration() {
return new WormholeMVCConfiguration();
}
}
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 文件,将我们的自动配置类配置进去
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.heima.wormhole.autoconfiguration.WormholeAutoConfiguration
6.5 服务监控方案
6.5.1 skywalking 简介
Skywalking 是一个 APM 系统,即应用性能监控系统,为微服务架构和云原生架构系统设计。它通过探针自动收集所需的指标,并进行分布式追踪。通过这些调用链路以及指标,Skywalking APM 会感知应用间关系和服务间关系,并进行相应的指标统计。目前支持链路追踪和监控应用组件如下,基本涵盖主流框架和容器,如国产 PRC Dubbo 和 motan 等,国际化的 spring boot,spring cloud 都支持了
SkyWalking 是分布式系统的应用程序性能监视工具,专为微服务、云原生架构和基于容器(Docker、K8S、Mesos)架构而设计
SkyWalking 是观察性分析平台和应用性能管理系统。提供分布式追踪、服务网格遥测分析、度量聚合和可视化一体化解决方案
6.5.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
6.5.2 配置 SkyWalking
6.5.2.1 下载 SkyWalking
下载 SkyWalking 的压缩包 , 解压后将压缩包里面的 agent 文件夹放进本地磁盘,探针包含整个目录,请不要改变目录结构。
6.5.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 实现对分布式系统的追踪和相关数据的上下文传递。
6.5.2.3 配置探针
配置 SpringBoot 启动参数,需要填写如下的运行参数,代码放在后面,需要的自己粘贴。
-javaagent:D:/data/skywalking/agent/skywalking-agent.jar
-Dskywalking.agent.service_name=storage-server
-Dskywalking.collector.backend_service=172.18.0.50:11800
- javaagent:复制的 agent 目录下探针的 jar 包路径
- skywalking.agent.service_name:需要在 skywalking 显示的服务名称
- skywalking.collector.backend_service:skywalking 服务端地址默认是 11800