Что это
Два противоположных способа доставки данных между компонентами:
- 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.