引言
TiDB 是一个开源的分布式数据库,它采用 Prometheus + Alertmanager + Grafana 作为其核心监控组合,提供了强大的监控和报警能力。Prometheus 负责收集和存储TiDB以及其它组件的性能指标,Alertmanager 处理由 Prometheus 发出的报警,并且通过多种渠道发送通知,而 Grafana 则为用户提供了一个直观的数据可视化平台,使得用户可以方便地查看和理解监控数据。
在这个监控组合中,每个组件都扮演着至关重要的角色,但为了确保TiDB的高可用性和稳定性,监控系统本身也必须是高可用的。即便面对基础设施故障、网络问题或任何其他可能的故障场景,监控系统仍需要保持运行,确保问题能被及时发现并且处理。
解决方案
在不引入其他组件的情况下建立一个高可用的监控系统,可以部署两组独立的 Prometheus + Alertmanager + Grafana 堆栈。每组作为对方的热备份。以下是具体的步骤和配置:
Prometheus HA 配置
-
部署两个 Prometheus 实例:部署两个独立的 Prometheus 实例,每个实例都配置有相同的目标。这样,如果一个实例失败,另一个可以继续收集数据。
- Prometheus A 配置文件 (
prometheus_A.yml
) - Prometheus B 配置文件 (
prometheus_B.yml
)
- Prometheus A 配置文件 (
-
配置文件中的
scrape_configs
部分应该相同,以确保两个实例抓取相同的数据。 -
配置文件去重:你不能完全避免数据重复,但可以在查询时选择只从一个 Prometheus 实例查询。
-
独立存储:每个 Prometheus 实例应该具有自己的本地存储。
Alertmanager HA 配置
-
部署两个 Alertmanager 实例:每个 Prometheus 实例应配置自己的 Alertmanager 实例。
- Alertmanager A 配置文件 (
alertmanager_A.yml
) - Alertmanager B 配置文件 (
alertmanager_B.yml
)
- Alertmanager A 配置文件 (
-
这两个 Alertmanager 实例应该具有相同的配置,以确保警报处理一致。
-
集群模式:alertmanager 采用 gossip 协议在多实例部署中同步警报状态和沉默配置,以确保一致性和高可用性。使两个实例彼此通信,避免收到警报时可能会收到两个相同的警报。
Grafana HA 配置
-
部署两个 Grafana 实例:每个 Grafana 实例连接到一个 Prometheus 实例。
- Grafana A 指向 Prometheus A
- Grafana B 指向 Prometheus B
-
相同的仪表板配置:确保两个 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
……
部署后的集群拓扑示例如下:
下面我们看下此拓扑下生成的配置/启动文件,是否符合我们规划的预期
Prometheus 各自 scrape targets
分别登录 prometheus 的 ui,可以看到它们各自拉取 metrics
Prometheus 推送 Alert 的配置
Prometheus 会同时向 2 个 Alertmanager 推送报警信息,符合我们的预期
Alertmanager 的 Gossip 集群模式配置
Alertmanager 启动脚本中配置了 cluster.peer 列表,其会以集群的形式工作,符合我们的预期。
在使用 Alertmanager 的集群模式时,
cluster.peer
配置项用于指定其他 Alertmanager 实例的地址,这样实例之间可以相互通信并同步数据,以确保集群中所有实例的状态一致。在集群中的每个 Alertmanager 实例都需要知道其他同伴(peers)的存在,以便它们可以相互交换警报状态和沉默配置。
在其 UI 的 Status Tab 中
Grafna 的 Prometheus DataSource 配置
两个 Grafana 中的 Prometheus 均会采用列表中的第一个值,因此需要修改其中一个 url 为另一个 prometheus 的地址。
去重验证
在 Alertmanager 的 HA 模式中,使用 gossip 协议同步的是报警状态,而不是原始的报警本身。这意味着每个 Alertmanager 实例仍然需要从 Prometheus 或其他监控系统接收到独立的报警事件。一旦报警到达任何一个 Alertmanager 节点,该节点会处理报警(例如,应用静默规则、发送通知等),并通过 gossip 协议与其他节点同步关于该报警的当前状态。
- 使用 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
- 设置 AlertManager Receiver,编辑 alertmanager.yml
……
route:
receiver: 'local-webhook'
receivers:
- name: 'local-webhook'
webhook_configs:
- url: 'http://localhost:5001/alert'
……
- 分别推送报警信息到 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
可以看到只收到了一份报警,符合我们的预期
其他考虑因素
当然,关于创建完全冗余和高可用性的 Prometheus 监控设置还有很多内容可以讨论。以下是一些其他与高可用性相关的课题:
- Prometheus 数据同步:如果两个 Prometheus 实例采集的目标和时间不一致,那么这两套系统的数据可能会存在差异。这可能导致警报不一致,即同一个问题可能只在一个 Prometheus 实例上触发警报。为了解决这个问题,可以使用 Prometheus 的远程写入功能,将数据复制到一个共同的远程存储中,或者使用 Thanos 这类支持 Prometheus 长期存储和全局视图的工具。
- 时间同步问题:Prometheus 依赖于时间序列数据,因此确保所有服务器的时间同步非常重要。时间偏差可能导致数据收集的不一致性,影响指标查询和警报的准确性。
- Prometheus 之间未互相监控:如果 Prometheus 实例不监控彼此,可能无法检测到另一个实例的故障。为了增加系统的健壮性,每个 Prometheus 应该监控对方的健康状态和性能指标。