Doris(原来叫POLO)是百度开源的一款高性能实时分析的MPP数据库,个人一直对MPP数据库的底层执行原理不是很了解,通过对Doris源码的学习,来熟悉底层的实现机制。Doris官方出了一个Apache Doris源码阅读与解析的视频课程,个人写这个系列文章,一是加深自己对这块内容的理解,二个人也会结合着自己的理解补充一些其他的内容,如一些原理类的内容,另外对读者而言相对于视频来说读文章会节省一些时间。 第一篇从一个建表语句的执行的流程来学习Doris的主要组件、SQL的整体执行过程以及元数据管理的内容。下面我们从Doris的整体结构开始
Doris架构
首先我们来了解下Doris的整体架构,通过官方文档,其主要有2个组件
- Frontend(FE): 前端部分,类似于Master节点,会有多个,主要负责用户请求、查询解析和生成计划任务、管理元数据和节点管理相关的工作
- Backend(BE): 后端节点,类似于Worker节点,负责数据的存储和查询计划的执行
相关的源代码分别有个fe(java)和be(c++),fe的主类是PaloFe.java,be的主类是doris_main.cpp。 接下来我们看看PaloFe的主要组成
public static void start(String dorisHomeDir, String pidDir, String[] args) {
...
// 1. HttpServer for HTTP Server
// 2. FeServer for Thrift Server
// 3. QeService for MySQL Server
QeService qeService = new QeService(Config.query_port, Config.mysql_service_nio_enabled, ExecuteEnv.getInstance().getScheduler());
FeServer feServer = new FeServer(Config.rpc_port);
feServer.start();
HttpServer httpServer = new HttpServer();
...
}
- HttpServer: 提供各类http接口功能,如集群管理、各类状态信息以及其他的一些操作功能,如上传文件等
- FeServer: thrift协议的各类功能接口,如获取各类元数据信息,状态信息和导入数据等,具体的接口功能见FrontendServiceImpl.java实现
- QeService: 实现了MySQL的协议,这样可以使用MySQL的客户端来连接Doris,同样也可以无缝的支持BI工具的连接。QeService里面实际是通过MysqlServer类来支持MySQL协议的网络连接
建表语句的执行
接下来我们通过一条建表语句的执行流程来学习Doris的各个组件是如何配合来完成此过程的。具体的调用过程如下图
下面重点介绍其中的几个主要的类
- ConnectProcessor: 负责一个mysql连接的请求,接收一个请求,处理请求,然后返回结果。其接收的相关请求方法有
| 方法名 | 说明 |
|---|---|
| handleInitDb | 改变当前session的默认数据库 |
| handleQuit | 退出 |
| handlePing | ping处理,直接返回ok |
| handleQuery | 这里名字叫Query,实际是处理所有SQL语句 |
| handleFieldList | 处理获取一个表的所有字段信息的请求 |
| 这里建表语句是通过handleQuery来处理的,其中主要有2步,第一步将原始的sql字符串解析成StatementBase对象(即一个语法树,学过编译原理的同学应该比较清楚,这个是一个词法和语法解析的过程,这块等后续介绍查询语句执行的时候再详细介绍); 第二步是将解析好的语法树(Doris是支持一次提交多个sql语句的,所以前面第一步可能会产生多个StatementBase对象)分别提交到StmtExecutor去执行 |
- StmtExecutor:负责处理一个sql语句的处理,这里定义了各种不同的handle**Stmt的方法来处理各种不同的stmt,如handleQueryStmt、handleSetStmt、handleDdlStmt。由于本篇重点介绍建表语句的流程,所以这里只介绍handleDdlStmt的处理,这个处理比较简单,直接是交给DdlExecutor的execute()方法来处理。
- DdlExecutor: 负责一个ddl语句的处理,ddl是做数据定义的,所以基本上是调用了Catalog类的相关方法来处理,里面实际是一个超长的if else if 语句,下面贴了部分代码
public static void execute(Catalog catalog, DdlStmt ddlStmt) throws Exception {
if (ddlStmt instanceof CreateClusterStmt) {
CreateClusterStmt stmt = (CreateClusterStmt) ddlStmt;
catalog.createCluster(stmt);
} else if (ddlStmt instanceof CreateTableStmt) {
catalog.createTable((CreateTableStmt) ddlStmt);
} else if (ddlStmt instanceof CreateTableLikeStmt) {
catalog.createTableLike((CreateTableLikeStmt) ddlStmt);
} else if (ddlStmt instanceof CreateTableAsSelectStmt) {
catalog.createTableAsSelect((CreateTableAsSelectStmt) ddlStmt);
} else if (ddlStmt instanceof DropTableStmt) {
catalog.dropTable((DropTableStmt) ddlStmt);
}
...
}
- Catalog: 负责doris的各种元数据操作处理,是一个超级大的类,这里只介绍createTable方法的部分,doris里面支持了多个不同的引擎,除Doris本身的olap外,还支持odbc、mysql、broker、elasticsearch、hive和iceberg。这里重点介绍下olap表的创建,具体分为如下几步
- 创建表信息,如字段、分区、分布、压缩、副本等。并做一些验证处理,如验证字段信息,包括表必须有字段、key字段需在value前面和表必须有key字段
- 根据创建的分区、MaterializedIndex、tablet(分桶)和副本数等信息,按tablet数*副本数闯将对应的CreateReplicaTask任务,然后提交到各个Backend上去执行(这块的具体处理后续再介绍)
- 将该表信息添加到内存的元数据信息中,具体的元数据结构下一节介绍
- 最后把这个信息记录到EditLog里面,用于出错的恢复
元数据管理
本节我们来了解下Doris中元数据的部分,元数据即存储数据库及表的定义信息的内容
元数据结构
首先我们看看元数据的结构,这个就常见的数据库层次结构Catalog->Database->Table->Column
- Catalog: Catalog下有多个Database,里面通过Map来记录相应的数据
private ConcurrentHashMap<Long, Database> idToDb;
private ConcurrentHashMap<String, Database> fullNameToDb;
- DataBase: 数据库,下面有多个Table, 除了记录这个外还有如用户自定义函数,数据库属性等
private Map<Long, Table> idToTable;
private Map<String, Table> nameToTable;
- Table: 表信息,里面记录了字段信息,Table有多种不同的类型,如前面介绍的olap、mysql、hive、odbc还有view等等,不同类型的表分别对应有一个子类的实现,如olap表有个OlapTable子类。
protected List<Column> fullSchema;
protected Map<String, Column> nameToColumn;
- Column: 字段信息,里面记录如字段名、类型等,Doris里面有个特殊的叫AggregateType,聚合类型,记录该字段聚合的方法,如SUM、MIN、MAX等。
元数据持久化
元数据是保存在内存中的,如果正常的服务停机,那需要将元数据进行持久化保存,后续启动服务的时候可以再加载。我们来看看Doris的元数据持久化处理。这里在Catalog类中有提供了loadImage()和saveImage()。具体保存的路径是metadir目录下的image子目录。另外为了保证机器出错导致元数据丢失的问题,这个也是通过EditLog来进行保障的。
总结
本篇我们介绍了Doris的整体架构,并通过一个建表语句的执行了解了sql提交到具体执行的整个过程,最后我们也介绍了一下Doris里面的元数据管理的部分。
参考资料: