ClickHouse是一款开源的列式数据库,主要应用于在线分析查询场景(OLAP)。其显著特点就是:性能强悍。
image.png
无论是通过官方还是非官方的Benchmark数据看,其性能强悍,值得深入分析其设计与实现。通常,分析服务器程序会从网络IO模块入手。
本文将试图深入浅出方式介绍ClickHouse网络IO模块,以期抛砖迎玉。
本文分析代码版本为19.10.16.44,并且只分析在Linux 平台下其实现。
本质上讲,ClickHouse在Linux平台上利用IO多路复用机制,实现了线程池并发处理客户端连接的功能。ClickHouse 网络IO模块基于著名开源C++类库——POCO C++ Libraries 实现。其中,POCO/NET将网络IO的细节封装,抽象出简单易用的接口,供ClickHouse使用。ClickHouse聚焦业务细节,将业务逻辑与网络IO细节剥离。
POCO是一个开源的C++类库,用于开发基于网络的应用程序。这个类库和C++标准库很好集成,并填补了C++标准库的功能空缺。
常见的一些基于IO多路复用机制实现多线程网络服务器程序的网络模型:
* 1Master线程/N Worker线程+ 非阻塞IO:Master线程和Worker线程 均有事件循环,Master 线程接收客户端请求,并将链接的 fd 发送给1个Worker 线程。Worker线程完成该 fd 上的事件等待与处理。使用这种网络模型的典型代表为Memcached.
* N Worker线程+非阻塞IO:N个Worker 线程各自拥有独立的事件循环,能够独立监听服务端口,并处理客户端链接的事件等待与处理。使用这种网络模型的典型代表为Nginx.
通过源码,发现ClickHouse的网络模型与 **1 Master线程/N Worker线程+非阻塞IO**模型类似,但有自己的特点。主要区别是,Worker线程并没有事件循环。
也就是说,Worker线程无法并发处理多链接的请求,只能FIFO的方式处理客户端链接。
需要说明的是POCO/NET 除了提供了多种网络模型的实现。对于ClickHouse并未使用的网络模型,不在本文讨论范围内。
ClickHouse-Server支持多种协议,其中包括TCP、HTTP/HTTPS等。其本质上是一个多线程服务器程序。
接下来,我们先看看POCO/NET为实现TCP服务器程序提供了哪些抽象。或者说,如何使用POCO/NET实现多线程TCP服务器程序?
POCO/NET 为编写多线程TCP服务器程序提供了如下接口:
有了上述接口,我们如何利用POCO/NET实现多线程TCP服务器程序呢? 很简单:
看看ClickHouse是如何实现的呢?
请对照代码,dbms/programs/server/Server.cpp Server::main函数中, 我们可以看到如下代码片段。
创建线程池:
604 Poco::ThreadPool server\_pool(3, config().getUInt("max\_connections", 1024));
构建Server Socket, 并绑定地址和端口:
743 Poco::Net::ServerSocket socket;
744 auto address = socket\_bind\_listen(socket, listen\_host, port);
745 socket.setReceiveTimeout(settings.receive\_timeout);
746 socket.setSendTimeout(settings.send\_timeout);
构建TCPServer对象:
747 std::make\_unique<TCPServer>(new TCPHandlerFactory(\*this), server\_pool, socket, new Poco::Net::TCPServerParams));
在dbms/programs/server/TCPHandlerFactory.h 文件中,继承TCPServerConnectionFactory类:
13 class TCPHandlerFactory : public Poco::Net::TCPServerConnectionFactory {...}
在dbms/programs/server/TCPHandler.h文件中,继承TCPServerConnection类,并实现了处理函数:
101 class TCPHandler : public Poco::Net::TCPServerConnection {...}
在dbms/programs/server/TCPHandler.cpp文件中,实现了处理客户链接的业务逻辑:
TCPHandler::run()->TCPHandler::runImpl(). 具体和ClickHouse 客户端TCP链接,均由 runImpl 函数处理。
最后,在dbms/programs/server/Server.cpp Server::main函数里调用 TCPServer::start 方法,开启TCP多线程程序,处理来自客户端的链接:
840 for (auto & server : servers) server->start();
至此,当客户端链接到来后,ClickHouse 实现的 TCPHandler 类的成员函数会触发,从而处理具体业务逻辑。
代码追踪到这来,我们是知道 ClickHouse 网络IO处理的大概了,能够知道业务逻辑入口了。如果只想分析 ClickHouse 自身逻辑,
完全可由此打住,去分析 ClickHouse 代码。
但是,POCO/NET如何处理网络IO事件,如何处理客户端连接?我们需要一探究竟。
使用POCO/NET 构建的TCP多线程服务器程序的核心在于TCPServer类。本文以该类为突破口,梳理内部逻辑:
在poco/Net/src/TCPServer.cpp, TCPServer::run 函数中,Master线程拥有简易的事件循环,伪代码如下:
128 while (!\_stop) {
133 _\_socket.poll(timeout, Socket::SELECT_\_READ);
137 auto ss = \_socket.acceptConnection();
148 \_pDispatcher->enqueue(ss);
172 }
为了不影响阅读,在不影响代码逻辑的前提下,省略了部分代码。
Master 线程收到客户端链接后,投入到Dispatcher的队列中,供线程池消费。其中,TCPServerDispatcher::enqueue 代码如下:
141 \_queue.enqueueNotification(new TCPConnectionNotification(socket));
146 _\_threadPool.startWithPriority(_\_pParams->getThreadPriority(), \*this, threadName);
其中,141行将Socket包装后,投入到TCPServerDispatcher内部队列中。146行,在线程池中寻找线程,执行TCPServerDispatcher::run方法:
103 for (; ;) {
105 AutoPtr<Notification> pNf = \_queue.waitDequeueNotification(idleTime);
111 std::unique\_ptr<TCPServerConnection> pConnection(\_pConnectionFactory->createConnection(pCNf->socket()));
115 pConnection->start();
125 }
Worker线程等待TCPServerDispatcher 内部队列上。若获取到队列中的客户端链接的Socket后,通过工厂类(应用程序自定义该类)创建TCPServerConnection对象(应用程序需要自定义该类,继承自TCPSercerConnection类即可),并执行其start方法。最终,触发应用程序自定义的TCPServerConnection::run方法。
在ClickHouse中,TCPHandler继承自TCPServerConnection类,并实现了其run函数。当run函数返回时,该链接将关闭。
ClickHouse是一款优秀的开源OLAP数据库。分析其源码,有助于在生产环境中,更好地使用它。
本文梳理ClickHouse网络IO的设计与实现,通过关键代码片段,剖析其网络IO的内部原理。这有助于加深对ClickHouse原理的理解。
更多ClickHouse技术交流问题,请留言,拉您进入ClickHouse技术交流群。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。