Восемь тысяч CAPTCHA в секунду это 28,8 миллиона в час и 691 миллион в сутки. Число такого размера обычно приводят как свойство решателя, будто ответ в более быстрой модели или более мощной машине. Это не так. Задолго до того, как это начнёт иметь значение, 8 000 решений в секунду это бюджет параллелизма, и размер этого бюджета считается на салфетке.

Пропускная способность равна параллелизму, делённому на время обслуживания. Если одно решение занимает две секунды, а вы хотите, чтобы каждую секунду завершалось 8 000, то 16 000 операций должны быть открыты постоянно. Ничто в этом числе не зависит от того, насколько умён решатель. Оно вытекает из задержки, которую вы не можете убрать.

Это смещает саму инженерную задачу. Интересна не сама расшифровка, а управляющий слой вокруг неё. Что-то должно принимать работу, ограничивать её, удерживать десятки тысяч операций в полёте не падая, и измерять себя достаточно точно, чтобы вы могли назвать ступень, задающую предел.

Сопровождающая реализация на Go в ScraperHub/how-we-solve-8000-captchas-per-second строит этот управляющий слой в четырёх файлах: ограниченная очередь, пул воркеров с обратным давлением, подключаемый интерфейс Solver с реализацией-заглушкой и реализацией через Crawlbase, и сборщик метрик, который сообщает число решений в секунду вместе с p50 и p99. Этот пост разбирает её, запускает оба бенчмарка, а затем честно подсчитывает, что эти запуски доказывают про 8 000, а что нет.

Коротко
  • Пропускная способность это параллелизм, делённый на время обслуживания. При двух секундах на решение 8 000 в секунду означают 16 000 запросов в полёте.
  • Управляющий слой на Go не является узким местом. Запуск с заглушкой держит 32 507 имитированных решений в секунду на одной машине, это в пределах 0,6% от собственного арифметического потолка.
  • Заглушка не покажет вам настоящий хвост распределения. Её p99 ограничен по построению базовой задержкой плюс дрожание.
  • Между 6 воркерами и 16 000 операций в полёте первыми ломаются три вещи: пул HTTP-соединений по умолчанию, неограниченный срез задержек и предположение, что один процесс это и есть развёртывание.
  • Ступень решения это та часть, которую лучше не писать самому. Именно её и берёт на себя Crawling API.
Управляющий слой, четыре файла. Производитель наполняет ограниченную очередь, пул воркеров её опустошает, каждый воркер вызывает то, что стоит за интерфейсом Solver , и каждый результат сходится в единственный сборщик. Ограничение очереди и есть механизм обратного давления.

Пропускная способность это параллелизм, делённый на время обслуживания

Закон Литтла это весь план по мощности в одну строку. Число операций в полёте равно скорости завершения, умноженной на длительность каждой. В переформулировке под наш вопрос: скорость, которую вы можете держать, это ваш параллелизм, делённый на ваше время обслуживания.

Это стоит проверить на числах самого репозитория, а не принимать на веру. Задокументированный запуск с заглушкой использует 256 воркеров и сообщает p50 в 7,83 мс. Делим: 256 воркеров при 7,83 мс на решение предсказывают 32 695 решений в секунду. Измерено 32 507. Очередь, канал результатов, сборщик и его мьютекс вместе стоят 0,57% теоретического потолка.

Проведите тот же расчёт в обратную сторону, и заголовок этого поста превратится в заказ на оборудование.

Целевая скорость Время обслуживания Требуемый параллелизм Что это означает
32 507/с 7,83 мс (имитация) 256 256 горутин на одной машине
8 000/с 1,9 с (измерено, реальный путь) 15 200 парк машин, а не процесс
8 000/с 2 с (округлённо) 16 000 тот же вывод, считать в уме проще

Эти две строки показывают, где на самом деле лежит сложность. Имитированная работа на каждую операцию в 250 раз быстрее реального сетевого обхода, поэтому локальному бенчмарку нужно на три порядка меньше параллелизма, чтобы показать большее число. Всё сложное в 8 000 реальных решений в секунду это 16 000 открытых сокетов, а не 8 000 решений.

Один закон, два режима. Параллелизм это площадь прямоугольника: скорость, умноженная на время обслуживания. Одна миллисекунда имитированной работы покупает у 256 воркеров большую скорость. Двум секундам реальной работы нужны 16 000 операций в полёте, чтобы достичь меньшей.

Четыре файла, один интерфейс

Каноническим запускаемым модулем является каталог final/ сопровождающего репозитория. Он намеренно маленький.

final/
pipeline.go   bounded queue, worker pool, results fan-in
solver.go     the Solver interface and its two implementations
metrics.go    throughput and latency percentiles
main.go       load harness and flags
config.go     token and target URL from the environment

Интерфейс Solver это тот шов, который делает остальное проверяемым. Конвейер никогда не узнаёт, ушла задача в локальную имитацию или через интернет; он отправляет работу и записывает то, что вернулось. Именно это позволяет измерить оркестрацию на 32 000 операций в секунду без сети, а затем направить тот же конвейер на живой endpoint и наблюдать, как числа обваливаются по причинам, которые вы можете назвать.

Шаг 1: очередь и пул воркеров

Задача это структура, а очередь это буферизованный канал. Ёмкость канала и есть весь механизм обратного давления: как только буфер заполнен, отправка у производителя блокируется, пока воркер не освободит место, так что перегруженная система замедляет приём вместо того, чтобы наращивать кучу.

Источник: final/pipeline.go

go
func NewPipeline(workers, queueSize int, solver Solver, metrics *Metrics, target string) *Pipeline {
    return &Pipeline{
        workers: workers,
        queue:   make(chan Challenge, queueSize), // bounded => backpressure
        results: make(chan Result, queueSize),
        solver:  solver,
        metrics: metrics,
        target:  target,
    }
}

Каждый воркер это горутина, которая проходит по этому каналу. Проход по каналу это ещё и протокол остановки: производитель закрывает очередь, когда работа заканчивается, циклы забирают остаток и выходят, а sync.WaitGroup сообщает вызывающему, когда закончил последний.

go
func (p *Pipeline) worker(ctx context.Context, id int, wg *sync.WaitGroup) {
    defer wg.Done()

    for ch := range p.queue {
        started := time.Now()

        err := p.solver.Solve(ctx, ch)

        p.results <- Result{
            ID:      ch.ID,
            OK:      err == nil,
            Latency: time.Since(started),
            Worker:  id,
        }
    }
}

Обратите внимание, чего воркер не делает: он не трогает структуру метрик. Он сообщает Result и идёт дальше. Одна горутина-сборщик опустошает канал результатов в сборщик, поэтому у счётчиков ровно один писатель, а горячий путь остаётся отправкой в канал.

Шаг 2: интерфейс решателя и две его реализации

Конвейер зависит от двух методов. Это весь контракт.

Источник: final/solver.go

go
type Solver interface {
    Solve(ctx context.Context, c Challenge) error
    Name() string
}

MockSolver подменяет ступень решения базовой задержкой, равномерным дрожанием и долей отказов, и не делает сетевых вызовов. Он существует, чтобы вы могли измерить оркестрацию отдельно.

go
func (s *MockSolver) Solve(ctx context.Context, _ Challenge) error {
    d := s.Base

    if s.Jitter > 0 {
        d += time.Duration(rand.Int63n(int64(s.Jitter)))
    }

    select {
    case <-time.After(d):
    case <-ctx.Done():
        return ctx.Err()
    }

    if s.FailRate > 0 && rand.Float64() < s.FailRate {
        return errors.New("mock solve failed")
    }

    return nil
}

CrawlbaseSolver это реальный путь. Он отправляет целевой URL в Crawling API, где обработка CAPTCHA и антибот-защиты происходит внутри самой загрузки, и считает чистый 200 с полностью вычитанным телом завершённой операцией. Вычитывание в io.Discard не косметика: непрочитанное тело нельзя вернуть в пул соединений, и через минуту это окажется очень важным.

go
func (s *CrawlbaseSolver) Solve(ctx context.Context, c Challenge) error {
    endpoint := fmt.Sprintf(
        "https://api.crawlbase.com/?token=%s&url=%s",
        url.QueryEscape(s.Token),
        url.QueryEscape(c.URL),
    )

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
    if err != nil {
        return err
    }

    resp, err := s.Client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    _, _ = io.Copy(io.Discard, resp.Body)

    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("crawlbase status %d", resp.StatusCode)
    }

    return nil
}

Та же подпись, совершенно другая физика. Пул воркеров не может их различить, и в этом весь смысл.

Шаг 3: измеряйте пропускную способность и хвост вместе

Число пропускной способности само по себе скрывает самые интересные отказы. Конвейер может показывать здоровое среднее, пока часть запросов выполняется на порядок дольше остальных, а в параллельной системе именно эти медленные запросы держат ваш бюджет параллелизма в залоге. Поэтому сборщик хранит каждую задержку и считает по ним перцентили.

Источник: final/metrics.go

go
func (m *Metrics) Report() Report {
    total := m.successes + m.failures
    elapsed := m.end.Sub(m.start)

    rate := 0.0
    if elapsed > 0 {
        rate = float64(total) / elapsed.Seconds()
    }

    return Report{
        Total:           total,
        Successes:       m.successes,
        Failures:        m.failures,
        Elapsed:         elapsed,
        SolvesPerSecond: rate,
        P50:             m.percentile(50),
        P99:             m.percentile(99),
    }
}

Отсюда выходят четыре сигнала: общий объём, разбивка на успехи и отказы, p50 и p99. Именно эта пара превращает подбор числа воркеров в измерение, а не в догадку. Добавляйте воркеров и следите за обоими: если скорость растёт, а p99 держится, дополнительный параллелизм усваивается. Если скорость выходит на плато, а p99 продолжает расти, вы копите очередь ниже по потоку, и добавление воркеров теперь делает только хуже.

Шаг 4: нагрузочная обвязка

main.go связывает части вместе и выводит регуляторы в командную строку, а config.go читает токен и целевой URL из окружения через маленький загрузчик .env без зависимостей.

Источник: final/main.go

go
metrics := NewMetrics(*requests)

pipeline := NewPipeline(
    *workers,
    *queueSize,
    solver,
    metrics,
    cfg.TargetURL,
)

report := pipeline.Run(
    context.Background(),
    *requests,
)

fmt.Println(report)

Флаги такие: -solver, -requests, -workers, -queue, -base-ms, -jitter-ms, и -fail-rate. По умолчанию это 20 000 запросов, 256 воркеров, очередь в 1 024, базовая задержка 5 мс с дрожанием 5 мс и доля отказов 1%.

Что запуск с заглушкой доказывает, а что нет

Сначала нагрузите управляющий слой, без сети на пути:

bash
go run . -requests 20000 -workers 256 -queue 1024
output
solver=mock requests=20000 workers=256 queue=1024 target=https://example.com

total=20000  ok=19804  fail=196  elapsed=615ms
solves/sec=32507  p50=7.83ms  p99=10.354ms

Три вещи в этом выводе стоит прочитать внимательно, потому что две из них подтверждения, а одна ограничение.

32 507 против потолка в 32 695. Оркестрация стоит 0,57%. Отправки в канал, один мьютекс и сведение в одной горутине это не то место, где живёт проблема пропускной способности, и теперь у вас есть чек, а не интуиция.

196 отказов из 20 000 это 0,98%, против настроенной доли отказов 1%. Значит, путь ошибки действительно проходится и корректно считается. Для чистого запуска передайте -fail-rate 0 , но решатель, который никогда не падает, это не тот, который вы выпускаете.

p99 в 10,354 мс это не хвост. Время обслуживания заглушки это 5 мс плюс равномерное дрожание меньше 5 мс, поэтому 10 мс это жёсткий арифметический максимум. Измеренный p99 лежит на 354 микросекунды выше потолка, встроенного в саму имитацию. Это измерение планировщика, а не распределение задержек. Настоящие хвосты складываются из DNS, TLS-рукопожатий, повторов, медленного источника и одного неудачного IP, и ничего из этого в этом запуске нет. Верить p99 из заглушки это самый простой способ удивиться в продакшене.

Заглушка измеряет ваш код, а не вашу зависимость

Бенчмарк с заглушкой это правильный первый шаг и плохой последний. Он доказывает, что очередь и пул способны учитывать десятки тысяч операций в секунду на одной машине, а это именно то, что вы хотите знать, прежде чем винить их. О ступени, которая на самом деле занимает две секунды, он не говорит ничего.

Реальный путь ограничен сетью

Теперь тот же конвейер, тот же код, против живого endpoint. Намеренно небольшой:

bash
go run . -solver crawlbase -requests 12 -workers 6 -queue 32
output
solver=crawlbase requests=12 workers=6 queue=32

total=12  ok=12  fail=0  elapsed=3.38s
solves/sec=4  p50=1.924091s  p99=2.409689s

Четыре решения в секунду, и сообщённое значение это 3,55, округлённое форматной строкой. Двенадцать выборок это слишком мало для осмысленного перцентиля, поэтому воспринимайте p50 как порядок величины: решение по реальному пути занимает около двух секунд, и большая часть этого времени уходит на загрузку и антибот-работу на дальней стороне, а не на что-либо внутри Go.

Именно это одно число важно для планирования мощности, и именно его заглушка вам дать не может. Две секунды времени обслуживания это то, что превращает 8 000 в секунду в 16 000 одновременных операций. Сравнение здесь не заглушка 32 507 против реальных 4; это управляющий слой с тремя порядками запаса, стоящий перед зависимостью, которая и задаёт настоящий бюджет.

Три вещи ломаются между 6 воркерами и 16 000 операций в полёте

Пример работает на 6 воркерах. Продакшен работает на четырёхзначных числах. Вот три стены, в которые вы упрётесь по пути, в том порядке, в котором это произойдёт.

Первым сдаётся пул соединений

NewCrawlbaseSolver создаёт свой клиент обычным способом:

go
Client: &http.Client{Timeout: 30 * time.Second}

Отсутствие поля Transport означает http.DefaultTransport, а http.DefaultTransport держит два простаивающих соединения на хост. Это DefaultMaxIdleConnsPerHost, и это значение равно 2 всё время, пока существует net/http . На шести воркерах против одного хоста API этого никто не замечает. На двух тысячах воркеров против одного хоста API все соединения, кроме двух, разрываются сразу после чтения ответа, поэтому почти каждое решение платит за новое TCP-рукопожатие и новое TLS-рукопожатие, прежде чем сможет отправить хоть байт. Вы добавили один-два сетевых обхода к двухсекундной операции, сожгли процессор на рукопожатиях и без причины начали перебирать эфемерные порты.

Рассчитайте транспорт под тот параллелизм, который вам действительно нужен:

go
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.MaxIdleConns = workers
transport.MaxIdleConnsPerHost = workers // default is 2
transport.MaxConnsPerHost = workers    // 0 means unlimited
transport.IdleConnTimeout = 90 * time.Second

client := &http.Client{Transport: transport, Timeout: 30 * time.Second}

Установка MaxConnsPerHost это ещё и способ сделать обратное давление честным от начала до конца. Без него пул из 16 000 воркеров охотно попытается открыть 16 000 сокетов. С ним воркеры вместо этого блокируются в ожидании соединения, а это гораздо лучший режим отказа, чем исчерпание файловых дескрипторов. Поднимите и лимит процесса, потому что значение по умолчанию в операционной системе далеко не дотягивает до четырёхзначного числа одновременных сокетов.

Два простаивающих соединения, N воркеров. На транспорте по умолчанию каждый воркер после второго переподключается и заново делает рукопожатие на каждое решение. Расчёт пула под число воркеров возвращает по одному тёплому соединению на воркер.

Срез задержек превращается в утечку памяти

Сборщик добавляет по одному time.Duration на каждое решение и заранее выделяет срез под число запросов. Для бенчмарка из 20 000 запросов это 160 КБ и сортировка, которую никто не заметит. Для сервиса он не перестаёт расти никогда.

При 8 000 решениях в секунду восемь байт на выборку это 62,5 КБ в секунду, 230 МБ в час и 5,5 ГБ в сутки. Хуже того, percentile копирует весь срез и сортирует копию при каждом вызове, поэтому отчёт по часу трафика сортирует 28,8 миллиона элементов и заодно удваивает занятую память. Для нагрузочной обвязки, вся задача которой сохранить каждую выборку ограниченного запуска, это правильно, а для чего-либо долгоживущего совершенно неверно.

Исправление стандартное: гистограмма задержек фиксированного размера со счётчиками по интервалам, сбрасываемая или сменяемая в каждом окне отчётности. Перцентили становятся интерполяцией по счётчикам вместо сортировки по всей истории, память становится постоянной, а числа перестают вам врать в тот момент, когда запуск переживает своё выделение памяти.

Один процесс перестаёт быть развёртыванием

Шестнадцать тысяч запросов в полёте это не задача о числе горутин. Горутины дешёвые; сокеты, TLS-сессии, файловые дескрипторы и сетевая карта перед ними нет. За пределами нескольких тысяч одновременных соединений к одному адресату форма должна измениться: много экземпляров воркеров, одна общая очередь перед ними, результаты, текущие в надёжное хранилище.

text
                    +----------------+
                    |  shared queue  |   bounded, same as the channel
                    +-------+--------+
                            |
             +--------------+--------------+
             |              |              |
             v              v              v
        worker group   worker group   worker group
             |              |              |
             +--------------+--------------+
                            |
                            v
                     solve path (API)
                            |
                            v
                      result stream

Модель переживает этот переезд, потому что ничто в ней не предполагало один процесс. Канал Go становится общей очередью, горутина становится экземпляром воркера, сведение становится конвейером метрик, а queue to workers to solver to results читается одинаково на обоих масштабах. Это и есть настоящий аргумент за то, чтобы держать управляющий слой таким маленьким: это те же четыре обязанности, запускаете вы 256 воркеров или 8 экземпляров по 2 000. То же рассуждение подробнее разобрано в нашем материале про построение распределённого движка обхода.

Crawlbase Crawling API

Управляющий слой это та часть, которую стоит написать самому. Ступень решения нет: обработка CAPTCHA и антибот-защиты происходит внутри загрузки, за ротацией резидентных IP, и возвращается одним чистым ответом, который ваш воркер либо засчитывает, либо повторяет. Направьте тот же конвейер на неё и начните бесплатно с 1 000 запросов, без карты.

Размер очереди и число воркеров делают разную работу

Эти два флага настраивают вместе и постоянно путают.

Число воркеров задаёт параллелизм. Оно решает, сколько операций решателя вы готовы держать открытыми одновременно, а это по закону Литтла единственный рычаг, меняющий скорость, которую вы можете держать.

Размер очереди задаёт буферизацию. Он решает, какой временный перекос между поступлением и мощностью вы можете впитать до того, как производитель начнёт блокироваться.

Более большая очередь не покупает никакой пропускной способности. Если воркеры справляются с 1 000 операций в секунду, а производители создают 2 000, глубина очереди определяет лишь то, сколько секунд пройдёт до её заполнения. Она заполняется, производитель блокируется, и обратное давление приходит точно туда, куда должно. Подбор размера очереди на самом деле это решение о терпимости к всплескам и о том, насколько старой вы позволите стать ждущей записи.

Настраивайте сначала заглушку, где итерация бесплатна: -base-ms меняет имитируемое время обслуживания, -jitter-ms добавляет разброс, а -fail-rate позволяет посмотреть на путь ошибки под нагрузкой. Эти три регулятора воспроизводят большую часть интересующего вас поведения ещё до отправки первого реального запроса.

Что забрать с собой в продакшен

Ограничивайте работу

Неограниченная очередь не предотвращает перегрузку; она скрывает перегрузку до момента, когда память становится механизмом отказа, и затем падает целиком, без полезного сигнала. Ограниченная очередь превращает ту же перегрузку в обратное давление, а оно видимо, переживаемо и измеримо.

Держите решатель за интерфейсом

Конвейер должен двигать работу, а не иметь мнение о том, как эта работа делается. Две реализации здесь и есть аргумент: тот же управляющий слой нагрузили до 32 507 операций в секунду, а затем направили на живой API, не изменив ни строки в pipeline.go.

Измеряйте ступень, а не систему

Пропускная способность и хвостовая задержка что-то значат только вместе, и только когда вы знаете, какая ступень их породила. Пул воркеров никогда не покажет больше полезной пропускной способности, чем держит решатель, поэтому скорость, перестающая отвечать на добавленных воркеров, это утверждение о зависимости. Наши заметки о масштабировании проектов веб-скрапинга и этот разбор случая на 1 миллиард запросов в месяц оба возвращаются к одной привычке: снимать метрики по каждой ступени.

Запуск сопровождающего репозитория

Репозиторию нужен Go 1.22 или новее. Путь с заглушкой не требует аккаунта; путь через Crawlbase требует токен.

bash
git clone https://github.com/ScraperHub/how-we-solve-8000-captchas-per-second.git
cd how-we-solve-8000-captchas-per-second/final
cp .env.example .env        # only needed for the crawlbase solver
go build -o captcha-pipeline .

Две переменные окружения, обе читаются файлом config.go:

Переменная Назначение
CRAWLBASE_TOKEN Токен для пути -solver crawlbase . Если его нет, выводится CRAWLBASE_TOKEN is required for the crawlbase solver и запуск прекращается.
TARGET_URL URL, который решатель Crawlbase загружает на каждую задачу. По умолчанию https://example.com.

final/ это канонический запускаемый модуль, а steps/ содержит копии только для чтения того файла, который вводился на каждом шаге выше, так что вы можете прочитать конвейер таким, каким он был после шага 1, а не только в готовом виде.

Раздел Путь к коду
Шаг 1: очередь и пул воркеров final/pipeline.go
Шаг 2: интерфейс решателя и две его реализации final/solver.go
Шаг 3: измеряйте пропускную способность и хвост вместе final/metrics.go
Шаг 4: нагрузочная обвязка final/main.go, final/config.go

Заключение

То, что нужно для обработки 8 000 CAPTCHA в секунду, это 16 000 операций в полёте, и всё сложное следует из этого одного числа, а не из самой расшифровки.

Управляющий слой на Go это простая половина, и измерения это подтверждают: ограниченная очередь, пул воркеров, интерфейс из двух методов и сборщик работают в пределах 0,6% от своего арифметического потолка при 32 507 имитированных операциях в секунду. Сложная половина это держать открытыми четырёхзначное число реальных соединений, сохранять при этом постоянный расход памяти на метрики и растянуть всё это по экземплярам, как только на одной машине заканчиваются сокеты.

Поэтому полезный итог этого упражнения не число воркеров. Это два числа, которые вы можете защитить, время обслуживания и параллелизм, и конвейер, снабжённый метриками достаточно хорошо, чтобы сказать, какая ступень владеет пределом. Есть они, и целевая скорость становится решением о мощности. Нет их, и она остаётся догадкой с большим числом рядом.

Часто задаваемые вопросы

Решает ли команда из примера 8 000 настоящих CAPTCHA в секунду?

Нет, и она для этого не предназначена. Команда с заглушкой сообщает 32 507 имитированных решения в секунду вообще без сети, а команда Crawlbase в репозитории использует намеренно крошечную нагрузку из 12 запросов и сообщает около 4 в секунду, потому что она ограничена сетью. Число 8 000 описывает архитектуру в продакшен-масштабе, то есть много экземпляров воркеров перед общей очередью, а не один локальный процесс. Пример даёт вам два входных значения, которыми это развёртывание и рассчитывается: запас управляющего слоя и время обслуживания реального пути.

Сколько воркеров нужно для 8 000 решений в секунду?

Разделите целевую скорость на скорость завершения одного воркера. При примерно двух секундах на решение один воркер справляется с 0,5 в секунду, поэтому 8 000 в секунду требуют около 16 000 воркеров в полёте. Будет это 8 экземпляров по 2 000 или 16 по 1 000, это вопрос сокетов, дескрипторов и радиуса поражения, а не вопрос Go. Измерьте собственный p50 на собственных целях, прежде чем доверять цифре в две секунды.

Зачем вообще тестировать с заглушкой вместо решателя?

Чтобы выяснить, не является ли узким местом ваш собственный код, прежде чем винить зависимость. Заглушка убирает сеть и имитирует решение с настраиваемой задержкой, дрожанием и долей отказов, что изолирует очередь, пул воркеров и сведение результатов. Здесь она показала, что оркестрация стоит 0,57% теоретического потолка, значит, недостаток пропускной способности в реальном запуске доказуемо не в управляющем слое.

Можно ли доверять p99 из запуска с заглушкой?

Нет. Время обслуживания заглушки это базовая задержка плюс ограниченное дрожание, поэтому её худший случай задан арифметикой: 5 мс плюс не более 5 мс дают потолок в 10 мс, а измеренный p99 в 10,354 мс это тот же потолок плюс накладные расходы планировщика. Настоящий хвост берётся из DNS, TLS, повторов и медленных источников, и ничего из этого заглушка не имитирует. Используйте перцентили заглушки, чтобы находить проблемы планирования в своём коде, и больше ни для чего.

Почему ограниченная очередь, а не неограниченная?

Потому что неограниченная очередь не убирает перегрузку, а лишь переносит симптом в память и откладывает отказ до того момента, когда он уже неисправим. Когда ограниченная очередь заполняется, производители ждут воркеров, и это обратное давление: давление уходит обратно к тому, кто создаёт работу, а ждущая работа остаётся под пределом, который вы выбрали осознанно.

Как подбирать число воркеров?

Поднимайте -workers на пути с заглушкой и следите за пропускной способностью и p99 вместе. Пока скорость растёт, а p99 держится ровно, добавленный параллелизм усваивается. Когда скорость выходит на плато, а p99 продолжает расти, следующая ступень стала пределом, и новые воркеры лишь углубляют очередь, которую вы не видите. Затем переключитесь на реальный решатель и повторите, потому что две кривые имеют совсем разную форму.

Что именно делает решатель Crawlbase?

Он отправляет целевой URL в Crawling API и считает чистый HTTP 200 с вычитанным телом завершённым решением. Работа с CAPTCHA и антибот-защитой происходит внутри этой загрузки, а не в вашем процессе, поэтому "решение" на этом пути это один запрос и одна проверка статуса. Подходы к той же задаче со стороны вызывающего разобраны в нашем руководстве по обходу CAPTCHA при веб-скрапинге.

Начать создавать

Обходите любой сайт в масштабе, без борьбы с инфраструктурой.

Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.

Самообслуживание · Звонок отдела продаж не требуется · Доступны корпоративные объёмы краулинга