Prometheus 源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:Web API 与联邦架构下,远程写入(Remote Write)请求全流程梳理(含 ProtoBuf/Snappy)与 Go 客户端 Demo 开发实战
Prometheus 源码专题【左扬精讲】—— 监控系统 Prometheus 3.4.0 源码解析:Web API 与联邦架构下,远程写入(Remote Write)请求全流程梳理(含 ProtoBuf/Snappy)与 Go 客户端 Demo 开发实战
在现代分布式系统监控领域,Prometheus 作为一款领先的开源监控解决方案,其远程写入功能扮演着举足轻重的角色。
简单来说,我这里说的 Prometheus 的远程写入指的是将从远程数据源采集到的数据写入本地的数据库 ,而非向远程发起数据传输。
以大规模分布式微服务架构为例,系统中存在众多的服务节点,每个节点都会产生大量的监控数据。如果仅依靠每个节点自身来存储这些数据,不仅会面临存储资源分散、难以统一管理的问题,而且在进行全局数据分析时,需要从各个节点分别获取数据,效率极低。通过 Prometheus 的远程写入功能,这些分布在不同节点上的数据可以被集中收集并写入到一个或多个本地的 Prometheus 数据库中。这样一来,运维人员可以在一个统一的平台上对所有服务节点的数据进行实时监控和分析,大大提高了监控效率和运维管理的便捷性。
再比如在混合云环境下,企业部分业务运行在公有云上,部分运行在私有云上。不同云环境下产生的监控数据也可以通过远程写入的方式汇总到企业内部的 Prometheus 数据库中,实现跨云环境的统一监控与管理。
理解 Prometheus 远程写入原理对系统监控和运维至关重要。从系统监控角度来看,只有深入了解远程写入原理,才能确保采集到的数据准确、完整地存储到本地数据库,从而为后续的监控分析提供可靠的数据基础。例如,当数据在远程传输过程中出现丢失或错误时,如果不了解远程写入的工作机制,就很难快速定位问题所在。从运维角度而言,掌握原理有助于优化运维策略,合理配置相关参数,提高系统的稳定性和性能。比如,通过调整远程写入的队列配置参数,可以在网络不稳定的情况下,有效避免数据丢失,保障监控数据的连续性。
一、远程写入请求的数据格式
1.1、ProtoBuf 数据格式
https://github.com/prometheus/prometheus/blob/v3.4.0/prompb/remote.proto#L22
message WriteRequest {
repeated prometheus.TimeSeries timeseries = 1 [(gogoproto.nullable) = false];
// Cortex uses this field to determine the source of the write request.
// We reserve it to avoid any compatibility issues.
reserved 2;
repeated prometheus.MetricMetadata metadata = 3 [(gogoproto.nullable) = false];
}
Prometheus 选择 ProtoBuf 作为远程写入数据格式,有着多方面的考量。ProtoBuf,即 Protocol Buffers ,是由 Google 开发的一种语言中立、平台中立、可扩展的数据序列化机制。与常见的 JSON、XML 等数据格式相比,它具有诸多显著优势。
从数据紧凑性角度来看,ProtoBuf 将数据序列化为紧凑的二进制格式。在存储相同数据量的情况下,ProtoBuf 序列化后的数据大小通常远小于 JSON 和 XML。例如,在一个包含 100 个监控指标的时间序列数据中,每个指标带有若干标签和时间戳数据点。若使用 JSON 格式表示,由于 JSON 采用文本形式,包含大量的键值对描述信息,其数据体积会较大。而 ProtoBuf 通过对数据进行二进制编码,去除了冗余的文本描述信息,数据体积可能仅为 JSON 的几分之一甚至更小 ,这大大节省了存储空间和网络传输带宽。
跨语言支持也是 ProtoBuf 的一大亮点。Prometheus 的应用场景往往涉及多种编程语言编写的组件和系统。ProtoBuf 支持 C++、Java、Python、Go、C# 等多种主流编程语言,这使得不同语言编写的数据源都可以轻松地将数据序列化为 ProtoBuf 格式,然后发送给 Prometheus 进行远程写入。例如,在一个大型企业级应用中,部分服务使用 Java 开发,部分使用 Python 开发,还有一些核心组件使用 C++ 开发。通过 ProtoBuf,这些不同语言开发的服务产生的监控数据都能够以统一的格式进行传输和处理,极大地提高了系统的兼容性和可扩展性。
在 Prometheus 远程写入中,ProtoBuf 有着具体的应用方式。首先,需要定义特定的.proto 文件来描述数据结构。在这个文件中,会定义诸如时间序列(TimeSeries)、样本(Sample)、标签(Label)等数据结构。以 TimeSeries 为例,它包含了一组带有相同标签的数据点,在.proto 文件中会定义其包含的 Labels(用于区分不同时间序列的键值对)、Samples(具体的数据点)等字段。通过这种方式,将监控数据的复杂结构进行了清晰的定义。然后,使用 Protobuf 编译器(protoc)根据.proto 文件为特定编程语言生成代码。这些生成的代码提供了便捷的方法来读写结构化数据,例如在 Go 语言中,生成的代码可以方便地将内存中的监控数据结构体序列化为 ProtoBuf 格式的字节流,以便通过网络发送给 Prometheus,或者将接收到的 ProtoBuf 数据反序列化为内存中的结构体,供后续处理和存储。
1.2、Snappy 压缩原理与作用
压缩:https://github.com/prometheus/prometheus/blob/v3.4.0/util/compression/compression.go#L52
解压缩:https://github.com/prometheus/prometheus/blob/v3.4.0/util/compression/compression.go#L97
Snappy 压缩算法在 Prometheus 远程写入中起着关键作用,它主要用于对 ProtoBuf 数据进行进一步压缩,以提高数据传输和存储的效率。Snappy 是由 Google 开发的一款专注于速度和低内存占用的压缩库。
Snappy 的压缩原理基于 LZ77 算法,并在此基础上进行了优化。它通过查找数据流中相对短的重复数据序列来实现压缩。在实际操作中,Snappy 将输入数据分成若干个块进行独立压缩,这样即使在并行处理中也能高效运作。例如,当处理一段包含大量重复监控数据的 ProtoBuf 字节流时,Snappy 会扫描数据块,寻找连续的重复字节序列。假设在数据中存在一段连续重复的字符串 “abcabcabc”,Snappy 会识别出这个重复模式,然后用一个更短的标记来表示这段重复数据,而不是重复存储整个字符串,从而达到压缩数据的目的。它使用一种变体的哈希表来快速检测重复数据,对于字节级别的数据匹配,采用简单且高效的编码方式来减少数据占用。
Snappy 压缩对减少网络传输数据量有着显著效果。在远程写入过程中,监控数据需要通过网络从数据源传输到 Prometheus 服务器。未压缩的 ProtoBuf 数据虽然已经比较紧凑,但仍然存在进一步压缩的空间。经过 Snappy 压缩后,数据量可以进一步大幅减少,从而降低网络带宽的消耗。例如,原本需要 10MB 带宽才能传输的 ProtoBuf 格式监控数据,经过 Snappy 压缩后,可能只需要 2 - 3MB 带宽即可完成传输,大大提高了数据传输的效率,尤其是在网络带宽有限的情况下,能够有效避免网络拥塞,确保监控数据能够及时、稳定地传输到 Prometheus 服务器。
在存储效率方面,Snappy 同样发挥着重要作用。Prometheus 需要存储大量的历史监控数据,这些数据占用的存储空间不容忽视。经过 Snappy 压缩后的数据存储在磁盘或其他存储介质上时,能够显著减少存储空间的占用。这不仅降低了存储成本,还能提高存储系统的整体性能,因为在读取和写入数据时,需要处理的数据量减少,从而加快了数据的读写速度。例如,在一个存储了数年监控数据的 Prometheus 数据库中,使用 Snappy 压缩可以节省大量的磁盘空间,同时在查询历史数据时,由于数据量的减少,查询响应时间也会相应缩短,提高了监控数据的查询效率和分析效率。
二、Web API 处理远程写入请求的入口
2.1、路由注册
注册入口:https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#L411

Handler 初始化:https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#L309

处理函数:https://github.com/prometheus/prometheus/blob/v3.4.0/web/api/v1/api.go#L1863

2.2、请求处理流程
HTTP入口:https://github.com/prometheus/prometheus/blob/v3.4.0/storage/remote/write_handler.go#L120
// ServeHTTP 实现 http.Handler 接口,处理远程写入请求
func (h *writeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 从请求头中获取 Content-Type
contentType := r.Header.Get("Content-Type")
if contentType == "" {
// 兼容 yolo 1.0 客户端,如果没有指定 Content-Type,默认使用应用协议内容类型
// 类似 2.0 版本之前的处理方式(参考注释中的代码链接)
// 本可以返回 http.StatusUnsupportedMediaType,但默认假设为 1.0 消息格式
contentType = appProtoContentType
}
// 根据 Content-Type 解析协议消息类型
msgType, err := h.parseProtoMsg(contentType)
if err != nil {
// 解析失败时,记录错误日志并返回不支持的媒体类型状态码
h.logger.Error("Error decoding remote write request", "err", err)
http.Error(w, err.Error(), http.StatusUnsupportedMediaType)
return
}
// 检查消息类型是否在服务器接受的范围内
if _, ok := h.acceptedProtoMsgs[msgType]; !ok {
// 不支持该消息类型时,生成包含支持类型的错误信息
err := fmt.Errorf("%v protobuf message is not accepted by this server; accepted %v", msgType, func() (ret []string) {
for k := range h.acceptedProtoMsgs {
ret = append(ret, string(k))
}
return ret
}())
h.logger.Error("Error decoding remote write request", "err", err)
http.Error(w, err.Error(), http.StatusUnsupportedMediaType)
}
// 获取请求头中的 Content-Encoding(压缩方式)
enc := r.Header.Get("Content-Encoding")
if enc == "" {
// 兼容 yolo 1.0 客户端,如果没有指定压缩方式,默认假设为 snappy 压缩
// 类似 2.0 版本之前的处理方式(参考注释中的代码链接)
} else if strings.ToLower(enc) != compression.Snappy {
// 如果指定了不支持的压缩方式,返回错误
err := fmt.Errorf("%v encoding (compression) is not accepted by this server; only %v is acceptable", enc, compression.Snappy)
h.logger.Error("Error decoding remote write request", "err", err)
http.Error(w, err.Error(), http.StatusUnsupportedMediaType)
}
// 读取请求体内容
body, err := io.ReadAll(r.Body)
if err != nil {
h.logger.Error("Error decoding remote write request", "err", err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 使用 snappy 解压请求体
decompressed, err := compression.Decode(compression.Snappy, body, nil)
if err != nil {
// 解压失败时记录错误并返回
// TODO(bwplotka): 为返回的错误添加更多上下文信息?
h.logger.Error("Error decompressing remote write request", "err", err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 现在有了解压后的缓冲区,可以进行反序列化
// 如果是 v1 版本的远程写入协议消息
if msgType == config.RemoteWriteProtoMsgV1 {
// PRW 1.0 流程使用不同的协议消息,且不支持部分写入处理
var req prompb.WriteRequest
// 反序列化 v1 请求
if err := proto.Unmarshal(decompressed, &req); err != nil {
// TODO(bwplotka): 为返回的错误添加更多上下文信息?
h.logger.Error("Error decoding v1 remote write request", "protobuf_message", msgType, "err", err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 处理写入请求
if err = h.write(r.Context(), &req); err != nil {
switch {
// 处理特定类型的错误(乱序样本、超出范围等),返回 400 错误阻止重试
case errors.Is(err, storage.ErrOutOfOrderSample), errors.Is(err, storage.ErrOutOfBounds), errors.Is(err, storage.ErrDuplicateSampleForTimestamp), errors.Is(err, storage.ErrTooOldSample):
http.Error(w, err.Error(), http.StatusBadRequest)
return
default:
// 其他错误视为服务器内部错误
h.logger.Error("Error while remote writing the v1 request", "err", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// 写入成功,返回 204 无内容状态码
w.WriteHeader(http.StatusNoContent)
return
}
// 处理 Remote Write 2.x 版本的协议消息
var req writev2.Request
// 反序列化 v2 请求
if err := proto.Unmarshal(decompressed, &req); err != nil {
// TODO(bwplotka): 为返回的错误添加更多上下文信息?
h.logger.Error("Error decoding v2 remote write request", "protobuf_message", msgType, "err", err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 处理 v2 版本的写入请求,获取响应统计信息、HTTP 错误码和错误
respStats, errHTTPCode, err := h.writeV2(r.Context(), &req)
// 在所有情况下,设置必需的 X-Prometheus-Remote-Write-Written-* 响应头
respStats.SetHeaders(w)
if err != nil {
// 如果是 5xx 错误,记录错误日志
if errHTTPCode/5 == 100 { // 5xx 状态码的判断(500/5=100)
h.logger.Error("Error while remote writing the v2 request", "err", err.Error())
}
// 返回错误信息和对应的 HTTP 状态码
http.Error(w, err.Error(), errHTTPCode)
return
}
// 写入成功,返回 204 无内容状态码
w.WriteHeader(http.StatusNoContent)
}
ProtoBuf 反序列化(v1):https://github.com/prometheus/prometheus/blob/v3.4.0/storage/remote/write_handler.go#L176
// 现在我们有了解压后的缓冲区,可以进行反序列化操作了
// 如果消息类型是 v1 版本的远程写入协议消息
if msgType == config.RemoteWriteProtoMsgV1 {
// PRW 1.0 流程使用不同的 proto 消息格式,且不支持部分写入处理
var req prompb.WriteRequest
// 将解压后的数据反序列化为 v1 版本的请求结构体
if err := proto.Unmarshal(decompressed, &req); err != nil {
// TODO(bwplotka): 为返回的错误添加更多上下文信息?
h.logger.Error("解析 v1 远程程写入请求失败", "protobuf_message", msgType, "err", err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 调用 v1 版本的写入方法处理请求
if err = h.write(r.Context(), &req); err != nil {
switch {
// 处理特定类型的错误(乱序样本、超出范围、时间戳重复样本、样本过旧)
// 这些错误被视为客户端请求错误,返回 400 状态码以阻止重试
case errors.Is(err, storage.ErrOutOfOrderSample),
errors.Is(err, storage.ErrOutOfBounds),
errors.Is(err, storage.ErrDuplicateSampleForTimestamp),
errors.Is(err, storage.ErrTooOldSample):
http.Error(w, err.Error(), http.StatusBadRequest)
return
default:
// 其他错误视为服务器内部错误
h.logger.Error("处理 v1 远程写入请求时出错", "err", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
// 写入成功,返回 204(无内容)状态码
w.WriteHeader(http.StatusNoContent)
return
}
// 处理 Remote Write 2.x 版本的 proto 消息
var req writev2.Request
// 将解压后的数据反序列化为 v2 版本的请求结构体
if err := proto.Unmarshal(decompressed, &req); err != nil {
// TODO(bwplotka): 为返回的错误添加更多上下文信息?
h.logger.Error("解析 v2 远程写入请求失败", "protobuf_message", msgType, "err", err.Error())
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 调用 v2 版本的写入方法处理请求,返回响应统计信息、HTTP 错误码和错误
respStats, errHTTPCode, err := h.writeV2(r.Context(), &req)
// 在所有情况下(无论成功或失败),设置必需的 X-Prometheus-Remote-Write-Written-* 响应头
respStats.SetHeaders(w)
if err != nil {
// 如果是 5xx 级别的错误(通过状态码除以 5 等于 100 判断,如 500/5=100)
if errHTTPCode/5 == 100 { // 5xx 状态码判断
h.logger.Error("处理 v2 远程写入请求时出错", "err", err.Error())
}
// 返回错误信息和对应的 HTTP 状态码
http.Error(w, err.Error(), errHTTPCode)
return
}
// 写入成功,返回 204(无内容)状态码
w.WriteHeader(http.StatusNoContent)
}
2.3、数据写入流程
V1 写入逻辑:https://github.com/prometheus/prometheus/blob/v3.4.0/storage/remote/write_handler.go#L225
// write 处理 v1 版本的远程写入请求,将时间序列数据写入存储
func (h *writeHandler) write(ctx context.Context, req *prompb.WriteRequest) (err error) {
// 统计乱序的 exemplar 错误数量
outOfOrderExemplarErrs := 0
// 统计带有无效标签的样本数量
samplesWithInvalidLabels := 0
// 统计成功追加的样本数量
samplesAppended := 0
// 创建带时间限制的 appender(用于写入数据)
// 最大时间设置为当前时间加最大超前时间(防止未来 时间戳过远的样本)
app := &timeLimitAppender{
Appender: h.appendable.Appender(ctx), // 基础的 appender 接口实现
maxTime: timestamp.FromTime(time.Now().Add(maxAheadTime)),
}
// 延迟执行:处理 appender 的提交或回滚
defer func() {
if err != nil {
// 如果有错误,回滚未提交的操作
_ = app.Rollback()
return
}
// 无错误则提交操作
err = app.Commit()
if err != nil {
// 提交失败时,记录已追加但未成功提交的样本数(无元数据)
h.samplesAppendedWithoutMetadata.Add(float64(samplesAppended))
}
}()
// 创建标签构建器(用于高效构建标签集)
b := labels.NewScratchBuilder(0)
// 遍历请求中的所有时间序列
for _, ts := range req.Timeseries {
// 将时间序列的标签转换为 Prometheus 内部的标签集格式
ls := ts.ToLabels(&b, nil)
// TODO(bwplotka): 根据 1.0 规范,这里应该返回 400 错误,
// 但其他样本可能仍需写入。或许可以与 v2 版本的实现统一处理。
// 检查标签集是否有效:必须包含 metric name 且符合 UTF8 验证规则
if !ls.Has(labels.MetricName) || !ls.IsValid(model.UTF8Validation) {
h.logger.Warn("无效的指标名或标签", "got", ls.String())
samplesWithInvalidLabels++
continue // 跳过当前时间序列
} else if duplicateLabel, hasDuplicate := ls.HasDuplicateLabelNames(); hasDuplicate {
// 检查是否有重复的标签名
h.logger.Warn("时间序列的标签无效", "labels", ls.String(), "duplicated_label", duplicateLabel)
samplesWithInvalidLabels++
continue // 跳过当前时间序列
}
// 追加当前时间序列的 v1 版本样本数据
if err := h.appendV1Samples(app, ts.Samples, ls); err != nil {
return err // 追加失败直接返回错误
}
// 累计成功追加的样本数
samplesAppended += len(ts.Samples)
// 处理当前时间序列的 exemplar(示例数据)
for _, ep := range ts.Exemplars {
// 将 exemplar 转换为 Prometheus 内部格式
e := ep.ToExemplar(&b, nil)
// 追加 exemplar 到存储
if _, err := app.AppendExemplar(0, ls, e); err != nil {
switch {
case errors.Is(err, storage.ErrOutOfOrderExemplar):
// 处理乱序的 exemplar 错误
outOfOrderExemplarErrs++
h.logger.Debug("乱序的 exemplar", "series", ls.String(), "exemplar", fmt.Sprintf("%+v", e))
default:
// 其他错误:由于 exemplar 存储仍处于实验阶段,不因此失败整个请求
h.logger.Debug("AppendExemplar 时添加 exemplar 出错", "series", ls.String(), "exemplar", fmt.Sprintf("%+v", e), "err", err)
}
}
}
// 追加当前时间序列的 v1 版本直方图数据
if err = h.appendV1Histograms(app, ts.Histograms, ls); err != nil {
return err // 追加失败直接返回错误
}
// 累计成功追加的直方图数量
samplesAppended += len(ts.Histograms)
}
// 记录乱序 exemplar 的警告日志(如果有)
if outOfOrderExemplarErrs > 0 {
h.logger.Warn("摄入乱序 exemplar 时出错", "num_dropped", outOfOrderExemplarErrs)
}
// 记录无效标签样本的统计(如果有)
if samplesWithInvalidLabels > 0 {
h.samplesWithInvalidLabelsTotal.Add(float64(samplesWithInvalidLabels))
}
return nil // 所有处理完成,返回成功
}
Append 实现:https://github.com/prometheus/prometheus/blob/v3.4.0/storage/remote/write_handler.go#L296
// appendV1Samples 用于将 v1 版本远程写入请求中的样本数据(ss)追加到存储中
// 参数说明:
// - app:存储的 Appender 接口实例,负责实际的样本写入操作
// - ss:待写入的 prompb.Sample 切片(v1 版本的样本数据结构)
// - labels:当前样本所属时间序列的标签集,用于标识唯一时间序列
// 返回值:写入过程中遇到的错误,无错误则返回 nil
func (h *writeHandler) appendV1Samples(app storage.Appender, ss []prompb.Sample, labels labels.Labels) error {
// ref 用于存储时间序列的引用(SeriesRef),首次写入时为初始值,后续可复用该引用提升效率
var ref storage.SeriesRef
// err 用于捕获写入过程中的错误
var err error
// 遍历所有待写入的样本
for _, s := range ss {
// 调用 Appender 的 Append 方法写入单个样本
// 参数说明:
// - ref:时间序列引用,首次为 0,成功写入后会返回该序列的有效引用,后续写入同一序列可复用
// - labels:样本所属时间序列的标签集
// - s.GetTimestamp():样本的时间戳(通过 protobuf 生成的 Get 方法获取,确保兼容性)
// - s.GetValue():样本的数值(同样通过 Get 方法获取)
ref, err = app.Append(ref, labels, s.GetTimestamp(), s.GetValue())
// 检查是否发生写入错误
if err != nil {
// 判断错误类型是否为样本写入的常见客户端错误(乱序、超出时间范围、时间戳重复)
if errors.Is(err, storage.ErrOutOfOrderSample) ||
errors.Is(err, storage.ErrOutOfBounds) ||
errors.Is(err, storage.ErrDuplicateSampleForTimestamp) {
// 记录错误日志,包含具体的错误信息、时间序列标签、样本时间戳(便于问题排查)
h.logger.Error("远程写入请求中出现无效样本(乱序/超出范围/时间戳重复)",
"err", err.Error(),
"series", labels.String(),
"timestamp", s.Timestamp)
}
// 直接返回错误,终止后续样本写入(v1 版本不支持部分写入,一个样本失败则整个批次失败)
return err
}
}
// 所有样本均成功写入,返回 nil
return nil
}
三、Prometheus 3.4.0 Remote Write 完整数据写入流程
| 阶段 | 序号 | 核心操作 | 源码文件路径 | 代码位置(行号) | 关键逻辑说明 |
|---|---|---|---|---|---|
| 阶段一:HTTP 请求接入与路由 | 1 | 客户端发起请求 | - | - | 客户端发送 POST /api/v1/write 请求 |
| 2 | 路由匹配 | web/web.go |
- | 通过 mux 将请求分发至 Handler.router |
|
| 3 | API v1 路由注册 | web/api/v1/api.go |
411 | 注册路由 r.Post("/write", api.ready(api.remoteWrite)) |
|
| 4 | 前置 Ready 检查 | web/api/v1/api.go |
411 | 通过 api.ready 包装器检查服务就绪状态 |
|
| 5 | 请求转发至 Handler | web/api/v1/api.go |
1863-1869 | 若启用远程写入接收器,则调用 remoteWriteHandler.ServeHTTP,否则返回 404 |
|
| 阶段二:Handler 初始化与上下文 | 6 | writeHandler 结构定义 |
storage/remote/write_handler.go |
49-59 | 定义处理器核心字段(日志、存储接口、指标计数器等) |
| 7 | Handler 创建入口 | storage/remote/write_handler.go |
68-93 | 初始化指标(如无效标签样本计数器)和支持的协议消息类型 | |
| 阶段三:请求头与协议校验 | 8 | Content-Type 处理 | storage/remote/write_handler.go |
121-134 | 兼容旧客户端,默认设为 appProtoContentType;解析消息类型并校验 |
| 9 | 消息类型校验 | storage/remote/write_handler.go |
136-145 | 检查消息类型是否在服务器支持的列表中,不支持则返回 415 | |
| 10 | Content-Encoding 校验 | storage/remote/write_handler.go |
147-156 | 兼容旧客户端,默认假设 Snappy 压缩;不支持其他压缩格式则返回 415 | |
| 阶段四:读取与解压 | 11 | 读取请求体 | storage/remote/write_handler.go |
158-164 | 读取请求体内容,失败则返回 400 |
| 12 | Snappy 解压 | storage/remote/write_handler.go |
166-172 | 调用 compression.Decode 解压,失败则返回 400;解压逻辑位于 util/compression/compression.go:97-109 |
|
| 阶段五:ProtoBuf 反序列化 | 13 | 版本判定 | storage/remote/write_handler.go |
176 | 根据消息类型判断是 v1 还是 v2 版本 |
| 14 | V1 反序列化 | storage/remote/write_handler.go |
176-184 | 将解压数据反序列化为 prompb.WriteRequest,失败则返回 400 |
|
| 15 | 数据结构定义 | prompb/remote.proto |
22-28 | WriteRequest 包含 timeseries(时间序列)和 metadata(元数据)字段 |
|
| 阶段六:写入与校验 | 16 | 初始化 Appender | storage/remote/write_handler.go |
230-233 | 创建带时间限制的 timeLimitAppender,限制未来时间戳范围 |
| 17 | 延迟事务处理 | storage/remote/write_handler.go |
235-244 | 延迟执行:出错则回滚,成功则提交;提交失败记录未成功写入的样本数 | |
| 18 | 遍历时间序列 | storage/remote/write_handler.go |
246-285 | 循环处理请求中的每个 Timeseries 数据 |
|
| 19 | 标签转换 | storage/remote/write_handler.go |
248 | 将时间序列标签转换为 Prometheus 内部 labels.Labels 格式 |
|
| 20 | 标签与名称校验 | storage/remote/write_handler.go |
252-260 | 校验标签有效性(必须包含 metric name、无重复标签名等),无效则跳过 | |
| 21 | 写入 Samples | storage/remote/write_handler.go |
262-265(调用)
|
调用 appendV1Samples 写入样本,处理乱序 / 重复时间戳等错误,失败则终止写入 |
|
| 22 | 写入 Exemplars | storage/remote/write_handler.go |
267-279 | 转换并追加示例数据,乱序错误仅计数不终止请求(实验性特性) | |
| 23 | 写入 Histograms | storage/remote/write_handler.go |
281-284 | 调用 appendV1Histograms 写入直方图数据,失败则终止 |
|
| 阶段七:事务提交与响应 | 24 | 统计与计数 | storage/remote/write_handler.go |
287-292 | 记录乱序 Exemplars 数量、无效标签样本数(更新指标) |
| 25 | 提交事务 | storage/remote/write_handler.go |
240 | 调用 app.Commit() 提交写入操作 |
|
| 26 | 错误处理 | storage/remote/write_handler.go |
185-195 | 区分客户端错误(400)和服务器错误(500),返回对应状态码 | |
| 27 | 返回成功响应 | storage/remote/write_handler.go |
197 | 写入成功则返回 204 No Content |
该表格完整映射了 Prometheus 3.4.0 远程写入的全流程,每个环节均对应具体源码位置,便于从代码实现角度理解数据流转逻辑。
四、编写一个 Remote Write 客户端的 golang demo
package main
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"log"
"net/http"
"time"
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
"github.com/prometheus/prometheus/model/timestamp"
prompb "github.com/prometheus/prometheus/prompb"
)
const (
// Prometheus Remote Write v1.0 规范使用的 Content-Type
remoteWriteContentType = "application/x-protobuf"
remoteWriteVersion = "0.1.0"
)
// RemoteWriteClient 封装了向 Prometheus 发送远程写入请求的客户端
type RemoteWriteClient struct {
url string
httpClient *http.Client
timeout time.Duration
}
// NewRemoteWriteClient 创建一个新的远程写入客户端
func NewRemoteWriteClient(url string, timeout time.Duration) *RemoteWriteClient {
return &RemoteWriteClient{
url: url,
httpClient: &http.Client{
Timeout: timeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: false, // 生产环境应该设为 false,使用正确的证书
},
},
},
timeout: timeout,
}
}
// Write 将时间序列数据写入到 Prometheus
func (c *RemoteWriteClient) Write(ctx context.Context, timeseries []*prompb.TimeSeries) error {
// 1. 构建 WriteRequest
req := &prompb.WriteRequest{
Timeseries: timeseries,
}
// 2. 序列化为 protobuf
data, err := proto.Marshal(req)
if err != nil {
return fmt.Errorf("failed to marshal protobuf: %w", err)
}
// 3. 使用 Snappy 压缩
compressed := snappy.Encode(nil, data)
// 4. 创建 HTTP 请求
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, bytes.NewReader(compressed))
if err != nil {
return fmt.Errorf("failed to create HTTP request: %w", err)
}
// 5. 设置必要的请求头
httpReq.Header.Set("Content-Type", remoteWriteContentType)
httpReq.Header.Set("Content-Encoding", "snappy")
httpReq.Header.Set("User-Agent", "Prometheus-Remote-Write-Client/"+remoteWriteVersion)
httpReq.Header.Set("X-Prometheus-Remote-Write-Version", remoteWriteVersion)
// 6. 发送请求
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("failed to send HTTP request: %w", err)
}
defer resp.Body.Close()
// 7. 检查响应状态
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return nil
}
// MakeSample 创建一个简单的 Prometheus 样本
func MakeSample(name string, value float64, labels map[string]string) *prompb.TimeSeries {
// 构建标签
pbLabels := make([]prompb.Label, 0, len(labels)+1)
// 必须包含 __name__ 标签
pbLabels = append(pbLabels, prompb.Label{
Name: "__name__",
Value: name,
})
// 添加其他标签
for k, v := range labels {
pbLabels = append(pbLabels, prompb.Label{
Name: k,
Value: v,
})
}
// 创建时间序列
return &prompb.TimeSeries{
Labels: pbLabels,
Samples: []prompb.Sample{
{
Value: value,
Timestamp: timestamp.FromTime(time.Now()),
},
},
}
}
// MakeSamples 批量创建时间序列
func MakeSamples(name string, values []float64, labels map[string]string) *prompb.TimeSeries {
pbLabels := make([]prompb.Label, 0, len(labels)+1)
pbLabels = append(pbLabels, prompb.Label{
Name: "__name__",
Value: name,
})
for k, v := range labels {
pbLabels = append(pbLabels, prompb.Label{
Name: k,
Value: v,
})
}
samples := make([]prompb.Sample, 0, len(values))
now := time.Now()
for i, value := range values {
samples = append(samples, prompb.Sample{
Value: value,
Timestamp: timestamp.FromTime(now.Add(time.Duration(i) * time.Second)),
})
}
return &prompb.TimeSeries{
Labels: pbLabels,
Samples: samples,
}
}
func main() {
// 配置 Prometheus 远程写入地址
// 这个地址应该是启用了 --web.enable-remote-write-receiver 的 Prometheus 实例
prometheusURL := "http://localhost:9090/api/v1/write"
// 创建客户端
client := NewRemoteWriteClient(prometheusURL, 10*time.Second)
// 示例 1: 发送单个指标
log.Println("示例 1: 发送单个 CPU 使用率指标")
cpuMetric := MakeSample(
"cpu_usage_percent",
75.5,
map[string]string{
"instance": "server-01",
"datacenter": "us-east-1",
"env": "production",
},
)
err := client.Write(context.Background(), []*prompb.TimeSeries{cpuMetric})
if err != nil {
log.Printf("发送失败: %v", err)
} else {
log.Println("✓ 发送成功")
}
time.Sleep(1 * time.Second)
// 示例 2: 批量发送多个指标
log.Println("示例 2: 批量发送多个指标")
metrics := []*prompb.TimeSeries{
MakeSample("memory_usage_bytes", 8589934592, map[string]string{
"instance": "server-01",
"datacenter": "us-east-1",
}),
MakeSample("disk_io_read_bytes_total", 1024000, map[string]string{
"instance": "server-01",
"device": "sda",
}),
MakeSample("network_receive_bytes_total", 2048000, map[string]string{
"instance": "server-01",
"interface": "eth0",
}),
}
err = client.Write(context.Background(), metrics)
if err != nil {
log.Printf("批量发送失败: %v", err)
} else {
log.Println("✓ 批量发送成功")
}
time.Sleep(1 * time.Second)
// 示例 3: 发送时间序列数据(多个数据点)
log.Println("示例 3: 发送时间序列数据(5 个数据点)")
timeSeries := MakeSamples(
"temperature_celsius",
[]float64{22.5, 23.1, 23.8, 24.2, 24.5},
map[string]string{
"sensor": "sensor-01",
"room": "living-room",
},
)
err = client.Write(context.Background(), []*prompb.TimeSeries{timeSeries})
if err != nil {
log.Printf("时间序列发送失败: %v", err)
} else {
log.Println("✓ 时间序列发送成功")
}
log.Println("\n所有示例执行完成!")
}
五、编写一个更高级的 Remote Write 客户端的 Go demo 示例,包含错误处理和重试
advanced_client.go:
package main
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"time"
"github.com/gogo/protobuf/proto"
"github.com/golang/snappy"
prompb "github.com/prometheus/prometheus/prompb"
)
// AdvancedRemoteWriteClient 增强版的远程写入客户端
// 包含错误重试、连接池、指标统计等功能
type AdvancedRemoteWriteClient struct {
url string
httpClient *http.Client
timeout time.Duration
maxRetries int
retryBackoff time.Duration
metrics *ClientMetrics
}
// ClientMetrics 客户端指标统计
type ClientMetrics struct {
TotalRequests int64
SuccessfulRequests int64
FailedRequests int64
RetriedRequests int64
}
// NewAdvancedRemoteWriteClient 创建增强版客户端
func NewAdvancedRemoteWriteClient(url string, timeout time.Duration, maxRetries int) *AdvancedRemoteWriteClient {
// 配置 HTTP 客户端,支持连接池和长连接
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
DisableKeepAlives: false,
DisableCompression: false,
ResponseHeaderTimeout: timeout,
}
return &AdvancedRemoteWriteClient{
url: url,
httpClient: &http.Client{
Timeout: timeout,
Transport: transport,
},
timeout: timeout,
maxRetries: maxRetries,
retryBackoff: 100 * time.Millisecond,
metrics: &ClientMetrics{},
}
}
// WriteWithRetry 带重试机制的写入方法
func (c *AdvancedRemoteWriteClient) WriteWithRetry(ctx context.Context, timeseries []*prompb.TimeSeries) error {
c.metrics.TotalRequests++
var lastErr error
for attempt := 0; attempt <= c.maxRetries; attempt++ {
if attempt > 0 {
c.metrics.RetriedRequests++
// 指数退避重试
backoff := c.retryBackoff * time.Duration(1<<uint(attempt-1))
log.Printf("重试发送 (第 %d 次), 等待 %v...", attempt, backoff)
time.Sleep(backoff)
}
err := c.write(ctx, timeseries)
if err == nil {
c.metrics.SuccessfulRequests++
return nil
}
lastErr = err
// 检查是否是可重试的错误
if !isRetryableError(err) {
log.Printf("非可重试错误: %v", err)
break
}
}
c.metrics.FailedRequests++
return fmt.Errorf("写入失败 (尝试 %d 次): %w", c.maxRetries+1, lastErr)
}
// write 内部写入方法
func (c *AdvancedRemoteWriteClient) write(ctx context.Context, timeseries []*prompb.TimeSeries) error {
// 1. 构建 WriteRequest
req := &prompb.WriteRequest{
Timeseries: timeseries,
}
// 2. 序列化为 protobuf
data, err := proto.Marshal(req)
if err != nil {
return fmt.Errorf("序列化失败: %w", err)
}
// 3. 使用 Snappy 压缩
compressed := snappy.Encode(nil, data)
log.Printf("准备发送 %d 个时间序列,压缩后大小: %d 字节", len(timeseries), len(compressed))
// 4. 创建 HTTP 请求
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, bytes.NewReader(compressed))
if err != nil {
return fmt.Errorf("创建请求失败: %w", err)
}
// 5. 设置必要的请求头
httpReq.Header.Set("Content-Type", remoteWriteContentType)
httpReq.Header.Set("Content-Encoding", "snappy")
httpReq.Header.Set("User-Agent", "Prometheus-Remote-Write-Client/"+remoteWriteVersion)
httpReq.Header.Set("X-Prometheus-Remote-Write-Version", remoteWriteVersion)
// 6. 发送请求
startTime := time.Now()
resp, err := c.httpClient.Do(httpReq)
duration := time.Since(startTime)
if err != nil {
return fmt.Errorf("HTTP 请求失败: %w", err)
}
defer resp.Body.Close()
// 7. 检查响应状态
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
// 读取响应体以便调试
var respBody bytes.Buffer
respBody.ReadFrom(resp.Body)
return fmt.Errorf("HTTP 错误: %d, 响应: %s", resp.StatusCode, respBody.String())
}
log.Printf("SUCCESS,发送成功,耗时: %v", duration)
return nil
}
// GetMetrics 获取客户端指标统计
func (c *AdvancedRemoteWriteClient) GetMetrics() *ClientMetrics {
return c.metrics
}
// PrintMetrics 打印指标统计
func (c *AdvancedRemoteWriteClient) PrintMetrics() {
log.Println("\n=== 客户端统计信息 ===")
log.Printf("总请求数: %d", c.metrics.TotalRequests)
log.Printf("成功请求: %d", c.metrics.SuccessfulRequests)
log.Printf("失败请求: %d", c.metrics.FailedRequests)
log.Printf("重试请求: %d", c.metrics.RetriedRequests)
if c.metrics.TotalRequests > 0 {
successRate := float64(c.metrics.SuccessfulRequests) / float64(c.metrics.TotalRequests) * 100
log.Printf("成功率: %.2f%%", successRate)
}
log.Println("====================\n")
}
// isRetryableError 判断错误是否可重试
func isRetryableError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
// 网络错误、超时错误、5xx 错误可以重试
retryablePatterns := []string{
"timeout",
"connection",
"network",
"dial",
"5xx",
"temporarily",
}
for _, pattern := range retryablePatterns {
if contains(errStr, pattern) {
return true
}
}
return false
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 || len(s) > 0 && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || indexOfSubstring(s, substr) >= 0))
}
func indexOfSubstring(s, substr string) int {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}
// 示例:使用增强版客户端
func ExampleAdvancedClient() {
log.Println("=== 增强版客户端示例 ===")
// 创建增强版客户端
client := NewAdvancedRemoteWriteClient(
"http://localhost:9090/api/v1/write",
10*time.Second,
3, // 最多重试 3 次
)
// 模拟多个批次的监控数据
batches := [][]*prompb.TimeSeries{
{
MakeSample("api_requests_total", 1000, map[string]string{
"method": "GET",
"path": "/api/users",
"status": "200",
}),
},
{
MakeSample("http_server_duration_seconds", 0.05, map[string]string{
"method": "GET",
"path": "/api/users",
}),
},
{
MakeSample("active_connections", 42, map[string]string{
"pool": "database",
}),
},
}
// 并发发送多个批次
done := make(chan error, len(batches))
for i, batch := range batches {
go func(id int, data []*prompb.TimeSeries) {
err := client.WriteWithRetry(context.Background(), data)
done <- err
if err == nil {
log.Printf("批次 %d 发送成功", id+1)
} else {
log.Printf("批次 %d 发送失败: %v", id+1, err)
}
}(i, batch)
}
// 等待所有批次完成
for i := 0; i < len(batches); i++ {
<-done
}
// 打印统计信息
client.PrintMetrics()
}
advanced_example.go:
package main
import (
"context"
"fmt"
"log"
"math/rand"
"sync"
"time"
)
// 高级示例:模拟真实的生产环境场景
// Producer 模拟生产环境的数据生产者
type Producer struct {
client *AdvancedRemoteWriteClient
stopCh chan struct{}
wg sync.WaitGroup
interval time.Duration
}
// NewProducer 创建数据生产者
func NewProducer(client *AdvancedRemoteWriteClient, interval time.Duration) *Producer {
return &Producer{
client: client,
stopCh: make(chan struct{}),
interval: interval,
}
}
// Start 开始生产数据
func (p *Producer) Start(ctx context.Context) {
p.wg.Add(1)
go func() {
defer p.wg.Done()
ticker := time.NewTicker(p.interval)
defer ticker.Stop()
for {
select {
case <-p.stopCh:
return
case <-ctx.Done():
return
case <-ticker.C:
p.produceMetrics()
}
}
}()
}
// Stop 停止生产数据
func (p *Producer) Stop() {
close(p.stopCh)
p.wg.Wait()
}
// produceMetrics 生产监控指标
func (p *Producer) produceMetrics() {
// 模拟多个服务的监控数据
metrics := []*prompb.TimeSeries{
// API 服务指标
MakeSample("api_requests_total", float64(rand.Intn(1000)+500), map[string]string{
"service": "api-gateway",
"method": "POST",
"status": "200",
"env": "production",
}),
MakeSample("api_requests_total", float64(rand.Intn(100)+50), map[string]string{
"service": "api-gateway",
"method": "POST",
"status": "500",
"env": "production",
}),
// 数据库服务指标
MakeSample("db_connection_pool_active", float64(rand.Intn(100)+50), map[string]string{
"service": "database",
"pool": "read",
"env": "production",
}),
MakeSample("db_query_duration_seconds", rand.Float64()*0.5, map[string]string{
"service": "database",
"type": "select",
"env": "production",
}),
// 缓存服务指标
MakeSample("cache_hit_rate", rand.Float64(), map[string]string{
"service": "redis",
"cluster": "prod-cluster-1",
"env": "production",
}),
MakeSample("cache_memory_usage_bytes", float64(rand.Intn(1000000000)+500000000), map[string]string{
"service": "redis",
"cluster": "prod-cluster-1",
"env": "production",
}),
// 消息队列指标
MakeSample("mq_messages_in_queue", float64(rand.Intn(10000)+1000), map[string]string{
"service": "kafka",
"topic": "user-events",
"env": "production",
}),
}
// 异步发送,不阻塞主流程
p.wg.Add(1)
go func() {
defer p.wg.Done()
err := p.client.WriteWithRetry(context.Background(), metrics)
if err != nil {
log.Printf("ERROR,发送失败: %v", err)
}
}()
}
// ExampleContinuousSending 演示持续发送数据
func ExampleContinuousSending() {
log.Println("\n=== 持续发送示例 ===")
// 创建客户端
client := NewAdvancedRemoteWriteClient(
"http://localhost:9090/api/v1/write",
10*time.Second,
3,
)
// 创建生产者
producer := NewProducer(client, 5*time.Second)
// 启动生产
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
log.Println("开始持续发送监控数据...")
producer.Start(ctx)
// 等待一段时间
select {
case <-ctx.Done():
log.Println("上下文已取消")
case <-time.After(30 * time.Second):
log.Println("发送时间到")
}
// 停止生产
producer.Stop()
// 打印统计信息
client.PrintMetrics()
}
// ExampleBatchSending 演示批量发送大数据量
func ExampleBatchSending() {
log.Println("\n=== 批量发送示例 ===")
client := NewAdvancedRemoteWriteClient(
"http://localhost:9090/api/v1/write",
30*time.Second,
2,
)
// 创建大量时间序列数据
batch := make([]*prompb.TimeSeries, 0, 1000)
for i := 0; i < 1000; i++ {
batch = append(batch, MakeSample(
fmt.Sprintf("sensor_temperature_%d", i),
rand.Float64()*30+15,
map[string]string{
"location": fmt.Sprintf("room-%d", i/10),
"sensor_id": fmt.Sprintf("sens-%d", i),
"building": "building-a",
},
))
}
log.Printf("准备发送 %d 个时间序列", len(batch))
start := time.Now()
err := client.WriteWithRetry(context.Background(), batch)
duration := time.Since(start)
if err != nil {
log.Printf("ERROR,批量发送失败: %v", err)
} else {
log.Printf("SUCCESS,批量发送成功,耗时: %v (%.2f 个/秒)",
duration, float64(len(batch))/duration.Seconds())
}
client.PrintMetrics()
}
// ExampleWithAuth 演示带认证的写入
func ExampleWithAuth() {
log.Println("\n=== 认证示例 ===")
// 注意:实际使用时需要修改 Client 添加认证支持
log.Println("注意:当前示例未实现认证,生产环境需要添加 Bearer Token 或 Basic Auth")
client := NewAdvancedRemoteWriteClient(
"https://prometheus.example.com/api/v1/write", // HTTPS
30*time.Second,
3,
)
metric := MakeSample("secure_metric", 42.0, map[string]string{
"service": "protected-service",
})
err := client.WriteWithRetry(context.Background(), []*prompb.TimeSeries{metric})
if err != nil {
log.Printf("ERROR,发送失败: %v", err)
} else {
log.Println("SUCCESS,发送成功")
}
}
// ExampleErrorHandling 演示错误处理
func ExampleErrorHandling() {
log.Println("\n=== 错误处理示例 ===")
// 使用错误的 URL 测试重试机制
client := NewAdvancedRemoteWriteClient(
"http://invalid-url:9090/api/v1/write",
5*time.Second,
3,
)
metric := MakeSample("test_metric", 1.0, map[string]string{
"test": "error_handling",
})
err := client.WriteWithRetry(context.Background(), []*prompb.TimeSeries{metric})
if err != nil {
log.Printf("预期错误: %v", err)
}
client.PrintMetrics()
}
// 主函数示例
func RunAdvancedExamples() {
log.Println("======================================")
log.Println("Prometheus Remote Write Client - 高级示例")
log.Println("======================================")
// 示例 1: 基础使用
ExampleAdvancedClient()
// 示例 2: 批量发送
ExampleBatchSending()
// 示例 3: 持续发送(注释掉,避免长时间运行)
// ExampleContinuousSending()
// 示例 4: 认证
ExampleWithAuth()
// 示例 5: 错误处理
ExampleErrorHandling()
log.Println("\n所有高级示例执行完成!")
}

浙公网安备 33010602011771号