緣起
首先聲明,本人無意叛變,依然是ClickHouse的忠實信徒。
對于Doris,一直聽圈內(nèi)的人在說,吹得神乎其神,但到底有多強,從來沒有真正的去嘗試一把。
直到這次,被人狠狠上了一課。
在一次全文檢索的模糊查詢的場景PK中,ClickHouse一敗涂地,讓本人很是沒面子,咳咳,大哥被人欺負了,這能忍?
一直知道Doris在多表join方面很強,沒想到在全文檢索也能彎道超車,這實在是猝不及防啊。
啥也別說了,盤他!
咱就是說,知己知彼,方能百戰(zhàn)不殆。就算要死,也得死得明白不是?何況咱的目的是接近它,了解它,成為它,并打敗它。
Doris到底是個啥
首先不得不說,Doris有完備的中文版文檔,這對于我這種英語渣來說,簡直太有誘惑力了。不得不說,在鯊人誅心方面,還是Doris玩得溜哇。
Doris的整體架構(gòu)分為兩大部分,F(xiàn)E和BE。
FE使用Java語言實現(xiàn),自帶前端頁面(前端代碼為vue3)。主要用來做數(shù)據(jù)接入,查詢解析,元數(shù)據(jù)管理,節(jié)點管理等。
BE使用C++實現(xiàn),主要用來存儲和查詢。
簡單來說,它把職責(zé)劃分得很清楚,所有的調(diào)度,交互都交給了FE,BE成了完完全全的工具人,F(xiàn)E讓我存我就存,讓我查我就查,查出來的數(shù)據(jù)怎么用,怎么返回,咱不關(guān)心。
打個比方,F(xiàn)E就是產(chǎn)品經(jīng)理,負責(zé)出設(shè)計出需求,BE就是苦逼的打工碼農(nóng),干活,背鍋,一樣不落。BE唯一的價值就是它的計算資源和存儲資源。
除了FE和BE兩大模塊之外,Doris還提供了一個叫做Brokers的模塊。這個組件對于Doris來說不是必須的。但是提供一定的擴展能力,如實現(xiàn)從HDFS、BOS、AFS導(dǎo)入數(shù)據(jù)等。它是一種異步的導(dǎo)入方式,因為這個組件是用Java寫的,資源占用相對來說比較高,在離線數(shù)據(jù)大批量導(dǎo)入場景,甚至比Spark Load要消耗更多的資源。所以對于這個組件,我的建議是,有"SPA"上"SPA",沒"SPA"咱還是"Bro"。
編譯
為了簡化流程,我們使用官方提供的docker編譯鏡像。
下載鏡像:
docker pull apache/doris:build-env-ldb-toolchain-latest
進入容器:
docker run --rm -w /var/src -v `pwd`/build/.m2:/root/.m2 -v `pwd`:/var/src -it apache/doris:build-env-ldb-toolchain-latest bash
上述命令中,我們將.m2目錄從容器里掛載出來,并將本地的源碼掛載到容器內(nèi)部。這樣做的好處是可以讓編譯的產(chǎn)物(包括臨時產(chǎn)物)都留在本地,下次編譯時無需重復(fù)下載依賴。(Java的maven依賴下載非常耗時)
直接執(zhí)行 build.sh。
./build.sh
整個編譯過程耗時接近一個半小時,相對來說還算可以接受的。
編譯完成后,產(chǎn)物在output目錄中:
友情提醒:對于沒有二次開發(fā)需求的用戶,建議直接下載官方提供的release版本使用即可。
集群部署
集群部署架構(gòu)初步規(guī)劃如下:
角色 | 節(jié)點 | ip |
---|---|---|
FE | leader | 192.168.101.94 |
BE | node1 | 192.168.101.93 |
BE | node2 | 192.168.101.94 |
BE | node3 | 192.168.101.96 |
Doris的官方安裝包分為avx2和非avx2版本,根據(jù)自己的CPU支持情況下載即可。整個安裝包2.8個G,請在網(wǎng)絡(luò)環(huán)境比較好的時候下載。
解壓后有三個文件夾:
fe文件夾用于fe部署,be文件夾用于be部署,extensions文件夾是一些擴展,如hdfs的brokers部署,可以先不用管。
FE部署
進入fe文件夾,修改配置文件:
修改conf/fe.conf:
priority_networks = 192.168.101.0/24 # CIDR方式尋址,fe集群之間,fe 與be之間通訊的關(guān)鍵, 可以寫多組
?
# 數(shù)據(jù)目錄
meta_dir = ${DORIS_HOME}/doris-meta
?
# 端口
http_port = 58030 # 前端頁面端口
rpc_port = 59020 # thrift server 端口
query_port = 59030 # 查詢的端口, mysql client可以使用該端口連接上Doris
edit_log_port = 59010 # FE集群之間通信的端口
端口需要看一下本地有沒有被占用,盡量找沒有占用的端口。由于我部署時默認的8030已被本地的hadoop應(yīng)用占用,所以使用了58030, 如果默認端口沒有被占用,使用默認端口即可。
啟動應(yīng)用:
bin/start_fe.sh --daemon
驗證fe是否成功啟動:
[root@master94 fe]# curl http://192.168.101.94:58030/api/bootstrap
{"msg":"success","code":0,"data":{"replayedJournalId":0,"queryPort":0,"rpcPort":0,"version":""},"count":0}
或直接登錄前端頁面:
http://192.168.101.94:58030/login
顯示如下頁面,說明部署fe成功:
默認登錄用戶為 root, 密碼為空。
登錄進去后頁面如圖所示:
BE部署
- 修改配置文件:
修改 conf/be.conf:
priority_networks = 192.168.101.0/24 #需要和fe配置一致
?
be_port = 59060 # 和FE通訊的端口,用來接收FE的請求
webserver_port = 58040 # http端口
heartbeat_service_port = 59050 # 心跳端口,用來接收FE的心跳
brpc_port = 58060 # BE之間通訊
?
# storage_root_path = ${DORIS_HOME}/storage
storage_root_path = /data01/app/apache-doris-2.0.3-bin-x64/data/ssd,medium:SSD;/data01/app/apache-doris-2.0.3-bin-x64/data/hdd,medium:HDD
# HDD 和 SSD 并不是真的固態(tài)盤和機械盤,只需要有相應(yīng)的目錄即可,SSD代表熱數(shù)據(jù)目錄,HDD代表冷數(shù)據(jù)目錄,用于BE數(shù)據(jù)的冷熱機制
- 設(shè)置JAVA_HOME:
修改bin/start_be.sh,在腳本第一行增加:
export JAVA_HOME=/usr/java/jdk1.8.0_201
以上操作在所有需要部署B(yǎng)E的節(jié)點上都要操作一遍。
- 將所有的BE節(jié)點加入到FE中:
從FE的頁面操作:
進入Playground頁面,執(zhí)行如下SQL:
ALTER SYSTEM ADD BACKEND "192.168.101.93:59050";
其中, 192.168.101.93為BE的節(jié)點IP,有幾個節(jié)點就需要增加幾次;需要注意的是該IP需要與IP匹配。
59050為heartbeat_service_port, 根據(jù)配置文件填寫即可。
如下圖所示:
通過上面的方式,將192.168.101.93和192.168.101.94都添加進去。
從命令行操作:
我們可以使用mysql的client登陸到Doris的FE:
mysql -h 192.168.101.94 -P 59030 -uroot
192.168.101.94指的是FE的地址。
59030是FE配置的query_port。
登錄進去后,執(zhí)行相同的SQL語句。
我們使用這種方式將192.168.101.96加進去:
[root@master94 be]# mysql -h 192.168.101.94 -P 59030 -uroot
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.99 Doris version doris-2.0.3-rc06-37d31a5
?
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
?
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
?
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
?
mysql> ALTER SYSTEM ADD BACKEND "192.168.101.96:59050";
Query OK, 0 rows affected (0.01 sec)
- 啟動BE:
sysctl -w vm.max_map_count=2000000
bin/start_be.sh --daemon
- 驗證BE是否正常啟動
在FE的web頁面執(zhí)行SQL: SHOW PROC '/backends';
當(dāng)看到Alive均為true, 集群即部署成功。
簡單操作驗證
- 創(chuàng)建數(shù)據(jù)庫:
create database demo;
- 創(chuàng)建表:
use demo;
?
CREATE TABLE IF NOT EXISTS demo.example_tbl
(
`user_id` LARGEINT NOT NULL COMMENT "用戶id",
`date` DATE NOT NULL COMMENT "數(shù)據(jù)灌入日期時間",
`city` VARCHAR(20) COMMENT "用戶所在城市",
`age` SMALLINT COMMENT "用戶年齡",
`sex` TINYINT COMMENT "用戶性別",
`last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用戶最后一次訪問時間",
`cost` BIGINT SUM DEFAULT "0" COMMENT "用戶總消費",
`max_dwell_time` INT MAX DEFAULT "0" COMMENT "用戶最大停留時間",
`min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用戶最小停留時間"
)
AGGREGATE KEY(`user_id`, `date`, `city`, `age`, `sex`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 1
PROPERTIES (
"replication_allocation" = "tag.location.default: 1"
);
- 準備數(shù)據(jù):
10000,2017-10-01,北京,20,0,2017-10-01 06:00:00,20,10,10
10000,2017-10-01,北京,20,0,2017-10-01 07:00:00,15,2,2
10001,2017-10-01,北京,30,1,2017-10-01 17:05:45,2,22,22
10002,2017-10-02,上海,20,1,2017-10-02 12:59:12,200,5,5
10003,2017-10-02,廣州,32,0,2017-10-02 11:20:00,30,11,11
10004,2017-10-01,深圳,35,0,2017-10-01 10:00:15,100,3,3
10004,2017-10-03,深圳,35,0,2017-10-03 10:20:22,11,6,6
將上面的數(shù)據(jù)保存在test.csv中。
- 導(dǎo)入數(shù)據(jù):
curl --location-trusted -u root: -T test.csv -H "column_separator:," http://127.0.0.1:58030/api/demo/example_tbl/_stream_load
執(zhí)行完結(jié)果如下:
[root@master94 apache-doris-2.0.3-bin-x64]# curl --location-trusted -u root: -T test.csv -H "column_separator:," http://127.0.0.1:58030/api/demo/example_tbl/_stream_load
{
"TxnId": 2,
"Label": "af5b1ad2-a86b-4dac-a1b8-ba91e950fc02",
"Comment": "",
"TwoPhaseCommit": "false",
"Status": "Success",
"Message": "OK",
"NumberTotalRows": 7,
"NumberLoadedRows": 7,
"NumberFilteredRows": 0,
"NumberUnselectedRows": 0,
"LoadBytes": 399,
"LoadTimeMs": 300,
"BeginTxnTimeMs": 25,
"StreamLoadPutTimeMs": 211,
"ReadDataTimeMs": 0,
"WriteDataTimeMs": 8,
"CommitAndPublishTimeMs": 53
}
- 查詢數(shù)據(jù)
關(guān)于Doris集群相關(guān)說明
Doris集群里并沒有集群名的概念。其中,F(xiàn)E集群和BE集群是互相獨立,互不干擾的。
你可以這么理解:FE集群相當(dāng)于CK中的zookeeper集群,BE集群才是真正的數(shù)據(jù)庫集群。
與zookeeper類似,F(xiàn)E采用了類似Raft的選舉機制,分為三類角色,分別為Leader, Follower, Oberserver。
一般可使用3 * Follower + n * Oberserver 的架構(gòu)來保證FE集群的讀寫高可用。
Oberserver 是只讀的擴展節(jié)點,可以通過水平擴展的方式來增加讀的能力。
Doris集群沒有分片和副本的概念。它有一點類似于kafka集群的機制,分片和副本是針對表維度的。也就是說,每張表,你可以設(shè)置不同的分片和副本數(shù)。
Doris的集群擴縮容非常簡單,幾乎不需要考慮配置的同步問題,以及元數(shù)據(jù)的同步的問題,僅需要一個命令就可以實現(xiàn)。
FE增加節(jié)點:
#擴容FE節(jié)點,可以將新節(jié)點添加為follower
ALTER SYSTEM ADD FOLLOWER "fe_host:edit_log_port";
?
#或新節(jié)點添加為observer
ALTER SYSTEM ADD OBSERVER "fe_host:edit_log_port";
BE增加節(jié)點:
ALTER SYSTEM ADD BACKEND "be_host:heartbeat_service_port";
增加節(jié)點后,集群內(nèi)部會自動進行數(shù)據(jù)的再均衡。
刪除節(jié)點也很簡單,只需要將上述SQL的ADD換成DECOMMISSION即可(FE使用DROP下線節(jié)點)。
由上面的操作可知,BE之間其實是互相不可見的,BE的集群統(tǒng)一通過FE來調(diào)度,因此不存在BE之間互相同步數(shù)據(jù)帶來的資源消耗。
Doris 常見概念介紹
數(shù)據(jù)模型
Doris的表也有Engine的概念,但是Doris的Engine和ClickHouse的表Engine不太一樣。
Doris的Engine主要用來區(qū)分數(shù)據(jù)源的,比如OLAP、MySQL、ES、BROKER等。只有OLAP這個engine是由Doris自己負責(zé)數(shù)據(jù)的存儲和管理。它也是默認的engine。
而真正和ClickHouse中表engine可以類比的概念則是數(shù)據(jù)模型。它決定了Doris在存儲數(shù)據(jù)時,在內(nèi)存的分布形式。
Doris提供了三類數(shù)據(jù)模型。
數(shù)據(jù)模型 | ||
---|---|---|
Aggregate | 數(shù)據(jù)按key做聚合 | 聚合類型包括SUM、REPLACE、MAX、MIN、REPLACE_IF_NOT_NULL、HLL_UNION、BITMAP_UNION等,支持agg_state操作。會丟失明細數(shù)據(jù)。 |
Unique | 數(shù)據(jù)按主鍵唯一 | 分merge_on_read和merge_on_write兩種。merge_on_read可以看成是特殊的Aggregate模型,相當(dāng)于按照key做replace,查詢性能比較慢;merge_on_write是在插入時即完成了去重,查詢性能高。 |
Duplicate | 數(shù)據(jù)可重復(fù) | 分為排序和不排序兩類。數(shù)據(jù)可重復(fù),不會丟失明細數(shù)據(jù)。類似clickhouse中的原生Mergetree。 |
bucket、tablet、partition
Doris的表分為兩層結(jié)構(gòu),分別為分區(qū)和分桶。
每個分桶文件就是一個數(shù)據(jù)分片(tablet)。tablet是數(shù)據(jù)劃分的最小邏輯單元。各個tablet之間的數(shù)據(jù)在物理上獨立,多個tablet組成一個partition,所以各partition之間的數(shù)據(jù)在物理上也是獨立的。多個partition組成了一張表。
不同于ClickHouse,Doris沒有本地表和分布式表的概念。它的查詢是由FE調(diào)度,在各個BE節(jié)點上查詢后進行匯總的,因此每次查詢都是分布式查詢。數(shù)據(jù)的分布由hash key保證。
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16
partition可以按range分區(qū)和list分區(qū)。
range分區(qū)即是指按時間列進行分區(qū),語法如下所示:
PARTITION BY RANGE(`date`)
(
PARTITION `p201701` VALUES LESS THAN ("2017-02-01"),
PARTITION `p201702` VALUES LESS THAN ("2017-03-01"),
PARTITION `p201703` VALUES LESS THAN ("2017-04-01")
)
如果導(dǎo)入的數(shù)據(jù)不在表的分區(qū)范圍內(nèi),則是無法導(dǎo)入的。
上述這種靜態(tài)分區(qū)是非常不靈活的,只適用于存量數(shù)據(jù)的性能測試場景。在不斷有實時數(shù)據(jù)生成的場景,是不適合的,此時我們可以采用動態(tài)分區(qū)的方式:
PROPERTIES (
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-30",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.create_history_partition"="true",
"replication_num" = "1"
);
如上參數(shù)說明如下:
- dynamic_partition.enable: 開啟動態(tài)分區(qū)開關(guān)
- dynamic_partition.time_unit:時間單位,可以是hour、day、week、month、year
- dynamic_partition.start: 以當(dāng)前時間為基準,往前保留多少個分區(qū),多余的分區(qū)會被刪除掉
- dynamic_partition.end:以當(dāng)前時間為基準,往后新創(chuàng)建多少個分區(qū)
- dynamic_partition.prefix:動態(tài)分區(qū)的前綴
- dynamic_partition.create_history_partition:是否創(chuàng)建歷史分區(qū)
list分區(qū),就是通過key進行分類了,相同的key的數(shù)據(jù)自動落在同一個分區(qū)內(nèi),語法如下:
PARTITION BY LIST(`city`)
(
PARTITION `p_cn` VALUES IN ("Beijing", "Shanghai", "Hong Kong"),
PARTITION `p_usa` VALUES IN ("New York", "San Francisco"),
PARTITION `p_jp` VALUES IN ("Tokyo")
)
Doris表的副本數(shù)指定是在建表時指定的,因此每張表可以有不同的副本數(shù)。
PROPERTIES
(
"replication_num" = "3"
);
如果不指定,默認副本數(shù)為3。由Doris自己保證數(shù)據(jù)在各個BE的均勻分布,如下圖所示。
如果BE節(jié)點發(fā)生了擴容,數(shù)據(jù)也會進行重新均衡,當(dāng)然均衡的基本單位是整個tablet移動。
因此,單個tablet不建議太大,否則會影響數(shù)據(jù)查詢和副本遷移的性能。當(dāng)然也不建議太小,否則聚合效果不佳,建議單個tablet在1-10G之間,理論上是可以沒有上限的。
tablet的數(shù)量 = partition數(shù)量 * bucket數(shù)量 * 副本數(shù)。
partition內(nèi)bucket的數(shù)量一旦指定就不能更改了,因此在指定bucket數(shù)量時,應(yīng)充分考慮到集群后續(xù)擴容的情況。
索引介紹
Doris的索引分類兩類。
一類是內(nèi)建的索引,包括前綴索引和ZoneMap索引。內(nèi)建索引不需要顯式創(chuàng)建。
另一類是二級索引,包括倒排、布隆過濾器、bitmap索引等,從概念上來說,和ClickHouse的跳數(shù)索引類似。
前綴索引
ZoneMap索引是在列存格式上,自動維護每一列的索引信息,包括MinMax、Null值等,沒有太多可以擴展講解的東西。
重點來看前綴索引。
前綴索引是在排序的基礎(chǔ)上,根據(jù)給定前綴列,快速查詢數(shù)據(jù)的索引方式。
Doris默認會取表結(jié)構(gòu)的前36個字節(jié)作為前綴索引列,特例是遇到VARCHAR自動截止。
這就要求我們在建表時,合理編排字段的順序,有助于加速查詢效率。
如下面的示例:
CREATE TABLE IF NOT EXISTS demo.example_tbl
(
`user_id` LARGEINT NOT NULL COMMENT "用戶id",
`date` DATE NOT NULL COMMENT "數(shù)據(jù)灌入日期時間",
`city` VARCHAR(20) COMMENT "用戶所在城市",
`age` SMALLINT COMMENT "用戶年齡",
`sex` TINYINT COMMENT "用戶性別",
`last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用戶最后一次訪問時間",
`cost` BIGINT SUM DEFAULT "0" COMMENT "用戶總消費",
`max_dwell_time` INT MAX DEFAULT "0" COMMENT "用戶最大停留時間",
`min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用戶最小停留時間"
)
AGGREGATE KEY(`user_id`, `date`, `city`, `age`, `sex`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 1
PROPERTIES (
"replication_allocation" = "tag.location.default: 1"
);
上例中,前綴索引就是 user_id(8字節(jié)) + date(3字節(jié)) + city(20)字節(jié),雖然說前三個字段沒有達到36字節(jié),但是由于遇到了varchar,所以自動截止,不再往下了。
如果我們查詢where age = 20
,那么就不會走到前綴索引。其查詢效率遠遠比不上where user_id = 1001 and age = 20
(走到了前綴索引)。
有人肯定有疑問了,where條件千變?nèi)f化,我怎么能保證一定能命中前綴索引呢?只有36字節(jié)的限制實在是太小了。不可能囊括所有查詢場景。
的確如此,所以Doris推出了rollup的概念,可以通過rollup來調(diào)整前綴索引,使得常用的查詢都能命中前綴索引,從而加速查詢。這點我們在后面介紹rollup的時候再說。
倒排索引
是的,你沒有看錯。這個才是原汁原味的倒排索引。
相比于ClickHouse里的inverted,Doris的倒排,才是真正的大殺器。這也是為什么本文迫不及待想要了解Doris的主要原因。
與Doris的inverted比起來,ClickHouse的倒排索引粗陋、簡單,更像一個還未進化成熟的小玩具。
語法如下:
CREATE TABLE table_name
(
columns_difinition,
INDEX idx_name1(column_name1) USING INVERTED [PROPERTIES("parser" = "english|unicode|chinese")] [COMMENT 'your comment']
INDEX idx_name2(column_name2) USING INVERTED [PROPERTIES("parser" = "english|unicode|chinese")] [COMMENT 'your comment']
INDEX idx_name3(column_name3) USING INVERTED [PROPERTIES("parser" = "chinese", "parser_mode" = "fine_grained|coarse_grained")] [COMMENT 'your comment']
INDEX idx_name4(column_name4) USING INVERTED [PROPERTIES("parser" = "english|unicode|chinese", "support_phrase" = "true|false")] [COMMENT 'your comment']
INDEX idx_name5(column_name4) USING INVERTED [PROPERTIES("char_filter_type" = "char_replace", "char_filter_pattern" = "._"), "char_filter_replacement" = " "] [COMMENT 'your comment']
INDEX idx_name5(column_name4) USING INVERTED [PROPERTIES("char_filter_type" = "char_replace", "char_filter_pattern" = "._")] [COMMENT 'your comment']
)
table_properties;
其中:
- parser: 分詞器,不指定代表不分詞。
- english: 英文分詞,用空格和標(biāo)點進行分詞
- chinese:中文分詞,性能比english低
- unicode:中英文混合場景
- parser_mode: 分詞模式
- fine_grained:細粒度模式
- coarse_grained:粗粒度模式(默認模式)
- support_phrease: 用于指定是否支持match_phrease短語查詢加速
- true為支持,但是需要更多存儲空間
- false為不支持,更省存儲空間,可以使用match_all來查詢多個關(guān)鍵字(默認配置)
- char_filter: 在分詞前對字符串提前處理
- char_filter_type:處理類型(當(dāng)前僅支持char_replace)
- char_replace:將pattern中的字符做替換
- char_fileter_pattern:需要被替換的字符數(shù)組
- char_filter_replacement:替換后的字符數(shù)組,可以不用替換,默認是空格
- char_replace:將pattern中的字符做替換
- char_filter_type:處理類型(當(dāng)前僅支持char_replace)
對已有表增加倒排索引,需要顯式物化,才能對存量數(shù)據(jù)生效。物化的語法如下:
BUILD INDEX index_name ON table_name [PARTITIONS(partition_name1, partition_name2)];
我們翻閱Doris源碼,來探究一下Doris的分詞原理??梢钥吹皆创a里,對分詞器一共分成了三種類型,分別為standard(unicode)、simple(english)和chinese。具體的實現(xiàn)還是直接使用了lucence的分詞邏輯, 因此可以達到和ES相差無幾的效果。
//be/src/olap/rowset/segment_v2/inverted_index_reader.cpp
std::unique_ptr<lucene::analysis::Analyzer> InvertedIndexReader::create_analyzer(
InvertedIndexCtx* inverted_index_ctx) {
std::unique_ptr<lucene::analysis::Analyzer> analyzer;
auto analyser_type = inverted_index_ctx->parser_type;
if (analyser_type == InvertedIndexParserType::PARSER_STANDARD ||
analyser_type == InvertedIndexParserType::PARSER_UNICODE) {
analyzer = std::make_unique<lucene::analysis::standard95::StandardAnalyzer>();
} else if (analyser_type == InvertedIndexParserType::PARSER_ENGLISH) {
analyzer = std::make_unique<lucene::analysis::SimpleAnalyzer<char>>();
} else if (analyser_type == InvertedIndexParserType::PARSER_CHINESE) {
auto chinese_analyzer =
std::make_unique<lucene::analysis::LanguageBasedAnalyzer>(L"chinese", false);
chinese_analyzer->initDict(config::inverted_index_dict_path);
auto mode = inverted_index_ctx->parser_mode;
if (mode == INVERTED_INDEX_PARSER_COARSE_GRANULARITY) {
chinese_analyzer->setMode(lucene::analysis::AnalyzerMode::Default);
} else {
chinese_analyzer->setMode(lucene::analysis::AnalyzerMode::All);
}
analyzer = std::move(chinese_analyzer);
} else {
// default
analyzer = std::make_unique<lucene::analysis::SimpleAnalyzer<char>>();
}
return analyzer;
}
上述代碼中,英文分詞采用的是simple的分詞器,其分詞邏輯非常簡單:
template<typename T>
bool SimpleTokenizer<T>::isTokenChar(const T c) const {
//return _istalnum(c)!=0;
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
}
中文分詞則是從本地的dict中讀取的,本地的路徑通過inverted_index_dict_path來配置,默認是在dict目錄下,我們打開be的dict目錄,可以看到如下內(nèi)容:
可以看到,Doris使用的中文分詞器是大名鼎鼎的jieba分詞。并在初始化階段,將所有的字典都加載進去。
static cppjieba::Jieba& getInstance(const std::string& dictPath = "") {
static cppjieba::Jieba instance(dictPath + "/" + "jieba.dict.utf8",
dictPath + "/" + "hmm_model.utf8",
dictPath + "/" + "user.dict.utf8",
dictPath + "/" + "idf.utf8",
dictPath + "/" + "stop_words.utf8");
return instance;
}
其中:
- jieba.dict.utf8: 最大概率法分詞
- hmm_model.utf8: 隱式馬爾科夫分詞
- idf.utf8: 在KeywordExtractor中,使用的是經(jīng)典的TF-IDF算法,所以需要這么一個詞典提供IDF信息
- stop_words.utf8: 停用詞詞典
Unicode采用standard分詞,其邏輯主要是利用flex詞法解析器去解析stop_word。
lucence standard95下包含的stop_word包含以下內(nèi)容:
static std::unordered_set<std::string_view> stop_words = {
"a", "an", "and", "are", "as", "at", "be", "but", "by",
"for", "if", "in", "into", "is", "it", "no", "not", "of",
"on", "or", "such", "that", "the", "their", "then", "there", "these",
"they", "this", "to", "was", "will", "with"};
由于是按照unicode進行分詞的,所以這種分詞幾乎支持所有的語言類型,包括中文、英文、韓文、日文、emoji表情等,所有類型列舉如下:
public:
static constexpr int32_t WORD_TYPE = 0; //單詞
static constexpr int32_t NUMERIC_TYPE = 1; // 數(shù)字
static constexpr int32_t SOUTH_EAST_ASIAN_TYPE = 2; //東南亞
static constexpr int32_t IDEOGRAPHIC_TYPE = 3; //表意文字
static constexpr int32_t HIRAGANA_TYPE = 4; //日語
static constexpr int32_t KATAKANA_TYPE = 5; // 片假名
static constexpr int32_t HANGUL_TYPE = 6; // 韓文
static constexpr int32_t EMOJI_TYPE = 7; //emoji表情
但是因為其是要實時通過詞法解析的,所以從效率上來講,肯定沒有純english的快。
BloomFilter
BloomFilter索引的概念在clickhouse中也有,所以此處就不深入介紹了。
其語法如下:
PROPERTIES (
"bloom_filter_columns"="saler_id,category_id",
)
我們只需要指定字段即可,而無需指定bloom filter的長度和hash函數(shù)的個數(shù)(和clickhouse的bloom filter的二級索引相同)。
它可以指定多個列,但不可以創(chuàng)建多個索引。其索引粒度是block。這里提一嘴,前綴索引是以Block為粒度創(chuàng)建的稀疏索引,一個Block包含1024行數(shù)據(jù),每個Block,以該Block的第一行數(shù)據(jù)的前綴列的值作為索引。
它的主要使用場景有:
- 非前綴過濾
- 適用于高基數(shù)列
- 查詢條件是in和=(不支持like我也是沒想到)
NGram BloomFilter
前面不是說BloomFilter不支持like么,這不就安排了?
NGram BloomFilter主要就是為了增強like查詢性能的二級索引。
其語法如下:
CREATE TABLE xxx (
...
INDEX idx_ngrambf (`username`) USING NGRAM_BF PROPERTIES("gram_size"="3", "bf_size"="256") COMMENT 'username ngram_bf index'
) ...
gram的個數(shù)跟實際查詢場景相關(guān),通常設(shè)置為大部分查詢字符串的長度,bloom filter字節(jié)數(shù),可以通過測試得出,通常越大過濾效果越好,可以從256開始進行驗證測試看看效果。當(dāng)然字節(jié)數(shù)越大也會帶來索引存儲、內(nèi)存cost上升。
如果數(shù)據(jù)基數(shù)比較高,字節(jié)數(shù)可以不用設(shè)置過大,如果基數(shù)不是很高,可以通過增加字節(jié)數(shù)來提升過濾效果。
需要注意的是, NGram BloomFilter索引和BloomFilter索引為互斥關(guān)系,即同一個列只能設(shè)置兩者中的一個。
Bitmap
位圖結(jié)構(gòu),主要用來加速查詢。這個索引使用的并不多,未來可能會被倒排所替代。
語法如下:
CREATE INDEX [IF NOT EXISTS] index_name ON table1 (siteid) USING BITMAP COMMENT 'balabala';
rollup和物化視圖
rollup
rollup稱之為"上卷",它的概念有點類似clickhouse中的projection。但與projection又有所不同(功能會弱很多)。
rollup的數(shù)據(jù)是會物化存儲到磁盤上的,其生命周期和base表一樣,同生同滅。
rollup語法如下:
ALTER TABLE table1 ADD ROLLUP rollup_city(citycode, pv);
修改后,無需顯式物化,它會自動在后臺進行存量數(shù)據(jù)的物化。
rollup的作用主要有兩個:
- 減少查詢的范圍
- 調(diào)整前綴索引,加速查詢
物化視圖
物化視圖其實是rollup的一種能力補足。因為rollup是不支持基于明細模型做預(yù)聚合的,而物化視圖是在rollup的基礎(chǔ)上增加了預(yù)聚合的能力。
物化視圖的數(shù)據(jù)依賴于底表,但是生命周期需要單獨管理,和底表完全獨立。你可以將物化視圖理解為一種特殊的表。
物化視圖語法示例:
CREATE MATERIALIZED VIEW < MV name > as
SELECT select_expr[, select_expr ...]
FROM [Base view name]
GROUP BY column_name[, column_name ...]
ORDER BY column_name[, column_name ...]
[PROPERTIES ("key" = "value")]
Doris物化視圖比較牛逼的地方在于,它可以在查詢時自動匹配,也就是說,在查詢時,我們依然可以查底表,Doris會根據(jù)查詢語句自動選擇一個最優(yōu)的物化視圖進行查詢,而不需要顯示地指定查詢物化視圖。
join查詢
Doris提供了多種join方式。FE在規(guī)劃分布式查詢計劃時,優(yōu)先選擇的順序為:
Colocate Join -> Bucket Shuffle Join -> Broadcast Join -> Shuffle Join
其中:
- colocate join:
- 提出CG的概念(colocation group), 即將需要進行查詢 的多張表編入一組group中,這些表具有相同的hash字段,相同的分桶類型,分桶樹以及副本數(shù)
- CG的作用是使所有的join操作都是本地join,而不需要分布式查詢。
- CG的創(chuàng)建語法:
CREATE TABLE tbl (k1 int, v1 int sum)
DISTRIBUTED BY HASH(k1)
BUCKETS 8
PROPERTIES(
"colocate_with" = "group1"
);
-
- 由于colocate表的數(shù)據(jù)要保證相同hash key的數(shù)據(jù)處于相同的node上,所以做均衡的時候,數(shù)據(jù)遷移是要同步的, 這難免會帶來遷移的資源開銷,且存在一定的數(shù)據(jù)傾斜風(fēng)險。
- 當(dāng)副本數(shù)據(jù)在進行修復(fù)或者遷移時,colocate表處于不可用狀態(tài),此時colocate join會退化為普通的join,會極大降低查詢性能
- bucket shuffle join:
- 只生效于join條件為等值的場景。依賴hash來計算確定的數(shù)據(jù)分布
- 右表加載到內(nèi)存,將右(?。┍硐炔槌鰜?,然后根據(jù)hash計算出來的數(shù)據(jù)分布,將小表的數(shù)據(jù)發(fā)送到各個節(jié)點進行本地join
- 只能保證左表為單分區(qū)時生效(where條件篩選出來的數(shù)據(jù)處于同一個分區(qū))
- 類似clickhouse的global join
- broadcast join:
- 將右表全量數(shù)據(jù)發(fā)送到各個節(jié)點,與左表在各個節(jié)點上做本地join, 內(nèi)存和網(wǎng)絡(luò)開銷都是N*B
- shuffle join:
- 將左表和右表的數(shù)據(jù)經(jīng)過hash計算分散到各個節(jié)點中,網(wǎng)絡(luò)開銷為 A+ B,內(nèi)存開銷為B。
除此之外,Doris為了加速join查詢,還提供了runtime filter機制。
所謂的runtime filter,因為一般左表join右表,右表需要加載到內(nèi)存,通常會比較小,所以當(dāng)掃描左表和加載右表同時進行時,右表一般會率先完成,此時根據(jù) join on cause動態(tài)生成一些過濾條件,并廣播給正在各個節(jié)點掃描的左表,使得左表掃描的數(shù)據(jù)量減少,從而加速整個查詢,避免不必要的網(wǎng)絡(luò)開銷。(有點類似謂詞下推,但不完全是)
因此,runtime filter主要對左表很大,右表很小的情況下有明顯的優(yōu)化效果。如果左表和右表的規(guī)模相差不大,則加速效果不大。
shuffle方式 | 網(wǎng)絡(luò)開銷 | 內(nèi)存開銷 | 物理算子 | 適用場景 |
---|---|---|---|---|
broadcast join | N* T(R) | N*T(R) | Hash Join/Nest Loop Join | 通用 |
shuffle join | T(S)+ T(R) | T(R) | Hash Join | 通用 |
bucket shuffle join | T(R) | T(R) | Hash Join | 左表為單分區(qū) |
colocate join | 0 | T(R) | Hash Join | 左右表屬于同一個CG |
數(shù)據(jù)寫入
Doris支持很多中數(shù)據(jù)源的數(shù)據(jù)導(dǎo)入,它提供了豐富的內(nèi)置數(shù)據(jù)導(dǎo)入方案。如insert 語法, 利用broker load導(dǎo)入,routine load導(dǎo)入, spark load導(dǎo)入等,同時,我們也可以通過諸如SetTuneal之類的第三方工具進行數(shù)據(jù)導(dǎo)入。
Doris內(nèi)置的數(shù)據(jù)導(dǎo)入方式支持CSV、ORC、JSON等多種格式。接下來我們就簡單介紹一下比較常見的數(shù)據(jù)導(dǎo)入方式。
broker load
在文章一開頭介紹Doris組件時,就介紹過Doris除了FE和BE之外,還有一類可選組件叫broker。broker組件就是專門用來寫入數(shù)據(jù)的。
broker組件需要額外安裝。
它的原理是由FE創(chuàng)建broker計劃,然后BE根據(jù)計劃從具體的broker去拉取數(shù)據(jù)。
broker支持的數(shù)據(jù)源包括HDFS、BOS、AFS等文件系統(tǒng)。
使用broker load導(dǎo)入數(shù)據(jù)的語法如下:
LOAD LABEL broker_load_2022_03_23
(
DATA INFILE("hdfs://192.168.20.123:8020/user/hive/warehouse/ods.db/ods_demo_detail/*/*")
INTO TABLE doris_ods_test_detail
COLUMNS TERMINATED BY ","
(id,store_id,company_id,tower_id,commodity_id,commodity_name,commodity_price,member_price,cost_price,unit,quantity,actual_price)
COLUMNS FROM PATH AS (`day`)
SET
(rq = str_to_date(`day`,'%Y-%m-%d'),id=id,store_id=store_id,company_id=company_id,tower_id=tower_id,commodity_id=commodity_id,commodity_name=commodity_name,commodity_price=commodity_price,member_price=member_price,cost_price=cost_price,unit=unit,quantity=quantity,actual_price=actual_price)
)
WITH BROKER "broker_name_1"
(
"username" = "hdfs",
"password" = ""
)
PROPERTIES
(
"timeout"="1200",
"max_filter_ratio"="0.1"
);
broker load 方式支持ORC、CSV、parquet、gzip等格式的數(shù)據(jù)。
stream load
stream load主要用于導(dǎo)入本地文件或者內(nèi)存中的數(shù)據(jù),它通過HTTP協(xié)議將數(shù)據(jù)寫入到Doris。支持CSV和JSON格式。
stream load方式寫入數(shù)據(jù),會先選定一個BE節(jié)點作為Coordinator(協(xié)調(diào)者)節(jié)點,數(shù)據(jù)會先發(fā)往Coordinator節(jié)點,然后由Coordinator節(jié)點分發(fā)到各個BE節(jié)點,因此會出現(xiàn)寫放大現(xiàn)象。
常用語法如下所示:
curl --location-trusted -u user:passwd [-H ""...] -T data.file -XPUT http://fe_host:http_port/api/{db}/{table}/_stream_load
stream load的任務(wù)無法手動取消,只能等待其成功或出錯退出。
routine load
例行導(dǎo)入,目前僅支持kafka數(shù)據(jù)源。支持CSV和JSON格式。
FE會將一個導(dǎo)入作業(yè)拆分成若干task,每個task負責(zé)導(dǎo)入一部分的數(shù)據(jù),不同的task被分配到不同的BE上去執(zhí)行。
在BE上,每個task會被當(dāng)成普通的stream load任務(wù)去執(zhí)行,導(dǎo)入完成后,向FE匯報。
FE根據(jù)匯報結(jié)果,繼續(xù)生成新的task,或重試失敗的task。
支持無認證的kafka集群,SSL認證的kafka集群,以及kerberos認證的kafka集群。
在作業(yè)運行期間,如果修改表的schema,或者刪除partition,可能會導(dǎo)致任務(wù)失敗或者阻塞。
由于有task失敗重試機制,所以在作業(yè)期間,即使kafka出現(xiàn)短暫失聯(lián),依然不影響數(shù)據(jù)的寫入。
其語法如下所示:
CREATE ROUTINE LOAD example_db.test_json_label_1 ON table1
COLUMNS(category,price,author)
PROPERTIES
(
"desired_concurrent_number"="3",
"max_batch_interval" = "20",
"max_batch_rows" = "300000",
"max_batch_size" = "209715200",
"strict_mode" = "false",
"format" = "json"
)
FROM KAFKA
(
"kafka_broker_list" = "broker1:9092,broker2:9092,broker3:9092",
"kafka_topic" = "my_topic",
"kafka_partitions" = "0,1,2",
"kafka_offsets" = "0,0,0"
);
其他
除了上面三種常見的寫入方式外,Doris還提供了一些其他的寫入方式。如:
- spark load:
- 通過spark任務(wù)實現(xiàn)對數(shù)據(jù)導(dǎo)入的預(yù)處理,如排序,分區(qū),聚合,構(gòu)建索引等
- 由于預(yù)處理被spark任務(wù)提前完成了,因此可以大大節(jié)省Doris的資源消耗(如果spark資源和Doris部署在同一臺機器上,當(dāng)老夫沒說)
- 支持所有spark資源可以訪問的數(shù)據(jù)源,如HDFS、HIVE
- mysql load:
- 說白了還是stream load,都是導(dǎo)入本地數(shù)據(jù)到Doris,不過是以mysql的SQL語法方式(load data infile xxxx into table)
- 僅支持CSV格式。
- s3 load:
- 顧名思義,就是將S3的數(shù)據(jù)導(dǎo)入到Doris
- 語法和broker類似,舉例如下:
LOAD LABEL example_db.exmpale_label_1
(
DATA INFILE("s3://your_bucket_name/your_file.txt")
INTO TABLE load_test
COLUMNS TERMINATED BY ","
)
WITH S3
(
"AWS_ENDPOINT" = "AWS_ENDPOINT",
"AWS_ACCESS_KEY" = "AWS_ACCESS_KEY",
"AWS_SECRET_KEY"="AWS_SECRET_KEY",
"AWS_REGION" = "AWS_REGION"
)
PROPERTIES
(
"timeout" = "3600"
);
- insert into:
- 這個就不用解釋了,使用SQL的方式插入數(shù)據(jù)。
與ClickHouse對比
集群部署運維難度
由于Doris自帶的FE具有元數(shù)據(jù)管理能力,不需要像ClickHouse還要依賴zookeeper或者clickhouse-keeper這類第三方元數(shù)據(jù)管理工具,所以Doris的集群的集群運維管理非常方便。
以增加節(jié)點為例。
Doris增加節(jié)點,只需要一條SQL就能搞定。
它自己內(nèi)部會自動維護配置文件,并同步表的結(jié)構(gòu)。
如果是ClickHouse,需要維護者手動修改集群配置,手動同步元數(shù)據(jù)信息,如果涉及到分片的擴容,還需要考慮數(shù)據(jù)的再均衡。整套流程操作下來繁瑣易出錯,沒有二十年的功力,根本擋不住。
但話又說回來,老夫若祭出ckman神器,閣下又該如何應(yīng)對?
說句大不要臉的話,這一局勉強算個“平手”,不過分吧(陰險笑)?
生態(tài)完備性
如果把ClickHouse比作手動擋超級跑車,那么Doris更像是具備了智能車機平臺的自動擋新能源勢力。
ClickHouse什么都需要自己去做 。集群管理需要自己做,數(shù)據(jù)寫入需要自己做,更別說讓人眼花繚亂的海量調(diào)優(yōu)的參數(shù)了。甚至SQL都搞了許多方言實現(xiàn),需要很多的額外改寫工作。如果不是對ClickHouse有專業(yè)級理解,很難玩得轉(zhuǎn)ClickHouse(不是說用不起來,而是說釋放不出ClickHouse的全部性能)。
而Doris就雞賊很多,完備的數(shù)據(jù)寫入能力,查詢能力,完全兼容MySQL協(xié)議的語法,你甚至用MySQL的client都能連接到Doris上,這對于刷慣了MySQL八股文的國人開發(fā)者來說,這在開發(fā)和使用上,幾乎沒有什么學(xué)習(xí)成本,豈不是有手就行?
數(shù)據(jù)寫入
Doris內(nèi)置了很多數(shù)據(jù)導(dǎo)入的工具,并且由于Doris本身事務(wù)的支持,使得Doris寫入數(shù)據(jù)容錯能力,各種數(shù)據(jù)源的接入能力都比較優(yōu)秀。更離譜的是,stream load、routine load、broker load等導(dǎo)入工具還支持簡單的過濾和解析邏輯,簡直是一條龍服務(wù)。
而ClickHouse并沒有提供專門的導(dǎo)數(shù)方案。雖然也有Kafka、HDFS等外部數(shù)據(jù)引擎可以直接查詢外部存儲的數(shù)據(jù),但由于不再是Mergetree 引擎,無法利用Mergetree的特性對查詢進行加速。我們必須自己實現(xiàn)或者依賴第三方的數(shù)據(jù)寫入工具來進行數(shù)據(jù)的導(dǎo)入,如clickhouse_sinker、Seatuneal等。
但從寫入性能方面來說,Doris單個任務(wù)只能做到50M/s左右,而clickhouse則可以達到200M/s, 速度可謂是碾壓。
當(dāng)然Doris可以通過增加并發(fā)度來提高寫入速度,但代價就是要消耗更多的資源。
數(shù)據(jù)查詢
首先全文檢索方面,Doris支持倒排索引,clickhouse僅支持通過布隆過濾器來進行加速,不消說完敗。(聽到了聽到了,別再鞭尸了)
其次是分布式j(luò)oin查詢能力。ClickHouse僅提供broadcast join,而Doris的bucket shuffle join以及colocate join都是非常牛逼的存在。雖然clickhouse可以通過手動指定hash key來達到colocate join的效果,但畢竟不是原生就支持的能力。
double kill。
clickhouse和Doris都是列存,同樣都支持向量化搜索。所謂向量化,說通俗點就是數(shù)據(jù)一批一批的去執(zhí)行,多批數(shù)據(jù)之間可以并發(fā)執(zhí)行,這種查詢可以大大加速查詢性能。在單表查詢以及聚合查詢場景下,clickhouse的能力還是要比Doris要強不少的。
而且Doris的列存不同于其他的OLAP數(shù)據(jù)庫,特別是在Aggregate模型下,由于其內(nèi)部加入了聚合算子,如果要計算count,性能會特別拉胯。而Clickhouse由于在批量插入時,會同時記錄count到磁盤,基本可以秒出結(jié)果。
用戶權(quán)限管理
Doris的權(quán)限管理比較粗糙,ClickHouse不僅支持完善的行級別查詢限制,以及用戶級別的內(nèi)存限制,查詢內(nèi)存限制,還支持查詢行數(shù)的限制,而這些都是Doris所不具備的。
clickhouse利用這些權(quán)限管理,可以提供更穩(wěn)定的體驗。比如限制查詢用戶的內(nèi)存和線程數(shù),保證導(dǎo)入數(shù)據(jù)的性能不受影響,以及一些大查詢的查詢次數(shù)限制,返回行數(shù)限制等。從而減小查詢帶來的大開銷。
總結(jié)
到底是我太天真了。以為靠一篇文章可以將Doris一把梭。
其實仔細想想怎么可能。clickhouse那樣的體量,研究并使用了這么多年尚且沒搞明白,Doris畢竟是對標(biāo)clickhouse的存在,自然有其該有的深度與咖位。
所以本文權(quán)且當(dāng)做一個引子,僅作為Doris入門讀物。后面我將分專題分享一些Doris的性能測試,以及與clickhouse的對比文章。讓我們看看Doris到底能快到什么地步,而clickhouse這部手動超級跑車,能否通過老司機的神(性)級(能)操(調(diào))作(優(yōu)),讓其發(fā)揮出不亞于Doris的性能。
讓我們拭目以待。文章來源:http://www.zghlxwxcb.cn/news/detail-815749.html
本專欄知識點是通過<零聲教育>的系統(tǒng)學(xué)習(xí),進行梳理總結(jié)寫下文章,對C/C++課程感興趣的讀者,可以點擊鏈接,查看詳細的服務(wù):C/C++Linux服務(wù)器開發(fā)/高級架構(gòu)師文章來源地址http://www.zghlxwxcb.cn/news/detail-815749.html
到了這里,關(guān)于Apache Doris 數(shù)據(jù)庫有哪些應(yīng)用場景?的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!