一、问题的本质
很多人以为 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("key" + i, "value" + i);
}
// 用 pipeline:1次网络往返
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
pipeline.set("key" + i, "value" + i);
}
pipeline.sync();
核心思想都是:减少网络往返,批量处理,异步刷盘。
五、实战调优建议
5.1 监控关键指标
bash
# ES 索引压力(拒绝率)
GET _nodes/stats/thread_pool
# 查看 bulk 线程池状态
"bulk": {
"rejected": 0, # 重点关注,非0说明压力大
"queue": 2, # 队列积压
"active": 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
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "5s", // 降低刷新频率,提升写入
"translog.durability": "async", // 异步刷盘,性能更好(可能丢5秒数据)
"translog.sync_interval": "5s"
},
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"message": {
"type": "text",
"index": false // 不需要全文检索的字段关闭索引
}
}
}
}
}
六、总结
ES 能扛住海量日志,核心原因是整个链路的设计:
Filebeat:
- 本地文件缓存(断点续传)
- 批量发送给 Logstash
Logstash:
- 内部队列缓冲
- 攒批调用
_bulkAPI - 背压传导(429 自动重试)
Elasticsearch:
- 分片并行写入
- 内存缓冲 + translog 保证不丢
- 异步 refresh 和 flush
- 拒绝过载请求保护自己
这套架构的本质是生产者-队列-消费者模式,是所有高并发系统的通用解法。理解了这套机制,你就能举一反三:
- 数据库批量插入(减少提交次数)
- MQ 批量拉取消息(降低网络开销)
- Redis Pipeline(合并命令)
- 异步日志框架(先缓存后刷盘)
一句话:高吞吐的秘诀 = 批量 + 异步 + 并行,而非单条同步。