跳到主要内容

Pushgw 队列、写入器与双写

写入路径:队列大小、背压、多写入器、同时转发到两个后端。

Pushgw 是「样本进来、转发出去」的那一段。它在 n9e 进程里默认就开着, 写入量不大时不用管;一旦要做双写、或者开始丢样本,就得知道队列长什么样、 背压怎么表现、以及哪些失败是静默的。

写入路径​

采集器把样本 POST 到下面这些端点,Pushgw 补上设备标签、按丢弃规则过滤, 放进内存队列,再由消费协程批量转发给每个配置好的写入器。

端点协议
POST /prometheus/v1/writePrometheus remote write(categraf、vmagent 走这个)
POST /opentsdb/putOpenTSDB
POST /openfalcon/pushOpen-Falcon
POST /datadog/api/v1/seriesDatadog v1 series
POST /proxy/v1/write原样透传的 remote write 代理,见最后一节

这些端点由 [HTTP.APIForAgent] Enable 统一控制(示例配置里是 true)。 关掉它,Pushgw 就只剩一个内部接口,采集器全部写不进来。

两个默认打开的行为(示例配置里都是 true):

  • LabelRewrite = true——序列里的标签和设备列表里登记的标签冲突时,以数据库里的为准;
  • ForceUseServerTS = true——用服务端时间覆盖样本时间,避免采集端时钟不准。 注意它只改每条序列的第一个样本,一次带多个点的序列后面几个点保持原时间。

队列:数量、容量和那个 10% 水位​

[Pushgw.WriterOpt]
QueueMaxSize = 1000000 # 每个队列的容量
QueuePopSize = 1000 # 每次从队列里取多少条转发

配置文件里注释掉的就这两项,但实际生效的还有几个没写进示例配置的:

项默认值含义
QueueNumberCPU 核数(单核时为 128)队列个数
QueueMaxSize1000000单个队列的容量
QueuePopSize1000每批取出的条数
QueueWaterMark0.1全局准入水位
RetryCount1000写失败重试次数
RetryInterval1重试间隔(秒)
OverLimitStatusCode499队列超限时返回的状态码

全局准入上限是 QueueNumber × QueueMaxSize × QueueWaterMark。 8 核机器上就是 8 × 1000000 × 0.1 = 80 万,不是 800 万——这个 10% 折扣很容易估错。

队列编号是按请求轮转分配的,不是按样本。一个请求里的所有样本进同一个队列, 所以少数几个连接发超大批次时,会把某一个队列打热。每个队列有且只有一个消费协程。

满了会发生什么​

三种丢弃是三回事,别混:

情况客户端看到计数器
全局水位超限(只在 /prometheus/v1/write 上判)HTTP 499,整个请求被拒n9e_pushgw_push_queue_over_limit_error_total
单个队列写满HTTP 200,样本静默丢弃n9e_pushgw_push_queue_error_total{queueid}
命中丢弃规则HTTP 200n9e_pushgw_drop_sample_total

第一种要特别注意:499 是 4xx。Prometheus、vmagent、categraf 这类客户端普遍把 4xx 当成「这批数据有问题」而直接丢掉,不会退避重试,所以背压并不会传导回采集端。

第二种更隐蔽:HTTP 返回 200,样本没了,唯一的证据是那个带 queueid 标签的计数器 和日志里的一行 warning(那行 warning 会把整条序列打出来,队列满的时候很容易刷爆日志)。

所以「有没有丢样本」不能靠客户端的返回码判断,只能看指标。

多写入器就是扇出​

每多写一个 [[Pushgw.Writers]] 块,就多一个后端,每一批样本都会发给每一个后端:

[[Pushgw.Writers]]
Url = "http://victoriametrics:8428/api/v1/write"

[[Pushgw.Writers]]
Url = "http://prometheus:9090/api/v1/write"

三个容易踩的地方:

  • 一个 Url 里用逗号分隔多个地址,是「依次尝试、第一个成功就停」的容灾,不是扇出。 要扇出就写多个 [[Pushgw.Writers]] 块。
  • 一个块里写两行 Url =,TOML 只保留后一行,前一行静默失效。 示例配置里正好有这么两行(一行注释掉了),复制粘贴时容易两行都留着。
  • 写入器收到 4xx 会被当成写成功。 目标地址写错、路径写错(比如少了 /api/v1/write)时, 样本会静默消失,n9e_pushgw_write_error_total 一动不动。配完新写入器一定要到目标库里 查一下数据真的到了。

写入器的超时默认值:Timeout = 10000、DialTimeout = 3000、TLSHandshakeTimeout = 30000(毫秒)。 WriteRelabels 是每个写入器各自的,写在哪个块里就只影响哪个后端。

一个卡住的写入器会拖垮整个队列​

默认的写入器是「关键后端」:消费协程串行地、同步地给每个关键后端写, 写失败重试 RetryCount(1000)次、每次间隔 RetryInterval(1 秒)。 一个后端不可达,最坏情况下这一批要卡将近 17 分钟, 而这期间同一个队列上的其他后端一个字节都写不出去,队列随即填满开始丢样本。

外部的、允许丢的那个后端,加上 AsyncWrite:

[[Pushgw.Writers]]
Url = "http://third-party:8428/api/v1/write"
AsyncWrite = true

打开之后这个后端变成「非关键」:每批开独立协程写,重试次数被压到 3 次, 它慢或者挂了不会拖住别的后端。代价是丢样本时更安静。

内置时序库也是一个写入器​

[EmbeddedTSDB] Enable = true 时,Center 会追加一个指向自己的写入器 (http://127.0.0.1:17000/prometheus/api/v1/write),你配的 [[Pushgw.Writers]] 原样保留。 这就是双写的实现方式——两者不是二选一。

由此推出两件事:

  • 迁移到外部时序库时,先加 [[Pushgw.Writers]],等外部库攒够历史再把 [EmbeddedTSDB] Enable 改成 false,全程不用停机;
  • 内置时序库那个写入器也是「关键后端」,它卡住同样会拖住外部写入器。

[EmbeddedTSDB] 只有 Center 处理。n9e-edge / n9e-alert / n9e-pushgw 读同一个 etc 目录时会打印一行 warning 然后忽略这一段。

/proxy/v1/write:不解包的透传​

这个端点不解 snappy、不解 protobuf、不补标签、不进队列、不应用 relabel 和丢弃规则, 只是把请求体原样再 POST 给每个 [[Pushgw.Writers]]。

限制默认值超限时
[Pushgw] ProxyInflightMax1000HTTP 429
[Pushgw] ProxyMaxBodyBytes33554432(32 MiB)HTTP 413

转发是尽力而为:不重试,失败只记日志,客户端拿到的永远是 200。 它的可观测性靠 n9e_pushgw_proxy_forward_error_total{url,reason}。

注意它返回的 429 才是标准的「稍后重试」,而队列路径返回的是 499。

该盯的指标​

指标看什么
n9e_pushgw_samples_received_total{channel}进来的总量,突然掉下去就是采集侧出问题了
n9e_pushgw_sample_queue_size{queueid}队列积压,某一个队列独高说明请求分布不均
n9e_pushgw_push_queue_over_limit_error_total请求被拒
n9e_pushgw_push_queue_error_total{queueid}样本被丢,这个非 0 就是真在掉数据
n9e_pushgw_write_total{url} / n9e_pushgw_write_error_total{url}每个后端写出去多少、失败多少
n9e_pushgw_forward_duration_seconds{url}每个后端的写入耗时,用来找那个拖后腿的

相关​