最近学习ClickHouse相关的原理知识,基本了解了它的存储结构和设计,但是对在ClickHouse中执行一个SQL语句的过程不是很了解,所以就再次学习了一下。在这里做个总结。
准备出一个系列文章,通过源码阅读的方式,来逐步拆解Clickhouse
的运行过程。今天,我们来看看Clickhouse
是如何启动的。
Poco框架简介
首先,大家需要知道一个名词:Poco
。Github 这是Clickhouse
选择的跨端网络编程框架。这里简单贴官方pdf中的几张图来介绍一下。
这是Poco
的整体架构:
官方做的介绍:
支持的平台:
Server应用相关介绍:阅读原文
入口
大家都知道,在启动Clickhouse
的时候,我们会执行clickhouse-server ...
或clickhouse server ...
或clickhouse --server ...
命令。那么,执行后Clickhouse
做了哪些事情呢?
首先我们需要找到入口。在最新的v18.14版本以后,Clickhouse
的入口文件调整到programs/main.cpp
,该文件为clickhouse入口文件:
我们直接来看看main
函数的代码:
int main(int argc_, char ** argv_)
{
inside_main = true;
SCOPE_EXIT({ inside_main = false; });
/// Reset new handler to default (that throws std::bad_alloc)
/// It is needed because LLVM library clobbers it.
std::set_new_handler(nullptr);
/// PHDR cache is required for query profiler to work reliably
/// It also speed up exception handling, but exceptions from dynamically loaded libraries (dlopen)
/// will work only after additional call of this function.
updatePHDRCache(); // 共享库缓存
std::vector<char *> argv(argv_, argv_ + argc_);
/// Print a basic help if nothing was matched
MainFunc main_func = printHelp; // 默认执行help函数
for (auto & application : clickhouse_applications)
{
if (isClickhouseApp(application.first, argv)) // 这里判断是不是clickhouse-xx
// 或 clickhous e --xx
// 或 clickhouse xx 命令
// 支持的命令看后面的说明
{
main_func = application.second; // 当匹配到指定的命令后退出循环
break;
}
}
return main_func(static_cast<int>(argv.size()), argv.data()); // 执行响应的函数
}
所以,main
函数实际上是从clickhouse_applications
中匹配指令,然后执行了响应的指令函数。
顺便看一下clickhouse
支持的命令:
指令的声明源码如下:
/// Add an item here to register new application
std::pair<const char *, MainFunc> clickhouse_applications[] =
{
#if ENABLE_CLICKHOUSE_LOCAL
{"local", mainEntryClickHouseLocal}, // 启动本地服务
#endif
#if ENABLE_CLICKHOUSE_CLIENT
{"client", mainEntryClickHouseClient}, // 启动clickhouse的内置客户端
#endif
#if ENABLE_CLICKHOUSE_BENCHMARK
{"benchmark", mainEntryClickHouseBenchmark},
#endif
#if ENABLE_CLICKHOUSE_SERVER
{"server", mainEntryClickHouseServer}, // 启动clickhouse服务端
#endif
#if ENABLE_CLICKHOUSE_EXTRACT_FROM_CONFIG
{"extract-from-config", mainEntryClickHouseExtractFromConfig},
#endif
#if ENABLE_CLICKHOUSE_COMPRESSOR
{"compressor", mainEntryClickHouseCompressor},
#endif
#if ENABLE_CLICKHOUSE_FORMAT
{"format", mainEntryClickHouseFormat},
#endif
#if ENABLE_CLICKHOUSE_COPIER
{"copier", mainEntryClickHouseClusterCopier},
#endif
#if ENABLE_CLICKHOUSE_OBFUSCATOR
{"obfuscator", mainEntryClickHouseObfuscator},
#endif
#if ENABLE_CLICKHOUSE_GIT_IMPORT
{"git-import", mainEntryClickHouseGitImport},
#endif
#if ENABLE_CLICKHOUSE_INSTALL // clickhouse部署命令
{"install", mainEntryClickHouseInstall},
{"start", mainEntryClickHouseStart},
{"stop", mainEntryClickHouseStop},
{"status", mainEntryClickHouseStatus},
{"restart", mainEntryClickHouseRestart},
#endif
{"hash-binary", mainEntryClickHouseHashBinary},
};
所以,我们在执行clickhouse-server ...
命令时,实际上运行了mainEntryClickHouseServer
函数。接下来我们看看这个函数做了什么。
启动服务 Server
我们从main.cpp
中找到mainEntryClickHouseServer
,该函数来自于programs/server/Server.cpp
文件:
int mainEntryClickHouseServer(int argc, char ** argv)
{
DB::Server app;
// ...
try
{
return app.run(argc, argv); # 调用Pococ的run函数启动Server服务
}
catch (...)
{
std::cerr << DB::getCurrentExceptionMessage(true) << "\n";
auto code = DB::getCurrentExceptionCode();
return code ? code : 1;
}
}
可以看到,Server
的启动最终是交给了Poco
框架来完成的。那么我们是否到这里就结束了呢?让我们来看一下Poco::Util::ServerApplication:run(int argc, char * * argv)
函数做了什么。这里我们找到Poco
的Poco/Util/src/ServerApplication.h
源码,可以很清楚的看到一段说明:
{
public:
ServerApplication();
/// Creates the ServerApplication.
~ServerApplication();
/// Destroys the ServerApplication.
// 就是这里:run方法执行的时候会调用当前Application类的main函数
int run(int argc, char** argv);
/// Runs the application by performing additional initializations
/// and calling the main() method.
// ...
protected:
int run();
void waitForTerminationRequest();
#if !defined(_WIN32_WCE)
void defineOptions(OptionSet& options);
#endif
我们再打开Poco/Util/src/ServerApplication.cpp
看一下run
函数的代码:
int ServerApplication::run(int argc, char** argv)
{
if (!hasConsole() && isService())
{
return 0;
}
else
{
int rc = EXIT_OK;
try
{
init(argc, argv);
switch (_action)
{
case SRV_REGISTER:
registerService();
rc = EXIT_OK;
break;
case SRV_UNREGISTER:
unregisterService();
rc = EXIT_OK;
break;
default:
rc = run(); // 这里会调用当前类的run()函数
}
}
catch (Exception& exc)
{
logger().log(exc);
rc = EXIT_SOFTWARE;
}
return rc;
}
}
//...
// run函数会直接调用Application的run方法
int ServerApplication::run()
{
return Application::run();
}
那么我们来打开Poco/Util/src/Application.cpp
看一下Application::run()
函数:
int Application::run()
{
int rc = EXIT_CONFIG;
initialize(*this); // 先调用initialize钩子
try
{
rc = EXIT_SOFTWARE;
rc = main(_unprocessedArgs); // 然后这里会调用main函数
}
catch (Poco::Exception& exc)
{
logger().log(exc);
}
//...
uninitialize(); // 最后调用uninitialize钩子
return rc;
}
兜兜转转我们又要回到Clickhouse
的programs/server/Server.cpp
,我们直接看Server::main()
函数吧,这个函数有点长。。。大致做了以下这些事吧。
int Server::main(const std::vector<std::string> & /*args*/)
{
Poco::Logger * log = &logger();
// 1. RegisterXXX 注册一些基础的函数等
// 2. 创建GlobalThreadPool
// 3. 初始化全局上下文以及内容(配置等)
// 4. 创建系统数据库(system、default等)、初始化内置表
// 5. 注册不同协议的服务(http/https/tcp等)
// 6. 启动所有服务(Poco::Net::TCPServer.start())
for (auto & server : *servers)
server.start();
// 最后等待结束
waitForTerminationRequest();
}
这里面创建http服务的关键代码为:
servers->emplace_back(
port_name,
std::make_unique<HTTPServer>(
context(), createHandlerFactory(*this, async_metrics, "HTTPHandler-factory"), server_pool, socket, http_params));
可以看到servers
中添加了HTTPServer
对象,这个HTTPServer
对象是继承了Poco::Net::TCPServer
的,所以实际上就是在servers
向量表中加了一个TCPServer
对象,等待后续的启动。
这里再进入到createHandlerFactory
中看一下,发现会调用Server/HTTPHandlerFactory.cpp
中的addDefaultHandlersFactory
添加默认的一些处理器:
void addDefaultHandlersFactory(HTTPRequestHandlerFactoryMain & factory, IServer & server, AsynchronousMetrics & async_metrics)
{
// 1. 添加默认的处理器:
// a. / -> 静态资源。如.js\.css等
// b. /ping -> 处理ping消息
// c. /replicas_status -> 处理集群状态查询指令
// d. /play -> 内置的查询ui页面
addCommonDefaultHandlersFactory(factory, server);
auto query_handler = std::make_shared<HandlingRuleHTTPHandlerFactory<DynamicQueryHandler>>(server, "query");
query_handler->allowPostAndGetParamsRequest();
factory.addHandler(query_handler);
/// ...
}
题外话:贴一下Clickhouse
内置的web客户端,丑归丑,估计大多数开发者都不知道这个地址:http://192.168.1.xx:8123/play.html
总结
到此,我们知道了整个Clickhouse
的启动过程。最终通过Poco
框架启动了一些个TCPServer
服务来监听客户端连接和消息。后续文章会源码分析Clickhouse
处理客户端的请求过程。后期有时间我也会跟大家唠唠Poco
框架的基础和源码。咱们下期见~