如何命名你的指标

指标是可观测性的量化支柱——它们是告诉我们系统运行状况的数字。这是我们 OpenTelemetry 命名系列的第三篇文章,我们已经探讨了如何命名 Span如何用有意义的属性丰富它们。现在让我们来处理命名那些重要测量值的艺术。

与讲述事件的 Span 不同,指标告诉我们数量:有多少,多快,多少。但关键在于——好好命名它们和命名 Span 一样至关重要,而且我们已经学过的原则在这里同样适用。“谁”仍然应该放在属性中,而不是名称里。

从传统系统学习

在深入研究 OpenTelemetry 最佳实践之前,让我们先审视一下传统的监控系统是如何处理指标命名的。以 Kubernetes 为例。它的指标遵循诸如

  • apiserver_request_total
  • scheduler_schedule_attempts_total
  • container_cpu_usage_seconds_total
  • kubelet_volume_stats_used_bytes

这样的模式。注意到模式了吗?**组件名 + 资源 + 操作 + 单位**。服务或组件名被直接嵌入到指标名称中。这种方法在数据模型较简单、存储上下文选项有限的情况下是合理的。

但这样做会带来几个问题

  • **可观测性后端混乱**:每个组件都有自己的指标命名空间,使得在数十或数百个同名指标中找到正确的指标更加困难。
  • **聚合不灵活**:难以跨不同组件对指标进行求和。
  • **供应商锁定**:指标名称会与特定实现绑定。
  • **维护开销**:添加新服务需要新的指标名称。

核心反模式:指标名称中的服务名称

以下是 OpenTelemetry 指标最重要的原则:**不要将你的服务名称包含在指标名称中**。

假设你有一个支付服务。你可能会想要创建这样的指标:

  • payment.transaction.count
  • payment.latency.p95
  • payment.error.rate

不要这样做。服务名称已经通过 `service.name` 资源属性作为上下文可用。取而代之的是,使用:

  • `transaction.count`,并附带 `service.name=payment`
  • `http.server.request.duration`,并附带 `service.name=payment`
  • `error.rate`,并附带 `service.name=payment`

为什么这样更好?因为现在你可以轻松地跨所有服务进行聚合。

sum(transaction.count)  // All transactions across all services
sum(transaction.count{service.name="payment"})  // Just payment transactions

如果每个服务都有自己的指标名称,你需要知道所有服务名称才能构建有意义的仪表板。有了清晰的名称,一个查询就可以适用于所有内容。

OpenTelemetry 丰富的上下文模型

OpenTelemetry 指标得益于我们在 Span 属性文章中讨论过的丰富的上下文模型。我们不必将一切都塞进指标名称,而是有多个层级可以存放上下文:

传统方法 (Prometheus 风格):

payment_service_transaction_total{method="credit_card",status="success"}
user_service_auth_latency_milliseconds{endpoint="/login",region="us-east"}
inventory_service_db_query_seconds{table="products",operation="select"}

OpenTelemetry 方法:

transaction.count
- Resource: service.name=payment, service.version=1.2.3, deployment.environment.name=prod
- Scope: instrumentation.library.name=com.acme.payment, instrumentation.library.version=2.1.0
- Attributes: method=credit_card, status=success

auth.duration
- Resource: service.name=user, service.version=2.0.1, deployment.environment.name=prod
- Scope: instrumentation.library.name=express.middleware
- Attributes: endpoint=/login, region=us-east
- Unit: ms

db.client.operation.duration
- Resource: service.name=inventory, service.version=1.5.2
- Scope: instrumentation.library.name=postgres.client
- Attributes: db.sql.table=products, db.operation=select
- Unit: s

这种三层分离遵循 OpenTelemetry 规范的**事件 → 指标流 → 时间序列**模型,其中上下文通过多个层级流动,而不是被塞进名称里。

单位:也请从名称中移除

正如我们学到的服务名称不属于指标名称一样,**单位也不属于其中**。

传统系统经常在名称中包含单位,因为它们缺乏适当的单位元数据:

  • response_time_milliseconds
  • memory_usage_bytes
  • throughput_requests_per_second

OpenTelemetry 将单位视为元数据,与名称分开:

  • `http.server.request.duration`,单位为 `ms`
  • `system.memory.usage`,单位为 `By`
  • `http.server.request.rate`,单位为 `{request}/s`

这种方法有几个好处:

  1. **名称清晰**:没有难看的后缀弄乱你的指标名称。
  2. **标准化单位**:遵循统一测量单位代码 (UCUM)
  3. **后端灵活性**:系统可以自动处理单位转换。
  4. **一致的约定**:与 OpenTelemetry 语义约定保持一致。

规范建议使用非前缀单位,如 `By` (字节),而不是 `MiBy` (Mebibytes),除非有技术原因另行处理。

实际命名指南

创建指标名称时,在适用的情况下,请应用我们为 Span 学到的相同的 `{动词} {名词}` 原则:

  1. **关注操作**:正在测量什么?
  2. **而不是操作者**:谁在进行测量?
  3. **遵循语义约定**:在可用时使用已建立的模式
  4. **单位作为元数据**:不要在名称后附加单位。

以下是遵循 OpenTelemetry 语义约定的示例:

  • http.server.request.duration (而不是 `payment_http_requests_ms`)
  • db.client.operation.duration (而不是 `user_service_db_queries_seconds`)
  • messaging.client.sent.messages (而不是 `order_service_messages_sent_total`)
  • `transaction.count` (而不是 `payment_transaction_total`)

真实迁移示例

传统 (上下文 + 单位在名称中)OpenTelemetry (清晰分离)为什么更好
payment_transaction_total`transaction.count` + `service.name=payment` + 单位 `1`跨服务可聚合
user_service_auth_latency_ms`auth.duration` + `service.name=user` + 单位 `ms`标准的业务名称,正确的单位元数据
inventory_db_query_seconds`db.client.operation.duration` + `service.name=inventory` + 单位 `s`遵循语义约定
api_gateway_requests_per_second`http.server.request.rate` + `service.name=api-gateway` + 单位 `{request}/s`名称清晰,速率单位正确
redis_cache_hit_ratio_percent`cache.hit_ratio` + `service.name=redis` + 单位 `1`比率是无单位的

清晰命名的好处

将上下文与指标名称分离提供了具体的技术优势,这些优势可以同时改善查询性能和运营工作流程。第一个好处是跨服务聚合。像 `sum(transaction.count)` 这样的查询可以返回所有服务的数据,而无需您了解或维护服务名称列表。在一个拥有 50 个微服务的系统中,这意味着一个查询而不是 50 个,而且当您添加第 51 个服务时,该查询也不会失效。

这种一致性使得仪表板可以跨服务重用。为监控身份验证服务中的 HTTP 请求而构建的仪表板,可以无需修改即可用于支付服务、库存服务或其他任何 HTTP 服务组件。您只需编写一次查询——`http.server.request.duration` 并按 `service.name` 过滤——然后将其应用于所有地方。无需维护数十个几乎相同的仪表板。一些可观测性供应商现在更进一步,根据语义约定指标名称自动生成仪表板——当您的服务发出 `http.server.request.duration` 时,平台就会确切知道哪些可视化和聚合对于该指标是有意义的。

清晰的命名也减少了指标命名空间的混乱。考虑一个拥有数十个服务、每个服务都有自己指标的平台。在传统命名方式下,您的指标浏览器会显示数百个特定于服务的变体:`apiserver_request_total`、`payment_service_request_total`、`user_service_request_total`、`inventory_service_request_total` 等等。找到正确的指标变成了一个滚动和搜索冗余变体的过程。通过清晰的命名,您只有一个指标名称 (`request.count`),并通过属性捕获上下文。这使得指标发现变得简单——您找到所需的测量值,然后按您关心的服务进行过滤。

当单位作为元数据而不是名称后缀时,单位处理变得系统化。可观测性平台可以自动执行单位转换——根据可视化要求,在同一个持续时间指标中显示毫秒或秒。该指标仍然是 `request.duration`,单位元数据为 `ms`,而不是两个单独的指标 `request_duration_ms` 和 `request_duration_seconds`。

这种方法还确保了手动和自动检测仪器之间的兼容性。当您遵循 `http.server.request.duration` 等语义约定时,您的自定义指标与自动检测库生成的指标保持一致。这创建了一个一致的数据模型,使得查询在手动和自动检测的服务之间都有效,工程师无需记住哪些指标来自哪个源。

要避免的常见陷阱

工程师经常将特定于部署的信息直接嵌入到指标名称中,从而创建 `user_service_v2_latency` 这样的模式。当版本 3 部署时,这就会中断——每个引用该指标名称的仪表板、警报和查询都必须更新。实例特定的名称,如 `node_42_memory_usage`,也会出现同样的问题。在一个动态扩展的集群中,您最终会得到数百个代表相同测量值的不同指标名称,这使得编写简单的聚合查询变得不可能。

环境特定的前缀会导致类似的维护问题。使用 `prod_payment_errors` 和 `staging_auth_count` 这样的指标名称,您无法编写一个在所有环境中都有效的查询。一个监控生产环境的仪表板在没有修改的情况下不能用于暂存环境。当您需要比较不同环境之间的指标时——这是一项常见的调试任务——您必须编写复杂的查询来显式引用每个环境的指标名称。

指标名称中的技术栈详细信息会在未来迁移时带来麻烦。一个名为 `nodejs_payment_memory` 的指标,当您用 Go 重写服务时,就会变得具有误导性。同样,如果您迁移到其他数据库,`postgres_user_queries` 也需要重命名。这些特定于技术的名称还会阻止您编写在不同技术栈的服务之间有效运行的查询,即使它们执行相同的业务功能。

将业务域与基础设施指标混合会违反系统做什么和如何做之间的分离。一个名为 `ecommerce_cpu_usage` 的指标混淆了业务目的(电子商务)和技术测量(CPU 使用率)。这使得在不同的业务域之间重用基础设施监控更加困难,并使为多个业务功能提供服务的多租户部署复杂化。

将单位包含在指标名称中的做法——`latency_ms`、`memory_bytes`、`count_total`——现在由于 OpenTelemetry 提供了正确的单位元数据而变得冗余。它还阻止了自动单位转换。使用 `request_duration_ms` 和 `request_duration_seconds` 作为单独的指标,您需要为不同的时间尺度编写不同的查询。使用一个包含单位元数据的 `request.duration` 指标,可观测性平台会自动处理转换。

模式很清晰:随部署、实例、环境或版本变化的上下文属于属性,而不是指标名称。指标名称应标识您正在测量的内容。所有其他内容——谁在测量它、它在哪里运行、它的版本是什么——都属于属性层,在那里可以根据需要进行过滤、分组和聚合。

培养更好的指标

就像本系列早些时候介绍的 Span 一样,命名良好的指标是为您未来的自己和您的团队留下的宝贵财富。它们在事件发生时提供清晰度,支持强大的跨服务分析,并使您的可观测性数据真正有用,而不仅仅是海量。

关键的见解与我们从 Span 那里学到的相同:**关注点分离**。指标名称描述您正在测量的内容。上下文——谁在测量、在哪里、何时以及如何测量——存在于 OpenTelemetry 提供的丰富属性层级中。

在下一篇文章中,我们将深入探讨**指标属性**——使指标真正强大的上下文层。我们将探讨如何构建不属于名称的丰富上下文信息,以及如何在信息丰富性和基数(cardinality)问题之间取得平衡。

在此之前,请记住:一个清晰的指标名称就像一条精心打理的花园小径——它会准确地引导您到需要去的地方。