本文来自作者 大闲人柴毛毛 在 GitChat 上分享 「高性能网站实用技巧之负载均衡篇」,「阅读原文」查看交流实录。
「文末高能」
编辑 | 哈比
1. 什么是负载均衡?
当一台服务器的性能达到极限时,我们可以使用服务器集群来提高网站的整体性能。
那么,在服务器集群中,需要有一台服务器充当调度者的角色,用户的所有请求都会首先由它接收,调度者再根据每台服务器的负载情况将请求分配给某一台后端服务器去处理。
在这个过程中,调度者如何合理分配任务,保证所有后端服务器都将性能充分发挥,从而保持服务器集群的整体性能最优,这就是负载均衡要解决的问题。
下面详细介绍负载均衡的四种实现方式。
2. HTTP 重定向实现负载均衡
2.1 过程描述
当用户向服务器发起请求时,请求首先被集群调度者截获;调度者根据某种分配策略,选择一台服务器,并将选中的服务器的 IP 地址封装在 HTTP 响应消息头部的 Location 字段中,并将响应消息的状态码设为 302,最后将这个响应消息返回给浏览器。
当浏览器收到响应消息后,解析 Location 字段,并向该 URL 发起请求,然后指定的服务器处理该用户的请求,最后将结果返回给用户。
在使用 HTTP 重定向来实现服务器集群负载均衡的过程中,需要一台服务器作为请求调度者。
用户的一项操作需要发起两次 HTTP 请求,一次向调度服务器发送请求,获取后端服务器的 IP,第二次向后端服务器发送请求,获取处理结果。
2.2 调度策略
调度服务器收到用户的请求后,究竟选择哪台后端服务器处理请求,这由调度服务器所使用的调度策略决定。
-
随机分配策略
当调度服务器收到用户请求后,可以随机决定使用哪台后端服务器,然后将该服务器的 IP 封装在 HTTP 响应消息的 Location 属性中,返回给浏览器即可。 -
轮询策略 (RR)
调度服务器需要维护一个值,用于记录上次分配的后端服务器的 IP。那么当新的请求到来时,调度者将请求依次分配给下一台服务器。
由于轮询策略需要调度者维护一个值用于记录上次分配的服务器 IP,因此需要额外的开销;
此外,由于这个值属于互斥资源,那么当多个请求同时到来时,为了避免线程的安全问题,因此需要锁定互斥资源,从而降低了性能。
而随机分配策略不需要维护额外的值,也就不存在线程安全问题,因此性能比轮询要高。
2.3 优缺点分析
采用 HTTP 重定向来实现服务器集群的负载均衡实现起来较为容易,逻辑比较简单,但缺点也较为明显。
在 HTTP 重定向方法中,调度服务器只在客户端第一次向网站发起请求的时候起作用。
当调度服务器向浏览器返回响应信息后,客户端此后的操作都基于新的 URL 进行的 (也就是后端服务器),此后浏览器就不会与调度服务器产生关系,进而会产生如下几个问题:
-
由于不同用户的访问时间、访问页面深度有所不同,从而每个用户对各自的后端服务器所造成的压力也不同。
而调度服务器在调度时,无法知道当前用户将会对服务器造成多大的压力,因此这种方式无法实现真正意义上的负载均衡,只不过是把请求次数平均分配给每台服务器罢了。
-
若分配给该用户的后端服务器出现故障,并且如果页面被浏览器缓存,那么当用户再次访问网站时,请求都会发给出现故障的服务器,从而导致访问失败。
3. DNS 负载均衡
3.1 DNS 是什么?
在了解 DNS 负载均衡之前,我们首先需要了解 DNS 域名解析的过程。
我们知道,数据包采用 IP 地址在网络中传播,而为了方便用户记忆,我们使用域名来访问网站。
那么,我们通过域名访问网站之前,首先需要将域名解析成 IP 地址,这个工作是由 DNS 完成的。也就是域名服务器。
我们提交的请求不会直接发送给想要访问的网站,而是首先发给域名服务器,它会帮我们把域名解析成 IP 地址并返回给我们。我们收到 IP 之后才会向该 IP 发起请求。
那么,DNS 服务器有一个天然的优势,如果一个域名指向了多个 IP 地址,那么每次进行域名解析时,DNS 只要选一个 IP 返回给用户,就能够实现服务器集群的负载均衡。
3.2 过程描述
首先需要将我们的域名指向多个后端服务器 (将一个域名解析到多个 IP 上),再设置一下调度策略,那么我们的准备工作就完成了,接下来的负载均衡就完全由 DNS 服务器来实现。
当用户向我们的域名发起请求时,DNS 服务器会自动地根据我们事先设定好的调度策略选一个合适的 IP 返回给用户,用户再向该 IP 发起请求。
3.3 调度策略
一般 DNS 提供商会提供一些调度策略供我们选择,如随机分配、轮询、根据请求者的地域分配离他最近的服务器。
3.4 优缺点分析
DNS 负载均衡最大的优点就是配置简单。服务器集群的调度工作完全由 DNS 服务器承担,那么我们就可以把精力放在后端服务器上,保证他们的稳定性与吞吐量。
而且完全不用担心 DNS 服务器的性能,即便是使用了轮询策略,它的吞吐率依然卓越。
此外,DNS 负载均衡具有较强了扩展性,你完全可以为一个域名解析较多的 IP,而且不用担心性能问题。
但是,由于把集群调度权交给了 DNS 服务器,从而我们没办法随心所欲地控制调度者,没办法定制调度策略。
DNS 服务器也没办法了解每台服务器的负载情况,因此没办法实现真正意义上的负载均衡。它和 HTTP 重定向一样,只不过把所有请求平均分配给后端服务器罢了。
此外,当我们发现某一台后端服务器发生故障时,即使我们立即将该服务器从域名解析中去除,但由于 DNS 服务器会有缓存,该 IP 仍然会在 DNS 中保留一段时间,那么就会导致一部分用户无法正常访问网站。这是一个致命的问题!好在这个问题可以用动态 DNS 来解决。
3.5 动态 DNS
动态 DNS 能够让我们通过程序动态修改 DNS 服务器中的域名解析。从而当我们的监控程序发现某台服务器挂了之后,能立即通知 DNS 将其删掉。
3.6 小结
DNS 负载均衡是一种粗犷的负载均衡方法,这里只做介绍,不推荐使用。
4. 反向代理负载均衡
4.1 什么是反向代理?
在介绍 “反向代理” 之前,首先要介绍一下 “正向代理” 的概念。
正向代理
在 NAT 技术 (Network Address Translation) 出现之前,所有主机无法直接与外网相连,要想上网,需要连接到一台能够访问外网的 Web 服务器,再通过这台服务器访问外网。而这台 Web 服务器就叫做 “正向代理服务器”。
现在的 “翻墙” 技术也是如何,我们把请求发给一台可以连接外面世界的 Web 服务器,由它转发我们的请求,再将结果返回给我们。这台 Web 服务器就是 “正向代理服务器”。
综上所述:正向代理服务器是客户端和目的服务器之间的一个中介,客户端通过正向代理服务器访问客户端原本无法访问的目标服务器。
反向代理
客户端向一个服务器 A 提交请求后,服务器 A 偷偷地去服务器 B 上获取资源,并返回给客户端。客户端天真地以为数据是服务器 A 给他的。
在这过程中,服务器 A 称为 “反向代理服务器”,服务器 B 称为反向代理服务器的 “后端服务器”。
“正向代理” 与 “反向代理” 的区别?
两者最直观的区别是在用户的角度。“正向代理” 是用户使用的技术。用户首先是知道自己要访问的目标服务器是谁,但由于某种原因无法直接访问该目标服务器,因此选择使用正向代理服务器帮忙转发请求。
而 “反向代理” 是服务器使用的技术。用户向服务器发送请求后,服务器在用户不知情的情况下去其他服务器上获取资源并返回给用户。
4.2 什么是反向代理服务器?
反向代理服务器用于存储静态数据和缓存数据,它处于 Web 服务器之前。当用户发起请求时,请求首先被反向代理服务器截获,若请求的是静态数据或缓存数据,则反向代理服务器直接将数据返回。
若请求的是动态数据,且缓存中不存在,则反向代理服务器将请求转发给后端的 Web 服务器,在获取后端服务器的数据后再返回给用户。
4.3 反向代理服务器有何作用?
反向代理服务器能够分担后端服务器的压力。在请求数很高的情况下,即使服务器使用了缓存,但仍然无法应对巨大的并发数,因此需要反向代理服务器的帮忙。
反向代理服务器收到请求后,如果请求的是缓存数据或静态数据,则直接返回给用户,而无需再劳驾后端服务器了,从而缓解后端服务器的压力。
4.4 如何选择反向代理服务器?
反向代理服务器有多种选择,可以使用 Nginx 的反向代理模块,但它毕竟是 Nginx 的一个插件,功能不够全面。
Squid 是一个缓存服务器,除提供反向代理外还拥有其他功能,但过于重量级,历史也比较悠久,性能不咋地。
Varnish 是一款专门用于反向代理的服务器,相对于 Squid 较为轻量,由于使用内存缓存,因此性能较好,但也收到了内存的存储容量的限制。
究竟哪一个反向代理服务器适合你,可以参考:
varnish / squid / nginx cache 有什么不同?
这里我们以 Varnish 为例。
4.5 什么是反向代理负载均衡?
反向代理服务器是一个位于实际服务器之前的服务器,所有向我们网站发来的请求都首先要经过反向代理服务器,服务器根据用户的请求要么直接将结果返回给用户,要么将请求交给后端服务器处理,再返回给用户。
之前我们介绍了用反向代理服务器实现静态页面和常用的动态页面的缓存。接下来我们介绍反向代理服务器更常用的功能——实现负载均衡。
我们知道,所有发送给我们网站的请求都首先经过反向代理服务器。那么,反向代理服务器就可以充当服务器集群的调度者,它可以根据当前后端服务器的负载情况,将请求转发给一台合适的服务器,并将处理结果返回给用户。
4.6 优点
1. 隐藏后端服务器
与 HTTP 重定向相比,反向代理能够隐藏后端服务器,所有浏览器都不会与后端服务器直接交互,从而能够确保调度者的控制权,提升集群的整体性能。
2. 故障转移
与 DNS 负载均衡相比,反向代理能够更快速地移除故障结点。当监控程序发现某一后端服务器出现故障时,能够及时通知反向代理服务器,并立即将其删除。
3. 合理分配任务
HTTP 重定向和 DNS 负载均衡都无法实现真正意义上的负载均衡,也就是调度服务器无法根据后端服务器的实际负载情况分配任务。但反向代理服务器支持手动设定每台后端服务器的权重。
我们可以根据服务器的配置设置不同的权重,权重的不同会导致被调度者选中的概率的不同。
4.7 缺点
1. 调度者压力过大
由于所有的请求都先由反向代理服务器处理,那么当请求量超过调度服务器的最大负载时,调度服务器的吞吐率降低会直接降低集群的整体性能。
2. 制约扩展
当后端服务器也无法满足巨大的吞吐量时,就需要增加后端服务器的数量,可没办法无限量地增加,因为会受到调度服务器的最大吞吐量的制约。
4.8 粘滞会话
反向代理服务器会引起一个问题。若某台后端服务器处理了用户的请求,并保存了该用户的 session 或存储了缓存。
那么当该用户再次发送请求时,无法保证该请求仍然由保存了其 Session 或缓存的服务器处理,若由其他服务器处理,先前的 Session 或缓存就找不到了。
1. 解决办法一
可以修改反向代理服务器的任务分配策略,以用户 IP 作为标识较为合适。相同的用户 IP 会交由同一台后端服务器处理,从而就避免了粘滞会话的问题。
2. 解决办法二
可以在 Cookie 中标注请求的服务器 ID,当再次提交请求时,调度者将该请求分配给 Cookie 中标注的服务器处理即可。
5. 反向代理 + 数据缓冲区
5.1 什么是数据缓冲区?
我们可以在数据库之前开辟一块内存缓冲区,我们把这块区域称为数据缓冲区。所有从数据库出来和进入的数据都要经过该缓冲区。
那么,数据想要进入数据库,首先需要进入缓冲区,当缓冲区存满时,一次性地写入数据库,从而降低了数据库操作的频率;
同理,从数据库出来的数据也会进入该缓冲区,那么下次需要相同数据的时候直接从缓冲区中取即可。
要知道,从内存中取数据要比从数据库中取数据快多了,因此缓冲区能大大提升数据插入和查询的性能。
5.2 如何构建数据缓冲区?
根据刚才对缓冲区的介绍,我们可以将数据缓冲区分为:读缓冲和写缓冲。
-
读缓冲:用于存放即将被存入数据库的数据
-
写缓存:用于存放最近一段时间访问频率较高的数据
使用 Memcache 实现数据缓冲区
这里我们使用 memcache 来实现数据缓冲区。具体的 Memcache 的介绍请自行百度吧,这里简单介绍下 Memcache 的几个优点:
1. 查询效率高
Memcache 采用健值对的形式存储数据,并且采用优化了的基于 Key 的 Hash 算法,因此不管 Memcache 中存储了多少数据,根据 key 查询 value 的时间复杂度永远是 O(1)!
2. 网络并发能力强
Memcache 采用了 libevent 函数库来实现 TCP 通信,因此在较高并发数的情况下仍然能高效工作。大家不用担心它的效率。
关于 Memcache 的使用请移步至:
在 Linux 上安装 Memcached 服务
构建写缓冲区
场景假设:实现点击量的记录
最 Low 的做法是每有一个用户点击,就把数据库中的相关值加 1. 但这种每次更新数据库的做法显然不够高效,当访问量很大时,需要不断更新数据库,大大降低服务器的整体性能。
因此,访问量的登记完全可以存入写缓存中,当访问量存到 1000 时,一次性写入数据库,从而数据库更新频率从 1000 次降低到 1 次,大大节省了开销。
当然,使用缓存随之会带来数据实时性降低的问题。但对于像访问量这种无关紧要的数据来说,用降低实时性来换取服务器性能开销,还是相当划算的。
注意:小心线程安全问题!
要实现缓存中的指定页面的访问量加一,一共需要三步:
-
将指定页面的当前访问量取出来
-
访问量加一
-
更新缓存中该页面的访问量
因此,若多线程同时访问,会出现线程安全问题。
因此我们需要使用 memcache 的原子加一操作 (increment) 来避免线程安全问题。
补充知识:如何判断用户是否登录?
用户点击 “记住密码” 后就不需要再输入密码,那么当用户再次访问网站时,服务器该如何判断该用户是否已经登录呢?
在很久以前,用户登录之后服务器会在用户的 Cookie 中存放该用户的 id,若用户再次访问网站时,如果请求中包含用户 id 就认为他已经登录,否则就需要重新登录。
但这种方法会引起安全隐患,由于 id 具有规律性,黑客往往会篡改他本地的 Cookie 来冒充其他用户登录。
为了避免这种情况的发生,在用户注册的时候,服务器会为每个用户生成一个无规律的随机字符串 ticket,用于标示该用户,并将其存入用户的 Cookie。由于字符串随机生成,没有了规律性,因此黑客没办法猜到其他用户的 ticket。
当用户每次访问网站时,如果请求中携带了 ticket,我们就查询数据库中是否存在该 ticket,若存在则表示该用户已经登录,否则需要重新登录。
那么问题来了,如果每次判断 ticket 是否存在都需要查询数据库的话,那么当用户量很大的时候会影响服务器整体性能。
因此我们可以将所有的 ticket 存入读缓存,并每隔一段时间更新,确保 ticket 的实时性。
当用户访问网站时,只需要从读缓存中查询 ticket 是否存在即可,无需查询数据库,从而节约了数据库开销。
5.3 如何构建分布式数据缓冲区?
当一个 memcache 存不下所有缓存的时候,我们需要使用多个 memcache 来实现分布式数据缓冲区。
“分布式数据缓冲区” 看似高大上,其实很简单。
假设现在有三台服务器上运行 Memcache,IP 分别是:
-
10.20.100.101
-
10.20.100.102
-
10.20.100.103
在存储之前,我们需要确定究竟把数据存储在哪台缓存服务器上。我们希望数据能够平均地存储在三台服务器上,从而实现负载均衡。可以采用随机分配的方法:
-
将每个请求的 URL 进行 MD5 运算,得到 32 字节的 16 进制数;
-
取前 5 位,模以 3;
-
得到的结果就是缓存服务器的 ID,把数据存到那台缓存服务器上即可。
注:通过概率论可以证明,当访问量很大的时候,采用随机分配的方式能够保证每台缓存服务器被选中的次数是一样的。
6. 反向代理 +Web 组件分离
6.1 什么是 Web 组件?
网站的静态网页 HTML、JavaScript 脚本、CSS 样式、图片、动态数据称为网站的 Web 组件。也就是说,一个 Web 应用由各种各样的 Web 组件构成。
6.2 为什么要进行 Web 组件分离?
一个网站的 Web 组件往往有各自的特点,比如:HTML 页面属于静态文件,当用户请求一个 HTML 页面的时候 Web 服务器会进行 IO 操作,读取 HTML 文件;而用户请求动态数据的时候 IO 操作会比较少,但会涉及到大量的 CPU 计算。
因此,如果静态内容和动态内容都使用相同服务器配置的话显然不能发挥 Web 应用最好的性能,因此我们需要对不同的 Web 组件采取不同的服务器配置方案。因此需要组件分离。
6.3 如何进行组件分离?
我们可以把不同的组件放在不同的服务器上,并且根据组件的特点,定制服务器配置,从而发挥组件最好的性能。要实现不同组件指向不同的服务器,我们首先需要为网站解析更多的子域名。
域名解析
假设我们已经拥有顶级域名 www.5188.help,那么我们可以到购买域名的网站上设置域名的 A 标签,从而分出二级域名。以下是我解析的二级域名:
-
static.5188.help #用于存放静态数据
-
api.5188.help #存放动态数据
-
css.5188.help #存放 css
-
js.5188.help #存放 js
-
upload.5188.help #存放图片、音频、文档
6.4 如何对待不同的组件?
下面具体介绍针对具体组件的服务器配置方案。
静态页面
静态页面 HTML 以文件的形式存储在存储设备,因此存储 HTML 页面的服务器需要有较高的 IO 读写速度,对 IO 密集型操作,我们要进行如下优化:
-
支持 epoll。使得 Web 在高并发情况下仍然保持稳定的吞吐率;
-
非阻塞 IO。避免不必要的 IO 等待;
-
异步 IO;
-
使用 sendfile() 系统调用。避免文件系统磁盘缓冲区到用户地址空间的数据复制;
-
单进程。避免多进程切换的不必要开销。对于 IO 密集型的静态内容处理,多进程并不能带来多大的意义;
-
使用高转速磁盘;
-
使用 RAID 分区。使得磁盘实现并行读写,提高磁盘吞吐量;
-
购买大带宽。
动态内容
动态内容的数据都实时计算生成,或查询数据库得到,为了提升运算速度,因此需要增加 CPU 核数,增加内存容量,具体做法如下:
-
使用快的 CPU;
-
使用大内存;
-
使用多进程;
-
使用数据库连接池,减去连接建立和释放的开销。
CSS 样式表和 avaScript 脚本
一般网站上线后 CSS 样式表和 JavaScript 脚本几乎不会发生变化,因此完全可以将 css 和 js 在用户浏览器的缓存有效期设置更长的时间。
注:在 css、js 的 URL 后可以加一个参数,用来标注当前 css、js 的版本,如:
<link href="css.5188.help/BSB/css/xxx.css?ver=1.0" />
当服务器中 css、js 发生修改后,需要将参数进行修改:
<link href="css.5188.help/BSB/css/xxx.css?ver=2.0" />
那么,当浏览器发现 css 后的参数发生修改时,会重新向服务器请求,而不会使用本地缓存。
图片
对于图片较多的页面,如果每个图片都向服务器请求的话需要消耗大量的时间带带宽,因此服务器向浏览器返回响应信息时一定要设置图片的 Keep-Alive 参数设为 true,延用 TCP 连接。
6.5 Web 组件分离的好处
浏览器对于同一域名的并发数会有限制。Web 组件分离之后,不同类型的 Web 组件需要请求不同的域名,从而能够支持更大的并发量,从而能够提升 Web 组件的下载速度。
近期热文
「阅读原文」看交流实录,你想知道的都在这里