关于高并发:并发提升-20-倍单节点数万-QPSApache-Doris-高并发特性解读

9次阅读

共计 10023 个字符,预计需要花费 26 分钟才能阅读完成。

随着用户规模的极速扩张,越来越多用户将 Apache Doris 用于构建企业外部的对立剖析平台,这一方面须要 Apache Doris 去承当更大业务规模的解决和剖析——既蕴含了更大规模的数据量、也蕴含了更高的并发承载,而另一方面,也意味着须要应答企业更加多样化的数据分析诉求,从过来的统计报表、即席查问、交互式剖析等典型 OLAP 场景,拓展到举荐、风控、标签画像以及 IoT 等更多业务场景中,而数据服务(Data Serving)就是其中具备代表性的一类需要。Data Serving 通常指的是向用户或企业客户提供数据拜访服务,用户应用较为频繁的查问模式个别是依照 Key 查问一行或多行数据,例如:

  • 订单详情查问
  • 商品详情查问
  • 物流状态查问
  • 交易详情查问
  • 用户信息查问
  • 用户画像属性查问

与面向大规模数据扫描与计算的 Adhoc 不同,Data Serving 在理论业务中通常出现为高并发的点查问—— 查问返回的数据量较少、通常只需返回一行或者大量行数据,但对于查问耗时极为敏感、冀望在毫秒内返回查问后果,并且面临着超高并发的挑战。

在过来面对此类业务需要时,通常采取不同的零碎组件别离承载对应的查问拜访。OLAP 数据库个别是基于列式存储引擎构建,且是针对大数据场景设计的查问框架,通常以数据吞吐量来掂量零碎能力,因而在 Data Serving 高并发点查场景的体现往往不迭用户预期。基于此,用户个别引入 Apache HBase 等 KV 零碎来应答点查问、Redis 作为缓存层来分担高并发带来的零碎压力。而这样的架构往往比较复杂,存在冗余存储、保护老本高的问题。交融对立的剖析范式为 Apache Doris 能承载的工作负载带来了挑战,也让咱们更加系统化地去思考如何更好地满足用户在此类场景的业务需要。基于以上思考,在行将公布的 2.0 版本中,咱们在原有性能根底上引入了一系列面向点查问的优化伎俩,单节点可达数万 QPS 的超高并发,极大拓宽了实用场景的能力边界。

#  如何应答高并发查问?

始终以来高并发就是 Apache Doris 的劣势之一。对于高并发查问,其外围在于如何均衡无限的系统资源耗费与并发执行带来的高负载。换而言之,须要最大化升高单个 SQL 执行时的 CPU、内存和 IO 开销,其关键在于缩小底层数据的 Scan 以及随后的数据计算,其次要优化形式有如下几种:

分辨别桶裁剪

Apache Doris 采纳两级分区,第一级是 Partition,通常能够将工夫作为分区键。第二级为 Bucket,通过 Hash 将数据打散至各个节点中,以此晋升读取并行度并进一步提高读取吞吐。通过正当地划分辨别桶,能够进步查问性能,以下列查问语句为例:

select * from user_table where id = 5122 and create_date = '2022-01-01'

用户以 create_time 作为分区键、ID 作为分桶键,并设置了 10 个 Bucket,通过分辨别桶裁剪后可疾速过滤非必要的分区数据,最终只需读取极少数据,比方 1 个分区的 1 个 Bucket 即可疾速定位到查问后果,最大限度缩小了数据的扫描量、升高了单个查问的延时。

索引

除了分辨别桶裁剪,Doris 还提供了丰盛的索引构造来减速数据的读取和过滤。索引的类型大体能够分为智能索引和二级索引两种,其中智能索引是在 Doris 数据写入时主动生成的,无需用户干涉。智能索引包含前缀索引和 ZoneMap 索引两类:

  • 前缀稠密索引(Sorted Index) 是建设在排序构造上的一种索引。Doris 存储在文件中的数据,是依照排序列有序存储的,Doris 会在排序数据上每 1024 行创立一个稠密索引项。索引的 Key 即以后这 1024 行中第一行的前缀排序列的值,当用户的查问条件蕴含这些排序列时,能够通过前缀稠密索引疾速定位到起始行。
  • ZoneMap 索引 是建设在 Segment 和 Page 级别的索引。对于 Page 中的每一列,都会记录在这个 Page 中的最大值和最小值,同样,在 Segment 级别也会对每一列的最大值和最小值进行记录。这样当进行等值或范畴查问时,能够通过 MinMax 索引疾速过滤掉不须要读取的行。

二级索引是须要用手动创立的索引,包含 Bloom Filter 索引、Bitmap 索引,以及 2.0 版本新增的 Inverted 倒排索引和 NGram Bloom Filter 索引,在此不细述,可从官网文档后行理解,后续将有系列文章进行解读。

官网文档:

  • 倒排索引:https://doris.apache.org/zh-CN/docs/dev/data-table/index/inve…
  • NGram BloomFilter 索引:https://doris.apache.org/zh-CN/docs/dev/data-table/index/ngra…

咱们以下列查问语句为例:

select * from user_table where id > 10 and id < 1024

假如依照 ID 作为建表时指定的 Key,那么在 Memtable 以及磁盘上依照 ID 有序的形式进行组织,查问时如果过滤条件蕴含前缀字段时,则能够应用前缀索引疾速过滤。Key 查问条件在存储层会被划分为多个 Range,依照前缀索引做二分查找获取到对应的行号范畴,因为前缀索引是稠密的,所以只能大抵定位出行的范畴。随后过一遍 ZoneMap、Bloom Filter、Bitmap 等索引,进一步放大须要 Scan 的行数。通过索引,大大减少了须要扫描的行数,缩小 CPU 和 IO 的压力,整体大幅晋升了零碎的并发能力。

物化视图

物化视图是一种典型的空间换工夫的思路,其本质是依据预约义的 SQL 剖析语句执⾏预计算,并将计算结果长久化到另一张对用户通明但有理论存储的表中。在须要同时查问聚合数据和明细数据以及匹配不同前缀索引的场景,命中物化视图时能够取得更快的查问相应,同时也防止了大量的现场计算,因而能够进步性能体现并升高资源耗费

// 对于聚合操作,间接读物化视图预聚合的列
create materialized view store_amt as select store_id, sum(sale_amt) from sales_records group by store_id;
SELECT store_id, sum(sale_amt) FROM sales_records GROUP BY store_id;

// 对于查问,k3 满足物化视图前缀列条件,走物化视图减速查问
CREATE MATERIALIZED VIEW mv_1 as SELECT k3, k2, k1 FROM tableA ORDER BY k3;
select k1, k2, k3 from table A where k3=3;

Runtime Filter

除了前文提到的用索引来减速过滤查问的数据,Doris 中还额定退出了动静过滤机制,即 Runtime Filter。在多表关联查问时,咱们通常将右表称为 BuildTable、左表称为 ProbeTable,左表的数据量会大于右表的数据。在实现上,会首先读取右表的数据,在内存中构建一个 HashTable(Build)。之后开始读取左表的每一行数据,并在 HashTable 中进行连贯匹配,来返回合乎连贯条件的数据(Probe)。而 Runtime Filter 是在右表构建 HashTable 的同时,为连贯列生成一个过滤构造,能够是 Min/Max、IN 等过滤条件。之后把这个过滤列构造下推给左表。这样一来,左表就能够利用这个过滤构造,对数据进行过滤,从而缩小 Probe 节点须要传输和比对的数据量。在大多数 Join 场景中,Runtime Filter 能够实现节点的主动穿透,将 Filter 穿透下推到最底层的扫描节点或者分布式 Shuffle Join 中。大多数的关联查问 Runtime Filter 都能够起到大幅缩小数据读取的成果,从而减速整个查问的速度。

OPN 优化技术

在数据库中查问最大或最小几条数据的利用场景十分宽泛,比方查问满足某种条件的工夫最近 100 条数据、查问价格最高或者最低的几个商品等,此类查问的性能对于实时剖析十分重要。在 Doris 中引入了 TOPN 优化来解决大数据场景下较高的 IO、CPU、内存资源耗费:

  • 首先从 Scanner 层读取排序字段和查问字段,利用堆排序保留 TOPN 条数据,实时更新以后已知的最大或最小的数据范畴,并动静下推至 Scanner
  • Scanner 层依据范畴条件,利用索引等减速跳过文件和数据块,大幅缩小读取的数据量。
  • 在宽表中用户通常须要查问字段数较多,在 TOPN 场景理论无效的数据仅 N 条,通过将读取拆分成两阶段,第一阶段依据大量的排序列、条件列来定位行号并排序,第二阶段依据排序后并取 TOPN 的后果失去行号反向查问数据,这样能够大大降低 Scan 的开销

通过以上一系列优化伎俩,能够将不必要的数据剪枝掉,缩小读取、排序的数据量,显著升高零碎 IO、CPU 以及内存资源耗费。此外,还能够利用包含 SQL Cache、Partition Cache 在内的缓存机制以及 Join 优化伎俩来进一步晋升并发,因为篇幅起因不在此详述。

#  Apache Doris 2.0 新个性揭秘

通过上一段中所介绍的内容,Apache Doris 实现了单节点上千 QPS 的并发反对。但在一些超高并发要求(例如数万 QPS)的 Data Serving 场景中,依然存在瓶颈:

  • 列式存储引擎对于行级数据的读取不敌对,宽表模型上列存格局将大大放大随机读取 IO;
  • OLAP 数据库的执行引擎和查问优化器对于某些简略的查问(如点查问)来说太重,须要在查问布局中布局短门路来解决此类查问;
  • SQL 申请的接入以及查问打算的解析与生成由 FE 模块负责,应用的是 Java 语言,在高并发场景下解析和生成大量的查问执行打算会导致高 CPU 开销;
  • ……

带着以上问题,Apache Doris 在别离从升高 SQL 内存 IO 开销、晋升点查执行效率以及升高 SQL 解析开销这三个设计点登程,进行一系列优化。

行式存储格局(Row Store Format)

与列式存储格局不同,行式存储格局在数据服务场景会更加敌对,数据按行存储、应答单次检索整行数据时效率更高,能够极大缩小磁盘拜访次数。因而在 Apache Doris 2.0 版本中,咱们引入了行式存储格局,将行存编码后存在独自的一列中,通过额定的空间来存储。用户能够在建表语句的 Property 中指定如下属性来开启行存:

"store_row_column" = "true"

咱们抉择以 JSONB 作为行存的编码格局,次要出于以下思考:

  • Schema 变更灵便:随着数据的变动、变更,表的 Schema 也可能产生相应变动。行存储格局提供灵活性以解决这些变动是很重要的,例如用户删减字段、批改字段类型,数据变更须要及时同步到行存中。通过应用 JSONB 作为编码方式,将列作为 JSONB 的字段进行编码,能够十分不便地进行字段扩大以及更改属性。
  • 性能更高:在行存储格局中拜访行能够比在列存储格局中拜访行更快,因为数据存储在单个行中。这能够在高并发场景下显著缩小磁盘拜访开销。此外,通过将每个列 ID 映射到 JSONB 其对应的值,能够实现对个别列的快速访问。
  • 存储空间:将 JSONB 作为行存储格局的编解码器也能够帮忙缩小磁盘存储老本。紧凑的二进制格局能够缩小存储在磁盘上的数据总大小,使其更具老本效益。

应用 JSONB 编解码行存储格局,能够帮忙解决高并发场景下面临的性能和存储问题。行存在存储引擎中会作为一个暗藏列(DORIS_ROW_STORE_COL)来进行存储,在 Memtable Flush 时,将各个列依照 JSONB 进行编码并缓存到这个暗藏列里。在数据读取时,通过该暗藏列的 Column ID 来定位该列,通过其行号定位到某一具体的行,并反序列化各列。

相干 PR:https://github.com/apache/doris/pull/15491

点查问短门路优化(Short-Circuit)

通常状况下,一条 SQL 语句的执行须要通过三个步骤:首先通过 SQL Parser 解析语句,生成形象语法树(AST),随后通过 Query Optimizer 生成可执行打算(Plan),最终通过执行该打算失去计算结果。对于大数据量下的简单查问,经由查问优化器生成的执行打算无疑具备更高效的执行成果,但对于低延时和高并发要求的点查问,则不合适走整个查问优化器的优化流程,会带来不必要的额定开销。为了解决这个问题,咱们实现了点查问的短门路优化,绕过查问优化器以及 PlanFragment 来简化 SQL 执行流程,间接应用疾速高效的读门路来检索所需的数据。

当查问被 FE 接管后,它将由布局器生成适当的 Short-Circuit Plan 作为点查问的物理打算。该 Plan 十分轻量级,不须要任何等效变换、逻辑优化或物理优化,仅对 AST 树进行一些根本剖析、构建相应的固定打算并缩小优化器的开销。对于简略的主键点查问,如select * from tbl where pk1 = 123 and pk2 = 456,因为其只波及单个 Tablet,因而能够应用轻量的 RPC 接口来间接与 StorageEngine 进行交互,以此防止生成简单的 Fragment Plan 并打消了在 MPP 查问框架下执行调度的性能开销。RPC 接口的详细信息如下:

message PTabletKeyLookupRequest {
    required int64 tablet_id = 1;
    repeated KeyTuple key_tuples = 2;
    optional Descriptor desc_tbl = 4;
    optional ExprList  output_expr = 5;
}

message PTabletKeyLookupResponse {
    required PStatus status = 1;
    optional bytes row_batch = 5;
    optional bool empty_batch = 6;
}
rpc tablet_fetch_data(PTabletKeyLookupRequest) returns (PTabletKeyLookupResponse);

以上 tablet_id 是从主键条件列计算得出的,key_tuples是主键的字符串格局,在下面的示例中,key_tuples相似于 [‘123’, ‘456’],在 BE 收到申请后 key_tuples 将被编码为主键存储格局,并依据主键索引来辨认 Key 在 Segment File 中的行号,并查看对应的行是否在 delete bitmap 中,如果存在则返回其行号,否则返回 NotFound。而后应用该行号直对__DORIS_ROW_STORE_COL__ 列进行点查问,因而咱们只需在该列中定位一行并获取 JSONB 格局的原始值,并对其进行反序列化作为后续输入函数计算的值。

相干 PR:https://github.com/apache/doris/pull/15491

预处理语句优化(PreparedStatement)

高并发查问中的 CPU 开销能够局部归因于 FE 层剖析和解析 SQL 的 CPU 计算,为了解决这个问题,咱们在 FE 端提供了与 MySQL 协定齐全兼容的预处理语句(Prepared Statement)。当 CPU 成为主键点查的性能瓶颈时,Prepared Statement 能够无效发挥作用,实现 4 倍以上的性能晋升

Prepared Statement 的工作原理是通过在 Session 内存 HashMap 中缓存事后计算好的 SQL 和表达式,在后续查问时间接复用缓存对象即可。Prepared Statement 应用 MySQL 二进制协定作为传输协定。该协定在文件 mysql_row_buffer.[h|cpp]  中实现,符合标准 MySQL 二进制编码,通过该协定客户端例如 JDBC Client,第一阶段发送PREPAREMySQL Command 将预编译语句发送给 FE 并由 FE 解析、Analyze 该语句并缓存到上图的 HashMap 中,接着客户端通过EXECUTEMySQL Command 将占位符替换并编码成二进制的格局发送给 FE,此时 FE 依照 MySQL 协定反序列化后失去占位符中的值,生成对应的查问条件。

除了在 FE 缓存 Statement,咱们还须要在 BE 中缓存被重复使用的构造,包含事后调配的计算 Block,查问描述符和输入表达式,因为这些构造在序列化和反序列化时会造成 CPU 热点,所以须要将这些构造缓存下来。对于每个查问的 PreparedStatement,都会附带一个名为 CacheID 的 UUID。当 BE 执行点查问时,依据相干的 CacheID 找到对应的复用类,并在 BE 中表达式计算、执行时重复使用上述构造。上面是在 JDBC 中应用 PreparedStatement 的示例:1. 设置 JDBC URL 并在 Server 端开启 PreparedStatement

url = jdbc:mysql://127.0.0.1:9030/ycsb?useServerPrepStmts=true
  1. 应用 Prepared Statement
// use `?` for placement holders, readStatement should be reused
PreparedStatement readStatement = conn.prepareStatement("select * from tbl_point_query where key = ?");
...
readStatement.setInt(1234);
ResultSet resultSet = readStatement.executeQuery();
...
readStatement.setInt(1235);
resultSet = readStatement.executeQuery();
...

相干 PR:https://github.com/apache/doris/pull/15491

行存缓存

Doris 中有针对 Page 级别的 Cache,每个 Page 中存的是某一列的数据,所以 Page Cache 是针对列的缓存。

对于后面提到的行存,一行里包含了多列数据,缓存可能被大查问给刷掉,为了减少行缓存命中率,就须要独自引入行存缓存(Row Cache)。行存 Cache 复用了 Doris 中的 LRU Cache 机制,启动时会初始化一个内存阈值,当超过内存阈值后会淘汰掉古老的缓存行。对于一条主键查问语句,在存储层上命中行缓存和不命中行缓存可能有数十倍的性能差距 (磁盘 IO 与内存的拜访差距), 因而行缓存的引入能够极大晋升点查问的性能,特地是缓存命中高的场景下。

开启行存缓存能够在 BE 中设置以下配置项来开启:

disable_storage_row_cache=false // 是否开启行缓存,默认不开启
row_cache_mem_limit=20% // 指定 row cache 占用内存的百分比,默认 20% 内存

相干 PR:https://github.com/apache/doris/pull/15491

#  Benchmark

基于以上一系列优化,帮忙 Apache Doris 在 Data Serving 场景的性能失去进一步晋升。咱们基于 Yahoo! Cloud Serving Benchmark(YCSB)规范性能测试工具进行了基准测试,其中环境配置与数据规模如下:

  • 机器环境:单台 16 Core 64G 内存 4*1T 硬盘的云服务器
  • 集群规模:1 FE + 3 BE
  • 数据规模:一共 1 亿条数据,均匀每行在 1K 左右,测试前进行了预热。
  • 对应测试表构造与查问语句如下:
// 建表语句如下:CREATE TABLE `usertable` (`YCSB_KEY` varchar(255) NULL,
  `FIELD0` text NULL,
  `FIELD1` text NULL,
  `FIELD2` text NULL,
  `FIELD3` text NULL,
  `FIELD4` text NULL,
  `FIELD5` text NULL,
  `FIELD6` text NULL,
  `FIELD7` text NULL,
  `FIELD8` text NULL,
  `FIELD9` text NULL
) ENGINE=OLAP
UNIQUE KEY(`YCSB_KEY`)
COMMENT 'OLAP'
DISTRIBUTED BY HASH(`YCSB_KEY`) BUCKETS 16
PROPERTIES (
"replication_allocation" = "tag.location.default: 1",
"in_memory" = "false",
"persistent" = "false",
"storage_format" = "V2",
"enable_unique_key_merge_on_write" = "true",
"light_schema_change" = "true",
"store_row_column" = "true",
"disable_auto_compaction" = "false"
);

// 查问语句如下:SELECT * from usertable WHERE YCSB_KEY = ?

开启优化(即同时开启行存、点查短门路以及 PreparedStatement)与未开启的测试后果如下:

开启以上优化项后均匀 查问耗时升高了 96%,99 分位的 查问耗时仅之前的 1/28,QPS 并发 从 1400 增至 3w、晋升了超过 20 倍,整体性能体现和并发承载实现数据量级的飞跃!

#  最佳实际

须要留神的是,在以后阶段实现的点查问优化均是在 Unique Key 主键模型进行的,同时须要开启 Merge-on-Write 以及 Light Schema Change 后应用,以下是点查问场景的建表语句示例:

CREATE TABLE `usertable` (
  `USER_KEY` BIGINT NULL,
  `FIELD0` text NULL,
  `FIELD1` text NULL,
  `FIELD2` text NULL,
  `FIELD3` text NULL
) ENGINE=OLAP
UNIQUE KEY(`USER_KEY`)
COMMENT 'OLAP'
DISTRIBUTED BY HASH(`USER_KEY`) BUCKETS 16
PROPERTIES (
"enable_unique_key_merge_on_write" = "true",
"light_schema_change" = "true",
"store_row_column" = "true",
);

留神:

  • 开启 light_schema_change 来反对 JSONB 行存编码 ColumnID
  • 开启 store_row_column 来存储行存格局

实现建表操作后,相似如下基于主键的点查 SQL 可通过行式存储格局和短门路执行失去性能的大幅晋升:

select * from usertable where USER_KEY = xxx;

与此同时,能够通过 JDBC 中的 Prepared Statement 来进一步晋升点查问性能。如果有短缺的内存,还能够在 BE 配置文件中开启行存 Cache,上文中均已给出应用示例,在此不再赘述。

#  总结

通过引入行式存储格局、点查问短门路优化、预处理语句以及行存缓存,Apache Doris 实现了单节点上万 QPS 的超高并发,实现了数十倍的性能飞跃。而随着集群规模的横向拓展、机器配置的晋升,Apache Doris 还能够利用硬件资源实现计算减速,本身的 MPP 架构也具备横向线性拓展的能力。因而 Apache Doris 真正具备了在 一套架构下同时 满足高吞吐的 OLAP 剖析和高并发的 Data Serving 在线服务的能力,大大简化了混合工作负载下的技术架构,为用户提供了多场景下的对立剖析体验

以上性能的实现得益于 Apache Doris 社区开发者共同努力以及 SelectDB 工程师的的继续奉献,以后已处于紧锣密鼓的发版流程中,在不久后的 2.0 版本就会公布进去。如果对于以上性能有强烈需要,欢送填写问卷提交申请,或者与 SelectDB 技术团队间接分割,提前取得 2.0-alpha 版本的体验机会,也欢送随时向咱们反馈应用意见。

作者介绍:

李航宇,Apache Doris Contributor,SelectDB 半结构化研发工程师。

正文完
 0