近两年我们团队维护的日志平台一直在增长,最老的Logstash节点已经跑了好几年,配置越改越复杂,内存越吃越多。于是我们决定把它逐步替换成Fluentd,但整个过程不敢直接“梭哈”,毕竟线上每天几十亿条日志,出一次事故够喝一壶。这篇文章就把我们亲身走过的一段平滑迁移路径,以及途中遇到的几个典型坑,用大白话讲给你听。

一、为什么放着好好的Logstash不用,非要换?

先说Logstash的优点:它出道早、生态成熟,文档丰富,出问题了一搜就是几百条答案。尤其是它那一整套grok正则,对处理非结构化日志几乎是“开箱即用”。但它的缺点也很明显:基于JVM运行,启动慢,常驻内存高。我们线上一个普通虚拟机,跑个Logstash实例,JVM堆加上元空间,轻轻松松就吃满2GB内存。数据量一上来,GC一卡,采集延迟也跟着抖。而Fluentd是用Ruby写的,本身是轻量进程,启动速度快,内存占用常常只有Logstash的一半。在容器环境里,Fluentd镜像小、CPU占用低,配合Kubernetes的资源限制,能省下可观的一笔预算。

当然,Fluentd并不是完美替换。它内置的数据处理能力没有Logstash那么“全乎”,很多高级转换需要额外插件。而且它的配置语法是<source><filter><match>这样的标签块,和Logstash的数组结构写法很不一样,现学需要一点时间。所以迁移前一定要心里有数:你图的是内存、是灵活,就得接受它某些插件需要花时间挑、花时间试。

1.1 迁移前先梳理现有管道

动手改配置之前,先把现有Logstash管道里所有的 input、filter、output 列个清单。我们要知道自己到底跑了几条管道,每条管道从哪读数据,做了哪些处理,写到哪。比如我们公司有一条管道是这样的:

  • 输入:读应用服务器上的 /var/log/app/app.log
  • 解析:用 grok 提取时间、日志级别、正文
  • 输出:写入 Elasticsearch,按天建索引

带着这个清单去Fluentd的插件仓库里找对应物。Fluentd的 tail、grok、elasticsearch 插件都比较成熟,可以放心用。

1.2 搭一个并行的测试环境

迁移一定不能直接在主环境上改。我们的建议是在同一台机器上,用另一个端口或不同的进程,把新老两套采集器并行跑起来。测试环境要尽量模拟生产的数据跑量,特别是日志解析正则,一定要拿真实样本跑一遍。你可以先手动写一条日志,分别向 Logstash 和 Fluentd 的测试配置灌,看解析出来的字段是不是一致。下面是我们当时搭建测试环境用的命令,先看Logstash侧:

# 技术栈:Shell
# 把旧版Logstash配置写到文件里
cat > /tmp/logstash-pipeline.conf <<'EOF'
input {
  file {
    path => "/var/log/app/app.log"
    start_position => "beginning"
    sincedb_path => "/dev/null"
  }
}
filter {
  grok {
    match => { "message" => "%{TIMESTAMP_ISO8601:log_time} %{LOGLEVEL:level} %{GREEDYDATA:msg}" }
  }
  date {
    match => ["log_time", "ISO8601"]
    target => "@timestamp"
  }
}
output {
  stdout { codec => rubydebug }   # 测试时先输出到控制台,方便对比
}
EOF
# 启动Logstash(前台运行,方便看日志)
logstash -f /tmp/logstash-pipeline.conf

请注意,上面这个配置文件里我特意把Elasticsearch输出换成了stdout,这样测试阶段更容易看清楚字段解析结果。等验证通过后,再把输出改回Elasticsearch。另外,sincedb_path设置为/dev/null,是为了每次重启都从文件头读,方便反复调试;正式运行时要改成真实路径。

然后是Fluentd侧对应的配置:

# 技术栈:Shell
# 安装需要的第三方插件
fluent-gem install fluent-plugin-grok-parser
fluent-gem install fluent-plugin-elasticsearch

# 生成Fluentd测试配置
cat > /tmp/fluentd-test.conf <<'EOF'
<source>
  @type tail
  path /var/log/app/app.log
  tag app.log
  pos_file /tmp/fluentd-test.pos
  read_from_head true          # 相当于Logstash的start_position => "beginning"
  <parse>
    @type grok
    grok_pattern %{TIMESTAMP_ISO8601:log_time} %{LOGLEVEL:level} %{GREEDYDATA:msg}
  </parse>
</source>

<filter app.log>
  @type record_transformer
  <record>
    @timestamp ${time.to_datetime.to_s}
  </record>
</filter>

<match app.log>
  @type stdout
</match>
EOF
# 启动Fluentd(前台运行)
fluentd -c /tmp/fluentd-test.conf

这里解释一下:Fluentd原生的 tail 输入需要配合 <parse> 来定义解析规则,grok_pattern 的写法与Logstash的grok正则基本兼容。注意 grok_pattern 是一个字符串值,不需要像Logstash那样用双引号包起来。启动后往日志文件里写几行真实数据,就能看到两边解析字段是否一致。

二、平滑过渡的具体方案

所谓平滑,核心是“不怕出问题,出了能回滚”。我们最后采用的是“并行运行、逐步切换流量”的方式。

2.1 第一阶段:双采集器同时跑

把Fluentd和Logstash都跑起来,让它们消费同一个数据源。比如两边都tail同一个日志文件,都往各自的Elasticsearch索引写入。这一步不切流量,只是观察Fluentd的解析结果和性能损耗。我们会定期对比两边写入的文档数量,以及字段值是否一致。如果发现Fluentd侧解析出来的字段少一个,或者时间格式不一样,就赶紧调整配置。

注意,如果使用同一个日志文件,两边需要各自独立记录读取位置(sincedb和pos_file),避免互相干扰。上面示例已经做了隔离。

2.2 第二阶段:切换消费组

如果你的数据源是Kafka,那么切换会更容易。让Logstash和Fluentd使用同一个Kafka consumer group,但是不同client id。由于同一个group里多个消费者会自动分配分区,所以Logstash和Fluentd会各消费一部分分区。我们先把Fluentd的group.id设置成和Logstash一样,这样两个采集器是竞争关系而不是重复消费。然后逐渐把Logstash的消费者退出,分区就会全部落入Fluentd。示例如下:

# 技术栈:Shell
# 用kafka自带工具观察消费组的情况
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group log-pipeline

# 假设上面命令显示Logstash和Fluentd都在这个组里,各占一些分区
# 此时如果停掉Logstash,Fluentd会通过rebalance自动接管所有分区

如果是文件输入,没有这样的自动rebalance机制,需要通过变更日志源或使用软链接来切换。实际上我们用得更多的是:在业务模块里同时把日志复制到两个目录,或者通过systemd的journal转发,先接一段时间的双写,确认无误后再改业务侧。

2.3 第三阶段:回滚预案

无论哪个阶段,都要保留回滚能力。我们的做法是给Fluentd写一个独立的输出索引,比如 app-log-fluentd-,这样跟Logstash写的 app-log- 分开。一旦发现Fluentd侧数据不对,随时停掉Fluentd,把Logstash进程重新拉起,或改回Kafka消费者。因为Logstash的配置文件还在,只是暂停,所以回滚非常快。

2.4 一个小型演练示例

为了让你有更直观的感受,我设计了一个易于复现的演练。假设我们要模拟从文件输入到Elasticsearch输出的切换,但暂时用stdout代替ES,这样不用装重型服务。我们写一个shell脚本,快速启动两个进程,然后分别向它们各自写入测试数据,最后通过日志颜色区分。脚本如下:

# 技术栈:Shell
# 演练:同时启动Logstash和Fluentd,用stdout输出对比结果

# 1. 准备测试信息
TEST_LOG=/tmp/pipeline-demo.log
touch $TEST_LOG

# 2. 后台启动Logstash(配置见前文,但output为stdout)
cat > /tmp/logstash-demo.conf <<'EOF'
input { file { path => "/tmp/pipeline-demo.log" sincedb_path => "/dev/null" } }
output { stdout { codec => rubydebug } }
EOF
logstash -f /tmp/logstash-demo.conf &

# 3. 后台启动Fluentd(配置见前文,但match为stdout)
cat > /tmp/fluentd-demo.conf <<'EOF'
<source>
  @type tail
  path /tmp/pipeline-demo.log
  tag demo.log
  pos_file /tmp/pipeline-demo.pos
  <parse>
    @type none
  </parse>
</source>
<match demo.log>
  @type stdout
</match>
EOF
fluentd -c /tmp/fluentd-demo.conf &

# 4. 写入一条日志触发采集
echo "hello from fluentd demo" >> $TEST_LOG

# 5. 等待几秒,观察两个进程的输出
sleep 5

请根据你的Fluentd插件情况,将 @type none 换成自己需要的解析器。这个演练不能直接用于生产,但可以帮助你快速建立迁移的信心。

三、常见坑点及其规避

下面这些坑,都是我们在迁移过程中真的碰到过的,有些还搞得我们熬夜回滚。单独拎出来讲,希望大家别走弯路。

3.1 Grok正则和字段名对不上

Logstash自带grok语法,直接写在filter里就行。Fluentd需要额外装 fluent-plugin-grok-parser,而且在写 grok_pattern 时,日志里的空格必须原样保留。普通字符串是可以直接复制的,但如果你Logstash里用了自定义正则别名,比如 MY_PATTERN (?<my>...),在Fluentd的grok插件里需要分开定义。Logstash的 patterns_dir 可以直接指定目录,Fluentd则通过 custom_pattern_path 指定文件。这个细节容易忽视,导致启动时报错或者匹配不上。

规避方法:在迁移前,把Logstash里所有的匹配模式整理成一个文件,在Fluentd中用 custom_pattern_path 引入。示例:

# 技术栈:Shell
# 定义自定义grok模式文件
cat > /tmp/custom_patterns <<'EOF'
MYAPP_STATUS (?<status>success|fail|pending)
EOF

# 在Fluentd配置中引用
cat >> /tmp/fluentd-prod.conf <<'EOF'
<source>
  ...
  <parse>
    @type grok
    custom_pattern_path /tmp/custom_patterns
    grok_pattern %{TIME:access_time} %{MYAPP_STATUS:status}
  </parse>
</source>
EOF

3.2 时间字段的格式差异

Logstash的 date 插件会直接替换掉 @timestamp,而Fluentd默认有 time 字段,但没有自动解析日志里的时间。如果你的Elasticsearch索引依赖 @timestamp,就必须用 record_transformer 把时间字符串塞进去。我们在上面已经展示过一段 record_transformer,但要注意它生成的是字符串,如果ES里定义了严格日期格式,可能需要先把字符串转成时间戳。更稳妥的方式是用Fluentd的解析器在source里把时间解出来,比如用 time_keytime_format

# 技术栈:Shell
# 在tail source中指定time_key
cat > /tmp/fluentd-time.conf <<'EOF'
<source>
  @type tail
  path /tmp/app.log
  tag app.log
  <parse>
    @type grok
    grok_pattern %{TIMESTAMP_ISO8601:log_time} %{GREEDYDATA:msg}
    time_key log_time
    time_format %Y-%m-%dT%H:%M:%S%z
  </parse>
</source>
<match app.log>
  @type stdout
</match>
EOF

这样Fluentd内部会原生生成time字段,而不是额外转换。

3.3 多行日志的处理

Java堆栈、Python traceback都是多行日志。Logstash里一般用 multiline codec 来合并。Fluentd里面也有 fluent-plugin-multiline-parser,但配置方式不太一样。你需要在 <parse> 里使用 multiline 模式,并给出 format_firstlineformat1 等参数。如果漏掉,就会把堆栈的每一行当成日志,ES里全是碎片。

规避方法:先把真实堆栈样本拿出来,在测试环境反复调正则。建议使用 format_firstline 靠类似 /^Exception|^Caused by|^\s+at / 这样的锚点来识别堆栈起始行。注意迁移期间不要同时改多行规则,否则你分不清是解析问题还是多行问题。参考配置如下:

# 技术栈:Shell
# 多行日志合并配置示例
cat > /tmp/fluentd-multiline.conf <<'EOF'
<source>
  @type tail
  path /tmp/java-stack.log
  tag java.log
  <parse>
    @type multiline
    format_firstline /^Exception|^Caused by|^\s+at /
    format1 /^(?<message>.*)$/
  </parse>
</source>
<match java.log>
  @type stdout
</match>
EOF

3.4 内存和缓冲的差异

Logstash基于JVM,调堆大小就行;Fluentd是Ruby进程,虽然没有JVM堆,但也有内部的缓冲区和线程。Fluentd的 buffer_chunk_limitflush_interval 这些参数如果不设置,默认值在某些场景下会丢数据。比如网络抖动时,Elasticsearch短暂不可用,Fluentd会一直重试并积压。如果你文件系统的缓冲路径和日志文件在同一个磁盘,磁盘满了会导致Fluentd假死。我们遇到过几次,后来将 buffer_path 单独挂载到一块SSD,并设置 retry_forever 为true,才算稳下来。

给出一个缓冲配置示例:

# 技术栈:Shell
cat > /tmp/fluentd-buffer.conf <<'EOF'
<match app.log>
  @type elasticsearch
  host localhost
  port 9200
  index_name app-log
  <buffer>
    @type file
    path /data/fluentd-buffer
    flush_interval 5s
    chunk_limit_size 5MB
    retry_timeout 30m
    retry_forever true
  </buffer>
</match>
EOF

3.5 标签路由和通配符

Fluentd的 <match> 是按标签精确或通配匹配的。很多新手会把 tag 设置得随意,导致match匹配不到,数据悄悄被丢弃。Logstash没有这个概念。所以迁移时务必保持tag的层级一致,比如 app.log。同时注意 <match **> 会匹配所有,如果放在前面,后面的match就全废了。建议把具体match写前面,兜底写最后。

3.6 性能调优上的几个关键参数

在Fluentd中,workers 参数可以设置多进程,rpc_endpoint 可以启动管理接口。对于高吞吐场景,可以这样调:

# 技术栈:Shell
# 启动时指定2个worker,并开启监控
fluentd -c /tmp/fluentd.conf --workers 2 --rpc-endpoint 127.0.0.1:24444

而Logstash当初我们主要调的是 -w 参数和JVM堆。两者调优思路差异很大,但有一点是通用的:一定要压测,不要只看CPU。

四、应用场景与技术优缺点总结

没有绝对完美的采集器,只有适合不适合自己的场景。我根据我们运维了多年的经验,给你梳理一下。

4.1 适合换到Fluentd的场景

  • 内存资源紧张,尤其是在Kubernetes里以DaemonSet方式运行采集器。
  • 日志量中等,每条日志不需要特别复杂的状态化处理。
  • 团队熟悉Ruby,或者愿意折腾插件。
  • 需要使用大量和云原生相关的插件,比如Docker日志、Kubernetes元数据等。

4.2 不适合的场景

  • 需要大量复杂的关联计算、流式聚合,Logstash的Java生态可能更强。
  • 对数据精确性要求极高,且需要精细控制内部缓冲队列时,Logstash的队列管理更成熟。
  • 团队没有Ruby基础,遇到问题难排查,可能还是留在Logstash更稳妥。

4.3 优点总述

Fluentd最大的优点是“轻”,资源占用小,部署灵活。它的插件系统基于Ruby gem,扩展方便,很多现成插件可以直接用。其次它和kafka、elasticsearch、s3、http等交互都有比较成熟的实现。

4.4 缺点总述

Fluentd文档相比Logstash要松散一些,不同插件版本之间兼容性偶尔出问题。而且它的配置语法自由度高,缺少像Logstash那样严格的schema,导致团队里不同的人写出来的风格差异很大,维护成本会增加。另外,Fluentd的 filter 之间的串联机制也更“隐性”,调试时要看日志去猜流程。

五、总结

这次迁移,我们得到的核心经验是:不要追求一步到位,先并行跑几个月,验证完所有细节再切流量。准备一套可回滚的流程,比急着追求性能要重要得多。迁移过程中你会遇到各种看似不起眼的坑,但只要坚持对比日志、梳理字段、压满测试,大部分问题都能提前发现。

最后,再强调一遍,日志采集器的选择服务于业务稳定性。如果当前Logstash运行很稳,团队没有资源投入,那就不必为了“新”而换。如果确实感受到压力,迁移时做好方案,你就成功了一大半。希望这篇分享能帮你在切换路上少走几条弯路。