- 获取在同一网段的虚拟机三台,互相之间配置 ssh 免密登录,用作 ceph 密钥与配置信息的同步;
- 在主节点启动 mon 进程,查看状态,将其他主机添加到ceph集群中;
- 在三个环境中启动 osd 进程配置存储盘;
- 创建存储池与 rbd 块设备镜像,并对创建好的镜像在各个节点进行映射即可实现块设备的共享;
- 对块设备进行 PolarFS 的格式化
- 进行一个读写节点,两个只读节点的PolarDB 部署。
系统盘25G,数据盘40G,image大小111G
内存大小>4G,CPU
ceph的部署需要:
1、Python3
2、Systemd【系统自带,不用安装】
3、运行容器的工具(Docker或Podman)
4、时间同步工具(NTP)
5、用于存储管理的工具(LVM2)【系统自带,不用安装】
yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo mkdir /etc/docker
sudo vi /etc/docker/daemon.json
#输入以下内容
{
"registry-mirrors": ["https://registry.docker-cn.com"]
}
systemctl start docker
systemctl enable docker
systemctl status ntpd
systemctl start ntpd #如果发现没有启动,则使用这个命令启动
yum install -y python3
docker pull quay.io/ceph/ceph:v15

curl --silent --remote-name --location https://github.com/ceph/ceph/raw/octopus/src/cephadm/cephadm
chmod +x cephadm
./cephadm add-repo --release octopus
./cephadm install
cephadm install ceph-common
任意选定一个节点作为ceph集群管理的主节点,通过ceph bootstrap在这个节点创建一个监控和管理的daemon进程,生成ceph集群的密钥文件/etc/ceph/ceph.pub、/etc/ceph/ceph.client.admin.keyring,以及集群的配置文件/etc/ceph/ceph.conf,跳过pull容器镜像quay.io/ceph/ceph:v15。
cephadm bootstrap --mon-ip $(hostname -I | cut -d ' ' -f1) --skip-pull #已在三.1拉取
在每台主机生成自己的公钥和私钥对
ssh-keygen
每台主机把自己的公钥发送给其他主机
ssh-copy-id -i .ssh/id_rsa.pub root@hostip
ceph orch host add $(hostname) $(hostname -I | cut -d ' ' -f1)
把主节点的/etc/ceph/* 文件内的公私钥对和配置文件信息通过scp的方式传输给其他准备加入ceph集群的主机
mkdir /etc/ceph
scp /etc/ceph/* hostip:/etc/ceph/

# hostname为主机名,hostip为添加的主机ip,例如添加10.24.14.244的主机名为10-24-14-244
ceph orch host add hostname hostip

ceph -s
注意看quorum后面是否加入了3个主机

ceph集群中的所有主机都要执行umount和osd添加
ssh -t hostip umount /dev/vdb

ceph orch daemon add osd hostname:/dev/vdb

检查ceph集群状态是否为health_ok,同时看看是否所有的节点是否都加入了,osd启动的数量是否正确,可能会存在一定的延迟
ceph -s

存储池和镜像块的创建(1、2步)仅在主节点执行1次,(3、4步)映射镜像文件需要在所有节点执行
ceph osd pool create rbd_polar
rbd create --size 113664 rbd_polar/image01 #小于单个/dev/vdb大小的3倍,这里使用的是111GB
ceph osd pool application enable rbd_polar rbd #启动osd pool application
rbd feature disable rbd_polar/image01 object-map fast-diff deep-flatten #关闭 rbd 不支持特性,才可以执行rbd map
rbd map rbd_polar/image01
rbd device list #检查是否成功创建映射

lsblk #检查是否成功出现rbd0

【定期更新】DockerHub 上的 PolarDB 开发镜像,其中已经包含了编译完毕的 PFS,无需再次编译安装,直接进入容器即可。
docker pull polardb/polardb_pg_devel
docker run -it \
--network=host \
--cap-add=SYS_PTRACE --privileged=true \
--name polardb_pg \
polardb/polardb_pg_devel bash

进入容器后执行,在所有的主机上执行(polarfs不能识别rb开头的磁盘,需要重映射)
sudo ln -s /dev/rbd0 /dev/vdc

选择任意一台主机,在共享存储块设备上格式化 PFS 分布式文件系统
sudo /usr/local/bin/pfs -C disk mkfs vdc

在能够访问共享存储的所有主机节点上分别启动 PFS 守护进程并挂载 PFS 文件系统
sudo /usr/local/polarstore/pfsd/bin/start_pfsd.sh -p vdc

从github拉取polardb的POLARDB_11_STABLE分支源代码(Gitee国内镜像)
git clone -b POLARDB_11_STABLE https://gitee.com/mirrors/PolarDB-for-PostgreSQL
进入源码目录,使用 --with-pfsd 选项编译 PolarDB 内核编译
cd PolarDB-for-PostgreSQL/
./polardb_build.sh --with-pfsd
脚本在编译完成后,会自动部署一个基于本地文件系统的实例,运行于 5432 端口上。手动键入以下命令停止这个实例,以便 在 PFS 和共享存储上重新部署实例
$HOME/tmp_basedir_polardb_pg_1100_bld/bin/pg_ctl \
-D $HOME/tmp_master_dir_polardb_pg_1100_bld/ \
stop
$HOME/primary/$HOME/tmp_basedir_polardb_pg_1100_bld/bin/initdb -D $HOME/primary

/vdc/shared_data/ 目录上初始化共享数据目录sudo /usr/local/bin/pfs -C disk mkdir /vdc/shared_data # 使用 pfs 创建共享数据目录
sudo $HOME/tmp_basedir_polardb_pg_1100_bld/bin/polar-initdb.sh $HOME/primary/ /vdc/shared_data/ # 初始化 db 的本地和共享数据目录

$HOME/primary/postgresql.conf
添加下方文本内容,同时注意修改polar_disk_name、polar_datadir、synchronous_standby_names
port=5432 #不同机器上相同端口号,相同机器上不同端口号(5433,5434)
polar_hostid=1 #不同节点上编号不同 1,2,3,...,读写节点编号为1
polar_enable_shared_storage_mode=on
polar_disk_name='nvme1n1' #需要修改成空闲磁盘名vdc
polar_datadir='/nvme1n1/shared_data/' #需要修改成空闲磁盘名vdc
polar_vfs.localfs_mode=off
shared_preload_libraries='$libdir/polar_vfs,$libdir/polar_worker'
polar_storage_cluster_name='disk'
logging_collector=on
log_line_prefix='%p\t%r\t%u\t%m\t'
log_directory='pg_log'
listen_addresses='*'
max_connections=1000
synchronous_standby_names='replica1,replica2' #有几个只读节点就写几个replica,如果有三个只读节点,那就是'replica1,replica2,replica3'
$HOME/primary/pg_hba.conf中添加行
host replication postgres 0.0.0.0/0 trust
$HOME/tmp_basedir_polardb_pg_1100_bld/bin/pg_ctl start -D $HOME/primary

psql
\l
create database t;
\l
\c t;
create table t1(k int);
\d
insert into t1 values(1);
select * from t1;

创建相应的 replication slot,用于只读节点的物理流复制,必须逐个进行replica slot的创建
$HOME/tmp_basedir_polardb_pg_1100_bld/bin/psql \
-p 5432 \
-d postgres \
-c "select pg_create_physical_replication_slot('replica1');"
$HOME/tmp_basedir_polardb_pg_1100_bld/bin/psql \
-p 5432 \
-d postgres \
-c "select pg_create_physical_replication_slot('replica2');"

假设有两个只读节点,分别初始化数据目录replica1和replica2,初始化需要定期更新
$HOME/replica1/,$HOME/replica2/$HOME/tmp_basedir_polardb_pg_1100_bld/bin/initdb -D $HOME/replica1
$HOME/tmp_basedir_polardb_pg_1100_bld/bin/initdb -D $HOME/replica2

$HOME/replica1/postgresql.conf
添加下方文本内容,注意修改polar_hostid、polar_disk_name、polar_datadir
port=5432 #不同机器上相同端口号,相同机器上不同端口号(5433,5434)
polar_hostid=2 #不同节点上编号不同,第一个读节点为2,第二个为3,...
polar_enable_shared_storage_mode=on
polar_disk_name='nvme1n1' #需要修改成空闲磁盘名vdc
polar_datadir='/nvme1n1/shared_data/' #需要修改成空闲磁盘名vdc
polar_vfs.localfs_mode=off
shared_preload_libraries='$libdir/polar_vfs,$libdir/polar_worker'
polar_storage_cluster_name='disk'
logging_collector=on
log_line_prefix='%p\t%r\t%u\t%m\t'
log_directory='pg_log'
listen_addresses='*'
max_connections=1000
$HOME/replica1/recovery.conf
此文件为新建,注意修改primary_slot_name、primary_conninfo
polar_replica='on'
recovery_target_timeline='latest'
primary_slot_name='replica1'
primary_conninfo='host=[读写节点所在IP] port=5432 user=postgres dbname=postgres application_name=replica1'
根据各个节点修改后运行
$HOME/tmp_basedir_polardb_pg_1100_bld/bin/pg_ctl start -D $HOME/replica1

作为只读节点,无法写入,主要检查两个:1、是否能看到读写节点上写下的数据2、在写入时是否能报不允许
psql
\l
\c t
\d
select * from t;
create table tt(k int);
至此,三节点部署已完成
systemctl status ntpd
systemctl start ntpd
修改在要对齐的主机上执行(授权 10.24.14.0-10.24.14.255 网段上的所有机器可以从这台机器上查询和同步时间),对下面这行取消注释
# restrict 10.24.14.0 mask 255.255.255.0 nomodify notrap
对下面这几行添加注释
server 0.centos.pool.ntp.org iburst
server 1.centos.pool.ntp.org iburst
server 2.centos.pool.ntp.org iburst
server 3.centos.pool.ntp.org iburst
添加以下行,当该节点无网络连接,使用本地时间作为时间服务器为其他节点提供时间同步
server 127.127.1.0 fudge 127.127.1.0 stratum 10
systemctl restart ntpd
systemctl enable ntpd
关闭所有节点机器上的ntpd服务与自启动
sudo systemctl stop ntpd
sudo systemctl disable ntpd
所有节点机器设置定时任务1 分钟与时间服务器同步一次
sudo crontab -e
*/1 * * * * -b /usr/sbin/ntpdate 主机ip
sudo date
手动同步相关命令
sudo ntpdate 10.11.6.135
sudo ntpdate -s 10.11.6.135
sudo clockdiff 10.11.6.33
sudo dmsetup status
sudo dmsetup remove_all
本篇内容主要探讨面向HTAP数据库的基准评测工具,以及研究进展。OceanBase 作为从 OLTP 数据库系统扩展而来的分布式 HTAP 数据库系统,它提供了两种资源隔离方案:OLAP 负载占比低时,在主副本上执行分析任务,以获得实时的数据;OLAP 占比高时,在只读副本上执行分析任务,以实现显式物理隔离,故而 OceanBase 在隔离性和数据库性能上有机会做出比较好的权衡,即可能具有较好的 HTAP 负载支持能力。后续我们也将陆续发布 OceanBase 对 HTAP 负载的支持能力测试报告。
HTAP 数据库实现的难点在于 TP/AP 的资源隔离和数据同步。因此,除了评测 TP 负载面对高并发负载的性能和 AP 负载面对复杂查询的性能外,现有的 HTAP benchmark 在评估性能的设计上更加关注以下两个问题:
混合负载生成:生成 TP 和 AP 负载,并且控制 AP 与 TP 负载之间数据访问的交叉。
负载指标:量化评测混合负载运行时的隔离性,即相互之间的干扰程度。
目前主流的 HTAP 评测基准(工具)有 CH-benCHmark(2011)[1]、HTAPBench(2017)[2]、OLxPBench(2022)[3]、HATtrick(2022)[4]。下面本文将对这 4 个工作从表模式和负载生成、测试方法、控制方法、测试指标等几个方面进行分析和总结。
它于 2011 年被提出,是第一个官方提出的混合负载评测基准,基于标准 OLTP 和 OLAP 基准完成定义。
![图1 CH-benCHmark的TP负载和AP负载运行模式[1]](proxy.php?url=/auto-image/picrepo/a8d96710-23c6-47ca-b526-6671be3c02c9.png)
图1 CH-benCHmark的TP负载和AP负载运行模式[1]
表模式和负载:把 TPC-C 和 TPC-H 表模式进行简单缝合。事务性负载使用 TPC-C 的 5 类负载,分析性负载使用 TPC-H 的 22 个查询。但是这种方式存在 AP 的扫描和 TP 的修改在数据访问空间上的不一致,较小的访问交叉使遇到读写冲突的概率较低,不同类型负载在处理上存在的资源干扰较低。
测试方法:分别运行 TPC-C 指定比例的事务和改装后类似 TPC-H 的 22 个查询,运行模式见图 1。在测试过程中,通过指定 OLAP 流的数量、OLTP 不同事务的初始占比和客户端数量,分别测量该负载模式下的 TP 和 AP 能力。同时,为了比较两类负载之间的相互干扰情况,测试需要至少进行三组,无 TP 流纯 AP 流负载、无 AP 流纯 TP 流负载和指定数量 TP 流和 AP 流负载,通过控制变量的方式人工地对测试结果进行隔离性、干扰性的分析。
测试指标:首次采用
和
。虽然指标很客观,但不适用于数据库间的横向比较,适用于个体数据库的性能展示。
HTAPBench[2] 在 2017 年首次提出以 TP 吞吐量为前提的评测流程。
表模式和负载:与 CH-benCHmark 所使用的保持一致。
测试方法:HTAPBench 通过指定应用可以容忍的 OLTP 目标吞吐下限范围,运行足够多的 TP 线程保证满足吞吐的初始下限,在执行过程中再根据 TP 吞吐的实时反馈来确定是否添加 OLAP 流,由此测得保证 TP 吞吐量下的最大 OLAP 能力,运行模式见图 2。这种测量方式包含了对于 TP/AP 之间相互干扰的考虑,只需要执行一次,较简便。
测试指标:HTAPBench 使用
进行单个 worker 性能之间的比较。
分布控制方法:HTAPBench 提出如何控制分析任务复杂度和查询访问模式这两个问题,主要目的是使得 AP 任务对 TP 生成数据的访问得到控制。同时提出使用密度估计的方法来确定当前数据库的数据分布,使之能够根据当前数据库状态动态确定分析查询。
![图2 HTAPBench运行模式示意图[2]](proxy.php?url=/auto-image/picrepo/cee27f8c-2fb2-4285-adbc-366965b24f41.png)
图2 HTAPBench运行模式示意图[2]
OLxPBench[3] 是中科院计算所研发的关于 HTAP 数据库基准的评测工具(工具架构见图 3),他们对 HTAP 数据库评测的任务进行分析,得出负载应当满足三个特征,即实时查询、语义一致性和面向特定领域。语义一致性需要 TP 修改的数据都被 AP 访问,实时查询包含实时查询和批查询,要能够模拟用户的行为和客户决策的需求。论文指出 CH-benCHmark 和 HTAPBench 两种基准采用的对原有基准简单缝合的方式以及 TPC-H 查询未能真实展现 TP/AP 之间的干扰是要解决的问题。
表模式和负载:OLxPBench 设计了面向通用场景(Subenchmark)、金融场景(Finbenchmark)和电信场景(Tabenchmark)的三种负载,包含对于实时查询、语义一致性的查询逻辑设计。Subenchmark 作为通用的负载,参考了 TPC-C 基准表模式的生成,使用 5 个事务 +9 个分析查询 +5 个混合事务;Fibenchmark参考 SmallBank 基准表模式的生成,使用 6 个事务 +4 个分析查询 +6 个混合事务;Tabenchmark 参考 TATP 基准表模式的生成,使用 7 个事务 +5 个分析查询 +6 个混合事务。
测试方法:与 HTAPBench 相同。
测试指标:结合了 HTAPBench 和 CH-benCHmark,使用
和
进行结果的呈现。
![图3 OLxPBench架构[3]](proxy.php?url=/auto-image/picrepo/21f75943-0a26-426c-bb96-1d7949a6508e.png)
图3 OLxPBench架构[3]
HATtrick[4] 是威斯康辛大学在 2022 年提出的针对 HTAP 数据库的基准,它提出不同任务之间的隔离性和控制新鲜数据的访问是 HTAP 数据库实现中面临的主要挑战。
表模式和负载:HATtrick 是从 SSB 表模式中扩展而来,新增了历史记录表、新鲜度记录表以及部分字段;共有两类负载,事务型负载受 TPC-C 启发使用自建的下单事务、付款事务和订单计数事务,分析型负载使用 13 个调整后的 SSB 查询。
测试方式:给定 TP/AP 客户端数量,同时执行事务和 SSB 的 13 个查询,查询按批的形式连续不断执行,批内查询顺序随机。
测试指标:针对隔离性和新鲜度提出了两个新的评价指标。首先,提出使用吞吐边界(throughput frontier)的概念,通过二维可视化的方式进行隔离性评测如图4所示。在栅格图中,随着客户端数量变化,线越平行坐标轴隔离性越好;而在综合图中,随扩展系数变化的图像中,吞吐边界线位于比例线之上越接近边界线隔离性能越好,越接近比例线表明事务负载和分析负载之间的代价权衡越多,低于比例线越接近坐标轴表示事务负载和分析负载之间的干扰程度越高,资源竞争越激烈。其次,对新鲜度给出了度量函数(查询发起版本与第一个不可见的 TP 版本之间的时间差)
。
![图4 各曲线示意图[4]](proxy.php?url=/auto-image/picrepo/01d02d37-b166-43ab-94fb-8ccda474f6d4.png)
图4 各曲线示意图[4]
根据调研,除了早期的 CH-benCHmark,最近的三款 Benchmark 在评测时明确要求保证 OLTP 的吞吐能力。HTAP 数据库系统上 OLTP 和 OLAP 访问“同一份数据”,而事务处理能力大概率受到同步的影响(新鲜度),如何做好资源共享与资源隔离的权衡[5]是一个难点问题。正如杨传辉在《真正的HTAP对用户和开发者意味着什么?》所说,真正的 HTAP 数据库系统要求先有高性能的 OLTP,然后在 OLTP 产生的新鲜数据上支持实时分析 [5]。
通过对现有的典型 HTAP 数据库评测基准的分析,发现已有基准评测工具在表模式和负载生成、测试方法、分布控制方法、测试指标等方面均各有特色,旨在服务于 HTAP 特性的评测。具体来说,HTAPBench考虑了对计算代价的控制,OLxPBench 考虑了实时查询的使用,HATtrick 考虑了新鲜度指标,值得我们参考和学习。
[1] Cole R, Funke F, Giakoumakis L, et al. The mixed workload CH-benCHmark[C]//Proceedings of the Fourth International Workshop on Testing Database Systems. 2011: 1-6.
[2] Coelho F, Paulo J, Vilaça R, et al. Htapbench: Hybrid transactional and analytical processing benchmark[C]//Proceedings of the 8th ACM/SPEC on International Conference on Performance Engineering. 2017: 293-304.
[3] Kang G, Wang L, Gao W, et al. OLxPBench: Real-time, Semantically Consistent, and Domain-specific are Essential in Benchmarking, Designing, and Implementing HTAP Systems[J]. arXiv preprint arXiv:2203.16095, 2022.
[4] Milkai E, Chronis Y, Gaffney K P, et al. How Good is My HTAP System?[C]//Proceedings of the 2022 International Conference on Management of Data. 2022: 1810-1824.
[6]弱一致性读,https://open.oceanbase.com/docs/observer-cn/V3.1.4/10000000000449449
]]>从源代码编译OceanBase
想要在OceanBase源代码上进行调试,优化,第一件要做的事就是从源代码编译OceanBase。这里推荐克隆OceanBase数据库大赛的比赛分支,克隆完成之后按照官方教程从源码构建OceanBase数据库,其中debug版本可以打断点调试,release版本可以用来测试性能。
安装Oceanbase部署工具OBD
接下来参照官方文档安装OceanBase的部署工具OBD,安装完成之后进入OceanBase源码的编译目录(如build_release),在ocenbase-ce v3.1.0的基础上创建tag为obcompetition的OBD镜像。
obd mirror create -n oceanbase-ce -V 3.1.0 -p ./usr/local -t obcompetition
部署数据库
创建完镜像之后,可以通过配置文件部署数据库,官方有一些配置文件的示例,本文使用的配置文件ob.yaml如下:
oceanbase-ce:
# tag设置为刚才创建镜像obcompetition的tag
tag: obcompetition
servers:
- name: test
ip: 127.0.0.1
global:
# home_path需要修改成自己想要部署的目录
home_path: *****
devname: lo
mysql_port: 2881
rpc_port: 2882
zone: zone1
cluster_id: 1
datafile_size: 10G
appname: obcompetition
test:
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
部署数据库前确定目录home_path为空,之后使用autodeploy自动部署名称为obcompetition的数据库。
obd cluster deploy obcompetition -c ob.yaml
启动数据库,创建测试用租户test,并且将除sys租户以外的资源全部给test租户。
obd cluster start obcompetition
obd cluster tenant create obcompetition --tenant-name test
创建完租户之后就可以通过mysql客户端,连接OceanBase的test租户或者sys租户。
mysql --host 127.0.0.1 --port 2881 -uroot@test
mysql --host 127.0.0.1 --port 2881 -uroot@sys
修改代码后重新部署数据库
在源代码上进行修改之后,首先需要重新编译代码,然后用编译完的内容替换正在运行的observer。首先查看正在运行的observer所在位置,即bin/observer所在的位置,然后ls -l看出这个observer是一个软连接,想要替换它只需要将软连接连接到刚编译出来的observer二进制文件。
ps -ef | grep observer
ls -l ***/bin/observer
最后重新启动obcompetition
obd cluster restart obcompetition
vscode远程调试
在阅读,修改OceanBase源码的时候,需要调试代码,不过OceanBase需要的配置比较高,一般部署在服务器上,这时候使用vscode进行远程调试就比较优雅。
首先在vscode装一下Remote - SSH插件,打开服务器上的OceanBase源代码目录,然后再Debug界面创建一个新的launch.json文件。

创建新的launch.json
将launch.json替换为下面的配置,”configurations”→”program”需要替换为OceanBase配置文件里对应内容。
{
"configurations": [
{
"name": "(gdb) Attach",
"type": "cppdbg",
"request": "attach",
// program需要替换为/home_path/bin/observer
// 其中home_path是OceanBase配置文件里的对应内容
"program": "****",
"processId": "${input:FindPID}",
"MIMode": "gdb",
"sudo": true,
"miDebuggerPath": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
// 这里建立了一些目录映射
// 如果调试的时候提示找不到source,还需要自己加上对应的目录映射
"sourceFileMap": {
"./build_debug/src/observer/./src/observer/omt": {
"editorPath": "${workspaceFolder}/src/observer/omt"
},
"./build_debug/src/sql/parser/./src/sql/parser": {
"editorPath": "${workspaceFolder}/src/sql/parser",
"useForBreakpoints": true
},
"./build_debug/src/sql/./src/sql": {
"editorPath": "${workspaceFolder}/src/sql",
"useForBreakpoints": true
},
"./build_debug/src/sql/engine/join/./src/sql/engine/join": {
"editorPath": "${workspaceFolder}/src/sql/engine/join",
"useForBreakpoints": true
},
"./build_debug/src/storage/./src/storage": {
"editorPath": "${workspaceFolder}/src/storage",
"useForBreakpoints": true
},
"./build_debug/src/rootserver/./src/rootserver": {
"editorPath": "${workspaceFolder}/src/rootserver",
"useForBreakpoints": true
},
"./build_debug/src/share/./src/share": {
"editorPath": "${workspaceFolder}/src/share",
"useForBreakpoints": true
}
}
}
],
"inputs": [
{
"id": "FindPID",
"type": "command",
"command": "shellCommand.execute",
"args": {
"command": "ps -aux | grep /bin/observer | awk '{print $2}' | head -1",
"description": "Select your observer PID",
"useFirstResult": true,
}
}
]
}
然后在服务器上装一个Tasks Shell Input插件,来通过脚本动态获取observer的进程id。

安装Tasks Shell Input插件
这样子在启动observer以后就能成功gdb attach了。

成功gdb attach
打日志调试
vscode调试还是存在一些问题的,比如打断点的位置可能有很多系统进程都会访问(尤其是存储层的代码),mysql客户端输入sql以后,catch住的进程不一定是执行sql的工作线程,函数的调用栈可能不是你想要的,这时候可以通过打日志的方式进行调试。
OceanBase的日志类型定义在deps/oblib/src/lib/oblog/ob_log_module.h里面,日志目录在/home_path/log,日志内容的格式:
[time]log_level[module_name]function_name(filename:file_no)[thread_id]
[Ytrace_id0_trace_id1][log=last_log_print_time]log_data
#time 日志记录时间
#log_level 日志级别
#module_name 模块名
#filename:file_no 文件名:行号
#thread_id 线程id
此外,官方也有讲一些调试手段。
OceanBase数据库大赛使用SysBench进行性能测试,首先在测试机(客户端)上安装sysbench。
subplan.lua
性能测试使用的是sysbench的subplan.lua脚本,该脚本在sysbench安装目录内,脚本里的schema为两张表t1和t2。
CREATE TABLE t1(
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)
);
CREATE TABLE t2(
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)
)
t1,t2表建完后插入数据。
INSERT INTO t1 (c1, c2, c3, v1, v2, v3, v4, v5, v6, v7, v8, v9) VALUES(...);
INSERT INTO t2 (c1, c2, c3, v1, v2, v3, v4, v5, v6, v7, v8, v9) VALUES(...);
插入数据后在t2表建索引,由于两个索引键都非主键,这两个索引都是二级索引,在查内表时会有一个回表操作,也就是根据索引键查询主键对应的行数据。
create index t2_i1 on t2(c2) local;
create index t2_i2 on t2(c3) local;
Select操作限定外表为200个元素的范围查询,通过Hint强制使用Nested-Loop-Join(Index Nested-Loop-Join),并且在c2,c3列进行等值连接。
select /*+ordered use_nl(A,B)*/ *
from t1 A, t2 B
where A.c1 >= ? and A.c1 < ? and A.c2 = B.c2 and A.c3 = B.c3
explain看一下OceanBase的查询执行计划,A表是一个全表的scan,查出200行数据作为内表,在一次查询内,对于内表的每一行数据,B表作为外表可以通过t2_i2索引快速定位到相对应的匹配的数据。
===============================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
-----------------------------------------------
|0 |NESTED-LOOP JOIN| |193 |138185|
|1 | TABLE SCAN |A |200 |188 |
|2 | TABLE SCAN |B(t2_i2)|1 |690 |
===============================================
Outputs & filters:
-------------------------------------
0 - output([A.c1], [A.c2], [A.c3], [A.v1], [A.v2], [A.v3], [A.v4], [A.v5], [A.v6], [A.v7], [A.v8], [A.v9], [B.c1], [B.c2], [B.c3], [B.v1], [B.v2],
[B.v3], [B.v4], [B.v5], [B.v6], [B.v7], [B.v8], [B.v9]), filter(nil),
conds(nil), nl_params_([A.c2], [A.c3]), batch_join=false
1 - output([A.c1], [A.c2], [A.c3], [A.v1], [A.v2], [A.v3], [A.v4], [A.v5], [A.v6], [A.v7], [A.v8], [A.v9]), filter(nil),
access([A.c1], [A.c2], [A.c3], [A.v1], [A.v2], [A.v3], [A.v4], [A.v5], [A.v6], [A.v7], [A.v8], [A.v9]), partitions(p0),
is_index_back=false,
range_key([A.c1]), range[200 ; 400),
range_cond([A.c1 >= 200], [A.c1 < 400])
2 - output([B.c2], [B.c3], [B.c1], [B.v1], [B.v2], [B.v3], [B.v4], [B.v5], [B.v6], [B.v7], [B.v8], [B.v9]), filter([? = B.c2]),
access([B.c2], [B.c3], [B.c1], [B.v1], [B.v2], [B.v3], [B.v4], [B.v5], [B.v6], [B.v7], [B.v8], [B.v9]), partitions(p0),
is_index_back=true, filter_before_indexback[false],
range_key([B.c3], [B.c1]), range(MIN ; MAX),
range_cond([? = B.c3])
测试脚本
USER=root@test
DB=test
HOST=127.0.0.1
PORT=2881
THREADS=128
TABLE_SIZE=100000
TABLES=3
TIME=300
REPORT_INTERVAL=10
sysbench --db-ps-mode=disable --mysql-host=$HOST --mysql-port=$PORT \
--rand-type=uniform --rand-seed=26765 \
--mysql-user=$USER --mysql-db=$DB \
--threads=$THREADS \
--tables=$TABLES --table_size=$TABLE_SIZE \
--time=$TIME --report-interval=$REPORT_INTERVAL \
subplan cleanup
sysbench --db-ps-mode=disable --mysql-host=$HOST --mysql-port=$PORT \
--rand-type=uniform --rand-seed=26765 \
--mysql-user=$USER --mysql-db=$DB \
--threads=$THREADS \
--tables=$TABLES --table_size=$TABLE_SIZE \
--time=$TIME --report-interval=$REPORT_INTERVAL \
subplan prepare
sysbench --db-ps-mode=disable --mysql-host=$HOST --mysql-port=$PORT \
--rand-type=uniform --rand-seed=26765 \
--mysql-user=$USER --mysql-db=$DB \
--threads=$THREADS \
--tables=$TABLES --table_size=$TABLE_SIZE \
--time=$TIME --report-interval=$REPORT_INTERVAL \
subplan run
iotop
首先看一下sysbench测试过程中,observer的磁盘IO情况,这里选用iotop来从系统/proc目录下读取进程的IO信息进行汇总。
sudo iotop -o



sysbench过程中observer的磁盘IO情况
可以看出在sysbench测试一段时间后,只有一些异步日志落盘和事务redo日志会产生写IO,读请求的内容不多,应该被cache在内存里了。再加上官方给的优化建议也是从内存优化入手,我们可以把重心放在内存优化上。
perf
perf是一个轻量级的profiling工具。perf top可以实时打印采样函数,显示出花费大部分CPU时间的函数。
sudo perf top -p observer_pid

perf top查看热点函数
perf top返回的界面还可以交互,通过annotate跳进函数,还可以看到每个指令的耗时占比。不过返回的都是反汇编的结果,难以将其与源代码联系起来。

annotate查看函数每条指令的执行时间占比
perf stat还能看程序的branch-misses情况。
sudo perf stat -p observer_pid

FlameGraph
perf工具的采样结果需要在终端一个个点开函数才能看到调用栈的信息,比较难以对代码的执行流程有一个宏观上的认识,FlameGraph能够帮助我们可视化perf采样的结果。

NestedLoopJoin算子的三个主要部分
跑sysbench的同时跑一下火焰图,可以看到在NLJ的负载模式下,OceanBase的NestedLoopJoin物理算子执行流程主要包含三个部分(三个蓝色箭头指示),中间部分是对左表的扫描,右边部分是根据左表的每一行,先通过B.c3列(其实就是A.c3列的值)查询索引t2_i2,获取到rowkey后再查询t2,左边部分是左表每一行匹配完,与右表完成Join之后,会重置右表的扫描状态。
优化右表回表逻辑
右表会从索引表一次拿batch rowkeys,然后根据rowkeys数组通过ObMultipleGetMerge查询主表,从火焰图可以看出,这一块占了很大的比重。事实上,如果从索引表只拿到一个rowkey,可以使用ObSingleMerge查询主表,效率更高。

原始代码的右表查索引表和回表过程
diff --git a/src/storage/ob_index_merge.cpp b/src/storage/ob_index_merge.cpp
index e6386773..82c59ba4 100644
--- a/src/storage/ob_index_merge.cpp
+++ b/src/storage/ob_index_merge.cpp
@@ -30,7 +30,9 @@ ObIndexMerge::ObIndexMerge()
rowkeys_(),
rowkey_allocator_(ObModIds::OB_SSTABLE_GET_SCAN),
rowkey_range_idx_(),
- index_range_array_cursor_(0)
+ index_range_array_cursor_(0),
+ table_iter_single_(),
+ is_single_(0)
{}
ObIndexMerge::~ObIndexMerge()
@@ -49,12 +51,20 @@ void ObIndexMerge::reset()
rowkey_allocator_.reset();
rowkey_range_idx_.reset();
index_range_array_cursor_ = 0;
+ if (is_single_) {
+ table_iter_single_.reset();
+ is_single_ = 0;
+ }
}
void ObIndexMerge::reuse()
{
table_iter_.reuse();
index_range_array_cursor_ = 0;
+ // if (is_single_) {
+ // table_iter_single_.reuse();
+ // is_single_ = 0;
+ // }
}
int ObIndexMerge::open(ObQueryRowIterator& index_iter)
@@ -71,11 +81,14 @@ int ObIndexMerge::init(const ObTableAccessParam& param, const ObTableAccessParam
int ret = OB_SUCCESS;
if (OB_FAIL(table_iter_.init(param, context, get_table_param))) {
STORAGE_LOG(WARN, "Fail to init table iter, ", K(ret));
+ } else if (OB_FAIL(table_iter_single_.init(param, context, get_table_param))) {
+ STORAGE_LOG(WARN, "Fail to init table single iter, ", K(ret));
} else {
index_param_ = &index_param;
access_ctx_ = &context;
rowkey_cnt_ = param.iter_param_.rowkey_cnt_;
}
+ is_single_ = 0;
return ret;
}
@@ -107,7 +120,6 @@ int ObIndexMerge::get_next_row(ObStoreRow*& row)
ObExtStoreRowkey dest_key;
rowkeys_.reuse();
rowkey_allocator_.reuse();
- table_iter_.reuse();
access_ctx_->allocator_->reuse();
for (int64_t i = 0; OB_SUCC(ret) && i < MAX_NUM_PER_BATCH; ++i) {
if (OB_FAIL(index_iter_->get_next_row(index_row))) {
@@ -139,10 +151,21 @@ int ObIndexMerge::get_next_row(ObStoreRow*& row)
}
if (OB_SUCC(ret)) {
- if (OB_FAIL(table_iter_.open(rowkeys_))) {
- STORAGE_LOG(WARN, "fail to open iterator", K(ret));
+ if (1 == rowkeys_.count()) {
+ table_iter_single_.reuse();
+ is_single_ = 1;
+ if (OB_FAIL(table_iter_single_.open(rowkeys_[0]))) {
+ STORAGE_LOG(WARN, "fail to open iterator", K(ret));
+ } else {
+ main_iter_ = &table_iter_single_;
+ }
} else {
- main_iter_ = &table_iter_;
+ table_iter_.reuse();
+ if (OB_FAIL(table_iter_.open(rowkeys_))) {
+ STORAGE_LOG(WARN, "fail to open iterator", K(ret));
+ } else {
+ main_iter_ = &table_iter_;
+ }
}
}
}
diff --git a/src/storage/ob_index_merge.h b/src/storage/ob_index_merge.h
index e7a6cde5..cdeb0da1 100644
--- a/src/storage/ob_index_merge.h
+++ b/src/storage/ob_index_merge.h
@@ -19,6 +19,7 @@
#include "storage/ob_multiple_get_merge.h"
#include "storage/ob_query_iterator_util.h"
#include "storage/blocksstable/ob_block_sstable_struct.h"
+#include "storage/ob_single_merge.h"
namespace oceanbase {
namespace storage {
@@ -50,6 +51,8 @@ class ObIndexMerge : public ObQueryRowIterator {
common::ObArenaAllocator rowkey_allocator_;
ObArray<int64_t> rowkey_range_idx_;
int64_t index_range_array_cursor_;
+ ObSingleMerge table_iter_single_;
+ int is_single_;
private:
DISALLOW_COPY_AND_ASSIGN(ObIndexMerge);
diff --git a/src/storage/ob_single_merge.cpp b/src/storage/ob_single_merge.cpp
index 42a34425..3c76aa0f 100644
--- a/src/storage/ob_single_merge.cpp
+++ b/src/storage/ob_single_merge.cpp
@@ -141,9 +141,10 @@ int ObSingleMerge::inner_get_next_row(ObStoreRow& row)
int64_t end_table_idx = 0;
int64_t row_cache_snapshot_version = 0;
const ObIArray<ObITable*>& tables = tables_handle_.get_tables();
- const bool enable_fuse_row_cache = is_x86() && access_ctx_->use_fuse_row_cache_ &&
- access_param_->iter_param_.enable_fuse_row_cache() &&
- access_ctx_->fuse_row_cache_hit_rate_ > 6;
+ // const bool enable_fuse_row_cache = is_x86() && access_ctx_->use_fuse_row_cache_ &&
+ // access_param_->iter_param_.enable_fuse_row_cache() &&
+ // access_ctx_->fuse_row_cache_hit_rate_ > 6;
+ const bool enable_fuse_row_cache = false;
access_ctx_->query_flag_.set_not_use_row_cache();
const int64_t table_cnt = tables.count();
ObITable* table = NULL;

rowkey为1时使用SingleMerge回表
rescan过程中尽量少进行对象析构
从火焰图中可以看出,reuse_row_iters会析构掉很多对象,优化思路是保证这些被析构的对象在整个查询过程中始终内存有效,并且每次rescan时重置一些状态。

reuse row iters的内容
diff --git a/src/storage/memtable/ob_memtable.cpp b/src/storage/memtable/ob_memtable.cpp
index 3a4d28f7..ba7d295d 100644
--- a/src/storage/memtable/ob_memtable.cpp
+++ b/src/storage/memtable/ob_memtable.cpp
@@ -1033,7 +1033,7 @@ int ObMemtable::get(const storage::ObTableIterParam& param, storage::ObTableAcce
TRANS_LOG(WARN, "invalid argument, ", K(ret), K(param), K(context));
} else if (OB_FAIL(context.store_ctx_->mem_ctx_->get_trans_status())) {
TRANS_LOG(WARN, "trans already end", K(ret));
- } else if (NULL == (get_iter_buffer = context.allocator_->alloc(sizeof(ObMemtableGetIterator))) ||
+ } else if (NULL == (get_iter_buffer = context.stmt_allocator_->alloc(sizeof(ObMemtableGetIterator))) ||
NULL == (get_iter_ptr = new (get_iter_buffer) ObMemtableGetIterator())) {
TRANS_LOG(WARN, "construct ObMemtableGetIterator fail");
ret = OB_ALLOCATE_MEMORY_FAILED;
@@ -1082,7 +1082,7 @@ int ObMemtable::scan(const storage::ObTableIterParam& param, storage::ObTableAcc
} else {
if (param.is_multi_version_minor_merge_) {
if (GCONF._enable_sparse_row) {
- if (NULL == (scan_iter_buffer = context.allocator_->alloc(sizeof(ObMemtableMultiVersionScanSparseIterator))) ||
+ if (NULL == (scan_iter_buffer = context.stmt_allocator_->alloc(sizeof(ObMemtableMultiVersionScanSparseIterator))) ||
NULL == (scan_iter_ptr = new (scan_iter_buffer) ObMemtableMultiVersionScanSparseIterator())) {
TRANS_LOG(WARN,
"construct ObMemtableMultiVersionScanSparseIterator fail",
@@ -1099,7 +1099,7 @@ int ObMemtable::scan(const storage::ObTableIterParam& param, storage::ObTableAcc
TRANS_LOG(WARN, "scan iter init fail", "ret", ret, K(real_range), K(param), K(context));
}
} else {
- if (NULL == (scan_iter_buffer = context.allocator_->alloc(sizeof(ObMemtableMultiVersionScanIterator))) ||
+ if (NULL == (scan_iter_buffer = context.stmt_allocator_->alloc(sizeof(ObMemtableMultiVersionScanIterator))) ||
NULL == (scan_iter_ptr = new (scan_iter_buffer) ObMemtableMultiVersionScanIterator())) {
TRANS_LOG(WARN,
"construct ObMemtableScanIterator fail",
@@ -1117,7 +1117,7 @@ int ObMemtable::scan(const storage::ObTableIterParam& param, storage::ObTableAcc
}
}
} else {
- if (NULL == (scan_iter_buffer = context.allocator_->alloc(sizeof(ObMemtableScanIterator))) ||
+ if (NULL == (scan_iter_buffer = context.stmt_allocator_->alloc(sizeof(ObMemtableScanIterator))) ||
NULL == (scan_iter_ptr = new (scan_iter_buffer) ObMemtableScanIterator())) {
TRANS_LOG(WARN,
"construct ObMemtableScanIterator fail",
@@ -1162,7 +1162,7 @@ int ObMemtable::multi_get(const storage::ObTableIterParam& param, storage::ObTab
TRANS_LOG(WARN, "invalid argument, ", K(ret), K(param), K(context), K(rowkeys));
} else if (OB_FAIL(context.store_ctx_->mem_ctx_->get_trans_status())) {
TRANS_LOG(WARN, "trans already end", K(ret));
- } else if (NULL == (mget_iter_buffer = context.allocator_->alloc(sizeof(ObMemtableMGetIterator))) ||
+ } else if (NULL == (mget_iter_buffer = context.stmt_allocator_->alloc(sizeof(ObMemtableMGetIterator))) ||
NULL == (mget_iter_ptr = new (mget_iter_buffer) ObMemtableMGetIterator())) {
TRANS_LOG(WARN,
"construct ObMemtableMGetIterator fail",
@@ -1212,7 +1212,7 @@ int ObMemtable::multi_scan(const storage::ObTableIterParam& param, storage::ObTa
TRANS_LOG(WARN, "invalid argument, ", K(ret), K(param), K(context), K(ranges));
} else if (OB_FAIL(context.store_ctx_->mem_ctx_->get_trans_status())) {
TRANS_LOG(WARN, "trans already end", K(ret));
- } else if (NULL == (mscan_iter_buffer = context.allocator_->alloc(sizeof(ObMemtableMScanIterator))) ||
+ } else if (NULL == (mscan_iter_buffer = context.stmt_allocator_->alloc(sizeof(ObMemtableMScanIterator))) ||
NULL == (mscan_iter_ptr = new (mscan_iter_buffer) ObMemtableMScanIterator())) {
TRANS_LOG(WARN,
"construct ObMemtableMScanIterator fail",
diff --git a/src/storage/ob_i_store.h b/src/storage/ob_i_store.h
index e13283f7..69971590 100644
--- a/src/storage/ob_i_store.h
+++ b/src/storage/ob_i_store.h
@@ -833,6 +833,8 @@ public:
}
virtual void reuse()
{}
+ virtual void reset()
+ {}
virtual bool is_base_sstable_iter() const
{
return false;
diff --git a/src/storage/ob_multiple_get_merge.cpp b/src/storage/ob_multiple_get_merge.cpp
index ebfd26a7..c5686543 100644
--- a/src/storage/ob_multiple_get_merge.cpp
+++ b/src/storage/ob_multiple_get_merge.cpp
@@ -82,7 +82,7 @@ void ObMultipleGetMerge::reset_with_fuse_row_cache()
handles_ = nullptr;
}
prefetch_cnt_ = 0;
- reuse_iter_array();
+ reset_iter_array();
}
void ObMultipleGetMerge::reset()
diff --git a/src/storage/ob_multiple_merge.cpp b/src/storage/ob_multiple_merge.cpp
index be8de75f..f5c92405 100644
--- a/src/storage/ob_multiple_merge.cpp
+++ b/src/storage/ob_multiple_merge.cpp
@@ -505,6 +505,10 @@ void ObMultipleMerge::reset()
if (NULL != (iter = iters_.at(i))) {
iter->~ObStoreRowIterator();
}
+ if (OB_NOT_NULL(access_ctx_->stmt_allocator_)) {
+ access_ctx_->stmt_allocator_->free(iter);
+ }
+ iter = NULL;
}
padding_allocator_.reset();
iters_.reset();
@@ -541,17 +545,31 @@ void ObMultipleMerge::reuse()
read_memtable_only_ = false;
}
-void ObMultipleMerge::reuse_iter_array()
+void ObMultipleMerge::reset_iter_array()
{
ObStoreRowIterator* iter = NULL;
for (int64_t i = 0; i < iters_.count(); ++i) {
if (NULL != (iter = iters_.at(i))) {
iter->~ObStoreRowIterator();
}
+ if (OB_NOT_NULL(access_ctx_->stmt_allocator_)) {
+ access_ctx_->stmt_allocator_->free(iter);
+ }
+ iter = NULL;
}
iters_.reuse();
}
+void ObMultipleMerge::reuse_iter_array()
+{
+ ObStoreRowIterator* iter = NULL;
+ for (int64_t i = 0; i < iters_.count(); ++i) {
+ if (NULL != (iter = iters_.at(i))) {
+ iter->reuse();
+ }
+ }
+}
+
int ObMultipleMerge::open()
{
int ret = OB_SUCCESS;
@@ -946,7 +964,7 @@ int ObMultipleMerge::refresh_table_on_demand()
} else if (need_refresh) {
if (OB_FAIL(save_curr_rowkey())) {
STORAGE_LOG(WARN, "fail to save current rowkey", K(ret));
- } else if (FALSE_IT(reuse_iter_array())) {
+ } else if (FALSE_IT(reset_iter_array())) {
} else if (OB_FAIL(prepare_read_tables())) {
STORAGE_LOG(WARN, "fail to prepare read tables", K(ret));
} else if (OB_FAIL(reset_tables())) {
diff --git a/src/storage/ob_multiple_merge.h b/src/storage/ob_multiple_merge.h
index ed227202..a560172d 100644
--- a/src/storage/ob_multiple_merge.h
+++ b/src/storage/ob_multiple_merge.h
@@ -80,6 +80,7 @@ protected:
const ObTableIterParam* get_actual_iter_param(const ObITable* table) const;
int project_row(const ObStoreRow& unprojected_row, const common::ObIArray<int32_t>* projector,
const int64_t range_idx_delta, ObStoreRow& projected_row);
+ void reset_iter_array();
void reuse_iter_array();
virtual int skip_to_range(const int64_t range_idx);
diff --git a/src/storage/ob_sstable.cpp b/src/storage/ob_sstable.cpp
index 13a3f0fa..c0713bbc 100644
--- a/src/storage/ob_sstable.cpp
+++ b/src/storage/ob_sstable.cpp
@@ -1105,14 +1105,14 @@ int ObSSTable::get(const storage::ObTableIterParam& param, storage::ObTableAcces
ObISSTableRowIterator* row_getter = NULL;
if (is_multi_version_minor_sstable() && (context.is_multi_version_read(get_upper_trans_version()) ||
contain_uncommitted_row() || !meta_.has_compact_row_)) {
- if (NULL == (buf = context.allocator_->alloc(sizeof(ObSSTableMultiVersionRowGetter)))) {
+ if (NULL == (buf = context.stmt_allocator_->alloc(sizeof(ObSSTableMultiVersionRowGetter)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
STORAGE_LOG(WARN, "Fail to allocate memory, ", K(ret));
} else {
row_getter = new (buf) ObSSTableMultiVersionRowGetter();
}
} else {
- if (NULL == (buf = context.allocator_->alloc(sizeof(ObSSTableRowGetter)))) {
+ if (NULL == (buf = context.stmt_allocator_->alloc(sizeof(ObSSTableRowGetter)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
STORAGE_LOG(WARN, "Fail to allocate memory, ", K(ret));
} else {
@@ -1163,14 +1163,14 @@ int ObSSTable::multi_get(const ObTableIterParam& param, ObTableAccessContext& co
ObISSTableRowIterator* row_getter = NULL;
if (is_multi_version_minor_sstable() && (context.is_multi_version_read(get_upper_trans_version()) ||
contain_uncommitted_row() || !meta_.has_compact_row_)) {
- if (NULL == (buf = context.allocator_->alloc(sizeof(ObSSTableMultiVersionRowMultiGetter)))) {
+ if (NULL == (buf = context.stmt_allocator_->alloc(sizeof(ObSSTableMultiVersionRowMultiGetter)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
STORAGE_LOG(WARN, "Fail to allocate memory, ", K(ret));
} else {
row_getter = new (buf) ObSSTableMultiVersionRowMultiGetter();
}
} else {
- if (NULL == (buf = context.allocator_->alloc(sizeof(ObSSTableRowMultiGetter)))) {
+ if (NULL == (buf = context.stmt_allocator_->alloc(sizeof(ObSSTableRowMultiGetter)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
STORAGE_LOG(WARN, "Fail to allocate memory, ", K(ret));
} else {
@@ -1269,21 +1269,21 @@ int ObSSTable::scan(const ObTableIterParam& param, ObTableAccessContext& context
void* buf = NULL;
ObISSTableRowIterator* row_scanner = NULL;
if (context.query_flag_.is_whole_macro_scan()) {
- if (NULL == (buf = context.allocator_->alloc(sizeof(ObSSTableRowWholeScanner)))) {
+ if (NULL == (buf = context.stmt_allocator_->alloc(sizeof(ObSSTableRowWholeScanner)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
STORAGE_LOG(WARN, "Fail to allocate memory, ", K(ret));
} else {
row_scanner = new (buf) ObSSTableRowWholeScanner();
}
} else if (is_multi_version_minor_sstable()) {
- if (NULL == (buf = context.allocator_->alloc(sizeof(ObSSTableMultiVersionRowScanner)))) {
+ if (NULL == (buf = context.stmt_allocator_->alloc(sizeof(ObSSTableMultiVersionRowScanner)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
STORAGE_LOG(WARN, "Fail to allocate memory, ", K(ret));
} else {
row_scanner = new (buf) ObSSTableMultiVersionRowScanner();
}
} else {
- if (NULL == (buf = context.allocator_->alloc(sizeof(ObSSTableRowScanner)))) {
+ if (NULL == (buf = context.stmt_allocator_->alloc(sizeof(ObSSTableRowScanner)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
STORAGE_LOG(WARN, "Fail to allocate memory, ", K(ret));
} else {
@@ -1435,14 +1435,14 @@ int ObSSTable::multi_scan(const ObTableIterParam& param, ObTableAccessContext& c
void* buf = NULL;
ObISSTableRowIterator* row_scanner = NULL;
if (is_multi_version_minor_sstable()) {
- if (NULL == (buf = context.allocator_->alloc(sizeof(ObSSTableMultiVersionRowMultiScanner)))) {
+ if (NULL == (buf = context.stmt_allocator_->alloc(sizeof(ObSSTableMultiVersionRowMultiScanner)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
STORAGE_LOG(WARN, "Fail to allocate memory, ", K(ret));
} else {
row_scanner = new (buf) ObSSTableMultiVersionRowMultiScanner();
}
} else {
- if (NULL == (buf = context.allocator_->alloc(sizeof(ObSSTableRowMultiScanner)))) {
+ if (NULL == (buf = context.stmt_allocator_->alloc(sizeof(ObSSTableRowMultiScanner)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
STORAGE_LOG(WARN, "Fail to allocate memory, ", K(ret));
} else {
diff --git a/src/storage/ob_sstable_row_iterator.cpp b/src/storage/ob_sstable_row_iterator.cpp
index 27c89147..cc0d2dd5 100644
--- a/src/storage/ob_sstable_row_iterator.cpp
+++ b/src/storage/ob_sstable_row_iterator.cpp
@@ -1539,7 +1539,7 @@ int ObSSTableRowIterator::alloc_micro_getter()
int ret = OB_SUCCESS;
void* buf = NULL;
if (NULL == micro_getter_) {
- if (NULL == (buf = access_ctx_->allocator_->alloc(sizeof(ObMicroBlockRowGetter)))) {
+ if (NULL == (buf = access_ctx_->stmt_allocator_->alloc(sizeof(ObMicroBlockRowGetter)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
STORAGE_LOG(WARN, "Fail to allocate memory, ", K(ret));
} else {
@@ -1572,14 +1572,14 @@ int ObSSTableRowIterator::open_cur_micro_block(ObSSTableReadHandle& read_handle,
if (NULL == micro_scanner_) {
// alloc scanner
if (!sstable_->is_multi_version_minor_sstable()) {
- if (NULL == (buf = access_ctx_->allocator_->alloc(sizeof(ObMicroBlockRowScanner)))) {
+ if (NULL == (buf = access_ctx_->stmt_allocator_->alloc(sizeof(ObMicroBlockRowScanner)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
STORAGE_LOG(WARN, "Fail to allocate memory for micro block scanner, ", K(ret));
} else {
micro_scanner_ = new (buf) ObMicroBlockRowScanner();
}
} else {
- if (NULL == (buf = access_ctx_->allocator_->alloc(sizeof(ObMultiVersionMicroBlockRowScanner)))) {
+ if (NULL == (buf = access_ctx_->stmt_allocator_->alloc(sizeof(ObMultiVersionMicroBlockRowScanner)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
STORAGE_LOG(WARN, "Fail to allocate memory for micro block scanner, ", K(ret));
} else {
diff --git a/src/storage/memtable/ob_memtable.cpp b/src/storage/memtable/ob_memtable.cpp
index ba7d295d..d1a02dc1 100644
--- a/src/storage/memtable/ob_memtable.cpp
+++ b/src/storage/memtable/ob_memtable.cpp
@@ -1048,6 +1048,7 @@ int ObMemtable::get(const storage::ObTableIterParam& param, storage::ObTableAcce
if (OB_FAIL(ret)) {
if (NULL != get_iter_ptr) {
get_iter_ptr->~ObMemtableGetIterator();
+ context.stmt_allocator_->free(get_iter_ptr);
get_iter_ptr = NULL;
}
TRANS_LOG(WARN, "get fail", K(ret), K_(key), K(param.table_id_));
@@ -1139,6 +1140,7 @@ int ObMemtable::scan(const storage::ObTableIterParam& param, storage::ObTableAcc
} else {
if (NULL != scan_iter_ptr) {
scan_iter_ptr->~ObIMemtableScanIterator();
+ context.stmt_allocator_->free(scan_iter_ptr);
scan_iter_ptr = NULL;
}
TRANS_LOG(
@@ -1182,6 +1184,7 @@ int ObMemtable::multi_get(const storage::ObTableIterParam& param, storage::ObTab
if (OB_FAIL(ret)) {
if (NULL != mget_iter_ptr) {
mget_iter_ptr->~ObMemtableMGetIterator();
+ context.stmt_allocator_->free(mget_iter_ptr);
mget_iter_ptr = NULL;
}
TRANS_LOG(WARN,
@@ -1233,6 +1236,7 @@ int ObMemtable::multi_scan(const storage::ObTableIterParam& param, storage::ObTa
if (OB_FAIL(ret)) {
if (NULL != mscan_iter_ptr) {
mscan_iter_ptr->~ObMemtableMScanIterator();
+ context.stmt_allocator_->free(mscan_iter_ptr);
mscan_iter_ptr = NULL;
}
TRANS_LOG(WARN,
diff --git a/src/storage/ob_multiple_merge.cpp b/src/storage/ob_multiple_merge.cpp
index f5c92405..6426010c 100644
--- a/src/storage/ob_multiple_merge.cpp
+++ b/src/storage/ob_multiple_merge.cpp
@@ -993,7 +993,7 @@ int ObMultipleMerge::release_table_ref()
STORAGE_LOG(WARN, "fail to check need refresh table", K(ret));
} else if (need_refresh) {
tables_handle_.reset();
- reuse_iter_array();
+ reset_iter_array();
is_tables_reset_ = true;
STORAGE_LOG(INFO, "table need to be released", "table_id", access_param_->iter_param_.table_id_,
K(*access_param_), K(curr_scan_index_));
diff --git a/src/storage/ob_sstable_multi_version_row_iterator.cpp b/src/storage/ob_sstable_multi_version_row_iterator.cpp
index 95b94b11..e295ccee 100644
--- a/src/storage/ob_sstable_multi_version_row_iterator.cpp
+++ b/src/storage/ob_sstable_multi_version_row_iterator.cpp
@@ -57,10 +57,13 @@ void ObSSTableMultiVersionRowIterator::reset()
void ObSSTableMultiVersionRowIterator::reuse()
{
- ObISSTableRowIterator::reuse();
+ ObISSTableRowIterator::reset();
+ // ObISSTableRowIterator::reuse();
query_range_ = NULL;
if (NULL != iter_) {
- iter_->reuse();
+ // iter_->reuse();
+ iter_->~ObSSTableRowIterator();
+ iter_ = NULL;
}
out_cols_cnt_ = 0;
range_idx_ = 0;
@@ -123,7 +126,7 @@ int ObSSTableMultiVersionRowGetter::inner_open(
if (OB_FAIL(ObVersionStoreRangeConversionHelper::store_rowkey_to_multi_version_range(
*rowkey_, access_ctx.trans_version_range_, *access_ctx.allocator_, multi_version_range_))) {
LOG_WARN("convert to multi version range failed", K(ret), K(*rowkey_));
- } else if (OB_FAIL(new_iterator<ObSSTableRowScanner>(*access_ctx.allocator_))) {
+ } else if (OB_FAIL(new_iterator<ObSSTableRowScanner>(*access_ctx.stmt_allocator_))) {
LOG_WARN("failed to new iterator", K(ret));
} else if (OB_FAIL(iter_->init(iter_param, access_ctx, table, &multi_version_range_))) {
LOG_WARN("failed to open scanner", K(ret));
@@ -213,7 +216,7 @@ int ObSSTableMultiVersionRowScanner::inner_open(
if (OB_FAIL(ObVersionStoreRangeConversionHelper::range_to_multi_version_range(
*range_, access_ctx.trans_version_range_, *access_ctx.allocator_, multi_version_range_))) {
LOG_WARN("convert to multi version range failed", K(ret), K(*range_));
- } else if (OB_FAIL(new_iterator<ObSSTableRowScanner>(*access_ctx.allocator_))) {
+ } else if (OB_FAIL(new_iterator<ObSSTableRowScanner>(*access_ctx.stmt_allocator_))) {
LOG_WARN("failed to new iterator", K(ret));
} else if (OB_FAIL(iter_->init(iter_param, access_ctx, table, &multi_version_range_))) {
LOG_WARN("failed to open scanner", K(ret));
@@ -306,7 +309,7 @@ int ObSSTableMultiVersionRowMultiGetter::inner_open(
}
}
if (OB_FAIL(ret)) {
- } else if (OB_FAIL(new_iterator<ObSSTableRowMultiScanner>(*access_ctx.allocator_))) {
+ } else if (OB_FAIL(new_iterator<ObSSTableRowMultiScanner>(*access_ctx.stmt_allocator_))) {
LOG_WARN("failed to new iterator", K(ret));
} else if (OB_FAIL(iter_->init(iter_param, access_ctx, table, &multi_version_ranges_))) {
LOG_WARN("failed to open multi scanner", K(ret));
@@ -431,7 +434,7 @@ int ObSSTableMultiVersionRowMultiScanner::inner_open(
}
if (OB_FAIL(ret)) {
- } else if (OB_FAIL(new_iterator<ObSSTableRowMultiScanner>(*access_ctx.allocator_))) {
+ } else if (OB_FAIL(new_iterator<ObSSTableRowMultiScanner>(*access_ctx.stmt_allocator_))) {
LOG_WARN("failed to new iterator", K(ret));
} else if (OB_FAIL(iter_->init(iter_param, access_ctx, table, &multi_version_ranges_))) {
LOG_WARN("failed to open scanner", K(ret));
diff --git a/src/storage/ob_sstable_row_iterator.cpp b/src/storage/ob_sstable_row_iterator.cpp
index cc0d2dd5..acc43774 100644
--- a/src/storage/ob_sstable_row_iterator.cpp
+++ b/src/storage/ob_sstable_row_iterator.cpp
@@ -469,13 +469,13 @@ int ObSSTableRowIterator::inner_open(
STORAGE_LOG(WARN, "Unexpected error, ", K(ret), K_(read_handle_cnt), K_(micro_handle_cnt));
} else if (OB_FAIL(init_handle_mgr(iter_param, access_ctx, query_range))) {
STORAGE_LOG(WARN, "fail to init handle mgr", K(ret), K(iter_param), K(access_ctx));
- } else if (OB_FAIL(read_handles_.reserve(*access_ctx.allocator_, read_handle_cnt_))) {
+ } else if (OB_FAIL(read_handles_.reserve(*access_ctx.stmt_allocator_, read_handle_cnt_))) {
STORAGE_LOG(WARN, "failed to reserve read handles", K(ret), K_(read_handle_cnt));
- } else if (OB_FAIL(micro_handles_.reserve(*access_ctx.allocator_, micro_handle_cnt_))) {
+ } else if (OB_FAIL(micro_handles_.reserve(*access_ctx.stmt_allocator_, micro_handle_cnt_))) {
STORAGE_LOG(WARN, "failed to reserve micro handles", K(ret), K_(micro_handle_cnt));
- } else if (OB_FAIL(sstable_micro_infos_.reserve(*access_ctx.allocator_, micro_handle_cnt_))) {
+ } else if (OB_FAIL(sstable_micro_infos_.reserve(*access_ctx.stmt_allocator_, micro_handle_cnt_))) {
STORAGE_LOG(WARN, "failed to reserve sstable micro infos", K(ret), K_(micro_handle_cnt));
- } else if (OB_FAIL(sorted_sstable_micro_infos_.reserve(*access_ctx.allocator_, micro_handle_cnt_))) {
+ } else if (OB_FAIL(sorted_sstable_micro_infos_.reserve(*access_ctx.stmt_allocator_, micro_handle_cnt_))) {
STORAGE_LOG(WARN, "failed to reserve sorted sstable micro infos", K(ret), K_(micro_handle_cnt));
} else {
sstable_ = static_cast<ObSSTable*>(table);
diff --git a/src/storage/ob_multiple_multi_scan_merge.cpp b/src/storage/ob_multiple_multi_scan_merge.cpp
index a7b2a571..55bdabd8 100644
--- a/src/storage/ob_multiple_multi_scan_merge.cpp
+++ b/src/storage/ob_multiple_multi_scan_merge.cpp
@@ -228,7 +228,7 @@ int ObMultipleMultiScanMerge::construct_iters()
iter->~ObStoreRowIterator();
STORAGE_LOG(WARN, "Fail to push iter to iterator array, ", K(ret), K(i));
}
- } else if (OB_ISNULL(iters_.at(tables.count() - 1 - i))) {
+ } else if (OB_ISNULL(iter = iters_.at(tables.count() - 1 - i))) {
ret = OB_ERR_UNEXPECTED;
STORAGE_LOG(WARN, "Unexpected null iter", K(ret), "idx", tables.count() - 1 - i, K_(iters));
} else if (OB_FAIL(iter->init(*iter_param, *access_ctx_, table, ranges_))) {
diff --git a/src/storage/blocksstable/ob_micro_block_row_scanner.cpp b/src/storage/blocksstable/ob_micro_block_row_scanner.cpp
index d6fd2648..cdc0297f 100644
--- a/src/storage/blocksstable/ob_micro_block_row_scanner.cpp
+++ b/src/storage/blocksstable/ob_micro_block_row_scanner.cpp
@@ -445,7 +445,7 @@ int ObMicroBlockRowScanner::init(const ObTableIterParam& param, ObTableAccessCon
STORAGE_LOG(WARN, "fail to get projector", K(ret));
} else if (OB_FAIL(param_->get_column_map(false /*is get*/, column_id_map))) {
STORAGE_LOG(WARN, "fail to get column id map", K(ret));
- } else if (OB_FAIL(column_map_.init(*context_->allocator_,
+ } else if (OB_FAIL(column_map_.init(*context_->stmt_allocator_,
param_->schema_version_,
param_->rowkey_cnt_,
0, /*store count*/
@@ -573,7 +573,7 @@ int ObMultiVersionMicroBlockRowScanner::init(
STORAGE_LOG(WARN, "fail to get projector", K(ret));
} else if (OB_FAIL(param_->get_column_map(context.use_fuse_row_cache_, column_id_map))) {
STORAGE_LOG(WARN, "fail to get column id map", K(ret));
- } else if (OB_FAIL(column_map_.init(*context_->allocator_,
+ } else if (OB_FAIL(column_map_.init(*context_->stmt_allocator_,
param_->schema_version_,
param_->rowkey_cnt_,
0, /*store count*/
@@ -1358,7 +1358,7 @@ int ObMultiVersionMicroBlockMinorMergeRowScanner::init(
// minor merge should contain 2
if (OB_FAIL(build_minor_merge_out_cols(*param_, out_cols, expect_multi_version_col_cnt))) {
STORAGE_LOG(WARN, "fail to build minor merge out columns", K(ret));
- } else if (OB_FAIL(column_map_.init(*context_->allocator_,
+ } else if (OB_FAIL(column_map_.init(*context_->stmt_allocator_,
param_->schema_version_,
param_->rowkey_cnt_,
0, /*store count*/
diff --git a/src/storage/memtable/ob_memtable.cpp b/src/storage/memtable/ob_memtable.cpp
index d1a02dc1..94050470 100644
--- a/src/storage/memtable/ob_memtable.cpp
+++ b/src/storage/memtable/ob_memtable.cpp
@@ -927,7 +927,7 @@ int ObMemtable::get(const storage::ObTableIterParam& param, storage::ObTableAcce
const ColumnMap* param_column_map = nullptr;
if (nullptr == row.row_val_.cells_) {
if (nullptr ==
- (row.row_val_.cells_ = static_cast<ObObj*>(context.allocator_->alloc(sizeof(ObObj) * out_cols->count())))) {
+ (row.row_val_.cells_ = static_cast<ObObj*>(context.stmt_allocator_->alloc(sizeof(ObObj) * out_cols->count())))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
TRANS_LOG(WARN, "Fail to allocate memory, ", K(ret));
} else {
@@ -940,11 +940,11 @@ int ObMemtable::get(const storage::ObTableIterParam& param, storage::ObTableAcce
TRANS_LOG(WARN, "fail to get column map", K(ret));
} else if (NULL == param_column_map) {
void* buf = NULL;
- if (NULL == (buf = context.allocator_->alloc(sizeof(ColumnMap)))) {
+ if (NULL == (buf = context.stmt_allocator_->alloc(sizeof(ColumnMap)))) {
ret = OB_ALLOCATE_MEMORY_FAILED;
TRANS_LOG(WARN, "Fail to allocate memory, ", K(ret));
} else {
- local_map = new (buf) ColumnMap(*context.allocator_);
+ local_map = new (buf) ColumnMap(*context.stmt_allocator_);
if (OB_FAIL(local_map->init(*out_cols))) {
TRANS_LOG(WARN, "Fail to build column map, ", K(ret));
}
复用handle mgr
ObSSTableRowIterator中使用了block_handle_mgr_和block_index_handle_mgr_来缓存访问到的block_handle和block_index_handle,可以识别出rescan场景并且保持mgr一直有效。
diff --git a/src/storage/ob_i_store.h b/src/storage/ob_i_store.h
index 69971590..eb5274c9 100644
--- a/src/storage/ob_i_store.h
+++ b/src/storage/ob_i_store.h
@@ -785,7 +785,7 @@ public:
class ObStoreRowIterator : public ObIStoreRowIterator {
public:
- ObStoreRowIterator() : type_(0)
+ ObStoreRowIterator() : type_(0), is_rescan_(false)
{}
virtual ~ObStoreRowIterator()
{}
@@ -855,8 +855,14 @@ public:
}
VIRTUAL_TO_STRING_KV(K_(type));
+ virtual void set_rescan_true()
+ {
+ is_rescan_ = true;
+ }
+
protected:
int type_;
+ int is_rescan_;
private:
DISALLOW_COPY_AND_ASSIGN(ObStoreRowIterator);
diff --git a/src/storage/ob_multiple_scan_merge.cpp b/src/storage/ob_multiple_scan_merge.cpp
index 958c335e..130b53e9 100644
--- a/src/storage/ob_multiple_scan_merge.cpp
+++ b/src/storage/ob_multiple_scan_merge.cpp
@@ -160,6 +160,9 @@ int ObMultipleScanMerge::construct_iters()
}
STORAGE_LOG(DEBUG, "[PUSHDOWN]", K_(consumer), K(iter->is_base_sstable_iter()));
STORAGE_LOG(DEBUG, "add iter for consumer", KPC(table), KPC(access_param_));
+ if (is_rescan()) {
+ iter->set_rescan_true();
+ }
}
}
diff --git a/src/storage/ob_sstable_row_iterator.cpp b/src/storage/ob_sstable_row_iterator.cpp
index a09c3e30..0fe498ae 100644
--- a/src/storage/ob_sstable_row_iterator.cpp
+++ b/src/storage/ob_sstable_row_iterator.cpp
@@ -218,6 +218,7 @@ void ObISSTableRowIterator::reset()
batch_rows_ = NULL;
batch_row_count_ = 0;
batch_row_pos_ = 0;
+ is_rescan_ = false;
}
void ObISSTableRowIterator::reuse()
@@ -428,7 +429,8 @@ ObSSTableRowIterator::ObSSTableRowIterator()
io_micro_infos_(),
micro_info_iter_(),
prefetch_handle_depth_(DEFAULT_PREFETCH_HANDLE_DEPTH),
- prefetch_micro_depth_(DEFAULT_PREFETCH_MICRO_DEPTH)
+ prefetch_micro_depth_(DEFAULT_PREFETCH_MICRO_DEPTH),
+ hdr_flag_(0)
{}
ObSSTableRowIterator::~ObSSTableRowIterator()
@@ -640,6 +642,7 @@ void ObSSTableRowIterator::reset()
storage_file_ = nullptr;
prefetch_handle_depth_ = DEFAULT_PREFETCH_HANDLE_DEPTH;
prefetch_micro_depth_ = DEFAULT_PREFETCH_MICRO_DEPTH;
+ hdr_flag_ = 0;
}
void ObSSTableRowIterator::reuse()
@@ -666,8 +669,6 @@ void ObSSTableRowIterator::reuse()
cur_range_idx_ = -1;
io_micro_infos_.reuse();
micro_info_iter_.reuse();
- block_index_handle_mgr_.reset();
- block_handle_mgr_.reset();
table_store_stat_.reuse();
skip_ctx_.reset();
storage_file_ = nullptr;
@@ -1683,6 +1684,29 @@ int ObSSTableRowIterator::init_handle_mgr(
const ObTableIterParam& iter_param, ObTableAccessContext& access_ctx, const void* query_range)
{
int ret = OB_SUCCESS;
+ if (is_rescan_) {
+ if (hdr_flag_ == 0) {
+ block_index_handle_mgr_.reset();
+ block_handle_mgr_.reset();
+ if (OB_FAIL(block_handle_mgr_.init(true, true, *access_ctx.stmt_allocator_))) {
+ STORAGE_LOG(WARN, "failed to init block handle mgr", K(ret), K(true), K(true));
+ } else if (OB_FAIL(block_index_handle_mgr_.init(true, true, *access_ctx.stmt_allocator_))) {
+ STORAGE_LOG(WARN, "failed to init block index handle mgr", K(ret), K(true), K(true));
+ }
+ hdr_flag_ = 1;
+ }
+ return ret;
+ } else {
+ bool is_multi = false;
+ bool is_ordered = false;
+ if (!block_handle_mgr_.is_inited() && OB_FAIL(block_handle_mgr_.init(false, true, *access_ctx.stmt_allocator_))) {
+ STORAGE_LOG(WARN, "failed to init block handle mgr", K(ret), K(is_multi), K(is_ordered));
+ } else if (!block_index_handle_mgr_.is_inited() && OB_FAIL(block_index_handle_mgr_.init(false, is_ordered, *access_ctx.stmt_allocator_))) {
+ STORAGE_LOG(WARN, "failed to init block index handle mgr", K(ret), K(is_multi), K(is_ordered));
+ }
+ return ret;
+ }
+ // never execute
int64_t range_count = 0;
bool is_multi = false;
bool is_ordered = false;
@@ -1703,9 +1727,9 @@ int ObSSTableRowIterator::init_handle_mgr(
range_count >= USE_HANDLE_CACHE_RANGE_COUNT_THRESHOLD);
}
if (OB_SUCC(ret)) {
- if (!block_handle_mgr_.is_inited() && OB_FAIL(block_handle_mgr_.init(false, true, *access_ctx.allocator_))) {
- STORAGE_LOG(WARN, "failed to init block handle mgr", K(ret), K(is_multi), K(is_ordered));
- } else if (!block_index_handle_mgr_.is_inited() && OB_FAIL(block_index_handle_mgr_.init(false, is_ordered, *access_ctx.allocator_))) {
+ if (!block_handle_mgr_.is_inited() && OB_FAIL(block_handle_mgr_.init(false, true, *access_ctx.stmt_allocator_))) {
+ STORAGE_LOG(WARN, "failed to init block handle mgr", K(ret), K(is_multi), K(is_ordered));
+ } else if (!block_index_handle_mgr_.is_inited() && OB_FAIL(block_index_handle_mgr_.init(false, is_ordered, *access_ctx.stmt_allocator_))) {
STORAGE_LOG(WARN, "failed to init block index handle mgr", K(ret), K(is_multi), K(is_ordered));
}
}
diff --git a/src/storage/ob_sstable_row_iterator.h b/src/storage/ob_sstable_row_iterator.h
index ebbbfc17..c6223425 100644
--- a/src/storage/ob_sstable_row_iterator.h
+++ b/src/storage/ob_sstable_row_iterator.h
@@ -426,6 +426,7 @@ private:
ObSSTableMicroBlockInfoIterator micro_info_iter_;
int64_t prefetch_handle_depth_;
int64_t prefetch_micro_depth_;
+ int hdr_flag_;
};
} // namespace storage
diff --git a/src/storage/ob_micro_block_handle_mgr.cpp b/src/storage/ob_micro_block_handle_mgr.cpp
index 028a2018..bb7f5e00 100644
--- a/src/storage/ob_micro_block_handle_mgr.cpp
+++ b/src/storage/ob_micro_block_handle_mgr.cpp
@@ -45,6 +45,13 @@ void ObMicroBlockDataHandle::reset()
io_handle_.reset();
}
+void ObMicroBlockDataHandle::reuse()
+{
+ block_index_ = -1;
+ cache_handle_.reset();
+ io_handle_.reset();
+}
+
int ObMicroBlockDataHandle::get_block_data(
ObMacroBlockReader& block_reader, ObStorageFile* storage_file, ObMicroBlockData& block_data)
{
@@ -104,7 +111,6 @@ int ObMicroBlockHandleMgr::get_micro_block_handle(const uint64_t table_id,
{
int ret = OB_SUCCESS;
bool found = false;
- micro_block_handle.reset();
if (IS_NOT_INIT) {
ret = OB_NOT_INIT;
STORAGE_LOG(WARN, "block handle mgr is not inited", K(ret));
@@ -128,6 +134,7 @@ int ObMicroBlockHandleMgr::get_micro_block_handle(const uint64_t table_id,
}
}
if (!found) {
+ micro_block_handle.reuse();
if (OB_FAIL(ObStorageCacheSuite::get_instance().get_block_cache().get_cache_block(
table_id, block_ctx.get_macro_block_id(), file_id, offset, size, micro_block_handle.cache_handle_))) {
if (OB_ENTRY_NOT_EXIST != ret) {
diff --git a/src/storage/ob_micro_block_handle_mgr.h b/src/storage/ob_micro_block_handle_mgr.h
index 37f6d005..1ff90688 100644
--- a/src/storage/ob_micro_block_handle_mgr.h
+++ b/src/storage/ob_micro_block_handle_mgr.h
@@ -30,6 +30,7 @@ struct ObMicroBlockDataHandle {
ObMicroBlockDataHandle();
virtual ~ObMicroBlockDataHandle();
void reset();
+ void reuse();
int get_block_data(blocksstable::ObMacroBlockReader& block_reader, blocksstable::ObStorageFile* storage_file,
blocksstable::ObMicroBlockData& block_data);
TO_STRING_KV(
diff --git a/src/storage/ob_micro_block_index_handle_mgr.cpp b/src/storage/ob_micro_block_index_handle_mgr.cpp
index 83beb4e0..4e938a81 100644
--- a/src/storage/ob_micro_block_index_handle_mgr.cpp
+++ b/src/storage/ob_micro_block_index_handle_mgr.cpp
@@ -37,6 +37,13 @@ void ObMicroBlockIndexHandle::reset()
io_handle_.reset();
}
+void ObMicroBlockIndexHandle::reuse()
+{
+ block_index_mgr_ = NULL;
+ cache_handle_.reset();
+ io_handle_.reuse();
+}
+
int ObMicroBlockIndexHandle::search_blocks(const ObStoreRange& range, const bool is_left_border,
const bool is_right_border, ObIArray<ObMicroBlockInfo>& infos, const ObIArray<ObRowkeyObjComparer*>* cmp_funcs)
{
@@ -107,7 +114,6 @@ int ObMicroBlockIndexHandleMgr::get_block_index_handle(const uint64_t table_id,
{
int ret = OB_SUCCESS;
bool found = false;
- block_idx_handle.reset();
if (IS_NOT_INIT) {
ret = OB_NOT_INIT;
STORAGE_LOG(WARN, "index handle mgr is not inited", K(ret));
@@ -127,6 +133,7 @@ int ObMicroBlockIndexHandleMgr::get_block_index_handle(const uint64_t table_id,
}
}
if (!found) {
+ block_idx_handle.reuse();
if (OB_FAIL(ObStorageCacheSuite::get_instance().get_micro_index_cache().get_cache_block_index(
table_id, block_ctx.get_macro_block_id(), file_id, block_idx_handle.cache_handle_))) {
if (OB_ENTRY_NOT_EXIST != ret) {
diff --git a/src/storage/ob_micro_block_index_handle_mgr.h b/src/storage/ob_micro_block_index_handle_mgr.h
index 2aea9dcf..89a19ac0 100644
--- a/src/storage/ob_micro_block_index_handle_mgr.h
+++ b/src/storage/ob_micro_block_index_handle_mgr.h
@@ -23,6 +23,7 @@ struct ObMicroBlockIndexHandle {
ObMicroBlockIndexHandle();
virtual ~ObMicroBlockIndexHandle();
void reset();
+ void reuse();
int search_blocks(const common::ObStoreRange& range, const bool is_left_border, const bool is_right_border,
common::ObIArray<blocksstable::ObMicroBlockInfo>& infos,
const common::ObIArray<ObRowkeyObjComparer*>* cmp_funcs = nullptr);
减少冗余的代码 & 逻辑优化(部分内容)
prefetch数据预取逻辑冗余。
int ObSSTableRowIterator::inner_open(
const ObTableIterParam& iter_param, ObTableAccessContext& access_ctx, ObITable* table, const void* query_range)
{
...
else if (OB_FAIL(prefetch())) {
STORAGE_LOG(WARN, "Fail to prefetch data, ", K(ret));
}
...
}
int ObMultipleGetMerge::construct_sstable_iter()
{
for (int64_t i = 0; OB_SUCC(ret) && i < prefetch_cnt; ++i) {
if (OB_FAIL(prefetch())) {
STORAGE_LOG(WARN, "fail to prefetch", K(ret));
}
}
...
}
索引回表时去掉多余的reuse。
void ObTableScanStoreRowIterator::reuse_row_iters()
{
...
if (NULL != index_merge_) {
index_merge_->reuse(); // 每次 rescan 都会进⾏ reuse
}
...
}
void ObIndexMerge::reuse()
{
// table_iter_.reuse();
index_range_array_cursor_ = 0; }
int ObIndexMerge::get_next_row(ObStoreRow*& row) {
......
table_iter_.reuse(); // 在这⾥ reuse
if (OB_FAIL(table_iter_.open(rowkeys_))) {
优化refresh table on demand逻辑
diff --git a/src/storage/ob_multiple_merge.cpp b/src/storage/ob_multiple_merge.cpp
index 9aa5cb01..be8de75f 100644
--- a/src/storage/ob_multiple_merge.cpp
+++ b/src/storage/ob_multiple_merge.cpp
@@ -922,7 +922,7 @@ int ObMultipleMerge::prepare_read_tables()
}
if (OB_SUCC(ret)) {
- relocate_cnt_ = access_ctx_->store_ctx_->mem_ctx_->get_relocate_cnt();
+// relocate_cnt_ = access_ctx_->store_ctx_->mem_ctx_->get_relocate_cnt();
if (OB_UNLIKELY(nullptr != row_filter_)) {
const ObPartitionKey& pkey = partition_store.get_partition_key();
row_filter_ = tables_handle_.has_split_source_table(pkey) ? row_filter_ : NULL;
@@ -987,24 +987,20 @@ int ObMultipleMerge::release_table_ref()
int ObMultipleMerge::check_need_refresh_table(bool &need_refresh)
{
int ret = OB_SUCCESS;
- if (OB_UNLIKELY(!inited_)) {
- ret = OB_NOT_INIT;
- STORAGE_LOG(WARN, "ObMultipleMerge has not been inited", K(ret));
+ if (NULL != access_ctx_->store_ctx_->mem_ctx_) {
+ temp = relocate_cnt_;
+ relocate_cnt_ = access_ctx_->store_ctx_->mem_ctx_->get_relocate_cnt();
+ need_refresh = relocate_cnt_ > temp;
} else {
- const bool relocated = NULL == access_ctx_->store_ctx_->mem_ctx_
- ? false
- : access_ctx_->store_ctx_->mem_ctx_->get_relocate_cnt() > relocate_cnt_;
- const bool memtable_retired = tables_handle_.check_store_expire();
- const int64_t relocate_cnt = access_ctx_->store_ctx_->mem_ctx_->get_relocate_cnt();
- need_refresh = relocated || memtable_retired;
+ need_refresh = tables_handle_.check_store_expire();
+ }
#ifdef ERRSIM
- ret = E(EventTable::EN_FORCE_REFRESH_TABLE) ret;
- if (OB_FAIL(ret)) {
- ret = OB_SUCCESS;
- need_refresh = true;
- }
-#endif
+ ret = E(EventTable::EN_FORCE_REFRESH_TABLE) ret;
+ if (OB_FAIL(ret)) {
+ ret = OB_SUCCESS;
+ need_refresh = true;
}
+#endif
return ret;
}
diff --git a/src/storage/ob_multiple_merge.h b/src/storage/ob_multiple_merge.h
index 12f8cdc2..ed227202 100644
--- a/src/storage/ob_multiple_merge.h
+++ b/src/storage/ob_multiple_merge.h
@@ -164,6 +164,7 @@ class ObMultipleMerge : public ObQueryRowIterator {
int64_t range_idx_delta_;
ObGetTableParam get_table_param_;
int64_t relocate_cnt_;
+ int64_t temp;
ObTableStoreStat table_stat_;
bool skip_refresh_table_;
bool read_memtable_only_;
修改代码的正确性是通过mysqltest运行测试样例来评定,OceanBase代码量庞大,逻辑复杂,自己做的修改难免会出现一些段错误之类的问题,这时候可以开vscode debug,在运行测试用例出错时就会catch住段错误的位置,方便找到问题的根源。
比如这个iter没有初始化的bug就是这样找出来的,改了代码以后会走到ObMultipleMultiScanMerge,mysqltest的测试样例正好测出了这个bug。

第一:Log-Structred,通过日志的方式来组织的
第二:Merge,可以合并的
第三:Tree,一种树形结构
实际上它并不是一棵树,也不是一种具体的数据结构,它实际上是一种数据保存和更新的思想。简单的说,就是将数据按照key来进行排序(在数据库中就是表的主键),之后形成一棵一棵小的树形结构,或者不是树形结构,是一张小表也可以,这些数据通常被称为基线数据;之后把每次数据的改变(也就是log)都记录下来,也按照主键进行排序,之后定期的把log中对数据的改变合并(merge)到基线数据当中。下面的图形描述了LSM Tree的基本结构。

图中的C0代表了缓存在内存中的数据,当内存中的数据达到了一定的阈值后,就会把数据内存中的数据排序后保存到磁盘当中,这就形成了磁盘中C1级别的增量数据(这些数据也是按照主键排序的),这个过程通常被称为转储。当C1级别的数据也达到一定阈值的时候,就会触发另外的一次合并(合并的过程可以认为是一种归并排序的过程),形成C2级别的数据,以此类推,如果这个逐级合并的结构定义了k层的话,那么最后的第k层数据就是最后的基线数据,这个过程通常被称为合并。
用一句话来简单描述的话,LSM Tree就是一个基于归并排序的数据存储思想。从上面的结构中不难看出,LSM Tree对写密集型的应用是非常友好的,因为绝大部分的写操作都是顺序的。但是对很多读操作是要损失一些性能的,因为数据在磁盘上可能存在多个版本,所以通常情况下,使用了LSM Tree的存储引擎都会选择把很多个版本的数据存在内存中,根据查询的需要,构建出满足要求的数据版本。

OceanBase 数据库的存储引擎基于 LSM Tree 架构,将数据分为静态基线数据(放在 SSTable 中)和动态增量数据(放在 MemTable 中)两部分,其中 SSTable 是只读的,一旦生成就不再被修改,存储于磁盘;MemTable 支持读写,存储于内存。数据库 DML 操作插入、更新、删除等首先写入 MemTable,等到 MemTable 达到一定大小时转储到磁盘成为 SSTable。在进行查询时,需要分别对 SSTable 和 MemTable 进行查询,并将查询结果进行归并,返回给 SQL 层归并后的查询结果。同时在内存实现了 Block Cache 和 Row cache,来避免对基线数据的随机读。
当内存的增量数据达到一定规模的时候,会触发增量数据和基线数据的合并,把增量数据落盘。同时每天晚上的空闲时刻,系统也会自动每日合并。
OceanBase 本质上是一个基线加增量的存储引擎,跟关系数据库差别很大,同时也借鉴了部分传统关系数据库存储引擎的优点。
传统数据库把数据分成很多页面,OceanBase 数据库也借鉴了传统数据库的思想,把数据分成很多 2MB 为单位的宏块。合并时采用增量合并的方式,OceanBase 数据库的合并代价相比 LevelDB 和 RocksDB 都会低很多。另外,OceanBase 数据库通过轮转合并的机制把正常服务和合并时间错开,使得合并操作对正常用户请求完全没有干扰。
由于 OceanBase 数据库采用基线加增量的设计,一部分数据在基线,一部分在增量,原理上每次查询都是既要读基线,也要读增量。为此,OceanBase 数据库做了很多的优化,尤其是针对单行的优化。OceanBase 数据库内部除了对数据块进行缓存之外,也会对行进行缓存,行缓存会极大加速对单行的查询性能。对于不存在行的“空查”,OceanBase 会构建布隆过滤器,并对布隆过滤器进行缓存。OLTP 业务大部分操作为小查询,通过小查询优化,OceanBase 数据库避免了传统数据库解析整个数据块的开销,达到了接近内存数据库的性能。另外,由于基线是只读数据,而且内部采用连续存储的方式,OceanBase 数据库可以采用比较激进的压缩算法,既能做到高压缩比,又不影响查询性能,大大降低了成本。
]]>💡 作者:华东师范大学 数据科学与工程学院 DBHammer项目组 东亚男儿团队
本文主体面向对OceanBase数据库源码以及系统性能优化感兴趣的初学者供以技术交流,笔者来自华东师范大学数据科学与工程学院DBHammer项目组。
本文主体分为三个部分:如何快速对OceanBase进行调试;系统性能优化利器-火焰图的简要介绍;面向赛题Nested Loop Join的应用场景,如何进行性能优化。
本文以在VSCode编辑器上OceanBase 3.1版本为例,进行Debug的教学,本地OB搭建的教程可以参考使用源码构建 OceanBase 数据库 和 使用OBD部署OceanBase。需要注意的是OBD目前只有rpm包,在Ubuntu环境下的具体安装方法可见Install RPM packages on Ubuntu | Linuxize。
在安装部署好OB后,接下来我们需要创建一个租户。当OB集群创建完成时,只有一个默认的sys租户,而sys租户仅用于集群管理,并不能支持测试服务,因此我们需要手动创建新的租户用于测试。
OBD提供了方便的创建租户的命令。 在OB比赛的简单场景中,我们仅创建一个租户:
obd cluster tenant create obadvanced --tenant-name mysql
这个命令创建了一个名为mysql 的租户,并为它分配了剩下的所有系统资源,没有设置密码。
接着我们使用mysql租户连接数据库,加上-c以确保之后输入的sql hint生效:
mysql -uroot@mysql -h127.0.0.1 -P 2881 -c
输入指令后即可看到数据库连接界面:

假定大家现在已经搭建了一款本地的OB且创建好了相应的租户,那么接下来我们需要在.vscode目录下创建launch.json文件以配置具体的gdb调试环境。
我们采取的是gdb attach
以下我们给出一个示例文件:
(其中出现的一些诸如ob-advanced这种创建OB时自定义的名称或者文件目录位置都需要再自行调整,sourceFileMap也可能需要根据需求手动增加mapping,更多的json配置项语义可见官网)
{
"configurations": [
{
"name": "(gdb) Attach",
"type": "cppdbg",
"request": "attach",
"program": "/data/ob-advanced/bin/observer",
"processId": "${input:FindPID}",
"MIMode": "gdb",
"sudo": true,
"miDebuggerPath": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"sourceFileMap": {
"./build_debug/src/observer/./src/observer/omt": {
"editorPath": "${workspaceFolder}/src/observer/omt"
},
"./build_debug/src/sql/parser/./src/sql/parser": {
"editorPath": "${workspaceFolder}/src/sql/parser",
"useForBreakpoints": true
},
"./build_debug/src/sql/./src/sql": {
"editorPath": "${workspaceFolder}/src/sql",
"useForBreakpoints": true
},
"./build_debug/src/storage/./src/storage": {
"editorPath": "${workspaceFolder}/src/storage",
"useForBreakpoints": true
},
"./build_debug/src/sql/engine/join/./src/sql/engine/join": {
"editorPath": "${workspaceFolder}/src/sql/engine/join",
"useForBreakpoints": true
}
}
}
],
"inputs": [
{
"id": "FindPID",
"type": "command",
"command": "shellCommand.execute",
"args": {
"command": "ps -aux | grep /bin/observer | grep -v \\"grep\\" | awk '{print $2}'",
"description": "Select your observer PID",
"useFirstResult": true,
}
}
]
}
💡 如果遇到下面的问题
Authentication is needed to run/usr/bin/gdb’ as the super user` 可以输入指令echo 0| sudo tee /proc/sys/kernel/yama/ptrace_scope调整权限解决
最终点击左上角的调试按钮,等待一段时间后,即可看到完整的调试界面,如下图所示:

接着我们通过obclient/mysql模式连接OB。
为了调试的方便,我们可能需要适当增大事务超时时间以避免调试内部因可能的超时原因提前终止而影响判断。
set global ob_trx_idle_timeout=120000000;
set global ob_trx_timeout=36000000000;
set global ob_query_timeout=3600000000
比如,我们想先观察一下当前的场景是否开启了batch操作,即可在ob_nested_loop_join_op.cpp:read_left_operate函数里打上断点(也可以右键编辑条件断点)。

接着我们在命令行中输入负载样例:
select /*+ordered use_nl(A,B)*/ * from t1 A, t2 B where A.c1 >= -100 and A.c1 < 200 and A.c2 = B.c2;
等待片刻即可看到如下所示的debug界面:具体有四大信息栏值得关注:

以上便是对OceanBase的debug调试方法的全部介绍。
除了通过debug从执行的细节上见微知著,我们还可以通过火焰图对程序的整体执行有一个宏观的把握,在此我们简单介绍一下火焰图的使用方法,希望对大家有所帮助。
火焰图由性能优化大师Brendan Gregg发明,以图像的形式形象地展示了程序执行时的调用堆栈信息,从底向上展示函数的执行比例,便于技术人员从中把握可能的性能瓶颈。因其颜色以红黄橙等暖色为主,像是跳动的火焰,故称Flame Graph,下图为OceanBase v3.1的整体火焰图。

悬浮其上便能看到某个函数具体的执行比例:

关于火焰图相关的介绍文档和视频有很多,我们在此就不再赘述了,仅在下面作一个简要的概括,更详细的介绍可参见文章底部提供的链接。
火焰图主体有以下特征(这里以 on-cpu 火焰图为例):

以Brendan Gregg所给示意图来说:
火焰图本身的制作是基于perf生成的data数据进行的,下面我们便进入工具的使用介绍:
git clone [<https://github.com/brendangregg/FlameGraph.git>](<https://github.com/brendangregg/FlameGraph.git>)perf script -i perf.data &> perf.unfold ,用perf script工具读取perf.data结果,并对perf.data进行解析,其输出格式如下:/read_left_operate
ffffffffb86dc289 finish_task_switch+0x199 ([kernel.kallsyms])
ffffffffb9200a46 __sched_text_start+0x2f6 ([kernel.kallsyms])
ffffffffb920109f schedule+0x4f ([kernel.kallsyms])
ffffffffb873bfd3 exit_to_user_mode_prepare+0xf3 ([kernel.kallsyms])
ffffffffb91f7be9 irqentry_exit_to_user_mode+0x9 ([kernel.kallsyms])
ffffffffb91f7c19 irqentry_exit+0x19 ([kernel.kallsyms])
ffffffffb91f6c7e sysvec_reschedule_ipi+0x7e ([kernel.kallsyms])
ffffffffb9400dc2 asm_sysvec_reschedule_ipi+0x12 ([kernel.kallsyms])
972e9a0 oceanbase::storage::ObQueryIteratorConsumer::set_consumer_num+0x0 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
9617ac1 oceanbase::storage::ObMultipleScanMergeImpl::inner_get_next_row+0xb1 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
961793e oceanbase::storage::ObMultipleScanMerge::inner_get_next_row+0x2e (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
9609eff oceanbase::storage::ObMultipleMerge::get_next_row+0x44f (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
9aa2c49 oceanbase::storage::ObTableScanStoreRowIterator::get_next_row+0x119 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
9aa3807 oceanbase::storage::ObTableScanRangeArrayRowIterator::get_next_row+0x117 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
9aa4bb7 oceanbase::storage::ObTableScanIterator::get_next_row+0x1d7 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
9b42a55 oceanbase::storage::ObTableScanIterator::get_next_row+0x25 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
48266b7 oceanbase::sql::ObTableScanOp::get_next_row_with_mode+0x97 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
4826aa8 oceanbase::sql::ObTableScanOp::inner_get_next_row+0x3d8 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
56aa469 oceanbase::sql::ObOperator::get_next_row+0x189 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
38731eb oceanbase::sql::ObJoinOp::get_next_left_row+0x2b (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
38752d3 oceanbase::sql::ObBasicNestedLoopJoinOp::get_next_left_row+0x1a3 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
3879c31 oceanbase::sql::ObNestedLoopJoinOp::group_read_left_operate+0xb71 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
3876cf9 oceanbase::sql::ObNestedLoopJoinOp::read_left_operate+0x39 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
38782a2 oceanbase::sql::ObNestedLoopJoinOp::inner_get_next_row+0x262 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
56aa469 oceanbase::sql::ObOperator::get_next_row+0x189 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
4cee252 oceanbase::sql::ObExecuteResult::get_next_row+0x152 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
4ced5c7 oceanbase::sql::ObExecuteResult::get_next_row+0x47 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
64084ee oceanbase::sql::ObResultSet::get_next_row+0x13e (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
a3c0b01 oceanbase::observer::ObSyncPlanDriver::response_query_result+0x3d1 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
a3bf493 oceanbase::observer::ObSyncPlanDriver::response_result+0x4b3 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
a3f5d09 oceanbase::observer::ObMPQuery::process_single_stmt+0x3669 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
a3f135d oceanbase::observer::ObMPQuery::process+0x222d (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
b3d20bd oceanbase::rpc::frame::ObReqProcessor::run+0x3ed (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
a750cb0 oceanbase::omt::ObWorkerProcessor::process_one+0x240 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
a72e41a oceanbase::omt::ObWorkerProcessor::process+0x8ea (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
a74ea86 oceanbase::omt::ObThWorker::process_request+0x3e6 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
a72c33d oceanbase::omt::ObThWorker::worker+0x10cd (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
a72c80c oceanbase::omt::ObThWorker::run+0x3c (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
2af48ec oceanbase::lib::CoKThreadTemp<oceanbase::lib::CoUserThreadTemp<oceanbase::lib::CoSetSched> >::start()::{lambda()#1}::operator()+0x4c (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
2af477d std::_Function_handler<void (), oceanbase::lib::CoKThreadTemp<oceanbase::lib::CoUserThreadTemp<oceanbase::lib::CoSetSched> >::start()::{lambda()#1}>::_M_invoke+0x1d (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
1e4f14e std::function<void ()>::operator()+0x3e (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
af877b5 oceanbase::lib::CoSetSched::Worker::run+0x45 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
af86155 oceanbase::lib::CoRoutine::__start+0x1b5 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
af7eeaf finish+0x0 (/data/ob-advanced/bin/observer.another-commit-915609a0-25_20:01:00-debug)
ccccccccccccccf4 [unknown] ([unknown])
FlameGraph/stackcollapse-perf.pl perf.unfold &> perf.folded ,接着将perf.unfold中的符号进行折叠,组织成火焰图所需的统一格式FlameGraph/flamegraph.pl perf.folded > perf.svg,最后生成svg格式的火焰图以下是一个样例脚本供自动化生成OceanBase的Flame Graph,感谢复赛的lhcmaple队伍提供了这个脚本,我们稍作了一些改进。
#!/bin/bash
# no parameter is needed, just run
PID=$(ps -aux | grep /bin/observer | grep -v \"grep\" | awk '{print $2}')
if [ ${#PID} -eq 0 ]
then
echo "observer is not running"
exit -1
fi
perf record -F 99 -g -p $PID -- sleep 20
perf script -i perf.data &> perf.unfold
FlameGraph/stackcollapse-perf.pl perf.unfold &> perf.folded
FlameGraph/flamegraph.pl perf.folded > perf.svg
当然,尽管火焰图的可视化对于性能优化debug的确大有裨益,但原生的perf操作对数据的统计信息和操控粒度会更加丰富和深入,二者互相配合,才能相得益彰。
比如我们在步骤一统计完perf.data信息后,可以直接输入perf report查看命令行内的树状信息,如下图所示:

我们输入/group_read_left_operate快速搜索定位到我们所需要的函数:

接着按下a,即可展开具体的堆栈信息,且以汇编的形式陈列,这便可以帮助我们确定某些优化是否真正起到了作用,比如循环展开(Loop Unrolling),可能我们需要通过汇编才能真正确定其是否优化到了我们预期的效果。

工欲善其事,必先利其器,有了如上如此方便快捷的调试方法和性能分析利器火焰图,接下来我们便需要开始着手思考赛题了。
本次赛题是在开源 OceanBase 基础之上,针对 Nested Loop Join(NLJ) 场景做性能优化。
测试所使用的查询语句为select /*+ordered use_nl(A,B)*/ * from t1 A, t2 B where A.c1 >= ? and A.c1 < ? and A.c2 = B.c2 and A.c3 = B.c3;。
我们explain这条SQL语句,可得到如下结果:
可以看到,查询语句中包含两个join条件和一个对t1表的范围过滤,在过滤后,左表t1的数据量会显著小于右表t2。同时,t1.c2=t2.c2这一join条件会使用到t2.c2这一索引。
explain extended select /*+ordered use_nl(A,B)*/ * from t1 A, t2 B where A.c1 >= 56107 and A.c1 < 56307 and A.c2 = B.c2 and A.c3 = B.c3;
| ================================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
------------------------------------------------
|0 |NESTED-LOOP JOIN| |2718 |1306675|
|1 | TABLE SCAN |A |200 |78 |
|2 | TABLE SCAN |B(t2_i1)|10 |6530 |
================================================
----
2 - is_index_back=true
----
NLJ的基本原理是每次从左表获取一行,然后用这行数据和右表进行JOIN。
通常来说,最简单的想法就是直接把右表的全部数据扫描上来,再跟左表的这行数据进行JOIN的话,那么程序整体的复杂度就是:M(左表行数)*N(右表行数)。
但在大部分的实际场景,为了降低复杂度,右表获取数据可以选取其他两种方案:
由于本次大赛的题目中右表上存在索引,因此可以应用方式2。同时,我们也可以从上述EXPLAIN执行计划印证这一点:左表是t1,右表是t2,扫描右表走了索引t2_i1回表。
具体来说,该场景的具体实现可以分为3个部分:左表scan、rescan和右表scan回表。
左表scan:在这个场景中就是对t1表,根据主键范围进行扫描,并逐行返回,该模块涉及到的函数调用关系为:
ObNestedLoopJoinOp.read_left_operate->ObJoinOp.get_next_left_row->ObOperator.get_next_row->
ObTableScanOp.inner_get_next_row->ObTableScanIterator.get_next_row->ObTableScanRangeArrayRowIterator.get_next_row->
//往下为存储层
ObTableScanStoreRowIterator.get_next_row->ObMultipleMerge.get_next_row->ObMultipleScanMerge.inner_get_next_row->...
ObStoreRowIterator.get_next_row_ext->...
rescan:rescan发生在左表的上一行针对右表已经完成了JOIN的情况,这个时候OB并不会直接关闭右表的扫描,而是通过rescan重置右表的扫描状态,之后在左表扫描下一行时可以直接开始右表的扫描,而不用重新打开。具体来说,该模块涉及到的函数调用关系为:
ObNestedLoopJoinOp.read_left_func_going->
ObTableScanOp.rescan->ObTableScanOp.rt_rescan->ObTableScanOp.rescan_after_adding_query_range->
//往下为存储层
ObTableScanIterIterator.rescan->ObTableScanStoreRowIterator.rescan->
...
右表scan回表:在这个场景中就是先通过B.c2列查询索引t2_i1,获取到rowkey后再查询t2的过程,该模块涉及到的函数调用关系为:
ObNestedLoopJoinOp.read_right_operate->ObJoinOp.get_next_right_row->ObOperator.get_next_row->
ObTableScanOp.inner_get_next_row->ObTableScanIterator.get_next_row->ObTableScanRangeArrayRowIterator.get_next_row->
//往下为存储层
ObTableScanStoreRowIterator.get_next_row->ObIndexMerge.get_next_row->
...
OB的存储是基于LSM-Tree实现的,具体内容可以参考OB开源官网文档 LSM Tree 架构,以及同一个章节下的其它内容。
存储层查询是在多个memtable、sstable上迭代、归并的过程,按单行返回给上层。
OB目前的实现是为每一个memtable/sstable对应分配一个iterator,多个memtable/sstable对应的iterators维护在ObMultipleMerge类中,由Merge类完成从每个iterator获取1行数据,然后再进行归并的任务。
同时,OB的sstable内部是按照宏块-微块-行三层存储粒度组成,在iterator内部会先根据查询range定位到微块,然后通过微块对应的ObMicroBlockRowScanner打开读行。
总结来说,NLJ查询涉及的到存储层结构可以分为3层,multiple merge层,store row iter层,micro scanner/getter层,其关联关系如下所示:
multiple merge
-- iters 维护多个memtable/sstable的iterator,查询可能涉及到多个memtable/sstable做归并
store row iter
-- 单个memtable/sstable对应的迭代器
micro scanner/getter
-- 微块迭代器
所以理论上对于同一个查询中,同一个memtable/sstable只需要1个iterator就可以了。尽管对NLJ rescan场景需要对右表进行多次遍历,但在理想情况下还是可以只用这1个iterator完成多次遍历,不过OB目前的版本并没有实现这一点。
同时,在sstable iterator内部实际读数据时,是通过预取(prefetch)的方式把IO和解析读到的行数据串联起来,从而避免不必要的IO,CPU也可以流水线执行,极大提升了效率。
而针对目前这个NLJ场景,从执行计划可以看出这个查询会对两张表进行table scan,对t1表使用普通的迭代,对t2表使用索引回表(OB的索引回表实现在ObIndexMerge中)。
其落实到查询层的调用链路和执行过程列举如下:
1. 创建iterator并开始第1次预取
ObTableScanStoreRowIterator.open_iter->ObMultipleScan(get)Merge.construct_iters->ObSSTable(ObMemtable).scan(get)-> //分配iterator
ObISSTableRowIterator.init->ObSSTableRowIterator.inner_open-> // 初始化iterator
ObSSTableRowIterator.prefetch //预取数据
2.1. 持续预取并读数据(非回表)
get_next_row->..
2.2. 回表读取数据
ObIndexMerge.get_next_row->ObMultipleMerge.get_next_row->ObMultipleScanMerge.inner_get_next_row->...
->ObMultipleMerge.get_next_row->ObMultipleGetMerge.inner_get_next_row->...

不难从OB的NLJ实现中和火焰图的占比上看出rescan和右表scan回表对性能影响比较大,我们以rescan为例分析当前的实现和可以改进的方向。
首先针对rescan,它的作用是使右表多次的扫描可以尽量复用对象,在如下代码中可以看到rescan释放和保留(重置)了哪些对象:



同时在scan函数中也会调用init进而通过inner_open函数进行实际的iter分配,最后设置row_iter的值。

所以对于多次扫描实际使用的都是sstable iter对象,这里直接的改进方向是,rescan中不要析构掉和清理iters数组,然后保持iters在整个查询(多次rescan)一直有效。
为了实现这个目标,我们的大致思路如下:
首先我们需要内存保持有效,在最开始分配iter的地方使用适当的资源分配器(allocator)保证整个查询期间内存都不会被释放;接着当遇到析构的时候不再直接释放变量,而是调用iter的reuse接口,但同时也需要保留某些必须的清理动作,最大化复用迭代器,以实现性能的提升。
以上即是本文的全部内容了,希望能为各位同仁提供一些帮助和思路上的启发,如有疑问也可联系我们进行交流。
💡 作者:华东师范大学 数据科学与工程学院 DBHammer项目组 东亚男儿团队
本文主体面向对OceanBase数据库源码以及系统性能优化感兴趣的初学者供以技术交流,笔者来自华东师范大学数据科学与工程学院DBHammer项目组。
在OceanBase数据库大赛的复赛阶段,我们需要对OceanBase 3.1版本的Nested Loop Join(下称NLJ)这一功能进行优化。因此,在这里我们以NLJ为例,简要介绍我们对OB从查询树的生成到查询执行的一些认识。文章主要从OB的基础代码组织逻辑、查询树的生成和查询执行三个方面进行介绍。
在刚开始研读OB的代码时,OB简洁的代码风格让人印象深刻。一般而言,OB的函数返回值不是函数的处理结果,而是函数的执行状态。与之相对的,函数的处理结果会通过修改引用参数的方式向上传递。因此,通过分析每个函数的返回值,可以简单得知这个函数的执行流程。所以,在OB的代码中经常能看到类似的代码:
if (OB_FAIL(func_1)){
logging
} else (OB_FAIL(func_2)){
logging
} else (OB_SUCC(func_3)){
logging
}
这样的代码实际上相当于顺序执行func_1、func_2、func_3,并在函数执行失败/成功的时候输出日志信息,不继续执行剩下的函数。为了后续叙述的简洁,对于类似的代码,我们将省略函数执行失败的异常处理。
在生成物理的查询树之前,OB会先生成逻辑的查询计划,然后根据这个逻辑的查询计划确定实际执行的物理计划。在NLJ中,最重要的就是join算子的生成,所以我们以join算子的生成为例介绍这一部分。
join算子的生成主要依赖于ObStaticEngineCG::generate_join_spec(ObLogJoin& op, ObJoinSpec& spec)函数。这一函数接收两个引用参数op和spec,op是join对应的逻辑计划,当有多个join条件时,op详细记录第一个join条件,并将其他的join条件存储在other_join_conditions里;spec是生成的物理执行计划。generate_join_spec()分为两个部分。在第一部分,它会处理首要的join条件,判断逻辑计划中首要的join条件的join类型,根据这个类型选择生成对应的物理计划。在第二部分,它遍历other_join_conditions,逐个调用接口函数生成物理执行计划。通过这种方式,OB能够自然地实现对逻辑计划的递归展开,从而构建出对应的物理执行树。我们对这一函数进行了整理和提取,抽象出其中的逻辑如下:
int ObStaticEngineCG::generate_join_spec(ObLogJoin& op, ObJoinSpec& spec)
{
int ret = OB_SUCCESS;
bool is_late_mat = (phy_plan_->get_is_late_materialized() || op.is_late_mat());
phy_plan_->set_is_late_materialized(is_late_mat);
spec.join_type_ = op.get_join_type();
if (MERGE_JOIN == op.get_join_algo()) {
// 查询计划是merge join时的处理,暂略
} else if (NESTED_LOOP_JOIN == op.get_join_algo()) { // nested loop join
if (0 != op.get_equal_join_conditions().count()) {
} else {
ObBasicNestedLoopJoinSpec& nlj_spec = static_cast<ObBasicNestedLoopJoinSpec&>(spec);
nlj_spec.enable_gi_partition_pruning_ = op.is_enable_gi_partition_pruning();
const ObIArray<std::pair<int64_t, ObRawExpr*>>& nl_params = op.get_nl_params();
if (nlj_spec.enable_gi_partition_pruning_ &&
OB_FAIL(do_gi_partition_pruning(op, nlj_spec))) { // 如果可以的话,进行分区裁剪
} else if (OB_FAIL(nlj_spec.init_param_count(nl_params.count()))) { // 初始化join的参数数量
} else {
ObIArray<ObRawExpr*>& exec_param_exprs = op.get_stmt()->get_exec_param_ref_exprs();
ARRAY_FOREACH(nl_params, i)// 遍历每个参数
{
// 根据参数生成物理执行算子,具体内容略
}
if (OB_SUCC(ret) && PHY_NESTED_LOOP_JOIN == spec.type_) {
// 判断能否使用batch nlj,如果可以就更新flag
bool use_batch_nlj = false;
ObNestedLoopJoinSpec& nlj = static_cast<ObNestedLoopJoinSpec&>(spec);
if (OB_FAIL(op.can_use_batch_nlj(use_batch_nlj))) {// 判断能不能用,当join条件有复数字段组成时不使用batch
} else if (use_batch_nlj) {
nlj.use_group_ = use_batch_nlj;
if (OB_ISNULL(nlj.get_right()) || PHY_TABLE_SCAN != nlj.get_right()->type_) { // 只有table scan的时候才会选择batch nlj
} else {
const ObTableScanSpec* right_tsc = static_cast<const ObTableScanSpec*>(nlj.get_right());
const_cast<ObTableScanSpec*>(right_tsc)->batch_scan_flag_ = true;
}
}
}
}
}
} else if (HASH_JOIN == op.get_join_algo()) {
// 查询计划是hash join时的处理,暂略
}
const common::ObIArray<std::pair<int64_t, ObRawExpr*>>& exec_params = op.get_exec_params();
// 2. add other join conditions
const ObIArray<ObRawExpr*>& other_join_conds = op.get_other_join_conditions();
// 初始化other condition
OZ(spec.other_join_conds_.init(other_join_conds.count()));
ARRAY_FOREACH(other_join_conds, i) // 遍历剩下的条件
{
// 逐个生成剩下条件的物理算子
} // end for
return ret;
}
generate_join_spec() 对于NLJ最为特别的处理就是判断能否使用batch NLJ,这也是我们所关注的第一个优化点。通过debug我们知道,由于比赛中的查询语句的join条件含有两个字段,这个函数不会选择使用batch NLJ。因此,我们通过扩展OB对batch NLJ的支持和修改判定条件,使得generate_join_spec()能将比赛中的查询转化为batch NLJ的物理执行计划。
当逻辑执行计划里的NLJ经过generate_join_spec转换为物理的执行算子之后,OB就会通过火山模型逐层执行物理算子,并一次向上层算子呈递一行数据。对于NLJ而言,这部分功能主要由ObNestedLoopJoinOp::inner_get_next_row()实现。这一函数并不需要返回值,是因为它会直接通过修改上下文信息的方式保存一次调用所获取的数据。
作为一个join算子,ObNestedLoopJoinOp很自然地含有两个子节点。对于比赛中的查询语句,我们可以很容易知道这两个子节点都是table scan算子。且不论左右节点的区别,每次调用join算子时需要从左右节点分别获取一行数据并连接。但在执行过程中可能出现很多种情况,比如右节点获取的数据与左节点获取的数据不匹配,遍历完右节点都无法与左节点当前的数据匹配等等。因此,为了更好地实现这部分逻辑,OB在函数中实现了一个小型的状态机,根据当前状态选择后续需要执行的函数。示意图如下:
start=>start: 开始inner_get_next_row
end=>end: 返回结果
left_op=>operation: read_left_operate()
left_going=>operation: read_left_func_going()
left_end=>operation: read_left_func_end()
right_op=>operation: read_right_operate()
right_going=>operation: read_right_func_going()
right_end=>operation: read_right_func_end()
iter_end1=>condition: 左节点存在下一行数据
iter_end2=>condition: 右节点存在下一行数据
output_product=>condition: 连接成功
start->left_op->iter_end1
iter_end1(yes)->left_going->right_op->iter_end2
iter_end1(no)->left_end->end
iter_end2(yes)->right_going->output_product
iter_end2(no)->right_end->output_product
output_product(yes)->end
output_product(no)->left_op
其代码抽象如下:
int ObNestedLoopJoinOp::inner_get_next_row()
{
int ret = OB_SUCCESS;
if (OB_UNLIKELY(LEFT_SEMI_JOIN == MY_SPEC.join_type_ || LEFT_ANTI_JOIN == MY_SPEC.join_type_)) {
// 处理半连接,具体内容略
} else {
state_operation_func_type state_operation = NULL;
state_function_func_type state_function = NULL;
int func = -1;
output_row_produced_ = false;
while (OB_SUCC(ret) && !output_row_produced_) {
state_operation = this->ObNestedLoopJoinOp::state_operation_func_[state_];// state_取值为left right,表示当前执行左节点还是右节点的任务
if (OB_ITER_END == (ret = (this->*state_operation)())) {
func = FT_ITER_END;// func的取值为end、going,对应取流程图中end或是going后缀的函数
ret = OB_SUCCESS;
} else if (OB_FAIL(ret)) {
} else {
func = FT_ITER_GOING;
}
if (OB_SUCC(ret)) {
state_function = this->ObNestedLoopJoinOp::state_function_func_[state_][func];
if (OB_FAIL((this->*state_function)()) && OB_ITER_END != ret) {
}
}
} // while end
}
return ret;
}
其中一共包含6个函数,分别是左右节点的operate(),func_going(),func_end()函数。其中,operate()函数负责从对应子节点获取一行数据;func_going()函数负责判断是否需要切换为另一个节点的函数,并做预处理,如left_func_going()会根据左节点获取的数据,准备扫描右节点所需要的参数(值得一提的是,因为实际上这个函数会深入底层更新右节点的迭代器,所以开销是很大的),right_func_going()负责判断是否连接成功;func_end()判断是否得到了需要的结果,并修改inner_get_next_row()的返回值。
在这6个函数中,func_going()和func_end()的函数逻辑都比较简单,我们不再在这里赘述。以下主要介绍左右节点的operate()函数,这也是我们优化batch_nlj所主要关心的函数。
根据在构建物理执行树时的判断,left_operate()存在两种执行逻辑,分别对应不使用batch NLJ和使用batch NLJ。当不使用batch nlj时,函数直接调用对应算子的next_row()方法。这一方法会不断向下获取一行数据,具体的调用栈如下:
oceanbase::storage::ObmultipleScanMergeImpI::supply_consume()
oceanbase::storage::ObmultipleScanMergeImpI::inner_get_next_row()
oceanbase::storage::ObmultipleScanMerge::inner_get_next_row()
oceanbase::storage::ObmultipleMerge::get_next_row()
oceanbase::storage::ObTableScanStoreRowIterator::get_next_row()
oceanbase::storage::ObTableScanRangeArrayRowIterator::get_next_row()
oceanbase::storage::ObTableScanIterator::get_next_row()
oceanbase::sql::ObTableScanOp::get_next_row_with_mode()
oceanbase::sql::ObTableScanOp::inner_get_next_row()
oceanbase::sql::ObOperator::get_next_row()
oceanbase::sql::ObJoinOp::get_next_left_row()
oceanbase::sql::ObBasicNestedLoopJoin::get_next_left_row()
通过检查代码,我们发现每层调用的逻辑都非常清晰,主要是调用下层接口以及异常处理,因此不再赘述。我们主要讨论使用batch NLJ时的执行逻辑。
当使用batch NLJ时,join算子会先取出左节点中的一批数据,然后再逐个与右节点进行匹配。这种方式可以更好地利用数据的局部性,提高对磁盘数据的访问效率。为了实现批量获取数据,同时又不改变程序其他部分的逻辑,使用batch NLJ时需要在第一次调用时连续调用多次算子的next_row()方法并存储对应的数据,在后续调用时直接从存储的数据中导出对应数据。其代码抽象如下:
int ObNestedLoopJoinOp::group_read_left_operate()
{
int ret = OB_SUCCESS;
ObTableScanOp* right_tsc = reinterpret_cast<ObTableScanOp*>(right_);
if (left_store_iter_.is_valid() && left_store_iter_.has_next()) {
// 当当前还有存储数据时,直接获取存储数据,具体内容略
} else { // 没有存储数据时,批量获取数据
if (OB_FAIL(right_tsc->group_rescan_init(MY_SPEC.batch_size_))) {
} else if (is_left_end_) { // 判断左节点是否已经取完
} else {
if (OB_ISNULL(mem_context_)) {
// 初始化存储数据的结构,具体内容略
}
bool ignore_end = false;
if (OB_SUCC(ret)) {
//初始化或重置访问数据和它的迭代器left_store_和left_store_iter_,具体内容略
}
save_last_row_ = false;
while (OB_SUCC(ret) && !is_full()) {
// 批量获取数据
clear_evaluated_flag(); // 清理访问参数
if (OB_FAIL(get_next_left_row())) { // 获取下一行
if (OB_ITER_END != ret) {
} else {
is_left_end_ = true;
}
} else if (OB_FAIL(left_store_.add_row(left_->get_spec().output_, &eval_ctx_))) {// 存储到存储到对应的结构left_store_中
} else if (OB_FAIL(prepare_rescan_params(true /*is_group*/))) {
} else if (OB_FAIL(deep_copy_dynamic_obj())) {
} else if (OB_FAIL(right_tsc->group_add_query_range())) { // 存储对应的右节点访问参数
} else {
ignore_end = true;
}
}
}
if (OB_SUCC(ret) || (ignore_end && OB_ITER_END == ret)) {
// 更新迭代器left_store_iter_,并更新右节点的访问参数,具体内容略
}
}
}
if (OB_SUCC(ret)) { // 从存储结构left_store_里拿一行数据,作为本次调用的返回结果
if (OB_FAIL(left_store_iter_.get_next_row(left_->get_spec().output_, eval_ctx_))) {
} else {
left_row_joined_ = false;
}
}
return ret;
}
可以注意到,在执行过程中,group_read_left_operate()会存储每一行数据对应的右节点访问参数。更具体来说,是这一行数据对应第一个join条件字段的值。因此,如果存在多于一个join字段,剩余的字段值不会被存储,这导致使用batch NLJ时无法正确根据join条件过滤结果,这也是OB原本只限制在join条件唯一时使用batch NLJ的原因。为了使用batch NLJ,我们对存储结构进行扩展,使它能存储剩余条件字段的值,从而保证对于比赛的查询语句也能有效且正确使用batch NLJ。
与左节点稍有不同,右节点并非是一个普通的table scan,而是一个带有index的scan。因此,在这个算子扫描结果的时候,会首先在索引表中进行搜索和定位,然后基于定位的结果直接构造数据表中的迭代器,从而加速数据的获取。这一流程在代码上的体现便是这个算子同时维护了两张表的迭代器,分别是数据表的main_iter_和索引表的index_iter_。其代码抽象如下:
int ObIndexMerge::get_next_row(ObStoreRow*& row)
{
int ret = OB_SUCCESS;
if (OB_UNLIKELY(NULL == index_iter_) || OB_UNLIKELY(NULL == access_ctx_)) { // 没有初始化
} else if (access_ctx_->is_end()) { // 已经扫描完了
} else {
while (OB_SUCC(ret)) {
if (NULL != main_iter_ && OB_SUCC(main_iter_->get_next_row(row))) { // 数据表获取到一行数据,直接结束
break;
} else {
if (OB_ITER_END == ret) {
if (!access_ctx_->is_end()) {
ret = OB_SUCCESS;
}
main_iter_ = NULL;
}
if (OB_SUCC(ret)) {
// batch get main table rowkeys from index table
// 初始化键值参数等结构,具体内容略
for (int64_t i = 0; OB_SUCC(ret) && i < MAX_NUM_PER_BATCH; ++i) {
if (OB_FAIL(index_iter_->get_next_row(index_row))) { // 索引表获取数据
if (OB_ARRAY_BINDING_SWITCH_ITERATOR == ret) {
++index_range_array_cursor_;
if (OB_FAIL(index_iter_->switch_iterator(index_range_array_cursor_))) {
}
} else if (OB_ITER_END != ret) {
}
} else { // 存储获取的数据
src_key.assign(index_row->row_val_.cells_, rowkey_cnt_);
if (OB_FAIL(src_key.deep_copy(dest_key.get_store_rowkey(), rowkey_allocator_))) {
} else {
dest_key.set_range_array_idx(index_row->range_array_idx_);
if (OB_FAIL(rowkeys_.push_back(dest_key))) {
}
}
}
}
if (OB_SUCC(ret)) {
if (OB_FAIL(table_iter_.open(rowkeys_))) { // 根据索引表的数据初始化数据表的迭代器
} else {
main_iter_ = &table_iter_;
}
}
}
}
}
}
return ret;
}
或许很多人会像我们一样,在一开始看到源码的时候不知所措。毕竟,OB作为一个非常庞大的项目,很难快速找到入手的部分。根据我们的经验,在这种情况下,一般可以先使用perf来快速找到最耗时的执行部分,这部分代码往往与核心逻辑紧密相关,如我们上述展示的部分代码。在此基础上,结合gdb debug,可以获取到执行过程中的主要调用栈。往往我们会发现调用栈很深,但是其中大部分的代码都不会涉及到核心逻辑,不需要逐个深入地研究。因此,再通过OB的代码自注释和基本的数据库知识可以有效地定位到所想要修改的部分。
当定位到要修改的部分之后,如何具体地分析源码的逻辑呢?首先需要熟悉OB的代码风格,能将连续的条件嵌套重新翻译为顺序执行。然后通过代码的自注释去感受实现的功能,由于OB中大部分的类都带有很多层的嵌套,在一开始不断深挖某个细节很容易晕头转向,采用“不求甚解”的态度,先宏观上把握整个执行流程,对于代码的分析会更有利。在了解了整体的流程之后,再结合逐步调试去深入理解其中的执行流程。
希望上述的方法分享,能够帮助大家分析OB代码,为大家在开源社区中的贡献添砖加瓦。
]]>在TPC-C中,NewOrder事务负责下订单任务,它会在Stock表中更新5-15个items的库存。NewOrder事务会有1%的概率更新远程的仓库的Stock信息,因此会产生1%的分布式事务。NewOrder的事务模板如下所示。
TX[NewOrder]
SELECT C_DISCOUNT, C_LAST, C_CREDIT FROM CUSTOMER WHERE C_W_ID = ? AND C_D_ID = ? AND C_ID = ?;
SELECT W_TAX FROM WAREHOUSE WHERE W_ID = ?;
SELECT D_NEXT_O_ID, D_TAX FROM DISTRICT WHERE D_W_ID = ? AND D_ID = ? FOR UPDATE;
UPDATE DISTRICT SET D_NEXT_O_ID = D_NEXT_O_ID + 1 WHERE D_W_ID = ? AND D_ID = ?;
INSERT INTO OORDER (O_ID, O_D_ID, O_W_ID, O_C_ID, O_ENTRY_D, O_OL_CNT, O_ALL_LOCAL) VALUES (?, ?, ?, ?, ?, ? , ?);
INSERT INTO NEW_ORDER (NO_O_ID, NO_D_ID, NO_W_ID) VALUES ( ?, ?, ?);
Multiple
SELECT I_PRICE, I_NAME, I_DATA FROM ITEM WHERE I_ID = ?; C_DISCOUNT
SELECT S_QUANTITY, S_DATA, S_DIST_01, S_DIST_02, S_DIST_03, S_DIST_04, S_DIST_05, S_DIST_06, S_DIST_07, S_DIST_08, S_DIST_09, S_DIST_10 FROM STOCK WHERE S_I_ID = ? AND S_W_ID = ? FOR UPDATE;
//此处有1%的可能s_w_id != w_id,从而产生分布式事务
UPDATE STOCK SET S_QUANTITY = ?, S_YTD = S_YTD + ?, S_ORDER_CNT = S_ORDER_CNT + 1, S_REMOTE_CNT = S_REMOTE_CNT + ? WHERE S_I_ID = ? AND S_W_ID = ?;
INSERT INTO ORDER_LINE (OL_O_ID, OL_D_ID, OL_W_ID, OL_NUMBER, OL_I_ID, OL_SUPPLY_W_ID, OL_QUANTITY, OL_AMOUNT, OL_DIST_INFO) VALUES (?,?,?, ?,?,?,?,?,?);
EndMultiple
EndTX
为了分析OceanBase对分布式事务的支持能力,我们将分布式事务比例进行参数化,将它分别设为1%,10%,20%,40%,80%和100%。关于BenchmarkSQL的代码修改见实验配置。
为测试分布式事务的影响,本次实验将OceanBase部署在10台机器上。其中9台机器的配置为:8核CPU,32G内存,上面各部署了一个OBServer;其中1台机器的配置为:16核CPU, 16G内存,上面部署了一个OBProxy,同时也作为实验的客户端。
## Only need to configure when remote login is required
user:
username: xxx
password: xxx
#key_file: .ssh/authorized_keys
oceanbase-ce:
servers:
- name: host1
ip: 10.24.14.8
- name: host2
ip: 10.24.14.136
- name: host3
ip: 10.24.14.75
- name: host4
ip: 10.24.14.178
- name: host5
ip: 10.24.14.60
- name: host6
ip: 10.24.14.120
- name: host7
ip: 10.24.14.126
- name: host8
ip: 10.24.14.171
- name: host9
ip: 10.24.14.181
global:
devname: eth0
cluster_id: 1
memory_limit: 28G
system_memory: 8G
stack_size: 512K
cpu_count: 16
cache_wash_threshold: 1G
__min_full_resource_pool_memory: 268435456
workers_per_cpu_quota: 10
schema_history_expire_time: 1d
net_thread_count: 4
major_freeze_duty_time: Disable
minor_freeze_times: 10
enable_separate_sys_clog: 0
enable_merge_by_turn: FALSE
datafile_disk_percentage: 35
syslog_level: WARN
enable_syslog_recycle: true
max_syslog_file_count: 4
appname: ob209
host1:
mysql_port: 2883
rpc_port: 2882
home_path: /data/obdata
zone: zone0
host2:
mysql_port: 2883
rpc_port: 2882
home_path: /data/obdata
zone: zone0
host3:
mysql_port: 2883
rpc_port: 2882
home_path: /data/obdata
zone: zone0
host4:
mysql_port: 2883
rpc_port: 2882
home_path: /data/obdata
zone: zone1
host5:
mysql_port: 2883
rpc_port: 2882
home_path: /data/obdata
zone: zone1
host6:
mysql_port: 2883
rpc_port: 2882
home_path: /data1/obdata
zone: zone1
host7:
mysql_port: 2883
rpc_port: 2882
home_path: /data1/obdata
zone: zone2
host8:
mysql_port: 2883
rpc_port: 2882
home_path: /data1/obdata
zone: zone2
host9:
mysql_port: 2883
rpc_port: 2882
home_path: /data1/obdata
zone: zone2
obproxy:
servers:
- 10.24.14.215
global:
listen_port: 2883
home_path: /data/obproxy
rs_list: 10.24.14.8:2883;10.24.14.136:2883;10.24.14.75:2883;10.24.14.178:2883;10.24.14.60:2883;10.24.14.120:2883;10.24.14.126:2883;10.24.14.171:2883;10.24.14.181:2883
enable_cluster_checkout: false
cluster_name: ob209
https://sourceforge.net/projects/benchmarksql/files/latest/download
修改benchmark-5.0/src/client/jTPCC.java 文件,增加分布式事务比例的参数化
private double tpmC;
private jTPCCRandom rnd;
private OSCollector osCollector = null;
//声明neworder事务中的分布式事务比例
private static double newOrderDistributedRate;
String iWarehouses = getProp(ini,"warehouses");
String iTerminals = getProp(ini,"terminals");
//获取配置文件参数
newOrderDistributedRate = Double.parseDouble(getProp(ini, "newOrderDistributedRate"));
String iRunTxnsPerTerminal = ini.getProperty("runTxnsPerTerminal");
String iRunMins = ini.getProperty("runMins");
private String getFileNameSuffix()
{
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
return dateFormat.format(new java.util.Date());
}
//创建外部类的访问接口
public static double getNewOrderDistributedRate() {
return newOrderDistributedRate;
}
修改benchmark-5.0/src/client/jTPCCTData.java 文件,修改NewOrder的分布式事务比例
while (i < o_ol_cnt) // 2.4.1.5
{
newOrder.ol_i_id[i] = rnd.getItemID();
//更改分布式事务比例
if (rnd.nextInt(1, 100) <= 100-jTPCC.getNewOrderDistributedRate()*100)
newOrder.ol_supply_w_id[i] = terminalWarehouse;
else
newOrder.ol_supply_w_id[i] = rnd.nextInt(1, numWarehouses);
newOrder.ol_quantity[i] = rnd.nextInt(1, 10);
修改benchmark-5.0/run/probs.ob文件
db=oracle
driver=com.alipay.oceanbase.obproxy.mysql.jdbc.Driver
conn=jdbc:oceanbase://10.24.14.188:2883/tpcc_100?useUnicode=true&characterEncoding=utf-8
user=tpcc@test
password=
warehouses=100
loadWorkers=30
terminals=100
//To run specified transactions per terminal- runMins must equal zero
runTxnsPerTerminal=0
//To run for specified minutes- runTxnsPerTerminal must equal zero
runMins=5
//Number of total transactions per minute
limitTxnsPerMin=0
//将生成的数据放在该目录
fileLocation=/data/ob/tpcc_100/
//Distributed Transaction Ratio For NewOrder Transaction
newOrderDistributedRate=0.01
//Set to true to run in 4.x compatible mode. Set to false to use the
//entire configured database evenly.
terminalWarehouseFixed=false
//The following five values must add up to 100
newOrderWeight=100
paymentWeight=0
orderStatusWeight=0
deliveryWeight=0
stockLevelWeight=0
// Directory name to create for collecting detailed result data.
// Comment this out to suppress.
resultDirectory=my_result_%tY-%tm-%td_%tH%tM%tS
osCollectorScript=./misc/os_collector_linux.py
osCollectorInterval=1
修改benchmark-5.0/run/sql.oceanbase/tableCreates.sql 文件
create table bmsql_config (
cfg_name varchar(30) primary key,
cfg_value varchar(50)
);
create tablegroup tpcc_group binding true partition by hash partitions 128;
create table bmsql_warehouse (
w_id integer not null,
w_ytd decimal(12,2),
w_tax decimal(4,4),
w_name varchar(10),
w_street_1 varchar(20),
w_street_2 varchar(20),
w_city varchar(20),
w_state char(2),
w_zip char(9),
primary key(w_id)
)tablegroup='tpcc_group' partition by hash(w_id) partitions 128;
create table bmsql_district (
d_w_id integer not null,
d_id integer not null,
d_ytd decimal(12,2),
d_tax decimal(4,4),
d_next_o_id integer,
d_name varchar(10),
d_street_1 varchar(20),
d_street_2 varchar(20),
d_city varchar(20),
d_state char(2),
d_zip char(9),
PRIMARY KEY (d_w_id, d_id)
)tablegroup='tpcc_group' partition by hash(d_w_id) partitions 128;
create table bmsql_customer (
c_w_id integer not null,
c_d_id integer not null,
c_id integer not null,
c_discount decimal(4,4),
c_credit char(2),
c_last varchar(16),
c_first varchar(16),
c_credit_lim decimal(12,2),
c_balance decimal(12,2),
c_ytd_payment decimal(12,2),
c_payment_cnt integer,
c_delivery_cnt integer,
c_street_1 varchar(20),
c_street_2 varchar(20),
c_city varchar(20),
c_state char(2),
c_zip char(9),
c_phone char(16),
c_since timestamp,
c_middle char(2),
c_data varchar(500),
PRIMARY KEY (c_w_id, c_d_id, c_id)
)tablegroup='tpcc_group' partition by hash(c_w_id) partitions 128;
create table bmsql_history (
hist_id integer,
h_c_id integer,
h_c_d_id integer,
h_c_w_id integer,
h_d_id integer,
h_w_id integer,
h_date timestamp,
h_amount decimal(6,2),
h_data varchar(24)
)tablegroup='tpcc_group' partition by hash(h_w_id) partitions 128;
create table bmsql_new_order (
no_w_id integer not null ,
no_d_id integer not null,
no_o_id integer not null,
PRIMARY KEY (no_w_id, no_d_id, no_o_id)
)tablegroup='tpcc_group' partition by hash(no_w_id) partitions 128;
create table bmsql_oorder (
o_w_id integer not null,
o_d_id integer not null,
o_id integer not null,
o_c_id integer,
o_carrier_id integer,
o_ol_cnt integer,
o_all_local integer,
o_entry_d timestamp,
PRIMARY KEY (o_w_id, o_d_id, o_id)
)tablegroup='tpcc_group' partition by hash(o_w_id) partitions 128;
create table bmsql_order_line (
ol_w_id integer not null,
ol_d_id integer not null,
ol_o_id integer not null,
ol_number integer not null,
ol_i_id integer not null,
ol_delivery_d timestamp,
ol_amount decimal(6,2),
ol_supply_w_id integer,
ol_quantity integer,
ol_dist_info char(24),
PRIMARY KEY (ol_w_id, ol_d_id, ol_o_id, ol_number)
)tablegroup='tpcc_group' partition by hash(ol_w_id) partitions 128;
create table bmsql_item (
i_id integer not null,
i_name varchar(24),
i_price decimal(5,2),
i_data varchar(50),
i_im_id integer,
PRIMARY KEY (i_id)
) locality='F,R{all_server}@zone0, F,R{all_server}@zone1, F,R{all_server}@zone2' duplicate_scope='cluster';
-- );
create table bmsql_stock (
s_w_id integer not null,
s_i_id integer not null,
s_quantity integer,
s_ytd integer,
s_order_cnt integer,
s_remote_cnt integer,
s_data varchar(50),
s_dist_01 char(24),
s_dist_02 char(24),
s_dist_03 char(24),
s_dist_04 char(24),
s_dist_05 char(24),
s_dist_06 char(24),
s_dist_07 char(24),
s_dist_08 char(24),
s_dist_09 char(24),
s_dist_10 char(24),
PRIMARY KEY (s_w_id, s_i_id)
)tablegroup='tpcc_group' partition by hash(s_w_id) partitions 128;
cd benchmark-5.0/run
./runDatabaseBuild.sh props.ob
obclient -h10.24.14.245 -P2883 -uroot@test -c -D tpcc_100 -e "load data /*+ parallel(80) */ infile '/data/ob/tpcc_100/warehouse.csv' into table bmsql_warehouse fields terminated by ',';"
obclient -h10.24.14.245 -P2883 -uroot@test -c -D tpcc_100 -e "load data /*+ parallel(80) */ infile '/data/ob/tpcc_100/district.csv' into table bmsql_district fields terminated by ',';"
obclient -h10.24.14.245 -P2883 -uroot@test -c -D tpcc_100 -e "load data /*+ parallel(80) */ infile '/data/ob/tpcc_100/config.csv' into table bmsql_config fields terminated by ',';"
obclient -h10.24.14.245 -P2883 -uroot@test -c -D tpcc_100 -e "load data /*+ parallel(80) */ infile '/data/ob/tpcc_100/item.csv' into table bmsql_item fields terminated by ',';"
obclient -h10.24.14.245 -P2883 -uroot@test -c -D tpcc_100 -e "load data /*+ parallel(80) */ infile '/data/ob/tpcc_100/order.csv' into table bmsql_oorder fields terminated by ',';"
obclient -h10.24.14.245 -P2883 -uroot@test -c -D tpcc_100 -e "load data /*+ parallel(80) */ infile '/data/ob/tpcc_100/stock.csv' into table bmsql_stock fields terminated by ',';"
obclient -h10.24.14.245 -P2883 -uroot@test -c -D tpcc_100 -e "load data /*+ parallel(80) */ infile '/data/ob/tpcc_100/cust-hist.csv' into table bmsql_history fields terminated by ',';"
obclient -h10.24.14.245 -P2883 -uroot@test -c -D tpcc_100 -e "load data /*+ parallel(80) */ infile '/data/ob/tpcc_100/new-order.csv' into table bmsql_new_order fields terminated by ',';"
obclient -h10.24.14.245 -P2883 -uroot@test -c -D tpcc_100 -e "load data /*+ parallel(80) */ infile '/data/ob/tpcc_100/order-line.csv' into table bmsql_order_line fields terminated by ',';"
obclient -h10.24.14.245 -P2883 -uroot@test -c -D tpcc_100 -e "load data /*+ parallel(80) */ infile '/data/ob/tpcc_100/customer.csv' into table bmsql_customer fields terminated by ',';"
实验中分别将newOrderDistributedRate设为0.01,0.1,0.2,0.4,0.6,0.8,1 分别代表不同的NewOrder事务的分布式事务比例
./runBenchmark.sh probs.ob

为了完成这个实验,需要修改benchmark-5.0/run/sql.oceanbase/tableCreates.sql 文件。
create table bmsql_config (
cfg_name varchar(30) primary key,
cfg_value varchar(50)
);
create table bmsql_warehouse (
w_id integer not null,
w_ytd decimal(12,2),
w_tax decimal(4,4),
w_name varchar(10),
w_street_1 varchar(20),
w_street_2 varchar(20),
w_city varchar(20),
w_state char(2),
w_zip char(9),
primary key(w_id)
)partition by hash(w_id) partitions 128;
create table bmsql_district (
d_w_id integer not null,
d_id integer not null,
d_ytd decimal(12,2),
d_tax decimal(4,4),
d_next_o_id integer,
d_name varchar(10),
d_street_1 varchar(20),
d_street_2 varchar(20),
d_city varchar(20),
d_state char(2),
d_zip char(9),
PRIMARY KEY (d_w_id, d_id)
)partition by hash(d_w_id) partitions 128;
create table bmsql_customer (
c_w_id integer not null,
c_d_id integer not null,
c_id integer not null,
c_discount decimal(4,4),
c_credit char(2),
c_last varchar(16),
c_first varchar(16),
c_credit_lim decimal(12,2),
c_balance decimal(12,2),
c_ytd_payment decimal(12,2),
c_payment_cnt integer,
c_delivery_cnt integer,
c_street_1 varchar(20),
c_street_2 varchar(20),
c_city varchar(20),
c_state char(2),
c_zip char(9),
c_phone char(16),
c_since timestamp,
c_middle char(2),
c_data varchar(500),
PRIMARY KEY (c_w_id, c_d_id, c_id)
)partition by hash(c_w_id) partitions 128;
create table bmsql_history (
hist_id integer,
h_c_id integer,
h_c_d_id integer,
h_c_w_id integer,
h_d_id integer,
h_w_id integer,
h_date timestamp,
h_amount decimal(6,2),
h_data varchar(24)
)partition by hash(h_w_id) partitions 128;
create table bmsql_new_order (
no_w_id integer not null ,
no_d_id integer not null,
no_o_id integer not null,
PRIMARY KEY (no_w_id, no_d_id, no_o_id)
)partition by hash(no_w_id) partitions 128;
create table bmsql_oorder (
o_w_id integer not null,
o_d_id integer not null,
o_id integer not null,
o_c_id integer,
o_carrier_id integer,
o_ol_cnt integer,
o_all_local integer,
o_entry_d timestamp,
PRIMARY KEY (o_w_id, o_d_id, o_id)
)partition by hash(o_w_id) partitions 128;
create table bmsql_order_line (
ol_w_id integer not null,
ol_d_id integer not null,
ol_o_id integer not null,
ol_number integer not null,
ol_i_id integer not null,
ol_delivery_d timestamp,
ol_amount decimal(6,2),
ol_supply_w_id integer,
ol_quantity integer,
ol_dist_info char(24),
PRIMARY KEY (ol_w_id, ol_d_id, ol_o_id, ol_number)
)partition by hash(ol_w_id) partitions 128;
create table bmsql_item (
i_id integer not null,
i_name varchar(24),
i_price decimal(5,2),
i_data varchar(50),
i_im_id integer,
PRIMARY KEY (i_id)
) locality='F,R{all_server}@zone0, F,R{all_server}@zone1, F,R{all_server}@zone2' duplicate_scope='cluster';
-- );
create table bmsql_stock (
s_w_id integer not null,
s_i_id integer not null,
s_quantity integer,
s_ytd integer,
s_order_cnt integer,
s_remote_cnt integer,
s_data varchar(50),
s_dist_01 char(24),
s_dist_02 char(24),
s_dist_03 char(24),
s_dist_04 char(24),
s_dist_05 char(24),
s_dist_06 char(24),
s_dist_07 char(24),
s_dist_08 char(24),
s_dist_09 char(24),
s_dist_10 char(24),
PRIMARY KEY (s_w_id, s_i_id)
)partition by hash(s_w_id) partitions 128;
为了简化实验,这边将props.ob中的变量newOrderDistributedRate=0.01
加载数据&运行负载
实验展示与分析

Pavlo A, Curino C, Zdonik S. Skew-aware automatic database partitioning in shared-nothing, parallel OLTP systems[C]//Proceedings of the 2012 ACM SIGMOD International Conference on Management of Data. 2012: 61-72. ↩
L. Qu, Q. Wang, T. Chen, K. Li, R. Zhang, X. Zhou, Q. Xu, Z. Yang, C. Yang, W. Qian, and A. Zhou, “Are current benchmarks adequate to evaluate distributed transactional databases?” BenchCouncil Transactions on Benchmarks, Standards and Evaluations, vol. 2, no. 1,p. 100031, 2022. [Online]. Available: https://www.sciencedirect.com/science/article/pii/S2772485922000187 ↩
在OLAP场景中,多表连接是十分常见的,查询的执行效率跟它涉及的表的连接顺序息息相关。以A、B、C三张表为例,有一条查询:SELECT * FROM A, B, C WHERE …,那么这三张表的连接顺序可以是(A⋈B)⋈C、(A⋈B)⋈C、(A⋈C)⋈B等共6种连接顺序,我们将全部连接顺序称为搜索空间。不同的连接顺序是语义等价的,即能获得相同的结果集,但是对于查询效率有着非常大的影响。从搜索空间中选出性能最优的连接顺序是一个关键的DBMS优化问题,但是随着连接表数量的增加,搜索空间的大小呈指数级增长,这使得连接顺序选择成为一个NP-hard问题。本文主要评测OceanBase连接顺序选择策略的优劣,以分析OceanBase对多表连接查询的处理能力以及优化空间。
为了得到OceanBase执行某查询时选择的连接顺序,我们需要分析该查询的执行计划,具体方法如下。
OceanBase可以利用EXPLAIN关键字得到查询的执行计划。
obclient> explain select count(*) from table_0, table_6, table_5, table_3
where table_0.fk_0 = table_6.primaryKey and table_0.fk_1 = table_5.primaryKey
and table_0.fk_3 = table_3.primaryKey and table_0.col_0 > 10000
and table_5.col_6 > 10000 and table_6.col_3 > 10000 and table_3.col_2 > 10000;
| Query Plan
===================================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
---------------------------------------------------
|0 |SCALAR GROUP BY | |1 |15299|
|1 | HASH JOIN | |334 |15236|
|2 | PX COORDINATOR | |338 |11647|
|3 | EXCHANGE OUT DISTR|:EX10000|338 |11519|
|4 | HASH JOIN | |338 |11519|
|5 | HASH JOIN | |341 |6906 |
|6 | TABLE SCAN |table_3 |400 |3148 |
|7 | TABLE SCAN |table_0 |344 |2932 |
|8 | TABLE SCAN |table_6 |452 |3580 |
|9 | TABLE SCAN |table_5 |313 |2486 |
===================================================
Outputs & filters:
-------------------------------------
0 - output([T_FUN_COUNT(*)]), filter(nil),
group(nil), agg_func([T_FUN_COUNT(*)])
1 - output([1]), filter(nil),
equal_conds([table_0.fk_1 = table_5.primaryKey]), other_conds(nil)
2 - output([table_0.fk_1]), filter(nil)
3 - output([table_0.fk_1]), filter(nil), is_single, dop=1
4 - output([table_0.fk_1]), filter(nil),
equal_conds([table_0.fk_0 = table_6.primaryKey]), other_conds(nil)
5 - output([table_0.fk_1], [table_0.fk_0]), filter(nil),
equal_conds([table_0.fk_3 = table_3.primaryKey]), other_conds(nil)
6 - output([table_3.primaryKey]), filter([table_3.col_2 > 10000]),
access([table_3.primaryKey], [table_3.col_2]), partitions(p0)
7 - output([table_0.fk_0], [table_0.fk_1], [table_0.fk_3]), filter([table_0.col_0 > 10000]),
access([table_0.fk_0], [table_0.fk_1], [table_0.fk_3], [table_0.col_0]), partitions(p0)
8 - output([table_6.primaryKey]), filter([table_6.col_3 > 10000]),
access([table_6.primaryKey], [table_6.col_3]), partitions(p0)
9 - output([table_5.primaryKey]), filter([table_5.col_6 > 10000]),
access([table_5.primaryKey], [table_5.col_6]), partitions(p0)
|
EXPLAIN输出的第一部分是查询执行计划的树形结构,第二部分是各个算子的详细信息。在第一部分中,ID表示查询执行树按照前序遍历的方式得到的编号,OPERATOR表示算子的名称,NAME表示操作涉及的表名,EST.ROWS表示该算子的估算中间结果行数,COST表示该算子的执行代价。
((table_3⋈table_0)⋈table_6)⋈table_5 。当查询涉及的表增多,我们可能无法一目了然地从EXPLAIN的查询计划中得到当前的连接顺序。因此,我们利用图形化的方式,将EXPLAIN的查询执行计划画成对应查询执行树,以更形象地展示各表的连接顺序。为了简化查询执行树,树上的节点只包含scan和join类型。
我们利用DOT语言来实现以上目的,DOT语言是一种文本图形描述语言,语法相对简单。
2.1节示例的DOT描述如下:
digraph binaryTree{
"1_HASH_JOIN"->"4_HASH_JOIN";"1_HASH_JOIN"->"table_5";
"4_HASH_JOIN"->"5_HASH_JOIN";"4_HASH_JOIN"->"table_6";
"5_HASH_JOIN"->"table_3";"5_HASH_JOIN"->"table_0";
}
命令:
dot -Tpng example.dot -o example.png
生成简化的查询执行树为:


对于每一个查询,我们枚举它所有可能的连接顺序,并利用hint关键字LEADING,强制OceanBase以特定连接顺序执行查询。
我们得到查询的所有连接顺序及其对应执行时间,从而得到OceanBase所选连接顺序在搜索空间中的排名情况。
示例:
query: SELECT * FROM A, B, C WHERE A.id = B.id AND A.id = C.id;
hint关键字_LEADING_应用举例:
SELECT /*+LEADING(A, B, C)*/ FROM A, B, C WHERE A.id = B.id AND A.id = C.id;
表示强制以 (A⋈B)⋈C 的顺序执行查询。
按照执行时间由小到大将连接顺序排序:

注意:由于随着连接的表数量增加,搜索空间的大小呈指数级增长,如6张表的搜索空间为720,7张表的搜索空间为5040,8张表的搜索空间为40320……因此想让数据库执行搜索空间中每一个连接顺序是十分耗时的。在实验中,当搜索空间大于100时,我们将OceanBase选择的连接顺序作为一个候选项放入评估池,在搜索空间中再随机选择100个连接顺序,共101个评估对象;然后我们评估OceanBase选择的连接对象在101个评估对象中的相对位置,实现对OceanBase多表join的效果评估。
Mean Reciprocal Rank (MRR)
在检索系统中,MRR值表示正确检索结果值在检索结果中的排名,用来评估检索系统的性能。
公式:

其中,|Q| 是query的个数,*ranki* 是对于第*i*个query,OB选择的连接顺序的执行时间在所有连接顺序执行时间中的排列位置(从小到大)。 * 举例:

得到 MRR值为:

Deviation 偏差
计算优化器所选连接顺序的执行时间与最优执行时间之间的偏差。
公式:

其中,T 表示优化器所选连接顺序的执行时间,Tb 表示枚举的连接顺序搜索空间中最优(最短)的执行时间。
本次实验将OceanBase部署在4台机器上,机器配置如下:

OceanBase数据库部署的配置文件如下:
user:
username: xxx
password: xxx
# key_file: .ssh/authorized_keys
oceanbase-ce:
version: 3.1.2
servers:
- name: host1
ip: 10.24.14.55
- name: host2
ip: 10.24.14.228
- name: host3
ip: 10.24.14.111
global:
devname: eth0
cluster_id: 1
memory_limit: 28G
system_memory: 8G
stack_size: 512K
cpu_count: 16
cache_wash_threshold: 1G
__min_full_resource_pool_memory: 268435456
workers_per_cpu_quota: 10
schema_history_expire_time: 1d
net_thread_count: 4
major_freeze_duty_time: Disable
minor_freeze_times: 10
enable_separate_sys_clog: 0
enable_merge_by_turn: FALSE
datafile_disk_percentage: 35
syslog_level: WARN
enable_syslog_recycle: true
max_syslog_file_count: 4
appname: obct
host1:
mysql_port: 3883
rpc_port: 3882
home_path: /data/obdata1
zone: zone0
host2:
mysql_port: 3883
rpc_port: 3882
home_path: /data/obdata1
zone: zone1
host3:
mysql_port: 3883
rpc_port: 3882
home_path: /data/obdata1
zone: zone2
obproxy:
servers:
- 10.24.14.188
global:
listen_port: 3883
home_path: /data/obproxy1
rs_list: 10.24.14.55:3883;10.24.14.228:3883;10.24.14.111:3883
enable_cluster_checkout: false
cluster_name: obct
表规模:
查询:
实验数据获取地址
下表展示了3~8张表连接时的MRR值。

同时,我们也在TiDB上进行了3 join 与4 join的实验,得到的MRR值分别为0.35与0.22,明显劣于OceanBase。
图1 展示了3~8张表连接时的deviation结果,横坐标表示参与连接表的数量,纵坐标表示优化器选择的连接顺序的执行时间与最优执行时间的偏差。

从图1中可以看到,当参与连接表的数量小于等于5时,偏差大部分低于20%。经过计算,我们得到去除异常值后,平均执行时间差低于42毫秒(执行时间差 = OceanBase选择的连接顺序执行时间 - 最优连接顺序执行时间)。
我们可以观察到,参与连接的表数目从3增长到6时,MRR值逐渐减少,deviation整体呈现增长趋势,如中位线从0.82%增长到15.35%(增长了17.7倍),均值从6.33%增长到17.89%(增长了1.8倍)。这个结果说明了随着参与连接的表数量增大,OceanBase选择的连接顺序在搜索空间中的排名越来越低,与最优连接顺序的执行时间偏差越来越大,优化器从连接顺序搜索空间中选择出最优连接顺序的性能下降了。
当表数量增加到7时,MRR值逐渐增大,deviation整体呈现下降趋势,这是因为我们仅仅从庞大的搜索空间中随机选择100个连接顺序进行评估,如7张表时仅选择了2% (100/5040)的连接顺序,8张表时仅选择了0.25% (100/40320)的连接顺序。我们很有可能并没有随机到最优的连接顺序。为此,我们增加了图2 的实验。

图2 展示了7张表参与连接时,分别随机选择100个连接顺序(random100)与随机选择200个连接顺序(random200)的deviation结果对比。我们可以看到,当随机的搜索空间增大时,deviation整体呈现增长趋势,中位线从11.29%增长到14.28%(增长了0.3倍),均值从12.53%增长到17.00%(增长了0.4倍)。同时,random200的MRR值为0.25,比random100的MRR值0.34下降了26.47%。这个结果说明了图1 的下降趋势与搜索空间的大小有关,即我们设定的搜索空间大小很大程度上影响了最终结果的准确性。
为了得到更精确的结果,更好的评估连接顺序选择,我们将在今后的实验中改进搜索空间的剪枝策略来代替随机选择。
从图1中,我们可以看到有不少偏差较大的离散点,下面我们对其中两个点进行分析,探究偏差产生的原因。
图1 4张表参与连接时最大偏差为58.02%。
select count(*) as result from table_0, table_9, table_4, table_5
where table_4.col_8 <= 1922008581 and table_5.col_0 < -348809115.7006844609905468
and table_0.col_7 > -4255518.1398595209726152 and table_9.col_5 < -211145821.32382347996192376
and table_4.fk_3 = table_5.primaryKey
and table_4.fk_11 = table_0.primaryKey
and table_9.fk_2 = table_0.primaryKey;
为了避免高偏差是由数据异常等原因导致的,我们重新执行该查询及其对应最优连接顺序:
分别执行7次,计算平均执行时间(去除最大最小值)。
两者偏差为30.43%,可见该查询的偏差依旧较大。
| ======================================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
------------------------------------------------------
|0 |SCALAR GROUP BY | |1 |923737|
|1 | HASH JOIN | |353299 |856253|
|2 | PX COORDINATOR | |7804 |19664 |
|3 | EXCHANGE OUT DISTR |:EX10000|7804 |18925 |
|4 | TABLE SCAN |table_9 |7804 |18925 |
|5 | HASH JOIN | |32468 |606413|
|6 | PX COORDINATOR | |710 |1777 |
|7 | EXCHANGE OUT DISTR |:EX20000|710 |1710 |
|8 | TABLE SCAN |table_0 |710 |1710 |
|9 | HASH JOIN | |32796 |573707|
|10| TABLE SCAN |table_4 |33127 |274978|
|11| PX COORDINATOR | |82996 |203567|
|12| EXCHANGE OUT DISTR|:EX30000|82996 |195711|
|13| TABLE SCAN |table_5 |82996 |195711|
======================================================
((table_4⋈table_5)⋈table_0)⋈table_9,实验得到最优的连接顺序为((table_0⋈table_9)⋈table_4)⋈table_5。我们画出两个连接顺序的查询树如下(上面为OceanBase选择的连接顺序,下面为最优连接顺序):
节点中”[]”内的数字是操作的真实基数(中间结果大小),其中join操作的基数是两表连接后的中间结果大小,scan操作的基数是表经过条件过滤后的中间结果大小。

从图1中,我们可以看到偏差最大的是3 join中的一点 (85.73%)。
该点查询为:
select count(*) as result from table_2, table_14, table_10
where table_2.col_4 < 1702600163 and table_14.col_1 <= 39586856.6599801245771715
and table_10.col_8 > -633452491.72604654429895750
and table_2.fk_0 = table_14.primaryKey
and table_10.fk_0 = table_14.primaryKey;
(table_14⋈table_2)⋈table_10,实验得到最优的连接顺序为(table_2⋈table_14)⋈table_10。为了避免高偏差是由数据异常等原因导致的,我们重新执行该query及其对应最优连接顺序:
分别执行7次,计算平均执行时间(去除最大最小值)。
两者偏差为64.85%,可见该query的偏差的确较大。
obclient> EXPLAIN select count(*) as result from table_2, table_14, table_10
where table_2.col_4 < 1702600163 and table_14.col_1 <= 39586856.6599801245771715
and table_10.col_8 > -633452491.72604654429895750
and table_2.fk_0 = table_14.primaryKey
and table_10.fk_0 = table_14.primaryKey;
| ======================================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
------------------------------------------------------
|0 |SCALAR GROUP BY | |1 |12360781|
|1 | HASH JOIN | |14725630 |9548003 |
|2 | PX COORDINATOR | |44555 |414608 |
|3 | EXCHANGE OUT DISTR|:EX10000|44555 |406173 |
|4 | HASH JOIN | |44555 |406173 |
|5 | TABLE SCAN |table_14|356 |874 |
|6 | TABLE SCAN |table_2 |45005 |363648 |
|7 | TABLE SCAN |table_10|118626 |287577 |
======================================================
obclient> EXPLAIN select /*+LEADING(table_2, table_14, table_10)*/ count(*) as result from table_2, table_14, table_10
where table_2.col_4 < 1702600163 and table_14.col_1 <= 39586856.6599801245771715
and table_10.col_8 > -633452491.72604654429895750
and table_2.fk_0 = table_14.primaryKey
and table_10.fk_0 = table_14.primaryKey;
| ======================================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
------------------------------------------------------
|0 |SCALAR GROUP BY | |1 |12360781|
|1 | HASH JOIN | |14725630 |9548003 |
|2 | PX COORDINATOR | |44555 |414608 |
|3 | EXCHANGE OUT DISTR|:EX10000|44555 |406173 |
|4 | HASH JOIN | |44555 |406173 |
|5 | TABLE SCAN |table_2 |45005 |363648 |
|6 | TABLE SCAN |table_14|356 |874 |
|7 | TABLE SCAN |table_10|118626 |287577 |
======================================================
其中,table_2经条件过滤后共450068行数据,table_14经条件过滤后共1066行数据。
这里我们发现一个有趣的现象,两个执行计划的区别在于table_2和table_14进行hash join时谁作为hash表,前者以table_14作为hash表,后者以table_2作为hash表。按理来说,小表作为hash表,然后去扫描另一张表的每一行数据,用得出来的行数据根据连接条件去映射建立的hash表。而这里table_14作为小表充当hash表的执行效率却不如table_2充当hash表。后续的hash join算子优化或许可以将这个现象考虑进去。
经过上述实验,我们将OceanBase在连接顺序选择上的表现作以下总结:
随着业务规模的增长,很多业务方都将自己的应用从集中的事务型数据库部署到分布式事务型数据库来追求可扩展和高可用。分布式事务型数据库根据存储和计算的不同扩展方式通常分为Share-Nothing架构、Share-Nothing计算和存储分离架构以及Share-Storage架构(如图1)。这三种不同的架构有不同的适用场景,而且不同的数据库也有存在不同的瓶颈点和优化技术,这就需要一种评测手段能够给出公平的评价和分析。
我们将常用的事务型评测基准标记在图1中,他们被分为三类,分别是:微观的评测基准、应用驱动的评测基准和目的驱动的评测基准。我们发现事务型评测基准大多数在2010前被提出来,而分布式事务型数据库却在2010年之后开始迅猛发展。 所以,这就引发了一个问题:现有的事务型评测基准是否仍然适用于评测分布式事务型数据库?

图1 分布式事务型数据库和OLTP评测基准的发展历史
在对现有的分布式事务型数据库进行了调研和总结之后,我们认为面向分布式事务型数据库的评测基准必须涵盖如下三个方面:分布式事务型数据库的瓶颈点(分布式事务比例、分布式事务跨节点、分布式查询的性能、冲突), 分布式数据库数据库的优化手段(读写分离、小表广播、数据放置方式)等和功能测试(分布式死锁、全局快照、分布式并发控制、一致性测试、可用性测试)。 然而,经过我们的调研和实验发现,现有的事务型评测基准没有完全覆盖到以上三个方面的评测。于是,我们提出并设计了一款面向分布式事务型数据库评测的基准并实现了Dike工具, 它不仅能够评判被测数据库是否满足分布式事务型数据库的基本特征,而且还能针对性地对分布式事务型数据库的瓶颈点和优化手段进行评测。
为了公平且有效地评测各个分布式事务型数据库,我们对Dike提出了以下四点设计要求:第一,功能健全性,它能帮助我们去回答当前的数据库是否是分布式事务型数据库;第二,微观性,它能够针对性地评测被测的分布式事务型数据库在研发过程中的优化点和瓶颈点;第三,定量性,它能够针对分布式事务型数据库中瓶颈点进行定量的评测分析,比如,通过定量地改变事务跨节点数的多少来看分布式事务型数据库的性能变化情况;第四,动态性,它能够提供模拟业务动态变化的情况,这也是符合现实的应用需求,如热点和负载量的动态变化。

图2 Dike架构图
根据分布式事务型数据库评测的要求,我们定义并实现了Dike,架构图如图2所示。Dike包括了数据生成器、负载生成器、混沌生成器、验证器和性能收集器。 为验证被测数据库是否满足分布式事务型数据库的基本特征,Dike首先从配置文件中读取数据库信息等,之后调用数据生成器来生成指定规格的数据。然后,Dike调用验证器来生成负载对被测数据库进行功能测试。最后,Dike将功能测试的结果反馈给用户。
为评测被测数据库潜在的瓶颈点和优化技术,Dike同样先从配置文件中读取数据库信息和评测内容(如分布式事务的处理能力),之后根据配置信息调用数据生成器来生成指定规格的数据。然后,Dike调用负载生成器来生成指定的负载进行性能测试。最后,Dike调用性能收集器将结果整合并反馈给用户。
为评测被测数据库的可用性,Dike专门设计了混沌生成器。它能够根据用户的要求进行一系列分布式场景中可能的故障注入。
此外,该工具提供了可定制化的服务,用户可以通过修改配置项来评测特定的内容。
]]>