推拉之间见真章:ELK海量日志吞吐优化与Prometheus Pull模型原理
推拉之间见真章ELK海量日志吞吐优化与Prometheus Pull模型原理上周优化ELK日志吞吐时有个实习生问我侯哥为什么Logstash是主动往ES推数据而Prometheus是ES去拉数据推和拉到底哪个更好这个问题看似简单但背后涉及分布式系统中数据采集的两种核心模式。今天以ELK的推送Push和Prometheus的拉取Pull为例聊聊这两种模型的原理、取舍和最佳实践。一、Push vs Pull两种数据采集模式的哲学从生活场景理解想象一下小区物业收垃圾Push模式每户居民自己把垃圾提到垃圾站。优点是不用物业派人挨家挨户收缺点是居民可能偷懒不送垃圾站容易爆满这不就是Logstash推到ES吗Pull模式物业派保洁车定时到每栋楼下收垃圾。优点是可控知道各家各户的情况缺点是保洁车调度复杂如果某栋楼临时没人就白跑一趟这不就是Prometheus拉Exporter吗技术本质对比维度PushELKPullPrometheus控制方向数据源主动发送监控系统主动采集流量控制数据源决定发送速率监控系统决定采集频率背压机制通过队列缓冲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文本存入TSDBPull模型的扩展PushgatewayPull模型有一个天然短板无法采集批处理任务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], registryregistry) g.labels(job_namedaily_report).set(245.3) # 推送到Pushgateway push_to_gateway(pushgateway:9091, jobetl_batch, registryregistry)然后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频繁fsyncHead 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是因为指标场景需要可靠发现和稳定采集。理解这两种模型的取舍比记住一堆配置参数重要得多。架构设计就是在做选择题知道了每种选项的代价才能做出合理的决策。本文作者侯万里万里侯云原生运维工程师专注可观测性体系架构设计与性能优化