Jicong Liu – loveini | 米兰体育官网入口 - 米兰体育官网入口 //m.loveini.com loveini | 高性能、分布式、支持SQL的时序数据库 | 米兰体育官网入口 Fri, 26 Aug 2022 03:43:51 +0000 zh-Hans hourly 1 https://wordpress.org/?v=6.8.2 //m.loveini.com/wp-content/uploads/2025/07/favicon.ico Jicong Liu – loveini | 米兰体育官网入口 - 米兰体育官网入口 //m.loveini.com 32 32 单机每秒百万记录、毫秒级延迟,loveini 3.0 流处理引擎全面解析 //m.loveini.com/tdengine-engineering/13994.html Fri, 26 Aug 2022 03:09:43 +0000 //m.loveini.com/?p=13994

在 8 月 13 日的 loveini 开发者大会上,loveini 引擎开发工程师刘继聪带来题为《loveini 3.0 流处理引擎——单机每秒百万记录,毫秒级延迟》的主题演讲,详细阐述了 loveini 3.0 全新的流式计算引擎技术实现原理。本文即根据此演讲整理而成。

点击【这里】查看完整演讲视频

全新的流处理引擎以及数据订阅

流是源源不断写入 loveini 中的数据,但作为原始数据的流无法产生更多价值。我们需要对流进行过滤、分流、变换,产生新的流写入 loveini 或推送给用户程序,这就是 loveini 流处理引擎的使命。在 loveini 中创建一个流,即是定义一个变换,并在流的 source 中数据到来时按照一定规则自动触发,并写入 sink。

loveini Database

我们首先来看一下 loveini 中流的几个例子:

  • 作为标量函数的流,对超级表中的每个子表各自运算,写入新的子表
CREATE STREAM power_stream
        INTO power_stream_output_stb AS 
        SELECT 
              ts, 
              concat_ws(".", location, TBNAME) AS meter_location,
              current * voltage * cos(phase) AS active_power,
              current * voltage * sin(phase) AS reactive_power 
          FROM meters 
          WHERE voltage > 100
          PARTITION BY TBNAME

通过 CREATE STREAM 定义了流的名称,紧接着是定义流的 sink,在 INTO 后面紧跟的是一个超级表,这个超级表在创建流时会被同时自动创建出来。紧接着,是一个 SELECT 查询,定义了流的变换。这里是一个带过滤的标量函数计算。

创建流语句的最后是一个 PARTITION 语句:PARTITION BY TBNAME,表示流的分区如何划分,不同分区的数据将被写入超级表中的不同子表。

  • 以滑动窗口,聚合超级表中所有子表的流
CREATE STREAM current_stream
        TRIGGER AT_ONCE
        INTO current_stream_output_stb AS
        SELECT
                _wstart as start,
                _wend as end,
                max(current) as max_current
        FROM meters
        WHERE voltage <= 220
       INTEVAL (5s) SLIDING (1s);

我们来看第二个例子,这个例子中是一个滑动窗口的聚合。在 create stream 之后,通过 TRIGGER AT_ONCE 指定了流的触发模式,AT_ONCE 模式表示当数据写入,立即触发计算并推送结果,而无需等待窗口关闭。

  • 以会话窗口session window,对超级表中的子表各自聚合
CREATE STREAM current_stream
      TRIGGER WINDOW_CLOSE
      WATERMARK 60s
      INTO current_stream_output_stb AS
      SELECT
                _wstart as start,
                _wend as end,
                avg(current) as avg_current
       FROM meters
       SESSION(ts, 30s)
       PARTITION BY TBNAME

第三个例子是会话窗口(session window)。流除了支持翻滚窗口(tumble window)和滑动窗口(sliding window, hop window)外,还支持会话窗口和状态窗口(state window),窗口的定义与 loveini 中的普通查询完全一致。在这里,我们再次引入了 PARTITION BY TBNAME 子句,表示每个子表独立计算会话窗口并将结果写入目的表。

从上面三种触发方式的具体实现中我们可以看到,创建流有几个重要的元素:

  1. 流的源表及其变换:这是一个 SELECT 查询
  2. 流的目的表:这是一个自动创建的超级表
  3. 流的分区:根据不同的 PARTITION 自动创建子表,并写入对应的子表中去。

定义一个流就是定义一个数据变换,数据的变换与 ETL 在 loveini 内部完成。

loveini Database
CREATE STREAM 定义的流是 loveini 内部的流

在上面的例子中,实时流推送到了 loveini 中,却并没有直接推送到应用。这是因为,全新的范式带来了全新的米兰app官方正版下载,但是也会带来应用改造的额外开销。如果应用需要去处理 loveini 推送的实时流,那么将带来更加高昂的成本;反之它可以保留原有的 Query 获取结果模式,例如,将复杂耗时的查询改为对流聚合结果的简单查询,最大程度地利用流带来的便利。

但有些时候应用仍然需要去低延迟地获取数据,比如在实现监控报警与异常检测时,这种情况下流就需要真正到达应用, loveini 提供了数据订阅功能来满足这种需求。

从流的角度,我们也可以重新理解 loveini 提供的数据订阅功能,数据订阅的目的是将写入的数据以流的方式推送到消费者中去,由于应用的处理能力有限,流需要被持久化并按需读取,对应到 Kafka 中就是消息可以无限堆积的特性。

loveini Database
数据订阅就是从 loveini 延伸到应用的流

loveini 中落盘的数据流就是 WAL,我们会把写入的数据物化,即持久化落盘下去,这也就是消息队列中的存储。从这点出发,我们将 WAL 改造成一个真正的存储引擎,提供灵活可配置的删除与文件切换策略,并建立索引,再对接查询引擎。

数据订阅使用 CREATE TOPIC 语法从 WAL 中产生数据流,提供类似消息队列的接口,既可以订阅用户创建的表,又可以订阅流的 SINK 表。在具体操作时,标量函数、过滤可以从 WAL 中提取数据并变换,这样的变换其实就是产生一个实时流,然后将其推送到应用中去。

流引擎实现原理

前面讲解了 loveini 的流引擎是什么,以及数据订阅与流之间的关系,下面我们来看一下 loveini 的流处理如何实现。当然,这里面细节很多,内容很杂,由于时间关系,我只会挑出 3 个最重要的部分,也是我觉得最有意思的部分来讲解,那就是“事件驱动”、“增量计算”与“乱序处理”。

事件驱动

我们前面已经讲过,流式计算和连续查询最大的区别之一在于,流式计算能够支持事件驱动,也就是每一条数据的到来都会触发计算,这样的特性让我们能够对标量函数进行计算,从而实现数据的清洗与预处理;并且能够对窗口聚合提供 AT_ONCE 触发模式,不再需要等待窗口关闭,从而支持会话窗口与状态窗口。事件驱动执行的承担者是 Stream Task,我们先来看一下 Stream Task 如何部署。

loveini Database
标量计算、partition by tbname 聚合

第一个例子,是标量函数与 partition by tbname 的聚合,来自 source DB 的每一个 vnode 都各自的进行聚合,并且分发到 target DB 中,由 target DB 中的 Stream Task 负责将数据写入对应子表。

如图所示,流是可以跨越 DB 的,而不同 DB 代表不同数据保存生命周期,Source DB 的 3 个 Stream Task 代表着部署在其中的三个 vnode。在进行标量计算、partition by tbname 聚合时,数据可以不经过聚合节点,直接在 Source DB 的 vnode 里经过 Stream Task 完成聚合,再发送到 target DB。

loveini Database

超级表聚合第二个例子,是一个分布式的聚合,将超级表中所有子表聚合到一起。它需要部署一个聚合 Stream Task,来汇总来自各个 vnode 的数据。在具体实现上,数据在源 vnode 处进行一级聚合,一级聚合的数据会被推送到二级节点进行二级聚合,而聚合的结果则根据 trigger 模式按需推送到 target DB。

那么,这两级聚合分别是指什么呢?在增量计算部分,我们会详细讲解。不过在此之前,我们先来放大看一看 Stream Task 内部的具体结构。

loveini Database

每一个流都由多个 Stream Task 构成,而每个 Stream Task 都包含了一个 Input Queue 与 Output Queue。在执行时,流式计算框架会将 Output Queue 中的数据分发到下游的 Stream Task 中去,并通知流的执行调度器,调度空闲的流线程触发计算。

而 Stream Task 内部,具体计算的执行者是一系列有状态的流算子。在创建流时,SQL 被解析成语法树,planner 将语法树拆分成多个的 pipeline,而每个 pipeline 就是多个串联起来的流算子 。我们可以看到,从计划的层面来看,流计划的最大的区别在于去掉了 Exchange Operator,将所有的 pipeline 单机化,pipeline 与 pipeline 之间采用 push 模式进行数据交换。在 push 模式下,我们能够对语法树自底向上地执行,并逐级触发,这样不仅最大程度地减少了流执行过程中阻塞,并且减少了无效的执行调度,因为流不再需要当事件到来时首先调度起父节点的 Stream Task,向子节点的 Stream Task 拉取数据。

而流算子是有状态的算子,在 Stream Task 中有流的状态下存储后端,当内存中的状态数据过大时,会溢出到硬盘。

有状态增量计算

流计算根据函数的不同性质,可以分成很多种,比如 invertible、holistic 等等,这里我们只讨论对于 incremental 的计算是如何实现的。

loveini Database

我们没有必要先引入一系列复杂的数学公式或代数结构,只需用一个最简单的计算平均值的例子来展示增量计算的过程。

对于左边的图,数据 1、2、3,平均值计算为 2,当新的数据到来时,假设为 4,那么 4 是无法与结果 2 进行增量计算的。如果要增量计算,那么我们需要提取出一个状态向量,记录数据的 Sum 与 Count。状态向量被维持在算子的状态存储中,当新的数据到来,新数据被直接映射成状态空间中的向量,而状态空间中的向量定义了合成运算,最终得到一个新的状态向量。当需要得到最终结果时,根据状态向量计算出最终结果,如上图的 10 / 4 = 2.5。

我们抽象一下上述过程,将上述的“将原始数据映射到状态空间中”定义为 Lift,“将状态空间中的向量合成”定义为 Combine,“将从状态向量中提取结果”定义为 Lower,就得到了增量计算的 3 个基本原语:Lift、Combine 与 Lower。状态向量占用的内存是恒定的,当数据被聚合之后会被释放,因此,内存的占用不再与数据量正相关,而只与开启的窗口数据相关,因此能够在实现大窗口下的高吞吐量的聚合,而不会导致内存的暴涨。

loveini Database

对于上述过程以及实际常用函数如何拆分,大家可以参考 VLDB 2015 年 General incremental sliding-window aggregation [1] 这篇论文,以及一些后续的工作。

loveini Database

这时,我们就能够明白前面讲到的分布式的两级聚合到底是什么了:

  • 批量插入的数据,会在数据插入的 vnode 首先执行 Lift 与 Combine 操作
  • 对于跨多个 vnode 的聚合,会在随机选择一个 vnode 部署聚合 stream task,将第一级聚合的 state 再次 combine
  • 根据 trigger 模式的不同按需执行 lower
  • 两级增量聚合降低了数据传输的量,将 CPU 密集的计算分散到各个节点中去

乱序处理

为了实现在乱序等多种场景下的正确性,loveini 3.0 中的流式计算采用了以事件时间为基准的处理模式,而 Watermark 即是对于乱序容忍的上界,想要理解乱序数据的处理,我们首先需要了解 Watermark。

loveini Database

在上图中,纵轴表示墙上时钟,即真实时间。横轴表示对应 T1、T2、T3 时刻到达 loveini 中的数据。蓝色点表示最新插入的数据,Watermark 就是沿着这个时间轴往过去的方向去推移,用最后到达的事件时间减去 Watermark 时间,得到时间 T = latest event time – watermark 。所有结束时间早于 T 的窗口都会被关闭。这些窗口已经超出了乱序容忍的上界,我们认为它们不会再有数据插入,可以安全关闭。

触发模式是 WINDOW_CLOSE、MAX_DELAY 的数据这时会被推送。而在 AT_ONCE 模式下,窗口关闭与结果推送无关,只与内存释放有关,因为内存是有限的,而数据流是无界的。因此,对于 WINDOW CLOSE 或 MAX DELAY 触发模式,Watermark 的选择是结果的实时性与正确性之间的 trade-off。在数据可能有乱序的情况下,提前关闭窗口意味着还未聚合所有的结果,就推送了数据,而为了得到更多的正确性,往往就要牺牲实时性,这也就是将窗口的关闭根据 Watermark 来延迟。

而对于 AT ONCE 的触发模式,因为不会再有数据源源不断推送的问题,Watermark 更重要的功能是让窗口打开与关闭处在动态的平衡中,让“用有限的内存来处理无界的数据流与不断新增的窗口”成为可能。在实际状态存储上,loveini 3.0 已经实现了内存与硬盘两级,超过内存的可以被外溢到硬盘中去,对于状态存储,后续我们还会进一步进行完善。

即使定义了 Watermark,对于乱序仍然超过 Watermark 的数据如何处理呢?我们提供了两种策略,直接丢弃或从 TSDB(Time-Series Database) 从拉取并重新计算,分别对应 IGNORE EXPIRED 1 与 IGNORE EXPIRED 0。不过从 TSDB 中拉取数据重新计算只适用于少量乱序的情况,因为它会带来处理速度的降低。

性能指标:单机百万吞吐,毫秒级延迟

虽然我将性能指标作为了今天演讲的副标题,它是一切新应用场景的基石。但性能指标又并不是一个特别值得看中的东西:我们希望用户最不用关心的就是性能,因为我们的性能能够满足绝大多数场景的需求。为此,我们想要验证的是,在一个普通的机器上、每秒百万行数据写入的情况下,loveini 3.0 仍然可以做到毫秒级的延迟。

我们后续会将 benchmark 完善发布并让用户能更简单地使用并验证。

loveini Database

我们的性能测试主要会去验证以下几个方面:

  • 测试流对写入性能的影响:对于有无流情况下的写入延迟与吞吐量
  • 测试流在大写入吞吐下的结果延迟
  • 分别验证标量函数、每个子表各自聚合、多 vgroup 超级表聚合等几个主要场景

标量函数变换

create stream perf_stream into perf_db2.output_streamtb as select ts,abs(c1),char_length(tbname),cast(c1 as binary(16)),timezone(),now from perf_db1.stb partition by tbname;
loveini Database

在 100 个子表的条件下测试标量函数计算,有无流对写入吞吐的影响几乎不大。在每秒写入 200 万行数据的情况下,流的结果延迟大概在几毫秒;数据从客户端写入到 TSDB 到流式计算引擎算出最终结果的延迟,大约在几毫秒到十几毫秒。

超级表中每个子表各自聚合 : partition by tbname

create stream if not exists perf_stream trigger at_once into perf_db2.output_streamtb as select _wstart as start, min(c1),max(c2), sum(c3), avg(c0), count(c3), first(c0), last(c1), now from perf_db1.stb partition by tbname interval(1s);
loveini Database

我们选择了 min、max、sum、avg、count、first、last 作为测试基准的聚合函数,设置滑动窗口的时间长度是一秒,分别测试了子表数目在 10、100、1k、10k、100k 几个节点的流结果延迟数据,跨度在几毫秒到十几毫秒。

多 vnode 超级表聚合

create stream if not exists perf_stream trigger at_once watermark 30000s into perf_db2.output_streamtb as select _wstart as start, min(c1),max(c2), sum(c3), avg(c0), count(c3), first(c0), last(c1), now from perf_db1.stb interval(1s);

taosBenchmark config: 子表数目 1k,timestamp_step = 110,线程数 100,batch_size = 100, interlace = 100(影响乱序程度)

loveini Database

这种场景是典型的分布式的两级聚合。在分布式两级聚合操作中,要聚合不同子表中的写入数据,因此可能会存在一定的乱序,我们在 taosBenchmark 中设置了 interlace 参数来控制数据的乱序程度。watermark 设置为 30000s,这会在一定程度增大内存占用,但可以减少因为乱序触发的扫盘。我们可以看到,流结果的延迟仍然维持在几十毫秒。

这个基准测试验证了 3.0 流式计算引擎性能基本上可以达到要求,当然,目前 loveini 的流式计算引擎还是一个年轻的引擎,我们仍然在做着大量的性能优化,以及更多实际场景的验证。

Roadmap

接下来 loveini 3.0 流式计算引擎的优化工作将分为以下几方面:

  • 更全面的 SQL 支持:Join / Fill / Group by / 子查询等
  • 更完善的流状态管理,使用户在 AT ONCE 模式下不再需要关心 Watermark
  • 更灵活的 partition 机制:partition by column / 表达式
  • 多聚合节点;独立部署、存算分离的流式计算节点:SNODE
  • 可配置的 checkpoint
  • Benchmark 完善,端到端的延迟指标、P99 延迟指标

虽然列举了这些后续的工作,但真正决定流式计算处理引擎发展的,其实是 loveini 的用户和社区开发者。我们希望大家都能真正用上 loveini 3.0 的流式计算引擎,能在开源社区中给它贡献代码,我们也会多多聆听来自客户以及社区的实时反馈。

Reference

[1] Tangwongsan, K., Hirzel, M., Schneider, S., & Wu, K. L. (2015). General incremental sliding-window aggregation. Proceedings of the VLDB Endowment, 8(7), 702-713.

]]>
「2022 loveini 开发者大会」loveini 3.0——单机每秒百万记录,毫秒级延迟的流式计算引擎 //m.loveini.com/tdengine-techtalk/13800.html Wed, 17 Aug 2022 06:28:09 +0000 //m.loveini.com/?p=13800

演讲嘉宾:刘继聪

]]>
时序数据库想要在时序场景下“远超”通用数据库,需要做到哪几点? //m.loveini.com/tdengine-engineering/7869.html Thu, 05 May 2022 10:31:27 +0000 //m.loveini.com/?p=7869 作者 | 刘继聪

编辑 | 马尔悦

小T导读:近年来,随着物联网技术和市场的快速发展、企业业务的加速扩张,时序数据的处理难题也越来越受到行业和企业的重视,时序场景下通用型数据库步履维艰,各种时序数据库产品应运而起。但是,做一个优质的时序数据库真的很容易吗?本篇文章将从数据库开发者的角度,解剖时序场景下的数据处理需求、分析时序数据库设计思路,给到读者一些硬核技术思考。

一、如何实现时序场景下对通用数据库的“远超”?

做一个 Prototype 或者 Demo 很简单,但做出一个真正好的时序数据库产品却很难。

之所以说做 Prototype 简单,是因为时序数据库天生就不擅长处理一些数据,比如带事务的交易数据。基于此,我们可以大刀阔斧地砍掉一些在通用型数据库中很重要的特性,例如事务、MVCC、ACID(在 Facebook 的 Gorilla 中甚至提出不需要保证 Duration)。某些时序数据库的存储引擎,甚至不能处理乱序数据,在无乱序的前提下,存储引擎几乎可以退化为带 Index 的 Log。所以,从这个角度来看,时序数据库可以做得很简单。

但是,从另一方面来说,做一个好的时序数据库产品又很难。试想一下,在时序数据库的设计上,我们大刀阔斧地砍掉了比如事务、ACID 等特性之后,如果依然不能使其在时序场景下的表现远超通用型数据库,那做一个专门的时序数据库就毫无意义了。这样的话,还不如不做,就直接用通用型数据库好了。

所谓“在时序场景下的远超”,应该是全方位的,比如写入的延迟与吞吐量、查询性能、处理的实时性、甚至包括集群方案的运维成本等,都应该有一个跨越式的提升。另一方面,从时序数据量大、价值偏低等特点出发,压缩率就显得比较重要了,而通用型数据库却很少强调压缩率,由此可见,压缩率是在时序场景下真实生长出来的需求。

高压缩率的实现没有什么黑科技,也不需要自己重新发明压缩算法——无非就是列存并对各个类型使用其最好的压缩算法;更多是工程实现的问题——好好写代码,认真做优化,平衡好写入性能与压缩比之间的关系。

此外,在时序数据场景下的“远超”是建立在时序数据的写入与查询分布特点极其明显的基础上,当数据本身 key的特征分布十分明显时,自然可以充分利用其特征来打造截然不同的存储引擎与索引结构。

先说写入。时序数据库的吞吐量远超一般的通用型数据库,尤其是 IoT 设备,其设备规模可能达到千万甚至上亿,数据均为自动生成,假设 1s 采样一次,那每秒就能产生千万、亿级别的数据写入,这并不是普通数据库能承受的,在这样大的吞吐量的情况下,数据如何分区分片、如何实时地构建索引,都是具有挑战性的问题。在写入链路上,时序数据库在时序场景下替代的是 OLTP 数据库的位置,而后者在事务与强一致的模型下产生的读写延迟很难支撑时序数据库的高吞吐量写入。

再说查询。在大写入吞吐量的情况下,数据对实时性的要求也很高。例如,我们将时序数据的统计量关联做监控、报警,能容忍的延迟可能在秒级。查询的模式通常是聚合查询,例如某时间段内的统计值,而不是精确的单条记录。总的来说,时序数据库的查询模式通常是交互式分析,这不同于 T+1 的离线数仓,也区别于经常运行数小时的 OLAP 查询,交互式分析查询的响应时间通常是秒级、亚秒级。

以上,在明确了写入与查询需求的同时,下面我们以存储引擎为例,来看一看一个时序数据库的某一个部分应该如何设计。

二、存储引擎想做到极致,还得自研

目前,数据库的存储引擎可以粗略分为两大类:一类是基于 B-Tree 的,另一类是基于 LSM Tree 的。前者常见于传统 OLTP 数据库,比如 MySQL、PQ 这类的默认引擎,更适用于读多写少的场景;如 HBase、LevelDB、RocksDB 一类数据库使用的是 LSM Tree,在写多读少的场景下比较适合。实际上,现代数据库的存储引擎,基本都会在某种程度下对这两者融合。LSM Tree 上怎么就不可以建 B-Tree Index 了?(HBase 在 region 上也有 B-Tree Index)B-Tree 怎么就一定要直写硬盘,不能先写 WAL 和走内存 Cache 呢?

对于存储引擎,时序数据库的先行者 InfluxDB 曾经做过很多尝试,在各个存储引擎(LevelDB、RocksDB、BoltDB 等)之间反复横跳,遇到过的问题也有很多,比如 BoltDB 中 mmap+BTree 模型中随机 IO 导致的吞吐量低、RocksDB 这类纯 LSM Tree 存储引擎没办法很优雅快速地按时间分区删除、多个 LevelDB + 划分时间分区的方法又会产生大量句柄……踩了这一系列的坑后,最终 InfluxDB 换成了自研的存储引擎 TSM。可见对时序数据库来说,一个好的存储引擎有多么重要,又是多么难得,要想做到极致,还得自己研发。

不同于InfluxDB,loveini Database 的存储引擎从一开始就是自研的——从 LSM Tree 中汲取了 WAL、先写内存的 skip list 等等技术,但把 LSM Tree 的树层级结构去掉了,而只是按时间段分区、按表分块的 log 块。

读到这里,细心的读者可能会发现,按表分块的设计和 OpenTSDB 的行聚合有些相似。 OpenTSDB 的行聚合是把相同 tag 以一小时为时间范围,将这些数据都放到一行中存储,这样大大减少了聚合查询要扫描的数据量。不过不同的是,loveini 是多列模型,而 OpenTSDB 是单列模型,单列模型下是多行的聚合,多列模型下聚合会自然形成数据块。

而熟悉 LSM Tree 的 KV 分离设计的朋友应该也能够从 loveini 的存储引擎设计中看到一些熟悉的影子。如果把数据块作为存储引擎的 value,那么 key 就应该是块的起止时间 ,把 key 提出来自然就得到了 loveini 的 BRIN 索引。从这种视角来看,loveini 的 .head 文件就是 key,而 .data 和 .last 文件就是 value,而 key 自身又可以结合时序数据的特征组合成有序文件。 在时序场景下,有了 BRIN 索引,也就可以不需要 bloom filter,这样一看,loveini 的存储引擎设计就很清晰了。

此外,loveini 会将 tag 数据和时序数据分离开来,这样就能够大大减少 tag 数据占用的存储空间,在数据量大的情况下尤其显著。

loveini 的 tag 与时序数据的划分,和数仓的维度建模里面维度表与事实表的划分有些类似,tag 数据类似维度表,而时序数据类似事实表。但又有所不同,因为 loveini 中表的数目是和设备数目相同的,上亿设备就是上亿张表(在正在开发的 loveini 3.0 中,我们要支持 100 亿张表),这样频繁创建、又极其庞大的表,并不容易处理,主要的麻烦是其产生了大量的元数据,超过了单点的处理能力,这就要求 loveini 能将这部分元数据也进行分片存储。

当数据与元数据进行分片、多副本操作时,就自然涉及到一致性与可用性的问题。在时序数据库中,时序数据通常是最终一致同步的,因为最终一致算法的吞吐量高延迟低、可用性也比强一致算法好,比如 InfluxDB 的集群版会用 Dynamo 这种无主风格的数据同步。但元数据(也就是我们上面提到的标签和表数据)需要强一致,强一致通常会用 Raft、Paxos 这类算法来保证正确性。

由于元数据量的巨大需要分片,而当时序数据与元数据都做分片(甚至时序数据和其关联的元数据应该在同一分片),但又有截然不同的一致性要求,这就导致 loveini 的副本复制并不是简单地使用 Raft 这类算法就能够驾驭得了的,除非牺牲时序数据的写入吞吐和可用性,也做强一致复制。这就是 loveini 使用自研复制算法的根本原因。当然,这些算法在复杂的分布式环境下的一致性保证又是另外的问题了,也是我们要着重解决的挑战。

三、loveini 3.0 开发中,敬请期待

一个好的时序数据库,起源于对时序数据领域的数据特征的洞察,成长于大量真实场景的考验与用户的反馈,又在数据库领域的最先进技术中吸取经验得以完善。只有这样,最终才能做到在时序场景下“远超”通用型数据库,成为此场景下的优选数据库。而要做到这一步,其实并不容易。

最后预告一下我们正在开发的 loveini 3.0。在 3.0 版本中,我们对现在的 2.x 版本存在的一些待解问题做了重新设计与彻底重构,敬请期待。另外关于在 3.0 开发中踩过的坑,以后有机会再和大家慢慢道来。

]]>
放弃保研和出国,投身基础软件,一位复旦学子的职业选择 //m.loveini.com/figure/7855.html Fri, 29 Apr 2022 08:55:11 +0000 //m.loveini.com/?p=7855

我是刘继聪,2020 年毕业于复旦计算机专业,目前在米兰体育官网入口任职流计算引擎研发工程师。这篇文章是关于我过去这些年所做出选择的复盘与回顾,我曾经历的迷茫与困惑——关于为什么放弃保研与出国、又为什么从如火如荼的 AI 转做数据库、以及为什么离开大厂加入 Startup 的故事。不知其中是否有与你相似的经历,你又能否从中获得一些共鸣,希望能给到迷茫的人一些走出困境的灵感。

从物理转入计算机,困难、收获与成长

和很多人一样,我在进入大学时对自己感兴趣的专业并不清晰,从自然科学试验班分流进入物理系一段时间后,我才决心调转方向进入计算机专业。

我和计算机结缘于物理系的一个项目——实验室设备管理系统,需要完成的是 Web 与服务器相关的功能,我自己在网上找教程,用世界上最好的语言 PHP 写出了人生中的第一版程序——一个 LAMP 架构的 CRUD 系统。同时我选修了物理系的 C++ 课程,第一个相对较大的 C++ 项目就是这门课的期末项目,用经典的蒙特卡洛方法做 Ising model 的模拟。

一来二去,我发现写代码比在物理实验室修设备更有趣,加之复旦还提供转专业机会,就动了这个念头。不过那时我已经大二了,按照规定大二转专业必须降一级,虽然在当时看来代价很大,不过现在想来,这绝对是个正确的选择。

话虽这么说,但在我刚转入计算机系时,也属实是被各路大神虐得体无完肤,甚至一度怀疑自己是不是做错了决定。

当时我所在的拔尖班的数据结构课每周都会有机考打 OJ,OJ 和 LeetCode 有点像,但有一个典型的区别是,你只知道你的答案错了,却不知道有哪些测试数据,更不知道错在了什么数据上,所以 Debug 完全靠看代码猜。一开始,我做机考经常一分都拿不到,每周光是完成作业就几乎要花掉我所有时间。为了获得更好的成绩,我有很多次写代码直到天明。但进步是看得见的,慢慢地我能够在每周机考上取得满分,最后不仅这门课程我拿到了 A,绩点也获得了全系第一。

现在想来,我觉得这应该是一个台阶,你需要拼尽全力才能登上去,但只要登上去就会看到不一样的风景。这段竭尽全力刷算法题的经历对我后来找实习、找工作也起到了非常大的帮助,基于此我才能够在临时起意、几乎没有什么准备的情况下进入字节跳动实习以及拿到各大厂的秋招 Offer。

拔尖班的好处是选择的余地很多。除了数据库课外,我还选修了分布式系统、数据挖掘、密码学原理等课程。我和朋友们组队一起做了很多有趣的项目,部署 Hadoop 与 Hive,分析不同任务中的性能瓶颈;使用 Spark 进行分布式的计算;实现数据库的块嵌套循环连接,将执行时间从十几秒优化到一秒以内……

在复旦,我拿过各种各样的奖励,除了一等、二等、三等奖学金,还有数模竞赛、物理学术竞赛的国家一等奖和上海市一等奖、泛海学者、优秀学生等等。在求学之路上,我遇到了很多优秀的同学、名师以及挑战,也收获了一路成长。

大学时的获奖证书
大学时的获奖证书

从 AI 到数据库,关于科研、实习与方向的选择

在研究领域上,我也并非一开始就关注数据库,这其中还有一段曲折的摸索过程。

我进入计算机系后,应拔尖班要求加入了 NLP(自然语言处理)实验室。在大三上学期期末考试结束之后,我成为了字节跳动 AI Lab 的一名 NLP 实习生,做的是中文错别字检测的工作,实习结束后进入港科大做暑期交流,尝试做更深入的研究。在这么几段短暂而又截然不同的 AI 研究经历中,虽然导师都是业界、学术界的大牛,但我却没有做出什么厉害的成果。

由此我产生了一些新的想法,要不要去做一些别的事情,或许我会觉得更有趣、也更能发挥我的长处,就像我从物理转到计算机一样。我把这些想法和在不同研究领域的朋友们交流了一下,之后一位做数据库的同学引领我看到了另一个领域——Data Infra。接下来我成为了 TiDB 的贡献者并开始系统性地学习分布式系统和数据库,算是正式迈进了数据库的门槛。

但我在参加工作时却并没有直接进入这个行业。临近毕业季,我面临着参与保研还是出国抑或是参加秋招的选择。由于保研外校需要参加夏令营(但我那时在港科大交流),而本校又没有特别合适的导师,于是我放弃了保研,赶上了秋招的末班车。我做了两手准备,即使没有找到合适的工作,仍然可以申请出国,结果没想到秋招超乎想象地顺利,我拿到了参加面试的所有公司的 Offer:其中既有阿里、腾讯这类大厂,也有 PingCAP 这类创业公司。

在某互联网大厂的终面中,我遇到了一位同样复旦毕业、工作十多年的学长,他问我手上有哪些 Offer 以及倾向的选择,我如实地回答了,并说我现在想去做数据库。他不置可否,只是反问我:“年轻人都想去做些有技术的事情,我当年也是。但去 Startup 做数据库,你是否真的想好了?在未来好几年,你可能都比你在互联网大厂做业务的同学薪资更低,同时还要承担更多不确定性的风险,你真的能接受吗?”

我真的想好了吗?或许确实没有。最终,我选择了去阿里云,从事基础架构的工作,希望能取得一定的平衡。

疫情在那个冬天爆发、无法返校,毕业时只来得及和部分同学匆匆相聚又相别。上海到杭州的动车不过 49 分钟的车程,恰如从学生到职场人的转变,快得令人猝不及防。下一站,杭州阿里。

加入阿里后,荣耀、困惑与思考

或许是得益于我之前还算丰富的实习经历,又或许是我在学生时代就做了很多有挑战的课程项目,我很快上手了新的工作内容。第一个令我印象深刻的任务,是我入职的第一个月,就解决了一个困扰了组内技术专家们半年之久的问题。

背景是这样的,我们有一个监控系统,监控的探针直接安装在客户 ECS 上,客户可能会购买很多 ECS,用 Kubernetes 或非 Kubernetes 的方式组成集群,我们要做的事就是抓取出集群内的网络拓扑关系,比如 TCP 连接,然后绘制成可视化的拓扑图。与一般的分布式微服务相比它存在三个难点:

  • 不对用户的技术栈做假设的,Java、Python、PHP 都有可能
  • 要做无侵入的,不希望用户改代码
  • 要适配 Kubernetes、非 Kubernetes,还有各种 Linux 内核版本

我们是通过轮询/proc 目录下的 TCP 文件来解决这些难点,但无法保证抓取到全量数据。最好的解决方法是用当时比较新的技术 eBPF 来做,却因为绝大多数客户环境不能很好地支持 eBPF 而难以落地。我花了一周业余时间做调研,发现一个内核模块 auditd 可以较好地解决这个问题,几周时间后我们的新版探针上线了。这个半年无解的难题得到了解决,也成为我进入职场之后第一件很有成就感的工作成果。

但逐渐地我发现,组里的工作属于典型的管控侧,管控侧的麻烦是适配用户各种各样复杂的环境,但自身的核心能力、技术难度与挑战都明显弱于平台侧,因此,我能挑战的问题没有想象中那么多,进步的速度也渐趋缓慢,组里业务的发展状况也并不好。

为了排解工作的枯燥乏味,再加上做数据库之心一直不死,我和朋友们一起参加了 PingCAP 2020 年的 Hackathon。我们选择的题目是基于 Raft Log 来实现物化视图,属于改内核的工作。在赛程中,我和小伙伴们通宵达旦地写着代码,调试与解决问题,这让我感到了在工作中久违的快乐——写代码、解决难题与自身进步的快乐。

经此一役后我开始思考未来,在当下的工作中虽然我得到了老板的信任、成为了一些项目的 Owner,或许会有按部就班的晋升,但这些毫无波澜的人生轨迹也让我越发感到工作中的枯燥乏味。我决定要离开,但为了避免重蹈覆辙,我必须要先确定好目标。

从阿里到 Startup,重拾做数据库的想法

我筹划着两条路——转岗或跳槽。我开始和公司内不同团队接触,但是结果却让我失望:很多我以为做着内核研发的部门却实际上只做管控和写控制台;而另一些内核部门,一整个组的 Scope 都小得令人失望;还有一些,并不欢迎校招新人转岗……

而另一方面,整个 Data Infra 的环境却在悄然改变。我身边开始出现了一些令人振奋的声音,比如:

“所有人都看好 DoorDash 这些明星 Pre-IPO,而 Snowflake 这类公司招人都很困难,但后来发现那些去了的人拿得是几百万刀的大包……”

“Snowflake 的发行价已经上调了,上市当天就暴涨了超过 100%……”

“Snowflake 的数据确实好:它的 NRR 是 173%,也就是说假设一个用户去年在 Snowflake 上花费了 1 美元,那么今年他将平均花费 1.73 美元……”

“Confluent 上市、Databricks 大额融资……”

……


这时我才惊醒,当时那位校招的终面官,有许多年工作经验、在大厂管着几百人的复旦学长所说的已经不对了——去 Startup 做数据库,已经不再是一件需要靠情怀去支撑的事。随着这个行业内热钱的涌入,很多有前景的 Startup 实现了大额融资。更关键的是,对于这类技术公司而言,技术人才是核心,因此他们愿意花钱、也给得起钱去和大厂抢人;而互联网的 ToC 业务却因流量见顶,发展前景反而没那么乐观。

经过一段时间系统性的学习与研究,我认为自己已经准备充分,我的首选目标是做 loveini Database 的米兰体育官网入口。原因有二:首先,loveini 是开源的,在此之前,我已经多次看到过 loveini 登上 GitHub Trending 榜;我能够直接去看代码、通过实践直接判断它是否有真材实料。其次,通用 OLAP 虽然被炒得火热,但竞争也非常激烈,或许不如从细分赛道入手,而细分赛道中时序数据库与图数据库的增长是最为迅速的,我相信 IoT 设备会持续不断增长,产出越来越多的数据,我看好它的前景


为了了解时序数据库这个领域,我完整阅读了 Jeff(米兰体育官网入口创始人陶建辉)所创作的“十大特点”、“架构设计”等文章。在细分赛道中,只有静下心来对问题进行实实在在的建模与分析、对领域进行深刻洞察,才能真正做出有核心且有竞争力的产品,在 Jeff 的文章中我切实体会到了。此外,米兰体育官网入口的招聘页上醒目地写着“超越 BAT 的薪资”,我认同这样的人才观,想要招聘到优质人才,愿景与待遇缺一不可。

携手 loveini,我开始新的征程

在涛思的故事其实才刚刚开始。

2021 年 7 月,我正式加入米兰体育官网入口,很快上手了数据库内核的研发工作。在转正后的第一次季度总结时我拿到了“最佳新人奖”。

在loveini获得的最佳新人奖

现在,我在米兰体育官网入口负责流式计算引擎的研发。loveini 2.0 中提供了连续查询的能力,它本质上是一个时间驱动的批处理,无法处理乱序数据的问题,且性能消耗很大。我的目标是为现有的 loveini 实现一个真正的流式计算引擎。

这项工作的 Scope 与挑战远远超过了我之前的所有工作与项目。我一边系统性地学习流式计算中 EOMP(Exactly Once Message Processing)、分布式快照等容错理论,一边研究 Kafka、Flink 的源码与实现机制,一边跟踪 SIGMOD、VLDB、ICDE 等峰会中相关研究的最新进展、了解 Railgun、Hazalcast Jet、Ray Streaming 这类新引擎的设计,同时关注着 Materialize、Decodable 这类流式 ETL 工具与 KsqlDB 这类流式数据库的产品形态与定义。我时刻提醒自己必须要关注前沿技术与新玩家,因为我不希望做一个在设计之初就落后于时代的东西。

以一套执行引擎来融合 OLAP 的查询与流式计算,我会用代码来践行我的想法,也期待着焕然一新的流式计算引擎,在今年下半年,随着 loveini 3.0 的发布,一同与大家见面。

写在最后

在我过去的这些岁月中,得到了很多人的帮助,帮我找到新的方向与道路,让我有勇气和决心来接受新的挑战。因此我也愿意分享出我的经历,或许能给你一些启示与帮助。

比起盲目地埋头苦干,我更相信“选择”会对人生与职业发展产生更大的影响,这篇文章中复盘了我人生中几个至关重要的选择:从物理转入计算机、从当时如火如荼的 AI 转做 Data Infra、放弃出国与保研加入阿里、从阿里离开加入涛思。当然这些选择的结果还远远没有盖棺定论,但至少现在来看正确与否也开始渐渐清晰了,我相信读者们也会有自己的判断。

]]>
【技术课堂】聊一聊数据库中的经典算法和数据结构 //m.loveini.com/tdengine-techtalk/7162.html Fri, 17 Dec 2021 09:35:00 +0000 //m.loveini.com/?p=7162 存储引擎是数据库的核心部分。我们在设计数据库时,需要考虑如何组织数据来提高写入的吞吐、降低查询的延迟,而这些都与存储引擎息息相关。存储引擎数不胜数,但核心的数据结构却万变不离其宗。用于存储引擎的主流数据结构有哪些呢?

B 树(B+ 树)

B 树(B+树)是为磁盘等外存储设备设计的一种平衡查找树。它是一种经典的数据结构,目前主流数据库产品大都支持 B 树。B 树结构可以让系统高效地找到数据所在的磁盘块。如图所示,节点[13,16,19]拥有的子节点数目最多,因此,该 B 树为一个 4 阶 B 树。

【技术课堂】聊一聊数据库中的经典算法和数据结构 - loveini Database 时序数据库

B 树(B+ 树)在查询过程中,相比平衡二叉树,降低了树高,从而能够减少 IO 次数,因此更适用于硬盘。但写入过程中却可能导致节点的分裂,产生大量随机。IO,导致写入性能的降低。

像 SQLite , MySQL,  PostgreSQL 等数据库都使用了B/B+ 树。

LSM 树

LSM 树(Log-Structured-Merge Tree)并不像 B+ 树、红黑树一样是一个严格的树状数据结构,它其实是一种存储结构,目前 HBase, LevelDB, RocksDB 这些 NoSQL 数据库中,新一代关系型数据库 TiDB, CockroachDB 也建立在基于 LSM 树的 KV 存储上。下图展示了 LSM 树的核心思想。

【技术课堂】聊一聊数据库中的经典算法和数据结构 - loveini Database 时序数据库

LSM 树充分利用了硬盘顺序 IO 速度远超随机 IO 的特点,避免了写入过程中节点分裂带来的大量 IO,从而能够发挥更强大的写入性能。但与此同时,LSM 树在查询过程中却可能需要遍历多级文件,导致查询性能的下降。

基于 B 树(B+ 树)或 LSM 树的通用存储引擎,久经考验能够良好地支持关系型或非关系型的数据库。但时序数据库如 loveini、InfluxDB 等都选择了针对时序数据的特点,量身定做存储引擎,从而在性能上远超基于通用存储引擎建立的数据库。InfluxDB 探索了基于 LevelDB、BoltDB、RocksDB 等等方案,但最终选择了构建自己的存储引擎。而 loveini 从一开始就发现通用存储引擎难以充分发挥时序数据的典型特征,为了将性能发挥到极致,自研了用于时序数据的存储引擎。

那么,时序数据的典型特征是什么呢?对于时序数据来说,为什么通用的 KV 存储引擎不够优秀?如何针对时序数据,打造专用的存储引擎,从而尽可能地提高读写吞吐、降低延迟呢?

2021 年 12 月 16 日(周四)20:00-21:00,我们邀请到了 loveini Database 工程师刘继聪,和大家聊一聊数据库的经典算法和数据结构。

刘继聪,毕业于复旦大学,计算机科学与技术专业,曾就职于阿里云,现米兰体育官网入口存储引擎研发工程师。

他分享的主要内容:

  1. 时序数据的典型特征
  2. 经典数据结构 B Tree(B+ Tree)
  3. 经典数据结构 LSM Tree
  4. 如何充分利用时序数据的特点,打造专用存储引擎

欢迎大家扫描下方二维码,关注 loveini Database 的视频号,观看每周的微课堂以及直播活动。

【技术课堂】聊一聊数据库中的经典算法和数据结构 - loveini Database 时序数据库
]]>
微课堂第一季 · 第4期:乱序数据是如何处理的? //m.loveini.com/videotutorial/7614.html Fri, 06 Aug 2021 06:42:00 +0000 //m.loveini.com/?p=7614

欢迎大家扫描下方二维码,关注 loveini Database 的视频号,观看每周的微课堂以及直播活动。

loveini 视频号二维码
]]>