惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Securelist
K
Kaspersky official blog
V
Vulnerabilities – Threatpost
C
Cisco Blogs
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hacker News: Ask HN
Hacker News: Ask HN
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Security Affairs
Forbes - Security
Forbes - Security
Spread Privacy
Spread Privacy
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Palo Alto Networks Blog
H
Hacker News: Front Page
L
Lohrmann on Cybersecurity
Cloudbric
Cloudbric
S
Schneier on Security
Simon Willison's Weblog
Simon Willison's Weblog
I
InfoQ
人人都是产品经理
人人都是产品经理
PCI Perspectives
PCI Perspectives
月光博客
月光博客
爱范儿
爱范儿
Jina AI
Jina AI
WordPress大学
WordPress大学
N
News and Events Feed by Topic
Cyberwarzone
Cyberwarzone
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
小众软件
小众软件
Google Online Security Blog
Google Online Security Blog
W
WeLiveSecurity
V
V2EX
阮一峰的网络日志
阮一峰的网络日志
美团技术团队
博客园 - 司徒正美
Scott Helme
Scott Helme
AI
AI
L
LangChain Blog
A
Arctic Wolf
博客园 - 【当耐特】
量子位
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
MyScale Blog
MyScale Blog
腾讯CDC
博客园 - 叶小钗
Last Week in AI
Last Week in AI
S
Secure Thoughts

土法炼钢兴趣小组的算法知识备份

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计 【系统架构设计】数据迁移与版本化:在线不停机的数据演进 【系统架构设计】数据湖与数据仓库:分析架构的演进路线 【系统架构设计】API 网关设计:入口层的职责边界 【系统架构设计】应用层数据一致性模式:在正确性与性能之间走钢丝 【系统架构设计】多模数据库选型:Polyglot Persistence 的工程实践 【系统架构设计】服务发现与注册:动态拓扑的基础设施 【系统架构设计】配置管理架构:从配置文件到配置中心 【系统架构设计】全链路压测:大规模系统的性能验证 【系统架构设计】幂等性设计:分布式环境下的安全重试 【系统架构设计】契约测试与 Schema 演进:服务间的信任协议 【系统架构设计】长连接与推送架构:WebSocket、SSE 与 MQTT 【系统架构设计】延迟分析:从 P50 到 P999 的全链路追踪 【系统架构设计百科】DDD 战术模式:聚合、实体与值对象 【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案 【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁 【系统架构设计百科】CQRS + Event Sourcing 完整实战:从领域建模到部署 【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界 【系统架构设计】DDD 战略设计:限界上下文与上下文映射 【系统架构设计百科】认证架构:从 Session 到 JWT 到 OIDC 【系统架构设计】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡 排序算法专题:从 TimSort 到并行排序 【密码学百科】国密算法体系:SM2/SM3/SM4/SM9 全景解读 【密码学百科】承诺方案:Pedersen 承诺、向量承诺与多项式承诺 【密码学百科】不经意传输与隐私信息检索:OT、OT 扩展与 PIR 【密码学百科】门限密码学:门限签名、门限解密与分布式密钥生成 完美哈希:从理论到 gperf 实践 【密码学百科】安全多方计算:从 Yao 的混淆电路到实用 MPC 【密码学百科】同态加密:从 Paillier 到全同态加密(FHE) 【密码学百科】零知识证明系统:zk-SNARKs、zk-STARKs 与 Bulletproofs 【密码学百科】概率论与密码分析:生日攻击、差分分析与线性分析 【密码学百科】计算复杂性与归约:密码安全性证明的基石 【密码学百科】秘密共享:Shamir 方案、VSS 与安全多方计算入口 【密码学百科】椭圆曲线代数:Weierstrass 方程、点群运算与曲线选择 【密码学百科】离散对数与配对密码学:从 DLP 到 BLS 签名 【密码学百科】格密码数学基础:SVP、LWE 与格基约化 【密码学百科】抽象代数:群、环、域的密码学视角 【密码学百科】有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用 【密码学百科】数论进阶:二次剩余、椭圆曲线上的 Weil 配对 【密码学百科】密码学简史:从凯撒密码到量子时代 【密码学百科】威胁模型与安全目标:CIA 三要素之外 【密码学百科】Kerckhoffs 原则与现代密码设计哲学 【密码学百科】随机性:密码学的基石 【密码学百科】信息论入门:熵、完美保密与 Shannon 定理 【密码学百科】分组密码原理:Feistel 网络与 SPN 结构 【密码学百科】AES 逐步拆解:SubBytes 到 MixColumns 的数学 【密码学百科】分组密码工作模式全览:ECB/CBC/CTR/OFB/CFB 【密码学百科】流密码:RC4 的兴衰与 ChaCha20 的崛起 【密码学百科】密码学哈希函数:MD5→SHA-2→SHA-3 的进化之路 【密码学百科】MAC 与 HMAC:消息认证的正确姿势 【密码学百科】认证加密(AEAD):GCM、ChaCha20-Poly1305 与 OCB 【密码学百科】密钥派生函数:HKDF、PBKDF2、Argon2 与密码存储 【密码学百科】公钥密码的数论基础:模运算、群、原根 【密码学百科】RSA 从原理到攻击:教科书 RSA 为什么不安全 【密码学百科】Diffie-Hellman 密钥交换与离散对数问题 【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算 【密码学百科】数字签名:ECDSA、EdDSA 与 Schnorr 签名 【密码学百科】现代密钥交换:X25519、ECDHE 与前向保密 【密码学百科】混合加密与 KEM/DEM 范式:ECIES 与 HPKE 【密码学百科】填充方案:PKCS#1 v1.5、OAEP 与 PSS 【密码学百科】TLS 协议全解析:从握手到 0-RTT 【密码学百科】PKI 与数字证书:信任链的构建与崩塌 【密码学百科】密码认证协议:从 SRP 到 OPAQUE 【密码学百科】零知识证明入门:如何证明你知道而不泄露 【密码学百科】安全信道构造:Noise 协议框架与 Signal 协议 【密码学百科】密钥管理工程:HSM、KMS 与密钥生命周期 【密码学百科】侧信道攻击:从时序攻击到功耗分析 【密码学百科】密码学实现陷阱:三层漏洞分类、审计工具链与系统性预防 密码敏捷性:如何设计可升级的密码系统 【密码学百科】OpenSSL/BoringSSL 架构剖析:ENGINE、Provider 与 FIPS 模块 排序基准测试:用数据说话
【可观测性工程】Logs:Loki、ClickHouse、Elasticsearch、OpenObserve 的取舍
2026-04-22 · via 土法炼钢兴趣小组的算法知识备份

日志是可观测性三大支柱中最”重”的一支:数据量是 Metrics 的 100 倍,格式比 Traces 更随意,查询需求又从”全文搜索”到”聚合分析”到”审计合规”都有。选错方案,要么月账单超预算,要么在关键故障时因查询太慢无从排查。本文从日志场景分类开始,逐一剖析四大主流方案的内部机制,给出可执行的选型依据。


一、日志场景分类

1.1 场景矩阵

不同业务对日志系统的需求本质不同:

场景 典型需求 关键指标
故障排查(Debug) 快速过滤关键词,查找错误堆栈 查询延迟 < 3s,支持全文搜索
聚合分析 统计 QPS 趋势、错误率、P99 SQL 或 LogQL,GROUP BY + 时间序列
审计日志 不可篡改,长期保存(3-7 年) 写入后不可删除,合规存储
追踪关联 通过 trace_id 关联 Logs↔︎Traces 高基数字段快速查询
安全分析(SIEM) 实时规则匹配,检测异常行为 低延迟写入,规则引擎集成

这五类场景没有一个方案能完美覆盖。选型的第一步是明确你的主场景是哪一类,或哪两类。

1.2 日志数据体量估算

生产环境日志量的粗略估算公式:

日志总量(GB/天)= 平均日志行大小(字节)× QPS × 86400 / 1e9

示例:
  平均日志行 = 500 字节(JSON 格式含 trace_id)
  服务 QPS = 10000
  日志采样率 = 10%(仅记录 10% 请求)
  日志量 = 500 × 10000 × 0.1 × 86400 / 1e9 ≈ 43 GB/天
  
加上压缩(zstd,约 5:1):≈ 8.6 GB/天存储
保留 30 天:≈ 258 GB 存储成本

二、Elasticsearch / OpenSearch 架构

2.1 核心架构

弹性搜索(Elasticsearch,简称 ES)由 Shay Banon 于 2010 年创建,基于 Apache Lucene,是目前全文搜索领域最成熟的方案。

写入路径:
  客户端  ──REST API──►  协调节点(Coordinating Node)
                              ↓ 路由(shard = hash(doc_id) % primary_shards)
                         主分片(Primary Shard)
                              ↓ 同步复制
                         副本分片(Replica Shard)×n

查询路径:
  客户端  ──REST API──►  协调节点
                              ↓ 广播查询到所有相关分片
                         所有分片(并行)
                              ↓ 返回局部结果
                         协调节点合并结果
                              ↓
                         返回客户端

2.2 Lucene 倒排索引

ES 的核心是 Lucene 的倒排索引(Inverted Index)。写入一条日志时,ES 对所有字段分词(Tokenize)并建立索引:

原始日志:
  {"message": "user login failed", "user": "alice", "status": 401}

倒排索引:
  term "user"   → [doc1, doc5, doc12, ...]
  term "login"  → [doc1, doc3, doc8, ...]
  term "failed" → [doc1, doc7, ...]
  user=alice    → [doc1, ...]
  status=401    → [doc1, doc3, ...]

查询 “user login failed” 时,取三个词的 Posting List 的交集,速度极快。这也是 ES 在全文搜索场景无可替代的原因。

但代价是:索引大小通常是原始数据的 2-5 倍。对于每天数 TB 日志的场景,存储成本急剧膨胀。

2.3 日志索引模板(Index Template)

// PUT _index_template/logs_template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1,
      "index.codec": "best_compression",
      "index.refresh_interval": "5s",
      "index.translog.durability": "async",
      "index.translog.sync_interval": "30s"
    },
    "mappings": {
      "dynamic_templates": [
        {
          "strings_as_keyword": {
            "match_mapping_type": "string",
            "match": "*_id",
            "mapping": {"type": "keyword"}
          }
        },
        {
          "strings_as_text": {
            "match_mapping_type": "string",
            "mapping": {
              "type": "text",
              "norms": false,
              "fields": {
                "keyword": {"type": "keyword", "ignore_above": 256}
              }
            }
          }
        }
      ],
      "properties": {
        "@timestamp":  {"type": "date"},
        "level":       {"type": "keyword"},
        "service":     {"type": "keyword"},
        "trace_id":    {"type": "keyword"},
        "span_id":     {"type": "keyword"},
        "message":     {"type": "text", "analyzer": "standard"},
        "http.status": {"type": "integer"},
        "duration_ms": {"type": "float"}
      }
    }
  }
}

2.4 ILM(Index Lifecycle Management)

// PUT _ilm/policy/logs_policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_primary_shard_size": "50gb",
            "max_age": "1d"
          },
          "set_priority": {"priority": 100}
        }
      },
      "warm": {
        "min_age": "3d",
        "actions": {
          "shrink": {"number_of_shards": 1},
          "forcemerge": {"max_num_segments": 1},
          "set_priority": {"priority": 50}
        }
      },
      "cold": {
        "min_age": "30d",
        "actions": {
          "freeze": {},
          "set_priority": {"priority": 0}
        }
      },
      "delete": {
        "min_age": "90d",
        "actions": {"delete": {}}
      }
    }
  }
}

2.5 ES Query DSL 常用查询

// 5xx 错误的全文检索 + 聚合
POST /logs-*/_search
{
  "size": 20,
  "query": {
    "bool": {
      "filter": [
        {"range": {"@timestamp": {"gte": "now-15m"}}},
        {"term": {"level": "ERROR"}},
        {"range": {"http.status": {"gte": 500}}}
      ],
      "must": [
        {"match": {"message": "NullPointerException"}}
      ]
    }
  },
  "aggs": {
    "by_service": {
      "terms": {"field": "service", "size": 10},
      "aggs": {
        "error_count": {"value_count": {"field": "_id"}},
        "timeline": {
          "date_histogram": {
            "field": "@timestamp",
            "fixed_interval": "1m"
          }
        }
      }
    }
  },
  "sort": [{"@timestamp": {"order": "desc"}}]
}

三、Grafana Loki:标签驱动的低成本日志

3.1 设计理念

Loki 由 Grafana Labs 于 2018 年创建,核心设计决策:不对日志内容建立全文索引,只索引标签(Labels)

这个决策让 Loki 的存储成本极低:

  • 日志内容以压缩块(Chunk)存储在对象存储(S3/GCS/阿里云 OSS)
  • 只有标签(如 appnamespacepod)建立索引
  • 查询时先通过标签过滤缩小范围,再对日志内容做正则/字符串匹配

代价是:纯全文搜索性能远低于 ES,必须先指定标签才能查询。

3.2 架构组件

写入路径:
  Promtail/Fluentd/Vector
    ──push──► Distributor(一致性哈希)
                ↓
              Ingester(内存 WAL + Chunk)
                ↓ flush(chunk 写满 / 超时)
              Object Store(S3/OSS/GCS)

查询路径:
  Grafana ──LogQL──► Query Frontend(分片 + 缓存)
                           ↓
                     Querier(并行查询)
                     ┌──────┴───────┐
                  Ingester        Store Gateway(历史)
                  (新鲜数据)    (对象存储块)

单体模式(-target=all)将所有组件合并为单个进程,适合小规模部署。

3.3 LogQL 查询语言

LogQL 是 Loki 的查询语言,语法类似 PromQL。

日志流选择器(Log Stream Selector):

# 选择 namespace=production 的 app=nginx 的所有日志
{namespace="production", app="nginx"}

# 正则过滤(慢,需全内容扫描)
{app="nginx"} |~ "status=5[0-9]+"

# 字符串包含过滤(快,KMP 算法)
{app="nginx"} |= "error"

# 多条件组合
{app="api"} |= "error" != "timeout" | json | level="ERROR"

指标提取(Metric Queries):

# 统计每分钟 5xx 错误数(从日志提取指标)
sum(
  rate(
    {app="nginx"} |= "HTTP/1.1 5" [1m]
  )
) by (pod)

# 解析 JSON 日志,计算 P99 延迟
histogram_quantile(0.99,
  sum by (le) (
    rate(
      {app="api"} | json | duration_ms > 0
      | unwrap duration_ms [5m]
    )
  )
)

# 提取字段并过滤(pipeline)
{app="order-service"}
  | json
  | line_format "{{.level}} {{.message}}"
  | label_format service="{{ .service }}"
  | level="ERROR"
  | duration_ms > 1000

3.4 Loki 配置示例

# loki.yaml
auth_enabled: true  # 多租户模式

server:
  http_listen_port: 3100
  grpc_listen_port: 9095

ingester:
  chunk_idle_period: 30m
  chunk_block_size: 262144    # 256 KiB
  chunk_target_size: 1572864  # 1.5 MiB(未压缩)
  chunk_retain_period: 0
  wal:
    enabled: true
    dir: /data/loki/wal

schema_config:
  configs:
    - from: "2024-01-01"
      store: tsdb
      object_store: s3
      schema: v13
      index:
        prefix: loki_index_
        period: 24h

storage_config:
  aws:
    s3: s3://loki-logs-bucket
    region: cn-northwest-1
    endpoint: s3.cn-northwest-1.amazonaws.com.cn
  tsdb_shipper:
    active_index_directory: /data/loki/tsdb-index
    cache_location: /data/loki/tsdb-cache

limits_config:
  ingestion_rate_mb: 64
  ingestion_burst_size_mb: 128
  max_streams_per_user: 100000
  max_label_names_per_series: 30
  max_label_value_length: 4096
  retention_period: 30d
  per_stream_rate_limit: 10MB
  per_stream_rate_limit_burst: 20MB

3.5 Promtail 配置示例

# promtail.yaml
server:
  http_listen_port: 9080

clients:
  - url: http://loki:3100/loki/api/v1/push
    tenant_id: production    # 多租户 ID

scrape_configs:
  - job_name: kubernetes-pods
    kubernetes_sd_configs:
      - role: pod
    pipeline_stages:
      # 解析 Docker JSON 格式
      - docker: {}
      # 解析应用 JSON 日志
      - json:
          expressions:
            level:      level
            message:    msg
            trace_id:   traceId
            service:    service
            duration:   duration
      # 提取为标签(注意:高基数字段不要设为标签)
      - labels:
          level:
          service:
      # 过滤 DEBUG 级别日志(节省存储)
      - drop:
          source: level
          value: DEBUG
    relabel_configs:
      - source_labels: [__meta_kubernetes_namespace]
        target_label: namespace
      - source_labels: [__meta_kubernetes_pod_name]
        target_label: pod

四、ClickHouse 作为日志后端

4.1 为什么 ClickHouse 适合日志

ClickHouse 是俄罗斯 Yandex 开发的列式 OLAP 数据库,2016 年开源。用于日志场景的优势:

  • 极高的写入吞吐:顺序写 MergeTree,SSD 可轻松达到 100 MB/s+
  • 极高的压缩率:zstd 列式压缩,日志数据通常 5:1-10:1
  • 强大的聚合:GROUP BY 可以秒级处理数十亿行
  • 灵活的 SQL:无需学新查询语言

代价:不支持 PromQL/LogQL,需要 SQL 适配层;不支持原生多租户。

4.2 日志表设计

-- 生产级日志表
CREATE TABLE logs (
    -- 分区键:按天分区
    date        Date DEFAULT toDate(timestamp),
    -- 时间戳(毫秒精度)
    timestamp   DateTime64(3, 'UTC'),
    -- 基础字段(作为 ORDER BY 提升范围查询性能)
    level       LowCardinality(String),
    service     LowCardinality(String),
    namespace   LowCardinality(String),
    -- 追踪相关(keyword 级别,高基数)
    trace_id    String,
    span_id     String,
    -- 日志内容
    message     String,
    -- 结构化字段(KV 存储)
    fields      Map(String, String),
    -- HTTP 相关
    http_method LowCardinality(String),
    http_status UInt16,
    duration_ms Float32,
    -- 原始 JSON(备用)
    raw         String CODEC(ZSTD(3))
)
ENGINE = MergeTree()
PARTITION BY (date)
ORDER BY (service, level, timestamp)
TTL date + INTERVAL 30 DAY DELETE
SETTINGS
    index_granularity = 8192,
    min_bytes_for_wide_part = 10485760;  -- 10 MiB

-- 跳数索引:加速 trace_id 点查
ALTER TABLE logs
ADD INDEX idx_trace_id trace_id TYPE bloom_filter(0.01) GRANULARITY 4;

-- 跳数索引:加速 message 全文搜索
ALTER TABLE logs
ADD INDEX idx_message message TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 4;

4.3 常用查询

-- 查询最近 5 分钟的错误日志(利用 ORDER BY 索引)
SELECT timestamp, service, message, trace_id
FROM logs
WHERE
    date >= today() - 1
    AND timestamp >= now() - INTERVAL 5 MINUTE
    AND level = 'ERROR'
ORDER BY timestamp DESC
LIMIT 100;

-- 统计各服务错误率(GROUP BY 聚合)
SELECT
    service,
    countIf(level = 'ERROR') AS errors,
    count() AS total,
    round(countIf(level = 'ERROR') * 100.0 / count(), 2) AS error_rate_pct
FROM logs
WHERE date = today()
GROUP BY service
ORDER BY error_rate_pct DESC;

-- trace_id 关联查询(通过 Bloom Filter 快速定位)
SELECT timestamp, service, level, message
FROM logs
WHERE trace_id = '4bf92f3577b34da6a3ce929d0e0e4736'
ORDER BY timestamp;

-- 全文搜索(tokenbf 索引加速)
SELECT timestamp, service, message
FROM logs
WHERE hasToken(message, 'NullPointerException')
  AND date >= today() - 3
ORDER BY timestamp DESC
LIMIT 50;

-- 时间序列:每分钟错误数(用于生成图表)
SELECT
    toStartOfMinute(timestamp) AS ts,
    service,
    count() AS error_count
FROM logs
WHERE date = today()
  AND level = 'ERROR'
GROUP BY ts, service
ORDER BY ts;

4.4 ClickHouse TTL 与分层存储

-- TTL + 数据移动(冷数据转移到对象存储磁盘)
ALTER TABLE logs MODIFY TTL
    date + INTERVAL 7 DAY TO DISK 'cold_disk',
    date + INTERVAL 90 DAY DELETE;

-- cold_disk 配置(config.xml)
/*
<storage_configuration>
  <disks>
    <cold_disk>
      <type>s3</type>
      <endpoint>https://oss-cn-hangzhou.aliyuncs.com/logs-cold/</endpoint>
      <access_key_id>...</access_key_id>
      <secret_access_key>...</secret_access_key>
    </cold_disk>
  </disks>
  <policies>
    <tiered>
      <volumes>
        <hot>
          <disk>default</disk>
          <max_data_part_size_bytes>10737418240</max_data_part_size_bytes>
        </hot>
        <cold>
          <disk>cold_disk</disk>
        </cold>
      </volumes>
      <move_factor>0.2</move_factor>
    </tiered>
  </policies>
</storage_configuration>
*/

五、OpenObserve

5.1 简介

OpenObserve(前身 ZincObserve)由 Prabhat Sharma 等人于 2023 年用 Rust 编写,主打低成本统一可观测性(Logs + Metrics + Traces)。核心特点:

  • 数据以 Apache Parquet 格式存储在对象存储(S3/MinIO/GCS/本地)
  • 内置 tantivy(Rust 全文搜索库)实现全文检索
  • 支持 SQL 和 LogQL 查询
  • 单二进制部署,极易运维
  • 号称比 ES 便宜 140×(存储成本)
# docker-compose.yml — OpenObserve 最小部署
version: "3"
services:
  openobserve:
    image: public.ecr.aws/zinclabs/openobserve:latest
    ports:
      - "5080:5080"
    environment:
      ZO_ROOT_USER_EMAIL: admin@example.com
      ZO_ROOT_USER_PASSWORD: admin_password
      ZO_DATA_DIR: /data
      ZO_S3_BUCKET_NAME: openobserve-data
      ZO_S3_REGION_NAME: cn-northwest-1
      ZO_S3_ACCESS_KEY: ${S3_ACCESS_KEY}
      ZO_S3_SECRET_KEY: ${S3_SECRET_KEY}
      ZO_S3_ENDPOINT: https://s3.cn-northwest-1.amazonaws.com.cn
    volumes:
      - ./data:/data

5.2 OpenObserve 查询

-- 使用 SQL 查询日志
SELECT *
FROM "default"."logs"
WHERE _timestamp > (NOW() - interval '5 minute')
  AND level = 'ERROR'
ORDER BY _timestamp DESC
LIMIT 100;

-- 全文搜索
SELECT _timestamp, service, message
FROM "default"."application_logs"
WHERE match_all('NullPointerException')
  AND _timestamp > (NOW() - interval '1 hour')
ORDER BY _timestamp DESC;

六、Quickwit 与 SigNoz 简介

6.1 Quickwit

Quickwit 是 Rust 编写的云原生搜索引擎,专为日志和可观测性设计:

  • 基于 Apache Tantivy(类 Lucene 的 Rust 实现)
  • 数据存储在对象存储,接近 ES 的全文检索能力
  • 支持 ES Query API(可作为 ES 替代)
  • 无状态节点,水平扩展简单
# quickwit.yaml 最小配置
version: 0.7
node_id: searcher-1
metastore_uri: s3://quickwit-meta/
default_index_root_uri: s3://quickwit-data/

searcher:
  fast_field_cache_capacity: 1G
  split_footer_cache_capacity: 500M
  max_num_concurrent_split_searches: 100

indexer:
  split_store_max_num_bytes: 100G

6.2 SigNoz

SigNoz 是基于 OpenTelemetry 的全栈可观测性平台(Logs + Metrics + Traces),使用 ClickHouse 作为存储后端,对 K8s 原生支持好,适合不想自己拼 Grafana 仪表盘的团队。


七、LogQL vs ES Query DSL 详细对比

7.1 基础过滤

需求:查找过去 15 分钟内包含 "database connection" 的 ERROR 日志

LogQL:
{app="api", namespace="prod"} |= "database connection" | json | level="ERROR"

ES Query DSL:
{
  "query": {
    "bool": {
      "filter": [
        {"range": {"@timestamp": {"gte": "now-15m"}}},
        {"term": {"level": "ERROR"}}
      ],
      "must": [
        {"match_phrase": {"message": "database connection"}}
      ]
    }
  }
}

7.2 聚合对比

需求:统计各服务最近 1h 每分钟 ERROR 数量

LogQL:
sum by (service) (
  count_over_time(
    {namespace="production"} | json | level="ERROR" [1m]
  )
)

ES Query DSL:
{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        {"range": {"@timestamp": {"gte": "now-1h"}}},
        {"term": {"level": "ERROR"}}
      ]
    }
  },
  "aggs": {
    "by_service": {
      "terms": {"field": "service.keyword"},
      "aggs": {
        "timeline": {
          "date_histogram": {
            "field": "@timestamp",
            "fixed_interval": "1m"
          }
        }
      }
    }
  }
}

7.3 能力对比总结

能力 LogQL ES Query DSL
全文搜索 简单正则/字符串 ✓ 完整倒排索引
字段聚合 ✓ 通过 unwrap 提取 ✓ Aggregation API
嵌套对象查询 通过 json parser ✓ 原生嵌套
地理位置查询 ✓ Geo Query
日志到 Metrics 转换 ✓ 内置 需要 Elastic APM
学习曲线 低(类 PromQL) 高(JSON DSL 冗长)

八、日志格式标准

8.1 JSON 格式(推荐)

{
  "timestamp": "2024-01-15T10:30:45.123Z",
  "level": "ERROR",
  "service": "order-service",
  "version": "1.2.3",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "message": "Failed to process order",
  "error": {
    "type": "DatabaseException",
    "message": "Connection timeout after 5000ms",
    "stack": "..."
  },
  "http": {
    "method": "POST",
    "path": "/api/v1/orders",
    "status": 500,
    "duration_ms": 5234
  },
  "user_id": "usr_12345",
  "order_id": "ord_98765"
}

8.2 Logfmt 格式

ts=2024-01-15T10:30:45.123Z level=ERROR service=order-service trace_id=4bf92f35 msg="Failed to process order" duration_ms=5234 status=500

Logfmt 更紧凑,Loki 的 logfmt parser 可以直接解析。

8.3 OpenTelemetry Logs Data Model

OpenTelemetry 定义的日志数据模型,是未来的标准方向:

# OTel Log Record 结构
LogRecord:
  Timestamp: Unix 纳秒时间戳
  ObservedTimestamp: 采集到的时间戳
  TraceId: bytes[16]       # 与 Trace 关联
  SpanId:  bytes[8]
  TraceFlags: uint8
  SeverityText: "ERROR"    # 人类可读级别
  SeverityNumber: 17       # 数字级别(枚举)
  Body: AnyValue           # 日志内容(string/bytes/map/array)
  Resource:                # 来源资源(服务、版本)
    Attributes:
      service.name: "order-service"
      service.version: "1.2.3"
      deployment.environment: "production"
  InstrumentationScope:    # 产生日志的库
    Name: "github.com/company/order"
    Version: "0.1.0"
  Attributes:              # 自定义属性
    http.method: "POST"
    http.status_code: 500
    order.id: "ord_98765"

九、国内案例:B 站与知乎的 ClickHouse 日志平台

9.1 B 站日志平台演进

B 站(哔哩哔哩)在 2021 年公开了其日志平台技术选型(基于公开技术博客整理):

演进路线:ELK(2016 年)→ ClickHouse(2020 年)

关键驱动因素: - 业务日志量增长至每天数十 TB,ES 的索引磁盘成本不可承受 - ES 在 GC 频繁时查询延迟 P99 超过 10s,影响故障排查 - ClickHouse 相同存储量的磁盘成本约为 ES 的 1/5

B 站 ClickHouse 日志架构

日志采集(Fluentd/自研 Agent)
    ↓
Kafka(缓冲 + 解耦)
    ↓
ClickHouse Kafka Engine(消费)
    ↓
MergeTree 表(热数据,SSD)
    ↓ TTL 迁移
MergeTree 表(冷数据,HDD)

关键配置决策: - 按日期 + 服务分区:PARTITION BY (toYYYYMM(timestamp), service) - 去掉 ES 的 message 字段全文索引,改为 ClickHouse tokenbf 跳数索引 - 使用 ZSTD(3) 压缩:同等数据量磁盘占用降低 70%

9.2 知乎日志分析平台

知乎在 2022 年分享了其日志分析平台(基于公开资料整理):

核心挑战:知乎的数据规模不支持 ES 的高索引成本,但又需要全文检索能力。

解决方案:双写架构

日志 → Fluentd
         ├──► Loki(轻量,用于故障排查,按标签过滤)
         └──► ClickHouse(重查询,用于聚合分析)
  • Loki 负责故障排查:开发者通过 Grafana 快速按 pod/namespace 过滤
  • ClickHouse 负责聚合报表:业务分析、SLO 计算、定期报告

这个双写架构实际上是对”没有一个方案能覆盖所有场景”这一结论的工程化回答。


十、工程坑点

10.1 ES Mapping 爆炸

现象:ES 集群节点频繁出现 circuit_breaking_exception,内存报警,最终 OOM。

根因:某服务将 JSON 日志中所有字段动态映射(Dynamic Mapping),有人在 extra 字段中放了随机 key(如 request_param_{uuid}),导致 mapping 字段数从 50 个增长到 500 万个,字段元数据全在内存中。

解决方案

// 禁用动态 mapping,只允许已知字段
PUT /logs-*/_mapping
{
  "dynamic": "strict",
  "properties": {
    "message":   {"type": "text"},
    "level":     {"type": "keyword"},
    "service":   {"type": "keyword"},
    "timestamp": {"type": "date"},
    "extra":     {"type": "object", "dynamic": false, "enabled": false}
  }
}

同时设置全局 mapping 字段上限:

# elasticsearch.yml
indices.mapping.total_fields.limit: 2000

10.2 Loki Label 错用导致流爆炸

现象:Loki Ingester OOM,Distributor 报错 “too many outstanding requests”,查询延迟从 0.1s 上升到 30s。

根因:Promtail 配置了如下 Label:

# 错误:把 trace_id 设为 Label!
pipeline_stages:
  - json:
      expressions:
        trace_id: trace_id
  - labels:
      trace_id:   # 每个请求不同,产生无数 stream!

Loki 的每个唯一 Label 集合都是一个独立 Stream,trace_id 有无数唯一值,导致 Stream 数量爆炸。

正确做法

pipeline_stages:
  - json:
      expressions:
        trace_id: trace_id
        service:  service
        level:    level
  # 只把低基数字段设为 Label
  - labels:
      service:
      level:
  # trace_id 作为 structured_metadata(Loki 2.9+)
  - structured_metadata:
      trace_id:

Loki 的最佳实践:Label 数量 < 20,每个 Label 的唯一值 < 1000。

10.3 ClickHouse TTL 设置不当导致数据丢失

现象:某次排查事故时发现 3 天前的关键日志已经消失。

根因:TTL 设置为天级别,但使用了 Date 类型(精度只到天):

-- 错误:TTL 基于 Date 字段,删除时机不可预测
TTL date + INTERVAL 7 DAY DELETE

ClickHouse 的 TTL 是惰性删除,在下次 Merge 时才真正删除。上述配置可能在 7 DAY 后的任意 Merge 时刻删除数据,并不保证 7 天内数据一定存在。

正确做法

-- 基于精确 DateTime64 字段,设置充裕的 TTL
ALTER TABLE logs MODIFY TTL
    toDateTime(timestamp) + INTERVAL 8 DAY DELETE;

-- 查看 TTL 删除进度
SELECT
    table,
    engine,
    partition,
    rows,
    bytes_on_disk
FROM system.parts
WHERE table = 'logs'
ORDER BY partition DESC;

十一、选型建议

11.1 场景到方案的映射

主场景:故障排查 + 全文搜索
  → 优先 ES(功能最强)
  → 成本受限:Quickwit / OpenObserve(tantivy 支持全文)

主场景:K8s 日志 + Grafana 生态 + 低成本
  → Loki(Grafana 原生集成,标签驱动)
  → 注意:不要把高基数字段设为 Label

主场景:日志聚合分析 + SQL 团队
  → ClickHouse(聚合性能极强,运维相对复杂)
  → 配合 Grafana ClickHouse 数据源插件

主场景:中小团队统一 Logs+Metrics+Traces
  → OpenObserve(单二进制,对象存储,成本极低)
  → SigNoz(OTel 原生,ClickHouse 后端)

主场景:既要全文搜索又要低成本
  → Loki + ClickHouse 双写(知乎方案)
  → 或 Quickwit(全文搜索 + 对象存储)

11.2 落地清单


参考资料

  1. Elasticsearch 官方文档 — https://www.elastic.co/guide/en/elasticsearch/reference/
  2. Grafana Loki 官方文档 — https://grafana.com/docs/loki/
  3. ClickHouse 官方文档 — https://clickhouse.com/docs/
  4. OpenObserve 官方文档 — https://openobserve.ai/docs/
  5. Quickwit 官方文档 — https://quickwit.io/docs/
  6. B 站技术博客,“从 ELK 迁移到 ClickHouse 的日志平台实践”(2021 年)
  7. 知乎技术博客,“知乎日志分析系统的演进与实践”(2022 年)
  8. Grafana Labs, “Loki: Like Prometheus, but for Logs” (KubeCon NA 2018)
  9. ClickHouse 开发者博客,“How to Store Logs in ClickHouse” — https://clickhouse.com/blog/
  10. OpenTelemetry Logs Specification — https://opentelemetry.io/docs/specs/otel/logs/
  11. Yury Izrailevsky (Netflix), “Architecting for Logs at Scale” (re:Invent 2019)

上一篇时序数据库内核:TSM、TSI、倒排索引与 Gorilla 压缩

下一篇日志管道:Fluent Bit、Vector、Logstash、Filebeat 实战

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-22 · architecture / observability

可观测性工程

从 Metrics、Logs、Traces 到 Profiling、eBPF、OpenTelemetry 与 SLO 治理,面向中国工程团队的可观测性系统化手册。