全链路压测的整体架构设计,以及5种实现方案流量染色方案、数据隔离方案、接口隔离方案、零侵入方案、服务监控方案【代码级别】

业务模块介绍

现在我们对整体的业务进行介绍以及演示

file

5. 全链路整体架构

上面介绍了为什么需要全链路压测,下面来看下全链路压测的整体架构。

    整体架构如下主要是对压测客户端的**压测数据染色**,全链路中间件识别出**染色数据**,并将正常数据和压测数据区分开,进行数据隔离,这里主要涉及到mysql数据库,RabbitMQ,Redis,还需要处理因为hystrix线程池不能通过**ThreadLocal**传递染色表示的问题。
file

5.1 需要应对的问题

5.1.1 业务问题

如何开展全链路压测?在说这个问题前,我们先考虑下,全链路压测有哪些问题比较难解决。

  1. 涉及的系统太多,牵扯的开发人员太多

     在压测过程中,做一个全链路的压测一般会涉及到大量的系统,在整个压测过程中,光各个产品的人员协调就是一个比较大的工程,牵扯到太多的产品经理和开发人员,如果公司对全链路压测早期没有足够的重视,那么这个压测工作是非常难开展的。
    
  2. 模拟的测试数据和访问流量不真实

     在压测过程中经常会遇到压测后得到的数据不准确的问题,这就使得压测出的数据参考性不强,为什么会产生这样的问题?主要就是因为压测的环境可能和生成环境存在误差、参数存在不一样的地方、测试数据存在不一样的地方这些因素综合起来导致测试结果的不可信。
    
  3. 压测生产数据未隔离,影响生产环境

     在全链路压测过程中,压测数据可能会影响到生产环境的真实数据,举个例子,电商系统在生产环境进行全链路压测的时候可能会有很多压测模拟用户去下单,如果不做处理,直接下单的话会导致系统一下子会产生很多废订单,从而影响到库存和生产订单数据,影响到日常的正常运营。
    
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进行服务监控和压测分析

file
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的请求,只需要要请求头上添加统一的压测请求头。

    通过在请求协议中添加压测请求的标识,在不同服务的相互调用时,一路透传下去,这样每一个服务都能识别出压测的请求流量,这样做的好处是与业务完全的解耦,只需要应用框架进行感知,对业务方代码无侵入。
file
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中,实现微服务之间的火炬传递。
file
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,其它的请求都会被拒绝,从而达到资源隔离和限流保护的作用。
file

线程池

    线程池隔离技术,是用 Hystrix 自己的线程去执行调用;而信号量隔离技术,是直接让 tomcat 线程去调用依赖服务。信号量隔离,只是一道关卡,信号量有多少,就允许多少个 tomcat 线程通过它,然后去执行。
file
6.1.4.2 Hystrix穿透
    如果使用线程池模式,那么存在一个ThreadLocal变量跨线程传递的问题,即在主线程的ThreadLocal变量,无法在线程池中使用,不过Hystrix内部提供了解决方案。
file

封装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数据源隔离
file

数据隔离需要对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 数据隔离
file
6.2.3.1 自动创建影子队列

因为SpringAMQP中的

中的关键方法是私有的,无法通过继承的方式进行实现对以配置好的队列进行扩展,所以需要自定义该类,来实现对自动创建影子队列,并和交换器进行绑定

file

代码实现

改造`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`来实现
file

代码实现

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接口调用。
file
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
file

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传过来的tracingmetric数据进行整合分析通过Analysis Core模块处理并落入相关的数据存储中,同时会通过Query Core模块进行二次统计和监控告警

  • Storage: Skywalking的存储,支持以ElasticSearchMysqlTiDBH2等作为存储介质进行数据存储

  • UI: Web可视化平台,用来展示落地的数据,目前官方采纳了RocketBot作为SkyWalking的主UI

6.5.2 配置SkyWalking
6.5.2.1 下载SkyWalking
    下载SkyWalking的[压缩包](https://www.apache.org/dyn/closer.cgi/skywalking/8.2.0/apache-skywalking-apm),解压后将压缩包里面的**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
file
  • javaagent:复制的agent目录下探针的jar包路径
  • skywalking.agent.service_name:需要在skywalking显示的服务名称
  • skywalking.collector.backend_service:skywalking服务端地址默认是11800

本文由育博学谷狂野架构师发布
如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力
转载请注明出处!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容