<audio id="audio" title="35 | 流量调度与负载均衡" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/33/24/33883dbeaf77a968ffe91a3399b50924.mp3"></audio>

你好，我是七牛云许式伟。

相比桌面程序而言，服务端程序依赖的基础软件不只是操作系统和编程语言，还多了两类：

- 负载均衡（Load Balance）；
- 数据库或其他形式的存储（DB/Storage）。

为什么会需要负载均衡（Load Balance）？今天我们就聊一下有关于流量调度与负载均衡的那些事情。

上一讲我们画了服务端程序的体系架构图，如下：

<img src="https://static001.geekbang.org/resource/image/89/82/895dbf7e39fb562215e0176ca4aad382.png" alt="">

什么是 “流量调度”？我们首先要了解这样几个常见的服务端程序运行实例（进程）相关的概念：

- 连接数；
- IOPS；
- 流量，入向流量和出向流量。

我们知道，一个基本的服务端程序的服务请求，通常是由一个请求包（Request）和一个应答包（Response）构成。这样一问一答就是一次完整的服务。

连接数，有时候也会被称为并发数，指的是同时在服务中的请求数。也就是那些已经发送请求（Request），但是还没有收完应答（Response）的请求数量。

IOPS，指的是平均每秒完成的请求（一问一答）的数量。它可以用来判断服务端程序的做事效率。

流量分入向流量和出向流量。入向流量可以这么估算：

- 平均每秒收到的请求包（Request）数量 `*` 请求包平均大小。

同样的，出向流量可以这么估算：

- 平均每秒返回的应答包（Response）数量 `*` 应答包平均大小。

不考虑存在无效的请求包，也就是存在有问无答的情况（但实际生产环境下肯定是有的）的话，那么平均每秒收到的请求包（Request）数量、平均每秒返回的应答包（Response）数量就是 IOPS。故此：

- 入向流量 ≈ IOPS `*` 请求包平均大小
- 出向流量 ≈ IOPS `*` 应答包平均大小

所谓流量调度，就是把海量客户并发的请求包按特定策略分派到不同的服务端程序实例的过程。

有很多手段可以做流量调度。

## DNS 流量调度

最基础的方式，是通过 DNS，如下图所示。

<img src="https://static001.geekbang.org/resource/image/79/cd/793c5e6b7a884e6816a60ebe2ee803cd.png" alt="">

一个域名通过 DNS 解析到多个 IP，每个 IP 对应不同的服务端程序实例。这样就完成了流量调度。这里我们没有用到常规意义的负载均衡（Load Balance）软件，但是我们的确完成了流量调度。

那么这种做法有什么不足？

**第一个问题，是升级不便。**

要想升级 IP1 对应的服务端程序实例，必须先把 IP1 从 DNS 解析中去除，等 IP1 这个实例没有流量了，然后我们升级该实例，最后把 IP1 加回 DNS 解析中。

看起来还好，但是我们不要忘记，DNS 解析是有层层缓冲的。我们把 IP1 从 DNS 解析中去除，就算我们写明 TTL 是 15 分钟，但是过了一天可能都还稀稀拉拉有一些用户请求被发送到 IP1 这个实例。

所以通过调整 DNS 解析来实现升级，有极大的不确定性，完成一个实例的升级周期特别长。

假如一个实例升级需要 1 天，我们总共有 10 个实例，那么就需要 10 天。这太夸张了。

**第二个问题，是流量调度不均衡。**

DNS 服务器是有能力做一定的流量均衡的。比如第一次域名解析返回 IP1 优先，第二次域名解析让 IP2 优先，以此类推，它可以根据域名解析来均衡地返回 IP 列表。

但是域名解析均衡，并不代表真正的流量均衡。

一方面，不是每次用户请求都会对应一次 DNS 解析，客户端自己有缓存。另一方面，DNS 解析本身也有层层缓存，到 DNS 服务器的比例已经很少了。

所以在这样情况下，按域名解析做流量调度均衡，是非常粗糙的，实际结果并不可控。

那么，怎么让流量调度能够做到真正均衡？

## 网络层负载均衡

第一种做法，是在网络层（IP 层）做负载均衡。

章文嵩博士发起的负载均衡软件 LVS（Linux Virtual Server）就工作在这一层。我们以 LVS 为代表介绍一下工作原理。

LVS 支持三种调度模式。

- VS/NAT：通过网络地址转换（NAT）技术做调度。请求和响应都会经过调度器中转，性能最差。
- VS/TUN：把请求报文通过 IP 隧道转发至真实服务器，而真实服务器将响应直接返回给客户，所以调度器只处理请求报文。这种做法性能比 VS/NAT 好很多。
- VS/DR：通过改写请求报文的MAC地址，将请求发送到真实服务器，真实服务器将响应直接返回给客户。这种做法相比 VS/TUN 少了 IP 隧道的开销，性能最好。

我们重点介绍下 VS/DR 技术。

<img src="https://static001.geekbang.org/resource/image/02/32/02d193a74158940f18a8562b771de732.png" alt="">

如上图所示。设客户端的 IP 和 MAC 为 CIP、CMAC。

第 1 步，客户端发起请求，其 IP 报文中，源 IP 为用户的 CIP ，目标 IP 是 VIP；源 MAC 地址为 CMAC ，目标 MAC 地址为 DMAC。

第 2 步，请求包到达 LVS 调度器（Director Server）。我们保持源 IP 和目标 IP 不变，仅仅修改目标 MAC 地址为 RMAC，将请求转发到真实的业务服务器实例 RS（Real Server）。

第 3 步，RS 收到数据包并经过处理，直接响应发送给客户端。

这里面的关键技巧，是 VIP 绑定在多台机器上，所以我们把它叫做虚拟 IP（Virtual IP）。它既绑定在 LVS 调度器（Director Server）上，也绑定在所有的业务服务器实例 RS（Real Server）上。

当然这里有一个很重要的细节是，ARP 广播查询 VIP 对应的 MAC 地址得到什么？答案当然是 LVS 调度器（Director Server）。在真实的业务服务器实例 RS（Real Server）上，我们把 VIP 绑定在 lo 接口上，并对 ARP 请求作了抑制，这样就避免了 IP 冲突。

LVS 这种在网络层底层来做负载均衡，相比其他负载均衡技术来说，其特点是通用性强、性能优势高。

但它也有一些缺点。假如某个业务服务器实例 RS 挂掉，但 LVS 调度器（Director Server）还没有感知到，在这个短周期内转发到该实例的请求都会失败。这样的失败只能依赖客户端重试来解决。

## 应用层负载均衡

有办法避免出现这种请求失败的情况吗？

可以。答案是：服务端重试。

怎么做服务端重试？应用层负载均衡。有时候我们也把它叫做应用网关。

HTTP 协议是应用最为广泛的应用层协议。当前应用网关，绝大多数都是 HTTP 应用网关。

Nginx 和 Apache 都是大家最为耳熟能详的 HTTP 应用网关。因为知道应用层协议的细节，所以 HTTP 应用网关的能力通常非常强大。这一点我们后面还会进一步进行探讨，今天我们先聊负载均衡（Load Balance）相关的内容。

HTTP 网关收到一个 HTTP 请求（Request）后，根据一定调度算法把请求转发给后端真实的业务服务器实例 RS（Real Server），收到 RS 的应答（Response）后，再把它转发给客户端。

整个过程的逻辑非常简单，而且重试也非常好做。

在发现某个 RS 实例挂了后，HTTP 网关可以将同一个 HTTP 请求（Request）重新发给其他 RS 实例。

当然一个重要的细节是为了能够支持重试，HTTP 请求（Request）需要被保存起来。不保存 HTTP 请求做重试是有可能的，但是只能支持业务实例完全挂掉 HTTP 请求一个字节都没发过去的场景。但在断电或异常崩溃等情况，显然会有很多进行中的请求是不符合这个前提的，它们就没法做重试。

大部分 HTTP 请求不大，直接在内存中存储即可，保存代价不高。但是文件上传型的请求，由于请求包中包含文件内容，可能就需要依赖临时文件或其他手段来保存 HTTP 请求。

## 优雅升级

有了负载均衡，不只是可以实现了流量的均衡调度，连带业务服务器的升级也会方便多了。

对于前端是 LVS 这种网络层负载均衡的场景，升级的核心步骤为：

- 升级系统通知 LVS 调度器（Director Server）下线要升级的业务服务器（Real Server）实例。
- LVS 调度器（Director Server）将该实例从 RS 集合中去除，这样就不再调度新流量到它。
- 升级系统通知要升级的 RS 实例退出。
- 要升级的 RS 实例处理完所有处理中的请求，然后主动退出。
- 升级系统更新 RS 实例到新版本，并重启。
- 升级系统将 RS 实例重新加回 RS 集合参与调度。

对于前端是 HTTP 应用网关这种负载均衡的场景，升级的过程可以更加简单：

- 升级系统通知升级的业务服务器（Real Server）实例退出。
- 要升级的 RS 实例进入退出状态，这时新请求进来直接拒绝（返回一个特殊的 Status Code）；处理完所有处理中的请求后，RS 实例主动退出。
- 升级系统更新 RS 实例到新版本，并重启。

可以看出，因 HTTP 应用网关支持重试，业务服务器的升级过程就变得简单很多。

## 结语

今天我们从流量调度谈起，聊了几种典型的调度手段和负载均衡的方式。

从流量调度角度来说，负载均衡的最大价值是让多个业务服务器的压力均衡。这里面隐含的一个前提是负载均衡软件的抗压能力往往比业务服务器强很多（为什么？欢迎留言讨论）。

这表现在：其一，负载均衡的实例数/业务服务器的实例数往往大大小于1；其二，DNS 的调度不均衡，所以负载均衡的不同实例的压力不均衡，有的实例可能压力很大。

当然，负载均衡的价值并不只是做流量的均衡调度，它也让我们的业务服务器优雅升级成为可能。

如果你对今天的内容有什么思考与解读，欢迎给我留言，我们一起讨论。下一讲我们将聊聊存储中间件。

如果你觉得有所收获，也欢迎把文章分享给你的朋友。感谢你的收听，我们下期再见。
