為什么?
為什么會有服務注冊和服務發現?在它以前我們是怎么做的?
舉個例子:
比如我們做MySQL讀寫分離,就在本地配置一個文件,然后程序讀取這個配置文件里的數據進行數據庫讀寫分離的設置。
但是隨着業務發展迅速,業務模塊越來越多,數據也越來越多,MySQL數據庫也越來越多,需要讀取MySQL服務的業務模塊也越來越多。每次增加MySQL實例,每個業務模塊都要手動去寫一次本地配置。
想一想這里有什么問題?可以改進嗎?
每次手動寫配置,是不是很浪費時間,能不能做到自動一些?能不能做到只配置一次,業務模塊就可以自動讀取更新后的配置?於是我們就開始動腦筋了,就開始琢磨解決的辦法了。
在計算機領域,沒有什么是增加一個中間層解決不了的問題,有的話,“那就在增加一層”,對,最后那句是我說的- -!。
於是就想:能不能把所有的配置集中存放,程序模塊的配置更新由各個程序自動的來配置中心讀取,更新配置。而不是每次手動的增加每個程序模塊的配置。
於是我們就增加了一個“中間層”- 配置中心,集中存儲,管理分散在各個程序中的配置。配置中心就向外面提供配置服務。
不過Etcd應該屬於服務配置中心,屬於更加細化的配置中心。
還有更加專注的配置中心,比如Apollo,Nacos,disconf等等。
是什么?
什么是服務注冊和服務發現?
- 服務注冊 :就是向配置中心注冊一個配置的功能。
- 服務發現:就是發現配置中心增加、修改、刪除了配置的功能。發現配置的異動情況,讓更新配置。
會遇到哪些問題?
服務發現和服務注冊怎么解決問題以及會遇到哪些問題?
- 怎么標識一個服務?
在計算機里,標識一個計算機中運行的程序可以用ip+端口形式來標識,配置服務也可以用這種方式。 - 怎么發現一個配置服務的異動?
配置更新,刪除(下線),增加了,怎么發現配置的這些異動情況?一種是長輪詢,一種是心跳,每隔多長時間檢查一次,還有發布訂閱模式。 - 服務是否可用?
這種就要檢查服務的可用情況了,一般是健康檢查。
... ...
等等問題
怎么解決
業內主要的幾個解決方案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