MidТеория7 min

Push vs Pull

Кто инициирует передачу данных: сравнение webhooks и polling, Prometheus и StatsD, fan-out моделей

Что это

Два противоположных способа доставки данных между компонентами:

  • Push: источник сам отправляет данные потребителю, как только они появились. Примеры: webhooks, WebSocket, StatsD, push-нотификации, Kafka producer, fan-out on write.
  • Pull: потребитель сам ходит к источнику по расписанию или по запросу. Примеры: HTTP polling, Prometheus scrape, Kafka consumer, cron-джоба, fan-out on read.
PUSH                                PULL
  источник                            источник
     │                                   ▲
     ▼                                   │ (scrape/poll)
  приёмник                           приёмник
  (должен быть готов)                (сам решает когда)

На первый взгляд эквивалентны -- но у них противоположные характеристики нагрузки, доступности, latency и complexity.

Когда Push лучше

Push снимает задержку и нагрузку на источник: событие отправляется ровно один раз, когда оно случилось.

  • Realtime-требования. Нотификации, биржа, чат, "новое сообщение" -- задержка должна быть в мс.
  • Редкие события. Push-уведомление о заказе -- зачем опрашивать каждые 10 сек, если событие происходит раз в день?
  • Много потребителей одного события. Один producer в Kafka -- N консьюмеров.
  • Коллбэки между системами. Stripe → ваш webhook при успехе оплаты: вам не надо опрашивать их API.
  • Fan-out on write. Лента соцсети для обычных пользователей.
  • Агенты за NAT. Агент в офисе клиента не может принимать входящие, но может пушить в облако.

ASCII-сценарий push

Stripe                                 Ваш API
  │                                      │
  │ 1. Event: payment_succeeded          │
  ├─────────────────────────────────────▶│
  │                                      │ 2. Обработать, 200 OK
  │ ◀─────────────────────────────────── │
  │                                      │
  │ retry if non-200, exponential backoff│

Когда Pull лучше

Pull даёт потребителю контроль над темпом и видимостью источников.

  • Потребитель управляет rate limit. Медленный consumer не захлебнётся -- он сам дёргает столько, сколько переварит.
  • Service discovery на стороне сервера. Prometheus знает список таргетов, сам решает, кого скрэпить -- не надо настраивать PUSH на каждом сервисе.
  • Надёжная ретрансляция. Если приёмник упал, при restart он сам догонит (cursor/offset).
  • Унифицированный health check. Если pull не удался -- цель очевидно нездорова.
  • Batching. Dump всех метрик за 15 сек раз в 15 сек дешевле, чем отправлять каждую.
  • Дёшево при редком чтении. Эмейл-клиент проверяет почту раз в минуту -- пушь не нужен.

ASCII-сценарий pull (Prometheus)

Prometheus                           Сервис 1 (/metrics)
  │                                      │
  │ 1. HTTP GET /metrics (каждые 15с)    │
  ├─────────────────────────────────────▶│
  │ 2. Counter/Histogram text            │
  │ ◀─────────────────────────────────── │
  │                                      │
  ├────────────▶ Сервис 2 /metrics       │
  ├────────────▶ Сервис 3 /metrics       │

Таблица: push vs pull по осям

Ось Push Pull
Latency до потребителя Миллисекунды Равно интервалу опроса
Нагрузка на источник Пик при bursts Равномерная
Нагрузка на сеть Пропорциональна событиям Пропорциональна числу клиентов × частота
Rate limit Сложно: источник не знает возможности клиента Просто: клиент сам себе хозяин
Service discovery Сложно: клиент должен знать, куда пушить Просто: сервер знает список таргетов
Надёжность доставки Нужны retry, DLQ, idempotency Автоматически: пропустил -- догонит
Поведение при падении клиента Потеря сообщений без queue Ничего не теряется
Firewall / NAT Работает изнутри наружу Требуется доступ снаружи внутрь
Пример Webhook, WebSocket, Kafka Prometheus, HTTP polling, Kafka consumer

Интересное наблюдение: Kafka producer -- это push, Kafka consumer -- это pull. Топик -- буфер, который примиряет две модели.

Гибридные решения

В реальности почти все крупные системы используют комбинацию.

Long-polling

Клиент делает HTTP GET. Сервер не отвечает сразу, а держит соединение открытым до события. Получается push через pull-транспорт.

Используется там, где webhook нельзя (нет публичного URL) и нужен realtime.

SSE (Server-Sent Events) и WebSocket

По HTTP: клиент открывает соединение, сервер пушит события. Pull-инициация, push-поведение.

Push + Pull

  • Push уведомление "есть изменения" + pull деталей. Мобильное приложение получает push "открой приложение", делает pull полных данных. Решает проблему payload size.
  • Prometheus pull для метрик + Alertmanager push для тревог.
  • Kafka topic: producer push, consumer pull.

Fan-out гибрид (Twitter)

  • Обычные пользователи (< 10k подписчиков) -- push в материализованную ленту.
  • Celebrity (> 1M) -- pull при просмотре (чтобы не писать миллионы раз на каждый твит).
  • Ленту собирает merge-сервис.

Матрица выбора

Критерий Вес Push Pull
Требуется low latency (< 1s) Высокий + -
Много целей, мало событий Средний + -
Потребитель слабее источника Высокий - +
Нужна гарантия доставки Высокий Требует queue +
Firewall/NAT на стороне потребителя Высокий + -
Service discovery централизован Средний - +
Backpressure нужен Высокий - +
Много потребителей на событие Средний + (pub/sub) -

Код: webhook receiver vs poller

<?php

declare(strict_types=1);

/**
 * Webhook receiver (push model).
 *
 * Trade-off notes:
 * - Caller (Stripe/GitHub) initiates delivery, we must be online.
 * - Idempotency is mandatory: providers retry on 5xx / timeout.
 * - Must verify signature to avoid spoofing.
 * - 200 OK must be sent fast; heavy work goes to a queue.
 */
final class StripeWebhookController
{
    public function __construct(
        private readonly SignatureVerifier $verifier,
        private readonly IdempotencyStore $idempotency,
        private readonly MessageQueue $queue,
    ) {}

    public function handle(Request $request): Response
    {
        $signature = $request->headers->get('Stripe-Signature') ?? '';
        $payload = $request->getContent();

        if (!$this->verifier->verify($payload, $signature)) {
            return new Response('invalid signature', 401);
        }

        $event = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
        $eventId = (string) $event['id'];

        // Idempotency: Stripe may retry, we process exactly once.
        if (!$this->idempotency->markProcessed($eventId)) {
            return new Response('duplicate', 200);
        }

        // Offload heavy work to worker, respond fast to avoid retry.
        $this->queue->publish('stripe.event', $event);

        return new Response('ok', 200);
    }
}
## Частая ошибка

Использовать polling с интервалом 1 секунда на 10k клиентов -- это 10k RPS на ровном месте, при том что реальных событий десяток в минуту. В таком сценарии push (webhook/WS/SSE) снижает нагрузку на два порядка.

Обратная ошибка: поставить webhook туда, где реально нужен audit (кто когда посмотрел) -- push не оставляет следа "посмотрел/не посмотрел", а pull оставляет в логе источника.

Выводы

  • Push -- низкая latency, сложнее доставка, source не знает клиентов. Pull -- простая доставка, latency = интервалу, клиент сам регулирует темп.
  • Для realtime и редких событий -- push (webhook, WS, SSE).
  • Для мониторинга, service discovery, агентов за NAT -- pull (Prometheus style).
  • Kafka -- важный пример гибрида: producer push, consumer pull, топик-буфер.
  • Реальные системы комбинируют: push "есть новое" + pull деталей.
  • Всегда добавляйте idempotency на стороне receiver push и cursor на стороне poller.