【Clickhouse源码】(系列一)启动过程

2,756 阅读3分钟

最近学习ClickHouse相关的原理知识,基本了解了它的存储结构和设计,但是对在ClickHouse中执行一个SQL语句的过程不是很了解,所以就再次学习了一下。在这里做个总结。

准备出一个系列文章,通过源码阅读的方式,来逐步拆解Clickhouse的运行过程。今天,我们来看看Clickhouse是如何启动的。

Poco框架简介

首先,大家需要知道一个名词:PocoGithub 这是Clickhouse选择的跨端网络编程框架。这里简单贴官方pdf中的几张图来介绍一下。

这是Poco的整体架构:

image.png

官方做的介绍:

image.png

支持的平台:

image.png

Server应用相关介绍:阅读原文

image.png

入口

大家都知道,在启动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支持的命令:

image.png

指令的声明源码如下:

/// 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)函数做了什么。这里我们找到PocoPoco/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;
}

兜兜转转我们又要回到Clickhouseprograms/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

image.png

总结

到此,我们知道了整个Clickhouse的启动过程。最终通过Poco框架启动了一些个TCPServer服务来监听客户端连接和消息。后续文章会源码分析Clickhouse处理客户端的请求过程。后期有时间我也会跟大家唠唠Poco框架的基础和源码。咱们下期见~