continue to run
Target
- learning ClickHouse
Notes
Begin
Developing
https://clickhouse.tech/docs/en/development/developer_instruction/
常用编译命令:
ninja -j 2 clickhouse-server clickhouse-client
server 启动命令:
cd ./dbms/programs/server/
../../../build/dbms/programs/clickhouse server
client 连接命令:
build/dbms/programs/clickhouse client
LLDB debug
本来尝试通过日志来观察整个写入流程,目前发现了更好的方式: lldb debug。 本来想采用 attach 的方式进行 debug,但是无论怎么尝试都会遇到 attach 上去,process 就跪掉的问题,没有动力去搞明白这个问题,因此我就尝试直接使用 lldb 启动 server:
cd ./dbms/programs/server/
lldb ../../../build/dbms/programs/clickhouse
breakpoint set --file src/Access/AccessRightsContext.cpp --line 141
run server
有两件事情需要注意一下:
- 不设置断点的话,会因为一直跑,停不下来,而无法进行调试
- run server 第二个参数是作为 clickhouse 的参数传入
启动好了 server,设置好了断点之后,我们就可以使用 client 接上去,注意很有可能一连接上去就会触发断点,触发了断点之后就很好操作了:
# continue to run
continue
# step over
n
# step in
s
# check variable value
fr v $var_name
Basic Architecture
没有仔细的阅读整个文档,着重浏览了 MergeTreee:
- 和 LSM 不一样,没有 WAL 和 memtable 的结构,只有纯粹的 tree file,所以写入性能不怎么样。
- 同时和 LSM 一样的是,存在 background merge thread,需要去做 tree 的合并,存在写放大。
- 此外由于数据按照 block 存储,查询少量数据也没有优势,存在读放大。
- 但是他有非常重要的特性: 查询大量数据、进行复杂的聚合操作时,性能非常好。
至于是如何这种查询场景性能优化的,ClickHouse 有两个比较重要的优化:vectorized query execution and runtime code generation,有相应的论文 可以阅读。
Basic Procedure
主要进行了主流程的理解,目前知道初始化工作是在 dbms/programs/server/Server.cpp 的 main 方法中启动的,其中所有的 databases 放在了 global_context 中,目前了解到的层级是:Server->global_context->Databases(IDataBase)->Tables(IStorage)。
其中 IDatabase 是是管理 table 的,而 table 是本质上是一个 IStorage,IStorage 管理具体的数据存储,因此明天的主体流程可以把经历集中在 IStorage 上面。
借助 lldb 的调试功能,摸清了写入流程:
┌──────────────────────────────┐
│TCPHandler.cpp:249 │
│executeQuery │
└──────────────────────────────┘
┌──────────────────────────────┐
│executeQuery.cpp:572 │
│executeQueryImpl │
└──────────────────────────────┘
┌──────────────────────────────┐
│executeQuery.cpp:328 │
│res = interpreter->execute(); │
└──────────────────────────────┘
┌───────────────────────────────────────┐
│InterpreterInsertQuery.cpp:204 │
│res.pipeline.addStorageHolder(table); │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│InterpreterInsertQuery.cpp:146 │
│make PushingToViewsBlockOutputStream │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│PushingToViewsBlockOutputStream.cpp:23 │
│make PushingToViewsBlockOutputStream │
└───────────────────────────────────────┘
┌──────────────────────────────┐
│TCPHandler.cpp:660 │
│state.io.onFinish(); │
└──────────────────────────────┘
目前发现写入流程的关键控制在于 PushingToViewsBlockOutputStream.cpp 里面的实现,明天需要再深入研究一下,明天的断点可以设置在:
breakpoint set --file src/DataStreams/PushingToViewsBlockOutputStream.cpp --line 168
ClickHouse 的主体流程是一个 TCP server,定义在 TCPHandler.cpp 里面,处理所有的 query,其中 Insert 也是 query。
今天通过调试把写入流程过了一遍,有几个问题留下来了:
- 可以看到 checksum 和一些元数据的写入点,但是究竟是什么持久化数据和 index 的呢?
- 经常看到的 mark 是什么呢?(dbms/src/Storages/MergeTree/MergeTreeDataPartWriterWide.cpp)
gdb 新命令 get:
# switch to specific thread
thread $number
# print expression value
p $expr
入口的断点可以打在:
breakpoint set --file programs/server/TCPHandler.cpp --line 478
明天可以从这个断点开始看:
breakpoint set --file src/Storages/MergeTree/MergeTreeDataPartWriterWide.cpp --line 90
Processors
structs:
- PipelineExecutor
- ExecutingGraph
- Edge
- Node
- ThreadsQueue
- Manage the threads by thread id.
- Use two arrays instead of ordered map(more efficient).
- TaskQueue
- Record which thread a task belongs to in a two-dimensions vector.
- ExecuteContexts: [thread_id] -> ExecuteContext
- ExecuteContext
- Node: current processor to run.
- task_list: expanded pipeline tasks
- async_tasks
- node: current processing Node(one ExecuteContext is for one thread and it may be used to process multiple nodes).
- ExecuteContext
- ExecutingGraph
procedures:
- Initialize
- Create ExecutingGraph
- Execute
- Initialize Execution
- Init TaskQueue.
- Init ThreadsQueue.
- Init ExecuteCongtext.
- addChildlessProcessToStack(Stack is a stack for processor id).
- prepareProcessor for every processor in the Stack(Childless processor).
- Call prepare on Childless processor.
- Set node ExecStatus and push the node to task queue or async task queue.
- Try to find updated edges belonging to the node and call prepare on the node that the edge points to.
- Do expand pipeline if necessary(ExecStatus==ExpandPipeline).
- Create ExpandPipelineTask and execute it concurrently(?).
- Do prepareProcessor for the current node and some other nodes(?) again.
- (Execute concurrently or single thread) executeStepImpl
- Find any task from local thread task queue or other thread task queue.
- Execute the task(node) by calling
processor.work()and prepare it(why prepare?). - Keeping finding and executiong until all the tasks are finished or executiong is yeilt.
- Initialize Execution
Misc
hashmap benchmark
There are many benchmarks for different hashmap in the directory src/interpreter/examples.
Questions
- global_context 中设定了一个指针用于指向自己,有什么用处?
- 动态绑定的逻辑确实是符合逻辑的,但是在这种模式下,是不是 OOP 使用起来会有比较多的误区?(可以参考 Server 和 Application 的关系)。
CPP language
动态绑定
基类里面调用一个 virtual method,如果 this 指针实际上是一个派生类的话,会动态绑定到派生类的方法上,可以参考下面的例子:
#include <iostream>
class Base {
public:
virtual void run() {
init();
std::cout << "Base::run" << std::endl;
}
virtual void init() { std::cout << "Base::init " << std::endl; }
};
class Derived : public Base {
public:
void run() override {
std::cout << "Derived::run" << std::endl;
Base::run();
}
void init() override { std::cout << "Derived::init" << std::endl; }
};
int main() {
Derived d;
d.run();
return 1;
}
try catch
此外还看到这样的 try catch block,用于 catch 所有的异常:
try {
}catch(...) {
}
继承模版类
一个模版类如果实例化之后,那么就是一个实例类,那么也就可以用来继承。
SCOPE_EXIT
#define SCOPE_EXIT_CONCAT(n, ...) \
const auto scope_exit##n = ext::make_scope_guard([&] { __VA_ARGS__; })
#define SCOPE_EXIT_FWD(n, ...) SCOPE_EXIT_CONCAT(n, __VA_ARGS__)
#define SCOPE_EXIT(...) SCOPE_EXIT_FWD(__LINE__, __VA_ARGS__)
Notes:
\is a marker for multiple lines in macro.scope_exit##nmeans concatnas the suffix toscope_exit.__LINE__is the line number where the macro code in the source code file.__VA_ARGS__are all the parameters of the macro.