目录
概述
ClickHouse 简要整体架构图
服务器实例启动逻辑
源码存放目录
ClickHouse程序启动服务,绑定端口逻辑
// 文件名称: /dbms/programs/server/Server.cpp
int Server::main()
{
// 初始化上下文, 上下文包含查询执行所依赖的所有内容:设置可用函数、数据类型、聚合函数、数据库等等
global_context = std::make_unique<Context>(Context::createGlobal());
global_context->setApplicationType(Context::ApplicationType::SERVER);
// zk初始化
zkutil::ZooKeeperNodeCache main_config_zk_node_cache([&] { return global_context->getZooKeeper(); });
//其他config的初始化
//...
//绑定端口,对外提供服务
auto address = make_socket_address(host, port);
socket.bind(address, /* reuseAddress = */ true);
//根据网络协议建立不同的server类型
//现在支持的server类型有:HTTP,HTTPS,TCP,Interserver,mysql
/// HTTP
create_server("http_port", [&](UInt16 port)
{
Poco::Net::ServerSocket socket;
auto address = socket_bind_listen(socket, listen_host, port);
auto handler_factory = createDefaultHandlerFatory<HTTPHandler>(*this,
"HTTPHandler-factory");
servers.emplace_back(std::make_unique<Poco::Net::HTTPServer>(...);
});
/// HTTPS
create_server("https_port", [&](UInt16 port)
{
Poco::Net::SecureServerSocket socket;
auto address = socket_bind_listen(socket, listen_host, port, /* secure = */ true);
servers.emplace_back(std::make_unique<Poco::Net::HTTPServer>(...);
});
/// TCP
create_server("tcp_port", [&](UInt16 port)
{
Poco::Net::ServerSocket socket;
auto address = socket_bind_listen(socket, listen_host, port);
servers.emplace_back(std::make_unique<Poco::Net::TCPServer>(...);
});
/// TCP with SSL
create_server("tcp_port_secure", [&](UInt16 port)
{
Poco::Net::SecureServerSocket socket;
auto address = socket_bind_listen(socket, listen_host, port, /* secure = */ true);
servers.emplace_back(std::make_unique<Poco::Net::TCPServer>(...);
});
/// Interserver IO HTTP
create_server("interserver_http_port", [&](UInt16 port)
{
Poco::Net::ServerSocket socket;
auto address = socket_bind_listen(socket, listen_host, port);
servers.emplace_back(std::make_unique<Poco::Net::HTTPServer>(...);
});
create_server("interserver_https_port", [&](UInt16 port)
{
Poco::Net::SecureServerSocket socket;
auto address = socket_bind_listen(socket, listen_host, port, /* secure = */ true);
servers.emplace_back(std::make_unique<Poco::Net::HTTPServer>(...);
});
create_server("mysql_port", [&](UInt16 port)
{
Poco::Net::ServerSocket socket;
auto address = socket_bind_listen(socket, listen_host, port, /* secure = */ true);
servers.emplace_back(std::make_unique<Poco::Net::TCPServer>(...);
});
/// Prometheus (if defined and not setup yet with http_port)
create_server("prometheus.port", [&](UInt16 port)
{
Poco::Net::ServerSocket socket;
auto address = socket_bind_listen(socket, listen_host, port);
auto handler_factory = new HTTPRequestHandlerFactoryMain(...);
handler_factory->addHandler<PrometheusHandlerFactory>(async_metrics);
servers.emplace_back(std::make_unique<Poco::Net::HTTPServer>(...);
});
//启动server
for (auto & server : servers)
server->start();
}
归纳总结主要逻辑说明
- 解析应用的配置文件信息;
- 初始化上下文(包含查询执行所依赖的所有内容:设置可用函数、数据类型、聚合函数、数据库等等);
- 初始化Zookeeper(分布式DDL执行、ReplicatedMergeTree表主备节点之间的状态同步);
- 常规配置初始化(flags目录、用户文件目录、dictionaries库目录);
- 绑定服务端的端口,根据网络协议初始化Handler,对客户端提供服务。
语句的执行链路
TCP端口响应用户SQL请求逻辑
// 文件名称: /dbms/programs/server/TCPHandler.cpp
void TCPHandler::runImpl()
{
//设置socket属性
socket().setReceiveTimeout(global_receive_timeout);
socket().setSendTimeout(global_send_timeout);
socket().setNoDelay(true);
//实例化套接字对应的输入和输出流缓冲区
in = std::make_shared<ReadBufferFromPocoSocket>(socket());
out = std::make_shared<WriteBufferFromPocoSocket>(socket());
while (1){
// 接收请求报文
receivePacket();
// 执行Query
state.io = executeQuery(state.query, *query_context, false, state.stage, may_have_embedded_data);
//根据Query种类来处理不同的Query
//处理insert Query
processInsertQuery();
//使用pipeline处理的Query和结果发送客户端
processOrdinaryQueryWithProcessors();
//不使用pipeline处理的Query和结果发送客户端
processOrdinaryQuery();
}
}
执行结果发送给客户端逻辑
// 文件名称: /dbms/programs/server/TCPHandler.cpp
void TCPHandler::processOrdinaryQuery()
{
/// Pull query execution result, if exists, and send it to network.
if (state.io.in)
{
/// This allows the client to prepare output format
if (Block header = state.io.in->getHeader())
sendData(header);
AsynchronousBlockInputStream async_in(state.io.in);
while (true)
{
if (async_in.poll(...))
{
const auto block = async_in.read();
sendData(block);
}
}
async_in.readSuffix();
sendData({});
}
state.io.onFinish();
}
归纳总结主要逻辑说明
客户端发来的请求根据各自网络协议的不同,转发给对应的“XXXHandler”来进行的,server在启动的时候“XXXHandler”会被初始化并绑定在指定端口中。
我们以TCPHandler为例,看看服务端是如何处理客户端发来的请求的,重点关注TCPHandler::runImpl 的函数实现:
- 设置Socket的属性;
- 初始化输入和输出流的缓冲区;
- 接受请求报文,拆包;
- 执行Query(包括整个词法语法分析,Query重写,物理计划生成和生成结果);
- 把Query结果保存到输出流,然后发送到Socket的缓冲区,等待发送回客户端。
SQL执行逻辑
// 文件名称: /dbms/src/Interpreters/executeQuery.cpp
static std::tuple<ASTPtr, BlockIO> executeQueryImpl()
{
// 构造SQL解析器
ParserQuery parser(end, settings.enable_debug_queries);
ASTPtr ast;
// 解析Query并转化为AST(抽象语法树)
ast = parseQuery(parser, begin, end, "", max_query_size);
// 根据AST的类型生成interpreter实例
auto interpreter = InterpreterFactory::get(ast, context, stage);
// SQL资源管理,检查当前节点是否还能处理SQL
std::shared_ptr<const EnabledQuota> quota;
if (!interpreter->ignoreQuota())
{
quota = context.getQuota();
if (quota)
{
quota->used(Quota::QUERIES, 1);
quota->checkExceeded(Quota::ERRORS);
}
}
// interpreter执行AST,结果是BlockIO
res = interpreter->execute();
// 返回结果是抽象语法树和解析后的结果组成的二元组,让上层把数据返回给客户端
return std::make_tuple(ast, res);
}
归纳总结主要逻辑说明
- 通过Parser,把Query解析成AST(抽象语法树)。
- InterpreterFactory根据AST生成对应的Interpreter实例并进行AST优化改写。
- 检测资源使用情况,判断节点是否能够执行这个Query。
- AST是由Interpreter来解析的,执行结果是一个BlockIO,并把BlockIO返回给上层,让上层发送给客户端。
这个函数是执行SQL的核心,那么我们把这个函数拆解一个一个单独的讲解。
Parser(解析器)中抽象语法树(AST)的生成逻辑
ClickHouse选择采用手写一个递归下降的Parser来对SQL进行解析,生成的结果是这个SQL对应的抽象语法树(AST),抽象语法树由表示各个操作的节点(IAST)表示。而本节主要介绍Parser背后的核心逻辑:
ClickHouse的Parser利用lexer将扫描用户下发的SQL字符串,将其分割为一个个的Token, token_iterator 即一个Token流迭代器,然后parser再对Token流进行解析生成AST抽象语法树。
// 文件名称: dbms/src/Parsers/parseQuery.cpp
ASTPtr tryParseQuery()
{
// Token为lexer词法分析后的基本单位,词法分析后生成的是Token流
Tokens tokens(pos, end, max_query_size);
IParser::Pos token_iterator(tokens, max_parser_depth);
ASTPtr res;
// Token流经过语法分析生成AST抽象语法树
bool parse_res = parser.parse(token_iterator, res, expected);
return res;
}
通过上面的代码,我们可以看到核心的代码是Parser中的parse方法,我们看下parse的实现方式
bool ParserQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected)
{
/*
ShowTablesQuery、SelectWithUnionQuery、TablePropertiesQuery、 DescribeTableQuery、
ShowProcesslistQuery、CreateQuery、AlterQuery、RenameQuery、DropQuery、CheckQuery、
OptimizeQuery、KillQueryQuery、WatchQuery、 ShowCreateAccessEntityQuery、
ShowGrantsQuery、ShowQuotasQuery、ShowRowPoliciesQuery
*/
ParserQueryWithOutput query_with_output_p(enable_explain);
// Parse INSERT INTO [db.]table xxxxx
ParserInsertQuery insert_p(end);
// Parse Query USE db
ParserUseQuery use_p;
// Parse SET name1 = value1, name2 = value2
ParserSetQuery set_p;
// Parse System Query
ParserSystemQuery system_p;
// Parse CREATE USER | ALTER USER
ParserCreateUserQuery create_user_p;
// Parse CREATE ROLE | ALTER ROLE
ParserCreateRoleQuery create_role_p;
// Parse CREATE QUOTA | ALTER QUOTA
ParserCreateQuotaQuery create_quota_p;
// Parse CREATE [ROW] POLICY | ALTER [ROW] POLICY
ParserCreateRowPolicyQuery create_row_policy_p;
// Parse CREATE SETTINGS PROFILE | ALTER SETTINGS PROFILE
ParserCreateSettingsProfileQuery create_settings_profile_p;
// Parse DROP USER | DROP ROLE | DROP QUOTA | DROP [SETTINGS] PROFILE | DROP [ROW] POLICY
ParserDropAccessEntityQuery drop_access_entity_p;
// Parse GRANT access_type | REVOKE access_type
ParserGrantQuery grant_p;
// Parse SET ROLE | SET DEFAULT ROLE
ParserSetRoleQuery set_role_p;
bool res = query_with_output_p.parse(pos, node, expected)
|| insert_p.parse(pos, node, expected)
|| use_p.parse(pos, node, expected)
|| set_role_p.parse(pos, node, expected)
|| set_p.parse(pos, node, expected)
|| system_p.parse(pos, node, expected)
|| create_user_p.parse(pos, node, expected)
|| create_role_p.parse(pos, node, expected)
|| create_quota_p.parse(pos, node, expected)
|| create_row_policy_p.parse(pos, node, expected)
|| create_settings_profile_p.parse(pos, node, expected)
|| drop_access_entity_p.parse(pos, node, expected)
|| grant_p.parse(pos, node, expected);
return res;
}
通过上面的代码可知,ClickHouse数据库大致把SQL类型分为下面几种情况
- QueryWithOutput
- InsertQuery
- UseQuery
- SetQuery
- SystemQuery
- CreateUserQuery
- CreateRoleQuery
- CreateQuotaQuery
- CreateRowPolicyQuery
- CreateSettingsProfileQuery
- DropAccessEntityQuery
- GrantQuery
- SetRoleQuery
每一种SQL类型都有自己专属的解析器,如果遇到接收到一个SQL,则调用专门的解析器进行SQL解析。以QueryWithOutput中的SELECT解析器为例
// 文件名称: dbms/src/Parsers/ParserSelectQuery.cpp
bool ParserSelectQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected)
{
ParserKeyword s_select("SELECT");
ParserKeyword s_distinct("DISTINCT");
ParserKeyword s_from("FROM");
ParserKeyword s_prewhere("PREWHERE");
ParserKeyword s_where("WHERE");
ParserKeyword s_group_by("GROUP BY");
ParserKeyword s_with("WITH");
ParserKeyword s_totals("TOTALS");
ParserKeyword s_having("HAVING");
ParserKeyword s_order_by("ORDER BY");
ParserKeyword s_limit("LIMIT");
ParserKeyword s_settings("SETTINGS");
ParserKeyword s_by("BY");
ParserKeyword s_rollup("ROLLUP");
ParserKeyword s_cube("CUBE");
ParserKeyword s_top("TOP");
ParserKeyword s_with_ties("WITH TIES");
ParserKeyword s_offset("OFFSET");
// ........
//依次对Token流爬取上述关键字
xxxx.parse(pos, tables, expected)
//根据语法分析结果设置AST的Expression属性,可以理解为如果SQL存在该关键字,这个关键字都会转化为AST上的一个节点
ast->setExpression(ASTSelectQuery::Expression::WITH, std::move(with_expression_list));
ast->setExpression(ASTSelectQuery::Expression::SELECT, std::move(select_expression_list));
ast->setExpression(ASTSelectQuery::Expression::TABLES, std::move(tables));
ast->setExpression(ASTSelectQuery::Expression::PREWHERE, std::move(prewhere_expression));
ast->setExpression(ASTSelectQuery::Expression::WHERE, std::move(where_expression));
ast->setExpression(ASTSelectQuery::Expression::GROUP_BY, std::move(group_expression_list));
ast->setExpression(ASTSelectQuery::Expression::HAVING, std::move(having_expression));
ast->setExpression(ASTSelectQuery::Expression::ORDER_BY, std::move(order_expression_list));
ast->setExpression(ASTSelectQuery::Expression::LIMIT_BY_OFFSET, std::move(limit_by_offset));
ast->setExpression(ASTSelectQuery::Expression::LIMIT_BY_LENGTH, std::move(limit_by_length));
ast->setExpression(ASTSelectQuery::Expression::LIMIT_BY, std::move(limit_by_expression_list));
ast->setExpression(ASTSelectQuery::Expression::LIMIT_OFFSET, std::move(limit_offset));
ast->setExpression(ASTSelectQuery::Expression::LIMIT_LENGTH, std::move(limit_length));
ast->setExpression(ASTSelectQuery::Expression::SETTINGS, std::move(settings));
}
归纳总结主要逻辑说明
- 通过内置的SQL解析器逐个尝试解析用户下发的SQL,如果可以解析器可以解析则使用当前解析器;
- 解析器中通过TokenIterator进行词法分析,再Token流中爬取这些关键词;
- 如果成功爬取,则 setExpression 函数会组装该关键字对应的AST节点。
Interpreter(解析器)的构建逻辑
根据抽象语法树的类型生成各种的Interpreter。
// 文件名称: /dbms/src/Interpreters/InterpreterFactory.cpp
std::unique_ptr<IInterpreter> InterpreterFactory::get(...)
{
if (query->as<ASTSelectQuery>())
return std::make_unique<InterpreterSelectQuery>(...);
else if (query->as<ASTSelectWithUnionQuery>())
return std::make_unique<InterpreterSelectWithUnionQuery>(...);
else if (query->as<ASTInsertQuery>())
return std::make_unique<InterpreterInsertQuery>(...);
else if (query->as<ASTCreateQuery>())
return std::make_unique<InterpreterCreateQuery>(...);
else if (query->as<ASTDropQuery>())
return std::make_unique<InterpreterDropQuery>(...);
else if (query->as<ASTRenameQuery>())
return std::make_unique<InterpreterRenameQuery>(...);
else if (query->as<ASTShowTablesQuery>())
return std::make_unique<InterpreterShowTablesQuery>(...);
else if (query->as<ASTUseQuery>())
return std::make_unique<InterpreterUseQuery>(...);
else if (query->as<ASTSetQuery>())
return std::make_unique<InterpreterSetQuery>(...);
else if (query->as<ASTSetRoleQuery>())
return std::make_unique<InterpreterSetRoleQuery>(...);
else if (query->as<ASTOptimizeQuery>())
return std::make_unique<InterpreterOptimizeQuery>(...);
else if (query->as<ASTExistsTableQuery>())
return std::make_unique<InterpreterExistsQuery>(...);
else if (query->as<ASTExistsDictionaryQuery>())
return std::make_unique<InterpreterExistsQuery>(...);
else if (query->as<ASTShowCreateTableQuery>())
return std::make_unique<InterpreterShowCreateQuery>(...);
else if (query->as<ASTShowCreateDatabaseQuery>())
return std::make_unique<InterpreterShowCreateQuery>(...);
else if (query->as<ASTShowCreateDictionaryQuery>())
return std::make_unique<InterpreterShowCreateQuery>(...);
else if (query->as<ASTDescribeQuery>())
return std::make_unique<InterpreterDescribeQuery>(...);
else if (query->as<ASTExplainQuery>())
return std::make_unique<InterpreterExplainQuery>(...);
else if (query->as<ASTShowProcesslistQuery>())
return std::make_unique<InterpreterShowProcesslistQuery>(...);
else if (query->as<ASTAlterQuery>())
return std::make_unique<InterpreterAlterQuery>(...);
else if (query->as<ASTCheckQuery>())
return std::make_unique<InterpreterCheckQuery>(...);
else if (query->as<ASTKillQueryQuery>())
return std::make_unique<InterpreterKillQueryQuery>(...);
else if (query->as<ASTSystemQuery>())
return std::make_unique<InterpreterSystemQuery>(...);
else if (query->as<ASTWatchQuery>())
return std::make_unique<InterpreterWatchQuery>(...);
else if (query->as<ASTCreateUserQuery>())
return std::make_unique<InterpreterCreateUserQuery>(...);
else if (query->as<ASTCreateRoleQuery>())
return std::make_unique<InterpreterCreateRoleQuery>(...);
else if (query->as<ASTCreateQuotaQuery>())
return std::make_unique<InterpreterCreateQuotaQuery>(...);
else if (query->as<ASTCreateRowPolicyQuery>())
return std::make_unique<InterpreterCreateRowPolicyQuery>(...);
else if (query->as<ASTCreateSettingsProfileQuery>())
return std::make_unique<InterpreterCreateSettingsProfileQuery>(...);
else if (query->as<ASTDropAccessEntityQuery>())
return std::make_unique<InterpreterDropAccessEntityQuery>(...);
else if (query->as<ASTGrantQuery>())
return std::make_unique<InterpreterGrantQuery>(...);
else if (query->as<ASTShowCreateAccessEntityQuery>())
return std::make_unique<InterpreterShowCreateAccessEntityQuery>(...);
else if (query->as<ASTShowGrantsQuery>())
return std::make_unique<InterpreterShowGrantsQuery>(...);
else if (query->as<ASTShowQuotasQuery>())
return std::make_unique<InterpreterShowQuotasQuery>(...);
else if (query->as<ASTShowRowPoliciesQuery>())
return std::make_unique<InterpreterShowRowPoliciesQuery>(...);
else
throw Exception(...)
}
AST(抽象语法树)进一步优化逻辑
InterpreterSelectQuery::InterpreterSelectQuery(....)
{
// 获取AST
ASTSelectQuery & query = getSelectQuery();
// 对AST做进一步语法分析,对语法树做优化重写
syntax_analyzer_result = SyntaxAnalyzer(...).analyzeSelect(...);
// 每一种Query都会对应一个特有的表达式分析器,用于爬取AST生成执行计划(操作链)
query_analyzer = std::make_unique<SelectQueryExpressionAnalyzer>(...);
}
语法分析直接生成的AST转化成执行计划可能性能上并不是最优的,因此需要SyntaxAnalyzer 对其进行优化重写,在其源码中可以看到其涉及到非常多基规则优化RBO(rule based optimization) 的功能。 SyntaxAnalyzer 会逐个针对这些规则对查询进行检查,确定其是否满足转换规则,一旦满足就会对其进行转换。
SyntaxAnalyzerResultPtr SyntaxAnalyzer::analyzeSelect(...) const
{
// 删除重复列
renameDuplicatedColumns(...);
// 根据配置判断是否进行谓词下推
replaceJoinedTable(...);
// 优化查询中的布尔表达式的函数
LogicalExpressionsOptimizer().perform();
// 公共子表达式的消除
QueryNormalizer(...).visit(...);
// 消除select从句后的冗余列
removeUnneededColumnsFromSelectClause(...);
// 执行标量子查询,并且用常量替代标量子查询结果
executeScalarSubqueries(...);
// 谓词表达式下推到子查询
PredicateExpressionsOptimizer(...).optimize();
// group by语句中删除内射函数调用和常量表达式
optimizeGroupBy(...);
// 删除order by后面的重复项
optimizeOrderBy(...);
// 删除order by limit后面的重复项
optimizeLimitBy(...);
// 删除using 的重复项
optimizeUsing(...);
}
物理执行器执行物理计划
ExpressionAnalyzer类的作用可以理解为解析优化重写后的AST,然后对所要进行的操作组成一条操作链,即物理执行计划,举个例子如下所示:
/** These methods allow you to build a chain of transformations over a block, that receives values in the desired sections of the query.
*
* Example usage:
* ExpressionActionsChain chain;
* analyzer.appendWhere(chain);
* chain.addStep();
* analyzer.appendSelect(chain);
* analyzer.appendOrderBy(chain);
* chain.finalize();
*/
上述代码把where,select,orderby操作都加入到操作链中,接下来就可以从Storage层读取Block,对Block数据应用上述操作链的操作。而执行的核心逻辑,就在对应Interpreter的 executeImpl 方法实现中,这里以select语句的Interpreter来了解下读取Block数据并且对block数据进行相应操作的流程。
void InterpreterSelectQuery::executeImpl(...)
{
// 对应Query的AST
auto & query = getSelectQuery();
// 物理计划,判断表达式是否有where,aggregate,having,order_by,litmit_by等字段
AnalysisResult expressions = ExpressionAnalysisResult(...);
// 从Storage读取数据
executeFetchColumns(...);
// 根据SQL的关键字在BlockStream流水线中执行相应的操作, 如where,aggregate,distinct, order by, order by limit, rollup|cube, having, union都分别由一个函数负责执行
executeWhere(...);
executeAggregation(...);
executeDistinct(...);
executeOrder(...);
executeLimitBy(...);
executeSubqueriesInSetsAndJoins(...);
executeMergeAggregated(...);
executeRollupOrCube(...);
executeHaving(...);
executeMergeSorted(...);
executeUnion(...);
}
分析出了执行计划AnalysisResult(即物理执行计划),接下来就需要从storage层中读取数据来执行对应的操作,核心逻辑在 executeFetchColumns 中: 核心操作就是从storage层读取所要处理列的Block,并组织成BlockStream。
void InterpreterSelectQuery::executeFetchColumns(...)
{
// 实例化Block Stream
auto streams = storage->read(...)
// 读取列对应的Block,并且组织成Block Stream
streams = {std::make_shared<NullBlockInputStream>(storage->getSampleBlockForColumns(...))};
streams.back() = std::make_shared<ExpressionBlockInputStream>(...);
}
读取完Block Stream之后就是对其执行各种execute操作如 executeWhere、executeAggregation、executeDistinct操作。
归纳总结主要逻辑说明
- 对AST进行优化重写 ;
- 解析重写后的AST并生成操作链(执行计划) ;
- 从存储引擎中读取要处理的Block数据 ;
- 对读取的Block数据应用操作链上的操作。
Interpreter处理后的结果会通过TCPHandler发送给客户端。详细代码请见上面的TCPHandler::processOrdinaryQuery函数的处理逻辑。
结论
上面讲述了一条查询语句在ClickHouse数据库中的执行流程和涉及到的模块,下面进行归纳总结一下。
- 服务端接收客户端发来的SQL请求,具体形式是一个网络包,Server的协议层需要拆包把SQL解析出来;
- Server负责初始化上下文与Network Handler,然后 Parser 对Query做词法和语法分析,解析成AST;
- Interpreter的 SyntaxAnalyzer 会应用一些启发式规则对AST进行优化重写;
- Interpreter的 ExpressionAnalyzer 根据上下文信息以及优化重写后的AST生成物理执行计划;
- 物理执行计划分发到本地或者分布式的executor,各自从存储引擎中获取数据,应用执行计划;
- Server把执行后的结果以Block流的形式输出到Socket缓冲区,Client从Socket中读取即可得到结果。
本文还比较粗略,还有一些细节内容会在后续文章中描述,比如说ClickHouse的表引擎的实现方式什么、Piepline处理器和调度器是如何运转的,Codegen和SIMD的实现方式等等。
参考资料
- https://bohutang.me/2020/07/25/clickhouse-and-friends-parser/
- https://blog.csdn.net/yunqiinsight/article/details/107953294