点击查看目录
前言
Service Mesh 在企业落地中有诸多挑战,当与传统微服务应用共同部署治理时可用性挑战更为严峻。本文将以 Service Mesh 与 Spring Cloud 应用互联互通共同治理为前提,着重介绍基于 Consul 的注册中心高可用方案,通过各种限流、熔断策略保证后端服务的高可用,以及通过智能路由策略(负载均衡、实例容错等)实现服务间调用的高可用。
Service Mesh 与 Spring Cloud 应用的互通、互联
微服务是时下技术热点,大量互联网公司都在做微服务架构的推广和落地。同时,也有很多传统企业基于微服务和容器,在做互联网技术转型。而在这个技术转型中,国内有一个现象,以 Spring Cloud 与 Dubbo 为代表的微服务开发框架非常普及和受欢迎。近年来,新兴的 Service Mesh 技术也越来越火热,受到越来越多开发者的关注,大有后来居上的趋势。
在听到社区里很多人谈到微服务技术选型时,注意到他们讨论一个非此即彼的问题:采用 Spring Cloud 还是以 Istio 为代表的 Service Mesh 技术?然而这个答案并非非黑即白、非你即我,一部分应用采用 Spring Cloud,另一部分采用 Service Mesh(Istio)是完全可能的。今天我就和大家一起来讨论这个问题。
首先,我们来看一下 Spring Cloud 这个传统侵入式微服务框架。它包含以下优点:
- 集大成者,Spring Cloud 包含了微服务架构的方方面面;选用目前各家公司开发的比较成熟的、经得住实践考验的服务框架;
- 轻量级组件,Spring Cloud 整合的组件大多比较轻量级,且都是各自领域的佼佼者;
- 开发简便,Spring Cloud 对各个组件进行了大量的封装,从而简化了开发;
- 开发灵活,Spring Cloud 的组件都是解耦的,开发人员可以灵活按需选择组件。
特别感谢 Netflix,这家很早就成功实践微服务的公司,几年前把自家几乎整个微服务框架栈贡献给了社区,早期的 Spring Cloud 主要是对 Netflix 开源组件的进一步封装。不过近两年,Spring Cloud 社区开始自研了很多新的组件,也接入了其他一些互联网公司的优秀实践。
接下来,我们简单看一下 Service Mesh 框架。它带来了两大变革:微服务治理与业务逻辑的解耦,异构系统的统一治理。此外,服务网格相对于传统微服务框架,还拥有三大技术优势:可观察性、流量控制、安全。服务网格带来了巨大变革并且拥有其强大的技术优势,被称为第二代“微服务架构”。
然而就像之前说的软件开发没有银弹,传统微服务架构有许多痛点,而服务网格也不例外,也有它的局限性。这些局限性包括:增加了链路与运维的复杂度、需要更专业的运维技能、带来了一定的延迟以及对平台的适配。
更多关于 Spring Cloud 与 Service Mesh 的优缺点与比较,请阅读 Istio-Handbook [Service Mesh 概述]。
前面提到过,对于传统微服务框架 Spring Cloud 与新兴微服务框架 Service Mesh,并非是个非黑即白,非你即我,延伸到微服务与单体架构,它们也是可以共存的。
也可以将其与混合云相类比,混合云中包含了公有云、私有云,可能还有其它的自有基础设施。目前来看,混合云是一种流行的实践方式;实际上,可能很难找到一个完全单一云模式的组织。对多数组织来说,将一个单体应用完全重构为微服务的过程中,对开发资源的调动是一个很严峻的问题;采用混合微服务策略是一个较好的方式,对开发团队来说,这种方式让微服务架构触手可及;否则的话,开发团队可能会因为时间、经验等方面的欠缺,无法接受对单体应用的重构工作。
构建混合微服务架构的最佳实践:
- 最大化收益的部分优先重构;
- 非 Java 应用优先采用 Service Mesh 框架。
混合微服务出现的原因是为了更好的支持平滑迁移,最大限度的提升服务治理水平,降低运维通信成本等,并且可能会在一个较长的周期存在着。而实现这一架构的前提,就是各服务的“互联互通”。
要想实现上述“混合微服务架构”,运行时支撑服务必不可少,它主要包括服务注册中心、服务网关和集中式配置中心三个产品。
传统微服务和 Service Mesh 双剑合璧(双模微服务),即“基于 SDK 的传统微服务”可以和“基于 Sidecar 的 Service Mesh 微服务”实现下列目标:
- 互联互通:两个体系中的应用可以相互访问;
- 平滑迁移:应用可以在两个体系中迁移,对于调用该应用的其他应用,做到透明无感知;
- 灵活演进:在互联互通和平滑迁移实现之后,我们就可以根据实际情况进行灵活的应用改造和架构演进。
这里还包括对应用运行平台的要求,即两个体系下的应用,既可以运行在虚拟机之上,也可以运行在容器 /K8s 之上。我们不希望把用户绑定在 K8s 上,因此 Service Mesh 没有采用 K8s 的 Service 机制来做服务注册与发现,这里就突出了注册中心的重要性。
百度智能云 CNAP 团队实现了上述混合微服务架构,即实现了两个微服务体系的应用互联互通、平滑迁移、灵活演进。上述混合微服务架构图包括以下几个组件:
- API Server:前后端解耦,接口权限控制、请求转发、异常本地化处理等等;
- 微服务控制中心:微服务治理的主要逻辑,包括服务注册的多租户处理、治理规则(路由、限流、熔断)的创建和转换、微服务配置的管理;
- 监控数据存储、消息队列:主要是基于 Trace 的监控方案使用的组件;
- 配置中心:微服务配置中心,最主要的功能是支持配置管理,包括治理规则、用户配置等所有微服务配置的存储和下发,微服务配置中心的特色是借助 SDK 可以实现配置/规则热更新。
接下来主要看一下注册中心的服务注册和发现机制:
- Spring Cloud 应用通过 SDK、Service Mesh 应用实现 Sidecar 分别向注册中心注册,注册的请求先通过微服务控制中心进行认证处理与多租户隔离;
- Mesh 控制面直接对接注册中心获取服务实例、Spring Cloud 应用通过 SDK 获取服务实例;
- 双模异构,支持容器与虚机两种模型。
注册中心与高可用方案
前面提到过,要想实现实现混合微服务架构,注册中心很关键。谈到注册中心,目前主流的开源注册中心包括:
- Zookeeper:Yahoo 公司开发的分布式协调系统,可用于注册中心,目前仍有很多公司使用其作为注册中心;
- Eureka:Netflix 开源组件,可用于服务注册发现组件,被广大 Spring Cloud 开发者熟知,遗憾的是目前已经不再维护,也不再被 Spring Cloud 生态推荐使用;
- Consul:HashiCorp 公司推出的产品,其可作为实现注册中心,也是本文介绍的重点;
- Etcd:Etcd 官方将其定义为可靠的分布式 KV 存储。
我们注册中心选择了 Consul,Consul 包含了以下几个重要的功能:
- 服务发现:可以注册服务,也可以通过 Http 或 DNS 的方式发现已经注册的服务;
- 丰富的健康检查机制;
- 服务网格能力,最新版本已经支持 Envoy 作为数据面;
- KV 存储:可以基于 Consul KV 存储实现一个分布式配置中心;
- 多数据中心:借助多数据中心,无需使用额外的抽象层,即可构建多地域的场景,支持多 DC 数据同步、异地容灾。
上图是 Consul 官网提供的架构图。Consul 架构中几个核心的概念如下:
- Agent: Agent 是运行在 Consul 集群的每个节点上的 Daemon 进程,通过 Consul Agent 命令将其启动,Agent 可以运行在 Client 或者 Server 模式下;
- Client:Client 是一种 Agent,其将会重定向所有的 RPC 请求到 Server,Client 是无状态的,其主要参与 LAN Gossip 协议池,其占用很少的资源,并且消耗很少的网络带宽;
- Server:Server 是一种 Agent,其包含了一系列的责任包括:参与 Raft 协议写半数(Raft Quorum)、维护集群状态、响应 RPC 响应、和其他 Datacenter 通过 WAN gossip 交换信息和重定向查询请求至 Leader 或者远端 Datacenter;
- Datacenter: Datacenter 其是私有的、低延迟、高带宽的网络环境,去除了在公共网络上的网络交互。
注册中心作为基础组件,其自身的可用性显得尤为重要,高可用的设计需要对其进行分布式部署,同时因在分布式环境下的复杂性,节点因各种原因都有可能发生故障,因此在分布式集群部署中,希望在部分节点故障时,集群依然能够正常对外服务。注册中心作为微服务基础设施,因此对其容灾和其健壮性有一定的要求,主要体现在:
- 注册中心作为微服务基础设施,因此要求出现某些故障(如节点挂掉、网络分区)后注册中心仍然能够正常运行;
- 当注册中心的发生故障时,不能影响服务间的正常调用。
Consul 使用 Raft 协议作为其分布式一致性协议,本身对故障节点有一定的容忍性,在单个 DataCenter 中 Consul 集群中节点的数量控制在 2*n + 1 个节点,其中 n 为可容忍的宕机个数。Quorum size: Raft 协议选举需要半数以上节点写入成功。
Q1: 节点的个数是否可以为偶数个?
A2:答案是可以的,但是不建议部署偶数个节点。一方面如上表中偶数节点 4 和奇数节点 3 可容忍的故障数是一样的,另一方面,偶数个节点在选主节点的时候可能会出现瓜分选票的情形(虽然 Consul 通过重置 election timeout 来重新选举),所以还是建议选取奇数个节点。
Q2: 是不是 Server 节点个数越多越好?
A2:答案是否定的,虽然上表中显示 Server 数量越多可容忍的故障数越多,熟悉 Raft 协议的读者肯定熟悉 Log Replication(如上文介绍,日志复制时过半写成功才返回写成功),随着 Server 的数量越来越多,性能就会越低,所以结合实际场景一般建议 Server 部署 3 个节点。
推荐采用三节点或五节点,最为有效,且能容错。
注册中心设计的一个重要前提是:注册中心不能因为自身的原因或故障影响服务之间的相互调用。因此在实践过程中,如果注册中心本身发生了宕机故障/不可用,绝对不能影响服务之间的调用。这要求对接注册中心的 SDK 针对这种特殊情况进行客户端容灾设计,『客户端缓存』就是一种行之有效的手段。当注册中心发生故障无法提供服务时,服务本身并不会更新本地客户端缓存,利用其已经缓存的服务列表信息,正常完成服务间调用。
我们在设计时采用同 Datacenter 集群内部部署 3 个 Server 节点,来保障高可用性,当集群中 1 个节点发生故障后,集群仍然能够正常运行,同时这 3 个节点部署在不同的机房,达到机房容灾的能力。
在云上环境,涉及多 region 环境,因此在架构设计设计时,我们首先将 Consul 的一个 Datacenter 对应云上一个 region,这样更符合 Consul 对于 Datecenter 的定义(DataCenter 数据中心是私有性、低延迟、高带宽的网络环境)。中间代理层实现了服务鉴权、多租户隔离等功能;还可以通过中间代理层,对接多注册中心。
云上环境存在多租户隔离的需求,即:A 租户的服务只能发现 A 租户服务的实例。针对此场景,需要在 『中间代理层』完成对多租户隔离功能的实现,其主要实践思路为使用 Consul Api Feature 具备 Filtering 功能:
- 利用 Filtering 功能实现租户隔离需求;
- 减少查询注册中心接口时网络负载。
通过治理策略保证服务高可用
什么是高可用?维基百科这么定义:系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。我们通常用 N 个 9 来定义系统的可用性,如果能达到 4 个 9,则说明系统具备自动恢复能力;如果能达到 5 个 9,则说明系统极其健壮,具有极高可用性,而能达到这个指标则是非常难的。
常见的系统不可用因素包括:程序和配置出 bug、机器故障、机房故障、容量不足、依赖服务出现响应超时等。高可用的抓手包括:研发质量、测试质量、变更管理、监控告警、故障预案、容量规划、放火盲测、值班巡检等。这里,将主要介绍通过借助治理策略采用高可用设计手段来保障高可用。
高可用是一个比较复杂的命题,所以设计高可用方案也涉及到了方方面面。这中间将会出现的细节是多种多样的,所以我们需要对这样一个微服务高可用方案进行一个顶层的设计。
比如服务冗余:
- 冗余策略:每个机器每个服务都可能出现问题,所以第一个考虑到的就是每个服务必须不止一份,而是多份。所谓多份一致的服务就是服务的冗余,这里说的服务泛指了机器的服务、容器的服务、还有微服务本身的服务。在机器服务层面需要考虑,各个机器间的冗余是否有在物理空间进行隔离冗余。
- 无状态化:我们可以随时对服务进行扩容或者缩容,想要对服务进行随时随地的扩缩容,就要求我们的服务是一个无状态化,所谓无状态化就是每个服务的服务内容和数据都是一致的。
比如柔性化/异步化:
- 所谓的柔性化,就是在我们业务允许的情况下,做不到给予用户百分百可用的,通过降级的手段给到用户尽可能多的服务,而不是非得每次都交出去要么 100 分或 0 分的答卷。柔性化更多是一种思维,需要对业务场景有深入的了解。
- 异步化:在每一次调用,时间越长存在超时的风险就越大,逻辑越复杂执行的步骤越多,存在失败的风险也就越大。如果在业务允许的情况下,用户调用只给用户必须要的结果,不是需要同步的结果可以放在另外的地方异步去操作,这就减少了超时的风险也把复杂业务进行拆分减低复杂度。
上面讲到的几种提高服务高可用的手段,大多需要从业务以及部署运维的角度实现。而接下来会重点介绍,可以通过 SDK/Sidecar 手段提供服务高可用的治理策略,这些策略往往对业务是非侵入或者弱侵入的,能够让绝大多数服务轻松实现服务高可用。
微服务之间一旦建立起路由,就意味着会有数据在服务之间流通。由于不同服务可以提供的资源和对数据流量的承载能力不尽相同,为了防止单个 Consumer 占用 Provider 过多的资源,或者突发的大流量冲击导致 Provider 故障,需要服务限流来保证服务的高可用。
在服务治理中,虽然我们可以通过限流规则尽量避免服务承受过高的流量,但是在实际生产中服务故障依然难以完全避免。当整个系统中某些服务产生故障时,如果不及时采取措施,这种故障就有可能因为服务之间的互相访问而被传播开来,最终导致故障规模的扩大,甚至导致整个系统奔溃,这种现象我们称之为“雪崩”。熔断降级其实不只是服务治理中,在金融行业也有很广泛的应用。比如当股指的波动幅度超过规定的熔断点时,交易所为了控制风险采取的暂停交易措施。
负载均衡是高可用架构的一个关键组件,主要用来提高性能和可用性,通过负载均衡将流量分发到多个服务器,同时多服务器能够消除这部分的单点故障。
以上治理规则在某种程度上可以在 Spring Cloud 与 Service Mesh 两个框架上进行对齐,即同一套治理配置,可以通过转换分发到 Spring Cloud 应用的 SDK 上以及 Service Mesh 的 Sidecar 上。可以由 Config-server 负责规则下发,也可以由 Service Mesh 的控制面负责下发,取决于具体的架构方案。
服务限流
对于一个应用系统来说一定会有极限并发/请求数,即总有一个 TPS/QPS 阀值,如果超了阀值则系统就会不响应用户请求或响应的非常慢,因此我们最好进行过载保护,防止大量请求涌入击垮系统。限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务或进行流量整形。
常用的微服务限流架构包括:
- 接入层(api-gateway)限流:
- 单实例;
- 多实例:分布式限流算法;
- 调用外部限流服务限流:
- 微服务收到请求后,通过限流服务暴露的 RPC 接口查询是否超过阈值;
- 需单独部署限流服务;
- 切面层限流(SDK):
- 限流功能集成在微服务系统切面层,与业务解耦;
- 可结合远程配置中心使用;
常用的限流策略包括:
- 拒绝策略:
- 超过阈值直接返回错误;
- 调用方可做熔断降级处理。
- 延迟处理:
- 前端设置一个流量缓冲池,将所有的请求全部缓冲进这个池子,不立即处理。然后后端真正的业务处理程序从这个池子中取出请求依次处理,常见的可以用队列模式来实现(MQ:削峰填谷);
- 用异步的方式去减少了后端的处理压力。
- 特权处理:
- 这个模式需要将用户进行分类,通过预设的分类,让系统优先处理需要高保障的用户群体,其它用户群的请求就会延迟处理或者直接不处理。
常用的限流算法包括:
-
固定时间窗口限流:
- 首先需要选定一个时间起点,之后每次接口请求到来都累加计数器,如果在当前时间窗口内,根据限流规则(比如每秒钟最大允许 100 次接口请求),累加访问次数超过限流值,则限流熔断拒绝接口请求。当进入下一个时间窗口之后,计数器清零重新计数;
- 缺点在于:限流策略过于粗略,无法应对两个时间窗口临界时间内的突发流量。
-
滑动时间窗口算法:
- 流量经过滑动时间窗口算法整形之后,可以保证任意时间窗口内,都不会超过最大允许的限流值,从流量曲线上来看会更加平滑,可以部分解决上面提到的临界突发流量问题,是对固定时间窗口算法的一种改进;
- 缺点在于:需要记录在时间窗口内每个接口请求到达的时间点,对内存的占用会比较多。
-
令牌桶算法:
- 接口限制 t 秒内最大访问次数为 n,则每隔 t/n 秒会放一个 token 到桶中;
- 桶中最多可以存放 b 个 token,如果 token 到达时令牌桶已经满了,那么这个 token 会被丢弃;
- 接口请求会先从令牌桶中取 token,拿到 token 则处理接口请求,拿不到 token 就阻塞或者拒绝服务。
-
漏桶算法:
- 对于取令牌的频率也有限制,要按照 t/n 固定的速度来取令牌;
- 实现往往依赖于队列,请求到达如果队列未满则直接放入队列,然后有一个处理器按照固定频率从队列头取出请求进行处理。如果请求量大,则会导致队列满,那么新来的请求就会被抛弃;
- 令牌桶和漏桶算法的算法思想大体类似,漏桶算法作为令牌桶限流算法的改进版本。
令牌桶算法和漏桶算法,在某些场景下(内存消耗、应对突发流量),这两种算法会优于时间窗口算法成为首选。
熔断
断路器模式是微服务架构中广泛采用的模式之一,旨在将故障的影响降到最低,防止级联故障和雪崩,并确保端到端性能。我们将比较使用两种不同方法实现它的优缺点:Hystrix 和 Istio。
在电路领域中,断路器是为保护电路而设计的一种自动操作的电气开关。它的基本功能是在检测到故障后中断电流,然后可以重置 (手动或自动),以在故障解决后恢复正常操作。这看起来与我们的问题非常相似:为了保护应用程序不受过多请求的影响,最好在后端检测到重复出现的错误时立即中断前端和后端之间的通信。Michael Nygard 在他的《Release It》一书中使用了这个类比,并为应用于上述超时问题的设计模式提供了一个典型案例,可以用上图来总结。
Istio 通过 DestinationRule 实现断路器模式,或者更具体的路径 TrafficPolicy (原断路器) -> OutlierDetection,根据上图模型:
- consecutiveErrors 断路器打开前的出错次数;
- interval 断路器检查分析的时间间隔;
- baseEjectionTime 最小的开放时间,该电路将保持一段时间等于最小弹射持续时间和电路已打开的次数的乘积;
- maxEjectionPercent 可以弹出的上游服务的负载平衡池中主机的最大百分比,如果驱逐的主机数量超过阈值,则主机不会被驱逐。
与上述公称断路器相比,有两个主要偏差:
- 没有半开放的状态。然而,断路器持续打开的时间取决于被调用服务之前失败的次数,持续的故障服务将导致断路器的开路时间越来越长。
- 在基本模式中,只有一个被调用的应用程序 (后端)。在更实际的生产环境中,负载均衡器后面可能部署同一个应用程序的多个实例。某些情况下有些实例可能会失败,而有些实例可能会工作。因为 Istio 也有负载均衡器的功能,能够追踪失败的实例,并把它们从负载均衡池中移除,在一定程度上:‘maxEjectionPercent’属性的作用是保持一小部分的实例池。
Hystrix 提供了一个断路器实现,允许在电路打开时执行 fallback 机制。最关键的地方就在 HystrixCommand 的方法 run() 和 getFallback():
- run() 是要实际执行的代码 e.g. 从报价服务中获取价格;
- getFallback() 获取当断路器打开时的 fallback 结果 e.g. 返回缓存的价格。
Spring Cloud 是建立在 Spring Boot 之上的框架,它提供了与 Spring 的良好集成。它让开发者在处理 Hystrix 命令对象的实例化时,只需注释所需的 fallback 方法。
实现断路器的方法有两种,一种是黑盒方式,另一种是白盒方式。Istio 作为一种代理管理工具,使用了黑盒方式,它实现起来很简单,不依赖于底层技术栈,而且可以在事后配置。另一方面,Hystrix 库使用白盒方式,它允许所有不同类型的 fallback:
- 单个默认值;
- 一个缓存;
- 调用其他服务。
它还提供了级联回退(cascading fallbacks)。这些额外的特性是有代价的:它需要在开发阶段就做出 fallback 的决策。
这两种方法之间的最佳匹配可能会依靠自己的上下文:在某些情况下,如引用的服务,一个白盒战略后备可能是一个更好的选择,而对于其他情况下快速失败可能是完全可以接受的,如一个集中的远程登录服务。
常用的熔断方法包括自动熔断与手动熔断。发生熔断时也可以选择 fail-fast 或者 fallback。这些用户都可以基于需求灵活使用。
智能路由
最后,我们来看一下智能路由带来的高可用。智能路由这里包括(客户端)负载均衡与实例容错策略。对于 Spring Cloud 框架来说,这部分能力由 Ribbon 来提供,Ribbon 支持随机、轮询、响应时间权重等负载均衡算法。而对于 Service Mesh 框架,这部分能力由 Envoy 提供,Envoy 支持随机、轮询(加权)、环哈希等算法。为了实现两套系统的规则统一对齐,可以采用其交集。
而容错策略包括:
- failover:失败后自动切换其他服务器,支持配置重试次数;
- failfast:失败立即报错,不再重试;
- failresnd:将失败请求放入缓存队列、异步处理,搭配 failover 使用。
Istio 支持重试策略配置,而 fail-fast 即对应与重试次数为 0。
总结
微服务的高可用是一个复杂的问题,往往需要从多个角度去看,包括:
- 从手段看高可用。主要使用的技术手段是服务和数据的冗余备份和失效转移,一组服务或一组数据都能在多节点上,之间相互备份。当一台机器宕机或出现问题的时候,可以从当前的服务切换到其他可用的服务,不影响系统的可用性,也不会导致数据丢失。
- 从架构看高可用。保持简单的架构,目前多数网站采用的是比较经典的分层架构,应用层、服务层、数据层。应用层是处理一些业务逻辑,服务层提供一些数据和业务紧密相关服务,数据层负责对数据进行读写。简单的架构可以使应用层,服务层可以保持无状态化进行水平扩展,这个属于计算高可用。同时在做架构设计的时候,也应该考虑 CAP 理论。
- 从硬件看高可用。首先得确认硬件总是可能坏的,网络总是不稳定的。解决它的方法也是一个服务器不够就来多几个,一个机柜不够就来几个,一个机房不够就来几个。
- 从软件看高可用。软件的开发不严谨,发布不规范也是导致各种不可用出现,通过控制软件开发过程质量监控,通过测试,预发布,灰度发布等手段也是减少不可用的措施。
- 从治理看高可用。将服务规范化,事前做好服务分割,做好服务监控,预判不可用的出现,在不可用出现之前发现问题,解决问题。比如在服务上线后,根据经验,配置服务限流规则以及自动熔断规则。