作者:xuty

本文起源:原创投稿

*爱可生开源社区出品,原创内容未经受权不得随便应用,转载请分割小编并注明起源。


一. 背景

记录下第一次应用 GDB 调试 ClickHouse 源码的过程,这里仅仅是通过简略的调试过程理解 ClickHouse 外部的机制,有助于解决纳闷,代码小白,有谬误见谅。

二. 调试问题

调试 ClickHouse 次要是为了解决集体遇到的一个理论问题,上面先形容下这个问题:

  1. 通过 clickhouse 自带的mysql表函数导入全量数据时(这里建了一张测试表memory_test50w行数据56G),因为超过最大的内存限度(CK服务器36G内存),导致了如下报错。
localhost :) insert into `test`.`memory_test`  select * mysql('192.168.1.1:3306','test','memory_test','root','xxxx');                                                  Received exception from server (version 20.8.12):Code: 241. DB::Exception: Received from 127.0.0.1:9000. DB::Exception: Memory limit (total) exceeded: would use 35.96 GiB (attempt to allocate chunk of 17181966336 bytes), maximum: 28.14 GiB: While executing SourceFromInputStream. 
  1. 大抵过程就是 ClickHouse 会将 mysql 的数据读入内存而后批量写入,这里波及到一个 buffer 的问题,ClickHouse 会读入多少行数据或者多少bytes的数据后再批量写入,依据show processlist察看来看,貌似达到100多w行数据就会写入一次,随后开释内存,再循环读写。那么咱们的问题其实读取MySQL原表中100多万行的数据超过了咱们 ClickHouse 内存配置大小。

  1. 对于这个问题,如果你CK服务器内存配置比拟大其实是不会遇到的,我这里CK服务内存仅为32G,所以可能碰到这个内存问题,最简略的其实扩容下内存就行了,然而为了防止有些我的项目上不好进行内存扩容,所以须要想下其余办法解决。

    • 最开始想到的是用swap分区,然而理论测试下来,ClickHouse 只会用物理内存,不会用到虚拟内存。
    • 第二个形式是通过where条件+分页查问mysql,减小内存占用的峰值,实测无效,然而比拟麻烦。
  2. 接着我想着是不是有什么参数能够管制这个批量写入的阀值,这样不就不会遇到内存不够的问题了嘛,100w会超过内存限度,50w应该就不会超过了把。然而我google了下并没有找到对应的参数,群里问了下也没人晓得,没方法,只能去源码里找找看,这个值到底是不是写死的。

三. 打印栈帧

首先咱们要通过 pstack 打印下堆栈信息,不然无奈晓得函数入口在哪,在这之前须要咱们额定装置下对应 ClickHouse 版本的clickhouse-common-static-dbg的 rpm 包(调试库),不然堆栈信息会比拟简陋,而且前面GDB调试也会有问题。

比方我的 ClickHouse 版本为 20.8.12.2 ,那么对应的rpm包就为clickhouse-common-static-dbg-20.8.12.2-2.x86_64.rpm

再 ClickHouse 中执行 insert 语句后,通过 pstack + clickhouse过程pid > /opt/ck_pstack.log 导入到一个日志文件中。

四. GDB调试

GDB 不多介绍,不过集体更喜爱应用CGDB,应用 Yum 装置即可,我应用的 OS 版本是 CentOS7.9 。

应用 GDB 调试前,还须要将对应 ClickHouse 的源码下载后解压到/build/目录下(默认的编译目录)。

而后调试步骤大略是:

  1. 首先新建个窗口,clickhouse-client 连贯进入 ClickHouse ,期待执行 SQL 。
  2. 关上 CGDB ,attach 到 Clickhouse 的 pid 上,在对应函数行打上断点,这里抉择的是DB::SourceFromInputStream::generate(从栈帧中抉择),CGDB 中须要配置疏忽信号量,不然 CGDB 会始终断开。
(gdb) att 1446(gdb) handle SIGUSR2 noprint nostopSignal        Stop      Print   Pass to program DescriptionSIGUSR2       No        No      Yes             User defined signal 2(gdb) handle SIGUSR1 noprint nostopSignal        Stop      Print   Pass to program DescriptionSIGUSR1       No        No      Yes             User defined signal 1(gdb) b DB::SourceFromInputStream::generateBreakpoint 1 at 0x16610f40: file ../src/Processors/Sources/SourceFromInputStream.cpp, line 135.
  1. 在第一步骤关上的窗口中执行 insert 语句。
  2. CGDB 中按 c 持续,就会跳到 generate 函数上

  1. 接着就是缓缓n,打印参数,一步一步看代码流程。

五. max_block_size

这里间接上调试发现的后果,当读取的行数等于 max_block_size 的时候,就会跳出循环读取,批量写入 ClickHouse ,开释内存,这个 max_block_size 的 GDB 打印的值为1048545,看着十分像是一个能够配置的参数。

搜寻源码中 max_block_size 的赋值,上面这段看着比拟像,由min_insert_block_size_rows配置参数决定。

接着从 system.settings 表中搜寻了下,发现 min_insert_block_size_rows 这个参数的形容和默认值的确都十分像,根本确定这个参数就会影响批量写入的行数。

六. 测试

在会话中批改参数为1w,而后执行 insert ,能够跑通,而且不会报错。

localhost :) set min_insert_block_size_rows = 10000;0 rows in set. Elapsed: 0.001 sec. localhost :) insert into `test`.`memory_test`  select * from mysql('192.168.213.222:3306','test','memory_test','root','xxxx');                                                  INSERT INTO test.memory_test SELECT     *FROM mysql('192.168.213.222:3306', 'test', 'memory_test', 'root', 'xxxx')Ok.0 rows in set. Elapsed: 2065.189 sec. Processed 500.00 thousand rows, 51.23 GB (242.11 rows/s., 24.81 MB/s.) 

show processlist 也能够看到的确是1w行就写入,那么就不会再产生内存不足的问题,至此这个问题根本解决。