golang微服務實踐:服務注冊與服務發現 - Etcd的使用


為什么?

為什么會有服務注冊和服務發現?在它以前我們是怎么做的?

舉個例子:
比如我們做MySQL讀寫分離,就在本地配置一個文件,然后程序讀取這個配置文件里的數據進行數據庫讀寫分離的設置。
但是隨着業務發展迅速,業務模塊越來越多,數據也越來越多,MySQL數據庫也越來越多,需要讀取MySQL服務的業務模塊也越來越多。每次增加MySQL實例,每個業務模塊都要手動去寫一次本地配置。

想一想這里有什么問題?可以改進嗎?
每次手動寫配置,是不是很浪費時間,能不能做到自動一些?能不能做到只配置一次,業務模塊就可以自動讀取更新后的配置?於是我們就開始動腦筋了,就開始琢磨解決的辦法了。

在計算機領域,沒有什么是增加一個中間層解決不了的問題,有的話,“那就在增加一層”,對,最后那句是我說的- -!。

於是就想:能不能把所有的配置集中存放,程序模塊的配置更新由各個程序自動的來配置中心讀取,更新配置。而不是每次手動的增加每個程序模塊的配置。
於是我們就增加了一個“中間層”- 配置中心,集中存儲,管理分散在各個程序中的配置。配置中心就向外面提供配置服務。

不過Etcd應該屬於服務配置中心,屬於更加細化的配置中心。
還有更加專注的配置中心,比如Apollo,Nacos,disconf等等。

是什么?

什么是服務注冊和服務發現?

  • 服務注冊 :就是向配置中心注冊一個配置的功能。
  • 服務發現:就是發現配置中心增加、修改、刪除了配置的功能。發現配置的異動情況,讓更新配置。

會遇到哪些問題?

服務發現和服務注冊怎么解決問題以及會遇到哪些問題?

  1. 怎么標識一個服務?
    在計算機里,標識一個計算機中運行的程序可以用ip+端口形式來標識,配置服務也可以用這種方式。
  2. 怎么發現一個配置服務的異動?
    配置更新,刪除(下線),增加了,怎么發現配置的這些異動情況?一種是長輪詢,一種是心跳,每隔多長時間檢查一次,還有發布訂閱模式。
  3. 服務是否可用?
    這種就要檢查服務的可用情況了,一般是健康檢查。

... ...

等等問題

怎么解決

業內主要的幾個解決方案ZooKeeper,Etcd,Consul ,等。我們介紹etcd的使用。

etcd介紹

etcd是CoreOS團隊於2013年6月發起的開源項目,它的目標是構建一個高可用的分布式鍵值(key-value)數據庫。etcd內部采用raft協議作為一致性算法,etcd基於Go語言實現。

前面博客文章也有一些它的使用情況。

它是一個kv鍵值的數據庫:
所以可以存儲數據,那么就可以存儲配置數據。

它有watch功能:
所以可以發現配置的異動情況。變化時候可以起到通知作用。

它有key TTL功能:
etcdv3可以通過lease租約,設置服務生存周期TTL,讓key自動過期,通過KeepAlive定期續租,避免過期。

當然,它還可以做分布式鎖、分布式隊列等等其他功能,你可以google去學習它的其他功能。

例子

  • 注冊服務和獲取服務
// PutValueDemo 插入值demo,配置里面put相當於注冊一個服務(服務注冊),get相當於獲取一個服務(服務發現)
func (demo EtcdDemo) PutValueDemo(client *clientv3.Client) {
    fmt.Println("\n...put value demo...")
    fmt.Println("put value: ", demo)
    _, err := demo.PutValue(client)
    if err != nil {
        fmt.Println("failed to put value, err: ", err)
        return
    }

    getVal, err := demo.GetValue(client)
    if err != nil {
        fmt.Println("get val err: ", err)
        return
    }
    fmt.Println("get key:",string(getVal.Kvs[0].Key),",value:", string(getVal.Kvs[0].Value), ", Revision: ", getVal.Header.Revision)
}

如果你單獨在main函數里測試PutValueDemo(),

client, err := clientv3.New(clientv3.Config{
        Endpoints:  []string{"127.0.0.1:2379"},
        DialTimeout: time.Second * 5,
        })
 if err != nil {
        fmt.Println("failed to connect etcd: ", err)
        return
 }
 defer client.Close()

 demo := EtcdDemo{Key: "test1", Val: "val1"}
 demo.PutValueDemo(client)

你會發現:
Revision 的值,每次都是增加的。

get val: val1 ,key: test1 , Revision:  1
get val: val1 ,key: test1 , Revision:  2
這也是etcd的一個特性,可以用個來做分布式鎖。

  • watch 監測服務變化
// watchDemo 監聽key的變化
func (demo EtcdDemo) WatchDemo(client *clientv3.Client) {
    fmt.Println("\n...watch demo...")
    stopChan := make(chan interface{}) // 是否停止信號
    go func() {
        watchChan := client.Watch(context.TODO(), demo.Key, clientv3.WithPrefix())
        for {
            select {
            case result := <- watchChan:
                for _, event := range result.Events {
                    fmt.Printf("%s %q : %q\n", event.Type, event.Kv.Key, event.Kv.Value)
                }
                case <-stopChan:
                    fmt.Println("stop watching...")
                    return
            }
        }
    }()

    for i := 0; i < 5; i++ {
        var demo EtcdDemo
        demo.Key = fmt.Sprintf("key_%02d", i)
        demo.Val = strconv.Itoa(i)
        demo.PutValue(client)
    }
    time.Sleep(time.Second * 1)

    stopChan <- 1 //停止watch,在插入就不會監聽到了
}

運行后:

...watch demo...
putResp Revision:  13
PUT "key_00" : "0"
PUT "key_01" : "1"
putResp Revision:  14
PUT "key_02" : "2"
putResp Revision:  15
PUT "key_03" : "3"
putResp Revision:  16
PUT "key_04" : "4"
putResp Revision:  17
stop watching...

Revision單調遞增變化的。
watch 可以監測到put變化的情況。

  • 租約lease
// LeaseDemo 租約
func (demo EtcdDemo) LeaseDemo(client *clientv3.Client) {
    fmt.Println("\n...lease demo...")

    lease, err := client.Grant(context.TODO(), 2) //創建一個租約
    if err != nil {
        fmt.Println("grant err: ", err)
        return
    }

    testKey := "testleasekey"
    // 給這個testkey一個 2秒的TTL租約
    client.Put(context.TODO(), testKey, "testvalue", clientv3.WithLease(lease.ID))
    getVal, err := client.Get(context.TODO(), testKey)
    if err != nil {
        fmt.Println("get val err: ", err)
        return
    }
    vallen := len(getVal.Kvs)
    fmt.Println("before time sleep, val len: ", vallen)

    fmt.Println("sleep 4 seconds")
    time.Sleep(4 * time.Second) //睡眠4秒,讓租約過期

    getVal, _ = client.Get(context.TODO(), testKey)
    vallen = len(getVal.Kvs)
    fmt.Println("after 4 seconds, val len: ", vallen)
}

運行:

...lease demo...
before time sleep, val len:  1
sleep 4 seconds
after 4 seconds, val len:  0 //這里租約到期,刪掉了這個key和val

完整代碼在這里github

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM