เว็บสแครปปิงและเอเจนต์

วิธีแก้ไขข้อผิดพลาด HTTP 429 และ Cloudflare 520 ใน AI Crawler และ Agent

คำตอบด่วน: ข้อผิดพลาด HTTP 429 บ่งชี้ว่าคำขอเกินอัตราที่กำหนด (Rate Limit) ของ API หรือ WAF แก้ไขได้ด้วย Decorrelated Jitter Backoff และพูลพร็อกซีที่อยู่อาศัยแบบหมุนเวียน ส่วนข้อผิดพลาด Cloudflare 520 ("Web Server Returned an Unknown Error") เกิดจากเซิร์ฟเวอร์ต้นทางล่มหรือส่งการรีเซ็ต TCP RST ภายใต้ภาระงานสูงของบอท ป้องกันได้ด้วยการปรับ Keep-Alive และใช้ Circuit Breaker

1. บทนำ: ความเปราะบางของ AI Web Crawler อัตโนมัติ

เมื่อ AI Agent พัฒนาไปสู่ระบบการให้เหตุผลหลายขั้นตอน (การวิจัยเว็บแบบเรียลไทม์, การตรวจสอบงบการเงิน, การดึงโค้ดความถี่สูง) คอขวดหลักไม่ได้อยู่ที่ความเร็วของโมเดล แต่เป็น ความเสถียรของเครือข่ายและการเข้าถึงระดับ Edge

Agent ในระดับโปรดักชันส่งคำขอ HTTP หลายพันครั้งต่อนาที แตกต่างจากบอทค้นหาทั่วไป (Scrapy, Googlebot) ตรงที่ AI Agent ต้องการ:

  1. การดึงข้อมูลเว็บแบบซิงโครนัสที่มีค่าความหน่วงต่ำมาก เพื่อไม่ให้ลูปการใช้เหตุผลหยุดชะงัก
  2. การรัน JavaScript ขั้นสูง เพื่อประมวลผล SPA
  3. การแปลงข้อมูลเป็น Markdown ที่สะอาด เพื่อประหยัด Token ของ Context Window

ทว่าระบบป้องกันระดับ Edge เช่น Cloudflare และ DataDome มักตรวจจับและตอบกลับด้วยสองข้อผิดพลาดหลัก:

  • HTTP 429 Too Many Requests: ส่งคำขอเกินขีดจำกัด
  • HTTP 520 Web Server Returned an Unknown Error: เซิร์ฟเวอร์ต้นทางตอบสนองผิดพลาดกับ Cloudflare Edge

2. โครงสร้างของ HTTP 429: Rate Limit และ Fingerprint

ข้อผิดพลาด 429 ตาม RFC 6585 เกิดขึ้นใน 3 ระดับ:

  1. ระดับที่ 1: Edge WAF (Cloudflare / DataDome): ตรวจพบความไม่สอดคล้องกันระหว่างลายนิ้วมือ TLS JA4 (เช่น Python OpenSSL) กับ User-Agent ของ Chrome
  2. ระดับที่ 2: API Gateway (Kong, Envoy, Nginx): อัลกอริทึม Token Bucket และ Leaky Bucket
  3. ระดับที่ 3: เซิร์ฟเวอร์ต้นทาง: กลไกป้องกันฐานข้อมูลและ CPU ทำงานหนักเกินไป

ส่วนหัวสำคัญที่ต้องตรวจสอบ: Retry-After, RateLimit-Limit, RateLimit-Remaining, และ RateLimit-Reset


3. สาเหตุของ Cloudflare Error 520

รหัส 520 เป็นรหัสเฉพาะของ Cloudflare ที่ระบุว่าเซิร์ฟเวอร์ต้นทางล้มเหลว:

  1. เวิร์กเกอร์ล่ม (OOM / SIGSEGV): หน่วยความจำไม่พอทำให้กระบวนการหยุดทำงานและส่ง TCP RST ไปยัง Cloudflare
  2. การไม่ซิงโครไนซ์ของ Keep-Alive: Cloudflare รอ 15 วินาที แต่ถ้า Nginx ปิดใน 5 วินาที คำขอจะถูกตัดขาด
  3. ขนาด Header เกินขีดจำกัด (>16KB): คุกกี้วนซ้ำจนล้นบัฟเฟอร์
  4. การตอบสนองเป็นศูนย์ไบต์ (Zero Bytes): ปิดการเชื่อมต่อโดยไม่ส่งข้อมูล

4. กลยุทธ์การแก้ไข: Decorrelated Jitter และ Circuit Breaker

การส่งคำขอซ้ำแบบธรรมดาทำให้เกิด Thundering Herd Problem ทางออกที่เหมาะสมที่สุดจาก AWS คือ Decorrelated Jitter:

# สูตร Decorrelated Jitter:
sleep = min(cap, random.uniform(base, previous_delay * 3.0))

ตัวอย่างโค้ด Python พร้อม Circuit Breaker

import asyncio, random, time, aiohttp
from urllib.parse import urlparse

class ResilientAgentCrawler:
    def __init__(self, base_delay=1.0, max_delay=60.0, max_retries=5, threshold=4, cooldown=30.0):
        self.base_delay = base_delay
        self.max_delay = max_delay
        self.max_retries = max_retries
        self.threshold = threshold
        self.cooldown = cooldown
        self.failures = {}
        self.opened_at = {}

    def _is_open(self, domain):
        t = self.opened_at.get(domain)
        if not t: return False
        if time.monotonic() - t > self.cooldown:
            del self.opened_at[domain]
            self.failures[domain] = 0
            return False
        return True

    async def fetch(self, session, url):
        domain = urlparse(url).netloc
        if self._is_open(domain):
            raise RuntimeError(f"Circuit Breaker ทำงานสำหรับโดเมน: {domain}")

        delay = self.base_delay
        for attempt in range(1, self.max_retries + 1):
            try:
                async with session.get(url) as resp:
                    if resp.status == 200:
                        self.failures[domain] = 0
                        return await resp.text()
                    elif resp.status == 429:
                        retry_after = resp.headers.get("Retry-After")
                        wait = float(retry_after) if retry_after else random.uniform(self.base_delay, delay * 3.0)
                        delay = min(self.max_delay, wait)
                        await asyncio.sleep(delay)
                    elif resp.status in (520, 502, 503, 504):
                        self.failures[domain] = self.failures.get(domain, 0) + 1
                        if self.failures[domain] >= self.threshold:
                            self.opened_at[domain] = time.monotonic()
                        delay = min(self.max_delay, random.uniform(self.base_delay, delay * 3.0))
                        await asyncio.sleep(delay)
                    else:
                        resp.raise_for_status()
            except Exception as e:
                if attempt == self.max_retries: raise e
                await asyncio.sleep(delay)

5. เลียนแบบลายนิ้วมือ TLS ด้วย curl_cffi

ในปี 2026 การเปลี่ยน User-Agent เพียงอย่างเดียวไม่สามารถหลบเลี่ยงได้ ให้ใช้ curl_cffi เพื่อจำลองลายนิ้วมือ JA4 ของ Chrome:

from curl_cffi.requests import AsyncSession

async def fetch_stealth(url: str):
    async with AsyncSession(impersonate="chrome124") as s:
        res = await s.get(url, timeout=15)
        return res.text

6. สถาปัตยกรรมพร็อกซีและการตั้งค่าเซิร์ฟเวอร์

  • พร็อกซีที่อยู่อาศัยแบบหมุนเวียน: กระจายคำขอไปยัง IP นับล้านเพื่อเลี่ยงขีดจำกัด 429
  • พร็อกซีมือถือ 4G/5G (CGNAT): ผ่านการตรวจสอบ WAF ได้ดีที่สุดเนื่องจากแชร์ IP กับผู้ใช้งานจริงจำนวนมาก
  • การตั้งค่า Nginx เพื่อป้องกัน 520:
http {
    keepalive_timeout 75s;
    keepalive_requests 10000;
    proxy_buffer_size 128k;
    proxy_buffers 4 256k;
}

7. สรุป

หลักการสำคัญสำหรับ AI Crawler:

  1. ใช้ Decorrelated Jitter เพื่อลดปัญหาการแย่งส่งคำขอ
  2. ใช้ curl_cffi เลียนแบบเบราว์เซอร์จริง
  3. ตั้งค่า Keep-Alive ฝั่งเซิร์ฟเวอร์ไว้ที่ 75 วินาที
  4. ใช้ Circuit Breaker ป้องกันการสิ้นเปลือง Token ของ LLM
← บทความทั้งหมด
0 / 4