流是一个有方向感的汉字,并且给人轻便迅捷的感觉。TDengine( Time Series Database ,TSDB) 产品最初的灵感之一,便是和“流”字相关:一台物联网设备便是一条数据流,十万台设备便是十万条数据流。它们像溪河汇聚一样,每秒每分源源不断地流向了数据处理平台。
面对这样规模的大数据挑战,TDengine 选择充分地利用时序数据本身的特点(可参考:https://mp.weixin.qq.com/s?__biz=MzIzNzg5MTcxNA==&mid=2247483956&idx=1&sn=86c55c40e935acd18835fec764fd7767&chksm=e8c0fbd9dfb772cf4e102a62498b034c6407cd9ea2c47d209b3e737800676cea7a108bf1d9d1&scene=21#wechat_redirect),来针对性地设计存储引擎。最终的目标其实就是:高效持续地吞吐、消化这些数据流,让数据能够无延迟地产生价值。
可以说,我们追求的便是“流”一般的产品能力。而关于文章标题的答案,本文将从 TDengine 的存储引擎的变化史说起:
由于认为时间序列拥有天然递增属性,所以在最早期的 1.6 版本中,TDengine 是不支持对乱序数据的处理的,当时乱序数据写入表中后,系统会做报错处理。但经过用户实际场景的磨练后,我们不得不把注意力聚焦在这个“害群之马”的身上。在生产环境中,由于设备损坏,网络延迟等原因,数据乱序到达是难免的。更关键的是,不管数据乱序与否,用户都有权利自行决定是否处理它,这是一个产品应该具有的灵活度。
它让理想中的数据流“乱”了,但我们却又不能放弃它。
于是,我们立刻在 2.0 版本中增加了数据在内存中的排序和硬盘中的数据子块来支持乱序数据的处理,以此维持了数据的完整性和有序性。
但是这对于 2.0 版本的流式计算和订阅来说,仍然有类似的麻烦。
当时的订阅/流计算(连续查询)是基于查询引擎的产物,它依靠连续不断地执行 SQL 查询结果作出实时反馈。以订阅为例,每条符合要求被消费掉的数据的时间戳,都会被记录下来,从而作为下次 SQL 查询中时间范围的起始点。这时,即便是新数据的时间戳只比这个记录早一秒,也不会被 SQL 轮询到。因此可以说, 2.0 的流计算(连续查询)/订阅同 1.6 一样,它只处理了有序部分的数据。本质上来说,由于 SQL 取到的只能是已经入库的数据,所以这个阶段的查询行为是滞后的。
因此,我们决定在 3.0 再次进行优化重构:
虽然数据的时间戳有乱序,但是他们到达数据库的时间永远分有先后,这组序列相当于“数据入库时间”。不过这个“数据入库时间” 不是时间戳,而是一个从 0 开始的整型数字,我们称之为“版本号”,代表的是数据库概念是“第 x 次的数据变更”。熟悉 WAL 概念的伙伴们都知道,TDengine 利用 WAL 技术来提供基本的数据可靠性:每一条 WAL 信息代表的是一次数据库的变更(增删改),所以每一条 WAL 信息都会有唯一的版本号。通过“版本号”,我们可以明确地告诉流计算/订阅该数据是否为新增数据,再通过对 WAL 的这组“版本号”创建索引,我们就可以快速定位要消费的数据了。
以订阅为例:3.0 的订阅引擎为时间驱动,它会解析每一条新写入的 WAL 的消息内容,然后判断是否匹配 topic 的 sql 条件,如果匹配则直接把 WAL 的消息转化成数据返回给用户 。
除了用于订阅,这组版本号还有很多十分重要的作用:
- 它本身核心的职责是 raft 日志的编号,用于多副本的数据同步;
- 把版本号下发给每行数据,以追加写入的形式完成数据的更新与删除。
比如:某条 wal 消息的内容是写入一行数据:
insert into d1 values ("2022-03-10 08:00:00.000",100);
那么这行数据就也拥有了这条 wal 消息的版本号(假设为 1) ,并且永久性地存储在数据文件中。
解下来的一条 wal 消息的内容是以相同时间戳更新这行数据:
insert into d1 values ("2022-03-10 08:00:00.000",200);
消息的版本号便是 “2”,这条数据会以写入的形式追加存储。查询的时候,在时间戳相同的情况下,通过比对版本号大小来选择最新的数据,因此我们得到的数据便是:”2022-03-10 08:00:00.000″,200。
删除同理,被删除的数据是取版本号较大的空数据,这样便可在不破坏原有数据文件结构的情况下实现高效的删除。以上部分具体实现细节可参考:https://mp.weixin.qq.com/s/imECB9dIFxZKeoaF3uoOgg
3.另外,当 follower 副本的数据落后,但 leader 上的 WAL 日志已经消失的情况下,版本号还可以做数据文件的快照。只把增量的数据传 输过去,大大节约 data recovery 的时间。
通过这些大刀阔斧的重构,TDengine 完美地把上面几大模块缝合了起来:
- 解决了流计算、订阅对于乱序数据处理的不支持;
- 让删除操作为逻辑删除,解决了删除操作的性能问题;
- 基于版本号引入了快照机制,大幅优化了特定场景下的数据恢复的速度。
回到标题上,为何 TDengine 用户应尽快切至 3.0 版本?答案已经都写在上面了。通过下沉到海量用户实际场景中反复迭代优化,从 1.6 到 2.0 再到 3.0, TDengine 从底层结构上解决了最初设计的瑕疵,各个模块之间结构变得更加清晰,衔接也更加自然,这样扎实的底层设计对产品未来的稳定和性能提升所带来的帮助都是巨大的。