问题:
某个接口的响应慢怎么解决
思路
最简单的思路是:打印每一个步骤的耗时情况,然后搜索关键词来查看每个步骤的耗时情况,通过比较这些耗时的操作,然后再看这个步骤要如何优化:
long start = System.currentTimeMillis();
processA();
Logs.info("process A cost " + (System.currentTimeMillis() - start));// 打印 A 步骤
start = System.currentTimeMillis();
processB();
Logs.info("process B cost " + (System.currentTimeMillis() - start));// 打印 B 步骤
start = System.currentTimeMillis();
processC();
Logs.info("process C cost " + (System.currentTimeMillis() - start));// 打印 C 步骤
问题是:由于同时会有多个下单请求并行处理,所以,这些下单请求的每个步骤的耗时日志,是相互穿插打印的。你无法知道这些日志,哪些是来自同一个请求,也就不能直观的看到,某一次请求耗时最多的步骤是哪一步了。那么,如何把单次请求,每个步骤的耗时情况串起来呢?
解决方法:给同一个请求的每一行增加日志,增加一个相同的标记。这样,只要拿到这个标记就可以查找到这个请求链路上,所有步骤的耗时了,我们把这个标记叫做requestId
。我们可以在程序的入口处生成一个requestId,然后把它放在线程的上下文中,这样就可以在需要时,随时从线程上下文中获取到requestId
String requestId = UUID.randomUUID().toString();
ThreadLocal<String> tl = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return requestId;
}
}; //requestId 存储在线程上下文中
long start = System.currentTimeMillis();
processA();
Logs.info("rid : " + tl.get() + ", process A cost " + (System.currentTimeMillis
start = System.currentTimeMillis();
processB();
Logs.info("rid : " + tl.get() + ", process B cost " + (System.currentTimeMillis
start = System.currentTimeMillis();
processC();
Logs.info("rid : " + tl.get() + ", process C cost " + (System.currentTimeMillis
解决了这个问题之后还是有问题:每次排查一个接口就需要增加日志、重启服务,这并不是一个好的办法
解决
经验上来看,一个接口响应时间慢,一般是出现在跨网络的调用上,比如说请求数据库、缓存或者依赖的第三方服务。所以,我们只需要针对这些调用的客户端类,做切面编程(AOP),通过插入一些代码打印他们的耗时就好了
切面编码是面向对象编程的一种延伸,可以在不修改源代码的前提下,给应用程序添加功能,比如说鉴权,打印日志等等。
就像开发人员在向代码仓库提交代码之后,需要对代码编译、构建、执行单元测试用例,以保证提交的代码是没有问题的。但是,如果每次/每个人提交代码都要做这么多事情,无疑会造成很大的负担,所以可以配置可以持续集成的流程,在提交代码之后,自动完成这些操作,这个持续集成的流程就可以认为是一个切面。
一般来说,切面编程的实现分为两类:
这两者有什么差别呢?
我们做切面的原因,是想生成一些调试的日志,所以我们期望尽量减少对原先接口性能的影响。因此,推荐使用静态代理实现切面编程。
使用 AspectJ 做切面的简单代码实现就像下面这样:
这样,在系统的每个接口中,打印出了所有访问数据库、缓存、外部接口的耗时情况,一次请求可能要打印十几条日志,如果系统的QPS是10000 的话,就是每秒钟会产生十几万条日志,对于磁盘IO的负载是巨大的。那么这时,就需要考虑如何减少日志的数量。
可以考虑对请求做采样。采样的方式也简单,比如你想采样 10% 的日志,那么你可以只打印“requestId%10==0”的请求。
有了这些日志之后,当给你一个requestID的时候,你发现自己并不能确认这个请求到了哪一台服务器上,所以你不得不登录所有服务器,去搜索这个requestId才能定位请求。这样无疑会增加问题的排查时间。
解决思路:把日志不打印到本地文件中,而是发送到消息队列里,再由消息处理程序写入到集中存储中,比如 Elasticsearch。这样,你在排查问题的时候,只需要拿着 requestId 到 Elasticsearch 中查找相关的记录就好了
总结:
为了排查单次请求响应时间长的原因,主要做了下面这些事情:
在一体化架构中,单次请求的所有耗时日志,都被记录在一台服务器上,而在微服务的场景下,单次请求可能跨越多个RPC服务,这就造成了单次的请求的日志会分布在多个服务器上。
当然,也可以通过requestId将多个服务器上的日志串起来,但是仅仅依赖requestId很难表达服务之间的调用关系,也就是从日志中,无法了解服务之间是谁在调用谁。因此,我们采用traceId和spanId这两个数据维度来记录服务之间的调用关系(这里traceId 就是 requestId)也就是使用 traceId 串起单次请求,用 spanId 记录每一次RPC 调用。
举个例子:请求从用户端过来,先到达 A 服务,A 服务会分别调用 B 和 C 服务,B 服务又会调用 D 和 E 服务。
那么 spanId 是何时生成的,又是如何传递的呢?
还有一个问题是性能问题,也就是引入了分布式追踪中间件导致对磁盘IO和网络IO的影响。建议:
在排查单次慢请求中哪一步是瓶颈时注意注意:
其实,分布式追踪系统不是一项新的技术,而是若干已有技术的整合,在实现上并不复杂,却能够实现跨进程调用链展示、服务依赖。所以,在微服务化过程中,它是一个必选项,无论是采用开源还是自研的方案,都应该在微服务化完成之前,尽快让它发挥应有的价值。