推拉之间见真章:ELK海量日志吞吐优化与Prometheus Pull模型原理
上周优化ELK日志吞吐时,有个实习生问我:"侯哥,为什么Logstash是主动往ES推数据,而Prometheus是ES去拉数据?推和拉到底哪个更好?"
这个问题看似简单,但背后涉及分布式系统中数据采集的两种核心模式。今天以ELK的推送(Push)和Prometheus的拉取(Pull)为例,聊聊这两种模型的原理、取舍和最佳实践。
一、Push vs Pull:两种数据采集模式的哲学
从生活场景理解
想象一下小区物业收垃圾:
- Push模式:每户居民自己把垃圾提到垃圾站。优点是不用物业派人挨家挨户收,缺点是居民可能偷懒不送,垃圾站容易爆满(这不就是Logstash推到ES吗?)
- Pull模式:物业派保洁车定时到每栋楼下收垃圾。优点是可控,知道各家各户的情况,缺点是保洁车调度复杂,如果某栋楼临时没人就白跑一趟(这不就是Prometheus拉Exporter吗?)
技术本质对比
| 维度 | Push(ELK) | Pull(Prometheus) |
|---|---|---|
| 控制方向 | 数据源主动发送 | 监控系统主动采集 |
| 流量控制 | 数据源决定发送速率 | 监控系统决定采集频率 |
| 背压机制 | 通过队列缓冲(Kafka) | 通过scrape_interval调节 |
| 故障隔离 | 数据源故障会影响下游 | 采集器故障不影响数据源 |
| 服务发现 | 数据源需要知道目的地 | 采集器通过SD发现目标 |
| 适用场景 | 日志、事件流 | 指标、时序数据 |
二、ELK的Push模型:Kafka缓冲层的核心角色
ELK采用Push模型不是偶然的。日志的特点是:量大、突发性强、丢失容忍度低。
为什么ELK需要Kafka做缓冲
看一个没有Kafka的ELK架构:
应用日志 → Filebeat → Logstash → ES当Logstash或ES抖动时,Filebeat会直接感知到反压,导致应用层日志发送阻塞——这是灾难性的。
有了Kafka之后:
应用日志 → Filebeat → Kafka → Logstash → ESKafka在这里扮演了三个角色:
- 削峰填谷:应对日志洪峰
- 解耦:采集和消费独立伸缩
- 持久化:Logstash挂了日志不丢
Kafka分区的Push优化
我们优化的时候,重点调整了Kafka的分区策略:
# Filebeat输出到Kafka — 按服务名分区保证有序 output.kafka: hosts: ["kafka-01:9092", "kafka-02:9092", "kafka-03:9092"] topic: "app-logs" partition: round_robin: reachable_only: true compression: gzip max_message_bytes: 10485760 required_acks: 1# Logstash从Kafka消费 — 多consumer并行 input { kafka { bootstrap_servers => "kafka-01:9092,kafka-02:9092,kafka-03:9092" topics => ["app-logs"] group_id => "logstash-prod" consumer_threads => 8 decorate_events => true auto_offset_reset => "latest" fetch_max_bytes => "104857600" max_poll_records => 500 } }关键参数consumer_threads设为8,意味着Logstash会拉起8个消费者线程并行消费Kafka的8个分区,充分利用多核CPU。
三、Prometheus的Pull模型:拉出来的可靠性
Pull模型的设计哲学
Prometheus的设计者认为:监控系统不应该信任被监控的服务。
如果被监控的服务挂了,它自己是没法主动上报"我挂了"这个事件的。但Prometheus通过Pull模型,到了一定时间发现Scrape不到数据,立刻就能判定服务宕机。
Pull模型的实现细节
# prometheus.yml — Scrape配置 scrape_configs: - job_name: 'kubernetes-pods' kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] action: keep regex: true - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] action: replace target_label: __metrics_path__ regex: (.+) - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port] action: replace regex: ([^:]+)(?::\d+)?;(\d+) replacement: $1:$2 target_label: __address__这套配置背后的逻辑是:
- Prometheus定期查询K8s API,获取所有带
prometheus.io/scrape: true注解的Pod - 自动解析Pod的IP和端口
- 按
scrape_interval(默认15s)发起HTTP GET请求 - 解析返回的Metrics文本,存入TSDB
Pull模型的扩展:Pushgateway
Pull模型有一个天然短板:无法采集批处理任务(CronJob)的指标。因为任务跑完就结束了,Prometheus还没来得及Scrape。
我们的解决方案是Pushgateway:
# Python批处理任务中推送指标到Pushgateway from prometheus_client import CollectorRegistry, Gauge, push_to_gateway registry = CollectorRegistry() g = Gauge('etl_job_duration_seconds', 'ETL job duration', ['job_name'], registry=registry) g.labels(job_name='daily_report').set(245.3) # 推送到Pushgateway push_to_gateway('pushgateway:9091', job='etl_batch', registry=registry)然后Prometheus把Pushgateway当做一个普通的Exporter来Scrape。这样既保持了Pull模型的统一性,又解决了批处理任务的监控问题。
四、推拉结合的混合架构实践
在实际的运维体系中,推和拉不是非此即彼的。我们目前的架构是混合模式:
[推模式 - 日志场景] 应用日志 → Filebeat(推) → Kafka(缓冲) → Logstash(拉) → ES [拉模式 - 指标场景] 应用Metrics → Exporter(暴露) → Prometheus(拉) → TSDB [混合 - 事件场景] 告警事件 → Alertmanager(推) → 钉钉/企微何时选Push,何时选Pull?
根据我们的实践经验,决策矩阵如下:
| 数据类型 | 推荐模式 | 原因 |
|---|---|---|
| 应用日志 | Push | 量大有突发,需要缓冲 |
| 性能指标 | Pull | 轻量高频,需要发现 |
| 链路追踪 | Push | 数据量大,采样上报 |
| 告警通知 | Push | 需要主动触达 |
| 批处理指标 | Push(Pushgateway) | 生命周期短 |
| 基础设施指标 | Pull(Noder Exporter) | 持续运行,标准协议 |
五、Pull模型的核心优势:时序数据库原理
Prometheus Pull模型能高效运作,离不开底层的时序数据库(TSDB)设计:
Prometheus TSDB写入路径: Pull → HTTP Receiver → WAL(写前日志) → Head Chunk(In-Memory) ↓ Compact → Block(Disk)Pull模型天然适配这个路径——因为采集是周期性的,数据以稳定的速率到达,WAL和Head Chunk的写入也平滑可控。如果换成Push模型,突发的数据写入会导致WAL频繁fsync,Head Chunk频繁切换,性能反而不如Pull。
// Prometheus TSDB的Append方法 // Pull模型下,每隔15s被调用一次 func (h *Head) Append(minValidTime int64, series labels.Labels, t int64, v float64) (uint64, error) { // 1. 获取或创建series // 2. 写入WAL(批量) // 3. 追加到Head chunk(内存) // Pull模型的稳定节奏让这三个步骤都很平滑 }结语
Push和Pull没有孰优孰劣,只有适不适合。ELK选Push是因为日志场景需要缓冲和削峰,Prometheus选Pull是因为指标场景需要可靠发现和稳定采集。
理解这两种模型的取舍,比记住一堆配置参数重要得多。架构设计就是在做选择题,知道了每种选项的代价,才能做出合理的决策。
本文作者:侯万里(万里侯),云原生运维工程师,专注可观测性体系架构设计与性能优化