时序数据从分表到分库
这里的时序数据泛指一切随时间推移而不断增长的数据,比如通话记录、银行交易记录等。
对于数据库来讲,时序数据并没有什么特殊性,可以和普通数据一样放在数据表中。不过,因为不断增长,积累时间较长后,这种数据的量常常都会很大。一个物理表的数据量太大时,就会影响查询和计算的性能。
现代数据库一般都提供有表分区(PARTITION)的机制,就是把一个大表纵向(按行)分成若干区段,分区规则由数据库管理员来设置,对应用程序员来讲是透明的,可以和不分区的表一样访问,数据库会自动根据查询条件决定读取哪些分区的数据,这样的接口体验非常好。
不过,在实战中,分区表的效果在某些场景下并不好,而且使用时也有些约束条件,并不总好用且能用的。结果,在实际业务中,我们常常会看到对于这种大数据采用手工物理分表的方案。
所谓物理分表,就是人为将一个大表分成若干较小的物理数据表。因为时序数据的结构中一定会有一个字段来表示事件发生的时刻,而事件发生的数量一般来讲也会按时间段相对平均分布(大多数情况会缓慢增长,但讨论时可以忽略),所以最常用的方案就是按时间段来做分表,比如一个月数据对应一个分表,这种方式在金融、电信行业比较普遍。
物理分表并不是数据库自动支持的方案,不能对应用程序做到透明,需要应用程序自己处理。在查询数据时一般都会有时间段参数,应用程序可以根据这个参数计算出该查询涉及哪些分表,然后将这些分表 UNION 起来拼到 SQL 语句的 FROM 后面。查询不涉及的时间段对应的分表不会被拼进来,这样就可以有效减少数据遍历的范围,从而提高性能。
这个方案在单个数据库时没啥毛病,但是不是能推广到多个数据库的情况呢?
数据量再大下去,一个数据库也无法承受了,而某些场景下又不允许我们上一套分布式数据库系统,毕竟分布式数据库是个沉重的工程,不仅造价高,而且维护管理都要复杂不少。这时候,我们可以摆多个数据库分别存储数据,类似物理分表的方案,也按时间段把数据分拆到各个数据库中,比如一年数据放入一个数据库中(一般来讲多个库会部署到多台机器上),这样就能分摊查询压力了。
这首先会有一个查询范围的问题,如果查询的时间跨度超过了一个物理分库时,这时候就不能象分表时那样用 UNION 拼起来了,数据库无法执行跨库的 SQL 语句。不过,这个问题还不算严重,只是查询明细数据时,要把各个分库的返回数据拼接起来,这并不算困难。甚至,要求前端查询范围必须落在一个分库内也不为过(比如必须先选择查询年份),因为一个分库的数据量并不算少,这样用户体验略有损失,但也可以容忍。
这种方案还会有压力不平衡的问题。
对于时序数据,近期数据的查询频繁度远远高于远期数据,大多数查询都集中在最近一段时间中,存放近期数据的分库上任务就很重,并发较多时仍然会有性能瓶颈,而存放远期数据的分库却几乎没事干,并不能有效分摊查询压力。
还有别的办法吗?
可以采用蛇形分布。比如将多年数据分拆到 10 个分库中,可以按日期拆分,所有年份中 1 月 1 日的数据放到 1 号分库中,1 月 2 日的放到 2 号分库,…,1 月 10 号的放到 10 号分库,1 月 11 号的再从 1 号分库轮回,…;其它情况的具体分法也可以根据时序数据的时刻字段的分布情况来决定。
这样分下来,每个分库存储的数据量差不多也就是 1/n,相对比较平均,还可以规避前面说的数据缓慢增长导致的不平衡;而且,无论近期数据还是远期数据的查询都会被分摊到各个分库中,看起来能够充分利用硬件资源了。
还有点注意事项!
蛇形分布时,每个分库中都有所有年份的数据,几乎每个查询都会涉及到所有分库的数据,不能只挑出某些分库来执行运算,这和前面说的分表方案的优化原理并不一样了。我们需要在分库中继续做分表,查询确实会涉及所有分库,但只涉及分库中的某些分表,这样仍然可以有效的减少查询范围,同时利用分库并行的优势。
第二个问题:每个分库都可能返回数据,应用程序需要把这些数据再做一次汇总,而不能象单库分表那样用 UNION 推给数据库去完成。对于常见的明细查询,那只要简单拼接再排序就可以了,开发起来并不难;但如果涉及到分组汇总就会麻烦很多,应用程序员并不擅长编写这种运算,这时候最好借助集算器这类外部计算引擎来协助实现跨库汇总运算。
当然,成本和条件允许时直接上分布式数据库就更简单,分布式数据库采用 HASH 方案基本上可以被理解成是蛇形分布的。