Современные реалии таковы, что не каждый бизнес может себе позволить сайт, особенно стартующий бизнес. Поэтому выгодно воспользоваться конструктором сайтов, например filandor.com . Запуск сайта через несколько минут.

Cloudflare обслуживает огромный объем трафика: в среднем 45 миллионов HTTP-запросов в секунду (по состоянию на 2023 год; 61 миллион на пике) из более чем 285 городов в более чем 100 странах. Что неизбежно происходит при таком масштабе, так это то, что программное обеспечение будет доведено до предела. По мере нашего роста одна из проблем, с которыми мы сталкивались, была связана с развертыванием нашего кода. Иногда выпуск задерживался из-за нехватки аппаратных ресурсов на наших серверах. Покупка все большего количества аппаратного обеспечения обходится дорого, и существуют ограничения, например, на то, сколько памяти мы реально можем иметь на сервере. В этой статье мы объясняем, как мы оптимизировали наше программное обеспечение и процесс его выпуска, чтобы не требовалось дополнительных ресурсов.

Для обработки трафика на каждом из наших серверов работает набор специализированных прокси. Исторически они были основаны на NGINX, но все чаще включают сервисы, созданные на Rust. Из наших прокси-приложений FL (Front Line) является самым старым и по-прежнему имеет широкий набор функций.

По сути, это одно из последних применений NGINX в Cloudflare. Он содержит большое количество бизнес-логики, которая запускает многие продукты Cloudflare с использованием различных библиотек Lua и Rust. В результате он потребляет большое количество системных ресурсов: до 50-60 ГиБ оперативной памяти. По мере роста FL выпускать его становилось все труднее. Механизм обновления требует вдвое больше памяти (которая иногда недоступна), чем во время выполнения. Это вызывало задержки с выпуском релизов. Теперь мы улучшили процедуру выпуска FL и, по сути, устранили необходимость в дополнительной памяти во время процесса выпуска, повысив его скорость и производительность.

Архитектура

Для выполнения всей своей работы каждый экземпляр FL запускает множество рабочих процессов (отдельных процессов). По замыслу отдельные процессы обрабатывают запросы, в то время как главный процесс контролирует их и следит за тем, чтобы они оставались в рабочем состоянии. Это позволяет NGINX обрабатывать больше трафика, добавляя больше воркеров. Мы используем эту архитектуру.

Количество рабочих зависит от количества имеющихся ядер ЦП. Обычно он равен половине доступных ядер ЦП, например, на 128-ядерном ЦП мы используем 64 рабочих FL.

Пока все хорошо, в чем проблема?

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

После установки новой версии FL на сервер запускается процедура обновления. Реализация NGINX вращается вокруг связи с основным процессом с помощью сигналов. Процесс обновления начинается с отправки сигнала USR2, который запускает новый главный процесс и его рабочие процессы.

В этот момент, как видно ниже, обе версии будут работать и обрабатывать запросы. К сожалению, побочным эффектом этого является удвоение объема памяти.

  PID  PPID COMMAND
33126     1 nginx: master process /usr/local/nginx/sbin/nginx
33134 33126 nginx: worker process (nginx)
33135 33126 nginx: worker process (nginx)
33136 33126 nginx: worker process (nginx)
36264 33126 nginx: master process /usr/local/nginx/sbin/nginx
36265 36264 nginx: worker process (nginx)
36266 36264 nginx: worker process (nginx)
36267 36264 nginx: worker process (nginx)

Затем сигнал WINCH будет отправлен главному процессу, который затем попросит своих рабочих корректно завершить работу. В конце концов, все они завершатся, оставив запущенным только исходный главный процесс (который можно остановить с помощью сигнала QUIT). Успешный результат этого оставит только новый экземпляр, который будет выглядеть примерно так:

  PID  PPID COMMAND
36264     1 nginx: master process /usr/local/nginx/sbin/nginx
36265 36264 nginx: worker process (nginx)
36266 36264 nginx: worker process (nginx)
36267 36264 nginx: worker process (nginx)

Стандартный механизм обновления NGINX представлен на этой диаграмме:

Это также хорошо видно на приведенном ниже графике использования памяти (обратите внимание на большой скачок во время обновления).

В описанном выше механизме обе версии некоторое время работают одновременно. Когда оба набора рабочих процессов работают, они по-прежнему используют одни и те же сокеты, поэтому все они принимают запросы. По мере продвижения выпуска «старые» рабочие процессы перестают слушать и принимать новые запросы (в этот момент только новые рабочие процессы принимают новые запросы). Поскольку мы выпускаем новый код несколько раз в неделю, этот процесс фактически удваивает наши требования к памяти. В нашем масштабе легко увидеть, как умножение этого события на количество серверов, с которыми мы работаем, приводит к огромной трате ресурсов памяти.

Кроме того, иногда на обновление серверов уходят часы (это особенно важно, когда нам нужно что-то быстро выпустить), так как мы ждем, когда будет достаточно памяти для запуска действия перезагрузки.

Новый механизм обновления

Мы решили эту проблему, изменив метод обновления исполняемого файла NGINX. Вот как это работает.

Суть проблемы в том, что NGINX рассматривает весь экземпляр (мастер + рабочие) как один. При обновлении нам нужно запустить всех рабочих, пока все предыдущие еще работают. Учитывая количество рабочих, которых мы используем, и их вес, это не является устойчивым.

Поэтому вместо этого мы модифицировали NGINX, чтобы иметь возможность контролировать отдельных воркеров. Вместо того, чтобы запускать и останавливать их все сразу, мы можем сделать это, выбрав их по отдельности. Для этого главный процесс и рабочие процессы понимают дополнительные сигналы по сравнению с теми, которые использует NGINX. Фактический механизм для выполнения этого в NGINX почти такой же, как и при массовой обработке рабочих процессов. Однако есть принципиальная разница.

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

Шаг за шагом

Наш усовершенствованный механизм обрабатывает каждого работника индивидуально. Вот шаги:

  1. Вначале у нас есть экземпляр FL с 64 рабочими процессами.
  2. Отключите функцию автоматического перезапуска рабочих процессов, которые выходят.
  3. Завершите работу воркера из первого (старого) экземпляра FL. У нас осталось 63 рабочих.
  4. Создайте новый экземпляр FL, но только с одним рабочим. Мы вернулись к 64 рабочим, но включая одного из новой версии.
  5. Повторно включите функцию для автоматического перезапуска рабочих процессов, которые завершаются.
  6. Мы продолжаем этот шаблон закрытия рабочего процесса из старого экземпляра и создания нового для его замены. Это можно наглядно представить на схеме ниже.

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

Но почему мы начинаем с закрытия рабочего процесса, принадлежащего более старому экземпляру (v1)? Это оказывается важным.

Закрепление рабочего процессора

Во время этого рабочего процесса нам также пришлось позаботиться о закреплении ЦП. Работники FL на наших серверах закреплены за ядрами ЦП, при этом один процесс занимает одно ядро ​​ЦП, чтобы помочь нам более эффективно распределять ресурсы. Если мы сначала запустим нового работника, он на короткое время будет делить ядро ​​процессора с другим. Это сделает один ЦП перегруженным по сравнению с другими, работающими с FL, что повлияет на задержку обслуживаемых запросов. Вот почему мы начинаем с освобождения ядра ЦП, которое затем может занять новый рабочий процесс, а не с создания нового рабочего процесса.

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

Мы начинаем с ядра ЦП 1 (или с того, что первым используется FL) и делаем следующее:

  1. Выключите рабочего, который там работает.
  2. Создайте нового работника. NGINX прикрепит его к ядру ЦП, которое мы освободили на предыдущем шаге.

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

Заключение

В Cloudflare нам необходимо выпускать новое программное обеспечение несколько раз в день для всего нашего парка. Стандартный механизм обновления, используемый NGINX, не подходит для наших масштабов. По этой причине мы настроили процесс, чтобы избежать увеличения объема памяти, необходимого для выпуска FL. Это позволило нам безопасно отправлять код везде, где это необходимо. Пользовательский механизм обновления позволяет нам надежно выпускать большие приложения, такие как FL, независимо от того, сколько памяти доступно на пограничном сервере. Мы показали, что можно расширить NGINX и его встроенный механизм обновления, чтобы удовлетворить уникальные требования Cloudflare.

Если вам нравится решать сложные проблемы с инфраструктурой приложений и вы хотите помочь поддерживать самый загруженный веб-сервер в мире, мы нанимаем вас!