Post

Prometheus 加载配置部分缺失问题记录

问题描述

prometheus 在执行配置文件热加载的时候出现了部分配置没有加载的现象。

主配置文件内容为:

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
32
global:
  scrape_interval:     15s
  evaluation_interval: 15s
  scrape_timeout:      15s

alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - 12.99.78.69:9093
      - 12.99.78.70:9093

rule_files:
  - '../rules/*rules.yml'

scrape_configs:
  - job_name: '核心业务系统'
    metrics_path: /actuator/prometheus
    file_sd_configs:
    - files:
      - '核心业务系统.yml'
  - job_name: '电销系统'
    metrics_path: /actuator/prometheus
    file_sd_configs:
    - files:
      - '电销系统.yml'
  - job_name: '收单服务系统'
    metrics_path: /actuator/prometheus
    file_sd_configs:
    - files:
      - '收单服务系统.yml'
   # 省略其他相同格式的系统 job 配置

主配置文件中通过 file_sd_config 指定需要采集系统的 target,以核心业务系统举例,它的配置为:

核心业务系统.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- targets:
  - 12.103.63.160:14600
  labels:
    system: 核心系统
    application: app-ltcb-task-r00
    module: RDCN00
    env: sit
- targets:
  - 12.103.6.179:10400
  labels:
    system: 核心系统
    application: app-taae-onl-a00
    module: 核心子系统
    env: uat
- targets:
  - 12.103.52.221:13402
  - 12.103.52.222:13402
  labels:
    system: 核心系统
    application: app-ltpb-onl-r02
    module: 冲销查证
    env: uat
# 省略其他配置

这样配置带来的好处是显而易见的,主配置文件中的 job 只跟系统关联,系统下的需要采集的应用,都能通过自定义标签区分开来。

为了方便对接入 prometheus 监控系统进行管理,我们开发了一套系统:主要就是维护系统、应用与配置文件的对应关系。通过前端表单填写接入信息生成对应的配置文件,然后请求 prometheus 的 reload 接口,在不需要重启 prometheus 服务的情况下热加载配置。

注:配置热加载需要在 promethues 启动参数中添加启动参数 --web.enable-lifecycle,发送以下请求触发热加载:

1
curl -X POST http://prometheus:9090/-/reload

没想到的是随着接入系统的不断增加,发现有些时候配置文件已经更新,但是执行 reload 操作后 prometheus 采集的 target 却没有生成,因为这个现象是偶发的,开始以为是误操作导致的,后来频率越来越高才发觉问题没那么简单。

排查过程

因为是配置文件过多导致出现了问题,因此编写脚本生成大量的配置文件来模拟真实环境。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/bin/bash

function append_conf() {
cat <<EOF >> prometheus.yml
  - job_name: system$1
    metrics_path: /actuator/prometheus
    file_sd_configs:
    - files:
      - system$1.yml
EOF
}

function gen_conf() {
cat <<EOF > system$1.yml
- targets:
  - 12.103.52.$i:13402
  labels:
    system: system$1
    application: system$1-app-A
    module: moduleA
    env: sit
EOF
}

function reload() {
  curl -X POST http://127.0.0.1:9090/-/reload
  sleep 5
}

function get_prom_target() {
  echo $(curl -s http://127.0.0.1:9090/api/v1/targets | jq ' .data.activeTargets | length ')
}

for i in {0..9}; do
  for j in {1..10}; do
    append_conf $(($i*10+$j))
    gen_conf $(($i*10+$j))
  done
  reload
  prom_target=$(get_prom_target)
  echo ${prom_target}
  if (( ${prom_target} != $(($i*10+10)) )); then
    echo "prometheus scrape target count ${prom_target} not match $(($i*10+10)) at round $(($i+1))"
    break
  fi
done

解释下这个脚本:

  1. 生成 prometheus 主配置文件中的 job
  2. 生成对应系统的 yaml 配置文件
  3. 每执行 10 次之后执行 reload 操作
  4. 比较 prometheus 抓取 target 数量是否正确

执行后产生输出:

1
2
3
4
5
6
7
8
9
$ ./gen.sh
10
20
30
40
50
60
69
prometheus scrape target count 69 not match 70 at round 7

在第 7 轮的时候,也就是添加了 70 个 job 的时候就出现了目标采集数量缺失的现象。

查看 prometheus 日志,发现报错信息:

1
level=error ts=2024-05-30T07:39:42.592Z caller=file.go:225 component="discovery manager scrape" discovery=file msg="Error adding file watcher" err="too many open files"

“too many open files” 好熟悉的错误,难道是文件描述符不够用了?

尝试增大 fs.file-max 参数值后发现这个问题依然存在,看来报错信息过于简单,只能从代码入手。

file.go:

1
2
3
4
5
6
7
8
9
// Run implements the Discoverer interface.
func (d *Discovery) Run(ctx context.Context, ch chan<- []*targetgroup.Group) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		level.Error(d.logger).Log("msg", "Error adding file watcher", "err", err)
		return
	}
	// ...
}

可以看出该报错是调用 fsnotify 库的 NewWatcher() 出错导致的

inotify.go:

1
2
3
4
5
6
7
8
9
// NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
func NewWatcher() (*Watcher, error) {
	// Create inotify fd
	fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC)
	if fd == -1 {
		return nil, errno
	}
	// ...
}

NewWatcher() 执行了系统调用,查看说明文档:

The fs.inotify.max_user_watches sysctl variable specifies the upper limit for the number of watches per user, and fs.inotify.max_user_instances specifies the maximum number of inotify instances per user. Every Watcher you create is an “instance”, and every path you add is a “watch”.

有两个系统变量影响了 inotfiy 的数量,可以在 /proc/sys/fs/inotify/ 路径下查看:

1
2
3
4
$ cat /proc/sys/fs/inotify/max_user_watches 
8192
$ cat /proc/sys/fs/inotify/max_user_instances 
128

查看 prometheus 已占用的 instance

1
2
$ lsof -p $(pidof prometheus) |grep notify |wc -l
68

虽然小于 fs.inotify.max_user_instances 定义的 128,但是这个值是针对用户级别的,可能有其他进程也有占用。

调整大小为 1024

1
2
$ sudo sysctl fs.inotify.max_user_instances=1024
fs.inotify.max_user_instances = 1024

调整后调整测试代码,增加轮数:

1
2
3
4
5
6
7
$ ./gen.sh
10
20
# skip output
200
209
prometheus scrape target count 209 not match 210 at round 21

增加到 200 多个才出现报错信息,说明是有效果的。

完。

This post is licensed under CC BY 4.0 by the author.