☰
Higress云原生网关可观测性落地:指标、面板、日志与链路追踪
2026/11/4 14:53:07 网站建设 项目流程

网关这个东西有个很尴尬的特点:它平时不起眼,出事的时候所有人第一个问的就是它。我印象很深的一次,某业务方反馈接口大量超时,运维第一反应是"网关挂了",结果翻遍手头的资料,只找到一个 Pod 的运行状态和几行干巴巴的容器日志,既没有 QPS 曲线,也没有延迟分位,更别提上游到底是哪一台机器慢。最后花了四个小时才定位到是上游某个实例的连接池被打满。从那以后我就给自己定了个规矩:任何一套网关上线之前,可观测性必须跟着一起上线,而不是等出事再补。这篇内容就围绕 Higress 这套云原生网关,把我实际落地的四个监控任务完整梳理一遍——指标接入、面板搭建、日志采集、链路打通。不管你是刚接手网关的新人,还是已经在用 Higress 但只停留在"能转发"阶段的老手,这里面的顺序、取舍和踩过的坑应该都能直接抄。

1. 网关可观测性为什么不能只装一个 Prometheus 就完事

1.1 Higress 的三个进程,指标分散在完全不同的地方

很多人第一次接触 Higress,会下意识把它当成一个单体程序,觉得"装一个 Prometheus 抓一下就行了"。真动手才发现,这套网关在 Kubernetes 里跑起来至少是三个独立的工作负载,各自职责、各自的生命周期、各自的指标出口都不一样。

higress-gateway是数据面,底层跑的是 Envoy。它是真正处理流量的进程,监听 80、443 这些业务端口,同时在 15020 端口上暴露一整套 Envoy 的统计指标,路径是/stats/prometheus;15021 则是健康检查端口。业务流量的所有指标——请求数、延迟、上游连接、重试次数——全部从这里出来。这个进程挂掉,流量立刻不通,问题很直接。

higress-controller是控制面,负责监听 Ingress、各类自定义资源以及配置变更,把用户写的路由规则翻译成 Envoy 能懂的配置,再推给数据面。它自己也暴露指标,端口在部署清单里能看到(不同版本和安装方式可能不同,一定要以实际部署为准),指标内容主要是配置推送次数、推送失败、监听资源数量这类控制面健康度信息。这个进程挂了不会立刻断流,但新配置不生效,表现为"改了路由怎么都不起作用",非常容易误判成配置写错。

higress-console是控制台,提供 Web 界面,同时它内部也集成了监控看板的能力,通常监听 8080。它有自己的一套指标和访问入口,但真正做长期存储和查询的还是外部的 Prometheus。

关键认知:数据面挂了是"现在就不通",控制面挂了是"从现在起改不动"。这两种故障的现象和排查路径完全相反,如果指标里不把两者分开看,你会浪费大量时间在错误的方向上。

我在早期就吃过这个亏。当时改了一条路由规则没生效,第一反应是 YAML 写错了,反复核对字段、反复重启数据面,折腾半天才发现是控制面到数据面的连接出了问题,配置压根没推下去。如果当时控制面的推送指标有面板,一眼就能看出来。

1.2 四个观测面各自回答的是不同的问题

把"可观测性"当成一个整体去搞,最后往往做成一个四不像。更合理的做法是先想清楚:我到底要回答哪些问题?不同的观测面回答的问题是不一样的,它们之间不是替代关系,而是互补关系。

观测面典型数据源能回答的问题不能回答的问题
指标 MetricsPrometheus 抓 15020整体 QPS、延迟分位、错误率趋势、上游健康度某一个具体请求为什么慢
访问日志 Access Log容器标准输出 / Loki / ES单个请求的完整字段、响应码、耗时、上游集群长期趋势和聚合统计
链路追踪 TracingSkyWalking / Zipkin / OTLP一个请求在网关和上游各花了多少时间全量请求(受采样率限制)
控制面事件controller 指标 + K8s 事件配置有没有推下去、推了几次、失败没有业务流量表现

这张表我建议贴在团队的文档里。因为最常见的沟通事故就是:业务方说"你帮我查一下昨天下午三点那个请求为什么失败",你手上只有 Prometheus,根本查不了;反过来,你手上只有日志,想知道整体错误率涨了多少,也只能干瞪眼。四个面各自有明确的分工,指标负责"发现异常",日志负责"定位到具体请求",链路负责"拆解耗时分布",控制面指标负责"排除配置因素"。

顺带说一句,这四个面并不是同等优先级。我的建议顺序是:指标优先、日志其次、链路第三、控制面指标随手补上。原因很简单,指标的成本最低、效果最直接,一条 QPS 曲线就能覆盖 80% 的日常判断;链路追踪的接入成本最高,需要上游服务配合改造,往往推不动。

2. 任务一:让 Prometheus 真的抓到数据面的指标

2.1 别急着写采集配置,先进容器手工看一眼端点

这一步跳过的人特别多,直接照抄一份采集配置就上线,然后对着 Grafana 上空空如也的面板发懵。正确的做法是先确认端点活着、指标长什么样、量有多大。

进入数据面容器手工请求一次:

kubectl -n higress-system exec -it deploy/higress-gateway \ -c higress-gateway -- \ curl -s localhost:15020/stats/prometheus | head -30

你会看到大量以envoy_和istio_开头的行。这一步有三个信息必须记下来:

第一,指标名前缀到底是什么。Higress 复用了 Envoy 的统计体系,同时也带了一部分 Istio 风格的指标,所以envoy_cluster_upstream_rq_total这种原生指标和istio_requests_total这种加工过的指标可能同时存在。后面写 PromQL 的时候用哪个,完全取决于这里看到什么,不要凭记忆写。

第二,指标总量有多大。数一下行数:

kubectl -n higress-system exec -it deploy/higress-gateway \ -c higress-gateway -- \ curl -s localhost:15020/stats/prometheus | grep -c '^envoy_'

这个数字直接决定后面要不要做指标裁剪。如果只有几千行,随便抓;如果到了几万行,就得认真考虑基数问题了。

第三,标签结构长什么样。挑一条istio_requests_total出来看它的标签,重点关注reporter、destination_service_name、source_workload、response_code这几个。这些标签决定了你的面板能不能按服务和按状态码拆分。

2.2 ServiceMonitor 抓不到?大概率是 Service 没声明指标端口

这是我在这个任务上花时间最多的一个坑。场景很典型:集群里装了 Prometheus Operator,写了一个 ServiceMonitor 指向 Higress 的 Service,kubectl get servicemonitor看着挺正常,但 Prometheus 的 Targets 页面里就是不出现,或者出现了但状态是 down。

原因在于 ServiceMonitor 是靠 Service 的端口定义来发现抓取目标的。而 Higress 安装时创建的 Service,通常只声明了 80、443 这类业务端口,并没有把 15020 这个指标端口列进去。ServiceMonitor 里写的port: metrics找不到对应的端口名,自然就抓不到。

两条路可以走,各有代价:

方案做法优点代价
改 Service给 Service 增加一个名为 metrics 的端口映射到 15020配置直观,ServiceMonitor 通用会改变 Service 定义,升级时可能被覆盖,需要额外维护
用 PodMonitor直接按 Pod 标签抓取,指定容器名和端口号不动 Service,Pod 重建自动跟随需要 Prometheus Operator 支持,标签选择器要写对

我个人更倾向 PodMonitor,因为它不侵入原有资源,而且对 Pod 滚动更新更友好——Pod IP 变了它会自动跟上,不用做任何处理。

apiVersion: monitoring.coreos.com/v1 kind: PodMonitor metadata: name: higress-gateway namespace: monitoring spec: namespaceSelector: matchNames: - higress-system selector: matchLabels: app: higress-gateway podMetricsEndpoints: - port: metrics path: /stats/prometheus interval: 15s scrapeTimeout: 10s

这里有两个细节值得单独说。scrapeTimeout必须小于interval,如果两者相等,Prometheus 会出现"上一次还没抓完下一次就开始了"的情况,日志里会刷context deadline exceeded。port字段填的是容器端口名而不是端口号,所以得先去 Pod 定义里确认 15020 这个端口有没有起名字,没有名字的话只能填数字。

2.3 指标基数:不算不知道,一算吓一跳

指标能抓到了,接下来是最容易被忽视、但后期最疼的问题——基数爆炸。

Envoy 的统计是分维度的:全局一份、每个 listener 一份、每个 cluster 一份、每个路由一份。假设你的网关上有 500 条路由规则,每条路由产生大约 20 个维度的统计项,那就是 10000 条时间序列。按 15 秒抓一次算,每条序列一天产生 5760 个采样点,加起来一天就是 5760 万个点。这个量级对 Prometheus 来说还不算致命,但如果路由数量继续涨,内存和磁盘会以肉眼可见的速度被吃掉。

更隐蔽的是 cluster 维度的指标。Envoy 会为每一个上游 cluster 生成几十个指标,如果上游服务数量多、又开了细粒度的统计,序列数量会翻好几倍。

处理办法分两层。源头收敛是首选:如果当前版本的配置里支持 stats 匹配规则(大致是通过前缀包含/排除的方式控制哪些统计项被导出),优先在这里把不需要的维度砍掉,比如不关心的方法级、状态码级的细分统计。这样连抓取都省了,效果最好。

抓取侧收敛是兜底:在 PodMonitor 里用metricRelabelConfigs丢弃明确不需要的指标。

metricRelabelConfigs: - sourceLabels: [__name__] regex: 'envoy_(listener|http).*' action: drop

提示:drop 规则一定要先在小范围验证。我见过有人写了过宽的正则,把istio_requests_total也一起丢掉了,结果面板全空,回头排查了半天才发现是自己的 relabel 干的。

判断标准很简单:如果count({__name__=~".+"})这个查询的返回结果超过几十万,就该动手裁剪了。

2.4 三步确认采集真的生效,而不是"看起来生效"

配置提交之后,别急着打开 Grafana。按这三步走,每一步都能排除掉一类问题。

第一步,去 Prometheus 的 Targets 页面,确认对应的 target 状态是 UP,并且Last Scrape的时间在几十秒之内。如果状态是 DOWN,看错误信息,常见的是连接被拒(端口写错)和超时(网络策略拦了)。

第二步,在 Prometheus 的查询框里执行一个最简查询,比如count(istio_requests_total)。返回非零说明数据已经进来了。如果这里返回空,但 Targets 是 UP,那基本可以断定是指标名写错了,回去翻 2.1 里手工看到的那份输出。

第三步,等五分钟后执行sum(increase(istio_requests_total[5m]))。这个值应该接近这五分钟内网关实际处理的请求数。如果它一直是 0 但 QPS 明明不为零,检查一下是不是查询里加了不匹配的标签过滤。

这三步走完,指标链路就算真正通了。很多"面板没数据"的问题,其实卡在第一步或第二步,只是没人愿意回去看 Targets 页面。

3. 任务二:把 Grafana 面板改造成自己能看懂的样子

3.1 面板变量设计得好不好,决定了这个面板有没有人用

官方或社区提供的面板 JSON 可以直接导入,但导入之后往往不好用,问题几乎都出在变量上。一个没有人愿意点开的面板,等于没做。

我一般会设三个变量,不多不少:

实例变量($instance),从up或istio_requests_total的instance标签里取值,用来在多个网关副本之间切换。查询写成label_values(istio_requests_total, instance)。

服务变量($service),从destination_service_name或对应的上游标签里取,支持多选,默认全选。这个变量是日常用得最多的,排查某业务线问题时就靠它过滤。

状态码变量($code),从response_code取,但一定要加正则约束,只保留2..|3..|4..|5..这类,否则如果网关把一些非标准的状态码也打进来,下拉框会变得极长,每次展开都要等好几秒。

这里有个性能细节:变量查询本身也是一次 Prometheus 查询,如果它扫的时间范围太大或者基数太高,整个面板打开会卡住十几秒。可以在变量定义里加上refresh: 2(只在时间范围变化时刷新),而不是每次打开都刷新。

3.2 四个必看面板,以及它们背后的指标

一个能用的网关看板,核心就四块。其他的都是锦上添花。

第一块:请求速率。这是最基础的,用来判断"现在有没有流量"和"流量是不是突然涨了"。

sum by (destination_service_name) ( rate(istio_requests_total{reporter="destination"}[1m]) )

注意这里的reporter="destination"。这是我最想强调的一个坑:这类指标通常会同时以 source 和 destination 两种视角各上报一份,如果不过滤,同一个请求会被算两次,QPS 直接翻倍。曾经有人拿着翻倍的曲线去扩容,扩完发现根本没效果,白折腾一场。

第二块:延迟分位。P50、P90、P99 三条线画在一起,能立刻看出是"整体变慢"还是"只有尾部变慢"。

histogram_quantile(0.99, sum by (le, destination_service_name) ( rate(istio_request_duration_milliseconds_bucket{reporter="destination"}[5m]) ) )

两个必须注意的点。le必须出现在by里面,漏掉就完全算错。rate 窗口要足够大,至少是抓取间隔的四倍以上,15 秒抓一次的话窗口起码用 1 分钟,用 5 分钟更稳。窗口太小会导致桶里的计数抖动,算出来的分位数上下乱跳,看起来像故障其实只是采样噪声。

还有一个认知上的问题:直方图的分位数是插值出来的,精度受桶边界限制。如果桶在 250ms 和 500ms 之间没有更细的划分,那么真实的 300ms 和 450ms 都会被算成接近 500ms。所以面板上看到的 P99 只能当趋势看,不要拿去做精确的 SLA 判定。

第三块:错误率。4xx 和 5xx 必须分开画,因为它们代表完全不同的含义。5xx 是服务端问题,4xx 大概率是客户端或鉴权问题。混在一起看,会把一次正常的鉴权策略调整误判成线上故障。

sum(rate(istio_requests_total{reporter="destination", response_code=~"5.."}[1m])) / sum(rate(istio_requests_total{reporter="destination"}[1m]))

第四块:上游连接与健康度。这块最容易被忽略,但在排查"偶发超时"时最有用。

sum by (cluster_name) (envoy_cluster_upstream_cx_active) sum by (cluster_name) (rate(envoy_cluster_upstream_rq_pending_total[1m]))

upstream_cx_active是当前活跃的上游连接数,如果它一直贴着连接池上限,说明连接复用出了问题;upstream_rq_pending_total是排队等待连接的请求数,这个值非零就意味着请求在上游侧排队,这就是"网关没毛病但用户就是慢"的典型原因。

3.3 从面板数字倒推故障归属,这才是面板的价值

面板不是用来看的,是用来做判断的。我总结了几种典型组合,实际排查时基本能覆盖八成场景:

现象组合大概率原因下一步查什么
入口 QPS 正常,5xx 上升,上游延迟同步上升上游服务自身问题上游的实例指标、GC、连接池
入口 QPS 正常,5xx 上升,上游完全没有请求路由、证书、鉴权、限流拦截访问日志的 response_flags 字段
P99 抖动但 P50 平稳,上游连接数接近上限连接池不足或上游排队上游连接池配置、pending 计数
所有指标同时掉零,Targets 变 DOWN数据面 Pod 异常或重启Pod 事件、重启次数
流量正常,但配置改了不生效控制面推送失败控制面的推送指标与日志

最后一行那种情况,如果没有控制面指标,你会一直以为是自己 YAML 写错了。这也是为什么第 1 节里强调要把控制面指标一起接进来。

4. 任务三:访问日志的字段清洗与关联查询

4.1 开日志之前,先算一笔存储账

访问日志是最容易失控的一项。指标是聚合数据,量再大也有限;日志是一条请求一条,量跟 QPS 直接成正比。

算一笔账:假设网关平均 1000 QPS,每条 JSON 日志大约 800 字节,那一天就是 1000 × 800 × 86400 ≈ 69 GB,这还是没压缩的原始量。如果全部进 Elasticsearch 并保留 30 天,那就是 2 TB 级别的存储。很多团队就是没算这笔账,开了日志两周后磁盘告警。

所以开日志之前必须先确定三件事:保留多久、全量还是采样、存哪里。我的经验是:核心业务接口全量保留 7 天,非核心接口采样 10% 保留 3 天,超过 7 天的只保留聚合统计不进原文。这套策略落地之后,存储成本能压到原来的三分之一左右。

4.2 日志格式和 response_flags 字段,是排障的关键

Higress 的访问日志通过在配置里指定编码格式和字段模板来控制,一般配置成 JSON 编码,方便后续解析。日志默认走容器标准输出,由节点上的日志采集组件收走,送进 Loki 或者 ES。

日志里字段不少,但真正高频使用的就那么几个。其中最有价值、也最容易被忽视的是response_flags。这个字段是 Envoy 用来标记请求异常原因的短代码,含义非常明确:

标记含义通常意味着
-正常完成无需关注
UF上游连接失败上游实例不可达、端口错、网络策略拦截
UT上游响应超时上游处理太慢,或超时配置过短
UO上游溢出熔断触发或连接池耗尽
NR没有匹配路由路由规则缺失或 host 不匹配
URX重试次数超限重试策略配置不当或上游持续失败
DC下游连接断开客户端主动断连,常见于超时后放弃
LH本地健康检查失败上游实例被健康检查摘除

有了这张表,很多事情不用翻上游代码就能定性。比如 5xx 上涨,如果日志里response_flags全是UF,那问题一定在网关到上游这一段,跟业务逻辑没关系;如果全是UT,那就是上游处理慢,得去查上游的耗时分布;如果全是NR,恭喜你,路由配错了。

我个人的习惯是,在日志查询面板里直接把response_flags做成一个筛选项,排障第一步就是按它分组统计,几秒钟就能把范围缩小一半。

4.3 用 request_id 把日志、指标、链路串成一条线

单独看日志,你只能看到"这个请求失败了";单独看指标,你只能看到"错误率涨了"。真正有价值的是能把两者关联起来。

关联的钥匙是请求 ID。网关会为每个请求生成或透传一个请求标识,同时把它写进访问日志和响应头。排查时的标准动作是:从用户或前端拿到这个 ID,在日志系统里查出对应的那一条日志,拿到它的完整字段——上游集群、耗时、状态码、response_flags,再拿这个 ID 去链路系统里找对应的 trace。

这里有个前提条件:上游服务必须把这个请求 ID 继续往下传,否则你只能追到网关这一层就断了。这一点需要在接入时就跟业务方约定好,把它当成接入规范的一部分,而不是可选项。

另外提醒一句,请求 ID 和 trace ID 不是一回事。请求 ID 是网关生成的、每条请求唯一的标识;trace ID 是链路追踪体系里的概念,需要上游链路组件配合才能贯穿。两者可以互相映射,但不能混用。我在早期就把它们当成一个东西,结果拿着请求 ID 去链路系统里搜,什么都搜不到。

5. 任务四:链路追踪接通网关到上游的最后一跳

5.1 采样率配置:那个最容易被误解的数字

链路追踪的配置在网关的全局配置里,一般是一段包含开关、采样率、上报地址和超时的配置块。开启之后,网关会在转发请求时生成或继承 trace 上下文,并把 span 上报到指定的后端。

这里有个坑我必须单独讲:采样率字段的语义。很多链路组件的配置里,这个字段是百分比,填100表示全采样,填1表示 1%。但也有些地方是 0 到 1 的小数。如果你以为是 0 到 1 的小数,填了个1想表达"全采",结果实际只采了 1%,然后你会陷入一个非常迷惑的状态——链路系统里明明有数据,但就是少得可怜,怎么调都对不上。

我的建议是:配置改完之后,用一小段确定性的流量去验证。比如手动发 100 个请求,然后去后端数一下 trace 条数。100 条说明是全采,1 条说明是 1%。这个验证花不了两分钟,但能省掉后面几个小时的困惑。

采样率怎么定?我的经验值是:日常 1% 到 5%,压测和大促期间临时调到 100%。全采的代价不只是存储,还有网关侧生成 span 的 CPU 开销和上报的网络开销,在几万 QPS 的场景下这个开销是能感知到的。

5.2 上下文透传失败的三种典型情况

链路追踪最容易失败的地方不是网关,而是网关到上游这一跳。我遇到过三种:

第一种,上游没有链路组件。网关这边 span 生成得好好的,也注入了 trace 头,但上游服务压根不认这个头,也不会创建子 span。表现就是链路树里只有网关一个节点,孤零零的。这不是网关的问题,得推上游接入。

第二种,传播格式不一致。链路上下文在 HTTP 头里传递是有格式标准的,常见的有两种格式,新老系统混用的情况非常普遍。如果网关注入的是 A 格式,上游解析的是 B 格式,上游就找不到父 span,会自己新建一条全新的 trace。表现是链路系统里同一个请求出现了两条互不相干的 trace,而不是一条完整的树。解决办法是把两边的传播格式配置统一。

第三种,头部被中间层清掉。有些架构里,网关到上游之间还有一层代理或者服务网格,如果它配置了请求头白名单,跟链路相关的头可能被过滤掉。这种问题最难查,因为每一层单独看都正常。定位方法是在上游服务里打印一下收到的请求头,看那个 trace 头还在不在。

5.3 端到端验证:怎么确认链路是真通了而不是"看起来通了"

验证方法很直接:找一个能确定经过网关的请求,发出去,然后去链路系统里找这条 trace,看它的结构。

正确的结构应该是这样的:最顶层是网关生成的 span,它的子节点是上游服务生成的 span,上游的子节点再往下是数据库、缓存这些。整条链路上每一层的耗时都能看到,加起来大致等于总耗时。

如果链路树里只有网关这一层,说明上游没接;如果出现了两条平级的 trace,说明传播格式没对上;如果上游的 span 有但耗时是 0,说明上游的埋点位置不对(通常埋在了响应返回之后)。

这一步做完,四个观测面就算全部打通了。指标告诉你哪里不对,日志告诉你具体是哪个请求不对,链路告诉你这个请求的时间花在哪儿,控制面指标排除掉"配置压根没生效"这种假故障。

6. 落地之后才发现的成本与告警问题

6.1 指标基数的增长是渐进的,不会一次爆掉

前面提到过基数问题,但真正麻烦的地方在于它不会一次性爆发。今天加五条路由,明天加十条,指标数量缓慢上涨,Prometheus 的内存曲线也是缓慢上涨,等到某天它突然 OOM,你会觉得"昨天还好好的"。

我的做法是加一个自监控指标:定期统计时间序列总数,画一条曲线,同时设一个软阈值。超过某个数量就在周会上提一下,让负责加路由的同学知道有成本。这比事后救火有效得多。

另外,给指标数据单独设保留策略也很必要。原始数据保留 15 天,超过的部分用降采样后的聚合数据代替,这是 Prometheus 长期存储的常规操作,不做的话磁盘迟早撑不住。

6.2 日志的坑不在采集,在字段解析

日志采集本身不难,难的是字段解析。JSON 格式的日志如果是嵌套结构,在 Loki 里的查询语法会变得很长;如果是扁平结构,可读性好但字段多的时候又很占空间。

我踩过的坑是用错了时间字段。网关日志里通常有多个时间相关的字段,一个是请求开始时间,一个是处理耗时。如果采集配置里把日志的时间戳解析成了采集时间而不是请求开始时间,那么所有日志的时间都会往后偏,排查时按时间窗口筛选会漏掉记录。这个错误非常隐蔽,因为日志内容本身是对的,只有时间戳偏了。验证办法是拿一条日志的实际内容和它的入库时间比对一下,差值应该在毫秒级。

6.3 别对 P99 直接告警

最后一个经验:告警规则的设计比面板设计更重要。最常见的错误是直接对 P99 延迟设阈值告警,结果要么天天误报(长尾本来就抖),要么阈值设得太松什么也抓不到。

我现在的做法是用错误率加上"延迟超过某个绝对阈值"的请求占比来告警。比如"5xx 比例连续 5 分钟超过 1%"或者"耗时超过 3 秒的请求占比超过 0.5%"。这类指标比 P99 稳定得多,不容易被个别极端值带偏,同时又能真实反映用户感受到的慢。

多窗口也是一个好办法,短窗口(1 分钟)用于快速发现,长窗口(10 分钟)用于确认持续性,两个都满足才触发。这样能过滤掉绝大部分瞬时抖动,告警的信噪比会明显提升。

这套东西从零搭到能用,我一个人大概花了两天半,其中一半时间卡在 ServiceMonitor 抓不到数据和采样率语义这两件事上。如果先把手工验证端点、算日志存储账这两个动作做在前面,实际能省掉大半天。真要说还有什么建议,那就是别想着一次把四个面全做齐,先做指标和面板,用起来之后自然会知道日志和链路该往哪个方向补。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询