总的来说,远程写入规范旨在记录 Prometheus 和 Prometheus 远程写入兼容发送器如何将数据发送到 Prometheus 或 Prometheus 远程写入兼容接收器的标准。
本文档旨在定义 Prometheus 远程写入 API 的第二个版本,该版本对协议和语义进行了细微更改。第二个版本添加了一个新的 Protobuf 消息,其中包含新功能,除了性能和成本节省外,还实现了更多用例和更广泛的采用。第二个版本还弃用了 1.0 远程写入规范中的先前 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 中。
远程写入协议旨在使样本能够实时可靠地从发送器传播到接收器,而不会丢失。
远程写入协议被设计为无状态的;严格来说,没有消息间通信。因此,该协议不被视为“流式传输”。为了实现流式传输效果,应使用例如 HTTP/1.1 或 HTTP/2 在同一连接上发送多条消息。曾考虑过 gRPC 等“花哨”技术,但当时并未被广泛采用,并且将 gRPC 服务公开到 AWS EC2 ELB 等负载均衡器后面的互联网具有挑战性。
远程写入协议包含批处理的机会,例如,在单个请求中为不同序列发送多个样本。预计不会在同一请求中为同一序列发送多个样本,尽管 Protobuf 消息中对此提供了支持。
测试套件可在 https://github.com/prometheus/compliance/tree/main/remote_write_sender 中找到。远程写入 2.0 兼容性的合规性测试仍在 进行中。
在本文档中,遵循以下定义
Remote-Write
是此 Prometheus 协议的名称。协议
是一种通信规范,使客户端和服务器能够传输指标。Protobuf 消息
(或 Proto 消息)是指此协议的数据结构的 内容类型 定义。由于规范专门使用 Google Protocol Buffers (“protobuf”),因此模式在 “proto” 文件中定义,并由单个 Protobuf “message” 表示。Wire Format
是数据在网络上传输时的格式(即在网络中)。在远程写入的情况下,这始终是压缩的二进制 protobuf 格式。发送器
是发送远程写入数据的东西。接收器
是接收(写入)远程写入数据的东西。Written
的含义取决于接收器,例如,通常意味着将接收到的数据存储在数据库中,但也只是验证、拆分或增强它。Written
指的是 接收器
已接收并正在接受的数据。是否已将此数据摄取到持久存储、写入 WAL 等,取决于 接收器
。唯一的区别是 接收器
已接受此数据,而不是明确地使用错误响应拒绝它。样本
是(时间戳,值)对。直方图
是(时间戳,直方图值)对。标签
是(键,值)对。序列
是样本列表,由一组唯一的标签标识。远程写入协议必须由 RPC 组成,请求正文使用 Google Protocol Buffers 序列化,然后压缩。
protobuf 序列化必须使用以下任一 Protobuf 消息
prometheus.WriteRequest
。截至 2.0 版本,此消息已弃用。它应仅出于兼容性原因使用。发送器和接收器可能不支持 prometheus.WriteRequest
。io.prometheus.write.v2.Request
。发送器和接收器应尽可能使用此消息。发送器和接收器必须支持 io.prometheus.write.v2.Request
。Protobuf 消息必须使用二进制 Wire Format。然后,必须使用 Google Snappy 进行压缩。必须使用 Snappy 的 块格式——不得使用 帧格式。
发送器必须在 HTTP POST 请求的正文中发送序列化和压缩的 Protobuf 消息,并通过 HTTP 将其发送到接收器提供的 URL 路径。接收器可以指定任何 HTTP URL 路径来接收指标。
发送器必须随 HTTP 请求发送以下保留标头
Content-Encoding
Content-Type
X-Prometheus-Remote-Write-Version
User-Agent
发送器可以允许用户添加自定义 HTTP 标头;他们不得允许用户以允许发送保留标头的方式配置它们。
Content-Encoding: <compression>
Content encoding 请求标头必须遵循 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 或更高版本中出现。
接收器必须使用内容类型标头来标识要使用的 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;发送器必须忽略响应正文。响应正文保留供将来使用。
如果接收器已知的任何已发送数据片段(例如样本、直方图、Exemplars)未成功写入(部分写入或完全写入拒绝),则接收器不得返回 2xx HTTP 状态代码。在这种情况下,接收器必须在响应正文中提供人类可读的错误消息。接收器的错误应包含有关被拒绝的样本数量以及拒绝原因的信息。发送器不得尝试解释错误消息,并且应按原样记录它。
以下小节指定了关于标头和不同写入错误情况的发送器和接收器语义。
Written
响应标头在成功的内容协商后,接收器处理(写入)接收到的数据批次。对于每个重要的数据片段(当前为样本、直方图和 Exemplars),一旦完成(成功或失败),接收器必须发送专用的 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 状态代码。对于不检查 Content-Type
请求标头的 1.0 接收器来说,这是一个常见问题;使用 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 除外)上重试,接收器必须使用 4xx HTTP 状态代码来指示写入操作永远无法成功,并且不应重试。发送器可以在 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 Format 中)本身在某些方面是向前/向后兼容的
换句话说,这意味着未来的 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
引用了旨在替换和弃用 Remote-Write 1.0 的 prometheus.WriteRequest
消息的新 Protobuf 消息。
完整模式和真理来源位于 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 { ... }
所有时间戳必须是 int64 类型,以自 Unix 纪元以来的毫秒数计算。样本的值必须是 float64 类型。
对于每个 TimeSeries
消息
labels_refs
。samples
或 histograms
中的至少一个元素。TimeSeries
不得同时包含 samples
和 histograms
。对于(很少)混合浮点和直方图样本的序列,必须使用单独的 TimeSeries
消息。metadata
子字段。接收器可以拒绝未指定 Metadata.type
的序列。created_timestamp
。接收器可以拒绝未设置 created_timestamp
的那些序列。以下小节详细定义了一些模式元素。
io.prometheus.write.v2.Request
Protobuf 消息旨在 实习所有字符串,以在标准压缩之上获得已证实的额外压缩和内存效率增益。
必须提供 symbols
表,并且它必须包含在序列、exemplar 标签和元数据字符串中使用的重复数据删除字符串。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 指南。
每个 exemplar,如果附加到 TimeSeries
trace_id
标签名称,作为最佳实践。与 1.0 中相同。
本节包含推测性计划,这些计划尚未被视为协议规范的一部分,但在此处提及是为了完整性。请注意,2.0 规范完成了 1.0 中的 3 个未来计划中的 2 个。
但是,远程写入 2.0 规范解决了 经典直方图桶 的一个重要的事务性问题。这要归功于原生直方图,它支持使用 io.prometheus.write.v2.Request
wire format 实现的自定义分桶。发送器可能会以这种方式将所有经典直方图转换为原生直方图,但这超出了本规范的范围来强制执行。但是,出于这个原因,接收器可以忽略某些指标类型(例如经典直方图)。
备选 wire formats。OpenTelemetry 社区已经展示了 Apache Arrow(以及潜在的其他列式格式)对于使用其 OTLP 协议进行线上传输的数据的有效性。我们希望进行实验以确认类似格式与 Prometheus 数据模型的兼容性,并包含任何资源使用变化的基准测试。出于兼容性原因,我们可能会长期维护 protobuf 和列式格式,并使用我们的内容协商为此目的添加不同的 Protobuf 消息。
全局符号。预定义的用于实习的字符串字典。该协议可以预定义 ref->symbol 的静态字典,其中包括被认为是常见的字符串,例如“namespace”、“le”、“job”、“seconds”、“bytes”等。发送器可以引用这些字符串,而无需将它们包含在请求的符号表中。此字典可以随着此协议的次要版本发布而逐步增长。
为什么不使用 gRPC? 因为 1.0 协议不使用 gRPC,破坏它会增加采用的摩擦。请参阅 1.0 原因。
为什么不流式传输 protobuf 消息? 如果您使用持久 HTTP/1.1 连接,它们非常接近流式传输。当然,标头必须重新发送,但这比新的 TCP 设置便宜。
为什么我们按顺序发送样本? 顺序约束来自我们用于 Prometheus 中的时间序列数据的编码,其实现针对仅追加工作负载进行了优化。但是,此要求也跨越生态系统中的许多其他数据库和供应商共享。实际上,启用了 OOO 功能的 Prometheus 允许无序写入,但会降低性能,因此保留用于罕见事件。总而言之,接收器可以支持无序写入,尽管规范不允许这样做。在未来(例如 2.x 规范版本)中,如果需要,我们可以扩展内容类型以协商无序写入。
如何在具有顺序约束的情况下并行化请求? 样本必须对于给定的序列是有序的。但是,即使接收器不支持无序写入,远程写入请求也可以并行发送,只要它们用于不同的序列即可。Prometheus 将样本按其标签分片到单独的队列中,然后在每个队列中按顺序进行写入。这保证了同一序列的样本按顺序交付,但不同序列的样本并行发送——并且可能在不同序列之间“无序”。
Remote-Write 2.0 和 OpenTelemetry 的 OTLP 协议之间有什么区别? OpenTelemetry OTLP 是一种用于在遥测源、中间节点和遥测后端之间传输遥测数据(例如指标、日志、跟踪和配置文件)的协议。推荐的传输方式涉及带有 protobuf 的 gRPC,但也描述了带有 protobuf 或 JSON 的 HTTP。它从头开始设计,旨在支持各种不同的可观察性信号、数据类型和额外信息。对于指标,这意味着额外的非识别标签、标志、时间聚合类型、资源或范围指标、模式 URL 等。OTLP 还要求使用 语义约定。
Remote-Write 的设计旨在实现简洁、高效和自然增长。第一个版本于 2023 年正式发布,当时 CNCF 生态系统中已经有数十个经过充分测试的采用者 使用该协议多年。Remote-Write 2.0 在前一个协议的基础上进行了迭代,增加了一些新元素(元数据、Exemplars、创建时间戳和原生直方图)和字符串驻留。Remote-Write 2.0 始终是无状态的,仅关注指标并且是有主见的;因此,它的范围被限定在 Prometheus 社区认为足以构建一个健壮指标解决方案的元素上。其目的是确保 Remote-Write 成为一个稳定的协议,并且比可观测性生态系统中的其他替代方案更经济、更易于采用和使用。
本文档是开源的。请通过提交 issue 或 pull request 来帮助改进它。