為什么會有服務注冊中心
為什么會有服務注冊中心?
在 client-server 服務-請求模式中,客戶端發送請求到服務端,完成一次服務請求。這時候,開發也比較簡單,寫服務端代碼就可以完成這種模式了。
但是,隨着業務的發展,功能會越來越多,對外提供的服務也會隨之增多。
服務越來越多,怎么才能對眾多服務進行簡單高效的管理?由原靜態的,變更(比如增加、刪除等)服務后難以及時通知到使用端的情況,變成服務頻繁變更也不會中斷服務,使用端也能感知到服務變更,這個要怎么做?
一,對服務進行單個單個管理比較麻煩,二,要是能動態支持感知服務上線下線的能力,那提供服務就方便許多。怎么解決這些問題?
單個單個管理麻煩,好,把服務集中起來存儲並進行管理怎么樣?這是一種方法。
有了對服務進行集中統一管理的需求,那么一個名叫服務注冊中心的軟件就應運而生。
服務注冊中心也可以理解為對客戶端消費者和服務端服務提供者之間進行解耦。
對功能進行分類整合,然后提供相應的服務。隨着功能越來越多,也會對原來的架構進行重構,這時候就可能用用微服務架構把原業務進行重構。
怎么做服務注冊中心
基本功能
怎么做服務注冊中心?首先要思考的是注冊中心要做哪些功能。
原來的客戶端請求-服務端響應模型,現在,中間就多了一個服務注冊中心,如下圖。

客戶端(client)先去服務注冊中心(service register center)查找,然后獲取服務,根據獲取的服務再向服務端(server)發送請求獲取服務。
最開始要獲取一個服務,先要知道服務后才能獲取服務,怎么知道一個服務,給它取一個名字。用名字來標識一個服務.
那用名字怎么標識一個服務呢?這個服務必須是唯一的,不能跟其他服務標識相同。
其實最早提供這種服務的是 DNS,請求一個域名,然后根據這個域名獲取一個 ip 地址,在根據這個 ip 地址向服務器發送服務請求。
怎么在服務中心標識一個唯一服務?
我們可以用 DNS 這種方式,也可以用 ip+端口 形式來標識一個唯一服務。當然,ip+端口這種方式使用不是很靈活,因為 ip、端口 是固定的。
另外一種方式,通過服務名稱來獲取具體的服務地址,然后在進行調用,這樣就靈活多了。
如果一個服務下線了,客戶端怎么知道呢?不然服務不可用,影響用戶使用。
第一種:客戶端不時到服務中心檢測服務是否存在。
第二種:服務中心主動通知客戶端,某條服務下線了,你需要處理下,或換成新服務,或終止這個服務,等處理。
其實就是服務出現了一些異常情況,客戶端能夠感知到,以便客戶端能及時的進行處理,避免服務不可用的情況。
根據上面,總結服務注冊中心最基本的功能:
服務注冊:對服務在服務注冊中心進行唯一的標識。
服務發現:客戶端要使用服務,到服務注冊中心獲取服務,用唯一標識。
服務下線處理:服務下線了,怎么處理。
其實跟我們常說的 CURD 這種基本操作類似,服務注冊中心的基本功能也是如此,只不過在這里叫法名字不同而已,操作內涵基本相似。
在進一步想一想,這里還有一個問題,就是服務不可用。
上面的服務下線是不可用的一種情況,這個指的是服務注冊中心里的服務下線了,
如果服務端服務本身不可用,但服務還存在於服務注冊中心里,客戶端可以到服務注冊中心查詢到這個服務。這時候怎么辦?
這就涉及到服務本身的健康檢查。所以服務注冊中心還要有一個功能:服務健康檢查。
基本功能總結
1.服務注冊
2.服務發現
3.服務下線處理
4.服務健康檢查
其他功能思考
上面總結了一個服務注冊中心最基本的功能。
但是一個可商用的注冊中心,還有許多其他的問題需要考慮:
-
高可用
如果一個服務中心宕機了,然后服務就不可用了,那就沒有做到高可用。
這就涉及服務中心本身是否高可用。
還有一個是服務自身的高可用。
-
異常處理
服務出現異常時怎么處理?是降級還是熔斷,還是其他處理。
-
服務注冊后,如何及時發現服務,如何更換到新服務
-
服務下線后,如何及時獲取下線服務通知
服務注冊模式
服務注冊模式
服務注冊模式:自注冊模式和第三方注冊模式
自注冊模式:
每條服務實例自己負責在服務注冊中心注冊和銷毀自身服務。
比如 Eureka client,它負責處理服務實例注冊和銷毀所有方面的功能。
優點:
簡單,不需要額外的組件了。
缺點:
服務實例與服務注冊中心耦合,必須為每種語言實現注冊代碼,比較麻煩。
第三方注冊模式:
服務的注冊和銷毀不是通過自己來完成,而是通過第三方來完成。這個第三方就叫做服務注冊器(service registrar),
它來負責。也就是說調用第三方來完成服務相關操作。
服務注冊器通過輪詢部署環境或訂閱事件來跟蹤服務實例的變更。
優點:
解耦,服務相關操作與服務注冊中心解耦。
缺點:
引入第三方組件,鏈路變長,復雜度上升。
常用的服務注冊中心特性對比
如圖:

從上面的特性可以看出,這些作為常用的軟件,提供了更加豐富的功能。
可以對上面的一些軟件,比如 etcd,euerka,zookeeper,consul 等進行更詳細的研究。
etcd 作為服務注冊中心
前面寫過etcd服務注冊和服務發現 和 golang操作etcd基本例子 的文章,可以看看。
etcd 是一個基於 raft 算法強一致性高可用的服務存儲軟件。
它可以注冊服務和監控服務健康狀態的機制,用戶可以注冊服務,並且對注冊的服務設置 key TTL,lease(租約),定時保持服務的心跳來達到監控服務,它還有 watch 功能。
lease 租約,etcd 集群支持具有生命周期的租約。如果 etcd 集群在給 key 設定的 TTL 時間內未收到回復,則租約到期。租約到期或被撤銷時,綁定到該租約的所有 key-value 鍵值對會被刪除。如果不想被刪除,可以通過 KeepAlive 定期續租。
prefix 前綴,etcd 可以查詢具有相同 key 前綴的所有值。這個功能在服務注冊時,可以作為組服務來應用。
watch 監聽 ,watch 機制可以監聽某一個 key 值變化,也支持監聽一個范圍。可以利用 watch 功能監聽注冊服務刪除、更新等變化,然后通知watch了該服務的所有用戶。
高可用,etcd 本身采用的是分布式架構,集群化設計。所以它是具備高可用。
etcd 的 clientv3 目錄下 ,
client.go
主要結構體 client struct,以及里面包含的一些字段
// https://github.com/etcd-io/etcd/blob/release-3.4/clientv3/client.go#L72
type Client struct {
Cluster
KV
Lease
Watcher
Auth
Maintenance
... ...
}
kv.go,kv的主要操作
// https://github.com/etcd-io/etcd/blob/release-3.4/clientv3/kv.go
type KV interface {
// Put puts a key-value pair into etcd.
Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)
// Get retrieves keys.
Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
// Delete deletes a key, or optionally using WithRange(end), [key, end).
Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)
// Compact compacts etcd KV history before the given rev.
Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)
Do(ctx context.Context, op Op) (OpResponse, error)
// Txn creates a transaction.
Txn(ctx context.Context) Txn
}
服務注冊和服務發現代碼
package main
import (
"log"
"time"
"go.etcd.io/etcd/clientv3"
"go.etcd.io/etcd/mvcc/mvccpb"
)
const (
dialTimeout = time.Second * 3
endpoints = []string{"192.168.1.109:2379"},
)
func NewConfig(dialTimeout time.Duration, endpoints []string) clientv3.Config {
return clientv3.Config{
DialTimeout: dialTimeout,
Endpoints: endpoints,
}
}
func NewClient(cfg clientv3.Config) clientv3.Client {
client, err := clientv3.New(cfg)
if err != nil {
log.Fatal(err)
}
return client
}
// func NewKV(client clientv3.Client) clientv3.KV {
// return clientv3.NewKV(client)
// }
type Service struct {
key string
val string
client *clientv3.Client
keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse
leaseId clientv3.LeaseID
}
func NewService(key, val string) *Service {
cli := NewClient(NewConfig(dialTimeout, endpoints))
return &Service{
client: cli,
key: key,
val: val,
}
}
// 注冊服務並給定一個租期(租約)
func (s *Service) RegisterServiceWithLease(lease int64) error {
// 申請一個 lease 時長的租約
respLease, err := s.client.Grant(context.TODO(), lease)
if err != nil {
return err
}
// 注冊服務並綁定租約
_, err = s.client.Put(context.TODO(), s.key, s.val, clientv3.WithLease(respLease.ID))
if err != nil {
return err
}
// 續租
keepAliveChan, err := s.client.KeepAlive(context.TODO(), respLease.ID)
if err != nil {
return err
}
s.keepAliveChan = keepAliveChan
s.leaseID = respLease.ID
return nil
}
// 獲取服務,根據前綴獲取所有服務
func (s *Service) GetServiceWithPrefix(prefix string) (map[string]string, error) {
serviceMap = make(map[string]string, 0)
resp, err := s.client.Get(context.TODO(), prefix, clientv3.WithPrefix())
if err != nil {
return serviceMap, err
}
if resp.Kvs == nil || len(resp.Kvs) == 0 {
return serviceMap, errors.New("this is no service")
}
for i := range resp.Kvs {
if val := resp.Kvs[i].Value; val != nil {
key := string(resp.Kvs[i].Key)
serviceMap[key] = string(resp.Kvs[i].Value)
}
}
return serviceMap, nil
}
// 監聽續租情況
func (s *Service) ListenLease() error {
go func() {
for {
select {
case keepResp := <-s.keepAliveChan:
if keepResp == nil {
log.Println("lease is failed")
goto END
} else {
log.Printfln("grant lease: ", s.leaseID)
}
}
}
END:
}()
}
// 撤銷租約
func (s *Service) RevokeLease() error {
if _, err := s.client.Revoke(context.TODO(), s.leaseID); err != nil {
return err
}
return nil
}
// 刪除服務,根據前綴刪除服務
func (s *Service) DeleteServiceWithPrefix(prefix string) error {
deleteResp, err := s.client.Delete(context.TODO(), prefix, clientv3.WithPrefix())
if err != nil {
log.Println(err)
return err
}
return nil
}
// 監聽服務變化,根據前綴來監聽服務操作變化
func (s *Service) ListenServiceWithPrefix(prefix string) error {
watchRespChan := s.client.Watch(context.TODO(), prefix, clientv3.WithPrefix())
for watchResp := range watchRespChan {
for _, event := range watchResp.Events {
switch event.Type {
case mvccpb.PUT:
log.Println("etcd put operation", string(event.Kv.Value))
case mvccpb.DELETE:
log.Printfln("etcd delete operation")
}
}
}
}
