На портале технических ресурсов Broadcom появилась полезная статья, посвященная производительности облачных приложений и причинам их земедления в производственной среде.
Часто бывает, что приложение, работающее на физическом сервере, демонстрирует хорошую производительность. Однако после упаковки приложения в образ, тестирования в Docker/Podman и последующей миграции в окружение Kubernetes производительность резко падает. По мере увеличения числа запросов к базе данных, выполняемых приложением, время отклика приложения возрастает.
Эта распространённая ситуация часто вызвана задержками ввода-вывода, связанными с передачей данных. Представьте, что это же приложение снова запускается на физическом сервере, но в следующем сценарии: устройство хранения базы данных создаётся на удалённом хранилище с Network File System (NFS).
Это означает, что необходимо учитывать следующие факторы:
Количество приложений, обращающихся к конечной точке хранилища (предполагается, что оно быстрое по своему устройству).
Это будет влиять на общую производительность отклика из-за производительности подсистемы ввода-вывода хранилища.
Способ, которым приложение извлекает данные из базы данных.
Кэширует ли оно данные, или просто отправляет очередной запрос? Являются ли запросы небольшими или крупными?
Задержка, вызванная сетевым доступом к хранилищу, и общая скорость работы хранилища.
При доступе к сетевому устройству несколько ключевых факторов влияют на скорость передачи данных:
Доступная скорость соединения
Используемое максимальное время передачи (MTU) / максимальный размер сегмента (MSS)
Размер передаваемой полезной нагрузки
Настройки TCP окна (скользящее окно)
Среднее время задержки при прохождении маршрута (RTT) от точки A до точки B в мс.
В датацентре скорость соединения обычно не является проблемой, поскольку все узлы соединены с пропускной способностью не менее 1 Гбит/с, иногда 10 Гбит/с, и подключены к одному коммутатору. MTU/MSS будет оптимальным для настроенного соединения. Остаются размер передаваемой полезной нагрузки, скользящее окно TCP и среднее RTT. Наибольшее влияние окажут размер полезной нагрузки, задержка соединения и его качество, которые будут влиять на параметры скользящего окна.
Вот пример задержек между узлами в текущей настройке Kubernetes:
При увеличении размера полезной нагрузки пакета в пинге, как только он превышает текущий MTU, время отклика начинает увеличиваться. Например, для node2 размер полезной нагрузки составляет 64k — это экстремальный случай, который наглядно демонстрирует рост задержки.
Учитывая эти три момента (размер передаваемой полезной нагрузки, скользящее окно TCP и среднее RTT), если разработчик создаёт приложение без использования встроенных возможностей кэширования, то запросы к базе данных начнут накапливаться, вызывая нагрузку на ввод-вывод. Такое упущение направляет все запросы через сетевое соединение, что приводит к увеличению времени отклика всего приложения.
На локальном хранилище или в среде с низкой задержкой (как сети, так и хранилища) можно ожидать, что приложение будет работать хорошо. Это типичная ситуация, когда команды разработки тестируют все в локальной среде. Однако при переходе на сетевое хранилище задержка, вызванная сетью, будет влиять на возможный максимальный уровень запросов. При проведении тех же тестов в производственной среде команды, вероятно, обнаружат, что система, способная обрабатывать, скажем, 1000 запросов к базе данных в секунду, теперь справляется только с 50 запросами в секунду. В таком случае стоит обратить внимание на три вероятные причины:
Переменные размеры пакетов, средний размер которых превышает настроенный MTU.
Сетевая задержка из-за того, что база данных размещена на отдельном узле, вдали от приложения.
Уменьшенное скользящее окно, так как все связанные системы подключаются к узлу базы данных. Это создаёт нагрузку на ввод-вывод файловой системы, из-за чего база данных не может обрабатывать запросы достаточно быстро.
Тесты показали, что алгоритм скользящего окна привёл к снижению скорости передачи данных (для справки: это механизм, используемый удалённым хранилищем на основе NFS). При среднем размере полезной нагрузки 4 Кб скорость передачи данных упала с 12 МБ/с на быстром соединении 1 Гбит/с (8 мс задержки) до ~15 Кбит/с, когда задержка соединения достигла 180 мс. Скользящее окно может вынудить каждую передачу данных возвращать пакет подтверждения (ACK) отправителю перед отправкой следующего пакета на соединениях с низкой задержкой.
Эту ситуацию можно протестировать локально с помощью утилиты tc на системе Linux. С её помощью можно вручную задать задержку для каждого устройства. Например, интерфейс можно настроить так, чтобы он отвечал с задержкой в 150 мс.
На Kubernetes, например, вы можете развернуть приложение с фронтендом, сервером приложений и бэкендом базы данных. Если сервер приложений и бэкенд базы данных развернуты на разных узлах, даже если эти узлы быстрые, они будут создавать сетевые задержки для каждого пакета, отправленного с сервера приложений в базу данных и обратно. Эти задержки суммируются и значительно ухудшают производительность!
Ниже приведён реальный пример, показывающий время отклика приложения. База данных развернута на node2.
В этом случае выполнение приложения требует большого количества запросов к базе данных, что значительно влияет на среднее время отклика затронутых компонентов. В данном случае среднее время отклика составляет 3,9 секунды.
Для этого теста функция graph_dyn запрашивает из базы данных 80 тысяч строк данных, где каждая строка содержит метаданные для построения различных точек графика.
Переместив базу данных на ту же машину, где размещена часть приложения, удалось снизить среднее время отклика до 0,998 секунды!
Это значительное улучшение производительности было достигнуто за счёт сокращения задержек сетевых запросов (round trip), то есть перемещения контейнера базы данных на тот же узел.
Ещё одним распространённым фактором, влияющим на производительность Kubernetes, являются ESXi-среды, где узлы кластера имеют избыточную нагрузку на ввод-вывод сети, а узлы Kubernetes развёрнуты в виде виртуальных машин. Узел Kubernetes не может увидеть, что CPU, память, ввод-вывод диска и/или подключённое удалённое хранилище уже находятся под нагрузкой. В результате пользователи сталкиваются с ухудшением времени отклика приложений.
Как оптимизировать производительность приложения
Учтите кэширование в дизайне приложения.
Убедитесь, что приложение учитывает кэширование (общих запросов, запросов к базе данных и так далее) и не выполняет лишних запросов.
Отключите переподписку ресурсов (oversubscription) в средах ESXi.
Если Kubernetes/OpenShift работают поверх ESXi, убедитесь, что переподписка ресурсов отключена для всех доступных CPU и памяти. Учтите, что даже небольшие задержки могут быстро накопиться и привести к эластичному поведению времени отклика приложения. Кроме того, если хост ESXi уже находится под нагрузкой, запущенные на нём образы не будут осведомлены об этом, и оркестратор Kubernetes может развернуть дополнительные поды на этом узле, считая ресурсы (CPU, память, хранилище) всё ещё доступными. Поэтому, если вы используете Kubernetes/OpenShift, а у вас есть возможность запустить их на физическом сервере, сделайте это! Вы получите прямую и полную видимость ресурсов хоста.
Минимизируйте сетевые задержки ввода-вывода в Kubernetes.
Сократите сетевые задержки ввода-вывода при развёртывании подов. Упакуйте контейнеры в один под, чтобы они развертывались на одном узле. Это значительно снизит сетевые задержки ввода-вывода между сервером приложений и базой данных.
Размещайте контейнеры с высокими требованиями к базе данных на узлах с быстрым хранилищем.
Развёртывайте контейнеры, которым требуется быстрый доступ к базе данных, на узлах с быстрым хранилищем. Учитывайте требования к высокой доступности и балансировке нагрузки. Если хранилище NFS недостаточно быстрое, рассмотрите возможность создания PV (Persistent Volume) на определённом узле с локальным RAID 10 диском. Учтите, что в этом случае резервирование придётся настраивать вручную, так как оркестратор Kubernetes не сможет управлять этим при отказе оборудования конкретного узла.
Как мониторить задержки между компонентами
Для Kubernetes, OpenShift и Docker (Swarm) можно использовать Universal Monitoring Agent (UMA), входящий в решения AIOps and Observability от Broadcom, для мониторинга взаимодействия всего окружения. Этот агент способен извлекать все релевантные метрики кластера. Если UMA обнаруживает поддерживаемую технологию, такую как Java, .Net, PHP или Node.js, он автоматически внедряет пробу (probe) и предоставляет детализированную информацию о выполнении приложения по запросу. Эти данные включают запросы ввода-вывода (I/O) от компонента к базе данных, удалённые вызовы и другие внутренние метрики приложения.
Если приложение работает некорректно, встроенное обнаружение аномалий UMA запускает трассировку транзакций (это не требует дополнительной настройки). UMA собирает подробные метрики с агентов, связанных с компонентами, по пути выполнения конкретного приложения. Эти возможности позволяют получить детализированное представление выполнения приложения вплоть до строки кода, вызывающей замедление. Администраторы могут настроить полный сквозной мониторинг, который предоставляет детали выполнения, включая производительность фронтенда (браузера).
Ниже приведен пример сбора полных сквозных метрик, чтобы команда администраторов могла проанализировать, что пошло не так, и при необходимости принять корректирующие меры:
Одним из преимуществ этого решения для мониторинга является то, что оно показывает среднее время выполнения компонентов внутри кластера. Со временем решение может начать считать плохие средние значения нормой в среде ESXi с избыточной загрузкой. Тем не менее, оператор может оценить реальные значения и сравнить их с показателями других приложений, работающих на том же узле.