关于微服务架构下的服务注册与发现

服务发现

假设我们正在编写一些代码来调用具有REST API或Thrift API的服务。 为了发出请求,代码需要知道服务实例的网络位置(IP地址和端口)。 在物理硬件上运行的传统应用程序中,服务实例的网络位置是相对静态的。 例如,您的代码可以从偶尔更新的配置文件中读取网络位置。

但是,在现代的基于云的微服务应用程序中,这是一个要解决的难题,如下图所示。

Service discovery is difficult in a modern, cloud-based microservices application because the set of instances, and their IP addresses, are subject to constant change

 

服务实例具有动态分配的网络位置。 而且,服务实例集会由于autoscaling,故障和升级而动态更改。 因此,您的客户端代码需要使用更复杂的服务发现机制。

有两种主要的服务发现模式:客户端发现和服务器端发现。 首先让我们看一下客户端发现。

The Client‑Side Discovery Pattern客户端服务发现

使用客户端发现时,客户端负责确定可用服务实例的网络位置,并在它们之间进行负载平衡请求。 客户端查询服务注册表,该服务注册表是可用服务实例的数据库。 然后,客户端使用负载平衡算法来选择可用的服务实例之一并发出请求。

下图显示了此模式的结构。

With client-side service discovery, the client determines the network locations of available service instances and load balances requests across them

服务实例的网络位置在启动时会在服务注册表中注册。实例终止时,将从服务注册表中将其删除。通常使用心跳机制定期刷新服务实例的注册。

Netflix OSS提供了客户端发现模式的一个很好的例子。 Netflix Eureka是一个服务注册表。它提供了一个REST API,用于管理服务实例注册和查询可用实例。 Netflix Ribbon是IPC客户端,可与Eureka一起在可用服务实例之间负载均衡请求。我们将在本文后面更深入地讨论Eureka。

客户端发现模式具有多种优点和缺点。这种模式相对简单,除了服务注册表之外,没有其他活动部分。此外,由于客户端知道可用的服务实例,因此它可以做出智能的,特定于应用程序的负载平衡决策,例如一致地使用哈希。这种模式的一个重大缺陷是它将客户端与服务注册表耦合在一起。您必须为服务客户端使用的每种编程语言和框架实现客户端服务发现逻辑。

现在,我们已经研究了客户端发现,让我们看一下服务器端发现。

The Server‑Side Discovery Pattern服务端发现模式

服务发现的另一种方法是服务器端发现模式。 下图显示了此模式的结构。

With the server-side service discovery, the load balancer queries a service registry about service locations; clients interact only with the load balancer

客户端通过负载平衡器向服务发出请求。负载平衡器查询服务注册表,并将每个请求路由到可用的服务实例。与客户端发现一样,服务实例在服务注册表中注册和注销。

AWS的 Elastic Load Balancer(ELB)是一个服务器端服务发现的例子。 ELB通常用于平衡来自Internet的外部流量。但是,我们还可以使用ELB负载均衡虚拟私有云(VPC)内部的流量。客户端使用其DNS名称通过ELB发出请求(HTTP或TCP)。 ELB在一组已注册的弹性计算云(EC2)实例或EC2容器服务(ECS)容器之间平衡流量。没有单独的服务注册表。而是将EC2实例和ECS容器注册到ELB本身。

HTTP服务器和负载平衡器(例如NGINX Plus和NGINX)也可以用作服务器端发现负载平衡器。例如,此博客文章介绍了如何使用Consul Template动态重新配置NGINX反向代理。 Consul模板是一种工具,可从存储在Consul服务注册表中的配置数据中定期重新生成任意配置文件。每当文件更改时,它将运行一个任意的shell命令。在博客文章描述的示例中,Consul模板生成一个nginx.conf文件,该文件配置反向代理,然后运行命令告诉NGINX重新加载配置。更复杂的实现可以使用其HTTP API或DNS动态重新配置NGINX Plus。

某些部署环境(例如Kubernetes和Marathon)在群集中的每个主机上运行代理。代理充当服务器端发现负载平衡器的角色。为了向服务提出请求,客户端使用主机的IP地址和服务的分配端口通过代理路由请求。然后,代理透明地将请求转发到在群集中某处运行的可用服务实例。

服务器端发现模式具有一些优点和缺点。这种模式的一大好处是服务发现机制的细节从客户端被抽象出来。客户只需向负载均衡器发出请求。这样就无需为服务客户端使用的每种编程语言和框架实现发现逻辑。另外,如上所述,某些部署环境是免费提供此功能的。但是,这种模式也有一些缺点。除非负载平衡器是由部署环境提供的,否则它是您需要设置和管理的又一个高度可用的系统组件。

The Service Registry服务注册

服务注册表是服务发现的关键部分。服务注册表其实就是一个数据库,其中包含服务实例的网络位置network locations。服务注册表需要高度可用且保证是最新。客户端可以缓存从服务注册表获得的网络位置。但是,当缓存的信息不是最新的时候,客户端就无法发现服务实例。因此,服务注册表由使用复制协议维护一致性的服务器群集组成。

如前所述,Netflix Eureka是服务注册表的一个很好的例子。它提供了一个REST API,用于注册和查询服务实例。服务实例使用POST请求注册其网络位置。它必须每30秒使用PUT请求刷新一次注册。通过使用HTTP DELETE请求或实例注册超时来删除注册。客户端可以使用HTTP GET请求检索注册的服务实例。

Netflix通过在每个Amazon EC2可用性区域中运行一台或多台Eureka服务器来实现高可用性。每个Eureka服务器都在具有弹性IP地址的EC2实例上运行。 DNS TEXT记录用于存储Eureka群集配置,该配置是从可用性区域到Eureka服务器网络位置列表的映射。当Eureka服务器启动时,它将查询DNS以检索Eureka群集配置,找到其对等方,并为其分配一个未使用的弹性IP地址。

Eureka客户端-服务和服务客户​​端-查询DNS以发现Eureka服务器的网络位置。客户端会优先在同一个可用性区域中使用Eureka服务器。但是,如果没有可用的客户端,则客户端将在另一个可用性区域中使用Eureka服务器。

服务注册表的其他示例包括:

  • etcd –提供高可用性,分布式,一致的键值存储,用于共享配置和服务发现。使用etcd的两个著名项目是Kubernetes和Cloud Foundry。
  • consul –用于发现和配置服务的工具。它提供了一个API,允许客户端注册和发现服务。领事可以执行运行状况检查以确定服务可用性。
  • Apache Zookeeper –广泛用于分布式应用程序的高性能协调服务。 Apache Zookeeper最初是Hadoop的子项目,但现在是顶级项目。

同样,如前所述,某些系统(例如Kubernetes,Marathon和AWS)没有明确的服务注册表。服务注册表对于他们而言是其基础结构的内置部分。是其一部分。

现在我们已经了解了服务注册表的概念,那么让我们看一下如何在服务注册表中注册服务实例。

Service Registration 服务注册实现

如前所述,服务实例必须在服务注册表中注册或注销。 有两种不同的方式来处理注册和注销。 一种选择是服务实例自行注册,即自我注册模式。 另一个选项是让某些其他系统组件管理服务实例的注册,即第三方注册模式。 首先让我们看一下自我注册模式。

Self‑Registration Pattern 自我注册模式
使用自我注册模式时,服务实例负责在服务注册表中进行自身注册和注销。 同样,如果需要,服务实例会发送'心跳'请求以防止其注册过期。 下图显示了此模式的结构。

With the self-registration pattern for service discovery, a service instance registers and deregisters itself with the service registry

Netflix OSS Eureka客户端就是这种方法的一个很好的例子。 Eureka客户端处理服务实例注册和注销的所有方面。 Spring Cloud项目实现了包括服务发现在内的各种模式,可以轻松地在Eureka中自动注册服务实例。 您只需使用@EnableEurekaClient注释对Java Configuration类进行注释。

自注册模式优点和缺点并存。 好处之一是它相对简单,不需要任何其他系统组件。 但是,主要缺点是它将服务实例耦合到服务注册表。 您必须使用服务使用的每种编程语言和框架来实现注册码。

将服务与服务注册表分离的另一种方法是第三方注册模式。

Third‑Party Registration Pattern第三方注册模式

使用第三方注册模式时,服务实例不负责在服务注册中心进行自身注册。 取而代之的是另一个称为服务注册器的系统组件来处理注册。 服务注册商通过轮询部署环境或订阅事件来跟踪对正在运行的实例集的更改。 当发现新的可用服务实例时,它将在服务注册表中注册该实例。 服务注册商还注销终止的服务实例。 下图显示了此模式的结构。With the third-party registration pattern for service discovery, a separate service registrar registers and deregisters service instances with the service registry

service registrar的一个示例是开源注册商Registrator 项目。它会自动注册和注销部署为Docker容器的服务实例。 Registrator支持多个服务注册表,包括etcd和Consul。

service registrar的另一个示例是NetflixOSS Prana。它主要用于用非JVM语言编写的服务,它是一个与服务实例并排运行的小程序应用程序。 Prana向Netflix Eureka注册和注销服务实例。

服务注册商是部署环境的内置组件。由自动伸缩组创建的EC2实例可以自动向ELB注册。 Kubernetes服务将自动注册并可供发现。

第三方注册模式的一个主要好处是服务与服务注册表分离。您无需为开发人员使用的每种编程语言和框架实施服务注册逻辑。而是在专用服务内以集中方式处理服务实例注册。

这种模式的一个缺点是,除非将其内置到部署环境中,否则它是另一个需要设置和管理的高度可用的系统组件。

总结

在微服务应用程序中,正在运行的服务实例集会动态更改。实例具有动态分配的网络位置。因此,为了使客户端对服务进行请求,它必须使用服务发现机制。

服务发现的关键部分是服务注册表。服务注册表是可用服务实例的数据库。服务注册表提供管理API和查询API。服务实例使用管理API在服务注册表中注册或注销。系统组件使用查询API查找可用的服务实例。

有两种主要的服务发现模式:客户端发现和服务端发现。在使用客户端服务发现的系统中,客户端查询服务注册表,选择可用实例,然后发出请求。在使用服务器端发现的系统中,客户端通过路由器发出请求,路由器查询服务注册表并将请求转发到可用实例。

服务实例在服务注册表中注册和注销的主要方法有两种。一种选择是让服务实例向服务注册表(自我注册模式)进行注册。另一个选项是让某些其他系统组件代表服务(第三方注册模式)来处理注册和注销。

在某些部署环境中,您需要使用服务注册表(例如Netflix Eureka,etcd或Apache Zookeeper)来设置自己的服务发现基础结构。在其他部署环境中,内置了服务发现。例如,Kubernetes和Marathon处理服务实例的注册和注销。他们还在充当服务器端发现路由器角色的每个群集主机上运行代理。

HTTP反向代理和负载平衡器(例如NGINX)也可以用作服务器端发现负载平衡器。服务注册中心可以将路由信息推送到NGINX并调用正常的配置更新。例如,您可以使用领事模板。 NGINX Plus支持其他动态重新配置机制–它可以使用DNS从注册表中获取有关服务实例的信息,并且提供用于远程重新配置的API。

Zookeeper实现服务注册和发现

zookeeper简介

ZooKeeper是用于分布式应用程序的分布式,开源的协作服务。 它提供了一组简单的原语,分布式应用程序可以基于这些原语来实现用于同步,配置维护以及分组和命名的更高级别的服务。 Zookeeper的设计易于编程,并使用了和文件系统的目录树结构样式类似的数据模型。 zookeeper运行于java环境上。

众所周知,协调服务很难做到。 它们特别容易出现诸如竞争情形和死锁之类的错误。 ZooKeeper项目创立背后的动机是减轻分布式应用程序从头开始实施协同服务的责任。

数据模型和分层名称空间

ZooKeeper提供的名称空间与标准文件系统的名称空间非常相似。 名称是由斜杠(/)分隔的一系列路径元素。 ZooKeeper名称空间中的每个节点都由路径标识。

zookeeper分层名称空间示意图如下:

ZooKeeper's Hierarchical Namespace

节点和ephemeral节点

与标准文件系统不同,ZooKeeper命名空间中的每个节点都可以具有与其关联的数据以及子节点。就像文件系统一样,该文件系统也允许文件成为目录。 (ZooKeeper旨在存储协调数据:状态信息,配置,位置信息等,因此存储在每个节点上的数据通常很小,在字节到千字节范围内。)常常用术语znode来表示ZooKeeper数据节点。

Znodes维护一个统计信息结构,其中包括用于数据更改,ACL更改和时间戳的版本号,以允许进行缓存验证和协调更新。 znode的数据每次更改时,版本号都会增加。例如,每当客户端检索数据时,它也接收数据的版本。

原子地读取和写入存储在名称空间中每个znode上的数据。读取将获取与znode关联的所有数据字节,而写入将替换所有数据。每个节点都有一个访问控制列表(ACL),用于限制谁可以执行操作。

ZooKeeper还具有短暂节点的概念。只有当创建znode的会话处于活动状态,这些znode就存在。会话结束时,将删除znode。临时节点在您想要实现时很有用。这一点在服务注册时非常有用,试想当一个服务down掉的时候,我们必然不能再让他被调用。

Conditional updates and watches

ZooKeeper支持watch的概念。 客户端可以在znode上设置watch。 znode更改时,将触发并删除监视。 触发监视后,客户端会收到一个数据包,说明znode已更改。 如果客户端与ZooKeeper服务器之一之间的连接断开,则客户端将收到本地通知。

在本地安装好zookeeper后之后,我们可以在zkcli命令行中管理和维护zookeeper当中的Znode,如下所示:

zookeeper实现服务注册和发现实例

下面是使用c#代码在zookeeper当中注册 url并且消费的关键代码示例:

注册的代码:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using org.apache.zookeeper;
using ServiceRegister.Zookeeper;
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static org.apache.zookeeper.ZooDefs;

namespace ServiceRegister
{
    public class Register
    {
        public static async void RegistAsync()
        {
            //we need to think about what if this part throw an exception
            WebHostBuilder webHostBuilder = new WebHostBuilder();
            var url = webHostBuilder.GetSetting("Urls");

            var configurationbuilder = new ConfigurationBuilder()
               .SetBasePath(Directory.GetCurrentDirectory())
               .AddJsonFile("appsettings.local.json");
            var config = configurationbuilder.Build();

            string serviceName = config.GetValue<string>("serviceName");
            string zookeeperUrl = config.GetValue<string>("zookeeperUrl");
            string projectName = config.GetValue<string>("projectName");

            ZookeeperClient zookeeperClient = new ZookeeperClient(zookeeperUrl, 5000);
          
            Thread.Sleep(1000);

            zookeeperClient.QueryPath = "/" + projectName;
            await create(zookeeperClient, projectName);

            zookeeperClient.QueryPath = "/" + projectName + "/" + serviceName;
            await create(zookeeperClient, serviceName);

            string uuid = Guid.NewGuid().ToString();

            string path = "/" + projectName + "/" + serviceName;
            ChildrenResult instance = await zookeeperClient.ZK.getChildrenAsync(path, false);

            foreach (string child in instance.Children)
            {
                zookeeperClient.QueryPath = "/" + projectName + "/" + serviceName + "/" + child;
                string childdata = await zookeeperClient.ReadConfigDataAsync();
                if (childdata == url)
                {
                    return;
                }
            }
            zookeeperClient.QueryPath = "/" + projectName + "/" + serviceName + "/" + uuid;
            await create(zookeeperClient, url);

        }

        public static async Task create(ZookeeperClient zookeeperClient, string data)
        {
            if (await zookeeperClient.ZK.existsAsync(zookeeperClient.QueryPath, false) == null)
            {
                zookeeperClient.ConfigData = Encoding.Default.GetBytes(data);
                await zookeeperClient.ZK.createAsync(zookeeperClient.QueryPath, zookeeperClient.ConfigData, Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
            }
        }
    }
}

ZookeeperClient代码:

using org.apache.zookeeper;
using org.apache.zookeeper.data;
using System;
using System.Text;
using System.Threading.Tasks;
using static org.apache.zookeeper.Watcher.Event;

namespace ServiceRegister.Zookeeper
{
    public class ZookeeperClient
    {
        public ZooKeeper ZK { get; set; }
        public string QueryPath { get; set; } = "/Configuration";
        public Stat Stat { get; set; }
        public byte[] ConfigData { get; set; } = null;

        public ZookeeperClient(string serviceAddress, int timeout)
        {
            ZK = new ZooKeeper(serviceAddress, timeout, new ConfigServiceWatcher(this));

        }

        public ZookeeperClient(string serviceAddress, int timeout, long sessionId, byte[] sessionPasswd)
        {
            ZK = new ZooKeeper(serviceAddress, timeout, new ConfigServiceWatcher2(this), sessionId, sessionPasswd);

        }

        public async Task<string> ReadConfigDataAsync()
        {
            if (this.ZK == null)
            {
                return string.Empty;
            }

            var stat = await ZK.existsAsync(QueryPath, true);

            if (stat == null)
            {
                return string.Empty;
            }

            this.Stat = stat;

            var dataResult = await ZK.getDataAsync(QueryPath, true);

            return Encoding.UTF8.GetString(dataResult.Data);
        }
        public class ConfigServiceWatcher : Watcher
        {
            private ZookeeperClient _cs = null;

            public ConfigServiceWatcher(ZookeeperClient cs)
            {
                _cs = cs;
            }

            public override async Task process(WatchedEvent @event)
            {
                Console.WriteLine($"Zookeeper connect sucessfully:{@event.getState() == KeeperState.SyncConnected}");

                if (@event.get_Type() == EventType.NodeDataChanged)
                {
                    var data = await _cs.ReadConfigDataAsync();

                    Console.WriteLine("{0}recieved the notice to modify the node value【{1}】," +
                        "the value has been changed to【{2}】。", Environment.NewLine, _cs.QueryPath, data);
                }
            }
        }

        public class ConfigServiceWatcher2 : Watcher
        {
            private ZookeeperClient _cs = null;

            public ConfigServiceWatcher2(ZookeeperClient cs)
            {
                _cs = cs;
            }

            public override async Task process(WatchedEvent @event)
            {
                Console.WriteLine($"Zookeeper connect sucessfully:{@event.getState() == KeeperState.SyncConnected}");

                if (@event.get_Type() == EventType.NodeDataChanged)
                {
                    var data = await _cs.ReadConfigDataAsync();

                    Console.WriteLine("{0}recieved the notice to modify the node value【{1}】," +
                        "the value has been changed to【{2}】。", Environment.NewLine, _cs.QueryPath, data);
                }
            }
        }

        public async Task Close()
        {
            if (this.ZK != null)
            {
                await ZK.closeAsync();
            }

            this.ZK = null;
        }
    }
}

服务发现端的代码如下:

using ServiceDiscover.Zookeeper;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ServiceDiscover
{
    public class Discover
    {
        public const string zookeeperAddress = "127.0.0.1:2181";
        public const string cpqZookeeperNode = "/cpq-ms-urls/";
        public static string GetUrlFromZookeeperAsync(string serviceName)
        {
            ZookeeperClient zookeeperClient = new ZookeeperClient("127.0.0.1:2181", 5000);
            //we need a zookeeper configuration, to manage the zookeeper configuration
            //currently hard code, later we can read from configuration

            string path = cpqZookeeperNode + serviceName;
            zookeeperClient.QueryPath = cpqZookeeperNode + serviceName;

            Thread.Sleep(1000);

            List<string> instances = (List<string>)zookeeperClient.ZK.GetChildren(path, false);

            int i = new Random().Next(instances.Count);//load balancer policy. to bo defined
            string randomInstance = instances[i];

            zookeeperClient.QueryPath = path + "/" + randomInstance;
            string childdata = zookeeperClient.ReadConfigDataAsync(path + "/" + randomInstance);

            string url = childdata;
            return url;
        }
    }
}

下面是client代码:

using System;
using System.Text;
using ZooKeeperNet;

namespace ServiceDiscover.Zookeeper
{
    public class ZookeeperClient
    {
        public ZooKeeper ZK { get; set; }

        public string QueryPath { get; set; } = "/Configuration";

        public byte[] ConfigData { get; set; } = null;


        public ZookeeperClient(string serviceAddress, int timeout)
        {
            ZK = new ZooKeeper(serviceAddress, new TimeSpan(0, 0, 0, timeout), new ConfigServiceWatcher(this));

        }

        public string ReadConfigDataAsync(string querypathstr)
        {
            if (this.ZK == null)
            {
                return string.Empty;
            }

            var stat = ZK.Exists(querypathstr, true);

            if (stat == null)
            {
                return string.Empty;
            }

            var dataResult = ZK.GetData(querypathstr, true, null);
            return Encoding.UTF8.GetString(dataResult);
        }

        public class ConfigServiceWatcher : IWatcher
        {
            private ZookeeperClient _cs = null;

            public ConfigServiceWatcher(ZookeeperClient cs)
            {
                _cs = cs;
            }

            public void Process(WatchedEvent @event)
            {
                Console.WriteLine("got one event in watcher!");
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值