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

【可观测性工程】Logs:Loki、ClickHouse、Elasticsearch、OpenObserve 的取舍

文章导航

分类入口
architectureobservability
标签入口
#loki#elasticsearch#clickhouse#openobserve#logs#logql#observability#quickwit#signoz

目录

Logs:Loki、ClickHouse、Elasticsearch、OpenObserve 的取舍

日志是可观测性三大支柱中最”重”的一支:数据量是 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 的存储成本极低:

代价是:纯全文搜索性能远低于 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 年开源。用于日志场景的优势:

代价:不支持 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)。核心特点:

# 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 编写的云原生搜索引擎,专为日志和可观测性设计:

# 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(重查询,用于聚合分析)

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


十、工程坑点

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 治理,面向中国工程团队的可观测性系统化手册。


By .