前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ClickHouse源码导读:网络IO

ClickHouse源码导读:网络IO

原创
作者头像
fastio
修改2020-08-28 11:58:31
1.1K0
修改2020-08-28 11:58:31
举报

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技术交流群。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.前言
  • 2. ClickHouse 网络模型
  • 3. ClickHouse 网络IO设计
  • 4. POCO/NET代码导读
  • 5. 结束
相关产品与服务
大数据
全栈大数据产品,面向海量数据场景,帮助您 “智理无数,心中有数”!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档