每天上亿条日志,Elasticsearch 是怎么扛住的?

一、问题的本质

很多人以为 Filebeat + Logstash 往 Elasticsearch(ES)写日志是这样的:

复制代码
来一条日志 → 立即发给 Logstash → 立即写入 ES

如果真是这样,一天几亿条日志,ES 早就挂了。

真相是:整个链路都是批量+异步的,没有任何一环是逐条同步处理

二、Logstash 的批量机制

2.1 核心原理

Logstash 不是邮递员(来一件送一件),而是物流中转站(攒够一车再发)。

graph LR A[Filebeat 持续采集日志] --> B[Logstash 内部队列] B --> C{攒够批次?} C -->|达到5000条| D[发送 bulk 请求] C -->|或超过2秒| D D --> E[Elasticsearch]

2.2 默认批量参数

Logstash 的 Elasticsearch output 插件默认行为:

  • 最多攒 20,000 条日志才发一次
  • 或者即使没攒够,最多等 1 秒就发

这意味着一次网络请求可以携带上万条数据,大幅降低了:

  • 网络往返次数(减少 99%+ 的连接开销)
  • ES 的请求处理压力
  • 认证鉴权的重复调用

2.3 实际配置示例

ruby 复制代码
output {
  elasticsearch {
    hosts => ["https://es-cluster:9200"]
    index => "app-logs-%{+yyyy.MM.dd}"
    user => "elastic"
    password => "${ES_PASSWORD}"
    
    # 关键批量参数
    bulk_actions => 5000      # 每批最多5000条(降低内存占用)
    bulk_size => "15MB"       # 或按体积限制
    flush_interval => 2       # 最多等2秒就发送
    
    # 性能优化
    action => "create"        # 日志场景用 create 比 index 快
    retry_on_conflict => 3    # 冲突重试
    
    # 安全配置
    ssl_enabled => true
    ssl_verification_mode => "certificate"
    cacert => "/path/to/ca.crt"
  }
}

重要说明:

  • bulk_actions 不是越大越好,5000-10000 是常见取值,太大会导致单次请求超时
  • flush_interval 是延迟和吞吐的平衡点,实时性要求高就设1秒,纯批处理可以设5-10秒
  • action => "create"index 快很多,因为不需要检查文档是否存在

三、Elasticsearch 的高吞吐设计

ES 能接住海量写入,靠的是三层设计。

3.1 第一层:分片并行写入

graph TB A[bulk 请求到达协调节点] --> B{路由计算} B --> C[分片1: 处理1/3数据] B --> D[分片2: 处理1/3数据] B --> E[分片3: 处理1/3数据] C --> F[并行写入] D --> F E --> F

一个索引会被拆成多个分片(shard),每个分片是独立的 Lucene 索引,可以:

  • 分布在不同节点上
  • 并行处理写入请求
  • 单个分片故障不影响其他分片

分片数量建议:

  • 小索引(200GB):考虑按时间切分索引(如按天),每个索引 3-5 个分片

3.2 第二层:异步刷新机制

ES 的写入分三步,故意不做实时持久化:

sequenceDiagram participant Client participant Memory as 内存缓冲区 participant Translog participant Segment as Lucene Segment Client->>Memory: 1. 写入数据 Client->>Translog: 2. 写入事务日志(防丢失) Note over Client: 此时就返回成功了 Note over Memory,Segment: 每1秒执行一次 Memory->>Segment: 3. refresh: 刷新到内存段(可搜索) Note over Translog,Segment: 每30秒或translog满2GB Segment->>Segment: 4. flush: 持久化到磁盘 Translog->>Translog: 5. 清空 translog

关键点:

  • 写入只需落内存+translog,几毫秒就能完成,然后立即返回成功
  • refresh(默认1秒):让数据可被搜索,但还在内存
  • flush(默认30秒):真正写磁盘,但频率很低

这就是为什么 ES 能支持超高写入:它把"写成功"和"持久化"解耦了。

3.3 第三层:背压机制

当 ES 太忙时(CPU、内存、磁盘 IO 打满),会:

graph LR A[ES 繁忙] --> B[返回 429 Too Many Requests] B --> C[Logstash 收到 429] C --> D[暂停从 Filebeat 读取] C --> E[指数退避重试] E --> F{ES 恢复?} F -->|是| G[继续发送] F -->|否| E

这个机制保证了:

  • ES 不会被压垮(拒绝过载请求)
  • Logstash 不会丢数据(重试+暂停消费)
  • Filebeat 不会内存爆炸(Logstash 不拉取了)

四、现实开发中的类似模式

这套"批量+异步+分片"的架构不是 ES 独有的,在其他场景也广泛使用:

4.1 数据库批量写入

MyBatis/JDBC 批量插入:

java 复制代码
// 错误做法:逐条 insert
for (User user : users) {
    userMapper.insert(user); // 10000次网络请求
}

// 正确做法:批量 insert
userMapper.batchInsert(users); // 1次网络请求

Spring Batch:

java 复制代码
@Bean
public Step batchStep() {
    return stepBuilderFactory.get("step")
        .chunk(5000)  // 每5000条提交一次,和 bulk_actions 一个道理
        .reader(reader())
        .writer(writer())
        .build();
}

4.2 消息队列批量拉取

Kafka Consumer:

java 复制代码
// 一次拉取多条消息,而不是每次拉1条
ConsumerRecords records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord record : records) {
    process(record); // 批量处理
}

4.3 日志框架的异步 Appender

Logback AsyncAppender:

xml 复制代码
    512  
    

4.4 Redis Pipeline

java 复制代码
// 不用 pipeline:1万次网络往返
for (int i = 0; i < 10000; i++) {
    jedis.set(&#34;key&#34; + i, &#34;value&#34; + i);
}

// 用 pipeline:1次网络往返
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
    pipeline.set(&#34;key&#34; + i, &#34;value&#34; + i);
}
pipeline.sync();

核心思想都是:减少网络往返,批量处理,异步刷盘。

五、实战调优建议

5.1 监控关键指标

bash 复制代码
# ES 索引压力(拒绝率)
GET _nodes/stats/thread_pool

# 查看 bulk 线程池状态
&#34;bulk&#34;: {
  &#34;rejected&#34;: 0,      # 重点关注,非0说明压力大
  &#34;queue&#34;: 2,         # 队列积压
  &#34;active&#34;: 4         # 活跃线程
}

# 索引速度
GET _cat/indices?v&s=index

5.2 分片策略

错误示例:

复制代码
每天一个索引,50个主分片 → 一个月1500个分片 → 集群卡死

正确做法:

复制代码
每天一个索引,3个主分片 → 一个月90个分片 → 正常运行
配合 ILM 自动删除30天前的索引

5.3 硬件资源分配

对于日志场景:

  • CPU:写入不敏感,4-8核够用
  • 内存:JVM 堆设置为物理内存的50%,但不超过31GB
  • 磁盘:SSD 必备,机械盘会成为瓶颈
  • 网络:万兆网卡,批量写入网络流量很高

5.4 索引模板优化

json 复制代码
PUT _index_template/logs_template
{
  &#34;index_patterns&#34;: [&#34;logs-*&#34;],
  &#34;template&#34;: {
    &#34;settings&#34;: {
      &#34;number_of_shards&#34;: 3,
      &#34;number_of_replicas&#34;: 1,
      &#34;refresh_interval&#34;: &#34;5s&#34;,        // 降低刷新频率,提升写入
      &#34;translog.durability&#34;: &#34;async&#34;,  // 异步刷盘,性能更好(可能丢5秒数据)
      &#34;translog.sync_interval&#34;: &#34;5s&#34;
    },
    &#34;mappings&#34;: {
      &#34;properties&#34;: {
        &#34;@timestamp&#34;: { &#34;type&#34;: &#34;date&#34; },
        &#34;message&#34;: { 
          &#34;type&#34;: &#34;text&#34;,
          &#34;index&#34;: false  // 不需要全文检索的字段关闭索引
        }
      }
    }
  }
}

六、总结

ES 能扛住海量日志,核心原因是整个链路的设计:

Filebeat

  • 本地文件缓存(断点续传)
  • 批量发送给 Logstash

Logstash

  • 内部队列缓冲
  • 攒批调用 _bulk API
  • 背压传导(429 自动重试)

Elasticsearch

  • 分片并行写入
  • 内存缓冲 + translog 保证不丢
  • 异步 refresh 和 flush
  • 拒绝过载请求保护自己

这套架构的本质是生产者-队列-消费者模式,是所有高并发系统的通用解法。理解了这套机制,你就能举一反三:

  • 数据库批量插入(减少提交次数)
  • MQ 批量拉取消息(降低网络开销)
  • Redis Pipeline(合并命令)
  • 异步日志框架(先缓存后刷盘)

一句话:高吞吐的秘诀 = 批量 + 异步 + 并行,而非单条同步。

相关推荐
CinzWS2 小时前
基于Cortex-M3 SoC的eFuse模块--实现与验证考量
fpga开发·架构·efuse
没有bug.的程序员2 小时前
JVM 与 Docker:资源限制的真相
java·jvm·后端·spring·docker·容器
思维新观察2 小时前
谷歌发新 XR 设备:眼镜能识零食热量,头显能转 3D 影像
后端·xr·ai眼镜
im_AMBER2 小时前
Canvas架构手记 08 CSS Transform | CSS 显示模型 | React.memo
前端·css·笔记·学习·架构
申阳2 小时前
Day 23:登录设计的本质:从XSS/CSRF到Session回归的技术演进
前端·后端·程序员
lkbhua莱克瓦242 小时前
项目知识——Monorepo(单体仓库)架构详解
架构·github·项目·monorepo
LYFlied2 小时前
单页应用与多页应用:架构选择与前端演进
前端·架构·spa·mpa·ssr
古城小栈2 小时前
Spring Boot 4.0 虚拟线程启用配置与性能测试全解析
spring boot·后端·python
松莫莫2 小时前
Spring Boot 整合 MQTT 全流程详解(Windows 环境)—— 从 Mosquitto 安装到消息收发实战
windows·spring boot·后端·mqtt·学习