1

Prometheus采不到数据了!居然是Prometheus client包的锅

 2 years ago
source link: https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Prometheus采不到数据了!居然是Prometheus client包的锅

prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package-1.png

本文永久链接 – https://tonybai.com/2022/06/15/prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package

在基于eBPF的新一代观测设施尚未成熟之前,我们采用了业界成熟的Prometheus+Grafana方案采集节点与应用度量指标(metrics)信息。众所周知,这样的方案是一种对应用有侵入的方案,即需要在应用内部嵌入采集度量信息及与Prometheus通信的client包。

Prometheus官方提供并维护了主流语言的client包,包括Go、Java、Python、Ruby、Rust等,如下图:

prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package-2.png

Prometheus的go client端使用起来也不算复杂,总共分两步:

  • 把你要获取的度量指标注册(Register)到Prometheus的Registry中;
  • 建立起一个HTTP Server,暴露度量指标采集端口即可。

Prometheus采用拉模型(pull)收集时序度量数据,数据拉取行为是由Prometheus服务端来决定的,比如可以设定Prometheus拉取各个采集点的时间周期。 一般来说,这个技术栈已经很成熟,配置完启动后,马上就能看到效果。这个技术栈也很稳定,我们使用后一直运行良好,直到本周压测时遇到一个问题:Prometheus采不到数据了

从最初的数据由连续的线变成“断断续续”的点,见下图:

prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package-3.png

到后来干脆就无法采到任何数据了:

prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package-4.png

之前Prometheus跑的好好的,为什么现在却采不到数据了呢?这次与之前的不同之处在于我们的压测用例情景下,每个服务节点都要建立百万以上的连接,而之前仅仅是10w左右的量级。

好在我们部署了在线Continuous Profiling工具,可以查看一下压测那段时间的资源占用,如下图:

prometheus-can-not-pick-up-data-because-of-the-prometheus-client-package-5.png

上面是一个alloc object的火焰图,我们看到Prometheus client的Registry.Gather方法占了50%的内存分配开销,这是很不正常的。继续沿着Gather函数的火焰图看,我们看到底端居然是readdir。我们应用注册的度量指标也没有哪个采集时需要readdir啊!

要想解决这个问题只有翻Prometheus client源码了!

我们使用的是prometheus client端的默认defaultRegistry。 从源码中可以看到:这个defaultRegistry在初始化时,默认注册了两个collector:

// registry.go
func init() {
    MustRegister(NewProcessCollector(ProcessCollectorOpts{}))
    MustRegister(NewGoCollector())
}

我们发现:第一个processCollector会采集如下度量指标数据:

// process_collector.go
func (c *processCollector) Describe(ch chan<- *Desc) {
    ch <- c.cpuTotal
    ch <- c.openFDs
    ch <- c.maxFDs
    ch <- c.vsize
    ch <- c.maxVsize
    ch <- c.rss
    ch <- c.startTime
}

在采集openFDs时,processCollector遍历了/proc/{pid}下面的fd目录:

// process_collector_other.go
func (c *processCollector) processCollect(ch chan<- Metric) {
    pid, err := c.pidFn()
    if err != nil {
        c.reportError(ch, nil, err)
        return
    }

    p, err := procfs.NewProc(pid)
    if err != nil {
        c.reportError(ch, nil, err)
        return
    }

    if stat, err := p.Stat(); err == nil {
        ch <- MustNewConstMetric(c.cpuTotal, CounterValue, stat.CPUTime())
        ch <- MustNewConstMetric(c.vsize, GaugeValue, float64(stat.VirtualMemory()))
        ch <- MustNewConstMetric(c.rss, GaugeValue, float64(stat.ResidentMemory()))
        if startTime, err := stat.StartTime(); err == nil {
            ch <- MustNewConstMetric(c.startTime, GaugeValue, startTime)
        } else {
            c.reportError(ch, c.startTime, err)
        }
    } else {
        c.reportError(ch, nil, err)
    }

    if fds, err := p.FileDescriptorsLen(); err == nil { // 这里获取openFDs
        ch <- MustNewConstMetric(c.openFDs, GaugeValue, float64(fds))
    } else {
        c.reportError(ch, c.openFDs, err)
    }

    if limits, err := p.Limits(); err == nil {
        ch <- MustNewConstMetric(c.maxFDs, GaugeValue, float64(limits.OpenFiles))
        ch <- MustNewConstMetric(c.maxVsize, GaugeValue, float64(limits.AddressSpace))
    } else {
        c.reportError(ch, nil, err)
    }
}

采集openFDS时,processCollector调用了FileDescriptorsLen方法,在FileDescriptorsLen方法调用的fileDescriptors方法中,我们找到了对Readdirnames的调用,见下面源码片段:

// github.com/prometheus/procfs/proc.go

// FileDescriptorsLen returns the number of currently open file descriptors of
// a process.
func (p Proc) FileDescriptorsLen() (int, error) {
    fds, err := p.fileDescriptors()
    if err != nil {
        return 0, err
    }

    return len(fds), nil
}  

func (p Proc) fileDescriptors() ([]string, error) {
    d, err := os.Open(p.path("fd"))
    if err != nil {
        return nil, err
    }
    defer d.Close()

    names, err := d.Readdirnames(-1) // 在这里遍历目录中的文件
    if err != nil {
        return nil, fmt.Errorf("could not read %q: %w", d.Name(), err)
    }

    return names, nil
}

通常情况下,读取/proc/{pid}/fd目录是没有问题的,但当我们的程序上连接着100w+的连接时,意味着fd目录下有100w+的文件,逐一遍历这些文件将带来很大的开销。这就是导致Prometheus在超时时间(通常是10几秒)内无法及时采集到数据的原因。

那么如何解决这个问题呢?

临时解决方法是将registry.go文件的init函数中的MustRegister(NewProcessCollector(ProcessCollectorOpts{}))这一行注释掉!这个进程度量指标信息对我们用处不大。不过这样做的不足是我们自己需要维护一份prometheus golang client包,需要用到go mod replace,十分不便,并且不便于prometheus golang client包的版本升级。

一劳永逸的解决方法是:不使用默认Registry,而是使用NewRegistry函数新建一个Registry。这样我们抛开默认注册的那些度量指标,并可以自行定义我们要注册的度量指标。需要时,我们也可以将ProcessCollector加进来,这个根据不同Go程序的需要而定。

按这个方案修改后,那些熟悉的连续曲线就又重现在眼前了!


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}
img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://51smspush.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
iamtonybai-wechat-qr.png

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

© 2022, bigwhite. 版权所有.

Related posts:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK