目录
安全基础 认证机制 授权模型 安全编码 可观测性三支柱
01

可观测性三支柱

背景 为什么需要三支柱?

传统监控只有指标 (Metrics)——CPU、内存、请求数等数值,但这只能告诉我们系统「现在有问题」,却很难回答「为什么有问题」。为了解决这个痛点,可观测性引入了三个核心信号:指标 (Metrics)日志 (Logging)追踪 (Tracing)。三者相互补充,共同构成了现代可观测性体系。

第一性原理: 可观测性的本质是「从不同的维度获取系统的运行状态信息」。Metrics 是「系统状态的聚合视图」,告诉你发生了什么;Logging 是「离散事件的详细记录」,告诉你具体细节;Tracing 是「请求在全链路的流转路径」,告诉你问题发生在哪个环节。三者的关系可以类比为:Metrics 是仪表盘,Logging 是黑匣子,Tracing 是地图

原理 三支柱的数据模型与相互配合

支柱数据类型典型工具主要用途
Metrics数值型时间序列Prometheus, Zabbix趋势分析、告警、容量规划
Logging文本型事件记录ELK, Loki故障排查、审计、业务分析
Tracing请求链路数据Jaeger, Zipkin分布式调用分析、性能瓶颈定位
可观测性三支柱 Metrics • CPU 使用率 • 内存占用 • 请求速率 • 错误率 聚合数据 · 趋势告警 Logging • 错误堆栈 • 访问日志 • 业务事件 • 调试信息 离散事件 · 详细上下文 Tracing • 请求链路 • 服务调用关系 • 耗时分布 • 依赖分析 调用链 · 跨服务关联 图:可观测性三支柱,相互补充,共同构成完整视图
图:可观测性三支柱,相互补充,共同构成完整视图
▸ 三支柱配合示例
# Metrics 告警:检测到高延迟 alert: 请求延迟超过 500ms for: 5m annotations: 告警 ID: "high_latency" # Logging 定位:查看具体日志 grep "high_latency" /var/log/app.log # 输出: [ERROR] 请求 12345 处理耗时 1200ms,DB 超时 # Tracing 追踪:找出具体瓶颈 jaeger 查询 trace_id = 12345 # 显示: API → 订单服务 (600ms) → 数据库 (500ms) → 返回

演进 Metrics 中心 → 三支柱融合

  • 监控时代 (1980s-): 以 Metrics 为主,关注系统资源利用率
  • 日志分析时代 (2000s-): ELK 等工具兴起,日志成为故障排查的主要手段
  • 分布式追踪时代 (2010s-): 微服务架构需要跨服务调用链追踪,Jaeger、Zipkin 出现
  • 可观测性融合 (2018+): OpenTelemetry 统一了 Metrics、Logs、Traces 的数据标准
"可观测性的演进是一个『从孤立到融合』的过程。从单一的 Metrics 监控,到三支柱的协同,再到 OpenTelemetry 的统一标准,我们终于有了一个完整的『系统透镜』。"
—— 可观测性设计哲学

取舍 设计中的权衡

📊 聚合度 vs 信息量
Metrics 聚合程度高,信息密度大但细节缺失;Logging 信息丰富但搜索效率低;Tracing 关联性强但存储开销大。
⚡ 采样 vs 精度
Tracing 通常需要采样,否则存储成本过高。采样率的选择需要在覆盖度和成本之间平衡。
🔧 标准 vs 生态
OpenTelemetry 提供了统一标准,但生态系统还在建设中,部分厂商仍在使用自家协议。
02

Prometheus 数据模型与 PromQL

背景 为什么 Prometheus 成为监控标准?

Prometheus 是受 Google 内部监控系统 Borgmon 启发而开发的。它的核心设计理念是「基于拉取的监控模型」「多维数据模型」。与传统的基于推的监控系统(如 Zabbix)不同,Prometheus 主动从目标端拉取指标,通过标签 (Label) 实现多维度的查询和分析。

第一性原理: Prometheus 的本质是一个「基于标签的时间序列数据库」。它的数据模型将每个指标视为 (指标名, 标签集)(时间戳, 值) 的映射。标签的设计使得我们可以从任意维度对数据进行切片和聚合,极大地提高了查询的灵活性。PromQL (Prometheus Query Language) 提供了丰富的时间序列操作符,使得我们可以进行趋势分析、聚合计算、告警规则编写等高级操作。

原理 指标 · 标签 · 时间序列 · 查询操作

数据模型:

  • metric_name:指标名,如 http_requests_total
  • labels:键值对,如 method="GET", status="200"
  • sample:时间戳 + 值

PromQL 核心操作:

  • rate():计算每秒增长率,用于 Counter
  • sum() by:按标签聚合,类似 SQL 的 GROUP BY
  • avg()max()min():聚合函数
  • [5m]:时间范围选择器
Prometheus 数据模型与 PromQL 时间序列:http_requests_total{method="GET", status="200"} t0: 100 t1: 105 t2: 110 t3: 115 t4: 120 t5: 130 t6: 140 t7: 150 PromQL 查询示例 rate(http_requests_total[5m]) → 每秒请求率 sum by (status) (http_requests_total) → 按状态码聚合 图:Prometheus 数据模型及 PromQL 查询示例
图:Prometheus 数据模型及 PromQL 查询示例
▸ PromQL 查询与告警
# 查询 CPU 使用率 (百分比) 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) # 查询 95 分位延迟 histogram_quantile(0.95, sum by(le) (http_request_duration_seconds_bucket)) # 告警规则: 请求错误率超过 5% groups: - name: http_errors rules: - alert: HighErrorRate expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05 for: 10m labels: severity: critical

演进 Borgmon → Prometheus → Mimir/Thanos

  • Borgmon (Google): Prometheus 的前身,内部使用的监控系统
  • Prometheus (2012): 开源版本,采用拉取模型和多维数据模型
  • Prometheus 2.0 (2017): 使用 TSDB 重新实现存储引擎,大幅提升性能
  • Mimir / Thanos (2018+): 提供长期存储和全局视图,解决 Prometheus 的单点问题
"Prometheus 的成功在于其『简洁而强大』的设计。拉取模型避免了推模型的配置复杂性,标签模型实现了灵活的数据聚合。它重新定义了现代监控的标准。"
—— 监控设计哲学

取舍 设计中的权衡

📦 拉取 vs 推送
拉取模式便于服务发现和故障检测,但需要目标端暴露端口,且对复杂网络环境(如 NAT)不友好。
⚡ 本地存储 vs 长期存储
Prometheus 的本地存储效率高,但数据保留时间有限。需要结合 Thanos/Mimir 实现长期存储和全局聚合。
🔧 标签基数爆炸
高基数标签(如用户 ID、请求 ID)会导致存储膨胀和查询性能下降。需要合理设计标签,避免高基数。
03

时序数据库原理

背景 普通数据库为什么不能存储时序数据?

时序数据具有写多读少数据点持续增加时间顺序写入等特点。传统关系数据库的 B+ 树索引机制在大量写入时性能较差,且存储膨胀严重。时序数据库 (TSDB) 专为时序数据优化,通过列式存储压缩技术时间分区等设计,实现了高效写入和查询。

第一性原理: 时序数据库的核心是「针对时序数据的存储和查询优化」。它通过 LSM 树(Log-Structured Merge Tree)处理写入,通过 列式存储Delta 编码 压缩数据,通过 倒排索引 实现多维查询。Prometheus 的 TSDB 采用了 Gorilla 压缩算法,实现了高达 10:1 的压缩率,使得时序数据可以高效存储。

原理 LSM 树 · 列式存储 · Gorilla 压缩 · 倒排索引

写入流程: 数据先写入内存中的 MemTable,定期刷写到磁盘的 SSTable(Sorted String Table)。通过 Compaction 合并和清理删除的数据。

压缩技术: Gorilla 压缩利用 XOR 编码存储浮点数变化,对时间戳使用 Delta-Delta 编码。

时序数据库核心结构 写入 → MemTable 刷写 → SSTable Compaction 倒排索引 (Inverted Index) metric: http_requests_total → [SSTable_1, SSTable_2] label: method="GET" → [SSTable_1, SSTable_3] Gorilla 压缩: 原始 100MB → 压缩后 10MB (压缩率 10:1) 图:时序数据库的写入、压缩、索引和存储结构
图:时序数据库的写入、压缩、索引和存储结构
▸ Gorilla 压缩伪代码
// Gorilla XOR 编码 (浮点数压缩) double previous_value; uint64_t prev_xor = 0; void compress_value(double value) { uint64_t current_bits = float_to_bits(value); uint64_t xor = current_bits ^ prev_xor; if (xor == 0) { // 值完全一样,只需写一个控制位 write_bit('0'); } else { write_bit('1'); int leading_zeros = count_leading_zeros(xor); int trailing_zeros = count_trailing_zeros(xor); // 使用前导零和后导零信息压缩 XOR 值 encode_xor_with_zeros(xor, leading_zeros, trailing_zeros); } prev_xor = current_bits; }

演进 B+ 树 → LSM 树 → 列式存储

  • B+ 树 (传统 DB): 随机读快但写入性能差,适合 OLTP
  • LSM 树 (LevelDB, 2011): 写入性能高,适合时序数据
  • Gorilla 压缩 (2015): Facebook 提出的时序压缩算法,压缩率 10:1
  • Prometheus TSDB (2017): 采用 LSM + Gorilla 压缩,专门为监控场景优化
"时序数据库的演进是『从通用到专用』的过程。B+ 树被设计为通用数据库,LSM 树为写密集型场景优化,Gorilla 压缩则专门针对时序浮点数。专用化设计让 TSDB 在处理时序数据时效率远超通用数据库。"
—— 存储设计哲学

取舍 设计中的权衡

⚡ 压缩 vs 查询性能
高压缩率减少存储成本,但解压缩需要额外 CPU。需要根据查询频率和数据访问模式选择压缩策略。
📈 列式 vs 行式
列式存储适合时序数据(按时间查询),但对单条记录的更新操作不友好。时序数据通常是只增不改的,因此列式存储是理想选择。
🔧 预聚合 vs 原始数据
预聚合可以减少查询时间,但丢失了原始数据的灵活性。需要根据常用查询模式设计预聚合策略。
04

ELK 日志架构

背景 如何从海量日志中快速检索信息?

传统日志分析使用 grepawk 在单机上进行,当服务器数量增加、日志量达到 TB 级别时,这种方式已经无法满足需求。ELK 架构(Elasticsearch + Logstash + Kibana)提供了一套分布式日志采集、存储、搜索、可视化的完整方案。

第一性原理: ELK 架构的本质是「一个分布式日志处理流水线」。Logstash 负责采集和解析,Elasticsearch 负责存储和索引,Kibana 负责可视化和查询。Elasticsearch 的核心是倒排索引 (Inverted Index),它使得我们可以像搜索引擎一样在海量日志中快速检索关键字。这种设计将日志分析从「文件扫描」转变为「索引查询」,效率提升了几个数量级。

原理 Logstash 流水线 · Elasticsearch 索引 · Kibana 可视化

Logstash 流水线: 输入 → 过滤器 → 输出。通过 grok 正则解析非结构化日志,mutate 转换字段,date 解析时间戳。

Elasticsearch 索引: 文档 → 倒排索引 → 搜索。支持 termmatchrange 等查询,支持聚合分析。

ELK 日志架构 日志源 App/Server Beats 采集器 Logstash 解析/过滤 Elasticsearch 存储/索引 Kibana 可视化 数据流水线:采集 → 解析 → 索引 → 查询 Elasticsearch 倒排索引 词条 文档列表 "error" [doc1, doc3, doc5, ...] "timeout" [doc2, doc3, doc6, ...] 图:ELK 架构的数据流水线及倒排索引原理
图:ELK 架构的数据流水线及倒排索引原理
▸ Logstash 配置示例
# Logstash 流水线配置 (解析 Nginx 日志) input { beats { port => 5044 } } filter { grok { match => { "message" => "%{IP:client_ip} - - [%{HTTPDATE:timestamp}] \"%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}\" %{NUMBER:status} %{NUMBER:bytes}" } } date { match => [ "timestamp", "dd/MMM/yyyy:HH:mm:ss Z" ] } useragent { source => "http_user_agent" target => "user_agent" } } output { elasticsearch { hosts => ["localhost:9200"] index => "nginx_logs_%{+YYYY.MM.dd}" } }

演进 ELK → ELK Stack → EFK → Loki

  • ELK (2010): Elasticsearch + Logstash + Kibana 日志分析平台
  • Beats (2015): 轻量级采集器(Filebeat、Metricbeat),替代 Logstash 的采集部分
  • EFK (2016): Elasticsearch + Fluentd + Kibana,Fluentd 作为替代 Logstash 的日志采集器
  • Loki (2018): 轻量级日志聚合系统,与 Prometheus 生态整合
"ELK 架构的演进反映了日志系统的『轻量化』『云原生』趋势。从 Logstash 到 Beats,从 Fluentd 到 Loki,日志采集和存储正在变得更轻、更高效。"
—— 日志设计哲学

取舍 设计中的权衡

📦 索引 vs 存储成本
Elasticsearch 的倒排索引提供了高效的查询,但也增加了存储成本。需要根据查询需求合理设计索引策略,如使用索引模板和生命周期管理。
⚡ Logstash vs Filebeat
Logstash 功能强大但资源消耗高;Filebeat 轻量但功能有限。通常使用 Filebeat 采集,Logstash 用于复杂解析。
🔧 结构化 vs 非结构化
结构化日志便于查询,但需要额外的解析工作;非结构化日志存储方便,但查询效率低。需要在采集时进行适当的结构化处理。
05

分布式链路追踪原理

背景 如何追踪跨服务的请求调用链?

在微服务架构中,一个请求可能跨越多个服务、多个进程。传统的日志和指标无法将同一个请求在不同服务中的行为关联起来。分布式链路追踪通过在请求中传递上下文 (Context),将请求的完整执行路径串联起来。

第一性原理: 分布式追踪的核心是「在请求传播过程中传递一个唯一的标识符」。这个标识符称为 Trace ID,每个服务在处理请求时生成 Span(一个操作单元),Span 中包含父 Span 的引用,形成树状结构。通过这个树状结构,我们可以完整地还原一个请求在分布式系统中的执行路径。OpenTelemetry 标准统一了 Trace 的生成和传播方式,使得不同语言的 SDK 可以无缝协作。

原理 Trace · Span · Context Propagation · 采样

核心数据结构:

  • Trace ID:全局唯一标识一个请求
  • Span ID:标识一个操作(如 HTTP 调用、数据库查询)
  • Parent Span ID:指向父 Span,构建调用树

Context Propagation: 通过 HTTP Headers(如 traceparent)或 gRPC Metadata 传递 Trace 上下文。

分布式链路追踪模型 服务 A Span A1 服务 B Span B1 服务 C Span C1 服务 D Span D1 Span 树结构 (Trace ID: abc-123) ├─ Span A1 (服务 A) 耗时: 200ms │ ├─ Span B1 (服务 B) 耗时: 150ms │ │ └─ Span C1 (服务 C) 耗时: 100ms │ └─ Span D1 (服务 D) 耗时: 30ms 图:分布式请求调用链及其对应的 Span 树结构
图:分布式请求调用链及其对应的 Span 树结构
▸ OpenTelemetry 埋点示例 (Python)
# 使用 OpenTelemetry 创建 Span from opentelemetry import trace from opentelemetry.trace import TracerProvider # 初始化 tracer tracer = trace.get_tracer("my-service") def handle_request(request): # 创建一个新的 Span,作为根 Span with tracer.start_as_current_span("handle_request") as span: span.set_attribute("http.method", request.method) span.set_attribute("http.url", request.url) # 调用下游服务 (自动传递 Trace Context) with tracer.start_as_current_span("call_service_b") as child_span: call_service_b(request) child_span.set_attribute("service.b.status", "success") # 记录异常 (如果有) span.record_exception(exception) span.set_status(trace.Status(trace.StatusCode.ERROR, "DB error")) # 传播 Trace Context 到 HTTP 请求头 # 在 outgoing HTTP 请求中注入 traceparent 头 headers = {} propagator.inject(headers) requests.get("http://service-b/api", headers=headers)

演进 Dapper → Zipkin/Jaeger → OpenTelemetry

  • Dapper (Google, 2010): 分布式追踪的开山之作,内部系统
  • Zipkin (2012): 开源的分布式追踪系统,基于 Dapper 的论文实现
  • Jaeger (2016): Uber 开源的分布式追踪系统,支持多种存储后端
  • OpenTelemetry (2019): 统一的标准,融合 OpenCensus 和 OpenTracing
"分布式追踪的演进是从『专有系统』『统一标准』的过程。Dapper 揭示了分布式追踪的通用模式,Zipkin 和 Jaeger 提供了开源实现,OpenTelemetry 则提供了跨语言、跨厂商的统一标准。"
—— 追踪设计哲学

取舍 设计中的权衡

📊 采样 vs 全覆盖
全覆盖追踪会带来巨大的存储和网络开销,通常采用采样策略(如 1% 或 10%)。采样率的选择需要在覆盖度和成本之间平衡。
⚡ 上下文传播 vs 性能
在 HTTP 头中传递 Trace 上下文会增加少量网络开销和代码复杂度,但这是跨服务追踪的必要代价。
🔧 埋点 vs 无侵入
手动埋点精确但需要修改代码;自动埋点(如代理模式)无侵入但可能丢失上下文信息。需要在精确性和便利性之间选择。