ClickHouse源码阅读笔记(一):Server是如何工作的

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);
    ...//省略
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值