This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
<audio id="audio" title="14 | 高性能数据库集群:读写分离" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c4/2b/c4d9e0115caebd88be350d842a7e202b.mp3"></audio>
“从0开始学架构”专栏已经更新了13期从各个方面阐述了架构设计相关的理论和流程包括架构设计起源、架构设计的目的、常见架构复杂度分析、架构设计原则、架构设计流程等掌握这些知识是做好架构设计的基础。
在具体的实践过程中,为了更快、更好地设计出优秀的架构,除了掌握这些基础知识外,还需要掌握业界已经成熟的各种架构模式。大部分情况下,我们做架构设计主要都是基于已有的成熟模式,结合业务和团队的具体情况,进行一定的优化或者调整;即使少部分情况我们需要进行较大的创新,前提也是需要对已有的各种架构模式和技术非常熟悉。
接下来,我将逐一介绍最常见的“高性能架构模式”“高可用架构模式”“可扩展架构模式”,这些模式可能你之前大概了解过,但其实每个方案里面都有很多细节,只有深入的理解这些细节才能理解常见的架构模式,进而设计出优秀的架构。
虽然近十年来各种存储技术飞速发展但关系数据库由于其ACID的特性和功能强大的SQL查询目前还是各种业务系统中关键和核心的存储系统很多场景下高性能的设计最核心的部分就是关系数据库的设计。
不管是为了满足业务发展的需要还是为了提升自己的竞争力关系数据库厂商Oracle、DB2、MySQL等在优化和提升单个数据库服务器的性能方面也做了非常多的技术优化和改进。但业务发展速度和数据增长速度远远超出数据库厂商的优化速度尤其是互联网业务兴起之后海量用户加上海量数据的特点单个数据库服务器已经难以满足业务需要必须考虑数据库集群的方式来提升性能。
从今天开始,我会分几期来介绍高性能数据库集群。高性能数据库集群的第一种方式是“读写分离”,其本质是将访问压力分散到集群中的多个节点,但是没有分散存储压力;第二种方式是“分库分表”,既可以分散访问压力,又可以分散存储压力。先来看看“读写分离”,下一期我再介绍“分库分表”。
## 读写分离原理
**读写分离的基本原理是将数据库读写操作分散到不同的节点上**,下面是其基本架构图。
<img src="https://static001.geekbang.org/resource/image/36/07/362d22168bf344687ec0c206aa115807.jpg" alt="" />
读写分离的基本实现是:
<li>
数据库服务器搭建主从集群,一主一从、一主多从都可以。
</li>
<li>
数据库主机负责读写操作,从机只负责读操作。
</li>
<li>
数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
</li>
<li>
业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
</li>
需要注意的是,这里用的是“主从集群”,而不是“主备集群”。“从机”的“从”可以理解为“仆从”,仆从是要帮主人干活的,“从机”是需要提供读数据的功能的;而“备机”一般被认为仅仅提供备份功能,不提供访问功能。所以使用“主从”还是“主备”,是要看场景的,这两个词并不是完全等同的。
读写分离的实现逻辑并不复杂,但有两个细节点将引入设计复杂度:**主从复制延迟**和**分配机制**。
## 复制延迟
以MySQL为例主从复制延迟可能达到1秒如果有大量数据同步延迟1分钟也是有可能的。主从复制延迟会带来一个问题如果业务服务器将数据写入到数据库主服务器后立刻1秒内进行读取此时读操作访问的是从机主机还没有将数据复制过来到从机读取数据是读不到最新数据的业务上就可能出现问题。例如用户刚注册完后立刻登录业务服务器会提示他“你还没有注册”而用户明明刚才已经注册成功了。
解决主从复制延迟有几种常见的方法:
1.写操作后的读操作指定发给数据库主服务器
例如注册账号完成后登录时读取账号的读操作也发给数据库主服务器。这种方式和业务强绑定对业务的侵入和影响较大如果哪个新来的程序员不知道这样写代码就会导致一个bug。
2.读从机失败后再读一次主机
这就是通常所说的“二次读取”二次读取和业务无绑定只需要对底层数据库访问的API进行封装即可实现代价较小不足之处在于如果有很多二次读取将大大增加主机的读操作压力。例如黑客暴力破解账号会导致大量的二次读取操作主机可能顶不住读操作的压力从而崩溃。
3.关键业务读写操作全部指向主机,非关键业务采用读写分离
例如,对于一个用户管理系统来说,注册+登录的业务读写操作全部访问主机,用户的介绍、爱好、等级等业务,可以采用读写分离,因为即使用户改了自己的自我介绍,在查询时却看到了自我介绍还是旧的,业务影响与不能登录相比就小很多,还可以忍受。
## 分配机制
将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:**程序代码封装**和**中间件封装**。
1.程序代码封装
程序代码封装指在代码中抽象一个数据访问层所以有的文章也称这种方式为“中间层封装”实现读写操作分离和数据库服务器连接的管理。例如基于Hibernate进行简单封装就可以实现读写分离基本架构是
<img src="https://static001.geekbang.org/resource/image/f8/df/f8d538f9201e3ebee37dfdcd1922e9df.jpg" alt="" />
程序代码封装的方式具备几个特点:
<li>
实现简单,而且可以根据业务做较多定制化的功能。
</li>
<li>
每个编程语言都需要自己实现一次,无法通用,如果一个业务包含多个编程语言写的多个子系统,则重复开发的工作量比较大。
</li>
<li>
故障情况下,如果主从发生切换,则可能需要所有系统都修改配置并重启。
</li>
目前开源的实现方案中淘宝的TDDLTaobao Distributed Data Layer外号:头都大了是比较有名的。它是一个通用数据访问层所有功能封装在jar包中提供给业务代码调用。其基本原理是一个基于集中式配置的 jdbc datasource实现具有主备、读写分离、动态数据库配置等功能基本架构是
<img src="https://static001.geekbang.org/resource/image/3b/07/3b87f6ce297c4af219fa316d29eb5507.jpg" alt="" />
2.中间件封装
中间件封装指的是独立一套系统出来实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供SQL兼容的协议业务服务器无须自己进行读写分离。对于业务服务器来说访问中间件和访问数据库没有区别事实上在业务服务器看来中间件就是一个数据库服务器。其基本架构是
<img src="https://static001.geekbang.org/resource/image/2a/8e/2a2dba7f07581fd055d9cd5a3aa8388e.jpg" alt="" />
数据库中间件的方式具备的特点是:
<li>
能够支持多种编程语言因为数据库中间件对业务服务器提供的是标准SQL接口。
</li>
<li>
数据库中间件要支持完整的SQL语法和数据库服务器的协议例如MySQL客户端和服务器的连接协议实现比较复杂细节特别多很容易出现bug需要较长的时间才能稳定。
</li>
<li>
数据库中间件自己不执行真正的读写操作,但所有的数据库操作请求都要经过中间件,中间件的性能要求也很高。
</li>
<li>
数据库主从切换对业务服务器无感知,数据库中间件可以探测数据库服务器的主从状态。例如,向某个测试表写入一条数据,成功的就是主机,失败的就是从机。
</li>
由于数据库中间件的复杂度要比程序代码封装高出一个数量级,一般情况下建议采用程序语言封装的方式,或者使用成熟的开源数据库中间件。如果是大公司,可以投入人力去实现数据库中间件,因为这个系统一旦做好,接入的业务系统越多,节省的程序开发投入就越多,价值也越大。
目前的开源数据库中间件方案中MySQL官方先是提供了MySQL Proxy但MySQL Proxy一直没有正式GA现在MySQL官方推荐MySQL Router。MySQL Router的主要功能有读写分离、故障自动切换、负载均衡、连接池等其基本架构如下
<img src="https://static001.geekbang.org/resource/image/c9/af/c9c7a3f3602a05d428484c571c1d4faf.jpg" alt="" />
奇虎360公司也开源了自己的数据库中间件AtlasAtlas是基于MySQL Proxy实现的基本架构如下
<img src="https://static001.geekbang.org/resource/image/75/31/75058a4145bb78880faa4e9c74d9d031.png" alt="" title="图片来源网络" />
以下是官方介绍,更多内容你可以参考[这里](https://github.com/Qihoo360/Atlas/wiki/Atlas%E7%9A%84%E6%9E%B6%E6%9E%84)。
>
Atlas是一个位于应用程序与MySQL之间中间件。在后端DB看来Atlas相当于连接它的客户端在前端应用看来Atlas相当于一个DB。Atlas作为服务端与应用程序通信它实现了MySQL的客户端和服务端协议同时作为客户端与MySQL通信。它对应用程序屏蔽了DB的细节同时为了降低MySQL负担它还维护了连接池。
## 小结
今天我为你讲了读写分离方式的原理,以及两个设计复杂度:复制延迟和分配机制,希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧,数据库读写分离一般应用于什么场景?能支撑多大的业务规模?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,162 @@
<audio id="audio" title="15 | 高性能数据库集群:分库分表" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ce/24/cea5da11a77fdba09f6405258a2c2524.mp3"></audio>
上期我讲了“读写分离”,读写分离分散了数据库读写操作的压力,但没有分散存储压力,当数据量达到千万甚至上亿条的时候,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在这几个方面:
<li>
数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。
</li>
<li>
数据文件会变得很大,数据库备份和恢复需要耗费很长时间。
</li>
<li>
数据文件越大,极端情况下丢失数据的风险越高(例如,机房火灾导致数据库主备机都发生故障)。
</li>
基于上述原因,单个数据库服务器存储的数据量不能太大,需要控制在一定的范围内。为了满足业务数据存储的需求,就需要将存储分散到多台数据库服务器上。
今天我来介绍常见的分散存储的方法“分库分表”,其中包括“分库”和“分表”两大类。
## 业务分库
**业务分库指的是按照业务模块将数据分散到不同的数据库服务器。**例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上。
<img src="https://static001.geekbang.org/resource/image/71/c9/71f41d46cc5c0405f4d4dc944b4350c9.jpg" alt="" />
虽然业务分库能够分散存储和访问压力,但同时也带来了新的问题,接下来我进行详细分析。
1.join操作问题
业务分库后原本在同一个数据库中的表分散到不同数据库中导致无法使用SQL的join查询。
例如“查询购买了化妆品的用户中女性用户的列表”这个功能虽然订单数据中有用户的ID信息但是用户的性别数据在用户数据库中如果在同一个库中简单的join查询就能完成但现在数据分散在两个不同的数据库中无法做join查询只能采取先从订单数据库中查询购买了化妆品的用户ID列表然后再到用户数据库中查询这批用户ID中的女性用户列表这样实现就比简单的join查询要复杂一些。
2.事务问题
原本在同一个数据库中不同的表可以在同一个事务中修改业务分库后表分散到不同的数据库中无法通过事务统一修改。虽然数据库厂商提供了一些分布式事务的解决方案例如MySQL的XA但性能实在太低与高性能存储的目标是相违背的。
例如,用户下订单的时候需要扣商品库存,如果订单数据和商品数据在同一个数据库中,我们可以使用事务来保证扣减商品库存和生成订单的操作要么都成功要么都失败,但分库后就无法使用数据库事务了,需要业务程序自己来模拟实现事务的功能。例如,先扣商品库存,扣成功后生成订单,如果因为订单数据库异常导致生成订单失败,业务程序又需要将商品库存加上;而如果因为业务程序自己异常导致生成订单失败,则商品库存就无法恢复了,需要人工通过日志等方式来手工修复库存异常。
3.成本问题
业务分库同时也带来了成本的代价本来1台服务器搞定的事情现在要3台如果考虑备份那就是2台变成了6台。
基于上述原因,对于小公司初创业务,并不建议一开始就这样拆分,主要有几个原因:
<li>
初创业务存在很大的不确定性,业务不一定能发展起来,业务开始的时候并没有真正的存储和访问压力,业务分库并不能为业务带来价值。
</li>
<li>
业务分库后表之间的join查询、数据库事务无法简单实现了。
</li>
<li>
业务分库后,因为不同的数据要读写不同的数据库,代码中需要增加根据数据类型映射到不同数据库的逻辑,增加了工作量。而业务初创期间最重要的是快速实现、快速验证,业务分库会拖慢业务节奏。
</li>
有的架构师可能会想:如果业务真的发展很快,岂不是很快就又要进行业务分库了?那为何不一开始就设计好呢?
其实这个问题很好回答,按照我前面提到的“架构设计三原则”,简单分析一下。
首先这里的“如果”事实上发生的概率比较低做10个业务有1个业务能活下去就很不错了更何况快速发展和中彩票的概率差不多。如果我们每个业务上来就按照淘宝、微信的规模去做架构设计不但会累死自己还会害死业务。
其次,如果业务真的发展很快,后面进行业务分库也不迟。因为业务发展好,相应的资源投入就会加大,可以投入更多的人和更多的钱,那业务分库带来的代码和业务复杂的问题就可以通过增加人来解决,成本问题也可以通过增加资金来解决。
第三单台数据库服务器的性能其实也没有想象的那么弱一般来说单台数据库服务器能够支撑10万用户量量级的业务初创业务从0发展到10万级用户并不是想象得那么快。
而对于业界成熟的大公司来说,由于已经有了业务分库的成熟解决方案,并且即使是尝试性的新业务,用户规模也是海量的,**这与前面提到的初创业务的小公司有本质区别**,因此最好在业务开始设计时就考虑业务分库。例如,在淘宝上做一个新的业务,由于已经有成熟的数据库解决方案,用户量也很大,需要在一开始就设计业务分库甚至接下来介绍的分表方案。
## 分表
将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。
单表数据拆分有两种方式:**垂直分表**和**水平分表**。示意图如下:
<img src="https://static001.geekbang.org/resource/image/13/40/136bc2f01919edcb8271df6f7e71af40.jpg" alt="" />
为了形象地理解垂直拆分和水平拆分的区别,可以想象你手里拿着一把刀,面对一个蛋糕切一刀:
<li>
从上往下切就是垂直切分因为刀的运行轨迹与蛋糕是垂直的这样可以把蛋糕切成高度相等面积可以相等也可以不相等的两部分对应到表的切分就是表记录数相同但包含不同的列。例如示意图中的垂直切分会把表切分为两个表一个表包含ID、name、age、sex列另外一个表包含ID、nickname、description列。
</li>
<li>
从左往右切就是水平切分因为刀的运行轨迹与蛋糕是平行的这样可以把蛋糕切成面积相等高度可以相等也可以不相等的两部分对应到表的切分就是表的列相同但包含不同的行数据。例如示意图中的水平切分会把表分为两个表两个表都包含ID、name、age、sex、nickname、description列但是一个表包含的是ID从1到999999的行数据另一个表包含的是ID从1000000到9999999的行数据。
</li>
上面这个示例比较简单,只考虑了一次切分的情况,实际架构设计过程中并不局限切分的次数,可以切两次,也可以切很多次,就像切蛋糕一样,可以切很多刀。
单表进行切分后,是否要将切分后的多个表分散在不同的数据库服务器中,可以根据实际的切分效果来确定,并不强制要求单表切分为多表后一定要分散到不同数据库中。原因在于单表切分为多表后,新的表即使在同一个数据库服务器中,也可能带来可观的性能提升,如果性能能够满足业务要求,是可以不拆分到多台数据库服务器的,毕竟我们在上面业务分库的内容看到业务分库也会引入很多复杂性的问题;如果单表拆分为多表后,单台服务器依然无法满足性能要求,那就不得不再次进行业务分库的设计了。
分表能够有效地分散存储压力和带来性能提升,但和分库一样,也会引入各种复杂性。
1.垂直分表
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如前面示意图中的nickname和description字段假设我们是一个婚恋网站用户在筛选其他用户的时候主要是用age和sex两个字段进行查询而nickname和description两个字段主要用于展示一般不会在业务查询中用到。description本身又比较长因此我们可以将这两个字段独立到另外一张表中这样在查询age和sex时就能带来一定的性能提升。
垂直分表引入的复杂性主要体现在表操作的数量要增加。例如原来只要一次查询就可以获取name、age、sex、nickname、description现在需要两次查询一次查询获取name、age、sex另外一次查询获取nickname、description。
不过相比接下来要讲的水平分表,这个复杂性就是小巫见大巫了。
2.水平分表
水平分表适合表行数特别大的表有的公司要求单表行数超过5000万就必须进行分表这个数字可以作为参考但并不是绝对标准关键还是要看表的访问性能。对于一些比较复杂的表可能超过1000万就要分表了而对于一些简单的表即使存储数据超过1亿行也可以不分表。但不管怎样当看到表的数据量达到千万级别时作为架构师就要警觉起来因为这很可能是架构的性能瓶颈或者隐患。
水平分表相比垂直分表,会引入更多的复杂性,主要表现在下面几个方面:
- 路由
水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性。
常见的路由算法有:
**范围路由:**选取有序的数据列例如整形、时间戳等作为路由的条件不同分段分散到不同的数据库表中。以最常见的用户ID为例路由算法可以按照1000000的范围大小进行分段1 ~ 999999放到数据库1的表中1000000 ~ 1999999放到数据库2的表中以此类推。
范围路由设计的复杂点主要体现在分段大小的选取上分段太小会导致切分后子表数量过多增加维护复杂度分段太大可能会导致单表依然存在性能问题一般建议分段大小在100万至2000万之间具体需要根据业务选取合适的分段大小。
范围路由的优点是可以随着数据的增加平滑地扩充新的表。例如现在的用户是100万如果增加到1000万只需要增加新的表就可以了原有的数据不需要动。
范围路由的一个比较隐含的缺点是分布不均匀假如按照1000万来进行分表有可能某个分段实际存储的数据量只有1000条而另外一个分段实际存储的数据量有900万条。
**Hash路由**选取某个列或者某几个列组合也可以的值进行Hash运算然后根据Hash结果分散到不同的数据库表中。同样以用户ID为例假如我们一开始就规划了10个数据库表路由算法可以简单地用user_id % 10的值来表示数据所属的数据库表编号ID为985的用户放到编号为5的子表中ID为10086的用户放到编号为6的字表中。
Hash路由设计的复杂点主要体现在初始表数量的选取上表数量太多维护比较麻烦表数量太少又可能导致单表性能存在问题。而用了Hash路由后增加子表数量是非常麻烦的所有数据都要重分布。
Hash路由的优缺点和范围路由基本相反Hash路由的优点是表分布比较均匀缺点是扩充新的表很麻烦所有数据都要重分布。
**配置路由:**配置路由就是路由表用一张独立的表来记录路由信息。同样以用户ID为例我们新增一张user_router表这个表包含user_id和table_id两列根据user_id就可以查询对应的table_id。
配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。
配置路由的缺点就是必须多查询一次,会影响整体性能;而且路由表本身如果太大(例如,几亿条数据),性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则又面临一个死循环式的路由算法选择问题。
- join操作
水平分表后数据分散在多个表中如果需要与其他表进行join查询需要在业务代码或者数据库中间件中进行多次join查询然后将结果合并。
- count()操作
水平分表后虽然物理上数据分散到多个表中但某些业务逻辑上还是会将这些表当作一个表来处理。例如获取记录总数用于分页或者展示水平分表前用一个count()就能完成的操作,在分表后就没那么简单了。常见的处理方式有下面两种:
**count()相加:**具体做法是在业务代码或者数据库中间件中对每个表进行count()操作然后将结果相加。这种方式实现简单缺点就是性能比较低。例如水平分表后切分为20张表则要进行20次count(*)操作,如果串行的话,可能需要几秒钟才能得到结果。
**记录数表:**具体做法是新建一张表假如表名为“记录数表”包含table_name、row_count两个字段每次插入或者删除子表数据成功后都更新“记录数表”。
这种方式获取表记录数的性能要大大优于count()相加的方式,因为只需要一次简单查询就可以获取数据。缺点是复杂度增加不少,对子表的操作要同步操作“记录数表”,如果有一个业务逻辑遗漏了,数据就会不一致;且针对“记录数表”的操作和针对子表的操作无法放在同一事务中进行处理,异常的情况下会出现操作子表成功了而操作记录数表失败,同样会导致数据不一致。
此外记录数表的方式也增加了数据库的写压力因为每次针对子表的insert和delete操作都要update记录数表所以对于一些不要求记录数实时保持精确的业务也可以通过后台定时更新记录数表。定时更新实际上就是“count()相加”和“记录数表”的结合即定时通过count()相加计算表的记录数,然后更新记录数表中的数据。
- order by操作
水平分表后,数据分散到多个子表中,排序操作无法在数据库中完成,只能由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序。
## 实现方法
和数据库读写分离类似分库分表具体的实现方式也是“程序代码封装”和“中间件封装”但实现会更复杂。读写分离实现时只要识别SQL操作是读操作还是写操作通过简单的判断SELECT、UPDATE、INSERT、DELETE几个关键字就可以做到而分库分表的实现除了要判断操作类型外还要判断SQL中具体需要操作的表、操作函数例如count函数)、order by、group by操作等然后再根据不同的操作进行不同的处理。例如order by操作需要先从多个库查询到各个库的数据然后再重新order by才能得到最终的结果。
## 小结
今天我为你讲了高性能数据库集群的分库分表架构,包括业务分库产生的问题和分表的两种方式及其带来的复杂度,希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧,你认为什么时候引入分库分表是合适的?是数据库性能不够的时候就开始分库分表么?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,263 @@
<audio id="audio" title="16 | 高性能NoSQL" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a7/e1/a7b968a4e929b09e8bc971f46562f3e1.mp3"></audio>
关系数据库经过几十年的发展后已经非常成熟强大的SQL功能和ACID的属性使得关系数据库广泛应用于各式各样的系统中但这并不意味着关系数据库是完美的关系数据库存在如下缺点。
- 关系数据库存储的是行记录,无法存储数据结构
以微博的关注关系为例“我关注的人”是一个用户ID列表使用关系数据库存储只能将列表拆成多行然后再查询出来组装无法直接存储一个列表。
- 关系数据库的schema扩展很不方便
关系数据库的表结构schema是强约束操作不存在的列会报错业务变化时扩充列也比较麻烦需要执行DDLdata definition language如CREATE、ALTER、DROP等语句修改而且修改时可能会长时间锁表例如MySQL可能将表锁住1个小时
- 关系数据库在大数据场景下I/O较高
如果对一些大量数据的表进行统计之类的运算关系数据库的I/O会很高因为即使只针对其中某一列进行运算关系数据库也会将整行数据从存储设备读入内存。
- 关系数据库的全文搜索功能比较弱
关系数据库的全文搜索只能使用like进行整表扫描匹配性能非常低在互联网这种搜索复杂的场景下无法满足业务要求。
针对上述问题分别诞生了不同的NoSQL解决方案这些方案与关系数据库相比在某些应用场景下表现更好。但世上没有免费的午餐NoSQL方案带来的优势本质上是牺牲ACID中的某个或者某几个特性**因此我们不能盲目地迷信NoSQL是银弹而应该将NoSQL作为SQL的一个有力补充**NoSQL != No SQL而是NoSQL = Not Only SQL。
常见的NoSQL方案分为4类。
<li>
K-V存储解决关系数据库无法存储数据结构的问题以Redis为代表。
</li>
<li>
文档数据库解决关系数据库强schema约束的问题以MongoDB为代表。
</li>
<li>
列式数据库解决关系数据库大数据场景下的I/O问题以HBase为代表。
</li>
<li>
全文搜索引擎解决关系数据库的全文搜索性能问题以Elasticsearch为代表。
</li>
今天我来介绍一下各种高性能NoSQL方案的典型特征和应用场景。
## K-V存储
K-V存储的全称是Key-Value存储其中Key是数据的标识和关系数据库中的主键含义一样Value就是具体的数据。
Redis是K-V存储的典型代表它是一款开源基于BSD许可的高性能K-V缓存和存储系统。Redis的Value是具体的数据结构包括string、hash、list、set、sorted set、bitmap和hyperloglog所以常常被称为数据结构服务器。
以List数据结构为例Redis提供了下面这些典型的操作更多请参考链接[http://redis.cn/commands.html#list](http://redis.cn/commands.html#list)
<li>
LPOP key从队列的左边出队一个元素。
</li>
<li>
LINDEX key index获取一个元素通过其索引列表。
</li>
<li>
LLEN key获得队列List的长度。
</li>
<li>
RPOP key从队列的右边出队一个元素。
</li>
以上这些功能如果用关系数据库来实现就会变得很复杂。例如LPOP操作是移除并返回 key对应的list的第一个元素。如果用关系数据库来存储为了达到同样目的需要进行下面的操作
<li>
每条数据除了数据编号例如行ID还要有位置编号否则没有办法判断哪条数据是第一条。注意这里不能用行ID作为位置编号因为我们会往列表头部插入数据。
</li>
<li>
查询出第一条数据。
</li>
<li>
删除第一条数据。
</li>
<li>
更新从第二条开始的所有数据的位置编号。
</li>
可以看出关系数据库的实现很麻烦而且需要进行多次SQL操作性能很低。
Redis的缺点主要体现在并不支持完整的ACID事务Redis虽然提供事务功能但Redis的事务和关系数据库的事务不可同日而语Redis的事务只能保证隔离性和一致性I和C无法保证原子性和持久性A和D
虽然Redis并没有严格遵循ACID原则但实际上大部分业务也不需要严格遵循ACID原则。以上面的微博关注操作为例即使系统没有将A加入B的粉丝列表其实业务影响也非常小因此我们在设计方案时需要根据业务特性和要求来确定是否可以用Redis而不能因为Redis不遵循ACID原则就直接放弃。
## 文档数据库
为了解决关系数据库schema带来的问题文档数据库应运而生。文档数据库最大的特点就是no-schema可以存储和读取任意的数据。目前绝大部分文档数据库存储的数据格式是JSON或者BSON因为JSON数据是自描述的无须在使用前定义字段读取一个JSON中不存在的字段也不会导致SQL那样的语法错误。
文档数据库的no-schema特性给业务开发带来了几个明显的优势。
1.新增字段简单
业务上增加新的字段无须再像关系数据库一样要先执行DDL语句修改表结构程序代码直接读写即可。
2.历史数据不会出错
对于历史数据,即使没有新增的字段,也不会导致错误,只会返回空值,此时代码进行兼容处理即可。
3.可以很容易存储复杂数据
JSON是一种强大的描述语言能够描述复杂的数据结构。例如我们设计一个用户管理系统用户的信息有ID、姓名、性别、爱好、邮箱、地址、学历信息。其中爱好是列表因为可以有多个爱好地址是一个结构包括省市区楼盘地址学历包括学校、专业、入学毕业年份信息等。如果我们用关系数据库来存储需要设计多张表包括基本信息ID、姓名、性别、邮箱、爱好ID、爱好、地址省、市、区、详细地址、学历入学时间、毕业时间、学校名称、专业而使用文档数据库一个JSON就可以全部描述。
```
{
&quot;id&quot;: 10000,
&quot;name&quot;: &quot;James&quot;,
&quot;sex&quot;: &quot;male&quot;,
&quot;hobbies&quot;: [
&quot;football&quot;,
&quot;playing&quot;,
&quot;singing&quot;
],
&quot;email&quot;: &quot;user@google.com&quot;,
&quot;address&quot;: {
&quot;province&quot;: &quot;GuangDong&quot;,
&quot;city&quot;: &quot;GuangZhou&quot;,
&quot;district&quot;: &quot;Tianhe&quot;,
&quot;detail&quot;: &quot;PingYun Road 163&quot;
},
&quot;education&quot;: [
{
&quot;begin&quot;: &quot;2000-09-01&quot;,
&quot;end&quot;: &quot;2004-07-01&quot;,
&quot;school&quot;: &quot;UESTC&quot;,
&quot;major&quot;: &quot;Computer Science &amp; Technology&quot;
},
{
&quot;begin&quot;: &quot;2004-09-01&quot;,
&quot;end&quot;: &quot;2007-07-01&quot;,
&quot;school&quot;: &quot;SCUT&quot;,
&quot;major&quot;: &quot;Computer Science &amp; Technology&quot;
}
]
}
```
通过这个样例我们看到使用JSON来描述数据比使用关系型数据库表来描述数据方便和容易得多而且更加容易理解。
文档数据库的这个特点,特别适合电商和游戏这类的业务场景。以电商为例,不同商品的属性差异很大。例如,冰箱的属性和笔记本电脑的属性差异非常大,如下图所示。
<img src="https://static001.geekbang.org/resource/image/81/6e/81c57d42e269521ba4b671cac345066e.jpg" alt="" /><img src="https://static001.geekbang.org/resource/image/83/e7/83614dfae6106ae3d08yy5a8b3bda5e7.jpg" alt="" />
即使是同类商品也有不同的属性。例如LCD和LED显示器两者有不同的参数指标。这种业务场景如果使用关系数据库来存储数据就会很麻烦而使用文档数据库会简单、方便许多扩展新的属性也更加容易。
文档数据库no-schema的特性带来的这些优势也是有代价的最主要的代价就是不支持事务。例如使用MongoDB来存储商品库存系统创建订单的时候首先需要减扣库存然后再创建订单。这是一个事务操作用关系数据库来实现就很简单但如果用MongoDB来实现就无法做到事务性。异常情况下可能出现库存被扣减了但订单没有创建的情况。因此某些对事务要求严格的业务场景是不能使用文档数据库的。
文档数据库另外一个缺点就是无法实现关系数据库的join操作。例如我们有一个用户信息表和一个订单表订单表中有买家用户id。如果要查询“购买了苹果笔记本用户中的女性用户”用关系数据库来实现一个简单的join操作就搞定了而用文档数据库是无法进行join查询的需要查两次一次查询订单表中购买了苹果笔记本的用户然后再查询这些用户哪些是女性用户。
## 列式数据库
顾名思义,列式数据库就是按照列来存储数据的数据库,与之对应的传统关系数据库被称为“行式数据库”,因为关系数据库是按照行来存储数据的。
关系数据库按照行式来存储数据,主要有以下几个优势:
<li>
业务同时读取多个列时效率高,因为这些列都是按行存储在一起的,一次磁盘操作就能够把一行数据中的各个列都读取到内存中。
</li>
<li>
能够一次性完成对一行中的多个列的写操作,保证了针对行数据写操作的原子性和一致性;否则如果采用列存储,可能会出现某次写操作,有的列成功了,有的列失败了,导致数据不一致。
</li>
我们可以看到行式存储的优势是在特定的业务场景下才能体现如果不存在这样的业务场景那么行式存储的优势也将不复存在甚至成为劣势典型的场景就是海量数据进行统计。例如计算某个城市体重超重的人员数据实际上只需要读取每个人的体重这一列并进行统计即可而行式存储即使最终只使用一列也会将所有行数据都读取出来。如果单行用户信息有1KB其中体重只有4个字节行式存储还是会将整行1KB数据全部读取到内存中这是明显的浪费。而如果采用列式存储每个用户只需要读取4字节的体重数据即可I/O将大大减少。
除了节省I/O列式存储还具备更高的存储压缩比能够节省更多的存储空间。普通的行式数据库一般压缩率在3:1到5:1左右而列式数据库的压缩率一般在8:1到30:1左右因为单个列的数据相似度相比行来说更高能够达到更高的压缩率。
同样,如果场景发生变化,列式存储的优势又会变成劣势。典型的场景是需要频繁地更新多个列。因为列式存储将不同列存储在磁盘上不连续的空间,导致更新多个列时磁盘是随机写操作;而行式存储时同一行多个列都存储在连续的空间,一次磁盘写操作就可以完成,列式存储的随机写效率要远远低于行式存储的写效率。此外,列式存储高压缩率在更新场景下也会成为劣势,因为更新时需要将存储数据解压后更新,然后再压缩,最后写入磁盘。
基于上述列式存储的优缺点,一般将列式存储应用在离线的大数据分析和统计场景中,因为这种场景主要是针对部分列单列进行操作,且数据写入后就无须再更新删除。
## 全文搜索引擎
传统的关系型数据库通过索引来达到快速查询的目的,但是在全文搜索的业务场景下,索引也无能为力,主要体现在:
<li>
全文搜索的条件可以随意排列组合,如果通过索引来满足,则索引的数量会非常多。
</li>
<li>
全文搜索的模糊匹配方式索引无法满足只能用like查询而like查询是整表扫描效率非常低。
</li>
我举一个具体的例子来看看关系型数据库为何无法满足全文搜索的要求。假设我们做一个婚恋网站,其主要目的是帮助程序员找朋友,但模式与传统婚恋网站不同,是“程序员发布自己的信息,用户来搜索程序员”。程序员的信息表设计如下:
<img src="https://static001.geekbang.org/resource/image/d9/39/d93121cecabc2182edb68bebfc467f39.jpg" alt="" />
我们来看一下这个简单业务的搜索场景:
- 美女1听说PHP是世界上最好的语言那么PHP的程序员肯定是钱最多的而且我妈一定要我找一个上海的。
美女1的搜索条件是“性别 + PHP + 上海”其中“PHP”要用模糊匹配查询“语言”列“上海”要查询“地点”列如果用索引支撑则需要建立“地点”这个索引。
- 美女2我好崇拜这些技术哥哥啊要是能找一个鹅厂技术哥哥陪我旅游就更好了。
美女2的搜索条件是“性别 + 鹅厂 + 旅游”,其中“旅游”要用模糊匹配查询“爱好”列,“鹅厂”需要查询“单位”列,如果要用索引支撑,则需要建立“单位”索引。
- 美女3我是一个“女程序员”想在北京找一个猫厂的Java技术专家。
美女3的搜索条件是“性别 + 猫厂 + 北京 + Java + 技术专家”,其中“猫厂 + 北京”可以通过索引来查询但“Java”“技术专家”都只能通过模糊匹配来查询。
- 帅哥4程序员妹子有没有漂亮的呢试试看看。
帅哥4的搜索条件是“性别 + 美丽 + 美女”,只能通过模糊匹配搜索“自我介绍”列。
以上只是简单举个例子,实际上搜索条件是无法列举完全的,各种排列组合非常多,通过这个简单的样例我们就可以看出关系数据库在支撑全文搜索时的不足。
1.全文搜索基本原理
全文搜索引擎的技术原理被称为“倒排索引”Inverted index也常被称为反向索引、置入档案或反向档案是一种索引方法其基本原理是建立单词到文档的索引。之所以被称为“倒排”索引是和“正排“索引相对的“正排索引”的基本原理是建立文档到单词的索引。我们通过一个简单的样例来说明这两种索引的差异。
假设我们有一个技术文章的网站,里面收集了各种技术文章,用户可以在网站浏览或者搜索文章。
正排索引示例:
<img src="https://static001.geekbang.org/resource/image/5f/87/5fe73007957ecfcca009fd81f673df87.jpg" alt="" title="注:文章内容仅为示范,文章内 [br] 实际上存储的是几千字的内容" />
正排索引适用于根据文档名称来查询文档内容。例如,用户在网站上单击了“面向对象葵花宝典是什么”,网站根据文章标题查询文章的内容展示给用户。
倒排索引示例:
<img src="https://static001.geekbang.org/resource/image/ea/f6/ea5dc300ec9c556dc13790b69f4d60f6.jpg" alt="" title="注:表格仅为示范,不是完整的倒排索引表格,[br] 实际上的倒排索引有成千上万行,因为每个单词就是一个索引" />
倒排索引适用于根据关键词来查询文档内容。例如,用户只是想看“设计”相关的文章,网站需要将文章内容中包含“设计”一词的文章都搜索出来展示给用户。
2.全文搜索的使用方式
全文搜索引擎的索引对象是单词和文档,而关系数据库的索引对象是键和行,两者的术语差异很大,不能简单地等同起来。因此,为了让全文搜索引擎支持关系型数据的全文搜索,需要做一些转换操作,即将关系型数据转换为文档数据。
目前常用的转换方式是将关系型数据按照对象的形式转换为JSON文档然后将JSON文档输入全文搜索引擎进行索引。我同样以程序员的基本信息表为例看看如何转换。
将前面样例中的程序员表格转换为JSON文档可以得到3个程序员信息相关的文档我以程序员1为例
```
{
&quot;id&quot;: 1,
&quot;姓名&quot;: &quot;多隆&quot;,
&quot;性别&quot;: &quot;男&quot;,
&quot;地点&quot;: &quot;北京&quot;,
&quot;单位&quot;: &quot;猫厂&quot;,
&quot;爱好&quot;: &quot;写代码,旅游,马拉松&quot;,
&quot;语言&quot;: &quot;Java、C++、PHP&quot;,
&quot;自我介绍&quot;: &quot;技术专家,简单,为人热情&quot;
}
```
全文搜索引擎能够基于JSON文档建立全文索引然后快速进行全文搜索。以Elasticsearch为例其索引基本原理如下
>
Elastcisearch是分布式的文档存储方式。它能存储和检索复杂的数据结构——序列化成为JSON文档——以实时的方式。
>
在Elasticsearch中每个字段的所有数据都是默认被索引的。即每个字段都有为了快速检索设置的专用倒排索引。而且不像其他多数的数据库它能在相同的查询中使用所有倒排索引并以惊人的速度返回结果。
[https://www.elastic.co/guide/cn/elasticsearch/guide/current/data-in-data-out.html](https://www.elastic.co/guide/cn/elasticsearch/guide/current/data-in-data-out.html)
## 小结
今天我为你讲了为了弥补关系型数据库缺陷而产生的NoSQL技术希望对你有所帮助。
这就是今天的全部内容留一道思考题给你吧因为NoSQL的方案功能都很强大有人认为NoSQL = No SQL架构设计的时候无需再使用关系数据库对此你怎么看
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,103 @@
<audio id="audio" title="17 | 高性能缓存架构" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f7/bf/f7117309a31732185fcc58b850f014bf.mp3"></audio>
虽然我们可以通过各种手段来提升存储系统的性能,但在某些复杂的业务场景下,单纯依靠存储系统的性能提升不够的,典型的场景有:
- 需要经过复杂运算后得出的数据,存储系统无能为力
例如一个论坛需要在首页展示当前有多少用户同时在线如果使用MySQL来存储当前用户状态则每次获取这个总数都要“count(*)”大量数据这样的操作无论怎么优化MySQL性能都不会太高。如果要实时展示用户同时在线数则MySQL性能无法支撑。
- 读多写少的数据,存储系统有心无力
绝大部分在线业务都是读多写少。例如微博、淘宝、微信这类互联网业务读业务占了整体业务量的90%以上。以微博为例一个明星发一条微博可能几千万人来浏览。如果使用MySQL来存储微博用户写微博只有一条insert语句但每个用户浏览时都要select一次即使有索引几千万条select语句对MySQL数据库的压力也会非常大。
缓存就是为了弥补存储系统在这些复杂业务场景下的不足,其基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。
缓存能够带来性能的大幅提升以Memcache为例单台Memcache服务器简单的key-value查询能够达到TPS 50000以上其基本的架构是
<img src="https://static001.geekbang.org/resource/image/c7/15/c70fdcaab49fe730380d2207017c4215.jpg" alt="" />
缓存虽然能够大大减轻存储系统的压力,但同时也给架构引入了更多复杂性。架构设计时如果没有针对缓存的复杂性进行处理,某些场景下甚至会导致整个系统崩溃。今天,我来逐一分析缓存的架构设计要点。
## 缓存穿透
**缓存穿透**是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。通常情况下有两种情况:
1.存储数据不存在
第一种情况是被访问的数据确实不存在。一般情况下,如果存储系统中没有某个数据,则不会在缓存中存储相应的数据,这样就导致用户查询的时候,在缓存中找不到对应的数据,每次都要去存储系统中再查询一遍,然后返回数据不存在。缓存在这个场景中并没有起到分担存储系统访问压力的作用。
通常情况下,业务上读取不存在的数据的请求量并不会太大,但如果出现一些异常情况,例如被黑客攻击,故意大量访问某些读取不存在数据的业务,有可能会将存储系统拖垮。
这种情况的解决办法比较简单,如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统。
2.缓存数据生成耗费大量时间或者资源
第二种情况是存储系统中存在数据,但生成缓存数据需要耗费较长时间或者耗费大量资源。如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。
典型的就是电商的商品分页,假设我们在某个电商平台上选择“手机”这个类别查看,由于数据巨大,不能把所有数据都缓存起来,只能按照分页来进行缓存,由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。通常情况下这样实现是基本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题。
具体的场景有:
<li>
分页缓存的有效期设置为1天因为设置太长时间的话缓存不能反应真实的数据。
</li>
<li>
通常情况下用户不会从第1页到最后1页全部看完一般用户访问集中在前10页因此第10页以后的缓存过期失效的可能性很大。
</li>
<li>
竞争对手每周来爬取数据爬虫会将所有分类的所有数据全部遍历从第1页到最后1页全部都会读取此时很多分页缓存可能都失效了。
</li>
<li>
由于很多分页都没有缓存数据从数据库中生成缓存数据又非常耗费性能order by limit操作因此爬虫会将整个数据库全部拖慢。
</li>
这种情况并没有太好的解决方案因为爬虫会遍历所有的数据而且什么时候来爬取也是不确定的可能是每天都来也可能是每周也可能是一个月来一次我们也不可能为了应对爬虫而将所有数据永久缓存。通常的应对方案要么就是识别爬虫然后禁止访问但这可能会影响SEO和推广要么就是做好监控发现问题后及时处理因为爬虫不是攻击不会进行暴力破坏对系统的影响是逐步的监控发现问题后有时间进行处理。
## 缓存雪崩
**缓存雪崩**是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。由于旧的缓存已经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。
缓存雪崩的常见解决方法有两种:**更新锁机制**和**后台更新机制**。
1.更新锁
对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
对于采用分布式集群的业务系统由于存在几十上百台服务器即使单台服务器只有一个线程更新缓存但几十上百台服务器一起算下来也会有几十上百个线程同时来更新缓存同样存在雪崩的问题。因此分布式集群的业务系统要实现更新锁机制需要用到分布式锁如ZooKeeper。
2.后台更新
由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。
后台定时机制需要考虑一种特殊的场景,当缓存系统内存不够时,会“踢掉”一些缓存数据,从缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了。解决的方式有两种:
<li>
后台线程除了定时更新缓存还要频繁地去读取缓存例如1秒或者100毫秒读取一次如果发现缓存被“踢了”就立刻更新缓存这种方式实现简单但读取时间间隔不能设置太长因为如果缓存被踢了缓存读取间隔时间又太长这段时间内业务访问都拿不到真正的数据而是一个空的缓存值用户体验一般。
</li>
<li>
业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。可能会出现多个业务线程都发送了缓存更新消息,但其实对后台线程没有影响,后台线程收到消息后更新缓存前可以判断缓存是否存在,存在就不执行更新操作。这种方式实现依赖消息队列,复杂度会高一些,但缓存更新更及时,用户体验更好。
</li>
后台更新既适应单机多线程的场景,也适合分布式集群的场景,相比更新锁机制要简单一些。
后台更新机制还适合业务刚上线的时候进行缓存预热。缓存预热指系统上线后,将相关的缓存数据直接加载到缓存系统,而不是等待用户访问才来触发缓存加载。
## 缓存热点
虽然缓存系统本身的性能比较高,但对于一些特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。例如,某明星微博发布“我们”来宣告恋爱了,短时间内上千万的用户都会来围观。
**缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力**。以微博为例对于粉丝数超过100万的明星每条微博都可以生成100份缓存缓存的数据是一样的通过在缓存的key里面加上编号进行区分每次读缓存时都随机读取其中某份缓存。
缓存副本设计有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。
## 实现方式
由于缓存的各种访问策略和存储的访问策略是相关的,因此上面的各种缓存设计方案通常情况下都是集成在存储访问方案中,可以采用“程序代码实现”的中间层方式,也可以采用独立的中间件来实现。
## 小结
今天我为你讲了高性能架构设计中缓存设计需要注意的几个关键点,这些关键点本身在技术上都不复杂,但可能对业务产生很大的影响,轻则系统响应变慢,重则全站宕机,架构师在设计架构的时候要特别注意这些细节,希望这些设计关键点和技术方案对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧,分享一下你所在的业务发生过哪些因为缓存导致的线上问题?采取了什么样的解决方案?效果如何?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,145 @@
<audio id="audio" title="18 | 单服务器高性能模式PPC与TPC" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b3/50/b38f5619ecd3bcb6c0fe8c49d557f350.mp3"></audio>
高性能是每个程序员的追求无论我们是做一个系统还是写一行代码都希望能够达到高性能的效果而高性能又是最复杂的一环磁盘、操作系统、CPU、内存、缓存、网络、编程语言、架构等每个都有可能影响系统达到高性能一行不恰当的debug日志就可能将服务器的性能从TPS 30000降低到8000一个tcp_nodelay参数就可能将响应时间从2毫秒延长到40毫秒。因此要做到高性能计算是一件很复杂很有挑战的事情软件系统开发过程中的不同阶段都关系着高性能最终是否能够实现。
站在架构师的角度,当然需要特别关注高性能架构的设计。高性能架构设计主要集中在两方面:
<li>
尽量提升单服务器的性能,将单服务器的性能发挥到极致。
</li>
<li>
如果单服务器无法支撑性能,设计服务器集群方案。
</li>
除了以上两点,最终系统能否实现高性能,还和具体的实现及编码相关。但架构设计是高性能的基础,如果架构设计没有做到高性能,则后面的具体实现和编码能提升的空间是有限的。形象地说,架构设计决定了系统性能的上限,实现细节决定了系统性能的下限。
单服务器高性能的关键之一就是**服务器采取的并发模型**,并发模型有如下两个关键设计点:
<li>
服务器如何管理连接。
</li>
<li>
服务器如何处理请求。
</li>
以上两个设计点最终都和操作系统的I/O模型及进程模型相关。
<li>
I/O模型阻塞、非阻塞、同步、异步。
</li>
<li>
进程模型:单进程、多进程、多线程。
</li>
在下面详细介绍并发模型时会用到上面这些基础的知识点所以我建议你先检测一下对这些基础知识的掌握情况更多内容你可以参考《UNIX网络编程》三卷本。今天我们先来看看单服务器高性能模式PPC与TPC。
## PPC
PPC是Process Per Connection的缩写其含义是指每次有新的连接就新建一个进程去专门处理这个连接的请求这是传统的UNIX网络服务器所采用的模型。基本的流程图是
<img src="https://static001.geekbang.org/resource/image/53/ba/53b17d63a31c6b551d3a039a2568daba.jpg" alt="" />
<li>
父进程接受连接图中accept
</li>
<li>
父进程“fork”子进程图中fork
</li>
<li>
子进程处理连接的读写请求图中子进程read、业务处理、write
</li>
<li>
子进程关闭连接图中子进程中的close
</li>
注意图中有一个小细节父进程“fork”子进程后直接调用了close看起来好像是关闭了连接其实只是将连接的文件描述符引用计数减一真正的关闭连接是等子进程也调用close后连接对应的文件描述符引用计数变为0后操作系统才会真正关闭连接更多细节请参考《UNIX网络编程卷一》。
PPC模式实现简单比较适合服务器的连接数没那么多的情况例如数据库服务器。对于普通的业务服务器在互联网兴起之前由于服务器的访问量和并发量并没有那么大这种模式其实运作得也挺好世界上第一个web服务器CERN httpd就采用了这种模式具体你可以参考[https://en.wikipedia.org/wiki/CERN_httpd](https://en.wikipedia.org/wiki/CERN_httpd))。互联网兴起后,服务器的并发和访问量从几十剧增到成千上万,这种模式的弊端就凸显出来了,主要体现在这几个方面:
<li>
fork代价高站在操作系统的角度创建一个进程的代价是很高的需要分配很多内核资源需要将内存映像从父进程复制到子进程。即使现在的操作系统在复制内存映像时用到了Copy on Write写时复制技术总体来说创建进程的代价还是很大的。
</li>
<li>
父子进程通信复杂父进程“fork”子进程时文件描述符可以通过内存映像复制从父进程传到子进程但“fork”完成后父子进程通信就比较麻烦了需要采用IPCInterprocess Communication之类的进程通信方案。例如子进程需要在close之前告诉父进程自己处理了多少个请求以支撑父进程进行全局的统计那么子进程和父进程必须采用IPC方案来传递信息。
</li>
<li>
支持的并发连接数量有限如果每个连接存活时间比较长而且新的连接又源源不断的进来则进程数量会越来越多操作系统进程调度和切换的频率也越来越高系统的压力也会越来越大。因此一般情况下PPC方案能处理的并发连接数量最大也就几百。
</li>
## prefork
PPC模式中当连接进来时才fork新进程来处理连接请求由于fork进程代价高用户访问时可能感觉比较慢prefork模式的出现就是为了解决这个问题。
顾名思义prefork就是提前创建进程pre-fork。系统在启动的时候就预先创建好进程然后才开始接受用户的请求当有新的连接进来的时候就可以省去fork进程的操作让用户访问更快、体验更好。prefork的基本示意图是
<img src="https://static001.geekbang.org/resource/image/3c/2f/3c931b04d3372ebcebe4f2c2cf59d42f.jpg" alt="" />
prefork的实现关键就是多个子进程都accept同一个socket当有新的连接进入时操作系统保证只有一个进程能最后accept成功。但这里也存在一个小小的问题“惊群”现象就是指虽然只有一个子进程能accept成功但所有阻塞在accept上的子进程都会被唤醒这样就导致了不必要的进程调度和上下文切换了。幸运的是操作系统可以解决这个问题例如Linux 2.6版本后内核已经解决了accept惊群问题。
prefork模式和PPC一样还是存在父子进程通信复杂、支持的并发连接数量有限的问题因此目前实际应用也不多。Apache服务器提供了MPM prefork模式推荐在需要可靠性或者与旧软件兼容的站点时采用这种模式默认情况下最大支持256个并发连接。
## TPC
TPC是Thread Per Connection的缩写其含义是指每次有新的连接就新建一个线程去专门处理这个连接的请求。与进程相比线程更轻量级创建线程的消耗比进程要少得多同时多线程是共享进程内存空间的线程通信相比进程通信更简单。因此TPC实际上是解决或者弱化了PPC fork代价高的问题和父子进程通信复杂的问题。
TPC的基本流程是
<img src="https://static001.geekbang.org/resource/image/25/e7/25b3910c8c5fb0055e184c5c186eece7.jpg" alt="" />
<li>
父进程接受连接图中accept
</li>
<li>
父进程创建子线程图中pthread
</li>
<li>
子线程处理连接的读写请求图中子线程read、业务处理、write
</li>
<li>
子线程关闭连接图中子线程中的close
</li>
注意和PPC相比主进程不用“close”连接了。原因是在于子线程是共享主进程的进程空间的连接的文件描述符并没有被复制因此只需要一次close即可。
TPC虽然解决了fork代价高和进程通信复杂的问题但是也引入了新的问题具体表现在
<li>
创建线程虽然比创建进程代价低,但并不是没有代价,高并发时(例如每秒上万连接)还是有性能问题。
</li>
<li>
无须进程间通信,但是线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题。
</li>
<li>
多线程会出现互相影响的情况,某个线程出现异常时,可能导致整个进程退出(例如内存越界)。
</li>
除了引入了新的问题TPC还是存在CPU线程调度和切换代价的问题。因此TPC方案本质上和PPC方案基本类似在并发几百连接的场景下反而更多地是采用PPC的方案因为PPC方案不会有死锁的风险也不会多进程互相影响稳定性更高。
## prethread
TPC模式中当连接进来时才创建新的线程来处理连接请求虽然创建线程比创建进程要更加轻量级但还是有一定的代价而prethread模式就是为了解决这个问题。
和prefork类似prethread模式会预先创建线程然后才开始接受用户的请求当有新的连接进来的时候就可以省去创建线程的操作让用户感觉更快、体验更好。
由于多线程之间数据共享和通信比较方便因此实际上prethread的实现方式相比prefork要灵活一些常见的实现方式有下面几种
<li>
主进程accept然后将连接交给某个线程处理。
</li>
<li>
子线程都尝试去accept最终只有一个线程accept成功方案的基本示意图如下
</li>
<img src="https://static001.geekbang.org/resource/image/11/4d/115308f686fe0bb1c93ec4b1728eda4d.jpg" alt="" />
Apache服务器的MPM worker模式本质上就是一种prethread方案但稍微做了改进。Apache服务器会首先创建多个进程每个进程里面再创建多个线程这样做主要是为了考虑稳定性即使某个子进程里面的某个线程异常导致整个子进程退出还会有其他子进程继续提供服务不会导致整个服务器全部挂掉。
prethread理论上可以比prefork支持更多的并发连接Apache服务器MPM worker模式默认支持16 × 25 = 400 个并发处理线程。
## 小结
今天我为你讲了传统的单服务器高性能模式PPC与TPC希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧,什么样的系统比较适合本期所讲的高性能模式?原因是什么?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,199 @@
<audio id="audio" title="19 | 单服务器高性能模式Reactor与Proactor" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4d/5b/4dc3c6e0416999ba98dd801ff341c25b.mp3"></audio>
[专栏上一期](http://time.geekbang.org/column/article/8697)我介绍了单服务器高性能的PPC和TPC模式它们的优点是实现简单缺点是都无法支撑高并发的场景尤其是互联网发展到现在各种海量用户业务的出现PPC和TPC完全无能为力。今天我将介绍可以应对高并发场景的单服务器高性能架构模式Reactor和Proactor。
## Reactor
PPC模式最主要的问题就是每个连接都要创建进程为了描述简洁这里只以PPC和进程为例实际上换成TPC和线程原理是一样的连接结束后进程就销毁了这样做其实是很大的浪费。为了解决这个问题一个自然而然的想法就是资源复用即不再单独为每个连接创建进程而是创建一个进程池将连接分配给进程一个进程可以处理多个连接的业务。
引入资源池的处理方式后会引出一个新的问题进程如何才能高效地处理多个连接的业务当一个连接一个进程时进程可以采用“read -&gt; 业务处理 -&gt; write”的处理流程如果当前连接没有数据可以读则进程就阻塞在read操作上。这种阻塞的方式在一个连接一个进程的场景下没有问题但如果一个进程处理多个连接进程阻塞在某个连接的read操作上此时即使其他连接有数据可读进程也无法去处理很显然这样是无法做到高性能的。
解决这个问题的最简单的方式是将read操作改为非阻塞然后进程不断地轮询多个连接。这种方式能够解决阻塞的问题但解决的方式并不优雅。首先轮询是要消耗CPU的其次如果一个进程处理几千上万的连接则轮询的效率是很低的。
为了能够更好地解决上述问题很容易可以想到只有当连接上有数据的时候进程才去处理这就是I/O多路复用技术的来源。
I/O多路复用技术归纳起来有两个关键实现点
<li>
当多条连接共用一个阻塞对象后进程只需要在一个阻塞对象上等待而无须再轮询所有连接常见的实现方式有select、epoll、kqueue等。
</li>
<li>
当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理。
</li>
I/O多路复用结合线程池完美地解决了PPC和TPC的问题而且“大神们”给它取了一个很牛的名字Reactor中文是“反应堆”。联想到“核反应堆”听起来就很吓人实际上这里的“反应”不是聚变、裂变反应的意思而是“**事件反应**”的意思,可以通俗地理解为“**来了一个事件我就有相应的反应**”这里的“我”就是Reactor具体的反应就是我们写的代码Reactor会根据事件类型来调用相应的代码进行处理。Reactor模式也叫Dispatcher模式在很多开源的系统里面会看到这个名称的类其实就是实现Reactor模式的更加贴近模式本身的含义即I/O多路复用统一监听事件收到事件后分配Dispatch给某个进程。
Reactor模式的核心组成部分包括Reactor和处理资源池进程池或线程池其中Reactor负责监听和分配事件处理资源池负责处理事件。初看Reactor的实现是比较简单的但实际上结合不同的业务场景Reactor模式的具体实现方案灵活多变主要体现在
<li>
Reactor的数量可以变化可以是一个Reactor也可以是多个Reactor。
</li>
<li>
资源池的数量可以变化:以进程为例,可以是单个进程,也可以是多个进程(线程类似)。
</li>
将上面两个因素排列组合一下理论上可以有4种选择但由于“多Reactor单进程”实现方案相比“单Reactor单进程”方案既复杂又没有性能优势因此“多Reactor单进程”方案仅仅是一个理论上的方案实际没有应用。
最终Reactor模式有这三种典型的实现方案
<li>
单Reactor单进程/线程。
</li>
<li>
单Reactor多线程。
</li>
<li>
多Reactor多进程/线程。
</li>
以上方案具体选择进程还是线程更多地是和编程语言及平台相关。例如Java语言一般使用线程例如NettyC语言使用进程和线程都可以。例如Nginx使用进程Memcache使用线程。
1.单Reactor单进程/线程
单Reactor单进程/线程的方案示意图如下(以进程为例):
<img src="https://static001.geekbang.org/resource/image/c2/c0/c2fafab3yybd83e97027b3e3f987f9c0.jpg" alt="" />
注意select、accept、read、send是标准的网络编程APIdispatch和“业务处理”是需要完成的操作其他方案示意图类似。
详细说明一下这个方案:
<li>
Reactor对象通过select监控连接事件收到事件后通过dispatch进行分发。
</li>
<li>
如果是连接建立的事件则由Acceptor处理Acceptor通过accept接受连接并创建一个Handler来处理连接后续的各种事件。
</li>
<li>
如果不是连接建立事件则Reactor会调用连接对应的Handler第2步中创建的Handler来进行响应。
</li>
<li>
Handler会完成read-&gt;业务处理-&gt;send的完整业务流程。
</li>
单Reactor单进程的模式优点就是很简单没有进程间通信没有进程竞争全部都在同一个进程内完成。但其缺点也是非常明显具体表现有
<li>
只有一个进程无法发挥多核CPU的性能只能采取部署多个系统来利用多核CPU但这样会带来运维复杂度本来只要维护一个系统用这种方式需要在一台机器上维护多套系统。
</li>
<li>
Handler在处理某个连接上的业务时整个进程无法处理其他连接的事件很容易导致性能瓶颈。
</li>
因此单Reactor单进程的方案在实践中应用场景不多**只适用于业务处理非常快速的场景**目前比较著名的开源软件中使用单Reactor单进程的是Redis。
需要注意的是C语言编写系统的一般使用单Reactor单进程因为没有必要在进程中再创建线程而Java语言编写的一般使用单Reactor单线程因为Java虚拟机是一个进程虚拟机中有很多线程业务线程只是其中的一个线程而已。
2.单Reactor多线程
为了克服单Reactor单进程/线程方案的缺点,引入多进程/多线程是显而易见的这就产生了第2个方案单Reactor多线程。
单Reactor多线程方案示意图是
<img src="https://static001.geekbang.org/resource/image/73/da/73a2d97c63c143a01b2e671942024fda.jpg" alt="" />
我来介绍一下这个方案:
<li>
主线程中Reactor对象通过select监控连接事件收到事件后通过dispatch进行分发。
</li>
<li>
如果是连接建立的事件则由Acceptor处理Acceptor通过accept接受连接并创建一个Handler来处理连接后续的各种事件。
</li>
<li>
如果不是连接建立事件则Reactor会调用连接对应的Handler第2步中创建的Handler来进行响应。
</li>
<li>
Handler只负责响应事件不进行业务处理Handler通过read读取到数据后会发给Processor进行业务处理。
</li>
<li>
Processor会在独立的子线程中完成真正的业务处理然后将响应结果发给主进程的Handler处理Handler收到响应后通过send将响应结果返回给client。
</li>
单Reator多线程方案能够充分利用多核多CPU的处理能力但同时也存在下面的问题
<li>
多线程数据共享和访问比较复杂。例如子线程完成业务处理后要把结果传递给主线程的Reactor进行发送这里涉及共享数据的互斥和保护机制。以Java的NIO为例Selector是线程安全的但是通过Selector.selectKeys()返回的键的集合是非线程安全的对selected keys的处理必须单线程处理或者采取同步措施进行保护。
</li>
<li>
Reactor承担所有事件的监听和响应只在主线程中运行瞬间高并发时会成为性能瓶颈。
</li>
你可能会发现我只列出了“单Reactor多线程”方案没有列出“单Reactor多进程”方案这是什么原因呢主要原因在于如果采用多进程子进程完成业务处理后将结果返回给父进程并通知父进程发送给哪个client这是很麻烦的事情。因为父进程只是通过Reactor监听各个连接上的事件然后进行分配子进程与父进程通信时并不是一个连接。如果要将父进程和子进程之间的通信模拟为一个连接并加入Reactor进行监听则是比较复杂的。而采用多线程时因为多线程是共享数据的因此线程间通信是非常方便的。虽然要额外考虑线程间共享数据时的同步问题但这个复杂度比进程间通信的复杂度要低很多。
3.多Reactor多进程/线程
为了解决单Reactor多线程的问题最直观的方法就是将单Reactor改为多Reactor这就产生了第3个方案多Reactor多进程/线程。
多Reactor多进程/线程方案示意图是(以进程为例):
<img src="https://static001.geekbang.org/resource/image/6c/ba/6cfe3c8785623f93da18ce3390e524ba.jpg" alt="" />
方案详细说明如下:
<li>
父进程中mainReactor对象通过select监控连接建立事件收到事件后通过Acceptor接收将新的连接分配给某个子进程。
</li>
<li>
子进程的subReactor将mainReactor分配的连接加入连接队列进行监听并创建一个Handler用于处理连接的各种事件。
</li>
<li>
当有新的事件发生时subReactor会调用连接对应的Handler即第2步中创建的Handler来进行响应。
</li>
<li>
Handler完成read→业务处理→send的完整业务流程。
</li>
多Reactor多进程/线程的方案看起来比单Reactor多线程要复杂但实际实现时反而更加简单主要原因是
<li>
父进程和子进程的职责非常明确,父进程只负责接收新连接,子进程负责完成后续的业务处理。
</li>
<li>
父进程和子进程的交互很简单,父进程只需要把新连接传给子进程,子进程无须返回数据。
</li>
<li>
子进程之间是互相独立的无须同步共享之类的处理这里仅限于网络模型相关的select、read、send等无须同步共享“业务处理”还是有可能需要同步共享的
</li>
目前著名的开源系统Nginx采用的是多Reactor多进程采用多Reactor多线程的实现有Memcache和Netty。
我多说一句Nginx采用的是多Reactor多进程的模式但方案与标准的多Reactor多进程有差异。具体差异表现为主进程中仅仅创建了监听端口并没有创建mainReactor来“accept”连接而是由子进程的Reactor来“accept”连接通过锁来控制一次只有一个子进程进行“accept”子进程“accept”新连接后就放到自己的Reactor进行处理不会再分配给其他子进程更多细节请查阅相关资料或阅读Nginx源码。
## Proactor
Reactor是非阻塞同步网络模型因为真正的read和send操作都需要用户进程同步操作。这里的“同步”指用户进程在执行read和send这类I/O操作的时候是同步的如果把I/O操作改为异步就能够进一步提升性能这就是异步网络模型Proactor。
Proactor中文翻译为“前摄器”比较难理解与其类似的单词是proactive含义为“主动的”因此我们照猫画虎翻译为“主动器”反而更好理解。Reactor可以理解为“来了事件我通知你你来处理”而Proactor可以理解为“**来了事件我来处理,处理完了我通知你**”。这里的“我”就是操作系统内核“事件”就是有新连接、有数据可读、有数据可写的这些I/O事件“你”就是我们的程序代码。
Proactor模型示意图是
<img src="https://static001.geekbang.org/resource/image/f4/fe/f431b2674eb0881df6a1d1f77a3729fe.jpg" alt="" />
详细介绍一下Proactor方案
<li>
Proactor Initiator负责创建Proactor和Handler并将Proactor和Handler都通过Asynchronous Operation Processor注册到内核。
</li>
<li>
Asynchronous Operation Processor负责处理注册请求并完成I/O操作。
</li>
<li>
Asynchronous Operation Processor完成I/O操作后通知Proactor。
</li>
<li>
Proactor根据不同的事件类型回调不同的Handler进行业务处理。
</li>
<li>
Handler完成业务处理Handler也可以注册新的Handler到内核进程。
</li>
理论上Proactor比Reactor效率要高一些异步I/O能够充分利用DMA特性让I/O操作与计算重叠但要实现真正的异步I/O操作系统需要做大量的工作。目前Windows下通过IOCP实现了真正的异步I/O而在Linux系统下的AIO并不完善因此在Linux下实现高并发网络编程时都是以Reactor模式为主。所以即使Boost.Asio号称实现了Proactor模型其实它在Windows下采用IOCP而在Linux下是用Reactor模式采用epoll模拟出来的异步模型。
## 小结
今天我为你讲了单服务器支持高并发的高性能架构模式Reactor和Proactor希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧,针对“前浪微博”消息队列架构的案例,你觉得采用何种并发模式是比较合适的,为什么?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,138 @@
<audio id="audio" title="20 | 高性能负载均衡:分类及架构" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ed/08/ed87d031c908465e0e6adaba27a92908.mp3"></audio>
单服务器无论如何优化,无论采用多好的硬件,总会有一个性能天花板,当单服务器的性能无法满足业务需求时,就需要设计高性能集群来提升系统整体的处理性能。
高性能集群的本质很简单,通过增加更多的服务器来提升系统整体的计算能力。由于计算本身存在一个特点:同样的输入数据和逻辑,无论在哪台服务器上执行,都应该得到相同的输出。因此高性能集群设计的复杂度主要体现在任务分配这部分,需要设计合理的任务分配策略,将计算任务分配到多台服务器上执行。
**高性能集群的复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法**。对于任务分配器,现在更流行的通用叫法是“负载均衡器”。但这个名称有一定的误导性,会让人潜意识里认为任务分配的目的是要保持各个计算单元的负载达到均衡状态。而实际上任务分配并不只是考虑计算单元的负载均衡,不同的任务分配算法目标是不一样的,有的基于负载考虑,有的基于性能(吞吐量、响应时间)考虑,有的基于业务考虑。考虑到“负载均衡”已经成为了事实上的标准术语,这里我也用“负载均衡”来代替“任务分配”,但请你时刻记住,**负载均衡不只是为了计算单元的负载达到均衡状态**。
今天我先来讲讲负载均衡的分类及架构,下一期会讲负载均衡的算法。
## 负载均衡分类
常见的负载均衡系统包括3种DNS负载均衡、硬件负载均衡和软件负载均衡。
**DNS负载均衡**
DNS是最简单也是最常见的负载均衡方式一般用来实现地理级别的均衡。例如北方的用户访问北京的机房南方的用户访问深圳的机房。DNS负载均衡的本质是DNS解析同一个域名可以返回不同的IP地址。例如同样是www.baidu.com北方用户解析后获取的地址是61.135.165.224这是北京机房的IP南方用户解析后获取的地址是14.215.177.38这是深圳机房的IP
下面是DNS负载均衡的简单示意图
<img src="https://static001.geekbang.org/resource/image/db/2f/dbb61acde016acb2f57212d627d2732f.jpg" alt="" />
DNS负载均衡实现简单、成本低但也存在粒度太粗、负载均衡算法少等缺点。仔细分析一下优缺点其优点有
<li>
简单、成本低负载均衡工作交给DNS服务器处理无须自己开发或者维护负载均衡设备。
</li>
<li>
就近访问提升访问速度DNS解析时可以根据请求来源IP解析成距离用户最近的服务器地址可以加快访问速度改善性能。
</li>
缺点有:
<li>
更新不及时DNS缓存的时间比较长修改DNS配置后由于缓存的原因还是有很多用户会继续访问修改前的IP这样的访问会失败达不到负载均衡的目的并且也影响用户正常使用业务。
</li>
<li>
扩展性差DNS负载均衡的控制权在域名商那里无法根据业务特点针对其做更多的定制化功能和扩展特性。
</li>
<li>
分配策略比较简单DNS负载均衡支持的算法少不能区分服务器的差异不能根据系统与服务的状态来判断负载也无法感知后端服务器的状态。
</li>
针对DNS负载均衡的一些缺点对于时延和故障敏感的业务有一些公司自己实现了HTTP-DNS的功能即使用HTTP协议实现一个私有的DNS系统。这样的方案和通用的DNS优缺点正好相反。
**硬件负载均衡**
硬件负载均衡是通过单独的硬件设备来实现负载均衡功能这类设备和路由器、交换机类似可以理解为一个用于负载均衡的基础网络设备。目前业界典型的硬件负载均衡设备有两款F5和A10。这类设备性能强劲、功能强大但价格都不便宜一般只有“土豪”公司才会考虑使用此类设备。普通业务量级的公司一是负担不起二是业务量没那么大用这些设备也是浪费。
硬件负载均衡的优点是:
<li>
功能强大:全面支持各层级的负载均衡,支持全面的负载均衡算法,支持全局负载均衡。
</li>
<li>
性能强大对比一下软件负载均衡支持到10万级并发已经很厉害了硬件负载均衡可以支持100万以上的并发。
</li>
<li>
稳定性高:商用硬件负载均衡,经过了良好的严格测试,经过大规模使用,稳定性高。
</li>
<li>
支持安全防护硬件均衡设备除具备负载均衡功能外还具备防火墙、防DDoS攻击等安全功能。
</li>
硬件负载均衡的缺点是:
<li>
价格昂贵最普通的一台F5就是一台“马6”好一点的就是“Q7”了。
</li>
<li>
扩展能力差:硬件设备,可以根据业务进行配置,但无法进行扩展和定制。
</li>
**软件负载均衡**
软件负载均衡通过负载均衡软件来实现负载均衡功能常见的有Nginx和LVS其中Nginx是软件的7层负载均衡LVS是Linux内核的4层负载均衡。4层和7层的区别就在于**协议**和**灵活性**Nginx支持HTTP、E-mail协议而LVS是4层负载均衡和协议无关几乎所有应用都可以做例如聊天、数据库等。
软件和硬件的最主要区别就在于性能硬件负载均衡性能远远高于软件负载均衡性能。Nginx的性能是万级一般的Linux服务器上装一个Nginx大概能到5万/秒LVS的性能是十万级据说可达到80万/秒而F5性能是百万级从200万/秒到800万/秒都有数据来源网络仅供参考如需采用请根据实际业务场景进行性能测试。当然软件负载均衡的最大优势是便宜一台普通的Linux服务器批发价大概就是1万元左右相比F5的价格那就是自行车和宝马的区别了。
除了使用开源的系统进行负载均衡如果业务比较特殊也可能基于开源系统进行定制例如Nginx插件甚至进行自研。
下面是Nginx的负载均衡架构示意图
<img src="https://static001.geekbang.org/resource/image/13/35/136afcb3b3bc964f2609127eb27a0235.jpg" alt="" />
软件负载均衡的优点:
<li>
简单:无论是部署还是维护都比较简单。
</li>
<li>
便宜只要买个Linux服务器装上软件即可。
</li>
<li>
灵活4层和7层负载均衡可以根据业务进行选择也可以根据业务进行比较方便的扩展例如可以通过Nginx的插件来实现业务的定制化功能。
</li>
其实下面的缺点都是和硬件负载均衡相比的,并不是说软件负载均衡没法用。
<li>
性能一般一个Nginx大约能支撑5万并发。
</li>
<li>
功能没有硬件负载均衡那么强大。
</li>
<li>
一般不具备防火墙和防DDoS攻击等安全功能。
</li>
## 负载均衡典型架构
前面我们介绍了3种常见的负载均衡机制DNS负载均衡、硬件负载均衡、软件负载均衡每种方式都有一些优缺点但并不意味着在实际应用中只能基于它们的优缺点进行非此即彼的选择反而是基于它们的优缺点进行组合使用。具体来说组合的**基本原则**为DNS负载均衡用于实现地理级别的负载均衡硬件负载均衡用于实现集群级别的负载均衡软件负载均衡用于实现机器级别的负载均衡。
我以一个假想的实例来说明一下这种组合方式,如下图所示。
<img src="https://static001.geekbang.org/resource/image/79/8d/79f371ecbf74818e2a34b4a31664668d.png" alt="" />
整个系统的负载均衡分为三层。
<li>
地理级别负载均衡www.xxx.com部署在北京、广州、上海三个机房当用户访问时DNS会根据用户的地理位置来决定返回哪个机房的IP图中返回了广州机房的IP地址这样用户就访问到广州机房了。
</li>
<li>
集群级别负载均衡广州机房的负载均衡用的是F5设备F5收到用户请求后进行集群级别的负载均衡将用户请求发给3个本地集群中的一个我们假设F5将用户请求发给了“广州集群2”。
</li>
<li>
机器级别的负载均衡广州集群2的负载均衡用的是NginxNginx收到用户请求后将用户请求发送给集群里面的某台服务器服务器处理用户的业务请求并返回业务响应。
</li>
需要注意的是上图只是一个示例一般在大型业务场景下才会这样用如果业务量没这么大则没有必要严格照搬这套架构。例如一个大学的论坛完全可以不需要DNS负载均衡也不需要F5设备只需要用Nginx作为一个简单的负载均衡就足够了。
## 小结
今天我为你讲了负载均衡的常见分类以及典型架构,希望对你有所帮助。
这就是今天的全部内容留一道思考题给你吧假设你来设计一个日活跃用户1000万的论坛的负载均衡集群你的方案是什么设计理由是什么
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,92 @@
<audio id="audio" title="21 | 高性能负载均衡:算法" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1b/30/1b85f493f0ba14ec86ada0982db41230.mp3"></audio>
负载均衡算法数量较多,而且可以根据一些业务特性进行定制开发,抛开细节上的差异,根据算法期望达到的目的,大体上可以分为下面几类。
<li>任务平分类:负载均衡系统将收到的任务平均分配给服务器进行处理,这里的“平均”可以是绝对数量的平均,也可以是比例或者权重上的平均。
</li>
<li>负载均衡类负载均衡系统根据服务器的负载来进行分配这里的负载并不一定是通常意义上我们说的“CPU负载”而是系统当前的压力可以用CPU负载来衡量也可以用连接数、I/O使用率、网卡吞吐量等来衡量系统的压力。
</li>
<li>性能最优类:负载均衡系统根据服务器的响应时间来进行任务分配,优先将新任务分配给响应最快的服务器。
</li>
<li>Hash类负载均衡系统根据任务中的某些关键信息进行Hash运算将相同Hash值的请求分配到同一台服务器上。常见的有源地址Hash、目标地址Hash、session id hash、用户ID Hash等。
</li>
接下来我介绍一下负载均衡算法以及它们的优缺点。
## 轮询
负载均衡系统收到请求后,按照顺序轮流分配到服务器上。
轮询是最简单的一个策略,无须关注服务器本身的状态,例如:
<li>某个服务器当前因为触发了程序bug进入了死循环导致CPU负载很高负载均衡系统是不感知的还是会继续将请求源源不断地发送给它。
</li>
<li>集群中有新的机器是32核的老的机器是16核的负载均衡系统也是不关注的新老机器分配的任务数是一样的。
</li>
需要注意的是负载均衡系统无须关注“服务器本身状态”,这里的关键词是“本身”。也就是说,**只要服务器在运行,运行状态是不关注的**。但如果服务器直接宕机了,或者服务器和负载均衡系统断连了,这时负载均衡系统是能够感知的,也需要做出相应的处理。例如,将服务器从可分配服务器列表中删除,否则就会出现服务器都宕机了,任务还不断地分配给它,这明显是不合理的。
总而言之,“简单”是轮询算法的优点,也是它的缺点。
## 加权轮询
负载均衡系统根据服务器权重进行任务分配,这里的权重一般是根据硬件配置进行静态配置的,采用动态的方式计算会更加契合业务,但复杂度也会更高。
加权轮询是轮询的一种特殊形式,其主要目的就是为了**解决不同服务器处理能力有差异的问题**。例如集群中有新的机器是32核的老的机器是16核的那么理论上我们可以假设新机器的处理能力是老机器的2倍负载均衡系统就可以按照2:1的比例分配更多的任务给新机器从而充分利用新机器的性能。
加权轮询解决了轮询算法中无法根据服务器的配置差异进行任务分配的问题,但同样存在无法根据服务器的状态差异进行任务分配的问题。
## 负载最低优先
负载均衡系统将任务分配给当前负载最低的服务器,这里的负载根据不同的任务类型和业务场景,可以用不同的指标来衡量。例如:
<li>LVS这种4层网络负载均衡设备可以以“连接数”来判断服务器的状态服务器连接数越大表明服务器压力越大。
</li>
<li>Nginx这种7层网络负载系统可以以“HTTP请求数”来判断服务器状态Nginx内置的负载均衡算法不支持这种方式需要进行扩展
</li>
<li>如果我们自己开发负载均衡系统可以根据业务特点来选择指标衡量系统压力。如果是CPU密集型可以以“CPU负载”来衡量系统压力如果是I/O密集型可以以“I/O负载”来衡量系统压力。
</li>
负载最低优先的算法解决了轮询算法中无法感知服务器状态的问题,由此带来的代价是复杂度要增加很多。例如:
<li>最少连接数优先的算法要求负载均衡系统统计每个服务器当前建立的连接其应用场景仅限于负载均衡接收的任何连接请求都会转发给服务器进行处理否则如果负载均衡系统和服务器之间是固定的连接池方式就不适合采取这种算法。例如LVS可以采取这种算法进行负载均衡而一个通过连接池的方式连接MySQL集群的负载均衡系统就不适合采取这种算法进行负载均衡。
</li>
<li>CPU负载最低优先的算法要求负载均衡系统以某种方式收集每个服务器的CPU负载而且要确定是以1分钟的负载为标准还是以15分钟的负载为标准不存在1分钟肯定比15分钟要好或者差。不同业务最优的时间间隔是不一样的时间间隔太短容易造成频繁波动时间间隔太长又可能造成峰值来临时响应缓慢。
</li>
负载最低优先算法基本上能够比较完美地解决轮询算法的缺点因为采用这种算法后负载均衡系统需要感知服务器当前的运行状态。当然其代价是复杂度大幅上升。通俗来讲轮询可能是5行代码就能实现的算法而负载最低优先算法可能要1000行才能实现甚至需要负载均衡系统和服务器都要开发代码。负载最低优先算法如果本身没有设计好或者不适合业务的运行特点算法本身就可能成为性能的瓶颈或者引发很多莫名其妙的问题。所以负载最低优先算法虽然效果看起来很美好但实际上真正应用的场景反而没有轮询包括加权轮询那么多。
## 性能最优类
负载最低优先类算法是站在服务器的角度来进行分配的,而性能最优优先类算法则是站在客户端的角度来进行分配的,优先将任务分配给处理速度最快的服务器,通过这种方式达到最快响应客户端的目的。
和负载最低优先类算法类似,性能最优优先类算法本质上也是感知了服务器的状态,只是通过响应时间这个外部标准来衡量服务器状态而已。因此性能最优优先类算法存在的问题和负载最低优先类算法类似,复杂度都很高,主要体现在:
<li>负载均衡系统需要收集和分析每个服务器每个任务的响应时间,在大量任务处理的场景下,这种收集和统计本身也会消耗较多的性能。
</li>
<li>为了减少这种统计上的消耗,可以采取采样的方式来统计,即不统计所有任务的响应时间,而是抽样统计部分任务的响应时间来估算整体任务的响应时间。采样统计虽然能够减少性能消耗,但使得复杂度进一步上升,因为要确定合适的**采样率**,采样率太低会导致结果不准确,采样率太高会导致性能消耗较大,找到合适的采样率也是一件复杂的事情。
</li>
<li>无论是全部统计还是采样统计,都需要选择合适的**周期**是10秒内性能最优还是1分钟内性能最优还是5分钟内性能最优……没有放之四海而皆准的周期需要根据实际业务进行判断和选择这也是一件比较复杂的事情甚至出现系统上线后需要不断地调优才能达到最优设计。
</li>
## Hash类
负载均衡系统根据任务中的某些关键信息进行Hash运算将相同Hash值的请求分配到同一台服务器上这样做的目的主要是为了满足特定的业务需求。例如
- 源地址Hash
将来源于同一个源IP地址的任务分配给同一个服务器进行处理适合于存在事务、会话的业务。例如当我们通过浏览器登录网上银行时会生成一个会话信息这个会话是临时的关闭浏览器后就失效。网上银行后台无须持久化会话信息只需要在某台服务器上临时保存这个会话就可以了但需要保证用户在会话存在期间每次都能访问到同一个服务器这种业务场景就可以用源地址Hash来实现。
- ID Hash
将某个ID标识的业务分配到同一个服务器中进行处理这里的ID一般是临时性数据的ID如session id。例如上述的网上银行登录的例子用session id hash同样可以实现同一个会话期间用户每次都是访问到同一台服务器的目的。
## 小结
今天我为你讲了常见负载均衡算法的优缺点和应用场景,希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧,微信抢红包的高并发架构,应该采取什么样的负载均衡算法?谈谈你的分析和理解。
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)