最近一位朋友的网站遭到了 CC 攻击,网站是基于 WordPress 的二次开发,性能可以说是差到了一塌糊涂,即使躲在 CDN 防护的后面,漏网之鱼也依旧打得 4C8G 的服务器严重超载,网站完全没有响应。

受朋友委托,帮忙「看一下」,简单梳理了一下当时的状况,大致如下:

  1. 遭到 CC 攻击已经有一段时间了,服务器已经放在了某云的『企业级防护』CDN 之后
  2. 攻击都发生在工作时间,其余时间和节假日都『休息』,目的非常明确
  3. 攻击的强度不大,高的时候大概也就 600 cpm 左右的样子
  4. 网站使用 WordPress,找人基于某个主题做的二次开发,插件用了一大堆,PHP 7.0
  5. 被攻击的 url 为 /wp-admin/admin-ajax.php, Wordpress 后台接收异步请求的统一网关
  6. 服务器 4C8G,自建 MySQL、Memcached,但 Memcached 并没有用起来
  7. 服务器使用『宝塔』部署,几乎所有配置都是通过『宝塔』控制台完成的
  8. 刚接手时,服务器磁盘已经写满,MySQL 的 CPU 占用率异常高

接下来,我会根据这个网站的情况,一层一层的梳理这次应对的措施,希望能帮到有类似需求的朋友。

1. 使用 CDN 或高防服务器

遭到攻击第一时间应想办法尽快恢复服务,服务不可用的时间越长对方的收益就越大,因此云服务商提供的各类防护工具永远是第一顺位的选择。

鉴于高防服务的价格通常比较昂贵,所以在应对小规模的攻击时,可以优先考虑带安全防护功能的 CDN 服务,通常来说 CC 攻击目的是消耗服务器 CPU 或 内存资源,本身的流量本不会太大,所以使用 CDN 防护就已经能收到不错的效果了。

无论是高防服务器还是 CDN,本质上都是维护了一套过滤机制对入网流量进行了清洗,滤掉明显的攻击流量,放过正常或无法判定的流量,因此即使在使用了上述服务之后,依旧会有少量的攻击到达服务器。比如朋友这台,每分钟大约会有 60 个左右的攻击到达服务器,已经是很小的量了。

2. NGINX WAF

对于已经到达服务器网关的攻击,我们还可以使用 WAF(Web Application Firewall)来进行一次防御。鉴于 WordPress 的网站几乎都会使用 Nginx 作为网关,所以在 Nginx 里面实现 WAF 是最简单可行的方案。

为 Nginx 实现请求过滤的开源项目有很多,绝大部分都是基于 OpenResty 的 lua-nginx-module。『宝塔』提供的 Nginx 已经内置了 ngx_lua_waf ,这是一个国人开发的轻量过滤器,支持多种过滤模式,还挺实用。

使用 WAF 的好处是可以在请求达到 WordPress 前就过滤掉攻击,这比使用 WordPress 插件来做防护高效不止一两个数量级。通过自定义过滤规则,可以最大限度过滤攻击流量并避免误杀。即便遇到攻击特征不明显的情况,仍可以通过屏蔽被攻击的接口来保障主要服务可用,再配上自动开启/关闭过滤器的脚本,可以轻松将攻击带来的影响最小化。

PS: 在应对小规模 CC 攻击时,其实并不一定需要 CDN 防护,Nginx 的 WAF 是可以直接胜任的。

3. PHP-FPM 与 PHP

我向来都不吝惜对大多数 php-fpm 使用者的吐槽,因为 php-fpm 具有「零配置开箱即用」的特性,所以很多人都忽视了它的重要性,即便是很多职业 phper 也是如此。

3.1. 最大并发数

很多人喜欢把 max_children 设置成一个足够大的值,低负载的时候确实不会有什么问题,但当负载足够高时,限制性能的主要瓶颈便不再是 IO,而是 CPU 的并行能力(CPU 核数),此时过多的进程反而会因为频繁的上下文切换造成资源浪费,让服务器雪上加霜。CC 攻击的主要目的就是耗尽服务器资源,所以在应对 CC 攻击时应特别注意 max_children 的设置。

在之前的公司,我学到了一个观点:并发数设置是算出来的,而不是凭感觉瞎猜的

一个经验公式是:

理论上能承受的最大并发 = max_children / (空载时)平均响应时长 * 性能衰减系数

性能衰减系数 是指 平均响应时长 随着并发量增加而增长的系数,可以从监控平台取得 并发量平均响应时长 的关系集做简单的回归处理获得,也可以通过压测获得,无需很严谨时也可以用 1 代替,即不考虑衰减。

3.2. 最大响应时长

很多 WordPress 的部署教程会教我们把 max_execution_time 设置成一个很大的值,以满足潜在的需要长时间来响应的请求,比如文件上传。而当请求因为数据库锁、session 锁、外部请求超时以及高负载导致性能下降等情况卡住时,过大的 max_execution_time 就成了一味毒药,夯住的请求会陷入长时间等待甚至假死,消耗大量资源做无用功。

max_execution_time 的默认值是 30s,我认为这个值也太长了,让用户为一个请求等待 30s 该是多么糟糕的用户体验。php-fpm 在 ondemand 模式下还可以通过 pm.process_idle_timeout 来限制执行时间,默认值是 10s,个人觉得这个值就合理多了。

对于大文件上传一类真的需要很长执行时间的请求,应该使用 set_time_limit() 临时延长请求执行时间。

3.3. OPcache

这个一定要开启,这个一定要开启,这个一定一定要开启!

CC 攻击的特性决定了它会针对某一个或某几个很慢的接口进行集中攻击,OPcache 在这种情况下正好能发挥出最大的功效。

3.4. PHP Session

早在 12 年我还在使用 CodeIgniter 的时候,社区就已经有大量针对 PHP Session 的讨论了,它是一把双刃剑,既能偷懒也能自残,坏就坏在他没有给使用者『欲练此功,必先自宫』的警告,因此很多菜鸟用着用着就自宫了。如果你有留意过,你会发现主流的框架、系统鲜有使用 PHP Session 的,使用加密的 cookie 来代替 session 是一种普遍的做法。

但 PHP Session 的设计本身并没有错,犯错的是不合格的使用者。

PHP Session Locking

PHP Session 默认使用 file 存储,这就注定了它的 IO 效率低下,不能支持分布式,且必须用锁来保障数据一致性。在 PHP 中需要使用 session_start() 来开启 session 操作的会话,此时 PHP 会给 session 加上独占锁,所以使用完后还需要调用 session_write_close() 来释放锁。需要持锁执行的处理,可以称作为一个事务,事务里面的代码应该逻辑越简单、执行时间越短越好。

如果使用者对这一点认识不够,就会导致一个进程长期持锁,当另一个进程开始处理这个用户的第二请求时,第二个进程就会因为获取不到锁而进入等待,如果该用户的请求接二连三的达到,那么这一系列请求就全夯住了。

很不幸,不少使用了 PHP Session 的 WordPress 插件就是存在这个问题,比如一个叫 Open Social 的插件,在插件一开始就使用了 session_start(),接着进行了各种外部请求,并且根本没有调用 session_write_close() 结束会话,session 锁要等到整个请求执行完才会被释放。更奇葩的是这段逻辑在任意一个 /wp-admin/admin-ajax.php 的请求中都会触发,想像一下 ajax 轮询的情况,堪称自杀神器。

朋友的服务器被打爆,绝对有一半是它的功劳,而这插件还是朋友花了 ¥300 元买的收费版,不得不说 WordPress 的钱真好赚 ~~

正确使用 PHP Session

对于 PHP 5.x 以上的版本,使用 session_start() 后一定记得要 session_write_close(),事务内部不要有可能导致阻塞的代码,比如外部请求、读写文件等等。

$homepage = file_get_contents('http://www.example.com/');

session_start();

$_SESSION['something'] = 'foo';
$_SESSION['length'] = strlen($homepage);

session_write_close();

echo $_SESSION['something']; 
// => 'foo' 
// $_SESSION 仍是可以读取的
// 但是不能再写入了,写入需要重新调用 session_start()

在 PHP 7.x 里,如果只需要读取 $_SESSION, 还可以使用新加的特性

session_start([
    'read_and_close' => true,
]);

详细介绍见:http://php.net/manual/en/migration70.new-features.php#migration70.new-features.session-options

关闭 PHP Session Locking

改代码是一件费时费力的事情,在面对攻击的时候,临阵磨枪可能是来不及的,因此想办法直接关掉 PHP Session Locking 才是更可行的方案。

  1. 使用 Memcached 作为 Session 存储,并将 memcached.sess_locking 设置为 Off(0);
  2. 使用 Redis 作为 Session 存储,phpredis 默认关闭了 session locking

4. MySQL

WordPress 在面对 CC 攻击时,数据库虽然很容易成为瓶颈,但也比较容易解决,毕竟相比起 WordPress 本身,MySQL 还是健壮得多。

4.1. 自建 MySQL 关闭 binlog

如果是自建的 MySQL 服务请首先确保 binlog 是关闭的,否则磁盘很快就会被撑爆,对于有定期备份的单点数据库来说,binlog 的上场机会实在有限。

4.2. 干掉慢查询

数据量大的 WordPress 站点多少都会存在一些慢查询,在平时影响不会很明显,但在面对 CC 攻击的时候,性能问题就会被放大。主要是因为 WordPress 的数据查询方式决定了它的每一个请求背后都可能会产生大量的 SELECT,若主题没有考虑过这方面的优化,情况就更遭,此时如果在某一个主要的数据表上有慢查询,那么 MySQL 的性能就会急剧下降,CPU 占用率持续飙高。

一个常见的慢查询是:

SELECT post_id FROM wp_postmeta WHERE meta_key = ? AND meta_value = ?

meta_value 是个 LongText,没有索引,这种情况下建立一个合适的索引就可以解决问题了

4.3. Object Cache Plugin

如上面说的,WordPress 容易触发大量的 SELECT,两个请求之间 SELECT 的内容又是大量重复的,所以各种 Object Cache Plugin 可以显著的减少数据库查询,用空间换时间。

  1. Memcached
  2. Redis

4.4. 应对搜索攻击

如果对方通过搜索随机词发起攻击,呃……似乎只有在攻击发生时禁用搜索功能了,毕竟对 WordPress 来说上搜索引擎还是有点太奢侈了。

5. WordPress

处理完上面这些外部因素,如果仍不见成效或成效不明显的话,那么就需要审视 WordPress 网站本身的问题了。有必要的话最好给网站加上性能监控,获取到网站性能的详细数据后再考虑对策,国内比较好用的 APM 服务有 Tingyun 和 OneAPM,在本次应对中我选用了 Tingyun(免费版),上文中的 session locking 问题和 MySQL 慢查询都是通过 APM 的报表发现的。

总之,做性能优化,绝对不能靠猜

5.1. 从慢事务记录中找到害群之马

如果使用了 APM 服务,或者 php-fpm 开启了慢事务日志,那么还可以从慢事务中找到些端倪,看看是什么拖垮了整个应用,并根据分析结果做相应的处理以提升应用自身的响应速度。

5.2. 保护 /wp-admin/admin-ajax.php

这个路由本来是给 WordPress 后台准备的异步请求路由,用来提高后台操作的用户体验。由于它很方便,又没有过多的限制,很多前台插件在需要使用 ajax 功能时就会直接选用这条路由来做。但由于它是设计给后台使用的,所以在它内部没有数据缓存机制,这就导致它成了 CC 攻击的重灾区,而且是一打一个准。

当对方攻击的是这条路由时,可以考虑在不影响主功能的情况下暂时禁用掉这些不规范的插件,也可以结合 Nginx 的 WAF 来设置针对性的过滤规则。

5.3 常见废操作

这是额外值得一提的,因为有些操作很容易被想到或者百度到,但其实并无卵用,包含且不仅限于:

  1. WP-Super-Cache 等静态缓存类插件 为什么没有卵用原因很简单,因为对手不可能傻到去攻击你能轻易静态化或静态缓存的 url

  2. BBQ 等防攻击/安全插件 这类插件在防范一些诸如 XXS、渗透、SQL 注入或者密码爆破等攻击时有一定效果,但在面对 CC 攻击时也无能为力。对于 CC 攻击而言,只要攻击到达 WordPress 基本上就算是成功了,此时即便识别出这是一个攻击也于事无补。

总结

WordPress 由于其巨大的使用量,所以研究的人也多,在面对 CC 之类的攻击时天生弱势。所以在遭遇 CC 攻击时,首先要利用好请求链路上的其他环节来增强防护能力,让网站尽快恢复服务,哪怕是部分服务,也要尽可能的让攻击的影响最小化。在此之后,再从最薄弱的环节——网站本身上着手进行优化,优先补掉网站在高并发情况下明显的性能短板。最后还要注意保持警惕,迅速反应,见招拆招,因为对手极有可能会在一波攻击失效后重新组织新一轮攻势。

最后还有几个忠告送上,面对攻击一定不要自乱阵脚,越艰难的时刻越要保持理性:

不要盲目追加防护开支

有些站长在面对攻击束手无策时,倾向于通过花钱购买各类服务来解决被攻击的问题,殊不知这样正中了攻击者的下怀,因为高防服务是很昂贵的,很多创业公司就是因为这笔额外的费用被活活拖死。所以切勿病急乱投医,有这钱还不如去请一个专家帮忙分析应对,这或许才是更稳妥的办法。

别妄想一次性解决问题

也有一些站长,在选择应对方案的时候,总倾向于怎么一次性解决问题,追求一劳永逸,甚至不惜考虑重写应用。

但请时刻谨记:攻击你的,也是一个人,面对防御他也会想办法

所以,在面对 CC 攻击的时候,一定要选用最快见效又最便宜的方法,将攻击的效果最小化,并做好和攻击者打持久战的准备,毕竟攻击也是要花钱的,如果长期没有很好的成效,背后的金主也是不会持续提供资金的。

好好想想得罪过谁

线上的问题不一定就得线上解决,有时候从线下突破可能更快,嘿嘿嘿(大雾~~~~)