0
0
2
0
专栏/.../

TiDB 监控告警高可用

 Kassadar  发表于  2024-04-07
原创

引言

TiDB 是一个开源的分布式数据库,它采用 Prometheus + Alertmanager + Grafana 作为其核心监控组合,提供了强大的监控和报警能力。Prometheus 负责收集和存储TiDB以及其它组件的性能指标,Alertmanager 处理由 Prometheus 发出的报警,并且通过多种渠道发送通知,而 Grafana 则为用户提供了一个直观的数据可视化平台,使得用户可以方便地查看和理解监控数据。

在这个监控组合中,每个组件都扮演着至关重要的角色,但为了确保TiDB的高可用性和稳定性,监控系统本身也必须是高可用的。即便面对基础设施故障、网络问题或任何其他可能的故障场景,监控系统仍需要保持运行,确保问题能被及时发现并且处理。

解决方案

在不引入其他组件的情况下建立一个高可用的监控系统,可以部署两组独立的 Prometheus + Alertmanager + Grafana 堆栈。每组作为对方的热备份。以下是具体的步骤和配置:

1280X1280.PNG

Prometheus HA 配置

  1. 部署两个 Prometheus 实例:部署两个独立的 Prometheus 实例,每个实例都配置有相同的目标。这样,如果一个实例失败,另一个可以继续收集数据。

    1. Prometheus A 配置文件 (prometheus_A.yml)
    2. Prometheus B 配置文件 (prometheus_B.yml)
  2. 配置文件中的 scrape_configs 部分应该相同,以确保两个实例抓取相同的数据。

  3. 配置文件去重:你不能完全避免数据重复,但可以在查询时选择只从一个 Prometheus 实例查询。

  4. 独立存储:每个 Prometheus 实例应该具有自己的本地存储。

Alertmanager HA 配置

  1. 部署两个 Alertmanager 实例:每个 Prometheus 实例应配置自己的 Alertmanager 实例。

    1. Alertmanager A 配置文件 (alertmanager_A.yml)
    2. Alertmanager B 配置文件 (alertmanager_B.yml)
  2. 这两个 Alertmanager 实例应该具有相同的配置,以确保警报处理一致。

  3. 集群模式:alertmanager 采用 gossip 协议在多实例部署中同步警报状态和沉默配置,以确保一致性和高可用性。使两个实例彼此通信,避免收到警报时可能会收到两个相同的警报。

Grafana HA 配置

  1. 部署两个 Grafana 实例:每个 Grafana 实例连接到一个 Prometheus 实例。

    1. Grafana A 指向 Prometheus A
    2. Grafana B 指向 Prometheus B
  2. 相同的仪表板配置:确保两个 Grafana 实例有相同的仪表板和数据源配置,以便在主实例失败时,可以无缝切换到备份实例。

In Action

在编写 TiDB 集群的 meta.yml 时,分别配置 2 个 monitoring_servers、grafana_servers、alertmanager_servers

……

monitoring_servers:
- host: 127.0.0.1
  ssh_port: 22
  port: 9090
  ng_port: 12020
  deploy_dir: /home/kay/tidb-deploy/prom-9090
  data_dir: /home/kay/tidb-data/prom-9090
  log_dir: /home/kay/tidb-deploy/prom-9090/log
  external_alertmanagers: []
  arch: amd64
  os: linux
- host: 127.0.0.1
  ssh_port: 22
  port: 19090
  ng_port: 22020
  deploy_dir: /home/kay/tidb-deploy/prom-19090
  data_dir: /home/kay/tidb-data/prom-19090
  log_dir: /home/kay/tidb-deploy/prom-19090/log
  external_alertmanagers: []
  arch: amd64
  os: linux
  
grafana_servers:
- host: 127.0.0.1
  ssh_port: 22
  port: 3000
  deploy_dir: /home/kay/tidb-deploy/grafana-3000
  arch: amd64
  os: linux
  username: admin
  password: admin
  anonymous_enable: false
  root_url: ""
  domain: ""
- host: 127.0.0.1
  ssh_port: 22
  port: 13000
  deploy_dir: /home/kay/tidb-deploy/grafana-13000
  arch: amd64
  os: linux
  username: admin
  password: admin
  anonymous_enable: false
  root_url: ""
  domain: ""
  
alertmanager_servers:
- host: 127.0.0.1
  ssh_port: 22
  web_port: 9093
  cluster_port: 9094
  deploy_dir: /home/kay/tidb-deploy/alert-9093
  data_dir: /home/kay/tidb-data/alert-9093
  log_dir: /home/kay/tidb-deploy/alert-9093/log
  arch: amd64
  os: linux
- host: 127.0.0.1
  ssh_port: 22
  web_port: 19093
  cluster_port: 19094
  deploy_dir: /home/kay/tidb-deploy/alert-19093
  data_dir: /home/kay/tidb-data/alert-19093
  log_dir: /home/kay/tidb-deploy/alert-19093/log
  arch: amd64
  os: linux
  
  ……

部署后的集群拓扑示例如下:

2.png

下面我们看下此拓扑下生成的配置/启动文件,是否符合我们规划的预期

Prometheus 各自 scrape targets

分别登录 prometheus 的 ui,可以看到它们各自拉取 metrics

3.png

Prometheus 推送 Alert 的配置

Prometheus 会同时向 2 个 Alertmanager 推送报警信息,符合我们的预期

4.png

Alertmanager 的 Gossip 集群模式配置

Alertmanager 启动脚本中配置了 cluster.peer 列表,其会以集群的形式工作,符合我们的预期。

在使用 Alertmanager 的集群模式时,cluster.peer 配置项用于指定其他 Alertmanager 实例的地址,这样实例之间可以相互通信并同步数据,以确保集群中所有实例的状态一致。在集群中的每个 Alertmanager 实例都需要知道其他同伴(peers)的存在,以便它们可以相互交换警报状态和沉默配置。

5.png

在其 UI 的 Status Tab 中

6.png

Grafna 的 Prometheus DataSource 配置

两个 Grafana 中的 Prometheus 均会采用列表中的第一个值,因此需要修改其中一个 url 为另一个 prometheus 的地址。

7.png

去重验证

在 Alertmanager 的 HA 模式中,使用 gossip 协议同步的是报警状态,而不是原始的报警本身。这意味着每个 Alertmanager 实例仍然需要从 Prometheus 或其他监控系统接收到独立的报警事件。一旦报警到达任何一个 Alertmanager 节点,该节点会处理报警(例如,应用静默规则、发送通知等),并通过 gossip 协议与其他节点同步关于该报警的当前状态。

  1. 使用 AIGC 创建一个简单的 webhook
vi app.py

from flask import Flask, request, render_template_string, jsonify

app = Flask(__name__)

# A list to store alerts
alerts = []

# Index route to display received alerts
@app.route('/')
def index():
    # Render a HTML table to display the alerts
    return render_template_string("""
<html>
  <head>
    <title>Alertmanager Alerts</title>
    <style>
      table {
        width: 100%;
        border-collapse: collapse;
      }
      th, td {
        border: 1px solid #ddd;
        padding: 8px;
      }
      th {
        background-color: #f2f2f2;
        text-align: left;
      }
      pre {
        white-space: pre-wrap;       /* Since CSS 2.1 */
        white-space: -moz-pre-wrap;  /* Mozilla, since 1999 */
        white-space: -pre-wrap;      /* Opera 4-6 */
        white-space: -o-pre-wrap;    /* Opera 7 */
        word-wrap: break-word;       /* Internet Explorer 5.5+ */
      }
    </style>
  </head>
  <body>
    <h1>Received Alerts</h1>
    {% if alerts %}
    <table>
      <thead>
        <tr>
          <th>Time</th>
          <th>Status</th>
          <th>Labels</th>
          <th>Annotations</th>
        </tr>
      </thead>
      <tbody>
      {% for alert in alerts %}
        <tr>
          <td>{{ alert['startsAt'] }}</td>
          <td>{{ alert['status'] }}</td>
          <td><pre>{{ alert['labels'] | tojson | safe }}</pre></td>
          <td><pre>{{ alert['annotations'] | tojson | safe }}</pre></td>
        </tr>
      {% endfor %}
      </tbody>
    </table>
    {% else %}
    <p>No alerts received yet.</p>
    {% endif %}
  </body>
</html>
""", alerts=alerts)

# Webhook route to receive alerts
@app.route('/alert', methods=['POST'])
def alert():
    # Retrieve JSON data from the request
    alert_data = request.json
    if not alert_data:
        return jsonify({"status": "error", "message": "Invalid JSON data"}), 400
    
    try:
        # Assume Alertmanager sends a list of alerts
        received_alerts = alert_data['alerts']
        alerts.extend(received_alerts)
        return jsonify({"status": "success", "message": "Alert received"}), 200
    except KeyError:
        # Handle the case where 'alerts' key is not present in the received JSON
        return jsonify({"status": "error", "message": "Missing 'alerts' key in JSON data"}), 400
    except Exception as e:
        # Handle other unexpected errors
        return jsonify({"status": "error", "message": str(e)}), 500

if __name__ == '__main__':
    app.run(port=5001, debug=True)

启动

FLASK_APP=app.py flask run --port=5001

8.png

  1. 设置 AlertManager Receiver,编辑 alertmanager.yml
……
route:
  receiver: 'local-webhook'

receivers:
- name: 'local-webhook'
  webhook_configs:
  - url: 'http://localhost:5001/alert'
 ……
  1. 分别推送报警信息到 AlertManager
curl -XPOST -d '[{"labels":{"alertname":"testalert","instance":"node1","severity":"warning"}}]' <node1-ip>:<alertmanager-web-port>/api/v1/alerts

curl -XPOST -d '[{"labels":{"alertname":"testalert","instance":"node1","severity":"warning"}}]' <node2-ip>:<alertmanager-web-port>/api/v1/alerts

可以看到只收到了一份报警,符合我们的预期

9.png

其他考虑因素

当然,关于创建完全冗余和高可用性的 Prometheus 监控设置还有很多内容可以讨论。以下是一些其他与高可用性相关的课题:

  1. Prometheus 数据同步:如果两个 Prometheus 实例采集的目标和时间不一致,那么这两套系统的数据可能会存在差异。这可能导致警报不一致,即同一个问题可能只在一个 Prometheus 实例上触发警报。为了解决这个问题,可以使用 Prometheus 的远程写入功能,将数据复制到一个共同的远程存储中,或者使用 Thanos 这类支持 Prometheus 长期存储和全局视图的工具。
  2. 时间同步问题:Prometheus 依赖于时间序列数据,因此确保所有服务器的时间同步非常重要。时间偏差可能导致数据收集的不一致性,影响指标查询和警报的准确性。
  3. Prometheus 之间未互相监控:如果 Prometheus 实例不监控彼此,可能无法检测到另一个实例的故障。为了增加系统的健壮性,每个 Prometheus 应该监控对方的健康状态和性能指标。

0
0
2
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论