微服务的模式语言-3-服务治理
# 服务发现
一个基于 RPI 的客户端如何在网络上发现服务实例的位置?
不同服务之间通常需要相互调用。在单体应用程序当中,服务间通过语言层级的方法或者过程实现相互调用。在传统的分布式系统部署下,服务在固定并且已知的位置(主机与端口)运行,从而确保各服务可利用HTTP/REST或者某种RPC机制进行相互调用。然而,现代化微服务应用程序通常在虚拟化或者容器化环境中运行,在这样的环境中服务的实例数量和位置是动态变化的,因此,要想实现客户端向动态变化的一组服务端实例发送请求,我们必须采用新的机制。
需求:
- 每一服务实例都会在特定位置(主机与端口)通过HTTP/REST或者Thrift等方式发布一个远程API。
- 服务端实例的具体数量及位置会发生动态变化。
- 虚拟机与容器通常会被分配动态IP地址。
- 服务实例的数量会发生动态变化。例如,EC自动伸缩组会根据负载情况随时调整实例数量。
# 服务注册表(Service registry)
一个记录了服务实例位置的数据。经常作为服务注册表使用的技术有:
- Eureka
- Apache Zookeeper
- Consul
- Etcd
- Kubernetes、Marathon 以及 AWS ELB 等系统中存在一套隐式的服务注册表。
# 服务注册方式
需求:
- 各服务实例必须在启动时被注册至服务注册表,并在关闭时进行注销。
- 崩溃的服务实例必须从服务注册表中注销。
- 在运行但无力处理请求的服务实例必须从服务注册表中注销。
# 自注册(Self registration)
一项服务实例必须可以自动注册到服务注册表中。在启动时,该服务实例将自身(主机与IP地址)注册至服务注册表,使自身可被发现。客户端必须定期更新其注册信息,确保注册表获悉其仍处于运行状态。在关闭时,服务实例从服务注册表中自动注销。
这一流程通常由微服务基底框架实现。典型的自注册框架有Netflix Eureka和Apache Zookeeper等。其中Eureka为服务实现自动注册(注销)提供一个注册API及一套客户端库;Zookeeper每项服务对应一个特定的Zookeeper znode,各服务实例在启动时会在该服务的znode下创建一个具有短暂存活时间的子znode,服务客户端通过检索该服务的子znode即可判断对应服务实例的可用性。
优势:
- 服务实例了解自身状态,因此能够实现比启动/停止更为复杂的状态模型
缺点:
- 将服务与服务注册表耦合起来。
- 需要为编写服务时使用的每种编程语言/框架分别实现服务注册逻辑,例如Node.JS/JavaScript、Java/Scala等等。
- 仍在运行但无法处理请求的服务实例通常无法自动在服务注册表中进行自我注销。
# 第三方注册(3rd party registration)
通过第三方模块来进行服务实例信息到服务注册表的注册过程。例如:
- Netflix Prana - 这是一款“边车”类应用,可与非JVM应用共同运行并利用Eureka为该应用注册,能够通过执行健康检查来判断当前服务实例的可用性。
- AWS Autoscaling Groups - 能够自动将EC2实例注册(注销)至Elastic Load Balancer。
- Joyent的Container buddy 运行在Docker容器当中,作为服务的父进程并将其注册至注册表。
- Registrator - 能够将Docker容器注册至多种服务注册表,或者从其注销。
- 以Kubernetes 与 Marathon 为代表的各类集群框架将服务实例注册至内置/隐式的注册表,或者从其注销。
优点:
- 与自注册模式相比,服务代码复杂程度更低,因为其无需实现自动注册。
- 注册工具可对服务实例执行健康检查,并根据检查结果注册或者注销该实例。
缺点:
- 第三方注册模式可能只了解服务实例的一些表层状态,例如其是否正在运行,因此 无法了解其是否能够处理请求
- 除非该注册工具属于基础设施的一部分,否则我们需要对其进行安装、配置与维护。另外,因为它是关键系统组件,因此需要保证其具有高度可用性。
# 服务发现方式
# 客户端服务发现(Client-side discovery)
客户端通过直接查询服务注册表获取服务实例的位置。在向某一服务发送请求时,客户端会通过查询服务注册表(Service Registry),以获取该服务实例的位置。该注册表中包含全部服务的位置。
典型示例 Netflix OSS:Eureka 充当其中的服务注册表、Ribbon Client 是一套HTTP客户端,负责向 Eureka 发出查询任务并将 HTTP 请求路由至可用的服务实例。
优点:
- 相较于服务器端服务发现,其活动部件与网络中转数量更少。
缺点:
- 这一模式使客户端与服务注册表相耦合。
- 需要为应用程序中使用的每种编程语言/框架建立客户端服务发现逻辑,例如Java/Scala以及JavaScript/Node JS。举例来说,Netflix Prana 就为非 JVM 客户端提供了一套基于HTTP代理的服务发现方案。
# 服务器端服务发现(Server-side discovery)
路由模块通过查询服务注册表获取服务实例的位置。在向某一服务发送请求时,客户端会通过在已知位置运行的路由器(或者是负载均衡器)发送请求。路由器会查询服务注册表,并向可用的服务实例转发该请求。服务注册表也可能背内建于路由器之中。
典型示例:
AWS Elastic Load Balancer(即AWS弹性负载均衡,简称ELB)便是一个服务器端服务发现模式的例子。客户端向该ELB发出HTTP(S)请求(或者开启TCP连接),而ELB则在一组EC2实例中对该流量进行负载均衡。ELB既能够对来自互联网的外部流量进行负载均衡,又能够被部署在VPC中,对内部流量进行负载均衡。ELB同样可作为服务注册表发挥作用。EC2实例可通过API调用或者借助自动伸缩分组机制注册至ELB。
一些集群解决方案如 Kubernetes 以及 Marathon,会在每台主机上运行一套代理,用来提供服务器端服务发现模式的路由机制。为了访问服务,客户端可以利用被分配至该服务的端口接入这个本地代理。该代理随后会将各请求转发给在集群某处运行的服务实例。
优点:
- 相较于客户端服务发现,其客户端代码由于无需实现发现功能而更为简单。而且客户端只需要向路由机制发送请求即可。
- 部分云环境能直接提供此项功能,例如AWS Elastic Load Balancer。
缺点:
- 除非成为云环境的一部分,否则该路由机制必须作为另一系统组件进行安装与配置。为实现可用性和一定的接入能力,还需要为其配置一定数量的副本。
- 相较于客户端服务发现,服务器端发现机制需要更多的网络跳转。
# 测试(Testing)
如何更便捷的测试?
# 服务组件测试(Service Component Test)
a test suite that tests a service in isolation using test doubles for any services that it invokes
# 服务集成协议测试(Service Integration Contract Test)
a test suite for a service that is written by the developers of another service that consumes it
# 可观测性(Observability)
如何掌握一个运行中微服务应用的行为并进行有效的故障排错?
# 应用日志(Log aggregation)
聚合应用程序产生的日志文件
# 应用指标(Application metrics)
在代码中实现收集应用运营过程中各类指标的功能
# 审计日志(Audit logging)
把用户行为记录在数据库中供日后核查
# 分布式追踪(Distributed tracing)
在服务代码中针对每一个外部访问,都分配一个唯一的标识符,并在跨服务访问时传递这个标识符以供追踪分布式引发的问题。例如,当通过一个集中式服务处理外部请求时,记录请求本身的信息以及请求的开始和结束时间。
# 异常追踪(Exception tracking)
把所有服务程序代码触发的异常信息都汇聚到集中的异常追踪服务,并根据一定的逻辑对开发者或运维人员发出通知。
# 健康检查 API(Health check API)
一个监控服务可调用的 API,通常返回服务健康度信息,或对 ping 等心跳检查请求做出响应。
# 可靠性
如何避免由于服务故障或网络中断所引起的故障蔓延到其他服务?
# 断路器(Circuit Breaker)
当远端服务返回的故障率超过一定的阀值时,客户端代理(比如 API 网关)对远程服务的调用将立刻返回失败的信息