最近在做个“枯树逢春”项目,迁移saiku到tidb上。在这个过程中发现并优化了tidbserver的oom问题。本文记录了整个oom问题的排查和解决过程。oom问题的解决在社区有
作者:张
原文来源:https://tidb.net/blog/de9bf174
【是否原创】是
【首发渠道】TiDB 社区
概述
最近在做个“枯树逢春”项目,迁移saiku到tidb上。在这个过程中发现并优化了tidb server的oom问题。本文记录了整个oom问题的排查和解决过程。oom问题的解决在社区有一些实践论述了,本文中尝试利用cgroup控制资源和STRAIGHT_JOIN注解优化join顺序实践比较少,撰文共享出来,希望能帮助遇到类似问题的同学选择合适的解决方案。因行业特殊,表的实际表名做了隐藏和转化(转化成A,B,C),带来的阅读体验下降,敬请见谅。
问题发现
saiku是个早已经没有维护的项目,由于用户习惯的原因(主要是用户肯付费),现在需要寻找一个数据库能够支撑saiku大数据量的查询,由于成本原因,最好还是开源(免费)的。
参考:https://github.com/OSBI/saiku
按照单表1.8亿的场景,断断续续测试过很多数据库:
- Mysql,单表过大,查询时间长,超过用户可忍受范围
- Mycat+Mysql,saiku的开发人员搞不定分表策略,我也不想搞
- GreenPlum,saiku存在sql查分,拆分后的sql主要用来进行维度校验,整个查询过程对GP来说不友好,查询也很慢
- ClickHouse,驱动问题,没有对接成功
- TiDB,勉强可以,但是三表关联有oom风险
本文描述的就是迁移saiku到TiDB上时,遇到的oom问题,以及解决过程。
问题描述参考:https://asktug.com/t/topic/574076
简单描述就是,A,B,C三表关联,A表约2亿数据,按日分区,700+分区,应用触发形如下列查询时:
select
`B`.`code` as `c0`,
`C`.`br_name` as `c1`,
sum(`A`.`ss_num`) as `m0`,
sum(`A`.`a_ss_num`) as `m1`,
sum(`A`.`cb_num`) as `m2`
from
`test`.`A2` as `A`,
`test`.`B` as `B`,
`test`.`C` as `C`
where
`B`.`code` = '1010'
and
`A`.`s_id` = `B`.`s_id`
and
`A`.`b_code` = `C`.`b_code`
group by
`B`.`code`,
`C`.`br_name`;
此时客户端返回:
The last packet successfully received from the server was 86,645 milliseconds ago. The last packet sent successfully to the server was 86,645 milliseconds ago.
排查过程
本次迁移中,TiDB部署架构如下:
nginx作为tidb的代理,应用连接nginx,代理到tidb上,tidb server可用资源是16C32G。
上述过程失败后查看了几个监控页面:
dashboard->集群信息
发现TiDB在查询时全都重启过一遍。
grafana->Overview->TiDB->Memory Usage
三台tidb server都是打满机器内存后,断崖式下降,初步怀疑TiDB重启了。
查看三台机器的/var/log/messages,在对应的时间出现明显的oom-killer,主要信息如下:
Mar 14 16:55:03 localhost kernel: tidb-server invoked oom-killer: gfp_mask=0x201da, order=0, oom_score_adj=0
Mar 14 16:55:03 localhost kernel: tidb-server cpuset=/ mems_allowed=0
Mar 14 16:55:03 localhost kernel: CPU: 14 PID: 21966 Comm: tidb-server Kdump: loaded Not tainted 3.10.0-1160.el7.x86_64 #1
Mar 14 16:55:03 localhost kernel: Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.14.0-0-g155821a1990b-prebuilt.qemu.org 04/01/2014
......
Mar 14 16:55:03 localhost kernel: Out of memory: Kill process 21945 (tidb-server) score 956 or sacrifice child
Mar 14 16:55:03 localhost kernel: Killed process 21945 (tidb-server), UID 1000, total-vm:33027492kB, anon-rss:31303276kB, file-rss:0kB, shmem-rss:0kB
Mar 14 16:55:07 localhost systemd: tidb-4000.service: main process exited, code=killed, status=9/KILL
Mar 14 16:55:07 localhost systemd: Unit tidb-4000.service entered failed state.
Mar 14 16:55:07 localhost systemd: tidb-4000.service failed.
Mar 14 16:55:22 localhost systemd: tidb-4000.service holdoff time over, scheduling restart.
Mar 14 16:55:22 localhost systemd: Stopped tidb service.
Mar 14 16:55:22 localhost systemd: Started tidb service.
Mar 14 16:55:26 localhost run_tidb.sh: [2022/03/14 16:55:26.327 +08:00] [INFO] [cpuprofile.go:111] ["parallel cpu profiler started"]
Mar 14 17:01:03 localhost systemd: Started Session 1108 of user root.
Mar 14 17:18:44 localhost systemd-logind: New session 1109 of user root.
Mar 14 17:18:44 localhost systemd: Started Session 1109 of user root.
以上日志说明,tidb被系统的oom-killer杀掉了,杀掉的原因是系统内存没有剩余了。
初步判断,TiDB发生oom问题了,继续排查发生的原因。
查看sql的执行计划:
A的扫描结果首先跟C做HashJoin,C做Build,A自拍Probe,然后A和C的结果与B做HashJoin,A和C的结果做build,B做Probe,怀疑,这个步骤出现问题,A和C的结果过大。
怀疑执行计划有问题,查看健康度:
SHOW STATS_HEALTHY where Table_NAME = 'A';
看到所有分区健康度都是100,但是注意那个178是个坑,后文详细分析。
由于这个问题,可以反复重现,多次执行相关SQL,并多次执行手动分析:
直到tidb不能完成heap的分析为止,取最后一次成功的heap分析:
```
github.com/pingcap/tidb/util/chunk.NewColumn (/home/jenkins/agent/workspace/build-common/go/src/github.com/pingcap/tidb/util/chunk/column.go:0)
github.com/pingcap/tidb/util/chunk.New (/home/jenkins/agent/workspace/build-common/go/src/github.com/pingcap/tidb/util/chunk/chunk.go:0)
github.com/pingcap/tidb/executor.(*HashJoinExec).fetchBuildSideRows (/home/jenkins/agent/workspace/build-common/go/src/github.com/pingcap/tidb/executor/join.go:0)
github.com/pingcap/tidb/executor.(*HashJoinExec).fetchAndBuildHashTable.func2 (/home/jenkins/agent/workspace/build-common/go/src/github.com/pingcap/tidb/executor/join.go:0)
github.com/pingcap/tidb/util.WithRecovery (/home/jenkins/agent/workspace/build-common/go/src/github.com/pingcap/tidb/util/misc.go:0)
```
fetchAndBuildHashTable这个过程占用了绝大多数内存,跟上面的执行计划分析结果吻合,判断是第二步join中build端的表占用内存过大。
解决方案
saiku的特点是根据模型定义自动生成查询sql,所以saiku端完全避免这种sql产生不太现实,解决的思路还是从tidb端做一些优化,优化分为三个方向:
- 优化,尝试调整join时build和probe两个端所对应数据集,节省内存使用,例如:调整join顺序
- 转化,限制内存使用,或者转化引擎,让sql能够出来结果。例如:尝试调整内存参数、尝试使用TiFlash、尝试非分区表
- 保护,限制资源占用,必要时牺牲掉其中一个tidb server,但不要影响混部的其他组件
解决方案的描述按照解决问题时尝试的顺序编写,并不按照以上分类顺序。
尝试调整内存参数
首先尝试调整了内存的相关参数:
server_configs:
tidb:
enable-batch-dml: true
mem-quota-query: 4294967296
performance.server-memory-quota: 30064771072
performance.txn-total-size-limit: 1073741824s
调整完成之后,进行回归测试,并没有效果,内存的波动仍然出现三个尖峰,并发现了oom-killer。
尝试使用tiflash
考虑到tiflash对ap友好,并且mpp架构正好可以应对这种单节点内存不足的场景,于是部署了tiflash,并增加tiflash的副本:
ALTER TABLE `test`.`A` SET TIFLASH REPLICA 1;
查看同步状态:
SELECT * FROM information_schema.tiflash_replica WHERE TABLE_SCHEMA = 'test' and TABLE_NAME = 'A';
完成同步后进行回归测试,内存的波动仍然出现三个尖峰,并发现了oom-killer。
从执行计划上看,虽然取数据用了tiflash,但是并没有使用mpp模式,即使设置强制使用:
set @@session.tidb_allow_mpp=1;
set @@session.tidb_enforce_mpp=1;
也没有使用,查找官方文档找到原因:
尝试非分区表
从前一个测试想到,如果非分区表,能否执行完成。
测试非分区表,因在上一步测试tiflash时,也同时为非分区表增加了tiflash副本,sql中增加注解:
select
/*+ read_from_storage(tikv[test.A]) */
`B`.`code` as `c0`,
`C`.`br_name` as `c1`,
sum(`A`.`ss_num`) as `m0`,
sum(`A`.`a_ss_num`) as `m1`,
sum(`A`.`cb_num`) as `m2`
from
`test`.`A1` as `A`,
`test`.`B` as `B`,
`test`.`C` as `C`
where
`B`.`code` = '1010'
and
`A`.`s_id` = `B`.`s_id`
and
`A`.`b_code` = `C`.`b_code`
group by
`B`.`code`,
`C`.`br_name`;
注意的是,注解需要使用数据库名+表名昵称,例如,A1或者test.A1,在我的测试中都不生效,A在当前session指定的数据库为test的情况下才生效,为了避免不必要的麻烦,采用数据库名+表名昵称,例如test.A
执行计划如下:
测试非分区表,使用tiflash,执行计划如下:
此两种方案都能正常运行出结果,跟saiku的研发沟通后发现,非分区表虽然解决三表关联的问题,但普通的按日期的两表关联查询反而变慢,影响了大部分模型,非分区表的方案也不能采用。
尝试利用cgroup限制资源使用
在其他项目应用Trino时,出现过Trino混部影响其他组件的问题,当时是采用cgroup相关策略解决的,尝试在tidb server上应用。
其中的关键设置:
- memory.soft_limit_in_bytes:内存软限制,超过此设置会优先回收超过限额的进程占用的内存,使之向限定值靠拢
- memory.limit_in_bytes:内存硬限制,默认超过此设置会触发oom-killer
- memory.oom_control:超过内存硬限制时,系统策略,值为0,则触发oom-killer,值为1,则挂起当前进程,等待有足够的内存后,继续运行。
实测步骤:
准备工作:
yum install -y libcgroup-tools.x86_64 libcgroup
cgcreate -g memory:/tidb
方案1:限制内存使用
cgset -r memory.soft_limit_in_bytes=30064771072 /tidb
cgset -r memory.limit_in_bytes=32212254720 /tidb
cgclassify -g memory:/tidb `ps -ef | grep tidb-server | grep -v grep | awk '{printf $2FS}'`
此方案中memory.soft_limit_in_bytes限制为28G,memory.limit_in_bytes限制为30G,
实测28G没有效果,内存很快到达30G限制,触发oom-killer,messages显示类似以前的oom日志。
方案2:关闭oom-killer行为
cgset -r memory.limit_in_bytes=32212254720 /tidb
cgset -r memory.oom_cOntrol=1 /tidb
cgclassify -g memory:/tidb `ps -ef | grep tidb-server | grep -v grep | awk '{printf $2FS}'`
此方案中
memory.limit_in_bytes限制为30G,内存达到30G之后,tidb server夯住,没有反应,强行重启之后才能继续使用,如图:
由于我测试过程中,挂的是第一个tidb server,所以dashboard无反应,查看tidb server进程还存活,处于不可中断的休眠状态。
调整join顺序
上述方案都不能达到目的之后,想要从控制执行计划方向,寻找一些方案。经查找,STRAIGHT_JOIN可以达到优化join顺序的目的:
STRAIGHT_JOIN 会强制优化器按照 FROM 子句中所使用的表的顺序做联合查询。当优化器选择的 Join 顺序并不优秀时,你可以使用这个语法来加速查询的执行
参考:https://docs.pingcap.com/zh/tidb/stable/sql-statement-select#%E8%AF%AD%E6%B3%95%E5%85%83%E7%B4%A0%E8%AF%B4%E6%98%8E
saiku在组织sql的时候,也通常会把大表放到第一位,其他维度表依次关联,查看执行计划:
按照优化后的顺序,A先和B进行join,结果做为probe端和C进行join,能够完成查询,耗时约2m,此方案可以作为一个备选方案。
健康度的误读
前面这张图,查看健康度的时候,显示的条数只有178,分析时忽略了这个信息,在整个优化过程复盘过程中,发现了这个问题,猜测是这个表的analyze其实并没有完整执行过一次,导致表的统计信息中只收集了178个分区,这意味着执行计划很可能是不准的,花了一整晚的时间完整的执行了一次analyze:
ANALYZE TABLE test.A;
查看健康度:
分区数达到了732。
再次查看执行计划:
符合预期,实际执行大约在2m8s,这个时间,基本上能够给用户方解释了。
总结
兜兜转转,此次的问题,仍然是个统计信息不准的问题,因为不熟悉分区表的统计信息记录方式,导致了开始的误判。因为正式环境需要混合部署:
经过此次测试,正式环境调整策略如下:
server {
listen 4000;
proxy_pass tidb;
proxy_next_upstream off;
}
关闭nginx失败转移策略,前面表述中,之所以有三个尖峰,是因为nginx的请求失败转移策略,这个慢sql会依次访问所有tidb server,导致tidb server依次重启,整个tidb上的请求会全部失败一次,影响太大。
cgset -r memory.limit_in_bytes=34359738368 /tidb
cgclassify -g memory:/tidb `ps -ef | grep tidb-server | grep -v grep | awk '{printf $2FS}'`
防止有统计信息不准的表,导致oom问题,影响到混合部署的其他组件,最差情况就是单个tidb server重启。
ANALYZE TABLE test.A PARTITION prt_20210101;
lighting在导数据之后,会有analyze的语句执行,但表比较大,重试三次都是失败。计划在每次导数据之后,定时设置一个analyze,对有变动的分区执行analyze。