基於負載均衡的服務相互調用指的是經過基於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