基于负载均衡的服务相互调用指的是经过基于Lvs、Haproxy、Nginx等负载均衡软件来构建一个负载均衡服务,全部的服务调用都经过负载均衡器java
从负载均衡的这种模式下其实有两个主要的问题: 一是中心化,整个系统都基于负载均衡器,负载均衡就至关于整个业务的中心,虽然咱们能够经过一些高可用手段来保证,但其实内部流量一般是巨大的,很容易出现性能瓶颈 二是增长了一次TCP交互node
固然也有不少好处,好比能够作一些负载均衡、长连接维护、分布式跟踪等,这不是本文重点windows
全部的服务都启动后都经过注册中心来注册本身,同时把注册中内心面的服务信息拉回本地,后续调用,就直接检查本地的服务和节点信息来进行服务节点的调用缓存
每一个服务节点都会来注册中心进行服务注册,那数据如何在服务端进行保存呢,其实就是注册表,其实等同于windows 里面的注册表,每一个服务都来注册,把本身的信息上报上来,而后注册中心吧注册表,返回给client端,那服务之间就知道要调用服务的节点啦bash
微服务注册注册中心一般会大量的服务注册, 那不能每次客户端来请求的时候,服务端都返回全量的数据,在数据传输的设计中,一般会有一种增量同步,其实在注册中心中也相似 注册中心经过将最近的服务变动事件保存在一个事件队列
中,后续每次客户端拉取只返回增量数据,这样服务端的忘了压力就会小不少数据结构
增量数据有一个问题就是,若是客户端错过啦某些事件,好比事件队列满了,则客户端与注册中心的注册表就会不一致, 因此eureka里面引入了一个hashcode的概念,经过比对hashcode是否相同, 若是不一样则客户端须要从新全量拉取架构
系统总体上分为两个端:客户端(Client)和注册中心(Server) Server: 提供服务注册和获取注册表的接口, 同时本地把保存服务和节点的对应信息,变动事件写入eventQueue Client: 调用server接口进行服务注册, 同时调用注册表拉取接口进行注册表拉取,保存懂啊LocalRegistryapp
Server端的服务注册表里面的服务和节点的信息,我经过Application和lease来维护 Application: 表明一个应用,里面会包含服务对应的节点信息 Lease: 维护一个节点的信息,好比心跳信息负载均衡
服务端注册表结构体Registry主要包含三部分信息: lock(读写锁)、apps(应用对应信息)、eventQueue(事件队列) Lock: 注册中心是典型的读多写少的应用,server端注册表可能同时提供给N个服务进行读取,因此这里采用读写锁 apps: 保存应用对应的信息, 其实后面写完发现,不必使用,只使用基础的map就能够搞定 eventQueue: 每次注册表变动都写入事件到里面分布式
// Registry 注册表 type Registry struct { lock sync.RWMutex apps sync.Map duration time.Duration eventQueue *EventQueue }
注册流程主要分为下面几部分:
// Registr 注册服务 func (r *Registry) Registr(name, node string) bool { r.lock.Lock() defer r.lock.Unlock() app := r.getApp(name) if app == nil { app = NewApplication(name) r.apps.Store(name, app) } if lease, ok := app.add(node, r.duration); ok { r.eventQueue.Push(&Event{lease: lease, action: ADD}) return true } return false }
全量拉取经过all接口拉取全量的返回的是服务对应的节点切片 增量拉取经过details接口返回增量的变动事件和服务端注册表的hashcode
// all 全量拉取 func (r *Registry) all() map[string][]string { r.lock.RLock() defer r.lock.RUnlock() apps := make(map[string][]string) r.apps.Range(func(k, v interface{}) bool { name, app := k.(string), v.(*Application) nodes := []string{} for key := range app.Node { nodes = append(nodes, key) } apps[name] = nodes return true }) return apps } // details 增量拉取 func (r *Registry) details() []*Event { r.lock.RLock() defer r.lock.RUnlock() events := []*Event{} for { event := r.eventQueue.Pop() if event == nil { break } events = append(events, event) } return events }
hashcode是一个一致性的保证,eureka里面主要是经过拼接全部的服务名称和节点的个数来生成的一个字符串,这里咱们也采用这种方式,
func (r *Registry) hashCode() string { r.lock.RLock() defer r.lock.RUnlock() hashCodes := []string{} r.apps.Range(func(_, value interface{}) bool { app := value.(*Application) hashCodes = append(hashCodes, app.HashCode()) return true }) sort.Sort(sort.StringSlice(hashCodes)) return strings.Join(hashCodes, "|") }
客户端本地注册表其实就比较简单了,只须要存储服务和节点的对应信息便可
// LocalRegistry 本地注册表 type LocalRegistry struct { lock sync.RWMutex apps map[string][]string }
func (c *Client) start() { c.wg.Add(1) c.registr() c.poll() go c.loop() }
func (c *Client) loop() { timer := time.NewTimer(time.Second) for { // 从服务的拉取增量事件,details内部会直接应用,而后返回服务端返回的注册表的hashcode respHashCode := c.details() localHashCode := c.registry.hashCode() // 若是发现本地和服务的的注册表的hashcode不一样,则全量拉取 if respHashCode != localHashCode { fmt.Printf("client app %s node %s poll hashcode: %s\n", c.App, c.Name, respHashCode) c.poll() } select { case <-timer.C: timer.Reset(time.Second) case <-c.done: c.wg.Done() return } } }
func main() { // 生成服务端 server := NewServer("aliyun", time.Second) // 注册两个test服务的节点 clientOne := NewClient("test", "1.1.1.1:9090", server) clientOne.start() clientTwo := NewClient("test", "1.1.1.2:9090", server) clientTwo.start() // 注册两个hello服务的节点 clientThree := NewClient("hello", "1.1.1.3:9090", server) clientThree.start() clientFour := NewClient("hello", "1.1.1.4:9090", server) clientFour.start() time.Sleep(time.Second * 3) // 验证每一个服务节点的注册表的hashcode是否一致 println(clientOne.details()) println(clientTwo.details()) println(clientThree.details()) println(clientFour.details()) println(clientTwo.details() == clientOne.details()) println(clientThree.details() == clientFour.details()) println(clientOne.details() == clientFour.details()) clientOne.stop() clientTwo.stop() clientThree.stop() clientFour.stop() }
经过结果咱们能够看出,节点增量拉取了注册表,同时若是发现与本地的hashcode不一样就进行全量拉取,并最终达成一致
lr event add 1.1.1.3:9090 hello lr event add 1.1.1.4:9090 hello lr event add client app hello node 1.1.1.4:9090 poll hashcode: hello_2|test_2 1.1.1.1:9090 test lr event add 1.1.1.2:9090 test client app test node 1.1.1.1:9090 poll hashcode: hello_2|test_2 client app test node 1.1.1.2:9090 poll hashcode: hello_2|test_2 client app hello node 1.1.1.3:9090 poll hashcode: hello_2|test_2 hello_2|test_2 hello_2|test_2 hello_2|test_2 hello_2|test_2 true true true
微服务注册中心注册表的这种实现机制,到这基本上就明白了,注册中心 经过增量、全量、hashcode三种机制来保证客户端与注册中心的注册表的同步
其实一个工业级的注册中心仍是很麻烦的,好比注册表中那个事件队列,我如今的实现只有一个节点能获取增量,其余的都会经过hashcode来触发全量拉取,后续文章里面会相信介绍下,这块缓存和定时器来实现增量数据的打包
其实在go里面你们注册中心都是基于etcd、consul直接watch去作的,基本上能够完成eureka服务的8/9十的功能,可是当须要与公司现有的java作集成,可能就须要eureaka这种注册中心了
未完待续 关注公共号: 布衣码农
更多精彩内容能够查看www.sreguide.com