1.新的问题
influxdb目前支持内存型索引inmem及文件型索引tsi1。之前追踪篇将influxd索引修改为tsi1之后,经过一段时间的运行,从监控观察到,由于调用方采用异步队列+批处理的方案将数据写入influxdb,会在某些时刻调用方内部出现数据堆积,指标如图:
- 横坐标: 时间轴,从12-29 00:00 到 12-30 00:00
- 纵坐标: 队列中数据堆积长度,坐标最大值250k,即最大25w个数据堆积
从上图可以看到,当天监控出现数次堆积,上午7:00-10:00尤为严重。在堆积时,登录influxdb服务器,查看机器状态如下:
top - 09:40:58 up 120 days, 19:18, 1 user, load average: 32.29, 32.32, 29.82 Tasks: 364 total, 1 running, 363 sleeping, 0 stopped, 0 zombie %Cpu(s): 0.4 us, 0.1 sy, 0.0 ni, 57.7 id, 41.8 wa, 0.0 hi, 0.0 si, 0.0 st KiB Mem : 65433892 total, 376024 free, 30179144 used, 34878724 buff/cache KiB Swap: 32833532 total, 32689404 free, 26624 used. 34607748 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 9571 root 20 0 0.269t 0.053t 0.025t D 14.6 86.2 1081:18 influxd
在出现堆积时,wa很高,说明问题再次出现在磁盘io上,而且influxd的SHR空间占用了25g,又是为什么? 面对新出现的问题,当前的主要手段仍然是从influxdb配置文件入手,但是该如何优化?
2.tsi原理
工欲善其事,必先利其器。
查阅了相关资料之后,整理了influxdb使用tsi索引时原理图:
说明:
- 写入influxdb时,会同时写wal文件及cache内存, wal用于宕机恢复cache
- cache在达到配置中的阈值时,会进行snapshot快照,进行落盘
- influxdb的series及index索引会在内存中全量保存,用于快速检索
- wal文件大小在达到配置中阈值时,会进行压缩转换到index索引
- influxdb会对磁盘数据文件{{index+data}}按照分片shard维度进行四次压缩(level1,2,3及full),以节约磁盘空间
3.堆积的原因
在队列堆积的时间点,经过多次对比influxdb的日志:
#调用方队列堆积时,influxdb的关键日志如下: #influx开始执行第四次全量压缩策略,tsm1_strategy=full ts=2021-01-05T10:30:02.049644Z lvl=info msg="TSM compaction (start)" log_id=0RVRbtjl000 engine=tsm1 tsm1_strategy=full tsm1_optimize=false trace_id=0RWXOKGG000 op_name=tsm1_compact_group op_event=start ... #省略 ... #第四次全量压缩结束 ts=2021-01-05T10:44:13.931365Z lvl=info msg="TSM compaction (end)" log_id=0RVRbtjl000 engine=tsm1 tsm1_strategy=full tsm1_optimize=false trace_id=0RWXOKGG000 op_name=tsm1_compact_group op_event=end op_elapsed=851881.724ms ... ...
此时influxdb进行分片数据的第四次的全量压缩,会进行大量的磁盘io及cpu压缩计算,导致服务的压力增大,所以调用方队列出现数据堆积,相关参数见配置文件:
#influxdb部分配置文件 #描述:内存快照的冷冻写入周期,默认10m #场景: 当创建新的shard分片开始接受数据之后,上个shard分片进入冷冻期, #冷冻期的shard分片不再接收写入的请求,10分钟之后,会将内存里冷冻shard的cache进行落盘操作 #应用: 本文设置为30分钟。个人认为时间越短,会越快释放上个shard的内存cache cache-snapshot-write-cold-duration = "30m" #描述: 使用全量策略压缩冷冻期分片的周期,默认4h #场景: 当shard进入冷冻期后,会经过4h,开始进行全量压缩策略,进一步减少shard落盘数据占用的空间 #与cache-snapshot-write-cold-duration配合使用 #可以从日志中看到,新分片开始写入数据之后,在4h+10m之后,会对上个分片进行全量压缩策略 #应用: 本文设置为80小时。目的是不进行full压缩策略,来避免io过多消耗,后面会介绍由于设置的retention policy为72小时,所以此处大于72即可。 compact-full-write-cold-duration = "80h" #描述: 最大并行压缩数,默认会使用golang的逻辑处理器的一半 #场景: 当进行level1,level2,level3及full策略压缩文件时使用的处理器数量, #当前服务器为16core32物理线程,则会在压缩时默认使用16个处理器 #应用: 本文设置为8。用于减轻压缩策略时,cpu与磁盘io的压力,但相应的会导致压缩周期变长。 max-concurrent-compactions = 8 #描述: 压缩文件时,每秒写入磁盘的数据量,默认48MB #应用:本文设置为16MB,用于减轻磁盘写入时的压力,同样会导致压缩周期变长。 compact-throughput = "16m" #描述: 压缩文件时,每秒最大写入磁盘的峰值数据量,默认48MB #应用:本文设置为16MB,用于减轻磁盘写入时的压力,同样会导致压缩周期变长。 compact-throughput-burst = "16m"
4.SHR占用
在本文第1小节中,使用linux的top命令可以看到,influxd进程占用RES内存为53g, SHR内存为25g。所以引申问题:
- 何为SHR?
- 为什么会有那么多的SHR?
- SHR对系统有什么影响?
使用man查看top命令的解释:SHR – Shared Memory Size (KiB) The amount of shared memory available to a task, not all of which is typically resident. It simply reflects memory that could be potentially shared with other processes. 共享内存的大小 程序共享内存的数据量,并不是全部驻留在内存空间,通常反映了与其他程序潜在共用的内存。
那为什么influxd会有那么高的共享内存?通过linux进程smaps分析当前实际占用内存的大小。
注: smaps中有些文件的引用仅占用虚拟内存,而不占用物理内存
#influxdb当前程序运行pid为: 9571 #1.计算influxdb数据文件通过shr占用物理内存大小,筛选出内存占用大于10m的文件: > cat /proc/9571/smaps | sed -n '/rp_iot_cloud/,+2p' | grep -v 'Size:' | sed 's/\ kB//' | awk '{print $NF}' | awk 'BEGIN{i=-1}{i++;a[i]=$0}END{for(k=0;k<length(a);k=k+2) {if(a[k+1]/1000>10){m+=a[k+1]/1000;print a[k],a[k+1]/1000,m}}}' --------- 文件名称 ---------------- 当前文件占用内存(MB) ----- 每行累计占用内存(MB) ---- /data/influxdb/data/iot_cloud/rp_iot_cloud/233/000000640-000000010.tsm 550.148 550.148 /data/influxdb/data/iot_cloud/rp_iot_cloud/233/000000640-000000009.tsm 322.392 872.54 /data/influxdb/data/iot_cloud/rp_iot_cloud/233/000000640-000000008.tsm 228.524 1101.06 /data/influxdb/data/iot_cloud/rp_iot_cloud/233/000000512-000000010.tsm 636.132 1737.2 /data/influxdb/data/iot_cloud/rp_iot_cloud/233/000000512-000000009.tsm 336.532 2073.73 /data/influxdb/data/iot_cloud/rp_iot_cloud/233/000000512-000000008.tsm 231.336 2305.06 ... #省略 ... /data/influxdb/data/iot_cloud/rp_iot_cloud/233/000000384-000000005.tsm 560.716 17183.6 /data/influxdb/data/iot_cloud/rp_iot_cloud/233/000001209-000000002.tsm 315.204 17498.8 /data/influxdb/data/iot_cloud/rp_iot_cloud/233/000001216-000000001.tsm 55.644 17554.4 /data/influxdb/data/iot_cloud/rp_iot_cloud/233/000000256-000000005.tsm 514 18068.4 #2.计算influxdb索引series文件通过shr占用物理内存大小,筛选出内存占用大于10m的文件: > cat /proc/9571/smaps | sed -n '/series/,+2p' | grep -v 'Size:' | sed 's/\ kB//' | awk '{print $NF}' | awk 'BEGIN{i=-1}{i++;a[i]=$0}END{for(k=0;k<length(a);k=k+2) {if(a[k+1]/1000>10){m+=a[k+1]/1000;print a[k],a[k+1]/1000,m}}}' --------- 文件名称 ---------------- 当前文件占用内存(MB) ----- 每行累计占用内存(MB) ---- /data/influxdb/data/iot_cloud/_series/07/index 517.4 517.4 /data/influxdb/data/iot_cloud/_series/07/0007 99.032 616.432 /data/influxdb/data/iot_cloud/_series/07/0006 98.896 715.328 /data/influxdb/data/iot_cloud/_series/07/0005 62.732 778.06 ... #省略 ... /data/influxdb/data/iot_cloud/_series/01/0002 16.352 7014.15 /data/influxdb/data/iot_cloud/_series/00/0002 16.36 7030.51
可以看出程序运行时,会加载iot_cloud库文件:
- tsm数据文件(主要为当前shard的文件,共约18G)
- series文件(所有series文件,共约7G)
加载的内存大小与SHR内存基本吻合。基于当前的分片策略retention policy周期为7天,每天一分片,所以可以从分片角度减少shard占用的内存。笔者尝试调整分片策略:
#当前influxdb中数据库名 use iot_cloud #修改保留策略为周期为3天,每2小时一分片 alter retention policy rp_iot_cloud on iot_cloud duration 3d REPLICATION 1 SHARD DURATION 2h default
调整为2小时一分片之后,SHR内存峰值会减少10-15g左右的占用。但是缩短分片间隔之后,influxdb会更频繁的进行内部自检及数据压缩,会造成cpu及磁盘io的消耗。所以继续考虑SHR占用较大对系统会有什么影响?应用程序在启动之后,会共享系统一些内存:
- 堆内存(共享函数库消耗的堆空间)
- 文件缓存(从磁盘读取文件进行缓存)
对于共享堆内存则是必须占用的物理空间,而文件缓存则是系统针对磁盘读取的优化。目前influxdb在内存中引入了大量文件,在内存充足时,会占用较多的空间,用于提高程序读取性能。
5.SMP与NUMA?
cpu硬件体系架构可以分为:
- SMP(Symmetric Multi-Processor)/UMA(Uniform Memory Access)模式SMP架构,所有的CPU争用一个总线来访问所有内存,优点是资源共享,而缺点是总线争用激烈。 实验证明,SMP服务器CPU利用率最好的情况是2至4个CPU
- NUMA(Non-Uniform Memory Access)模式NUMA架构引入了node和distance的概念。对于CPU和内存这两种最宝贵的硬件资源, NUMA用近乎严格的方式划分了所属的资源组(node),而每个资源组内的CPU和内存是几乎相等。
在influxdb服务器上,查看当前cpu及numa相关信息如下:
#lscpu用于查看当前cpu相关信息 > lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 32 #共32个逻辑核数 On-line CPU(s) list: 0-31 Thread(s) per core: 2 #每个核心支持2个物理线程 Core(s) per socket: 8 #每个卡槽有8个核心 Socket(s): 2 #共2个卡槽,总共16个核心 NUMA node(s): 2 #共2个numa节点 ... ... L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 20480K NUMA node0 CPU(s): 0-7,16-23 #numa节点分布 NUMA node1 CPU(s): 8-15,24-31 #numactl --hardware用于查看相关硬件信息 > numactl --hardware available: 2 nodes (0-1) node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23 node 0 size: 32365 MB #节点0分配内存为32G node 0 free: 185 MB #节点0剩余内存185M node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31 node 1 size: 32768 MB #节点1分配内存为32G node 1 free: 2492 MB #节点1剩余内存2492M node distances: node 0 1 0: 10 21 1: 21 10 #numastat查看当前状态 #miss值和foreign值越高,就要考虑绑定的问题。 > numastat node0 node1 numa_hit 46726204589 16958785317 numa_miss 1636704898 11155932344 #numa_miss较高 numa_foreign 11155932344 1636704898 interleave_hit 22442 22958 local_node 46726202598 16958838294 other_node 1636706889 11155879367 #other_node较高 #numactl --show 用于查看当前numa策略 > numactl --show policy: default #使用默认策略(localalloc) preferred node: current physcpubind: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 cpubind: 0 1 nodebind: 0 1 membind: 0 1
通过查询,发现当前的numa策略会出现大量的miss。由于influxdb基于go语言开发,go语言社区中有关于 numa 感知调度的设计文档,但是本身的实现过于复杂,所以 go 语言团队在最新1.15版本还没有着手实现。目前根据相关资料,考虑influxdb运行时会占用大规模内存,建议通过如下方式启动influxdb:
numactl --interleave=all /usr/bin/influxd -config /usr/bin/influxdb.conf
6.读取端优化
从系统角度,应该同时关注influxdb写入和读取两个维度。 写入端应从具体业务场景,提前划分好写入的tags及fields,从而避免产生大量的series导致内存膨胀过快。而读取端,应明确查询时间范围,命中更少的分片数据,来防止加载大量的无用查询结果而导致程序OOM。 走查了读取端相关influxdb查询语句,发现几处类似如下消耗内存及性能的语句:
#表描述 - 表名: table - tags: productKey,deviceName - fields: identifier #修改之前的sql #由于没有时间范围,会导致查询所有shard数据,并从磁盘加载到内存,最后进行排序 select * from table where productKey=? and deviceName=? and identifier=? order by time desc limit 1 #优化之后的sql #根据业务场景,此处可以仅查询最近2小时内的数据,避免全分片 select * from table where time > now()-2h and productKey=? and deviceName=? and identifier=? order by time desc limit 1
7.总结
本文整理了业务中使用influxdb遇到的问题,并提出了一些优化方案。目前来看,influxdb对于笔者仍然是一个黑盒程序,更细致的内容就需要从源码追寻。 当前采用了如下方式进行优化(由于需要对配置参数,策略各方面权衡,这是一个持续的过程):
(1) retention policy
#将保留策略修改为:3天一周期;1天一分片 alter retention policy rp_iot_cloud on iot_cloud duration 3d REPLICATION 1 SHARD DURATION 1d default
(2) 配置文件
#influxdb配置文件,主要参数如下: [data] #wal日志落盘周期,官方建议0-100ms #尝试了100ms,50ms,20ms之后,目前折中采用50ms wal-fsync-delay = "50ms" #使用tsi1索引 index-version = "tsi1" #分片允许最大内存,当超过最大内存会拒绝写入 #内存越大,多个新老分片会占用更多的堆空间 cache-max-memory-size = "2g" #当cache超过128m时,会进行快照落盘 cache-snapshot-memory-size = "128m" #cache冷冻写入时间 cache-snapshot-write-cold-duration = "30m" #进行全量压缩时间 #由于retention policy为72小时 #超过72小时,可以认为不进行全量压缩 compact-full-write-cold-duration = "80h" #并行压缩处理器 max-concurrent-compactions = 8 #压缩每秒落盘数据量 compact-throughput = "16m" #压缩每秒最大落盘数据量 compact-throughput-burst = "16m" #wal日志超过128m时会被压缩为索引文件,并删除 max-index-log-file-size = "128m" [monitor] #关闭监控 store-enabled = false
(3) 程序启动
numactl --interleave=all env GODEBUG=madvdontneed=1 /usr/bin/influxd -config /usr/bin/influxdb.conf
由于笔者水平有限,上述如有问题,欢迎指正。
influxdb内存消耗分析及性能优化【探索篇】 – 知乎 (zhihu.com):
https://zhuanlan.zhihu.com/p/361044794
参考
- 理解virt res shr之间的关系: https://www.orchome.com/298
- 服务器体系(SMP, NUMA, MPP)与共享存储器架构(UMA和NUMA): https://cloud.tencent.com/developer/article/1372348
- SWAP的罪与罚: https://blog.huoding.com/2012/11/08/198
- go调度器: https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/
- NUMA-aware scheduler for Go: https://docs.google.com/document/u/0/d/1d3iI2QWURgDIsSR6G2275vMeQ_X7w-qxM2Vp7iGwwuM/pub