Решение проблем с узкими местами в работе сторонних API в WordPress

Ни один сайт на WordPress не существует изолированно. Сайты регулярно интегрируются с внешними CRM-системами, корпоративными ERP-системами, менеджерами управления запасами, платежными шлюзами и API для выполнения заказов. Эти интеграции добавляют ценность, но также создают архитектурные уязвимости.

Опасность заключается не только в том, что сторонний сервис может выйти из строя и привести к прекращению работы какой-либо конкретной функции. Реальный риск — это каскадный сбой. Если внешний API испытывает задержки или полностью отключается, плохо изолированный сайт WordPress выйдет из строя вместе с ним.

Ваш сервер рухнет не из-за сбоя базы данных или нехватки памяти. Он рухнет из-за того, что воркеры PHP-FPM вашего сервера застряли в синхронной очереди ожидания ответа, который так и не поступит.

Для обеспечения по-настоящему отказоустойчивого сайта необходима изоляция кода. Вот как отделить доступность фронтенда вашего сайта от надежности сторонних зависимостей.

Анатомия зависания PHP-воркера

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

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

По умолчанию HTTP-запросы, выполняемые с помощью базовых функций WordPress, таких как wp_remote_get() или wp_remote_request(), являются синхронными и блокирующими. PHP по своей природе однопоточный. Когда воркер выполняет блокирующий HTTP-запрос, он полностью останавливается и переходит в режим ожидания ответа от внешнего сервера.

[Входящий запрос] ➔ [Назначен воркер PHP-FPM] ➔ [Выполнение wp_remote_get()] ➔ [Воркер приостановлен ​​/ Ожидание API]

На большинстве продакшн-серверов используется менеджер процессов, такой как PHP-FPM, с ограниченным пулом воркеров (например, от 20 до 50 параллельных воркеров в зависимости от конфигурации сервера). Если на вашем сайте внезапно увеличится трафик и 20 пользователей одновременно зайдут на страницу, которая инициирует вызов тормозного стороннего API, весь пул воркеров может быть исчерпан мгновенно.

В итоге ваш сервер зависнет. 21-й посетитель будет помещен в очередь, даже если он просто пытается загрузить полностью статическую, состоящую из одного текста главную страницу, которая не использует API. Когда очередь заполнится, Nginx или Apache выдадут ошибку 502 Bad Gateway или 504 Gateway Timeout. Медленная работа API-эндпоинта на одной странице приведет к сбою всей вашей сети.

Ловушка для Transient и давка кэша

Распространенная первая линия защиты от узких мест API — это кэширование внешнего ответа с помощью WordPress Transients API. Обернув ваш запрос в функции get_transient() и set_transient(), вы гарантируете, что ваш сервер будет обращаться к внешнему API только один раз в час или день, а не при каждой загрузке страницы.

Хотя кэширование является обязательным, полагаться исключительно на стандартные временные данные (transients) при высокой нагрузке нельзя. Это чревато серьезной уязвимостью, известной как «давка кэша» (или «эффект стада»).

Рассмотрим интернет-магазин с высокой посещаемостью, на котором истек срок действия API transient. В эту миллисекунду на сайт одновременно заходят 50 пользователей. Поскольку функция get_transient() возвращает false для всех 50 воркеров одновременно, каждый из них независимо выполнит функцию wp_remote_get() для обновления кеша.

Вместо защиты внешнего API и вашего локального сервера вы спровоцировали резкий, синхронизированный всплеск исходящих запросов! Если сторонний API будет медленно отвечать во время этой «давки», ваш пул воркеров мгновенно зависнет, и сайт выйдет из строя.

Как предотвратить «давку»: блокировка transient

Для решения проблемы «давки кэша» нам необходимо ввести блокировку transient.

Когда основной кэш истек, первый же PHP-воркер, заметивший отсутствие данных, немедленно устанавливает блокировку transient (например, на 30 секунд). Остальные 49 параллельных воркеров проверяют кэш и видят, что он пуст; они также видят блокировку. Они понимают: «Кто-то другой уже запрашивает данные!» Вместо того чтобы перегружать внешний API, эти 49 воркеров мгновенно возвращают fallback-данные, полностью нейтрализуя проблему «давки» данных.


function fetch_api_data_with_lock() {
    $cache_key = 'external_api_data';
    $lock_key  = 'external_api_data_lock';

    // 1. Try to get the cached data
    $cached_data = get_transient( $cache_key );
    if ( false !== $cached_data ) {
        return $cached_data; // Cache hit! Return immediately.
    }

    // 2. Cache is empty. Check if another worker is already fetching it.
    if ( get_transient( $lock_key ) ) {
        // The lock exists. Another process is currently hitting the API.
        // Return fallback data to prevent a stampede.
        return get_fallback_api_data();
    }

    // 3. No lock exists. Set the lock immediately for 30 seconds!
    set_transient( $lock_key, true, 30 );

    // 4. Safely fetch the data
    $response = wp_remote_get( 'https://api.external-service.com/v1/data', [
        'timeout' => 1.5,
    ] );

    // Handle failure
    if ( is_wp_error( $response ) ) {
        delete_transient( $lock_key ); // Clear lock so we can try again sooner
        return get_fallback_api_data();
    }

    $body = wp_remote_retrieve_body( $response );
    $data = json_decode( $body, true );

    // 5. Success! Save the actual data for 1 hour and remove the lock.
    if ( $data ) {
         set_transient( $cache_key, $data, HOUR_IN_SECONDS );
    }
    delete_transient( $lock_key );

    return $data;
}

Защитная маршрутизация: настройка HTTP-запросов

Первое правило защитной инженерии — никогда не принимайте значения дефолтной конфигурации слепо. По умолчанию WordPress устанавливает 5-секундный таймаут для HTTP-запросов, выполняемых через wp_remote_get(). Пять секунд — это целая вечность. Если воркер зависает на 5 секунд на сайте с высокой посещаемостью, истощение ресурсов практически гарантировано.

При взаимодействии с внешними API на стороне фронтенда необходимо значительно снижать таймауты и корректно обрабатывать возникающие ошибки с помощью функции is_wp_error().

Вот как можно настроить защитную обертку для вызова API:


function fetch_defensive_api_data() {
    $url = 'https://api.external-service.com/v1/data';

    // Step 1: Lower timeouts aggressively
    $response = wp_remote_get( $url, [
        'timeout'   => 1.5, // 1.5 seconds max before failing fast
        'sslverify' => true,
    ] );

    // Step 2: Catch timeouts or network failures gracefully
    if ( is_wp_error( $response ) ) {
        // Log the error for internal auditing
        error_log( 'API Failure: ' . $response->get_error_message() );

        // Return fallback data instead of breaking the execution
        return get_fallback_api_data();
    }

    $body = wp_remote_retrieve_body( $response );
    return json_decode( $body, true );
}

function get_fallback_api_data() {
    // Provide safe, stale, or static default data to keep the UI intact
    return [
        'status' => 'offline',
        'items'  => [],
    ];
}


Сократив время ожидания до 1,5 секунд, вы гарантируете, что тормозной API сможет удерживать PHP-воркер в заложниках лишь короткое время, давая вашему серверу шанс перезапустить его и обработать следующий запрос.

Внедрение схемы автоматического выключателя (Circuit Breaker Pattern)

Снижение таймаутов защищает ваш пул воркеров от длительных зависаний, но это не значит, что ваш сервер перестанет непрерывно «долбить» неработающий или недоступный API. Если внешний сервис недоступен в течение часа, ваш сервер все равно будет тратить 1,5 секунды на каждую загрузку страницы, пытаясь связаться с ним.

Для решения этой проблемы мы можем позаимствовать классическую стратегию микросервисной архитектуры: схему автоматического выключателя.

Концепция проста:

  • Состояние closed (замкнут). API функционирует в обычном режиме; запросы проходят.
  • Состояние open (разомкнут). API несколько раз подряд дал сбой. Выключатель «срабатывает», и ваш код полностью прекращает попытки вызова удаленного API. Немедленно возвращаются fallback-данные, не расходуя ресурсы сервера.
  • Состояние half-open (полуразомкнут). После периода ожидания схема пропускает один запрос для проверки восстановления работы внешнего сервиса.

Мы можем реализовать легковесный и высокоэффективный механизм автоматического выключателя непосредственно в WordPress, используя Object Cache или Transients API:


function fetch_api_with_circuit_breaker() {
    $circuit_status_key  = 'api_circuit_breaker_tripped';
    $failure_counter_key = 'api_failure_counter';

    // 1. Closed or Open? Check if the circuit is currently "Open" (Tripped)
    if ( get_transient( $circuit_status_key ) ) {
        // Circuit is open; fail fast immediately and return fallback data
        return get_fallback_api_data();
    }

    // 2. The circuit is Closed (or Half-Open). Attempt the remote request.
    $response = wp_remote_get( 'https://api.external-service.com/v1/data', [
        'timeout' => 1.5,
    ] );

    // 3. Handle a network failure or timeout
    if ( is_wp_error( $response ) ) {
        $failures = (int) get_transient( $failure_counter_key );
        $failures++;

        if ( $failures >= 3 ) {
            // Trip the circuit for 5 minutes. 
            // CRITICAL: We do NOT delete the failure counter here! 
            // We keep it so we can test a "Half-Open" state later.
            set_transient( $circuit_status_key, true, 5 * MINUTE_IN_SECONDS );
        } 

        // Always update the failure count, letting it live longer than the circuit trip time
        set_transient( $failure_counter_key, $failures, 1 * HOUR_IN_SECONDS );

        return get_fallback_api_data();
    }

    // 4. Success! If we get here, the API is healthy.
    // Clear the failure counter to fully reset to a "Closed" state.
    delete_transient( $failure_counter_key );

    $body = wp_remote_retrieve_body( $response );
    return json_decode( $body, true );

    // 5. Validate the payload to ensure it isn't malicious or malformed
    if ( ! is_array( $data ) || empty( $data ) ) {
        return get_fallback_api_data();
    }

    return $data;
}


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

Как только эти пять минут прошли, схема переходит в состояние half-open. Обратите внимание, что в коде мы не удаляем счетчик ошибок при срабатывании выключателя; мы удаляем его только при успешном ответе. После истечения срока действия $circuit_status_key цепь переходит в состояние half-open и пропускает один запрос для проверки API. Если этот запрос не удается, счетчик ошибок увеличивается с 3 до 4, снова активируя 5-минутную цепь, не заставляя сервер ждать 3 новых ошибок. Если запрос успешно выполнен, счетчик удаляется, и цепь полностью замыкается.

Не допускайте состояния гонки

Приведённый выше код автоматического выключателя использует функции get_transient() и set_transient() для подсчёта сбоев. Хотя это приемлемо для стандартных конфигураций, на сайтах с чрезвычайно высокой посещаемостью это создаёт состояние гонки.

Transients не являются атомарными; если 10 пользователей вызовут таймаут API в одну и ту же миллисекунду, все 10 PHP-воркеров могут одновременно запросить базу данных, увидеть 0 в счетчике ошибок и переписать счетчик в 1. Для срабатывания выключателя может потребоваться больше времени, чем 3 фактических сбоя.

Если на вашем сервере используется постоянный объектный кэш, например Redis или Memcached, откажитесь от API Transients для подсчета сбоев. Вместо этого используйте встроенную функцию WordPress wp_cache_incr(). Она выполняет строго атомарное увеличение непосредственно в оперативной памяти сервера, гарантируя абсолютно точный подсчет ошибок независимо от одновременного трафика.

Источник: https://deliciousbrains.com

Дмитрий/ автор статьи
CCO, Senior SEM/PPC Specialist, WordPress-энтузиаст, переводчик с английского и немецкого. Серый кардинал русскоязычного WP-комьюнити.
Блог про WordPress
Добавить комментарий

Получать новые комментарии по электронной почте.