StarRocks 的分布式架构和 ElasticSearch 及其相似,无论是集群节点上,还是数据存储上。
StarRocks 架构通常包括两类节点:Frontend(FE)节点和 Backend(BE)节点。
1. Frontend(FE)节点
2. Backend(BE)节点
FE 节点更像是数据库的大脑,负责理解 SQL 请求、计划查询以及管理集群的配置和状态。BE 节点则像是执行器,负责存储数据和在物理硬件上执行查询计划。
在 StarRocks 的架构中,FE(Frontend)节点可以有三种不同的角色,每个角色都有其特定的职责。这三种角色分别是:
1. Master
Master FE 节点是集群的主节点,它负责处理所有的写操作,包括更新元数据(如数据库、表、分区等的变更)、处理 DDL(数据定义语言)操作,以及处理客户端的查询请求。Master FE 节点还负责协调和管理其他 FE 节点,包括同步元数据的变更给 Follower 和 Observer FE 节点。在集群中通常只有一个 Master FE 节点。
2. Follower
Follower FE 节点是主节点的热备份。它们通过复制 Master FE 节点的日志来同步元数据变化。在 Master FE 发生故障时,其中一个 Follower FE 节点可以被提升为新的 Master FE,从而确保集群的高可用性。Follower FE 节点可以处理读操作,但所有的写操作都需要通过 Master FE 节点来协调。
3. Observer
Observer FE 节点提供了一个只读视图的集群状态,它们同样通过复制 Master FE 的日志来保持元数据的同步。Observer FE 节点主要用于提供冗余和读取扩展,以及在一些架构中,可能用于负载均衡或实现读写分离。Observer 节点在故障转移中不会被提升为 Master FE 节点。
这种设计允许 StarRocks 集群实现高可用性,通过 Follower 和 Observer 节点可以在 Master 节点宕机时快速进行故障切换,而不会丢失关键的元数据信息,并且还可以在高负载情况下扩展读操作。
此外,Observer FE 节点可以用作备份,以便在集群升级或维护时保证服务的持续可用性。
FE、BE是从集群架构上体现分布式,从表数据存储上,也同样有分布式的体现。
1. 分区(Partitioning)
表数据首先被逻辑上划分为多个分区,通常基于一个或多个列的值,这些列称为分区键。
每个分区可以包含一段连续的数据范围,比如日期列,可以按月或年来分区。
分区使得数据管理更为高效,特别是针对大数据量的表,因为它可以缩小查询作用域,提高数据访问速度。
2. 分桶(Bucketing)
在每个分区内部,数据进一步被划分为多个桶(Bucket),这通常是基于数据的某个列的哈希值。
分桶策略有助于在 BE 节点间均衡数据分布,保证负载均衡和高效地并行处理查询。
3. 数据副本(Replication)
为了提高数据的可靠性和可用性,StarRocks 会在不同的 BE 节点上创建数据的副本。
数据副本可以保证在某个 BE 节点失败时,系统仍然能够访问到数据,从而实现故障恢复。
StarRocks 支持四种数据模型,在建表时,根据业务场景选准合适的数据模型,是最基本性能优化。
明细模型是默认的建表模型。如果在建表时未指定任何模型,默认创建的是明细类型的表。
明细模型并不会对数据做任何处理,DUPLICATE KEY
只用于定义排序键,相同 DUPLICATE KEY
的记录会同时存在。 所以推荐用来存储原始的日志明细数据。
注意:
1DUPLICATE KEY
显式定义。如果未指定,则默认选择表的前三列作为排序键。建表时,支持定义排序键和指标列,并为指标列指定聚合函数。当多条数据具有相同的排序键时,指标列会进行聚合。
在分析统计和汇总数据时,聚合模型能够减少查询时所需要处理的数据,提升查询效率。
注意:
AGGREGATE KEY
显式定义。AGGREGATE KEY
必需包含所有维度列,否则建表会失败。如果不通过 AGGREGATE KEY 显示定义排序键,则默认除指标列之外的列均为排序键。建表时,支持定义主键和指标列,查询时返回主键相同的一组数据中的最新数据。
更新模型可以视为聚合模型的特殊情况,指标列指定的聚合函数为 REPLACE
,返回具有相同主键的一组数据中的最新数据。
注意:
UNIQUE KEY
定义。建表时,支持定义主键和指标列,查询时返回主键相同的一组数据中的最新数据。
PRIMARY KEY
定义。分区列
和分桶列
必须在主键中。更新模型、主键模型的区别
更新模型整体上采用了 Merge-On-Read 的策略。虽然写入时处理简单高效,但是查询时需要在线聚合多版本。
主键模型采用了 Delete+Insert 的策略,保证同一个主键下仅存在一条记录,这样就完全避免了 Merge 操作。具体实现方式如下:
在建表时,可以指定一个或多个列构成排序键 (Sort Key)。表中的行会根据排序键进行排序以后再落入磁盘存储。
因为数据是按照顺序存储的,在查询数据时,使用排序列指定过滤条件。有些情况下 StarRocks 不需要扫描全表即可快速找到需要处理的数据(例如二分法),降低搜索的复杂度,从而加速查询。
为了进一步加速查询,StarRocks 在排序键的基础上又引入了前缀索引 (Prefix Index)
。前缀索引是一种 稀疏索引
。表中每 1024 行数据构成一个逻辑数据块 (Data Block)。每个逻辑数据块在前缀索引表中存储一个索引项,索引项的长度不超过 36 字节,其内容为数据块中第一行数据的排序列组成的前缀,在查找前缀索引表时可以帮助确定该行数据所在逻辑数据块的起始行号。前缀索引的大小会比数据量少 1024 倍,因此会全量缓存在内存中,在实际查找的过程中可以有效加速查询。
1. 各模型创建排序键
2. 创建排序键注意点
在定义排序列时,需要注意以下几点:
3. 举例说明
例如,建表语句中声明要创建 site_id、city_code、user_id 和 pv 四列。这种情况下,正确的排序列组合和错误的排序列组合举例如下:
正确的排序列
错误的排序列
因为数据是按照排序键顺序存储的,当基于排序键进行查询时,通过二分法会更容易定位到数据。
假设排序键是 city_code
、site_id
。那么查询条件中包含 city_code
、site_id
,或者只有city_code
,可以达到查询优化效果,但如果只有site_id
则不行。
当然排序键基于查询的优化不主要取决于此,主要是前缀索引。因为就算排序后的数据可以基于二分法定位,但都需要将排序列的全部数据都先载入内存。而通过前缀索引可以定位到数据所在的逻辑数据块,每个数据块最大只有1024行数据,然后针对这个数据块内的数据做二分法即可。而前缀索引本身的数据量只占实际数据的1/1024。
首先说明,前缀索引不需要我们手动创建,在建表时随着我们指定了排序键,StarRocks就默认创建了前缀索引。
因为前缀索引是基于排序列自动创建的,如果指定的排序列非常多,前缀索引也会占用大量内存。为了避免这种情况,StarRocks 对前缀索引做了如下限制:
1. 前缀索引限制
2. 举例说明
假设某表中排序列依次为:
那么建表后创建的前缀索引是:user_id(8) + age(4) + address(截取前24个字节)
在建表时如何选择合适的排序列,以及排序键中列的顺序,都需要仔细思考。有以下建议:
如果希望快速查询基数较高的列,那么建议在这个列创建Bloom filter(布隆过滤器)索引。
底层实现时,StarRocks为每个数据块中的索引列生成 Bloom Filter。每个Bloom Filter包含所有该数据块中该列的值。
那么在查询时,先通过每个数据库的 Bloom filter 索引过滤:
1. 布隆过滤器的假阳性概率
Bloom filter 索引有一定的误判率,也称为假阳性概率,即判断某行中包含目标数据,而实际上该行并不包含目标数据的概率。但如果判断某行中不包含目标数据,就一定不包含目标数据。
2. 高基数列
基于布隆过滤器的特点,如果Bloom filter 索引判断存在目标数据的话,还需要将完整数据块加载进内存,做进一步判断,如果索引判断不存在,则可以直接跳过去。
如果索引列的基数不高的话,在索引匹配阶段,会有大量的Bloom filter 索引判断存在目标数据。无法充分发挥Bloom filter 索引的优化性能。
如果索引列的基数很高的话,在索引匹配阶段,会有大量的Bloom filter 索引判断不存在目标数据,而被跳过。只需要进一步加载很少一部分Bloom filter的数据块就行。
主键模型
和明细模型
中所有列都可以创建 Bloom filter 索引;聚合模型
和更新模型
中,只有维度列(即 Key 列)支持创建 Bloom filter 索引。in
和 =
过滤条件的查询效率,例如 Select xxx from table where xxx in ()
和 Select xxx from table where column = xxx
。1. 建表时创建
建表时,通过在 PROPERTIES 中指定 bloom_filter_columns 来创建索引。例如,如下语句为表 table1 的 k1 和 k2 列创建 Bloom filter 索引。多个索引列之间需用逗号 (,) 隔开。
CREATE TABLE table1
(
k1 BIGINT,
k2 LARGEINT,
v1 VARCHAR(2048) REPLACE,
v2 SMALLINT DEFAULT "10"
)
ENGINE = olap
PRIMARY KEY(k1, k2)
DISTRIBUTED BY HASH (k1, k2) BUCKETS 10
PROPERTIES("bloom_filter_columns" = "k1,k2");
2. 修改
可以使用 ALTER TABLE 语句来增加,减少和删除索引。
(1) 如下语句增加了一个 Bloom filter 索引列 v1。
ALTER TABLE table1 SET ("bloom_filter_columns" = "k1,k2,v1");
(2) 如下语句减少了一个 Bloom filter 索引列 k2。
ALTER TABLE table1 SET ("bloom_filter_columns" = "k1");
(3) 如下语句删除了 table1 的所有 Bloom filter 索引。
ALTER TABLE table1 SET ("bloom_filter_columns" = "");
如果希望快速查询基数较低的列(值大量重复,例如 ENUM 类型的列),那么建议在这个列创建BitMap(位图)索引。
BitMap 即为一个 bit 数组,一个 bit 的取值有两种:0 或 1。
针对某一列创建BitMap索引时,就会给该列的每一种值创建一个BitMap结构。
BitMap的长度等于表数据的长度,表数据是有顺序的。假设针对某列创建BitMap索引,该列有值为“A”,那么在值“A”对应的BitMap结构中,如果表中第n行数据的列值也为A,那么BitMap中第n位值为1,否则就为0.
1. 举例说明
假设有一张用户表(User),其中包含两列:国家(Country)和用户ID(UserID)。考虑到国家的数量相对于用户数来说通常很少,因此Country列具有低基数特点,适合创建BitMap索引。
我们为Country列创建BitMap索引后,假设有如下数据:
UserID Country
1 USA
2 Canada
3 USA
4 Mexico
对于这个例子,BitMap索引将创建如下:
当我们需要查询所有来自"USA"的用户时:
通过使用BitMap索引,StarRocks能够快速过滤掉不匹配的行,大大减少了查询所需的时间和资源消耗。特别是在涉及联合查询或者多条件查询时,通过对多个BitMap索引进行逻辑 "AND
"、"OR
"操作,StarRocks可以有效地定位满足所有条件的数据行,提高查询效率。
倒排索引
倒排索引(Inverted Index):是一种数据库索引类型,它用于存储一个单词或者一个字段值到包含它的文档或记录的映射。
很明显 BitMap索引就是一种倒排索引,不过不像es对文本进行分词再与记录映射,StarRocks是将索引列完整的值做映射。
=
) 查询或 [NOT] IN
范围查询的列。主键模型
和明细模型
中所有列都可以创建 Bitmap 索引;聚合模型
和更新模型中
,只有维度列(即 Key 列)支持创建 bitmap 索引。
- 建表时创建
建表时,通过在 PROPERTIES 中指定 bloom_filter_columns 来创建索引。例如,如下语句为表 table1 的 k1 和 k2 列创建 Bloom filter 索引。多个索引列之间需用逗号 (,) 隔开。
CREATE TABLE d0.table_hash
(
k1 TINYINT,
k2 DECIMAL(10, 2) DEFAULT "10.5",
v1 CHAR(10) REPLACE,
v2 INT SUM,
INDEX index_name (column_name) [USING BITMAP] [COMMENT '']
)
ENGINE = olap
AGGREGATE KEY(k1, k2)
DISTRIBUTED BY HASH(k1) BUCKETS 10
PROPERTIES ("storage_type" = "column");
参数说明:
- 单独创建索引
CREATE INDEX index_name ON table_name (column_name) [USING BITMAP] [COMMENT'']
参数说明:
- 删除索引
删除指定表的 Bitmap 索引。
DROP INDEX index_name ON [db_name.]table_name;
- 查看索引
查看索引,当前仅支持查看指定表的所有 Bitmap 索引。创建 bitmap 索引为异步过程,使用该语句只能查看到已经创建完成的索引
SHOW INDEX FROM [db_name.]table_name [FROM db_name]
或
SHOW KEY FROM [db_name.]table_name [FROM db_name]
分区用于将数据划分成不同的区间。分区的主要作用是将一张表按照分区键拆分成不同的管理单元,针对每一个管理单元选择相应的存储策略,比如副本数、分桶数、冷热策略和存储介质等。
StarRocks 支持在一个集群内使用多种存储介质,您可以将新数据所在分区放在 SSD 盘上,利用 SSD 优秀的随机读写性能来提高查询性能,将旧数据存放在 SATA 盘上,以节省数据存储的成本。
业务系统中⼀般会选择根据时间进行分区,以优化大量删除过期数据带来的性能问题,同时也方便冷热数据分级存储。
分区单位的选择,需要综合考虑数据量、查询特点、数据管理粒度等因素。
分区的下一级是分桶,StarRocks 采⽤ Hash 算法作为分桶算法。
在同一分区内,分桶键哈希值相同的数据形成 Tablet,Tablet 以多副本冗余的形式存储,是数据均衡和恢复的最⼩单位。Tablet 的副本由一个单独的本地存储引擎管理,数据导入和查询最终都下沉到所涉及的 Tablet 副本上。
建表时,必须指定分桶键。
基于分区、分桶的搭配,常见有2种数据分布方式。
- Hash 数据分布方式
一张表为一个分区,分区按照分桶键和分桶数量进一步进行数据划分。
如下表,只有一个分区,但有10个分桶,数据基于site_id的HASH值,分布在这10个分桶内。只要保证site_id的值是均匀的,就能保证数据是均匀分布在不同分桶内。
CREATE TABLE site_access(
site_id INT DEFAULT '10',
city_code SMALLINT,
user_name VARCHAR(32) DEFAULT '',
pv BIGINT SUM DEFAULT '0'
)
AGGREGATE KEY(site_id, city_code, user_name)
DISTRIBUTED BY HASH(site_id) BUCKETS 10;
- Range+Hash 数据分布方式
一张表拆分成多个分区,每个分区按照分桶键和分桶数量进一步进行数据划分。
如下表中,分区键为event_day,基于时间范围生成3个分区,对应不同时间范围的数据落在不同的分区内。
在每个分区内,再基于site_id的HASH值,将数据分布在10个分桶内。因此该表数据一共分布在30个桶内。
CREATE TABLE site_access(
event_day DATE,
site_id INT DEFAULT '10',
city_code VARCHAR(100),
user_name VARCHAR(32) DEFAULT '',
pv BIGINT SUM DEFAULT '0'
)
AGGREGATE KEY(event_day, site_id, city_code, user_name)
PARTITION BY RANGE(event_day)
(
PARTITION p1 VALUES LESS THAN ("2020-01-31"),
PARTITION p2 VALUES LESS THAN ("2020-02-29"),
PARTITION p3 VALUES LESS THAN ("2020-03-31")
)
DISTRIBUTED BY HASH(site_id) BUCKETS 10;
创建分区的方式简单可分为手动创建分区
和动态创建分区
,最常见的是动态创建分区。
目前支持分区键的数据类型为日期类型
和整数类型
。
在手动创建分区又可分为单个创建分区
和批量创建分区
。
1. 单个创建分区
如下,需要我们指定每个分区的实际范围。
CREATE TABLE site_access(
event_day DATE,
site_id INT DEFAULT '10',
city_code VARCHAR(100),
user_name VARCHAR(32) DEFAULT '',
pv BIGINT SUM DEFAULT '0'
)
AGGREGATE KEY(event_day, site_id, city_code, user_name)
PARTITION BY RANGE(event_day)
(
PARTITION p1 VALUES LESS THAN ("2020-01-31"),
PARTITION p2 VALUES LESS THAN ("2020-02-29"),
PARTITION p3 VALUES LESS THAN ("2020-03-31")
)
DISTRIBUTED BY HASH(site_id) BUCKETS 10;
短板:某些场景,如果我们需要创建分区的数量比较多,就不适合挨个指定每个分区的范围了,就可以用到下面的批量创建分区。
2. 批量创建分区
如下,我们指定从 "2021-01-01" 到 "2021-02-01" 每隔一天创建一个分区,只要就一次性创建了31个分区。
CREATE TABLE site_access (
datekey DATE,
site_id INT,
city_code SMALLINT,
user_name VARCHAR(32),
pv BIGINT DEFAULT '0'
)
ENGINE=olap
DUPLICATE KEY(datekey, site_id, city_code, user_name)
PARTITION BY RANGE (datekey) (
START ("2021-01-01") END ("2021-02-01") EVERY (INTERVAL 1 DAY)
)
DISTRIBUTED BY HASH(site_id) BUCKETS 10;
短板:上述手动创建分区的方式,都是基于固定范围创建时间。通常我们的表是不断有数据写入的。就算我们提前创建1年的分区,那么1年后能否自动创建分区呢?之前老的分区以及里面的历史数据是否可以自动删除呢?动态创建分区可以解决这个问题。
CREATE TABLE site_access(
event_day DATE,
site_id INT DEFAULT '10',
city_code VARCHAR(100),
user_name VARCHAR(32) DEFAULT '',
pv BIGINT DEFAULT '0'
)
DUPLICATE KEY(event_day, site_id, city_code, user_name)
PARTITION BY RANGE(event_day)(
PARTITION p20200321 VALUES LESS THAN ("2020-03-22"),
PARTITION p20200322 VALUES LESS THAN ("2020-03-23"),
PARTITION p20200323 VALUES LESS THAN ("2020-03-24")
)
DISTRIBUTED BY HASH(event_day, site_id) BUCKETS 32
PROPERTIES(
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-3",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "32",
"dynamic_partition.history_partition_num" = "0"
);
上面建表sql中,先手动预创建3个分区,PROPERTIES
中 dynamic_partition.enable
前缀的配置则是配置动态分区,列几个常见配置项:
dynamic_partition.enable
:开启动态分区特性,取值为 true(默认)或 false。dynamic_partition.time_unit
:必填,动态分区的时间粒度,取值为 HOUR、DAY、WEEK、MONTH 或 YEAR。例如:为MONTH时,动态创建的分区名后缀格式为 yyyyMM,每个月创建一个分区。dynamic_partition.time_zone
:动态分区的时区。默认与系统时区一致。dynamic_partition.start
:保留的动态分区的起始偏移,取值范围为负整数。根据 dynamic_partition.time_unit
属性的不同,以当天(周/月)为基准,分区范围在此偏移之前的分区将会被删除。比如设置为-3,并且dynamic_partition.time_unit为day,则表示 3 天前的分区会被删掉。dynamic_partition.end
:必填,提前创建的分区数量,取值范围为正整数。根据 dynamic_partition.time_unit 属性的不同,以当天(周/月)为基准,提前创建对应范围的分区。dynamic_partition.prefix
:动态分区的前缀名,默认值为 p。dynamic_partition.buckets
:动态分区的分桶数量。默认与 BUCKETS 保留字指定的分桶数量、或者 StarRocks 自动设置的分桶数量保持一致。dynamic_partition.replication_num
:在动态创建的分区中,每个 tablet 副本的数量。默认值与建表时配置的副本数量相同。动态分区相关 FE 配置项
ynamic_partition_check_interval_seconds
:FE 配置项,动态分区检查的时间周期,默认为 600,单位为 s,即每10分钟检查一次分区情况是否满足 PROPERTIES
中动态分区属性,如不满足,则会自动 创建
和 删除
分区。
手动增加分区
如下示例中,在 site_access 表添加新的分区,用于存储新月份的数据,并且调整分桶数量为 20:
ALTER TABLE site_access
ADD PARTITION p4 VALUES LESS THAN ("2020-04-30")
DISTRIBUTED BY HASH(site_id) BUCKETS 20;
手动删除分区
执行如下语句,删除 site_access 表中分区 p1 及数据:
说明:分区中的数据不会立即删除,会在 Trash 中保留一段时间(默认为一天)。如果误删分区,可以通过 RECOVER 命令恢复分区及数据。
ALTER TABLE site_access
DROP PARTITION p1;
手动恢复分区
执行如下语句,恢复 site_access 表中分区 p1 及数据:
RECOVER PARTITION p1 FROM site_access;
查看表当前的分区情况
SHOW PARTITIONS FROM [db_name.]table_name
查看表动态分区属性
SHOW CREATE TABLE [db_name.]table_name
修改表的动态分区属性
执行 ALTER TABLE,修改动态分区的属性,例如暂停或者开启动态分区特性。
ALTER TABLE [db_name.]table_name SET("dynamic_partition.enable"="false");
ALTER TABLE [db_name.]table_name SET("dynamic_partition.enable"="true");
对每个分区的数据,StarRocks 会根据分桶键和分桶数量进行哈希分桶。
分桶键的设置,一方面应该将数据均匀分布开,避免数据倾斜情况严重;另一方面如果查询简单,可以利用分桶优化查询。
优化查询
如果查询比较简单,建议选择经常作为查询条件的列为分桶键,提高查询效率。
假设某表常见查询条件包含:WHERE code={code}
,有10个分桶,如果将 code
作为分桶键。那么常见的查询场景中会过滤掉9个分桶,只需要扫描1个分桶的数据即可。
前提是,code
作为分桶键不会造成太大的数据倾斜,如果极端情况下基于“二八原则”,常用code
所在分桶内的数据量或访问量,远高于其他几个桶之和,那查询的性能也没有多大的提高。
避免数据倾斜
倘若查询的场景比较多,查询条件复杂,大多数场景无法通过某列轻松过滤掉很多数据,那么尽量选择高基数的列作为分桶键,保证数据在各个分桶中尽量均衡,提高集群资源利用率。
要结合实际业务场景,除了避免存储数据的倾斜,还要避免查询数据的倾斜,导致系统局部的性能瓶颈。
如果单列无法保证数据均匀分散,可以使用多个列作为数据的分桶键,但是建议不超过 3 个列。
注意
建表时设置
CREATE TABLE site_access
(
site_id INT DEFAULT '10',
city_code SMALLINT,
user_name VARCHAR(32) DEFAULT '',
pv BIGINT SUM DEFAULT '0'
)
AGGREGATE KEY(site_id, city_code, user_name)
DISTRIBUTED BY HASH(site_id,city_code) BUCKETS 10;
如上,在建表时通过 BUCKETS
指定每个分区的分桶数量。
新增分区时设置
已创建分区的分桶数量是不能修改的。
但是在新增分区的时,可以再为新的分区设定分桶数量。
ALTER TABLE <table_name> ADD PARTITION <partition_name>
[DISTRIBUTED BY HASH (k1[,k2 ...]) [BUCKETS num]];
自 2.5.7 版本起, StarRocks 能够自动设置分桶数量。如果需要启用该功能,则您需要执行 ADMIN SET FRONTEND CONFIG ("enable_auto_tablet_distribution" = "true");
以开启该 FE 动态参数。
假设 BE 数量为 X,StarRocks 推断分桶数量的策略如下:
X <= 12 tablet_num = 2X
X <= 24 tablet_num = 1.5X
X <= 36 tablet_num = 36
X > 36 tablet_num = min(X, 48)
此时建表就不需要再设置分桶数量:
CREATE TABLE site_access(
site_id INT DEFAULT '10',
city_code SMALLINT,
user_name VARCHAR(32) DEFAULT '',
pv BIGINT SUM DEFAULT '0'
)
AGGREGATE KEY(site_id, city_code, user_name)
DISTRIBUTED BY HASH(site_id,city_code); --无需手动设置分桶数量
对于 StarRocks 而言,分区和分桶的选择是非常关键的。在建表时选择合理的分区键和分桶键,可以有效提高集群整体性能。因此建议在选择分区键和分桶键时,根据业务情况进行调整。