一般来说,Remote-Write 规范旨在记录 Prometheus 和 Prometheus Remote-Write 兼容发送器如何将数据发送到 Prometheus 或 Prometheus Remote-Write 兼容接收器的标准。
本文档旨在定义 Prometheus Remote-Write API 的第二个版本,其中对协议和语义进行了少量更改。第二个版本添加了一个新的 Protobuf 消息,其中包含新功能,可以在性能和成本节约的基础上实现更多的用例和更广泛的采用。第二个版本还弃用了来自 1.0 Remote-Write 规范的之前的 Protobuf 消息,并添加了强制性的 X-Prometheus-Remote-Write-*-Written
HTTP 响应头以提高可靠性。最后,本规范概述了如何使用现有的基本内容协商请求头来实现向后兼容的发送器和接收器(即使在单个端点下)。如果需要,未来可能会推出更高级的、自动的内容协商机制。有关 2.0 规范背后的原理,请参阅正式提案。
本文档中的关键词“必须 (MUST)”、“不得 (MUST NOT)”、“必需 (REQUIRED)”、“应当 (SHALL)”、“不应当 (SHALL NOT)”、“应该 (SHOULD)”、“不应该 (SHOULD NOT)”、“推荐 (RECOMMENDED)”、“可以 (MAY)”和“可选 (OPTIONAL)”应按照 RFC 2119 中的描述进行解释。
注意:这是 Remote-Write 2.0 规范的候选发布版本。这意味着本规范目前处于实验状态——预计不会有重大更改,但我们保留在必要时基于早期采用者的反馈打破兼容性的权利。潜在的反馈、问题和建议应作为评论添加到带有开放提案的 PR中。
Remote-Write 协议旨在使数据能够从发送器实时可靠地传播到接收器,而不会丢失。
Remote-Write 协议被设计为无状态的;严格来说,没有消息间的通信。因此,该协议不被认为是“流式”的。为了实现流式效果,应使用 HTTP/1.1 或 HTTP/2 等在同一连接上发送多个消息。曾考虑过 gRPC 等“花哨”的技术,但当时未被广泛采用,并且很难将 gRPC 服务暴露给 AWS EC2 ELB 等负载均衡器后面的互联网。
Remote-Write 协议包含批处理的机会,例如,在单个请求中为不同的序列发送多个样本。预计不会在同一请求中发送同一序列的多个样本,尽管 Protobuf 消息中对此提供了支持。
测试套件可以在 https://github.com/prometheus/compliance/tree/main/remote_write_sender 中找到。用于远程写入 2.0 兼容性的合规性测试仍在 进行中。
在本文档中,遵循以下定义
Remote-Write
是此 Prometheus 协议的名称。协议
是一种通信规范,使客户端和服务器能够传输指标。Protobuf 消息
(或 Proto 消息)是指此协议的数据结构的 内容类型 定义。由于该规范专门使用 Google Protocol Buffers(“protobuf”),因此模式定义在 “proto”文件中,并由单个 Protobuf “消息”表示。线路格式
是指数据在网络上传输时的格式(即在网络中)。在 Remote-Write 的情况下,这始终是压缩的二进制 protobuf 格式。发送器
是发送 Remote-Write 数据的内容。接收器
是接收(写入)Remote-Write 数据的内容。Written
的含义取决于接收器,例如,通常意味着将接收到的数据存储在数据库中,但也可能只是验证、拆分或增强数据。Written
是指接收器
已接收并正在接受的数据。是否已将此数据摄取到持久存储中、写入 WAL 等,取决于接收器
。唯一的区别是接收器
已接受此数据,而不是通过错误响应显式拒绝它。样本
是一对(时间戳,值)。直方图
是一对(时间戳,直方图值)。标签
是一对(键,值)。序列
是由唯一标签集标识的样本列表。Remote-Write 协议必须由使用 Google Protocol Buffers 序列化,然后压缩的请求体组成的 RPC 组成。
protobuf 序列化必须使用以下任一 Protobuf 消息
prometheus.WriteRequest
。从 2.0 开始,此消息已弃用。仅应出于兼容性原因使用它。发送器和接收器可能不支持 prometheus.WriteRequest
。io.prometheus.write.v2.Request
。发送器和接收器应尽可能使用此消息。发送器和接收器必须支持 io.prometheus.write.v2.Request
。Protobuf 消息必须使用二进制线路格式。然后,必须使用 Google Snappy 进行压缩。必须使用 Snappy 的 块格式——不得使用 框架格式。
发送器必须将序列化和压缩的 Protobuf 消息放在 HTTP POST 请求的正文中,并通过 HTTP 将其发送到接收器,网址路径为提供的路径。接收器可以指定任何 HTTP URL 路径来接收指标。
发送器必须随 HTTP 请求发送以下保留标头
Content-Encoding
Content-Type
X-Prometheus-Remote-Write-Version
User-Agent
发送器可以允许用户添加自定义 HTTP 标头;它们不得允许用户以发送保留标头的方式配置它们。
Content-Encoding: <compression>
内容编码请求头必须遵循 RFC 9110。发送者必须使用 snappy
值。接收者必须支持 snappy
压缩。新的可选压缩算法可能会在 2.x 或更高版本中出现。
Content-Type: application/x-protobuf
Content-Type: application/x-protobuf;proto=<fully qualified name>
Content-Type 请求头必须遵循 RFC 9110。发送者必须仅使用 application/x-protobuf
作为媒体类型。发送者可以在头部的值中添加 ;proto=
参数,以指示所使用的 Protobuf 消息的完整限定名,即上面提到的两个消息之一。因此,发送者必须发送以下三种受支持的头部值中的任何一个
对于 PRW 1.0 中引入的已弃用消息,由 prometheus.WriteRequest
标识
Content-Type: application/x-protobuf
Content-Type: application/x-protobuf;proto=prometheus.WriteRequest
对于 PRW 2.0 中引入的消息,由 io.prometheus.write.v2.Request
标识
Content-Type: application/x-protobuf;proto=io.prometheus.write.v2.Request
当与 1.x 接收者通信时,发送者应使用 Content-Type: application/x-protobuf
以实现向后兼容。否则,发送者应使用 Content-Type: application/x-protobuf;proto=io.prometheus.write.v2.Request
。更多 Protobuf 消息可能会在 2.x 或更高版本中出现。
接收者必须使用 content type 头来标识要使用的 Protobuf 消息模式。意外选择错误的模式可能会导致不确定的行为(例如,数据损坏)。
注意:由于io.prometheus.write.v2.Request
中保留的字段,接收者意外使用prometheus.WriteRequest
的错误模式将导致空消息。这通常是为了方便起见,以避免意外错误,但不要依赖它 —— 未来的 Protobuf 消息可能不具备此功能。
X-Prometheus-Remote-Write-Version: <Remote-Write spec major and minor version>
当与 1.x 接收者通信时,发送者必须使用 X-Prometheus-Remote-Write-Version: 0.1.0
以实现向后兼容。否则,发送者应使用它兼容的最新远程写入版本,例如 X-Prometheus-Remote-Write-Version: 2.0.0
。
User-Agent: <name & version of the Sender>
发送者必须包含一个用户代理头,该头应遵循 RFC 9110 User-Agent 头部格式。
成功写入所有数据的接收者必须返回一个 成功的 2xx HTTP 状态码。在这种成功的情况下,接收者的响应体应为空,并且状态码应为 204 HTTP No Content;发送者必须忽略响应体。响应体保留供将来使用。
如果接收者已知发送的任何数据片段(例如,样本、直方图、示例)未成功写入(包括部分写入或完全写入拒绝),则接收者不得返回 2xx HTTP 状态码。在这种情况下,接收者必须在响应体中提供人类可读的错误消息。接收者的错误应包含有关被拒绝的样本数量以及拒绝原因的信息。发送者不得尝试解释错误消息,而应按原样记录它。
以下小节指定了发送者和接收者围绕头部和不同写入错误情况的语义。
Written
响应头在成功的内容协商后,接收者会处理(写入)接收到的数据批次。一旦完成(成功或失败),对于每个重要的数据片段(目前是样本、直方图和示例),接收者必须发送一个专用的 HTTP X-Prometheus-Remote-Write-*-Written
响应头,其中包含成功写入元素的精确数量。
每个头部值必须是一个 64 位整数。头部名称必须如下
X-Prometheus-Remote-Write-Samples-Written <count of all successfully written Samples>
X-Prometheus-Remote-Write-Histograms-Written <count of all successfully written Histogram samples>
X-Prometheus-Remote-Write-Exemplars-Written <count of all successfully written Exemplars>
在接收到 2xx 或 4xx 状态码后,发送者可以假设任何缺失的 X-Prometheus-Remote-Write-*-Written
响应头意味着接收者没有写入此类别(例如,样本)的任何元素(计数为 0
)。由于存在遇到没有此功能的 1.0 接收者的风险,因此当使用已弃用的 prometheus.WriteRequest
Protobuf 消息时,发送者不得做相同的假设。
发送者可以使用这些头部来确认接收者成功写入了哪些数据部分。常见用例
io.prometheus.write.v2.Request
请求发送数据并接收到 2xx HTTP 状态码,但接收器没有返回任何 X-Prometheus-Remote-Write-*-Written
响应头时,发送者应假定 415 HTTP Unsupported Media Type 状态码。 这是 1.0 接收器的一个常见问题,它们不检查 Content-Type
请求头;使用 prometheus.WriteRequest
模式意外解码 io.prometheus.write.v2.Request
有效负载会导致空结果且没有解码错误。发送者不得从远程写入响应头中推断接收者实现的远程写入规范版本。
未来可能会出现更多(可选)头部,例如,当添加更多实体或字段并值得确认时。
发送者应使用远程写入在单个请求中发送多个序列的样本。因此,接收者可以在一个写入请求中写入有效的样本,该请求还包含一些无效或其他未写入的样本,这表示部分写入的情况。在这种情况下,接收者必须按照无效样本和部分写入重试部分返回非 2xx 状态码。
如果接收者不支持发送者提供的给定内容类型或编码,则必须返回 415 HTTP Unsupported Media Type 状态码。
为了向后兼容,发送者应预期从 1.x 接收器收到 400 HTTP Bad Request 的上述原因。
接收者可能不支持某些指标类型或样本(例如,接收者可能会拒绝未指定元数据类型或未指定创建时间戳的样本,而另一个接收者可能会接受此类样本)。由接收者决定哪个样本无效。对于包含任何无效样本的写入请求,接收者必须返回 400 HTTP Bad Request 状态码,除非发生部分可重试写入。
发送者不得在 4xx HTTP 状态码上重试(429 除外),接收者必须使用该状态码来指示写入操作永远不会成功,不应重试。 发送者可以使用不同的内容类型或编码在 415 HTTP 状态码上重试,以查看接收者是否支持它。
接收者可以返回 429 HTTP Too Many Requests 状态码来指示服务器过载的情况。接收者可以返回 Retry-After 头来指示下一次写入尝试的时间。接收者可以返回 5xx HTTP 状态码来表示内部服务器错误。
发送者可以在 429 HTTP 状态码上重试。发送者必须在 5xx HTTP 上重试写入请求。发送者必须使用退避算法来防止服务器过载。发送者可以处理 Retry-After 响应头来估计下一次重试时间。
429 与 5xx 处理之间的差异是由于当接收者无法跟上请求量时,或者接收者选择限制发送者的速率以保护其可用性时,发送者可能“落后”的潜在情况。因此,当存在发送者端错误(例如,流量过多)时,发送者可以选择不在 429 上重试,这允许取得进展,而在存在接收者端错误时(5xx),数据不会丢失。
当接收者期望发送者重试整个请求时,接收者可以在部分写入或部分无效样本情况下返回 5xx HTTP 或 429 HTTP 状态码。在这种情况下,接收者必须支持幂等性,因为发送者可以使用相同的请求重试。
该协议遵循 语义版本控制 2.0:任何 2.x 兼容的接收者必须能够读取任何 2.x 兼容的发送者,反之亦然。破坏性或向后不兼容的更改将导致该规范的 3.x 版本。
Protobuf 消息(以 Wire 格式)本身在某些方面是向前/向后兼容的
换句话说,这意味着未来的 2.x 次要版本可能会向 io.prometheus.write.v2.Request
添加新的可选字段、新的压缩方式、Protobuf 消息和协商机制,只要它们是向后兼容的(例如,对接收者和发送者都是可选的)。
2.x 协议通过引入新的强制性 io.prometheus.write.v2.Request
Protobuf 消息并弃用 prometheus.WriteRequest
,从而破坏了与 1.x 的兼容性。
2.x 发送者可以通过允许用户配置发送者应使用的内容类型来支持 1.x 接收者。如果接收者返回 415 HTTP 状态码,2.x 发送者也可以自动回退到不同的内容类型。
io.prometheus.write.v2.Request
io.prometheus.write.v2.Request
引用了新的 Protobuf 消息,该消息旨在替换并弃用 Remote-Write 1.0 的 prometheus.WriteRequest
消息。
完整的模式和真实来源在 Prometheus 存储库中的 prompb/io/prometheus/write/v2/types.proto
中。 gogo
依赖项和选项可以忽略(最终将被删除)。 它们不是规范的一部分,因为它们不会影响序列化格式。
下面介绍了新的 io.prometheus.write.v2.Request
的简化版本。
// Request represents a request to write the given timeseries to a remote destination.
message Request {
// Since Request supersedes 1.0 spec's prometheus.WriteRequest, we reserve the top-down message
// for the deterministic interop between those two.
// Generally it's not needed, because Receivers must use the Content-Type header, but we want to
// be sympathetic to adopters with mistaken implementations and have deterministic error (empty
// message if you use the wrong proto schema).
reserved 1 to 3;
// symbols contains a de-duplicated array of string elements used for various
// items in a Request message, like labels and metadata items. For the sender's convenience
// around empty values for optional fields like unit_ref, symbols array MUST start with
// empty string.
//
// To decode each of the symbolized strings, referenced, by "ref(s)" suffix, you
// need to lookup the actual string by index from symbols array. The order of
// strings is up to the sender. The receiver should not assume any particular encoding.
repeated string symbols = 4;
// timeseries represents an array of distinct series with 0 or more samples.
repeated TimeSeries timeseries = 5;
}
// TimeSeries represents a single series.
message TimeSeries {
// labels_refs is a list of label name-value pair references, encoded
// as indices to the Request.symbols array. This list's length is always
// a multiple of two, and the underlying labels should be sorted lexicographically.
//
// Note that there might be multiple TimeSeries objects in the same
// Requests with the same labels e.g. for different exemplars, metadata
// or created timestamp.
repeated uint32 labels_refs = 1;
// Timeseries messages can either specify samples or (native) histogram samples
// (histogram field), but not both. For a typical sender (real-time metric
// streaming), in healthy cases, there will be only one sample or histogram.
//
// Samples and histograms are sorted by timestamp (older first).
repeated Sample samples = 2;
repeated Histogram histograms = 3;
// exemplars represents an optional set of exemplars attached to this series' samples.
repeated Exemplar exemplars = 4;
// metadata represents the metadata associated with the given series' samples.
Metadata metadata = 5;
// created_timestamp represents an optional created timestamp associated with
// this series' samples in ms format, typically for counter or histogram type
// metrics. Created timestamp represents the time when the counter started
// counting (sometimes referred to as start timestamp), which can increase
// the accuracy of query results.
//
// Note that some receivers might require this and in return fail to
// write such samples within the Request.
//
// For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go
// for conversion from/to time.Time to Prometheus timestamp.
//
// Note that the "optional" keyword is omitted due to
// https://cloud.google.com/apis/design/design_patterns.md#optional_primitive_fields
// Zero value means value not set. If you need to use exactly zero value for
// the timestamp, use 1 millisecond before or after.
int64 created_timestamp = 6;
}
// Exemplar represents additional information attached to some series' samples.
message Exemplar {
// labels_refs is an optional list of label name-value pair references, encoded
// as indices to the Request.symbols array. This list's len is always
// a multiple of 2, and the underlying labels should be sorted lexicographically.
// If the exemplar references a trace it should use the `trace_id` label name, as a best practice.
repeated uint32 labels_refs = 1;
// value represents an exact example value. This can be useful when the exemplar
// is attached to a histogram, which only gives an estimated value through buckets.
double value = 2;
// timestamp represents the timestamp of the exemplar in ms.
// For Go, see github.com/prometheus/prometheus/model/timestamp/timestamp.go
// for conversion from/to time.Time to Prometheus timestamp.
int64 timestamp = 3;
}
// Sample represents series sample.
message Sample {
// value of the sample.
double value = 1;
// timestamp represents timestamp of the sample in ms.
int64 timestamp = 2;
}
// Metadata represents the metadata associated with the given series' samples.
message Metadata {
enum MetricType {
METRIC_TYPE_UNSPECIFIED = 0;
METRIC_TYPE_COUNTER = 1;
METRIC_TYPE_GAUGE = 2;
METRIC_TYPE_HISTOGRAM = 3;
METRIC_TYPE_GAUGEHISTOGRAM = 4;
METRIC_TYPE_SUMMARY = 5;
METRIC_TYPE_INFO = 6;
METRIC_TYPE_STATESET = 7;
}
MetricType type = 1;
// help_ref is a reference to the Request.symbols array representing help
// text for the metric. Help is optional, reference should point to an empty string in
// such a case.
uint32 help_ref = 3;
// unit_ref is a reference to the Request.symbols array representing a unit
// for the metric. Unit is optional, reference should point to an empty string in
// such a case.
uint32 unit_ref = 4;
}
// A native histogram, also known as a sparse histogram.
// See https://github.com/prometheus/prometheus/blob/remote-write-2.0/prompb/io/prometheus/write/v2/types.proto#L142
// for a full message that follows the native histogram spec for both sparse
// and exponential, as well as, custom bucketing.
message Histogram { ... }
所有时间戳必须是以自 Unix 纪元以来的毫秒数计数的 int64。样本的值必须是 float64。
对于每个 TimeSeries
消息
labels_refs
。samples
或 histograms
中的至少一个元素。 TimeSeries
不得同时包含 samples
和 histograms
。对于(很少)将浮点样本和直方图样本混合的序列,必须使用单独的 TimeSeries
消息。metadata
子字段。接收者可以拒绝未指定 Metadata.type
的序列。created_timestamp
。接收者可以拒绝那些未设置 created_timestamp
的序列。以下小节详细定义了一些模式元素。
io.prometheus.write.v2.Request
Protobuf 消息旨在实习所有字符串,以在标准压缩的基础上获得经过验证的额外压缩和内存效率提升。
必须提供 symbols
表,并且其中必须包含在序列、示例标签和元数据字符串中使用的去重字符串。symbols
表的第一个元素必须是空字符串,用于表示空值或未指定的值,例如当没有提供 Metadata.unit_ref
或 Metadata.help_ref
时。引用必须指向 symbols
字符串数组中现有的索引。
必须为每个 Sample
或 Histogram
样本发送完整的标签集。此外,与样本关联的标签集
__name__
标签。指标名称、标签名称和标签值必须是任何 UTF-8 字符序列。
指标名称应该符合正则表达式 [a-zA-Z_:]([a-zA-Z0-9_:])*
。
标签名称应该符合正则表达式 [a-zA-Z_]([a-zA-Z0-9_])*
。
不符合上述规则的名称,PromQL 用户可能难以使用(请参阅UTF-8 提案了解更多详细信息)。
以“__”开头的标签名称保留供系统使用,不应使用,请参阅Prometheus 数据模型。
接收器也可能对标签的数量和长度施加限制,但这取决于接收器,不在此文档的范围之内。
发送器必须按照时间戳顺序为任何给定的 TimeSeries
发送 samples
(或 histograms
)。发送器可以并行发送多个针对不同序列的请求。
当时间序列不再被追加时,发送器应该发送过时标记。如果可以检测到时间序列的中断,则发送器必须发送过时标记,例如
通常,对于已中断的序列不发送过时标记会导致接收器出现非平凡的查询时间对齐问题。
过时标记必须由特殊的 NaN 值 0x7ff0000000000002
表示。该值不能用于其他用途。
通常,发送器可以使用以下技术检测何时不再追加时间序列
元数据应该遵循官方 Prometheus 关于类型和帮助的指南。
元数据可以遵循官方 OpenMetrics 关于单位的指南。
每个示例,如果附加到 TimeSeries
trace_id
标签名称。与1.0中的相同。
本节包含尚未被视为协议规范一部分的推测性计划,但为了完整性在此处提及。请注意,2.0 规范完成了1.0 中 3 个未来计划中的 2 个。
但是,远程写入 2.0 规范解决了经典直方图桶的一个重要的事务性问题。这要归功于原生直方图支持使用 io.prometheus.write.v2.Request
线格式进行自定义分桶。发送器可能会以这种方式将所有经典直方图转换为原生直方图,但这不在本规范的强制范围内。但是,出于这个原因,接收器可以忽略某些指标类型(例如,经典直方图)。
替代线格式。OpenTelemetry 社区已经展示了 Apache Arrow(以及可能其他列式格式)用于通过 OTLP 协议进行无线数据传输的有效性。我们希望进行实验以确认类似格式与 Prometheus 数据模型的兼容性,并包括任何资源使用变化的基准测试。我们可能会长期维护 protobuf 和列式格式,以实现兼容性,并使用我们的内容协商为此目的添加不同的 Protobuf 消息。
全局符号。用于实习的预定义字符串字典。该协议可以预定义一个静态的 ref->符号字典,其中包括被认为是常见的字符串,例如“namespace”、“le”、“job”、“seconds”、“bytes”等。发送器可以引用这些字符串,而无需将其包含在请求的符号表中。此字典可能会随着此协议的次要版本发布而逐步增长。
为什么不使用 gRPC? 因为 1.0 协议不使用 gRPC,破坏它会增加采用的摩擦。请参阅 1.0 原因。
为什么不流式传输 protobuf 消息? 如果您使用持久的 HTTP/1.1 连接,它们非常接近流式传输。当然,必须重新发送标头,但这比新的 TCP 设置便宜。
为什么我们按顺序发送样本? 按顺序约束来自我们在 Prometheus 中用于时间序列数据的编码,其实现针对仅附加工作负载进行了优化。但是,此要求也在生态系统中的许多其他数据库和供应商之间共享。实际上,启用 OOO 功能的 Prometheus允许乱序写入,但会带来性能损失,因此保留用于罕见事件。总而言之,接收器可以支持乱序写入,尽管规范不允许这样做。在未来,例如 2.x 规范版本中,如果需要,我们可以扩展内容类型以协商乱序写入。
如何在按顺序约束的情况下并行化请求? 样本必须针对给定的序列按顺序排列。但是,即使接收器不支持乱序写入,只要它们针对不同的序列,也可以并行发送远程写入请求。Prometheus 会按标签将其样本分片到单独的队列中,然后每个队列中都会按顺序进行写入。这保证了同一序列的样本按顺序传递,但不同序列的样本并行发送 - 并且可能在不同序列之间“乱序”。
远程写入 2.0 和 OpenTelemetry 的 OTLP 协议之间有什么区别?OpenTelemetry OTLP是一种在遥测源、中间节点和遥测后端之间传输遥测数据(例如指标、日志、跟踪和配置文件)的协议。推荐的传输方式是使用带有 protobuf 的 gRPC,但也描述了带有 protobuf 或 JSON 的 HTTP。它从头开始设计,旨在支持各种不同的可观察性信号、数据类型和额外信息。对于指标,这意味着额外的非标识标签、标志、时间聚合类型、资源或范围指标、架构 URL 等。OTLP 还要求使用语义约定。
远程写入的设计宗旨是简单、高效和有机增长。第一个版本于 2023 年正式发布,当时已经在 CNCF 生态系统中经过实战考验的数十家采用者使用了此协议多年。远程写入 2.0 通过添加一些新元素(元数据、示例、创建时间戳和原生直方图)和字符串实习来迭代之前的协议。远程写入 2.0 始终是无状态的,仅关注指标,并且具有主观性;因此,它将范围缩小到 Prometheus 社区认为足以提供稳健指标解决方案的元素。目的是确保远程写入是一种稳定的协议,与可观察性生态系统中的替代方案相比,它更便宜且更易于采用和使用。
此文档是开源的。请通过提交问题或拉取请求来帮助改进它。