全链路压测框架设计思路

658 阅读3分钟

1. 架构图

jiagou.png

2. 染色流量透传

2.1 SpringMVC拦截器

这里要注意preHandle的时候一定要执行 FullLinkContextHolder.clear() 去清除染色标识,避免后面正常流量请求复用线程池里面的线程(这里的线程池是tomcat线程池)。

package com.hdu.mvcInterceptor;

import com.hdu.context.FullLinkContext;
import com.hdu.context.FullLinkContextHolder;
import org.springframework.ui.ModelMap;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.WebRequestInterceptor;

import static com.hdu.constant.FullLinkConstant.FULL_LINK_STRESS_HEADER;
import static com.hdu.constant.FullLinkConstant.IS_FULL_LINK_STRESS;

public class FullLinkMvcInterceptor implements WebRequestInterceptor {

    @Override
    public void preHandle(WebRequest webRequest) {
        // 防止tomcat线程池线程复用问题
        FullLinkContextHolder.clear();
        String header = webRequest.getHeader(FULL_LINK_STRESS_HEADER);
        if (IS_FULL_LINK_STRESS.equals(header)) {
            FullLinkContextHolder.markFullLinkStress(FullLinkContext.of());
        }
    }

    @Override
    public void postHandle(WebRequest webRequest, ModelMap modelMap) {
        FullLinkContextHolder.clear();
    }

    @Override
    public void afterCompletion(WebRequest webRequest, Exception e) {
        FullLinkContextHolder.clear();
    }
}

2.2. RPC拦截器增强

对于RPC,这里拿openFeign举例子。openFeign在发送http请求的时候,会依次调用 RequestInterceptor ,所以在此我们就可以通过 RequestInterceptor 将染色流量透传到下一个微服务

package com.hdu.feignInterceptor;

import com.hdu.context.FullLinkContext;
import com.hdu.context.FullLinkContextHolder;
import feign.RequestInterceptor;
import feign.RequestTemplate;

import java.util.Objects;

import static com.hdu.constant.FullLinkConstant.FULL_LINK_STRESS_HEADER;
import static com.hdu.constant.FullLinkConstant.IS_FULL_LINK_STRESS;

public class FeignFullLinkInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        FullLinkContext fullLinkContext = FullLinkContextHolder.getFullLinkContext();
        if (Objects.nonNull(fullLinkContext)) {
            // 染色流量透传
            requestTemplate.header(FULL_LINK_STRESS_HEADER, IS_FULL_LINK_STRESS);
        }
    }
}





2.3. Hystrix

对于微服务的保护我们会使用线程隔离技术,将每个下游微服务的请求隔离。所以我们需要让 Histricx 线程池里面的线程感知 染色流量。

这里采用的方案是包装提交到 Hystrix隔离线程池的任务,将染色流量标识透传到 Hystrix线程池中。

包装提交到 Hystrix隔离线程池的任务

import com.hdu.context.FullLinkContext;
import com.hdu.context.FullLinkContextHolder;

import java.util.concurrent.Callable;

public class DelegatingFullLinkContextCallable <V> implements Callable<V> {


    /**
     * 原始全链路压测上下文
     */
    private final FullLinkContext fullLinkContext;

    /**
     * 原始执行逻辑
     */
    private final Callable<V> original;


    public DelegatingFullLinkContextCallable(Callable<V> callable, FullLinkContext fullLinkContext) {
        this.original = callable;
        this.fullLinkContext = fullLinkContext;
    }


    @Override
    public V call() throws Exception {
        FullLinkContextHolder.clear();
        FullLinkContextHolder.markFullLinkStress(this.fullLinkContext);
        try {
            return this.original.call();
        }
        finally {
            FullLinkContextHolder.clear();
        }
    }
}

自定义 HystrixConcurrencyStrategy, 实现任务包装

import com.hdu.context.FullLinkContextHolder;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;

public class ThreadLocalAwareStrategy extends HystrixConcurrencyStrategy {


    private final HystrixConcurrencyStrategy existingStrategy;

    public ThreadLocalAwareStrategy(HystrixConcurrencyStrategy existingStrategy) {
        this.existingStrategy = existingStrategy;
    }

    @Override
    public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
        return existingStrategy != null
            ? existingStrategy.getBlockingQueue(maxQueueSize)
            : super.getBlockingQueue(maxQueueSize);
    }

    @Override
    public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) {
        return existingStrategy != null
            ? existingStrategy.getRequestVariable(rv)
            : super.getRequestVariable(rv);
    }

    @Override
    public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, HystrixThreadPoolProperties threadPoolProperties) {
        return existingStrategy != null
            ? existingStrategy.getThreadPool(threadPoolKey, threadPoolProperties)
            : super.getThreadPool(threadPoolKey, threadPoolProperties);
    }


    /**
     * 传递全链路压测上下文
     * @param callable
     * @return
     * @param <T>
     */
    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        return existingStrategy != null
            ? existingStrategy
            // 这里要将任务包装
            .wrapCallable(new DelegatingFullLinkContextCallable<>(callable, FullLinkContextHolder.getFullLinkContext()))
            // 这里要将任务包装
            : super.wrapCallable(new DelegatingFullLinkContextCallable<>(callable, FullLinkContextHolder.getFullLinkContext()));
    }
}

Hystrix配置类

注册 自定义 HystrixConcurrencyStrategy

import com.hdu.hystrix.ThreadLocalAwareStrategy;
import com.netflix.hystrix.strategy.HystrixPlugins;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier;
import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook;
import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher;
import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Configuration
public class FullLinkHystrixThreadLocalAutoConfiguration {


    private final HystrixConcurrencyStrategy existingStrategy;

    public FullLinkHystrixThreadLocalAutoConfiguration(@Autowired(required = false)
                                                       HystrixConcurrencyStrategy existingStrategy) {
        this.existingStrategy = existingStrategy;
    }


    @PostConstruct
    public void init() {
        HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
        HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
        HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance().getPropertiesStrategy();
        HystrixCommandExecutionHook executionHook = HystrixPlugins.getInstance().getCommandExecutionHook();

        HystrixPlugins.reset();

        HystrixPlugins.getInstance().registerConcurrencyStrategy(new ThreadLocalAwareStrategy(existingStrategy));
        HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
        HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
        HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
        HystrixPlugins.getInstance().registerCommandExecutionHook(executionHook);
    }
}

3. 压测数据和普通数据隔离

MySQL数据隔离

MySQL数据隔离可以采用的方式如下:

  1. 影子数据,即给一张表添加一个字段专门用于存储影子数据。
  2. 影子表,给一张表同时生成一张影子表,专门用于影子数据。
  3. 影子库,将影子数据专门放到一个隔离的数据库。

这里采用影子库的实现思路。

spring官方提供了一个类,叫做 AbstractRoutingDataSource,他是专门用于实现动态数据源的,我们只需要实现关键方法 determineCurrentLookupKey,就可以实现动态数据源(压测流量选择影子库,正常流量选择正常库)

/**
 * 动态数据源
 */
public class FullLinkDynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        FullLinkContext fullLinkContext = FullLinkContextHolder.getFullLinkContext();
        if (fullLinkContext != null) {
            // 压测数据 使用影子库
            DynamicDataSourceContextHolder.setDataSourceKey(DataSourceKey.SHADOW_DB);
        }
        else {
            DynamicDataSourceContextHolder.setDataSourceKey(DataSourceKey.PRIMARY_DB);
        }
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
}

动态数据源配置类,需要注入 影子库和普通库。

@ConditionalOnClass({DataSource.class})
@ConditionalOnProperty({"spring.datasource.primary.type"})
@Configuration
public class FullLinkDataSourceAutoConfiguration {

    @Value("${spring.datasource.primary.type}")
    private String primaryJdbcType;


    @Value("${spring.datasource.shadow.type}")
    private String shadowJdbcType;

    @ConditionalOnProperty("spring.datasource.primary.type")
    @Bean(name = "primaryDatasource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.build(primaryJdbcType);
    }


    @ConditionalOnProperty("spring.datasource.shadow.type")
    @Bean(name = "shadowDatasource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.shadow")
    public DataSource shadowDataSource() {
        return DataSourceBuilder.build(shadowJdbcType);
    }

}

Redis数据隔离

对于Redis数据隔离,我们只需要给对key进行处理就好了。比如说,压测流量的 key 可以添加前后缀进行标识 比如添加前缀 auto,后缀 shadow,最终赶得到 auto-nomolKey-shadow

package com.hdu.redis;

import com.hdu.context.FullLinkContext;
import com.hdu.context.FullLinkContextHolder;
import org.springframework.data.redis.serializer.StringRedisSerializer;

public class KeyStringRedisSerializer extends StringRedisSerializer {

    private final RedisShadowKeyConfiguration redisShadowKeyConfiguration;

    public KeyStringRedisSerializer(RedisShadowKeyConfiguration redisShadowKeyConfiguration) {
        this.redisShadowKeyConfiguration = redisShadowKeyConfiguration;
    }

    @Override
    public byte[] serialize(String redisKey) {
        FullLinkContext fullLinkContext = FullLinkContextHolder.getFullLinkContext();
        if (fullLinkContext != null) {
            // 全链路压测数据, 添加key前缀 or 后缀
            redisKey = redisShadowKeyConfiguration.getKey(redisKey);
        }
        return super.serialize(redisKey);
    }
}

package com.hdu.redis;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("redis.isolation")
@Data
public class RedisShadowKeyConfiguration {
    private String keyPrefix;
    private String keySuffix;

    public String getKey(String oriKey) {
        return keyPrefix + ":" + oriKey + ":" + keySuffix;
    }
}

MQ数据隔离

4. 源码

full_link: full_link (gitee.com)