首页 > 代码库 > 数据密集型系统架构设计
数据密集型系统架构设计
按照使用的资源类型划分,我们可以把系统分为三大类型:IO密集型、计算密集型,数据密集型。系统的类型反映了系统的主要瓶颈。现实情况中,大部分系统在由小变大的过程中,最先出现瓶颈的是IO。IO问题体现在两个方面:高并发,存储介质的读写(例如数据库,磁盘等)。随着业务逻辑的复杂化,接下来出现瓶颈的是计算,也就是常说的CPU idle不足。出现计算瓶颈的时候,一般会使用水平扩展(加机器)和垂直扩张(服务拆分)两个方法。随着数据量(用户数量,客户数量)的增长,再接下来出现瓶颈的是内存。
如今,内存的合理使用比以往更加重要。一方面,大数据理论已经非常普及,用数据驱动产品也已经被普遍接受并落地,同时数据分析也促使产品设计的更加精细,因此系统承载的数量比以前有了很大的变化,系统遇到内存瓶颈的时间也比以前大大缩短了。另一方面,内存依然是相对昂贵的硬件,不能无限制的使用。即使在Amazon等云服务上,大内存的实例也是很昂贵的,并且大内存的实例往往伴随着高性能型CPU,这对一些数据密集型系统是一个浪费。因此,本文重点探讨数据密集系统如何应对出现的瓶颈。
1. 拆库
任何工程上的问题最基本的思路都是“分而治之”。因此,当内存不够时,很自然的想法是将数据拆分到多台机器中,俗称拆库。沿用数据库拆分的术语,拆库又分为“水平拆分”和“垂直拆分”两个派别。
1.1 水平拆分
水平拆分是指将同一种数据的不同记录进行拆分。
例如我们有一亿条商品数据供查询。如果单机无法存储,可以使用四台机器,每台机器存储2500万条商品数据。其中,每台机器称为一个“分片”,同一个分片的多台机器组成一个“分组”,从四个分组各选出一台机器组成一个完整的服务。当上游服务进行查询时,同时查询四台机器,并对返回结果做合并。
在使用水平拆分的方案时,需要重点考虑以下问题:
索引服务
如前几篇文章所述,任何大数据量系统中,在启动之前都需要加载索引数据。索引数据一般是预先计算好的,并且以二进制格式持久化的文件。因为服务进行了拆分,每一台机器只需要加载一部分数据,因此需要为每个分组的机器单独计算索引数据,这样减少了系统启动时处理的数据量,加快启动速度。
数据更新
同样,由于每台机器只需要加载一部分数据,那么也只需要处理这部分数据的更新。目前主流的更新数据流都是使用 Mesage Queue 作为传输和持久化系统个,在服务端接收 Message Queue 的数据并持久化到本地,供在线服务定期读取。一般同一类的数据使用一个 Topic 传输,同时 Message Queue 一般都支持 Partition 的机制。即在向 MQ 中发送一条数据时,可以指定将该条数据发送到哪个 Partition;在从 MQ 中读取数据时,可以指定只读取哪些 Partition 的数据。例如上文的例子,存储商品数据的服务器分了四个组,因此可以将传输商品更新数据的 Topic 划分为四个 Partition,每个分组的机器只需要订阅其需要的 Partition 即可。在实际操作中,为了保持未来的扩展性,一般 Partition 的数量都会设置为分组数量的若干倍,例如八个或者十六个,这样在未来数据量进一步增长导致分组个数进一步增加时,不需要修改 MQ 的 Partition 配置。
利用 MQ 这个机制,可以使每台机器只订阅自己需要处理的数据,减少带宽,也减少更新时处理的数据量,避免浪费资源。
服务管理的复杂性
在我们管理上下游机器时,一般会使用以 ZooKeeper 为核心的服务管理系统。即每个服务都注册在 ZooKeeper 中,当上游服务需要访问下游服务时,去 ZooKeeper 中查询可用的下游服务列表,并同时考虑负载均衡等因素,选择最合适的一个下游服务实例。
当一个服务出现分组时,管理的难度会增大。服务管理系统需要确保一个服务的每个分组的实例同样多,并且负载基本保持平衡。另外,当任何一台机器出现 故障导致的宕时,需要启动备用机器。这时,需要判断是哪个分组的机器发生了故障,并启动相关分组的机器实例,重新注册到 ZK 中。
无法拆分的数据
有很多数据是无法拆分的。一方面有些数据是天然不可拆分的,例如各种策略使用的词典;另一方面,有些数据即使可以拆分,但和系统中其他数据的拆分规则不同,那么系统也无法保证所有数据都能被拆分,只能优先拆分主要数据。
1.2 垂直拆分
在传统关系型数据库的设计上,垂直拆分是指将一种数据的不同列进行拆分;在对系统架构的设计上,垂直拆分是只将一个服务的不同计算逻辑拆分为多个服务。在使用垂直拆分的方案时,需要重点考虑以下问题:
增加网络请求次数,增加系统响应时间
如果是对响应时间要求很高的系统,一定会尽可能地避免垂直拆分,例如搜索。而有一些对逻辑确实很复杂,对时间又不太敏感的系统,一般都会优先选择垂直拆分,例如支付。
增加系统复杂度
将服务进行了分层,更加了开发成本,对运维的要求也更高。
数据冗余
有一些数据会被拆分过的多个服务使用,会出现在上下游多个服务中,那么数据的分发、更新都会更加复杂,即浪费资源,又进一步增加了系统的复杂度。因此,在垂直拆分的过程中,一定要尽可能将服务的功能做良好的划分,避免一种数据被多个服务使用的情况。
垂直拆分的方案中,有一种情况可以大幅减少机器数量,即:一部分数据的存在并不是在处理请求的时候被直接使用,其存在是为了维护被处理请求的逻辑直接使用的数据。
一个典型的例子是检索服务中的正排索引。检索服务在查询时,直接使用的是倒排索引,而倒排索引是根据正排索引生成的。正排索引往往有多种数据,当一条数据发生更新时,会影响其他类别的数据。因此,一条数据的更新信息无法被单独处理,在系统的内存中往往同时维护正排索引和倒排索引,导致内存翻倍。这种情况下,如果我们把正排索引独立到一台离线机器中,这台机器维护正排索引的全部数据,当正排索引发生更新时,倒排索引的更新信息,并分发给所有在线机器。那么,在线服务就不需要维护正排索引,能够大幅度减少内存的使用。
1.3 综述
实际情况中,大型系统往往同时使用水平拆分和垂直拆分两种方案。一方面,水平拆分虽然服务内部进行了分组,但对外仍然是单一的服务,因此从业务逻辑上来讲更加简单。另一方面,垂直拆分可以将非常复杂、计算资源有不同需求的业务逻辑进行很好的隔离,方便系统中各业务逻辑可以针对自己的特点进行开发和部署。因此,在选择拆分方案时,要结合系统的主要矛盾以及目前团队成员的技术特点,综合考虑做出选择。
2. 多级存储
俗话说,当上帝为你关上了一扇门,必(可)定(能)为你打开了一扇窗。如果说大数据是上帝为架构师关上的一扇门,那么热点数据就是打开的那扇窗。虽然在现实世界中的数据是海量难以估算的,但幸运的是,有价值或者说值得关注的数据总是少数的。在大型系统中,请永远把二八法则的重要性放在第一位。
一般来说,计算机的存储系统分为三级:CPU Cache,内存,磁盘。这三者的访问速度依次降低(并且是数量级的降低),单位存储的成本也依次降低(也是数量级的降低)。多级存储的基本思想是,按照被访问频率的不同给数据分类,访问频率越高的数据应当放在访问速度越快的存储介质中。
三种系统都使用页式存储的结构,页也是其处理数据的最小单位。由于这个特性,我们一般在编写程序时,尽可能地将连续访问的数据放在内存的相邻位置,以提高CPU Cache的命中率,也就是常说的 locality principle。
随着SSD的出现,对磁盘的使用已经出现了新的方法论。机械磁盘的随机读写速度在10ms左右,不太可能供实时系统使用。而SSD磁盘的随机读写速度在100us左右,对于有些秒级响应的系统来说,已经可以作为实时系统的存储介质。一种典型的情况是系统存在相当数量的冷门数据。系统对于热点数据可以快速地反馈,对于很少被访问的冷门数据可以存储在SSD磁盘中。当冷门数据被访问时,只要latency仍然可以控制在秒级,就可以在保证用户体验只有很少的损害的情况下,大幅减少系统成本。
一种典型的场景是电商的商品信息。经常被访问的商品可能不到商品总量的1%。像淘宝这样规模的电商系统,实际可能比1%还低。
另一种典型的场景是用户评论。无论按评论发表的先后顺序,还是按某种规则计算出的评论的质量度排序,总是前100个左右的评论被经常访问,后面的评论几乎不会被访问到。
另外,回想上文提到的检索服务的案例。正排索引除了可以拆分为单独的服务之外,还可以存储在磁盘中。更新正排索引的时候直接从磁盘读取数据,修改后写会磁盘,同时更新内存的倒排索引。如果使用SSD磁盘,虽然更新的延迟会增长,但也会控制在毫秒级,对于系统完全是可以接受的。要知道,在一条数据到达检索服务之前,都会经过若干次网络传输,由磁盘引起的延迟并不是主要因素。
在使用磁盘作为可以提供实时查询功能的存储介质时,很常见的方案是将磁盘作为二级缓存,将最近访问的数据保存在内存中,当访问的数据不在内存中时,从磁盘读取,并放入内存中。这个方案的假设是,最近被访问的数据很可能在接下来仍然被访问。采用这种方案需要重点注意,防止爬虫或者外部的恶意请求短期内访问大量冷门数据,造成实际的热点数据被换出缓存,导致处理真实请求时有大量的缓存失效。
大数据技术对商业效果的提升已经在越来越多的行业中被证明,未来的服务,无论是在线还是离线,处理的数据都会有数量级甚至几个数量级的增长。同时,我们看到内存除了访问速度越来越快,在存储的数据量和成本上并没有太大的变化。因此,未来越来越多的系统的主要瓶颈会从计算、IO转移到数据量上,内存密集型系统会变得越来越重要,相信其架构在未来几年也会有很多新的方式出现。
数据密集型系统架构设计