TOC
Open TOC
info
https://zhuanlan.zhihu.com/p/459185153
https://zhuanlan.zhihu.com/p/513498617
https://zhuanlan.zhihu.com/p/445201899
https://zhuanlan.zhihu.com/p/508331407
https://www.bilibili.com/video/BV1Ng411976V
https://space.bilibili.com/645669485/article
ref
快速体验 OceanBase
使用源码构建 OceanBase 数据库
规划 OceanBase 集群部署
observer
下载安装 OBD
源码构建 OceanBase 数据库
配置文件 ob-advanced-auto.yaml
部署
修改 obadvanced/bin/observer
的软链接,并重启集群,即可反映修改
obclient
开放 2881 端口
增大事务超时时间,单位微秒
调整参数
cli
vim / less
gdb debug
或者考虑使用 cgdb
gdb-add-index
gdb core
vscode debug
插件
VS Code Remote-SSH: The vscode server failed to start SSH - Stack Overflow
未正确关闭 ssh 连接
- Native Debug
- Tasks Shell Input
用于 FindPID
添加 launch.json
注意此处 sourceFileMap 的编写
.
代表 xxx_static.a 所在的目录
更准确的,应该参考 gdb 中给出的路径
local setup
https://mirror.nju.edu.cn/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso
手动分区,打开网络连接,内存 12G,显存 128MB
共享文件夹
修改 /etc/security/limits.conf 添加
查看
- To set the
aio-max-nr
value, add the following line to the /etc/sysctl.conf
file:
- To activate the new setting, run the following command:
root vscode
structure
root
deps
deps/oblib/src/common
每一行必须有主键,用户可见的无主键表是通过一个隐藏的自增列做 rowkey 的
deps/oblib/src/lib/oblog
模板和宏的奇技淫巧
src
sql
兼容 MySQL 协议
connect
建立连接的过程在 obmp_connect
,它执行用户认证鉴权
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/observer/mysql/obmp_connect.cpp#L186
如果鉴权成功,会创建一个 ObSQLSession 对象唯一表示一个数据库连接
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/sql/session/ob_sql_session_info.h#L175
所有其他命令处理都会访问这个 session 对象
query
所有的 SQL 语句类型,包括 DML、DDL,以及 multi-statement 都是用 query 命令处理的
- src/observer/mysql/obmp_query.cpp:process
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/observer/mysql/obmp_query.cpp#L84
TraceId
,它是一条 SQL 一次处理过程的一个唯一标识
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/observer/mysql/obmp_query.cpp#L103
split_multiple_stmt
把每条语句拆分出来
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/observer/mysql/obmp_query.cpp#L212
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/sql/parser/ob_parser.cpp#L64
- src/observer/mysql/obmp_query.cpp:process_single_stmt
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/observer/mysql/obmp_query.cpp#L384
- src/observer/mysql/obmp_query.cpp:do_process
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/observer/mysql/obmp_query.cpp#L527
调用 stmt_query
,进入 sql 模块
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/observer/mysql/obmp_query.cpp#L624
sql
stmt_query
输入 SQL 语句字符串,输出一个包含物理执行计划和元信息的 ResultSet
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/sql/ob_sql.cpp#L153
ObString (stmt) -> ParseResult (ParseNode)
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/sql/parser/ob_parser.cpp#L210
ParseNode -> ObStmt
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/sql/resolver/ob_resolver.cpp#L148
这个模块是面向对象设计的,每种语句类型有一个 Resolver 和一个 Stmt
ObResolver 负责 dispatch,Resolver 基类为 ObStmtResolver
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/sql/resolver/ob_stmt_resolver.h#L32
ObDMLResolver 继承 ObStmtResolver
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/sql/resolver/dml/ob_dml_resolver.h#L49
ObSelectResolver 继承 ObDMLResolver
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/sql/resolver/dml/ob_select_resolver.h#L47
Stmt 类似,不再赘述
以 select 的 resolve 过程为例
对于非 SELECT 和 DML 之外的语句,如大多数 DDL 语句解析到这里就可以执行了,由 engine/cmd
目录下的 executor 直接执行
DDL 是通过 rootservice (RS) 执行的,所以其 executor 实际是发送 RPC
以 create table 为例
事务控制语句则在本机直接调用事务层
以 begin 为例
对于 SELECT 和 DML 及带数据操作的 DDL,则需要产生执行计划
优化器 sql/optimizer
的接口类是 ObOptimizer,以上一步生成的 ObDMLStmt 为输入,执行基于代价的优化,生成一个逻辑执行计划 ObLogPlan
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/sql/optimizer/ob_optimizer.cpp#L24
以 select 为例
改写 sql/rewrite
是优化器的一部分,执行等价的关系运算改写,产生潜在更好的执行计划候选,这里有一系列改写规则
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/sql/rewrite/ob_transformer_impl.cpp#L49
负责把逻辑执行计划转换为能够高效执行的物理执行计划
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/sql/code_generator/ob_code_generator.cpp#L23
产生的物理执行计划一般会被保存到计划缓存
https://gitee.com/oceanbase/oceanbase/blob/oceanbase_competition/src/sql/plan_cache/ob_plan_cache.cpp#L508
把上面的流程串起来
以 COM_QUERY 为例
以 COM_STMT_EXECUTE 为例
ex
https://github.com/oceanbase/oceanbase/pull/372
TPC-H
How to generate dataset using TPC-H
修改 makefile.suite
构建
生成 1GB 量级数据
去掉末尾的 |
shuffle
load data
建表
设置全局安全路径
导入数据
ObLoadDataSPImpl
ObLoadDataDirectDemo
建表的时候不加 primary key,会出现报错不支持 heap table
https://gitee.com/oceandb-space/oceanbase/blob/9ede93b3a8129dea89cf7bf12c64b0ae58e2c984/src/sql/engine/cmd/ob_load_data_direct_demo.cpp#L937
init
租户 ID,读取 schema,有一个对应的 guard
- ObLoadCSVPaser (ObCSVGeneralParser)
使用 ObArenaAllocator 分配一行记录的内存空间,需要使用租户 ID
ObObj 包含类型信息的值
ObNewRow 一行记录
ObCollationType 参考 https://dev.mysql.com/doc/refman/8.0/en/charset-unicode-sets.html
使用 ObArenaAllocator 分配 FILE_BUFFER_SIZE (2M) 的内存
从 FieldOrVarStruct 得到 column 的索引
- ObLoadExternalSort (ObExternalSort<ObLoadDatumRow, ObLoadDatumRowCompare>)
ObLoadDatumRow 包装 (ObStorageDatum)
rowkey 有两个,可能存在隐藏的 rowkey
overview
利用 ObLoadDataBuffer,从文件中读取一行,并解析为 ObNewRow
ObNewRow: wrap row pointer (an ObObj array) and row size
利用 ObLoadRowCaster,将 ObNewRow 转换为 ObLoadDatumRow,并加入到 ObLoadExternalSort 中
读取并解析完成全部的行后,在外存排序 (ExternalSortRound)
不断从 ObLoadExternalSort 获取 ObLoadDatumRow,并加入到 ObLoadSSTableWriter 中 (ObMacroBlockWriter)
ObDataStoreDesc 中存储了对应的 ObSSTableIndexBuilder,用于建立中间层索引
所有的 ObLoadDatumRow 添加完成后,建立 sstable 即可
cast
从 ObLoadCSVPaser 得到的 ObNewRow,其中 ObObj 的类型均为 ObVarcharType
需要根据 schema,将其转换为对应的类型
然后将 ObObj 转换为 ObStorageDatum
ObDatum 的结构如下
https://gitee.com/oceandb-space/oceanbase/blob/9ede93b3a8129dea89cf7bf12c64b0ae58e2c984/src/share/datum/ob_datum.h#L167
ObStorageDatum 继承了 ObDatum
https://gitee.com/oceandb-space/oceanbase/blob/9ede93b3a8129dea89cf7bf12c64b0ae58e2c984/src/storage/blocksstable/ob_datum_row.h#L288
ObLoadDatumRow 和 blocksstable::ObDatumRow 中包含 ObStorageDatum
sort
ob_parallel_external_sort.h
作为模板类的头文件,似乎打不了断点
init
add item
MemorySortRound add_item https://gitee.com/oceandb-space/oceanbase/blob/9ede93b3a8129dea89cf7bf12c64b0ae58e2c984/src/storage/ob_parallel_external_sort.h#L1478
do sort
此处 curr round 和 next round 均为 ExternalSortRound
https://gitee.com/oceandb-space/oceanbase/blob/9ede93b3a8129dea89cf7bf12c64b0ae58e2c984/src/storage/ob_parallel_external_sort.h#L1768
- ObMemorySortRound finish
- ExternalSortRound finish write
- loop for all fragment (curr round reader iter)
- next round init
- merge curr round with next round
- 将 curr round 的所有 fragments 合并到 next round
- next round finish write
- curr round clean up (便于在下一轮复用)
- swap curr round and next round
- curr round build merger
- assign fragment iter and compare
在默认配置下,实际上 loop for all fragment 这一步,在数据量小于 256 GB 时并不会进行
DAG
https://gitee.com/oceandb-space/oceanbase/blob/b27c6c973099645a7fb13f32fdfb6f12c89a8130/src/storage/ob_parallel_external_sort.h#L1249
get next item
如果全部数据能够存放在 buf mem limit 中,则从 MemorySortRound 获取 next item,否则从 ExternalSortRound 获取 next item
- MemorySortRound get next item
此处的 iterator 为 ObMemoryFragmentIterator
- ExternalSortRound get next item
- open merger
- prefetch all fragment iters
- 若 iters 数量大于 1,才需要 build heap
- 从每个 iter 中获取最小的 item,存放在 heap 中
- 若 iters 数量大于 1,则直接从第一个 fragment iter 中 get next item
- 否则从 merger 中 get next item
- 第一次调用会从 heap 中获取最小的 item,之后会先从 last iter 中获取下一个 item,存放在 heap 中,然后再从 heap 中获取最小的 item
此处的 iterator 为 ObFragmentReaderV2
sstable
append row
写宏块 https://gitee.com/oceandb-space/oceanbase/blob/55eda68d9e8c9e3793a2083ba9de58c779171663/src/storage/blocksstable/ob_macro_block_writer.cpp#L386
其中存在 index builder 的操作
create sstable
ObSSTableIndexBuilder 的结果传递给 ObSSTableMergeRes,再传递给 ObTabletCreateSSTableParam
正式 create sstable https://gitee.com/oceandb-space/oceanbase/blob/55eda68d9e8c9e3793a2083ba9de58c779171663/src/sql/engine/cmd/ob_load_data_direct_demo.cpp#L1092
更新存储 https://gitee.com/oceandb-space/oceanbase/blob/55eda68d9e8c9e3793a2083ba9de58c779171663/src/sql/engine/cmd/ob_load_data_direct_demo.cpp#L1100
macro
SMART_VAR
gcc -E
LIKELY
LOG K
KR(ret) 与 OB_FAIL 配合使用
KP -> pointer
lsm
compaction
https://developer.aliyun.com/article/758369
https://zhuanlan.zhihu.com/p/112574579
可以减小空间放大和读放大
较高层级使用
可以减小写放大
较低层级使用
delete and tombstone
https://developer.aliyun.com/article/765567
ob design
storage
存储分层结构
类图
内存数据格式
磁盘文件格式
openvpn
172.16.0.30
能 ping 通,但是 ssh 连接超时
config.ovpn
内容如下
ssh-copy-id
免密登录云服务器
修改 ~/.ssh/config
然后键入 ssh-copy-id ob
配置,之后只需 ssh ob
即可
如果云服务器设置不允许使用密码登录,上述方案会失效
将 ip address 写入配置文件,之后只需 ssh -i <已下载的与实例关联的私钥文件的路径> tencent
即可
script
写了一个 python 脚本自动化下列任务
- 部署
- 生成数据
- 启动 perf
- 配置并导入数据
- 生成火焰图
区分 python 的三个库
- subprocess
- multiprocessing
- threading
https://stackoverflow.com/questions/13606867/what-is-the-difference-between-multiprocessing-and-subprocess
subprocess 主要用于启动 obclient 与 observer 通信,使用 subprocess.run
为同步操作
multiprocessing 主要用于启动 perf,为异步操作,但是无法优雅的终止
尝试在脚本中 kill perf 进程,会导致无法正确生成 perf.data
于是只能在 load data 结束后手动发送 SIGINT 信号,然后手动生成火焰图
boot autodeploy
修改 /etc/rc.d/rc.local
并添加如下权限
使 centos 云服务器 boot 后自动部署 ob
似乎无效,还是在 ~/.bashrc
里面加 alias 吧
perf
https://www.brendangregg.com/linuxperf.html
hypothesis
csv 文件格式如下
注意每一行的结尾有一个 field_term_str
field_term_str
和 line_term_str
均为单字符
不考虑转义字符,不考虑编码
不存在 NULL 字段
type cast
映射关系如下
row idx | datum idx | sql type | ob type |
---|
0 | 0 | INTEGER | ObInt32Type |
3 | 1 | INTEGER | ObInt32Type |
1 | 2 | INTEGER | ObInt32Type |
2 | 3 | INTEGER | ObInt32Type |
4 | 4 | DECIMAL(15, 2) | ObNumberType |
5 | 5 | DECIMAL(15, 2) | ObNumberType |
6 | 6 | DECIMAL(15, 2) | ObNumberType |
7 | 7 | DECIMAL(15, 2) | ObNumberType |
8 | 8 | CHAR(1) | ObCharType |
9 | 9 | CHAR(1) | ObCharType |
10 | 10 | DATE | ObDateType |
11 | 11 | DATE | ObDateType |
12 | 12 | DATE | ObDateType |
13 | 13 | CHAR(25) | ObCharType |
14 | 14 | CHAR(10) | ObCharType |
15 | 15 | VARCHAR(44) | ObVarcharType |
ObNewRow (all string)
中 obj 的 type 为 ObVarcharType
ObNewRow (all string) -> ObNewRow (schema)
需要下述四种转换函数
https://gitee.com/oceandb-space/oceanbase/blob/5da1176773ad23b26880bb74a7ee33c811e84331/src/share/object/ob_obj_cast.cpp#L4584
https://gitee.com/oceandb-space/oceanbase/blob/5da1176773ad23b26880bb74a7ee33c811e84331/deps/oblib/src/lib/timezone/ob_time_convert.cpp#L788
https://gitee.com/oceandb-space/oceanbase/blob/c360ba5f573f222d691d005ba499ade685d1157d/src/share/object/ob_obj_cast.cpp#L4480
https://gitee.com/oceandb-space/oceanbase/blob/c360ba5f573f222d691d005ba499ade685d1157d/deps/oblib/src/lib/number/ob_number_v2.h#L1629
https://gitee.com/oceandb-space/oceanbase/blob/c360ba5f573f222d691d005ba499ade685d1157d/deps/oblib/src/lib/number/ob_number_v2.h#L1540
https://gitee.com/oceandb-space/oceanbase/blob/c360ba5f573f222d691d005ba499ade685d1157d/deps/oblib/src/lib/number/ob_number_v2.h#L585
https://gitee.com/oceandb-space/oceanbase/blob/c360ba5f573f222d691d005ba499ade685d1157d/deps/oblib/src/lib/number/ob_number_v2.cpp#L763
thread pool
Thread
A wrapper of Linux thread that supports normal thread operations.
https://gitee.com/oceandb-space/oceanbase/blob/b56869e4074d32a6036eb106af15ac5a6fbf3178/deps/oblib/src/lib/thread/thread.h#L24
__th_start
作为 runnable entry 的 wrapper 函数
https://gitee.com/oceandb-space/oceanbase/blob/b56869e4074d32a6036eb106af15ac5a6fbf3178/deps/oblib/src/lib/thread/thread.cpp#L227
Threads (ThreadPool)
线程池
管理 Thread
内部启动 Thread
https://gitee.com/oceandb-space/oceanbase/blob/b56869e4074d32a6036eb106af15ac5a6fbf3178/deps/oblib/src/lib/thread/threads.cpp#L179
其 runnable entry 为 run
其中 thread_idx_
为线程局部变量,run
和 run1
均为虚函数,可被 override
submit
函数似乎被弃用了
IRunWrapper
用于 pre_run
和 end_run
https://gitee.com/oceandb-space/oceanbase/blob/b56869e4074d32a6036eb106af15ac5a6fbf3178/deps/oblib/src/lib/thread/threads.cpp#L144
对应的单测
https://gitee.com/oceandb-space/oceanbase/blob/b56869e4074d32a6036eb106af15ac5a6fbf3178/deps/oblib/unittest/lib/thread/test_threads.cpp#L100
ObSimpleThreadPool
override run1
函数
内部通过 ObLightyQueue
管理 task,ObLightyQueue
存在一个 capacity
提供虚函数 handle
和 handle_drop
处理 task
task 与 thread pool 耦合
相当于 thread_num
个线程处理最多容纳 task_num_limit
个的 task 的 queue 中的 task
对应的单测
https://gitee.com/oceandb-space/oceanbase/blob/d9cc67c39e31e947577a6f963f9f047ed9eab0a3/deps/oblib/unittest/lib/thread/test_simple_thread_pool.cpp#L79
ObDynamicThreadPool
类似 ObSimpleThreadPool
,但内部线程数量动态可变,不超过一个上限
ObDynamicThreadInfo
结构体封装了线程信息
在 ObDynamicThreadTask
中提供虚函数 process
处理 task
task 与 thread pool 解耦合
对应的单测
https://gitee.com/oceandb-space/oceanbase/blob/d9cc67c39e31e947577a6f963f9f047ed9eab0a3/deps/oblib/unittest/lib/thread/test_dynamic_thread_pool.cpp#L104
ObReentrantThread
start and stop run task, can be called repeatedly, if created.
UNITY_BUILD
https://cmake.org/cmake/help/latest/prop_tgt/UNITY_BUILD.html
log
https://www.oceanbase.com/docs/community-observer-cn-10000000000901115
日志格式
这里以一条日志举例说明
对应的日志信息如下
日志级别
从低到高有 6 种,DEBUG、TRACE、INFO、WARN、USER_ERR、ERROR
其中 ERROR 日志比较特殊,会将打日志时所在的堆栈打印出来(需要通过符号表解析)
注意
开启 DEBUG 日志将耗费大量资源,在较新版本中,DEBUG 日志在 release 编译下会自动去掉,即使开启也无法生效
设置日志打印级别
修改日志限流量
- 修改
src/share/parameter/ob_parameter_seed.ipp
alter system set syslog_io_bandwidth_limit=10240;
- 修改配置文件
syslog_io_bandwidth_limit: 10GB
printf
仍然输出到日志中
parallel cast
基本思路
- 一个 thread 读取文件,将 ObNewRow 存放到 queue 中
- 其余 thread 从 queue 中读取 ObNewRow,cast 之后 append 到 sorter 中
内存管理
- ObNewRow 中包含 ObObj,两者都需要分配内存,而 ObFixedQueue 中只能存放指针
- 所以需要将 ObNewRow 存放到 queue 前,需要分配内存并进行深拷贝,避免对 ObLoadCSVParserDemo 中的 ObNewRow 形成数据竞争
- 另一种思路是手写一个线程安全的 queue,其中预先为 ObNewRow 和 ObObj 分配内存
- cast 的时候 caster 可能需要使用 allocator,由于 ObArenaAllocator 不是线程安全的,所以考虑每个线程单独一个 allocator
- 或者预分配内存并循环利用,如 string buf
内存依赖关系
- 为 ObNewRow 和 ObObj 分配的内存,在 squash buffer 后才可以释放,即本轮 buffer 产生的 ObNewRow 已经被 cast 并 append 到 sorter 中
- 实际上 cast 之后就可以释放对应的 ObNewRow,但是难以建立对应关系
- cast 的时候分配的内存,在 append 到 sorter 后即可释放
现象观察
- timeout
- contention 过于严重,包括 queue 和 append 到 sorter 时需要加锁
- rowkey order error
- segmentation fault
- 内存问题,主要可能是 use after free
data dist
处理 dbgen 生成的 50 GB 量级数据
然后使用 python pandas 包
对于精确的数据分布,可以参考 dbgen 源码
summary
赛题描述
- 背景:OceanBase 需要支持数据导入功能。之前的做法是将数据表(比如 csv 格式)中的数据拿出来通过 batch insert 插入 OB 数据库中。这样做需要经历完整的 SQL 引擎层,效率不高。
- 内容:本次复赛要求在开源 OceanBase 数据库代码上实现『旁路导入』功能。所谓旁路导入,就是直接从数据表转换成 OB 存储引擎要求的格式并建立索引。排名依据转换花费的时间。
- 具体来讲,OB 采用 LSM-Tree 的索引结构。因此我们需要做的就是将 csv 文件转换成 SSTable 并且建立对应的 index。
- 流程:赛方给了一个 baseline 的实现,其反映的基本流程如下:
- 首先逐行读取 csv 文件,将每一行数据转换成 OB 内部的数据对象;
- 对转换形成的所有 OB 数据对象进行外存排序(因为数据量极大,且建立 SSTable 要求数据有序);
- 将有序数据写成一个个宏块(OB 组织 SSTable 的单位),然后调用实现好的 IndexBuilder 建立索引。
- 测试用例大概有 3 亿行数据,因此整个过程耗时很久。baseline 版大概需要 70 分钟,我们目前的版本可以做到 25 分钟左右。排行榜前列的队伍可以在 6 ~ 10 分钟内完成。
- 测试机器配置为 8C 16G。
尝试
- 我们首先通过 perf 生成的火焰图定位出从 csv line 到 OB 数据对象这一步转换耗时很久。因为这一步一开始走的是 OB 内部的数据转换流程,考虑了多种数据格式、数据编码方式、安全检查等。我们砍掉了多余的部分,优化到 60 分钟左右。
- 其它的尝试主要是在将原本串行的流程改为并行:
- 实现了并行写宏块,优化到了 30 分钟左右;
- 实现了并行读取 csv 文件,优化到了 25 分钟左右。
- 还根据火焰图砍掉了一些看起来很消耗 CPU 的函数,但几乎没有提升。