溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

徒手教你使用zookeeper編寫服務(wù)發(fā)現(xiàn)

發(fā)布時間:2020-08-10 16:41:04 來源:ITPUB博客 閱讀:120 作者:碼洞 欄目:數(shù)據(jù)庫

徒手教你使用zookeeper編寫服務(wù)發(fā)現(xiàn)

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

徒手教你使用zookeeper編寫服務(wù)發(fā)現(xiàn)

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

徒手教你使用zookeeper編寫服務(wù)發(fā)現(xiàn)

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

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

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

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

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

徒手教你使用zookeeper編寫服務(wù)發(fā)現(xiàn)

圖中可以看出當服務(wù)器節(jié)點增多時,復(fù)雜度會隨之提升。因為每個節(jié)點和其它節(jié)點之間要進行p2p的連接。3個節(jié)點可以容忍掛掉1個節(jié)點,5個節(jié)點可以容忍掛掉2個節(jié)點。

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

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

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

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

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

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

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

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

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

徒手教你使用zookeeper編寫服務(wù)發(fā)現(xiàn)

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

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

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

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

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

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

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

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é)點
	if err := client.ensureRoot(); err != nil {
		client.Close()
		return nil, err
	}
	return client, nil}// 關(guān)閉連接,釋放臨時節(jié)點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)用可能會返回節(jié)點已存在錯誤,這是正常現(xiàn)象,因為會存在多進程同時創(chuàng)建節(jié)點的可能。如果創(chuàng)建根節(jié)點出錯,還需要及時關(guān)閉連接。我們不關(guān)心節(jié)點的權(quán)限控制,所以使用zk.WorldACL(zk.PermAll)表示該節(jié)點沒有權(quán)限限制。Create參數(shù)中的flag=0表示這是一個持久化的普通節(jié)點。

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

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
}

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

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

func (s *SdClient) GetNodes(name string) ([]*ServiceNode, error) {
	path := s.zkRoot + "/" + name
	// 獲取字節(jié)點名稱
	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é)點列表時,我們先獲取字節(jié)點的名稱列表,然后依次讀取內(nèi)容拿到服務(wù)地址。因為獲取字節(jié)點名稱和獲取字節(jié)點內(nèi)容不是一個原子操作,所以在調(diào)用Get獲取內(nèi)容時可能會出現(xiàn)節(jié)點不存在錯誤,這是正?,F(xiàn)象。

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

最后我們看看如果使用以上代碼,為了方便起見,我們將多個服務(wù)提供者和消費者寫在一個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)
	}
}

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

徒手教你使用zookeeper編寫服務(wù)發(fā)現(xiàn)
向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI