用 Prometheus group_left 关联服务 owner 元数据并按 team 聚合
问题场景
服务请求指标 http_requests_total{service,method,code} 通常只带服务名,团队 owner 信息维护在配置或 CMDB 中。直接把 team 写进请求指标会带来两个问题:标签随组织调整而变动,且每条时间序列都重复存储元数据。更合适的做法是建立一条低基数的 info 指标,例如包含 service、team、owner 标签且值为 1 的 service_owner_info。请求指标保持原样,查询时用 group_left 把 team 附加到请求指标上,再按 team 聚合。
最小可运行示例
下面用一个 Python 标准库 exporter 暴露示例指标。计数为静态占位值,仅用于验证 PromQL 结构,不表示真实流量。
from http.server import BaseHTTPRequestHandler, HTTPServer
REQUESTS = [
('checkout', 'GET', '200', 120),
('checkout', 'POST', '500', 3),
('catalog', 'GET', '200', 80),
]
OWNERS = {
'checkout': ('payments', 'alice'),
'catalog': ('storefront', 'bob'),
}
def render():
q = chr(34)
lines = []
for service, method, code, value in REQUESTS:
lines.append(
'http_requests_total{service=' + q + service +
',method=' + q + method + ',code=' + q + code + '} ' + str(value)
)
for service, (team, owner) in OWNERS.items():
lines.append(
'service_owner_info{service=' + q + service +
',team=' + q + team + ',owner=' + q + owner + '} 1'
)
return chr(10).join(lines) + chr(10)
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path != '/metrics':
self.send_response(404)
self.end_headers()
return
body = render().encode()
self.send_response(200)
self.send_header('Content-Type', 'text/plain; version=0.0.4')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
if __name__ == '__main__':
HTTPServer(('127.0.0.1', 9101), Handler).serve_forever()
保存为 exporter.py,运行并检查输出:
python3 exporter.py
curl -s http://127.0.0.1:9101/metrics
再准备 prometheus.yml:
global:
scrape_interval: 15s
scrape_configs:
- job_name: demo
static_configs:
- targets: ['127.0.0.1:9101']
启动 Prometheus(配置文件与 exporter 在同一目录):
promtool check config prometheus.yml
prometheus --config.file=prometheus.yml
在 Prometheus 查询界面执行:
sum by (team) (
http_requests_total
* on(service) group_left(team) service_owner_info
)
示例数据应得到 payments = 123、storefront = 80。生产环境应把左侧换成 rate(http_requests_total[5m]):
sum by (team) (
rate(http_requests_total[5m])
* on(service) group_left(team) service_owner_info
)
on(service) 表示只按 service 匹配;group_left(team) 表示左侧可以有多条请求序列,右侧每个 service 只能有一条 info 序列,并把 team 附加到结果。若还要保留 owner,可写成 group_left(team, owner)。
验证是否有放大
先确认 info 指标每个服务唯一:
count by (service) (service_owner_info)
每个 service 应为 1。找重复:
count by (service) (service_owner_info) > 1
结果应为空。再比较 join 前后的序列数:
count(http_requests_total)
count(http_requests_total * on(service) group_left(team) service_owner_info)
无放大时两者应相等。还可以检查没有 owner 的服务:
count by (service) (http_requests_total)
unless on(service) service_owner_info
最后比较全局总量与按 team 汇总的总量:
sum(rate(http_requests_total[5m]))
sum(
sum by (team) (
rate(http_requests_total[5m])
* on(service) group_left(team) service_owner_info
)
)
若所有请求都能匹配且 info 唯一,两者应一致。
边界与限制
service标签必须在两侧完全一致,大小写、空格、环境后缀都要统一;缺失service的请求序列不会参与 join,按团队汇总会漏掉。- 一个
service只能对应一条service_owner_info。如果同一服务有多个团队,不要用多条 info 直接做group_left,否则会出现重复匹配,查询可能失败或口径被放大。应明确主 owner,或另建多对多关系并单独定义统计口径。 team、owner属于组织元数据,基数应受控;不要把请求 ID、实例 IP、工单号放进 info 指标。- 元数据变更后要等下一次抓取才生效;变更期间 join 结果可能为空或仍为旧值,需要结合发布流程处理。
- 示例中的静态计数只用于验证查询结构。实际使用 counter 时注意
rate窗口、重启和抓取间隔;窗口过短可能没有足够样本。
- 点赞
- 收藏
- 关注作者
评论(0)