背景
在微服务的场景下,企业内部服务少则几个到几十个,多则可能有上百个。每一个服务一般都以集群 HA 的方式进行部署。这时候自然会产生两个问题,一个是服务发现,也就是服务的消费方 consumer 如何发现找到服务的提供方 provider。第二个问题,就是负载均衡,服务消费方如何以某种负载均衡策略去访问集群当中的服务提供方的实例呢?如果你理解了这两个问题,可以说你已经理解了微服架构的最核心的技术问题。
服务发现与负载均衡的技术演进
服务发现和负载均衡并不是什么新问题。业界在较早的时候就已经探索出了通用的这个解决思路,也就是代理 proxy 的技术。也就是说在这个服务消费方和服务提供方之间增加一层代理,由代理负责服务发现和负载均衡等功能。消费方通过这个代理间接的去访问目标服务。在服务发现技术演进的过程当中,先后发展出三代的服务发现方案。这三代方案的核心都是代理,只不过代理在这个架构当中所处的位置是不同的。下面我们进一步分析每一种方案。
集中式代理
早前很多企业采用第一代的集中代理方案做服务发现,这种方案比较简单,在服务的消费方和提供方之间,独立的部署一层集中代理。这层代理有独立的团队,一般是运维或者是中间件团队负责运维和治理。常用的这个集中是代理有硬件负载均衡器,比如说 F5,或者软件的负载均衡器,比如说这个 Nginx、HAproxy 也可以软硬结合。比如说这个前面是 F5,再加上 Nginx,这种两层代理也是业界常见的做法。这种做法兼顾了这个配置的灵活性,因为 Nginx 比 F5 更容易配置。
这种方案一般需要引入 DNS 域名服务器进行配合,每一个服务在 DNS 上需要注册申请注册一个域名。并且每个服务需要在代理商配置服务域名和对应的这个服务实例 IP 列表的这个映射关系,DNS 和代理的这个配置一般是由运维人员手工完成的。消费者依赖于服务域名,这个域名指向这个集中代理调用的时候,代理先接收到请求,通过查找这个映射表,找到这个服务实例的 IP 列表。通过某种负载均衡的策略将请求转发到这个目标服务。
客户端嵌入式代理
第一代的这个传统集中代理方式,主要是靠运维人员手工配置的,效率不高,也缺乏灵活性,对开发人员不友好。随着微服和云技术的兴起,企业对这个服务发现的效率和灵活性提出了更高的要求。于是就出现了第二代的方案,也就是客户端嵌入式代理。这种方案将这个代理,包括服务发现和负载均衡这些功能逻辑以客户库 library 的形式嵌入到这个应用或者是服务程序当中。
这种方式一般需要独立的服务注册中心配合。服务启动的时候,先自动注册到这个服务注册中心,并且定期的报心跳进行保活。客户端代理可以通过这个服务注册中心发现服务的实例 IP 列表。调用的时候,根据某种负载均衡的策略选择某个服务实例进行调用。这种方式对开发人员比较友好,可以做到开发自助,不需要太多的运维介入。
这种做法目前是很多互联网公司的一个主流,相应的开源产品也很多。比方说 Netflix 开源的这个 Eureka,它就是一个注册中心。配套这个 Ribbon,它是一个客户端代理。Eureka 和 Ribbon 是这种方案的典型代表,国内开源的这个 Dubbo 也是采用这种模式,阿里开源的这个 Nacos 也提供这个 Nacos client 支持这种服务发现的模式。另外国外有个公司叫 Hashicorp,开源的 Consul 在社区也很热,它也可以对接 Ribbon 支持这种服务发现模式。
主机独立进程代理
第二代的方案虽然有高效灵活的好处,但是有语言依赖的问题。也就是说需要为不同的语言栈开发不同的客户端。我们知道在微服场景下面,企业一般会有多套语言栈同时并存的情况。如果要为每套语言栈开发一个客户端代理,显然这个开发成本太高。另外这个嵌入式代理方式也给这个客户端引入了复杂性。为了克服这些问题,随着容器和云原生技术的兴起,业界出现了第三代的方案,也就是主要机独立进程方案。
第三代的方案,它是上面两种方案的一个折中代理,既不是集中部署,也不是嵌入在这个客户应用程序当中,而是作为独立的进程部署在每一个主机上,这个主机可以是物理的或者是虚拟机。一个主机上的多个消费者,他可以共享这个代理,实现服务发现和负载均衡。
第三代的方案的架构原理和第二代方案是类似的,也需要引入这个服务注册中心来进行配合,只不过代理的位置部署为主机上的一个独立的进程。这个方案有一个更时髦的称谓叫 ServiceMesh。目前有一些开源产品,比如说这个 Envoy,linkerd,都可以做这个方案的代理。Istio 则可以对应到这个服务注册中心,当然除了这个服务注册和发现实际的这个 ServiceMesh,Istio 还具有其他的流量治理和监控安全等高级功能。另外这个 Kubernetes 平台内置也是支持这个服务发现机制。它的本质也是主机独立进程方案的一种变体。还有之前提到这个 Nacos、Consul 也支持这个 sidecar 的模式,也就是主机独立进程代理模式。
Kubernetes 服务发现机制详解
在 Kubernetes 当中,一个服务是有一组 pod 构成的集群。Pod 是 Kubernetes 当中的最基本的调度单位,它相当于是 Kubernetes 云平台当中的一个虚拟机概念。每一个 pod 都有一个 PodIP,并且 pod 之间可以通过这个 PodIP 是相互访问的。但是这个 PodIP 在 Kubernetes 当中是不固定的。可能会变,这种变的话可能是预期的,比方说正常的发布。也可能是非预期的,比如说自己挂了。为了屏蔽这种可能的变化,Kubernetes 平台当中引入了 Service 这样一个抽象。用于实现服务发现和负载均衡。
在服务发布的时候,Kubernetes 还会为每一个服务分配一个虚拟的 ClusterIP。在 Kubernetes 平台的每一个 worker 节点上,都部署有两个组件,一个叫 Kubelet,另一个叫 Kube-Proxy,其中 Kube-Proxy 是 Kubernetes 实现服务发现的关键。下面我们看一下简化的服务注册发现的一个流程。首先在服务 Pod 的实例发布的时候,Kubelet 会负责启动这些 Pod 实例,启动完成以后,Kubelet 会把服务的这些 PodIP 列表注册到 Master 节点上的 APIServer 中。之后通过这个服务 service 的发布,Kubernetes 会为服务分配相应的这个 ClusterIP,相关的信息也会记录在 APIServer 上面。
第三步在服务发现阶段,Kube-Proxy 会发现这个服务的 ClusterIP 和这个 Pod IP 列表之间的映射关系,并且修改本地的这个 iptables 的转发规则,也就是说只是这个 iptables 在接受到某个 ClusterIP 请求的时候就会进行负载均衡并转发到对应的 PodIP,当有 pod 要访问某个目标服务实例的时候,他就通过这个 ClusterIP 发起调用。这个 ClusterIP 会被本地的这个 iptables 的机制截获,进行负载均衡转发到目标 Pod 实例上面。实际这个 Pod 也不是直接调用这个 ClusterIP 的,而是调用这个服务名。
因为 ClusterIP 也会变,为了屏蔽这个 ClusterIP 的可能的变化 Kubernetes 在每一个 worker 节点上还引入了一个叫 KubeDNS 这样一个组件。它可以通过这个 master 发现服务名和 ClusterIP 之间的映射关系。这样的话消费者 pod 可以通过这个 KubeDNS 间接的发现服务的这个 ClusterIP。
总结
集中式代理方案,这个方案的优势是可以集中运维和治理,而且是具体语言栈无关的。它的不足是配置效率低,灵活性不高,一般需要运维介入配合的对开发人员不太友好。另外该方案还有单点失败和网络链路多一跳性能开销等问题。但是该方案总体比较简单,大中小规模的公司都是适用的,但是要求有一定的集中代理运维的能力。
第二种客户端嵌入式代理,它的优势是配置灵活,对开发人员自助友好,而且没有单点问题、性能好。它的不足是多语言麻烦,客户端开发成本比较高。另外该方案也会给这个客户端的应用程序引入复杂性,而且不好集中治理。该方案主要是适用于中大规模语言在比较统一的一些公司。
第三种主机独立进程方案是上面两种方案的一个折中,它同时兼顾获得了两者的优势。比方说它语言栈无关的,性能好,它也有不足,主要是引入门槛比较高,运维部署比较复杂。这个方案主要适用于中大规模运维能力比较强的公司。