在亲手安装和使用了OceanBase后,各位是不是有种想要深入了解OceaBase源码的欲望呢?接下来我们将简单介绍下OceanBase的源码学习方法。
3.1 源码目录
OceanBase源码目录说明如表3-1所示
oceanbase目录 | 描述 |
|-- doc | Oceanbase的相关文档 |
|-- rpm | Build rpm包需要的文件 |
|-- script | 主要是部署ha需要的脚本 |
|-- src | Source目录 |
|-- tests | 测试用例 |
|-- tools | 外围工具 |
Src目录说明如表3-2所示。
Src目录 | 描述 |
|-- common | 基础库,公用模块 |
|-- sql | sql相关(语法解析,执行计划,物理运算符) |
|-- sstable | v1 &v2 SSTable |
|-- compactsstable | ups分发的紧凑型SSTable |
|-- compactsstablev2 | v3 SSTable |
|-- chunkserver | ChunkServer |
|-- mergeserver | MergeServer |
|-- rootserver | RootServer |
|-- updateserver | UpdateServer |
|-- importserver | 控制旁路导入importserver |
|-- obmysql | OB MySQL Client |
|-- lsync | 同步UPS Commit工具 |
3.2 编码特征
在开始阅读OceanBase代码之前,希望你了解一下OceanBase代码的一些主要特征。
参考依据:《OceanBase c&c++编码规范》
常规的方法请参见“[more] effective c++”之类的读本,只摘录几条和其他项目明显不同的:
-
系统中内存分配和释放分别为ob_malloc()和ob_free(),这个是对系统内存分配函数的简单包装,调用的时候最好带上mod_id,系统可以跟踪是哪个模块分配的内存。
-
尽量不使用new和delete,不得已的时候用placement new。在调用比较频繁的函数中分配内存是不利于性能的。由此带来很多惯用法:
-
我们的大对象一般是被重用的,一般都实现一个reset方法,每次使用之前会被调用确保对象是干净的。
-
有一个叫GET_TSI_MULT的宏,用于分配线程专用的对象,在线程内部被不停的重复使用。
-
有一个叫做PageArena的内存分配器,用于零星分配内存,统一释放的场景。
-
OB中不使用STL容器,所以会有ObArray, ObList, ObVector之类的容器。
-
所有函数中只能有一个return语句,因此你会看到每个函数多数都是此类的代码写成:
int ret = OB_SUCCESS; if (OB_SUCCESS != (ret = do_something1())) { print_do_something1_error_log(); } else if (OB_SUCCESS != (ret = do_something2())) { print_do_something2_error_log(); } else if (OB_SUCCESS != (ret = do_something3())) { print_do_something3_error_log(); } return ret;
-
多数完成一般功能的函数会以OB_SUCCESS返回成功,其他值返回错误代码。
-
一个函数如果要分几个步骤完成,请把每个步骤封装成一个函数,符合以上返回值规范。
-
每个错误处理都需打印尽可能助于分析错误的日志,包括输入参数,状态信息。
3.3 common
common下面的都是一些公用代码,有几种:
-
基础类,替代STL里面的一些容器,通用数据结构(ObArray, ObVector, hash, btree)。
-
内存分配器(ob_malloc.cpp, ob_memory_pool.cpp, page_arena.cpp)。
-
各个模块都会使用的公共数据结构、接口。
-
序列化/反序列化方法,远程接口封装。
-
网络接口封装(ObClientManager, ObBaseClient)。
-
schema封装。
3.4 网络框架封装
网络框架封装分为服务端和客户端两部分。
3.4.1 服务端
我们的每个Server(RS,UPS,CS,MS)都是使用相同的网络io模型进行服务(UPS有少许差别),都是基于libeasy库的封装而成。
如果你想理解libeasy的工作细节请参见立德同学的分析:“OceanBase使用libeasy原理源码分析.doc”
server网络io处理模型如图3-1所示。
处理流程如下:
-
libeasy的io处理线程从connection上读到客户端发送的请求数据。
-
调用预先设置好的handler.decode方法创建OB需要的packet。
-
调用handler.process方法, process有两种:
-
直接利用io线程处理完成,发送response。
-
绝大多数采用方式是将packet放入任务队列,等待任务线程池中的某个线程处理完毕,构造返回包(reponse),挂在request->opacket上,调用easy_request_wakeup唤醒io线程发送响应给客户端。
-
每个server都会有一个Ob*Server的类,继承于ObSingleServer(common/ob_single_server.cpp),后者继承于ObBaseServer;对于有多个任务队列的server(比如ObUpdateServer来说),直接继承自ObBaseServer(common/ob_base_server.cpp),自己创建任务队列。
ObSingleServer适用于一个Server只有一个任务队列的情况(CS,MS,RS);完成一系列的初始化工作(创建io线程,设置handler,监听网络端口,启动任务线程池等)。
ObSingleServer实现了一个handPacket接口,这个接口会在easy的io thread读到请求并转化为packet以后push到任务队列中;ObSingleServer有一个ObPacketQueueThread((common/ob_packet_queue_thread.cpp))的成员,里面封装了一个任务队列和一堆任务线程,任务线程会等待在一个信号量上,一旦queue中有了packet,信号量会被singal,会有某个线程被唤醒。ObSingleServer会同时实现ObPacketQueueThread的一个handler,任务线程拿到这个packet进行处理,就调用这个handler. handle_request ()方法,而这个handle_request最终会调用一个do_request的模板方法,由具体的Ob*Server类进行实现。具体过程请参考“ObPacketQueueThread实现”。
Ob*Server一般会有一个Ob*Service的成员,用于处理具体的消息。以ObChunkServer(chunkserver/ob_chunk_server.cpp)为例,有一个对应的ObChunkService类。ObChunkServer实现的消息处理函数do_request会调用ObChunkService::do_request,这个函数就是具体的消息处理过程了。
easy.handler.process() |→ ObSingleServer:: handPacket |→ ObPacketQueueThread::push |→ wakeup task thread |→ ObSingleServer::hand_request() |→ ObSingleServer::do_request |→ Ob*Server::do_request |→ Ob*Service::do_request
3.4.2 客户端
具体的发送消息的类是由ObClientManager(common/ob_client_manager.cpp)封装的,这个类中有一系列(send_packet,post_packet)的方法,如果是在Server里想要与别的Server进行消息交互,那么直接使用这个ObClientManager即可,每个Ob*Server类中都会有一个ObClientManager成员,初始化过程中会设定好,直接使用即可。
如果要一个独立的客户端,可以使用另外的一个简单的类ObBaseClient封装。
因为ObClientManager发送的都是ObPacket,发送消息之前还需要进行打包输入参数,并解包返回的response packet,使用起来多是一些重复的代码,因此有一个类ObRpcStub用来封装上述过程。
ObRpcStub利用宏和模板实现了如下成员函数:
ObRpcStub::send_x_return_y(server, timeout, request_packet_code, rpc_version, input_params, output_params);
这一个函数系列,其中x,y分别表示输入和输出参数的个数; 0 <= x <= 6; 0 <= y <= 3;
如果你要往指定的server发送一个消息,并得到返回的结果,都可以通过调用这个函数(我们称为远程调来实现用),其中前4个参数是固定的:
-
server: 远程调用的目的Server。
-
timeout: 远程调用等待响应的超时时间。
-
request_packet_code:发送的消息代码。
-
rpc_version:远程调用的协议版本。
-
input_params: 如果你有多个输入参数,请将这些参数依次传入,传入顺序与server解析的顺序相同。
-
out_params: 如果你有多个输出参数,请将输出参数依次传入,传入顺序与server打包顺序相同。
对于input_params和output_params中的每个参数有些限制,要么是基本类型(如int , double, float, bool, const char*);要么是一个普通类,实现了serialize和deserialize方法。
3.5 OBMySQL
讲完了网络框架部分以后,请求已经以包的形式传入到Server端了;以一个具体的查询请求为例,我们可以流转每个Server,讲述Server之间是如何交互的。
假设有如下查询的SQL:
Select t1.c1, t2.c1, sum(t2.qty) From t1, t2 Where t1.c2 = t2.c2 And t1.c3 > 5 And t2.c3 < sin10° Group by t1.c1, t2.c1 Having count(t2.qty) < 3 Order by t2.c1 Limit 2, 5;
通过MySQL客户端发送到MergeServer端,首先被MergeServer中的ObMySQLServer::handlePacketQueue(obmysql/ob_mysql_server.cpp)获取,注意ObMySQLServer不是从ObSingleServer继承的,因为要处理mysql command,有两个任务队列(ObMySQLCommandQueueThread), 但处理过程基本相同。
ObMySQLServer::handlePacketQueue |→ ObMySQLServer::do_com_query |→ ObSql::direct_execute? |→ ObMySQLServer::send_response
ObMySQLServer在执行SQL之前还处理了mysql client login过程,这里面有些特殊的方法,具体说明可参考“OceanBase使用libeasy原理源码分析.doc”,具体细节请参见代码。
3.6 SQL
SQL模块下包含了整个SQL语句前期的处理过程,包括SQL语法解析,物理执行计划生成,执行计划优化。
物理执行计划,是由若干物理运算符(Physical Operator)组成的树形结构。每个物理操作符会完成一个特定的数据代数运算,如投影(Project)、过滤(Filter)、排序(Sort)、聚集(GroupBy)等。
物理执行计划执行的过程就是树形结构的根节点执行迭代数据的过程。根节点会驱动字节子节点进行数据迭代。每层操作符之间利用行接口进行数据传递。
物理运算符的设计与实现细节,请参见竹翁编写的“OceanBase SQL物理运算符详细设计”。
接“3.5 OBMySQL”,SQL的执行过程:
ObSql::direct_execute |→ ObSql::process_special_stmt_hook? //处理一些mysql的命令的(show warnings) |→ ObSql::generate_logical_plan //生成逻辑执行计划 |→ ObSql::generate_physical_plan //生成物理执行计划 |→ ObResultSet:: set_physical_plan? //这里只是把物理执行计划挂上去了,还没执行。
真正的物理计划是在哪儿执行的呢?在“3.5 OBMySQL”中调用的最后一步: ObMySQLServer::send_response。
ObMySQLServer::send_response |→ ObMySQLResultSet::open ObPhyOperator::open() //调用根节点open,一次调用各个字节点open,完成一些初始化工作 |→ ObMySQLServer ::send_row_packets |→ ObMySQLResultSet::next_row //这个是不停迭代的过程,每次迭代一行数据, 一直到OB_ITER_END |→ ObResultSet::get_next_row |→ ObMySQLResultSet::close() //关闭物理执行计划,释放资源,完成善后工作
物理执行计划的生成是一个比较复杂的过程,目前我们并没太多的描述文档,主要还是要靠阅读代码(上面提到的几个函数)。
以上面的SQL为例,最终生成的物理执行计划如图3-2所示。。
物理执行计划涉及到的就是一个个的物理操作符,物理操作符的功能相对比较单一,可以直接阅读相关的类实现,都在源码的“sr\csql”目录下。
3.7 MergeServer
对于查询语句来说,一般执行计划的最底层都是一个TableScan(sql/ob_table_scan.h),MergeServer模块有很大部分都是在实现这个操作符。
TableScan的功能就是完成对指定table,指定范围的数据扫描。为了提高效率,TableScan同时还会完成一些filter、groupby之类的操作。
TableScan的主体是ObTableRpcScan(sql/ob_table_rpc_scan.h),最终完成实际工作的是ObMsSqlScanRequest(mergeserver/ob_ms_sql_scan_request.h)。
TableScan -> ObTableRpcScan -> ObRpcScan -> ObMsSqlScanRequest
ObMsSqlScanRequest进行scan的过程是一个并发执行的过程:
-
将设置要扫描的range根据RootServer的RootTable里面的分片信息,拆分为1个或多个sub range,每个sub range对应一个ChunkServer上的Tablet。
-
为每个sub range构造一个ObMsSqlSubScanRequest(mergeserver/ob_ms_sql_sub_scan_request.h);并发将每个SubRequest发送给对应的个ChunkServer,最终阻塞在线程上等待个ChunkServer返回结果。
-
MergeServer会检查返回的结果,一旦返回的结果满足了操作符的要求(比如Limit拿到了足够的行),返回给上层操作符。
当然实际的过程比上面描述的复杂,每个SubRequest无法一次返回完整的数据就会涉及到流式接口,另外每次请求还有并发量限制,具体细节请参见晓楚编写的“MergeServer请求流程.pptx”。
3.8 ChunkServer
ObMsSqlSubScanRequest是针对一个Tablet的查询。ChunkServer只处理针对单个Tablet的查询请求。ObMsSqlSubScanRequest的实现实际上是发送了一个CS_SQL_SCAN的packet,ChunkServer在收到这个类型的packet以后,会构造一个操作符ObTabletScan(sql/ob_tablet_scan.cpp)执行实际的查询请求。
ObChunkService::do_request |→ ObChunkService:: cs_sql_scan |→ ObChunkService::cs_sql_read |→ ObTabletService::open |→ ObTabletScan::create_plan |→ ObTabletScan::open |→ ObTabletService::fill_scan_data |→ ObTabletScan::get_next_row
ObTabletScan的主要工作是将两部分的数据进行合并的过程,一部分数据是通过操作符ObSSTableScan(sql/ob_sstable_scan.h)从本地SSTable文件中读取的基准数据,另一部分是通过操作符ObUpsScan(sql/ob_ups_scan.h)从UPS远程读取的增量数据,然后通过ObTabletScanFuse(sql/ob_tablet_scan_fuse.h)进行合并。如果scan的表配置为内置join功能,那么还需要在ObTabletScanFuse上架一层ObTabletJoin(sql/ob_tablet_join.h)。
ObTabletScan::create_plan就是根据需要构造上面这些操作符。
ChunkServer除了负责查询以外,还有一大部分工作是进行数据每日合并。每日合并就是将本地SSTable的静态数据与UPS的冻结表数据进行合并的过程。这个合并以Tablet为单位来进行的,其中读取数据合并的工作也是由ObTabletScan来实现的。
每日合并代码请参见: ObChunkMerge(chunkserver/ob_chunk_merge.cpp), ObTabletMergerV1(chunkserver/ob_tablet_merger_v1.cpp), ObTabletMergerV2(chunkserver/ob_tablet_merger_v2.cpp)。
ChunkServer利用一个tablet image文件对Tablet进行管理,参考类实现为ObTabletImage<ob_tablet_image.h>,文件格式请参见文档“chunkserver_tablet索引元数据结构.docx”
其他参考文档“chunkserver概述.ppt”。
3.9 SSTable/compactsstablev2
SSTable模块主要实现了静态数据SSTable文件的读写过程。
“src\sstable”目录下是v1,v2的SSTable实现,“src\compactsstablev2”目录下是v3的SSTable实现。两者实现了相同的接口ObRowkeyPhyOperator(sql/ob_rowkey_phy_operator.h)。
SSTable的代码包含了几部分:
-
SSTableScanner (sstable/ob_sstable_scanner.cpp):实现ObSSTableScan的接口,读SSTable
-
SSTableGetter(sstable/ob_sstable_getter.cpp):实现读单条数据
-
SSTableWriter (sstable/ob_sstable_writer.cpp):生成sstable文件,合并的时候写SSTable文件
-
BlockCache, BlockIndexCache (sstable/ob_blockcache.cpp,sstable/ob_block_index_cache.cpp):分别缓存SSTable block, SSTable index
说明:ob0.4以前的版本都是基于cell接口的,每次迭代一个cell, 而新的ob0.4采用的行接口get_next_row。新的接口是以physical operator的形式出现,针对老版本(v1,v2)SSTable的读操作的行接口都实现在sql目录中。新版本的SSTable读操作实现在compactsstablev2中。
SSTable读写的代码都与SSTable的具体格式相关,阅读代码之前必须熟读相关的格式文档:
-
Sstable_format_and_SstableWriter_interface.doc (v1&v2)
-
OceanBase紧凑型格式SSTable写入详细设计.doc (v3)
-
OceanBase紧凑型格式SSTable读详细设计.doc (v3)
接“3.8 ChunkServer”的流程:
ObTabletScan::create_plan |→ ObSSTableScan::open_scan_context ObTabletScan::open |→ ObTabletScanFuse::open |→ ObSSTableScan::open |→ ObSSTableScan::init_sstable_scanner //这里会根据不同的版本实现不同的SSTable scanner。
3.10 RootServer
rootserver管理root table,提供root table查询功能:
-
管理各个Server的上下线,对UPS进行选主操作
-
维护内部表
-
Tablet的复制与迁移
-
Tablet的合并
-
每日合并过程控制
参考文档:
-
RootServer启动注意事项.docx
-
RS状态转换及Tablet合并与核对.docx
-
OceanBase内部表定义.docx
3.11 UpdateServer
UpdateServer是更新数据服务器,实现了多索引(btree,hash)的MemTable更新与读取、redo log机制、事务管理机制。
参考文档:updateserver概述.ppt
-