Kyle's Notebook

《凤凰架构》阅读笔记(五):服务构建与流量治理

Word count: 4.6kReading time: 15 min
2021/07/25

《凤凰架构》阅读笔记(五):服务构建与流量治理

微服务架构是通过服务实现独立自治的组件,相比起在编译期静态链接到程序中的类库(Library),服务(Service)在进程间通过调用远程方法来实现功能,需要解决三大问题:

  • 消费者:如何定位服务提供者。

  • 生产者:以何种形式,暴露或隐藏内部服务,如何分配请求。

  • 调用过程:流量调度、服务质量与可靠性。

服务发现

远程服务调用通过 全限定名(FQDN,域名+主机名,定位网络中的主机)、端口号(定位 TCP/UDP 服务程序)与 服务标识(定位程序中的方法入口)定位服务远程服务的精确坐标。

早期服务发现只依赖 DNS 将 FQDN 翻译为 IP 地址或者 SRV 等类型的记录,结合负载均衡器,完成外部 IP 地址到服务内部实际 IP 的转换。在微服务架构中,这种做法难以应对服务的非正常宕机重启、正常的上下线越发频繁的情况。

可用与可靠

服务发现包括三个过程:

  • 服务注册:服务启动时应通过某些形式(如调用 API、产生事件消息、在 ZooKeeper/Etcd 的指定位置记录、存入数据库等)将自身坐标通知到服务注册中心。

    • 自注册:由应用程序本身来完成,比如 Spring Cloud 的@EnableEurekaClient 注解;

    • 第三方注册:由容器编排框架或第三方注册工具来完成,比如 Kubernetes 和 Registrator。

  • 服务维护:服务发现框架保证服务列表的正确性,避免告知消费者服务的坐标后得到的服务无法使用。通过多种协议(HTTP、TCP 等)、多种方式(长连接、心跳、探针、进程状态等)监控服务健康状态,将不健康的服务从服务注册表中剔除。

  • 服务发现:消费者从服务发现框架中,把符号(譬如 Eureka 中的 ServiceID、Nacos 中的服务名、或者通用的 FQDN)转换为服务实际坐标的过程,该过程一般通过 HTTP API 请求或 DNS Lookup 操作完成,也能通过诸如 Kubernetes 注入环境变量等方式实现。

以及其它可选功能:负载均衡、流量管控、键值存储、元数据管理、业务分组等。

服务发现功能需要分布式部署,就要在可用性和可靠性之间取舍。但服务发现被所有服务依赖、且不依赖于其它服务(类似配置中心,因此两者功能可以集成在一起),一旦崩溃则整个系统都不可用,因此必须兼顾两者。两种实现方法:

  • Eureka 优先保证高可用性。异步复制数据,新服务注册时在该服务发现节点宣告服务状态可见,但不保证其他节点上何时达到一致。旧服务下线或断网只由超时机制来实现从服务注册表中移除,而不会实时同步变动信息。服务端和客户端持有基于 TTL 机制更新的注册表缓存,即使注册中心崩溃仍维持最低限度的可用。对于节点关系相对固定,服务不会频繁上下线的系统很合适,以较小的同步代价换取了最高的可用性;当客户端拿到已发生变动的旧地址,也能通过 Ribbon 和 Hystrix 实现故障转移(Failover)或快速失败(Failfast)。

  • Consul 优先保证高可靠性。采用 Raft 算法,要求多数派节点写入成功后才算完成服务注册或变动,保证在集群外部读取结果必然一致;同时采用 Gossip 协议,支持多数据中心间大规模服务同步。由于它不像 Netflix OSS 全家桶式的微服务组件,无法为错误兜底。

除了服务注册信息同步速度的差异,应考虑系统形成不同分区时,每个分区只能从该区域的服务发现节点获取该区域的服务坐标:

  • 没有太大影响甚至有可能有益,应倾向选择 AP 式。比如当两个分区都可以独立提供完整、正确的服务能力,甚至还带来链路优化的效果。

  • 如果影响甚至比系统宕机更严重,应倾向选择 CP 式。比如系统中大量依赖集中式缓存、消息总线、或者其他有状态服务。

实现方式

分布式存储框架

基于分布式共识算法,利用软件自身完成服务注册与发现,但需要用户完成很多额外工作才能满足服务发现需求。

比如 ZooKeeper、Doozerd、Etcd,这类系统都是 CP 的(Redis 则 AP),通常尽最大努力实现高可用性。在整体较高复杂度的架构和算法的外部,维持着简单的应用接口,只有 CRUD、Watch 等少量 API,要在上面完成功能齐全的服务发现都必须自己实现。

基础设施

在云原生架构中,在基础设施和网络协议层面(主要是 DNS)透明化地实现服务发现,对应用无感知,更方便使用。代表是 Kubernetes 的 SkyDNS、CoreDNS。

采用这种方案,是 CP 或 AP 取决于后端采用的存储。好处是对应用透明,任何语言、框架、工具都支持 HTTP、DNS,不受程序技术选型约束。

开发者须考虑如何做客户端负载均衡、调用远程方法等问题,必须遵循或受限于基础设施的实现机制(比如服务健康检查时,DNS 协议规定服务缓存期限由 TTL 决定,要改用 KeepAlive 长连接实时判断服务存活情况相对麻烦)。

服务发现框架和工具

比如 Eureka、Consul 等 Spring 的远程服务发现的解决方案。除了通过 DNS 或 HTTP 请求完成符号与实际地址的转换,还支持服务健康检查、集中配置、K/V 存储、跨中心数据交换等功能。可自主决定 CP 或 AP 实现(Nacos 采用类 Raft 协议做 CP,采用自研的 Distro 协议做 AP,二选其一)。

对应用不透明(Consul 的主体逻辑在服务进程外以边车形式提供,Consul、Nacos 也支持基于 DNS 的服务发现。这些框架基本上做到以声明代替编码,比如修改 pom.xml、配置文件和注解即可实现,但依然可被应用程序感知),需要考虑程序语言、技术框架的集成问题。

有时不透明也为开发带来一定便捷,比如 Open Feign、Ribbon,使用配置或声明式接口就能使用。

网关路由

在单体架构中,由负载均衡器为单体系统副本分发流量,作为内部服务与外部请求之间的网关。

在微服务架构中,网关作为统一的出口对外提供服务,将外部访问网关地址的流量,根据适当的规则路由到内部集群服务节点上。能解耦服务的生产者与消费者,使消费者不受服务集群网络、安全、依赖等限制。

网关 = 路由器(基础职能)+ 过滤器(可选职能)

网关作为路由而言,需要考量:

  • 网络协议层次:虽然在实现上与负载均衡器没有区别,但在目的上,负载均衡器根据均衡算法对流量进行平均路由,而网关则根据流量特征进行准确路由,因此对网关有支持后端服务工作所在的层次协议解析的要求(TCP、HTTP、WebSocket、SOAP 等)。

  • 性能与可用性:作为服务对外地总出口,网关的路由性能将导致全局的、系统性的影响。性能与工作模式有关,比如 DSR 三角传输模式比代理模式强。但由于基于 HTTP 协议的服务接口一般是主流,网关默认支持七层路由,只能采用代理模式、无法直接进行流量转发。因此网关性能主要取决于网络 I/O 模型。

网络 I/O 模型

异步 I/O

数据到达缓冲区后,不需要由调用进程主动从缓冲区复制数据,而由操作系统复制完成后向线程发送信号,所以是非阻塞。

受限于操作系统,Windows NT 3.5+ 通过 IOCP 实现;Linux 2.6+ 后首次引入,但至今未完善(因此常用以 I/O 多路复用为主)。

同步 I/O

即由发起调用的进程自行从缓冲区复制数据,共有四种情况:

  • 阻塞 I/O:发起 I/O 调用后阻塞等待调用结果返回以及数据复制完成。最直观的 I/O 模型,逻辑清晰,节省 CPU 资源,缺点是线程休眠带来上下文切换,是需要切换到内核态的重负载操作,不应当频繁进行。

  • 非阻塞 I/O:发起 I/O 调用后不断轮询,直到有返回结果就开始复制数据。能够避免线程休眠,对于很快就能返回结果的请求可以节省切换上下文切换的消耗,对于较长时间才能返回的请求则浪费 CPU 资源,目前不常用。

  • 多路复用 I/O:多路复用 I/O 本质上是阻塞 I/O 的一种,好处是在同阻塞线程上处理多个端口的监听,当其中任意一个就绪即开始复制数据,然后继续阻塞监听。是目前高并发网络应用的主流,其下可细分 select、epoll、kqueue 等不同实现。

  • 信号驱动 I/O:发起调用后即返回,当结果就绪后由系统发起信号通知应用前来执行复制操作。

网关分析

性能:七层服务网关处理一次请求代理时,需要处理作为服务端对外部请求的应答,和作为客户端对内部服务的请求。因此这两组网络操作采用何种网络模型决定了网关的性能。

  • 以 Zuul 为例:1.0 采用阻塞 I/O,每个线程对应一个连接来代理流量,因此有线程休眠以及上下文切换的成本。当后端服务普遍属于计算密集型(服务耗时比较长,主要消耗在 CPU 上)时,能相对节省网关 CPU 资源;而当普遍是 I/O 密集型(服务很快返回,主要消耗在 I/O 上),会频繁发生上下文切换而降低性能。2.0 改用异步 I/O,大幅度减少了线程数,有更高的性能和更低的延迟。

  • 一些网关支持自行配置,或根据环境选择网络 I/O 模型,比如 Nginx 可在配置文件中指定 select、poll、epoll、kqueue 等并发模型。

由于网关的性能与实际使用场景密切相关,一般只能做定性分析(很难说哪种更高性能、高多少)。

可用性:由于用户只通过一个地址访问系统,任何系统的网络调用过程中都至少会有一个单点存在。而作为后端服务代理和流量入口的网关很容易成为单点。且其地址是唯一的,无法像注册中心一样以多个节点处理请求。因此网关设计:

  • 应尽可能轻量。尽管网关作为统一出入口可实现安全、认证、授权、限流、监控等功能,但仍需要仔细权衡,取得功能性与可用性之间的平衡,避免过度增加网关的职责。

  • 尽可能选择成熟的产品实现,譬如 Nginx Ingress Controller、KONG、Zuul 等,而不能只考虑性能选择新产品。

  • 需要高可用时,应考虑在网关前部署负载均衡器或 等价路由器(ECMP),让更成熟健壮的设施充当系统入口地址,使网关也可以扩展。

BFF 网关

网关不必为所有的前端提供无差别服务,而针对不同的前端聚合不同的服务,提供不同的接口和网络访问协议支持。比如为浏览器的 Web 程序提供 HTTP 协议的服务,为桌面系统的程序部署另一套网关、提供高性能的 gRPC 协议的服务。

在网关这种边缘节点上针对同一后端集群,裁剪、适配、聚合出适应不一样的前端服务,有助于后端稳定以及前端赋能。

1
2
3
4
5
 [Web APP]    ----REST---->  [Web APP Gateway]    ----+     [Warehouse Service]
|---> [Acccout Service]
[Mobile APP] ----RMI-----> [Mobile APP Gateway] ----+ [Payment Service]
|---> [Security Service]
[Desktop APP] ----gRPC----> [Desktop APP Gateway] ----+ [...]

客户端负载均衡

传统的网络调用过程一般涉及以下步骤:

  • 服务发现:从客户端浏览器发起服务请求,由 DNS 服务器将请求优先分配给传输距离最短的机房集群处理(IP -> 域名)。

  • 网关路由:服务网关将请求与配置中的特征比对,从 URL 中可得知该请求访问的服务,将请求的 IP 地址转换为内网服务集群入口地址。

  • 负载均衡:集群中部署多个服务,负载均衡器根据一定规则从中选取一个响应本次调用。由于均衡器部署在服务集群的前端,因此称为 集中式负载均衡

  • 服务容错:如果访问失败、没有需要的结果,则抛出 500 错误;根据预置的故障转移(Failover)策略,重试将调用分配给能够提供该服务的其他节点,最终成功响应。

从最近机房内网发出服务请求,绕到了网络边缘的网关、负载均衡器等设施上,再分配回内网中另外一个服务去响应。

客户端均衡器

用于简化服务调用过程:区别于服务端均衡器,客户端均衡器和服务实例一一对应,与服务实例并存于同一个进程中。最具代表性的是 Ribbon 和 Spring Cloud Load Balancer。其具备以下优点:

  • 进程内调用:均衡器与服务是进程内的方法调用,不存在额外的网络开销。

  • 简化流程:不依赖集群边缘设施,内部流量仅在服务集群内部循环,避免复杂的调用过程,能节省带宽、提高性能、降低运维复杂度。

  • 避免单点:其带宽资源将不像集中式均衡器般敏感,在以七层均衡器为主流、不能通过 IP 隧道和三角传输节省带宽的微服务环境中显得更具优势。

  • 灵活配置:能针对每个服务实例单独设置均衡策略等参数,亲和性,选择服务的策略等,都可以单独设置而不影响其它服务。

  • ……

但也有缺点:

  • 难实现技术异构:与服务运行于同一个进程内,意味着选型受服务编程语言的限制,要为每种语言实现对应的、能够支持复杂网络情况的均衡器非常困难。
  • 由于共用一个进程,均衡器稳定性会直接影响服务稳定性,CPU、内存等资源消耗也影响到服务可用资源。在服务数量达成千乃至上万规模时,均衡器消耗资源总量相当可观。
  • 请求来源可能是来自集群任一服务节点,使得内部网络安全和信任关系变得复杂,当攻破任一服务时更容易通过该服务突破集群中的其他部分。
  • 每个客户端均衡器必须持续跟踪其它服务的健康状况,实现服务上线下线、自动重连等功能。这些操作需要通过访问注册中心完成,数量庞大的客户端均衡器持续轮询服务注册中心,会为它带来不小的负担。
  • ……
1
2
3
+-------------+  +---> [Svc2-1]
| [Svc1 + LB] +--+---> [Svc2-2]
+-------------+ +---> [Svc2-3]

代理均衡器

随着服务网格(Service Mesh)逐渐流行,代理均衡器开始备受关注。它以边车代理的方式实现均衡器,解决了客户端均衡器的多数缺陷。

服务实例与代理均衡器之间通信要经过操作系统协议栈和网络协议栈。而 Kubernetes 可确保同一个 Pod 中的容器不会跨节点,因此置于同一个 Pod 中的服务实例和均衡器之间的交互本质上是本机回环设备的访问,开销比网络通信小得多。这种做法带来的收益:

  • 不受语言限制,可集中不同编程语言生态的优势,容易打造出能面对复杂网络情况的、高效健壮的均衡器。

  • 独立于服务进程的均衡器不会影响到服务进程稳定性。

  • 边车代理受控制平面统一管理,服务节点拓扑关系变化时,控制平面主动向边车代理发送更新服务清单的控制指令,避免客户端均衡器长期轮询注册中心造成浪费。

  • 在安全性、可观测性上,由于边车代理都是一致的实现,有利于在服务间建立双向 TLS 通信,有利于对整个调用链路给出更详细的统计信息。

  • ……

目前这项技术相对较新,对操作系统、网络运维知识要求较高,但在微服务架构中很有发展前景。

1
2
3
+-----------------+  +---> [Svc2-1]
| [Svc1] <-> [LB]-+--+---> [Svc2-2]
+-----------------+ +---> [Svc2-3]

地域与区域

待续。

CATALOG
  1. 1. 《凤凰架构》阅读笔记(五):服务构建与流量治理
    1. 1.1. 服务发现
      1. 1.1.1. 可用与可靠
      2. 1.1.2. 实现方式
        1. 1.1.2.1. 分布式存储框架
        2. 1.1.2.2. 基础设施
        3. 1.1.2.3. 服务发现框架和工具
    2. 1.2. 网关路由
      1. 1.2.1. 网络 I/O 模型
        1. 1.2.1.1. 异步 I/O
        2. 1.2.1.2. 同步 I/O
      2. 1.2.2. 网关分析
      3. 1.2.3. BFF 网关
    3. 1.3. 客户端负载均衡
      1. 1.3.1. 客户端均衡器
      2. 1.3.2. 代理均衡器
      3. 1.3.3. 地域与区域