Day 18 / 共 20 天 · 第 4 周 扩展/可观测/构建

可观测性 + Admin

"看不见就管不了。" Envoy 提供强大的可观测能力:指标(stats)、访问日志、分布式追踪,外加 Admin 接口内省运行状态。今天看它们怎么实现,尤其是 stats 的无锁设计。

📍 你在整门课的位置 · 第 4 周「扩展/可观测/构建」(D17 Wasm 讲完扩展 → 今天讲怎么"看见"运行状态,且观测本身零开销)
D16 扩展机制 D17 Wasm D18 可观测 D19 Buffer/Bazel D20 收官
L01

可观测三支柱

Metrics / Logs / Traces 可观测性有三大支柱:指标(Metrics)——聚合数字(请求数、错误率、延迟分位数),看趋势;日志(Logs)——每条请求的记录,看细节;追踪(Traces)——一个请求跨多个服务的完整链路,看瓶颈在哪。Envoy 三者都内建支持。作为流量必经之地,Envoy 是采集这些数据的绝佳位置——所有请求都过它,天然能统计和记录。这就是为什么用了 Envoy/Istio 后,服务的可观测性"免费"提升一大截。
💡 三支柱换个记法:仪表盘 / 行车记录仪 / 物流轨迹 Metrics(指标)= 汽车仪表盘——聚合数字看趋势(时速、油量、错误率);Logs(日志)= 行车记录仪——每件事的细节回放(第 3 秒谁请求了啥、返回啥);Traces(追踪)= 快递物流轨迹——一个请求跨 10 个服务的完整链路,看卡在哪一环。三者视角不同、互补。
📝 举个例子:一次 GET /api 同时产出三种数据 指标upstream_rq_total +1rq_time 记 42ms(进直方图);
日志:一行 [2026-07-12T10:00] GET /api 200 42ms upstream=10.0.0.2
追踪:一个 span {traceId: abc, service: gateway, 42ms},串进整条调用链。
一个请求 📊 Metrics 仪表盘Counter/Gauge/Histogram 📼 Logs 记录仪每条请求一行 🛰 Traces 物流轨迹跨服务 span 链
Envoy 在流量必经之处,一个请求同时喂给三支柱——所以用了 Envoy,可观测性"免费"提升一大截。
L02

Stats 接口

// envoy/stats/stats.h
class Counter : public Metric {   // :126 只增不减(如"总请求数")
  void add(uint64_t);  void inc();  uint64_t value();  uint64_t latch();  // 取周期增量
};
class Gauge : public Metric { };  // :142 可增可减(如"当前活跃连接数")
class Histogram { void recordValue(uint64_t); };  // 分布(如延迟分位数)
读法:三种指标类型:Counter(累计只增,如请求总数)、Gauge(瞬时值可增减,如活跃连接)、Histogram(分布,如 P50/P99 延迟)。Scope(scope.h:56)是指标的命名空间(如某集群的指标)。这套模型是 Prometheus 等监控系统的标准。
🤔 痛点:每个请求都要更新一堆指标,加锁就废了 请求量百万级,每个请求都要 +1 请求数记一次延迟…如果每次更新都抢一把全局锁,热路径直接被拖垮——可观测性反倒成了性能杀手。
💡 本质:原子操作 + 线程本地(又是 D13 那招) 两招:①用 std::atomic 无锁地加数字(Counter 的 value_ += n 是原子的);②线程本地缓存指标指针(呼应 D13 集群、D03 线程模型)——每个 Worker 缓存自己常用的指标,不必每次查全局表加锁。Counter 还用第二个原子 pending_increment_ 供采集器定期 latch() 取走并清零。观测再重要,也不能拖慢数据面——所以做到"几乎零开销"。
⚠️ 常见误解:以为 Counter 和 Gauge 差不多。其实 Counter 只增不减(如"累计请求数",重启才归零),Gauge 可增可减(如"当前活跃连接数",实时波动)。监控系统对两者的处理方式完全不同(Counter 常算速率 rate、Gauge 直接看值)。
L03

无锁 stats

// source/common/stats/allocator_impl.cc:140 CounterImpl
std::atomic<uint64_t> value_;              // :166 永久累计
std::atomic<uint64_t> pending_increment_;  // :167 周期增量
void add(uint64_t) { value_ += ...; pending_increment_ += ...; }  // 原子累加
uint64_t latch() { return pending_increment_.exchange(0); }       // 原子取出并清零

// ThreadLocalStoreImpl(thread_local_store.h:161):热路径无锁
//   每线程 TlsCacheEntry 缓存指标指针,未命中才加锁访问 CentralCacheEntry
指标统计怎么"不拖慢请求"? 每个请求都要更新一堆指标(+1 请求数、记延迟…)。如果每次更新都加锁,热路径就废了。Envoy 两招:①用原子操作(std::atomic)——无锁地加数字;②又是线程本地存储(Day 03)——每线程缓存指标指针,避免每次都查全局表加锁。Counter 用两个原子变量:value_ 永久累计、pending_increment_ 供 sink 定期"取走"(latch 原子交换清零)。可观测性再重要,也不能拖慢数据面——这个无锁设计保证了"观测几乎零开销"。Histogram 每线程独立累积、刷新时合并。
L04

SymbolTable

指标名很长(如 cluster.user-service.upstream_cx_total),成千上万个指标存完整字符串很费内存。SymbolTablesymbol_table.h:76)把点分名字按 token 驻留成整型 Symbol,变长编码。

SymbolTable = "字符串去重压缩" 想象你有 1 万个指标,名字里都含 clusterupstream 这些重复词。SymbolTable 把每个词(token)映射成一个短整数,指标名就存成"整数序列"而非完整字符串——大幅省内存。好比把常用词编号,说话时报编号而非全词。这就是为什么 Scope 的接口偏爱 counterFromStatName(用 SymbolTable 编码的名字)而废弃 counterFromString(原始字符串)。海量指标场景下,这个优化省下可观的内存。
L05

访问日志

// envoy/access_log/access_log.h:81
class Instance {
  virtual void log(const LogContext&, const StreamInfo& stream_info) PURE;  // :91
};
// 格式化 FormatterImpl(substitution_formatter.h:85):把 %START_TIME% %REQ(x)% 解析成 provider 序列
// 扩展:access_loggers/ 下 file/grpc/stream/fluentd/open_telemetry/wasm
读法:访问日志记录每条请求(时间、方法、路径、响应码、延迟…)。格式用 %START_TIME%%REQ(header)% 这样的占位符模板(SubstitutionFormatter 解析)。输出目标(file/grpc/…)又是 Day 16 的工厂模式扩展。可配 JSON 格式方便机器解析。
L06

分布式追踪

// envoy/tracing/trace_driver.h
class Span {   // :61 一次操作的追踪跨度
  void setTag(...);  void finishSpan();  void injectContext(...);  SpanPtr spawnChild(...);
};
class Driver { SpanPtr startSpan(...); };  // :167
// 扩展 tracers/:zipkin / datadog / opentelemetry / skywalking / xray
追踪:一个请求的"全程录像" 一个用户请求可能穿过 10 个微服务。出了问题(慢/错),怎么知道卡在哪一环?分布式追踪给每个请求一个 traceId,每经过一个服务/组件就记一个"span"(带时间戳),最后串成一条完整调用链——像快递的物流轨迹。Envoy 在请求头里注入/传递 traceId(injectContext),并上报 span 给 Zipkin/Jaeger/SkyWalking 等后端。因为 Envoy 在每个服务边界都有,它能自动串起跨服务的链路——这是服务网格可观测性的杀手锏。又是工厂模式扩展(各追踪后端一个实现)。
L07

Admin 接口

AdminImplsource/server/admin/admin.h:65)在管理端口上提供内省端点:

端点作用
/stats所有指标(支持 text/json/prometheus 格式)
/clusters所有集群及其端点状态
/config_dump当前完整配置(各 xDS 子系统,脱敏)
/server_info /ready服务信息 / 就绪探针
/logging /quitquitquit动态改日志级别 / 优雅退出
读法:Admin 是运维/调试的窗口——线上 curl localhost:港/config_dump 就能看 Envoy 当前应用了什么配置(排查 xDS 下发问题神器),/clusters 看后端健康状态,/stats/prometheus 给监控系统抓取。Admin 本身复用正常 HTTP 栈(AdminImpl 实现了 HTTP 相关接口)。mutates_server_state_ 的端点必须 POST(防误操作)。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 可观测三支柱各是什么?为什么 Envoy 是采集的好位置?
  • Counter/Gauge/Histogram 各用于什么?
  • stats 怎么做到无锁?(原子 + TLS)
  • SymbolTable 优化什么?
  • 分布式追踪解决什么?Admin 的 /config_dump 有什么用?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/envoy
sed -n '126,145p' envoy/stats/stats.h
sed -n '140,170p' source/common/stats/allocator_impl.cc   # CounterImpl 原子
grep -n 'handlers_{' source/server/admin/admin.cc | head
明天预告 · Day 19Buffer 零拷贝 + Bazel——Envoy 的高性能内存管理(Slice 三段模型、零拷贝 move、水位缓冲背压)+ Bazel 构建系统为何选它。
← Day 17 Wasm Day 19 · Buffer 零拷贝 + Bazel →