我们接下来继续讲链路追踪
我自己最印象深刻的一个场景是什么呢
当我们以前做BFF网关的同事
他现在出现了一些内网的错误
就是他的接口报错了 以前是一个什么动作
他先看日志 发现有一排接口报的信息
他看到的是某一个RPC服务报错了
然后他就很尴尬
他就是得去日志里面再去找哪个RPC服务报错的日志
然后一层层去找
好多人才最终定位到下游是哪个
因为你的BFF是接入层
你看到别人报错对吧
你要么就打电话找别人 让别人去看
要么就是你翻别人的日志去帮他看
然后因为你找他要花时间
如果是他可能还调用别人还报错
你得一层层去找
这样的话其实排查起来问题是非常耗时的
这是第一个
第二个 我们有时候性能问题
链路一旦多了以后
到底是哪个环节出问题
你也得一层层去问去找
所以它非常耗时耗力
链路追踪其实就是为了解决这个问题
链路追踪核心就是通过一个TraceID
针对对一个请求生成一个这样TraceID
让整个链路的一些涉及的RPC也好 组件也好
能够关联起来
然后通过这些数据分析
你可以搞定很多事情
比方说你去诊断性能问题
哪些地方容易出故障
日志能够关联起来 等等一系列的动作
我们这里讲的链路追踪
其实有很多成熟的开源产品
比方像Jaeger Zipkin都是非常成熟的
你可以直接拿来用
就没什么好说的
你自己去看一下它官方文档
去搭建一下依赖一下
我们Kratos现在也是参考的OpenTelemetry的规范
通过它接口设计来规范第三方实现
让第三方实现来适配它
Kratos也是核心按照这种思路来做的
除此之外
这里重点是要讲链路追踪它底层的一些原理
它的核心思想是什么
我们要借助它背后的一篇论文
叫做Google Dapper的一篇论文
链路追踪的整个设计目标有几个
第一就是无处不在的监控
你得覆盖尽可能多的组件
尽可能多的微服务
所以你得到处都有
第二它得7×24小时持续的监控 采集一些异常的信息
第三是低消耗
低消耗指的是
你不能因为做链路追踪的数据收集采集
导致我的接口的耗时变高
或者说我要使用大量的计算或者存储资源
来查看这些链路追踪的一些视图一些UI
你不能太吃资源
第4个叫应用级的透明
应用级透明就是 期望我们链路追踪的时候
（不应）需要每次写业务代码的时候还要手动去埋点
它应该是植入到我们的框架
植入到我们的各个组件里面
你用的时候其实对你来说是透明的
还有它的整个延展性
还有低延迟等比较基础的能力
那么基于这些目标
我们来看看这篇paper
参考Google Dapper它的一个论文实现
它的核心思想就是给每个独立的请求
生成一个全局唯一的TraceID
它是端到端
从第一个节点一层层 逐层透传到上下游所有节点的
那么这个怎么传传递呢
我们之前讲过很多次
gRPC最核心的一个能力叫什么
叫metadata exchange
就是元数据交换能力
假设你是用的Spring Cloud
可能就是一个HTTP接口
在调别人接口的时候带上你的request header
同样如果你服务接收到这个request header以后
你再挂载到你自己的代码的进程里面
可能用context 或者用spring context 或者用go context
然后通过这样的context上下文
然后你再发起别的RPC再到其他下游的时候
又带上这个HTTP request header
一层层逐层传递就全部打通了
甚至如果你们公司中间件也支持元数据传递能力
你可以把你们公司内网的这些中间件也可以协调起来
我们看一下整个请求树
先了解一下请求树
基本上一个请求
因为它有很扇出
从用户看起来
先发一个请求RequestX到A服务
它是一个叫Frontend接入层
那么A可能会依赖B服务
可能是调个鉴权 A可能会调一个C服务
C服务可能是个账号
账号可能依赖关系链
账号可能依赖用户装备
它其实像一个放大的一棵树一样
就是它的请求逐层往下游去传递
你要把整套的那些设计的组件关联起来
那不就是从请求第一个节点开始
生成一个ID然后逐层传递
所以第一个核心概念就是
我们一个请求其实它就是call的Graph
或者叫call的一个Tree
一个调用的树
那么第二个叫一个单元 什么叫一个单元
我们这里叫Span
对于A来说 对于B来说 对于C来说
它都是一个单元
那么除了TraceID标示一个请求
每个单元它还有一个叫做SpanID的东西
这个ID实际上是标识每一个单元的ID
就区分ABC他们有自己的ID
那么SpanID的作用是什么
因为现在其实A和B是有一个父子关系的
如果不是带上一个SpanID
然后他这边再带上一个ParentID
我可以指向它的ID
比方说B的ParentID等于A
B自己有一个ID
或者说C有一个SpanID等于10
然后D的ParentID可以指向它
它的ParentID可以指向A
最终想办法把整条链路通过这样的方式给关联起来
你就可以获取非常多的信息了
所以在一个完整的跟踪的树的结构中
树节点是整个架构的一个基本单元
每一个单元其实就是对Span的一个引用
Span内部到底包含些什么样的信息
我们看一下第一个请求叫Frontend.Request
就是我们刚刚A节点Frontend的RequestX这个请求
它分配了一个SpanID等于1
它没有ParentID 因为它是root节点
这个时候它调了一个Backend.Call
RPC调用了B服务
这个Backend.Call
这个时候他自己也生成了SpanID等于2
那么它的ParentID等于1指向这个请求
还原到这个树里面 它也是指向A请求
同样的Frontend又调用了Backend.Something
调用的是C服务
同样的分配的一个SpanID等于3
ParentID等于1它指向Frontend.Request这个RPC服务
Backend的内部调用了其他的一些组件
比方这两个Helper.Call方法
它们又分别分配了SpanID等于4和5
它们的ParentID都指向了Backend.DoSomething
这样的话其实就完完全全构成了一棵调用树
刚才我们理解的就是SpanID表示一个调用单元
ParentID表示上游是谁来调用我的
我的上游是谁
那么还有一个很关键的叫Family&Title
一个单元我得表示出它到底是谁
Family其实就是这个名字
就是Backend是哪个服务
Title是调的哪个方法
所以Family和Title
就是服务名加方法名
对应gRPC就是service服务名称
和RPC的方法名称
这两个就是A.B类似这样一个结构
那么TraceID其实是所有人都是同一个TraceID
比方说可能都等于100或都等于10
因为它是同一个请求
跟请求关联的就是相等的TraceID
我们再展开看一下
我们这里举例
这个单元它等于Helper.Call
这是它的一个Span的名称
它的SpanID=5
它的TraceID=100
它的ParentID=3
针对这个单元 实际上它有好多种事件
它具有什么事件呢
我们可以想象一个RPC的完整请求
一开始它是要发包的
我要RPC调用别人
先要发包发给我的对端服务
所以第一个事件是叫Client Send的一个事件
那么它会从这个时间戳开始
发完包以后的第二步
从Server的视角 叫Server Recv
它收到一个请求包
这个是它的第二个时间点
这个时候它在内部Server内部执行了一个foo的方法
它又经过了一些时间
那么下一步我要响应了
对它的响应结果做序列化返回
所以他叫Server Send这个是一波
最后我的客户端再收到这个请求
就叫Client Recv
最终结束
那么这四个事件
Client Send和Server Recv
Server Send和Client Recv
这四个事件实际是事件加上时间戳
我们可以想象一下 把Server Send减去Server Recv
是不是得到foo在我服务端接收到RPC请求后
到底执行了多少时间
如果把Client Recv减去Client Send
得出的是从Client视角看到的
全局的加上网络传输的耗时时间
如果是把Server Recv减去Client Send
得到的是我的RPC请求发送阶段
网络传输花了多少时间
如果是把Client Recv减去Server Send得到的是
响应回复经过网络得到多少时间
所以通过这几个事件可以得到很丰富的信息
为什么需要分别采集这四个事件
因为我们通常用指标采集
比方用Prometheus去采集一个RPC服务的一些指标
你看到的视角是你内存里面
你看到的是你服务内的耗时时间
也就是Server Send减去Server Recv
得出的foo方法的执行时间
它不包含网络传输时间
有可能你跟别人对的时候发现
你的服务耗时时间监控看起来很少
但是人家Client的角度就是
人家客户调用你的接口的时候
他的感受感觉耗时特别久
其实你们两个就扯不清了
所以如果有了这个视图的话
我就能知道到底是你的执行事件耗时
还是我网络传输过程中哪里有问题 导致的耗时特别久
但是这里面又有另外一个问题
我们知道客户端和服务器上的时间戳
它来自不同的主机
因为你服务器和客户端肯定是来自不同的服务器的
我们必须要考虑到时间偏差
虽然我们现在Linux服务器上
默认都会去安装时间同步服务ntpd
但是你不能绝对保证两个机器的时间没有差异
因为时间有差得出来的就不准
Server的一个节点基本上是准确的
或者Client内这两个时间相对来说是准确的
我现在时间有一些漂移的问题 怎么去修正呢
论文里面这样提的
客户端发送一个请求之后
服务端才能收到
那么对于响应也是一样的
就服务端必须先Response发送
然后我的客户端才能收到
基于这样的一个事实
我们就会给这些Server Send也好
Server Recv也好 Client Send也好
Client Recv也好 这几个事件的时间戳
设定一个时间戳的上限和下限
如果我的Server Recv的时间比Client Send还早
这不可能
我就只能修正它 让Server Recv的时间
等于Client Send时间 你只能等于它
因为不可能比它早
那这个时候我就忽略这个网络耗时
基本认为你们的网络耗时等于0
数据做一些修正之后
还勉强的可以看得下去
所以不会导致太夸张的结果
所以这个是第一个我们要注意的
就是追踪信息里面的几个事件和时间修正的一个做法
第二个我们前面讲到
链路追踪有个核心的要点是什么
透明
要对开发者
尤其是对我的业务开发者
要几乎零侵入的成本来支持分布式链路追踪
论文里面想法也很简单
只要对我们核心的kit基础库做一次改造
让各个组件都支持链路追踪
因为你后续就是用的通用的这些组件
比方用Redis用gRPC用HTTP框架
我只要在middleware里面把这些功能做好了
后续其实你用的时候你就不用管了
基本上会包含这些链路追踪的上报信息
比方Redis的上报Memcached的上报
MySQL的上报都可以做掉
问题就来了 比如我在加gRPC的一个服务里面
生成了一个Trace的链路追踪信息
想象它是头节点
这些信息是怎么传递到下游
传递到我的一个服务内的各个组件呢
这是第一个问题 就是进程内怎么传递
所以如果我们学过Go的context的话
我们应该知道 用context的WithValue
我们可以携带一些context的信息
携带一些上下文的信息
甚至携带链路追踪的信息 这个是第一个
你的context只要传递到各个方法里面
你都能最终获取到它的Trace的一些信息
那么你在调别的RPC的时候
就可以重新上报Trace的链路调用信息
所以这个是第一个
对于跨服务我们刚才也讲了
跨服务的时候 在gRPC的传递过程中
你就要利用跨服务的元数据传递
传递到对端的时候再重新挂载到context
这样每个人保证好自己进进程的传递
以及每个人保证好跨服务的时候元数据的传递
最终所有的路径都能覆盖好
我们当时在Go里面其实就核心用context
其他语言可能有ThreadLocal
或者是自己的框架的context都可以支持的
我们当时主要是kit库
只要涉及到网络通讯的组件
我们都会去覆盖链路追踪
包含不限于Redis Memcached
RPC HTTP
database消息队列queue
这些我们都会去覆盖
我们可以看Kratos里面的一个demo
在Kratos middleware目录里面
有个tracing目录
这个目录其实就是支持链路追踪
我们目前规范用的就是用的Go的OpenTelemetry
用了它以后 我们只要对于server节点
我就实现它的一个middleware
这里是new一个trace对象
如果是HTTP的话
如果是gRPC的话
我会设定一些组件信息 哪个指令
挂载到它的就挂在它的carrier 挂载到上下文传递里面去
挂载到context里面去
最终在挂载好之后
就可以开启链路追踪的启动
和defer它的结束
然后最终去执行我的业务的handler
其实是非常简单的
包括client也是一样的
就是一个是server端
一个是client端 链路追踪我们都会去包含
如果未来Kratos引入Redis和Memcached
我们同样的都会去用同样的方式去把它的链路追踪信息
通过middleware的方式
通过插件的方式打入到我的基础设施的组件里面
那么你用的时候其实感知比较少
就会用起来非常简单
所以他基本上对我们业务的开发人员来说是零侵入的
这个叫植入点
再看一下 我们当时因为一些历史原因
一开始没有用开源
我们最早是自己去做的一套链路追踪体系
对于某一台主机上的某一个容器里面的某一个应用
我们使用了自己的一个SDK
那么它可能是通过UNIX socket
上报到我们的log agent
这个log agent就是我们刚刚日志采集的log agent
当然你的dapper SDK也可以不走网络直接写日志文件
那么只要我的log agent支持采集
最终通过我们的消息队列
我们的传输体系
然后再被我们消费者消费以后
当时是持久化用的HBase
因为HBase RowKey刚好是可以设定为TraceID
我们再把数据冗余一份写到ElasticSearch
因为方便做搜索
我要去搜某一个服务
某个时间段内哪个耗时最高
这些其实涉及到排序和过滤
我们就用ElasticSearch来做搜索
最终落入了一个叫Dapper Analysis的分析服务
这个分析服务其实就是一套很炫酷的UI
我们的开发人员通过我们的APM平台
就可以看到这套炫酷的UI
然后他去诊断问题也好
解决问题也好 都会非常的方便
所以开源生态里面的组件基本上也是类似的
这里面有个小细节 就是TraceID的生成
TraceID其实要全局唯一
我们目前其实是用的UUID 因为它特别大
所以我们再用CityHash
用一个哈希算法把它转成一个INT64
就是用CityHash加上UUID得到一个INT64
同样SpanID的算法也是类似的
当然你可以用MurmurHash也是一样的
用MurmurHash算法也是可以的
我们在设计目标里面也提到过一个什么问题
就是它链路追踪的性能问题
我其实要关注两个东西的开销
第一就是我正在被监控的服务本身
生成追踪和收集追踪导致的性能下降
这个问题怎么做
我们可以想象一下 你哪个请求分配一个Trace对象
就是在这个请求结束后
你肯定这个对象就扔掉了
我们最容易想到就是用一个sync.Pool
把这个对象做一个cache
这个其实跟我们当时用OpenTracing
包括新的OpenTelemetry的实现是一样的
看他内部是不是用sync.Pool
去减少这种对象的频繁申请和分配
第二个开销其实来自TraceID的一个生成
UUID加CityHash的计算成本还是蛮低的
我认为这个也是可接受的 所以这个是第一个
第二个问题其实要复杂一点
你需要使用一部分的存储和计算资源来存储这些数据
来分析这些数据
你采集到一个地方去
你肯定要做存储以及要做分析
那么这个其实是Dapper性能影响最关键的部分
你不能说我因为要做链路追踪
每一个请求所有的信息全部都收集起来
它的存储成本其实非常高 怎么办
就是第一 假设在我们紧急的一些情况下
我后端的HBase或者ElasticSearch压力特别大
写查询压力很大或者存储压力很大
我是可以通过我们的log agent下发一个策略
就是管控一下
不让他去采集这么多数据上报上来
让我的系统可以缓口气
另外就是如果log agent负载很高
一般来说log agent其实跟实现有关
我们是既会修改它的nice值
让它的调度权重变低
同时我们在实现上也是尽可能只吃到一个核
不会去吃很多核 尽量只吃一个核
这个属于agent的一个优化
还有刚刚说的紧急情况下
你可以去流控
或者是丢弃一些数据
让我们的HBase或者ElasicSearch不会扛受那么大压力
就是下放策略
还有一个方法叫采样
我们想象一下 如果全量的数据都去采集
每一个请求它依赖的组件又非常多
每一个组件 每一个服务它又要调用很多RPC
每一个RPC又要调用很多组件
一个请求进来 可能到内部系统
可能是乘以100的放大倍数
你可以自己算一算 真的有可能是乘以100
就比方说你会调Redis
调Memcached 调MySQL等组件
然后你要调别的好多个RPC
好多个RPC又要调很多Redis
Memcached MySQL各种组件还有消息队列
其实整个加起来它真的就还蛮大的
有可能就是乘100乘50都是有可能的
就是一个请求它的扇出的组件 网络组件非常之多
所以你如果全量去存储这些数据
我认为成本非常高
我们是不是可以这样 曾经发生过的事情
有可能还会频繁的发生
曾经出现过性能衰减
性能下降的这种情况下
不可能只出现一次抖一下
你这个服务就真的只这么抖一次 以后再也不出现了
你追踪下来其实也分析不出来为什么
如果它是一个常态 也不是叫常态
如果它出现过一次
它的确是因为你的代码导致的性能问题
那么我认为它以后还会出现
并且有可能会出现上千次
那么基于这个假设
我们不全量收集数据
我们进行数据的采样
也就是说采样的过程中
有可能还是会把这种case给采到
那么如果能采到
我去分析性的问题的时候就能利用这个数据
说到采样跟踪采样
我们程序员一般最容易喜欢什么
喜欢1024对吧
所谓的固定采样
那么固定采样来说
就1/1024随便拍一个数字
这个简单的方案对我们高吞吐的线上服务来说是非常有用的
什么叫高吞吐的服务
就是你的QPS非常高
那么1/1024
你的整个采样就是采到的数据其实也不少
你那些感兴趣的事情在大吞吐流量的情况下
仍然有可能会经常出现
所以说它容易被捕捉到 那这个没问题
因为你的吞吐量足够大
这种偶尔出现的一些性能抖动
性能下降其实很容易被踩到
但是有些服务
他QPS没那么高
如果你的采样率特别低
比方说QPS一秒钟就10个
你采样了1/1024 我什么时候才能采到
会导致错过一些重要的事件
其实会大量的错过这种重要事件
你如果要开较高的采样率的话
你的性能就是有又有些损耗的
我们同学很容易想到我是不是可以通过配置中心
去针对不同的服务去改配置的策略
你通过配置中心配置A服务多采一点
B服务少采一点 这个接口调的就少采一点
这种手动干预其实是很需要运营和运维成本的
我们尽量试图避免
在Dapper中这种手动去干预的
有没有一个比较好的算法呢
我们其实希望一个算法是什么
就是在高QPS情况下低采样率
在低QPS情况下高采样率
所以我们就用积极采样
理解为单位时间内你期望采多少条
比方说我期望一秒钟至少给我来个5条
那么在高QPS下一秒钟才5条
你一秒钟一万的QPS只采5条 它采用率是多少
5/10000
然后采用率自然就下降了
那么在低QPS下 你的QPS一秒钟10个请求
那么我一秒钟期望才5条
是不是我的采样率就是50%
所以它是随着你的QPS增加 采样率下降
随着你的QPS下降 采样率升高
所以它这个算法就实现非常简单了
做一个滑动窗口
然后希望采个几条
而且很容易实现
这个是非常容易做到的
我们后面就把固定采样延伸到了机器采样算法上
是不是就做完了呢
没有
我们刚刚讲了
当我们的容器的节点数量非常多
你去使用积极采样
注意刚才说一秒钟采5条 我有1000个节点
是不是我一秒钟采5000条记录
意味着积极采样仍然可能会导致采样的样本非常多
有时候我们需要控制写入到中央的存储
比方说HBase或ElasticSearch的数据总规模
我希望去做一个二级采样
我希望管控一下 不要上报那么多
很容易想到下发一个策略到log agent
让它无脑丢弃20%
但是这样丢会导致某一个TraceID的某一个单元
某一个Span就被你真的丢掉了
所以你的整个调用链路是不完整的
你的数据某一个请求过程中
它的数据是不完整的
你这个TraceID看下来好像似乎是有些问题的
因为它不符合事实 中间有些数据被你丢掉了
怎么做二级采样可以来管控
可以设计这样的一个算法
我们利用一个假设所有Span
如果它是一个特定的请求
特定的跟踪
它们共享同一个TraceID
也就是同一个请求的TraceID的创建的所有Span单元
它必然是共享同一个TraceID的
也就是这个Span可能横跨了好几千台主机
但是它的TraceID就是只有一个
是不是可以用一个哈希算法把这个TraceID转换成一个标量Z
这个标量Z这就是个浮点数 0~1的浮点数
是不是可以再下放一个策略
丢20%的流量
我只要用这个TraceID用一个哈希算法把它转成一个浮点数
比方说大于0.2的我就放行
小于0的话我就丢弃
这样子不就丢掉了20%的流量
这样的话好处是什么
因为你无论横跨多少台主机
大家发现这个TraceID算出来肯定都是一样的
那么你这个策略在多少台主机上都会生效
所以它可以很优雅的去掉
我们不想把它写入到HBase或者ElasticSearch的这些多余数据
同时横跨几千台主机
他的TraceID算出来都是结果都是一样的
所以他一丢全丢
要么全保留 要么全丢弃
所以不会出现一些残留的孤儿Span
你的调用链路不完整
这种情况其实不太会出现的
所以我们针对接口级 针对服务级
针对全局级
都可以用二级采样的策略去管控某个接口
某个服务 某个全局
对我下游来进行一些保护
比方说HBase或ElasticSearch
空间不够或者是压力过大的时候
我就可以进行一些采样来进行保护
下发agent其实效率是最好的
因为下发agent还没到传输层
就在本地给drop掉了
这个其实是最好的
如果更好的话
其实可以把这个策略下发到我们应用的框架里面
但这样可能对业务的侵入
对我们基础库的侵入会比较大一些
所以目前我们其实是下方发策略到agent
也就是说我应用其实不管 写到本地以后
我的agent在上报的时候把它丢掉
这样的话其实是把这个逻辑搬到了agent里面
然后你的基础框架是不用去管的
那么还有一种情况
有一些服务它处于很底层
比方说账号服务存储这些越被依赖多的服务
网关层使用积极采样以后
对于下游downstream的服务采样率仍然很高 怎么办
可能需要考虑这种场景
针对底层的服务再做一次采样
这个我觉得大部分情况都遇不到
但是如果你遇到的话
在对最底层的一些服务还要进行一些数据丢弃
可能才能不会采那么多
一个请求扇出到它下面的时候肯定是非常多的
基于我们刚刚的一些它的实现
以及它的性能的一些优化
我们最终把这套系统搭出来
就做了几个有用的API 第一个就是搜索
我们一般出问题 比方说我是BFF的开发人员
我肯定是按照服务名按照某一个接口
在这地方搜某一个服务 搜某一个接口
这里有排序
比方开始时间排序
按耗时排序
我排个序之后我就可以找到头部的
耗时比较大的这些接口是谁
内部因为他有很多标签
这些标签其实就是一些规范 它是什么组件
IP是什么 它是什么方法名之类的
可以继续搜索
所以第一个接口就是ElasticSearch做检索的
那么检索完之后我就查看详情
详情其实就是查看单个TraceID
查看它整个的一个调用链路信息
包含像每一个调用单元
它的耗时的一些统计
它的Span内部的组件的名称
调的哪个Key 哪个方法
还有他很多依赖的服务
就是所谓的单元全部会在里面
比方说我们这个地方有个接口
它调用的是我们稿件服务
查了两次Memcached和一次Redis
然后它的整个耗时的分布是什么样的
其实跟开源的这个图做的都基本上类似
基本上是非常类似的
我去根据排序查看单个TraceID
找到头部的这些请求
我再去做一些分析
找到我到底这段时间内我的接口为什么这么慢
它可能是不是有一些哪个地方有性能下降
我去诊断这个问题就非常方便了
这个是第一个
我们还有个场景
我们是不是经常在公司内老板要求
画一画服务之间的架构图
我们现在内网服务上千个
你要画全局的依赖图
其实是非常复杂的
而且服务之间的依赖关系他会经常改变
因为你经常要发版
有时候去掉一些依赖 有时候加上一些依赖
所以从配置或者从代码去推断
所有的这些服务的依赖关系 能做得到 但是很复杂
有没有一个比较好的方式呢
我们想象一下 如果你的全局的各个系统都接入了链路追踪
那么你就可以通过这个链路追踪的信息
推断出所有服务之间的依赖关系
比方说任务和任务的依赖关系
服务与服务的调用关系
服务和组件的依赖关系
你就可以出一个每个服务之间的全局概览图
这个全局概念图我认为有几个作用
我在去做分析热点的时候
我们就看哪些线越密集连到某一个人
说明它就是一个被请求扇出非常多的节点
那么他就要考虑到多集群
甚至是独占资源去隔离
或者是多集群去扩容
这是第一个
第二就是说我通过全局概览图去摸盘
就是去盘点我的整个架构
哪个地方可能会存在一些隐患
或者说我要去做我们公司的多机房的建设的时候
哪些服务可以优先搬出去
哪些依赖关系是是合理的
甚至说哪些地方我去优化一下
减少一些调用的层级
这些都是可以值得考虑的
我们利用接口的话
我们还支持搜索某一个BFF 或者搜索任何一个节点
它的下游可以全部列出来
他的上游也可以列出来 它的下游可以列出来
我们不可能每天看一个全貌的图
我可能是某一个BFF的owner
我就搜它的名字
我自己去盘点我哪些服务需要去做架构优化什么的
所以它也支持局部的搜索
除此之外还可以支持一个叫推断环依赖的功能
在一个复杂的业务架构体系里面
我们很难避免全是层级的调用
就是A调B B调C这样完全向下调用的关系
内部的服务之间真的非常难保证
但是我们应该尽可能保证一点
就是调用栈向下
或者说我们调用栈不应该产生环依赖
什么叫环依赖 A调B B调A就产生一个环
A调B B调C C调A
就是从某一个节点出发
不管它这种通过哪个路径它可以走回来
我们就把这种叫做环依赖
我们尽量去试图避免产生环依赖
尽量保证我们的调用栈是向下的
不能高层调低层
低层也调高层 肯定就容易产生环依赖了
比方我们当时就自己发现内部就存在A调B B调A
账号调关系链 关系链调帐号
这个就很尴尬了
我们要去避免掉
通过做链路追踪
我们做完很长一段时间
其实也有一些经验和优化的总结
简单来说性能优化
我们发现大部分人写代码的时候
容易写同步调用的代码
就是能够并行的 他没有并行
他就是A调B B调C这么一路写下去
那么它其实耗时是什么
就是A+B+C
你的耗时肯定增加
所以我们当时有看到这种代码
感觉两个服务没有任何关系
但它就是两个耗时
一条线的尾部和另外一条线的起点是粘在一起的
肯定是个串行调用
这种是第一个
第二个就是缓存读放大或者写放大
或者数据库的读放大写放大
举个例子 拿到一批ID
他非要写个for循环去Redis里面一个个Key去查
或者在数据库里面一个个去查
或者在update更新的时候也是一个个去update
或者insert的时候去for循环一个个insert
我觉得都不好 其实都是很高频密集的
对基础设施像缓存数据库
频繁的网络密集其实是不好的
像Redis支持MGET支持一些批量的pipeline
像我们的数据库支持in操作
insert支持逗号分隔插多个value
update会相对不一样一点
尽可能我们去batch
尽可能去打包一个请求
batch优化其实在各个地方都看得到
比如我们讲goim的时候
其实就有个很关键的点
我们通过消息队列Kafka消息发到
Kafka的时候Job不是会消费吗
Job消费我是不是来一条就要广播一次
我是不是可以按时按量凑够一波消息
再去调我们comet的广播服务
这样的话你就会尽可能减少广播的次数
因为它都是网络调用
网络的System call调用
这种合并的思路其实到处可见
你发到某一个节点的时候
RPC只发一次
在它内部的时候尽量让消息打包去传递
比方说像我们的直播弹幕
直播弹幕在服务端是做了一些合并的
有可能是好几十条弹幕一个指令全部收到
然后弹幕的一些时间轴把它打散一下
因为有些发的快慢一点
然后你不可能来的时候一下全出来
你再把它打散一下
效果上看上去有个几百毫秒的延迟
但是你的batch聚合的消息足够大的话
性能其实就会好
因为你减少了系统调用
你减少了系统频繁高密集的调用
包括你的函数也好 包括你的系统调用也好
包括你的网络调用也好
包括你的发送到某个节点的RPC网络流量也好
它其实都是一种合并
所以这个其实是非常关键的一个点
另外我们还发现有很多这种服务接口的聚合调用
其实什么意思
我们发现像我们的账号服务二级缓存的
这种以前账号服务是没做二级缓存的
也就是说它依赖关系链
依赖用户基础信息
依赖passport信息要扇出很大
后来我们在这种聚合调用的服务
都会做二级缓存
减少整个向下游扇出的RPC的次数
某些业务形态上你可以适当考虑一下
合久必分 分久必合
让这些RPC的这些服务整合回来变成一个服务
这些经验优化还不好玩
还有一个点算是非常有用的一个经验
第一
如果我们有时候接口会报错
报错以后我们通常会在ElasticSearch里面打日志的时候
通常都会把TraceID给打上
那么你把正确的也好 错误的也好
TraceID都打到日志里面去
我是不是很方便的点一下TraceID
可以跳到我的链路追踪的系统去查看
是不是有一些链路信息可以辅助我来分析
这个是第一个点
第二个我前面讲了一个例子
有时候不一定是链路追踪
它可能没被采用到 你就看不到
另外你看指标因为它是汇总维度的 看不到一个明细
指标是汇总的
链路追踪不一定被采样到
这个时候日志肯定是不丢的
因为日志无论是INFO
无论是某一个接口的正确还是错误
我们都会去打日志
现在要分析问题
但是要分析问题的时候
下游的那些日志又不在我这里
要一个个去点很麻烦
所以我们当时的想法就是说我的TraceID和SpanID仍然生成
但是我采样不上报到云端
不上报到我们的HBase和ElasticSearch
但是TraceID和SpanID仍然生成
生成这些ID以后
还是通过我们的gRPC的metadata和context
在进程间和跨进程传递
但是我在记日志的时候都会把TraceID在SpanID都打上
就是虽然采样 但是我会日志里面都打上这个ID
打上以后有什么用
前面我们有一个API查询某一个微服务
它的下游依赖是什么
我查出了这个信息以后
我是不是可以再去ElasticSearch
根据TraceID和SpanID
把对应的服务的索引调一下它的搜索
把它的日志捞出来
我把我的下游的那些依赖的服务
他的日志都去调一遍
其实就是一个迭代 每个人都查一遍
再捞到内存里面
然后针对我这个接口
根据SpanID TraceID还有ParentID
再把他的关系调用链还原回去
然后再把日志填上去 一个个填上去
看到一个完整的整套系统的调用日志
通过TraceID SpanID还有ParentID
你可以把它还原出你的真实日志的情况
这个是非常爽的
你去看日志的时候
可以把下游的这些服务的日志全给你关联上
全捞回来
我觉得这个是非常好用的
我在分析问题的时候可能用这个功能
整个问题我觉得就非常爽了
叫与异常日志系统去集成
好第二个叫与用户日志集成
这里给两个 我们有时候老板问我这个接口为什么报错
我这个APP为什么打不开
问的时候你去查链路追踪没采样
你去查客户端的日志查不到
我建议一定要支持白名单
对于老板 或者对一些重要的VIP客户
他们的日志全采样 这个是非常重要的
是他们的这些特殊ID是全采样的
你在分析问题的时候就不会问你的时候你查不着
然后它又没有现场
你就很尴尬
这个是必杀技
另外我们链路追踪生成的ID
我是不是可以通过HTTP header带到我们的移动端
带到我们的Web前端
那么拿到这个以后 我们有时候遇到某个功能
点完之后页面崩溃或者点完之后接口报错
我是不是可以在我的APP或者Web上
把这些用户的调用的报错的TraceID给存起来
他点用户反馈的时候或者说自动上报的时候
把TraceID的信息给报上来
用户原来在界面上可能就遇到这个问题了
我在分析它哪一个界面以及它哪个TraceID
把这些信息关联起来之后
可以打包一个日志上传到我们的服务器
我就可以针对用户去做一些诊断
就可以协助用户解决他的问题
也是非常有用的一个点
链路追踪和日志很多地方是可以集成的
我们也要多想一想哪些地方可以关联起来
这个也是我们的一个优化经验
除了这些数据
你采的大量的采样数据
它其实也可以用于做容量的预估
如果我们再追踪里面把request size
和response size也记录进去
以及我知道某一个 A服务
它到底对下游放大的是一倍还是两倍
以前为什么知道容量预估是困难的
因为你上有扩大一倍
你下游其实是一定扩大一倍吗
不一定
因为你上游扩大一倍
有可能你的扇出请求等于二
那么下游其实要扩大两倍
所以为什么容量预估做不准 怎么办
如果我们有了这套系统
我们可以怎么做容量预估
比方我要压下单的场景
下单是个BFF我现在预估双十一的时候
下单这个场景它会扩大一倍的流量
我只要根据历史的整个链路追踪的这套体系
上游扩大一倍
下游到底那些系统 它的放大系数是多少
我就相应地对下的服务进行扩缩容
这样其实相对来说是准确的
另外我们采集到很多这些信息
每个单元他都会带上hostname
带上一些节点的信息 IP的信息
是不是把这些数据放到Flink实时计算
或者放到我们的离线计算
我就可以去经常做实时或者离线计算运算
看是哪个地方掉哪个地方容易出故障
那是不是有网络链路有问题
是不是跨机房调用导致它耗时
是不是因为这两个节点
它们的容器宿主机上可能有一些风险
因为你报错有可能就真的是
某一个节点到某个节点经常报错
那你就分析出网络的热点
分析出网络的一些易故障点
那么用这些统计的一些数据来改善
我们的整套部署里面的某一些环节
可能有些问题有些漏洞
其实也是非常有用的
因为我也非常鼓励大家
包括我们Kratos也是用的OpenTelemetry规范
标准化的一个规范
以前是我们自己实现的接口
现在我们就统一用它的规范了
这样的话就避免内部的什么特殊系统跟开源不太适配
就避免这种不配的问题
这个是链路追踪
我们前面讲的日志和链路追踪有些结合
我们最后一个必杀技就是指标
指标也有很多跟别人做的不一样的
我会把我的一些思路
包括你们自己去构建你们自己的仪表盘的时候
也有很多可以借鉴的经验