ClickHouse源码导读:网络IO

2020-08-28 11:58:31 浏览数 (1)

1.前言

ClickHouse是一款开源的列式数据库,主要应用于在线分析查询场景(OLAP)。其显著特点就是:性能强悍。

image.png

无论是通过官方还是非官方的Benchmark数据看,其性能强悍,值得深入分析其设计与实现。通常,分析服务器程序会从网络IO模块入手。

本文将试图深入浅出方式介绍ClickHouse网络IO模块,以期抛砖迎玉。

本文分析代码版本为19.10.16.44,并且只分析在Linux 平台下其实现。

2. ClickHouse 网络模型

本质上讲,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并未使用的网络模型,不在本文讨论范围内。

3. ClickHouse 网络IO设计

ClickHouse-Server支持多种协议,其中包括TCP、HTTP/HTTPS等。其本质上是一个多线程服务器程序。

接下来,我们先看看POCO/NET为实现TCP服务器程序提供了哪些抽象。或者说,如何使用POCO/NET实现多线程TCP服务器程序?

POCO/NET 为编写多线程TCP服务器程序提供了如下接口:

  • ThreadPool: 可自适应调整线程数量的线程池
  • TCPServer: 多线程TCP服务器抽象,以多线程方式处理客户端链接;
  • TCPServerConnection: 处理TCP链接的接口,应用程序通常要继承该类,实现自身业务逻辑;
  • TCPServerParams: TCP服务器程序参数;
  • TCPServerConnectionFactory: TCP链接工厂类。

有了上述接口,我们如何利用POCO/NET实现多线程TCP服务器程序呢? 很简单:

  • 构建线程池(ThreadPool)对象,处理客户端链接
  • 继承TCPServerConnection, 实现处理客户端连接的业务逻辑
  • 继承TCPServerConnectionFactory, 实现构造步骤2中代表客户链接的对象;
  • 构建服务端Socket对象, 并通过系统调用绑定端口和地址;
  • 构造TCPServer对象,将ThreadPool对象、Socket对象、TCPServerConnectionFactory实例、TCPServerParams对象作为参数;
  • 调用TCPServer::start方法,开始接收并处理来自客户端的链接;

看看ClickHouse是如何实现的呢?

请对照代码,dbms/programs/server/Server.cpp Server::main函数中, 我们可以看到如下代码片段。

创建线程池:

代码语言:javascript复制
604  Poco::ThreadPool server_pool(3, config().getUInt("max_connections", 1024));

构建Server Socket, 并绑定地址和端口:

代码语言:javascript复制
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对象:

代码语言:javascript复制
 747  std::make_unique<TCPServer>(new TCPHandlerFactory(*this), server_pool, socket, new Poco::Net::TCPServerParams));

在dbms/programs/server/TCPHandlerFactory.h 文件中,继承TCPServerConnectionFactory类:

代码语言:javascript复制
 13 class TCPHandlerFactory : public Poco::Net::TCPServerConnectionFactory {...}  

在dbms/programs/server/TCPHandler.h文件中,继承TCPServerConnection类,并实现了处理函数:

代码语言:javascript复制
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多线程程序,处理来自客户端的链接:

代码语言:javascript复制
840 for (auto & server : servers) server->start(); 

至此,当客户端链接到来后,ClickHouse 实现的 TCPHandler 类的成员函数会触发,从而处理具体业务逻辑。

代码追踪到这来,我们是知道 ClickHouse 网络IO处理的大概了,能够知道业务逻辑入口了。如果只想分析 ClickHouse 自身逻辑,

完全可由此打住,去分析 ClickHouse 代码。

但是,POCO/NET如何处理网络IO事件,如何处理客户端连接?我们需要一探究竟。

4. POCO/NET代码导读

使用POCO/NET 构建的TCP多线程服务器程序的核心在于TCPServer类。本文以该类为突破口,梳理内部逻辑:

  • TCPServer 有代表线程(Thread)的对象,充当Master线程角色,拥有自己的事件循环,等待客户端连接,并将连接投入队列中。
  • TCPServer 有线程池,消费Master线程存入队列中的客户端链接。

在poco/Net/src/TCPServer.cpp, TCPServer::run 函数中,Master线程拥有简易的事件循环,伪代码如下:

代码语言:javascript复制
128 while (!_stop) {

133  __socket.poll(timeout, Socket::SELECT__READ);

137 auto ss = _socket.acceptConnection();

148 _pDispatcher->enqueue(ss);

172 }

为了不影响阅读,在不影响代码逻辑的前提下,省略了部分代码。

Master 线程收到客户端链接后,投入到Dispatcher的队列中,供线程池消费。其中,TCPServerDispatcher::enqueue 代码如下:

代码语言:javascript复制
141 _queue.enqueueNotification(new TCPConnectionNotification(socket));

146 __threadPool.startWithPriority(__pParams->getThreadPriority(), *this, threadName); 

其中,141行将Socket包装后,投入到TCPServerDispatcher内部队列中。146行,在线程池中寻找线程,执行TCPServerDispatcher::run方法:

代码语言:javascript复制
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函数返回时,该链接将关闭。

5. 结束

ClickHouse是一款优秀的开源OLAP数据库。分析其源码,有助于在生产环境中,更好地使用它。

本文梳理ClickHouse网络IO的设计与实现,通过关键代码片段,剖析其网络IO的内部原理。这有助于加深对ClickHouse原理的理解。

更多ClickHouse技术交流问题,请留言,拉您进入ClickHouse技术交流群。

0 人点赞