1. 架构图
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数据隔离可以采用的方式如下:
- 影子数据,即给一张表添加一个字段专门用于存储影子数据。
- 影子表,给一张表同时生成一张影子表,专门用于影子数据。
- 影子库,将影子数据专门放到一个隔离的数据库。
这里采用影子库的实现思路。
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;
}
}