zookeeper編寫服務(wù)發(fā)現(xiàn)

zookeeper是一個(gè)順序一致性的分布式數(shù)據(jù)庫,由多個(gè)節(jié)點(diǎn)共同組成一個(gè)分布式集群,掛掉任意一個(gè)節(jié)點(diǎn),數(shù)據(jù)庫仍然可以正常工作,客戶端無感知故障切換??蛻舳讼蛉我庖粋€(gè)節(jié)點(diǎn)寫入數(shù)據(jù),其它節(jié)點(diǎn)可以立即看到最新的數(shù)據(jù)。

image

zookeeper的內(nèi)部是一個(gè)key/value存儲(chǔ)引擎,key是以樹狀的形式構(gòu)成了一個(gè)多級(jí)的層次結(jié)構(gòu),每一個(gè)節(jié)點(diǎn)既可以存儲(chǔ)數(shù)據(jù),又可以作為一個(gè)目錄存放下一級(jí)子節(jié)點(diǎn)。


image

zookeeper提供了創(chuàng)建/修改/刪除節(jié)點(diǎn)的api,如果父節(jié)點(diǎn)沒有創(chuàng)建,字節(jié)點(diǎn)會(huì)創(chuàng)建失敗。如果父節(jié)點(diǎn)還有子節(jié)點(diǎn),父節(jié)點(diǎn)不可以被刪除。

zookeeper和客戶端之間以socket形式進(jìn)行雙向通訊,客戶端可以主動(dòng)調(diào)用服務(wù)器提供的api,服務(wù)器可以主動(dòng)向客戶端推送事件。有多種事件可以watch,比如節(jié)點(diǎn)的增刪改,子節(jié)點(diǎn)的增刪改,會(huì)話狀態(tài)變更等。

zookeeper的事件有傳遞機(jī)制,字節(jié)點(diǎn)的增刪改觸發(fā)的事件會(huì)向上層依次傳播,所有的父節(jié)點(diǎn)都可以收到字節(jié)點(diǎn)的數(shù)據(jù)變更事件,所以層次太深/子節(jié)點(diǎn)太多會(huì)給服務(wù)器的事件系統(tǒng)帶來壓力,節(jié)點(diǎn)分配要做好周密的規(guī)劃。

zookeeper滿足了CAP定理的分區(qū)容忍性P和強(qiáng)一致性C,犧牲了高性能A。zookeeper的存儲(chǔ)能力是有限的,當(dāng)節(jié)點(diǎn)層次太深/子節(jié)點(diǎn)太多/節(jié)點(diǎn)數(shù)據(jù)太大,都會(huì)影響數(shù)據(jù)庫的穩(wěn)定性。所以zookeeper不是一個(gè)用來做高并發(fā)高性能的數(shù)據(jù)庫,zookeeper一般只用來存儲(chǔ)配置信息。

zookeeper的讀性能隨著節(jié)點(diǎn)數(shù)量的提升能不斷增加,但是寫性能會(huì)隨著節(jié)點(diǎn)數(shù)量的增加而降低,所以節(jié)點(diǎn)的數(shù)量不宜太多,一般配置成3個(gè)或者5個(gè)就可以了。


image

圖中可以看出當(dāng)服務(wù)器節(jié)點(diǎn)增多時(shí),復(fù)雜度會(huì)隨之提升。因?yàn)槊總€(gè)節(jié)點(diǎn)和其它節(jié)點(diǎn)之間要進(jìn)行p2p的連接。3個(gè)節(jié)點(diǎn)可以容忍掛掉1個(gè)節(jié)點(diǎn),5個(gè)節(jié)點(diǎn)可以容忍掛掉2個(gè)節(jié)點(diǎn)。

客戶端連接zookeeper時(shí)會(huì)選擇任意一個(gè)節(jié)點(diǎn)保持長鏈接,后續(xù)通信都是通過這個(gè)節(jié)點(diǎn)進(jìn)行讀寫的。如果該節(jié)點(diǎn)掛了,客戶端會(huì)嘗試去連接其它節(jié)點(diǎn)。

服務(wù)器會(huì)為每個(gè)客戶端連接維持一個(gè)會(huì)話對(duì)象,會(huì)話的ID會(huì)保存在客戶端。會(huì)話對(duì)象也是分布式的,意味著當(dāng)一個(gè)節(jié)點(diǎn)掛掉了,客戶端使用原有的會(huì)話ID去連接其它節(jié)點(diǎn),服務(wù)器維持的會(huì)話對(duì)象還繼續(xù)存在,并不需要重新創(chuàng)建一個(gè)新的會(huì)話。

如果客戶端主動(dòng)發(fā)送會(huì)話關(guān)閉消息,服務(wù)器的會(huì)話對(duì)象會(huì)立即刪除。如果客戶端不小心奔潰了,沒有發(fā)送關(guān)閉消息,服務(wù)器的會(huì)話對(duì)象還會(huì)繼續(xù)存在一段時(shí)間。這個(gè)時(shí)間是會(huì)話的過期時(shí)間,在創(chuàng)建會(huì)話的時(shí)候客戶端會(huì)提供這個(gè)參數(shù),一般是10到30秒。

也許你會(huì)問連接斷開了,服務(wù)器是可以感知到的,為什么需要客戶端主動(dòng)發(fā)送關(guān)閉消息呢?

因?yàn)榉?wù)器要考慮網(wǎng)絡(luò)抖動(dòng)的情況,連接可能只是臨時(shí)斷開了。為了避免這種情況下反復(fù)創(chuàng)建和銷毀復(fù)雜的會(huì)話對(duì)象以及創(chuàng)建會(huì)話后要進(jìn)行的一系列事件初始化操作,服務(wù)器會(huì)盡量延長會(huì)話的生存時(shí)間。

zookeeper的節(jié)點(diǎn)可以是持久化(Persistent)的,也可以是臨時(shí)(Ephemeral)的。所謂臨時(shí)的節(jié)點(diǎn)就是會(huì)話關(guān)閉后,會(huì)話期間創(chuàng)建的所有臨時(shí)節(jié)點(diǎn)會(huì)立即消失。一般用于服務(wù)發(fā)現(xiàn)系統(tǒng),將服務(wù)進(jìn)程的生命期和zookeeper子節(jié)點(diǎn)的生命期綁定在一起,起到了實(shí)時(shí)監(jiān)控服務(wù)進(jìn)程的存活的效果。

zookeeper還提供了順序節(jié)點(diǎn)。類似于mysql里面的auto_increment屬性。服務(wù)器會(huì)在順序節(jié)點(diǎn)名稱后自動(dòng)增加自增的唯一后綴,保持節(jié)點(diǎn)名稱的唯一性和順序性。

還有一種節(jié)點(diǎn)叫著保護(hù)(Protected)節(jié)點(diǎn)。這個(gè)節(jié)點(diǎn)非常特殊,但是也非常常用。在應(yīng)用服務(wù)發(fā)現(xiàn)的場(chǎng)合時(shí),客戶端創(chuàng)建了一個(gè)臨時(shí)節(jié)點(diǎn)后,服務(wù)器節(jié)點(diǎn)掛了,連接斷開了,然后客戶端去重連到其它的節(jié)點(diǎn)。因?yàn)闀?huì)話沒有關(guān)閉,之前創(chuàng)建的臨時(shí)節(jié)點(diǎn)還存在,但是這個(gè)時(shí)候客戶端卻無法識(shí)別去這個(gè)臨時(shí)節(jié)點(diǎn)是不是自己創(chuàng)建的,因?yàn)楣?jié)點(diǎn)內(nèi)部并不存儲(chǔ)會(huì)話ID字段。所以客戶端會(huì)在節(jié)點(diǎn)名稱上加上一個(gè)GUID前綴,這個(gè)前綴會(huì)保存在客戶端,這樣它就可以在重連后識(shí)別出哪個(gè)臨時(shí)節(jié)點(diǎn)是自己之前創(chuàng)建的了。

接下來我們使用Go語言實(shí)現(xiàn)一下服務(wù)發(fā)現(xiàn)的注冊(cè)和發(fā)現(xiàn)功能。

image

如圖所示,我們要提供api.user這樣的服務(wù),這個(gè)服務(wù)有3個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)有不一樣的服務(wù)地址,這3個(gè)節(jié)點(diǎn)各自將自己的服務(wù)注冊(cè)進(jìn)zk,然后消費(fèi)者進(jìn)行讀取zk得到api.user的服務(wù)地址,任選一個(gè)節(jié)點(diǎn)地址進(jìn)行服務(wù)調(diào)用。為了簡(jiǎn)單化,這里就沒有提供權(quán)重參數(shù)了。在一個(gè)正式的服務(wù)發(fā)現(xiàn)里一般都有權(quán)重參數(shù),用于調(diào)整服務(wù)節(jié)點(diǎn)之間的流量分配。

go get github.com/samuel/go-zookeeper/zk

首先我們定義一個(gè)ServiceNode結(jié)構(gòu),這個(gè)結(jié)構(gòu)數(shù)據(jù)會(huì)存儲(chǔ)在節(jié)點(diǎn)的data中,表示服務(wù)發(fā)現(xiàn)的地址信息。

type ServiceNode struct {
    Name string `json:"name"` // 服務(wù)名稱,這里是user
    Host string `json:"host"`
    Port int    `json:"port"`
}

在定義一個(gè)服務(wù)發(fā)現(xiàn)的客戶端結(jié)構(gòu)體SdClient。

type SdClient struct {
    zkServers []string // 多個(gè)節(jié)點(diǎn)地址
    zkRoot    string // 服務(wù)根節(jié)點(diǎn),這里是/api
    conn      *zk.Conn // zk的客戶端連接
}

編寫構(gòu)造器,創(chuàng)建根節(jié)點(diǎn)

func NewClient(zkServers []string, zkRoot string, timeout int) (*SdClient, error) {
    client := new(SdClient)
    client.zkServers = zkServers
    client.zkRoot = zkRoot
    // 連接服務(wù)器
    conn, _, err := zk.Connect(zkServers, time.Duration(timeout)*time.Second)
    if err != nil {
        return nil, err
    }
    client.conn = conn
    // 創(chuàng)建服務(wù)根節(jié)點(diǎn)
    if err := client.ensureRoot(); err != nil {
        client.Close()
        return nil, err
    }
    return client, nil
}

// 關(guān)閉連接,釋放臨時(shí)節(jié)點(diǎn)
func (s *SdClient) Close() {
    s.conn.Close()
}

func (s *SdClient) ensureRoot() error {
    exists, _, err := s.conn.Exists(s.zkRoot)
    if err != nil {
        return err
    }
    if !exists {
        _, err := s.conn.Create(s.zkRoot, []byte(""), 0, zk.WorldACL(zk.PermAll))
        if err != nil && err != zk.ErrNodeExists {
            return err
        }
    }
    return nil
}

值得注意的是代碼中的Create調(diào)用可能會(huì)返回節(jié)點(diǎn)已存在錯(cuò)誤,這是正常現(xiàn)象,因?yàn)闀?huì)存在多進(jìn)程同時(shí)創(chuàng)建節(jié)點(diǎn)的可能。如果創(chuàng)建根節(jié)點(diǎn)出錯(cuò),還需要及時(shí)關(guān)閉連接。我們不關(guān)心節(jié)點(diǎn)的權(quán)限控制,所以使用zk.WorldACL(zk.PermAll)表示該節(jié)點(diǎn)沒有權(quán)限限制。Create參數(shù)中的flag=0表示這是一個(gè)持久化的普通節(jié)點(diǎn)。

接下來我們編寫服務(wù)注冊(cè)方法

func (s *SdClient) Register(node *ServiceNode) error {
    if err := s.ensureName(node.Name); err != nil {
        return err
    }
    path := s.zkRoot + "/" + node.Name + "/n"
    data, err := json.Marshal(node)
    if err != nil {
        return err
    }
    _, err = s.conn.CreateProtectedEphemeralSequential(path, data, zk.WorldACL(zk.PermAll))
    if err != nil {
        return err
    }
    return nil
}

func (s *SdClient) ensureName(name string) error {
    path := s.zkRoot + "/" + name
    exists, _, err := s.conn.Exists(path)
    if err != nil {
        return err
    }
    if !exists {
        _, err := s.conn.Create(path, []byte(""), 0, zk.WorldACL(zk.PermAll))
        if err != nil && err != zk.ErrNodeExists {
            return err
        }
    }
    return nil
}

先要?jiǎng)?chuàng)建/api/user節(jié)點(diǎn)作為服務(wù)列表的父節(jié)點(diǎn)。然后創(chuàng)建一個(gè)保護(hù)順序臨時(shí)(ProtectedEphemeralSequential)子節(jié)點(diǎn),同時(shí)將地址信息存儲(chǔ)在節(jié)點(diǎn)中。什么叫保護(hù)順序臨時(shí)節(jié)點(diǎn),首先它是一個(gè)臨時(shí)節(jié)點(diǎn),會(huì)話關(guān)閉后節(jié)點(diǎn)自動(dòng)消失。其它它是個(gè)順序節(jié)點(diǎn),zookeeper自動(dòng)在名稱后面增加自增后綴,確保節(jié)點(diǎn)名稱的唯一性。同時(shí)還是個(gè)保護(hù)性節(jié)點(diǎn),節(jié)點(diǎn)前綴增加了GUID字段,確保斷開重連后臨時(shí)節(jié)點(diǎn)可以和客戶端狀態(tài)對(duì)接上。

接下來我們實(shí)現(xiàn)消費(fèi)者獲取服務(wù)列表方法

func (s *SdClient) GetNodes(name string) ([]*ServiceNode, error) {
    path := s.zkRoot + "/" + name
    // 獲取字節(jié)點(diǎn)名稱
    childs, _, err := s.conn.Children(path)
    if err != nil {
        if err == zk.ErrNoNode {
            return []*ServiceNode{}, nil
        }
        return nil, err
    }
    nodes := []*ServiceNode{}
    for _, child := range childs {
        fullPath := path + "/" + child
        data, _, err := s.conn.Get(fullPath)
        if err != nil {
            if err == zk.ErrNoNode {
                continue
            }
            return nil, err
        }
        node := new(ServiceNode)
        err = json.Unmarshal(data, node)
        if err != nil {
            return nil, err
        }
        nodes = append(nodes, node)
    }
    return nodes, nil
}

獲取服務(wù)節(jié)點(diǎn)列表時(shí),我們先獲取字節(jié)點(diǎn)的名稱列表,然后依次讀取內(nèi)容拿到服務(wù)地址。因?yàn)楂@取字節(jié)點(diǎn)名稱和獲取字節(jié)點(diǎn)內(nèi)容不是一個(gè)原子操作,所以在調(diào)用Get獲取內(nèi)容時(shí)可能會(huì)出現(xiàn)節(jié)點(diǎn)不存在錯(cuò)誤,這是正常現(xiàn)象。

將以上代碼湊在一起,一個(gè)簡(jiǎn)單的服務(wù)發(fā)現(xiàn)包裝就實(shí)現(xiàn)了。

最后我們看看如果使用以上代碼,為了方便起見,我們將多個(gè)服務(wù)提供者和消費(fèi)者寫在一個(gè)main方法里。

func main() {
        // 服務(wù)器地址列表
    servers := []string{"192.168.0.101:2118", "192.168.0.102:2118", "192.168.0.103:2118"}
    client, err := NewClient(servers, "/api", 10)
    if err != nil {
        panic(err)
    }
    defer client.Close()
    node1 := &ServiceNode{"user", "127.0.0.1", 4000}
    node2 := &ServiceNode{"user", "127.0.0.1", 4001}
    node3 := &ServiceNode{"user", "127.0.0.1", 4002}
    if err := client.Register(node1); err != nil {
        panic(err)
    }
    if err := client.Register(node2); err != nil {
        panic(err)
    }
    if err := client.Register(node3); err != nil {
        panic(err)
    }
    nodes, err := client.GetNodes("user")
    if err != nil {
        panic(err)
    }
    for _, node := range nodes {
        fmt.Println(node.Host, node.Port)
    }
}

值得注意的是使用時(shí)一定要在進(jìn)程退出前調(diào)用Close方法,否則zookeeper的會(huì)話不會(huì)立即關(guān)閉,服務(wù)器創(chuàng)建的臨時(shí)節(jié)點(diǎn)也就不會(huì)立即消失,而是要等到timeout之后服務(wù)器才會(huì)清理。

轉(zhuǎn)自:
https://zhuanlan.zhihu.com/p/34156758

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • zookeeper是一個(gè)強(qiáng)一致的分布式數(shù)據(jù)庫,由多個(gè)節(jié)點(diǎn)共同組成一個(gè)分布式集群,掛掉任意一個(gè)節(jié)點(diǎn),數(shù)據(jù)庫仍然可以正...
    碼洞閱讀 1,305評(píng)論 0 49
  • 本文將從系統(tǒng)模型、序列化與協(xié)議、客戶端工作原理、會(huì)話、服務(wù)端工作原理以及數(shù)據(jù)存儲(chǔ)等方面來揭示ZooKeeper的技...
    端木軒閱讀 3,911評(píng)論 0 42
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,662評(píng)論 19 139
  • 親愛的,大坑女神: 在你生日前幾天,就一直想寫封信給你,說說我們,想給你一個(gè)特別的生日禮物和祝福,想跟你總書信的方...
    Lemon_ecd6閱讀 334評(píng)論 0 0
  • 《他》 他信步走來,已是深秋。 不時(shí)已走到我面前,只見他兩鬢微霜,面色憔悴,卻依舊可瞧出俊朗的容顏,尤其那雙眸子烏...
    桃者亂太郎閱讀 554評(píng)論 0 0

友情鏈接更多精彩內(nèi)容