微服务架构学习与思考(08):服务注册中心(服务注册与服务发现)
为什么会有服务注册中心
为什么会有服务注册中心?
在 client-server 服务-请求模式中,客户端发送请求到服务端,完成一次服务请求。这时候,开发也比较简单,写服务端代码就可以完成这种模式了。
但是,随着业务的发展,功能会越来越多,对外提供的服务也会随之增多。
服务越来越多,怎么才能对众多服务进行简单高效的管理?由原静态的,变更(比如增加、删除等)服务后难以及时通知到使用端的情况,变成服务频繁变更也不会中断服务,使用端也能感知到服务变更,这个要怎么做?
- 一,对服务进行单个单个管理比较麻烦,
- 二,要是能动态支持感知服务上线下线的能力,那提供服务就方便许多。怎么解决这些问题?
单个单个管理麻烦,好,把服务集中起来存储并进行管理怎么样?这是一种方法。
有了对服务进行集中统一管理的需求,那么一个名叫服务注册中心的软件就应运而生。
服务注册中心也可以理解为对客户端消费者和服务端服务提供者之间进行解耦。
对功能进行分类整合,然后提供相应的服务。随着功能越来越多,也会对原来的架构进行重构,这时候就可能用用微服务架构把原业务进行重构。
怎么做服务注册中心
基本功能
怎么做服务注册中心?首先要思考的是注册中心要做哪些功能。
原来的客户端请求-服务端响应模型,现在,中间就多了一个服务注册中心,如下图。
客户端(client)先去服务注册中心(service register center)查找,然后获取服务,根据获取的服务再向服务端(server)发送请求获取服务。
最开始要获取一个服务,先要知道服务后才能获取服务,怎么知道一个服务,给它取一个名字。用名字来标识一个服务.
那用名字怎么标识一个服务呢?
这个服务必须是唯一的,不能跟其他服务标识重复。
其实最早提供这种服务的是 DNS,请求一个域名,然后根据这个域名获取一个 ip 地址,在根据这个 ip 地址向服务器发送服务请求。
怎么在服务中心标识一个唯一服务?
我们可以用 DNS 这种方式,也可以用 ip+端口 形式来标识一个唯一服务。当然,ip+端口这种方式使用不是很灵活,因为 ip、端口 是固定的。
另外一种方式,通过服务名称来获取具体的服务地址,然后在进行调用,这样就灵活多了。
如果一个服务下线了,客户端怎么知道呢?不然服务不可用,影响用户使用。
第一种:客户端不时到服务中心检测服务是否存在。
第二种:服务中心主动通知客户端,某条服务下线了,你需要处理下,或换成新服务,或终止这个服务,等处理。
其实就是服务出现了一些异常情况,客户端能够感知到,以便客户端能及时的进行处理,避免服务不可用的情况。
根据上面,总结服务注册中心最基本的功能:
服务注册:对服务在服务注册中心进行唯一的标识。
服务发现:客户端要使用服务,到服务注册中心获取服务,用唯一标识。
服务下线处理:服务下线了,怎么处理。
其实跟我们常说的 CURD 这种基本操作类似,服务注册中心的基本功能也是如此,只不过在这里叫法名字不同而已,操作内涵基本相似。
在进一步想一想,这里还有一个问题,就是服务不可用。
上面的服务下线
是不可用的一种情况,这个指的是服务注册中心里的服务下线了,
如果服务端服务本身不可用,但服务还存在于服务注册中心里,客户端可以到服务注册中心查询到这个服务。这时候怎么办?
这就涉及到服务本身的健康检查。所以服务注册中心还要有一个功能:服务健康检查。
基本功能总结
1.服务注册
2.服务发现
3.服务下线处理
4.服务健康检查
其他功能思考
上面总结了一个服务注册中心最基本的功能。
但是一个可商用的注册中心,还有许多其他的问题需要考虑:
高可用
如果一个服务中心宕机了,然后服务就不可用了,那就没有做到高可用。
这就涉及服务中心本身是否高可用。
还有一个是服务自身的高可用。
异常处理
服务出现异常时怎么处理?是降级还是熔断,还是其他处理。
服务注册后,如何及时发现服务,如何更换到新服务
服务下线后,如何及时获取下线服务通知
服务注册模式
服务注册模式
服务注册模式:自注册模式和第三方注册模式
1.自注册模式:
每条服务实例自己负责在服务注册中心注册和销毁自身服务。
比如 Eureka client,它负责处理服务实例注册和销毁所有方面的功能。
优点:
简单,不需要额外的组件了。
缺点:
服务实例与服务注册中心耦合,必须为每种语言实现注册代码,比较麻烦。
2.第三方注册模式:
服务的注册和销毁不是通过自己来完成,而是通过第三方来完成。这个第三方就叫做服务注册器(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")
}
}
}
}