1. 微服务架构上篇:4. 基于etcd的服务发现与注册

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
全局流量管理 GTM,标准版 1个月
网络型负载均衡 NLB,每月750个小时 15LCU
简介: 1. 微服务架构上篇:4. 基于etcd的服务发现与注册

1. LB方案介绍



构建高可用、高性能的通信服务,通常采用服务注册与发现、负载均衡和容错处理等机制实现。根据负载均衡实现所在的位置不同,通常可分为以下三种解决方案:


1.1 集中式LB(Proxy代理模式)


640.png

在服务消费者和服务提供者之间有一个独立的LB,通常是专门的硬件设备如 F5,或者基于软件如 LVS,HAproxy等实现。LB上有所有服务的地址映射表,通常由运维配置注册,当服务消费方调用某个目标服务时,它向LB发起请求,由LB以某种策略,比如轮询(Round-Robin)做负载均衡后将请求转发到目标服务。LB一般具备健康检查能力,能自动摘除不健康的服务实例。


该方案主要问题:


  1. 单点问题,所有服务调用流量都经过LB,当服务数量和调用量大的时候,LB容易成为瓶颈,且一旦LB发生故障影响整个系统;
  2. 服务消费方、提供方之间增加了一级,有一定性能开销。


1.2 进程内LB(客户端负均衡)


640.png

针对第一个方案的不足,此方案将LB的功能集成到服务消费方进程里,也被称为软负载或者客户端负载方案。服务提供方启动时,首先将服务地址注册到服务注册表,同时定期报心跳到服务注册表以表明服务的存活状态,相当于健康检查,服务消费方要访问某个服务时,它通过内置的LB组件向服务注册表查询,同时缓存并定期刷新目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。LB和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能比较好。


该方案主要问题:

  1. 开发成本,该方案将服务调用方集成到客户端的进程里头,如果有多种不同的语言栈,就要配合开发多种不同的客户端,有一定的研发和维护成本;
  2. 另外生产环境中,后续如果要对客户库进行升级,势必要求服务调用方修改代码并重新发布,升级较复杂。


1.3 独立LB进程(外部负载均衡服务(Sidecar))


640.png

该方案是针对第二种方案的不足而提出的一种折中方案,原理和第二种方案基本类似。不同之处是将LB和服务发现功能从进程内移出来,变成主机上的一个独立进程。主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立LB进程做服务发现和负载均衡。该方案也是一种分布式方案没有单点问题,一个LB进程挂了只影响该主机上的服务调用方,服务调用方和LB之间是进程内调用性能好,同时该方案还简化了服务调用方,不需要为不同语言开发客户库,LB的升级不需要服务调用方改代码。


该方案主要问题:部署较复杂,环节多,出错调试排查问题不方便。


2. grpc服务发现与负载均衡实现



gRPC开源组件官方并未直接提供服务注册与发现的功能实现,但其设计文档已提供实现的思路,并在不同语言的gRPC代码API中已提供了命名解析和负载均衡接口供扩展。

640.png

其基本实现原理:

  1. 服务启动后gRPC客户端向命名服务器发出名称解析请求,名称将解析为一个或多个IP地址,每个IP地址标示它是服务器地址还是负载均衡器地址,以及标示要使用那个客户端负载均衡策略或服务配置。
  2. 客户端实例化负载均衡策略,如果解析返回的地址是负载均衡器地址,则客户端将使用grpclb策略,否则客户端使用服务配置请求的负载均衡策略。
  3. 负载均衡策略为每个服务器地址创建一个子通道(channel)。
  4. 当有rpc请求时,负载均衡策略决定那个子通道即grpc服务器将接收请求,当可用服务器为空时客户端的请求将被阻塞。


根据grpc官方提供的设计思路,基于进程内LB方案(即第2个案),结合分布式一致的组件(如Zookeeper、Etcd),可找到grpc服务发现和负载均衡的可行解决方案。


3. 核心代码实战



接下来以Go语言为例,简单介绍下基于Etcd3的关键代码实现。


3.1 服务注册实现: register.go


注册的核心逻辑就是将服务的启动IP和Port以name的形式注册到基于etcd的target中,这样当客户端去发现服务的时候以name的形式去请求target(etcd)获取IP和Port,这样就会将请求直接打到以这个IP和port的服务器上。


const schema = "etcdv3_resolver"
func Register(target, service, host, port string, interval time.Duration, ttl int) error {
// 以IP和Port作为etcd的value
 serviceValue := net.JoinHostPort(host, port)
// 以schema+name(服务名称)+value作为etcd的key
 serviceKey := fmt.Sprintf("/%s/%s/%s", schema, service, serviceValue)
 // 获取endpoints(即etcd的nodehost:port列表)用于实例化etcd对象
 var err error
 cli, err := clientv3.New(clientv3.Config{
  Endpoints: strings.Split(target, ","),
 })
// 获取租约
 resp, err := cli.Grant(context.TODO(), int64(ttl))
 if err != nil {
  return err
 }
// 将key和value存储到etcd中
 if _, err := cli.Put(context.TODO(), serviceKey, serviceValue, clientv3.WithLease(resp.ID)); err != nil {
  return err
 }
// 根据租约ID保持与etcd的存活探针
 if _, err := cli.KeepAlive(context.TODO(), resp.ID); err != nil {
  return err
 }
}


3.2 服务发现实现: watcher.go


服务发现核心逻辑就是根据客户端上传的name去etcd查询后端服务,查询到就把服务注册列表返回给grpc客户端,客户端根据负载均衡策略(轮循)直接发起rpc调用,不用夸网络。


func (r *Resolver) watch(prefix string) {
// 根据name和schema拼接的前缀去匹配key,那么一定是一个列表,因为我们注册的key是schema+name+host:port 所以schema+name可以匹配多个key/value
 resp, err := r.cli.Get(context.Background(), prefix, clientv3.WithPrefix())
 if err == nil {
  for i := range resp.Kvs {
   addrDict[string(resp.Kvs[i].Value)] = resolver.Address{Addr: string(resp.Kvs[i].Value)}
  }
 }
//通知到grpc客户端地址列表
 update := func() {
  addrList := make([]resolver.Address, 0, len(addrDict))
  for _, v := range addrDict {
   addrList = append(addrList, v)
  }
  r.cc.UpdateState(resolver.State{Addresses: addrList})
 }
}
update()
//监听etcd注册上来的服务变化(新增,修改或者删除)
rch := r.cli.Watch(context.Background(), prefix, clientv3.WithPrefix(), clientv3.WithPrevKV())
 for n := range rch {
  for _, ev := range n.Events {
   switch ev.Type {
   case mvccpb.PUT: //新增或者修改服务
    addrDict[string(ev.Kv.Key)] = resolver.Address{Addr: string(ev.Kv.Value)}
   case mvccpb.DELETE: //删除服务
    delete(addrDict, string(ev.PrevKv.Key))
   }
  }
  update()
 }


3.3 server进行注册: main.go


var (
 serv = flag.String("service", "hello_service", "service name")
 host = flag.String("host", "localhost", "listening host")
 port = flag.String("port", "50001", "listening port")
 reg  = flag.String("reg", "http://localhost:2379", "register etcd address")
)
func main() {
 flag.Parse()
 lis, err := net.Listen("tcp", net.JoinHostPort(*host, *port))
 if err != nil {
  panic(err)
 }
//调用注册 将服务注册到etcd中 并且添加续约时间
 err = grpclb.Register(*reg, *serv, *host, *port, time.Second*10, 15)
 if err != nil {
  panic(err)
 }
}


3.4 client发现server: main.go


var (
 svc = flag.String("service", "hello_service", "service name")
 reg = flag.String("reg", "http://localhost:2379", "register etcd address")
)
func main() {
 flag.Parse()
 // 实例化name命名解析器
 r := grpclb.NewResolver(*reg, *svc)
 resolver.Register(r)
 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
// 通过r.Scheme()+"://authority/"+*svc发现服务的IP+Port列表
// 设置负载均衡策略:轮循 roundrobin
 conn, err := grpc.DialContext(ctx, r.Scheme()+"://authority/"+*svc, grpc.WithInsecure(), grpc.WithBalancerName(roundrobin.Name), grpc.WithBlock())
 cancel()
 if err != nil {
  panic(err)
 }
 ticker := time.NewTicker(1000 * time.Millisecond)
 for t := range ticker.C {
  client := pb.NewGreeterClient(conn)
  resp, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "world " + strconv.Itoa(t.Second())})
  if err == nil {
   logrus.Infof("%v: Reply is %s\n", t, resp.Message)
  }
 }
}


3.5 运行


1. 以docker方式启动etcd


➜  ~ docker run -d\
  -p 2379:2379 \
  -p 2380:2380 \
  --volume=${DATA_DIR}:/etcd-data \
  --name etcd ${REGISTRY}:latest \
  /usr/local/bin/etcd \
  --data-dir=/etcd-data --name node1 \
  --initial-advertise-peer-urls http://127.0.0.1:2380 --listen-peer-urls http://0.0.0.0:2380 \
  --advertise-client-urls http://127.0.0.1:2379 --listen-client-urls http://0.0.0.0:2379 \
  --initial-cluster node1=http://127.0.0.1:2380


2. 启动测试程序

分别启动服务端


终端1: go run -mod vendor cmd/svr/svr.go -port 50001
终端2: go run -mod vendor cmd/svr/svr.go -port 50002
终端3: go run -mod vendor cmd/svr/svr.go -port 50003


启动客户端

go run -mod vendor cmd/cli/cli.go
3. 结果


客户端响应:

640.png

可以看出来成功请求三次都是不同的服务,说明服务发现和负载均衡生效了。

服务端响应:

640.png

640.png

640.png


可以看出来三个server都响应了客户端请求,尚且只有一次,再次证明轮循负载均衡策略和服务注册生效了。

以上代码参考:https://github.com/wwcd/grpc-lb


4. 小结



这片文章是【微服务架构上篇】系列重点,因为服务注册和发现以及负载均衡策略是微服务架构中不可或缺的一环,只有彻底弄懂和掌握了此技术,才能在基于云原生微服务架构中得心应手,所以学好grpc+etcd+golang的技术栈走遍天下大厂都不怕!

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
12天前
|
缓存 监控 API
探索微服务架构中的API网关模式
【10月更文挑战第5天】随着微服务架构的兴起,企业纷纷采用这一模式构建复杂应用。在这种架构下,应用被拆分成若干小型、独立的服务,每个服务围绕特定业务功能构建并通过HTTP协议协作。随着服务数量增加,统一管理这些服务间的交互变得至关重要。API网关作为微服务架构的关键组件,承担起路由请求、聚合数据、处理认证与授权等功能。本文通过一个在线零售平台的具体案例,探讨API网关的优势及其实现细节,展示其在简化客户端集成、提升安全性和性能方面的关键作用。
45 2
|
16天前
|
存储 缓存 监控
探索微服务架构中的API网关模式
【10月更文挑战第1天】探索微服务架构中的API网关模式
51 2
|
2天前
|
负载均衡 监控 Cloud Native
云原生架构下的微服务治理策略与实践####
在数字化转型加速的今天,云原生技术以其高效、灵活、可扩展的特性成为企业IT架构转型的首选。本文深入探讨了云原生环境下微服务治理的策略与实践路径,旨在为读者提供一个系统性的微服务治理框架,涵盖从服务设计、部署、监控到运维的全生命周期管理,助力企业在云端构建更加稳定、高效的业务系统。 ####
|
3天前
|
运维 监控 Cloud Native
云原生架构下,微服务治理的艺术与实践####
【10月更文挑战第14天】 在数字化转型的大潮中,云原生技术以其高效、灵活与可扩展性成为企业IT架构的首选。本文深入探讨了云原生架构的核心理念,聚焦于微服务治理的策略与实践,揭示了如何通过精细化管理提升系统的响应速度、稳定性和可维护性。不同于传统的摘要概述,本文摘要旨在直接触及读者关注的核心——即如何在复杂多变的云环境中,实现微服务的高效协同与治理,为读者提供一个清晰的行动指南。 ####
12 1
|
14天前
|
Kubernetes 安全 微服务
使用 Istio 缓解电信 5G IoT 微服务 Pod 架构的安全挑战
使用 Istio 缓解电信 5G IoT 微服务 Pod 架构的安全挑战
39 8
|
1月前
|
安全 应用服务中间件 API
微服务分布式系统架构之zookeeper与dubbo-2
微服务分布式系统架构之zookeeper与dubbo-2
|
1月前
|
负载均衡 Java 应用服务中间件
微服务分布式系统架构之zookeeper与dubbor-1
微服务分布式系统架构之zookeeper与dubbor-1
|
2月前
|
Kubernetes Cloud Native Docker
云原生之旅:从容器到微服务的架构演变
【8月更文挑战第29天】在数字化时代的浪潮下,云原生技术以其灵活性、可扩展性和弹性管理成为企业数字化转型的关键。本文将通过浅显易懂的语言和生动的比喻,带领读者了解云原生的基本概念,探索容器化技术的奥秘,并深入微服务架构的世界。我们将一起见证代码如何转化为现实中的服务,实现快速迭代和高效部署。无论你是初学者还是有经验的开发者,这篇文章都会为你打开一扇通往云原生世界的大门。
|
17天前
|
消息中间件 负载均衡 Cloud Native
云原生之旅:从容器到微服务的架构演变
在数字化转型的风潮中,云原生技术以其灵活性、可扩展性和弹性而备受青睐。本文将通过一个虚拟的故事,讲述一个企业如何逐步拥抱云原生,实现从传统架构向容器化和微服务架构的转变,以及这一过程中遇到的挑战和解决方案。我们将以浅显易懂的方式,探讨云原生的核心概念,并通过实际代码示例,展示如何在云平台上部署和管理微服务。
|
1月前
|
JSON 监控 安全
探索微服务架构中的API网关模式
【9月更文挑战第22天】在微服务架构的海洋中,API网关如同一位智慧的守门人,不仅管理着服务的进出,还维护着整个系统的秩序。本文将带你一探究竟,看看这位守门人是如何工作的,以及它为何成为现代云原生应用不可或缺的一部分。从流量控制到安全防护,再到服务聚合,我们将一起解锁API网关的秘密。