原文:Jaeger源码分析——服务注册与服务发现html
Jaeger官方并无明确说明其 服务注册和服务发现的具体使用和介绍,这部分功能是在分析源码的时候,发现其原理与 服务注册和服务发现相似,因此结合本身对服务注册和服务发现的认识,作一次总结,有错还请指点。
Jaeger不借助第三方工具也能实现服务注册和服务发现,这部分功能由其依赖的RPC框架提供。
go run cmd/agent/main.go --collector.host-port=192.168.0.10:14267,192.168.0.11:14267
在启动agent的时候,可配置多个collector静态地址,这部分地址会造成一张注册表。
github.com/uber/tchannel-go/peer.go #59 type PeerList struct { sync.RWMutex parent *RootPeerList //以hostPort为下标组成注册表 peersByHostPort map[string]*peerScore //负载均衡实现 peerHeap *peerHeap scoreCalculator ScoreCalculator lastSelected uint64 }
github.com/jaegertracing/jaeger/pkg/discovery/peerlistmgr/peer_list_mgr.go #150 func (m *PeerListManager) ensureConnections() { peers := m.peers.Copy() minPeers := m.getMinPeers(peers) numConnected, notConnected := m.findConnected(peers) //有必定量的连接,就不进行健康检查 if numConnected >= minPeers { return } ...... for i := range notConnected { // swap current peer with random from the remaining positions r := i + m.rnd.Intn(len(notConnected)-i) notConnected[i], notConnected[r] = notConnected[r], notConnected[i] // try to connect to current peer (swapped) peer := notConnected[i] m.logger.Info("Trying to connect to peer", zap.String("host:port", peer.HostPort())) //用于控制超时 ctx, cancel := context.WithTimeout(context.Background(), m.connCheckTimeout) conn, err := peer.GetConnection(ctx) cancel() if err != nil { m.logger.Error("Unable to connect", zap.String("host:port", peer.HostPort()), zap.Duration("connCheckTimeout", m.connCheckTimeout), zap.Error(err)) continue } ...... } }
在注册表上的地址,TChannel都会进行健康检查,每秒进行一次,若是0.25秒没有链接上,视为服务不可用。若是链接成功则保留当前服务实例,供agent提交数据使用。
github.com/uber/tchannel-go/connection.go #228 func (ch *Channel) newOutboundConnection(timeout time.Duration, hostPort string, events connectionEvents) (*Connection, error) { conn, err := net.DialTimeout("tcp", hostPort, timeout) if err != nil { if ne, ok := err.(net.Error); ok && ne.Timeout() { ch.log.WithFields(LogField{"hostPort", hostPort}, LogField{"timeout", timeout}).Infof("Outbound net.Dial timed out") err = ErrTimeout } return nil, err } return ch.newConnection(conn, hostPort, connectionWaitingToSendInitReq, events), nil }
github.com/uber/tchannel-go/peer.go #149 func (l *PeerList) choosePeer(prevSelected map[string]struct{}, avoidHost bool) *Peer { var psPopList []*peerScore var ps *peerScore ...... size := l.peerHeap.Len() for i := 0; i < size; i++ { //把peer从Heap头部弹出来 popped := l.peerHeap.popPeer() if canChoosePeer(popped.HostPort()) { ps = popped break } psPopList = append(psPopList, popped) } //不符合的放入Heap尾部 for _, p := range psPopList { heap.Push(l.peerHeap, p) } if ps == nil { return nil } //符合条件的打分,再放入Heap尾部 l.peerHeap.pushPeer(ps) ps.chosenCount.Inc() return ps.Peer }
当Agent须要提交数据的时候,会从TChannel的负载均衡获取peer(服务信息),当有多个的时候,TChannel经过轮询方式,查询peer。实现方式:注册表把全部peer放入peerHeap,先把peer从头部弹出,再把peer放回尾部,从而实现轮询策略的负载均衡。
github.com/uber/tchannel-go/retry.go #212 func (ch *Channel) RunWithRetry(runCtx context.Context, f RetriableFunc) error { var err error opts := getRetryOptions(runCtx) rs := ch.getRequestState(opts) defer requestStatePool.Put(rs) //默认重试5次 for i := 0; i < opts.MaxAttempts; i++ { rs.Attempt++ if opts.TimeoutPerAttempt == 0 { err = f(runCtx, rs) } else { attemptCtx, cancel := context.WithTimeout(runCtx, opts.TimeoutPerAttempt) err = f(attemptCtx, rs) cancel() } if err == nil { return nil } if !opts.RetryOn.CanRetry(err) { if ch.log.Enabled(LogLevelInfo) { ch.log.WithFields(ErrField(err)).Info("Failed after non-retriable error.") } return err } ...... } // Too many retries, return the last error return err }
网络之间的通信避免不了网络异常,因此为了提升可用性,重试是其中一种方式。当从负载均衡获取peer提交数据到Collector,若是提交失败,会再从负载均衡获取peer,最多5次,若是5次都不成功就会放弃此次提交。
使用consul实现服务注册和服务发现是一件很简单的事情。不少功能都是开箱即用。
docker run -itd --network=backend \ -p 8400:8400 -p 8500:8500 -p 8600:53/udp \ -h node1 progrium/consul -server -bootstrap -ui-dir /ui
docker run \ -itd --network=backend \ --name=jaeger-agent \ -p5775:5775/udp \ -p6831:6831/udp \ -p6832:6832/udp \ -p5778:5778/tcp \ --dns-search="service.consul" --dns=172.18.0.2 \ jaegertracing/jaeger-agent \ /go/bin/agent-linux --collector.host-port=jaeger-collector:14267
#node1 docker run -itd --network=backend \ --name=jaeger-collector-node1 \ -p :14267 \ --dns-search="service.consul" --dns=172.18.0.2 \ jaegertracing/jaeger-collector \ /go/bin/collector-linux \ --span-storage.type=cassandra \ --cassandra.keyspace=jaeger_v1_dc \ --cassandra.servers=cassandra:9042 #node2 docker run -itd --network=backend \ --name=jaeger-collector-node2 \ -p :14267 \ --dns-search="service.consul" --dns=172.18.0.2 \ jaegertracing/jaeger-collector \ /go/bin/collector-linux \ --span-storage.type=cassandra \ --cassandra.keyspace=jaeger_v1_dc \ --cassandra.servers=cassandra:9042
docker run -itd --net=backend --name=registrator \ --volume=/var/run/docker.sock:/tmp/docker.sock \ gliderlabs/registrator:latest \ consul://172.18.0.2:8500
使用consul+docker的形式,只要部署好服务,就会被自动注册到consul,十分简单。
查看注册表信息 http://localhost:8500/ui/#/dc1/nodes/node1
能够看到启动的2个Collector服务ip分别为:172.18.0.5和172.18.0.8
consul提供了不少种健康检查方式:HTTP、TCP、Docker、Shell和TTL。详情能够查看官网。
Consul相对于Agent和Collector是远程服务,因此提供了2种服务发现方式:HTTP和DNS,在这里主要使用是DNS,由于简单,轻量。
当Agent经过DNS解析出多个IP的时候,Consul会 随机选择一个IP给Agent实现负载均衡。因为DNS存在缓存,因此有可能出现,服务不健康,同样会被正常解析,因此在默认状况下Consul是没有设置缓存时间,TTL为0,可是也考虑到了不缓存对Consul的压力,因此开放配置,让咱们去决定缓存时间点DNS Caching。node
TChannel与Consul+docker实现的服务发现和服务注册中都有他们的优缺点:
TChannel的服务注册适用于一些基础服务,例如Jaeger就属于一种基础服务,这种服务一旦部署不多会变更。
在如今docker流行的大环境下使用Consul实现的服务注册会简单不少,docker有一个特色就是ip地址是动态,因此它很适合业务场景,由于业务常常变更,服务也随着变化。
TChannel和Consul都提供了健康检查,可是都只是检测服务是否正在运行,没法了解是否可以正常处理请求。
TChannel使用的是客户端服务发现,这种方式相对于Consul的服务端服务发现的优势就是没有了远程网络开销,单点问题。同时缺点就是各个语言都须要本身实现注册表,负载均衡等功能。
Consul使用服务端服务发现,它能够很好的和其余服务结合使用,不须要关心注册表,负载均衡等。并且关于网络开销和单点问题都提供了方案。