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 个服务的完整链路,看卡在哪一环。三者视角不同、互补。
📝 举个例子:一次
日志:一行
追踪:一个 span
GET /api 同时产出三种数据
指标:upstream_rq_total +1、rq_time 记 42ms(进直方图);日志:一行
[2026-07-12T10:00] GET /api 200 42ms upstream=10.0.0.2;追踪:一个 span
{traceId: abc, service: gateway, 42ms},串进整条调用链。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),成千上万个指标存完整字符串很费内存。SymbolTable(symbol_table.h:76)把点分名字按 token 驻留成整型 Symbol,变长编码。
SymbolTable = "字符串去重压缩"
想象你有 1 万个指标,名字里都含
cluster、upstream 这些重复词。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 接口
AdminImpl(source/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 19:Buffer 零拷贝 + Bazel——Envoy 的高性能内存管理(Slice 三段模型、零拷贝 move、水位缓冲背压)+ Bazel 构建系统为何选它。