OceanBase新手入门 | 手把手玩转OceanBase

2024年 5月 7日 103.1k 0

手把手带你玩转OceanBase

前言

学校系统能力培养,由于我们学校与ob有合作,所以数据库赛道的选题就是oceanbase举办的一个ob大赛。由于暑期在ob实习过,自己家的比赛还是要捧个场的,所以选的数据库赛道。

赛程介绍

初赛

2021.10.15号开始的初赛,初赛一共五周,代码库是miniob,赛题以及相应的指导书在这里。

第一周

第一周,纯坐牢,题干描述不清晰;测试是根据client的输出和你的程序的输出作比较来测试的,然而并没有说清楚输出规范;测试仅测试哪些场景也不说清楚;测试用例不公开,但也没有像15445那样搞一个在线网站,自己提交自己的代码来测试,导致一天就两次测试机会,而且还不知道哪里错了,麻麻子。第一周我们队因为加了一些基础设施,改了cmakefile.list,由于用了cmake的新特性但测试环境cmake版本太低了,所以为数不多的测试机会因为我们的编译错误又浪费了好几次。

第二周

把简单一点的选做题整完了,但效率依旧很低,感觉自己写的没问题,但是测试就是一直过不了,也不知道哪里错了。

第三周

来哥在github的一个issue里面整了一个在线评测系统,可以看到挂掉的case了,效率蹭的一下就起来了,把剩下的题目全给做完了。

OceanBase新手入门 | 手把手玩转OceanBase-1

OceanBase新手入门 | 手把手玩转OceanBase-2

思路分享

查询元数据校验

建议第一题做这个,但,其实框架代码已经实现好了,你只用把一些地方的输出从No data改为Failure,还有一个地方会导致段错误的代码删了就好了。只能说麻麻子。

不过后续加入新功能之后,元数据校验都需要做出对应的修改。

官网有详细教程:https://oceanbase-partner.github.io/lectures-on-dbms-implementation/miniob-select-meta-implementation.html

drop table

官网有源码级详细教程:https://oceanbase-partner.github.io/lectures-on-dbms-implementation/miniob-drop-table-implementation.html,就不多加赘述了。

实现update功能

update t set age=20 where id>100;

我队友的做法是模仿SelectExenode实现了一个UpdateExenode,作用就是把满足id>100的record全部取出来,然后把这些record的age设置为20。

设置值的方法也很简单,就是操纵内存地址就好了

OceanBase新手入门 | 手把手玩转OceanBase-3

这里可能会对record的结构有疑惑,Record的结构(table_meta.cpp):

Sysfield | value1 | value2 | value3 | … | valuen

OceanBase新手入门 | 手把手玩转OceanBase-4

Sysfield貌似是记录trx的,暂时不用去理会它。一个record就是一行数据,一行可能有多列,每一列数据在record的哪里是通过offset来找的,然后强转指针解引用就好了。

此外,需要注意索引的处理,更新操作相当于删除旧值和插入更新后的新值两步操作。

OceanBase新手入门 | 手把手玩转OceanBase-5

增加date字段

官网源码级解析:https://oceanbase-partner.github.io/lectures-on-dbms-implementation/miniob-date-implementation.html

我是新加了一个class Date,把所有Date相关的操作封装在这个类里面

OceanBase新手入门 | 手把手玩转OceanBase-6

多表查询

select * from t1,t2 where t1.id=t2.id and t1.age > 10;

这题其实非常简单,首先弄清楚, t1.age > 10这个是表内条件,我们的select_executor在执行的时候可以过滤掉不满足这个条件的tuple;t1.id=t2.id是表间条件,这个在select_executor执行的时候没有办法过滤。

OceanBase新手入门 | 手把手玩转OceanBase-7

这里的tuple_sets就是一个vector,存放所有的tuple_set,一个tuple_set目前可以理解为存放的就是一张表查出来的数据。tuple_set存放的就是许多tuple,一个tuple就是一行的数据。Tuple里面又是一些tupleValue,也就是一列的数据。

我们要做的就是处理这个tuple_sets,把这些tuple_set做一个笛卡尔积。

OceanBase新手入门 | 手把手玩转OceanBase-8

然后将笛卡尔积之后得到的大表通过表间条件过滤一遍就好了。

OceanBase新手入门 | 手把手玩转OceanBase-9

然后对于每一个tuple,遍历所有的表间条件,看是否满足就好了。

OceanBase新手入门 | 手把手玩转OceanBase-10

由于我们的条件里面可能出现输出模式没有的字段,所以我们在select_executor里面需要拿到所有的字段,最后处理完后再投影一遍就好了。

聚合运算

所谓聚合就是把一系列tuple变成一个标量值的过程。最初的commit是存在bug的版本,了解一下思路就行

OceanBase新手入门 | 手把手玩转OceanBase-11

OceanBase新手入门 | 手把手玩转OceanBase-12

多表join操作

select * from mtst1 inner join mtst2 on mtst1.id < mtst2.id and mtst1.f < 4.0 inner join mtst3 on mtst3.id > 5;

类似于必做题中多表查询,对于两张表进行join,然后针对表间条件,也就是on语句的条件进行过滤即可。

一次插入多条数据

insert into t1 values(1,1),(2,2),(3,3);

存储上面添加对应的支持,把要插入的多条数据存储起来,然后把这些值全部插进去就行,注意,当有一条插入失败时,前面插入成功的需要回滚。

OceanBase新手入门 | 手把手玩转OceanBase-13

OceanBase新手入门 | 手把手玩转OceanBase-14

OceanBase新手入门 | 手把手玩转OceanBase-15

OceanBase新手入门 | 手把手玩转OceanBase-16

唯一索引

create unique index i_id on t1(id); insert into t1 values(1,1); insert into t1 values(1,2); – failed

首先在索引里面加一个标记位,代表它是否是唯一索引

OceanBase新手入门 | 手把手玩转OceanBase-17

检测分为三部分:

  • 创建唯一索引的时候,需要检查这一列上本来已经就插入的数据是否违反唯一性约束,是的话创建失败
  • 插入一行数据的时候,需要检查是否违反唯一性约束
  • 更新一行数据的时候,需要检查是否违反唯一性约束

OceanBase新手入门 | 手把手玩转OceanBase-18

支持NULL类型

Sysfield | nullfield | value1 | value2 | value3 | … | valuen

核心思想就是在record里面sysfield之后加一个nullfield,用这个nullfield来标记对应列究竟是不是null,因为你无法通过在value字段用一个特殊值标记来做到,比如int类型,你用啥标记?用-1?那万一有一个数据就是-1呢?是吧,这就二义性了。

每个允许存储NULL的列对应一个二进制位,二进制位按照列的顺序逆序排列,二进制位表示的意义如下:

二进制位的值为1时,代表该列的值为NULL。

二进制位的值为0时,代表该列的值不为NULL。

OceanBase新手入门 | 手把手玩转OceanBase-19

图片出处

下面是部分代码实现

 RC Table::make_record(int value_num, const Value *values, char * &record_out) {
   // 检查字段类型是否一致
   if (value_num + TableMeta::sys_field_num() + TableMeta::null_field_num() != table_meta_.field_num()) {
     return RC::SCHEMA_FIELD_MISSING;
   }
 
   int null_flag = 0;
   const int null_field_start_index = TableMeta::sys_field_num();
   const int normal_field_start_index = TableMeta::sys_field_num() + TableMeta::sys_field_num();
 
   // 复制所有字段的值
   int record_size = table_meta_.record_size();
   int count = 0;
   char *record = new char [record_size];
   memset(record, 0, record_size);
 
   // NOTE: 此处已经做好了插入的值的类型与字段的类型是否匹配的校验逻辑
   for (int i = 0; i < value_num; i++) {
     const FieldMeta *field = table_meta_.field(i + normal_field_start_index);
     const Value &value = values[i];
     // 对 null field 进行修改
     if (value.is_null && field->isAllowNull()) {
       null_flag |= (1 << (i % INT32_BITS));
     } else if (field->type() != value.type) {
       LOG_ERROR("Invalid value type. field name=%s, type=%d, but given=%d",
         field->name(), field->type(), value.type);
       return RC::SCHEMA_FIELD_TYPE_MISMATCH;
     }
 
     count++;
     if (count >= INT32_BITS) {
       // 将 null_flag 插入到 record + sys
       *((int *)(record + table_meta_.field(null_field_start_index)->offset()) + i / INT32_BITS) = null_flag;
       null_flag = 0;
       count = 0;
     }
   }
 
   if (null_flag != 0) {
     *((int *)(record + table_meta_.field(null_field_start_index)->offset()) + value_num / INT32_BITS) = null_flag;
   }
 ...

OceanBase新手入门 | 手把手玩转OceanBase-20

简单子查询

select * from t1 where t1.age > (select avg(t2.age) from t2) and t1.age > 20.0;

思路比较简单,但实现起来比较复杂。就是执行子查询,然后把子查询执行得出来的结果替换condition中的value,由于简单子查询里面不会出现相关子查询,所以子查询是可以直接执行的。

比如上面这个sql语句的子查询(select avg(t2.age) from t2),这条子查询语句是可以直接执行得到一个标量值的,比如说是4,那么上述sql语句就变成了select * from t1 where t1.age > 4 and t1.age > 20.0;这就变成了一条普通的sql语句了。直接执行它就好了。

复杂子查询

SELECT * FROM CSQ_1 WHERE COL1 NOT IN (SELECT CSQ_2.COL2 FROM CSQ_2 WHERE CSQ_2.ID IN (SELECT CSQ_3.ID FROM CSQ_3 WHERE CSQ_1.ID = CSQ_3.ID));

区别:子查询和父查询相关,并且可能存在多级相关。也就是说子查询的执行依赖于父查询的一些属性值,子查询因为依赖于父查询的参数,当父查询的参数改变时,子查询需要根据新参数值重新执行,然后根据判断条件判断出这个record是否符合条件。

拿上面这条sql语句举例子,(SELECT CSQ_3.ID FROM CSQ_3 WHERE CSQ_1.ID = CSQ_3.ID))这个子查询中的CSQ_1.ID是和父表CSQ_1相关的,那么我们可以在condition_filter里面的bool DefaultConditionFilter::filter(const Record &rec)函数当中,拿到CSQ_1的rec之后,对它的子查询进行递归的替换:

OceanBase新手入门 | 手把手玩转OceanBase-21

之后CSQ_1.ID就变成了确定值,那么子查询就拥有足够的信息执行了。当然,这个case比较简单,如果对于其他sql语句CSQ_1.ID替换后,比方说依然存在父表CSQ_0.ID未知,那么还需要继续向上,在CSQ_0的filter函数中拿到一个rec,然后进行替换,这是一个递归的过程。

多列索引

要改的地方实在太多了,不好讲,不讲了

排序

就是一个很简单的多关键字排序

OceanBase新手入门 | 手把手玩转OceanBase-22

OceanBase新手入门 | 手把手玩转OceanBase-23

OceanBase新手入门 | 手把手玩转OceanBase-24

还有一些题目我队友做的,我也不是很清楚思路。

基础设施

Makefile

这次比赛虽然提供了cmakefile,但是里面并没有集成bison和flex,也就是我们修改了.l和.y文件之后需要自己手动通过flex和bison进行编译。然后毕佬帮我们写了一个Makefile,我只能说,毕佬太强了orz

 .PHONY: build
 configure:
     rm -rf ./build
     mkdir -p ./build
     cd build && cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..
 configure_debug:
     rm -rf ./build
     mkdir -p ./build
     cd build && cmake .. -DDEBUG=ON
 build:
     @# Note: we can't assure that the judge machine
     @# have the bison and flex libraries, hence, we shall
     @# proceed the compilation progress in-place
     @echo "Compiling the Yacc and Lex files"
     @flex --outfile=./src/observer/sql/parser/lex.yy.c  --header-file=./src/observer/sql/parser/lex.yy.h ./src/observer/sql/parser/lex_sql.l 
     @bison -o ./src/observer/sql/parser/yacc_sql.tab.c --defines=./src/observer/sql/parser/yacc_sql.tab.h ./src/observer/sql/parser/yacc_sql.y
     rm -rf ./miniob
     cd build && make -j16
     ./build/bin/observer -f ./etc/observer.ini
 run_server:
     ./build/bin/observer -f ./etc/observer.ini
 run_client:
     ./build/bin/obclient
 none:
 

readline

这个比赛的测试是通过client进行的,通过输入sql语句,然后比对输出来判断你的程序是否正确。既然你要这样测试,能不能把client做的好一点啊喂,光标不能左右移动,不能上下翻看历史记录,很难受,于是自己改了一下:

OceanBase新手入门 | 手把手玩转OceanBase-25

OceanBase新手入门 | 手把手玩转OceanBase-26

OceanBase新手入门 | 手把手玩转OceanBase-27

OceanBase新手入门 | 手把手玩转OceanBase-28

批量测试

既然是依赖sql来测试,那么每次都自己手敲sql语句着实8太行,咱们肯定得自动化是吧,通过观看源代码,发现支持load命令,那么我们可以这样搞

 CREATE TABLE test1(d date);
 LOAD data INFILE 'test/dateTest/test1.csv' into table test1;
 SELECT * FROM test1 WHERE d < '2035-1-1';
 SELECT * FROM test1 WHERE d <= '2023-3-5';
 SELECT * FROM test1 WHERE d >= '2023-3-5';
 SELECT * FROM test1 WHERE d = '2023-3-5';
 SELECT * FROM test1 WHERE d > '2023-3-5';
 SELECT * FROM test1 WHERE d <> '2023-3-5';
 2018-10-1
 1999-10-1
 2023-3-5
 2049-5-3
 1953-2-7
 2084-7-8

如果表是多列的话,csv文件用 | 隔开就可以了,就像下面一样

 hust|1
 andrew|2

sql脚本直接输入重定向就可以了。

gitignore

很多东西我不想被git跟踪,于是这个就派上用场了,其实我有点搞不懂,既然官方不想让我们上传build,因为太大了,他们拉测试clone要很久,那为什么basic代码框架不加一个gitignore呢?

 build/*
 .idea/*
 .vscode/*
 cmake-build-debug/*
 observer.log.*
 miniob/db/*
 deps/*
 
 # Prerequisites
 *.d
 
 # Compiled Object files
 *.slo
 *.lo
 *.o
 *.obj
 
 # Precompiled Headers
 *.gch
 *.pch
 
 # Compiled Dynamic libraries
 *.so
 *.dylib
 *.dll
 
 # Fortran module files
 *.mod
 *.smod
 
 # Compiled Static libraries
 *.lai
 *.la
 *.a
 *.lib
 
 # Executables
 *.exe
 *.out
 *.app

想法和建议

miniob已经做的挺不错了,让我学习到了许多东西,但还有许多地方可以做的更好!

数据类型

添加一个数据类型需要修改的地方太多,我觉得把相关的数学运算,比较操作,序列化,反序列化等等一系列操作都放在一个类里面比较好,这样增加一个新类型只需要填充这个类里面的函数就好了。而且这样处理还有一个好处,数据库当中很多类型其实是有继承关系的,举个例子:Type -> NumericType-> IntegerParentType-> BigintType / IntegerType / SmallintType。这样可以更好的复用我们的代码。

项目抽象

一般来说,数据库里面会有很多的exeuctor,比如insert,delete,aggregation,limit等等,所以应该把exector抽象成一个类然后放到单独一个文件比较好,而不是作为一个create_selection_executor函数放在exeuctor_stage里面,以一个执行阶段作为抽象那这个抽象着实是太大了。对于拿数据,一般数据库系统会采用火山模型或者向量模型,然后调用对应的exeuctor的next方法拿到对应的数据即可,但miniob是我们自己创建完exeuctor之后,调用execte拿到所有数据,之后ConditionFilter的创建,初始化,以及过滤操作全部得自己处理,抽象得不太好。

机制与策略分离

比如plan可以和executor分离,plan可以只包含计划的类型,是insert还是update;plan的输出schema,我需要哪些列?还有这个计划有哪些子节点(树状执行计划)。而executor则包含这个plan具体应该怎么去执行,executor的核心就是next函数了。再比如disk_buffer_pool,当bpm满了时的替换策略是高度耦合在代码当中的,可以考虑增加一个replacer模块完成这件事情,当需要驱逐一个页面时,我们的bpm只需要负责调用replacer的victim方法即可,采用的什么策略我们不care

文件目录

cpp文件和h头文件分开存放比较好,可以使用一个单独的include目录用于存放头文件。storage存储模块下面文件夹分类过于笼统:比如common和default,common点开可以看见索引,table,还有db等等文件全在里面,这里可以再细分一下。disk_buffer_pool 在default文件夹,这个是缓存相关的,应该放到buffer里面更为合适。另外bpm感觉不属于storage模块,应该处于disk manager的上一层才对。

测试框架

本次比赛的测试程序是通过在client输入一些相关的sql语句,然后看执行拿到的结果是否符合要求,这样测试当然可以,但是问题就在于,粒度太大了。对于选手而言,我们只能知道对于这一个试题我们的实现是对还是错,没有告诉我们更多的信息,比如具体是哪一个文件或者哪一个函数实现有问题,这就导致调试难度很大,我们只能凭感觉打断点,然后运行对应的出错的sql语句,单步调试,一步步的查看状态。而且使用sql语句测试的方式需要等到一个需求开发完才可以测试,因此导致把多个函数的bug放在一起调试,徒增开发复杂性。可以考虑给定一些public api,然后在此基础之后实现一个单元测试框架。

文档和注释

可以考虑写一些代码导读的文档,对每个模块都讲解一下,以及一些比较重要的函数增加一些注释,表明这个函数的功能等,不然直接阅读源代码还是比较的困难。

复赛

复赛直接优化oceanbase,主要是优化Nested Loop Join场景,但是算子层其实已经实现的很好了,BLOCK NESTED LOOP JOIN也实现了已经,所以这次优化主要就是搞内存调优,然后就开启了三周纯坐牢模式,500w行量级的项目,没文档,没注释真的难顶啊QwQ

环境配置

大赛举办方给我们提供了两台云服务器,配置如下所示:

服务端配置: CPU 8 core, Memory 16G 客户端配置: CPU 4 core, Memory 8G 操作系统: CentOS 7.9

服务端用来跑observer,客户端则是用来跑obclient或者mysql的。亲试在这个环境下面编译运行是没有问题的。

编译OceanBase

  • fork一份oceanbase的仓库到你的github上面,然后git clone到你的本地,github与gitee均可,但是由于比赛机器代理的问题,我用的gitee,并且用的https协议而不是ssh。

比赛的分支是oceanbase_competition,所以我们指定clone这个分支

OceanBase新手入门 | 手把手玩转OceanBase-29

  • 然后参考这一份教程编译oceanbase数据库。
  • 安装必要的工具
 yum install wget rpm* cpio make glibc-devel glibc-headers

OceanBase新手入门 | 手把手玩转OceanBase-1

  • 确认您的编译机可以访问OceanBase官方yum源
 curl http://mirrors.aliyun.com/oceanbase/OceanBase.repo

OceanBase新手入门 | 手把手玩转OceanBase-31

  • 执行初始化脚本,下载构建依赖
sh build.sh init

OceanBase新手入门 | 手把手玩转OceanBase-32

  • 构建/打包 OceanBase 数据库

由于大赛要性能调优,所以这里采用release构建

# 在源码目录下执行 release 版的预制构建命令
sh build.sh release

# 进入生成的 release 构建目录
cd build_release

# 进行构建
make -j{N} observer

# 查看构建产物
stat src/observer/observer

OceanBase新手入门 | 手把手玩转OceanBase-33OceanBase新手入门 | 手把手玩转OceanBase-34

第一次是全量编译,巨慢无比!!!

  • 如果机器内存比较少,编译时内存不太够的话,可以考虑开启swap进行编译,开启swap的方法
# 创建⼀个20G⼤⼩的⽂件. 其中swapfile是想要作为swap空间的⽂件,可以⾃⾏指定路径
sudo dd if=/dev/zero of=swapfile bs=1M count=20480 status=progress
sudo mkswap swapfile
# 开启swap
sudo swapon swapfile
# 编译完成后,可以关闭swap
sudo swapoff swapfile
# ⽂件可以删除,也可以不删除

使用OBD部署

  • 参考这篇文章安装OBD
sudo -E yum-config-manager --add-repo https://mirrors.aliyun.com/oceanbase/OceanBase.repo
sudo -E yum install -y ob-deploy
source /etc/profile.d/obd.sh
sudo -E obd update # 更新obd到最新版本。更新obd可以避免出现⼀些⽬前已知的BUG

如果没有yum-config-manager命令,执⾏这个命令安装:

sudo -E yum -y install yum-utils

这里需要注意一点,sudo 会重置命令执⾏的环境变量,使⽤ sudo -E 'your command' 可以使sudo 继承当前的环境变量。因为大赛给的服务器访问外网需要设置代理,所以不加-E就相当于没有设置代理,就下载不了,麻了,当时我们队没发现这个问题,于是在自己电脑下载rpm包,然后scp到服务器上手动安装的,好蠢QwQ。

创建OBD镜像

进入编译目录build_release,执行下面这条命令,将编译完成的内容安装到当前⽬录。

make DESTDIR=./ install

OceanBase新手入门 | 手把手玩转OceanBase-35

出现这个报错是正常的,只要usr/local/bin ⽬录下⾯有 observer 就可以了

OceanBase新手入门 | 手把手玩转OceanBase-36

接着我们执行下面这条shell语句创建我们的obd镜像

obd mirror create -n oceanbase-ce -V 3.1.0 -p ./usr/local -t obadvanced

因为我已经创建过了,所以可以加-f参数来强制更新

OceanBase新手入门 | 手把手玩转OceanBase-37

我们来检测一下是否成功

OceanBase新手入门 | 手把手玩转OceanBase-38

使用OBD部署

⾸先要有⼀个配置⽂件,配置⽂件可以参考https://github.com/oceanbase/obdeploy/tree/master/example ⽬录下⾯的*.yaml,这⾥给出⼀个最⼩示例配置,然后使⽤ obd autodeploy 的功能,⾃动补全配置。

 oceanbase-ce:
   tag: obadvanced
   servers:
     - name: test
       ip: 127.0.0.1
 
   global:
     home_path: /home/test/ob-advanced
     devname: lo
     mysql_port: 2881
     rpc_port: 2882
     zone: zone1
     cluster_id: 1
     memory_limit: 12G
     datafile_size: 10G
     appname: obcluster
 
   test:
     devname: lo
     syslog_level: INFO
     enable_syslog_recycle: true
     enable_syslog_wf: true
     max_syslog_file_count: 4
     memory_limit: 12G
     system_memory: 6G
     cpu_count: 16

使用autodeploy部署

obd cluster autodeploy obadvanced -c ob-advanced-auto.yaml

其中obadvanced是集群名字。你可以使⽤⾃⼰喜欢的名字,后续所有obd相关的操作,也都与这个名字相关。-c 后是配置⽂件名字。注意修改为⾃⼰的⽂件名。

OceanBase新手入门 | 手把手玩转OceanBase-39

检查运⾏时配置⽂件

obd cluster edit-config obadvanced

OceanBase新手入门 | 手把手玩转OceanBase-40

检查是否运⾏成功

obd cluster list

OceanBase新手入门 | 手把手玩转OceanBase-41

检查是否可以正常连接

mysql -uroot -h{IP} -P 2881 -c

OceanBase新手入门 | 手把手玩转OceanBase-42

创建测试用租户

OB集群创建完成时,只有⼀个sys租户,⽽sys租户是不能⽤来测试的,⽽是⽤来管理集群⽤的。OBD也提供了⼀个⾮常⽅便的创建租户的命令。因为OB⽐赛场景简单,所以仅创建⼀个租户。

obd cluster tenant create obadvanced --tenant-name test

这个命令创建了⼀个名字叫 test 的租户,并且将剩下的所有资源都给了test。

OceanBase新手入门 | 手把手玩转OceanBase-43

使⽤test租户连接数据库

mysql -uroot@test -h{IP} -P 2881 -c

OceanBase新手入门 | 手把手玩转OceanBase-44

至此,环境已经配置好了!

那么,如果我们修改了代码,如何让集群运行我们修改之后的版本呢?改一下软连接就好了

OceanBase新手入门 | 手把手玩转OceanBase-45

之后每次修改编译完成之后重启我们的observer就好了,一劳永逸

obd cluster restart obadvanced

OceanBase新手入门 | 手把手玩转OceanBase-46

性能测试的倚天剑 - sysbench

安装

wget https://download-ib01.fedoraproject.org/pub/epel/7/x86_64/Packages/s/sysbench-1.0.17-2.el7.x86_64.rpm
sudo yum localinstall -y sysbench-1.0.17-2.el7.x86_64.rpm

使用

将下面的shell脚本保存为一个.sh文件

echo $1
HOST={IP}
PORT=2881
USER=root@test
DB=test
THREADS=128
TABLE_SIZE=100000
TIME=600
REPORT_INTERVAL=10
sysbench --db-ps-mode=disable --mysql-host=$HOST --mysql-port=$PORT \
        --mysql-user=$USER --mysql-db=$DB \
        --threads=$THREADS --table_size=$TABLE_SIZE \
        --time=$TIME --report-interval=$REPORT_INTERVAL \
        subplan $1

比如文件名为ob_local_test.sh,那么

./ob_local_test.sh cleanup # 清理环境
./ob_local_test.sh prepare # 准备环境
./ob_local_test.sh run     # 执⾏测试

OceanBase新手入门 | 手把手玩转OceanBase-47

这里的subplan其实是sysbench 测试脚本 subplan.lua。在/usr/share/sysbench里面,具体内容如下所示

-- Copyright (C) 2006-2018 Alexey Kopytov <akopytov@gmail.com>

-- This program is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.

-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.

-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

-- -----------------------------------------------------------------------------
-- Common code for OLTP benchmarks.
-- -----------------------------------------------------------------------------

function init()assert(event ~= nil,
           "this script is meant to be included by other OLTP scripts and " ..
              "should not be called directly.")
 end

 if sysbench.cmdline.command == nil thenerror("Command is required. Supported commands: prepare, prewarm, run, " ..
             "cleanup, help")
 end

 -- Command line options
 sysbench.cmdline.options = {
    table_size =
       {"Number of rows per table", 10000},
    range_size =
       {"Range size for range SELECT queries", 100},
    tables =
       {"Number of tables", 1},
    point_selects =
       {"Number of point SELECT queries per transaction", 10},
    simple_ranges =
       {"Number of simple range SELECT queries per transaction", 1},
    sum_ranges =
       {"Number of SELECT SUM() queries per transaction", 1},
    order_ranges =
       {"Number of SELECT ORDER BY queries per transaction", 1},
    distinct_ranges =
       {"Number of SELECT DISTINCT queries per transaction", 1},
    index_updates =
       {"Number of UPDATE index queries per transaction", 1},
    non_index_updates =
       {"Number of UPDATE non-index queries per transaction", 1},
    delete_inserts =
       {"Number of DELETE/INSERT combinations per transaction", 1},
    range_selects =
       {"Enable/disable all range SELECT queries", true},
    auto_inc =
    {"Use AUTO_INCREMENT column as Primary Key (for MySQL), " ..
        "or its alternatives in other DBMS. When disabled, use " ..
        "client-generated IDs", false},
    skip_trx =
       {"Don't start explicit transactions and execute all queries " ..
           "in the AUTOCOMMIT mode", false},
    secondary =
       {"Use a secondary index in place of the PRIMARY KEY", false},
    create_secondary =
       {"Create a secondary index in addition to the PRIMARY KEY", true},
    mysql_storage_engine =
       {"Storage engine, if MySQL is used", "innodb"},
    pgsql_variant =
       {"Use this PostgreSQL variant when running with the " ..
           "PostgreSQL driver. The only currently supported " ..
           "variant is 'redshift'. When enabled, " ..
           "create_secondary is automatically disabled, and " ..
           "delete_inserts is set to 0"}
 }

 -- Prepare the dataset. This command supports parallel execution, i.e. will-- benefit from executing with --threads > 1 as long as --tables > 1function cmd_prepare()local drv = sysbench.sql.driver()local con = drv:connect()if sysbench.tid == 1 then
      create_table(drv, con, 1)
    elseif sysbench.tid == 2 then
      create_table(drv, con, 2)
      create_index(drv, con)
    endend

 -- Implement parallel prepare and prewarm commands
 sysbench.cmdline.commands = {
    prepare = {cmd_prepare, sysbench.cmdline.PARALLEL_COMMAND}
 }

 -- Template strings of random digits with 11-digit groups separated by dashes

 -- 10 groups, 119 characterslocal c_value_template = "###########-###########-###########-" ..
    "###########-###########-###########-" ..
    "###########-###########-###########-" ..
    "###########"

 -- 5 groups, 59 characterslocal pad_value_template = "###########-###########-###########-" ..
    "###########-###########"

 function get_c_value()return sysbench.rand.string(c_value_template)
 end

 function get_pad_value()return sysbench.rand.string(pad_value_template)
 endfunction create_table(drv, con, table_id)local query

    print(string.format("Creating table 't%d'...", table_id))
    query = string.format("CREATE TABLE t%d(c1 int primary key, c2 int, c3 int, v1 CHAR(60), v2 CHAR(60), v3 CHAR(60), v4 CHAR(60), v5 CHAR(60), v6 CHAR(60), v7 CHAR(60), v8 CHAR(60), v9 CHAR(60))", table_id)
    con:query(query)

    if (sysbench.opt.table_size > 0) thenprint(string.format("Inserting %d records into 't%d'",
                           sysbench.opt.table_size, table_id))
       query = "INSERT INTO t" .. table_id .. "(c1, c2, c3, v1, v2, v3, v4, v5, v6, v7, v8, v9) VALUES"
       con:bulk_insert_init(query)
       local i_val
       local c_val
       local pad_val

       for j = 1, sysbench.opt.table_size do
          i_val = sysbench.rand.default(1, sysbench.opt.table_size)
          c_val = get_c_value()
          pad_val = get_pad_value()
          query = string.format("(%d, %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')", j, j, i_val, pad_val, pad_val, pad_val, pad_val, pad_val, pad_val, pad_val, pad_val, pad_val)
          con:bulk_insert_next(query)
       end
       con:bulk_insert_done()
    endend

 function create_index(drv, con)if drv:name() == "mysql"then
     do_query(drv, con, "create index t2_i1 on t2(c2)")
     do_query(drv, con, "create index t2_i2 on t2(c3)")
   else
     do_query(drv, con, "create index t2_i1 on t2(c2) local")
     do_query(drv, con, "create index t2_i2 on t2(c3) local")
   endend


 function do_query(drv, con, q)print(q)
   con:query(q)
 end

 function prepare_begin()
    stmt.begin = con:prepare("BEGIN")
 end

 function prepare_commit()
    stmt.commit = con:prepare("COMMIT")
 end

 function prepare_rollback()
    stmt.rollback = con:prepare("rollback")
 end

 function thread_init()
    drv = sysbench.sql.driver()
    con = drv:connect()
 end

 function thread_done()--close_statements()
    con:disconnect()
 end

 function cleanup()local drv = sysbench.sql.driver()
    local con = drv:connect()

    for i = 1, 3 doprint(string.format("Dropping table 't%d'...", i))
       con:query("DROP TABLE IF EXISTS t" .. i )
    endend

 local function get_table_num()return sysbench.rand.uniform(1, sysbench.opt.tables)
 end

 local function get_id()return sysbench.rand.default(1, sysbench.opt.table_size)
 end

 function begin()
    stmt.begin:execute()
 end

 function commit()
    stmt.commit:execute()
 end

 function rollback()
    stmt.rollback:execute()
 end

 -- Re-prepare statements if we have reconnected, which is possible when some of-- the listed error codes are in the --mysql-ignore-errors listfunction sysbench.hooks.before_restart_event(errdesc)if errdesc.sql_errno == 2013 or -- CR_SERVER_LOST
       errdesc.sql_errno == 2055 or -- CR_SERVER_LOST_EXTENDED
       errdesc.sql_errno == 2006 or -- CR_SERVER_GONE_ERROR
       errdesc.sql_errno == 2011    -- CR_TCP_CONNECTIONthen
       close_statements()
       prepare_statements()
    endend

 function event(thread_id)local query
    --db_query("select /*+ordered use_nl(A,B)*/ count(*) from t1 A, v1 B where A.c1 = B.c1 and A.c2 = B.c2")--db_query("select /*+ordered use_nl(A,B)*/ count(*) from t1 A, t2 B where A.c1 = B.c1")--db_query("select /*+ordered use_nl(A,B)*/ count(*) from t1 A, t2 B where A.c1 = B.c1 and A.c2 = B.c2")--db_query("select /*+ordered use_nl(A,B)*/ * from t1 A, v6 B where A.c1 = B.c1")--db_query("select /*+ordered use_nl(A,B)*/ * from t1 A, v10 B where A.c1 = B.c2 and A.c3 = B.c3")
    ival = sysbench.rand.default(1, sysbench.opt.table_size)
    left_min = ival - 100;
    left_max = ival + 100;
    cond = string.format("A.c1 >= %d and A.c1 < %d and A.c2 = B.c2 and A.c3 = B.c3", left_min, left_max)
    --query = "select /*+ordered use_nl(A,B)*/ * from t1 A, t2 B where " .. cond
    query = "select /*+ordered use_nl(A,B)*/ * from t1 A, t2 B where " .. cond
    con:query(query)
 end

失败的尝试-调试环境的打造

前面的环境配置其实都挺简单的,但是由于以前对图形化界面调试的依赖,所以想要在vscode和或者clion上远程进行调试,这一块官网有调试讲解的文章,网上也有文章,但都没屁用,也可能是我太菜了

  • clion

之前就听说过clion的远程开发做的很烂,但由于没用过服务器,一直在我的ubuntu上面用clion开发,所以对这个说法深信不疑,直到自己用了之后才发现,前人诚不欺我也!真的拉。

  • vscode

这个相比clion需要建立目录的映射体验好太多了,但是单步的时候还是提示找不到source,我怀疑是需要设置当前目录之类的,但不知道是哪个参数,也不知道咋搜,麻了

反正前两天一直在折腾这玩意,后来放弃了,不搞了,大不了打日志算了。

gdb才是yyds

gdb这个东西我一直以来都是望而生畏,平常也都是用clion的点点点调试,所以也接触不到这个东西,这次oceanbase大赛限于本机的配置带不动,所以只能被迫用服务器;由于远程调试环境搞不定,所以被迫用gdb。但感谢这次机会让我跳出舒适区,gdb才是yyds!

首先进入我们的oceanbase目录,然后ps -ef | grep observer得到observer的进程号,拿到进程号的方法太多了,不多赘述。

OceanBase新手入门 | 手把手玩转OceanBase-48

然后gdb attach进去

OceanBase新手入门 | 手把手玩转OceanBase-49

我们队刚拿到这个项目的时候也不知道从哪里上手,但后来硬着头皮从最开的run函数开始,单步了两天,把整个流程搞懂了之后就感觉有点懂了,然后针对nlj相关的代码部分又看了几天,差不多就掌握了,然后可以开始上手写代码了。

接下来演示一下一些基本操作:

我们先在int ObMPQuery::process()这个函数打一个断点然后continue

b obmp_query.cpp:95
c

然后客户端连上我们的服务端

mysql -uroot@test -h{IP} -P 2881 -c

由于会执行一些内部sql,所以虽然我们还没有执行sql语句,但是还是在断点处停下来了,想要避免内部sql的影响可以试一下条件断点,b file:line if cond

OceanBase新手入门 | 手把手玩转OceanBase-50

然后我们layout就可以看到源代码了

OceanBase新手入门 | 手把手玩转OceanBase-51

输入n单步往下走,这个时候我们也可以结合vscode来看代码,gdb主要用来看走了哪一条路。

比方说我们对这个函数比较感兴趣,但是这个函数单步有点远,那么我们有以下几种办法

OceanBase新手入门 | 手把手玩转OceanBase-52

  • n 10单步10次
  • b obmp_query.cpp:247在第247行打一个断点,然后c过去,接着s
  • 直接通过vscode全局搜索找到这个函数,然后在这个函数上面打一个断点c过去

这里我采用的第二种方法,在c过去之前,我执行了dis 1把第一个断点禁用了,避免执行其他内部sql又在第一个断点这里停下来了,就很麻QwQ

OceanBase新手入门 | 手把手玩转OceanBase-53

然后我们输入s,看下图可以发现进入这个函数里面了。

OceanBase新手入门 | 手把手玩转OceanBase-54

有时候我们会想要看某些变量的值,如果是局部变量的话,那么可以直接p var_name,如果是成员变量的话,就直接p this->var_name就可以了。

如下图所示,我打印一个局部变量的值,但是它被O2优化给优化掉了,解决这种方法的办法有两个,第一个就是优化级别调整为O0,第二种就是打日志,把这个变量的值打印出来。下面一个是我打印的成员变量,可以看到这条内部sql的内容

OceanBase新手入门 | 手把手玩转OceanBase-55

接着,我看到了一个check_and_refresh_schema函数

OceanBase新手入门 | 手把手玩转OceanBase-56

然后我s进去了,发现这个函数没啥意思,那么我就可以fini提前退出这个函数

OceanBase新手入门 | 手把手玩转OceanBase-57

我们可能还想知道当前这个函数调用栈帧,那么bt帮你搞定

OceanBase新手入门 | 手把手玩转OceanBase-58

然后我们可能像切换函数堆栈,查看上层的一些参数,变量值之类的,可以用up n/down n,向上或者向下选择函数堆栈帧,n是层数。

更多的操作可以看gdb的100个小技巧,目前说到的这些已经可以覆盖80%场景的需求了。

屠龙技!火焰图

这次比赛最大的收获就是学会了如何利用火焰图扁鹊图等来分析程序性能瓶颈,其实实现功能啥的对我的提升不是很大,感觉就是在搬砖。

可以先来一睹火焰图的真容!是不是非常炫酷

OceanBase新手入门 | 手把手玩转OceanBase-59

关于火焰图的使用相关,这一篇博客写的相当好,下面介绍一下如何根据火焰图来寻找瓶颈:

首先附上懒哥的一个脚本

 #!/bin/bash
 PID=`ps -ef | grep observer | grep -v grep | awk '{print $2}'`
 if [ ${#PID} -eq 0 ]
 then
     echo "observer not running"
     exit -1
 fi
 echo "1:"
 perf record -F 99 -g -p $PID -- sleep 360
 echo "2:"
 perf script -i perf.data &> perf.unfold
 echo "3:"
 FlameGraph/stackcollapse-perf.pl perf.unfold &> perf.folded
 echo "4:"
 FlameGraph/flamegraph.pl perf.folded > perf.svg
 rm -rf perf.data* perf.folded perf.unfold

注意,需要clone FlameGraph这个仓库,然后跑sysbench的同时运行这个脚本就可以了。

OceanBase新手入门 | 手把手玩转OceanBase-60

最后生成的结果如下图所示:

OceanBase新手入门 | 手把手玩转OceanBase-61

接下来举几个具体的例子来讲解一下如何根据火焰图来寻找程序的瓶颈,这也是这次比赛学到的最重要的一点!不过程序的瓶颈和数据的分布,特征也存在一定的关系。

这几个其实都是杨学长教我的ヾ(≧▽≦*)o 学长太强了!

#1 reserve

OceanBase新手入门 | 手把手玩转OceanBase-62

一般是找那种比较偏上面,然后它的上一层比它短很多的层,因为这说明这一层的调用本身很耗时。比如上图这个例子,我们点进去放大之后可以看到这个函数本身的开销占了一大半。

OceanBase新手入门 | 手把手玩转OceanBase-63

我们可以看一下这个函数

OceanBase新手入门 | 手把手玩转OceanBase-64

alloc的地方开销比较大,但是分配内存没有办法避免。我圈出来的地方开销也很大,但是还是有优化空间的,这一部分代码的逻辑是在上面分配的buf所在的内存地址初始化count个对象,这里我们可以只创建一个对象,剩下的直接memcpy就好了,就不用调用构造函数啥的了,性能会快一些。

OceanBase新手入门 | 手把手玩转OceanBase-65

其实这里还有优化空间,就是如果count是2的幂,那么我们第一次memcpy1个,这样我们就有2个了,接下来memcpy两个,这样我们就得到4个了,再然后我们直接memcpy4个,就得到8个了,以此类推,不过这里实现了之后服务起不来了,就没搞了。

#2 compare_row_key

再看到这个地方

OceanBase新手入门 | 手把手玩转OceanBase-66

OceanBase新手入门 | 手把手玩转OceanBase-67

从上图我们可以发现这个compare的过程花销非常大,所以咱们可以看一看这一块的代码有啥优化空间没有,由于调用的太多,随便优化一点对性能的提升都是巨大的。

OceanBase新手入门 | 手把手玩转OceanBase-68

我们再去这个compare函数看看,里面开销比较大的其实是compare_nullsafe函数,而这个函数里面开销比较大的有两部分,一部分就是根据两个obj的类型找到一个对应的比较函数,还有一个就是调用比较函数得到结果。后者没有优化空间,因为你不可能凭空得到比较结果,而前者是有优化空间的,因为在一次lower_bound当中,对应列的类型是不变的,那么我们可以在这次lower_bound当中把找到的比较函数缓存起来,下一回就不用找了,直接用就好了。

OceanBase新手入门 | 手把手玩转OceanBase-69

前面提到的bound是在这里调用的

OceanBase新手入门 | 手把手玩转OceanBase-70

#3 缓存过大

这个是实验室另一个大佬教的,这个buf占了六十多万字节,我们可以把它缩小一点,因为右表提不上来啥数据。

OceanBase新手入门 | 手把手玩转OceanBase-71

#4 歪门邪道

在12.16公布荒谬的复赛榜单之后,钉钉群里无声胜有声,我想大家也都心照不宣了,那这里就来聊一聊歪门邪道。

先来说一下我们队做的正经优化究竟提升了多少,我的测试机器基准性能是1560tps,resue了iters之后大概能到1800,然后reuse了mgr,并且打开了is_multi之后又加了100tps,有1900了,之后优化了reserve,删掉了trace和diagnosed之后性能大概是2200tps。其实还有几个已知的正经优化点没有做,潘总告诉了我还有几个对象可以reuse,我问他能加多少,他说不到100;学长说的缓存比较函数的优化点也只能加100;同门说的缓存数组初始开小一点的优化点也只能提升几十,这些都做了之后顶天2400。但我做这些的意义是什么呢?为了学习吗?前面做的那些复用以及一些失败的尝试已经让我学到很多了,接着做这个只是搬砖罢了;那是为了晋级吗?2400恐怕真没啥用吧。当时官方的态度也是懒得管了,能过mysqltest就行,笑,于是当时我们也没做正经优化,开始着手歪门邪道了

#4.1 single merge

这是第一个歪门邪道

OceanBase新手入门 | 手把手玩转OceanBase-72

相信做过这个优化点的都知道其实再加一个single merge对象,然后特判count==1,特判成功的话走single merge,但是tps没有变是吧。这是因为你多增加一个对象引入了新的开销,刚好抵掉了。打印了一下count,发现永远为1,于是直接把MultiMerge改成了Single merge,tps+60,mysqltest太捞了,一样可以过

#4.2 index merge

这个比上面那个single merge还暴力

OceanBase新手入门 | 手把手玩转OceanBase-73

我只拿一条数据,然后返回ITER_END,骗上层说我数据已经拿完了。也可以过那个过于捞的mysqltest,这个"优化"tps可以加好几百

#4.3 query range

相信在终端跑过测试case的都知道,很多case拿到的结果其实是一个空表,行,那咱们就hack这一点,举个例子:A.c1 >= 300 and A.c1 < 500是得到的是空结果,A.c1 >= 500 and A.c1 < 700得到的是空结果,那么400 ~ 600肯定也是空结果是不是?直接这样做,tps是3万

OceanBase新手入门 | 手把手玩转OceanBase-74

但直接交这个也太不讲武德了QwQ

继续利用query range,既然300500得到的结果是空表,那么300450是不是结果也是空表呢?没毛病吧。那么我们hack一下从上层传下来的range,把这个range缩小一点是不是tps就上去了呢?缩的越小跑的越快,所以理论上我们可以通过调参将tps控制在2200 ~ 3w之间

我们商量了一下,只调参到3000,如果进不了,那我没话说了。12号中午,最后一次榜单出来了,我只能说,666。

OceanBase新手入门 | 手把手玩转OceanBase-75

#4.4 rescan

这个也挺离谱的,就是跳过rescan的很多东西,然后tps就能很轻松的到6000,然后刚好又可以过那个很捞的test。但自己找几个其他的测试都会挂。

#4.5 跳过右表

就是在get_next_row_with_mode里面特判右表回表,然后直接return OB_ITER_END,大概能有4000左右tps。我当时在obqueryrange.cpp文件里面找了一个地方采用这个思路提前返回,能到8000,但过不了mysqltest。

以上仅供参考,由于sysbench只测性能,不测正确性,而mysqltest数据量只有个位数,所以操作空间很大。

内存复用

stmt_allocator_与allocator_的区别

stmt_allocator_是sql语句级别的分配器,也就是说它分配的内存在整个sql语句执行期间都有效;allocator_在一个语句内的每个操作都会区申请和释放内存。

// TODO

一些失败的优化尝试

query cache

由于测试用例的sql语句出了A.c1的范围条件不一样之外,其他都是一样的,所以一种很暴力的方法就是缓存计算结果。

select /*+ordered use_nl(A,B)*/ * from t1 A, t2 B where A.c1 >= 85300 and A.c1 < 85500 and A.c2 = B.c2 and A.c3 = B.c3;

对于这种sql语句,我们可以直接缓存select /*+ordered use_nl(A,B)*/ * from t1 A, t2 B where A.c2 = B.c2 and A.c3 = B.c3;这条sql语句的执行结果,然后手动的对范围条件进行过滤就好了,相当于整个存储层以及sql层的大部分都没有走,提升巨大。

在讲具体hack过程之前可以先看一看ob中相关代码:

故事要从ObMPQuery::process开始讲起,这个函数比较长,但是不要担心,大部分无关代码我会略去

int ObMPQuery::process()
{
...
ret = process_single_stmt(ObMultiStmtItem(true, i, queries.at(i)),
                    session,
                    has_more,
                    force_sync_resp,
                    async_resp_used,
                    need_disconnect);
...
}

我们只需要关心上面这一行函数调用即可,其他的都是为这条调用铺路

int ObMPQuery::process_single_stmt(const ObMultiStmtItem& multi_stmt_item, ObSQLSessionInfo& session,
    bool has_more_result, bool force_sync_resp, bool& async_resp_used, bool& need_disconnect)
{
...
ret = do_process(session, has_more_result, force_sync_resp, async_resp_used, need_disconnect);
...
}

然后这个里面调用do_process

OB_INLINE int ObMPQuery::do_process(
    ObSQLSessionInfo& session, bool has_more_result, bool force_sync_resp, bool& async_resp_used, bool& need_disconnect)
{
...

中间有一系列的过程,比如说sql rewriter,比如词法语法解析,比如tree rewriter生成物理计划,最后发给优化器进行优化,这一部分我可以指一下路,在ob_sql.cpp里面可以看到对应的过程,但是我们现在不关心这个过程,因为我们不需要改这个。什么意思呢?我们不是要拿到等值条件查询后得到的结果缓存起来嘛?改plan tree当然是一种思路,但是我在最前面把sql语句给它改了岂不是更容易?除了第一条需要走一遍这个过程拿到join后的结果,其他的直接复用这个join的结果即可。

好的,我们已经缓存了结果,但这个结果不是我们想要的,因为还有一个A.c1的过滤条件,我们可以模仿miniob中多表查询的思路来过滤,只不过在ob里面schema存放在ObResultSet这个类里面。

对了,好像还没有讲我们怎么拿到join后的结果,你可能会以为结果存在ObResultSet里面,这个名字确实很有迷惑性,但其实不然。玄机藏在在ob_sync_plan_driver.cpp的response_query_result函数中:

OceanBase新手入门 | 手把手玩转OceanBase-76

第一个红框就是火山模型,不断的向下调用get_next_row函数,最后拿到一行row,不过新版引擎row可不只是通过参数返回上来这么简单,后面向量化模型的尝试当中会具体讲到。然后把这个row通过通信协议编码之后send出去。我们hack这里把拿到的结果缓存起来就好了。

这个简单的实现了之后性能也是十分的逆天:

OceanBase新手入门 | 手把手玩转OceanBase-77

不过后来被官方被ban了,因为太过于耍赖了

火山模型->向量化模型

向量化执行模型对火山模型做了针对性优化,在以下几方面有明显改善:

  • 减少虚函数调用数量,提高了分支预测准确性;
  • 以向量块为单位处理数据,利用 CPU 的数据预取特性,提高了 CPU 缓存命中率;
  • 多行并发处理,发挥了 CPU 的并发执行和 SIMD 特性。

我们要做的很简单,就是把get_next_row接口改成get_next_batch接口,然后调用这个get_next_batch接口即可。

我们首先要知道新引擎的算子是怎么把数据传到上层的,稍微单步了一下,如下述调用链所示:

OceanBase新手入门 | 手把手玩转OceanBase-78

OceanBase新手入门 | 手把手玩转OceanBase-79

OceanBase新手入门 | 手把手玩转OceanBase-80

OceanBase新手入门 | 手把手玩转OceanBase-81

OceanBase新手入门 | 手把手玩转OceanBase-82

OceanBase新手入门 | 手把手玩转OceanBase-83

OceanBase新手入门 | 手把手玩转OceanBase-84

OceanBase新手入门 | 手把手玩转OceanBase-85

OceanBase新手入门 | 手把手玩转OceanBase-86

OceanBase新手入门 | 手把手玩转OceanBase-87

然后进入supply_consume函数的第81行

OceanBase新手入门 | 手把手玩转OceanBase-88

接着进入这个函数的get_next_row

OceanBase新手入门 | 手把手玩转OceanBase-89

接着单步进去,inner_get_next_row

OceanBase新手入门 | 手把手玩转OceanBase-90

再往下存储层真没啥好看的了

OceanBase新手入门 | 手把手玩转OceanBase-91

经过了上面的单步之后发现,其实ObNewRow* row这个参数返回上来的结果是没有用的,它仅仅在MultiMerge层给两个成员赋值了,赋值的目的就是做一些检测。

OceanBase新手入门 | 手把手玩转OceanBase-92

在检测做完之后,在这一步就被扔掉了

OceanBase新手入门 | 手把手玩转OceanBase-93

那么算子是怎么把得到的row向上传递的呢?

OceanBase新手入门 | 手把手玩转OceanBase-94

在这里做的project,把存储层的store_row投影到sql层的datum里面。obj是原有能够自解释类型的值,datum是新引擎使用强类型的值。

OceanBase新手入门 | 手把手玩转OceanBase-95

然后在上层在这里把datum计算出来,然后再datum2obj。

OceanBase新手入门 | 手把手玩转OceanBase-96

这里就是eval的过程,通过offset+类型强转

OceanBase新手入门 | 手把手玩转OceanBase-97

分析到这里之后,我就已经感觉到了这个思路的可行性不高,如果一次投影多个row,这里的下标操作我该怎么搞?后来和实验室同门开会交流后发现,其实这些下标在sql执行前就已经确定了,所以,这个思路在新引擎应该是行不通的,遂放弃QwQ

干掉虚函数

在和谢老师交流之后,她给了我一些优化思路:

  1. 把对性能影响比较大的虚函数改成普通函数,
  2. 对取真值概率高的条件判断用likely()做分支预测,这两个可以提高cache命中率;
  3. 不知道I/O对目前性能的影响大不大,如果有的话可以考虑把NLJ优化成BNL;
  4. mysql里有一套内存临时表的接口,不知道OB有没有,如果有,可以考虑为A表建内存临时表并在范围查询那一列上建索引,然后把查询中的A表替换成这个内存临时表

按照第一个的优化思路,我当时找到了这样一个类,他的父类只有它这一个子类,于是我就把这个虚函数给安全的替换为了普通函数,但,性能非但没有提升,还降了30,于是作罢QwQ

Row Cache

想法是把从存储层拿到的row给缓存起来,这样下次需要拿这个row的时候就不需要取sstable或者memtable里面取了。

核心数据结构是hashmap<int table_id, hashmap<int key, ObStoreRow row>>。

对于每一个table,会有一个hashmap,存储对应key拿到的row。当某个table上面出现了插入,删除或者更新等操作时,那么缓存失效即可。另外设置一个容量上限即可。

针对这个比赛,我主要针对右表回表的场景应用这个思路,所谓回表就是非聚簇索引里面仅存储了索引列和主键,没有完全包含我们需要的列,那么就需要根据主键回到主表去查找了。在命中cache之后,自己调用project,把这个row投影一下就好了。

不过在测sysbench128个线程的时候,hahsmap貌似出现了线程安全问题,懒得搞了。

源码解析

本来准备写我比赛期间读过的一些模块的源码解析的,但是服务器被回收了QwQ,并且我对整个系统其实还是缺少一个全面的认识的,所以就不写了,日后官方出了相关的博客我看能不能做一些细节上面的补充吧(那个知乎的代码导读专栏属实格局太大了,讲的太抽象了)。

比赛总结

初赛最大的收获:实际去编写数据库的相关功能的过程中才发现有些功能实现起来是多么的复杂,并且对于复杂的sql语句,许多功能联动还要保证正确性是一件多么有挑战性的事情,向数据库开发人员致以诚挚的敬意,瑞斯拜!现在在使用sql语句时也会不由自主的去思考这一条sql语句背后底层引擎是如何实现的。

复赛最大的收获:在学会了性能分析工具的使用之后,我明白的最重要的一个道理就是,不要提前优化!有可能你引以为傲的把O(n^2)优化到O(1)的代码压根就没走两次,优化了也是白优化QwQ,用曾博的话说就是,没用~~~( •̀ ω •́ )y

总体评价:初赛搬砖,复赛坐牢ƪ(˘⌣˘)ʃ

番外

miniob需要用到的git知识

安装git

https://www.liaoxuefeng.com/wiki/896043488029600/896067074338496

创建远程private仓库

OceanBase新手入门 | 手把手玩转OceanBase-98

OceanBase新手入门 | 手把手玩转OceanBase-99

创建本地版本库

cd ~
mkdir miniob
cd miniob
git init

添加远程库

git remote add origin {你的git仓库地址}

你的git仓库的地址就是下图红框框出来的部分

OceanBase新手入门 | 手把手玩转OceanBase-100

从miniob官方仓库拉取比赛代码

OceanBase新手入门 | 手把手玩转OceanBase-101

添加miniob远程仓库
git remote add ob git@github.com:oceanbase/miniob.git
将远程仓库更新至本地
git fetch ob
将ob比赛仓库main分支的代码merge到我们的master分支
git merge ob/main

OceanBase新手入门 | 手把手玩转OceanBase-102

将比赛代码同步到自己的远程仓库

OceanBase新手入门 | 手把手玩转OceanBase-103

如何合作开发?

邀请队友进入你的私有仓库

由于private仓库其他人是看不到的,所以需要邀请队友之后,队友才有权限

OceanBase新手入门 | 手把手玩转OceanBase-104

多人协作

https://www.liaoxuefeng.com/wiki/896043488029600/900375748016320

https://www.liaoxuefeng.com/wiki/896043488029600/1216289527823648

git常用命令

常用的有这几个, 具体可以百度一下

  • git status
  • git add
  • git commit
  • git push
  • git log

如何快速上手项目

项目文档

项目有文档的话,先从文档看起,会事半功倍。 https://oceanbase-partner.github.io/lectures-on-dbms-implementation/miniob-introduction

OceanBase新手入门 | 手把手玩转OceanBase-105

我们主要关注红框的这三个模块即可

架构图:

OceanBase新手入门 | 手把手玩转OceanBase-106

阅读源码

关于这一点有很多不同的方法论,最基础的是从程序入口点开始,对于C++程序找main函数,看看入口部分有怎样的逻辑,有主循环的话每个循环做了哪些事,一条客户端发过来的请求的处理流程都有哪些步骤,从什么位置开始进入多态的部分,接口是什么样子的。要分清主干和枝节代码,即要先阅读主干代码,其他枝枝节节的代码没明白的可以放一边。切忌一开始就深入细节然后出不来了,这样就会造成只见冰山一角而看不到全貌的感觉。

接着我们可以关注数据流,先分析输入输出,然后顺着数据结构观察输入参数如何传递,如何进行变换,在哪里存储,又被哪些模块取出最终产生输出

如果熟悉同类系统,也可以直接从代码组织结构上猜想模块间的相互关系,然后到具体代码中去验证。这个比较需要经验。

最后也可以直接根据需要分析的业务,猜想它会如何设计,中间会包含怎样的典型逻辑,大概会组织到哪个文件里,函数名可能叫什么,然后直接找相关代码。

如果项目有完整的git记录的话,可以从最开始的commit记录开始看起。超大规模的项目都是由一次次的迭代累积起来的。比如某个代码文件有1000行,一般不太可能是某个人一次性写出来的,那你可以看这个文件的commit记录,看看最早创建这个文件的人写了哪些功能,后来每次commit增加了什么,删除了什么,修改了什么,以及这么做的目的是什么。还可以看看同一个commit里其他文件的修改记录。

对于动手能力强的同学可以尝试从一个小功能开始,去慢慢的熟悉一个项目,因为一个小功能实现思路虽然简单,但是困难的地方在于不知道去哪里加代码,这个时候你就会强迫自己阅读相关部分的源代码了,由点及面,慢慢地整个系统也就懂了。

学会问人,如果你花了几个小时毫无头绪的话,一定要找到熟悉这部分功能的人,让他大致给你讲一下这部分的逻辑和设计理念。自己埋头苦干的话效率太低。


每个人都有自己阅读源码的方式,这里没有最好的,只有最适合的,我来介绍一下我是怎么读源码的。

直接肉眼看代码会出现很多困难,比如不知道对于某条客户请求,会走哪一条代码分支,不知道一些参数具体会取什么样的值,很容易就懵圈了,所以我习惯通过debug的方式去阅读项目源代码。

对于某些没办法通过单步调试玩起来的项目,比如tinykv,我会选择打日志的方式,然后运行起来之后分析收集到的日志来理解代码的执行逻辑,这里的经验就是在哪里打日志,日志的格式是怎样的才方便我们进行分析。

对于一个大型项目,它能运行必然时经过大量测试的,那么我们可以通过给单元测试打断点,然后查看调用栈来理解这个项目。想重点学习某个类,就运行这个类对应的单元测试,根据单元测试的角度了解函数的目的,用法等。

OceanBase新手入门 | 手把手玩转OceanBase-107

OceanBase新手入门 | 手把手玩转OceanBase-108

OceanBase新手入门 | 手把手玩转OceanBase-109

OceanBase新手入门 | 手把手玩转OceanBase-110

OceanBase新手入门 | 手把手玩转OceanBase-111

OceanBase新手入门 | 手把手玩转OceanBase-112

这里非常重要,因为常规的调试都是单步往下走,但是这里可以通过左下角的调用栈,查看已经入栈的栈帧,查看上层函数调用!!!

OceanBase新手入门 | 手把手玩转OceanBase-113

团队协作

对于大型项目,协作,沟通比什么都重要。

OceanBase新手入门 | 手把手玩转OceanBase-114

miniob需要用到的编译原理知识

词法分析

这个阶段的任务是从左到右一个字符一个字符地读入源程序,即对构成源程序的字符流进行扫描然后根据构词规则识别单词(也称单词符号或符号)。词法分析程序实现这个任务。词法分析程序可以使用lex等工具自动生成。

语法分析

把 token 进行递归的组装,生成 AST,按照不同的语法结构,来把一组单词组合成对象。如“程序”,“语句”,“表达式”等等。

下面是我内部分享写的一个简陋的ppt

OceanBase新手入门 | 手把手玩转OceanBase-115

OceanBase新手入门 | 手把手玩转OceanBase-116

OceanBase新手入门 | 手把手玩转OceanBase-117

OceanBase新手入门 | 手把手玩转OceanBase-118

OceanBase新手入门 | 手把手玩转OceanBase-119

OceanBase新手入门 | 手把手玩转OceanBase-120

OceanBase新手入门 | 手把手玩转OceanBase-121

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论