---
title: 真实业务经历：CMS 网站
taxonomy:
    category: docs
---

六年前，笔者曾经负责维护过一个面向 SEO 的 CMS（内容管理系统）网站，该网站每日 PV 达到两百万。这类网站往往有很多的“相关内容”需求，需要进行类似于搜索引擎的“相关查询”，导致页面相应十分缓慢。由于内容的独特性，经常受到爬虫和采集机器人的关注，导致频繁被爬取，给运维工作带来了很大压力。

随着网站内容的逐渐丰富，除了被正规蜘蛛访问外，某一天还突然遭遇了每秒 100 次 HTTP 请求的采集机器人袭击，当时的配置是可怜的 1 核 2G 云主机和 1 核 1G 的 MySQL 数据库，网站瞬间就宕机了。后来，在网站恢复以后，笔者测试了一下，由于页面的复杂度非常高，当 QPS 达到 7 以后，数据库就会满载，随后云主机也会满载，网站就挂了。

那么，笔者是如何解决这个问题的呢？

### 动静分离

首先，笔者实施了动静分离的策略。将静态资源全部使用 CDN 进行分发，由于 CDN 节点具备强缓存的特点，只需文件名不变，即可维持长时间的缓存效果。这一举措显著降低了静态资源对服务器的负载，使系统的总 CPU 消耗下降了 80%。

### 利用 Elasticsearch 提升网页响应速度

当时，这个网站使用关键词`like`的方法来实现“类似文章推荐”功能。具体来说，我们预设了一个关键词库，然后根据页面上的文章标题和内容进行匹配，匹配到的第一个关键词就会被用于各个表中的 SQL 查询语句，以获取相关内容。然而，这种方法在数据量庞大的情况下会出现显著的性能问题。

为了解决这个问题，笔者采购了一台 8 核 32GB 的云主机，并在其上安装了一个单节点的 Elasticsearch。通过将搜索任务交由 Elasticsearch 处理，不仅成功降低了单页面的响应时间，从 300-500ms 降至约 200ms 左右，同时推荐内容的精准性也得到了显著提升。

### MySQL 索引优化

在绝大多数情况下，为关系型数据库添加索引都是首选的性能优化策略。这种方法通常可以带来数十倍到数万倍的性能提升，尽管需要增加明显的磁盘消耗和略微的内存消耗作为代价。

例如，笔者为一张主要的大表增加了一些简单的单字段索引，结果页面的响应时间就从 200ms 降低到了大约 150ms，效果显著。

### 利用 Redis 缓存

在页面展示的过程中，有很多很难被优化的高耗时操作，是无法利用索引来做性能优化的，例如大表的`select count(*)`。于是，本着“解决不了问题就解决提问题的人”的思想，笔者将大表的总数统计工作利用后台定时任务来承载，强制压制了业务需求：每个页面无法实时地获取文章总数了，只能获取到一分钟之前的数据。

每分钟，笔者会统计一次那几个内容表的总数，存入 Redis，而页面如果需要使用，直接读 Redis 中的数字即可。经过这一步的优化，笔者把页面的响应时间从 150ms 降低到了 120ms。

### 网络请求并行化

由于每个页面都会获取多个其他表的“类似文章推荐”，所以每个页面对 Elasticsearch 的 HTTP 请求都有多个，而 PHP 默认是一个一个阻塞运行的：上一个不回来，PHP 进程就等待。笔者将这个阻塞操作并行化了：虽然 PHP 是一种阻塞语言，但是一次性发送多个 HTTP 请求的能力还是有的，等到所有请求都返回了数据之后，再继续阻塞地运行 PHP 代码。

这个系统平均每个页面请求了五次 Elasticsearch，每次 15ms。并行化之前，这五个请求总共需要消耗 75ms。并行化之后，总时间从 75ms 减少到了 25ms。

经过上述的优化措施，笔者将单个页面的平均响应时间压缩到了 70ms，这个数字和之前的 300~500ms 比是一个飞跃，在同样后端计算资源的情况下，系统容量提升了五倍左右，我们 1 核 2G 的小云主机也能顶住 100 QPS 的压力了。

### 为什么不做静态化？

这个时候可能有人会问了，既然是内容网站，为什么不静态化呢？因为数据量太大了，500 万个页面，一个页面 100KB，就是 476GB 的磁盘容量，这个量级太大了。

在这个规模下，和缓慢但可用的数据库相比，这么多静态资源的管理和刷新反而是个更大的问题，不如选择做数据库和架构优化，问题会更少。在百万量级下，数据库绝对是更好的数据存储解决方案，远比自己管理文件要更简单更稳定。如果我们从头观察数据库的发展史，就会发现数据库就是为了处理单靠读写一堆磁盘文件已经无法满足需求的场景而被发明出来的。

### 反爬措施

即便笔者做了那么多，还是不乏有一些爬虫愣头青在学习了 Swoole 和 Go 协程之后，对笔者的网站发动数千 QPS 的死亡冲锋，这个时候再怎么性能优化都是没用的，需要掏出倒数第二个工具：限流。

笔者做了三道限流关卡才最终顶住采集机器人的 DOS：

1. 针对单个 ip 做请求频率限制
2. 针对整个 “123.123.123.123/24” ip 段做请求频率限制（很多爬虫采用同一段内的多个 ip 绕过限流）
3. 针对每个 UA 做请求频率限制

在这三板斧使出来以后，天下太平了，网站再也没有被突然发起的死亡冲锋搞挂过。

对了，既然限流是倒数第二个工具，肯定有人好奇最后一个工具是什么？那就是熔断，熔断属于系统鲁棒性工具，是善后用的，在本书的最后一章，我们将对此进行[简要的探讨](/part-five/unlimited-capacity/section-4)。
