一个简单的单体服务流量标记demo

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
性能测试 PTS,5000VUM额度
简介: 【2月更文挑战第17天】一个简单的单体服务流量标记demo

一、概念

在全链路压测中生成流量后,实际业务中需要区分流量(正常流量 & 压测流量),我们称之为链路打标,也可以叫做流量标记,而一般对外的接口都是使用 http 的方式暴露的,http 是一个比较通用的协议,一般我们会通过 header 的增加一个标记项。例如 key 是 “flag”,value 是你需要携带的数据,可以是普通的字符串,也可以 json 串,但是要注意控制 value 的长度,因为中间件有限制 header 的长度。

在向下游服务发起请求时,如果是压测流量把 header 头中的标记字段往下透传,下游继续在业务中往下透传,接收到如果是压测流量,就使用相应的压测数据。

我们知道目前微服务架构中「分布式跟踪系统」作为基础设施,不会限制「使用线程池等会池化复用线程的组件」,并期望对业务逻辑尽可能的透明。从技术能力上讲,「全链路压测」 与 「分布式跟踪系统」 是一样的,即链路打标。
目前市面上,几乎所有分布式链路跟踪的实现,理论基础都是基于 Google Dapper 的那篇论文,其中最重要的核心概念就是 traceIdspanId

二、设计方案

我们这里演示的 demo 很简单,主要就是使用自定义拦截器和 logback 日志自定义格式化跟踪:

  • 首先流量标记在客户端上生成,在请求业务接口的时候传递给服务端;
  • 然后服务端进行拦截,在请求真正的接口前获取 header 中传递的标记,并存储在 ThreadLocal 中,做为请求线程共享的局部变量;
  • 在请求结束的时候需要手动调用 remove() 方法清除 Map 中的标记条目,避免内存泄漏,标记随着时间推移会有很多;
  • 最后借助 logback 实现自定义日志打印跟踪(线上需要做到日志隔离)。

ThreadLocal 知识参考:

1、开发环境

  • IDEA 2020.10
  • Maven 3.6.2
  • SpringBoot 2.2.0

    2、构建项目

    新建一个 SpringBoot 工程,并引包:
 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

3、自定义拦截器

创建 FlagTrackInterceptor 拦截器类:

/**
 * @Description: 标记追踪拦截器
 * @Param:
 * @return:
 * @Author: zuozewei
 * @Date: 2021/3/15
 */

@Component
public class FlagTrackInterceptor implements HandlerInterceptor {
   
   
    /**
     * 存储 flag
     */
    private static final ThreadLocal<String> FLAG_THREAD_LOCAL = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   
   
        /**
         * 获取请求头 header 中传递的 flag,若没有,则 UUID 代替
         */
        String flag = Optional.ofNullable(request.getHeader("flag")).orElse(UUID.randomUUID().toString().replaceAll("-",""));
        // 请求前设置
        FLAG_THREAD_LOCAL.set(flag);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
   
   
        // 移除,防止内存泄漏
        FLAG_THREAD_LOCAL.remove();
    }

    public static String getFlag() {
   
   
        return FLAG_THREAD_LOCAL.get();
    }

    public static void setFlag(String flag){
   
   
        FLAG_THREAD_LOCAL.set(flag);
    }

}

这里处理 flag,通过 ThreadLocal 使用,可以保证每个请求都拥有唯一的一个流量标记。

4、注册自定义拦截器

新建 CustomInterceptorConfig 自定义拦截器注册配置类:

**
 * @Description: 注册自定义拦截器
 * @Param:
 * @return:
 * @Author: zuozewei
 * @Date: 2021/3/26
 */

@Configuration
public class CustomInterceptorConfig implements WebMvcConfigurer {
   
   

    @Autowired
    private FlagTrackInterceptor flagTrackInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
   
   
        registry.addInterceptor(flagTrackInterceptor);
    }
}

5、自定义 logback 日志格式化

新建 FlagPatternConverter 自定义日志格式化类:

/**
 * @Description:  自定义日志格式化
 * @Param:
 * @return:
 * @Author: zuozewei
 * @Date: 2021/3/26
 *
 */

public class FlagPatternConverter extends ClassicConverter {
   
   
    @Override
    public String convert(ILoggingEvent iLoggingEvent) {
   
   
        String flag = FlagTrackInterceptor.getFlag();
        return StringUtils.isEmpty(flag) ? "flag" : flag;
    }
}

需要继承 ClassicConverter 类,并重写 convert 方法,继承 PatternLayout,加入自己要自定义的内容,对于全链路压测,需要加入的是流量标记。

6、控制器

新建 FlagTrackController 控制器类:

/**
 * @Description:  测试日志追踪
 * @Param:
 * @return:
 * @Author: zuozewei
 * @Date: 2021/3/26
 */

@Slf4j
@RestController
@RequestMapping("/test")
public class FlagTrackController {
   
   

    @GetMapping("/log")
    public String flagTrack(){
   
   
        log.info("-----> 测试 info <-----");
        log.warn("-----> 测试 warn <-----");
        log.error("-----> 测试 error <-----");
        return null;
    }

}

7、logback 日志配置文件

创建 logback 日志配置文件 logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 获取 flag 配置类-->
    <conversionRule conversionWord="flag" converterClass="com.zuozewei.flagtrack.config.FlagPatternConverter" />
    <!-- 自定义 logback 日志格式-->
    <property name="CUSTOM_LOG_PATTERN"
              value="[[[%date{yyyy-MM-dd HH:mm:ss} | %-5level | %flag | %thread | %file:%line | %logger : %.1000m]]]%n" />

    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CUSTOM_LOG_PATTERN}</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <!-- 控制台打印 -->
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

最后,我们需要在 logback.xml 里面配置一下日志的 pattern 和 PatternLayout

8、项目结构

最终的项目结构如下:
image.png

四、测试

1、正常流量

通过 Postman 模拟请求:http://localhost:8080/test/log(header 中不添加 flag )

image.png

控制台输出日志结果如下:
image.png

2、流量标记

通过 Postman 模拟请求:http://localhost:8080/test/log(header 中添加标记 「flag:7d-test」 )

image.png

控制台输出日志结果如下:
image.png

五、小结

简单基于 SpringBoot,使用拦截器及自定义日志演示一个简单的单体服务流量标记方案。如果接口内部存在多线程异步调用,这时用上面提供的方案的流量标记还会有效吗?如果不能实现真实的链路传递,那么又该如何实现呢?

源码地址:

相关实践学习
通过性能测试PTS对云服务器ECS进行规格选择与性能压测
本文为您介绍如何利用性能测试PTS对云服务器ECS进行规格选择与性能压测。
目录
相关文章
|
5月前
|
消息中间件 缓存 Kafka
go-zero微服务实战系列(八、如何处理每秒上万次的下单请求)
go-zero微服务实战系列(八、如何处理每秒上万次的下单请求)
|
5月前
|
缓存 前端开发 数据格式
构建前端防腐策略问题之保证组件层的代码不受到接口版本变化的问题如何解决
构建前端防腐策略问题之保证组件层的代码不受到接口版本变化的问题如何解决
|
5月前
|
缓存 NoSQL Redis
go-zero微服务实战系列(七、请求量这么高该如何优化)
go-zero微服务实战系列(七、请求量这么高该如何优化)
|
5月前
|
消息中间件 缓存 监控
go-zero微服务实战系列(六、缓存一致性保证)
go-zero微服务实战系列(六、缓存一致性保证)
|
7月前
|
监控 前端开发 Java
SSMP整合案例第七步 前后端业务异常消息统一处理
SSMP整合案例第七步 前后端业务异常消息统一处理
44 1
|
8月前
|
负载均衡 前端开发 Java
字节后端面试题(前端发送请求到后端的过程(MVC),网关gateway作用,怎么解决跨域,各微服务组件作用)
字节后端面试题(前端发送请求到后端的过程(MVC),网关gateway作用,怎么解决跨域,各微服务组件作用)
448 0
|
前端开发 Java 微服务
微服务之间调用的异常应该如何处理
在分布式服务的场景下,业务服务都将进行拆分,不同服务之间都会相互调用,如何做好异常处理是比较关键的,可以让业务人员在页面使用系统报错后,很清楚的看到服务报错的原因,而不是返回代码级别的异常报错,比如NullException、IllegalArgumentException、FeignExecption等异常报错,这样就会让非技术人员看到了一头雾水,从而很降低用户的体验感。
|
设计模式 前端开发 Java
如何实现自定义MVC框架(最终版本)
如何实现自定义MVC框架(最终版本)
55 0
|
Dubbo 应用服务中间件 测试技术
带你读《Apache Dubbo微服务开发从入门到精通》—— 三、 标签路由规则
带你读《Apache Dubbo微服务开发从入门到精通》—— 三、 标签路由规则
241 6
|
负载均衡 Kubernetes 算法
Go微服务架构实战 中篇:4. 基于ingress的限流,路径匹配和重写实战
Go微服务架构实战 中篇:4. 基于ingress的限流,路径匹配和重写实战