一、先聊聊日志分散这个痛点

平时咱们用Docker部署服务,一个系统拆成十几个微服务,每个服务再开几个实例,容器一多,日志就散落在各处。出了问题想查个请求链路,得用docker logs这个看,那个看,运气不好还得登到宿主机器上去翻文件。好不容易定位到出错的容器,发现日志已经被冲掉了一部分。这种体验,干过运维或者后端的人应该都懂。

后来大家就想着,把日志统一收集起来,弄一个集中式的日志平台。EFK就是其中的一套经典组合:Fluentd负责收,Elasticsearch负责存和搜,Kibana负责展示和可视化。搭起来之后,所有容器只要把日志输出到标准输出或者一个固定的文件,Fluentd就能自动采集,然后一股脑送进Elasticsearch。从此再也不用满世界找日志了。

但问题也随之而来。日志不像业务数据,量大、增长快、还有一堆无关紧要的调试信息。如果你不管不顾地把所有日志都塞进Elasticsearch,用不了几天,磁盘空间就要报警,查询也会越来越慢。这时候你就得认真想想索引怎么优化、分片怎么规划,甚至要不要把老日志扔到便宜一点的存储上去。这,就是咱们这篇文章要聊的事儿。

二、EFK里最需要操心的就是Elasticsearch

Fluentd本身很成熟,配置也不算复杂。Kibana呢,开箱即用,更多是前端展示。整个系统的命门其实在Elasticsearch——它要负责存储、索引、搜索这些真正吃资源的工作。日志是典型的写入多、查询少、数据量无限增长的数据。Elasticsearch默认配置是按通用搜索场景设计的,用在日志场景里,如果不做一些针对性的调优,性能很容易翻车。

比如,Elasticsearch默认会为每个索引创建固定数量的分片。你每天生成一个新的日志索引,每个索引都有几个分片。时间一长,分片数量堆积如山,每个分片都要占用对应的内存和文件句柄,集群光是为了维护这些分片就累得够呛。再比如,默认的副本数是1,所有索引都存两份,存储成本直接翻倍。所以,搭建EFK的时候,最值得花心思的地方就是Elasticsearch的索引策略。

三、索引优化:别让你的日志把磁盘撑爆

3.1 日志索引的“成长烦恼”

日志数据的特点,就是只增不减。你可以按天建索引,比如 logs-2025.05.01、logs-2025.05.02。每天的数据量可能都很大,有的团队一天几个TB的日志也常见。如果不加限制,单个索引可能膨胀到几百GB。到了那一步,查询慢不说,想删除旧数据还得删除整个索引,非常不灵活。

另外,索引不是越多越好。Elasticsearch中每个索引都有元数据,每个分片又有自己的开销。你创建了100个日索引,每个索引5个分片,那就是500个分片。这些分片在节点之间分配、做集群状态同步,都会消耗CPU和内存。所以,咱们要让索引在一个可控的范围内“滚动”,而不是无限胖下去。

3.2 用索引模板给日志“规规矩矩”地分类

索引模板是Elasticsearch提供的一个利器,它可以在新索引创建的时候,自动套用预设的setting和mapping。比如,我们可以设置分片数、副本数,以及后面要说的生命周期策略。下面这个示例,创建了一个名为logs_template的模板,它会对所有以logs-开头的索引生效。

技术栈:Elasticsearch(REST API)

# 创建索引模板,所有以 logs- 开头的索引都会自动应用下面的设置
# 注意:新索引会被分配3个主分片和1个副本,并且强制放到热节点,同时绑定后续的滚动策略
curl -X PUT "localhost:9200/_index_template/logs_template" -H 'Content-Type: application/json' -d'
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1,
      "routing.allocation.require.node_type": "hot",
      "index.lifecycle.name": "nginx_logs_policy",
      "index.lifecycle.rollover_alias": "logs-write"
    }
  }
}
'

这个模板里头的设置,咱们一个个说。number_of_shards是主分片数,number_of_replicas是副本数。routing.allocation.require.node_type告诉Elasticsearch,把新建的索引放到带有node_type=hot属性的节点上,这是为冷热分离做的准备。index.lifecycle.name和rollover_alias是生命周期管理的入口,稍后会有更详细的例子。

3.3 滚动更新和生命周期管理(ILM)

光有模板还不够,因为索引还是会长大。Elasticsearch提供了Index Lifecycle Management(简称ILM),就是一条“从出生到销毁”的流水线。你可以定义索引在什么条件下从“热”阶段进入“温”阶段,什么时候进入“冷”阶段,什么时候删除。通过rollover(滚动)机制,当一个索引满足某个条件(比如大小超过50GB,或者创建超过1天),就自动新建一个索引,并把写入请求导向新的索引。

下面创建一个日志生命周期策略。这里的技术栈仍然是Elasticsearch。

# 创建一条ILM策略:日志写入后,超过50GB或1天就滚动出新索引,30天后删除
curl -X PUT "localhost:9200/_ilm/policy/nginx_logs_policy" -H 'Content-Type: application/json' -d'
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_size": "50GB",
            "max_age": "1d"
          },
          "set_priority": {
            "priority": 100
          }
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}
'

注意,这里只写了hot和delete两个阶段,冷阶段会在后面冷热分离的完整示例中加上。这样设置之后,日志索引就会自动滚动。比如logs-000001写满了,会自动创建logs-000002,写入请求通过logs-write别名继续走。老索引不会一直变大,避免单个索引过大导致的性能问题。

四、日志分片策略:分得好才能跑得快

4.1 分片是啥?为啥不能乱设

分片简单理解,就是把一个索引拆成多份,每一份就是Lucene的一个独立索引。主分片负责读写,副本分片负责容灾和分担读流量。分片数直接决定了索引在集群里怎么分布。咱们用日志场景来说,分片设得太少,单分片数据量太大,搜索时吞吐量不足;分片设得太多,每个分片都要消耗内存和CPU,集群光维护分片就累死了。

4.2 分片数到底怎么定

业内有一个经验值:单分片控制在30GB到50GB之间比较舒适。当然这也不是绝对的,还要看具体机器和写入速率。你可以先估计一下每天日志量。假设每天日志50GB,想保留30天,那总数据量是1.5TB。如果每个分片50GB,那么总共需要30个分片。如果你有3个数据节点,每个节点10个分片,看起来还可以。但要注意,分片数是索引创建时定死的,后面改不了(除非重建索引)。所以一开始宁可分得稍多一点,也别抠抠搜搜。

4.3 基于时间维度来拆索引

日志场景最常见的做法就是按天建索引,配合rollover滚动。这样每个索引的大小就由分片数和rollover条件共同决定。比如你设置了主分片数3,rollover在50GB滚动,那么每个索引最大约50GB,每个分片约16GB,压力很小。如果觉得浪费,可以增加到5个分片,让单个分片约10GB。总之,根据实际写入量和查询需求来调整。

五、冷热分离:省钱的王道

5.1 日志数据也有“温冷”

大部分日志,头几天会被频繁查询,比如排障、监控、报表。超过一周甚至一个月,基本就没人看了。如果这些老日志还放在昂贵的SSD上,纯属浪费。冷热分离的思路很简单:把最新最频繁访问的数据放在高性能节点上(热节点),把老数据挪到便宜的大容量节点上(冷节点)。甚至更老的日志,直接删除。

5.2 用节点属性给Elasticsearch分组

要实现冷热分离,首先得让Elasticsearch区分出哪些是热节点、哪些是冷节点。这需要在Elasticsearch的配置文件elasticsearch.yml里给节点打标签。比如,在热节点的配置里写:

# 热节点的 elasticsearch.yml 片段
node:
  attr:
    node_type: hot

在冷节点的配置里写:

# 冷节点的 elasticsearch.yml 片段
node:
  attr:
    node_type: cold

然后通过ILM策略,让不同年龄的索引自动落到不同节点上。下面是一个完整的ILM策略,包含hot、cold、delete三个阶段。

# 完整ILM策略:数据先写热节点,30天后挪到冷节点,60天后删除
curl -X PUT "localhost:9200/_ilm/policy/logs_lifecycle" -H 'Content-Type: application/json' -d'
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_size": "50GB",
            "max_age": "1d"
          },
          "set_priority": {
            "priority": 100
          }
        }
      },
      "cold": {
        "min_age": "30d",
        "actions": {
          "allocate": {
            "require": {
              "node_type": "cold"
            }
          }
        }
      },
      "delete": {
        "min_age": "60d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}
'

这里有个关键动作:allocate。它会把满足条件的索引迁移到指定节点上。配合索引模板里设置的node_type=hot,新索引一创建就在热节点;超过30天后,ILM自动把它搬到冷节点;超过60天,自动删除。整个过程不需要人工干预。

5.3 完整示例:从日志收集到冷热分离

咱们把前面的内容串成一个完整流程。假设你的Fluentd已经把日志送进了Elasticsearch,索引名是logs-2025.06.01这样的格式。你只需要做三步:第一步,创建好上面的ILM策略logslifecycle;第二步,创建索引模板,绑定这个策略,并设置分片数和节点属性;第三步,给索引提供一个写别名logs-write。初始时需要手动创建一个索引logs-write-000001,并把它设为写别名。

下面是创建初始索引的示例:

# 创建初始索引,并指定别名logs-write,is_write_index为true表示只有这个索引接收写请求
curl -X PUT "localhost:9200/logs-write-000001" -H 'Content-Type: application/json' -d'
{
  "aliases": {
    "logs-write": {
      "is_write_index": true
    }
  }
}
'

之后,当logs-write-000001满足rollover条件时,Elasticsearch会自动创建logs-write-000002,并自动把它设为写别名。这样索引滚动、冷热迁移、自动删除全部齐活,非常省心。

六、这些方案的优缺点和注意事项

6.1 优点

这套组合拳打下来,好处非常明显。首先,存储成本大幅下降。冷热分离让老日志跑在机械硬盘上,成本可能只有SSD的六分之一到四分之一。其次,查询性能更稳定。因为索引通过rollover保持一个合理大小,分片数量也固定,集群不会因为索引过多而性能抖动。再者,运维省心。ILM自动滚动、迁移、删除,不需要每天盯着磁盘写脚本清理。

6.2 缺点

当然,没有完美的方案。冷热分离需要规划节点类型,多了一层运维复杂度。如果冷节点上的查询量意外变大,或者跨节点搜索很多,查询延迟会增加。另外,ILM策略虽然好,但如果你设置错时间,比如min_age写错,可能造成数据过早删除或迁移混乱。

6.3 注意事项

有几个坑一定要避开。第一,分片数定了就别轻易改,所以前期预估要留点余量。第二,rollover依赖别名,写请求必须走别名,否则不会触发滚动。第三,ILM的min_age是从索引创建时间开始算的,不是从写入时间开始算,老索引迁移和删除的时间点要心里有数。第四,冷热节点的分配规则要确保有足够容量,不然冷节点满了迁移就会卡住。第五,监控磁盘水位和节点负载,别等到红色告警才处理。

七、总结

日志分散在多个容器里,本身是个让人头疼的问题。EFK能帮我们一把,但搭建的时候,千万不要以为装上就万事大吉。索引优化要提早做,分片和生命周期策略要在上线之前就规划好。冷热分离是省钱的大招,特别适合日志这种“前天热、昨天温、今天冷”的数据。只要按咱们上面说的思路,把模板、分片、ILM和节点属性结合起来,你就能搭出一个稳定、省钱、又不用天天人肉的日志平台。下次同事跟你说日志难查,你可以拍拍胸脯说:看我的EFK,连老日志都乖乖躺在机械硬盘里呢。