当前位置:   article > 正文

Nginx监控不再难:简化部署流程,提升监控效率

Nginx监控不再难:简化部署流程,提升监控效率

前段时间接到一个需求,希望可以监控 Nginx 的运行状态。

我们都知道 Nginx 作为一个流行的 Web 服务器提供了多种能力,包括反向代理、负载均衡;也支持了许多协议,包括:

  • gRPC

  • http

  • WebSocket 等 作为一个流量入口的中间件,对其的监控就显得至关重要了。

市面上也有一些现成的产品可以监控 Nginx,比如知名的监控服务商 datadog 也提供了 Nginx 的监控。

8e97a4611de35f2eaa529be46b397252.png

但是我这是一个内网服务,并不能使用这些外部的云厂商,所有就只能在内部搭建 Nginx 的监控服务了。

不过 Nginx 默认情况下并没有提供 /metrics 的 endpoint,但好在它提供了一个额外的模块:stub_status 可以用于获取监控数据。

  1. server {
  2.       listen 80;
  3.       server_name _;
  4.       location /status {
  5.         stub_status on;
  6.         access_log off;
  7.       }
  8.       location / {
  9.           root /usr/share/nginx/html;
  10.           index index.html index.htm;
  11.       }
  12.     }

5850f9085c618a0f65ed96344d84a1d1.png这样访问 http://127.0.0.1:80/status 就可以拿到一些基本的运行数据。

但这个格式明显不是 Prometheus 所支持的 metrics 格式,无法直接将数据采集到 Prometheus 中然后通过 Grafana 进行查看。

所以还得需要一个中间层来将这些数据转换为 Prometheus 可以接收的 metrics 数据。

nginx-prometheus-exporter

好在社区已经提供了类似的工具:nginx-prometheus-exporter 它读取刚才 status endpoint 所暴露的数据,然后转换为 Prometheus 格式,并对外提供了一个 /metrics 的 endpoint 供 Prometheus 来采集。

转换数据

我们在启动这个 nginx-exporter 时需要传入刚才 Nginx 暴露的 /status endpoint。

docker run -p 9113:9113 nginx/nginx-prometheus-exporter:1.1.0 --nginx.scrape-uri=http://<nginx>:8080/stub_status
  1. const templateMetrics string = `Active connections: %d
  2. server accepts handled requests
  3. %d %d %d
  4. Reading: %d Writing: %d Waiting: %d
  5. `
  6. // 读取 Nginx status 数据
  7. body, err := io.ReadAll(resp.Body)
  8. if err != nil {
  9.  return nil, fmt.Errorf("failed to read the response body: %w", err)
  10. }
  11. r := bytes.NewReader(body)
  12. stats, err := parseStubStats(r)
  13. if err != nil {
  14.  return nil, fmt.Errorf("failed to parse response body %q: %w"string(body), err)
  15. }
  16. // 解析 Nginx status 数据
  17. func parseStubStats(r io.Reader) (*StubStats, error) {
  18.  var s StubStats
  19.  if _, err := fmt.Fscanf(r, templateMetrics,
  20.   &s.Connections.Active,
  21.   &s.Connections.Accepted,
  22.   &s.Connections.Handled,
  23.   &s.Requests,
  24.   &s.Connections.Reading,
  25.   &s.Connections.Writing,
  26.   &s.Connections.Waiting); err != nil {
  27.   return nil, fmt.Errorf("failed to scan template metrics: %w", err)
  28.  }
  29.  return &s, nil
  30. }

最后会把刚才解析到的数据生成 metrics:

  1. ch <- prometheus.MustNewConstMetric(c.metrics["connections_active"],  
  2.     prometheus.GaugeValue, float64(stats.Connections.Active))  
  3. ch <- prometheus.MustNewConstMetric(c.metrics["connections_accepted"],  
  4.     prometheus.CounterValue, float64(stats.Connections.Accepted))  
  5. ch <- prometheus.MustNewConstMetric(c.metrics["connections_handled"],  
  6.     prometheus.CounterValue, float64(stats.Connections.Handled))  
  7. ch <- prometheus.MustNewConstMetric(c.metrics["connections_reading"],  
  8.     prometheus.GaugeValue, float64(stats.Connections.Reading))  
  9. ch <- prometheus.MustNewConstMetric(c.metrics["connections_writing"],  
  10.     prometheus.GaugeValue, float64(stats.Connections.Writing))  
  11. ch <- prometheus.MustNewConstMetric(c.metrics["connections_waiting"],  
  12.     prometheus.GaugeValue, float64(stats.Connections.Waiting))  
  13. ch <- prometheus.MustNewConstMetric(c.metrics["http_requests_total"],  
  14.     prometheus.CounterValue, float64(stats.Requests))

这些 metrics 是一开始就定义好的:

  1. // NewNginxCollector creates an NginxCollector.
  2. func NewNginxCollector(nginxClient *client.NginxClient, namespace string, constLabels map[string]string, logger log.Logger) *NginxCollector {
  3.  return &NginxCollector{
  4.   nginxClient: nginxClient,
  5.   logger:      logger,
  6.   metrics: map[string]*prometheus.Desc{
  7.    "connections_active":   newGlobalMetric(namespace, "connections_active""Active client connections", constLabels),
  8.    "connections_accepted": newGlobalMetric(namespace, "connections_accepted""Accepted client connections", constLabels),
  9.    "connections_handled":  newGlobalMetric(namespace, "connections_handled""Handled client connections", constLabels),
  10.    "connections_reading":  newGlobalMetric(namespace, "connections_reading""Connections where NGINX is reading the request header", constLabels),
  11.    "connections_writing":  newGlobalMetric(namespace, "connections_writing""Connections where NGINX is writing the response back to the client", constLabels),
  12.    "connections_waiting":  newGlobalMetric(namespace, "connections_waiting""Idle client connections", constLabels),
  13.    "http_requests_total":  newGlobalMetric(namespace, "http_requests_total""Total http requests", constLabels),
  14.   },
  15.   upMetric: newUpMetric(namespace, constLabels),
  16.  }
  17. }

而这个函数是在 exporter 启动时候会调用:

  1. "github.com/prometheus/client_golang/prometheus"
  2. prometheus.MustRegister(collector.NewNginxCollector(ossClient, "nginx", labels, logger))

使用的是 prometheus 包提供的注册函数,将我们刚才自定义的获取 metrics 的逻辑注册进去,这样当我们在 Prometheus 中配置好采集任务之后就可以定期扫描 /status 的数据然后转换为 Prometheus 指标返回。

  1. global:
  2.   scrape_interval: 10s
  3. scrape_configs:
  4.   - job_name: nginx-exportor
  5.     static_configs:
  6.     - targets: ['127.0.0.1:9113']

这样就可以将 nginx status 的数据定期采集到 Prometheus 中了,最后使用社区提供的 grafana 面板便可以可视化的查看这些监控数据:c9bf3ef198ec50f56665a39754c188f1.png

Nginx Plus

同时这个 nginx-exporter 还支持 Nginx Plus(这是 Nginx 的商用增强版),它的实现原理类似,只是它支持的指标更多一些而已。

  1. type NginxPlusCollector struct {  
  2.     upMetric                       prometheus.Gauge  
  3.     logger                         log.Logger  
  4.     cacheZoneMetrics               map[string]*prometheus.Desc  
  5.     workerMetrics                  map[string]*prometheus.Desc  
  6.     nginxClient                    *plusclient.NginxClient  
  7.     streamServerZoneMetrics        map[string]*prometheus.Desc  
  8.     streamZoneSyncMetrics          map[string]*prometheus.Desc  
  9.     streamUpstreamMetrics          map[string]*prometheus.Desc  
  10.     streamUpstreamServerMetrics    map[string]*prometheus.Desc  
  11.     locationZoneMetrics            map[string]*prometheus.Desc  
  12.     resolverMetrics                map[string]*prometheus.Desc  
  13.     limitRequestMetrics            map[string]*prometheus.Desc  
  14.     limitConnectionMetrics         map[string]*prometheus.Desc  
  15.     streamLimitConnectionMetrics   map[string]*prometheus.Desc  
  16.     upstreamServerMetrics          map[string]*prometheus.Desc  
  17.     upstreamMetrics                map[string]*prometheus.Desc  
  18.     streamUpstreamServerPeerLabels map[string][]string  
  19.     serverZoneMetrics              map[string]*prometheus.Desc  
  20.     upstreamServerLabels           map[string][]string  
  21.     streamUpstreamServerLabels     map[string][]string  
  22.     serverZoneLabels               map[string][]string  
  23.     streamServerZoneLabels         map[string][]string  
  24.     upstreamServerPeerLabels       map[string][]string  
  25.     workerLabels                   map[string][]string  
  26.     cacheZoneLabels                map[string][]string  
  27.     totalMetrics                   map[string]*prometheus.Desc  
  28.     variableLabelNames             VariableLabelNames  
  29.     variableLabelsMutex            sync.RWMutex  
  30.     mutex                          sync.Mutex  
  31. }

Prometheus 社区中提供不少这类 exporter83cda7f61f7534a7fc56df2ace600f7e.png

这些 exporter 要解决的问题都是类似的,对于一些没有暴露 /metrics 的中间件通过他们提供的客户端直连,然后将获取到的数据转换为 Prometheus 所支持的格式。

需要单独的 exporter 支持的中间件大部分都是一些老牌产品,在设计之初就没有考虑可观测性的需求,现在一些新的中间件几乎都原生支持 metrics,这种产品只需要在 Prometheus 中配置采集任务即可。

Cprobe

不知道大家发现没有,社区中提供的 exporter 还是挺多的,但如果我们都需要在自己的生产环境将这些 exporter 部署起来多少会有些繁琐:

  • 不同的 exporter 需要的参数可能不同

  • 暴露的端口可能不同

  • 配置文件难以统一管理

在这个背景下社区有大佬发起了一个 cprobe 项目,这是一个大而全的项目,可以将散落在各处的 exporter 都整合在一起。

并且统一抽象了接入方式,使得所有的插件都可以用类似的配置书写方式来维护这些插件。

目前已经支持以下一些常用的中间件:

106a8a0141976dac5d09876a26860cd8.png

这里的 Nginx 就是本次监控的需求贡献的,因为还需要监控这里支持的一些其他中间件,所以最终也是使用 cprobe 来部署监控。

整合 Nginx exporter 到 Cprobe 中

下面来看看如何将社区中已经存在的 Nginx exporter 整合到  cprobe 中:

在开始之前我们先要抽象出这个插件需要哪些配置?

这个其实很好解决,我们直接看看需要实现的 exporter 中提供了哪些参数,这里以 Nginx 的为例:729aff5473fe65fc344ad325c8ae838e.png

排除掉一些我们不需要的,比如端口、日志级别、endpoint等配置之外,就只需要一些关于 SSL 的配置,所以最终我们需要的配置文件如下:

  1. nginx_plus = false
  2. # Path to the PEM encoded CA certificate file used to validate the servers SSL certificate.
  3. ssl_ca_cert = ''
  4. # Path to the PEM encoded client certificate file to use when connecting to the server.
  5. ssl_client_cert = ''
  6. # Path to the PEM encoded client certificate key file to use when connecting to the server.
  7. ssl_client_key = ''
  8. # Perform SSL certificate verification.
  9. ssl_verify = false
  10. timeout = '5s'

然后将这个 toml 里的配置转换为一个 struct。

在 cprobe 中有一个核心的接口:

  1. type Plugin interface {
  2.  // ParseConfig is used to parse config
  3.  ParseConfig(baseDir string, bs []byte) (any, error)
  4.  // Scrape is used to scrape metrics, cfg need to be cast specific cfg
  5.  Scrape(ctx context.Context, target string, cfg any, ss *types.Samples) error
  6. }

ParseConfig 用于将刚才的配置文件流格式化为插件所需要的配置。

Scrape 函数则是由 cprobe 定时调用的函数,会传入抓取的目标地址,每个插件将抓到的数据写入 *types.Samples 中即可。

cprobe 会将 *types.Samples 的数据发送到 remote 的 Prometheus 中。

接下来看看 Nginx 插件的实现:

  1. type Config struct {
  2.  NginxPlus     bool          `toml:"nginx_plus"`
  3.  SSLCACert     string        `toml:"ssl_ca_cert"`
  4.  SSLClientCert string        `toml:"ssl_client_cert"`
  5.  SSLClientKey  string        `toml:"ssl_client_key"`
  6.  SSLVerify     bool          `toml:"ssl_verify"`
  7.  Timeout       time.Duration `toml:"timeout"`
  8. }
  9. func (n *Nginx) ParseConfig(baseDir string, bs []byte) (any, error) {
  10.  var c Config
  11.  err := toml.Unmarshal(bs, &c)
  12.  if err != nil {
  13.   return nil, err
  14.  }
  15.  if c.Timeout == 0 {
  16.   c.Timeout = time.Millisecond * 500
  17.  }
  18.  return &c, nil
  19. }

ParseConfig 很简单,就是将配置文件转换为 struct。

抓取函数 Scrape 也很简单:

  1. collect, err := registerCollector(transport, target, nil, conf)  
  2. if err != nil {  
  3.     return err  
  4. }  
  5.   
  6. ch := make(chan prometheus.Metric)  
  7. go func() {  
  8.     collect.Collect(ch)  
  9.     close(ch)  
  10. }()

就是构建之前在 nginx exporter 中的 prometheus.Collector,其实代码大部分也是从那边复制过来的。88189e61ed3060ab3710d01760db24f4.png15879724a828e600ad4f028712233037.png所以其实迁移一个 exporter 到 cprobe 中非常简单,只需要:

  • 定义好需要的配置。

  • 去掉不需要的代码,比如日志、端口之类的。

  • 适配好刚才那两个核心函数 ParseConfig/Scrape 即可。

但这样也有些小问题,现有的一些 exporter 还在迭代,那边更新的版本需要有人及时同步过来。

除非有一天 cprobe 可以作为一个标准,版本更新都在 cprobe 这边完成,这样就真的是做大做强了。

不过这些依旧是适配老一代的中间件产品,逐步都会适配现代的可观测体系,这些 exporter 也会逐渐走下历史舞台。

最后夹带一点私活,最近做了一个知识星球,已经试运行一段时间,效果还不错(具体详情可以扫码查看);感兴趣的朋友可以扫码领取优惠券以 49 元的价格加入(支持三天内无条件退款)。

4739b950338c9027f86045a4c29269d9.png

参考链接:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/880110?site
推荐阅读
相关标签
  

闽ICP备14008679号