第 5 章:应用程序冗余:水平扩展和无状态

云原生应用的高可用性和弹性依赖于冗余部署、水平扩展和无状态设计。本章系统阐释了有状态服务的挑战、粘性会话的局限,以及如何通过无状态架构实现高效扩展和故障恢复。

本章要点

  • 水平扩展是云原生应用程序的核心思想
  • 云原生软件中有状态应用程序的陷阱
  • 对于应用程序来说,无状态意味着什么
  • 有状态服务,以及如何用在无状态应用程序中
  • 为什么不选择使用粘性会话

虽然标题强调“水平扩展”,但本章核心是冗余。无论应用组件大小、配置方式或回滚能力,容忍变化的关键是避免单点故障,因此应用总是部署多个实例。

下图说明了冗余实例的一致性要求:

图 5.1 给定相同的输入,每个应用程序实例产生的结果必须相同,不管应用程序有一个、两个还是一百个实例。
图 5.1 给定相同的输入,每个应用程序实例产生的结果必须相同,不管应用程序有一个、两个还是一百个实例。

即使每个实例运行环境不同,输入一致时结果必须一致。环境参数、配置和用户交互都会影响实例行为。

下图展示了外部影响与一致性挑战:

图 5.2 即使可能受到不同外部因素的影响,云原生必须保证一致的结果。
图 5.2 即使可能受到不同外部因素的影响,云原生必须保证一致的结果。

第 6 章讨论了系统环境和应用程序配置等影响因素。在本章中,我将介绍请求历史会造成的影响。

我首先会介绍部署多个应用程序实例的好处,你会立即看到对有状态应用程序的影响。我们会继续以上一章的食谱程序为例,将单体应用程序分解成多个可独立部署和管理的微服务。在其中一个微服务中,我会引入本地状态,主要是存储身份验证令牌。没错,粘性会话是解决这类会话状态的一种常见模式,但在云计算中这不是一个好主意,我会解释这其中的原因。我还会介绍“有状态服务”的概念,这是一种特殊的服务,通过精心的设计来处理状态的复杂性。最后,我会向你展示如何将软件中有状态的部分与无状态的部分分隔开来。

云原生应用程序会部署许多实例

在云计算环境中,增加或减少应用程序的容量来应对变化的流量,我们称为水平伸缩。这里指的不是增加或者减少单个应用程序实例的容量(垂直伸缩),而是通过添加或者删除应用程序实例,来增加或者减少能够处理的请求数量。

这并不是说不能为应用程序的实例分配更大的计算资源,像 Google Cloud Platform(GCP)现在已经可以提供 1.5TB 内存的机器,而 AWS 可以提供将近 2TB 内存的机器。但是,改变应用程序运行机器的规格会带来很多影响。例如,假设你已经估计 16GB 的内存对于应用程序来说是足够的,并且在一段时间内程序都可以正常运行。但是随后你的请求量开始增加,于是打算增加机器的容量。在 AWS、Azure 或者 GCP 等云环境中,你无法更改正在运行的机器的配置类型。相反,你必须创建一个新的、32GB 内存的机器(即使你只需要大约 20GB,但是没有提供 20GB 内存的机器类型),部署你的应用程序,然后将用户流量尽可能无缝地迁移到新的实例。

我们现在改为水平扩展的方式。相比为应用程序提供 16GB 的内存,我们选择提供 4 个实例,每个实例 4GB 内存。当你需要更大的容量时,你只需要增加第五个应用程序的实例,让它变得可用,例如,把它注册到一个动态路由上,那么现在你运行的总共内存是 20GB。这样你不仅可以更细粒度的控制资源消耗,而且也更容易实现更大的规模。

但是灵活的伸缩性并不是采用多个实例的唯一动机,高可用、可靠性和运维效率也同样是考虑因素。回到本书的第一个例子,在图 1.2 和 1.3 所描述的假想场景中,是应用程序的多个实例让 Netflix 在遇到 AWS 基础设施宕机时依然可用。显然,如果你将应用程序部署为单个实例,那么它就是单点故障。

在生产环境中运维软件时,多个实例也会带来好处。例如,越来越多的应用程序在一些平台上运行,这些平台提供了一组在原始计算、存储和网络资源上的服务。例如,现在已经没有应用程序团队(开发和运维)需要自己来提供操作系统。相反,他们只是简单地将代码发送到平台,由平台建立运行时环境并部署应用程序。如果平台(从应用程序的角度来看属于基础设施的一部分)需要升级操作系统,理想情况下,应用程序应该在整个升级过程中依然保持运行。当一个主机正在升级它的操作系统时,它上面运行的程序必须停止,但是如果你在其他主机上也运行着应用程序的实例(回想一下第 3 章关于应用程序实例分布的讨论),那么你可以每次升级一个主机。当一个应用程序实例离线时,其他实例依然可以保持在线状态。

最后,当你同时结合水平扩展和基于微服务的架构时,会在系统的整体资源消耗方面获得极大的灵活性。如上一章中所介绍的烹饪社区软件所示,多个独立的应用程序组件允许你水平扩展帖子服务的实例数量,处理大量的请求,而其他服务,例如关系服务依然只需要少量实例来处理更少的请求,如图 5.3 所示。

图 5.3 部署应用程序的多个实例,会显著增加对资源的使用效率,以及弹性能力和其他运维好处。
图 5.3 部署应用程序的多个实例,会显著增加对资源的使用效率,以及弹性能力和其他运维好处。

云环境中的有状态服务

灵活伸缩和弹性需求促使多实例部署,但有状态与无状态的设计直接影响系统表现。以下通过食谱程序示例,说明有状态服务的挑战。

解耦单体程序并绑定数据库

将单体应用拆分为独立微服务,每个服务独立运行并连接外部数据库(如 MySQL),实现无状态架构。配置通过配置文件指定服务 URL。

持久化数据采用外部数据库,提升弹性和可靠性。构建与运行流程如下:

  • 克隆示例仓库,切换到对应分支。
  • 配置数据库依赖(如 H2、MySQL)。
  • 使用 Docker 启动数据库容器。
  • 创建数据库并初始化数据。
  • 启动各微服务,分别指定端口和数据库连接信息。
  • 通过 curl 命令访问各服务接口,验证服务间调用。

尤其是在调用相关帖子服务时,可以观察到请求如何在多个微服务间流转。

此版本实现了如图 5.3 所示的部署拓扑结构,各应用可独立伸缩。所有服务均为无状态,随时可重启而不丢失数据。

错误处理会话状态

为实现用户鉴权,相关帖子服务新增登录控制器。用户登录后生成令牌,令牌存储于服务内存,并通过 cookie 传递。后续请求需携带令牌,服务验证后返回数据,否则返回 401 未授权。

  • 登录接口生成令牌并存储于内存映射。
  • 获取用户帖子时,从 cookie 读取令牌并校验。
  • 构建并部署服务后,先登录获取令牌,再用令牌访问受保护接口。

此实现将状态存储于应用内存,导致服务重启或多实例部署时令牌丢失,用户需重新登录。

在云环境中,部署多个实例后,负载均衡器会将请求分发到不同实例。由于令牌只在本地存储,部分请求会因令牌无效而失败,造成用户体验不一致。

HTTP 会话和粘性会话

粘性会话通过负载均衡器将同一用户请求固定路由到同一实例,依赖本地状态。若实例不可用,路由器会将请求转发到其他实例,导致本地状态丢失,用户需重新登录。

粘性会话在云原生环境下并不可靠,实例随时可能变更或消失,建议避免使用。

有状态服务和无状态应用程序

正确做法是将状态存储于外部有状态服务(如数据库、缓存),应用本身保持无状态。以 Redis 为例,登录令牌存储于外部缓存,所有实例均可访问,实现真正的无状态架构。

  • 登录令牌存储于 Redis 等外部服务。
  • 所有应用实例均可访问和校验令牌。
  • 扩展实例后,所有实例都能正确响应请求,系统弹性和一致性显著提升。

总结

  • 有状态应用无法在云原生环境中高效运行,易受实例变更影响。
  • 会话状态是应用引入状态的常见方式,需谨慎设计。
  • 有状态服务需专门处理分布式一致性和弹性问题,应用应保持无状态。
  • 无状态应用易于扩展、回收和迁移,显著提升云环境下的可用性和弹性。
  • 通过外部有状态服务(如数据库、缓存)实现状态管理,是云原生架构的关键模式。

文章导航

独立页面

这是书籍中的独立页面。

书籍首页

评论区