Webhook Là Gì? Cách Hoạt Động, Ưu Nhược Điểm và Các Trường Hợp Thực Tế

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Polling vs. Webhook: Hai Cách Nhận Thông Báo

Khi lần đầu tôi xây dựng tích hợp giữa các service, cách mặc định của tôi là polling — gọi một API endpoint mỗi 30 giây, kiểm tra có gì thay đổi không, rồi xử lý. Cũng ổn đấy. Nhưng sau vài tháng chạy production, vấn đề bắt đầu lộ ra: API call lãng phí, nhức đầu với rate limit, và latency luôn tối thiểu bằng nửa khoảng thời gian polling.

Webhook đảo ngược mô hình đó. Thay vì app của bạn hỏi “có gì mới không?” theo lịch định sẵn, external service sẽ chủ động thông báo cho bạn ngay khi có sự kiện xảy ra. Hãy nghĩ như thế này: thay vì ra hộp thư kiểm tra mỗi giờ, bạn có người đưa thư bấm chuông cửa.

Một webhook là một HTTP callback — một POST request được hệ thống khác gửi tới URL bạn chỉ định, kích hoạt khi có sự kiện cụ thể xảy ra. Thanh toán thành công? Stripe POST tới endpoint của bạn. Commit mới được push? GitHub POST tới CI server của bạn. Code vừa deploy? Tool monitoring của bạn POST tới Slack webhook URL.

Đây là so sánh trực quan để thấy rõ sự khác biệt:

# Phương pháp Polling (chạy mỗi 30 giây)
while True:
    response = requests.get('https://api.example.com/orders?status=new')
    for order in response.json():
        process_order(order)
    time.sleep(30)

# Phương pháp Webhook (endpoint của bạn, được external service gọi)
@app.route('/webhook/new-order', methods=['POST'])
def handle_new_order():
    order = request.json
    process_order(order)
    return '', 200

Phiên bản polling chạy bất kể có hay không có gì xảy ra. Phiên bản webhook chỉ chạy đúng khi đơn hàng đến. Ở traffic thấp, sự khác biệt không đáng kể. Nhưng ở quy mô lớn, đó là sự khác biệt giữa 50.000 API call lãng phí mỗi ngày và con số bằng không.

Ưu và Nhược Điểm Sau 6 Tháng Chạy Production

Điểm Mạnh

  • Giao nhận thời gian thực — Sự kiện đến trong vài mili giây. Email xác nhận thanh toán của tôi giờ được gửi đi chưa đầy một giây sau khi Stripe charge thành công.
  • Giảm tải server — Không còn vòng lặp polling nhàn rỗi. Server chỉ xử lý khi thực sự có việc cần làm.
  • Code đơn giản hơn — Webhook handler chỉ là một HTTP endpoint nhỏ gọn. Không cần state machine để theo dõi timestamp “lần kiểm tra cuối”.
  • Hệ sinh thái bên thứ ba — GitHub, Stripe, Shopify, Twilio, Slack — tất cả đều hỗ trợ webhook sẵn. Nếu một service đáng tích hợp, thì gần chắc chắn nó có hỗ trợ webhook.

Điểm Đau Đầu

  • Endpoint phải truy cập được từ Internet — Trong môi trường phát triển local, điều này khá phiền. Bạn cần một công cụ tunnel như ngrok hoặc localtunnel để expose localhost.
  • Bạn là người nhận, không phải người gọi — Bạn không thể dễ dàng retry nếu server đang down khi sự kiện xảy ra. Hầu hết service sẽ retry vài lần, nhưng nếu bạn down quá lâu, bạn sẽ mất sự kiện.
  • Giao nhận trùng lặp là có thật — Timeout mạng gây ra retry. Tôi đã từng xử lý cùng một sự kiện thanh toán hai lần trong production. Kiểm tra idempotency không phải tùy chọn.
  • Bảo mật là trách nhiệm của bạn — Bất kỳ ai biết URL endpoint của bạn đều có thể POST vào đó. Bạn phải xác minh request thực sự đến từ nguồn mong đợi.

Nhận xét thẳng thắn: webhook hoàn toàn vượt trội hơn polling cho các trường hợp event-driven, nhưng chúng chuyển trách nhiệm vận hành sang phía bạn. Polling thì đơn giản và đáng tin; webhook thì thông minh hơn nhưng đòi hỏi sự cẩn thận.

Thiết Lập Khuyến Nghị cho Production

1. Xác Minh Chữ Ký Webhook

Mọi nhà cung cấp webhook lớn đều đính kèm một signature header. Với GitHub là X-Hub-Signature-256, với Stripe là Stripe-Signature (còn nhúng thêm timestamp để chặn replay attack). Luôn xác minh trước khi xử lý payload.

import hmac
import hashlib

def verify_github_signature(payload_body: bytes, signature_header: str, secret: str) -> bool:
    """Xác minh chữ ký webhook của GitHub."""
    if not signature_header:
        return False
    
    hash_object = hmac.new(
        secret.encode('utf-8'),
        msg=payload_body,
        digestmod=hashlib.sha256
    )
    expected_signature = 'sha256=' + hash_object.hexdigest()
    
    # Dùng hmac.compare_digest để ngăn timing attack
    return hmac.compare_digest(expected_signature, signature_header)

Sáu tháng traffic production, không có một request giả mạo nào lọt qua. Tôi thấy khoảng chục POST không hợp lệ vào endpoint — chủ yếu là automated scanner. HMAC check đã chặn tất cả.

2. Trả 200 Nhanh, Xử Lý Bất Đồng Bộ

Webhook endpoint của bạn nên phản hồi trong vòng 2–5 giây, nếu không người gửi sẽ coi là thất bại và retry. Đừng xử lý nặng trực tiếp trong endpoint — hãy đưa công việc vào hàng đợi và trả về ngay lập tức.

import redis
import json

r = redis.Redis()

@app.route('/webhook/stripe', methods=['POST'])
def stripe_webhook():
    payload = request.get_data()
    sig_header = request.headers.get('Stripe-Signature')
    
    # Xác minh chữ ký trước
    if not verify_stripe_signature(payload, sig_header):
        return 'Chữ ký không hợp lệ', 400
    
    # Đưa vào hàng đợi, trả về ngay lập tức
    event = json.loads(payload)
    r.lpush('webhook_queue', json.dumps(event))
    
    return '', 200  # Phản hồi nhanh

# Worker riêng biệt xử lý hàng đợi này
def worker():
    while True:
        _, job = r.brpop('webhook_queue')
        process_stripe_event(json.loads(job))

3. Xử Lý Trùng Lặp bằng Idempotency Key

Lưu lại ID của các sự kiện đã xử lý. Trước khi xử lý bất kỳ sự kiện nào, kiểm tra xem bạn đã gặp nó chưa.

def process_stripe_event(event: dict) -> None:
    event_id = event['id']  # ví dụ: 'evt_1ABC123...'
    
    # Kiểm tra xem đã xử lý chưa
    if r.sismember('processed_events', event_id):
        print(f'Bỏ qua sự kiện trùng lặp: {event_id}')
        return
    
    # Xử lý sự kiện
    if event['type'] == 'payment_intent.succeeded':
        handle_payment(event['data']['object'])
    
    # Đánh dấu đã xử lý (hết hạn sau 7 ngày)
    r.sadd('processed_events', event_id)
    r.expire('processed_events', 604800)

4. Dùng ngrok cho Phát Triển Local

# Cài đặt ngrok
brew install ngrok  # macOS
# hoặc tải từ ngrok.com cho Linux/Windows

# Khởi động server local ở port 5000, rồi expose ra ngoài
ngrok http 5000

# ngrok cung cấp cho bạn một public URL dạng:
# https://a1b2c3d4.ngrok.io
# Dùng URL này làm webhook URL khi test

Hướng Dẫn Triển Khai: GitHub Webhook cho Auto-Deploy

Đây là pattern thực tế tôi đang dùng: push lên nhánh main → webhook kích hoạt → server pull code và khởi động lại.

Bước 1: Cài Đặt Webhook Receiver

from flask import Flask, request, abort
import hmac, hashlib, subprocess, os

app = Flask(__name__)
SECRET = os.environ['GITHUB_WEBHOOK_SECRET']

@app.route('/deploy', methods=['POST'])
def deploy():
    # 1. Xác minh chữ ký
    sig = request.headers.get('X-Hub-Signature-256', '')
    body = request.get_data()
    expected = 'sha256=' + hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        abort(403)
    
    # 2. Chỉ xử lý khi push lên main
    data = request.json
    if data.get('ref') != 'refs/heads/main':
        return 'Bỏ qua', 200
    
    # 3. Kích hoạt deploy (không chặn)
    subprocess.Popen(['/opt/scripts/deploy.sh'])
    return 'Đang deploy', 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

Bước 2: Đăng Ký Webhook trên GitHub

# Dùng GitHub CLI
gh api repos/{owner}/{repo}/hooks \
  --method POST \
  --field 'name=web' \
  --field 'active=true' \
  --field 'events[]=push' \
  --field 'config[url]=https://yourdomain.com/deploy' \
  --field 'config[content_type]=json' \
  --field 'config[secret]=your_secret_here'

Bước 3: Chạy Sau Reverse Proxy

# Đoạn cấu hình Nginx
location /deploy {
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $host;
    
    # Giới hạn tốc độ để tránh lạm dụng
    limit_req zone=webhook burst=10 nodelay;
}

Webhook Phát Huy Hiệu Quả Nhất ở Đâu

Auto-deploy chỉ là một pattern. Đây là những trường hợp tôi liên tục thấy webhook vượt trội hơn polling:

  • Xử lý thanh toán — Stripe và PayPal kích hoạt sự kiện cho charge thành công, hoàn tiền và tranh chấp. Thiết yếu cho luồng thanh toán async khi bạn không thể giữ người dùng chờ xác nhận.
  • CI/CD pipeline — Webhook của GitHub và GitLab kích hoạt build trên mỗi push hoặc cập nhật PR. Vòng phản hồi nhanh hơn bất kỳ polling interval nào có thể đạt được.
  • Tích hợp chat — Incoming webhook của Slack và Discord cho phép bạn đăng thông báo từ bất kỳ service nào chỉ với một HTTP POST. Không cần OAuth, không cần SDK — chỉ cần một URL.
  • Thương mại điện tử — Webhook của Shopify thông báo khi có đơn hàng mới, tồn kho giảm dưới ngưỡng, hoặc khách hàng đăng ký. Một số cửa hàng có traffic cao xử lý hàng nghìn webhook mỗi giờ.
  • Cảnh báo giám sát — PagerDuty, Datadog và Grafana đều hỗ trợ outbound webhook tới endpoint tùy chỉnh khi cảnh báo kích hoạt. Hữu ích để định tuyến cảnh báo tới công cụ nội bộ.

Quy tắc cá nhân của tôi: nếu tôi đang polling một external API nhiều hơn một lần mỗi phút, tôi sẽ tìm webhook thay thế trước. Chi phí dựng một webhook endpoint là có thật — nhưng gần như lúc nào cũng xứng đáng.

Share: