├── dapper ├── span.png ├── bigtable.png └── dapper.md ├── README.md ├── jaeger ├── Features.md ├── tchannel-go │ ├── readme.md │ ├── payload-Reader.md │ ├── arguments.md │ ├── peer01.md │ ├── peerScore-and-idleSweep.md │ ├── framepool-and-calloptions.md │ ├── frame.md │ ├── peer03.md │ ├── allchannel-and-subchannel.md │ ├── message-exchange.md │ ├── payload-WriteBuffer-ReadBuffer.md │ └── context.md ├── TChannel │ ├── affinity.md │ ├── 流控.md │ ├── http-and-json.md │ ├── thrift.md │ ├── go1.5版本的一种tcp监听关闭处理方式.md │ ├── 熔断器.md │ ├── metrics.md │ └── keyvalue例子.md └── jaeger的技术演进之路.md ├── opentracing-go ├── Log.md ├── Propagation.md └── opentracer-go源码阅读一.md ├── trace系列文章笔记目录.md ├── basictracer-go ├── spanrecorder.md ├── example.md ├── tracer.md ├── span.md └── event-propagation.md ├── opentracing标准 ├── 设计分布式追踪系统的有效方法.md ├── 概念与目标.md ├── 名词与术语.md └── OpenTracing APIs.md ├── 分布式跟踪系统——产品对比.md ├── SUMMARY.md └── appdash ├── recentstore-and-limitstore.md ├── opentracing.md ├── tracer-and-span.md ├── annotations-and-event.md ├── store.md ├── recorder-and-collector.md └── reflect.md /dapper/span.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1046102779/opentracing/HEAD/dapper/span.png -------------------------------------------------------------------------------- /dapper/bigtable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1046102779/opentracing/HEAD/dapper/bigtable.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opentracing 2 | 3 | 电子书地址: 4 | 5 | 1. [看云地址](https://www.kancloud.cn/cdh0805010118/opentracing) 6 | 7 | 2. [love2.io](https://love2.io/@1046102779/doc/opentracing) 8 | 9 | 10 | 为了对分布式跟踪系统有系统的了解,我的阅读清单 11 | 12 | 1. Dapper论文 13 | 2. OpenTracing标准、以及basictracer-go源码; 14 | 3. Appdash 15 | 4. Jaeger, 包括其中的rpc框架tchannel-go。并阅读了tchannel协议 16 | 17 | 不过以上都是关于golang的实现,有一些局限性,没有看其他语言实现的分布式跟踪系统pinpoint, zipkin, CAT等 18 | 19 | 希望通过以上的标准理解、协议理解和源代码阅读,能够对分布式跟踪系统有比较深入的了解 20 | 21 | 这个电子书访问比较慢,以后有合适的,再放到其他平台上 22 | -------------------------------------------------------------------------------- /jaeger/Features.md: -------------------------------------------------------------------------------- 1 | ### Features 2 | 3 | Jaeger用于微服务监控和故障排除的分布式跟踪系统,它的主要特性有: 4 | 1. 分布式上下文传播 5 | 2. 分布式事务监控 6 | 3. 根本原因分析 7 | 4. 服务依赖分析 8 | 5. 性能/延迟优化 9 | 10 | #### 可扩展性 11 | Jaeger后端设计的两个特性: 12 | 1. 没有单点故障 13 | 2. 根据业务需要动态扩容 14 | 15 | 例如:在Uber任意给定的一个Jaeger安装可以很容易地每天处理几十亿spans 16 | 17 | #### 对OpenTracing的原生支持 18 | Jaeger后端、Web UI和工具库都被设计成从底层就支持OpenTracing标准 19 | 1. 通过span的引用,traces是一个有向无环图 20 | 2. 支持强大地带自定义类型的tag和结构化日志 21 | 3. 通过baggage,支持通用分布式上下文传播机制 22 | 23 | #### 多后端存储支持 24 | Jaeger支持两种流行的开源NoSQLs数据库,作为trace存储:Cassandra 3.4+ 和 ElasticSearch 5.x/6.x. 同时Jaeger也正在和其他数据库展开合作,包括:ScyllaDB, InfluxDB, Amazon DynamoDB. 为了支持简单的测试使用,Jaeger也可用于内存作为后端存储,这仅仅是用于测试使用。 25 | #### 现代化的Web UI 26 | Jaeger WebUI 是使用React开发实现的。几个优化点在v1.0版本发布,主要是允许UI有效地处理大量数据,并且展示带上千个spans的traces(例如:我们尝试了一个带有8000spans的trance) 27 | 28 | #### 云原生部署 29 | Jaeger后端是有多个Docker Images构成。这个二进制支持多种配置方法, 包括命令行参数,环境变量和多格式的配置文件(yaml, toml等等), 支持使用K8s模板和Helm chart来部署k8s集群 30 | 31 | #### 观测 32 | 所有Jaeger后端组件都暴露了Prometheus默认的metrics(其他后端metrics也被支持), 日志通过[zap](https://github.com/uber-go/zap)写入 33 | 34 | #### 与ZipKin兼容性 35 | 尽管我们强烈推荐使用OpenTracing API检测应用程序,并且绑定到未来具有更多特性的Jaeger客户库, 但是如果你的组织已经投入使用了ZipKin库去检测应用程序,你不必要重写代码。Jaeger通过HTTP接收ZipKins格式(Thrift or JSON v1/v2)接收Zipkins定义的spans. 切换到Zipkin后端只是将来自ZipKin库的流量路由到Jaeger后端 36 | -------------------------------------------------------------------------------- /jaeger/tchannel-go/readme.md: -------------------------------------------------------------------------------- 1 | # TChannel 2 | 3 | TChannel是一个支持多路复用和帧协议的RPC框架。tchannel-go是这个协议的Go版本实现,包括Hyperbahn的客户端库。 4 | 5 | 如果你想要首先编写一个小型Thrift和TChannel服务,请查看[本指南](https://github.com/uber/tchannel-go/blob/dev/guide/Thrift_Hyperbahn.md)。如果你不喜欢这个指南,可以帮忙做一些[贡献](https://github.com/uber/tchannel-go/blob/dev/CONTRIBUTING.md). 6 | 7 | ## 总览 8 | 9 | TChannel是一个网络协议,它支持: 10 | 11 | - 一个request/response模型; 12 | - 在同一个TCP socket上复用多个请求; 13 | - 无序响应; 14 | - 流请求和流响应; 15 | - Checksummed帧; 16 | - 任意负载的传输; 17 | - 多语言的实现简单; 18 | - 类似redis的高性能; 19 | 20 | 这个协议为IPC,意图运行在数据中心网络上。 21 | 22 | ## 协议 23 | 24 | TChannel帧有个固定长度的头部和3个可变长度字段。底层协议没有给这些字段赋予含义,但是client/server实现使用第一个字段去表示RPC模型中唯一endpoint或者函数名称。接下来的两个字段可用于任意数据。对于这三个字段的使用,有些建议如下: 25 | 26 | - URI path + HTTP method and headers as JSON + body 27 | - Function name + headers + thrift/protobuf 28 | 29 | 上面两条建议,都是针对arg3个参数赋予了含义, 30 | 31 | 对于第一条建议: 32 | 33 | 1. arg1 为URI路径; 34 | 2. arg2 为HTTP的请求method和headers; 35 | 3. arg3 为HTTP的body数据 36 | 37 | 对于第二条建议: 38 | 39 | 1. arg1 为方法名; 40 | 2. arg2 为headers 41 | 3. arg3 为thrift/protobuf的序列化数据 42 | 43 | 注意,TChannel编码只支持UTF-8。如果你想要使用JSON,你需要在TChannel之外进行字符串化和解析。 44 | 45 | 这个设计支持高效路由和路由转发:routers需要解析第一个或者第二个字段,但是没有解析也能够转发第三个字段; 46 | 47 | 在这个系统中没有客户端和服务端之前的概念。每个TChannel实例能够发起和接收请求,只要求一个可以监听的唯一端口。这个要求可能未来会发生变化。 48 | 49 | 详见[protocol specification](http://tchannel.readthedocs.org/en/latest/protocol/)。 50 | 51 | ## 例子: 52 | 53 | - [ping](https://github.com/uber/tchannel-go/blob/dev/examples/ping)。一个使用raw TChannel的ping/pong例子。 54 | - [thrift](https://github.com/uber/tchannel-go/blob/dev/examples/thrift)。一个使用Thrift协议的client/server例子。 55 | - [keyvalue](https://github.com/uber/tchannel-go/blob/dev/examples/keyvalue)。 具有单独server和client二进制的一个keyvalue Thrift服务 56 | -------------------------------------------------------------------------------- /opentracing-go/Log.md: -------------------------------------------------------------------------------- 1 | # Log 2 | 3 | 该log设计参考了[uber-go/zap](https://github.com/uber-go/zap), 日志值类型非常多,该设计把日志类型分为三类:string, int64和interface{}, 其中: 4 | 5 | 1. string={string}; 6 | 2. int64={bool, int, int32, uint32, int64, uint64, float32, float64}, `notice: 这里没有覆盖int16和uint16` 7 | 3. interface{}={error, object, lazyLogger, noop} 8 | 9 | 根据Span interface中的LogFields和LogKVs方法, log.Field应该会包含{key, value}, 同时因为value值类型包含三类,所以Field的结构如下: 10 | 11 | ```shell 12 | type Field struct { 13 | key string 14 | fieldType fieldType 15 | numericVal int64 16 | stringVal string 17 | interfaceVal interface{} 18 | } 19 | ``` 20 | 21 | Field提供了返回key,返回value和String三个方法操作,这已满足需要; 22 | 23 | **注意,Span Tags和Span Logs不同的一点在于,Logs是可以累加的,并不是map结构,是列表存储[]log.Fields** 24 | 25 | 实现Span Logs的标准化结构如下: 26 | 27 | ```shell 28 | func String(key, val string) Field { 29 | return Field{ 30 | key: key, 31 | fieldType: stringType, 32 | stringVal: val, 33 | } 34 | } 35 | 36 | // 其他类型都类似处理, 对于error和lazyLogger类型,不需要指定key 37 | func Error(err error) Field{ 38 | return Field{ 39 | key: "error", 40 | fieldType: errorType, 41 | interfaceVal: err, 42 | } 43 | } 44 | 45 | // LazyLogger 对厂商暴露出的自定义日志实现 46 | type LazyLogger func(fv Encoder) 47 | 48 | func Lazy(ll LazyLogger) Field{ 49 | return Field{ 50 | fieldType: lazyLoggerType, 51 | interfaceVal: ll, 52 | } 53 | } 54 | 55 | // Noop用于忽略一些日志处理; 56 | // 例如:dev/test/prod的trace日志等 57 | func Noop() Field{ 58 | return Field{ 59 | fieldType: noopType, 60 | } 61 | } 62 | ``` 63 | 64 | 我们可以兑log.Field进行编码操作,类似于json/xml解析库;这个Encoder interface列出了log.Field中value值所有类型;所以,如果要对log.Field编码操作,则需要实现所有值类型,共三大类。 65 | 66 | 67 | 对于Span interface中的LogFields和LogKVs两个方法都是对[]log.Fields进行存储,这两个方法存在一个数据结构化转换方法: **InterleavedKVToFields**, 把key-value键值对列表转换成[]log.Field结构化日志数据。 68 | -------------------------------------------------------------------------------- /jaeger/tchannel-go/payload-Reader.md: -------------------------------------------------------------------------------- 1 | # Reader 2 | 3 | 该Reader提供一个32字节的内存空间,用于读写小字节的数据;每次读写都是从index=0的位置开始。一次性读取,反复使用;如果超过32字节的空间,则直接使用外界存储。 4 | 5 | 所以这个Reader一般用于小字节空间分配,超过maxPoolLenStringLen大小,则该Reader无意义。 6 | 7 | ```shell 8 | const maxPoolLenStringLen = 32 9 | 10 | // 把io.Reader的数据读取指定字节到buf中,每次读取io.Reader指定字节并写入到buf中。 11 | type Reader struct { 12 | reader io.Reader 13 | err error 14 | buf [maxPoolStringLen]byte 15 | } 16 | 17 | // 临时对象池 18 | var readerPool = sync.Pool { 19 | New: func() interface{}{ 20 | return &Reader{} 21 | } 22 | } 23 | 24 | // 从临时对象池readerPool获取一个Reader对象 25 | func NewReader(reader io.Reader) *Reader { 26 | r :=readerPool.Get().(*Reader) 27 | 28 | r.reader = reader 29 | r.err = nil 30 | } 31 | 32 | // 从io.Reader中读取2字节数据 33 | func (r *Reader) ReadUint16() uint16 { 34 | if r.err != nil{ 35 | return 0 36 | } 37 | 38 | // 从buf中获取2字节空间 39 | buf := buf[:2] 40 | 41 | var readN int 42 | readN, r.err = io.ReadFull(r.reader, buf) 43 | // io.Reader不够2字节 44 | if readN < 2 { 45 | return 0 46 | } 47 | 48 | // 大端读写2字节数据 49 | return binary.BigEndian.Uint16(buf) 50 | } 51 | 52 | // 从io.Reader中读取指定大小的字符串 53 | func (r *Reader) ReadString(n int) string { 54 | if r.err != nil{ 55 | return "" 56 | } 57 | 58 | var buf []byte 59 | if n <= maxPoolStringLen { 60 | buf = buf[:n] 61 | } else { 62 | buf = make([]byte, n) 63 | } 64 | 65 | var readN int 66 | readN, r.err = io.ReadFull(r.reader, buf) 67 | // io.Reader的数据长度不够n 68 | if readN < n { 69 | return "" 70 | } 71 | 72 | // 拷贝一次 73 | return string(buf) 74 | } 75 | 76 | // 和payload部分读取数据类似。先读取头部获取占用内存大小,然后再读取实际数据 77 | func (r *Reader) ReadLen16String() string { 78 | // 读取2个字节,获取真实数据长度 79 | n := r.ReadUint16() 80 | return r.ReadString(n) 81 | } 82 | 83 | func (r *Reader) Err() error { 84 | return r.err 85 | } 86 | 87 | // 释放临时对象Reader 88 | func (r *Reader) Release() { 89 | readerPool.Put(r) 90 | } 91 | ``` 92 | 93 | # 小结 94 | 95 | 与上节ReaderBuffer不同的是, 上节是数据内存块空间,而这小节是使用的io.Reader文件流形式。同时,该Reader适合32字节的内存临时空间,不适合超过该字节空间。 96 | -------------------------------------------------------------------------------- /trace系列文章笔记目录.md: -------------------------------------------------------------------------------- 1 | # Tracer目录 2 | 3 | | trace | 系列文章 | 描述 | 4 | | ------ | ------ | ------ | 5 | | [Google Dapper](https://static.googleusercontent.com/media/research.google.com/en//archive/papers/dapper-2010-1.pdf) | [《分布式跟踪系统论文》](https://gocn.vip/article/849) | Dapper是分布式跟踪系统的研究论文,基本上厂商都是参考这篇论文设计的| 6 | | 厂商trace产品对比 | [《分布式跟踪系统——产品对比》](https://gocn.vip/article/852) | 后续会陆续补充 | 7 | | [OpenTracing标准](http://opentracing.io/) | [OpenTracing——概念与目标](https://gocn.vip/article/854)| | 8 | | | [OpenTracing——相关概念术语](https://gocn.vip/article/856) | | 9 | | | [OpenTracing APIs](https://gocn.vip/article/858) | | 10 | | [opentracing-go](https://github.com/opentracing/opentracing-go) | [opentracing-go源码阅读一](https://gocn.vip/article/861) | 它是OpenTracing标准的schema实现| 11 | | | [opentracing-go源码阅读——信息携带](https://gocn.vip/article/862) | | 12 | | | [opentracing-go源码阅读——Log存储(完结篇)](https://gocn.vip/article/864) | | 13 | | [basictracer-go](https://github.com/opentracing/basictracer-go) | [basictracer源码阅读——TracerImpl](https://gocn.vip/article/868) | 它是对OpenTracing标准的最小实现,各大厂商可以不基于它实现自己的trace系统,直接以OpenTracing标准实现,并与basictracer-go同级 | 14 | | | [basictracer-go源码阅读二——Span](https://gocn.vip/article/874) | | 15 | | | [basictracer-go源码阅读——event&propagation](https://gocn.vip/article/875) | | 16 | | | [basictracer-go源码阅读——SpanRecorder与wire](https://gocn.vip/article/876) | | 17 | | | [basictracer-go源码阅读——examples(完结)](https://gocn.vip/article/878) | | 18 | | [Appdash](https://github.com/sourcegraph/appdash) | [Appdash源码阅读——Tracer&Span](https://gocn.vip/article/879) | 如果从trace角度看Appdash,它并没有遵循OpenTracing,同时从如果不使用opentracing,则Appdash与OpenTracing标准没有任何关系。它的出生早于OpenTracing标准, 只不过后来对Appdash做了一个很小的扩展,而且设计考虑得很弱| 19 | | | [Appdash源码阅读——Annotations与Event](https://gocn.vip/article/881) | | 20 | | | [Appdash源码阅读——Recorder与Collector](https://gocn.vip/article/882) | | 21 | | | [Appdash源码阅读——Store存储](https://gocn.vip/article/883) | | 22 | | | [Appdash源码阅读——RecentStore和LimitStore](https://gocn.vip/article/892) | | 23 | | | [Appdash源码阅读——reflect](https://gocn.vip/article/893) | | 24 | | | [Appdash源码阅读——部分opentracing支持](https://gocn.vip/article/894) | | 25 | -------------------------------------------------------------------------------- /basictracer-go/spanrecorder.md: -------------------------------------------------------------------------------- 1 | # SpanRecorder 2 | 3 | SpanRecorder接口用于Span的存储,也就是担任Collector和Storage的职责。它向厂商开放了自定义实现自己的Collector和Storage服务或者组件;存储数据={TraceID, SpanID, Sampled, Baggage}; 4 | 5 | ```shell 6 | type SpanRecorder interface { 7 | RecordSpan(span RawSpan) 8 | } 9 | ``` 10 | 11 | basictracer-go内部已经实现了一个基于内存的SpanRecorder。SpanRecorder具体实现赋值是在初始化globaltracer时指定的,也即tracer.options.SpanRecorder变量值; 12 | 13 | 基于内存的SpanRecorder实现: 14 | 15 | ```shell 16 | type InMemorySpanRecorder struct { 17 | sync.RWMutex 18 | spans []RawSpan 19 | } 20 | 21 | func NewInMemoryRecorder() *InMemorySpanRecorder { 22 | return new(InMemorySpanRecorder) 23 | } 24 | 25 | // 对RawSpan进行收集和存储工作 26 | func (r *InMemorySpanRecorder) RecordSpan(span RawSpan) { 27 | r.Lock() 28 | defer r.Unlock() 29 | r.spans = append(r.spans, span) 30 | } 31 | 32 | // 获取[]RawSpan的快照,因为瞬时干净列表, 只是如果要并发获取[]RawSpan以及厂商实现不限制内存的话,则内存猛增。 33 | func (r *InMemorySpanRecorder) GetSpans() []RawSpan { 34 | r.RLock() 35 | defer r.RUnlock() 36 | spans := make([]RawSpan, len(r.spans)) 37 | copy(spans, r.spans) 38 | return spans 39 | } 40 | 41 | // 存在GetSpans和GetSampledSpans两种方法,主要是因为: 42 | // Tracer和Span都存在Sampled,之间存在一定的关系 43 | // 1. 当Tracer设置这个tracer需要尽量保存下来时,Tracer.Sampled=true,不用理会Span.Sampled值 44 | // 2. 当Tracer.Sampled=false时,则可以根据span执行单元情况,选择是否要进行Span保留; 45 | func (r *InMemorySpanRecorder) GetSampledSpans() []RawSpan { 46 | ... // 所以这个是用来保留采样的Span, 与Tracer无关 47 | } 48 | 49 | // 清空[]RawSpan内存存储 50 | func (r *InMemorySpanRecorder) Reset(){ 51 | r.Lock() 52 | defer r.Unlock() 53 | r.spans = nil 54 | } 55 | ``` 56 | 57 | ## util 58 | 59 | 主要用来生成随机数, 赋值给TraceID和SpanID值; 60 | 61 | ## wire 62 | 63 | wire为binaryPropagator网络传输的协议数据流,该协议为google的protobuffer,在grpc中广泛使用。 64 | 65 | ```shell 66 | // bp协议数据格式 67 | message TracerState { 68 | fixed64 trace_id = 1; 69 | fixed64 span_id = 2; 70 | bool sampled =3; 71 | map baggage_items = 4; 72 | } 73 | ``` 74 | 75 | 该协议的数据格式在[《basictracer-go源码阅读——event&propagation》](https://gocn.vip/article/875)的binaryPropagator中通过Inject和Extract方法说明过。 76 | 77 | 同时,TracerState也实现了DelegatingCarrier接口,这个在accessPropagator中给厂商开放了自定义实现Carrier的数据存储和转换;这里不再赘述了。 78 | -------------------------------------------------------------------------------- /opentracing标准/设计分布式追踪系统的有效方法.md: -------------------------------------------------------------------------------- 1 | 当开始设计分布式跟踪系统时,第一步先把跨**RPC/IPC**之间的span串联起来,这样整条trace才能通。你可以从使用支持OpenTracing标准的服务框架开始(如:gRPC)。 2 | 3 | ## 专注高价值区域 4 | 5 | 从RPC层和你的web框架开始构建并走通整条trace链路。下一步,再从没有被RPC和WEB服务框架覆盖到的其他关键节点上,比如:redis、mysql、消息中间件等数据存储耗时的操作上进行节点监控。为高价值的事务创建一条关键链路的追踪轨迹。`注意:是关键路径,一条链路过多的span节点,会对业务性能造成的不良影响` 6 | 7 | ## 先走再跑,逐步提高 8 | 9 | 设计分布式跟踪系统,最大的价值在于为关键事务生产端到端的追踪。可视化展示所有sample到的追踪列表,并对error或者timeout等trace列表进行分析,并找到相应trace span执行单元的业务耗时,并进行业务优化。并对业务开发人员进行业务优化关键点有一定的指导意义,并根据丰富的数据报表统计,对同一类trace问题多,流量大的业务关键路径进行优化或者重新设计。 10 | 11 | 同时trace关键路径上的所有span节点,也可以根据dashboard UI分析数据统计的情况,进行细粒度的追踪或者粗粒度的追踪。 12 | 13 | # 监控框架 14 | 15 | 总体来说,集成OpenTracing,你需要做两件事情: 16 | 17 | 服务端框架修改需求: 18 | 19 | 1. 过滤器、拦截器、中间件或者其他处理输入请求的组件; 20 | 2. span的存储,存差异request context或者request到span的映射表; 21 | 3. 通过某种方式对tracer进行配置 22 | 23 | 客户端框架修改需求: 24 | 25 | 1. 过滤器、拦截器、中间件或者其他处理对外调用的请求组件; 26 | 2. 通过某种方式对tracer进行配置 27 | 28 | ## Operation Name 29 | 30 | 当业务方没有指定Operation Name时,默认的Operation Name示例: 31 | 32 | 1. request handler的方法名 33 | 2. web 请求路径 34 | 3. RPC服务名+方法名 35 | 36 | ```shell 37 | 注意: 38 | 如果是RESTful风格,url上不要带值, 例如: 39 | /v1/products/:product_id/code/:code 40 | 41 | request url: 42 | /v1/products/123/code/YN_001 43 | 44 | 则操作名默认最好为前者, 45 | product_id: 123, code: YN_001 附带在span tag上。 46 | ``` 47 | 48 | ## 确定需要追踪的请求 49 | 50 | 有些用户希望追踪所有的请求,同时,有些用户只需要追踪特定的请求。所以分布式跟踪系统的设计,应该允许用户去设置是否需要追踪,以满足这两种场景。两种方法: 51 | 52 | 1. 使用程序的注解,提供`@trace`标注,被标注的方法会被追踪。 53 | 2. 提供一种配置,允许用户去设置他们是否使用标准,所有的请求是不是应该被追踪。 54 | 55 | 当然,第一种方式是最友好方便的。GO语言标注实现可以参考Beego web框架 56 | 57 | ## 追踪请求的属性 58 | 59 | 用户可能需要追踪关于请求的一些信息,而不希望直接去操作span或者为span设置tag。为用户提供一种方式设置需要追踪的请求属性,并自动追踪这些属性值,是十分有帮助的。[Functional options for friendly APIs 60 | ](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis)。 61 | 62 | ```shell 63 | // SpanDecorator binds a function that decorate gRPC Spans. 64 | func SpanDecorator(decorator SpanDecoratorFunc) Option{ 65 | return func(o *options) { 66 | o.decorator = decorator 67 | } 68 | } 69 | ``` 70 | 71 | 另一种方式,是设置`TRACED_REQUEST_ATTRIBUTES`, 允许用户传递一个列表(例如:`URL`, `MOTHOD`, `HEADERS`), 然后你会在追踪过滤器中,包含这些属性中: 72 | 73 | ```shell 74 | for attr in setting. TRACED_REQUEST_ATTRIBUTES: 75 | if hasattr(request, attr): 76 | payload = str(getattr(request, attr)) 77 | span.set_tag(attr, payload) 78 | ``` 79 | 80 | 81 | -------------------------------------------------------------------------------- /分布式跟踪系统——产品对比.md: -------------------------------------------------------------------------------- 1 | ## 分布式跟踪系统设计目标 2 | 1. 低侵入性 3 | 2. 灵活的应用策略;(可以 [最好随时] 决定所收集数据的范围和粒度) 4 | 3. 时效性;从agent采样,到collect、storage和display尽可能快 5 | 4. 决策支持; 6 | 5. 可视化是王道;(丰富的数据报表) 7 | 6. 低消耗;在web请求链路中,对请求的响应影响尽可能小 8 | 7. 延展性;随着业务量暴增,分布式跟踪系统的高可用、和高性能表现依然好 9 | 10 | ## 分布式跟踪系统标准——[OpenTracing](http://opentracing.io/) 11 | 为了规范业界的分布式跟踪系统产品的统一范式,CNCF(大名鼎鼎的Cloud Native Computing Foundation)设计了trace标准,目前uber、apple等知名公司完全遵循该标准设计trace系统。 12 | 13 | ## 各大厂商trace系统对比 14 | 分布式跟踪系统各类产品,根据设计目标和标准形成综合对比: 15 | 16 | 产品名称 | 厂商 | 开源 | OpenTracing标准 | 侵入性 | 应用策略 | 时效性 | 决策支持 | 可视化 | 低消耗 | 延展性 17 | ---|---|---|---|---|---|---|---|---|---|--- 18 | [jaeger](https://github.com/jaegertracing/jaeger) | uber | 开源 | 完全支持 | 部分侵入 | 策略灵活 | 时效性高, UDP协议传输数据(在Uber任意给定的一个Jaeger安装可以很容易地每天处理几十亿spans) | 决策支持较好,并且底层支持metrics指标 | 报表不丰富,UI比较简单 | 消耗低 | jaeger比较复杂,使用框架较多,比如:rpc框架采用thrift协议,不支持pb协议之类。后端存储比较复杂。但经过uber大规模使用,延展性好 19 | [zipkin](https://github.com/openzipkin/zipkin) | twitter | 开源 | 部分支持 | 侵入性强 | 策略灵活 | 时效性好 | 决策一般(功能单一,监控维度和监控信息不够丰富。没有告警功能) | 丰富的数据报表 | 系统开销小 | 延展性好 20 | [CAT](https://github.com/dianping/cat) | 大众点评 [吴其敏](http://www.infoq.com/cn/presentations/the-practice-of-open-source-distributed-monitoring-cat-system?utm_source=infoq&utm_medium=videos_homepage&utm_campaign=videos_row3) | 开源 | - | 侵入性强 | 策略灵活 | 时效性较好,rpc框架采用tcp传输数据| 决策好 | 报表丰富,满足各种需求 | 消耗较低 , 国内很多大厂都在使用 | - 21 | [Appdash](https://github.com/sourcegraph/appdash) | sourcegraph | 开源 | 完全支持 | 侵入性较弱 | 采样率支持(粒度:不能根据流量采样,只能依赖于请求数量);没有trace开关 | 时效性高 | 决策支持低 | 可视化太弱,无报表分析 | 消耗方面。不支持大规模部署, 因为appdash主要依赖于memory,虽然可以持久化到磁盘,以及内存存储支持hash存储、带有效期的map存储、以及不加限制的内存存储,前者存储量过小、后者单机内存存储无法满足 | 延展性差 22 | [MTrace](https://tech.meituan.com/mt-mtrace.html) | 美团 | 不开源 | - | - | - | - 23 | CallGraph | 京东 | 不开源 | - 24 | [Watchman](http://www.infoq.com/cn/articles/weibo-watchman) | sina微博 | 不开源 | - | 25 | EagleEye | 淘宝 | 不开源 | - 26 | [skywalking](https://github.com/apache/incubator-skywalking) | 华为 吴晟 | 开源 | 完全支持 | 侵入性很低 | 策略灵活 | 时效性较好 | 由于调用链路的更细化, 但是作者在性能和追踪细粒度之间保持了比较好的平衡。决策好 | 丰富的数据报表 | 消耗较低 | 延展性非常好,水平理论上无限扩展 27 | 28 | 综合来说, 29 | ```shell 30 | 1. jaeger对于go开发者来说,可能比较合适一些,但是入手比较困难。它的rpc框架采用thrift协议,现在主流grpc并不支持。后端丰富存储,社区正在积极适配 31 | 2. appdash对于go开发者想搭建一个小型的trace比较合适,不适合大规模使用 32 | 3. zipkin项目,github很活跃,star数量很多,属于java系。很多大厂使用 33 | 4. CAT项目也属于java系, github不活跃,已不太更新了。不过很多大厂使用,平安、大众点评、携程... 34 | 5. skywalking项目, 也属于java系。目前已成为Apache下的项目了,github活跃,作者也非常活跃,当当、华为正在使用。 35 | 6. appdash项目,go语言开发,因为可视化过于简单、且完全内存存储,不太适合大规模项目使用。 36 | 37 | 所以选择的话,可以从skywalking、cat、jaeger和zipkin中选择。 38 | ``` 39 | 40 | ## 参考资料 41 | [当当11·11:高可用移动入口与搜索新架构实践](http://www.uml.org.cn/zjjs/201711161.asp) 42 | 43 | [分布式调用跟踪系统调研笔记](http://ginobefunny.com/post/learning_distributed_systems_tracing/) 44 | 45 | [京东分布式服务跟踪系统-CallGraph](http://kuaibao.qq.com/s/20180228B0W70I00?refer=spider) 46 | 47 | [全链路监控(一):方案概述与比较](https://juejin.im/post/5a7a9e0af265da4e914b46f1) 48 | 49 | [各大厂分布式链路跟踪系统架构对比](https://cloud.tencent.com/developer/article/1137651) 50 | 51 | [skywalking](http://skywalking.io/) 52 | -------------------------------------------------------------------------------- /opentracing标准/概念与目标.md: -------------------------------------------------------------------------------- 1 | ## OpenTracing概念和目标 2 | 随着数据爆炸式增长,体应用已完全不能满足业务需要,分布式服务,特别是微服务越来越成为趋势。达到服务高可用的N个9,以及服务出现问题后定位问题,也变得越来越困难。APM(Application Performance Management)中有三大块:trace、logging和metrics,这三个支撑后续运维提供业务支撑的稳定性、运营人员问题跟踪、以及反馈给运营产品的建议。这三大块有交集。 分布式跟踪系统主要是讲Trace部分。 3 | 4 | 在OpenTracing没有出现之前,主要依据Google的Dapper论文和一些厂商自己的想法,设计的trace系统。没有太多规范,这样导致的一个大问题是,业务方采用一个厂商的trace系统,则如果因为一些原因,想换另一个trace系统投入生产,则如果trace在设计方面的侵入性、或者rpc组件、存储访问组件植入,则改造成本无法想象。如果有一个标准可以规范各大厂商在rpc组件,存储组件等封装中,注入trace系统是标准化的,则只需要改造与业务无关的系统部分,例如trace的collect、storage和dashboard部分,就可以实现。因此,大名鼎鼎的CNCF机构设计了分布式跟踪系统标准——OpenTracing 5 | 6 | **OpenTracing通过提供与平台无关、厂商无关的API,使得开发人员能够方便的添加(或者更换)分布式追踪系统的实现**, 同时OpenTracing提供了用于运营支撑系统的和针对特定平台的辅助程序库,可在[OpenTracing](https://github.com/opentracing)找到。 7 | 8 | ## Trace系统本质理解 9 | 1. 一个web请求打到端口,自带traceid或者gateway生成traceid, 然后在响应到外部的整个链路都带有这个traceid,那么根据这个traceid,我们就能搜索到整个链路。 10 | 2. 进一步地,想知道traceid经过的服务列表、存储、以及所花费的时长等,调用链路的服务拓扑图。这就需要spanid和rpc请求上下文携带信息等 11 | 3. 最终Trace系统要反馈给开发业务人员、产品运营人员并作出有价值的决策,这就需要借助dashboard丰富的报表统计数据。 12 | 13 | **一个traceid所经过的链路,一定是有向无环图** 14 | 15 | 下面的两幅图: 16 | 17 | 1. 一个web请求所经过的所有rpc服务,表示该链路的拓扑图,侧重微服务之间的拓扑关系。 18 | ![调用链路](https://gewuwei.oss-cn-shanghai.aliyuncs.com/tracelearning/OTOV_2.png) 19 | 2. 已发现问题和时间序列的角度,看各个微服务上下文执行时间的花费。 20 | ![时间维度的链路](https://gewuwei.oss-cn-shanghai.aliyuncs.com/tracelearning/OTOV_3.png) 21 | 22 | ## OpenTracing设计理念 23 | OpenTracing最重要的作用是,当你的系统按照标准规范设计后,增加一个分布式跟踪系统变得非常简单。通常不同分布式跟踪系统产品如果遵循了OpenTracing标准规范,则**更换trace产品时,只需要初始化服务时,指定存储服务、Dashboard服务和初始化全局Trace实例,就ok了**。其他的使用OpenTracing标准interface中的方法就行了,因为Trace产品已经实现了这些interface。 24 | 25 | 例如:初期时,使用AppDash作为分布式跟踪系统的DEMO 26 | ```shell 27 | func main(){ 28 | store := appdash.NewMemoryStore() 29 | // 初始化collector客户端,以及对接collector的后端存储服务 30 | // notice:这里直接在服务本身内部初始化collector,是不合理的。因为最好存储服务和collector服务是独立的两个分布式服务。因为appdash就是小业务量的服务,所以都是在本地搜集和存储 31 | cs:=appdash.NewServer(l, appdash.NewLocalCollector(store)) 32 | // dashboard UI, ui 也应该是一个独立的查询服务 33 | tapp, err:= traceapp.New(nil, appdashURL) 34 | // 初始化全局tracer, 同时指定collector服务的rpc端口服务。 35 | // 也即指定全局tracer和agent, 只是appdash比较简单 36 | tracer:= appdashot.NewTracer(appdash.NewRomoteCollector(collectorPort)) 37 | opentraceing.InitGlobalTracer(tracer) 38 | // 初始化服务本身 39 | // ... 40 | } 41 | ``` 42 | 当业务量爆发时, 觉得Appdash无法适应业务需要,需要替换其他的Trace产品方案,如:Zipkin。在原来Appdash的基础上,替换掉func main中的collector服务、存储服务以及初始化全局Tracer,其他不变, DEMO如下: 43 | ```shell 44 | func main(){ 45 | // 初始化collector服务的client端 46 | collector, err:= zipkin.NewKafkaCollector("ZIPKIN_ADDR") 47 | // 初始化全局tracer 48 | tracer, err := zipkin.NewTracer( 49 | zipkin.NewRecorder(collector, false, ":8000", "example"), 50 | ) 51 | opentracing.InitGlobalTracer(tracer) 52 | } 53 | ``` 54 | 55 | 从这两个例子,我们可以看到若要在分布式业务各个微服务本身,替换掉Trace系统,则只需要告诉业务方的全局初始化Tracer方法,以及对应的Collector服务连接方式,其他的都不关心,因为其他的,比如:Span的创建,微服务之间的Trace关联(通过上下文)等都是通过OpenTracing的标准库做到的,类似于多态。 56 | 57 | ## 参考资料 58 | 59 | [opentracing文档中文版 ( 翻译 ) 吴晟](https://wu-sheng.gitbooks.io/opentracing-io/content/pages/quick-start.html) 60 | -------------------------------------------------------------------------------- /jaeger/TChannel/affinity.md: -------------------------------------------------------------------------------- 1 | [Hyperbahn](https://github.com/uber-archive/hyperbahn) 是 Uber 开源的一套服务发现和路由系统,专门用于包含大量微服务的大规模系统,可以使服务间的发现和沟通非常简单和可靠。 目前该库已经不活跃了,不再更新。与etcd、zookeeper和consul等产品具有相同的作用。 2 | 3 | # Affinity(亲和性策略) 4 | ```shell 5 | |----------------------------------------------------------------------| 6 | | ____________ ____________ -------> worker..1 | 7 | | | | | | | | 8 | | | Hyperbahn |---------->| relay ring|-------->|-------> worker... | 9 | | | server | | | | | 10 | | |___________| |___________| |-------> worker..N | 11 | | | 12 | |----------------------------------------------------------------------| 13 | 14 | Hyperbahn服务与后端服务之间的关系 15 | ``` 16 | 17 | 上面画的这幅图,我是在微信开放平台时了解到relay的,微信说是中继服务器,用来刷token和托管公众号的token的,这样web用户访问时,就不需要进行token获取了。托管业务也就直接可以操作相关微信业务了。 18 | 19 | 这个Hyperbahn relay应该也是同理,负责处理Hyperbahn与后端服务之间的连接管理。 20 | 21 | **这样再翻译Affinity这篇文章,就是按照这个意思来了。不然比较难理解。** 22 | 23 | 一个服务对应一组服务实例。 24 | 25 | 对于每个服务,都会有很多实例以防止单个服务负载过高和容错,以及与Hyperbahn relays建立好的连接集——the affine relays(它是relay ring的子集,负责发送和响应指定service的请求数据)。这个最小可行的Hyperbahn全连接到service相关的the affinie relays上。然而随着时间的推移,这些relays可能会超过最大文件描述符的数量ulimit。下面的策略就是为了解决这个问题。 26 | 27 | *minPeersPerWorker*值是每个服务worker连接数量的最小保证。也就是说一个服务worker至少有三个连接可以供Hyperbahn relay使用。还有一个*minPeersPerRelay*值,它是Hyperbahn relay至少连接到一个服务的worker数量。 28 | 29 | **那么由此可见一个relay ring至少与一个服务建立的连接数量为:`minPeersPerWorker*minPeersPerRelay`** 30 | 31 | 32 | *relayCount*值是指Hyperbahn relay已经与服务workers建立好的连接数量。是这个服务的近似`k`值 33 | 34 | 对于每个服务通过gossiping advertisement传送给relay ring中的每个worker成员。每个Hyperbahn relay知道所关联服务worker的host:worker, *workerCount*是这个服务的worker数量,并且被每个相关的relay所知道。 35 | 36 | **这里有几个概念,我感觉有点混:relay ring, Hyperbahn relay,affine relay** 37 | 38 | 这个relay ring可以通过排序所有的workers,并对每个worker给一个合适的位置`workerIndex`。 39 | 40 | 在relay ring中的每个位置有一个线性映射位置到每个worker ring。映射`relayIndex`到`workerIndex`的计算公式: 41 | 42 | ```shell 43 | ratio = workerCount / relayCount 44 | 45 | workerIndex = relayIndex *ratio 46 | ``` 47 | 48 | 每个服务的worker数量应该为: 49 | 50 | ```shell 51 | max(minPeersPerRelay, minPeersPerWorker * ratio) 52 | 53 | minPeersPerRelay表示relay至少与服务workers建立的连接数,则至少有minPeersPerRelay个worker,否则这个条件无法满足。 54 | 55 | minPeersPerWorker * ratio = 56 | minPeersPerWorker * (workerCount/relayCount) = 57 | minPeersPerWorker * workerCount / relayCount = 58 | ``` 59 | 60 | 每个relay都应该在worker组内映射对应的位置,然后连接到服务worker上,以确保每个relay和服务worker之间的最小连接数。此范围从worker ring的相应位置开始,但不包括range的末尾: 61 | 62 | ```shell 63 | workerIndex = round( 64 | relayIndex * ratio + 65 | max( 66 | minPeersPerRelay, 67 | minPeersPerWorker * ratio, 68 | ) 69 | ) 70 | ``` 71 | 72 | 这种方法的一个期望特性是,这个affine relay集或者worker集的小变化应该在很大程度上,与先前的worker集重叠,保留了许多现有的连接。但是小的变化会影响每个relay的边界,并可能导致整个系统的调整。 73 | 74 | 请参与affinity.py以获取验证一系列方案的对等选择算法的静态属性模拟 75 | 76 | ## 疑问 77 | 78 | 应为这个Affinity没有给出相应的框架图,同时对relay ring, Hyperbahn relay,affine relay几个概念不清楚,所以感觉这个图和翻译有误。后面再改正 79 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | 2 | # 分布式跟踪系统目录: 3 | 4 | * [dapper](dapper/README.md) 5 | - [Dapper论文](dapper/dapper.md) 6 | * [opentracing标准](opentracing标准/README.md) 7 | - [概念和目标](opentracing标准/概念与目标.md) 8 | - [名词与术语](opentracing标准/名词与术语.md) 9 | - [APIs](opentracing标准/OpenTracing APIs.md) 10 | - [设计分布式跟踪系统的有效方法](opentracing标准/设计分布式追踪系统的有效方法.md) 11 | * [trace产品对比](分布式跟踪系统——产品对比.md) 12 | * [opentracing-go](opentracing-go/README.md) 13 | - [Tracer实例](opentracing-go/opentracer-go源码阅读一.md) 14 | - [上下文信息携带](opentracing-go/Propagation.md) 15 | - [日志](opentracing-go/Log.md) 16 | * [basictracer-go](basictracer-go/README.md) 17 | - [Tracer实例](basictracer-go/tracer.md) 18 | - [Span对象](basictracer-go/span.md) 19 | - [事件与上下文信息携带](basictracer-go/event-propagation.md) 20 | - [span存储与pb协议](basictracer-go/spanrecorder.md) 21 | - [DEMO](basictracer-go/example.md) 22 | * [appdash](appdash/README.md) 23 | - [Tracer与Span](appdash/tracer-and-span.md) 24 | - [annotations与事件](appdash/annotations-and-event.md) 25 | - [存储](appdash/store.md) 26 | - [近期存储与限制存储](appdash/recentstore-and-limitstore.md) 27 | - [反射](appdash/reflect.md) 28 | - [SpanRecorder与Collector](appdash/recorder-and-collector.md) 29 | - [OpenTracing部分支持](appdash/opentracing.md) 30 | * [jaeger](jaeger/README.md) 31 | - [特性](jaeger/Features.md) 32 | - [jaeger的演变](jaeger/jaeger的技术演进之路.md) 33 | - [TChannel协议](jaeger/TChannel/README.md) 34 | * [协议标准](jaeger/TChannel/协议标准.md) 35 | * [Affinity](jaeger/TChannel/affinity.md) 36 | * [熔断器](jaeger/TChannel/熔断器.md) 37 | * [http与json](jaeger/TChannel/http-and-json.md) 38 | * [metrics](jaeger/TChannel/metrics.md) 39 | * [流控](jaeger/TChannel/流控.md) 40 | * [Thrift](jaeger/TChannel/thrift.md) 41 | * [keyvalue例子](jaeger/TChannel/keyvalue例子.md) 42 | * [tcp监听关闭处理方式](jaeger/TChannel/go1.5版本的一种tcp监听关闭处理方式.md) 43 | - [tchannel-go](jaeger/tchannel-go/README.md) 44 | * [channel](jaeger/tchannel-go/channel.md) 45 | * [PeerList](jaeger/tchannel-go/peer01.md) 46 | * [PeerList与Peer](jaeger/tchannel-go/peer02.md) 47 | * [RootPeerList](jaeger/tchannel-go/peer03.md) 48 | * [peerScore与idleSweep](jaeger/tchannel-go/peerScore-and-idleSweep.md) 49 | * [allchannel与subchannel](jaeger/tchannel-go/peerScore-and-idleSweep.md) 50 | * [connection](jaeger/tchannel-go/connection.md) 51 | * [inboundcall与outboundcall](jaeger/tchannel-go/OutboundCall-and-InboundCall.md) 52 | * [message exchange](jaeger/tchannel-go/message-exchange.md) 53 | * [arguments](jaeger/tchannel-go/arguments.md) 54 | * [message](jaeger/tchannel-go/message.md) 55 | * [payload-WriteBuffer&ReadBuffer](jaeger/tchannel-go/payload-WriteBuffer-ReadBuffer.md) 56 | * [payload-Reader](jaeger/tchannel-go/payload-Reader.md) 57 | * [frame](jaeger/tchannel-go/frame.md) 58 | * [frame pool与call options](jaeger/tchannel-go/framepool-and-calloptions.md) 59 | * [context](jaeger/tchannel-go/context.md) 60 | * [json](jaeger/tchannel-go/json.md) 61 | -------------------------------------------------------------------------------- /jaeger/TChannel/流控.md: -------------------------------------------------------------------------------- 1 | # 流控Rate Limiting 2 | 3 | 流控限制了Hyperbahn进程的请求转发率。这样做的好处是,不会使得Hyperbahn进程因为一个服务的的压力,而导致所有服务质量受到影响。通常,流控被认为是服务之间的防火墙。 4 | 5 | ## 目标 6 | 7 | Hyperbahn作为所有服务的消息总线。实际上,由于各种原因(例如:业务需求猛增,故障重试),一个服务可能会有大量请求。当这些事件出现时,这些请求可能会使Hyperbahn节点饱和并导致提供服务的质量迅速下降。 8 | 9 | 流控的两个目标: 10 | 11 | 1. 控制一个服务可以在Hyperbahn节点上拥有的RPS(每秒请求数)。 12 | 2. 控制一个Hyperbahn节点能够处理的总RPS(每秒请求数)。 13 | 14 | Non-goals: 15 | 16 | 1. 流控不能限制一个服务请求的健康增长,RPS要设置合理值 17 | 2. 流控不是熔断器,它同等对待所有的请求; 18 | 3. 流控不是SLA(Service Level Agreement)。它仅仅确保控制请求的上限。 19 | 20 | 21 | ## 设计和实现 22 | 23 | ### 速率 24 | 25 | 我们使用RPS去量化请求率。在任何时候,RPS都是过去一秒内的请求总数量。有很多方式可以统计RPS。我们使用滑动窗口去维护RPS计数。下面图显示,如果我们把每秒划分为5个buckets,每个bucket包含200ms的请求数量。随着时间的推移,这个窗口向前移动,这样最老的bucket会被丢弃,新的bucket会进入。这这个例子中,当时间向前移动200ms时,这个RPS由19变为16。很容易推断出使用的bucket越多,这个统计就会越精确,也就是占用的时间区间越小。实现上,一个循环数组应用可以来模拟无线的时间跨度。 26 | 27 | ![滑动窗口](https://gewuwei.oss-cn-shanghai.aliyuncs.com/tracelearning/rate_limiting_sliding_window1.png) 28 | 29 | ![滑动窗口](https://gewuwei.oss-cn-shanghai.aliyuncs.com/tracelearning/rate_limiting_sliding_window2.png) 30 | 31 | 32 | ### 限制速率 33 | 34 | 所有请求接收到一个响应为busy帧,code类型0x03的消息。 35 | 36 | ### 每个节点的最大RPS 37 | 38 | 每个节点的最大RPS是Hyperbahn节点能够处理的最大请求数。当超过这个限制后,`Busy(0x03)`错误帧会返回知道这个RPS降到上限以内。 39 | 40 | ### 在出口EGRESS节点每个服务的最大RPS 41 | 42 | 在出口节点每个服务的最大RPS是Hyperbahn出口节点能够处理的最大请求数。当超过这个限制后,处理同上。 43 | 44 | ### 在入口EGRESS节点上每个服务的最大RPS 45 | 46 | 在入口节点上每个服务的最大RPS是Hyperbahn入口节点能够处理的最大请求数。当超过这个限制后,处理同上。 47 | 48 | ### 服务警告 49 | 50 | 执行这个RPS限制时,alerts应该也可以提供帮助转移超过限制的新请求。 51 | 52 | 1. Warnings:当前RPS达到了给定的上限(例如:85%), 警告会发送出去。这个警告阈值也可以考虑RPS的增长速度; 53 | 2. Errors:当前RPS超过了这个上限,errors应该被发送 54 | 55 | 这样做,可以让业务方增加容量或者采取其他措施预防流量等 56 | 57 | ### 测试计划 58 | 59 | 1. 单元测试 60 | 2. 在Hyperbahn集群内做集成测试 61 | 62 | 63 | # Raw 64 | 65 | ## as=raw for TChannel 66 | 67 | 这篇文章阐述了raw编码 68 | 69 | `as=raw`编码意味着你可以使用自定义编码方式,不是TChannel的部分,它是应用程序业务逻辑的编码方式; 70 | 71 | 在使用`as=raw`之前,请先考虑使用`thrift`,`sthrift`,`json`或者`http`编码方式 72 | 73 | ### Arguments 74 | 75 | 1. `arg1`: raw比特流 76 | 2. `arg2`: raw比特流 77 | 3. `arg3`: raw比特流 78 | 79 | 80 | # Streaming Thrift 81 | 82 | Streaming thrift允许以流的request/Response通过TChannel。 83 | 84 | Streaming Thrift将会使用`as=sthrift`,依赖[标准Trift arg scheme](https://github.com/uber/tchannel/blob/master/docs/thrift.md), 唯一的例外是在"call req"和"call res"中编码arg3. 85 | 86 | Streaming thrift方法有以下限制: 87 | 88 | 1. 如果这个请求是流式的,这个IDL应该仅仅指定一个类型为struct的参数; 89 | 2. 如果这个响应是流式的,这个方法的返回参数类型也应该是struct类型; 90 | 3. 在任何流结果发送之前,Thrift异常都必须被返回; 91 | 92 | 93 | 流式传输arg3的数据编码是一个4字节长度前缀的块。格式如下: 94 | 95 | `chunk~4 chunk~4 chunk~4` 96 | 97 | ## Arguments 98 | 99 | ```shell 100 | When streaming request arguments, each arg3 chunk is the encoded payload for the method's first (and only) argument. For example, if a method defines a single argument that is a string, then each chunk is just a Thrift encoded string. 101 | ``` 102 | 103 | 当流式请求参数时,每个arg3 chunk是访问方法的第一个参数的编码;例如:如果一个方法定义了一个参数,类型为string,则每个chunk仅仅是一个Thrift编码的strig。 104 | 105 | 上面这段英文不太理解,::TODO 106 | 107 | ## Responses 108 | 109 | 当流成功响应时,方法的返回结果编码到每个arg3 chunk中。例如,如果一个方法返回一个string类型,则每个chunk应该是一个Thrift编码字符串。 110 | -------------------------------------------------------------------------------- /jaeger/tchannel-go/arguments.md: -------------------------------------------------------------------------------- 1 | # argument 2 | 3 | ```shell 4 | // ArgReader为OutboundCallResponse和InboundCall读取arg2和arg3数据流 5 | type ArgReader io.ReaderCloser 6 | 7 | // ArgWriter是OutboundCall和InboundCallResponse arg2和arg3的数据流 8 | type ArgWriter interface { 9 | io.WriteCloser 10 | 11 | Flush() error 12 | } 13 | 14 | // ArgWritable为OuboundCall和InboundCallResponse提供arg2和arg3的写入 15 | // 16 | // 例如:reqResWriter实现了ArgWritable 17 | type ArgWritable interface { 18 | Arg2Writer() (ArgWriter, error) 19 | 20 | Arg3Writer() (ArgWriter, error) 21 | } 22 | 23 | // ArgReadable为InboundCall和OutboundCallResponse提供了读取arg2和arg3的interface 24 | // 25 | // 例如:reqResReader实现了ArgReadable 26 | type ArgReadable interface { 27 | Arg2Reader() (ArgReader, error) 28 | Arg3Reader() (ArgReader, error) 29 | } 30 | 31 | // ArgReadHelper提供了一个读取arguments的简单接口 32 | type ArgReadHeaper struct { 33 | reader ArgReader 34 | err error 35 | } 36 | 37 | // 这样ArgReader既实现了读取arg2和arg3的接口,又实现了读取错误时的返回 38 | func NewArgReader(reader ArgReader, err error) ArgReadHelper { 39 | return ArgReadHelper{reader, err} 40 | } 41 | 42 | // 这个read方法很有意思 43 | // 44 | // f函数值为闭包结构,它里面通过读取r.reader数据,并存放到闭包传入的bs *[]byte, 最后再校验r.reader是否已经读取完,否则返回错误。 所以read方法读取arg到外界传入的引用值中,并校验比特流是否读取完成。 45 | // 46 | // 它最大的有点是屏蔽了如何读取数据,并返回给外部的变量值结构也不关心。这就是闭包的最大好处 47 | func (r ArgReaderHelper) read(f func() error) error { 48 | if r.err != nil { 49 | return r.err 50 | } 51 | 52 | if err := f(); err != nil { 53 | return err 54 | } 55 | if err := argreader.EnsureEmpty(r.reader, "read arg"); err != nil { 56 | return err 57 | } 58 | 59 | return r.reader.Close() 60 | } 61 | 62 | // 封装读取数据流并校验, 通过闭包函数屏蔽读取比特流和返回值类型的细节 63 | func (r ArgReaderHelper) Read(bs *[]byte) error { 64 | return r.read(func() error{ 65 | var err error 66 | *bs, err = ioutil.ReadAll(r.reader) 67 | return err 68 | }) 69 | } 70 | 71 | // 封装读取数据流并解析成json数据 72 | func (r ArgReadHelper) ReadJSON(data interface{}) error { 73 | return r.read(func() error { 74 | reader := bufio.NewReader(r.reader) 75 | // Reader.Peek(n int) 方法是获取前n个字符的引用,pos不会移动。也就是尝试获取一个数据 76 | // 校验数据流是否已经结束 77 | if _, err := reader.Peek(1); err == io.EOF { 78 | return nil 79 | } else if err !=nil { 80 | return err 81 | } 82 | 83 | // 数据流解析成json数据流 84 | d := json.NewDecoder(reader) 85 | return d.Decode(data) 86 | }) 87 | } 88 | 89 | // 与ArgReaderHelper类似,写入argument 90 | // 这里有个设计上的不对称问题,在ArgReaderHelper属性元素有个ArgReader类型,但是这里直接写的io.WriterCloser类型 91 | type ArgWriteHelper struct { 92 | writer io.WriteCloser 93 | err error 94 | } 95 | 96 | // 获取一个ArgWriter实例 97 | func NewArgWriter(writer io.WriteCloser, err error) ArgWriteHelper { 98 | return ArgWriteHelper{writer, err} 99 | } 100 | 101 | // 和上面的read方法相同,也是通过闭包函数值抽象写入外部变量值和类型。 102 | // 103 | // 闭包函数值执行完毕后,关闭流 104 | func (w ArgWriteHelper) write(f func() error) error { 105 | if w.err != nil { 106 | return w.err 107 | } 108 | 109 | if err := f(); err != nil { 110 | return err 111 | } 112 | 113 | return w.writer.Close() 114 | } 115 | 116 | // 直接比特流写入w 117 | func (w ArgWriteHelper) Write(bs []byte) error { 118 | return w.write(func() error { 119 | _, err := w.write.Write(bs) 120 | return err 121 | }) 122 | } 123 | 124 | // data通过json写入到w.writer中 125 | func (w ArgWriteHelper) WriteJSON(data interface{}) error { 126 | return w.write(func() error { 127 | e := json.NewEncoder(w.writer) 128 | return e.Encode(data) 129 | }) 130 | } 131 | ``` 132 | 133 | # 总结 134 | 135 | 通过Argument相关interface,把argument读取或者写入到指定的流中,它的承载体在Inbound和Outbound中。另外一个大家可以看到的闭包函数值抽象具体实现,并赋值给外部,这个比较有意思。 136 | -------------------------------------------------------------------------------- /opentracing-go/Propagation.md: -------------------------------------------------------------------------------- 1 | # Propagation 2 | 3 | 1. 跨进程的trace信息传输通过SpanContext传递baggage信息,把baggage存储到context上下文中,则需要key:value, 这个key决定了value值和value类型; 4 | 2. key:value存储到context中,需要借助于读写操作;`notice: 这个借鉴了io.Reader和io.Writer等思想,通过组合模式使得实现变得更加灵活` 5 | 3. 目前支持三种key值,对应三种value值类型,分别是:byte流、TextMap和http.Header;其中后两者都是map结构;只是RPC或者Web时,用http.Header,其他使用TextMap或者byte流; 6 | 7 | ## Baggage读写 8 | 9 | 目前支持三种类型的value,BuiltinFormat={Binary=0, TextMap=1, HTTPHeaders=2}, 对于第一种比特流方式,直接通过`io.Reader`和`io.Writer`方式即可; 10 | 11 | 对于后两者的读写操作: 12 | 13 | ```shell 14 | type TextMapWriter interface { 15 | Set(key, val string) 16 | } 17 | 18 | type TextMapReader interface { 19 | ForeachKey(handler func(key, value string) error) error 20 | } 21 | ``` 22 | 23 | 从TextMapReader来看,ForeachKey的传参类型是一个函数,使得读取交给了具体的trace系统实现,灵活度高; 24 | 25 | Baggage的TextMap和Http.Header两种存储,分别实现了上面两个接口: 26 | 27 | ```shell 28 | // TextMap 29 | type TextMapCarrier map[string]string 30 | 31 | func (c TextMapCarrier) ForeachKey(handler func(key, val string) error) error{ 32 | for k, v := range c{ 33 | if err := handler(k, v); err!=nil{ 34 | return err 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func (c TextMapCarrier) Set(key, val string){ 41 | c[key] = val 42 | } 43 | 44 | 45 | // Http.Header 46 | type HTTPHeadersCarrier http.Header 47 | 48 | func (c HTTPHeadersCarrier) Set(key, val string){ 49 | h:= http.Header(c) 50 | h.Add(key, val) 51 | } 52 | 53 | func (c HTTPHeadersCarrier) ForeachKey(handler func(key, value string) error) error{ 54 | for k, vals:= range c{ 55 | for _, v := range vals{ 56 | if err:= handler(k, v); err !=nil{ 57 | return errr 58 | } 59 | } 60 | } 61 | return nil 62 | } 63 | ``` 64 | 65 | # ext/tags 66 | 67 | 根据OpenTracing标准,[《OpenTracing APIs》](https://gocn.vip/article/858)中的数据约定模块,已经在标准中集成了一些常用的Span Tag。在opentracing-go库中,也就集成了这一部分子集,如: 68 | 69 | ```shell 70 | SpanKind:"span.kind" 代表有关RPC、WEB、消息发布与订阅等关系;这类Span Tag值={"Client", "Server", "Producer", "Consumer"} 71 | 72 | 第三方库或者组件 Component: "component"; 这类span tag值表示调用的第三方库或者组件名称; 73 | 74 | 采样率:SamplingPriority: "sampling.priority"; 这类span tag值大于或者等于0,当value>0时,尽量保留这条trace;否则,丢弃这条trace; 75 | 76 | peer:这类span tag表示对等的相关信息,也即调用方上游或者下游的相关服务信息;具体有: 77 | PeerService: "peer.service"; tag值为服务名; 78 | PeerAddress: "peer.address"; tag值为服务连接地址; 79 | PeerHostName:"peer.hostname"; tag值为主机名; 80 | PeerHostIPv4, PeerHostIPv6: "peer.ipv4"和"peer.ipv6"; tag值为IP地址 81 | PeerPort: "peer.port";tag值为服务端口; 82 | 83 | HTTP相关tags列表: 84 | HTTPUrl: "http.url"; tag值为http url; 85 | HTTPMethod:"http.method"; tag值为http.method={"get", "post", "delete", "head", ...}; 86 | HTTPStatusCode: "http.status_code"; tag值为http.status_code={403, 404, 200, 502, ...}; 87 | 88 | DB相关tags列表: 89 | DBInstance: "db.instance", tag值为db实例名称; 90 | DBStatement:"db.statement", tag值为db sql语句; 91 | DBType: "db.type", tag值为db type = {"redis", "mysql", "progresql", ...}; 92 | DBUser: "db.user", tag值为访问db的用户名 93 | // 这里不给出DBPassword是因为,passwd属于非常敏感信息; 94 | 95 | // message bus 消息总线tag: 96 | MessageBusDestination: "message_bus.detination", tag值为address; 97 | 消息总线,我还不太了解; 98 | 99 | // error tag: 表示span执行单元过程是否有业务系统发生错误,true|false 100 | Error: "error", tag值:true|false; 101 | ``` 102 | 103 | **我们注意到,当跟踪某一类tag A时,如果这类tag存在多个指标;这A.a, A.b, ..., A.z等方式是非常友好的,便于识别含义,且不会冲突;** 104 | 105 | 上面的这些tag都会有相应的读写操作:Set, 但是**目前没有发现Tag读取操作**, 大多数tag set操作类似于: 106 | 107 | ```shell 108 | func (tag xxxTagName) Set(span opentracing.Span, value xxtype){ 109 | span.Set(string(tag), value) 110 | } 111 | ``` 112 | 113 | **这里说明一点: 对于已知的TagName和可枚举的value,直接使用StartSpanOptions中的Apply方法即可,如:span.kind; 其他的通过TagName类型自带的Set实现存储,这些tag数据都是存储在StartSpanOptions的tags中** 114 | 115 | # 参考资料 116 | 117 | [opentracing-go](https://github.com/opentracing/opentracing-go) -------------------------------------------------------------------------------- /basictracer-go/example.md: -------------------------------------------------------------------------------- 1 | 在服务启动时,开启两个goroutine,一个为client,终端输入发出web请求;一个为http server,监听端口为8080。这里不用浏览器或者其他做client的原因是,需要client端创建trace,这样才能够在client/server跨进程形成调用链。 client直接回车,则结束进程。 2 | 3 | 在启动两个goroutine之前,初始化了global tracer, 并指定了Collector和Storage于一身的TrivialRecorder. 它的RecordSpan方法只是输出到终端的Span所有相关信息:操作名、创建时间、生命周期时间、日志列表长度、上下文携带信息和日志列表中的每行日志 4 | 5 | 其中:dapprish/random.go没有作用。 6 | 7 | # Server 8 | 9 | 该服务启动了http的8080端口服务,接受所有请求,http服务的网络传输数据Carrier是采用的TextMapPropagation协议 10 | 11 | 它会在每个请求到来时,会根据Client的SpanContext信息创建一个名称为"ServerSpan"的Span。这个DEMO健壮性差,容易panic的原因在于: 12 | 13 | `如果非下面的Client请求,而是其他web请求进来的,则不会带有Carrier的网络传输数据,则在TextMapPropagation的Extract解析时,报ErrSpanContextNotFound错误, 则在server中直接panic掉` 14 | 15 | 获取request body数据流后,通过Span.Logs记录请求数据,并通过span.Finish方法存储RawSpan数据={TraceID、SpanID、Sampled和Baggage} 16 | 17 | # Client 18 | 19 | client通过终端输入发送web http POST请求,它首先创建名称为"getInput"的Span, 并在span的logs中记录ctx、user text、返回response等内容;以及携带除了TraceID、SpanID、Sampled和Baggage{"User": 当前终端登录用户名}, 当span生命周期结束时,通过span.Finish方法存储RawSpan信息到内存中。 20 | 21 | 22 | 输入"hello,world",执行结果如下所示: 23 | 24 | ```shell 25 | Enter text (empty string to exit): hello,world 26 | RecordSpan: serverSpan[2018-07-05 14:19:44.151121569 +0800 CST m=+7.236240362, 57.771µs us] --> 1 logs. context: {5671206282793949525 9024173529197870936 false map[user:chenchao]}; baggage: map[user:chenchao] 27 | log 0 @ 2018-07-05 14:19:44.151178152 +0800 CST m=+7.236296945: [request body:hello,world] 28 | RecordSpan: getInput[2018-07-05 14:19:36.916512195 +0800 CST m=+0.001382988, 7.235279179s us] --> 3 logs. context: {5671206282793949525 978135273461507120 false map[User:chenchao]}; baggage: map[User:chenchao] 29 | log 0 @ 2018-07-05 14:19:36.916526835 +0800 CST m=+0.001397628: [ctx:context.Background.WithValue(opentracing.contextKey{}, &basictracer.spanImpl{tracer:(*basictracer.tracerImpl)(0xc4201340c0), event:(func(basictracer.SpanEvent))(nil), Mutex:sync.Mutex{state:1, sema:0x0}, raw:basictracer.RawSpan{Context:basictracer.SpanContext{TraceID:0x4eb42c111de59955, SpanID:0xd9308d94cf3e430, Sampled:false, Baggage:map[string]string{"User":"chenchao"}}, ParentSpanID:0x0, Operation:"getInput", Start:time.Time{wall:0xbec78bfe36a0ddc3, ext:1382988, loc:(*time.Location)(0x14960c0)}, Duration:7235279179, Tags:opentracing.Tags(nil), Logs:[]opentracing.LogRecord{opentracing.LogRecord{Timestamp:time.Time{wall:0xbec78bfe36a116f3, ext:1397628, loc:(*time.Location)(0x14960c0)}, Fields:[]log.Field{log.Field{key:"ctx", fieldType:10, numericVal:0, stringVal:"", interfaceVal:(*context.valueCtx)(0xc4200a6f00)}}}, opentracing.LogRecord{Timestamp:time.Time{wall:0xbec78c0008cf8373, ext:7232936124, loc:(*time.Location)(0x14960c0)}, Fields:[]log.Field{log.Field{key:"user text", fieldType:0, numericVal:0, stringVal:"hello,world", interfaceVal:interface {}(nil)}}}, opentracing.LogRecord{Timestamp:time.Time{wall:0xbec78c0009085b42, ext:7236661387, loc:(*time.Location)(0x14960c0)}, Fields:[]log.Field{log.Field{key:"response", fieldType:10, numericVal:0, stringVal:"", interfaceVal:(*http.Response)(0xc42018a090)}}}}}, numDroppedLogs:0})] 30 | log 1 @ 2018-07-05 14:19:44.147817331 +0800 CST m=+7.232936124: [user text:hello,world] 31 | log 2 @ 2018-07-05 14:19:44.151542594 +0800 CST m=+7.236661387: [response:&{200 OK 200 HTTP/1.1 1 32 | 1 map[Date:[Thu, 05 Jul 2018 06:19:44 GMT] Content-Length:[0]] {} 0 [] false false map[] 0xc42014c000 33 | }] 34 | ``` 35 | 36 | # 总结 37 | 38 | opentracing-go库是对OpenTracing标准的代码范式表达,而basictracer-go是对前者的最小子集扩展和实现,理论上这个basictracer-go已经可以进行业务开发和使用了,但是这个库还非常弱,主要表现在:collector和storage基于一身,且还是基于内存的,没有dashboard UI支持,无法用于生产。虽然无法用于生产,但是Span的实现,Carrier的支持已经做得很好了,同时这个库希望厂商采用OpenTracing标准实现时,可以把它作为中间层或者组件使用,因为它对厂商开放了一些接口自定义实现,例如:Carrier的AccessPropagation(IPC/RPC数据转换和存储),SpanRecord接口(RawSpan信息存储)。但是厂商也可以完全自己基于opentracing-go设计一套完整的分布式跟踪系统,不用再在opentracing-go和厂商中间加一层basictracer-go。 39 | -------------------------------------------------------------------------------- /opentracing标准/名词与术语.md: -------------------------------------------------------------------------------- 1 | ## OpenTracing——概念与术语 2 | OpenTracing中的概念与术语, 基本上都是从Dapper论文中提取的。包括Trace、Span、Inter-Span Reference、Annotation等术语 3 | 4 | **Spans**: 表示定义的一个执行单元的粗细粒度,包括执行单元的开始时间和执行时长。通过把一条链路的所有Spans按照一定的规则排列,形成时间序列图。 5 | 6 | **Operation Name**: 每个span都需要一个操作名,要求:简单、可读性高。即看到这个操作名,大概就知道这个span所在的执行单元做了什么事情。例如:可以采用rpc方法名、函数名、自定义执行单元的命名。当一个执行单元无法用简单的语言表达时,那么具体描述可以使用**Tags**. 7 | 8 | **Inter-Span Reference**: 因为span代表执行单元,那么执行单元之间存在一定的是否依赖关系。如:嵌入的执行单元,两个独立的执行单元等等。 9 | 10 | **[Inter-Span Reference](https://github.com/opentracing/specification/blob/master/specification.md)**中的ChildOf和FollowsFrom两个概念,我觉得还是有些模糊不清。我理解的含义如下: 11 | 12 | ```shell 13 | 1. ChildOf: 表示执行单元的嵌入,也就是说:执行单元之间有比较强的结果依赖; 14 | 2. FollowsFrom: 表示两个执行单元相对独立,不是强依赖。 15 | ``` 16 | 17 | 官方文档: 18 | 19 | ```shell 20 | ChildOf references: A Span may be the ChildOf a parent Span. In a ChildOf reference, the parent Span depends on the child Span in some capacity. All of the following would constitute ChildOf relationships: 21 | 22 | 1. A Span representing the server side of an RPC may be the ChildOf a Span representing the client side of that RPC 23 | 2. A Span representing a SQL insert may be the ChildOf a Span representing an ORM save method 24 | 3. Many Spans doing concurrent (perhaps distributed) work may all individually be the ChildOf a single parent Span that merges the results for all children that return within a deadline 25 | 26 | FollowsFrom references: Some parent Spans do not depend in any way on the result of their child Spans. In these cases, we say merely that the child Span FollowsFrom the parent Span in a causal sense. There are many distinct FollowsFrom reference sub-categories, and in future versions of OpenTracing they may be distinguished more formally. 27 | ``` 28 | 这里面的ChildOf含义:在某些程度上, 父级span依赖于子span。**模糊概念**,同时又举了三个例子: 29 | 30 | 1. rpc所在的服务端是客户端的子级,则**rpc server = ChildOf(rpc client)**; client -> server 31 | 2. sql所在的服务端是客户端的子级,则**sql server = ChildOf(sql client)**; client -> server 32 | 3. 相互独立的各个span,是同一个span的子级。例如:下订单请求,在新增订单方法中,可能会同时又扣库存、新增订单、获取商品信息等,这些事同级的,但是与新增订单方法是一父多子关系。 33 | 34 | 第一点和第二点可以归为一类,微服务之间的调用都是ChildOf关系;在一个执行单元中的所有嵌入执行单元是FollowsFrom关系。可能在spans之间存在的FollowsFrom和ChildOf两个概念本身,没有清晰的边界定义,取决于业务开发者本身,带一些主观。官网中的一些Spans之间的关系时序图,其实没有什么意义。直接理解一点:**span是一个执行单元**,至于这个执行单元的粗细粒度,取决于业务需要。 35 | 36 | **Log**: Log不能在span之间传递,它的生命周期在执行单元中,和分布式日志系统的概念完全不同,它只是做一些事件日志,例如:发生error时的错误日志,非常轻量级。用途:Trace dashboard查询和问题追踪 37 | 38 | **Tags**: Tags不能在span之间传递,在Span操作名无法满足时,使用Tags操作存储简单数据,例如:某个orderid=123的订单,可以使用tags存储orderid: 123的标签,这样我们去定位问题时,直接使用tags搜索,就可以找到追踪到具体时间、调用时长,然后再在分布式日志系统中,根据分布式跟踪系统中得到的时间、服务和订单号,追踪业务细节。 39 | 40 | **Baggage**: 中文:行李。它是Span之间的上下文信息携带者,通过SpanContext存储,例如:存储traceid等信息。notice:不要在把大量的信息存储在SpanContext中,因为当业务量过大时,网络传输量会爆炸式增长,会造成大的业务性能和时间消耗。造成服务严重抖动,非业务因素影响了业务的高可用。**分布式跟踪系统的一个设计目标:消耗低** 41 | 42 | **Inject & Extract**: SpanContext通过Inject和Extract方法,通过指定的key注入到header头部或者通过指定的key从header取出SpanContext 43 | 44 | ## OpenTracing interface 45 | ### Span interface必须实现以下功能: 46 | 47 | 1. Get the span's SpanContext; 48 | 2. Finish; span执行单元结束 49 | 3. Set Tag{Key: value};key:string,value:string|布尔值|数字类型 50 | 4. Add a new event log;增加一个log事件。 51 | 5. Set a Baggage item。 52 | 6. Get a Baggage item。 53 | 54 | ### Tracer interface必须实现以下功能: 55 | 56 | 1. Start a new span。notice:可以从上下文中获取SpanContext,然后利用span之间的关系,创建span。 57 | 2. Inject a SpanContext into Carrier。 58 | 3. Extract a SpanContext from Carrier。 59 | 60 | ### Global & No-op Tracer 61 | 每一个平台的OpenTracing API库(opentracing-go, opentracing-java等),都必须实现一个空的Tracer,No-op Tracer的实现必须不会出错,并且不会有任何副作用。这样在业务方没有指定collector服务、storage、和初始化全局tracer时,但是rpc组件,orm组件或者其他组件加入了探针。这样全局默认是No-op Tracer实例,则对业务不会有任何影响。 62 | 63 | ## 参考资料 64 | 65 | [opentracing文档中文版 ( 翻译 ) 吴晟](https://wu-sheng.gitbooks.io/opentracing-io/content/) 66 | 67 | [opentracing.io](https://github.com/opentracing/specification/blob/master/specification.md) 68 | -------------------------------------------------------------------------------- /jaeger/TChannel/http-and-json.md: -------------------------------------------------------------------------------- 1 | # TChannel与HTTP的错误码映射 2 | 3 | **稳定性:不稳定** 4 | 5 | 当发送请求和响应请求时TChannel能够返回不同类型的错误 6 | 7 | ## TChannel Client Errors 8 | 9 | 作为http状态机集成的一部分,tchannel错误需要映射到http合适的错误码,以方便http客户端能够区分不同类型的错误并作出合适的后续动作。 10 | 11 | tchannel错误码在[Jaeger TChannel ——protocol](https://gocn.vip/article/900)一文中已经陈述过了 12 | 13 | http的状态返回码: [http status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) 14 | 15 | 下面是tchannel与http的状态错误码映射关系表: 16 | 17 | | code | name | http status code | 18 | | --- | --- | --- | 19 | | `0x01` | timeout | 504 Gateway Timeout | 20 | | `0x02` | cancelled | 500 TChannel Cancelled | 21 | | `0x03` | busy | 429 Too Many Requests | 22 | | `0x04` | declined | 503 Service Unavailable | 23 | | `0x05` | upexpected error | 500 Internal Server Error | 24 | | `0x06` | bad request | 400 Bad Request | 25 | | `0x07` | network error | 500 TChannel Network Error | 26 | | `0x08` | unhealthy | 503 | Service Unvailable | 27 | | `0xFF` | fatal protocol error | 500 TChannel Protocol Error | 28 | 29 | 30 | # HTTP over TChannel 31 | 32 | 这篇文章阐述了我们怎么样把HTTP编码进TChannel。 33 | 34 | 对于HTTP请求调用,这个Transport Headers中存在key:`as`, 值必须设置为`http`。请求时消息类型为"call req"和响应时消息类型为"call res",它们带有`arg1`、`arg2`和`arg3`,定义如下: 35 | 36 | ## Arguments 37 | 38 | 1. `arg1` 是一个任意circuit字符串,可以留空; 39 | 2. `arg2` 是一个编码后的request/response元数据,详见下文; 40 | 3. `arg3` 是来自http请求/响应的原始字节流 41 | 42 | ### `arg2`:request meta data 43 | 44 | Binary schema: 45 | 46 | ```shell 47 | method~1 48 | url~N 49 | numHeaders: 2(headerName~2 headerValue~2){numHeaders} 50 | 51 | 例如: 52 | method: GET 53 | url: /v1/accounts/:account_id 54 | numHeaders:{secret: xxxx2j02} 55 | ``` 56 | 57 | 注意: 58 | 59 | 1. 这个url字段长度是变长的。`则只能根据arg2的总长度,间接计算url的变长大小` 60 | 61 | ### `arg2`:response meta data 62 | 63 | Binary schema 64 | 65 | ```shell 66 | statusCode:2 67 | message~N 68 | numHeaders:2(headerName~2 headerValue~2){numHeaders} 69 | 70 | 例如: 71 | statusCode: 200 72 | message: {err_code: 0, err_msg: ""} 73 | numHeaders: {status: "00"} 74 | ``` 75 | 76 | 注意: 77 | 78 | 1. statusCode是HTTP的状态响应码; 79 | 2. 消息是utf-8编码; 80 | 3. 消息长度也是一个变长; 81 | 4. headers可以使用multi-map或者列表键值对实现`这个在Appdash的RawSpan中见过`;单值是不够的 82 | 83 | # JSON over TChannel 84 | 85 | 这篇文档阐述了我们怎么样把JSON编码进TChannel中 86 | 87 | 对于JSON的请求调用,则Transport Headers一定有key: `as`,值为`json`的键值对。对于Request消息类型为"call req",和Response消息类型为"call res", 都会带有`arg1`,`arg2`和`arg3`,这三个参数的定义如下: 88 | 89 | 对于每个"call req"消息,这个服务名应该设置为被调用的TChannel服务 90 | 91 | 对于每个"call res"消息,如果这个响应是成功的,这个响应码必须设置为`0`;如果这个响应是失败的,则这个响应码必须设置为`1`。 92 | 93 | ## Arguments 94 | 95 | 对于"call req"和"call res": 96 | 97 | 1. `arg1`一定是方法名; 98 | 2. `arg2`一定是JSON编码的application headers 99 | 3. `arg3`一定是application response 100 | 101 | 102 | ### arg1 103 | 104 | 这个方法一定是UTF-8编码。建议您使用字母数字字符和`_`。 105 | 106 | ### arg3 107 | 108 | `arg3`必须是JSON序列化编码 109 | 110 | 对于“call req”消息,这只是一个任意的JSON有效载荷 111 | 112 | 对于"call req"消息: 113 | 114 | 1. 在成功的情况下,响应是任意JSON负载数据; 115 | 2. 在失败的情况下,响应时JSON错误信息。errors中有个`message`字段,以及一个`type`字段(告知错误类型) 116 | 117 | # TChannel Service 118 | 119 | 客户端库应该注册一个默认服务,这个服务提供有关客户端库和内部状态的元数据信息。此默认服务应该在"tchannel"下注册,可在不知道托管endpoint应用程序的服务名称情况下使用该服务 120 | 121 | 这个Thrift scheme详见[meta.thrift](https://github.com/uber/tchannel/blob/master/thrift/meta.thrift) 122 | 123 | # 多语言支持 124 | 125 | ## TChannel 126 | 127 | TChannel是一个支持多路复用和帧协议的RPC服务框架 128 | 129 | 这个协议当前已经支持的语言,如下所示 130 | 131 | 1. Go 132 | 133 | 1). [Guide](https://github.com/uber/tchannel/blob/master/docs/go-guide.md) 134 | 135 | 2). [API Documentation](http://godoc.org/github.com/uber/tchannel-go) 136 | 2. Node 137 | 138 | 1). [Guide](http://tchannel-node.readthedocs.org/en/latest/GUIDE/) 139 | 140 | 2). [API Documentation](http://tchannel-node.readthedocs.org/en/latest/) 141 | 3. Python 142 | 143 | 1). [Guide](http://tchannel.readthedocs.org/projects/tchannel-python/en/latest/guide.html) 144 | 145 | 2). [API Documentation](http://tchannel.readthedocs.org/projects/tchannel-python) 146 | 147 | 148 | -------------------------------------------------------------------------------- /jaeger/jaeger的技术演进之路.md: -------------------------------------------------------------------------------- 1 | jaeger uber的分布式跟踪系统,它是CNCF基金会的第15个项目。在16年前,uber内部使用的分布式跟踪系统,是采用的twitter公司的解决方案——`Zipkin`。 后来,uber内部想把Zipkin的拉取改为推送架构,就逐渐的形成了自己的分布式跟踪系统,最终演变为产品:jaeger。 2 | 3 | ## Merckx 4 | 5 | uber早期的追踪系统叫做Merckx,它应该是使用了Zipkin解决方案,Merckx采用了拉取架构,可以从kafka队列中拉取数据流。但是它最大的不足之处表现在两个方面: 6 | 7 | 1. 它的设计主要面向Uber使用整体式API的年代。缺乏分布式上下文传播context的概念,虽然可以记录SQL查询、Redis调用,甚至对其他服务的调用,但是无法进一步深入。(具体不太清楚) 8 | 2. 另一个有趣的局限是数据存储在全局线程的本地存储中,随着Tornado web框架的引入,这种方式变得不可行。 9 | 10 | ## TChannel 11 | 12 | 随后,随着微服务的需要求到来,RPC框架变得越来越重要,2015年初内部开始开发RPC框架——TChannel。其中设计目标之一是将类似于Dapper分布式追踪能力融入到协议中,而OpenTracing标准产生于2016年11月份左右,所以TChannel一开始并不遵循OpenTracing标准。至于后面的发展,后面再看。::TODO 13 | 14 | 虽然TChannel是与Zipkin解决方案完全无关,但是还是借鉴了后者的一些追踪设计。从内部来看,TChannel的Span在格式上与Zipkin几乎完全相同,也使用了Zipkin所定义的注释,例如:"cs"(Client Send)和"cr"(Client Receive)。 15 | 16 | TChannel使用追踪报告程序(Reporter)接口将收集到的进程外追踪Span发送至追踪系统的后端。该技术自带的库默认包含一个使用TChannel本身和Hyperbahn实现的报告程序以及发现和路由层,借此将Thrift格式的Span发送至收集器集群。 17 | 18 | TChannel客户端库接近我们所需要的分布式追踪系统,该客户端库提供了下列模块: 19 | 20 | 1. 追踪上下文传播以及带内请求(IPC/RPC) 21 | 2. 通过编排API记录追踪Span;(也就是,把trace跟踪进行组件封装) 22 | 3. 追踪上下文的进程内传播;(这个一般都很少使用) 23 | 4. 将进程外追踪数据报告至追踪后端所需的格式和机制(也就是,Span相关数据转换为Collector和Stoage能够接收和存储的数据格式,这个工作既可以交给Collector来做,也可以Agent主动做好再推送) 24 | 25 | 该系统唯独缺少了追踪后端本身,即Collector和Storage。追踪上下文的传输格式和报表程序使用的默认Thrift格式在设计上都可以非常简单直接地将TChannel和Zipkin后端集成。然而当时只能通过Scribe将Span发送至Zipkin,而Zipkin只支持Cassandra格式的数据存储。因为当时Uber对这个存储没有什么技术经验,所以,他们自己开发了一套后端原型系统,并结合Zipkin UI的一些自定义组件构建了一个完成的分布式跟踪系统。 26 | 27 | 也即,uber开发的后端原型系统Collector和Storage分别是tcollector(node.js)和Riak存储(Spans),Solr索引库(Indexing), 然后通过Zipkin UI来进行查询。 28 | 29 | 但是随着业务迅猛发展, 后端原型系统架构所使用的Riak/Solr存储系统无法妥善缩放以适应Uber的流量,同时很多查询功能依然无法与Zipkin UI实现足够好的交互操作。同时Uber内部系统语言上的异构,以及还有很多核心业务使用的自己RPC框架,这些异构的技术环境使得分布式追踪系统的构建变得困难。 30 | 31 | ## Jaeger 32 | 33 | 针对Merckx产品和Uber内部技术的异构,使得需要更专职的团队做分布式跟踪系统——Jaeger。Jaeger的目标:**将现有的Merckx原型系统转换为可以全局运用的生产系统,让分布式追踪功能可以适用并适应Uber的微服务** 34 | 35 | 新的团队在Cassandra集群方面已经具备运维经验,该数据库直接为Zipkin后端提供支持,因此团队决定弃用Riak/Solr存储系统。同时,为了接受TChannel流量并将数据兼容Zipkin的二进制格式存储在Cassandra中,使用Golang重新实现了collector。这样对于Zipkin的dashboard就无需改动,完全兼容了。 36 | 37 | 同时一个很大的改进点,他们还为每个收集器构建了一套可动态配置的倍增系统(Multiplication factor),借此将入站流量倍增N次,这主要是为了分布式跟踪系统的压测,看看Jaeger的延展性。 38 | 39 | 这里我们可以看到,Jaeger的早期架构依然依赖于Zipkin UI和Zipkin存储格式。 40 | 41 | 还面临的一个业务需求,uber内部还有很多核心业务没有使用TChannel RPC框架,为了给公司内部提供透明无侵入的trace服务,组件或者公共服务的编排变得非常重要,各种语言的客户端库提供,为了用不同语言提供一致的编排API,所有客户端库从一开始就采用了**OpenTracing API**。 42 | 43 | Jaeger还提供了一个采样策略,防止流量过大,trace对业务造成抖动比较大。策略包括:**1. 全量采样;2. 基于概率的采样;3.限速采样** 44 | 45 | 46 | Jaeger将有关最恰当的采样策略决策交给追踪后端系统Collector服务,服务的开发者不再需要猜测最合适的采样速率。而后端可以根据流量变化动态地调整采样速率。 47 | 48 | ```shell 49 | ps: 其实这里也有另一个问题:如果交给后端collector服务进行采样决策,那么agent肯定是全量trace推送给collector,那么agent所在的业务服务压力也大,同时TChannel网络的压力也很大 50 | 51 | 对于上面我提到的一个问题,Jaeger是这样回答的: 52 | 53 | 后端collector服务可以按照流量模式的变化动态地调整采样速率,并反馈到各个服务的agent,形成反馈环路, 在线路中叫做:Control Flow 54 | 55 | 上面的回答,非常吸引人;因为它既不需要业务方考虑流量的增长趋势来选择合适的采样速率,同样,Jaeger的采样率是基于全局流量控制的,所以它具有动态的调整和反馈。这样的策略让人非常舒适 56 | ``` 57 | 58 | 另一个需要解决的问题是,TChannel框架的服务发现和服务注册,需要依赖Hyperbahn。但是对于希望在自己的服务中运用追踪能力的工程师,这种依赖造成了不必要的摩擦。(ps: 这句话含义不是很明白?是有自己的服务发现和服务注册吗?比如:etcd,zookeeper, consul等) 59 | 60 | 61 | 为了解决上面这个问题,我们事先了一种jaeger-agent边车(Sidecar)进程, 并将其作为基础架构组件,与负责收集度量值的代理一起部署到所有宿主机上,所有与路由与发现有关的依赖项都封装在这个jaeger-agent中。 62 | 63 | 此外uber还重新设计了客户端库,可将追踪Span报告给本地UDP端口,并能轮询本地会换接口上的代理获取采样策略。新的客户端只需要最基本的网络库,架构上的这种变化向着我们先追踪后采样的愿景迈出了一大步,我们可以在代理的内存中对追踪记录进行缓存,**这点类似于Appdash的ChunkedCollector方法,在agent以时间和大小两个维度进行缓存。** 64 | 65 | 66 | 目前的Jaeger架构:后端组件使用Golang实现,客户端库使用了四种支持OpenTracing标准的语言,一个基于React的Web前端,以及一个机遇Apache Spark的后处理和聚合数据管道。 67 | 68 | ## Jaeger UI 69 | 70 | Zipkin UI是Uber在Jaeger中使用的最后一个第三方软件。由于要将Span以Zipkin Thrift格式存储在Cassandra中并与UI兼容,这对后端和数据模型都有了很大的限制,而且数据转换也是个频繁的操作。尤其是Zipkin模型不支持OpenTracing标准;不支持客户端库两个非常重要的功能:1. 键值对日志;2. 更为通用的有向无环图而非span树所代表的跟踪。所以uber下决心彻底革新后端所用的数据模型,并编写新的UI。则表示Collector和Storage的数据存储模型抛弃了Zipkin,使用了OpenTracing标准。其他优化点这里不展开了。 71 | 72 | 73 | ## 参考资料 74 | 75 | [sidecar](https://docs.microsoft.com/zh-cn/azure/architecture/patterns/sidecar) 76 | 77 | [优步分布式追踪技术再度精进](http://www.infoq.com/cn/articles/evolving-distributed-tracing-at-uber-engineering#anch150996) 78 | 79 | -------------------------------------------------------------------------------- /jaeger/TChannel/thrift.md: -------------------------------------------------------------------------------- 1 | # Thrift over TChannel 2 | 3 | 这篇文章阐述了我们意图在TChannel使用Thrift协议 4 | 5 | 对于Thrift数据流通过TChannel,则Transport Headers中存在`as=thrift`。请求带有"call req"消息和响应带有"call res", `arg1, arg2, arg3`值定义在[Arguments](https://github.com/uber/tchannel/blob/master/docs/thrift.md#arguments)。 6 | 7 | 对于`call res`, 8 | 9 | 1. 调用成功,则响应码(`code:1`)一定设置为`0x00`。 10 | 2. 调用失败,则响应吗(`code:1`)一定设置为`0x01`。 11 | 12 | ## Arguments 13 | 14 | 对于 `call req` 和 `call res`, 15 | 16 | 1. `arg1` 必须设置为请求的方法名; 17 | 2. `arg2` 必须是application headers, 格式`nh:2 (k~2 v~2){nh}` 18 | 3. `arg3` 必须是Thrift payload 19 | 20 | ### `arg1` 21 | 22 | 这个一定是服务方法名和Thrift服务名的级联,通过`::`连接。它与指向服务endpoint是一样的。 23 | 24 | 例如:对于下面服务的`ping`方法,`arg1`是`PingService::ping`。 25 | 26 | ```shell 27 | service PingService { 28 | void ping() 29 | } 30 | ``` 31 | 32 | 注意,这个Thrift服务名并不需要与TChannel服务名相同。也就是,在`call req/res`消息协议中的`service~1`可以与定义在Thrift IDL的服务名可以不相同。 33 | 34 | ### `arg3` 35 | 36 | `arg3`必须包含一个使用`TBinaryProtocol`编码的Thrift结构 37 | 38 | 对于`call req`消息,它是一个仅仅包含这个方法参数的struct结构 39 | 40 | 对于 `call res`消息, 41 | 42 | 1. 调用成功时,这个响应包含一个字段身份标志位`0`的结构体,它包含了这个方法的返回值。对于方法返回类型为`void`,这个结构体一定是空的; 43 | 2. 调用失败时,这个响应包含一个异常字段标志的结构体。用以表示调用失败返回异常值; 44 | 45 | 例如: 46 | 47 | ```shell 48 | service CommentService { 49 | list getComments ( 50 | 1: EntityId id 51 | 2: i32 offset 52 | 3: i32 limit 53 | ) throws ( 54 | 1: InvalidParametersException invalidParameters 55 | 2: EntityDoesNotExist doesNotExist 56 | ) 57 | } 58 | ``` 59 | 60 | 对于`getComments(1234, 10, 1000`,对于`call req`的这个`arg3`将包含以下结构体二进制编码的版本: 61 | 62 | ```shell 63 | { 64 | 1: 1234, 65 | 2: 10, 66 | 3: 100, 67 | } 68 | ``` 69 | 70 | 如果这个调用成功,则`call res`的消息体包含以下二进制编码内容: 71 | 72 | ```shell 73 | { 74 | 0: [ 75 | { /* comment fields go here */ }, 76 | { /* comment fields go here */ }, 77 | // ... 78 | ] 79 | } 80 | ``` 81 | 82 | 如果这个调用返回失败,则消息体会带有`EntityDoesNotExist`的异常,这个body包含以下二进制编码的结构体: 83 | 84 | ```shell 85 | { 86 | 2: { /*EntityDoesNotExist fields go here */ } 87 | } 88 | ``` 89 | 90 | ## Multiple Services 91 | 92 | 为了避免疑惑,这些定义在这个部分被使用。 93 | 94 | 1. **Service** 是指在Thrift IDL中定义为`service`, 也就是说一个服务单独在Thrift IDL定义为一个serivce; 95 | 2. **System** 是指定义在Thrift IDL的所有服务。一个系统包含多个不同的服务; 96 | 97 | 对于一个系统的Thrift IDL可以包含多个Thrift服务,这些服务划分这个系统为多子域,也就是微服务划分。例如: 98 | 99 | ```shell 100 | service UserService { 101 | UserId createUser(1: UserDetail details) 102 | void verifyEmailAddress(1: UserId userId, 2: VerificationToken token) 103 | } 104 | 105 | service PostService { 106 | PostId submitPost(1: UserId userId, 2: PostInfo post) 107 | } 108 | ``` 109 | 110 | 有两种方式使用这种多服务系统: 111 | 112 | 1. 同一台服务器上有多个不同的服务; 113 | 2. 在系统中每个服务不同端口或者不同机器上设置单独的server。消费者需要指定这些不同的端口以供clients访问。 114 | 115 | 在这个部分我聚焦第一种方案,由于第二种方案每个服务存在多个不同的系统中是非常不同的。 116 | 117 | 提及`arg1`,每个服务方法都由TChannel服务进行注册,格式`{serviceName}::{methodName}`。上面的例子,我们有三个endpoints,分别是`UserService::createUser`, `UserService::verifyEmailAddress`和`PostService::submitPost`。 118 | 119 | 当发起请求时,调用者必须使用完整的endpoint。例如: 120 | 121 | ```shell 122 | send( 123 | { 124 | service: "UserSerivce", // 这个是TChannel服务名 125 | endpoint: "PostService::submitPost", 126 | // ... 127 | } 128 | ) 129 | ``` 130 | 131 | 为方便起见,如果TChannel服务名匹配到Thrift服务名,则客户端实现允许`{serviceName}::`前缀省略。例如: 132 | 133 | ```shell 134 | send({service: "UserService", endpoint: "createUser"}) 135 | 136 | // 这实现应该翻译成下面这样: 137 | send({Service: "UserService", endpoint: "UserService::createUser"}) 138 | ``` 139 | 140 | ## Service Inheritance 141 | 142 | Thrift支持服务继承概念,例如: 143 | 144 | ```shell 145 | service BaseService { 146 | bool isHealthy() 147 | } 148 | 149 | service UserService extends BaseService { 150 | // ... 151 | } 152 | 153 | service PostService extends BaseService { 154 | // ... 155 | } 156 | ``` 157 | 158 | 在这个服务继承例子中,我们不想"parent"服务方法在子服务中注册。则我们不想`BaseService::isHealthy`分别在`UserService`和`PostService`中注册。使用extends方式,则继承了公共服务 159 | 160 | ## Uncaught Exceptions 161 | 162 | 当没有捕捉到服务异常时,他们没有在Thrift IDL中定义,服务实例应该尝试响应一个TChannel `error`消息错误,错误码`code:1`为`0x05`(unexpected error) 163 | -------------------------------------------------------------------------------- /appdash/recentstore-and-limitstore.md: -------------------------------------------------------------------------------- 1 | Appdash除了提供PersistentStore可持久化文件存储外,还提供了基于内存的限制存储两种,RecentStore和LimitStore, 这两种的Span Recorder都有生命周期,到了一定时间或者大小,Appdash认为这些Recorder不会再使用,删除它们。 2 | 3 | 当然基于内存的Span Recorder存储肯定有很多弊端,比如:报表统计做不到全局,只能看近期;针对第一种RecentStore存储,它是以时间为维度,进行Span Recorder的过期删除工作。缺点:1. 延展性差;2. 报表具有局部性等;延展性差是指,当业务量小时,Span Recorder本身就不多,则生命周期内的Recorder就少,不聚集。另一个,当业务量猛增时,生命周期长,则周期内的Span Recorder就非常多,容易造成内存瓶颈;对于第二种LimitStore存储,它是以内存占用大小为维度,进行Span Recorder删除。它的缺点是当业务量猛增时,Span Recorder很容易突破设置的内存限制,则数据量不全,或者搜索traceid时,无法搜索到。 4 | 5 | 6 | 因为内存存储的Span Recorder都是有限制的,所以肯定会有Span Recorder删除行为,则Appdash提供了相关删除接口 7 | 8 | ```shell 9 | type DeleteStore interface{ 10 | // 因为是基于Store interface操作 11 | Store 12 | 13 | // 提供了Delete删除Span Recorder操作 14 | Delete(...ID) error 15 | } 16 | ``` 17 | # RecentStore 18 | 19 | ```shell 20 | type RecentStore struct { 21 | // 基于时间周期的SpanRecorder删除 22 | MinEvictAge time.Duration 23 | 24 | DeleteStore 25 | 26 | // 带有Span Recorder的生命周期管理 27 | created map[ID]int64 28 | 29 | // 上一次删除的时间 30 | lastEvicted time.Time 31 | 32 | mu sync.Mutex 33 | } 34 | // 由上可以看出,RecentStore是利用MinEvictAge和lastEvicted两个时间,来对Span Recorder进行生命周期的管理。 35 | 36 | func (rs *RecentStore) Collect(id SpanID, anns ...Annotation) error) { 37 | ... // 并发 38 | // 存储新来的Span Recorder 39 | if _, present := rs.created[id.Trace]; !present { 40 | rs.created[id.Trace] = time.Now().UnixNano() 41 | } 42 | // 删除过期的Span Recorder列表 43 | if time.Since(rs.lastEvicted) > rs.MinEvictAge { 44 | rs.evictBefore(time.Now().Add(-1*rs.MinEvictAge)) 45 | } 46 | 47 | return rs.DeleteStore.Collect(id, anns...) 48 | } 49 | 50 | // 删除小于t时间的Span Recorder。 51 | func (rs *RecentStore) evictBefore(t time.Time) { 52 | evictStart := time.Now() 53 | 54 | rs.lastEvicted = evictStart 55 | 56 | tnano := t.UnixNano() 57 | 58 | var toEvict []ID 59 | for id, ct := range rs.created{ 60 | if ct < tnano { 61 | toEvict = append(toEvict, id) 62 | delete(rs.created, id) 63 | } 64 | } 65 | 66 | if len(toEvict) ==0 { 67 | return 68 | } 69 | 70 | // 删除内存中的Span Recorder列表 71 | go func(){ 72 | rs.DeleteStore.Delete(toEvict...) 73 | }() 74 | return 75 | } 76 | ``` 77 | 78 | 由RecentStore的Collect可以看出,虽然删除和存储Span Recorder是异步操作。理论上应该是单独的goroutine去对所有收集到的Span Recorder的生命周期进行管理,否则,缺点两个: 79 | 80 | 1. 如果没有新的Span Recorder到来,则无法触发过期的Span Recorder删除。 81 | 2. 如果过期时间MinEvictAge设置得很小,则不断新到来的Span Recorder会不断触发goroutine操作,这个也是不合理的。 82 | 83 | # LimitStore 84 | 85 | ```shell 86 | type LimitStore struct { 87 | // 存储的Span Recorder最大数量 88 | Max int 89 | 90 | DeleteStore 91 | 92 | mu sync.Mutex 93 | 94 | // 带Max的Span Recorder生命周期管理 95 | traces map[ID]struct{} 96 | 97 | // 通过环状存储管理 98 | ring []int64 99 | 100 | // 下一个插入Span Recorder位置 101 | nextInsertIdx int 102 | } 103 | // 对于LimitStore存储,它是基于存储Span Recorder最大数量的维度来维护所有的Span Recorder。它是通过traces、ring和nextInsertIdx两个存储来维护的。traces用来表示trace是否存在;ring和nextInsertIdx用来表示环插入trace维护 104 | 105 | 其实,环traces的维护只需要ring数组和nextInsertIdx两个变量来维护,但是在ring数组中查找traceid太慢,所以引入了traces map结构存储,时间复杂度为o(1) 106 | 107 | func (ls *LimitStore) Collect(id SpanID, anns ...Annotation) error { 108 | ... // 并发 109 | if ls.ring ==nil { 110 | ls.ring = make([]int64, ls.Max) 111 | ls.traces = make(map[ID]struct{}, ls.Max) 112 | } 113 | 114 | // 获取traceid是否存在, 存在即更新 115 | if _, ok := ls.traces[id.Trace]; ok { 116 | return ls.DeleteStore.Collect(id, anns...) 117 | } 118 | 119 | // 如果ring环的下个插入位置不为0,则表示ring环满了,需要覆盖(删除并插入) 120 | if nextInsert := ls.ring[ls.nextInsertIdx]; nextInsert !=0 { 121 | old := ID(ls.ring[ls.nextInsertIdx]) 122 | delete(r.traces, old) 123 | ls.DeleteStore.Delete(old) 124 | } 125 | 126 | // 插入 127 | ls.traces[id.Trace] = struct{}{} 128 | ls.ring[ls.nextInsertIdx] = int64(id.Trace) 129 | ls.nextInsertIdx = (ls.nextInsertIdx+1)%ls.Max 130 | return ls.DeleteStore.Collect(id, anns...) 131 | } 132 | ``` 133 | 134 | 135 | 由此可看,RecentStore和LimitStore都是基于DeleteStore实现,且DeleteStore interface都是在Store上添加了Delete方法实现。而MemoryStore持久化存储则不仅实现了本地文件存储,还实现了DeleteStore interface。所以RecentStore和LimitStore都是基于MemoryStore实现的。 136 | 137 | MemoryStore的定期本地文件存储,策略不够丰富,目前只支持一种策略:内存全局写入文件,因为MemoryStore限制有写入磁盘的有效时间,一旦过期,则会导致有些trace无法落地磁盘;而且每次全局写入,持久化需要的时间过长。 138 | 139 | 其中RecentStore的设计和实现是存在缺陷的。 140 | -------------------------------------------------------------------------------- /appdash/opentracing.md: -------------------------------------------------------------------------------- 1 | 说过几次Appdash的出生早于OpenTracing标准,为了支持OpenTracing标准,Appdash做了一些扩展,但是我觉得它并不遵循OpenTracing标准, 添加了一个库和简单的补充。用户既可以使用老版本的实现,也可以使用OpenTracing标准扩展的实现。 2 | 3 | 在Appdash源码有个opentracing目录,这个是扩展实现。 4 | 5 | # OpenTracing 6 | 7 | Appdash基于部分OpenTracing标准的扩展实现,它是采用了basictracer-go库进行兼容实现的。这样的改动代价是最小的。 8 | 9 | 10 | ## Tracer 11 | 12 | 因为Appdash扩展的OpenTracing标准实现,是直接在basictracer-go实现的基础上扩展的,所以相关的扩展都是针对basictracer-go的相关结构扩展 13 | 14 | ```shell 15 | // 校验是否实现了interface 16 | var _ opentracing.Tracer = NewTracer(nil) 17 | 18 | // 在basictracer-go库中的Tracer参数,是采用的Options传递,所以Appdash的扩展也继承了这种方式便于赋值, Appdash中的Options相关参数在basictracer-go中都能找到对应的 tracer 参数。 19 | type Options struct { 20 | ShouldSample func(traceID uint64) bool 21 | 22 | Verbose bool 23 | 24 | Logger *log.Logger 25 | } 26 | 27 | // 默认的Tracer采样是全量的, 每条tracer都尽量被存储。 28 | func DefaultOptions() Options { 29 | return Options { 30 | ShouldSample: func(_ uint64) bool { return true }, 31 | Logger: newLogger(), 32 | } 33 | } 34 | 35 | func NewTracer(c appdash.Collector) opentracing.Tracer { 36 | return NewTracerWithOptions(c, DefaultOptions()) 37 | } 38 | 39 | func NewTracerWithOptions(c appdash.Collector, options Options) opentracing.Tracer { 40 | opts := basictracer.DefaultOptions() 41 | opts.ShouldSample = options.ShouldSample 42 | opts.Recorder = NewRecorder(c, options) 43 | return basictracer.NewWithOptions(opts) 44 | } 45 | ``` 46 | 47 | 说到这里,大家要想到一个问题,因为basictracer-go库是完全遵循OpenTracing标准的,但是Appdash的Span设计是没有遵循标准的。所以在RPC/IPC网络传输和本地RawSpan进行数据转换时,以及网络数据解析时,都应该会遇到解析和数据转换问题,所以Tracer的Inject和Extract需要重新实现,而不能使用basictracer-go库。但是这里并没有看到相关实现,所以这个问题后面再看。::TODO 48 | 49 | 50 | ## Recorder 51 | 52 | appdash扩展的opentracing中,Recorder实现了basictracer-go库的Recorder interface 53 | 54 | basictracer-go Recorder: 55 | 56 | ```shell 57 | type SpanRecorder interface { 58 | RecordSpan(span RawSpan) 59 | } 60 | 61 | // basictracer-go库已经给出了一个实现InMemorySpanRecorder 62 | ``` 63 | 64 | 这里Appdash也给出了一个实现Recorder 65 | 66 | ```shell 67 | type Recorder struct { 68 | collector appdash.Collector 69 | logOnce sync.Once 70 | varbose bool 71 | Log *log.Logger 72 | } 73 | 74 | // 新建Recorder 75 | func NewRecorder(collector appdash.Collector, opts Options) *Recorder { 76 | ... 77 | return &Recorder{ 78 | collector: collector, 79 | verbose: opts.Verbose, 80 | Log: opts.Logger, 81 | } 82 | } 83 | 84 | // 实现basictracer-go的Recorder interface 85 | func (r *Recorder) RecorderSpan(sp basictracer.RawSpan) { 86 | ... // 校验是否需要采样 87 | // baisctracer-go中的RawSpan数据转换为Appdash中的Span数据 88 | spanID := appdash.SpanID { 89 | Span: appdash.ID(uint64(sp.Context.SpanID)), 90 | Trace: appdash.ID(uint64(sp.Context.TraceID)), 91 | Parent: appdash.ID(uint64(sp.ParentSpanID)), 92 | } 93 | 94 | // 存储span name的event 95 | r.collectEvent(spanID, appdash.Span(sp.Operation)) 96 | 97 | // 存储span logs的event, 首先进行basictracer-go库的logs序列化, 然后打上log event 98 | for _, log := range sp.Logs { 99 | logs, _ := materializeWithJSON(log.Fields) 100 | // collectEvent主要工作:进行event的序列化为Annotations, 然后存储 101 | r.collectEvent(spanID, appdash.LogWithTimestamp(string(logs), log.Timestamp)) 102 | } 103 | 104 | // tags属于非event事件, 存储Tags 105 | for key, value := range sp.Tags { 106 | val := []byte(fmt.Sprintf("%+v", value)) 107 | r.collectAnnotation(spanID, appdash.Annotation{Key: key, Value: val}) 108 | } 109 | 110 | // Carrier携带信息存储 111 | for key, val := range sp.Context.Baggage { 112 | r.collectAnnotation(spanID, appdash.Annotation{Key: key, Value: []byte(val)}) 113 | } 114 | 115 | // 存储span的timespan event 116 | approxEndTime := sp.Start.Add(sp.Duration) 117 | r.collectEvent(spanID, appdash.Timespan{S: sp.Start, E: approxEndTime}) 118 | } 119 | ``` 120 | 121 | 通过RecorderSpan方法可以知道,执行basictracer.RawSpan的span信息存储,需要大量的TCP网络传输,且每个事件都需要,非常耗时,且对于collector服务的性能也会有较大的影响。所以最好的方式是使用Appdash中的**ChunkedCollector**,进行agent本地存储一定的span信息后,在一次通过tcp发送到Collector服务,进行存储。 122 | 123 | ## 总结 124 | 125 | 针对上面提到的问题,Appdash中扩展支持OpenTracing的实现,与自身Span信息结构有很大的不同,无法对SpanID和Annotations进行Inject和Extract,以及本地与Baggage数据转换等,所以如果采用OpenTracing标准,同时Collector还是基于原来的SpanID和Annotations存储,则需要进行Recorder层面的basictracer-go RawSpan信息与appdash Span信息转换,这个正是由SpanRecorder方法实现的。 126 | 127 | 我们由此可以知道,Appdash对OpenTracing标准的支持并不高,而且支持力度很弱,官方并没有对Appdash做了大改动,只是做了一个基于basictracer-go扩展的一个小功能,没有做很深入的工作。 128 | -------------------------------------------------------------------------------- /jaeger/tchannel-go/peer01.md: -------------------------------------------------------------------------------- 1 | # peer 2 | 3 | peer中文翻译为:对等体, 它在tchannel-go中表示rpc调用或者被调用的一方。 4 | 5 | peer相关错误码: 6 | 7 | ```shell 8 | ErrInvalidConnectionState // 连接处于无效状态 9 | 10 | ErrNoPeers // 没有可用的peer 11 | 12 | ErrPeerNoFound // 没有发现peer 13 | 14 | ErrNoNewPeers // 没有新的可用peer 15 | 16 | ``` 17 | 18 | 相关peer代码: 19 | 20 | ```shell 21 | // peer使用该interface,创建connection。peer所拥有的channel实现了该interface 22 | type Connectable interface { 23 | Connect(ctx context.Context, hostPort string) (*Connection, error) 24 | 25 | Logger() Logger 26 | } 27 | 28 | // channel拥有所有建立连接的Peers 29 | type PeerList struct { 30 | sync.RWMutex 31 | 32 | // parent:后续补充 33 | parent *RootPeerList 34 | // channel所有连接,对于每个远端hostPort所对应的peer 35 | peersByHostPort map[string]*peerScore 36 | // peerHeap: 后续补充, 它基于peers的score,来维护peers得最小heap 37 | peerHeap *peerHeap 38 | // channel所有连接的peers score计算 39 | scoreCalculator ScoreCalculator 40 | // lastSelected: 后续补充 41 | lastSelected uint64 42 | } 43 | 44 | // 创建PeerList,并初始化。RootPeerList后续补充 45 | func newPeerList(root *RootPeerList) *PeerList { 46 | reutrn &PeerList{ 47 | parent: root, 48 | peersByHostPort: make(map[string]*peerScore), 49 | scoreCalculator: newPreferIncomingCalculator(), 50 | peerHeap: newPeerHeap(), 51 | } 52 | } 53 | 54 | // 设置channel所有连接peers的score计算方式 55 | // 最后并重新计算当前channel下所有peers的score 56 | func (l *PeerList) SetStrategy(sc ScoreCalculator) { 57 | l.Lock() 58 | defer l.Unlock() 59 | 60 | l.scoreCalculator = sc 61 | for _, ps := range l.peersByHostPort { 62 | newScore := l.scoreCalculator.GetScore(ps.Peer) 63 | // 更新peer的score 64 | l.updatePeer(ps, newScore) 65 | } 66 | } 67 | 68 | // 根据peerList创建一个它的兄弟PeerList。 69 | func (l *PeerList) newSibling() *PeerList { 70 | sib := newPeerList(l.parent) 71 | return sib 72 | } 73 | 74 | // 在channel的PeerList中增加一个peer, 这个方法引出了后记 75 | func (l *PeerList) Add(hostPort string) *Peer { 76 | // 如果hostPort已存在,则不需要增加peer 77 | if ps, ok := l.exists(hostPort); ok { 78 | return ps.Peer 79 | } 80 | 81 | l.Lock() 82 | defer l.Unlock() 83 | 84 | // 再次校验是否已经存在peer 85 | if p, ok := l.peersByHostPort[hostPort]; ok { 86 | return p.Peer 87 | } 88 | 89 | // 确定不存在peer,创建peer, 增加RootPeerList的peersByHostPort映射关系 90 | p := l.parent.Add(hostPort) 91 | // 增加subchannel的引用计数 92 | p.addSC() 93 | // 根据peer创建,peerScore对象 94 | ps := newPeerScore(p, l.scoreCalculator.GetScore(p)) 95 | 96 | // 通过channel的peersByHostPort存储映射关系 97 | l.peersByHostPort[hostPort] = ps 98 | // push peer到peer heap中,后面再介绍 99 | l.peerHeap.addPeer(ps) 100 | 101 | return p 102 | } 103 | ``` 104 | 105 | ## 后记 106 | 107 | 108 | ### 第一个讨论点 109 | 110 | 我们在看tchannel-go源代码过程中经常遇到一个这样的写法,大家可以注意下: 111 | 112 | `tips: 下面我们都假设YYY已经初始化过了。` 113 | 114 | ```shell 115 | type XXX struct { 116 | sync.Mutex 117 | 118 | YYY map[string]interface{} 119 | } 120 | 121 | func (x *XXX) Add(key string, value interface{}) { 122 | x.RLock() 123 | if _, ok := x.YYY[key]; ok { 124 | return 125 | } 126 | x.RUnlock() 127 | 128 | x.Lock() 129 | if _, ok := x.YYY[key]; ok { 130 | return 131 | } 132 | 133 | x.YYY[key]=value 134 | x.Unlock() 135 | } 136 | ``` 137 | 138 | 在其他项目中也会存在这种写法。首先对于存在竞态的数据,在读写操作时需要加锁互斥操作。所以,下面这种做法也是经常见到的。 139 | 140 | ```shell 141 | func (x *XXX) Add(key string, value interface{}) { 142 | x.Lock() 143 | defer x.Unlock() 144 | 145 | if _, ok := x.YYY[key]; ok { 146 | return 147 | } 148 | 149 | x.YYY[key] = value 150 | return 151 | } 152 | ``` 153 | 154 | 对比这两者的写法,没有太多不同。这里要考虑的第一点: 155 | 156 | **读锁和读写锁,时效性明显不同。对于读锁,只有在写时阻塞,可以并发读;对于读写锁,读写都互斥。后者明显时耗大一些,如果并发量够大,则效果更明显** 157 | 158 | 我们考虑第二点,为何第一种写法,在尝试读取后,下一个锁操作内还要取尝试取一次数据,失败然后再进行存储操作? 159 | 160 | **因为每一次锁操作是一个完整操作,如果在临界区间,goroutine被调度,则其他想要操作该锁也得等待该锁释放,最后该goroutine被调度,完成释放锁后;如果该goroutine又被调度,则其他goroutine可能会操作该互斥YYY变量,所以下次进入临界区时,需要尝试再取一次数据,如果没有则进行存储操作** 161 | 162 | 163 | ### 第二个讨论点 164 | 165 | 对于PeerList的Add方法,或者说整个tchannel-go有一点做得不太好的地方是,方法里的锁操作太多,而且没有标识,比如`l.parent.Add`和`p.addSC`方法都存在锁操作,如果外界直接调用,锁相同则会导致死锁。在go有关的很多项目都是这样写的: 166 | 167 | ```shell 168 | func (x *XXX) AddLock() { 169 | x.Lock() 170 | defer x.Unlock() 171 | ... // other operations 172 | } 173 | 174 | func (x *XXX) AddNoLock() { 175 | ... // other operations 176 | } 177 | 178 | 或者 179 | 180 | func (x *XXX) Add() { 181 | ... // other operations 182 | } 183 | ``` 184 | 185 | 这个方法显示的告诉大家,方法内是否存在锁操作,这样尽量减少死锁反应。 186 | -------------------------------------------------------------------------------- /basictracer-go/tracer.md: -------------------------------------------------------------------------------- 1 | 首先,我们先说一下,opentracing-language是支撑OpenTracing标准的最小子集,而basictracer-language是对这最小子集的基本实现;我们可以不使用basictracer,直接厂商作为最小子集的扩展就行;所以basictracer是其中一个扩展子集,方便大家做tracer设计参考; 2 | 3 | # Tracer 4 | 5 | basictracer的Tracer扩展了一些参数集options,支持以下操作: 6 | 7 | 1. ShouldSample函数:可以根据一定规则,对匹配到的tracer则发起跟踪; 8 | 2. TrimUnsampledSpans布尔值:当某个tracer被丢弃时,直接不会在这条链路上的各个span产生数据存储; 9 | 3. Recorder:当Span执行单元结束被Finish后,则形成特定span recorder并通过agent发送到collector存储; 10 | 4. NewSpanEventListener:trace事件监听,// ::TODO 后面更新 11 | 5. DropAllLogs布尔值: 如果NewSpanEventListener被设置了,则该布尔值变量无效;否则,如果为True,则所有的Span Logs都指向noop操作 12 | 6. MaxLogsPerSpan:因为span logs在OpenTracing标准中只是说减少Baggage信息存储,因为跨IPC/RPC网络延迟大,这里是指span本地logs存储尽量少,毕竟OpenTracing不是分布式日志管理系统。 13 | 7. EnableSpanPool布尔值:复用已经finish的span列表,减少内存使用; 14 | 15 | 默认的Options: 16 | 17 | 1. ShouldSample函数为mod 64,尽最大能力保存该条trace; 18 | 2. MaxLogsPerSpan值为100 19 | 20 | 创建Tracer实例的方法有两种:NewWithOptions和New。前者传入Options参数,最常用;后者使用默认的Options,传入Recorder来存储Tracer 21 | 22 | ## SpanContext Impl 23 | 24 | basictracer中的SpanContext实现了opentracing-go中的SpanContext interface, 本地数据存储格式如下: 25 | 26 | ```shell 27 | type SpanContext struct { 28 | TraceID uint64 29 | SpanID uint64 30 | Sampled bool 31 | Baggage map[string]string 32 | } 33 | 34 | func (c SpanContext) ForeachBaggageItem(handler func(k, v string) bool){ 35 | for k, v := range c.Baggage{ 36 | if !handler(k, v){ 37 | break 38 | } 39 | } 40 | } 41 | 42 | func (c SpanContext) WithBaggageItem(key, val string) SpanContext{ 43 | var newBaggage map[string]string 44 | if c.Baggage ==nil{ 45 | newBaggage = map[string]string{ 46 | key: val, 47 | } 48 | } else { 49 | newBaggage = make(map[string]string, len(c.Baggage)+1) 50 | for k, v := range c.Baggage{ 51 | newBaggage[k] = v 52 | } 53 | newBaggage[key] = val 54 | } 55 | 56 | return SpanContext{c.TraceID, c.SpanID, c.Sampled, newBaggage} 57 | } 58 | ``` 59 | 60 | 上面的SpanContext实现了opentracing-go的SpanContext interface,并且提供了额外的操作和存储: 61 | 62 | 1. TraceID、SpanID和Sampled,没有存储到Baggage的map中,而是单独拎出来,含义更加鲜明,易于理解; 63 | 2. 提供了SpanContext的WithBaggageItem方法,装载Baggage携带信息; 64 | 65 | 同时,这里的WithBaggageItem方法,我有**两个疑问**: 66 | 67 | 1. SpanContext的生命周期在于本地Span,在RPC/IPC时会通过TextMapCarriers、HTTPHeaderCarriers和BinaryCarriers进行携带,同一个Span执行单元不会并发,所以应该不需要新建一个newBaggage存储,写入然后再赋予给SpanContext中的Baggage; 68 | 2. 这个方法为何还要返回值SpanContext,如果不返回值,我觉得没什么问题; 69 | 70 | ## TracerImpl 71 | 72 | tracerImpl实现了Tracer最基本的最小子集OpenTracing标准——opentracing-go; 73 | 74 | ```shell 75 | type tracerImpl struct{ 76 | // tracer的扩展参数集 77 | options Options 78 | // TextMap和HTTPHeader 79 | textPropagator *textMapPropagator 80 | // 比特流 81 | binaryPropagator *binaryPropagator 82 | // 如果厂商打算基于basictracer进行扩展,这里提供一个Baggage上下文信息传输的interface接口方法列表 83 | accessorPropagator *accessorPropagator 84 | } 85 | ``` 86 | 87 | tracerImpl中的StartSpan, Inject和Extract方法,其中,后两个方法: 88 | 89 | 1. Inject方法根据RPC/IPC传输数据格式,来进行具体的Inject操作;如:format为TextMap或者HTTPHeader,则通过textMapPropagator进行SpanContext的数据转换;如果是binary,则通过binaryPropagator进行数据转换;如果是厂商自己实现,则通过accessPropagator委托实现;`notice: TextMap和HTTPHeader虽然存储数据格式不同,但是opentracing-go的TextMapReader和TextMapWriter已经屏蔽了数据格式存储方式` 90 | 2. Extract方法,同上,只是反方向,把RPC/IPC网络传输数据通过SpanContext数据存储格式进行数据转换; 91 | 92 | 对于StartSpan方法的代码片段,我有个疑问: 93 | 94 | ```shell 95 | ReferencesLoop: 96 | for _, ref := range opts.References { 97 | switch ref.Type { 98 | case opentracing.ChildOfRef, 99 | opentracing.FollowsFromRef: 100 | 101 | refCtx := ref.ReferencedContext.(SpanContext) 102 | sp.raw.Context.TraceID = refCtx.TraceID 103 | sp.raw.Context.SpanID = randomID() 104 | sp.raw.Context.Sampled = refCtx.Sampled 105 | sp.raw.ParentSpanID = refCtx.SpanID 106 | 107 | if l := len(refCtx.Baggage); l > 0 { 108 | sp.raw.Context.Baggage = make(map[string]string, l) 109 | for k, v := range refCtx.Baggage { 110 | sp.raw.Context.Baggage[k] = v 111 | } 112 | } 113 | break ReferencesLoop 114 | } 115 | } 116 | ``` 117 | 以上我们可以看到,当前要创建的Span与其关联的References列表,只取出了首个Reference,且ParentSpanID的赋值,无论是ChildOf或者FollowsFrom都可以;我认为兄弟关系不可以成为ParentSpanID; 118 | 119 | `备注:在SpanReferences列表中,只会存在[0,1]个ParentSpan和[0, N]个兄弟span` 120 | 121 | 在Options中有个属性变量:EnableSpanPool布尔值,如果为true,则使用sync.Pool临时对象池,当服务关闭后,则释放这些内存空间;它用来获取可用的Span,减少内存使用; 122 | -------------------------------------------------------------------------------- /jaeger/tchannel-go/peerScore-and-idleSweep.md: -------------------------------------------------------------------------------- 1 | # peer分数计算策略 2 | 3 | ScoreCalculator定义了计算score方法的interface, 默认的分数值实现包括:zeroCalculator的零分数值实现、leastPendingCalculator的调用次数总数量和preferPendingCalculator的调出次数总数量 4 | 5 | ```shell 6 | // 分值计算方法 7 | type ScoreCalculator interface { 8 | GetScore(p *Peer) uint64 9 | } 10 | 11 | // ScoreCalculatorFunc是一个分数值的计算适配器 12 | 13 | // 类似于http中的ServeHTTP, 如下所示: 14 | //**************************************************** 15 | type Handler interface { 16 | ServeHTTP(ResponseWriter, *Request) 17 | } 18 | 与 19 | func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) 20 | //**************************************************** 21 | 22 | type ScoreCalculatorFunc func(p *Peer) uint64 23 | 24 | func (f ScoreCalculatorFunc) GetScore(p *Peer) uint64 { 25 | return f(p) 26 | } 27 | 28 | 29 | // 所有分数值都为0的ScoreCalculator interface实现 30 | type zeroCalculator struct{} 31 | 32 | func (zeroCalculator) GetScore(p *Peer) uint64 { 33 | return 0 34 | } 35 | 36 | 37 | // 调用次数越多,score越大的计算方法。获取channel与该Peer的所有调入和调出连接的message exchange总数量 38 | // 39 | // 这里的math.MaxUint64有点不理解? 后续再看::TOOD 40 | type leastPendingCalculator struct{} 41 | 42 | func (leastPendingCalculator) GetScore(p *Peer) uint64 { 43 | inbound, outbound := p.NumConnections() 44 | if inbound + outbound == 0 { 45 | return math.MaxUint64 46 | } 47 | 48 | return uint64(p.NumPendingOutbound()) 49 | } 50 | 51 | // 所有调出的总数量score计算方法。 52 | type preferIncomingCalculator struct{} 53 | 54 | func (preferIncomingCalculator) GetScore(p *Peer) uint64 { 55 | inbound, outbound := p.NumConnections() 56 | if inbound + outbound == 0 { 57 | return math.MaxUint64 58 | } 59 | 60 | numPendingOutbound := uint64(pNumPendingOutbound()) 61 | if inbound == 0 { 62 | return math.MaxInt32 + numPendingOutbound 63 | } 64 | 65 | return numPendingOutbound 66 | } 67 | ``` 68 | 69 | # idle sweep 70 | 71 | idle sweep是一个goroutine定时任务,去检测connection空闲时间,并清除空闲时间过长的连接 72 | 73 | ```shell 74 | type idleSweep struct { 75 | ch *Channel 76 | // 每个连接的最大空闲时间 77 | maxIdleTime time.Duration 78 | // 多长时间间隔进行检测 79 | idleCheckInterval time.Duration 80 | // 用于停止检测,goroutine退出 81 | stopCh chan struct{} 82 | // start表示该goroutine是否已经启动 83 | start bool 84 | } 85 | 86 | // 开启idle sweep检测goroutine 87 | func startIdleSweep(ch *Channel, opts *ChannelOptions) *idleSweep { 88 | is := &idleSweep { 89 | ch: ch, 90 | maxIdleTime: opts.MaxIdleTime, 91 | idleCheckInterval: opts.IdleCheckInterval, 92 | } 93 | 94 | is.start() 95 | return is 96 | } 97 | 98 | // 接着上面的startIdleSweep方法,继续执行goroutine启动任务 99 | // 主要是初始化idleSweep相关参数, 启动一个goroutine 100 | func (is *idleSweep) start() { 101 | // 如果goroutine已经启动,或者定期检测时长间隔为空, 则直接返回 102 | if is.started || is.idleCheckInterval <=0 { 103 | return 104 | } 105 | 106 | ... // log records 107 | 108 | is.started = true 109 | is.stopCh = make(chan struct{}) 110 | 111 | go is.pollerLoop() 112 | } 113 | 114 | // 启动定时机制,并定时检测检测channel上的所有空闲连接 115 | func (is *idleSweep) pollerLoop() { 116 | // 这里的timeTicker是使用的channel中channelConnectionCommon参数 117 | ticker := is.ch.timeTicker(is.idleCheckInterval) 118 | 119 | for { 120 | select { 121 | case <-ticker.C: 122 | // 到时间则检测超时空闲连接,并清除 123 | is.checkIdleConnections() 124 | case <- is.stopCh: 125 | // 如果外界有干预,则直接退出 126 | ticker.Stop() 127 | return 128 | } 129 | } 130 | } 131 | 132 | // 检测超时空闲连接,并清除 133 | func (is *idleSweep) checkIdleConnections() { 134 | now := is.ch.timeNow() 135 | 136 | idleConnections := make([]*Connection, 0, 10) 137 | is.ch.mutable.RLock() 138 | // 获取channel的所有连接, 并拿着这个当前时间和连接上次活跃时间做差值,结果与maxIdleTime比较 139 | for _, conn := range is.ch.mutable.conns { 140 | idleTime := now.Sub(conn.getLastActivityTime()) 141 | if idleTime >= is.maxIdleTime { 142 | idleConnections = append(idleConnections, conn) 143 | } 144 | } 145 | is.ch.mutable.RUnlock() 146 | 147 | // 获取的超时空闲连接,全部清除掉 148 | for _, conn := range idleConnections { 149 | // 如果连接不是活跃的,则直接过滤掉; 否则,则直接关闭该连接 150 | if !conn.IsActive() { 151 | continue 152 | } 153 | 154 | conn.close(LogField{"reason", "Idle connection closed"}) 155 | } 156 | } 157 | ``` 158 | 159 | # 总结 160 | 161 | 对于idle sweep的这种goroutine启动机制,并做定时任务这种写法,是一种很标准的写法。总结步骤为: 162 | 163 | 1. 传入外部参数,并初始化一个goroutine所需要的参数; 164 | 2. 设置内部参数,并start,在start方法中,启动一个goroutine; 165 | 3. 在goroutine中,通过time.TimeTicker触发定时任务执行,并使用for循环和select阻塞机制;且设置有goroutine退出外部介入机制; 166 | 4. 外部传入goroutine退出信号,或者定期执行任务。 167 | 168 | 169 | -------------------------------------------------------------------------------- /jaeger/tchannel-go/framepool-and-calloptions.md: -------------------------------------------------------------------------------- 1 | # frame pool interface 2 | 3 | frame pool提供了创建Frame的临时对象池,包括Frame的分配和回收,节约内存。 4 | 5 | ```shell 6 | // frame pool的分配和回收interface 7 | type FramePool interface { 8 | Get() *Frame 9 | 10 | Release(f *Frame) 11 | } 12 | ``` 13 | 14 | tchannel提供了三种对frame pool interface的实现, 分别是disable、sync和channel,默认frame pool实例为sync.Pool 15 | 16 | ## disabled frame pool 17 | 18 | disabled含义为关闭,没有使用临时对象池 19 | 20 | ```shell 21 | var DisabledFramePool = disabledFramePool{} 22 | 23 | type disabledFramePool struct{} 24 | 25 | func (p disabledFramePool) Get() *Frame { 26 | return NewFrame(MaxFramePayloadSize) 27 | } 28 | 29 | func (p disabledFramePool) Release(frame *Frame) {} 30 | ``` 31 | 32 | ## sync frame pool 33 | 34 | 协议帧实例默认使用sync.Pool临时对象池 35 | 36 | ```shell 37 | var DefaultFramePool = NewSyncFramePool() 38 | 39 | type syncFramePool struct { 40 | pool *sync.Pool 41 | } 42 | 43 | // 通过sync.Pool对象池创建Frame实例 44 | func NewSyncFramePool() FramePool { 45 | return &syncFramePool { 46 | pool: &sync.Pool{ 47 | New: func() interface{} { 48 | return NewFrame(MaxFramePayloadSize) 49 | }, 50 | }, 51 | } 52 | } 53 | 54 | // 从sync.Pool临时对象池获取一个frame 55 | func (p *syncFramePool) Get() *Frame{ 56 | return p.pool.Get().(*Frame) 57 | } 58 | 59 | // 把frame释放到sync.Pool临时对象池中 60 | func (p *syncFramePool) Release(f *Frame) { 61 | return p.pool.Put(frame) 62 | } 63 | ``` 64 | 65 | ## channel frame pool 66 | 67 | 使用channel,在队列上存储指定数量的Frame 68 | 69 | ```shell 70 | type channelFramePool chan *Frame 71 | 72 | // 新建channel队列数量为capacity数量的frame 73 | func NewChannelFramePool(capacity int) FramePool { 74 | return channelFramePool(make(chan *Frame, capacity)) 75 | } 76 | 77 | // 从channel队列上获取一个frame,如果队列上为空,则直接在临时对象池之外新建一个Frame。 78 | func (c *channelFramePool) Get() *Frame { 79 | select { 80 | case f :=<-c: 81 | return f 82 | default: 83 | return NewFrame(MaxFramePayloadSize) 84 | } 85 | } 86 | 87 | // 当channel队列上满时,则frame只能靠GC了。 88 | func (c *channelFramePool) Release(f *Frame) { 89 | select { 90 | case c <- f: 91 | default: 92 | } 93 | } 94 | ``` 95 | 96 | # call options 97 | 98 | 这节内容比较少,我们增加call req和call res协议payload部分的transport header相关内容 99 | 100 | 如果大家对tchannel的transport headers不了解,可以看看tchannel协议规范中的protocal部分call req和call res的transport headers。 101 | 102 | ```shell 103 | nh~1 (key~1 value~1){nh} 104 | ``` 105 | 106 | ```shell 107 | type Format string 108 | 109 | func (f Format) String() string { 110 | return string(f) 111 | } 112 | 113 | const ( 114 | HTTP Format = "http" 115 | JSON Format = "json" 116 | Raw Format = "raw" 117 | Thrift Format = "thrift" 118 | ) 119 | 120 | // 对于transport headers部分,当key="as" ,也即the Arg Scheme。 value可以为:thirft, sthrift, json, http和raw。这里我们看到没有使用sthrift协议传输 121 | 122 | // transport headers所有相关的key 123 | type CallOptions struct { 124 | // the Arg Scheme, 协议类型 125 | Format Format 126 | // 一个请求去指定的节点 127 | ShardKey string 128 | // 重试相关Retry Flags 129 | RequestState *RequestState 130 | // ... 131 | RoutingKey string 132 | // 当协议不指定服务名时,可以通过该参数指定 133 | RoutingDelegate string 134 | // 发起请求的调用名 135 | callerName string 136 | } 137 | 138 | // 由上面和tchannel协议对比,我们可以看到,这里还缺少双方通信的Host:Port, sepculative execution,Failure Domain熔断几个key。 139 | 140 | // 默认的call options是空值 141 | var defaultCallOptions = &CallOptions{} 142 | 143 | // 这个transportHeaders在message章节说过. 144 | // 145 | // 为何这里直接把the Arg Scheme设置为raw协议呢?是默认值为原生? 146 | func (c *CallOptions) setHeaders(headers transportHeaders) { 147 | // 把call options参数赋值给transport headers 148 | headers[ArgScheme] = Raw.String() 149 | c.overrideHeaders(headers) 150 | } 151 | 152 | func (c *CallOptions) overrideHeaders(headers transportHeaders) { 153 | // 把call options赋值给transport headers 154 | if c.Format != "" { 155 | headers[ArgScheme] = c.Format.String() 156 | } 157 | if c.ShardKey != ""{ 158 | headers[ShardKey] = c.ShardKey 159 | } 160 | 161 | if c.RoutingKey != "" { 162 | headers[RoutingKey] = c.RoutingKey 163 | } 164 | 165 | if c.RoutingDelegate != ""{ 166 | headers[RoutingDelegate] = c.RoutingDelegate 167 | } 168 | 169 | if c.callerName != "" { 170 | headers[CallerName] = c.callerName 171 | } 172 | } 173 | 174 | // 只是赋值个ArgScheme, 其他不用改变。用这么大的方法? 175 | func setResponseHeaders(reqHeaders, respHeaders transportHeaders) { 176 | respHeaders[ArgScheme] = reqHeaders[ArgScheme] 177 | } 178 | ``` 179 | 180 | # 总结 181 | 182 | 对于transport headers比较奇怪的是,在tchannel协议规范中并不存在routing key,但是实现中却存在。 183 | -------------------------------------------------------------------------------- /opentracing-go/opentracer-go源码阅读一.md: -------------------------------------------------------------------------------- 1 | # Tracer 2 | 3 | ## Tracer Interface 4 | 5 | ```shell 6 | type Tracer interface{ 7 | StartSpan(operationName string, opts ...StartSpanOption) Span 8 | 9 | Inject(sm SpanContext, format interface{}, carrier interface{}) error 10 | 11 | Extract(format interface{}, carrier interface{}) (SpanContext, error) 12 | } 13 | ``` 14 | 15 | 以上是实现一个tracer调用链跟踪的**最小子集**,任何一个底层跟踪系统的实现,如果实现了它,则可以把整个调用链串起来。 16 | 17 | 在Tracer interface中StartSpan方法传参,传入Span结构体参数,如:`SpanReference列表`,`创建时间`和`Span Tag信息`;其中: 18 | 19 | `SpanReference`列表:表示新创建的Span与【curr_depth+1】Span之间的关系,如FollowsFrom和ChildOf;注意,这个是RPC/IPC跨进程调用关系,所以SpanContext表示OpenTracing标准中可以携带Baggage信息; 20 | 21 | `Tags`: 类型:map[string]interface{}, 生命周期:span执行单元; 22 | 23 | 因为`SpanReference`, `StartTime`和 `Tags`三个变量为StartSpanOptions参数,所以必须实现StartSpanOption接口中Apply方法。 **这个StartSpanOptions代表Span的必要参数子集**。 24 | 25 | ## globaltracer 26 | 27 | 每一个微服务都会有一个globaltracer值, 我们通过SetGlobalTracer方法设置业务系统采用的分布式跟踪系统产品;一个产品会有1个到多个微服务,则每个微服务都需要指定全局Tracer变量值,这一个Tracer就代表一个遵循OpenTracing标准的厂商产品,**在一个产品中,所有微服务的Tracer变量值都必须指向同一个厂商产品,否则,分布式跟踪系统服务生效**。 28 | 29 | 其中,我们可以通过`GlobalTracer()`方法获取微服务中的Tracer值; 30 | 31 | 如果我们没有对Tracer进行初始化,则意味着没有指定采用哪个厂商的分布式跟踪系统产品,则默认情况下,采用opentracing-go底层标准下的默认空Tracer——**NoopTracer**,这个是在实现OpenTracing标准API库时,必须要做的,因为如果业务系统采用的组件或者第三方库中有探针,如果不实现空的Tracer,则直接报错,无法使用,从而导致Tracer与业务强耦合了。 32 | 33 | ## NoopTracer 34 | 微服务中默认的空Tracer,包含:NoopTracer、noopSpan和noopSpanContext三个变量值;其中: 35 | 36 | 1. noopSpanContext是在SpanReference中使用,它代表SpanContext上下文传输tracer时的span获取。并携带Baggage信息;`ForeachBaggageItem(func(k, v string) bool){}` 37 | 2. noopSpan实现了Span interface。方法实现全部为空; 38 | 3. noopTracer实现了Tracer interface,包括StartSpan、Inject和Extract三个方法;全部为空实现; 39 | 40 | 微服务中,NoopTracer是Tracer未指定时的默认实现,不会在Tracer中生成任何数据,也不会产生Tracer调用链路; 41 | 42 | # Span 43 | 44 | ## Span interface 45 | 46 | ```shell 47 | type Span interface{ 48 | // Span执行单元结束 49 | Finish() 50 | // 带结束时间和日志记录列表信息,Span执行单元结束;也就是说结束时间可以业务指定; 日志列表也可以直接添加; 目前还不知道这个的使用场景; 51 | FinishWithOptions(opts FinishOptions) 52 | // 把Span的Baggage封装成SpanContext 53 | Context() SpanContext 54 | // 设置Span的操作名称 55 | SetOperationName(operationName string) Span 56 | // 设置Span Tag 57 | SetTag(key string, value interface{}) Span 58 | // 设置Span的log.Field列表; 59 | // span.LogFields( 60 | // log.String("event", "soft error"), 61 | // ) 62 | LogFields(fields ...log.Field) 63 | // key-value列表={"event": "soft error", "type": "cache timeout", "waited.millis":1500} 64 | LogKV(alternatingKeyValues ...interface{}) 65 | // LogFields与LogKV类似,只是前者已封装好; 66 | 67 | // 设置span的Baggage:key-value, 用于跨进程上下文传输 68 | SetBaggageItem(restrictedKey, value string) Span 69 | // 通过key获取value; 70 | BaggageItem(restrictedKey string) string 71 | // 获取Span所在的调用链tracer 72 | Tracer() Tracer 73 | // 废弃,改用LogFields 或者 LogKV 74 | LogEvent(event string) 75 | // 同上 76 | LogEventWithPayload(event string, payload interface{}); 77 | // 同上 78 | Log(data LogData) 79 | } 80 | ``` 81 | 82 | ## SpanContext 83 | 84 | 首先,需要看一小段代码: 85 | 86 | ```shell 87 | package main 88 | 89 | import "fmt" 90 | 91 | type Fun struct{} 92 | 93 | func main() { 94 | var fun1, fun2 = Fun{}, Fun{} 95 | if fun1 == fun2 { 96 | fmt.Println("fun1==fun2") 97 | } else { 98 | fmt.Println("fun1!=fun2") 99 | } 100 | } 101 | 102 | // 执行结果:fun1==fun2 103 | // 比较两个值是否相等,取决于:值和类型,都相等这表示相同; 104 | ``` 105 | 106 | Context用于上下文数据传输使用,在OpenTracing标准中,Span之间跨进程调用时,会使用SpanContext传输Baggage携带信息。通过context标准库实现;如: 107 | 108 | ```shell 109 | type contextKey struct{} 110 | 111 | var activeSpanKey = contextKey{} 112 | 113 | // 封装span到context中 114 | func ContextWithSpan(ctx context.Context, span Span) context{ 115 | return context.WithValue(ctx, activeSpanKey, span) 116 | } 117 | 118 | // 从ctx中通过activeSpanKey取出Span,这里可以看到不同服务的activeSpanKey值,是相同的,上面的DEMO可以说明。 119 | func SpanFromContext(ctx context.Context) Span{ 120 | val:=ctx.Value(activeSpanKey) 121 | if sp, ok := val.(Span); ok{ 122 | return sp 123 | } 124 | return nil 125 | } 126 | ``` 127 | 128 | 通过context上下文的activeSpanKey,我们可以获得Span,并创建新的span,如: 129 | 130 | ```shell 131 | func startSpanFormContextWithTracer(ctx context.Context, tracer Tracer, operationName string, opts ...StartSpanOption) (Span, context.Context){ 132 | // 首先从上下文看是否能够获取到span,如果获取不到,再创建tracer和span; 133 | if parentSpan:= SpanFromContext(ctx); parentSpan !=nil { 134 | opts = append(opts, ChildOf(parentSpan.Context())) 135 | } 136 | 137 | span := tracer.StartSpan(operationName, opts...) 138 | return span, ContextWithSpan(ctx, span) 139 | } 140 | ``` 141 | -------------------------------------------------------------------------------- /jaeger/TChannel/go1.5版本的一种tcp监听关闭处理方式.md: -------------------------------------------------------------------------------- 1 | tchannel-go项目作者prashantv对golang1.5 linux版本socket Accept方法的封装 2 | 3 | 4 | ## 问题描述 5 | 6 | 作者在linux上测试tchannel-go项目时发现,当关闭服务端的listener后,有时候还是有一些client connection能够进来。后来作者对当时的go1.5版本进行大量测试,确实发现存在,当服务端主动关闭后,client还能够进来。但是在osx其他平台不会发现这个问题。 7 | 8 | 当listener做关闭操作后,然后client再发起请求建立连接。实际上它只是标记socket为Closed状态,但是不会影响epoll接收新连接。 9 | 10 | ```shell 11 | 详细解释: 12 | 13 | 如果epoll所在的监听队列上有新来的连接,这时socket accept正在从该队列上获取新来的连接。这时,如果server主动关闭listener,则因为server端存在连接引用,所以暂时不会关闭,需要等待accept当前新来的连接处理完成后,再关闭并destory fd。 14 | 15 | 所以出现该问题的主要原因是,当accept正在获取新来的连接时,因为引用计数不为0,所以导致监听无法真正关闭。只有当前accept获取到新来的连接后,才会使得引用计数降为0,则时才会真正关闭监听。 16 | ``` 17 | 18 | ## 问题解决 19 | 20 | 所以针对这个go1.5版本linux等存在的缺陷,需要在外层引入引用计数和条件变量,当accept正在阻塞或获取新来的连接时,如果server直接关闭监听,则正在阻塞状态的goroutine,直接收到server关闭error;如果正在获取新来的连接,则外层加一个引用计数,当获取完成后,在减去这个引用计数;在server的close方法包装一层,如果这个引用计数不为0,则阻塞当前这个goroutine,直到引用计数等于0,再退出。这样保证了accept不会再接收到新的连接。 21 | 22 | ### 代码示例 23 | 24 | ```shell 25 | // 对net.Listener的封装,引入引用计数和条件变量 26 | type SaneListener struct { 27 | l net.Listener 28 | c *sync.Cond 29 | refCount int 30 | } 31 | 32 | // 当进入Accept之前,引用计数做加一操作,防止server主动Close listener操作时,因为底层的引用计数不为0,导致暂时不会发生真正的close fd操作。 33 | // SaleListener的Close操作,因为refCount引用计数不为0,则Close暂时不会退出。底层的监听已关闭,但是会等待accept获取新连接操作处理完成,这样close操作就相当于滞后了一个连接处理。 34 | func (s *SaneListener) incRef() { 35 | s.c.L.Lock() 36 | s.refCount++ 37 | s.c.L.Unlock() 38 | } 39 | 40 | // 当accept获取到新来的连接或者获取到一个server监听关闭error,引用计数减一 41 | // 这样SaneListener的Close操作,因为引用计数关闭,则真正关闭。 42 | // 43 | // 由于SaneListener的Close操作,如果server正在获取新连接,则该goroutine会发生条件阻塞;等待accept操作完成后,通过Broadcast操作唤醒睡眠的goroutine,继续Close操作。 44 | func (s *SaneListener) decRef() { 45 | s.c.L.Lock() 46 | s.refCount--- 47 | s.c.Broadcast() 48 | s.c.L.Unlock() 49 | } 50 | 51 | // accept操作 52 | func (s *SaneListener) Accept() (net.Conn, error){ 53 | s.incRef() 54 | defer s.decRef() 55 | return s.l.Accept() 56 | } 57 | 58 | // Close操作:底层的监听是提前关闭了,但是epoll队列中正在被accept的新连接还尚在处理中,所以底层的引用计数不等于0,则需要该操作完成后,再退出Close调用。这样,在Close操作后,server不会接收新的连接了 59 | func (s *SaneListener) Close() error { 60 | err := s.l.Close() 61 | if err == nil { 62 | s.c.L.Lock() 63 | for s.refCount > 0 { 64 | s.c.Wait() 65 | } 66 | s.c.L.Unlock() 67 | } 68 | return err 69 | } 70 | 71 | func (s *SaneListener) Addr() net.Addr { 72 | return s.l.Addr() 73 | } 74 | ``` 75 | 76 | 条件变量cond,使得refCount大于0时,主动阻塞该goroutine;等待accept完成获取新连接或者获取到error操作后,在通过decRef的Broadcast广播唤醒阻塞的goutines。 77 | 78 | ## 小结 79 | 80 | 我们可以看到server对监听关闭操作,当accept正在获取新来的连接时,因引用计数不为0,则不会真正的destroy掉net fd。通过引入上层引用计数,来达到当关闭监听后,确保server不会再接收新连接了。这个引用计数和条件变量大家可以认真学习,同时学习下golang的网络库。 81 | 82 | 83 | ## 参考资料 84 | 85 | [tchannel-go net.listener](https://github.com/uber/tchannel-go/blob/dev/tnet/listener.go) 86 | 87 | [net: Listener sometimes accepts connections after Close](https://github.com/golang/go/issues/13762) 88 | 89 | [Golang网络:核心API实现剖析(一)](https://juejin.im/entry/5a24c5456fb9a044fa19b086) 90 | 91 | [关于TCP 半连接队列和全连接队列](http://jm.taobao.org/2017/05/25/525-1/) 92 | 93 | ## 后记 94 | 95 | ```shell 96 | // go1.10.2/src/internal/poll/fd_unix.go 第93行 97 | // 我们可以看到当底层accept获取新连接的引用计数为0时,才会真正destory掉net fd。 98 | 99 | 77 // Close closes the FD. The underlying file descriptor is closed by the 100 | 78 // destroy method when there are no remaining references. 101 | 79 func (fd *FD) Close() error { 102 | 80 if !fd.fdmu.increfAndClose() { 103 | 81 return errClosing(fd.isFile) 104 | 82 } 105 | 83 106 | 84 // Unblock any I/O. Once it all unblocks and returns, 107 | 85 // so that it cannot be referring to fd.sysfd anymore, 108 | 86 // the final decref will close fd.sysfd. This should happen 109 | 87 // fairly quickly, since all the I/O is non-blocking, and any 110 | 88 // attempts to block in the pollDesc will return errClosing(fd.isFile). 111 | 89 fd.pd.evict() 112 | 90 113 | 91 // The call to decref will call destroy if there are no other 114 | 92 // references. 115 | 93 err := fd.decref() 116 | 94 117 | 95 // Wait until the descriptor is closed. If this was the only 118 | 96 // reference, it is already closed. Only wait if the file has 119 | 97 // not been set to blocking mode, as otherwise any current I/O 120 | 98 // may be blocking, and that would block the Close. 121 | 99 if !fd.isBlocking { 122 | 100 runtime_Semacquire(&fd.csema) 123 | 101 } 124 | 102 125 | 103 return err 126 | 104 } 127 | 128 | // go1.10.2/src/internal/poll/fd_mutex.go 129 | 209 func (fd *FD) decref() error { 130 | 210 if fd.fdmu.decref() { 131 | 211 return fd.destroy() 132 | 212 } 133 | 213 return nil 134 | 214 } 135 | ``` 136 | -------------------------------------------------------------------------------- /basictracer-go/span.md: -------------------------------------------------------------------------------- 1 | # Span 2 | 3 | span interface扩展了opentracing-go最小子集,新增了两个方法 4 | 5 | ```shell 6 | type Span interface{ 7 | opentracing.Span 8 | 9 | // 获取operation name 10 | Operation() string 11 | 12 | // 创建span的时间 13 | Start() time.Time 14 | } 15 | ``` 16 | 17 | ## Span Impl 18 | 19 | spanImpl实现了Span interface, span的创建在[《basictracer源码阅读——TracerImpl》](https://gocn.vip/article/868)最后一说明,通过sync.Pool临时对象池创建小对象; 20 | 21 | 大家如果想对sync.Pool有更深入的理解,可以看看[《真有趣达达写的slab》](https://github.com/funny/slab), 非常有意思,特别是无锁的小对象池。 22 | 23 | ```shell 24 | type spanImpl struct { 25 | tracer *tracerImpl 26 | event func(SpanEvent) 27 | // span是否存在并发,取决于执行单元的粗细粒度,如果跨goroutine使用,则会存在并发 28 | sync.Mutex 29 | // 记录了OpenTracing标准中Span最小子集的所有信息 30 | raw RawSpan 31 | // 这个span因为在tracerImpl中设置了MaxLogsPerSpan,存储被丢弃的日志记录数 32 | numDroppedLogs int 33 | } 34 | 35 | type RawSpan struct { 36 | // Baggage信息,并且还携带了TraceID、SpanID和Sampled 37 | Context SpanContext 38 | 39 | // 父SpanID,如果当前Span为tracer的头结点,则ParentSpanID=0 40 | ParentSpanID uint64 41 | 42 | // Span的Operation name 43 | Operation string 44 | 45 | // 创建Span的时间 46 | Start time.Time 47 | 48 | // Span执行单元的时间长度 49 | Duration time.Duration 50 | 51 | // 本身Tags信息 52 | Tags opentracing.Tags 53 | 54 | // 日志事件记录列表 55 | Logs []opentracing.LogRecord 56 | } 57 | 58 | // 设置span的操作名称 59 | func (s *spanImpl) SetOperationName(operationName string) opentracing.Span { 60 | ... // 注意:并发上锁 61 | } 62 | 63 | // 当span不需要被记录时,判定是否要对span的tags进行本地存储 64 | func (s *spanImpl) trim() bool{ 65 | return !s.row.Context.Sampled && s.tracer.options.TrimUnsampledSpans 66 | } 67 | 68 | // 为span设置tags 69 | // 注意:当为span存储sampling.priority时,这个值是存储在spanImpl的属性rawspan中的sampled值,所以它在basictracer实现中并没有写入tags; 70 | // 另外对于basictracer的实现,值得说的一点是: 71 | // 1. 在tracerImpl中的Options中已经通过ShouldSample函数指定该tracer是否被跟踪; 72 | // 2. 同时, 无论是否有tracer前置条件,span本身都可以选择是否记录并存储 73 | func (s *spanImpl) SetTag(key string, value interface{}) opentracing.Span{ 74 | defer s.onTag(key, value) 75 | ... // 注意:并发上锁 76 | } 77 | 78 | // 通过key-value键值对列表转换为[]log.Field, 并存储到spanImpl Logs中 79 | func (s *spanImpl) LogKV(keyValues ...interface{}) { 80 | ... 81 | // 当发生错误时,记录错误日志的错误信息和错误位置 82 | s.LogFields(log.Error(err), log.String("function", "LogKV") 83 | } 84 | 85 | // Span当发生日志记录时,格式如下: 86 | log: 87 | time: 2006-01-02 15:04:05 log.Fields: 88 | [ 89 | {key: function, value: LogKV} 90 | {key: error, value: "non-even keyValues len: 3"} 91 | ] 92 | time: 2006-01-02 15:04:08 log.Fields: 93 | [ 94 | {key: update, value: record not found} 95 | ] 96 | 97 | // 当span.raw.Logs列表长度大于等于tracer.Options.MaxLogsPerSpan(非零)时,这需要丢弃日志 98 | // 这里丢弃日志的做法第一次见到,感觉还不错: 99 | // 它是把一个列表中上半部分的日志反复覆盖写,这样的话,有个问题日志出现开头部分连续和结尾部分连续,中间断层。你可以通过问题发生的地方和结尾地方开始追踪,并在分布式日志管理系统中,根据trace的相关开始和结束日志,定位时间区域,和问题开始和结束的位置和原因等, 中间断层的日志在具体的日志系统中查找定位。 100 | func (s *spanImpl) appendLog(lr opentracing.LogRecord) { 101 | ... 102 | 103 | numOld:=(maxLogs -1 ) /2 104 | numNew = maxLogs - numOld 105 | s.raw.Logs[numOld+(s.numDroppedLogs%numNew)]=lr 106 | s.numDroppedLogs++ 107 | } 108 | 109 | // 该方法记录一个错误发生时,相关的调用栈具体日志错误位置和错误信息 110 | func (s *spanImpl) LogFields(fields ...log.Field) { 111 | ... 112 | } 113 | 114 | func (s *spanImpl) Finish() { 115 | s.FinishWithOptions(opentracing.FinishOptions{} 116 | } 117 | 118 | func (s *spanImpl)FinishWithOptions(opts opentracing.FinishOptions) { 119 | ... 120 | // 这里涉及到一个列表旋转的算法, 本节下面有旋转介绍和理解 121 | rotateLogBuffer(s.raw.Logs[numOld:], s.numDroppedLogs%numNew) 122 | // 做完之后,还要把span释放到sync.Pool临时对象池中复用 spanPool.Put(s) 123 | } 124 | ``` 125 | 126 | **乍一看,卧槽,这个旋转有毛线用,再仔细理解理解appendLog方法;会再来一句,这想法牛逼;** 127 | 128 | ```shell 129 | 为啥牛逼呢? 130 | 131 | 因为appendLog方法在span.RawSpan.Logs溢出后,以后都会产生日志丢弃行为,而且是从后半段丢弃和重写,这样后半段反复覆盖重写,注意一点,我们在LogRecord存储时,需要保证日志输出的时间有序性,但是后半段反复覆盖写,则会导致一种情况: 132 | 133 | 当(s.numDroppedLogs)/numNew>1时,则会出现后半段的前N个日志时间戳大于后面的日志时间戳,这个分界线的位置则是:s.numDroppedLogs%numNew,所以需要这个左边位置向左旋转N。 134 | 135 | 这样的话,后半段日志记录时间戳就是有序递增的,那么输出也就是有序的 136 | ``` 137 | 138 | **spanImpl中的LogEvent, LogEventWithPayload, Log需要废弃,因为标准中opentracing-go的LogData已废弃,只保留LogKVs和LogFields方法。** 139 | 140 | 141 | 对于数组/列表旋转算法,可以采用C++中的官方算法库[rotate left](http://www.cplusplus.com/reference/algorithm/rotate/),[《STL 源码分析》](https://blog.csdn.net/ww32zz/article/details/48995423)有助于这个算法的理解 142 | 143 | 这个算法是采用前向移动, 且每次移动[first, pos]大小固定数据单元 144 | 145 | ```shell 146 | 一维旋转,向(左|右)旋转N位的理解: 147 | 148 | 把一维数组分为两部分,则它当前由三部分构成:左半部分和右半部分; 149 | 150 | 记住一点:左半部分是整体、右半部分也是整体。 151 | 152 | 例如:字符串abcdefg ,向(左|右)旋转3位: 153 | 1. 向左旋转3位: 154 | 整体可以看成:A'C'。其中:A'表示abc, C'表示defg, 则左旋转结果为:C'A', 展开结果为:efgdabc 155 | 2. 向右旋转3位: 156 | 整体可以看成:A'C'。其中:A'表示abcd,C'表示efg,则右旋转结果为:C'A', 展开结果为:efgabcd 157 | ``` 158 | 159 | # 参考资料 160 | 161 | [basictracer-go](https://github.com/opentracing/basictracer-go) 162 | -------------------------------------------------------------------------------- /opentracing标准/OpenTracing APIs.md: -------------------------------------------------------------------------------- 1 | # OpenTracing APIs 2 | ## OpenTracing的多语言支持 3 | 目前官方OpenTracing API支持的平台有:[Go](https://github.com/opentracing/opentracing-go)、[Python](https://github.com/opentracing/opentracing-python)、[JS](https://github.com/opentracing/opentracing-javascript)、[Objective-C](https://github.com/opentracing/opentracing-objc)、[Java](https://github.com/opentracing/opentracing-java)和[C++](https://github.com/opentracing/opentracing-cpp)。PHP和Ruby正在开发中。 4 | 5 | ## Data Conventions 数据约定 6 | OpenTracing APIs的设计和规范,对追踪软件开发和探针软件开发都有通用指导意义;**追踪系统的开发者不必严格遵守指南,但是强烈推荐大家这么做。** 7 | 8 | ### Spans 9 | 1. Span命名 10 | 在上一章OpenTracing interface中提到的Span interface包含operation name、tags、logs和baggage。这些可以代表span中进行的工作类型,例如:RPC或者HTTP调用端点、执行SQL语句,一个进程、类库或者模块名称。使得这个span所附带的信息是很有价值的。 11 | 2. Span结构 12 | Span结构也是非常重要的,主要表示Span本身所带重要信息,以及Spans之间的关联关系。后文具体介绍 13 | 14 | #### Span Tag用例 15 | Span Tag所携带的信息,特定存在的常量Key,有Errors、HTTP、Sample等等。不同的底层会对这些信息做一些其他处理。例如:获取、删除或者清空Span Tag下的所有信息,这个使用时可能是用得到的,但是需要注意的是,这些额外的操作可能会对业务系统本身造成不良影响,所以请仔细斟酌这些额外的实现。 16 | 17 | #### Errors 18 | 表示一个span实例的错误状态,通过一个tag来标注。如果设置了一个span tag特定常量errors的值为true,这我们可以通过dashboard ui的tag查询功能,查询所有trace error列表,并通过tag的log日志,查看具体的web request错误信息,快速定位。并可以对一段时间的所有traces error列表统计,并报表输出。这个是非常重要的 19 | 20 | #### Component Identification 组件定义 21 | 我们十分推荐库或者模块为监控程序提供组件的定义,最终用户可能会用拥有一个由框架和第三方混合提供的监控。例如: 22 | 23 | 1. span tag的key:`component`,value:需要被监控的类库、模块或者包的基本名称;例如:`httplib`、`JDBC`、`mongoose`等; 24 | 2. span tag的key:`span.kind`。value:`client | server`。指定这个span代表一个客户端或者服务端。 25 | 26 | 27 | 如果引入的package都自带分布式跟踪系统监控标签,则监控粒度就可以很容易的植入进来。 28 | 29 | #### HTTP Server Tags 30 | 因为是http请求服务入口,所以这些都是在框架层做的事情。例如: 31 | 32 | 1. span tag的key:`http.url`;value:url地址;如:`http://www.google.com.hk` 33 | 2. span tag的key: `http.method`; value: `get|post|head`等 34 | 3. span tag的key:`http.status_code`; value: `200; 404; 503`等 35 | 36 | 37 | #### Peer Tags 38 | 这个用于rpc服务使用,描述远程请求过程中,请求调用的方向。(客户端记录下行访问,服务端记录上行访问)。 39 | 1. `peer.hostname`目标主机名,类型:string 40 | 2. `peer.ipv4`目标IPv4地址,类型:string 41 | 3. `peer.ipv6`目标IPv6地址,类型:string 42 | 4. `peer.port`目标端口,类型:int 43 | 5. `peer.service`目标服务名称,string 44 | 45 | peer翻译:对等,比如:client设置tag的peer{hostname, ipv4, service , port ...},表示server的具体信息;server设置tag的peer{hostname...}, 表示client的具体信息。 46 | 47 | #### Sampling采样 48 | OpenTracing API不强调采样的概念。有些情况下,当业务量非常大时,如果业务使用了分布式追踪系统功能,则可能会对业务系统的性能造成很大影响。如果追踪系统产品本身实现了采样的特性,可以支持多规则采样, 49 | 50 | 1. 流量规则; 51 | 2. web request数量; 52 | 3. 预期的指定trace(特定的订单ID、支付ID、用户ID,预先植入,追踪特定请求trace); 53 | 54 | 对于以上三种都是非常有价值的采样规则;span tag中有个采样key:`sampling.priority`, value值为整数类型; 55 | 56 | 1. value>0;追踪系统尽可能保留这条trace; 57 | 2. value=0;追踪系统不保存这条调用链; 58 | 59 | 如果此tag没有提供,追踪系统使用自己的默认采样规则; 60 | 61 | ### Logs 62 | Logs日志是轻量级trace日志,与分布式日志系统无关;它是记录span事件日志,例如:当span执行单元发生错误时,错误日志存储在span logs中event:`error`, message: 具体的错误信息; 63 | 64 | ### Inject和Extract 65 | 在[《OpenTracing——相关概念术语》](https://gocn.vip/article/856)一文中提到,spans之间的trace信息携带,如果是是跨进程,需要把数据通过`Baggage`SpanContext携带,主要用于: 66 | 67 | 1. rpc服务; 68 | 2. 发布-订阅机制; 69 | 3. 通用消息队列; 70 | 4. HTTP请求调用; 71 | 5. UDP传输和其他传输方式; 72 | 73 | 74 | 把携带信息通过OpenTracing APIs中的Inject和Extract方法,在跨进程追踪时进行写入和读取。 75 | 76 | `Inject`和`Extract`方式实现的设计,必须遵循以下要求: 77 | 78 | 1. 必须不需要使用OpenTracing使用中的特定代码; 79 | 2. 必须不需要针对每一种已知的跨进程通讯机制都处理; 80 | 3. 这套传播机制是最利于扩展的; 81 | 82 | #### 基本方法:Inject、Extract和Carriers 83 | 追踪过程中的SpanContext可以被Inject方法注入到Carriers中,这个Carriers可以是一个`接口`或者一个`数据载体`。 并在跨进程间传输,OpenTracing标准包含两种必须的Carriers格式,自定义的Carriers也是可以的。同时在这个Span的ChildOf中通过Extract方法抽取Carriers信息,得到一个SpanContext。这个SpanContext表示被Inject到Carriers中的信息。 84 | 85 | Inject伪代码: 86 | 87 | ```shell 88 | span_context = ... 89 | 90 | carrier = {} // 初始化数据载体 91 | // 把span_context存放到HTTP_HEADERS格式的数据载体中 92 | tracer.inject(span_context, opentracing.Format.HTTP_HEADERS, carrier) 93 | 94 | // 并把carrier载体数据写入到跨进程调用client端的网络传输中。 95 | for key, value in carrier: 96 | outbount_request.header[key] = value 97 | ``` 98 | 99 | Extrace伪代码: 100 | 101 | ```shell 102 | inbound_request = ... 103 | 104 | 105 | // 把跨进程服务端接收到的传输数据,按照指定的格式和字段,抽取到span_context中 106 | carrrier = inbound_request.headers 107 | span_context = tracer.extract(opentracing.Format.HTTP_HEADERS, carrier) 108 | 109 | // 并把tracer串联起来,创建一个新的span 110 | span = tracer.start_span("...", child_of=span_context) 111 | ``` 112 | 113 | Carrier格式: 114 | 115 | 所有的Carriers都有自己的格式。一般格式都以常量表达,例如:Binary, TextMap, HTTPHeaders常量或者字符串指定;另一些,则通过Carriers的静态类型指定。 116 | 117 | 自定义的Carriers格式: 118 | 119 | 分布式跟踪系统底层的实现可以采用自定义的Carriers格式,进行Inject和Extract操作。例如: 120 | **ArrrPC private RPC SubSystem**,我们希望增加OpenTracing的数据在RPC请求过程中传输。伪代码: 121 | 122 | ```shell 123 | span_context = ... 124 | outbound_request = ... 125 | 126 | try: 127 | // 尝试使用自定义的carrier格式,把span_context写入到数据载体中 128 | // 如果失败,直接使用标准的HTTP_HEADERS格式封装 129 | tracer.inject(span_context, arrrpc.ARRRPC_OT_CARRIER, carrier) 130 | 131 | except opentracing.UnsupportedFormatException: 132 | 133 | carrier = {} 134 | tracer.inject(span_context, opentracing.Format.HTTP_HEADERS, carrier) 135 | 136 | for key, value in carrier: 137 | outbound_request.header[key] = escape(value) 138 | ``` 139 | 140 | -------------------------------------------------------------------------------- /appdash/tracer-and-span.md: -------------------------------------------------------------------------------- 1 | Appdash是基于basictracer-go扩展实现的,在此基础上丰富了的Collector和Storage。以及增加了dashboard UI 一些简单的查询统计功能。 2 | 3 | Appdash中的Trace和Span struct与basictracer-go中的traceImpl和spanImpl struct的关系,后面再看。 **::TODO** 4 | 5 | 6 | # trace 7 | 8 | basictracer-go的扩展实现中,虽然是形成了完整的调用链,但是如果想要通过dashboard去进行绘图这条调用链, 则只能通过SpanRecorder的[]RawSpan,去通过算法找到一条完整的链路,可能这个操作还非常耗时。 9 | 10 | 所以Appdash针对Dashboard的业务场景,定义了基于Trace的显式链路结构: 11 | 12 | ```shell 13 | // Appdash的Trace结构设计:采用了广度遍历思想,层次树存储。 14 | // ChildOf和FollowsFrom理解:Sub列表为FollowsFrom关系,Trace和Sub列表为ChildOf关系 15 | type Trace struct { 16 | Span // 根部span 17 | Sub []*Trace // child node 18 | } 19 | 20 | type Span struct { 21 | ID SpanID // SpanID= {TraceID, SpanID, ParentID} 22 | // Annotations携带了Span的所有信息; 23 | // 包括Tags,Baggage,Logs,OperationName等; 24 | 25 | // 其中span.Log进行分类,Appdash自身已定义了:五种Log 26 | // SpanNameEvent, logEvent, msgEvent, timespanEvent和Timespan 27 | // 类型分别是:name, log, msg, timespan和TimeSpan 28 | // 其中后两者的不同后面再理解 ::TODO 29 | // 对于Event也可以进行自定义实现, 但是Annotations不是直接的分类,需要对Annotations与Event进行数据转换存储才行 30 | Annotations // []{key:values} 31 | } 32 | 33 | // 对于span.Logs的类型定义,在Appdash中可以扩展,只需要实现Event interface, 并通过RegisterEvent方法注册自定义span logs事件 34 | // Schema方法的返回值,作为Log的key类型值 35 | // 需要说明的一点是:内部的五种类型Log,具有一定的标识前缀: 36 | // SchemaPrefix = "_schema:" 37 | type Event interface { 38 | Schema() string 39 | } 40 | 41 | func (t *Trace) String() string { 42 | ... // 序列化Trace数据 43 | } 44 | 45 | func (t *Trace) TreeString() string { 46 | ... // 打印树数据,以树结构的形式输出。 47 | } 48 | 49 | // 从根节点开始遍历递归查找SpanID,返回Trace 50 | // 可能大家有个疑问:为何返回Trace,而不是Span? 51 | // 因为Trace类型是层次树结构,所以每个节点都可以看成树节点,等同看的话,这就是Trace。实际上是Span节点,但是我们通过任意的子节点,可以获取到TraceID等调用链的全局信息。 52 | func (t *Trace) FindSpan(spanID ID) *Trace { 53 | ... 54 | } 55 | 56 | func (t *Trace) TimespanEvent() (TimespanEvent, error) { 57 | ... // 它是在当前调用链的span节点上,找到span logs中五类的TimespanEvent事件并返回 58 | // 这个过程在下节会详细介绍 59 | } 60 | ``` 61 | 62 | **在Appdash span logs的分类与Span的Annotations的数据转换与存储比较复杂,我会单独一节讲解span log内部五类,并对数据转换与存储进行详细介绍。** 63 | 64 | # span 65 | 66 | span作为调用链树中的子节点,它的存储结构如下(Span携带的annotations下节再详细介绍): 67 | 68 | ```shell 69 | type Span struct { 70 | // SpanID在序列化时String,变成了"TraceID/SpanID/ParentID" 71 | ID SpanID 72 | 73 | Annotations 74 | } 75 | 76 | // 对span进行序列化,信息包括:TraceID/SpanID/ParentID,以及span的所有携带信息:Tags、Baggages、Sampled、OperationName和Logs等 77 | func (s *Span) String() string { 78 | ... // 通过json序列化 79 | } 80 | 81 | // 返回Span的OperationName,它是通过Annotations的key为"Name"获取。 82 | func (s *Span) Name() string { 83 | ... 84 | } 85 | 86 | // SpanID管辖Span本身的信息,与basictracer-go的设计雷同。表现在: 87 | // basictracer-go中RawSpan包括了SpanContext,它并没有把SpanID,TraceID、Sampled信息放在Baggage中,而是单独拎出来。 88 | // 这里的Appdash Span设计把TraceID、SpanID和ParentSpanID没有放在Annotations信息中获取,也是单独拎出来,更加清晰直观,这个信息是必须的,非携带信息,而是Span自身信息。 89 | type SpanID struct { 90 | Trace ID 91 | Span ID 92 | Parent ID 93 | } 94 | 95 | func (id SpanID) string { 96 | ... // 序列化SpanID为"TraceID/SpanID/ParentSpanID" 97 | // 后面提到的tracesByIDSpan排序,则可以做到全局Span排序,且这个排序结果是有层次的 98 | // 做到TraceID有序,SpanID有序,且都是连续的,例如: 99 | // TraceID_01/SpanID_01/0 100 | // TraceID_01/SpanID_02/SpanID_01 101 | // TraceID_01/SpanID_03/SpanID_02 102 | // TraceID_02/SpanID_01/0 103 | // TraceID_02/SpanID_02/SpanID_01 104 | // ... 105 | // 我们可以看到前面三个为一个整体Trace调用链,后面两个为一个整体调用链,且全局有序,trace中的Span有序,这个设计很棒! 106 | } 107 | 108 | // 这个猜测是用来序列化span信息的,包括携带信息Annotations 109 | for (id SpanID) Format(s string, args ...interface{}) string{ 110 | args = append([]interface{}{id.String()}, args...) 111 | return fmt.Sprintf(s, args...) 112 | } 113 | 114 | func (id SpanID) IsRoot() bool { 115 | return id.Parent == 0 // 校验该span是否为根节点 116 | } 117 | 118 | // 封装SpanID,通过protobuffer协议网络传输数据流 119 | func (id SpanID) wire() *wire.CollectPacket_SpanID { 120 | return &wire.CollectPacket_SpanID { 121 | Trace: (*uint64)(&id.Trace), 122 | Span: (*uint64)(&id.Span), 123 | Parent: (*uint64)(&id.Parent), 124 | } 125 | } 126 | 127 | // 把protobuffer协议网络流转换成SpanID数据 128 | func spanIDFromWire(w *wire.CollectPacket_SpanID) SpanID { 129 | return SpanID{ 130 | Trace: ID(*w.Trace), 131 | Span: ID(*w.Span), 132 | Parent: ID(*w.Parent), 133 | } 134 | } 135 | 136 | // 生成根节点的SpanID信息 137 | func NewRootSpanID() SpanID { 138 | return SpanID{ 139 | Trace: generateID(), 140 | Span: generateID(), 141 | } 142 | } 143 | 144 | // 生成子节点的SpanID信息 145 | func NewSpanID(parent SpanID) SpanID { 146 | return SpanID { 147 | Trace: parent.Trace, 148 | Span: generateID(), 149 | Parent: parent.Span, 150 | } 151 | } 152 | ``` 153 | 154 | 155 | ```shell 156 | // 如果这个树类型结构是这样的,大家怎么看呢? 157 | // ::TODO 后面再理解 158 | type Span struct { 159 | ParentID SpanID 160 | SpanID SpanID 161 | TraceID TraceID 162 | Annotations []{key-values} 163 | Sub []*Span 164 | } 165 | ``` 166 | 167 | 在trace代码中有个sort.Interface的接口实现类型, 内部是快排算法。 168 | 169 | ```shell 170 | // 排序算法:只需要满足三点,即可实现 171 | // 1. 比较;2. 交换;3. 参与排序的元素数量。 172 | type Interface interface { 173 | Len() int 174 | Less(i, j int) bool 175 | Swap(i, j int) 176 | } 177 | 178 | type tracesByIDSpan []*Trace 179 | // 这个类型是对Span列表进行ID排序,从小到大排列。这个意义后面再看,因为Trace树层次本身就是按时间排序的,看看这个ID uint64类型值的生成算法. 180 | // ::TODO 181 | ``` 182 | -------------------------------------------------------------------------------- /jaeger/TChannel/熔断器.md: -------------------------------------------------------------------------------- 1 | # 熔断器 2 | 3 | 本节概述Hyperbahn的[熔断器机制](https://martinfowler.com/bliki/CircuitBreaker.html)设计: 4 | 5 | 1. 在一个复杂的分布式系统中停止级联故障; 6 | 2. 快速失败和快速恢复; 7 | 3. 在可能的情况下,回退版本并优雅地降级; 8 | 4. 实现近实时监控、警报和操作控制 9 | 10 | 这个术语和想法是从JVM resiliancy库、Netflix's Hystrix和[《Release it! by M.Nygard》](http://www.amazon.com/gp/product/0978739213) 11 | 12 | ## 问题 13 | 14 | 复杂的分布式系统应用的依赖非常多,在某些方面每个依赖都可能导致失败。如果这个host应用没有从外部失败中隔离,那么很可能它本身也会面临down掉的风险 15 | 16 | 例如,一个应用依赖30个服务,如果每个服务的可用性为99.99%, 这你可以计算出: 17 | 18 | ```shell 19 | 99.99*...*99.99{30} = 99.7%的可用性,则1billion个请求,会有3,000,000个失败。每个月有两个小时多的服务不可用。 20 | ``` 21 | 22 | 实现情况可能更糟糕 23 | 24 | 甚至当所有的依赖执行得非常好时,**如果你没有为整个系统设计弹性**,这个0.01%的宕机概率也会对多个服务总体影响,也相当于每月停机一小时。 25 | 26 | ## 解决方案 27 | 28 | [熔断器设计模式](https://martinfowler.com/bliki/CircuitBreaker.html)可以在分布式系统中阻止系统失败和传播的不可以接受的延迟。它是这样做的: 29 | 30 | 1. 定义服务circuits。通过划分一个服务为多个逻辑单元,可以停止部分服务,而其他部分继续运行。 31 | 2. 跟踪对服务进行请求数据统计(包括:success, failure, average latency等) 32 | 3. 当数据统计通过Healthy Criteria的检查时,标记circuits为健康。当一个请求遇到不健康的Circuits时,直接熔断返回报错失败,则大量的请求到来时,也不会耗费大量的资源。`Circuits that are Unhealthy deny traffic - this affords failing systems "room to breathe" so that they are not bombarded by doomed requests.` 33 | 4. 当数据统计通过了Healthy Criteria检查时,标记Cricuits为健康。这涉及到在将Circuit标记为Healthy之前,必须成功运行X次的状况探测。 34 | 5. 允许对所有Circuits的监控,整个分布式系统的天眼dashboard,并可以手动调整Circuit状态和参数。 35 | 36 | ### 术语 37 | 38 | 与熔断器相关的术语使得问题更难理解。为了解决这个问题, 39 | 40 | | 术语 | 定义 | 同义词 | 41 | | --- | --- | --- | 42 | | Caller | 一个服务通过Circuit调用另一个服务 | source | 43 | | Circuit | 流量根据健康或者不健康状态,进行限制流量的单元 | Breaker,Bulkhead | 44 | | Healthy | 一个流量可以经常流过的Circuit单元 | Closed Circuit、Reset Circuit | 45 | | Healthy Criteria | 一个检查Circuit是健康的标准方法 | | 46 | | Probe | 当一个Circuit处于不健康状态时,会有一个定期的request去进行健康检查 | Health Check | 47 | | Service | 一个正在使用熔断器设计模式的API/Application | System、API、Application | 48 | | Unhealthy | 当流量被拒绝时的Circuit状态 | Open Circuit,Half-open Circuit | 49 | | Unhealthy Criteria | 一个检查Circuit是不健康的标准方法 | | 50 | 51 | 还有一个些词汇,像:“trip the Circuit”, “break the Circuit”和“reset the Circuit”都是明确地避免使用。替代的是更明显的含义,例如:"Healthy state change"和“Unhealthy state change” 52 | 53 | ## 实现 54 | 55 | 下面的图说明了Hyperbahn的熔断器实现: 56 | 57 | ![Hyperbahn Circuit Breaking](https://gewuwei.oss-cn-shanghai.aliyuncs.com/tracelearning/circuit_breaking.png) 58 | 59 | 1. 每个服务都需要在Hyperbahn上进行服务注册,Hyperbahn维护所有的Circuit集合,并负责拒绝不健康的Circuit流量访问; 60 | 2. 这个Circuits的健康状态可以选择在服务的出口之间传播,以提高Circuit的灵敏度;(::TODO 不太理解); 61 | 3. 该服务者为其拥有的每个Caller维护一个Circuit,它所暴露的每个endpoint,名称格式为:`CALLER->SERVICE::ENDPOINT`。在上图中,这意味着即使`CatsForCharity->Petshop::listCats` Ciruits是不健康的,它会拒绝所有过来的流量。`PetBreeders->Petshop::listCats` Circuit是健康的,会继续允许流量通过该出口。 62 | 4. 为了确定他们是否是健康或者不健康的,Circuits会定期的检测这些请求统计数据。健康的Circuits运行流量通过,然而不健康的Circuit拒绝流量通过,并定期对不健康的Circuit做定期探测检查。 63 | 64 | 65 | 从这幅图,我们可以看到每个Circuit包含的内容,它主要是由Source,Service,Endpoint和State四部分组成,它们分别代表:Client,Server,访问的方法和这个访问链路的健康状况。 66 | 67 | 至于EGRESS各个出口是怎样维护Circuit的,我这里自行理解下,后面再看::TODO 68 | 69 | ```shell 70 | 在上面这幅图中,我们看到PetShop服务在Hyperbahn存在5个出口,每个出口都映射了右边的一张表,这表有两个Circuit。 71 | 72 | 当然这是最简单的,如果PetShop提供了N个可以访问的服务路由,同时有M个外部服务会使用Petshop服务,则每个入口需要维护得一张表的Circuits最多为 M * N,多个出口的原因在于防止出口拥塞,因为多个出口维护的信息相同,那么就存在数据一致性问题,则通过gossip协议来实现。 73 | 74 | 根据这个理解,后面再看Affinity文章 75 | ``` 76 | 77 | ### 不健康标准 78 | 79 | Healthy Circuit会使用以下标准逐步检查其统计数据以确定Circuit是否变得不健康了。 80 | 81 | | Parameter | How it's used | Example | 82 | | --- | --- | --- | --- | 83 | | `Window` | 从中抽样请求统计数据的时间段 | 1秒 | 84 | | `Threshold` | 在这个窗口时间内,请求失败数量最低阈值 | 50% | 85 | | `MinRequests` | 在窗口内最小的请求数 `Minimum amount of request before window can be checked` | 10 | 86 | | `NonCircuitingExceptions` | `Valid exceptions that do not contribute to circuit health` | `tchannel.bad-request`, `tchannel.cancelled` | 87 | 88 | ### 健康标准 89 | 90 | Unhealthy Circuit会使用一下标准追捕检查其统计数据已确定Circuit是否恢复健康了。 91 | 92 | | Parameter | How it's used | Example | 93 | | --- | --- | --- | 94 | | `Windows` | 健康检查必须通过的窗口数量 | 5 | 95 | | `ProbeRate` | 健康检查频率 | 每秒1个 | 96 | | `Threshold` | 成功率 | 100% | 97 | 98 | 99 | ### 监控 100 | 101 | 1. 如何近乎实时地显示所有Circuits统计数据和状态; 102 | 2. 如果允许手动更改Circuit参数和状态; 103 | 3. 在Circuit状态变化时,如何警告; 104 | 4. 随着时间变化如何影响Circuit参数。 105 | 106 | 下面是从Hystrix's Dashboard找到的灵感: 107 | 108 | ![Hystrix's Dashboard](https://gewuwei.oss-cn-shanghai.aliyuncs.com/tracelearning/hystrix_dashboard.png) 109 | 110 | ### 故障 111 | 112 | 下表使用了实现图中显示的Service,并详细说明了Hyperbahn对常见故障情况的反应: 113 | 114 | | 故障 | 结果 | 115 | | --- | --- | 116 | | server的硬件故障 | 在该server上的服务会失去在Hyperbahn上所有打开的连接,同时将会停止接收流量 | 117 | | 由于PetShop Service的变化,导致外部的所有callers调用一些或者所有endpoints失败 | 过来的流量因为调用失败会导致`caller -> PetShop Service::endpoint`变得不健康。Circuit警告会使得工程师关注一些失败的调用,对于这些失败的endpoints,流量会被阻止直到这个PetShop service被修复 | 118 | | 由于PetShop Service的变化,外部的所有callers调用PetShop::listCats发生不可接受的延迟,导致失败 | 因为超时被看做为失败,这个结果与上面类似。Callers提供了TTL,告诉所有依赖的服务它们有多长的处理时间响应。如果这个PetShop Service超出预期的依赖性,这会导致这些Circuits会变得不健康 | 119 | | 更改CatsForCharity调用Petshop service的方式,导致调用`PetShop::listCats`失败 | 当调用失败出现的次数足够时,`CatsForCharty->Petshop::listCats` Circuit会变得不健康。Circuit警告会促使工程师知道系统出了一些问题,来之CatsForCharity的流量都会被阻止调用`Petshop::listCats`endpoint(处理正常的健康检查),直到CatsForCharity服务被修复 | 120 | | 更改CatsForCharity调用PetShop服务的方式,导致调用`PetShop::listCats`不可接受的延迟 | 当超时出现次数足够多时,`CatsForCharity->PetShop::listCats` Circuit会变得不健康。Circuit警告会促使工程师知道系统出了一些问题....| 121 | | 间歇性的网络导致调用Petshop service变得可不接受的延迟 | 进来的调用因为超时最终导致`Caller->PetShop::latentMethod` Circuits变得不健康。Circuit警告... | 122 | 123 | ### 挑战 124 | 125 | 1. 调整Circuit参数是非常困难的。为了达到所期望的行为,不同服务经常要求不同的Circuit参数。通常这些参数是由服务的开发人员进行调节的。自动化调节Circuit参数则可能更加困难; 126 | 2. `Very granular Circuits (a Circuit for every CALLER->SERVICE::ENDPOINT combination) means lots of Circuits. Controlling these manually when needed will not be trivial. Ability to control many circuits by Service, for example to disable all of PetShops Circuits, or all Circuits in general, will probably be needed.` 127 | 3. 如果在ENGRESS节点之间共享Circuit状态,则会造成大量的流量拥塞,同时使得问题变得更加困难; 128 | 4. 对于整个系统查看天眼Circuit统计数据,意味着可以查看每个Egress节点的Circuit。 129 | 130 | ## 参考资料 131 | 132 | [熔断器设计模式](http://www.cnblogs.com/yangecnu/p/Introduce-Circuit-Breaker-Pattern.html) 133 | 134 | 135 | -------------------------------------------------------------------------------- /basictracer-go/event-propagation.md: -------------------------------------------------------------------------------- 1 | # event 2 | 3 | 这个event是有关span操作时产生的事件,这些事件统称为:`SpanEvent`,相关事件数据存储格式如下: 4 | 5 | ```shell 6 | // span创建时产生的事件 7 | type EventCreate struct {OperationName string} 8 | 9 | // 存储tag操作时的事件 10 | type EventTag struct{ 11 | Key string 12 | Value interface{} 13 | } 14 | 15 | // 存储baggage时的事件 16 | type EventBaggage struct { 17 | Kye, Value string 18 | } 19 | 20 | // 存储日志时的事件 21 | type EventLogFields opentracing.LogRecord 22 | 23 | // span Finish时的事件 24 | type EventFinish RawSpan 25 | 26 | // 针对这些事件数据类型,有一些span操作事件的方法,如下所示: 27 | func (s *spanImpl) onCreate(opName string) { 28 | if s.event !=nil { 29 | s.event(EventCreate{OperationName: opName}) 30 | } 31 | } 32 | 33 | func (s *spanImpl) onTag(key string, value interface{}){ 34 | if s.event !=nil { 35 | s.event(EventTag{Key: key, Value: value}) 36 | } 37 | } 38 | 39 | func (s *spanImpl) onLogFields(lr opentracing.LogRecord){ 40 | if s.event !=nil { 41 | s.event(EventLogFields(lr)) 42 | } 43 | } 44 | 45 | func (s *spanImpl) onBaggage(key, value string) { 46 | if s.event !=nil { 47 | s.event(EventBaggage{Key: key, Value: value}) 48 | } 49 | } 50 | 51 | func (s *spanImpl) onFinish(sp RawSpan) { 52 | if s.event!=nil { 53 | s.event(EventFinish(sp)) 54 | } 55 | } 56 | // 以上这些都是针对span发生变化时的相关事件,这些可以记录,也可以不记录,取决于span.event是否被关注。 57 | ``` 58 | 59 | # propagation 60 | 61 | propagation主要是IPC/RPC跨进程数据传输。它包括上下文信息携带的数据存储格式,以及网络传输数据与本地数据格式转换 62 | 63 | 我们在[《basictracer源码阅读——TracerImpl》](https://gocn.vip/article/868)的SpanImpl部分说过三种Baggage携带格式,包括TextMapPropagator、BinaryPropagator和AccessPropagator。 64 | 65 | **这三种数据类型存在两种行为:Inject和Extract。用来进行SpanContext和Baggages之间的数据转换** 66 | 67 | ## textMapPropagator 68 | 69 | 70 | ```shell 71 | // 父级:Client或者Producer 72 | func (p *textMapPropagator) Inject(spanContext opentracing.SpanContext, 73 | opaqueCarrier interface{}) error{ 74 | sc, ok := spanContext.(SpanContext) // 获取span本身相关信息 75 | // 这个无需指明或者猜测是否为HTTPHeader或者TextMap,因为这是opentracing-go底层透明实现Set和ForeachKey两个方法 76 | carrier, ok := opaqueCarrier.(opentracing.TextMapWriter) 77 | 78 | // 把SpanContext需要上下文携带的信息写入到网络传输数据中 79 | carrier.Set(fieldNameTraceID, strconv.FormatUint(sc.TraceID, 16)) 80 | carrier.Set(fieldNameSpanID, strconv.FormatUint(sc.SpanID, 16)) 81 | carrier.Set(fieldNameSampled, strconv.FormatBool(sc.Sampled)) 82 | 83 | // baggage信息, 在SpanImpl章节说过,为了凸显TraceID、SpanID和Sampled三个变量的价值,没有放入到Baggage中。 84 | for k, v := range sc.Baggage{ 85 | carrier.Set(prefixBaggage+k, v) 86 | } 87 | return nil 88 | } 89 | 90 | // 子级:Server或者Consumer 91 | func (p *textMapPropagator) Extract(opaqueCarrier interface{}) (opentracing.SpanContext, error) { 92 | // opaqueCarrier数据格式为HTTPHeader或者TextMap,这不用上层关心,因为这两者已经实现了ForeachKey方法 93 | carrier, ok := opaqueCarrier.(opentracing.TextMapReader) 94 | 95 | carrier.ForeachKey(func(k, v string) error{ 96 | ... // 根据Inject方法,解出对应的SpanContext相关信息 97 | // 包括TraceID、SpanID、Sampled和Baggage信息 98 | }) 99 | } 100 | ``` 101 | 102 | ## binaryPropagator 103 | 104 | 它是网络传输比特流,通过io.Reader和io.Writer进行流读写操作 105 | 106 | ```shell 107 | // 父级:Client或者Producer 108 | func (p *binaryPropagator) Inject(spanContext opentracing.SpanContext, 109 | opaqueCarrier interface{}) error{ 110 | // 获取span的SpanContext 111 | sc, ok := spanContext.(SpanContext) 112 | // 把SpanContext通过io.Writer, 采用protobuffer协议, 113 | // 把它转换为网络流中数据opaqueCarrier 114 | carrier, ok := opaqueCarrier.(io.Writer) 115 | 116 | // 然后把数据流通过pb协议和大端模式转化为网络流carrier 117 | state:=wire.TracerState{} 118 | state.TraceId = sc.TraceID 119 | state.SpanId = sc.SpanID 120 | state.Sampled = sc.Sampled 121 | state.BaggateItems = sc.Baggage // pb 支持map数据格式 122 | b, err :=proto.Marshal(&state) 123 | 124 | length:=uint32(len(b)) 125 | binary.Write(carrier, binary.BigEndian, &length) 126 | carrier.Write(b) 127 | // 这里要注意的是,网络数据传输的格式是[length(b)+b]。读取的时候,前uint32八个字节为数据长度,偏移8字节后,计算uint32存储值,则是实际数据大小 128 | // 在linux内存管理基本上都是这种数据传输形式,例如: 存储struct{...}格式的数据流为:struct本身大小+struct中数据大小+struct数据体 129 | } 130 | 131 | // 子级:Server或者Consumer 132 | func (p *binaryPropagator) Extract(opaqueCarrier interface{}) (opentracing.SpanContext, error) { 133 | // 获取binary网络流 134 | carrier, ok := opaqueCarrier.(io.Reader) 135 | binary.Read(carrier, binary.BigEndian, &length) //var length uint32 136 | buf := make([]byte, length) 137 | // 以上我们可以看到,Inject注入到网络流的数据大小,Extract解析时也是一样的大小解析 138 | carrier.Read(buf) 139 | // buf是pb协议存储, 并通过pb协议解析 140 | ctx:=wire.TracerState{} 141 | proto.Unmarshal(buf, &ctx) 142 | return SpanContext{ 143 | TraceID: ctx.TraceId, 144 | SpanID: ctx.SpanId, 145 | Sampled: ctx.Sampled, 146 | Baggage: ctx.BaggageItems, 147 | } 148 | } 149 | ``` 150 | 151 | ## accessPropagator 152 | 153 | basictracer开放出了一个可供厂商自定义SpanContext与网络数据传输的存储协议。 154 | 155 | ```shell 156 | type DelegatingCarrier interface{ 157 | // 因为要遵循basicTracer的SpanContext,所以提供了SetState方法,读写traceID、SpanID和Sampled 158 | SetState(traceID, spanID uint64, sampled bool) 159 | State() (traceID, spanID uint64, sampled bool) 160 | // 再就是需要对SpanContext和Carrier进行数据转换 161 | // 这两个方法类似于TextMapReader和TextMapWriter接口 162 | SetBaggageItem(key, value string) 163 | GetBaggage(func(key, value string)) 164 | } 165 | 166 | // 把SpanContext转换为网络可传输的Carrier,依赖于具体实现 167 | func (p *accessorPropagator) Inject(spanContext opentracing.SpanContext, 168 | carrier interface{}) error{ 169 | // 转换SpanContext到Carrier中 170 | dc, ok := carrier.(DelegatingCarrier) 171 | sc, ok := spanContext.(SpanContext) 172 | dc.SetState... 173 | for k, v:= range sc.Baggage{ 174 | dc.SetBaggageItem(k, v) 175 | } 176 | } 177 | 178 | // 把网络传输数据Carrier转换为SpanContext 179 | func ( p *accessorPropagator) Extract(carrier interface{}) (opentracing.SpanContext, error) { 180 | // 网络数据校验是否为DeletingCarrier 181 | dc, ok := carrier.(DelegatingCarrier) 182 | // 获取Span相关信息 183 | traceID, spanID, sampled:= dc.State() 184 | sc := SpanContext{ 185 | TraceID: traceID, 186 | SpanID: spanID, 187 | Sampled: sampled, 188 | Baggage: nil, 189 | } 190 | // 遍历获取baggage 191 | dc.GetBaggage(func(k, v string) { 192 | ... 193 | sc.Baggage[k] = v 194 | }) 195 | return sc, nil 196 | } 197 | ``` 198 | -------------------------------------------------------------------------------- /jaeger/tchannel-go/frame.md: -------------------------------------------------------------------------------- 1 | # Frame 2 | 3 | 首先需要对tchannel协议规范非常了解,可以看前面翻译的规范。下面是一个简表,通过这个我们在看Frame源码时容易理解 4 | 5 | | Position | Contents| 6 | |---|---| 7 | |0-7 |size:2 type:1 reserved:1 id:4| 8 | |8-15 |reserved:8| 9 | |16+ |payload - based on type| 10 | 11 | 12 | 我们看到消息帧由header和payload两部分构成,header是固定的16字节,payload为header首2字节大小-16得到结果 13 | 14 | 固定长度header为16字节,帧最大长度默认无限制 15 | 16 | ```shell 17 | const ( 18 | MaxFrameSize = math.MaxUint16 19 | 20 | FrameHeaderSize = 16 21 | 22 | MaxFramePayloadSize = MaxFrameSize - FrameHeaderSize 23 | ) 24 | ``` 25 | 26 | ## Frame Header 27 | 28 | ```shell 29 | type FrameHeader struct { 30 | // 协议帧总长度 31 | size uint16 32 | 33 | // 消息类型共9种, init req, init res, call req ...... 34 | messageType messageType 35 | 36 | // 1字节保留 37 | reserved1 byte 38 | 39 | // 4字节的message id 40 | ID uint32 41 | 42 | 8字节的保留 43 | reserved [8]byte 44 | } 45 | 46 | // payload大小加上头部的16字节,为frame的总大小 47 | func (fn *FrameHeader) SetPayloadSize(size uint16) { 48 | fh.size = size + FrameHeaderSize 49 | } 50 | 51 | // 获取payload大小 52 | func (fn *FrameHeader) PayloadSize() uint16 { 53 | return fn.size - FrameHeaderSize 54 | } 55 | 56 | // 返回消息帧的总大小 57 | func (fh FrameHeader) FrameSize() uint16 { 58 | return fh.size 59 | } 60 | 61 | // 提供Format操作, 返回消息类型和消息ID 62 | func (fh *FrameHeader) String() string { 63 | return fmt.Sprintf("%v[%d]", fh.messageType, fh.ID) 64 | } 65 | 66 | // json序列化消息帧 67 | func (fh *FrameHeader) MarshalJSON() ({ 68 | s := struct{ 69 | ID uint32 `json:"id"` 70 | MsgType messageType `json:"msgType"` 71 | Size uint16 `json:"size"` 72 | }{ 73 | fh.Id, 74 | fh.message.Type, 75 | fh.size, 76 | } 77 | 78 | return json.Marshal(s) 79 | } 80 | 81 | // 我们又碰到了typed.ReadBuffer,在上上节我们详细介绍过, 通过ReadBuffer读取remaining数据 82 | // 83 | // 这里的ReadBuffer中的buffer是完整的协议消息帧 84 | func (fh *FrameHeader) read(r *typed.ReadBuffer) error { 85 | // 读取header中的首2字节 86 | fh.size = r.ReadUint16() 87 | // 读取1字节的消息类型 88 | fh.messageType = messageType(r.ReadSingleByte()) 89 | // 读取1字节的保留位 90 | fh.reserve1 = r.ReadSingleByte() 91 | // 读取4字节的消息ID 92 | fh.ID = r.ReadUint32() 93 | // 读取8字节的保留位 94 | r.ReadBytes(len(fh.reserved)) 95 | 96 | // 剩余的就是payload部分 97 | return r.Err() 98 | } 99 | 100 | // 与上面一样,碰到了typed.WriteBuffer,通过WriteBuffer写入buffer 101 | func (fh *FrameHeader) write(w *typed.WriteBuffer) error { 102 | // 写入2字节的帧总大小 103 | w.WriteUint16(fh.size) 104 | // 写入1字节的消息类型 105 | w.WriteSingleByte(byte(fh.messageType)) 106 | // 写入1字节的保留 107 | w.WriteSingleByte(fh.reserved1) 108 | // 写入4字节的消息ID 109 | w.WriteUint32(fh.ID) 110 | // 写入8字节的保留位 111 | w.WriteBytes(fh.reserved[:]) 112 | 113 | return w.Err() 114 | } 115 | ``` 116 | 117 | ## Frame 118 | 119 | 下面介绍整个消息帧的读写 120 | 121 | ```shell 122 | // 消息帧结构 123 | type Frame struct { 124 | // buffer由header和payload两部分构成 125 | buffer []byte 126 | // header存储空间 127 | headerBuffer []byte 128 | 129 | // header 130 | Header FrameHeader 131 | 132 | // payload存储空间 133 | Payload []byte 134 | } 135 | 136 | // 创建Frame 137 | func NewFrame(payloadCapacity int) *Frame { 138 | f := &Frame{} 139 | // 创建消息帧的内存空间 140 | f.buffer = make([]byte, FrameHeaderSize + payloadCapacity) 141 | // payload从16字节开始所属的空间 142 | f.Payload = f.buffer[FrameHeaderSize:] 143 | // header为buffer的前16字节空间 144 | f.headerBuffer = f.buffer[:FrameHeaderSize] 145 | 146 | return f 147 | } 148 | 149 | // ReadBody方法的第一个参数表示Frame的header,io.Reader表示payload部分 150 | // 151 | // 作用:把header和io.Reader数据写入到Frame的Buffer中 152 | func (f *Frame) ReadBody(header []byte, r io.Reader) error { 153 | // copy的第一个参数既可以是headerBuffer,也可以是buffer,共享内存 154 | copy(f.headerBuffer, header) 155 | 156 | // 读取header数据到FrameHeader,这样就不用反复解析headerBuffer了 157 | if err := f.Header.read(typed.NewReadBuffer(header)); err != nil{ 158 | return err 159 | } 160 | 161 | // 读取payload 162 | switch payloadSize := f.Header.PayloadSize() { 163 | case payloadSize > MaxFramePayloadSize: 164 | return fmt.Errorf("invalid frame size %v", f.Header.Size) 165 | case payloadSize > 0: 166 | _, err := io.ReadFull(r, f.SizedPayload()) 167 | default: 168 | return nil 169 | } 170 | } 171 | 172 | // 从io.Reader io流读取一个完整的Frame消息帧 173 | // 174 | // 先读取header,再传入ReadBody解析header和payload到Frame中 175 | // 这里为何不写成一个方法,可能是有很多地方要用到ReadBody? 176 | func (f *Frame) ReadIn(r io.Reader) error { 177 | header := make([]byte, FrameHeaderSize) 178 | if _, err := io.ReadFull(r, header); err !=nil{ 179 | return err 180 | } 181 | 182 | return f.ReadBody(header, r) 183 | } 184 | 185 | // 这个方法,看源码写得莫名其妙 186 | func (f *Frame) WriteOut(w io.Writer) error { 187 | var wbuf typed.WriteBuffer 188 | wbuf.Wrap(f.headerBuffer) 189 | 190 | if err := f.Header.Write(&wbuf); err != nil{ 191 | return err 192 | } 193 | 194 | // 以上这段代码写得... 195 | // 看这份代码好像是要把Header头部写入到Frame的buffer前16字节 196 | // 但是buffer本身就是已经包含了header和payload。为何还要再写? 197 | 198 | fullFrame := f.buffer[:f.Header.FrameSize()] 199 | if _, err := w.write(fullFrame); err != nil{ 200 | return err 201 | } 202 | return nil 203 | } 204 | 205 | // 获取payload内存空间 206 | func (f *Frame) SizedPayload() []byte { 207 | return f.Payload[:f.Header.PayloadSize()] 208 | } 209 | 210 | // 返回帧的消息类型 211 | func (f *Frame) messageType() messageType { 212 | return f.Header.messageType 213 | } 214 | 215 | // 把header和payload写入Frame, message interface在前面一节介绍过 216 | func (f *Frame) write(msg message) error { 217 | // 通过message写入到typed.WriteBuffer 218 | // 也就写入到了Frame的payload空间 219 | var wbuf typed.WriteBuffer 220 | wbuf.Wrap(f.Payload[:]) 221 | if err := msg.write(&wbuf); err != nil{ 222 | return err 223 | } 224 | 225 | // 获取message相关信息,写入Header 226 | f.Header.ID = msg.ID() 227 | f.Header.reserve1 = 0 228 | f.header.messageType = msg.messageType() 229 | f.Header.SetPayloadSize(uint16(w.buf.BytesWritten())) 230 | } 231 | 232 | // 读取Frame的payload内存到msg中 233 | func (f *Frame) read(msg message) error { 234 | var rbuf typed.ReadBuffer 235 | rbuf.Wrap(f.SizedPayload()) 236 | return msg.read(&rbuf) 237 | } 238 | ``` 239 | 240 | ## 后记 241 | 242 | 我们可以看到所有的Frame、Header和Payload的数据读取,都是通过typed的WriteBuffer和ReadBuffer底层引用进行读写的。同时Frame也提供了io流和buffer进行读写操作。 243 | 244 | 通过message interface、WriteBuffer、ReadBuffer、Read和Frame这四节,我们对整个Frame的封装、拆包的讲解全部结束了。 245 | -------------------------------------------------------------------------------- /jaeger/tchannel-go/peer03.md: -------------------------------------------------------------------------------- 1 | # RootPeerList 2 | 3 | RootPeerList是作为PeerList的parent,而Channel存储的是PeerList。每个PeerList都有一个parent。 4 | 5 | ```shell 6 | type RootPeerList struct { 7 | sync.RWMutex 8 | 9 | channel Connectable 10 | onPeerStatusChanged func(*Peer) 11 | peersByHostPort map[string]*Peer 12 | } 13 | 14 | // 新建RootPeerList对象 15 | func newRootPeerList(ch Connectable, onPeerStatusChanged func(*Peer)) *RootPeerList { 16 | return &RootPeerList{ 17 | channel: ch, 18 | onPeerStatusChanged: onPeerStatusChanged, 19 | peersByHostPort: make(map[string]*Peer), 20 | } 21 | } 22 | 23 | // 根据RootPeerList对象,创建子对象PeerList 24 | func (l *RootPeerList) newChild() *PeerList { 25 | // PeerList的scoreCalculator默认分值计算方法 26 | return newPeerList(l) 27 | } 28 | 29 | // RootPeerList对象中的peersByHostPort属性 30 | // 添加Peer 31 | // 新建Peer,并把RootPeerList对象中的channel、onPeerStatusChanged和onClosedConnRemoved方法赋值给Peer对象 32 | // 33 | // 前半段代码可以通过l.Get(hostPort)方法替代 34 | func (l *RootPeerList) Add(hostPort string) *Peer { 35 | l.RLock() 36 | 37 | if p, ok := l.peersByHostPort[hostPort]; ok { 38 | l.RUnlock() 39 | return p 40 | } 41 | l.RUnlock() 42 | 43 | l.Lock() 44 | defer l.Unlock() 45 | 46 | if p, ok := l.peersByHostPort[hostPort]; ok { 47 | return p 48 | } 49 | 50 | var p *Peer 51 | p = newPeer(l.channel, hostPort, l.onPeerStatusChanged, l.onClosedConnRemoved) 52 | l.peersByHostPort[hostPort] = p 53 | return p 54 | } 55 | 56 | // 如果RootPeerList对象不存在,则增加hostPort;否则直接返回Peer 57 | // 58 | // 其中的l.Get代码片段是和l.Add方法存在冗余 59 | func (l *RootPeerList) GetOrAdd(hostPort string) *Peer { 60 | peer, ok := l.Get(hostPort) 61 | if ok { 62 | return peer 63 | } 64 | 65 | return l.Add(hostPort) 66 | } 67 | 68 | // 通过hostPort,获取Peer对象 69 | func (l *RootPeerList) Get(hostPort string) (*Peer, bool) { 70 | l.RLock() 71 | p, ok := l.peersByHostPort[hostPort] 72 | l.RUnlock() 73 | return p, ok 74 | } 75 | 76 | // 关闭连接时,移除Peer 77 | func (l *RootPeerList) onClosedConnRemoved(peer *Peer) { 78 | // 从RootPeerList对象中获取hostHost对应的Peer 79 | hostPort := peer.HostPort() 80 | p, ok := l.Get(hostPort) 81 | if !ok { 82 | return 83 | } 84 | 85 | // 如果Peer中的调入、调出的connections和subchannel计数累积和为0,则表示可以删除Peer了 86 | if p.canRemove() { 87 | l.Lock() 88 | delete(l.peersByHostPort, hostPort) 89 | l.Unlock() 90 | l.channel.Logger().WithFields( 91 | ... 92 | ) 93 | } 94 | return 95 | } 96 | 97 | // 获取RootPeerList对象的所有peersByHostPort,快照一份 98 | func (l *RootPeerList) Copy() map[string]*Peer { 99 | l.RLock() 100 | defer l.RUnlock() 101 | 102 | listCopy := make(map[string]*Peer) 103 | for k, v := range l.peersByHostPort { 104 | listCopy[k] = v 105 | } 106 | return listCopy 107 | } 108 | ``` 109 | 110 | # PeerHeap 111 | 112 | PeerHeap用于维护PeerScore列表的最小堆, 并保证PeerScore存储的有序 113 | 114 | ```shell 115 | // peerHeap最小堆, peerScore最小堆各个节点 116 | // 117 | // 实现heap最小堆,就需要实现container/heap的Interface接口 118 | type Interface { 119 | sort.Interface 120 | Push(x interface{}) 121 | Pop() interface{} 122 | } 123 | 124 | type peerHeap struct { 125 | peerScores []*peerScore 126 | rng *rand.Rand 127 | order uint64 128 | } 129 | 130 | func newPeerHeap() *peerHeap { 131 | return &peerHeap{rng: trand.NewSeeded()} 132 | } 133 | 134 | // peerHeap实现了sort Interface: 三个方法Len、Less和Swap 135 | func (ph peerHeap) Len() int { 136 | return len(ph.peerScores) 137 | } 138 | 139 | // 比较大小 140 | // 当peerScore两个节点的score值不同时,直接比较大小 141 | // 否则,比较peerScore的order 142 | // 143 | // 至于order值是如何来的,后面再看 ::TODO 144 | func (ph peerHeap) Less(i, j int) bool { 145 | if ph.peerScores[i].score == ph.peerScores[j].score { 146 | return ph.peerScores[i].order < ph.peerScores[j].order 147 | } 148 | return ph.peerScores[i].score < ph.peerScores[j].score 149 | } 150 | 151 | // 交换peerScore,并修改该对象属性index值,它的peerScores索引值变了 152 | func (ph peerHeap) Swap(i, j int) { 153 | ph.peerScores[i], ph.peerScores[j] = ph.peerScores[j], ph.peerScores[i] 154 | ph.peerScores[i].index = i 155 | ph.peerScores[j].index = j 156 | } 157 | 158 | // 追加peerScore节点到peerScores列表后, 并记住索引位置 159 | func (ph *peerHeap) Push(x interface{}) { 160 | n := len(ph.peerScores) 161 | item := x.(*peerScore) 162 | item.index = n 163 | ph.peerScores = append(ph.peerScores, item) 164 | } 165 | 166 | // 获取peerScores列表最后一个节点,并把该节点的index值设置为-1,为了防止取到其他节点。 167 | func (ph *peerHeap) Pop() interface{} { 168 | if len(ph.peerScores) <=0 { 169 | return nil 170 | } 171 | 172 | item := ph.peerScores[len(ph.peerScores) -1 ] 173 | item.index = -1 // for safety 174 | ph.peerScores = ph.peerScores[:len(ph.peerScores)-1] 175 | return item 176 | } 177 | 178 | // 以上则实现了heap最小堆所需要的基础方法 179 | 180 | // 所有的heap最小堆操作,都是操作index,底层的peerScore都是通过共享内存修改的 181 | // 根据score分数值,调整最小堆中的节点位置 182 | func (ph *peerHeap) updatePeer(peerScore *peerScore) { 183 | heap.Fix(hp, peerScore.index) 184 | } 185 | 186 | // 从heap最小堆中删除节点 187 | func (ph *peerHeap) removePeer(peerScore *peerScore) { 188 | heap.Remove(hp, peerScore.index) 189 | } 190 | 191 | // 从最小堆中获取堆顶节点,并返回 192 | func (ph *peerHeap) popPeer() *peerScore { 193 | return heap.Pop(ph).(*peerScore) 194 | } 195 | 196 | // 理论上,只需要最小堆添加节点就行,但是这里对hp.Order进行的相关操作,以及peerScore的order也进行了相应计算 197 | // 这个peerScore的order一般来说,新进来peerScore的order值一般都比较占有优势 198 | // 具体后续再看 ::TODO 199 | func (ph *peerHeap) pushPeer(peerScore *peerScore) { 200 | ph.order++ 201 | newOrder := ph.order 202 | // randRange will affect the deviation of peer's chosenCount 203 | randRange := ph.Len()/2 + 1 204 | peerScore.order = newOrder + uint64(ph.rng.Intn(randRange)) 205 | 206 | heap.Push(ph, peerScore) 207 | } 208 | 209 | // 交换peerScore的order,可能会影响排序 210 | func (ph *peerHeap) swapOrder(i, j int) { 211 | if i == j{ 212 | return 213 | } 214 | hp.peerScores[i].order, hp.peerScores[j].order = 215 | hp.peerScores[j].order, ph.peerScores[i].order 216 | 217 | heap.Fix(hp, i) 218 | heap.Fix(hp, j) 219 | } 220 | 221 | // 添加一个peerScore节点到heap中,但是不理解为何要进行随机交换order排序? 222 | // 223 | // 这个需要好好考虑下..... ::TODO 224 | func (ph *peerHeap) addPeer(peerScore *peerScore) { 225 | ph.pushPeer(peerScore) 226 | 227 | r := ph.rng.Intn(ph.Len()) 228 | ph.swapOrder(peerScore.Index, r) 229 | } 230 | ``` 231 | 232 | # 总结 233 | 234 | 对于RootPeerList,我们可以看到PeerList的生成都是通过root生成child。并把root的相关方法, 例如:分数值计算方法等,传给child。其他都是对RootPeerList的peersByHostPort进行相关处理 235 | 236 | 对于heapPeer,主要是实现了container/heap所需要的最小子集。但是对于其中的order、以及addPeer一些概念和实现,不能很好的理解,我们后续再看 ::TODO 237 | -------------------------------------------------------------------------------- /appdash/annotations-and-event.md: -------------------------------------------------------------------------------- 1 | # annotations 2 | 3 | Annotations用于存储Span携带信息,除了Span自身部分信息={TraceID、SpanID和ParentID},通过Span存储Span自身信息和Annotations携带信息。 4 | 5 | **这里要说明的一点,我觉得Appdash并没有把Span自身信息和其他信息区分好,但是OpenTracing官方的basictracer-go库的spanImpl区分比较好;因为前者Span的OperationName应该是属于Span本身的,但是在Appdash中被存储到Annotation["Name"]中;** 6 | 7 | ```shell 8 | type Annotations []Annotation 9 | 10 | type Annotation struct { 11 | Key string 12 | Value []byte 13 | } 14 | 15 | // 用于校验Annotation的Key值是否在已注册事件中是关键信息 16 | func (a Annotation) Important() bool { 17 | ... // 遍历获取所有已注册事件列表,校验每个事件是否为重要事件,并对获取到的重要事件 18 | // 通过event.Important方法获取关键字列表keys 19 | // 遍历keys并与a.Key值比较,如果相同,返回true;否则返回false 20 | } 21 | 22 | func (as Annotations) String() string{ 23 | ... // 序列化Annotations的键值对列表 24 | } 25 | 26 | func (as Annotations) schemas() []string { 27 | ... // Annotations的所有key,并返回 28 | // 注意一点:Appdash内部的五种Event数据key都是带有前缀"_schema:"。内部标识 29 | // 其他都是外部自定义注册事件值 30 | } 31 | 32 | func (as Annotations) get(key string) []byte { 33 | ... // 在Annotations中找到键为key的value值,并返回 34 | } 35 | 36 | func (as Annotations) StringMap() map[string]string { 37 | ... // 把Annotations列表转换成map结构数据后,返回 38 | } 39 | // 这里有个疑问,为何Annotations不直接采用map[string][]byte结构存储,而是使用slice列表结构存储? 40 | // 答案能想到的就是,map的相同key是会做覆盖处理,而slice结构是做append追加处理。 41 | // 但是前面所看到的通过key,获取value,并没有考虑重复,所以还是有疑问。 42 | 43 | func (as Annotations) wire() (w []*wire.CollectPacket_Annotation) { 44 | ... // 遍历Annotations列表,把携带信息转换为protobuffer协议传输的数据信息 45 | // 注意一点:这里是使用的Annotations的快照存储,防止数据动态修改。这里是否有必要使用快照,后面再看. 46 | } 47 | 48 | func annotationFromWire(as []*wire.CollectPacket_Annotations) Annotations { 49 | ... // 从protobuffer协议数据流中把网络数据转换为Annotations结构数据存储。 50 | } 51 | ``` 52 | 53 | # event 54 | 55 | ```shell 56 | // Span携带信息数据Annotations中,有一些是事件信息。 57 | // Appdash内部自身定义了五种Event 58 | // Schema方法用于返回事件的Key值。这个Key值代表事件名称 59 | type Event interface{ 60 | Schema() string 61 | } 62 | 63 | // 在事件列表中,哪些事件是值得关注的事件或者metrics 64 | // Important方法返回一个列表值,这个后面再看 65 | type ImportantEvent interface { 66 | Important() []string 67 | } 68 | 69 | // 由于Event事件数据是存储在Span的Annotations数据中,而Annotations并没有显式指明哪些是Event列表数据,所以就存在一对解析方法。类似于encoding/decoding 70 | type EventMarshaler interface { 71 | MarshalEvent() (Annotations, error) 72 | } 73 | 74 | type EventUnmarshaler interface{ 75 | UnmarshalEvent(Annotations) (Event, error) 76 | } 77 | 78 | // 标准化使用Marshal方法,把Event数据转换成Annotations 79 | // 如果传入的参数不支持MarshalEvent,则使用默认的方法去Marshal event数据 80 | // 简单的Event可以不用MarshalEvent,直接使用默认的 81 | // 复杂的Event数据可以自定义实现MarshalEvent和UnmarshalEvent方法序列化和反序列化数据为指定的数据类型Annotations 82 | // Appdash内部的五类Event,类型简单,使用默认的序列化就行了 83 | func MarshalEvent(e Event) (Annotations, error) { 84 | // 先尝试校验Event是否实现了MarshalEventer接口,如果是的话,直接序列化 85 | if v, ok := e.(EventMarshaler); ok { 86 | as, err := v.MarshalEvent() 87 | ... 88 | as = append(as, Annotation{Key: SchemaPrefix + e.Schema()} 89 | return as, nil 90 | } 91 | 92 | // 否则,使用默认的序列化方法 93 | var as Annotations 94 | flattenValue("", reflect.ValueOf(e), func(k, v string) { 95 | as = append(as, Annotation{Key: k, Value: []byte(v)}) 96 | }) 97 | as = append(as, Annotation{Key: SchemaPrefix+e.Schema()}) 98 | return as, nil 99 | } 100 | ``` 101 | 102 | MarshalEvent方法对于Annotations的Event存储理解很重要;它是解答**为何Span的Annotations是Slice存储key-value,而不是使用Map[string][]byte结构存储?**的关键。 103 | 104 | 针对这个问题,我还提了一个issue,最后自己解答了, [issue:206](https://github.com/sourcegraph/appdash/issues/206) 。 主要原因是作者希望Annotations存储Event事件时,希望与该事件相关的信息存储在一起连续,这样取数据时,可以以`_schema:xxx`为边界,获取与该事件相关的所有信息。这也就是为何对于事件要加一个前缀的原因。类似于分隔符。如果事件内部的连续信息使用了`_schema:xxx`前缀,则会导致事件信息的分裂。 105 | 106 | 在MarshalEvent方法中,都是先进行事件其他信息的序列化,然后再在这个单元结尾处添加一个`_schema:xxx`, 表示这个事件类型名称和结束符,且这个事件的value是空值。 107 | 108 | 另外一点,默认序列化方法是使用的flattenValue,它会采用递归算法对传入的event值进行操作,把这个事件的所有属性和子属性全部添加到Annotations中。 109 | 110 | 111 | 把Annotations中的所关注的事件,反序列化给传入的event值。 112 | ```shell 113 | func UnmarshalEvent(as Annotations, e Event) error { 114 | aSchemas := as.schemas() 115 | // 获取Annotations所有的事件key列表, 并通过遍历与传入的Event值比较,如果找不到,返回错误 116 | // 和MarshalEvent类似 117 | // 先尝试校验Event是否支持反序列化,如果不支持,则使用默认的反序列化方法 118 | unflattenValue("", reflect.ValueOf(&e), reflect.TypeOf(&e), mapToKVs(as.StringMap())) 119 | return nil 120 | } 121 | ``` 122 | 123 | 对于unflattenValue方法是耗时操作,因为它是全局Annotations检索。我觉得最理想的方法是继续对Annotations的存储进行规范化,例如:把Annotations存储数据进行分类,分为Event、Log等类型。 124 | 125 | ## 内部Event 126 | 127 | Appdash内部已存在五种Event,分别是SpanNameEvent,logEvent,msgEvent,timespanEvent和Timespan。它们都是先Event接口:Schema方法 128 | 129 | ```shell 130 | // _schema:name 131 | type SpanNameEvent struct {Name string } 132 | 133 | func (SpanNameEvent) Schema() string { return "name"} 134 | 135 | // 由此可以看到Span的OperationName存储在Event中,且最终通过MarshalEvent方法序列化存储在Annotations中 136 | func SpanName(name string) Event { 137 | return SpanNameEvent{Name: name} 138 | } 139 | 140 | // _schema:msg 141 | type msgEvent struct { Msg string} 142 | 143 | func (msgEvent) Schema() string { return "msg" } 144 | 145 | func Msg(msg string) Event { 146 | return msgEvent{Msg: msg} 147 | } 148 | 149 | // _schema:timespan 150 | type timespanEvent struct { 151 | S, E time.Time 152 | } 153 | func (timespanEvent) Schema() string { 154 | return "timespan" 155 | } 156 | 157 | func (ev timespanEvent) Start() time.Time { 158 | return ev.S 159 | } 160 | func (ev timespanEvent) End() time.Time { 161 | return ev.E 162 | } 163 | 164 | // _schema:Timespan 165 | type Timespan struct { 166 | S time.Time `trace: "Span.Start"` 167 | E time.Time `trace: "Span.End"` 168 | } 169 | 170 | func (Timespan) Schema() string { return "Timespan"} 171 | 172 | func (s Timespan) Start() time.Time {return s.S} 173 | func (s Timespan) End() time.Time {return s.E} 174 | 175 | 上面的timespanEvent与Timespan两种事件的不同,我目前还不理解,::TODO 176 | 177 | // _schema:log 178 | type logEvent struct { 179 | Msg string 180 | Time time.Time 181 | } 182 | 183 | func Log(msg string) Event { 184 | return logEvent{Msg: msg, Time: time.Now()} 185 | } 186 | 187 | func LogWithTimestamp(msg string, timestamp time.Time) Event { 188 | return logEvent{ 189 | Msg: msg, 190 | Time: timestamp, 191 | } 192 | } 193 | 194 | func (logEvent) Schema() string { 195 | return "log" 196 | } 197 | 198 | func (e *logEvent) Timestamp() time.Time { 199 | return e.Time 200 | } 201 | ``` 202 | 203 | 本章小结: 204 | 205 | Appdash是早于OpenTracing标准出现的,所以它并没有遵循OpenTracing标准。虽然该trace的log部分使用了basictracer-go,但是其实可以取代不要它,它只是一个扩展。见 [issue 207](https://github.com/sourcegraph/appdash/issues/207)。 206 | 207 | 对于Span的Annotations,存储数据包含了所有的Event列表,Event与Annotations的数据转换(序列化和反序列化), 通过MarshalEvent和UnmarshalEvent方法实现。 208 | 209 | 文中也解释了Annotations为何不使用Map[string][]byte存储,而要用slice结构存储数据。 210 | 211 | 这部分Annotations与Event的序列化和反序列化涉及到大量的reflect,在后面会有相应的分析。 212 | 213 | -------------------------------------------------------------------------------- /jaeger/TChannel/metrics.md: -------------------------------------------------------------------------------- 1 | # TChannel流量指标 2 | 3 | 本文档定义了每种实现语言中TChannel stack必须发出的通用流量指标。度量指标定义为名称(必需的和可选的tags),其中包含有关特定事件的其他上下文。不支持tags(例如:stasd或者carbon)的度量指标收集系统,应将这个tag信息转换为度量标准名称层次结构中的组件。 4 | 5 | 在这篇文章中的这个指标对任意应用程序都是通用的;他们不包括在Hyperbahn路由层的特殊指标。 6 | 7 | ### Relationship to Zipkin 8 | 9 | Zipkin跟踪信息可用于导出包含在指标中的时间信息, 但是这些指标不能包含span特殊信息。在某种程度上,可以将定时度量视为在该过程中收集的特定于请求的定时信息的预聚合。 10 | 11 | 12 | ### Relationship to Circuit Breaking 13 | 14 | 应该使用相同的源事件来发出用于graphql/monitoring的度量指标,并收集熔断器的统计数据,但这两个是独立的系统,并且应该具有单独的存储和数据循环模型 15 | 16 | 17 | ## Common Tags 18 | 19 | 所有指标必须包含以下tags: 20 | 21 | 1. `app`。 客户端或者服务端的应用名称/ID。TChannel指标按照层次结构排列——进程运行一个或者多个应用程序,应用程序托管一个或者多个服务, 服务暴露一个或者多个endpoints,一个应用程序可以在多个进程中传播。应用程序名称被认为是TChannel stack的已知先验,并且不依赖于来自线路的任何信息。TChannel实现应该提供一种方法将应用程序名称传递给它们暴露的任何初始化函数。 22 | 23 | 24 | 所有指标可以包括以下tags: 25 | 26 | 1. `host`。运行报告者的host名称; 27 | 2. `cluster`。 运行报告者的集群身份ID; 28 | 29 | 区分节点运行在canary vs 生产环境 vs 私有模式 30 | 31 | 3. `version`。 正在运行的应用程序版本号。 这个版本号作为指标由进程发送给Zipkin的Annotations中存储,以支持相关指标数据与依赖性调用图关联起来。 32 | 33 | 34 | ## Call Metrics 35 | 36 | call metrics跟踪有关应用程序之间的RPC调用数据统计。 37 | 38 | ### Outbound call metrics 39 | 40 | outbound call metrics是指作为client调用server的rpc指标。call定义为可见的应用程序调用代码,除非下面特别提到,否则speculative请求和重试不会包含在outbound call metrics中。 41 | 42 | 所有outbound call metrics必须包含`service`和`target-service`, 并且应该包含`target-endpoint` tag。这个`service` tag是表示这个发起这次rpc调用的服务名(transport headers中的key是`cn`, caller name), 这个`target-service` tag是包含被调服务名(在"call req"消息格式中的service字段表示), 这个`target-endpoint` tag是包含目标服务endpoint的id(在消息由arg1表示)。如果这个endpoint值空间是有限的,这些实现应该仅仅包括目标endpoint;"as" transport,表示"arg schema", 例如:值为"HTTP-over-tchannel", 在他们的endpoints中包含了uuid,使得这个值空间的利用率最大化。这些实现应该取消target-endpoint tag或者将arg1值标准化为有限集。 43 | 44 | #### Counters 45 | 46 | **outbound.calls.send** 47 | 48 | 这个服务调用目标服务的总次数 49 | 50 | **outbound.calls.success** 51 | 52 | 这个服务调用目标服务,并且响应成功的总次数 53 | 54 | **outbound.calls.system-errors** 55 | 56 | 这个服务调用目标服务,并报告给服务的错误帧响应总次数 57 | 58 | **outbound.calls.per-attempt.system-errors** 59 | 60 | 这个服务调用目标服务,在每次发起调用或者调用失败触发重试时,错误帧响应的总次数 61 | 62 | **outbound.calls.operational-errors** 63 | 64 | 这个服务调用目标服务发送"call init"时发送给应用程序错误的总次数。 这个不是错误帧,实际上是本地socket级别错误或者超时错误。应该包含一个`type` tag执行接收的错误类型。标准化操作错误类型是在核心指标之外 65 | 66 | **outbound.calls.per-attempt.operational-errors** 67 | 68 | 这个服务调用目标服务每次尝试调用发生的错误总次数,这个不是错误帧,除了本地socket错误和超时错误。应该包括一个error的`type`tag。标准化操作错误类型是在核心指标之外 69 | 70 | **outbound.calls.app-errors** 71 | 72 | 这个服务调用目标服务,响应给应用程序CallResponse/NotOk的总数量。可以包含一个应用程序业务级别的错误`type` tag。标准化应用业务级别错误在核心指标之外。 73 | 74 | **outbound.calls.per-attempt.app-errors** 75 | 76 | 这个服务调用目标服务,每次尝试发生的错误总次数。可以包含一个应用业务级别错误的`type` tag。标准化应用程序业务级别错误在核心指标之外。 77 | 78 | **outbound.calls.retries** 79 | 80 | 这个服务发起一个调用,内部所做的重试次数。应该有一个每次请求重试次数的指标`retry-count` tag,例如,一次请求所调用的重试次数为1,则`retry-count` tag值为1等。 81 | 82 | **outbound.request.size** 83 | 84 | 所有发送到目标服务的总字节数累计和,包括帧信息部分。 85 | 86 | **outbound.response.size** 87 | 88 | 由目标服务响应,且没有框架内的系统错误,所累积的总字节数和,包括帧信息部分。 89 | 90 | 91 | #### Timers 92 | 93 | **outbound.calls.latency** 94 | 95 | 这个服务调用目标服务所发生的端到端延迟(单位:ms)。这个是一次应用程序调用到获取结果所测量的时间。它的测量方式是,从应用发起请求到接收到最后一帧并处理完成所消耗的时间,这包括所有的重试次数。 96 | 97 | **outbound.calls.per-attempt.latency** 98 | 99 | 尝试一次调用(初始化请求或者重试)所耗费的时间。测试方式是,从第一个请求写入到帧,到最后一个响应帧接收所耗费的时间。这次调用应该包含两端的host:port tags, 重试次数的tag同上。 100 | 101 | ### Inbound call metrics 102 | 103 | 流入调用指标,也称接收来自外部服务的调用。Inbound call metrics应该包含`serivce`, `endpoint`和`calling-service` tags。这个service和endpoint tag只想被调用服务的服务名和端口号,这个calling-service tag是指发起这次请求调用的服务名称。 104 | 105 | #### Counters 106 | 107 | **inbound.calls.recvd** 108 | 109 | 这个服务接收调用的总次数 110 | 111 | **inbound.calls.success** 112 | 113 | 这个服务接收调用,并且返回成功的总次数 114 | 115 | **inbound.calls.system-errors** 116 | 117 | 这个服务接收调用,并错误响应的总次数。必须包含一个类型为error的tag指标,其值表示 outbound.calls.system-errors错误 118 | 119 | **inbound.calls.app-errors** 120 | 121 | 这个服务接收调用,并响应CallResponse/NotOk的总次数。可以包含一个应用程序业务级别的错误`type`tag 122 | 123 | **inbound.cancels.requested** 124 | 125 | 这个服务接收并取消请求的总次数。 126 | 127 | **inbound.cancels.honored** 128 | 129 | 这个服务所取消的取消总次数。在分发到一个应用程序之前,造成调用被丢弃,则表示一个cancel被取消了。另一种是,如果这个应用程序丢弃了这个请求调用,也表示一个cancel被取消了。 130 | 131 | **inbound.protocol-errors** 132 | 133 | 被调用者反馈给调用者的协议错误消息总次数 134 | 135 | **inbound.request.size** 136 | 137 | 被调用者接收到所有请求数据的总字节数,包括帧信息。 138 | 139 | **inbound.response.size** 140 | 141 | 被调用者响应所有请求,且没有发生报错的数据总字节数,包括帧信息 142 | 143 | #### Timers 144 | 145 | **inbound.calls.latency** 146 | 147 | 被掉服务处理请求所耗费的时间,单位:ms。它是指接收外部请求开始,到处理请求结束并返回最后一帧所耗费的时间. 148 | 149 | ### Connection Metrics 150 | 151 | Connection指标测量统计数与socket connections相关。所有的connection指标必须包含`host-port`和`peer-host-port` tags. 这个`host-port` tag是指reporting进程的host和端口号。对于短连接客户端,它可以是0.0.0.0:0。这个`peer-host-port` tag是对等一方的host和端口号。 152 | 153 | #### Gauges 154 | 155 | 对于Gauges概念,我们在Prometheus了解过,它不是线性递增的。 156 | 157 | **connections.active** 158 | 159 | 这个进程与被调用者建立的当前活跃连接数。active connection是用来初始化或者接收流量的连接,也包括以一个友好地shutdown的连接 160 | 161 | #### Counters 162 | 163 | **connections.initiated** 164 | 165 | 在进程的整个生命周期内初始化建立连接到目标服务的所有连接总数 166 | 167 | **connections.connect-errors** 168 | 169 | 当这个进程尝试初始化一个连接到被调服务时的错误总次数 170 | 171 | **connections.accepted** 172 | 173 | 在这个进程的生命周期内,目标服务接受连接的总次数。 174 | 175 | **connections.accept-errors** 176 | 177 | 在这个进程的生命周期内,接受连接错误的总次数 178 | 179 | **connections.errors** 180 | 181 | 在连接建立初始化时,连接发生错误的总次数。必须包括一个指向错误类型的`type`tag标签。标准的type tags包括: 182 | 183 | 1. *network-error*。 这个连接遭遇到网络错误(例如:ECONNRESET)。 184 | 2. *network-timeout*。在read/write期间,这个连接接收到的网络超时。 185 | 3. *protocol-error*。被调用服务响应一个违法协议消息。 186 | 4. *peer-protocol-error*。这个进程发送一个消息,被调用服务发送一个解析协议错误的消息。 187 | 188 | 189 | **connections.closed** 190 | 191 | 由于error或者友好地shutdown,连接关闭的总数量(例如,空闲连接超时,应用初始化关闭)。可以包含一个指向关闭原因的`reason` tag。标准的reason tags包括: 192 | 193 | 1. *idle-timeout*。由于连接空闲超时导致连接被关闭。 194 | 2. *app-initated*。 由于应用初始化连接导致的连接关闭; 195 | 3. *protocol-error*。由于协议错误导致连接关闭。 196 | 4. *network-error*。由于网络层错误导致连接关闭。 197 | 5. *network-timeout*。由于网络超时导致连接关闭。 198 | 6. *peer-closed*。有关对方引起的连接关闭 199 | 200 | 201 | **connections.bytes-sent** 202 | 203 | 在已建立的所有连接上,所有发送的流量字节总数。 204 | 205 | **connections.bytes-recvd** 206 | 207 | 在已建立的所有连接上,所有接收的流量字节总数 208 | 209 | ### Circuit Metrics 210 | 211 | Circuit状态指标是由Hyperbahn服务代理发送的,不是由TChannel直接发送的。这个指标是从circuits实例中的`circuitStateChange`事件获取的 212 | 213 | Circuit状态变化,健康或者不健康, 并在每个Circuit中跟踪统计数据。代替对tag的支持,Circuits是有调用者名称和服务名称的临时索引。 214 | 215 | 这个Circuit技术统计方式模式如下: 216 | 217 | 1. `circuits` 218 | 2. `{healthy, unhealthy}`,并且包含`total`, 或者`by-caller.$caller.$service.$endpoint`, 或者 `by-service.$service.$caller.$endpoint`中的一种。 219 | 220 | 221 | ### Rate Limiting Metris 222 | 223 | 这个流控指标跟踪了每秒发送到一个服务的请求数(RPS), 以及由于流控生效而产生的busy响应。 224 | 225 | #### Gauges 226 | 227 | **rate-limiting.service-rps** 228 | 229 | 每秒发送给一个服务的请求数。它必须包含`target-service` tag。 230 | 231 | **rate-limiting.service-rps-limit** 232 | 233 | 每秒发送给一个服务的最大请求数量。如果这个服务的RPS达到了这个上限,请求将会返回busy帧的响应消息。它必须包含一个`target-service` tag。 234 | 235 | **rate-limiting.total-rps** 236 | 237 | 一个tchannel进程接收的请求数量 238 | 239 | **rate-limiting.total-rps-limit** 240 | 241 | 一个tchannel进程能够处理的最大请求数。如果这个RPS达到了这个上限,请求将会获得busy帧的响应消息。 242 | 243 | #### Counters 244 | 245 | **rate-limiting.service-busy** 246 | 247 | 由于发送给一个服务的请求数量超过了这个限制,生成busy帧响应的数量。它必须包括`target-service` tag。 248 | 249 | **rate-limiting.total-busy** 250 | 251 | 由于发送给一个tchannel进程的请求数量超过了这个限制,生成busy帧响应的总数量。当这个信息是可用时,它应该包含`target-service` tag。 252 | -------------------------------------------------------------------------------- /jaeger/tchannel-go/allchannel-and-subchannel.md: -------------------------------------------------------------------------------- 1 | # 全局channel 2 | 3 | tchannel-go是一个rpc服务框架,使用所有内部服务和连接都是全局管理,这个依赖于channelMap全局变量。 4 | 5 | ```shell 6 | // rpc服务治理所有的服务以及相关连接, 一个服务可以有多个channel 7 | var channelMap = struct { 8 | sync.Mutex 9 | existing map[string][]*Channel 10 | }{ 11 | existing: make(map[string][]*Channel), 12 | } 13 | 14 | // 注册channel到channelMap中 15 | func registerNewChannel(ch *Channel) { 16 | // 获取该channel的服务名 17 | serviceName := ch.ServiceName() 18 | // 创建最大为10M的stack空间 19 | ch.createdStack = string(getStacks(false)) 20 | ... // log records 21 | 22 | // 全局锁,增加服务的channel 23 | channel.Lock() 24 | defer channelMap.Unlock() 25 | existing := channelMap.existing[serviceName] 26 | channelMap.existing[serviceName] = append(existing, ch) 27 | } 28 | 29 | // 从channelMap中移除channel 30 | // 31 | // 交换第i个和最后一个交换 32 | func removeClosedChannel(ch *Channel) { 33 | channelMap.Lock() 34 | defer channelMap.Unlock() 35 | 36 | // 获取指定服务的channels列表,并找出指定的channel删除 37 | channels := channelMap.existing[ch.ServiceName()] 38 | for i, v := range channels { 39 | if v != ch { 40 | continue 41 | } 42 | channels[i] = channels[len(channels)-1] 43 | 44 | channels = channels[:len(channels)-1] 45 | break 46 | } 47 | 48 | channelMap.existing[ch.ServiceName()] = channels 49 | return 50 | } 51 | ``` 52 | 53 | 以上就是tchannel-go rpc服务治理框架对所有服务的全局管理, 理论上通过channelMap可以知道任何服务的任何细节 54 | 55 | 56 | # subchannel 57 | 58 | subchannels作为channel的属性列表,用于在一个channel允许调用一个指定服务 59 | 60 | ```shell 61 | type SubChannelOption func(*SubChannel) 62 | 63 | // 为subchannel创建一个独立的PeerList, 因为PeerList的PeerScore默认score计算方式:PreferIncomingCalculator。但是想使用LeastPendingCalculator策略。上一节详细介绍 64 | func Isolated(s *SubChannel) { 65 | s.Lock() 66 | s.peers = s.topChannel.peers.NewSibling() 67 | s.peers.SetStrategy(newLeastPendingCalculator()) 68 | s.Unlock() 69 | } 70 | 71 | // SubChannel相关属性 72 | type SubChannel struct { 73 | sync.RWMutex 74 | // SubChannel调用所指向的服务名 75 | serviceName string 76 | // 所在的channel 77 | topChannel *Channel 78 | // rpc调用协议参数 79 | defaultCallOptions *CallOptions 80 | // 调用serviceName服务的所有peers 81 | peers *PeerList 82 | // 83 | handler Handler 84 | logger Logger 85 | // 数据统计 86 | statsReporter StatsReporter 87 | } 88 | 89 | // 一个channel可以调用多个服务,并维护管理subchannel的所有peerList 90 | type subChannelMap struct { 91 | sync.RWMutex 92 | subchannels map[string]*SubChannel 93 | } 94 | 95 | // 创建SubChannel,它继承了channel的所有peerList。 96 | // 即channel中的subchannelMap每个subchannel中的peerList全部一样。 97 | func newSubChannel(serviceName string, ch *Channel) *SubChannel { 98 | return &SubChannel{ 99 | serviceName: serviceName, 100 | peers: ch.peers, 101 | topChannel: ch, 102 | handler: &handlerMap{}, // 默认处理方法 103 | logger: logger, 104 | statsReporter: ch.StatsReporter(), // 数据统计报告interface 105 | } 106 | } 107 | 108 | // 返回subChannel调用对方的服务名 109 | func (c *SubChannel) ServiceName() string { 110 | return c.serviceName 111 | } 112 | 113 | // 准备发起一个调用,需要填充tchannel协议头部 114 | // 根据subchannel指定的serviceName,同时获取一个peer 115 | // 最后通过Peer填充协议头部 116 | func (c *SubChannel) BeginCall( 117 | ctx context.Context, 118 | methodName string, 119 | callOptions *CallOptions) ( 120 | *OutboundCall, error 121 | ){ 122 | if callOptions == nil { 123 | callOptions = defaultCallOptions 124 | } 125 | 126 | // 获取没有被之前选中的peer, 由它发起一个rpc调用 127 | peer, err := c.peers.Get(callOptions.RequestState.PrevSelectedPeers()) 128 | if err !=nil{ 129 | return nil, err 130 | } 131 | 132 | // 前面章节介绍过,它会填充tchannel协议头。从该peer中获取一个可用的connection,调用serviceName服务 133 | return peer.BeginCall(ctx, c.ServiceName(), methodName, callOptions) 134 | } 135 | 136 | // 返回该channel与远程serviceName建立连接的所有peerList 137 | // 138 | func (c *SubChannel) Peers() *PeerList { 139 | return c.peers 140 | } 141 | 142 | // 如果是通过Isolated方法建立的SubChannel, 则peers是独立的,与channel不同 143 | func (c *SubChannel) Isolated() bool { 144 | c.RLock() 145 | defer c.RUnlock() 146 | return c.topChannel.Peers() != c.peers 147 | } 148 | 149 | // 注册local service提供的服务 150 | // local service通过handlerMap对象存储本地服务的methodName与handler 151 | // 当外部请求进来时,则通过请求参数methodName获取handler,并做相应的处理 152 | func (c *SubChannel) Register(h Handler, methodName string) { 153 | handlers, ok := c.handler.(*handlerMap) 154 | if !ok { 155 | panic() 156 | } 157 | handlers.register(h, methodName) 158 | } 159 | 160 | // 拷贝local service注册的所有服务, 快照 161 | func (c *SubChannel) GetHandlers() map[string]Handler { 162 | handlers, ok := c.handler.(*handlerMap) 163 | if !ok { 164 | panic(...) 165 | } 166 | 167 | handlers.RLock() 168 | handlersMap := make(map[string]Handle, len(handlers.handlers)) 169 | for k, v := range handlers.handlers { 170 | handlersMap[k] = v 171 | } 172 | handlers.RUnlock() 173 | return handlersMap 174 | } 175 | 176 | // 设置local service对外提供的服务处理 177 | func (c *SubChannel) SetHandler(h Handler) { 178 | c.handler = h 179 | } 180 | 181 | // 获取该channel的所有tags 182 | func (c *SubChannel) StatsTags() map[string]string { 183 | tags := c.topChannel.StatsTags() 184 | tags["subchannel"] = c.serviceName 185 | return tags 186 | } 187 | 188 | // 在channel的subChannelMap中注册一个新的subChannel 189 | func (subChMap *subChannelMap) registerNewSubChannel( 190 | serviceName string, 191 | ch *Channel) ( 192 | _ *SubChannel, 193 | added bool, 194 | ){ 195 | subChMap.Lock() 196 | defer subChMap.Unlock() 197 | 198 | if subChMap.subchannels == nil{ 199 | subChMap.subchannels = make(map[string]*SubChannel) 200 | } 201 | 202 | if sc, ok := subChMap.subchannels[serviceName]; ok { 203 | return sc, false 204 | } 205 | 206 | sc := newSubChannel(serviceName, ch) 207 | subChMap[serviceName] = sc 208 | return sc, true 209 | } 210 | 211 | // 通过remote service的serviceName,获取subChannel 212 | func (subChMap *subChannelMap) get(serviceName string) (*SubChannel, bool) { 213 | subChMap.RLock() 214 | sc, ok := subChMap.subchannels[serviceName] 215 | subChMap.RUnlock() 216 | return sc, ok 217 | } 218 | 219 | // 获取serviceName对应的subChannel,如果不存在,则注册remote service 220 | func (subChMap *subChannelMap) getOrAdd( 221 | serviceName string, 222 | ch *Channel) ( 223 | _ *SubChannel, 224 | added bool 225 | ){ 226 | if sc, ok := subChMap.get(serviceName); !ok { 227 | return sc, false 228 | } 229 | 230 | return subChMap.registerNewSubChannel(serviceName, ch) 231 | } 232 | 233 | // 这里只针对Isolated的subChannel 234 | // 这里我们可以看到它是对channel的subChannelMap所有subChannel中的peer进行更新。 235 | func (subChMap *subChannelMap) updatePeer(p *Peer) { 236 | subChMap.RLock() 237 | for _, subCh := range subChMap.subChannels { 238 | if subCh.Isolated() { 239 | subCh.RLock() 240 | subCh.Peers().onPeerChange(p) 241 | subCh.RUnlock() 242 | } 243 | } 244 | subChMap.RUnlock() 245 | } 246 | ``` 247 | 248 | # 总结 249 | 250 | 这里说明下channel、subchannel与peerList之间的关系,再不说大家可能会看得有些累 251 | 252 | 一个channel是指发起调用的serviceName,如果这个channel要调用其他微服务,则需要对调用的微服务建立一个subChannelMap存储结构,每个微服务对应一个subChannel, 这里的每个subChannel中都有一个远程服务serviceName,那么这里我们就可以把rpc调用的双方建立起连接。 253 | 254 | 然后对于远程serviceName,可能会存在多个相同服务实例,对应不同的IP:PORT。这样就需要对subChannel细化,这就产生了subChannel的peerList存储结构,它里面有peersByHostPort,由此,channel 本地serviceName通过channel、subChannel和peerList, 就可以知道local service调用remote service的IP:PORT,以及针对所有连接的管理。 255 | 256 | 257 | 有个关于新建subChannel的疑问,我们可以从代码看到subChannel的Peers是从channel中拷贝的,而subchannel是针对指定remote service服务的,如果直接拷贝channel的peers,则PeersByHostPort并不只是subChannel指定服务的所有IP:PORT, 那映射的意义就有问题?除非全部是Isolated 258 | 259 | 如果上个疑问是没有问题的,则前面的结论则是有些不合理的 260 | -------------------------------------------------------------------------------- /dapper/dapper.md: -------------------------------------------------------------------------------- 1 | ## 学习笔记 2 | ### dapper-Google分布式跟踪系统论文 3 | #### 1. 背景 4 | 分布式系统在生产环境中运行可能存在一些耗时请求,我们可能很难定位,这里有三种情况: 5 | > **工程师无法准确地定位到这次全局搜索是调用了哪些服务,因为新的服务、或者服务上的某些片段,都有可能在任何时间上线或者修改过。** 6 | 7 | > **不能苛求工程师对所有参与这次全局搜索的服务都了如指掌,每个服务都有可能是由不同的团队开发或维护的。** 8 | 9 | > **这些暴露出来的服务或者服务器有可能同时被其他客户端使用着,所以这些全局搜索的性能问题可能是由其他应用造成的。** 10 | 11 | 上面这些情况都是可能出现的,所以针对Dapper,我们只有两点要求: 12 | > **无处不在的部署** 13 | 14 | > **持续的监控** 15 | 16 | 上面两点如果做到了,这我们可以对分布式系统中的任何服务都能做到持续监控和报表输出,以及监控告警 17 | 18 | 针对上面的两点要求,我们可以直接推出三个具体的设计目标: 19 | > **低消耗:跟踪系统对在线服务的影响做到足够小。** 20 | 21 | > **应用级的透明:对于应用的开发者来说,是不需要知道有跟踪系统这回事的。应用透明,才可以无所不在的透明侵入** 22 | 23 | > **延展性:Google至少在未来几年的服务和集群的规模下,监控系统都应该能完全把控住** 24 | 25 | 一个额外的设计目标是为跟踪数据产生之后,进行分析的速度要快,理想情况是数据存入跟踪仓库后一分钟内就能统计出来。 26 | 27 | 做到真正的应用级别的透明,这应该是当下面临的最有挑战性的设计目标。具体做法:**我们把核心跟踪代码做得很轻巧,然后把它植入到哪些无所不在的公共组件中,比如:线程调用、控制流以及RPC库。** 我们发现,Dapper数据往往侧重性能方面的调查。 28 | 29 | Dapper吸收了一些优秀文章(Pinpoint, Magpie和X-Trace)的设计理念, 同时提出了做出了新的贡献。例如:要想实现低损耗,采用率是很必要的, 即便是1/1000的采用率,对于跟踪数据的使用层面上,也可以提供足够多的信息。 30 | 31 | #### 2. 设计方法 32 | 分布式跟踪系统需要记录在一次特定请求后,系统完成的所有工作信息。例如:前端A、两个中间层BC,以及两个后端DE。当一个用户发起一个请求时,首先到达前端,然后发送两个RPC到中间层BC,B立即返回,但是C需要和DE交互后再返回A,由A来响应最初的请求。 33 | 34 | **对于这样一个请求,简单使用的分布式跟踪实现,就是为各个服务上每一次你发送和接收动作,来收集跟踪标识符(message identifiers)和时间戳(timestamp)**, 前者用于调用链跟踪,后者表示各个节点的时间开销 35 | 36 | 为了将所有记录条目与一个给定的发起者关联上并记录所有信息,现在有两种解决方案:黑盒(black-box)和基于标注(annotation-based)的监控方案。 37 | > **黑盒方案:假定需要跟踪的除了上述信息之外没有额外的信息,这样使用统计回归技术来推断两者之间的关系。** 38 | 39 | > **基于标注的方案:依赖于应用程序或者中间件明确地标记一个全局ID,从而链接每一条记录和发起者的请求。** 40 | 41 | **两者比较,黑盒方案比标注方案更轻便,他们需要更多的数据,以获得足够的精度,因为它们依赖于统计推论。标注方案最主要的缺点是,需要代码植入。** 42 | 43 | 我们倾向于认为,Dapper跟踪架构像是内嵌在RPC调用的树形结构。然而,我们的核心数据模型不只局限于我们特性RPC框架,我们还能跟踪其他行为。从形式上看,我们的Dapper跟踪模型使用的树形结构,Span以及Annotation 44 | 45 | ##### 2.1 跟踪树和span 46 | 在Dapper跟踪树结构中,树节点是整个架构的基本单元,而每个节点又是对span的引用。节点之间的连线表示该节点span和它的父span直接的关系。虽然**span在日志文件中只是简单的代表span的开始和结束时间,他们在整个树形结构中却是相对独立的。** 47 | 48 | ``` 49 | |------------------------------time-----------------------------------------------> 50 | | |-----------------------------------------------------------------| | 51 | |-----| Frontend.Request[no parent id, and span_id: 1] |---------| 52 | | |_________________________________________________________________| | 53 | | | backend.Call | | 54 | |---------| [parent id: 1, and span_id: 2] |--------------------------------------| 55 | | |________________________________| _________________________________ | 56 | | | backend.DoSomething | | 57 | |-------------------------------------------| [parent id: 1, span_id: 3 ] |---| 58 | | |_________________________________| | 59 | | | helper.Call | | 60 | |---------------------------------------------| [parent id: 3, span_id: 4] |---| 61 | | |_______________________________| | 62 | | | helper.Call | | 63 | |----------------------------------------------| [parent id: 3, span_id: 5] |---| 64 | | |______________________________| | 65 | |---20-----22------24------26------28-------30-------32------34-----36--------38--| 66 | ``` 67 | 68 | 上面这幅图说明了span在一个大的跟踪过程中是什么样的。Dapper记录了span名称,以及每个span的ID和父ID,已重建在一次追踪过程中不同span之间的关系。如果一个span没有父ID被称为root span。所有span都挂在 一个特定的跟踪上,也共用一个跟踪ID。所以这些ID用全局唯一的64位证书标示。在一个典型的Dapper跟踪中,我们希望为每一个RPC对应到一个单一的span上,而且每个额外的组件层都对应一个跟踪树形结构的层级。 69 | 70 | ![上幅图所示的一个单独span的细节图](span.png) 71 | 72 | 上图给出了一个更详细的典型Dapper跟踪span记录点视图。它表示helper.Call的RPC(分别为server端和client端). span的开始时间和结束时间,以及任何RPC的时间信息都通过Dapper在RPC组件库的植入记录下来。如果应用程序开发者选择在跟踪中增加他们自己的注释(如图中"foo"的注释)(业务数据),这些信息也会和其他span信息一样记录下来。 73 | 74 | 记住,任何一个span可以包含来自不同的主机信息,这些也要记录下来。事实上,每一个RPC span可以包含客户端和服务器两个过程的注释,使得连接两个主机的span会成为模型中所说的span。 75 | 76 | 77 | ##### 2.2 植入点-Dapper的透明性 78 | Dapper可以对应用开发者近乎零侵入的成本,对分布式控制路径进行跟踪,几乎完全依赖于基于少量通用组件库的改造。如下: 79 | > **当一个线程在处理跟踪控制路径的过程中,Dapper把这次跟踪的上下文在ThreadLocal中进行存储。追踪上下文是一个小而且容易复制的容器,其中承载了Scan的属性如跟踪ID和span id。** 80 | 81 | > **当计算过程是延迟调用的或者异步的,大多数Google开发者通过线程池或者其他执行器,使用一个通用的控制流库来回调。Dapper确保所有的回调可以存储这次跟踪的上下文,而当回调函数被处罚时,这次跟踪的上下文会与适当的线程关联上。在这种方式上,Dapper可以使用trace ID和span ID来辅助构建异步调用的路径。** 82 | 83 | > **几乎所有的Google进程间通信是建立在一个用C++和Java开发的RPC框架上。我们把跟踪植入该框架来定义RPC中所有的span。span ID和跟踪ID会从客户端发送到服务端。像那样的基于RPC的系统被广泛应用在Google中,这是一个重要的植入点。当那些非RPC通信框架发展成熟并找到了自己的用户群之后,我们会计划对RPC通信框架植入。** 84 | 85 | Dapper的跟踪数据是独立于语言的,很多在生产环境中的跟踪结合了用C++和Java写的进程数据。在后面的章节中,我们讨论应用程序的透明度时,我们会把这些理论是如何实践的进行讨论。 86 | 87 | ##### 2.3 标注:Annotation 88 | ```language 89 | // C++ 90 | const string& request = ...; 91 | if (HitCache()) 92 | TRACEPRINTF("cache hit for %s", request.c_str()); 93 | else 94 | TRACEPRINTF("cache miss for %s", request.c_str()); 95 | 96 | 97 | // Java: 98 | Tracer t = Tracer.getCurrentTracer(); 99 | string request = ...; 100 | if (hitCache()) 101 | t.Record("cache hit for "+ request); 102 | else 103 | t.Record("cache miss for "+ request); 104 | ``` 105 | 106 | 上述植入点足够推导出复杂的分布式系统的跟踪细节,使得Dapper的核心功能在不改动Google应用的情况下可用。然而,Dapper还允许应用程序开发人员在Dapper跟踪的过程中添加额外的信息,已监控更高级别的系统行为,或帮助调试问题。我们允许用户通过一个简单的API定义带时间戳的Annotation,核心的示例代码如图4所示。这些Annotation可以添加任意内容。为了保护Dapper的用户过分对日志记录特别感兴趣,每个跟踪span有一个可配置的总Annotation量的上限。但是,应用程序的Annotation是不能代替用于表示span结构的信息和记录着RPC相关的信息 107 | 108 | ##### 2.4 采样率 109 | 低损耗是Dapper的一个关键设计目标。具体做法:**为了减少分布式跟踪系统对应用性能的损耗影响,那就是在遇到大量请求时只记录其中的一小部分。** 110 | 111 | ##### 2.5 跟踪的收集 112 | ![Dapper日志收集流程](bigtable.png) 113 | 114 | Dapper的跟踪记录和收集管道的过程分为三个阶段(见上图)。 115 | > **span数据写入(1)本地日志文件中。** 116 | 117 | > **Dapper的守护进程和收集组件把这些数据从生产环境的主机中拉出来(2)** 118 | 119 | > **写到(3)Dapper的Bigtable仓库中。** 120 | 121 | 一次跟踪被设计成Bigtable中的一行,每一列相当于一个span。且Bigtable支持稀疏矩阵 122 | 123 | ##### 2.6 带外数据跟踪收集 124 | tip1: 带外数据:传输层协议使用带外数据(out-of-band, OOB)来发送一些重要数据,如果通信一方有重要的数据需要通知对方时,协议能够将这些数据快速地发送到对方。为了发送这些数据,协议一般不适用与普通数据相同的通道,而是使用另外的通道。 125 | 126 | tip2: 这里指的in-band策略是把跟踪数据随着调用链进行传送,out-of-band是通过其它的链路进行跟踪数据的收集,Dapper的写日志然后进行日志采集的方式就属于out-of-band策略 127 | 128 | #### 3. Dapper部署情况 129 | ##### 3.1 Dapper运行库 130 | Dapper代码中最关键的部分,就是对基础RPC、线程控制和流程控制组件库的植入,其中包括span的创建,采样率的设置,以及把日志写入本地磁盘。 131 | 132 | #### 3.2 生产环境下的涵盖面 133 | Dapper的渗透总结为两个方面: 134 | > **可以创建Dapper跟踪过程,这个需要在生产环境上运行Dapper跟踪收集守护进程** 135 | 136 | > **在某些情况下Dapper是不能正确的跟踪控制路径的,这些通常源于使用非标准的控制流,或是Dapper错误地把路径关联归到不相关的事情上** 137 | 138 | > **考虑到生产环境的安全,Dapper也是可以直接关闭的** 139 | 140 | Dapper提供了一个简单的库来帮助开发者手动控制跟踪传播作为一种变通方法。 还有一些程序使用了非组件性质的通信库(比如:原生的TCP Socket或者SOAP RPC),这些也不能直接支持Dapper的跟踪。 141 | 142 | #### 3.3 跟踪Annotation的使用 143 | 开发者倾向于使用特定应用程序的Annotation, 无论是作为一种分布式调试日志文件,还是通过一些应用程序特定的功能对跟踪进行分类。 144 | 145 | 目前,70%的Dapper span和90%的所有Dapper跟踪,都至少有一个特殊应用的Annotation。 146 | 147 | ### 4. 处理跟踪损耗 148 | 跟踪系统的成本由两部分构成: 149 | 1. 正在被监控的系统,在生成追踪和手机追踪数据的消耗导致系统性能下降。 150 | 2. 需要使用一部分资源来存储和分析跟踪数据。 151 | 152 | 下面主要展示三个方面: 153 | 1. Dapper组件操作的消耗; 154 | 2. 跟踪收集的消耗; 155 | 3. Dapper对生产环境负载的影响。 156 | 157 | #### 4.1 生成跟踪的损耗 158 | 生成跟踪的开销是Dapper性能影响中最关键的部分,因为收集和分析可以更容易在紧急情况下关闭。 159 | 160 | Dapper运行库中最重要的跟踪生成消耗在于创建和销毁span和annotation, 并记录到本地磁盘供后续的收集 161 | 162 | 根span的创建和销毁需要损耗平均204ns,而同样的操作在其他span上需要消耗176ns。时间上的差别主要在于需要给span跟踪分配一个全局唯一的ID 163 | 164 | 如果一个span没有被采样的话,那么这个额外的span创建Annotation的成本几乎可以忽略不计 165 | 166 | 在Dapper运行期写入到本地磁盘,是最昂贵的操作,但是通过异步写入,他们可见损耗大大减少 167 | 168 | #### 4.2 跟踪收集的消耗 169 | 独处跟踪数据也会对正在被监控的负载产生干扰。 170 | 171 | 在最坏的情况,跟踪数据处理中,Dapper守护进程被拉取跟踪数据时的CPU使用率,也没有超过0.3%。而且限制Dapper守护进程为内核scheduler最低的优先级,以防发生CPU竞争 172 | 173 | #### 4.3 在生产环境下对负载的影响 174 | 延迟和吞吐量带来的损失在把采样率调整到1/16之后就全部在实验误差范围内。在实践中,我们发现即便采样率调整到1/1024,仍然是有足够量的跟踪数据的用来跟踪大量的服务。保持Dapper的性能损耗基线在一个非常低的水平是很重要的。 175 | -------------------------------------------------------------------------------- /jaeger/TChannel/keyvalue例子.md: -------------------------------------------------------------------------------- 1 | # 创建一个Go+Thrift+Hyperbahn服务 2 | 3 | 符合这个指南的代码[示例](https://github.com/uber/tchannel-go/blob/dev/examples/keyvalue) 4 | 5 | 对于Go语言,TChannel+Thrift集成使用thrift-gen生成的代码。 6 | 7 | ## 依赖 8 | 9 | 在follow这个指南之前,确保你的GOPATH路径是已经设置了。 10 | 11 | 你需要`go get`遵循: 12 | 13 | - github.com/uber/tchannel-go 14 | - github.com/uber/tchannel-go/hyperbahn 15 | - github.com/uber/tchannel-go/thrift 16 | - github.com/uber/tchannel-go/thrift/thrift-gen 17 | 18 | 19 | 使用[Godep](https://github.com/tools/godep)去管理依赖包,这个API仍然在开发中。 20 | 21 | 这个例子假定,这个服务在下面的目录下创建:`$GOPATH/src/github.com/uber/tchannel-go/examples/keyvalue` 22 | 23 | 你应该使用你自己的path,并根据自身情况更新引入包 24 | 25 | ## Thrift服务 26 | 27 | 创建[thrift](https://thrift.apache.org/)文件去定义你的服务。对于这个指南,我们使用: 28 | 29 | `keyvalue.thrift`: 30 | 31 | ```shell 32 | service baseService { 33 | string HealthCheck() 34 | } 35 | 36 | exception KeyNotFound { 37 | 1: string key 38 | } 39 | 40 | exception InvalidKey {} 41 | 42 | service KeyValue extends baseService { 43 | string Get(1: string key) throws ( 44 | 1: KeyNotFound notfound 45 | 2: InvalidKey invalidKey 46 | ) 47 | 48 | void Set(1: strign key, 2: string value) 49 | } 50 | 51 | exception NotAuthorized {} 52 | 53 | service Admin extends baseService { 54 | void clearAll() throws ( 55 | 1: NotAuthorized notAuthorized 56 | ) 57 | } 58 | ``` 59 | 60 | 这个Thrift规范定义了两个服务: 61 | 62 | 1. `KeyValue`:一个简单的key-value存储; 63 | 2. `Admin`: 对于key-value存储的管理 64 | 65 | 这两个服务继承了`baseService`,也就继承了`HealthCheck`方法。 66 | 67 | 这些方法可能返回异常,而不是返回预期的结果,这些异常也在规范中定义。 68 | 69 | 一旦你定义了你的服务,应该通过客户库运行以下命令,去生成Thrift服务: 70 | 71 | ```shell 72 | cd $GOPATH/src/github.com/tchannel-go/examples/keyvalue 73 | 74 | thrift-gen --generateThrift --inputFile keyvalue.thrift 75 | ``` 76 | 77 | 这个运行Thrift编译器,然后生成service和client绑定。你能够手动运行这些命令: 78 | 79 | ```shell 80 | thrift -r --gen go:thrift_import=github.com/apache/thrift/lib/go/thrift keyvalue.thrift 81 | 82 | thrift-gen --inputFile "$THRIFTFILE" --outputFile "THRIFT_FILE_FOLDER/gen-go/thriftName/tchan-keyvalue.go" 83 | ``` 84 | 85 | ## Go server 86 | 87 | 为了使server ready,下面是需要做的: 88 | 89 | 1. 在网络协议层创建TChannel; 90 | 2. 创建一个handler,来处理在Thrift文件中定义的方法,然后把它注册到tchannel/thrift; 91 | 3. 创建一个Hyperbahn client和发布你的服务到Hyperbahn。 92 | 93 | ### Create a TChannel 94 | 95 | 使用[tchannel.NewChannel](http://godoc.org/github.com/uber/tchannel-go#NewChannel)创建一个channel,使用[Channel.ListenAndServe](http://godoc.org/github.com/uber/tchannel-go#Channel.ListenAndServe) 96 | 97 | 监听的地址应该是一个远程IP,它能够用于其他机器的请求连接建立。你能够有使用[tchannel.ListenIP](http://godoc.org/github.com/uber/tchannel-go#ListenIP), 它使用探索式方式选择一个好的服务 98 | 99 | 当创建一个channel时,你能够传入[options](http://godoc.org/github.com/uber/tchannel-go#ChannelOptions)参数 100 | 101 | ### Create and register Thrift handler 102 | 103 | 创建一个由Thrift生成接口的自定义方法。你能通过在`gen-go/keyvalue/thcan-keyvalue.go`文件查找这个接口。例如,生成的文件中的接口如下: 104 | 105 | ```shell 106 | type TChannelAdmin interface{ 107 | HealthCheck(ctx thrift.Context) (string, error) 108 | ClearAll(ctx thrift.Context) error 109 | } 110 | 111 | type TChannelKeyValue interface{ 112 | Get(ctx thrift.Context, key string) (string, error) 113 | HealthCheck(ctx thrift.Context) (string, error) 114 | Set(ctx thrift.Context, key string, value string) error 115 | } 116 | ``` 117 | 118 | 创建一个handler类型的实例,然后创建一个[thrift.Server](http://godoc.org/github.com/uber/tchannel-go/thrift#NewServer) 和[register](http://godoc.org/github.com/uber/tchannel-go/thrift#Server.Register)你的thrift handler。 你能够在相同的`thrift.Server`上注册多Thrift服务 119 | 120 | 121 | 每个Handler方法是运行在一个新的goroutine,因此必须是线程安全的。你的handler方法能够返回两类错误: 122 | 123 | - 在Thrift文件中声明的Error(例如:`KeyNotFound`) 124 | - Unexpected errors 125 | 126 | 如果你返回了一个unexpected error,一个error帧发送带有Thrift的消息。如果能够枚举错误类型,最好在Thrift文件中声明他们,然后直接返回。例如: 127 | 128 | ```shell 129 | if value, ok := map[key]; ok { 130 | return value, "" 131 | } 132 | 133 | return "", &keyvalue.KeyNotFound{Key: key} 134 | ``` 135 | 136 | ### Advertise with Hyperbahn 137 | 138 | 使用[hyperbahn.NewClient](http://godoc.org/github.com/uber/tchannel-go/hyperbahn#NewClient)创建一个Hyperbahn client,在当前环境下它要求冲一个配置文件中加载一个Hyperbahn配置对象。当创建client时,你也能够传递更多的[options](http://godoc.org/github.com/uber/tchannel-go/hyperbahn#ClientOptions) 139 | 140 | 调用[Advertise](http://godoc.org/github.com/uber/tchannel-go/hyperbahn#Client.Advertise)去在Hyperbahn发布这个服务。 141 | 142 | ### Serving 143 | 144 | 你的服务现在已经在Hyperbahn上了。你能够使用[tcurl](https://github.com/uber/tcurl)进行调用测试: 145 | 146 | ```shell 147 | node tcurl.js -p [HYPERBAHN-HOSTPORT] -t [DIR-TO-THRIFT] keyvalue KeyValue::Set -3 '{"key": "hello", "value": "world"}' 148 | 149 | node tcurl.js -p [HYPERBAHN-HOSTPORT] -t [DIR-TO-THRIFT] keyvalue KeyValue::Get -3 '{"key": "hello"}' 150 | ``` 151 | 152 | 用一个Hyperbahn节点的Host:port替代`[HYPERBAHN-HOSTPORT]`, .thrift文件存储在`[DIR-TO-THRIFT]`目录下 153 | 154 | 你的服务现在能够用任何语言通过Hyperbahn+TChannel进行访问 155 | 156 | ## Go client 157 | 158 | 注意:这个client实现仍然在积极的开发中 159 | 160 | 为了使一个client能够通信,你需要做: 161 | 162 | 1. 创建一个TChannel(or 重用一个已存在的TChannel) 163 | 2. 创建Hyperbahn 164 | 3. 创建一个Thrift+TChannel client; 165 | 4. 使用Thrift client做远程调用 166 | 167 | ### Create a TChannel 168 | 169 | TChannel是一个双向流,因此这个client使用与server相同的代码(tchannel.NewChannel)去创建一个TChannel。你不需要在channel上调用ListenAndServe。甚至这个channel不需要一个host service,但是对于TChannel,一个服务名是需要的。这个serviceName对于这个client应该是唯一的。 170 | 171 | 你能够使用一个已存在的TChannel发起client调用。 172 | 173 | ### Set up Hyperbahn 174 | 175 | 与server代码类似,使用Hyperbahn.NewClient去创建一个新的Hyperbahn client。你不需要调用Advertise,这个client没有任何服务需要发布到Hyperbahn中 176 | 177 | 如果你已经创建一个已存在的client,你不需要在做任何事情了。 178 | 179 | ### Create a Thrift client 180 | 181 | 这个Thrift client有两部分: 182 | 183 | 1. 这个`thrift.TChanClient`配置为命中特定的Hyperbahn服务。 184 | 2. 使用底层的`thrift.TChannClient`生成的client,为一个指定thrift服务调用方法。 185 | 186 | 187 | 创建一个`thrift.TChanClient`, 使用`thrift.NewClient`。这个client能够用于创建一个生成的client: 188 | 189 | ```shell 190 | thriftClient := thrift.NewClient(ch, "keyvalue", nil) 191 | 192 | client := keyvalue.NewTChanKeyValueClient(thriftClient) 193 | 194 | adminClient := keyvalue.NewTChanAdminClient(thriftClient) 195 | ``` 196 | 197 | ### Make remote calls 198 | 199 | 在这个client上通过TChannel发起一个远程过程调用,例如: 200 | 201 | ```shell 202 | err := client.Get(ctx, "hello", "world") 203 | 204 | val, err := client.Get(ctx, "hello") 205 | ``` 206 | 207 | 当发起方法调用时,你必须传入一个context。这个调用会携带deadline、tracing和应用headers。一个简单的root context是: 208 | 209 | ```shell 210 | ctx, cancel := thrift.NewContext(time.Second) 211 | ``` 212 | 213 | 所有通过TChannel的调用必须要求有timeout,tracing信息。NewContext应该仅仅用于调用链的开头。所有其他节点通过传入的context,并传递下去。当你通过context传递,你也会传递deadline,tracing信息和这个headers。trace context的上下文传播机制。 214 | 215 | ## Headers 216 | 217 | Thrift+TChannel允许client发送headers, servers能够增加响应headers到任何响应中; 218 | 219 | 在Go语言中,在使用[WithHeaders](http://godoc.org/github.com/uber/tchannel-go/thrift#WithHeaders)调用之前,headers附加在context上。 220 | 221 | ```shell 222 | headers := map[string]string{"user":"prashant"} 223 | 224 | ctx, cancel := thrift.NewContext(time.Second) 225 | 226 | ctx = thrift.WithHeaders(ctx) 227 | ``` 228 | 229 | 这个server通过[Headers](http://godoc.org/github.com/uber/tchannel-go/thrift#Context)读取这些headers,并且能够通过`SetResponseHeaders`方法,增加额外的响应头。 230 | 231 | ```shell 232 | func (h *kvHandler) ClearAll(ctx thrift.Context) { 233 | headers := ctx.Headers() 234 | 235 | respHeaders := map[string]string{ 236 | "count": 10, 237 | } 238 | ctx.SetResponseHeaders(respHeaders) 239 | } 240 | ``` 241 | 242 | 在同一个context,通过调用`ctx.REsponseHeaders`方法,这个client能够读取到响应头。 243 | 244 | ```shell 245 | ctx := thrift.WithHeaders(thrift.NewContext(time.Second), headers) 246 | err := adminClient.ClearAll() 247 | 248 | responseHeaders := ctx.ResponseHeaders() 249 | ``` 250 | 251 | 头部不要加上调用方法的参数,应该放入Thrift request/response结构相应位置。 252 | 253 | 254 | ## Limitations & Upcoming Changes 255 | 256 | TChannel的对等选择还没有针对节点的详细运行状况模型,并且选择不会平衡节点之间的负载。 257 | 258 | 这个thrift-gen自动生成的代码是新的,可能不支持所有Thrift功能(例如:注释,includes,多文件等) 259 | -------------------------------------------------------------------------------- /jaeger/tchannel-go/message-exchange.md: -------------------------------------------------------------------------------- 1 | # message exchange 2 | 3 | 在前面的章节中,经常提及到message exchange概念,它用来表示一个peer与connection的消息交换。每个message exchange都有一个channel,它常常用于从这个peer上接受frames,当这个message exchange超时或者取消时,一个context上下文能够控制并作出其他相应的处理操作 4 | 5 | ```shell 6 | // 从messageExchange结构体,我们可以看出通过recvCh管道接收Frame。 7 | type messageExchange struct { 8 | recvCh chan *Frame 9 | errCh errNotifier 10 | // 如果context上下文超时或者取消,则做出相应动作 11 | ctx context.Context 12 | // 消息ID,每一次调用是一个message id 13 | msgID uint32 14 | // 帧消息类型:call req; call req continue;call res; call res continue, 15 | // call ping req; call ping res; call init req; call init res等 16 | msgType messageType 17 | // 下面有介绍 18 | mexset *messageExchangeSet 19 | framePool FramePool 20 | 21 | shutdownAtomic atomic.Uint32 22 | errChNotified atomic.Uint32 23 | } 24 | 25 | // 发送frame到connection上 26 | func (mex *messageExchange) forwardPeerFrame(frame *Frame) error { 27 | ... // check error 28 | select { 29 | // frame从Peer通过channel传输到connection上, 它会被goroutine recvMessage接收到 30 | case mex.recvCh <- frame: 31 | return nil 32 | // 增加recvCh buffer大小 33 | case <-mex.ctx.Done(): 34 | return GetContextError(mex.ctx.Err()) 35 | case <-mex.errCh.c: 36 | select { 37 | case mex.recvCh <- frame: 38 | return nil 39 | default: 40 | } 41 | return mex.errCh.err 42 | } 43 | } 44 | 45 | func (mex *messageExchange) checkFrame(frame *Frame) error { 46 | if rame.Header.ID != mex.msgID { 47 | ... // err log 48 | return errUnexpectedFrameType 49 | } 50 | return nil 51 | } 52 | 53 | func (mex *messageFrame) recvPeerFrame() (*Frame, error){ 54 | ... // err handler 55 | 56 | select { 57 | // frame从connection传输到Peer上 58 | case frame := <-mex.recvCh: 59 | if err := mex.checkFrame(frame); err != nil{ 60 | return nil, err 61 | } 62 | return frame, nil 63 | case <-mex.ctx.Done(): 64 | return nil, GetContextError(mex.ctx.Err()) 65 | case <- mex.errCh.c: 66 | ... 67 | } 68 | } 69 | 70 | // message exchange通知关闭通道,并从message exchange set中移除message id 71 | func (mex *messageExchange) shutdown() { 72 | if !mex.shutdownAtomic.CAS(0, 1) { 73 | return 74 | } 75 | 76 | if mex.errChNotified.CAS(0, 1) { 77 | mex.errCh.Notify(errMexShutdown) 78 | } 79 | 80 | mex.mexset.removeExchange(mex.msgID) 81 | } 82 | 83 | // 流入的message exchange数据通道超时 84 | func (mex *messageExchange) inboundExpired() { 85 | mex.mex.expireExchange(mex.msgID) 86 | } 87 | ``` 88 | 89 | # message exchange set 90 | 91 | 它主要用于把frame从peer路由到一个合适的messageExchange上;每一个connection都有两个messageExchangeSets,一个用于outbound,另一个用于inbound;具体类型的handlers负责注册message exchange和从对应的messageExchangeSet移除。 92 | 93 | ```shell 94 | type messageExchangeSet struct { 95 | sync.RWMutex 96 | 97 | log Logger 98 | name string 99 | onRemoved func() 100 | onAdded func() 101 | // 一个服务的消息通道列表全部清除之前,goroutine不能退出 102 | sedChRefs sync.WaitGroup 103 | 104 | // 每个message id对应一个messageExchange;所以同一个messageExchange是一次rpc调用 105 | exchanges map[uint32]*messageExchange 106 | // 这个map[msgID]struct{}使用场景太多了 107 | // 校验msgID所对应的message exchange是否超时 108 | expiredExchanges map[uint32]struct{} 109 | // 校验message exchange通道是否关闭 110 | shutdown bool 111 | } 112 | 113 | // 很有意思的地方:messageExchange存在元素messageExchangeSet;同时messageExchangeSet存在元素messageExchange; 114 | func newMessageExchangeSet(log logger, name string) *messageExchangeSet { 115 | return &messageExchangeSet { 116 | name: name, 117 | log: ... // exchange name log 118 | exchanges: make(map[uint32]*messageExchange), 119 | expiredExchanges: make(map[uint32]struct{}), 120 | } 121 | } 122 | 123 | // 增加一个peer到connection的消息通道,用于一次的rpc调用数据传输 124 | func (mexset *messageExchangeSet) addExchange(mex *messageExchange) error { 125 | // 如果一次rpc调用完成,则这个通道会关闭。并返回message exchange的错误 126 | if mexset.shutdown { 127 | return errMexSetShutdown 128 | } 129 | 130 | // 校验message id的消息通道是否已存在, 存在则返回错误 131 | if _, ok := mexset.exchanges[mex.msgID]; ok { 132 | return errDuplicateMex 133 | } 134 | 135 | // 不存在则增加通道 136 | mexset.exchanges[mex.msgID] = mex 137 | mexset.sendChRefs.Add(1) 138 | } 139 | 140 | // 创建一个messageExchange,并添加到message exchange set中 141 | func (mexsest *messageExchangeSet) newExchange( 142 | ctx context.Context, 143 | framePool FramePool, 144 | msgType messageType, 145 | msgID uint32, 146 | bufferSize int) (*messageExchange, error){ 147 | 148 | // 创建一个message exchange 149 | mex := &MessageExchange{ 150 | msgType: msgType, 151 | msgID: msgID, 152 | ctx: ctx, 153 | recvCh: make(chan *Frame, bufferSize), 154 | errCh: newErrNotifier(), 155 | mexset: mexset, 156 | framePool: framePool, 157 | } 158 | 159 | // 加锁添加messageExchange 160 | mexset.Lock() 161 | addErr := mexset.addExchange(mex) 162 | mexset.Unlock() 163 | 164 | mexset.onAdded() 165 | 166 | return mex, nil 167 | } 168 | 169 | // delete message exchange ; 外围需要加锁操作 170 | func (mexset *messageExchangeSet) deleteExchange(msgID uint32) (found, timeout bool){ 171 | // 如果msgID所对应的message exchange存在,则删除并返回 172 | if _, found := mexset.exchanges[msgID]; found { 173 | delete(mexset.exchanges, msgID) 174 | return true, false 175 | } 176 | 177 | // 如果不存在,则查看exchange message是否已超时,超时则返回不存在,且超时操作 178 | // 它的作用:后面再看 ::TODO 179 | if _, expired := mexset.expiredExchanges[msgID]; expired { 180 | delete(mexset.expiredExchanges, msgID) 181 | return false, true 182 | } 183 | 184 | return false, false 185 | } 186 | 187 | // 删除msgID 188 | func (mexset *messageExchangeSet) removeExchange(msgID uint32) { 189 | // 删除msgID 190 | mexset.Lock() 191 | founc, expired := mexset.deleteExchange(msgID) 192 | mexset.Unlock() 193 | 194 | // message exchange移除时,waitGroup的变量减一操作;当为0时,goroutine直接退出; 195 | mexset.sendChRefs.Done() 196 | mexset.onRemoved() 197 | } 198 | 199 | // 删除message exchange, 并设置message exchange超时 200 | func (mexset *messageExchangeSet) expireExchange(msgID uint32) { 201 | mexset.Lock() 202 | found, expired := mexset.deleteExchange(msgID) 203 | if found || expired { 204 | mexset.expireExchanges[msgID] = struct{}{} 205 | } 206 | mexset.Unlock() 207 | 208 | mexset.onRemoved() 209 | } 210 | 211 | // 等待message exchange清除完成,goroutine退出 212 | func (mexset *messageExchangeSet) waitForSendCh() { 213 | mexset.sedChRefs.Wait() 214 | } 215 | 216 | // 统计该连接的当前message exchanges总数量 217 | func (mexset *messageExchangeSet) count() int { 218 | mexset.RLock() 219 | count := len(mexset.exchanges) 220 | mexset.RUnlock() 221 | 222 | return count 223 | } 224 | 225 | // forwardPeerFrame方法是把一个frame从peer发送到合适的message exchange 226 | func (mexset *messageExchangeSet) forwardPeerFrame(frame *Frame) error { 227 | // 从message exchange set中获取message exchange 228 | mexset.RLock() 229 | mex := mexset.exchanges[frame.Header.ID] 230 | mexset.RUnlock() 231 | 232 | ... // if mex == nil, return 233 | 234 | mex.forwardPeerFrame(frame) 235 | return 236 | } 237 | 238 | // 获取exchange exchange set的快照 239 | func (mexset *messageExchangeSet) copyExchanges() ( 240 | shutdown bool, 241 | exchanges map[uint32]*messageExchange) { 242 | // 如果message exchange set已经关闭 243 | if mexset.shutdown { 244 | return true, nil 245 | } 246 | 247 | exchangesCopy := make(map[uint32]*messageExchange, len(mexset.exchanges)) 248 | for k, mex := range mexset.exchanges { 249 | exchangesCopy[k] = mex 250 | } 251 | 252 | return false, exchangesCopy 253 | } 254 | 255 | // 停止所有的message exchange进行数据传递, 256 | // 257 | // 它是通过message exchange set遍历 message exchange,然后通过信号量channel终止数据传递 258 | func (mexset *messageExchangeSet) stopExchanges(err error) { 259 | mexset.Lock() 260 | shutdown, exchanges := mexset.copyExchanges() 261 | mexset.shutdown = true 262 | mexset.Unlock() 263 | 264 | if shutdown { 265 | return 266 | } 267 | 268 | // 遍历获取的快照message exchange,然后通过channel发送信号量停止数据传递 269 | for _, mex := range exchanges { 270 | if mex.errChNotified.CAS(0, 1) { 271 | mex.errCh.Notify(err) 272 | } 273 | } 274 | } 275 | ``` 276 | 277 | # 总结 278 | 279 | 每个message exchange set都是针对一个服务而言,该服务上有多个连接。每个message exchange负责frame从peer传输到connection上。且每个message exchange都是一次RPC调用,用message id表示; 并通过channel控制这个这个服务的所有连接进行释放操作 280 | -------------------------------------------------------------------------------- /appdash/store.md: -------------------------------------------------------------------------------- 1 | 解析完Collector后,本节讲解Appdash的后端存储,目前Appdash支持的后端存储全部都是基于内存的存储,也可以自定义对接后端存储,比如:mysql、redis等,需要做一些Collect方法数据存储的适配工作{SpanID, Annotations} 2 | 3 | # Store 4 | 5 | 为了丰富Collect服务本地存储和网络存储,提供了可自定义实现的Store interface;同时也提供了一些基于内存的本地存储localmemory等; 6 | 7 | ```shell 8 | // SpanID与Annotations的数据存储 9 | type Store interface { 10 | Collector 11 | 12 | Trace(ID) (*Trace, error) 13 | } 14 | 15 | // 在做Dashboard时的查询条件 16 | // 支持基于时间范围和TraceID列表的查询操作 17 | type TracesOpts struct { 18 | Timespan Timespan 19 | 20 | TraceIds []ID 21 | } 22 | 23 | // 查询接口,通过TraceOpts实现Trace条件搜索, 返回符合条件的Trace列表。 24 | type Queryer interface { 25 | Traces(opts TracesOpts) ([]*Trace, error) 26 | } 27 | 28 | // Trace聚合数据, 数据通过Aggregator interface获取 29 | type AggregateResult struct { 30 | RootSpanName string 31 | 32 | Average, Min, Max, StdDev time.Duration 33 | 34 | Samples int64 35 | 36 | Slowest []ID 37 | } 38 | 39 | // 聚合操作, 它也是指定时间范围内进行Trace数据聚合操作,返回聚合数据列表 40 | // 注意一点:这里的时间范围是指:过去一段时间范围 41 | type Aggregator interface{ 42 | // Aggregate(-72*time.Hour, 0) 43 | Aggregate(start, end time.Duration) ([]*AggregateResult, error) 44 | } 45 | ``` 46 | ## 持久化存储 47 | 48 | 持久化存储也就是把数据最终存储到磁盘上。PersistentStore interface是表示持久化存储,它在Store interface的基础上新增了ReadFrom和Write方法,用于持久化存储。 49 | 50 | Appdash持久化存储的实现包括:MemoryStore 51 | 52 | ```shell 53 | type PersistentStore interface{ 54 | Write(io.Writer) error 55 | ReadFrom(io.Reader) (int64, error) 56 | Store 57 | } 58 | ``` 59 | 60 | 我们知道,无论是业务微服务的agent采集,或者Collector服务端,都是已Recorder单元进行数据处理,并没有形成完整的trace调用链。所以Storage首先就是要把离散的Span,聚合成一条条完整的trace列表。这样在Dashboard中显示时才能够形成调用链列表。那么在Store处理数据过程中,就会有插入分裂节点操作等 61 | 62 | ### MemoryStore 63 | 64 | ```shell 65 | type MemoryStore struct { 66 | 67 | trace map[ID]*Trace // trace ID -> trace tree 68 | 69 | span map[ID]map[ID]*Trace // trace ID -> span ID -> sub trace tree 70 | 71 | sync.Mutex // 并发操作 72 | } 73 | ``` 74 | 75 | 这个MemoryStore比较有意思,Trace类型我们在[《Appdash源码阅读——Tracer&Span》](https://gocn.vip/article/879)文章中介绍过,它是一个Trace调用链完整的层次树。那么通过trace的map结构,我们可以通过TraceID获取整颗完整的trace树,这个map结构的trace存储的是一颗颗完整的trace树。 76 | 77 | span的map结构存储的数据是通过TraceID可以获取到每个子节点的树,也就是把上一个map结构的trace展开存储,后者只能通过递归形式获取到某个span节点,但是这个map结构的span可以获取到全局到任一span节点,也就是说查询到的TraceID列表各个span的时间复杂度o(1)。 而前者是logN。 78 | 79 | 所以trace和span拥有的span节点是相同的,也就相当于2倍Trace存储。 80 | 81 | 上面这个结构设计,不太合理。 82 | 83 | 我们再看MemoryStore的创建、节点插入和读取等操作 84 | 85 | ```shell 86 | // 创建一个MemoryStore实例 87 | func NewMemoryStore() *MemoryStore { 88 | return &MemoryStore{ 89 | trace: map[ID]*trace{}, 90 | span: map[ID]map[ID]*trace{}, 91 | } 92 | } 93 | 94 | // 这里提一个小技巧,如果我们想确认自定义的后端存储是否实现了Store、或者PersistentStore接口,可以在编译时确定,做法如下, 这种做法非常常见 95 | 96 | var _ interaface { 97 | Store 98 | Queryer 99 | } = (*MemoryStore)(nil) 100 | 101 | // 存储Recorder 102 | func (ms *MemoryStore) Collect(id SpanID, anns ...Annotation) error { 103 | ms.Lock() 104 | defer ms.Unlock() 105 | return ms.collectNoLock(id, anns...) 106 | } 107 | 108 | // recorder数据转换存储到MemoryStore中 109 | func (ms *MemoryStore) collectNoLock(id SpanID, anns ...Annotation) error { 110 | // 增加span节点 111 | if _, present := ms.span[id.Trace]; !present { 112 | ms.span[id.Trace] = map[ID]*Trace{} 113 | } 114 | 115 | // insert || update span 116 | s, present := ms.span[id.Trace][id.Span] 117 | if !present { 118 | s = &Trace{ 119 | Span: Span{ 120 | ID: id, 121 | Annotations: anns, 122 | }, 123 | } 124 | ms.span[id.Trace][id.Span] = s 125 | } else { 126 | s.Annotations = append(s.Annotations, anns...) 127 | return nil 128 | } 129 | 130 | // insert || update trace tree 131 | root, present := ms.trace[id.Trace] 132 | if !present { 133 | ms.trace[id.Trace] = s 134 | root = s 135 | } 136 | 137 | // 后面一段代码做了整个层次树的重建。 138 | // 做法: 139 | // 1. 如果当前trace的TraceID已存在,不管目前存储的trace tree根节点是否为真正的root节点; 分三种情况: 140 | // (1). 新来的span是真正的根节点 141 | // (2). 当前根节点的父亲是新来的span 142 | // (3). 当前根节点的父亲不是新来的span 143 | // 前两者归为一类, 都把新来的span作为根节点处理 144 | // 对于(1), (2)表明已存在Trace,且新来的span是根节点的父亲,则做树的重建, 新的root为span 145 | if ... { 146 | // 改变根节点为新的span 147 | ms.trace[id.Trace] = root 148 | // 调整原来合适但不正确的depth=2的子节点,到新的root下。 149 | ms.reattachChildren(root, oldRoot) 150 | // 调整MemoryStore下的span 151 | ms.insert(root, oldRoot) 152 | 153 | 154 | } 155 | // 2. 当前trace不存在和(3)点归为一类: 156 | // 不做处理 157 | 158 | // 对于这个算法,后面有详细介绍,便于大家理解 159 | ... 160 | 161 | // 存储span到MemoryStore的span合适位置上 162 | if !id.IsRoot() && s != root { 163 | ms.insert(root, s) 164 | } 165 | 166 | // 调整原来root下depth=2的子节点 167 | if s != root { 168 | ms.rettachChildren(s, root) 169 | } 170 | return 171 | } 172 | 173 | // 插入span到ms的span存储中 174 | func (ms *MemoryStore) insert(root, t *Trace) { 175 | p, present := ms.span[t.ID.Trace][t.ID.Parent] 176 | if present { 177 | // 如果能够找到父亲,则直接添加在父亲的子节点列表中 178 | p.Sub = append(p.Sub, t) 179 | } else { 180 | // 如果不能找到父亲,则直接添加到root的子节点中,暂存。 181 | root.Sub = append(root.Sub, t) 182 | } 183 | } 184 | 185 | // 调整原来root的depth=2相关子节点 186 | // 由于dst和src已在前面建立好了关系,所以这里只是对depth=2的子节点进行适当调整 187 | // dst与src的链条不会变化 188 | func (ms *MemoryStore) rettachChildren(dst, src *Trace) { 189 | var sub2 []*Trace 190 | 191 | for _, sub := range src.Sub { 192 | if sub.Span.ID.Parent == dst.Span.ID.Span { 193 | // 该节点和dst是父子关系,直接提上去 194 | dst.Sub = append(dst.Sub, sub) 195 | } else { 196 | sub2 = append(sub2, sub) 197 | } 198 | } 199 | // 因为src下面子节点已发生改变,所以直接赋予新的子节点列表即可 200 | src.Sub = sub2 201 | } 202 | ``` 203 | 204 | 针对collectNoLock方法,有个疑问。我们看到首先对span进行存储操作,当发现span节点已存在时,在追加Annotations后就直接返回了,但是trace的span子节点并没有追加Annotations。所以会导致trace和span的子节点数据不一致。 205 | 206 | 在collectNoLock后面代码对trace层次树进行重建的原因在于: 207 | 208 | 并不是创建第一个trace id时,它就是根节点,它也可能在网络中滞留了,导致span子节点先到,而root节点后到,如果不重建,则会导致调用链的乱序,或者无序。这颗树的重建依赖于各个span的parentspanid值 209 | 210 | 我们要理解层次树的重建,要搞清楚几点: 211 | 212 | 1. 重建后的树也不一定是有序的层次树; 213 | 2. 每次重建后,如果新来的span尚不能放在正确的位置,对于MemoryStore的trace存储,span就放在临时根节点的子节点上,距离为1.也就是root.Sub列表中。 214 | 3. 每次重建后,还不能放在正确位置的span,对于MemoryStore的span存储,span就存放在根节点的Sub列表中 215 | 216 | **所以对于已存在的trace,如果新到来的span还不能存放在正确的位置,那就存放在depth=2的节点上。** 217 | 218 | 再加上一些对于MemoryStore的方法列表,包括查询、删除等操作 219 | 220 | ```shell 221 | // 根据TraceID,获取MemoryStore中trace对应ID的调用链 222 | func (ms *MemoryStore) Trace(id ID) (*Trace, error) { 223 | ms.Lock() 224 | defer ms.Unlock() 225 | return ms.traceNoLock(id) 226 | } 227 | 228 | func (ms *MemoryStore) traceNoLock(id ID) (*Trace, error) { 229 | t, present:= ms.trace[id] 230 | ... 231 | } 232 | 233 | // MemoryStore在开头说到编译时也校验,它实现了Queryer接口 234 | // 这里虽然实现了Traces查询功能,但是并没有使用输入参数。这个是全局输出所有的调用链 235 | func (ms *MemoryStore) Traces(opts TraceOpts) ([]*Trace, error) { 236 | ms.Lock() 237 | defer ms.Unlock() 238 | 239 | for id := range ms.trace { 240 | t, err := ms.traceNoLock(id) 241 | ts = append(ts, t) 242 | } 243 | ... 244 | } 245 | 246 | // 删除MemoryStore中的trace和span 247 | func (ms *MemoryStore) Delete(traces ...ID) error { 248 | ms.Lock() 249 | defer ms.Unlock() 250 | return ms.deleteNoLock(traces...) 251 | } 252 | 253 | func (ms *MemoryStore) deleteNoLock(traces ...ID) error { 254 | for _, id := range traces { 255 | delete(ms.trace, id) 256 | delete(ms.span, id) 257 | } 258 | return nil 259 | } 260 | 261 | // 删除MemoryStore中的span,以及对应trace中的span 262 | // 这里存在一个问题,为何有这种删除完整调用链的需求?只删除Annotations,我还可以理解。 263 | // 如果删除Span,则会导致这个Span的所有子节点全部失联的情况。 264 | func (ms *MemoryStore) deleteSubNoLock(s SpanID, annotationsOnly bool) bool { 265 | ... 266 | } 267 | 268 | // 因为MemoryStore实现了持久化存储PersistentStore, 所以存在io.Writer和io.Reader操作 269 | // 使用gob进行序列化写入 270 | func (ms *MemoryStore) Write(w io.Writer) error { 271 | ... //并发 272 | 273 | data :=memoryStoreData{m.trace, m.span} 274 | return gob.NewEncoder(w).Encode(data) 275 | } 276 | 277 | // 从io流读取数据到内存MemoryStore中 278 | func (ms *MemoryStore) ReadFrom(r io.Reader) (int64, error) { 279 | ... // 并发 280 | 281 | var data memoryStoreData 282 | gob.NewDecoder(r).Decode(&data) 283 | ms.trace = data.Trace 284 | ms.span = data.Span 285 | return int64(len(ms.trace)), nil 286 | } 287 | ``` 288 | 289 | 290 | 291 | 从Appdash中,我们可以看到对于并发操作,都会存在两个方法一个是XXX(), 另一个则是XXXNoLock() ,如果存在并发操作,则使用前者,如果非并发,则使用后者。 292 | 293 | 另一个场景则是,对于mysql事务处理,在业务中也可以提供两类操作,一类是带事务,一类是不带事务操作。 294 | 295 | 296 | ## PersistentStore 297 | 298 | 持久化存储操作,前面提到如果要内存数据持久化到本地,则需要实现PersistetStore接口,内存本地化操作如下: 299 | 300 | ```shell 301 | // 通过实现PersistentStore接口的Storage,通过对应的接口把io流定时写入到文件中。 302 | // 这个是对全局调用链信息的持久化,定期全局覆盖备份, 在没有完成备份前,会写入到临时文件中 303 | func PersistentEvery(s PersistentStore, interval time.Duration, file string) { 304 | for { 305 | time.Sleep(interval) 306 | 307 | f, err := ioutil.TempFile("", "appdash") 308 | 309 | s.Write(f) 310 | s.Close() 311 | os.Rename(f.Name(), file) 312 | } 313 | } 314 | ``` 315 | 316 | 317 | ## RecentStore 318 | 319 | 这两者有时间,我再介绍。 320 | 321 | ## LimitStore 322 | 323 | -------------------------------------------------------------------------------- /appdash/recorder-and-collector.md: -------------------------------------------------------------------------------- 1 | # Recorder 2 | 3 | 在Appdash中,Recorder是可以与Collector交互,以及Span对外的操作入口或者操作单元。 4 | 5 | ```shell 6 | type Recorder struct { 7 | SpanID 8 | annotations []Annotation 9 | 10 | finished bool // 表示该span是否已经结束了生命周期 11 | 12 | collector Collector // Recorder作为Collector的输入 13 | 14 | errors []error // 在span的生命周期内操作出现错误的error列表 15 | errorsMu sync.Mutex // 并发errors 16 | } 17 | 18 | // 当StartSpan后,再指定Collector,用于存储trace数据 19 | // 这里有个疑问:为何Collector不是在各个服务启动时,初始化global tracer时并指定Collector服务,而是把Collector细化到Recorder中,这样如果服务在运行中,Collector不变,则直接提到Tracer上就可以了,如果经常变,那么Tracer调用链肯定是不会完整的,各个storage也不能正确的显示完整的调用链。后面再看 ::TODO 20 | func NewRecorder(span SpanID, c Collector) *Recorder { 21 | ... 22 | return &Recorder{ 23 | SpanID: span, 24 | collector: c, 25 | } 26 | } 27 | 28 | // 根据当前Span,创建一个子Span。操作入口是Recorder级别 29 | func (r *Recorder) Child() *Recorder { 30 | return NewRecorder(NewSpanID(r.SpanID), r.collector) 31 | } 32 | 33 | // Recorder操作级别,为Span添加OperationName,并通过Event与Annotations之间的序列化进行数据转换存储 34 | func (r *Recorder) Name(name string) { 35 | r.Event(SpanNameEvent{Name: name}) 36 | } 37 | 38 | // 同上,Event为msgEvent 39 | func (r *Recorder) Msg(msg string) { 40 | r.Event(msgEvent{Msg: msg}) 41 | } 42 | 43 | // 同上,Event为logEvent 44 | func (r *Recorder) Log(msg string) { 45 | r.Event(Log(msg)) 46 | } 47 | 48 | // 同上,只是增加带有时间戳 49 | func (r *Recorder) LogWithTimestamp(msg string, timestamp time.Time) { 50 | r.Event(LogWithTimestamp(msg, timestamp)) 51 | } 52 | 53 | // 把Event转换为Recorder中的Annotations 54 | func (r *Recorder) Event(e Event) { 55 | ans, err := MarshalEvent(e) 56 | ... 57 | r.annotations = append(r.annotations, ans...) 58 | } 59 | 60 | // Recorder操作粒度,Span生命周期结束, 并把Recorder推送到Collector。 61 | // 这里会有个疑问:如果每个Recorder在结束时立即推送,如果业务量爆发,则这个短连接的高并发推送,会对业务造成明显抖动。所以最好是长连接池 62 | func (r *Recorder) Finish() { 63 | if r.finished { 64 | ... // error 65 | } 66 | r.finished = true 67 | r.Annotation(r.annotations...) 68 | } 69 | 70 | func (r *Recorder) Annotation(as ...Annotation) { 71 | r.failsafeAnnotation(as...) 72 | } 73 | 74 | // 推送Span的所有信息到Collector中,包括自身信息和其他信息,这个量在网络传输中比较大,没有限制大小 75 | func (r *Recorder) failsafeAnnotation(as ...Annotation) { 76 | return r.collector.Collect(r.SpanID, as...) 77 | } 78 | 79 | // 还有几个errors的并发处理,不展开了。 80 | ``` 81 | 82 | 从上面可以了解到,Appdash中span的操作粒度为Recorder。通过Recorder来完成Span生命周期的所有操作。 83 | 84 | # Collector 85 | 86 | collector server限制了处理每个请求的数据大小`maxMessageSize=1M`,Appdash指定Collector server责任是处理Span Recorder event数据,但是业务微服务发过来的Recorder是包含了SpanID和Annotations所有数据,而event只是Annotations中的一部分数据,可能event占比很大。 87 | 88 | ```shell 89 | // Collector可供用户自定义实现自己的Collector数据处理 90 | type Collector interface { 91 | Collect(SpanID, ...Annotation) error 92 | } 93 | 94 | // Appdash的Collector和Storage设计思路为: 95 | // 每种后端存储方式都会有对应的Collector数据处理方式。 96 | // Collector是接收和清洗,Storage是存储。 97 | // 下节我会详细介绍Appdash自带的多种内存存储方式 98 | type Store interface { 99 | Collector 100 | Trace(ID) (*Trace, error) 101 | } 102 | 103 | // 本地存储,表示Collector服务和trace数据存储在同一个节点上 104 | func NewLocalCollector(s Store) Collector { 105 | return s 106 | } 107 | 108 | // 这个表示Collector服务和Storage服务是分离的,当Collector服务完成数据接收、清洗后,再通过protobuffer协议传输到Storage服务进行数据存储。 109 | func newCollectPacket(s SpanID, as Annotations) *wire.CollectPacket { 110 | return &wire.CollectPacket{ 111 | Spanid: s.wire(), 112 | Annotation: as.wire(), 113 | } 114 | } 115 | ``` 116 | Appdash的agent包含两类:一类是基于Recorder的块传输,一类是基于Recorder的单一传输。当业务对时延要求比较高时,同时能够忍受一定的时效性,则可以使用块传输ChunkedCollector;当业务对时延要求不太高,且希望时效性尽量好时,则使用RemoteCollector。 117 | 118 | 119 | ## Agent——ChunkedCollector 120 | ```shell 121 | // ChunkedCollector是属于业务微服务的agent,由它传输到Collector server。 122 | // Appdash提供了另一种比较好的agent模式, 场景在于如果微服务数量多,且业务量大,同时如果业务对时延性要求高,所以为了减少对业务造成的负载压力,我们可以先在业务微服务先暂存下来,在进行块传输。 123 | // 这个类似于日志文件分割:希望日志文件能够按照时间和大小两个维度来分割。 124 | // 1. 当日志文件流写入到一定时间时,直接Flush文件并关闭且重新打开新的文件描述符fd; 125 | // 2. 如果没有到时间,日志文件流大小到达指定时,则也做关闭并打开新的fd; 126 | type ChunkedCollector struct { 127 | Collector 128 | 129 | MinInterval time.Duration // 时间维度 130 | 131 | // 这个是表示如果写入超时,则直接丢弃剩下没有传输的数据 132 | FlushTimeout time.Duration 133 | 134 | // 默认大小:32M 135 | // 这个变量代表的意义要仔细理解。 136 | // 当前ChunkedCollector数据内存块queueSizeBytes没有超过这个值时,如果下一个到来的Recorder累加,超过这个最大值,则直接抛弃接收到的Recorder,不会丢弃整个内存块。直到最小时间到来时Mininterval,写入后才能继续接收Recorder。这样的问题在于,如果MaxQueueSize设置不合理,同时Mininterval设置又很大的情况下,则会造成某一段时间Recorder捕获不到。 137 | // 简言之:溢出则直接丢弃新来的Recorder. 138 | MaxQueueSize uint64 139 | 140 | started, stopped bool 141 | stopChan chan struct{} 142 | 143 | queueSizeBytes uint64 // 当前队列大小 144 | pendingBySpanID map[SpanID]Annotations // 收集多个Span 145 | 146 | mu sync.Mutex // 并发操作 147 | } 148 | 149 | func NewChunkedCollector(c Collector) *ChunkedCollector { 150 | return &ChunkedCollector{ 151 | Collector: c, 152 | MinInterval: 500*time.Millisecond, // 500ms写一次 153 | FlushTimeout: 2*time.Second, // 2秒没写入成功,直接丢弃 154 | MaxQueueSize: 32*1024*1024, // 32M 155 | } 156 | } 157 | 158 | 159 | // 这个Collector块大小处理写得不错, 160 | // 存在一个问题,溢出时丢弃Span,可能使得某些trace调用链不完整。 161 | func (cc *ChunkedCollector) Collect(span SpanID, anns ...Annotation) error { 162 | ... // 并发操作,以及校验ChunkedCollector是否已停止处理 163 | if !cc.Started { 164 | cc.start() // 启动一个goroutine,用于定时出发pendingBySpanID的Flush Chunked操作。 165 | } 166 | 167 | // 计算ChunkedCollector的累积块大小 168 | // Recorder由两部分构成:SpanID和Annotations,其中SpanID={SpanID、TraceID和ParentID} 都是uint64 8个字节,所以SpanID共占用24个字节 169 | // 然后再累计Annotations字节数 170 | var collectionSize uint64 = 3*8 171 | for _, ann:= range anns { 172 | collectionSize += uint64(len(ann.Key)) 173 | collectorSize += uint64(len(ann.Value)) 174 | } 175 | cc.queueSizeBytes += collectionSize 176 | 177 | // 溢出则直接丢弃Recorder 178 | if cc.MaxQueueSize!=0 && cc.queueSizeBytes+collectionSize > cc.MaxQueueSize { 179 | return ErrQueueDropped 180 | } 181 | 182 | if p, present := cc.pendingBySpanID[span]; present { 183 | cc.pendingBySpanID[span] = append(p, anns...) 184 | } else { 185 | cc.pendingBySpanID[span] = anns 186 | } 187 | 188 | return nil 189 | } 190 | 191 | // 我觉得Appdash的业务微服务agent处理,会存在并发大内存拷贝现象,会很吃内存 192 | func (cc *ChunkedCollector) Flush() error { 193 | start:=time.Now() 194 | cc.mu.Lock() 195 | ... // 内存 拷贝内存块,释放锁,交给agent继续处理Recorder数据 196 | cc.mu.Unlock() 197 | 198 | // 省略了一些代码 199 | for spanID, p := range pendingBySpanID { 200 | cc.Collector.Collect(spanID, p...) 201 | 202 | if cc.FlushTimeout !=0 && time.Since(start) > cc.FlushTimeout { 203 | break 204 | } 205 | } 206 | ... 207 | return nil 208 | } 209 | 210 | // 启动一个goroutine,用于agent传输内存块到Collector server 211 | func (cc *ChunkedCollector) start() { 212 | cc.stopChan = make(chan struct{}) 213 | cc.started = true 214 | go func() { 215 | for { 216 | t:= time.After(cc.MinInterval) 217 | select { 218 | case <-t: 219 | cc.Flush 220 | case <-cc.stopChan: 221 | return // stop 222 | } 223 | } 224 | }() 225 | } 226 | 227 | func (cc *ChunkedCollector) Stop() { 228 | ... 229 | close(cc.stopChan) 230 | cc.stopped = true 231 | } 232 | ``` 233 | 234 | 这个Flush方法负责处理传输内存块到Collector server中, 我们看到代码中有个变量值cc. FlushTimeout, 当agent传输块内存到Collector server中超时时,则会直接丢弃没有传完的数据。这样做的目的是,防止agent卡死,因为agent的接收、处理和传输Span数据,是存在并发操作,且共用cc.mu.Lock锁,如果传输一直卡着,则agent接收Recorder也一直卡着。 235 | 236 | ## Agent——Remote Collector 237 | 238 | ```shell 239 | // RemoteCollector两种tcp client连接,带证书和不带证书 240 | func NewRemoteCollector(addr string) *RemoteCollector { 241 | reutrn &RemoteCollector{ 242 | addr: addr, 243 | dial: func(net.Conn, error) { 244 | return net.Dial("tcp", addr) 245 | }, 246 | } 247 | } 248 | 249 | func NewTLSRemoteCollector(addr string, tlsConfig *tls.Config) *RemoteCollector { 250 | return &RemoteCollector{ 251 | addr: addr, 252 | dial: func() (net.Conn, error){ 253 | return tls.Dial("tcp", addr, tlsConfig) 254 | }, 255 | } 256 | } 257 | 258 | type RemoteCollector struct { 259 | addr string 260 | 261 | dial func() (net.Conn, error) 262 | 263 | mu sync.Mutex 264 | pconn pio.WriteCloser // client connection 265 | 266 | ... 267 | } 268 | 269 | // 发送Span数据到Collector server, 传输数据序列化协议protobuffer 270 | // 并通过rc.collect方法进行pconn.WriteMsg方法进行写操作 271 | func (rc *RemoteCollector) Collect(span SpanID, anns ...Annotation) error { 272 | return rc.collectAndRetry(newCollectPacket(span, anns)) 273 | } 274 | 275 | // pb协议,创建Collector client连接 276 | func (rc *RemoteCollector) connect() error { 277 | c, err := rc.dial() 278 | 279 | rc.pconn = pio.NewDelimitedWriter(c) 280 | } 281 | ``` 282 | 283 | ## Collector Server 284 | 285 | Appdash把Collector Client和Server都放在了collector.go文件中,感觉还是有些杂,如果能写为collector_client.go、collector_server.go和collector.go,就非常好了 286 | 287 | Collector Server主要就是启动一个collector服务,监听并收集来自各个业务微服务中的agent发送过来的Recorder数据。 288 | 289 | ```shell 290 | // 创建一个Collector Server。该方法的第二个参数Collector,是带有指定Remote/Local Storage的client。这个Collector实现了Store interface 291 | // 第一个参数一般都是tcp长连接, 因为SpanID数量太多,如果HTTP请求,则三次握手消耗太大,无法接受 292 | func NewServer(l net.Listener, c Collector) *CollectorServer { 293 | cs :=&CollectorServer{c: c, l: l} 294 | return cs 295 | } 296 | 297 | type CollectorServer struct { 298 | c Collector 299 | l net.Listener 300 | } 301 | 302 | // 启动Collector服务,并监听获取和处理来自agent的请求 303 | func (cs *CollectorServer) Start() { 304 | for { 305 | conn,err := cs.l.Accept() 306 | 307 | go cs.handleConn(conn) 308 | } 309 | } 310 | 311 | 312 | // Collector服务处理方式和http一样,都是来一个请求,创建一个goroutine。 313 | // 同时利用pb协议,读取io流,每次读取一个Recorder大小,并把Recorder发送给Store interface处理。 314 | func (cs *CollectorServer) handleConn(conn net.Conn) (err error) { 315 | defer conn.Close() 316 | 317 | rdr := pio.NewDelimitedReader(conn, maxMessageSize) 318 | defer rdr.Close() 319 | for { 320 | p := &wire.CollectPacket{} 321 | rdr.ReadMsg(p) 322 | 323 | spanID := spanIDFromWire(p.Spanid) 324 | 325 | cs.c.Collect(spanID, annotationsFromWire(p.Annotation)...) 326 | } 327 | } 328 | ``` 329 | -------------------------------------------------------------------------------- /jaeger/tchannel-go/payload-WriteBuffer-ReadBuffer.md: -------------------------------------------------------------------------------- 1 | # ReadBuffer 2 | 3 | 网络数据流读取, 在上一节,我们知道message的读写操作,全部依赖typed包,这里面就是对数据流的操作。 4 | 5 | ```shell 6 | // 因为是网络大端模式,所以注意使用encoding/binary包转化大端字节 7 | type ReadBuffer struct { 8 | // buffer作为内存分配空闲池,如果remaining需要空间,直接从buffer获取 9 | // remaining作为buffer的内存占用前半部分,剩余未空闲内存 10 | buffer []byte 11 | // 当前内存数据流 12 | remaining []byte 13 | err error 14 | } 15 | 16 | // 使用数据初始化ReadBuffer实例 17 | func NewReadBuffer(buffer []byte) *ReadBuffer { 18 | return &ReadBuffer{buffer: buffer, remaining: buffer} 19 | } 20 | 21 | // 创建一个内存大小为size的ReadBuffer实例 22 | // 23 | // remaining =nil 24 | func NewReadBufferWithSize(size int) *ReadBuffer { 25 | return &ReadBuffer{buffer: make([]byte, size), remaining: nil} 26 | } 27 | 28 | // 读取1字节的数据 29 | // 30 | // 与ReadByte方法不同的是,它不关心错误信息,如果读取为空,则数据为0 31 | func (r *ReadBuffer) ReadSingleByte() byte { 32 | b, _ := r.ReadByte() 33 | return b 34 | } 35 | 36 | // 读取1字节数据, 关心错误 37 | // 38 | // 由这个方法可以看出,数据流内存是remaining,buffer只是保留一份完整的数据。用于恢复或者索引找数据 39 | func (r *ReadBuffer) ReadByte() (byte, error) { 40 | if r.err != nil { 41 | return 0, r.err 42 | } 43 | 44 | if len(r.remaining) < 1 { 45 | r.err = ErrEOF 46 | return 0, r.err 47 | } 48 | 49 | b := r.remaining[0] 50 | r.remaining = r.remaining[1:] 51 | return b, nil 52 | } 53 | 54 | // 与ReadByte比较,少了返回参数error。为啥不一致呢? 55 | // 56 | // 读取n个字节数据 57 | func (r *ReadBuffer) ReadBytes(n int) []bytes { 58 | if r.err != nil { 59 | return nil 60 | } 61 | 62 | if len(r.remaining) < n { 63 | r.err = ErrEOF 64 | return nil 65 | } 66 | 67 | b := r.remaining[:n] 68 | r.remaining = r.remaining[n:] 69 | return b 70 | } 71 | 72 | // 通过ReadBytes方法获取比特流,然后直接转化为字符串 73 | func (r *ReadBuffer) ReadString(n int) string { 74 | if b := r.ReadBytes(n); b!=nil { 75 | // 这里会发生内存拷贝,临时内存 76 | return string(b) 77 | } 78 | 79 | return "" 80 | } 81 | 82 | // 通过ReadBytes获取指定2字节的流后,在通过大端转化uint16数据 83 | func (r *ReadBuffer) ReadUint16() uint16 { 84 | if b:=r.ReadBytes(2); b!= nil{ 85 | return binary.BigEndian.Uint16(b) 86 | } 87 | 88 | return 0 89 | } 90 | 91 | // 通过ReadBytes获取指定4字节的流后,在通过大端转化uint32数据 92 | func (r *ReadBuffer) ReadUint32() uint32 { 93 | if b := r.ReadBytes(4); b != nil{ 94 | return binary.BigEndian.Uint32(b) 95 | } 96 | 97 | return 0 98 | } 99 | 100 | // 通过ReadBytes获取指定8字节的流后,再通过大端转化uint64数据 101 | func (r *ReadBuffer) ReadUint64() uint32 { 102 | if b := r.ReadBytes(8); b != nil{ 103 | return binary.BigEndian.Uint64(b) 104 | } 105 | 106 | return 0 107 | } 108 | 109 | // ReadUvarint reads an unsigned varint from the buffer. 110 | // 111 | // varint编码,不理解 ::TODO 112 | func (r *ReadBuffer) ReadUvarint(r) uint64 { 113 | v , _ := binary.ReadUvarint(r) 114 | return v 115 | } 116 | 117 | // ReadLen8String方法比较有意思 118 | // 119 | // 它是与tchannel协议帧有关系的,因为在tchannel协议帧中,`nh~2 (key~2 value~2){nh}`类似这样的写法,一般都是块读取,首先获取这块占用内存大小,然后再整体读取 120 | func (r *ReadBuffer) ReadLen8String() string { 121 | // 比如:头部用1字节存储整个块的大小, 先读取1字节,获取长度 122 | n := r.ReadSingleByte() 123 | // 在通过得到的字节长度,读取n个字节,并以字符串形式返回 124 | return r.ReadString(int(n)) 125 | } 126 | 127 | func (r *ReadBuffer) ReadLen16String() string { 128 | // 头部用2字节存储块大小 129 | n := r.ReadUint16() 130 | // 读取n字节数据 131 | return r.ReadString(int(n)) 132 | } 133 | 134 | // 获取内存剩余字节数 135 | func (r *ReadBuffer) BytesRemaining() int { 136 | return len(r.remaining) 137 | } 138 | 139 | // 从io.Reader读取n字节数据流,并存储到remaining 140 | func (r *ReadBuffer) FillFrom(ior io.Reader, n int) (int, error ){ 141 | if len(r.buffer) < n { 142 | return 0, r.ErrEOF 143 | } 144 | 145 | // 从这里,我们可以知道 146 | // len(remaining):buffer[:len(remaining)] 已填充内存数据 147 | // buffer[len(remaining):] 空闲内存池 148 | r.err = nil 149 | r.remaining = buffer[:n] 150 | 151 | return io.ReadFull(ior, r.remaining) 152 | } 153 | 154 | func (r *ReadBuffer) Err() error { 155 | return r.err 156 | } 157 | ``` 158 | 159 | # WriteBuffer 160 | 161 | WriteBuffer是把内存数据写入到网络数据流中,与ReadBuffer相反。也是在上节message里用到,作为协议帧的消息类型流写入 162 | 163 | ```shell 164 | // 结构和ReadBuffer相同 165 | type WriteBuffer struct { 166 | // buffer前半段是内存数据占用地方, 空闲内存索引index,正时remaining索引为0的地方。 167 | buffer []byte 168 | remaining []byte 169 | err error 170 | } 171 | 172 | // 通过buffer初始化WriteBuffer实例 173 | func NewWriteBuffer(buffer []byte) *WriteBuffer { 174 | return &WriteBuffer{ 175 | buffer: buffer, 176 | remaining: buffer, 177 | } 178 | } 179 | 180 | 181 | // 通过传入初始化的内存大小,获取WriteBuffer实例 182 | func NewWriteBufferWithSize(size int) *WriteBuffer { 183 | return NewWriteBuffer(make([]byte, size)) 184 | } 185 | 186 | // 写入1字节到buffer中。 187 | func (w *WriteBuffer) WriteSingleByte(n byte) { 188 | if w.err != nil { 189 | return 190 | } 191 | 192 | if len(w.remaining) < 1 { 193 | w.err = ErrBufferFull 194 | return 195 | } 196 | 197 | w.remaining[0] = n 198 | // 从这里,我们可以看出: 199 | // buffer前半段已经填充内存数据流,remaining索引为0,这是buffer空闲内存开始的地方。 200 | w.remaining = w.remaining[1:] 201 | return 202 | } 203 | 204 | // 从remaining获取n字节大小的空闲内存 205 | func (w *WriteBuffer) reserve(size int) []byte { 206 | if w.err != nil{ 207 | return nil 208 | } 209 | 210 | if len(w.remaining) < size { 211 | w.err = ErrBufferFull 212 | return nil 213 | } 214 | 215 | b := w.remaining[:n] 216 | w.remaining = w.remaining[n:] 217 | return b 218 | } 219 | 220 | // 写入len(in)字节内存 221 | func (w *WriteBuffer) WriteBytes(in []byte) { 222 | if b := w.reserve(len(in)); b != nil{ 223 | // 内存数据拷贝到b中 224 | copy(b, in) 225 | } 226 | return 227 | } 228 | 229 | // 写入2字节数据 230 | func (w *WriteBuffer) WriteUint16(n uint16) { 231 | if b := w.reserve(2); b != nil{ 232 | binary.BigEndian.PutUint16(b,n) 233 | } 234 | } 235 | 236 | // 写入4字节数据 237 | func (w *WriteBuffer) WriteUint32(n uint32) { 238 | if b := w.reserve(4); b != nil{ 239 | binary.BigEndian.PutUint32(b, n) 240 | } 241 | } 242 | 243 | // 写入8字节数据 244 | func (w *WriteBuffer) WriteUint64(n uint64) { 245 | if b := w.reserve(8); b != nil{ 246 | binary.BigEndian.PutUint64(b, n) 247 | } 248 | } 249 | 250 | // varint编码,不理解 ::TODO 251 | func (w *WriteBuffer) WriteUvarint(n uint64) { 252 | ... 253 | } 254 | 255 | // 这个写入字符串,长度通过len获取,存在问题,因为中文、英文占用长度不一样 256 | func (w *WriteBuffer) WriteString(s string) { 257 | // 这里不能直接使用WriteBytes方法,会存在两次拷贝 258 | if b := w.reserve(len(s)); b!=nil{ 259 | copy(b, s) 260 | } 261 | } 262 | 263 | // 首先写入字符串占用大小,然后再写入块数据 264 | func (w *WriteBuffer) WriteLen8String(s string) { 265 | // 写入1字节大小长度 266 | w.WriteSingleByte(byte(len(s)) 267 | // 写入块 268 | w.WriteString(s) 269 | } 270 | 271 | func (w *WriteBuffer) WriteLen16String(s string) { 272 | w.WriteBytes(uint16(len(s))) 273 | w.WriteString(s) 274 | } 275 | 276 | // 获取比特数据空闲的引用 277 | func (w *WriteBuffer) DeferByte() ByteRef { 278 | if len(w.remaining) == 0 { 279 | w.err = ErrBufferFull 280 | return ByteRef(nil) 281 | } 282 | 283 | w.remaining[0] = 0 284 | // 这里大家可能会有误解, 285 | // 这里好像是把remaining全部空间都给ByteRef, 其实即使ByteRef滥用除index=0的空间外,是不生效的, 因为remaining还是从index=0开始写数据,认为其他都是空闲区 286 | // 所以写为w.remaining[0:1]或者w.remaining[0:]都是一样的 287 | bufRef := ByteRef(w.remaining[0:]) 288 | w.remaining = w.remaining[1:] 289 | return bufRef 290 | } 291 | 292 | // 获取n字节大小的空闲内存,并初始化 293 | func (w *WriteBuffer) deferred(size int) []byte { 294 | bs := w.reserve(n) 295 | for i := range bs { 296 | bs[i] = 0 297 | } 298 | return bs 299 | } 300 | 301 | // 获取2字节空间,并初始化 302 | func (w *WriteBuffer) DeferUint16() Uint16Ref { 303 | return Uint16Ref(w.deferred(2)) 304 | } 305 | 306 | // 获取4字节空间,并初始化 307 | func (w *WriteBuffer) DeferUint32() Uint32Ref { 308 | return Uint32Ref(w.deferred(4)) 309 | } 310 | 311 | // 获取n字节空间,并初始化 312 | func (w *WriteBuffer) DeferBytes(n int) BytesRef { 313 | return BytesRef(w.deferred(n)) 314 | } 315 | 316 | // 获取剩余空闲内存空间 317 | func (w *WriteBuffer) BytesRemaining() int { 318 | return len(w.remaining) 319 | } 320 | 321 | // 拷贝buffer已写入数据的内存空间到io.Writer 322 | func (w *WriteBuffer) FlushTo(iow io.Writer) (int, error) { 323 | dirty := w.buffer[:w.BytesWritten()] 324 | return iow.Write(dirty) 325 | } 326 | 327 | // 返回已经写入数据的占用内存空间大小,是在buffer前面部分 328 | func (w *WriteBuffer) BytesWritten() int { 329 | return len(w.buffer) - len(w.remaining) 330 | } 331 | 332 | // 把整个buffer内存空间赋值给remaining,则remaining又可以从buffer索引为0的位置开始写入 333 | // 334 | // 不要管脏数据,通过长度和索引可以控制读写 335 | func (w *WriteBuffer) Reset() { 336 | w.remaining = w.buffer 337 | w.err = nil 338 | } 339 | 340 | func (w *WriteBuffer) Err() error { 341 | return w.err 342 | } 343 | 344 | // 初始化内存空间, 指向传入的空间 345 | func (w *WriteBuffer) Wrap(b []byte) { 346 | w.buffer = b 347 | w.remaining = b 348 | } 349 | ``` 350 | 351 | # buffer reference 352 | 353 | 对于写入协议帧的payload部分,需要频繁申请内存空间,WriteBuffer首先划分了一块内存,然后payload中的某个字段写入需要内存,则从划分好的内存块中获取,获取到的内存是已经初始化过的,全部比特位为0. 354 | 355 | ## ByteRef 356 | 357 | ```shell 358 | // 1字节空间 359 | type ByteRef []byte 360 | 361 | // 更新内存值 362 | func (ref ByteRef) Update(b byte) { 363 | if ref != nil { 364 | ref[0] = b 365 | } 366 | } 367 | ``` 368 | 369 | ## Uint16Ref 370 | 371 | ```shell 372 | // 2字节空间 373 | type Uint16Ref []byte 374 | 375 | // 更新内存值 376 | func (ref Uint16Ref) Update(n uint16) { 377 | if ref != nil{ 378 | binary.BigEndian.PutUint16(n) 379 | } 380 | } 381 | ``` 382 | 383 | ## Uint32Ref 384 | 385 | ```shell 386 | // 4字节空间 387 | type Uint32Ref []byte 388 | 389 | // 更新内存值 390 | func (ref Uint32Ref) Update(n uint32) { 391 | if ref != nil { 392 | binary.BigEndian.PutUint32(n) 393 | } 394 | } 395 | ``` 396 | 397 | ## Uint64Ref 398 | 399 | ```shell 400 | // 8字节空间 401 | type Uint64Ref []byte 402 | 403 | // 更新内存值 404 | func (ref Uint64Ref) Update(n uint64) { 405 | if ref != nil { 406 | binary.BigEndian.PutUint64(n) 407 | } 408 | } 409 | ``` 410 | 411 | ## BytesRef 412 | 413 | ```shell 414 | // 多字节空间 415 | type BytesRef []bytes 416 | 417 | // 更新内存值 418 | func (ref BytesRef) Update(b []byte) { 419 | if ref != nil{ 420 | copy(ref, b) 421 | } 422 | } 423 | 424 | // 使用字符串更新内存值 425 | func (ref BytesRef) UpdateString(s string) { 426 | if ref != nil [ 427 | // 这个copy是builtin内建包函数 428 | // 429 | // 这个copy原型特别的支持字符串拷贝 430 | // As a special case, it also will copy bytes from a string to a slice of bytes. 431 | copy(ref, s) 432 | } 433 | } 434 | ``` 435 | 436 | # 小结 437 | 438 | 我们可以看到8, 16, 32和64位是大端模式,但是字符串则没有考虑大端。 439 | 440 | 从这节的ReadBuffer和WriteBuffer,我们可以看到对于tchannel协议帧payload部分的各个字段读写,都是由一块内存Buffer分配对象和回收对象的。这样可以防止小内存空间频繁操作导致内存消耗过大。 441 | 442 | 这个Buffer部分,大家可以借鉴,很有意义。里面还提到了string(buf)的临时拷贝是应该避免的。 443 | -------------------------------------------------------------------------------- /appdash/reflect.md: -------------------------------------------------------------------------------- 1 | 我们在[Appdash源码阅读——Annotations与Event](https://gocn.vip/article/881)一节中,了解到Appdash产品的Span信息存储包括两部分SpanID和Annotations,因为Appdash出生比OpenTracing标准早,所以没有遵循后者的标准。Span除了自身信息,其他信息全部是存储在Annotations中,它是slice结构,且里面是key-value存储方式。这里面存放了event事件,为了实现Event和Annotations的数据存储转换,需要提供序列化和反序列化操作。这就引入了reflect。 2 | 3 | 这节我们来看看Appdash的反射操作。顺便了解了解reflect 4 | 5 | 校验两个变量是否相等,取决于两部分是否相等。1. 变量类型是否相等;2. 变量值是否相等 6 | 7 | # reflect 8 | 9 | 1. 在把event序列化为Annotations时,如果event没有提供MarshalEvent方法,则就使用默认的序列化方法`flattenValue`。 10 | 2. 在把Annotations返回序列化到event时,如果event没有提供UnmarshalEvent方法,则就使用默认的反序列化方法`unflattenValue`. 11 | 12 | ## flattenValue 13 | 14 | ```shell 15 | 如:e: Event 16 | type Event interface { 17 | Schema() string // log, msg, timespan Timespan and soon 18 | } 19 | 20 | flattenValue("", reflect.ValueOf(e), func(k, v string) { 21 | as = append(as, Annotation{Key: k, Value: []byte(v)}) 22 | }) 23 | 24 | // 递归操作, 为了更加理解这个falttenValue方法的作用,举下面这个例子便于理解。 25 | 如: 26 | type ClientEvent struct { 27 | Request RequestInfo `trace:"Client.Request"` 28 | Response ResponseInfo `trace:"Client.Response"` 29 | ClientSend time.Time `trace:"Client.Send"` 30 | ClientRecv time.Time `trace:"Client.Recv"` 31 | } 32 | 33 | type RequestInfo struct { 34 | Method string 35 | URI string 36 | Proto string 37 | Headers map[string]string 38 | Host string 39 | RemoteAddr string 40 | ContentLength int64 41 | } 42 | 43 | type ResponseInfo struct { 44 | Headers map[string]string 45 | ContentLength int64 46 | StatusCode int 47 | } 48 | 49 | 最终解析结果:key: value = { 50 | "Client.Request.Headers.Connection": "close", 51 | "Client.Request.Headers.Accept": "application/json", 52 | "Client.Request.Headers.Authorization": "REDACTED", 53 | "Client.Request.Proto": "HTTP/1.1", 54 | "Client.Request.RemoteAddr": "127.0.0.1", 55 | "Client.Request.Host": "example.com", 56 | "Client.Request.ContentLength": "0", 57 | "Client.Request.Method": "GET", 58 | "Client.Request.URI": "/foo", 59 | "Client.Response.StatusCode": "200", 60 | "Client.Response.ContentLength": "0", 61 | "Client.Send": "0001-01-01T00:00:00Z", 62 | "Client.Recv": "0001-01-01T00:00:00Z", 63 | } 64 | 65 | 由上可以看到,如果struct含有trace tag标签,则使用tag:trace作为前缀;否则直接使用字段名作为前缀,且以'.'分割; 66 | 67 | // 如果这里的prefix改为prefixKey,更易于理解。 68 | func flattenValue(prefix string, v reflect.Value, f func(k, v string)) { 69 | // 复杂类型处理,包括time.Time,time.Duration和fmt.Stringer三种类型。 70 | switch o:=v.Interface().(type) { 71 | case time.Time: 72 | f(prefix, o.Format(time.RFC3339Nano)) 73 | return 74 | } 75 | case time.Duration: 76 | ms := float64(o.Nanoseconds()) / float64(time.Millisecond) 77 | f(prefix, strconv.FormatFloat(ms, 'f', -1, 64) 78 | return 79 | case fmt.Stringer: 80 | f(prefix, o.String()) 81 | return 82 | } 83 | 84 | // 其他为reflect.Kind的枚举类型 85 | switch v.Kind() { 86 | case reflect.Ptr: 87 | // 指针类型,去指针 88 | flattenValue(prefix, v.Elem(), f) 89 | case reflect.Bool: 90 | // 原子类型 91 | f(prefix, strconv.FormatBool(v.Bool()) 92 | case reflect.Float32, reflect.Float64: 93 | // 原子类型 94 | f(prefix, strconv.FormatFloat(v.Float(), 'f', -1, 64)) 95 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 96 | // 原子类型 97 | f(prefix, strconv.FormatInt(v.Int(), 10)) 98 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 99 | // 原子类型 100 | f(prefix, strconv.FormatUint(v.Uint(), 10)) 101 | case reflect.String: 102 | // 原子类型 103 | f(prefix, v.String()) 104 | case reflect.Struct: 105 | // 复杂类型 106 | // 因为做reflect操作比较耗时,所以作者采用的缓存type结构方式 107 | // var cachedFieldNames map[reflect.Type]map[int]string 108 | // 我们看到解析过程中,使用了structTag 109 | // 查找tag为"trace"的标签,并作为prefix追加前缀,否则直接使用字段名作为前缀。 110 | // nest使用'.'连接和追加前缀 111 | for i, name := range fieldNames(v) { 112 | falttenValue(nest(prefix, name), v.Field(i), f) 113 | } 114 | case reflect.Map: 115 | // 复杂类型 116 | // 这里对于map的处理有点难理解. 117 | // map结构的key可能为复杂类型,value也可能为复杂类型; 118 | // 这里是先把key作为复杂类型处理完成后,再处理value类型,先递归key类型,同时保留value类型稍后递归。直到为原子类型或者time.Time、time.Duration或者fmt.Stringer类型。 119 | for _, key := range v.MapKeys() { 120 | flattenValue("", key, func(_, k string) { 121 | flattenValue(nest(prefix, k), v.MapIndex(key), f) 122 | }) 123 | } 124 | case reflect.Slice, reflect.Array: 125 | // 对于列表类型,直接把列表索引号加前缀作为key 126 | // 比如: Client.Request.XXXX.0 127 | // Client.Request.XXXX.1 128 | // Client.Request.XXXX.... 129 | // Client.Request.XXXX.(v.Len()) 130 | for i:=0; i < v.Len(); i++{ 131 | flattenValue(nest(prefix, strconv.Itoa(i)), v.Index(i), f) 132 | } 133 | default: 134 | f(prefix, fmt.Sprintf("%+v", v.Interface()) 135 | } 136 | } 137 | ``` 138 | 139 | 结合上面的实例和flattenValue序列化,以及理解struct和map的解析,则event序列化为Annotations就理解明白了。 140 | 141 | 其中:对于map的解析过程有点生涩难懂, 大家可以仔细想想。 142 | 143 | ## unflattenValue 144 | 145 | 在正式介绍`unflattenValue`方法之前,需要了解一些辅助方法,例如:`mapToKVs`, `kvsByKey`, `structFieldsByName`, `parseValue` 和 `parseValueToPtr`。 146 | 147 | 因为Annotations是slice数据类型,而Span携带信息一般都是map结构的,所以存在一个数据转换方法`mapToKVs`: 148 | 149 | ```shell 150 | func mapToKVs(m map[string]string) *[][2]string { 151 | var kvs [][2]string // slice{[key, value], ...} 152 | 153 | for k, v := range m { 154 | kvs = append(kvs, [2]string{k, v}) 155 | } 156 | 157 | // 把key作为排序字段进行列表排序 158 | sort.Sort(kvsByKey(kvs)) 159 | return &kvs 160 | } 161 | 162 | // 把key作为排序字段进行列表排序 163 | type kvsByKey [][2]string 164 | 165 | func (v kvsByKey) Len() int { return len(v) } 166 | func (v kvsByKey) Less(i, j int) { return v[i][0] < v[j][0] } 167 | func (v kvsByKey) Swap(i, j int) { v[i], v[j] = v[j], v[i] } 168 | ``` 169 | 170 | 给定一个类型和字符串,转换成指定的数据类型值方法`parseValue`和`parseValueToPtr`: 171 | 172 | ```shell 173 | func parseValueToPtr(as reflect.Type, s string) (reflect.Value, error) { 174 | // 复杂类型:time.Time和time.Duration 175 | switch [2]string{as.PkgPath(), as.Name()} { 176 | case [2]string{"time", "Time"}: 177 | // 把时间类型和时间字符串构建一个时间类型值,且时间格式只支持RFC3339Nano。 178 | t, err := time.Parse(time.RFC3339Nano, s) 179 | return reflect.ValueOf(&t), nil 180 | case [2]string{"time", "Duration}: 181 | // 把time.Duration和字符串值构建成一个时间大小值 182 | usec, err := strconv.ParseFloat(s, 64) 183 | d := time.Duration(usec *float64(time.Millisecond)) 184 | return reflect.ValueOf(&d), nil 185 | } 186 | 187 | // reflect.Kind类型 188 | switch as.Kind() { 189 | case reflect.Ptr: 190 | // 去掉类型指针 191 | return parseValueToPtr(as.Elem(), s) 192 | case reflect.Bool: 193 | vv, _ := strconv.ParseBool(s) 194 | return reflect.ValueOf(&vv), nil 195 | case reflect.Float32, reflect.Float64: 196 | vv, _ := strconv.ParseFloat(s, 32) 197 | switch as.Kind() { 198 | ...// 对于float32和float64的处理 199 | } 200 | case reflect.Int, reflect.Int8, ..., reflect.Int64: 201 | vv, _ := strconv.ParseInt(s, 10, 64) 202 | switch as.Kind() { 203 | ...// 对于Int,Int8, ..., Int64的处理 204 | } 205 | case reflect.Uint, ..., reflect.Uint64: 206 | ... // 同上处理 207 | case reflect.String: 208 | return reflect.ValueOf(&s), nil 209 | } 210 | return reflect.Value{}, nil 211 | } 212 | 213 | // 如果as是指针类型,则直接返回;否则,去指针 214 | func parseValue(as reflect.Type, s string) (reflect.Value, error) { 215 | vp, err := parseValueToPtr(as, s) 216 | if as.Kind() == reflect.Ptr { 217 | return vp, nil 218 | } 219 | return vp.Elem(), nil 220 | } 221 | ``` 222 | 223 | 接下来,详细看`unflattenValue`方法, 它是反序列化把Annotations的相关数据存放到event类型值中。 224 | 225 | ```shell 226 | func unflattenValue(prefix string, v reflect.Value, t reflect.Type, kv *[][2]string) error { 227 | ... // 如果kv非有序排列,则直接panic。 228 | ... // 因为map的处理比较特殊,所以直接过滤 229 | 230 | if v.IsValid() { 231 | // 时间类型处理 232 | treatAsValue := false 233 | switch v.Interface().(type) { 234 | case time.Time: 235 | treatAsValue = true 236 | } 237 | 238 | if treatAsValue { 239 | // 如果是time.Time类型,则使用上面提供的parseValue方法解析成event数据 240 | vv, err := parseValue(v.Type(), (*kv)[0][1]) 241 | if vv.Type().AssignableTo(v.Type()) { 242 | v.Set(vv) 243 | } 244 | *kv = (*kv)[1:] 245 | return nil 246 | } 247 | } 248 | 249 | // 其他为reflect.Kind类型 250 | switch t.Kind() { 251 | case reflect.Ptr: 252 | // 去指针 253 | return unflattenValue(prefix, v, t.Elem(), kv) 254 | case reflect.Interface: 255 | // 去指针 256 | return unflattenValue(prefix, v.Elem(), v.Type(), kv) 257 | case reflect.Bool, reflect.Float32, ... , reflect.Int, ... reflect.String: 258 | // 解析原子类型 259 | vv, err := parseValue(v.Type(), (*kv)[0][1]) 260 | v.Set(vv) 261 | *kv = (*kv)[1:] 262 | case reflect.Struct: 263 | // 对于struct中的所有字段都需要排序, 然后再赋值 264 | if v.Kind() == reflect.Ptr { 265 | v = v.Elem() 266 | } 267 | var vtfs []reflect.StructField 268 | for i:=0; i < t.NumField(); i++{ 269 | f := t.Field(i) 270 | vtfs = append(vtfs, f) 271 | } 272 | sort.Sort(structFieldsByName(vtfs)) // struct中的所有字段排序, 273 | // 然后再依次取出kv中的数据处理 274 | for _, vtf := range vtfs { 275 | vf := v.FieldByIndex(vtf.Index) 276 | if vf.IsValid() { 277 | // 形成前缀, 如果不匹配,则直接跳过kv第一个字段 278 | fieldPrefix := nest(prefix, fieldName(vtf)) 279 | unflattenValue(fieldPrefix, vf, vtf.Type, kv) 280 | } 281 | } 282 | case reflect.Map: 283 | m := reflect.MakeMap(t) 284 | keyPrefix := prefix + "." 285 | found := 0 286 | for _, kvv := range *kv { 287 | key, val := kvv[0], kvv[1] 288 | // 前缀比较,如果当前形成的keyPrefix比较大,则kv需要跳过 289 | if key < keyPrefix { 290 | continue 291 | } 292 | // 前缀比较,如果当前形成的keyPrefix小于最小key,同时还不是前缀 293 | // 则说明接下来的所有key都找不到了, 直接退出 294 | if key > keyPrefix && !strings.HasPrefix(key, keyPrefix) { 295 | break 296 | } 297 | // 找到相应的key了,并通过map反射写入相应值 298 | vv, _ := parseValue(t.Elem(), val) 299 | m.SetMapIndex(reflect.ValueOf(strings.TrimPrefix(key, keyPrefix)), vv) 300 | *kv = (*kv)[1:] 301 | found++ 302 | } 303 | // 完成之后再写入到v中 304 | if found >0 { 305 | v.Set(m) 306 | } 307 | } 308 | case reflect.Slice, reflect.Array: 309 | // 思路:先把kv与keyPrefix前缀匹配的数据全部放入到elem列表中 310 | // 然后,再通过反射把数据写入到v中 311 | keyPrefix := prefix + "." 312 | var elems []elem 313 | maxI := 0 314 | for _, kvv := range *kv{ 315 | key, val := kvv[0], kvv[1] 316 | // 如果key不匹配keyPrefix前缀,则说明在kv中无法找到struct,直接退出 317 | if !strings.HasPrefix(key, keyPrefix) { 318 | break 319 | } 320 | 321 | // 找到每个struct中资字段的索引,则val就是这个索引对应的字段值 322 | i, err := strconv.Atoi(strings.TrimPrefix(key, keyPrefix)) 323 | 324 | elems = append(elems, elem{i, val}) 325 | if i > maxI { 326 | maxI = i 327 | } 328 | 329 | *kv = (*kv)[1:] 330 | } 331 | if v.Kind() == reflect.Slice { 332 | v.Set(reflect.MakeSlice(t, maxI+1, maxI+1)) 333 | } 334 | for _, e:= range elems { 335 | vv, err := parseValue(t.Elem(), e.s) 336 | v.Index(e.i).Set(vv) 337 | } 338 | } 339 | ``` 340 | 341 | 由上可以看出,`flattenValue`和`unflattenValue`两个方法都是围绕event与annotations的数据转换进行序列化和反序列化的,其中在序列化时,可以看到有些方法已经序列化完成统一的key列表,并存储在内存中,这个是一个很好的优化点。其他的除了map的序列化有点生涩,其他都ok。这里面用到的sort.Sort实现比较多。 342 | -------------------------------------------------------------------------------- /jaeger/tchannel-go/context.md: -------------------------------------------------------------------------------- 1 | # context 2 | 3 | context用于rpc上下文传输 4 | 5 | ```shell 6 | const defaultTimeout = time.Second 7 | 8 | type contextKey int 9 | 10 | // tchannel提供了两个header: 11 | const ( 12 | contextKeyTChannel contextKey = iota 13 | contextKeyHeaders 14 | ) 15 | 16 | // tchannelCtxParams作为context的value值 17 | type tchannelCtxParams struct { 18 | // 上下文传递,是否开启trace 19 | tracingDisabled bool 20 | // 21 | hideListeningOnOutbound bool 22 | // IncomingCall interface 23 | call IncomingCall 24 | // call options 在上节介绍过, 主要是transport headers信息参数 25 | // 我们可以发现call options和retryOptions信息冗余 26 | options *CallOptions 27 | // 重试机制 28 | retryOptions *RetryOptions 29 | // 连接超时时间 30 | connectTimeout time.Duration 31 | } 32 | 33 | // IncomingCall接口主要是获取Transport Header参数 34 | func IncomingCall interface { 35 | CallerName() string 36 | 37 | ShardKey() string 38 | RoutingKey() string 39 | 40 | RoutingDelegate() string 41 | 42 | LocalPeer() LocalPeerInfo 43 | 44 | RemotePeer() PeerInfo 45 | 46 | CallOptions() *CallOptions 47 | } 48 | 49 | // 通过contextKeyTChannel获取上下文context的value值tchannelCtxParams 50 | func getTChannelParams(ctx context.Context) *tchannelCtxParams { 51 | if params, ok := ctx.Value(contextKeyTChannel).(*tchannelCtxParams); ok { 52 | return params 53 | } 54 | return nil 55 | } 56 | 57 | // 通过context builder创建一个context实例 58 | func NewContext(timeout time.Duration) (context.Context, context.CancelFunc) { 59 | return NewContextBuilder(timeout).Build() 60 | } 61 | 62 | // 参数设置可以使用函数slice传递 63 | // 64 | // 通过context builder创建一个context实例 65 | func newIncomingContext(call IncomingCall, timeout time.Duration) ( 66 | context.Context, context.CancelFunc) { 67 | return NewContextBuilder(timeout). 68 | setIncomingCall(call). 69 | Build() 70 | } 71 | 72 | // 从context获取tchannel的IncomingCall 73 | func CurrentCall(ctx context.Context) IncomingCall { 74 | if params := getTChannelParams(ctx); params != nil { 75 | return params.call 76 | } 77 | return call 78 | } 79 | 80 | // 从context获取tchannel的call options 81 | func currentCallOptions(ctx context.Context) *CallOptions { 82 | if params := getTChannelParams(ctx); params != nil{ 83 | return params.call 84 | } 85 | 86 | return nil 87 | } 88 | 89 | // 从context获取tchannel中的tracing标记,校验是否开启trace 90 | func isTracingDisabled(ctx context.Context) bool { 91 | if params := getTChannelParams(ctx); params != nil { 92 | return params.tracingDisabled 93 | } 94 | return nil 95 | } 96 | ``` 97 | 98 | 上面主要是构建context上下文或者从上下文获取tchannel的transport header相关参数 99 | 100 | ## context header 101 | 102 | ```shell 103 | type ContextWithHeaders interface { 104 | context.Context 105 | 106 | // rpc请求的headers 107 | Headers() map[string]string 108 | 109 | // rpc响应的headers 110 | ResponseHeaders() map[string]string 111 | 112 | // 在context上设置响应headers 113 | SetResponseHeaders(map[string]string) 114 | 115 | // 通过parent context创建子child 116 | Child() ContextWithHeaders 117 | } 118 | 119 | 120 | // 这个headerCtx的上下文context的key为contextKeyHeaders 121 | type headerCtx struct { 122 | context.Context 123 | } 124 | 125 | // 在ContextWithHeaders中存在rpc请求头和rpc响应头 126 | type headersContainer struct { 127 | reqHeaders map[string]string 128 | respHeaders map[string]string 129 | } 130 | 131 | // 通过contextKeyHeaders值获取context的value值headersContainer 132 | func (c headerCtx) headers() *headersContainer { 133 | if h, ok := c.Value(contextKeyHeaders).(*headersContainer); ok { 134 | return h 135 | } 136 | return nil 137 | } 138 | 139 | // 获取rpc请求头 140 | func (c headerCtx) Headers() map[string]string { 141 | if h := c.headers() ; h != nil { 142 | return h.reqHeaders 143 | } 144 | 145 | return nil 146 | } 147 | 148 | // 获取rpc响应头 149 | func (c headerCtx) ResponseHeaders() map[string]string { 150 | if h := c.headers(); h!=nil{ 151 | return h.reqHeaders 152 | } 153 | 154 | return nil 155 | } 156 | 157 | // 设置rpc响应头部 158 | func (c headerCtx) SetResponseHeaders(headers map[string]string) { 159 | if h := h.headers(); h != nil { 160 | h.respHeaders = headers 161 | return 162 | } 163 | 164 | panic(...) 165 | } 166 | 167 | // Child创建一个子context, 且hedersContainer是独立的 168 | func (c headerCtx) Child() ContextWithHeaders { 169 | var headersCopy headersContainer 170 | if h := c.headers(); h != nil{ 171 | headerCopy = *h 172 | } 173 | 174 | return Wrap(context.WithValue(c.Context, contextKeyHeaders, &headerCopy)) 175 | } 176 | 177 | // 通过传入参数context,新建一个ContextWithHeaders 178 | func Wrap(ctx context.Context) ContextWithHeaders { 179 | hctx := headerCtx{Context: ctx} 180 | if h := hctx.headers() ; h != nil { 181 | return hctx 182 | } 183 | 184 | return WrapWithHeaders(ctx, nil) 185 | } 186 | 187 | // 把headers以headerContainer的形式存储到context上下文中 188 | func WrapWithHeaders(ctx context.Context, headers map[string]string) ContextWithHeaders { 189 | h := &headersContainer { 190 | reqHeaders: headers, 191 | } 192 | newCtx := context.WithValue(ctx, contextKeyHeaders, h) 193 | return headerCtx{Context:newCtx) 194 | } 195 | 196 | // 移除context的key为contextKeyTChannel的value值 197 | func WithoutHeaders(ctx context.Context) context.Context { 198 | return context.WithValue( 199 | context.WithValue(ctx, contextKeyTChannel, nil), 200 | contextKeyHeaders, 201 | nil) 202 | } 203 | ``` 204 | 205 | ## context builder 206 | 207 | 通过ContextBuilder构建一个context,我们在上面已经涉及到contextbuilder了。 208 | 209 | ```shell 210 | type ContextBuilder struct { 211 | // 跨进程上下文传递,是否开启trace 212 | TracingDisabled bool 213 | 214 | // 当创建调出的连接时,关闭发送server端的host:port 215 | hideListeningOnOutbound bool 216 | 217 | // true, 强制parentContext被忽略; false, 表示需要合并Parent context 218 | replaceParentHeaders bool 219 | 220 | // 如果值为0, 则ContextBuilder使用默认值设置defaultTimeout; 221 | Timeout time.Duration 222 | 223 | // application headers,json/thrift会把headers编码进arg2 224 | Headers map[string]string 225 | 226 | // call options 前文已经说过; 是transport headers的相关参数列表 227 | CallOptions *CallOptions 228 | 229 | // 重试机制,这个在CallOptions是存在的,为何要独立出来 230 | RetryOptions *RetryOptions 231 | 232 | // 创建一个tchannel连接的timeout 233 | ConnectTimeout time.Duration 234 | 235 | // ParentContext构建一个新的builder,如果ParentConext为空,则直接使用context.Background() 236 | ParentContext context.Context 237 | 238 | // 隐藏字段:不想tchannel外部设置它 239 | incomingCall IncomingCall 240 | } 241 | 242 | // 创建一个ContextBuilder实例 243 | func NewContextBuilder(timeout time.Duration) *ContextBuilder { 244 | return &ContextBuilder{ 245 | Timeout: timeout, 246 | } 247 | } 248 | 249 | // 设置ContextBuilder的Timeout参数 250 | func (cb *ContextBuilder) SetTimeout(timeout time.Duration) *ContextBuilder { 251 | cb.Timeout = timeout 252 | return cb 253 | } 254 | 255 | // 给app添加header 256 | func (cb *ContextBuilder) AddHeader(key, value string) *ContextBuilder { 257 | if cb.Headers == nil { 258 | cb.Headers = make(map[string]string) 259 | } 260 | cb.Headers[key] = value 261 | return cb 262 | } 263 | 264 | // 给app设置headers 265 | func (cb *ContextHeader) SetHeaders(headers map[string]string) *ContextBuilder { 266 | cb.Headers = headers 267 | cb.replaceParentHeaders = true 268 | return cb 269 | } 270 | 271 | // 设置transport headers中的shardkey参数, 通过这个参数可以选择指定的node 272 | func (cb *ContextHeader) SetShardKey(sk string) *ContextBuilder { 273 | if cb.CallOptions == nil { 274 | cb.CallOptions = new(CallOptions) 275 | } 276 | cb.CallOptions.ShardKey = sk 277 | return cb 278 | } 279 | 280 | // 设置transport headers中的arg scheme 281 | func (cb *ContextHeader) SetFormat(f Format) *ContextBuilder { 282 | if cb.CallOptions == nil { 283 | cb.CallOptions = new(CallOptions) 284 | } 285 | cb.CallOptions.Format = f 286 | return cb 287 | } 288 | 289 | // 设置transport headers中的routing key。这个在规范协议中没有. 290 | func (cb *ContextBuilder) SetRoutineKey(rk string) *ContextBuilder { 291 | if cb.CallOptions == nil { 292 | cb.CallOptions = new(CallOptions) 293 | } 294 | cb.CallOptions.RoutingKey = rk 295 | return cb 296 | } 297 | 298 | // 设置transport headers中的routing delegate 299 | func (cb *ContextBuilder) SetRoutineDelegate(rd string) *ContextBuilder { 300 | if cb.CallOptions == nil { 301 | cb.CallOptions = new(CallOptions) 302 | } 303 | cb.CallOptions.RoutingDelegate = rd 304 | } 305 | 306 | // 设置创建连接的超时时间 307 | func (cb *ContextBuilder) SetConnectionTimeout(d time.Duration) *ContextBuilder { 308 | cb.ConnectTimeout = d 309 | return cb 310 | } 311 | 312 | // 设置屏蔽传输server的host:port 313 | func (cb *ContextBuilder) HideListeningOnOutbound() *ContextBuilder { 314 | cb.hideListeningOnOutbound = true 315 | return cb 316 | } 317 | 318 | // 关闭trace 319 | func (cb *ContextBuilder) DisableTracing() *ContextBuilder { 320 | cb.TracingDisabled = true 321 | return cb 322 | } 323 | 324 | // 设置transport headers中的重试机制 325 | func (cb *ContextBuilder) SetRetryOptions(retryOptions *RetryOptions) *ContextBuilder { 326 | cb.RetryOptions = retryOptions 327 | return cb 328 | } 329 | 330 | // 设置transport headers中的重试机制中的timeoutPerAttempt 331 | func (cb *ContextBuilder) SetTimeoutPerAttempt(timeoutPerAttempt time.Duration) *ContextBuilder { 332 | if cb.RetryOptions == nil { 333 | cb.RetryOptions = &RetryOptions{} 334 | } 335 | cb.RetryOptions.TimeoutPerAttempt = timeoutPerAttempt 336 | return cb 337 | } 338 | 339 | // 设置context的parentContext 340 | func (cb *ContextBuilder) SetParentContext(ctx context.Context) *ContextBuilder { 341 | c.ParentContext = ctx 342 | return cb 343 | } 344 | 345 | // 设置InComingCall 346 | func (cb *ContextBuiler) setIncomingCall(call IncomingCall) *ContextBuilder { 347 | cb.incomingCall = call 348 | return cb 349 | } 350 | 351 | // 获取ContextBuilder的headers。 352 | // 353 | // 注意: 合并 354 | func (cb *ContextBuilder) getHeaders() map[string]string { 355 | if cb.ParentContext == nil || cb.replaceParentHeaders { 356 | return cb.Headers 357 | } 358 | 359 | // 获取context key为contextKeyHeaders的value值headersContainer 360 | parent, ok := cb.ParentContext.Value(contextKeyHeaders).(*headersContainer) 361 | if !ok || len(parent.reqHeaders) ==0 { 362 | return cb.Headers 363 | } 364 | 365 | // 合并parent的headers和ContextBuilder的headers 366 | mergedHeaders := make(map[string]string, len(cb.Headers) + len(parent.reqHeaders)) 367 | 368 | for k, v := range parent.reqHeaders { 369 | mergedHeaders[k] = v 370 | } 371 | 372 | for k, v := range cb.Headers { 373 | mergedHeaders[k] = v 374 | } 375 | return mergedHeaders 376 | } 377 | 378 | // 构建Context实例, context带有key为contextKeyTChanell和contextKeyHeaders。 379 | func (cb *ContextBuilder) Build() (ContextWithHeaders, context.CancelFunc) { 380 | // 构建context的key为contextKeyTChannel的value值 381 | params := &tchannelCtxParams { 382 | options: cb.CallOptions, 383 | call: cb.incomingCall, 384 | retryOptions: cb.RetryOptions, 385 | connectTimeout: cb.ConnectTimeout, 386 | hideListeningOnOutbound: cb.hideListeningOnOutbound, 387 | tracingDisabled: cb.TracingDisabled, 388 | } 389 | 390 | // 获取context的key为contextKeyHeaders的value值 391 | parent := cb.ParentContext 392 | if parent == nil { 393 | parent = context.Background() 394 | } else if headerCtx, ok := parent.(headerCtx); ok { 395 | parent = headerCtx.Context 396 | } 397 | 398 | var ( 399 | ctx context.Context 400 | cancel context.CancelFunc 401 | ) 402 | 403 | // 继承parent的截止时间或者当前的截止时间 404 | _, parentHasDeadline := parent.Deadline() 405 | if cb.Timeout == 0 || parentHasDeadline { 406 | ctx, cancel = context.WithCancel(parent) 407 | } else { 408 | ctx, cancel = context.WithTimeout(parent, cb.Timeout) 409 | } 410 | 411 | ctx = context.WithValue(ctx, contextKeyTChannel, params) 412 | return WrapWithHeaders(ctx, cb.getHeaders()), cancel 413 | } 414 | ``` 415 | 416 | 417 | # 总结 418 | 419 | 通过context、context headers和context builder,我们可以对rpc跨进程调用的服务,进行参数设置,并传递下去,例如:trace,timeout、retry、headers等 420 | --------------------------------------------------------------------------------