文章结尾有彩蛋
随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。而诸多的服务可能分布在了几千台服务器,横跨多个不同的数据中心。为了快速定位和解决故障,应用性能进行分析,全链路监控组件就在这样的问题背景下产生了。最出名的是谷歌公开的论文提到的Google Dapper。本文主要介绍了Spring Cloud Gateway全链路实现解决方案。主要讲解全链路原理和方案;最后分享如何实现探针全链路。
全链路原理
主流的全链路方案都是采用谷歌公开的论文提到的Google Dapper方案。
一个Span表示一个服务的调用开始到结束。
如果一个Span没有父ID则被称之为入口Span,需负责生成本次链路调用全局唯一的TxId。
如果服务间有调用,则透传TxId、SpanId和pSpanId,每个SpanId需重新生成,pSpanId采用调用方的SpanId。
被调用服务需将透传的头信息恢复,继续透传后续节点。
最终根据Span的关联性生成链路树。
将应用以角色进行区分,分为Server端和Client端。
Server端负责接收请求,其流程主要分为三步:
创建入口Span
解析并恢复上游头信息: TxId、SpanId和pSpanId
结束入口Span
Client端负责发送请求,其流程也分为三步:
创建子Span
传递给下游头信息:TxId、新的SpanId和pSpanId(上游节点的SpanId)
结束子Span
其中Dubbo Attachment、HttpRequest Header、MQ Property是Map类型,都可以进行头信息传输。
全链路方案
那么怎么实现服务端和客户端TxId的透传和恢复呢?
通常采用代码埋点的方式。其中主流的方案有2类,运维人员采用无感知的探针方案;开发人员常采用API或插件方案。
探针JavaAgent
JDK 1.5以后引入了JavaAgent技术,JavaAgent是运行方法之前的拦截器。利用JavaAgent和ASM字节码技术,可以在JVM加载class二进制文件时动态修改加载的class文件。
通过在启动脚本中添加-javaagent:/
主流的开源框架有Pinpoint、SkyWalking等。
全自动探针Attach
JDK 6之后提供了Attach API,通过VirtualMachine attach到目标进程,也是利用JavaAgent和ASM字节码技术,动态修改已加载的class类。
与第一种方案相比,无需重启应用和修改启动脚本。
主流的实现有Dynatrace的OneAgent。
API
除了上述的探针方案,也有厂家提供API供开发人员手动调用并扩展采集的数据。
主要通过提供拦截器、过滤器和AOP方式。
开源框架有Cat。
插件
随着Spring Boot的普及,开发只需引入spring-cloud-starter-zipkin,就能自动实现微服务的全链路追踪。
Spring Cloud Gateway探针全链路实现
Spring Cloud Gateway是采用NIO模型通信的网关,通过Netty暴露服务、Reactor Netty转发请求。具体Spring Cloud Gateway处理流程此处不做表述,有兴趣可阅读:《SpringCloud Gateway 全链路实现分析》。
虽然Zipkin插件已经提供了Spring Cloud Gateway全链路解决方案,但探针还没有相关实现,以下是Http请求的实现要点。
TxId提取——串联上游节点
全链路的重要指标是所有节点共用同一个TxId。HttpRequestDecoder负责解码接收Http请求,只需解析Http头信息提取TxId,如果是入口节点则需主动创建TxId。
TxId保持——跨线程传递TxId
之前提到Spring Cloud Gateway是通过Netty提供服务,但Netty作为NIO框架,其接收请求的线程和请求返回线程很可能是不同线程;而且作为转发者,Reactor Netty使得请求转发的线程是不同的线程。
那么怎么保证不同的线程间TxId的传递,最终保证传递给下游节点时TxId是传入的TxId呢?
Netty服务端请求和返回跨线程
以上是Netty4 I/O事件的处理流程,作为服务端来说,左侧是入站事件,HTTP对应的是HttpRequestDecoder,负责解码Http请求;右侧是出站事件,Http对应的是HttpResponseEncoder,负责编码Http返回。
虽然2个步骤是不同线程处理,但共用的是同一个ChannelPipeline。
ChannelPipeline没有Map等额外可扩展的属性,需要探针扩展ChannelPipeline类新增字段用于存储跨线程的TxId信息。
在请求接收时需将TxId信息存储到ChannelPipeline,当请求结束时从ChannelPipeline获取TxId结束请求,并清空存储的TxId。
Spring Cloud Gateway转发请求跨线程
Spring Cloud Gateway封装了Netty的调用,通过ServerWebExchange传递请求和返回,无需关注Netty的Client是怎么调用的。只需将TxId存储到可扩展的attribute属性中就能跨线程获取TxId。
TxId传递——串联下游节点
如下图所示,在转发请求时会用到ServerWebExchange的头信息,只需在头信息中扩充TxId就能直接传递给下游节点。
关联下游节点
有时候当前节点需要知道下游节点是谁,是否被监控,会让下游节点将自身信息回写到返回头信息中,需解析返回的头信息。
请求超时
如果客户端设置超时时间,请求超时会直接断开连接,触发Channel的inactive事件而导致请求不会触发HttpResponseEncoder,此时需提前结束请求清空TxId信息。
采样
作为网关,TPS是个重要指标,在大流量的生产环境下,不可能将所有的请求全部记录,这就要求对请求进行采样,需根据实际情况选择采样策略。
Q&A
Q:Gateway服务这块如果要做防跨站攻击,有什么好的方案?
A:使用前置的Filter过滤,Jsoup的标签白名单机制可以用来进行防止XSS攻击。
Q:灰度方案实现?
A:网关从配置中心获取实时的配置,动态变更路由权重信息。
Q:如何在Gateway做权限认证。你们是如果做的
A:通过JWT实现,每个请求传递Token进行权限认证。
Q:如何跟踪分析Gateway调用耗时的问题?
A:目前主流的方案是对各组件进行埋点,但不一定能定位到真正耗时的情况,此时要进行Sampler的方式获取慢方法。
Q:JWT一般怎么做吊销或者超时校验?每次都从Redis读会不会压力比较大?
A:将JWT存储到中间件Redis中,设置超时时间。吊销问题比较好解决,客户端退出或修改密码后调用中间件清除。本来JWT这种方式就属于比较弱安全的方式,JWT本身可以放时间属性,设置过期时间。这种方式带来的问题就会是需要续签,定时续签。另外一种方式就是Redis不会设置过期时间或者说时间比较长,依赖logout来注销。这两种方式并不是一直都是访问Redis,对Redis压力不会很大,续签可以设置时间稍微长一点。
Q:可以展示下你们的全链路监控产品吗?你们怎么怎么做到监控信息的收集,存储和转发,展示以及告警策略的制定。你从头到尾讲解APM监控原理讲的很不错,可以介绍下你们的产品吗?与开源的Pinpoint和SkyWalking的区别?
A:暂时没有提供公网访问,开源Pinpoint 和SkyWalking是专业的APM开源产品,从产品定位上说就有区别,我们监控产品是矩阵式监控,APM只是我们其中一个技术。我们监控的定位主要从客户在运维过程中处理故障的实际需求,根据报告故障只有24%是纯应用问题,还有很大部分是网络的问题,所以我们矩阵从纵向来说包含了网络质量的监控和应用依赖的基础设施的资源监控,就是为了解决运维过程中的综合性问题。横向才是全链路追踪的层面。当然在APM本身层面我们也做了一些细节的优化。
Q:目前用SkyWalking定位到耗时发生在Gateway的WebFlux的handle上面,但是不知道怎么分析如何产生的耗时。
A:上面我提到我们对慢方法的捕获做了优化,特别是针对开发写的代码出现慢的场景,有时候开源根据实现原理的细微差别会捕获不到。现在我觉得是SkyWalking监控粒度不够,导致没有获取到慢的调用,通过我们这种方式应用是可以解决的,具体问题还是需要我们来具体分析。
spring cloud分布式事务从入门到精通
(如下载连接失效,请私信我)
百度云下载链接:https://pan.baidu.com/s/1EtYyj7MEBqIGSYg2ljBFyg 提取码:n8p7
备份下载地址 https://huoyan-dabing.lanzous.com/b0d253o4h 密码:9sem