Elasticsearch 80 亿日志治理:P99 8s→200ms 磁盘 -60%

ES 7.17 集群 80 亿日志,搜索 P99 8s 写入打不住磁盘告警。一个月优化全实录:索引模板 strict + 分片设计 + ILM 冷热分离(hot/warm/cold) + bulk processor + JVM 调参 + 查询改写。P99 从 8s 降到 200ms,磁盘节约 60%。

2024 年我们的 Elasticsearch 集群存了 80 亿日志,每天新增 5 亿,搜索 P99 涨到 8s,写入也开始顶不住,磁盘 70% 告警。投了一个月做索引设计 + 分片优化 + 冷热分离 + 查询调优,P99 从 8s 降到 200ms,磁盘节约 40%,写入 QPS 从 5w 提到 15w。本文复盘 ES 优化全套手段。

问题背景

集群:Elasticsearch 7.17,12 节点(6 主 + 6 数据 + 3 协调)
机器:32C 128G,2TB NVMe x 6
索引:logs-* (每天 1 个,80 亿文档)
分片:每索引 24 主分片 + 1 副本
存储:80 亿 × 3KB = 240TB(含副本)

性能问题:
- 搜索 P99 8s,关键词搜索 30s+
- 写入吞吐 5w/s,峰值打不住
- 磁盘 70% 告警
- 查询 OOM:大聚合直接挂

需要全面优化

优化 1:索引设计

PUT _index_template/logs-template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "number_of_shards": 12,
      "number_of_replicas": 1,
      "refresh_interval": "30s",
      "translog.durability": "async",
      "translog.sync_interval": "10s",
      "translog.flush_threshold_size": "1gb",
      "index.codec": "best_compression",
      "index.merge.policy.max_merge_at_once": 5,
      "index.merge.policy.segments_per_tier": 5,
      "index.queries.cache.enabled": true,
      "index.requests.cache.enable": true,
      "index.search.idle.after": "30s",
      "index.lifecycle.name": "logs-policy"
    },
    "mappings": {
      "dynamic": "strict",
      "properties": {
        "@timestamp": { "type": "date" },
        "level": { "type": "keyword" },
        "service": { "type": "keyword" },
        "trace_id": { "type": "keyword", "doc_values": false },
        "user_id": { "type": "long" },
        "message": {
          "type": "text",
          "analyzer": "standard",
          "fields": {
            "keyword": { "type": "keyword", "ignore_above": 256 }
          }
        },
        "host": { "type": "keyword" },
        "ip": { "type": "ip" },
        "duration_ms": { "type": "integer" },
        "tags": { "type": "keyword" }
      }
    }
  }
}

// 关键点:
// 1. dynamic: strict - 不让自动加字段(避免 mapping 膨胀)
// 2. 不分词字段用 keyword,需要全文搜的用 text + multi-field
// 3. trace_id 不需要聚合 → doc_values: false 节省 30% 空间
// 4. best_compression 压缩节省 60% 磁盘

优化 2:分片策略

分片数计算原则:
- 单分片大小:20-50GB(过小:协调开销;过大:慢)
- 总分片数 ≤ 节点数 × 1000(Cluster level limit)
- 主分片数:数据量 / 30GB

我们的情况:
- 单日数据:5 亿 × 1.5KB(压缩后)= 750GB
- 单分片目标 30GB → 25 个主分片
- 副本 1,总 50 分片 / 天
- 6 数据节点,每节点 8 分片(均衡)

错误做法:
- 单索引一年:9000 亿数据 + 100 个分片(单分片 9TB,完全没法管)
- 按用户分:每用户 1 索引(分片数爆炸)

正确做法:
- 按天分:logs-2024-03-15(25 分片)
- 老数据滚动归档(冷热分离)

优化 3:冷热分离(ILM)

PUT _ilm/policy/logs-policy
{
  "policy": {
    "phases": {
      "hot": {
        "actions": {
          "rollover": {
            "max_size": "750gb",
            "max_age": "1d",
            "max_docs": 500000000
          },
          "set_priority": { "priority": 100 }
        }
      },
      "warm": {
        "min_age": "7d",
        "actions": {
          "allocate": {
            "include": { "data_tier": "warm" },
            "number_of_replicas": 0
          },
          "forcemerge": { "max_num_segments": 1 },
          "shrink": { "number_of_shards": 6 },
          "set_priority": { "priority": 50 }
        }
      },
      "cold": {
        "min_age": "30d",
        "actions": {
          "allocate": {
            "include": { "data_tier": "cold" }
          },
          "freeze": {},
          "searchable_snapshot": {
            "snapshot_repository": "s3-repo"
          }
        }
      },
      "delete": {
        "min_age": "180d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

// 节点角色配置(elasticsearch.yml)
// 热节点(NVMe SSD,32C 128G)
node.roles: [data_hot, data_content]

// 温节点(SATA SSD,16C 64G)
node.roles: [data_warm]

// 冷节点(机械盘,8C 32G)
node.roles: [data_cold]

优化 4:写入吞吐

// 客户端 Bulk 写入
RestHighLevelClient client = ...;

BulkProcessor bulkProcessor = BulkProcessor.builder(
    (request, listener) -> client.bulkAsync(request, RequestOptions.DEFAULT, listener),
    new BulkProcessor.Listener() {
        public void beforeBulk(long executionId, BulkRequest request) {}
        public void afterBulk(long executionId, BulkRequest request, BulkResponse response) {
            if (response.hasFailures()) {
                log.error("bulk failures: {}", response.buildFailureMessage());
            }
        }
        public void afterBulk(long executionId, BulkRequest request, Throwable failure) {
            log.error("bulk exception", failure);
        }
    })
    .setBulkActions(5000)                    // 5000 条 flush
    .setBulkSize(new ByteSizeValue(10, ByteSizeUnit.MB))    // 或 10MB
    .setFlushInterval(TimeValue.timeValueSeconds(5))         // 5s 强 flush
    .setConcurrentRequests(4)                                // 4 个并发 bulk
    .setBackoffPolicy(BackoffPolicy.exponentialBackoff(TimeValue.timeValueMillis(100), 5))
    .build();

// 配合 ES 端
PUT logs-*/_settings
{
  "refresh_interval": "30s",         // 默认 1s 太频繁
  "index.translog.durability": "async",
  "index.translog.sync_interval": "10s",
  "index.translog.flush_threshold_size": "1gb",
  "index.number_of_replicas": 0       // 写入期间临时去副本
}

// 写完恢复副本
PUT logs-*/_settings
{
  "index.number_of_replicas": 1,
  "refresh_interval": "5s"
}

// 效果:
// - bulk 5w → 15w/s
// - CPU 利用率从 60% 提到 85%
// - 副本暂时关闭,写入快 2x

优化 5:查询优化

// 错误 1:使用 wildcard 慢查询
GET logs-*/_search
{
  "query": {
    "wildcard": { "message": "*error*" }    // 全文档扫描,慢
  }
}

// 正确:用 match
GET logs-*/_search
{
  "query": {
    "match": { "message": "error" }          // 用倒排索引
  }
}

// 错误 2:深度分页
GET logs-*/_search
{
  "from": 100000, "size": 20    // 协调节点要排序 100020 条,慢且 OOM
}

// 正确:search_after / scroll(导出场景)
GET logs-*/_search
{
  "size": 20,
  "query": {...},
  "sort": [{"@timestamp": "desc"}, {"_id": "asc"}],
  "search_after": ["2024-03-15T10:00:00", "doc-id-12345"]
}

// 错误 3:大聚合无 size
GET logs-*/_search
{
  "size": 0,
  "aggs": {
    "by_user": { "terms": { "field": "user_id" } }    // 默认 10,但全文档扫
  }
}

// 改进:加 filter 缩范围
GET logs-*/_search
{
  "size": 0,
  "query": {
    "bool": {
      "filter": [
        { "range": { "@timestamp": { "gte": "now-1h" } } },
        { "term": { "service": "order-api" } }
      ]
    }
  },
  "aggs": {
    "by_user": { "terms": { "field": "user_id", "size": 100 } }
  }
}

// 错误 4:should 没 minimum_should_match
"should": [..., ..., ...]   // 默认任一匹配即可,但计算所有 score(慢)

// 改:用 filter 替代(不算 score)
"filter": [...]
// 或 minimum_should_match: 1 + bool 拆开

优化 6:JVM + 系统参数

# jvm.options
-Xms31g
-Xmx31g          # 不超过 32G(压缩指针失效)
-XX:+UseG1GC
-XX:G1HeapRegionSize=32m
-XX:G1ReservePercent=25
-XX:InitiatingHeapOccupancyPercent=30

# /etc/sysctl.conf
vm.swappiness = 1
vm.max_map_count = 262144

# /etc/security/limits.conf
elasticsearch soft nofile 65536
elasticsearch hard nofile 65536
elasticsearch soft memlock unlimited
elasticsearch hard memlock unlimited

# elasticsearch.yml
bootstrap.memory_lock: true       # 禁 swap
thread_pool.write.queue_size: 1000
thread_pool.search.queue_size: 1000
indices.queries.cache.size: 10%
indices.fielddata.cache.size: 20%
indices.requests.cache.size: 2%
indices.memory.index_buffer_size: 30%

优化 7:Mapping 瘦身

// 默认 mapping 浪费严重:
// 1. text 字段默认带 norms(查询长度归一化),日志不用
// 2. keyword 默认有 doc_values(用于聚合排序),不聚合的字段关掉
// 3. _source 可关(但失去原文)
// 4. _all(7.x 已删)

PUT logs-template
{
  "mappings": {
    "properties": {
      "message": {
        "type": "text",
        "norms": false,         // 关掉 norms 省 30% 磁盘
        "index_options": "freqs"  // 只存词频,不存位置(短语搜索不可用)
      },
      "trace_id": {
        "type": "keyword",
        "doc_values": false     // 不参与聚合,关 doc_values
      },
      "raw_data": {
        "type": "text",
        "index": false          // 不索引,只存储
      },
      "debug_info": {
        "enabled": false        // 完全不索引不存储(占位字段)
      }
    },
    "_source": {
      "enabled": true,
      "excludes": ["debug_*"]   // 不存某些字段(节省存储)
    }
  }
}

// 效果:
// - mapping 优化后,80 亿日志从 240TB → 95TB
// - 写入快 25%(少计算 norms / doc_values)

压测 + 优化效果

指标                    优化前       优化后       变化
=====================================================
搜索 P50                 1.2s         50ms         -96%
搜索 P99                 8s           200ms        -97%
写入吞吐                 5w/s         15w/s        +200%
磁盘占用                 240TB        95TB         -60%
节点 CPU                 80%          40%          -50%
JVM GC pause p99         300ms        30ms         -90%
集群健康                 黄色频繁     绿色         稳定

业务影响:
- 日志搜索从"基本不能用"到"秒级响应"
- 节省 60% 存储成本(年省 ¥300w)
- 大促日志洪峰能扛住

监控告警

# Prometheus + elasticsearch_exporter
- alert: ESClusterRed
  expr: elasticsearch_cluster_health_status{color="red"} == 1
  for: 1m

- alert: ESJvmHeapHigh
  expr: elasticsearch_jvm_memory_used_bytes / elasticsearch_jvm_memory_max_bytes > 0.85
  for: 5m

- alert: ESDiskHigh
  expr: 1 - elasticsearch_filesystem_data_available_bytes / elasticsearch_filesystem_data_size_bytes > 0.80
  for: 5m

- alert: ESPendingTasks
  expr: elasticsearch_cluster_pending_tasks > 100
  for: 5m

- alert: ESSlowSearch
  expr: rate(elasticsearch_indices_search_query_time_seconds[5m]) > 1
  for: 5m

避坑清单

  1. 单分片 20-50GB,不要太小也不要太大
  2. dynamic mapping 必须 strict,避免字段爆炸
  3. 不聚合的 keyword 关 doc_values,省 30% 空间
  4. refresh_interval 写多读少调到 30s
  5. 写入期间关副本,完成后再开
  6. ILM 必上,冷热分离省钱
  7. JVM heap ≤ 31G(压缩指针),不要给到 64G
  8. 查询用 filter 替代 query 部分(可缓存)
  9. 深分页用 search_after,不要 from+size
  10. 慢查询日志开,定期分析 top N

总结

Elasticsearch 是个看起来"装上就能用"实则"用不好就翻车"的工具。这次 80 亿日志的优化让我深刻理解:ES 性能是设计出来的,不是调出来的。Mapping 一开始就要严格,分片数一开始就要算对,后期改这些都很贵。冷热分离 + ILM 是省钱关键,180 天的日志在 S3 上 vs 在 NVMe 上,差 100 倍成本。最大的认知改变:ES 不是数据库,不要拿它当 OLTP 用 — 大聚合慢、深度分页慢、频繁更新慢,这是架构决定的。日志、搜索、推荐场景才是 ES 的真主场。如果你的 ES 用得很难受,大概率是用错了场景,该换工具(ClickHouse / Doris)就换。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Nginx 高并发调优实录:单机 10w→60w QPS 全过程

2026-5-19 12:51:08

技术教程

Docker 镜像瘦身实录:80 服务从 800MB 平均降到 120MB

2026-5-19 12:55:28

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索