亿级流量系统架构之如何支撑百亿级数据的存储与计算【转载 石杉的架构笔记】-1
亿级流量系统架构之如何支撑百亿级数据的存储与计算【石杉的架构笔记】
“本文聊一下笔者几年前所带的团队负责的多个项目中的其中一个,用这个项目来聊聊一个亿级流量系统架构演进的过程。
一、背景引入
首先简单介绍一下项目背景,公司对合作商家提供一个付费级产品,这个商业产品背后涉及到数百人的研发团队协作开发,包括各种业务系统来提供很多强大的业务功能,同时在整个平台中包含了一个至关重要的核心数据产品,这个数据产品的定位是全方位支持用户的业务经营和快速决策。
这篇文章就聊聊这个数据产品背后对应的一套大型商家数据平台,看看这个平台在分布式、高并发、高可用、高性能、海量数据等技术挑战下的架构演进历程。
因为整套系统规模过于庞大,涉及研发人员很多,持续时间很长,文章难以表述出其中各种详细的技术细节以及方案,因此本文主要从整体架构演进的角度来阐述。
至于选择这个商家数据平台项目来聊架构演进过程,是因为这个平台基本跟业务耦合度较低,不像我们负责过的C端类的电商平台以及其他业务类平台有那么重的业务在里面,文章可以专注阐述技术架构的演进,不需要牵扯太多的业务细节。
此外,这个平台项目在笔者带的团队负责过的众多项目中,相对算比较简单的,但是前后又涉及到各种架构的演进过程,因此很适合通过文字的形式来展现出来。
二、商家数据平台的业务流程
下面几点,是这个数据产品最核心的业务流程:
-
每天从用户使用的大量业务系统中实时的采集过来各种业务数据
-
接着存储在自己的数据中心里
-
然后实时的运算大量的几百行~上千行的SQL来生成各种数据报表
-
最后就可以提供这些数据报表给用户来分析。
基本上用户在业务系统使用过程中,只要数据一有变动,立马就反馈到各种数据报表中,用户立马就可以看到数据报表中的各种变化,进而快速的指导自己的决策和管理。
整个过程,大家看看下面的图就明白了。
三、从0到1的过程中上线的最low版本
看着上面那张图好像非常的简单,是不是?
看整个过程,似乎数据平台只要想个办法把业务系统的数据采集过来,接着放在MySQL的各种表里,直接咔嚓一下运行100多个几百行的大SQL,然后SQL运行结果再写到另外一些MySQL的表里作为报表数据,接着用户直接点击报表页面查询MySQL里的报表数据,就可以了!
其实任何一个系统从0到1的过程,都是比较low的,刚开始为了快速开发出来这个数据平台,还真的就是用了这种架构来开发,大家看下面的图。
其实在刚开始业务量很小,请求量很小,数据量很小的时候,上面那种架构也没啥问题,还挺简单的。
我们直接基于自己研发的数据库binlog采集中间件(这个是另外一套复杂系统了,不在本文讨论的范围里,以后有机会可以聊聊),感知各个业务系统的数据库中的数据变更,毫秒级同步到数据平台自己的MySQL库里;
接着数据平台里做一些定时调度任务,每隔几秒钟就运行上百个复杂大SQL,计算各种报表的数据并将结果存储到MySQL库中;
最后用户只要对报表刷新一下,立马就可以从MySQL库里查到最新的报表数据。
基本上在无任何技术挑战的前提下,这套简易架构运行的会很顺畅,效果很好。然而,事情往往不是我们想的那么简单的,因为大家都知道国内那些互联网巨头公司最大的优势和资源之一,就是有丰富以及海量的C端用户以及B端的合作商家。
对C端用户,任何一个互联网巨头推出一个新的C端产品,很可能迅速就是上亿用户量;
对B端商家,任何一个互联网巨头如果打B端市场,凭借巨大的影响力以及合作资源,很可能迅速就可以聚拢数十万,乃至上百万的付费B端用户。
因此,很不幸,接下来的一两年内,这套系统将要面临业务的高速增长带来的巨大技术挑战和压力。
四、海量数据存储和计算的技术挑战
其实跟很多大型系统遇到的第一个技术挑战一样,这套系统遇到的第一个大问题,就是海量数据的存储。
你一个系统刚开始上线也许就几十个商家用,接着随着你们产品的销售持续大力推广,可能几个月内就会聚拢起来十万级别的用户。
这些用户每天都会大量的使用你提供的产品,进而每天都会产生大量的数据,大家可以想象一下,在数十万规模的商家用户使用场景下,每天你新增的数据量大概会是几千万条数据,记住,这可是每天新增的数据!这将会给上面你看到的那个很low的架构带来巨大的压力。
如果你在负责上面那套系统,结果慢慢的发现,每天都要涌入MySQL几千万条数据,这种现象是令人感到崩溃的,因为你的MySQL中的单表数据量会迅速膨胀,很快就会达到单表几亿条数据,甚至是数十亿条数据,然后你对那些怪兽一样的大表运行几百行乃至上千行的SQL?其中包含了N层嵌套查询以及N个各种多表连接?
我跟你打赌,如果你愿意试一下,你会发现你的数据平台系统直接卡死,因为一个大SQL可能都要几个小时才能跑完。然后MySQL的cpu负载压力直接100%,弄不好就把MySQL数据库服务器给搞宕机了。
所以这就是第一个技术挑战,数据量越来越大,SQL跑的越来越慢,MySQL服务器压力越来越大。
我们当时而言,已经看到了业务的快速增长,因此绝对要先业务一步来重构系统架构,不能让上述情况发生,第一次架构重构,势在必行!
五、离线计算与实时计算的拆分
其实在几年前我们做这个项目的时候,大数据技术已经在国内开始运用的不错了,而且尤其在一些大型互联网公司内,我们基本上都运用大数据技术支撑过很多生产环境的项目了,在大数据这块技术的经验积累,也是足够的。
针对这个数据产品的需求,我们完全可以做到,将昨天以及昨天以前的数据都放在大数据存储中,进行离线存储和离线计算,然后只有今天的数据是实时的采集的。
因此在这种技术挑战下,第一次架构重构的核心要义,就是将离线计算与实时计算进行拆分。
大家看上面那张图,新的架构之下,分为了离线与实时两条计算链路。
一条是离线计算链路:每天凌晨,我们将业务系统MySQL库中的昨天以前的数据,作为离线数据导入Hadoop HDFS中进行离线存储,然后凌晨就基于Hive / Spark对离线存储中的数据进行离线计算。
如果有同学不清楚大数据的知识,可以参加我之前写的一篇文章:《兄弟,用大白话告诉你小白都能听懂的Hadoop架构原理》。Hadoop与Spark作为世界上最优秀、运用最广泛的大数据技术,天然适合PB级海量数据的分布式存储和分布式计算。
在离线计算链路全面采用大数据相关技术来支撑过后,完美解决了海量数据的存储,哪怕你一天进来上亿条数据都没事,分布式存储可以随时扩容,同时基于分布式计算技术天然适合海量数据的离线计算。
即使是每天凌晨耗费几个小时将昨天以前的数据完成计算,这个也没事,因为凌晨一般是没人看这个数据的,所以主要在人家早上8点上班以前,完成数据计算就可以了。
另外一条是实时计算链路:每天零点过后,当天最新的数据变更,全部还是走之前的老路子,秒级同步业务库的数据到数据平台存储中,接着就是数据平台系统定时运行大量的SQL进行计算。同时在每天零点的时候,还会从数据平台的存储中清理掉昨天的数据,仅仅保留当天一天的数据而已。
实时计算链路最大的改变,就是仅仅在数据平台的本地存储中保留当天一天的数据而已,这样就大幅度降低了要放在MySQL中的数据量了。
举个例子:比如一天就几千万条数据放在MySQL里,那么单表数据量被维持在了千万的级别上,此时如果对SQL对应索引以及优化到极致之后,勉强还是可以在几十秒内完成所有报表的计算。
六、持续增长的数据量和计算压力
但是如果仅仅只是做到上面的架构,还是只能暂时性的缓解系统架构的压力,因为业务还在加速狂飙,继续增长。
你老是期望单日的数据量在千万级别,怎么可能?业务是不会给你这个机会的。很快就可以预见到单日数据量将会达到几亿,甚至十亿的级别。
如果一旦单日数据量达到了数十亿的级别,单表数据量上亿,你再怎么优化SQL性能,有无法保证100多个几百行的复杂SQL可以快速的运行完毕了。
到时候又会回到最初的问题,SQL计算过慢会导致数据平台核心系统卡死,甚至给MySQL服务器过大压力,CPU 100%负载后宕机。
而且此外还有另外一个问题,那就是单个MySQL数据库服务器的存储容量是有限的,如果一旦单日数据量达到甚至超过了单台MySQL数据库服务器的存储极限,那么此时也会导致单台MySQL数据库无法容纳所有的数据了,这也是一个很大的问题!
第二次架构重构,势在必行!
七、大数据领域的实时计算技术的缺陷
在几年前做这个项目的背景下,当时可供选择的大数据领域的实时计算技术,主要还是Storm,算是比较成熟的一个技术,另外就是Spark生态里的Spark Streaming。当时可没有什么现在较火的Flink、Druid等技术。
在仔细调研了一番过后发现,根本没有任何一个大数据领域的实时计算技术可以支撑这个需求。
因为Storm是不支持SQL的,而且即使勉强你让他支持了,他的SQL支持也会很弱,完全不可能运行几百行甚至上千行的复杂SQL在这种流式计算引擎上的执行。
Spark Streaming也是同理,当时功能还是比较弱小的,虽然可以支持简单SQL的执行,但是完全无法支持这种复杂SQL的精准运算。
因此很不幸的是,在当时的技术背景下,遇到的这个实时数据运算的痛点,没有任何开源的技术是可以解决的。必须得自己根据业务的具体场景,从0开始定制开发自己的一套数据平台系统架构。
八、分库分表解决数据扩容问题
首先我们要先解决第一个痛点,就是一旦单台数据库服务器无法存储下当日的数据,该怎么办?
第一个首选的方案当然就是分库分表了。我们需要将一个库拆分为多库,不用的库放在不同的数据库服务器上,同时每个库里放多张表。
采用这套分库分表架构之后,可以做到每个数据库服务器放一部分的数据,而且随着数据量日益增长,可以不断地增加更多的数据库服务器来容纳更多的数据,做到按需扩容。
同时,每个库里单表分为多表,这样可以保证单表数据量不会太大,控制单表的数据量在几百万的量级,基本上性能优化到极致的SQL语句跑起来效率还是不错的,秒级出结果是可以做到的。
同样,给大家来一张图,大家直观的感受一下:
九、读写分离降低数据库服务器的负载
此时分库分表之后,又面临着另外一个问题,就是现在如果对每个数据库服务器又是写入又是读取的话,会导致数据库服务器的CPU负载和IO负载非常的高!
为什么这么说呢?因为在此时写数据库的每秒并发已经达到几千了,同时还频繁的运行那种超大SQL来查询数据,数据库服务器的CPU运算会极其的繁忙。
因此我们将MySQL做了读写分离的部署,每个主数据库服务器都挂了多个从数据库服务器,写只能写入主库,查可以从从库来查。
大家一起来看看下面这张图:
十、自研的滑动窗口动态计算引擎
但是光是做到这一点还是不够的,因为其实在生产环境发现,哪怕单表数据量限制在了几百万的级别,你运行几百个几百行复杂SQL,也要几十秒甚至几分钟的时间,这个时效性对付费级的产品已经有点无法接受,产品提出的极致性能要求是,秒级!
因此对上述系统架构,我们再次做了架构的优化,在数据平台中嵌入了自己纯自研的滑动窗口计算引擎,核心思想如下:
-
在数据库binlog采集中间件采集的过程中,要将数据的变更切割为一个一个的滑动时间窗口,每个滑动时间窗口为几秒钟,对每个窗口内的数据打上那个窗口的标签
-
同时需要维护一份滑动时间窗口的索引数据,包括每个分片的数据在哪个窗口里,每个窗口的数据的一些具体的索引信息和状态
-
接着数据平台中的核心计算引擎,不再是每隔几十秒就运行大量SQL对当天所有的数据全部计算一遍了,而是对一个接一个的滑动时间窗口,根据窗口标签提取出那个窗口内的数据进行计算,计算的仅仅是最近一个滑动时间窗口内的数据
-
接着对这个滑动时间窗口内的数据,可能最多就千条左右吧,运行所有的复杂SQL计算出这个滑动时间窗口内的报表数据,然后将这个窗口数据计算出的结果,与之前计算出来的其他窗口内的计算结果进行合并,最后放入MySQL中的报表内
-
此外,这里需要考虑到一系列的生产级机制,包括滑动时间窗口如果计算失败怎么办?如果一个滑动时间窗口计算过慢怎么办?滑动窗口计算过程中系统宕机了如何在重启之后自动恢复计算?等等
通过这套滑动窗口的计算引擎,我们直接将系统计算性能提升了几十倍,基本上每个滑动窗口的数据只要几秒钟就可以完成全部报表的计算,相当于一下子把最终呈现给用户的实时数据的时效性提升到了几秒钟,而不是几十秒。
同样,大家看看下面的图。
十一、离线计算链路的性能优化
实时计算链路的性能问题通过自研滑动窗口计算引擎来解决了,但是离线计算链路此时又出现了性能问题。。。
因为每天凌晨从业务库中离线导入的是历史全量数据,接着需要在凌晨针对百亿量级的全量数据,运行很多复杂的上千行复杂SQL来进行运算,当数据量达到百亿之后,这个过程耗时很长,有时候要从凌晨一直计算到上午。
关键问题就在于,离线计算链路,每天都是导入全量数据来进行计算,这就很坑了。
之所以这么做,是因为从业务库同步数据时,每天都涉及到数据的更新操作,而hadoop里的数据是没法跟业务库那样来进行更新的,因此最开始都是每天导入全量历史数据,作为一个最新快照来进行全量计算。
在这里,我们对离线计算链路进行了优化,主要就是全量计算转增量计算:每天数据在导入hadoop之后,都会针对数据的业务时间戳来分析和提取出来每天变更过的增量数据,将这些增量数据放入独立的增量数据表中。
同时需要根据具体的业务需求,自动分析数据计算的基础血缘关系,有可能增量数据需要与部分全量数据混合才能完成计算,此时可能会提取部分全量历史数据,合并完成计算。计算完成之后,将计算结果与历史计算结果进行合并。
在完成这个全量计算转增量计算的过程之后,离线计算链路在凌晨基本上百亿级别的数据量,只要对昨天的增量数据花费一两个小时完成计算之后,就可以完成离线计算的全部任务,性能相较于全量计算提升至少十倍以上。
十二、阶段性总结
到此为止,就是这套系统在最初一段时间做出来的一套架构,不算太复杂,还有很多缺陷,不完美,但是在当时的业务背景下效果相当的不错。
在这套架构对应的早期业务背景下,每天新增数据大概是亿级左右,但是分库分表之后,单表数据量在百万级别,单台数据库服务器的高峰期写入压力在2000/s,查询压力在100/s,数据库集群承载的总高峰写入压力在1万/s,查询压力在500/s,有需要还可以随时扩容更多的数据库服务器,承载更多的数据量,更高的写入并发与查询并发。
而且,因为做了读写分离,因此每个数据库服务器的CPU负载和IO负载都不会在高峰期打满,避免数据库服务器的负载过高。
而基于滑动时间窗口的自研计算引擎,可以保证当天更新的实时数据主要几秒钟就可以完成一个微批次的计算,反馈到用户看到的数据报表中。
同时这套引擎自行管理着计算的状态与日志,如果出现某个窗口的计算失败、系统宕机、计算超时,等各种异常的情况,这个套引擎可以自动重试与恢复。
此外,昨天以前的海量数据都是走Hadoop与Spark生态的离线存储与计算。经过性能优化之后,每天凌晨花费一两个小时,算好昨天以前所有的数据即可。
最后实时与离线的计算结果在同一个MySQL数据库中融合,此时用户如果对业务系统做出操作,实时数据报表在几秒后就会刷新,如果要看昨天以前的数据可以随时选择时间范围查看即可,暂时性是满足了业务的需求。
早期的几个月里,日增上亿数据,离线与实时两条链路中的整体数据量级达到了百亿级别,无论是存储扩容,还是高效计算,这套架构基本是撑住了。
十三、下一阶段的展望
这个大型系统架构演进实践是一个系列的文章,将会包含很多篇文章,因为一个大型的系统架构演进的过程,会持续很长时间,做出很多次的架构升级与重构,不断的解决日益增长的技术挑战,最终完美的抗住海量数据、高并发、高性能、高可用等场景。
下一篇文章会说说下一步是如何将数据平台系统重构为一套高可用高容错的分布式系统架构的,来解决单点故障、单系统CPU负载过高、自动故障转移、自动数据容错等相关的问题。包括之后还会有多篇文章涉及到我们自研的更加复杂的支撑高并发、高可用、高性能、海量数据的平台架构。
十四、上篇文章的答疑
上一篇文章写了一个分布式锁的高并发优化的文章,具体参见:《每秒上千订单场景下的分布式锁高并发优化实践》。收到了大家很多的提问,其实最终都是一个问题:
针对那篇文章里的用分布式锁的分段加锁的方式,解决库存超卖问题,那如果一个分段的库存不满足要购买的数量,怎么办?
第一,我当时文章里提了一句,可能没写太详细,如果一个分段库存不足,要锁其他的分段,进行合并扣减,如果你做分段加锁,那就是这样的,很麻烦。
如果大家去看看Java 8里的LongAdder的源码,他的分段加锁的优化,也是如此的麻烦,要做段迁移。
第二,我在那篇文章里反复强调了一下,不要对号入座,因为实际的电商库存超卖问题,有很多其他的技术手段,我们就用的是其他的方案,不是这个方案,以后有机会给大家专门讲如何解决电商库存超卖问题。
那篇文章仅仅是用那个例作为一个业务案例而已,阐述一下分布式锁的并发问题,以及高并发的优化手段,方便大家来理解那个意思,仅此而已。
第三,最后再强调一下,大家关注分段加锁的思想就好,切记不要对号入座,不要关注过多在库存超卖业务上了。
END