众里寻TiDB千百度,蓦然回首,填坑记录都在这篇文章中

1,086 阅读22分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

俗话说得好,架构重构一时爽,一直重构一直爽,又到了架构重构的时节;俗话又说了,饱暖思X欲,在产品技术架构稳定运行了好久之后,又迎来了躁动的撩拨,重构之心蠢蠢欲动;俗话最后说了,技术有风险,重构需谨慎,但是遗憾的是,前两句俗话都听了,最后一句却没听,于是就走上了痛并快乐的填坑之旅,而这次的目标就是one stack to rule them all的NewSql数据库TiDB。余下的文章仅对这次技术调研中遇到的问题填过的坑记录下,帮助即将要或者将来要跳坑的各位兄弟姐妹们少走点弯路。

正文

什么是TiDB

直接套用官网的介绍:TiDB是 PingCAP 公司自主设计、研发的开源分布式关系型数据库,是一款同时支持在线事务处理与在线分析处理 (Hybrid Transactional and Analytical Processing, HTAP) 的融合型分布式数据库产品,具备水平扩容或者缩容、金融级高可用、实时 HTAP、云原生的分布式数据库、兼容 MySQL 5.7 协议和 MySQL 生态等重要特性。目标是为用户提供一站式 OLTP (Online Transactional Processing)、OLAP (Online Analytical Processing)、HTAP 解决方案。TiDB 适合高可用、强一致要求较高、数据规模较大等各种应用场景。

TiDB的使用场景

替换Mysql

传统的 MySQL 数据库在数据量急速增长后,使用分库分表的技术来对数据库进行扩展,在分布式数据库系统中也是使用分片技术,但是这些技术不管在维护成本或开发成本上都很高。

而 TiDB 提供了一个可弹性的横向扩展的分布式数据库,并且具有高可用性,它兼容 MySQL 协议和绝大多数的 MySQL 语法,在通常情况下,用户无须修改代码就可以将 MySQL 无缝迁移到 TiDB。

替换其他N个NOSQL数据库

NoSQL 数据库拥有弹性的伸缩能力,具有实时并发写入能力,但是 NoSQL 数据库不支持 SQL,也不支持事务的 ACID 特性,NoSQL 无法满足某些强一致性的场景下的需求。而TiDB 具备 SQL 所有的特性,同时满足数据的在线扩展,同时具备HTAP特性,理论上可以做到one stack to rule them all的效果,可以用来替换诸多NOSQL数据库。之前这些数据存储于 MySQL、Redis、HBase、Elasticsearch 等不同系统中,数据扩容不方便,维护成本也高,现在使用TiDB来统一管理,成本与效率提升效果明显。我们这次的架构重构也正是瞄准了这个目标来进行的。

搭建实时数仓

目前企业大多数的数据分析场景的解决方案都是围绕着 Hadoop 生态系统展开的,包括 HDFS、Hive、Spark 等。但是单纯使用 Hadoop 已经无法满足一些实时的 OLTP 和复杂的 OLAP 需求。

随着 TiDB 的子项目 TiSpark 的发布,以及本身自带的TiCDC等扩展组件,TiDB可以在拥有关系数据库的事务写入能力同时进行复杂的分析。有能力一站式解决实时数仓的需求。

上面介绍和交代了一些选型背景和基础知识,下面就开始正式干活了。

准备工作

测试环境服务器配置

整个测试集群使用4台ZStack云主机搭建,配置如下:

主机配置:

集群配置:

此处需要注意一点,由于tidb server是无状态服务,可以随意扩展,对于磁盘IO以及网络IO要求不敏感,可以和pd共同部署到同一块硬盘下。

但是pd与tikv都是有状态服务,而且对磁盘以及网络IO很敏感,所以最好不要部署在一台服务器上,如果条件限制必须部署在一台服务器上,那么必须要将二者部署在不同的磁盘上,否则两者相互影响,系统的稳定性以及效率会受到很大的影响。

名词解释

tiup:用来管理tidb数据库的组件

运维三兄弟:其实都是第三方的开源组件,tidb对它们进行了集成和整合,用以支撑tidb的运维

alertmanager:告警管理系统,可以通过配置的告警规则对产生的告警进行管理。

Prometheus:开源监控平台,主要是提供了业务数据采集和存储功能。

grafana:开源数据可视化工具,配合Prometheus做各种监测指标的展示以及告警的管理。

tidb组件四兄弟:tidb的基础组成部分,每个都是单独的小集群,合在一起就组成了tidb的主体部分

pd:即Placement Driver,这个集群的”大脑“,负责整个 TiDB 集群的元信息管理模块,负责存储每个 TiKV 节点实时的数据分布情况和集群的整体拓扑结构,提供 TiDB Dashboard 管控界面,并为分布式事务分配事务 ID

tidb:SQL 层,对外暴露 MySQL 协议的连接 endpoint,负责接受客户端的连接,执行 SQL 解析和优化,最终生成分布式执行计划。

tikv:负责存储数据,从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。

tiflash:借鉴了clickhouse的实现,数据是以列式的形式进行存储,主要的功能是为OLAP场景进行加速。

系统配置优化

tidb.performance.txn-total-size-limit: 10737418240

该值控制一个请求中事务最大的大小,默认的值比较小,在某些大批量数据导入时候会报错,建议调大该值,我的经验是直接调到最大值10G,如果还不行的话,就只能拆分数据了

tikv.storage.scheduler-concurrency: 2048000

scheduler 内置一个内存锁机制,防止同时对一个 key 进行操作。每个 key hash 到不同的槽,减少键冲突的概率。

raftdb.defaultcf.target-file-size-base: 32MB
 
rocksdb.defaultcf.target-file-size-base: 32MB
 
rocksdb.writecf.target-file-size-base: 32MB

上面的上个选项是配置底层rocksdb对应的三个qualifier的level1-level6各层的单个sst文件大小,默认是8M,有点小,单个sst太小会影响到单次查询的效率,可能需要扫秒更多的文件和造成更大的IO。

raftdb.defaultcf.compression-per-level["no""no""lz4""lz4""lz4""zstd""zstd"]
 
rocksdb.defaultcf.compression-per-level["no""no""lz4""lz4""lz4""zstd""zstd"]
 
rocksdb.writecf.compression-per-level["no""no""lz4""lz4""lz4""zstd""zstd"]

这是底层rocksdb的存储level0-level6层的压缩算法,默认配置是l0和l1不压缩;l2-l4采用lz4压缩算法;l5和l6采用zstd压缩算法,大多数场景下默认配置即可。但通过Prometheus监控可以观察CPU与磁盘IO之间的关系,如果CPU占用较高,top -H 发现有大量的 bg 开头的线程(RocksDB 的 compaction 线程)在运行,但磁盘IO还有剩余,则可以将根据情况可以考虑用 I/O 资源换取 CPU 资源,将压缩方式改成"no:no:no:lz4:lz4:zstd:zstd";反之发现系统的 I/O 压力很大(使用 iostat 发现 %util 持续 100% 或者使用 top 命令发现 iowait 特别多),而 CPU 的资源还比较充裕,这个时候可以考虑将 level0 和 level1 开启压缩,用 CPU 资源换取 I/O 资源。

region副本数修改

首先说明下,在tidb中修改配置有两种方式:

  • 通过命令修改配置后,重启生效;
  • 通过pd的专用接口来进行pd相关配置的修改,即时生效,不需要重启。

但是在tidb中,副本数修改无法通过这两种方式来生效,而是需要通过Placement Rules来进行操作。

Placement Rules默认是关闭的,此处有一点需要划重点说一下:如果tidb集群启用了tiflash组件,则Placement Rules默认是开启的,且无法关闭。这个会影响到后续副本数修改相关的操作,这里先说明一下。由于我的集群开启了tiflash,所以Placement Rules默认也是开启的,配置如下:

如果没有开启Placement Rules,可以使用

pd-ctl config placement-rules enable
来开启Placement Rules

开启之后需要先导出配置文件:

pd-ctl -u http://172.16.43.165:2379 config placement-rules load --out="rules.json";

然后再导入配置文件:

pd-ctl -u http://172.16.43.165:2379 config placement-rules save --in="rules.json";

修改后的配置文件,counter就是备份数目,测试环境节省硬盘将数据备份数设置为1。

其他常用的命令以及设置

常用命令

查看目标表的分区情况:

SELECT PARTITION_NAME,TABLE_ROWS,PARTITION_EXPRESSION,PARTITION_DESCRIPTION FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME = 'table_name';

查看系统配置:

show config;

如果想按照系统角色来过滤配置,则可以增加type过滤:

show config where type = 'tikv';

打开交互式pd配置专用接口:

./pd-ctl -i -u http://172.16.43.165:2379

查看数据表的region信息:

show table table_name regions;

常用设置

@@tidb_scatter_region

该变量用于控制建表完成后是否等待预切分和打散 Region 完成后再返回结果。如果建表后有大批量写入,需要设置该变量值为 1,表示等待所有 Region 都切分和打散完成后再返回结果给客户端。否则未打散完成就进行写入会对写入性能影响有较大的影响。

由于该参数是个全局参数,设置一次永久生效。所以需要使用set global来设置:

查看该参数:

@@session.tidb_batch_insert

该变量用于将大批量写入分成多份并行写入到tidb,用于防止大量写入导致OOM或者超时等问题。

由于该参数是个会话参数,所以每打开一次客户端,就需要设置一次:

查看该参数:

tidb设置乐观锁悲观锁

悲观锁

set @@global.tidb_txn_mode = 'pessimistic';

乐观锁

set @@global.tidb_txn_mode = 'optimistic';

数据证明,在修改居多且事务要求严格的场景下,悲观锁的性能以及稳定性会更好些。所以除非业务本身并不要求事务的强一致性,且追求极限的读写性能,则可以使用乐观事务模型并开启重试,否则建议使用悲观锁。

写数据

写数据这里就不多说了,仅仅通过我的批量导入数据发现以及解决的问题展开来说一下相关的各种”坑“。

数据表分区

数据表分区是要将一个大表分成多个partition,便于数据查询和数据管理。当前支持的类型包括 Range 分区,List 分区和 List COLUMNS 分区和Hash 分区。Range 分区,List 分区和 List COLUMNS 分区可以用于解决业务中大量删除带来的性能问题,支持快速删除分区。Hash 分区则可以用于大量写入场景下的数据打散。

另外,合理的分区可以在查询的时候可以使用分区裁剪相关的优化。

下面就是一个使用时间的range分区建表语句:

CREATE TABLE app_commons (
dt_server_time DATETIME,
server_time BIGINT default 0 ,
ip_lan VARCHAR(255) ,
ip_wan VARCHAR(255) ,
agent_id SMALLINT default 0 ,
start_id VARCHAR(255) default '' ,
net_type Enum('NETWORK_WIFI','4G','WIFI','NETWORK_4G','NETWORK_NO','NETWORK_2G','NETWORK_3G','N/A','NETWORK_UNKNOWN','null','3G','2G','Unknow') ,
version VARCHAR(255) ,
platform Enum('android','ios') ,
protol_type VARCHAR(255) ,
position VARCHAR(255) ,
self_md5 VARCHAR(255) ,
client_time DATETIME ,
udid VARCHAR(36) default '00000000-0000-0000-0000-000000000000',
index idx_start_id (start_id),
index idx_protol_type (protol_type),
index idx_dt_server_time (dt_server_time)
)shard_row_id_bits = 4 pre_split_regions=2
PARTITION BY RANGE ( TO_DAYS(dt_server_time) ) (
PARTITION p00000000 VALUES LESS THAN ( TO_DAYS('2020-01-26') ),
PARTITION p20200126 VALUES LESS THAN ( TO_DAYS('2020-01-27') ),
PARTITION p20200127 VALUES LESS THAN ( TO_DAYS('2020-01-28') ),
PARTITION p20200128 VALUES LESS THAN ( TO_DAYS('2020-01-29') ),
PARTITION p20200129 VALUES LESS THAN ( TO_DAYS('2020-01-30') ),
PARTITION p20200130 VALUES LESS THAN ( TO_DAYS('2020-01-31') ),
PARTITION p20200131 VALUES LESS THAN ( TO_DAYS('2020-02-01') ),
PARTITION p20200201 VALUES LESS THAN ( TO_DAYS('2020-02-02') ),
PARTITION p20200202 VALUES LESS THAN ( TO_DAYS('2020-02-03') ),
PARTITION p20200203 VALUES LESS THAN ( TO_DAYS('2020-02-04') ),
PARTITION p20200204 VALUES LESS THAN ( TO_DAYS('2020-02-05') ),
PARTITION p20200205 VALUES LESS THAN ( TO_DAYS('2020-02-06') ),
PARTITION p20200206 VALUES LESS THAN ( TO_DAYS('2020-02-07') ),
PARTITION p20200207 VALUES LESS THAN ( TO_DAYS('2020-02-08') ),
PARTITION p20200208 VALUES LESS THAN ( TO_DAYS('2020-02-09') ),
PARTITION p20200209 VALUES LESS THAN ( TO_DAYS('2020-02-10') ),
PARTITION p20200210 VALUES LESS THAN ( TO_DAYS('2020-02-11') ),
PARTITION p20200211 VALUES LESS THAN ( TO_DAYS('2020-02-12') ),
PARTITION p20200212 VALUES LESS THAN ( TO_DAYS('2020-02-13') ),
PARTITION p20200213 VALUES LESS THAN ( TO_DAYS('2020-02-14') ),
PARTITION p20200214 VALUES LESS THAN ( TO_DAYS('2020-02-15') ),
PARTITION p20200215 VALUES LESS THAN ( TO_DAYS('2020-02-16') ),
PARTITION p20200216 VALUES LESS THAN ( TO_DAYS('2020-02-17') ),
PARTITION p20200217 VALUES LESS THAN ( TO_DAYS('2020-02-18') ),
PARTITION p20200218 VALUES LESS THAN ( TO_DAYS('2020-02-19') ),
PARTITION p20200219 VALUES LESS THAN ( TO_DAYS('2020-02-20') ),
PARTITION p20200220 VALUES LESS THAN ( TO_DAYS('2020-02-21') ),
PARTITION p20200221 VALUES LESS THAN ( TO_DAYS('2020-02-22') ),
PARTITION p20200222 VALUES LESS THAN ( TO_DAYS('2020-02-23') ),
PARTITION p20200223 VALUES LESS THAN ( TO_DAYS('2020-02-24') ),
PARTITION p20200224 VALUES LESS THAN ( TO_DAYS('2020-02-25') ),
PARTITION p20200225 VALUES LESS THAN ( TO_DAYS('2020-02-26') ),
PARTITION p20200226 VALUES LESS THAN ( TO_DAYS('2020-02-27') ),
PARTITION p99999999 VALUES LESS THAN (MAXVALUE)
);

可以看到在建表时指定dt_server_time为分区字段,将所有的数据按天分区后存储。

注:如果tidb在建表时没有指定分区,则建表完毕后无法再对表进行分区操作(未查到相关的操作,也有可能是我没找到),而对应的mysql则可以进行类似操作。另外两者都支持分区的添加以及删除操作。

region预分区

如果默认情况下创建表,则该表默认只有1个region,如果表只有一个region的话,那么在前期势必会出现写入热点的情况,后续tidb会通过region的拆分以及转移来进行负载均衡,这种情况也算正常,通常被叫做”预热“,当预热结束后,所有的写入就恢复正常。但是针对大量写入的场景,这种”预热“还是需要尽量避免的,否则会造成系统性能的抖动。而解决写入热点的方式就是region预分区。

在region预分区之前先了解下数据写入后是怎么落到对应的region中的,总所周知,所有的region都是有key范围的。刨除不变的table_id以及常量之外,就剩下数据的row_id了。对于有主键的数据,row_id就是主键;对于没有主键的数据,row_id是tidb自己维护的一个递增的数值。所以如果数据的主键是递增的整数或者没有主键的场景下,数据写入初期妥妥的有热点情况啊。

针对这种情况,tidb提供了打散热点的解决方案:

建表时增加表属性 SHARD_ROW_ID_BITS,它用来设置隐式 _tidb_rowid 分片数量的 bit 位数。

基本概念: 对于非整数主键或没有主键的表,TiDB 会使用一个隐式的自增 rowid。大量执行 INSERT 插入语句时会把数据集中写入单个 Region,造成写入热点。

通过设置 SHARD_ROW_ID_BITS,可以把 rowid 打散写入多个不同的 Region,缓解写入热点问题。但是设置的过大会造成 RPC 请求数放大,增加 CPU 和网络开销。

SHARD_ROW_ID_BITS = 4 表示 16 个分片 SHARD_ROW_ID_BITS = 6 表示 64 个分片 SHARD_ROW_ID_BITS = 0 表示默认值 1 个分片 语句示例:

CREATE TABLECREATE TABLE t (c int) SHARD_ROW_ID_BITS = 4;
ALTER TABLEALTER TABLE t SHARD_ROW_ID_BITS = 4;

但是使用过程中一般都会配合pre_split_regions共同使用:

create table t (a int, b int,index idx1(a)) shard_row_id_bits = 4 pre_split_regions=2;

上面的语句执行完毕后,会对这个表 t 预切分出 4 + 1 个 Region。4 (2^2) 个 Region 是用来存 table 的行数据的,1 个 Region 是用来存 idx1 索引的数据。

注:pre_split_regions的值必须小于等于shard_row_id_bits的值。

从上面建表语句可以看出,预分区的做法仅仅是对数据region进行了分区,未对索引分区进行presplit,在实际写数据的过程中,尤其索引的数据比较大的场景,索引的写入量是数据写入量的n倍,虽然每个索引的大小比较小,但是架不住数量多,实际场景下索引region split比较频繁,也会造成热点,所以需要手动split下索引的region。

下面根据上面建表语句中的三个索引进行region的split,语句如下:

SPLIT TABLE app_commons INDEX idx_start_id BETWEEN ("155") AND ("158") REGIONS 2;
SPLIT TABLE app_commons INDEX idx_protol_type BETWEEN ("a") AND ("y") REGIONS 2;
SPLIT TABLE app_commons INDEX idx_dt_server_time BETWEEN ("2020-01-27 00:00:00") AND ("2020-02-26 00:00:00") REGIONS 2;

上述语句执行完毕后,会将索引表按照上述的范围拆分成2个region。

这里需要注意的是,由于上述的建表语句以及进行了partition操作,而每个partition相当于一个子表,拥有自己的索引region,所以索引拆分是针对每一个partition进行的,即最终的索引region从34个变成了68个。

数据导入

数据导入有两种方式,tidb-lightening以及load data,由于测试环境的限制,tidb-lightening的效果不佳,总是出现超时以及失败,于是采用load data的方式:

LOAD DATA LOCAL INFILE '/data/csv/everisk.app_commons2.89.csv' INTO TABLE app_commons FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' (dt_server_time, server_time, ip_lan, ip_wan, agent_id, start_id, net_type, version, platform, protol_type, position, self_md5, client_time, udid);

此处需要注意,按照官网上的说法,一次导入的文件夹最好别太大,否则容易失败或者超时,建议每个文件256M左右,大概需要3-5分钟能导入完毕,由于我的测试环境的限制以及数据索引较多,在”预热之后“数据写入的速度大概是5-8分钟一个256M的文件,而负载不均衡时单个256M文件可能会达到10分钟左右。

写入之后查看各分区的数据,发现和预期不一致,磁盘空间已经占用了,但是数据呢?

后来在论坛上咨询了下,应该是没有执行analyze table造成的,可以配置自动执行analyze。但是我查了下配置,发现默认已经配置了,但是却不生效,不知为何。

手动执行了下analyze:

确实有效果,分区内有数据了:

读数据

读数据我是通过mysql client连接到tidb server进行的相关操作,从sql优化的最基础的三个原则展开来说明下相关的原则。这三点基本上和SQL执行计划优化是一样的,了解的可以直接略过了。至于配置参数级别的优化,暂时我收集到的资料并不多,后续有发现我会继续更新,暂时以官网的优化方案为准。

谓词下推

要了解谓词下推,需要先理解下什么是谓词

在SQL中,谓词就是返回boolean值即true和false的函数,或是隐式转换为bool的函数。SQL中的谓词主要有 LIKE、BETWEEN、IS NULL、IS NOT NULL、IN、EXISTS,判断字段想否相等或者按某个字段过滤也属于此范畴。

知道了什么是谓词后,再来说说谓词下推,所谓的谓词下推就是:

将过滤表达式尽可能移动至靠近数据源的位置,以使真正执行时能直接跳过无关的数据。

这样的做好处就是将无用的数据直接在服务端存储的位置上直接过滤掉,减少后续计算以及传输所消耗的各种资源,包含但不限于磁盘IO,网络IO,CPU以及内存等,提升查询执行效率。

下面就用两个语句来对比说明下:

select a.udid, b.protol_type, b.server_time
from app_start as a
inner join (select start_id, protol_type, server_time
from app_commons
where protol_type = 'inject'
and dt_server_time between '2020-02-25' and '2020-02-25 23:59:59'as b
on a.start_id = b.start_id
order by b.server_time desc
limit 10;
select a.udid, b.protol_type, b.server_time
from app_start as a
inner join app_commons b
on a.start_id = b.start_id where b.protol_type = 'inject'
and b.dt_server_time between '2020-02-25' and '2020-02-25 23:59:59'
order by b.server_time desc
limit 10;

单从SQL语句上来看,上面的SQL语句的执行效率应该会比下面的高,因为上面的SQL语句在innner join之前已经做了谓词下推,将b表的数据优先进行了一轮过滤,减少了不必要的开销。但是实际执行起来,两者的执行时间基本上没差别。这是为何,难道谓词下推不起作用了吗?

实际上谓词下推已经起作用了,通过查看上面语句执行计划,都有谓词下推的操作,所以谓词下推不是没起作用,而是在可能的范围内,在逻辑执行计划生成物理执行计划时由SQL优化器把这件事情给做了。这也是两个语句物理执行计划几乎一致的原因。

列裁剪

这个就不必多说了,就是查询时需要什么列就查询什么列,如非必要不要写"select * from xxx;"

分区裁剪

分区裁剪的目的就是根据某一个常用的有顺序的条件将表进行划分,常用的就是时间字段,这样如果有时间条件的查询就会被限制在某一个或者几个分区中,这样能减少需要操作的数据量,从而提升查询性能。

如上面的建表语句和查询语句,就是在建表时按天分区,查询时也按天查询,那么查询的数据范围就会落在20200225这个分区内,从而减少查询的数据量。

下图就是上面查询执行计划的截图:

从上面可见,此次查询只用了table:b, partition:p20200225这一个分区,和预期一致。

使用tiflash组件

上面已经介绍了tiflash的定义,查询中如果启用了tiflash,在某些场景下,尤其是OLAP场景,tiflash会对查询进行加速。

tiflash的数据是从tikv中同步而来,命令如下:

ALTER TABLE table_name SET TIFLASH REPLICA 1
SELECT * FROM information_schema.tiflash_replica WHERE TABLE_SCHEMA = 'db_name' and TABLE_NAME = 'table_name'

上面命令的作用是将某个表的数据同步到tiflash中并且数据存一个备份。

虽然tiflash会对查询进行加速,但是SQL优化器会在逻辑执行计划优化生成物理执行计划时进行选择,并非一定会使用tiflash,有可能仍旧会使用tikv中的数据。这个和SQL优化器的算法有关系,如果想手动指定SQL语句使用tiflash进行查询,则可以使用hint optimizer来进行设定:

select /*+ read_from_storage(tiflash[table_name]) */ ... from table_name;

总结

TiDB作为一款NewSql数据库的有点事毋庸置疑的,但是金无足赤人无完人,TiDB也有自己的缺点:

  • TiDB声称99%兼容mysql的语法,但是某些高级功能包含但不限于存储过程与函数、触发器、实践。这些功能由于分布式实现的难度以及必要性暂时不支持,所以针对Mysql升级到TiDB的场景涉及到对应功能需要进行确认以及人工适配
  • TiDB的硬件需求非常高,否则很难达到理想的IO以及性能。至于硬盘官方直接建议使用全部的SSD....
  • TiDB的学习和使用成本比较高,TiDB基本上属于上手容易,优化难的存在,不过考虑到它支持如此多的功能以及特性就释然了。加上每个小组件都是一个集群,然后配置参数几十上百项,使得我再调研过程中很长一段时间摸不到头脑,而且优化也需要配合硬件以及场景,还是很有门槛的。

针对上面的问题,还是有应对方案的:

  • mysql中TiDB不支持的高级属性在大数据框架下已经不是什么问题,大数据圈中有无数种方式可以做到那些高级功能

最后各种原因TiDB的调研暂时告一段落,后续开始调研clickhouse,会将两者在部分场景下的对比总结下,然后找到最合适的技术选型。后续有新的进展我会第一时间整理分享。

重构的路上任重道远,逆水行舟不进则退,希望我能坚定地坚持的走下去吧。

最后,如果大家感觉笔者写的还不错,麻烦大家多多点赞和分享转发,也许你的朋友也喜欢。