> For the complete documentation index, see [llms.txt](https://wtifs.gitbook.io/diva-notes/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wtifs.gitbook.io/diva-notes/system-design/fu-zai-jun-heng.md).

# 负载均衡

李大牛创业了，由于前期没啥流量，所以他只部署了一台 `tomcat server`，让客户端将请求直接打到这台 `server` 上

![lb1](https://4237436490-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxhDxW69eCVrvTGotZqcT%2Fuploads%2Fgit-blob-e8769ead1c0c8f49d172573fea17f2d13fcae280%2Flb1.jpg?alt=media)

这样部署一开始也没啥问题，因为业务量不大，单机足以扛住。但后来业务迅猛发展，单机性能遇到瓶颈，而且由于只部署了一台，一旦机器挂掉业务就凉了。

为了避免单机性能瓶颈与解决单点故障的隐患，李大牛决定多部署几台机器（假设为三台），这样可以让 `client` 随机打向其中的一台机器，这样就算其中一台机器挂了，另外的机器还存活，让 `client` 打向其它没有宕机的机器即可

![lb2](https://4237436490-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxhDxW69eCVrvTGotZqcT%2Fuploads%2Fgit-blob-57878706234291ee0be01c488eb1846b851ebeb7%2Flb2.jpg?alt=media)

但问题来了，`client` 到底该打向这三台机器的哪一台呢，如果让 `client` 来选择肯定不合适，因为如果让 `client` 来选择 `server`，那么它必须知道有哪几台 `server`，然后再用轮询等方式连接其中一台机器。

但如果某台 `server` 宕机了，`client` 是无法提前感知到的，很可能就连到了这台挂掉的 `server` 上。所以选择哪台机器来连接的工作最好放在 `server` 中。

具体怎么做呢，在架构设计中有个经典的共识：没有什么是加一层解决不了的，如果有那就再加一层，所以我们在 `server` 端再加一层，将其命名为 `LB`（`Load Balance`，负载均衡），由 `LB` 统一接收 `client` 的请求，然后再由它来决定具体与哪一个 `server` 通信，比如用 `nginx` 作为 `LB`

![lb3](https://4237436490-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxhDxW69eCVrvTGotZqcT%2Fuploads%2Fgit-blob-2aa825926e7634f4f6b2e0a85757941cda302d14%2Flb3.jpg?alt=media)

采用这样的架构设计总算支撑了业务的快速增长，但所有的流量都能打到 `server` 上，这显然是有问题的，不太安全。所以在流量打到 `server` 前再做一层鉴权操作呢，鉴权通过了我们才让它打到 `server` 上，这一层叫做网关（为了避免单点故障，网关也要以集群的形式存在）

![lb4](https://4237436490-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxhDxW69eCVrvTGotZqcT%2Fuploads%2Fgit-blob-35a73bc1b45f02f5ae8008e37679c091e950cb5b%2Flb4.jpg?alt=media)

这样的话所有的流量在打到 `server` 前都要经过网关这一层，鉴权通过后才把流量转发到 `server` 中，否则就向 `client` 返回报错信息，除了鉴权外，网关还起到风控（防止羊毛党），协议转换（比如将 `HTTP` 转换成 `Dubbo`），流量控制等功能，以最大程度地保证转发给 `server` 的流量是安全的，可控的。

这样的设计其实还是有问题，不管是动态请求，还是静态资源（如 js，css文件）请求都打到 `tomcat` 了，这样在流量大时会造成 `tomcat` 承受极大的压力。其实对于静态资源的处理 `tomcat` 不如 `nginx`，`tomcat` 每次都要从磁盘加载文件比较影响性能，而 `nginx` 有 `proxy cache` 等功能，可以将资源缓存在本地的内存+磁盘中，下次请求如果命中缓存就从缓存中中直接返回，极大提升对静态资源的处理能力

所以李大牛又作了如下优化：如果是动态请求，则经过 `gateway` 打到 `tomcat`，如果是静态资源，则打到静态资源服务器上

![lb5](https://4237436490-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxhDxW69eCVrvTGotZqcT%2Fuploads%2Fgit-blob-22e00236ea17b2f8496a1df2b5b079d51f09a43f%2Flb5.jpg?alt=media)

这就是我们所说的**动静分离**，将静态请求与动态请求分开，这样 `tomcat` 就可以专注于处理其擅长的动态请求，而静态资源由于利用到了 `nginx` 的 `proxy cache` 等功能，后端的处理能力又上了一个台阶。

另外需要注意的是并不是所有的动态请求都需要经过网关，像我们的运营中心后台由于是内部员工使用的，所以它的鉴权与网关的 `api` 鉴权并不相同，所以我们直接部署了两台运营中心的 `server` ，让 `nginx` 将运营中心的请求打到这两台 `server` 上，绕过了网关。

![lb6](https://4237436490-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxhDxW69eCVrvTGotZqcT%2Fuploads%2Fgit-blob-5e011d2499b64d05bc014a128d6ca87bbfbfe4ea%2Flb6.jpg?alt=media)

当然为了避免单点故障 `nginx` 也需要部署至少两台机器，于是我们的架构变成了下面这样 `nginx` 部署两台，以主备的形式存在，备 `nginx` 会通过 `keepalived` 机制（发送心跳包） 来及时感知到主 `nginx` 的存活，发现宕机自己就顶上充当主 `nginx` 的角色

![lb7](https://4237436490-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxhDxW69eCVrvTGotZqcT%2Fuploads%2Fgit-blob-180ec3c5e4a85fa1a209a3b1950a97e3969ec2d9%2Flb7.jpg?alt=media)

看起来这样的架构确实不错，但要注意的是 `nginx` 是七层（即应用层）负载均衡器 ，这意味着如果它要转发流量首先得和 `client` 建立一个 `TCP` 连接，并且转发的时候也要与转发到的上游 `server` 建立一个 `TCP` 连接，而 `TCP` 连接其实是需要耗费内存（`TCP Socket`，接收/发送缓存区等需要占用内存）的，客户端和上游服务器要发送数据都需要先发送暂存到到 `nginx` 再经由另一端的 `TCP` 连接传给对方。

所以 `nginx` 的负载能力受限于机器 I/O，CPU 内存等一系列配置，一旦连接很多（比如百万）的话，`nginx` 抗负载能力就会急遽下降。

`nginx` 的负载能力较差主要是因为它是七层负载均衡器必须要在上下游分别建立两个 `TCP` 所致，那么是否能设计一个类似路由器那样的只负载转发包但不需要建立连接的负载均衡器呢，这样由于不需要建立连接，只负责转发包，不需要维护额外的 `TCP` 连接，它的负载能力必然大大提升，于是四层负载均衡器 `LVS` 就诞生了

![lb9](https://4237436490-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxhDxW69eCVrvTGotZqcT%2Fuploads%2Fgit-blob-e0a1f110c6a113adec2b54dd53dc2c1869cc3460%2Flb9.jpg?alt=media)

可以看到 `LVS` 只是单纯地转发包，不需要和上下游建立连接即可转发包，相比于 `nginx` 它的抗负载能力强、性能高（能达到 F5 硬件的 60%），对内存和 CPU 资源消耗比较低

负载均衡设备在接收到第一个来自客户端的 `SYN` 请求时，即通过负载均衡算法选择一个最佳的服务器，并对报文中 目标IP地址 进行修改（改为后端服务器 IP），直接转发给该服务器。`TCP` 的三次握手是客户端和服务器直接建立的，负载均衡设备只是起到一个类似路由器的转发动作。在某些部署情况下，为保证服务器回包可以正确返回给负载均衡设备，在转发报文的同时可能还会对报文原来的源地址进行修改。

综上所述，我们在 `nginx` 上再加了一层 `LVS`，以让它来承接我们的所有流量。为了保证 `LVS` 的可用性，我们也采用主备的方式部署 `LVS`，另外采用这种架构如果 `nginx` 容量不够我们可以很方便地进行水平扩容。

当然只有一台 `LVS` 的话在流量很大的情况下也是扛不住的，需要多加几台，使用 `DNS` 负载均衡，在 `DNS` 服务器解析域名的时候随机打到其中一台 `LVS`

`nginx` 在 1.9 之后也开始支持四层负载均衡了，但 `LVS` 是 `Linux` 的内核模块，工作在内核态，而 `nginx` 工作在用户态，也相对比较重，所以在性能和稳定性上 `nginx` 是不如 `LVS` 的，这就是为什么我们要采用 `LVS + nginx` 的部署方式。

![lb11](https://4237436490-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxhDxW69eCVrvTGotZqcT%2Fuploads%2Fgit-blob-37b4ed7cdc44e613239854d78e6f8cf6c88eb881%2Flb11.jpg?alt=media)

另外相信大家也注意到了，如果流量很大时，静态资源应该部署在 `CDN` 上， `CDN` 会自动选择离用户最近的节点返回给用户，所以我们最终的架构改进如下：

![lb13](https://4237436490-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxhDxW69eCVrvTGotZqcT%2Fuploads%2Fgit-blob-b244751fbfe6d17a232bbe53580ad98c09bda61f%2Flb13.jpg?alt=media)

## 总结

架构一定要结合业务的实际情况来设计，脱离业务谈架构其实是耍流氓，可以看到上文每一个架构的衍化都与我们的业务发展息息相关。

对于中小型流量没有那么大的公司，其实用 `nginx` 作为负载均衡足够，在流量迅猛增长后则考虑使用 `LVS + nginx`。而像美团这样的巨量流量（数十 `Gbps` 的流量、上千万的并发连接），`LVS` 也不管用了（依然会出现不少丢包的现象），所以它们开发出了自己的一套四层负载均衡器 `MGW`

另外看了本文相信大家对分层的概念应该有更透彻的理解，没有什么是分层解决不了的事，如果有，那就再多加一层，分层使每个模块各司其职，功能解藕，而且方便扩展，大家很熟悉的 `TCP/IP` 就是个很好的例子，每层只管负责自己的事，至于下层是什么实现的上层是不 care 的

**参考**

[坤哥 - 你管这破玩意儿叫负载均衡?](https://mp.weixin.qq.com/s/apuayoQCiKVog9J7y2Igdg)
