(1)什么是全链路压测
全链路压测是指基于真实业务场景,通过模拟海量的用户请求,对整个后台服务进行压力测试,从而评估整个系统的性能水平。
(2)全链路压测的实施步骤
(3)全链路压测实施关键技术难点
(1)什么是流量回放
流量回放就是通过记录线上流量,在开发或者测试环境回放,来发现系统是否能够正常运行,降低代码变动整体系统带来的风险。
(2)流量回放工具GoReplay
官网:https://goreplay.org/
github:https://github.com/buger/goreplay
GoReplay是GO语言编写的http流量复制工具,使用流程简单,支持多个系统,mac、linux、win。
GoReplay 不是代理,而是在后台侦听网络接口上的流量,无需更改生产基础架构,只需在与服务相同的机器上运行 GoReplay 守护程序。
(3)Linux服务器安装Go环境和GoReplay
# 下载之后解压
tar -C /usr/local -zxvf go1.5.3.linux-amd64.tar.gz
# 打开文件
vim /etc/profile
# 添加环境变量
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
# 编译生效
source /etc/profile
# 测试
go version
# go version go1.21.5 linux/386
tar xvzf gor_1.3.1_x64.tar.gz
(4)使用方式
输入
--input-raw : 用于捕获 HTTP 流量时,应指定 IP 地址或界面以及应用程序端口
--input-file :接收以前使用过的文件记录
--input-tcp :如果决定将多个转发器Gor实例转发流量到它,Gor聚合实例使用
输出
--output-http :重播HTTP流量到给定的端点
--output-file :记录传入到文件的流量
--output-tcp :将传入的数据转发到另一个Gor实例
--output-stdout :用于调试,输出所有数据。
./gor --input-raw :8082 --output-file=requests.gor
./gor --input-file requests.gor --output-http="http://ip:8082"
./gor --input-raw :8082 --output-stdout
./gor --input-raw :8082 --output-http="http://ip:8082"
./gor --input-file "requests.gor|200%" --output-http="http://ip:8082"
./gor --input-file "requests.gor|20%" --output-http="http://ip:8082"
./gor --input-raw :80 --http-allow-method GET --output-http http://target_server:8080
./gor --input-raw :8080 --output-http staging.com --http-allow-url /api
./gor --input-raw :8080 --output-http staging.com --http-allow-headerapi-version:^1\.0\d
./gor --input-tcp :28020 --output-http"http://staging.com|10"# (每秒请求数限制10个以内)
./gor --input-raw :80 --output-tcp"replay.local:28020|10%" # (每秒请求数限制10%以内)
./gor --input-raw :80 --output-tcp"replay.local:28020|10%" --http-header-limiter "X-API-KEY:10%"
./gor --input-raw :80 --output-tcp"replay.local:28020|10%" --http-param-limiter "api_key:10%"
./gor --input-raw :80 --output-http "http://target_server:8080"--output-http "http://target_server2:8080"
./gor --input-raw :80 --output-http "http://staging.com" --output-http "http://dev.com"--split-output true
(1)什么是流量染色
流量染色就是让压测流量可以被程序代码识别,方便做好数据隔离。对压测的请求增加特色的流量标识,比如请求里面增加url参数或header增加请求头。区分压测流量和真实流量,正常用户不会访问到压测数据,压测数据不会影响正式业务。染色后的压测流量,产生的数据可以再压测结束后直接清理。
(2)流量链路改造
数据库隔离
压测产生的数据需要和真实数据库的进行隔离,一般采用数据库的影子库、影子表进行隔离。
具体来说,影子库是生产环境数据库的一份完整拷贝,包含与生产环境相同的表结构和数据。
影子表是在影子库中创建的与生产环境表相对应的测试表,压测的数据进入影子表。
生产和压测环境的隔离,通过在压测环境中使用影子库和影子表,可以避免对生产环境数据的直接修改和干扰。
完整的数据环境,通过生成影子库和影子表的完整拷贝,全链路压测的时候可以在准确、真实的数据环境中进行工作。
线上问题还原,当线上出现问题时,可以使用影子库和影子表进行问题还原和分析,在相同数据环境中重现问题。
消息队列隔离
业务产生消息到MQ后,消费者会进行消费,压测过程产生的数据不能直接投递到MQ中。
一般是采用队列隔离或者消息隔离,隔离策略也是基于消息的生产者封装方法进行投递。
队列隔离:创建不同的消息队列,压测的队列和正式的队列采用不同的前缀进行区分。
消息隔离:消息里面增加参数,标记消息是否是压测还是正式的数据。
缓存隔离
缓存里面的数据隔离,对key进行区分,根据流量标识是否是压测流量,增加相关的key前缀标识。
不直接操作redis,而是封装redis工具类,在工具类里面判断是否是压测流量,里面对key的读写进行操作。
(3)流量标识透传
流量标识透传是一种将请求上下文信息从发起端(如客户端)传递到目标端(如后端服务)的方案。可以在测试过程中追踪和识别请求的来源,并对不同的请求进行分类和分析。压测流量全部带标识,结合拦截器,存储在ThreadLocal里面进行不同服务直接传递。
(4)流量标识透传方案
X-Request-ID
:请求标识ID,用于唯一标识每个请求。X-Trace-ID
:链路追踪ID,用于追踪请求在分布式系统中的路径。X-Forwarded-For
:客户端真实IP地址,用于透传客户端IP。(5)跨服务器之间流量传递
(1)阿里云docker部署mysql
docker run -d -p 3306:3306 --name mysql --privileged=true -v /data/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mysql8test. mysql:8.0.23
CREATE TABLE `product_order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_id` bigint DEFAULT NULL,
`product_title` varchar(255) DEFAULT NULL,
`amount` int DEFAULT NULL,
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
`stock` int DEFAULT NULL,
`amount` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 分别在两个库中加入数据
INSERT INTO `product` (`id`, `title`, `stock`, `amount`) VALUES (1, '名称', 200, 10);
(2)阿里云docker部署RabbitMQ
docker run -d --name rabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=password -p 15672:15672 -p 5672:5672 rabbitmq:3.8.9-management
(3)阿里云docker部署Redis
docker run -itd --name redis -p 6379:6379 -v /mydata/redis/data:/data redis:7.0.8 --requirepass 123456
(4)阿里云docker部署Nacos
docker run -d -e MODE=standalone -e JVM_XMS=128m -e JVM_XMX=128m -e JVM_XMN=128m -p 8848:8848 -p 9848:9848 --restart=always --privileged=true --name nacos nacos/nacos-server:v2.2.3
(1)需求模块划分
(2)业务逻辑
(3)流量染色和RPC透传
/**
* @author lixiang
* @date 2024/1/6 15:50
*/
@Slf4j
public class RequestInterceptor implements HandlerInterceptor {
public static TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response,@NonNull Object handler) throws Exception {
//前端在header中传入test_flag标识来区分是否为正式流量或者压测流量
String testFlag = request.getHeader("test_flag");
if(StringUtils.isNotBlank(testFlag)){
//通过threadLocal传递信息
log.info("压测流量,path = {}",request.getRequestURI());
threadLocal.set(1);
}else{
log.info("正式流量,path = {}",request.getRequestURI());
threadLocal.set(0);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
threadLocal.remove();
}
}
/**
* @author lixiang
* @date 2024/1/6 15:50
*/
@Configuration
public class FeignConfig implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String values = request.getHeader(name);
requestTemplate.header(name, values);
}
}
}
}
(4)缓存隔离
/**
* 构建 key,用于区分压测和正式流量
* @param key
* @return
*/
private String buildFinalKey(String key){
Integer testFlag = RequestInterceptor.threadLocal.get();
String finalKey = "";
//压测流量
if(testFlag !=null && testFlag ==1){
finalKey = "shadow:"+key;
}else {
finalKey = key;
}
return finalKey;
}
(5)消息队列隔离
/**
* 构建 routingKey的时候,用于区分压测和正式流量
* @param routingKey
* @return
*/
private String buildFinalRoutingKey(String routingKey){
Integer testFlag = RequestInterceptor.threadLocal.get();
String finalKey = "";
//压测流量
if(testFlag !=null && testFlag ==1){
finalKey = "SHADOW."+routingKey;
}else {
finalKey = routingKey;
}
return finalKey;
}
@Slf4j
@Component
public class OrderMQListener {
@RabbitListener(queuesToDeclare = { @Queue("ORDER_QUEUE") })
public void orderQueue(ProductOrderDO productOrderDO, Message message, Channel channel) throws IOException {
log.info("监听到正式消息:{}",message);
long msgTag = message.getMessageProperties().getDeliveryTag();
handleOrderMsg(productOrderDO);
channel.basicAck(msgTag,false);
}
@RabbitListener(queuesToDeclare = { @Queue("SHADOW_ORDER_QUEUE") })
public void shadowOrderQueue( ProductOrderDO productOrderDO,Message message, Channel channel) throws IOException {
log.info("监听到影子消息:{}",message);
long msgTag = message.getMessageProperties().getDeliveryTag();
handleOrderMsg(productOrderDO);
channel.basicAck(msgTag,false);
}
private void handleOrderMsg(ProductOrderDO productOrderDO){
String type = "1".equals(productOrderDO.getType())?"正式逻辑":"压测逻辑";
log.info("{}-处理订单消息",type);
}
}
(6)数据源隔离
/**
* @author lixiang
* @date 2024/1/6 15:50
*/
@Configuration
public class DynamicDataSourceConfig {
public static final String MASTER = "MASTER";
public static final String SHADOW = "SHADOW";
@Bean("masterDataSourceProperties")
@ConfigurationProperties("spring.datasource.master")
public DataSourceProperties masterDataSourceProperties() {
return new DataSourceProperties();
}
@Bean("masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master.hikari")
public HikariDataSource primaryDataSource() {
return masterDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean("shadowDataSourceProperties")
@ConfigurationProperties("spring.datasource.shadow")
public DataSourceProperties shadowDataSourceProperties() {
return new DataSourceProperties();
}
@Bean("shadowDataSource")
@ConfigurationProperties(prefix = "spring.datasource.shadow.hikari")
public HikariDataSource secondaryDataSource() {
return shadowDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
}
/**
* @author lixiang
* @date 2024/1/6 15:50
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final TransmittableThreadLocal<String> dataSourceContextHolder = new TransmittableThreadLocal<>();
/**
* 配置DataSource, defaultDataSource为主数据库
*/
public DynamicDataSource(DataSource defaultDataSource, Map<Object,Object> targetDataSourceMap) {
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSourceMap);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
public static void setDataSource(String dataSource) {
dataSourceContextHolder.set(dataSource);
}
public static String getDataSource() {
return dataSourceContextHolder.get();
}
public static void clearDataSource() {
dataSourceContextHolder.remove();
}
}
@Slf4j
@Aspect
@Component
public class DataSourceAspect {
@Pointcut("execution(public * com.lixiang.controller..*.*(..))")
public void controllerPointcut() {
}
@Before(value = "controllerPointcut()")
public void methodBefore(JoinPoint joinPoint) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert requestAttributes != null;
HttpServletRequest request = requestAttributes.getRequest();
// 获取请求头
String testFlag = request.getHeader("test_flag");
// 通过 testFlag 判断
if (StringUtils.isNotBlank(testFlag)) {
System.out.println("压测流量,影子库,path = "+request.getRequestURI());
DynamicDataSource.setDataSource(DynamicDataSourceConfig.SHADOW);
} else {
System.out.println("正式流量,正式库,path = "+request.getRequestURI());
DynamicDataSource.setDataSource(DynamicDataSourceConfig.MASTER);
}
}
}
/**
* 创建多个数据源对象
* @param masterDataSource
* @param shadowDataSource
* @return
*/
@Bean
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource, DataSource shadowDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("SHADOW", shadowDataSource);
targetDataSources.put("MASTER", masterDataSource);
return new DynamicDataSource(masterDataSource, targetDataSources);
}
项目的代码我会传到资源文件中哦,大家记得去找【案例实战】业务稳定性运行之全链路混合压测
这个标题的资源文件。
(1)启动项目,查看服务情况
(2)接口准备
ip:8082/api/product/v1/findById 查看商品详情
ip:8082/api/product/v1/list 查看商品列表
ip:8082/api/product/v1/lock 扣件商品库存
ip:8081/api/order/v1/add 下单
(3)配置jmeter
OK,至此全链路压测就已经完成啦,大家可以根据公司自己的业务去实施。记得给博主三连哦!