0 简介
ClickHouse是一个OLAP(联机分析)系统,适用于大数据分析的场景。最近开始读它的源代码,今天介绍一下ClickHouse的Server是如何启动直到接收到一条客户端指令。我手里这份代码是21.2.7版本的。文章参考了这篇博客。
1 从main函数说起
ClickHouse的入口main函数位于programs/main.cpp
,大致代码如下:
int main(int argc_, char ** argv_)
{
...//省略
std::vector<char *> argv(argv_, argv_ + argc_);
/// Print a basic help if nothing was matched
MainFunc main_func = printHelp;
for (auto & application : clickhouse_applications)
{
if (isClickhouseApp(application.first, argv))
{
main_func = application.second;
break;
}
}
return main_func(static_cast<int>(argv.size()), argv.data());
}
通过解析命令行参数argv,主程序寻找需要执行的application。如果是启动服务端的命令,函数指针main_func将会指向 int mainEntryClickHouseServer(int argc, char ** argv),然后执行这个函数启动服务端。
2 启动Server
mainEntryClickHouseServer定义在programs/server/Server.cpp
之中,它的大概逻辑如下:
int mainEntryClickHouseServer(int argc, char ** argv)
{
DB::Server app;
...//省略
try
{
return app.run(argc, argv);
}
catch (...)
{
std::cerr << DB::getCurrentExceptionMessage(true) << "\n";
auto code = DB::getCurrentExceptionCode();
return code ? code : 1;
}
}
可以发现在函数中它定义了一个Server,然后执行了Server的run方法启动服务器,接下来让我们追踪一下run方法。
在追踪之前,梳理一下Server的继承关系,在programs/server/Server.h
可以看到Server继承了BaseDaemon:
class Server : public BaseDaemon, public IServer
{
...//省略
};
而BaseDaemon继承了ServerAppication,
class BaseDaemon : public Poco::Util::ServerApplication, public Loggers
{
};
ServerApplication又在哪呢?
查询了一下,ClickHouse的网络部分是使用了Poco库,ServerApplication属于Poco库,继承自Poco的Application,Application准备了run()、init()、main()三个接口,run方法会调用init和main。
app.run(argc, argv)调用链如下Server::run()
->ServerApplication::run(int argc, char** argv)
->ServerApplication::run()
->Application::run()
,而Application::run()
的代码逻辑如下:
int Application::run()
{
rc = EXIT_SOFTWARE;
// 核心main方法
rc = main(_unprocessedArgs);
return rc;
}
int Application::main(const ArgVec& args)
{
return EXIT_OK;
}
main函数是多态的。懂设计模式的读者可以发现这里其实就是一个模板模式的使用。不仅是Server,所有的app都会走这样的一条启动链路。最终调用到自己实现的启动逻辑。
3 启动之后…
通过刚刚的分析,在Server启动以后,最终的逻辑会落入Server的main函数中,Server的main是一个长达1000行的函数,它完成注册核心函数、全局配置、启动不同协议的服务器的工作。以启动TCP服务器为例,通过代码注释可以知道,servers是一个服务器配饰器容器的智能指针,通过调用createServer函数将启动的服务器添加进容器里,createServer函数第三个参数这里传入lambda函数。
int Server::main(const std::vector<std::string> & /*args*/)
{
//注册一些核心功能
registerFunctions();
registerAggregateFunctions();
registerTableFunctions();
registerStorages();
registerDictionaries();
registerDisks();
//做一些全局配置 占用很大篇幅...
auto servers = std::make_shared<std::vector<ProtocolServerAdapter>>();
//servers是一个服务器容器,支持不同协议的服务器,如HTTP,TCP等等
{
//注册各种不同的服务器...
/// TCP
port_name = "tcp_port";
//createServer第三个参数是lambda函数,将创建的TcpServer加入servers里
createServer(listen_host, port_name, listen_try, [&](UInt16 port)
{
Poco::Net::ServerSocket socket;
auto address = socketBindListen(socket, listen_host, port);
socket.setReceiveTimeout(settings.receive_timeout);
socket.setSendTimeout(settings.send_timeout);
servers->emplace_back(port_name, std::make_unique<Poco::Net::TCPServer>(
new TCPHandlerFactory(*this, /* secure */ false, /* proxy protocol */ false),
server_pool,
socket,
new Poco::Net::TCPServerParams));
LOG_INFO(log, "Listening for connections with native protocol (tcp): {}", address.toString());
});
...
}
...
for (auto & server : servers)
server->start();
...
Tcp服务器在启动之后,监听套接字通过IO复用的方式监听是否有客户端连接到来,当连接到来,接受连接,分发器_pDispatcher会将这个请求加入到请求队列中(enqueue函数)。而enqueue函数的逻辑是将客户端套接字封装成一个Notification对象添加到通知队列中等待调度。
void TCPServer::run()
{
while (!_stopped)
{
Poco::Timespan timeout(250000);
try
{
//监听套接字通过IO复用的方式等待事件发生
if (_socket.poll(timeout, Socket::SELECT_READ))
{
try
{
// 等待连接
StreamSocket ss = _socket.acceptConnection();
if (!_pConnectionFilter || _pConnectionFilter->accept(ss))
{
// enable nodelay per default: OSX really needs that
#if defined(POCO_OS_FAMILY_UNIX)
if (ss.address().family() != AddressFamily::UNIX_LOCAL)
#endif
{
ss.setNoDelay(true);
}
// 将请求放入队列中 TCPServerDispatcher* _pDispatcher
_pDispatcher->enqueue(ss);
}
}
catch (Poco::Exception& exc)
...
}
}
void TCPServerDispatcher::enqueue(const StreamSocket& socket)
{
FastMutex::ScopedLock lock(_mutex);
// Poco::NotificationQueue _queue;
if (_queue.size() < _pParams->getMaxQueued())
{
if (!_queue.hasIdleThreads() && _currentThreads < _pParams->getMaxThreads())
{
try
{// 没有空闲线程,但是未达到最大线程数,创建新的线程
_threadPool.startWithPriority(_pParams->getThreadPriority(), *this, threadName);
++_currentThreads;
}
catch (Poco::Exception&)
{
++_refusedConnections;
return;
}
}
// 封装成一个Notification对象添加到通知队列中
_queue.enqueueNotification(new TCPConnectionNotification(socket));
}
else
{
++_refusedConnections; // 队列满了直接拒绝
}
}
void NotificationQueue::enqueueNotification(Notification::Ptr pNotification)
{
poco_check_ptr (pNotification);
FastMutex::ScopedLock lock(_mutex);
if (_waitQueue.empty())
{
// 入队
_nfQueue.push_back(pNotification);
}
else
{
WaitInfo* pWI = _waitQueue.front();
_waitQueue.pop_front();
pWI->pNf = pNotification;
pWI->nfAvailable.set();
}
}
分发器_pDispatche一边将连接请求放入通知队列,一边会监听这个通知队列,有连接了则会调用createConnection,实际上是创建TCPHandler来处理具体的业务逻辑。
void TCPServerDispatcher::run()
{
AutoPtr<TCPServerDispatcher> guard(this, true); // ensure object stays alive
int idleTime = (int) _pParams->getThreadIdleTime().totalMilliseconds();
for (;;) // 死循环,一直在监听消息
{
{
ThreadCountWatcher tcw(this);
try
{
// 等待消息
AutoPtr<Notification> pNf = _queue.waitDequeueNotification(idleTime);
if (pNf)
{
TCPConnectionNotification* pCNf = dynamic_cast<TCPConnectionNotification*>(pNf.get());
if (pCNf)
{
// 创建连接 _pConnectionFactory是创建TCP服务的时候传进来的,上面也可以找到TCPHandlerFactory
std::unique_ptr<TCPServerConnection> pConnection(_pConnectionFactory->createConnection(pCNf->socket()));
poco_check_ptr(pConnection.get());
beginConnection();
// 运行的是TCPHandler的start方法,处理具体的请求
pConnection->start();
endConnection();
}
}
}
catch (Poco::Exception &exc) { ErrorHandler::handle(exc); }
catch (std::exception &exc) { ErrorHandler::handle(exc); }
catch (...) { ErrorHandler::handle(); }
}
if (_stopped || (_currentThreads > 1 && _queue.empty())) break;
}
}
// TCPHandlerFactory
Poco::Net::TCPServerConnection * createConnection(const Poco::Net::StreamSocket & socket) override
{
try
{
// 实际创建了一个TCPHandler
return new TCPHandler(server, socket);
}
catch (const Poco::Net::NetException &)
{
...
}
}
那么TcpHandler又是如何处理具体的业务呢,核心是这个runImpl函数,这个函数也有数百行,目前我们关心的就是它调用了executeQuery来执行sql命令。至于executeQuery的逻辑是怎样的,那就是另一个故事了,后面等我看完再说吧哈哈。
void TCPServerConnection::start()
{
try
{
// 很显然这里又使用了模板方法模式,由于当前对象是TCPHander,所以去找他的run方法
run();
}
catch (Exception& exc)
...
}
void TCPHandler::run()
{
try
{
runImpl();
}
catch (Poco::Exception & e)
{
...
}
}
void TCPHandler::runImpl()
{
...//省略
setThreadName("TCPHandler");
ThreadStatus thread_status;
while (true){
/// Processing Query 开始处理sql
state.io = executeQuery(state.query, *query_context, false, state.stage, may_have_embedded_data);
...//省略
}
}