让网站变得更加健壮

上次修订:2019 年 1 月 5 日
原因:修改变量名称错误。

  我们的城市会为居民提供非常多的便利和价值,这都是每一个个体的功劳,当城市遭受洪水、地震、雷暴,乃至恐怖分子袭击时,自身的防御就显得尤其重要。和城市一样,因特网上的一个网站就好比一座村庄、一座城市,同样会受到各类潜在灾害的威胁,因此,我们需要在威胁来临之前,让网站尽可能健壮,当暴风雨来临时,我们即可最大限度地避免瘫痪的发生。

* 网站基础设施的健壮 *
  无论什么网站,它都需要一个立足点,它不可能是一个空中楼阁,我们在建造一座城市的时候,就会考虑选址的问题,我们都不会愿意把城市建立在一个地震、火山之类的自然灾害频发的地方,网站也是类似的。如今非常流行将网站托管在 IDC(因特网数据中心),当我们选择 IDC 的时候,就需要确定 IDC 的存在史,以及经营的编年史,看看候选的 IDC 是否运行了足够长的时间,以及他们最近几年是否发生过运营灾难,重点是信誉度,以及服务中断次数和客户数据丢失之类的问题。
  一家优秀的 IDC,当遇到技术上的问题时,或者直白一点说,他们遇到了灾难,会优先考虑客户的数据保全,以及快速的业务恢复,而不良的 IDC,则会首先剥离自身与运营问题的关系,甚至把客户的数据保全问题置之度外。而那些运营时间短的 IDC,他们在处理账务时可能会出现问题,例如突然蒸发,最容易发生这种问题的是那些主打性价比的单人运营的 IDC。
  我们除了要看 IDC 的运营时间和灾难应对史,还应当了解 IDC 的目标客户群体,像是 Azure 这类的服务商,他们的目标客户是大型企业,当发生灾难后,针对散户的善后工作可能会迟于他们的主要客户。
  除了这些极端情况,我们还需要考虑日常运行的情况,例如,我们的网站主要面向中国大陆的访问者,由于备案制度的原因,很多中小站长都会把网站托管到海外,这样,数据中心到中国大陆的链路就是我们要考虑的重点问题了。从中国大陆到海外,有几条主要的链路,分别是:中国电信的 163 承载网,以及 CN2 承载网,中国联通和中国移动自身的国际链路,由于中国电信拥有几乎 80% 的出海带宽,有时候它还未其它运营商提供接入能力,最典型的例子是中国电信的 CN2 GIA 链路。一些 IDC 接入了中国电信的 CN2 GIA 链路,并且,中国联通和中国移动还接入了 CN2 GIA 链路,从中国大陆访问这类 IDC,速度往往非常有保障。
  如何测试我们到 IDC 之间经过了哪条链路呢?很简单,只要找到该 IDC 的一个 IP 地址,然后使用 tracert -d 加 IP 地址,例如:tracert -d www.example.com。如果回显中出现少量的 202.97 开头的 IP 地址(通常有 0 到 3 个),并且有大量 59.43 开头的 IP 地址(通常有 3 个或更多),这就是 CN2 GIA 链路,属于最佳链路,如果出现大量 202.97 开头的 IP 地址,并且有少量 59.43 开头的 IP 地址,则是性价比较高的 CN2 GT(CN2 全球承载)链路,如果全都是 202.97 开头的 IP 地址,甚至没有,这就是普通链路。

* 服务器操作系统的健壮 *
  我们找到了合适的基础设施,现在可以在上面安装服务器了,如果选定了 WebHosting,也就是大家常说的虚拟主机,这一部分可以略过。租用裸机服务器对于大多数人来说过于昂贵,并且有很大的性能浪费问题,而 WebHosting 又不能满足需求,例如安装软件,或者更改操作系统设置。为了迎合这样的需求,市场上出现了虚拟专用服务器这样的解决方案,也就是说,用软件来模拟一台全功能的实体服务器,而价格是实体服务器的二十分之一。我们可以把这样的服务器当做实体服务器来使用,包括安装操作系统。
  在服务器上,我们应当安装受支持的操作系统,例如,2018 年,如果要安装 Windows Server 版,则至少应该安装 Server 2012,如果要安装基于 Linux 的系统,我们应当选用 CentOS 7,这是因为,使用不受支持的操作系统,会让攻击者有机会利用已知漏洞来攻击操作系统本身,从而入侵服务器。除非需要运行微软系列的软件,不然,首选使用的操作系统是 CentOS,为什么不是 Debian?因为 CentOS 来源于 Red Hat Enterprise Linux,而 RHEL 受到红帽公司的维护,Debian 是由社区来维护的,RHEL 还受到各大服务器制造商的认证,而且也有大量具有影响力的公司在使用,它的品质是被业界承认的。
  我们可以通过 IDC 的“一键安装”功能,来快速安装操作系统,当系统安装好之后,首要事项是变更 root 帐户的密码,一个合格的密码应当包含大写字母、小写字母、数字和符号中任意三项,且位长不低于 7 位,并且不应出现常见的词语(如果非要使用,请使用形象的字母,或者发音相近的数字来代替)。此外,在密码中,相邻的字符不可以是相同的。当设定了复杂的密码后,请不要忘记定期修改它,如果不希望使用密码,还可以选择使用秘钥来进行登录验证,确认秘钥登录配置正确后,不要忘记禁止 root 帐户通过密码进行身份验证,只应该允许通过秘钥进行身份验证。设置了复杂的密码后,请立即为操作系统安装所有更新,这会修复很多安全漏洞,指令是 yum -y update。
  对于外部发起的入侵,我们可以通过系统自带的防火墙(firewall)来阻挡,通常,我们不应当关闭系统的 firewall。防火墙的配置原则是:允许尽可能少的协议和端口被外部连接,对于要求更高的同学,还可以限制某些端口只能从某个 IP 地址连接。对于 SSH 服务,我们可以改变它的默认端口,然而请不要忘记更改防火墙的配置,否则服务器就会无法被远程管理,最简单的方法是复制 /usr/lib/firewalld/services/ssh.xml 为一个新文件,命名随意,然后,打开复制到的文件,将 port 后面的数值,从 22 改为 SSH 的新端口,然后保存。打开 /etc/firewalld/zones/public.xml,复制 ssh 所在行,在其下另开一行并粘贴,把复制到的行的 ssh 修改为 /usr/lib/firewalld/services 中复制到的文件的名称,不含扩展名,然后保存。在开启了 SELinux 的系统上,我们还应当修改 SELinux 的配置,否则 SSH 服务会无法启动,请执行这条命令:semanage port -a -t ssh_port_t -p tcp 端口号,这样,SELinux 的配置就完成了。至于为什么没有介绍如何修改 SSH 端口,这是因为,你可以很容易找到修改它的方法。
  我们还可以修改系统的网络参数,从而让服务器更加健壮,由于修改这些参数具有一定风险,因此本文不展开介绍。

* 网站服务器的健壮 *
  操作系统已经很健壮了,我们可以通过专门的网页服务器软件来搭建网站运行的环境,一个软件并不能满足日常的需要,将软件组合起来一起使用便成为了当今流行的做法,主流的组合是 Nginx + PHP-FPM + MariaDB。Nginx 负责处理一般的网页,例如 html 页面,它的特长是将静态资源发给客户端,有点像是餐厅里的服务员,PHP 是根据脚本文件生成网页的软件,也叫 PHP 运行时,它有点像餐厅里的厨师,根据客户的要求,烹制满足他们口味的菜肴。MariaDB 是数据库软件,它有点像餐厅里的导师,教授厨师如何烹制符合规范的菜肴。稍微扩展一下:客户来到餐厅后,会找服务员点菜,服务员把菜单交给厨师,厨师烹制好之后,交给服务员,服务员再拿给客户,厨师并不是全能的,在烹饪的时候,Ta 们可能会请教导师。
  在这样的架构中,如果碰到了恶意的客户,他们只想点菜后马上倒掉,从而拖垮服务员和厨师,甚至是导师,那他们完全可以得逞,很简单,他们要做的只是发出大量的请求。那么,我们该如何来应对呢?我们可以限制某个地方来的客户(IP 地址)的同时连接数和请求数,为什么限制连接数呢?因为,一个原本只能容纳 50 人的餐厅,突然来了 500 人,那它是会被挤爆的,请求数就是客户在一定的时间内能够找服务员的次数。如果我们仅仅限制同时能够进入餐厅的人数,而没有对他们请求服务员的次数进行限制,来了一个速度很快,而且又喜欢刁难服务员的怪客,餐厅照样会瘫痪。
  在 Nginx 中,我们如何来限制连接数和请求数呢?首先,Nginx 需要具有当前连接到服务器的 IP 地址,以及某个 IP 地址的请求数的列表,为了保存这些信息,我们需要为它配置两组内存区块,为什么是两组内存区块呢?因为连接数和请求数是由不同的模块进行限制的,而且,针对单个 IP,以及整个网站的配置又需要单独进行,所以,我们需要将两个限制模块都进行配置,让它们协力工作,限制才有意义。在 64 位系统上,为限制连接数的模块配置一个 8 MB 的内存区块就能够存储大约 131, 000 个状态信息,然而,限制请求数的模块需要比限制连接数的模块多一倍的内存来存储等量的状态信息。稍微扩展一下,状态信息包含了 IP 地址,请求哪个网站,现在的连接数和请求数是多少这样的信息。
  如何配置 Nginx,以便限制单个 IP 的连接数,以及单个 IP 的请求数呢?首先,我们在 Nginx 的主配置文件的 http 区段中,加入如下两组配置参数:
limit_conn_zone $binary_remote_addr zone=limit_connections_number:8m;
limit_conn_zone $server_name zone=limit_total_connections_number:8m;
limit_req_zone $binary_remote_addr zone=limit_requests_number:16m rate=4r/s;
limit_req_zone $server_name zone=limit_total_requests_number:16m rate=20r/s;
配置参数的含义是什么呢?
limit_conn_zone:配置限制连接数的模块,可以指定多个,只要内存区块的名称不同即可 $binary_remote_addr:为限制连接数的模块设定键值,键值可以是不同的变量,以便我们根据不同的条件进行限制,这是限制单个 IP 的连接数 zone=limit_connections_number:8m:内存区块的名称是 limit_connections_number,并且分配 8 MB 内存;
limit_conn_zone:配置限制连接数的模块,可以指定多个,只要内存区块的名称不同即可 $server_name:为限制连接数的模块设定键值,键值可以是不同的变量,以便我们根据不同的条件进行限制,这是限制整个网站的总连接数 zone=limit_total_connections_number:8m:内存区块的名称是 limit_total_connections_number,并且分配 8 MB 内存;
limit_req_zone:配置限制请求数的模块,可以设定多个,只要内存区块的名称不同即可 $binary_remote_addr:为限制请求数的模块设定键值,键值可以是不同的变量,以便我们根据不同的条件进行限制,这是限制单个 IP 的请求数 zone=limit_requests_number:16m rate=4r/s:内存区块的名称是 limit_requests_number,并且分配 16 MB 内存,同时,我们限制每个 IP 地址每秒只能请求 4 次;
limit_req_zone:配置限制请求数的模块,可以设定多个,只要内存区块的名称不同即可 $server_name:为限制请求数的模块设定键值,键值可以是不同的变量,以便我们根据不同的条件进行限制,这是限制整个网站的总请求数 zone=limit_total_requests_number:16m rate=20r/s:内存区块的名称是 limit_total_requests_number,并且分配 16 MB 内存,同时,我们限制网站每秒只能有 20 个请求;
  当我们分配了足够的内存区块后,即可开始配置 IP 连接数,以及单个 IP 的请求数了,请在 server 区段中,加入下面两组配置参数:
limit_conn limit_connections_number 30;
limit_conn limit_total_connections_number 300;
limit_req zone=limit_requests_number burst=4 [nodelay];
limit_req zone=limit_total_requests_number burst=4 [nodelay];
配置参数的含义是什么呢?
limit_conn limit_connections_number 30,限制单个 IP 的连接数为 30;
limit_conn limit_total_connections_number 300:限制整个网站的总连接数为 300;
limit_req zone=limit_requests_number burst=4 [nodelay],限制单个 IP 的请求数为每秒 4 次,请求数的限制是在 http 区段中,限制请求数的内存区块配置中设定的,除了限制单个 IP 的请求数为每秒 4 次,这里还允许超出 4 个请求,另外,对于超出限制的请求,Nginx 会将它们加入队列,直到内存区块耗尽,或者超出其它限制而关闭连接。我们可以让 Nginx 向超出请求数限制的客户端发送 503 错误提示,方法是,在配置参数的末尾加上 nodelay;
limit_req zone=limit_total_requests_number burst=4 [nodelay],限制网站每秒只允许被请求 20 次,请求数的限制是在 http 区段中,限制请求数的内存区块配置中设定的,除了限制最大允许的请求数为每秒 20 次,这里还允许超出 4 个请求,另外,对于超出限制的请求,Nginx 会将它们加入队列,直到内存区块耗尽,或者超出其它限制而关闭连接。我们可以让 Nginx 向超出请求数限制的客户端发送 503 错误提示,方法是,在配置参数的末尾加上 nodelay;
  在 server 区段的配置信息,也可以放到 http 区段,这样就会应用到所有 server 区段中,对于配置较为统一的服务器,这样做将会减小配置文件的体积。
  同学们,我们可以通过限制连接数和请求数,来避免服务器遭到蛮力攻击的威胁,然而,有时候,我们会迎来正常的访问高峰,例如,秋游或者春游的时候,一个年级,乃至一个学校的同学都来我们这里,人数显然超出了限制,简单粗暴地把它们赶走,显得太不友好了。对于这个问题,我们要如何解决呢?很简单,我们只要配置缓存即可,为了更形象,或者更确切地说明缓存的工作原理,我们来把餐厅换成照相馆。
  一个班级有 60 位同学,老师呢?先不管他,他们今天来照集体照,每位同学都想要一份,照相机一次只能拍出一张照片,要拍 60 张吗?是的,确实有这个必要,然而,且慢,您,真的能确定拍 60 张照片的时候,同学们的表情都是一样的吗?而且,照相师不会被累死?哦,那我们可以复印啊。
  刚才的场景,在没有缓存的情况下,PHP 确实需要为每个请求创建一次一模一样的页面,这样既消耗时间,也容易在出现大量访问的时候耗尽资源引发崩溃,如果我们分别配置 Nginx、PHP 和 MariaDB,那就可以最大限度地利用好缓存这个神奇的东西。Nginx 的职责是缓存 PHP 生成的 HTML 页面,下一个同样的请求到来时,可以直接返回网页,而不必请求 PHP。PHP 的职责是缓存那些通过脚本文件编译成的二进制机器码,这样,同一个脚本再有请求到来时,可以不需要重新编译为二进制机器码。而 MariaDB 的职责是缓存数据库查询结果,当有同样的请求到来时,它可以直接返回查询结果,而不需要再去执行 select 这类费时的操作。由于篇幅已经够长,这里就不展开介绍了,下次有机会,Armstrong 再给同学们慢慢道来。

* 建造城墙,防止灾难直接攻入城市 *
  经过刚才的设定,我们的城市已经足够健壮,然而,还有更加厉害的攻击者可以击垮我们的家园,这样,我们就需要对城市的外部进行加固,那就是——修建城墙。
  网站服务器经常会受到 CC 攻击,这样,我们可以通过 CDN(内容分发网络)服务来抵挡外部传来的攻击,也就是,修改域名的指向,让访问网站的客户端首先进入到 CDN 服务商的网络,让他们的防火墙和服务器去阻挡这些攻击。有的 CDN 很聪明,它们利用了 AnyCast 技术,简单地说,8.8.8.8 这个地址,你从中国大陆访问,和从美国洛杉矶,德国柏林,法国巴黎访问,到达的服务器都是不一样的,虽然 IP 地址完全相同。AnyCast 在很大程度上可以阻断攻击,在爱知、山形、茨城、神奈川、大阪、京都、北海道、东京各设定一组节点,来自神奈川的攻击不会影响东京、大阪、京都、爱知的服务器,反之亦然。另外,有的 CDN 还会进行人机身份验证,如果通不过验证,它会自动断开连接,并在它的网络内阻断该 IP 随后的连接请求,无论该 IP 是从哪个服务器开始登陆的。

* 后记 *
  网站服务器的保安并不是通过简单的几个步骤就能完成,不过,同学们也不要被困难吓倒,我们只需要了解它的工作原理,并且有针对性、系统性地进行防护,网站是可以非常平稳地运行的。

加入对话

3条评论

  1. “最容易发生这种问题的是那些主打性价比的独人 IDC。”这个独人IDC的说法听起来有点奇怪呢,我感觉单人运营的IDC或者是一人IDC会不会好一些?

  2. 学习了,现在这段时间,无论是大型网站还是,个人博客网站,有事没事的就被CC一下,也不知道啥情况,亏着有CDN,要不然估计它们都打不开了。

留下评论

电子邮件地址不会被公开。 必填项已用*标注