В Uber все рабочие нагрузки, хранящие состояние, запускают на единой контейнеризованной платформе. Аппаратной основой этой платформы является обширный парк серверов. Среди таких рабочих нагрузок можно отметить MySQL, Apache Cassandra, ElasticSearch, Apache Kafka, Apache HDFS, Redis, Docstore, Schemaless. Во многих случаях они совместно работают на одних и тех же физических хостах.
Речь идёт о 65000 таких хостов, о 2,4 миллионах процессорных ядер и о 200000 контейнеров. Мы постоянно боремся за повышение эффективности использования серверов, делая это ради снижения затрат на их поддержку. Это — важная задача, но до недавнего времени её достойному решению мешал троттлинг процессоров. Это явление указывало на то, что нашим программам выделялось недостаточно ресурсов.
Оказалось, что проблема заключалась в том, как именно ядро Linux распределяет процессорное время между процессами. В этом материале мы расскажем о переходе с квот CPU (квот на ресурсы процессора, CPU quotas) на механизм распределения процессов по процессорам и памяти (cpusets; эта технология ещё известна как CPU pinning — закрепление процессора). Такой переход позволил нам значительно снизить задержки в 99 перцентиле (P99) в обмен на небольшой рост задержек в 50 перцентиле (P50). Это, в свою очередь, позволило нам снизить уровень выделения процессорных ядер во всём нашем серверном парке на 11% благодаря уменьшению различий в требованиях к ресурсам.
Cgroups, квоты и cpusets
Квоты на ресурсы процессора и cpusets — это возможности планировщика ядра Linux. В ядре Linux изоляция ресурсов обеспечивается посредством механизма контрольных групп (cgroups, control groups). Все платформы контейнеризации основаны на этом механизме. Обычно контейнер связывают с контрольной группой, которая управляет ресурсами процессов, выполняемых в контейнере.
Существует два типа контрольных групп (их ещё называют контроллерами, controllers), используемых для выполнения изоляции процессоров. Это — CPU и cpuset. И тот и другой контроллеры управляют тем, сколько ресурсов процессора позволено использовать набору процессов. Но делается это разными способами. В первом случае — через квотирование времени использования процессора, во втором — через закрепление процессора.
Квоты на ресурсы процессора
Контроллер CPU поддерживает изоляцию посредством квот. CPU-набору процессов выделяют долю процессора (ядер), которую позволено использовать этому набору. Это переводится в квоту на заданный период времени (обычно — 100 мс) с использованием следующей формулы:
квота = количество ядер * период
В предыдущем примере имеется контейнер, которому нужны 2 ядра, что приводит к тому, что контейнеру выделяется, в каждом периоде, 200 мс процессорного времени.
Квоты и троттлинг
Такой подход, к сожалению, вызывает проблемы, возникающие из-за того, что внутри контейнера используется многопроцессорность/многопоточность. Из-за этого контейнер слишком быстро выбирает квоту, что приводит к тому, что он, до конца периода, подвергается троттлингу. Это показано на следующем рисунке.
Это выливается в реальную проблему для контейнеров, обслуживающих запросы, которые необходимо обрабатывать с низкой задержкой. Внезапно оказывается, что, из-за троттлинга, запросы, на выполнение которых раньше уходило несколько миллисекунд, теперь выполняются более чем за 100 мс.
Есть простой способ решения этой проблемы. Он заключается в том, чтобы выделять процессам больше процессорного времени. И хотя это — рабочий подход, он, в крупномасштабных системах, оказывается достаточно дорогим. Ещё одно решение проблемы — это совсем не применять изоляцию. Но в случае с рабочими нагрузками, совместно использующими один и тот же сервер, это — плохая идея. Дело в том, что так один из процессов может забрать себе все ресурсы, лишив другие процессы возможности нормально работать.
Предотвращение троттлинга с использованием cpusets
Контроллер cpusets использует, вместо квот, закрепление процессора. В целом — речь идёт об указании того, на каких именно ядрах может выполняться контейнер. Это означает, что можно распределить контейнеры по разным ядрам, а значит — каждое ядро будет обслуживать лишь один контейнер. Это ведёт к полной изоляции, при использовании закрепления процессора больше не нужны квоты или троттлинг. Другими словами — можно пойти на компромисс, обменяв возможность использования дополнительных ядер и простоту настройки системы на единообразие в плане задержек и на более громоздкую систему управления ядрами. При таком подходе предыдущий рисунок будет выглядеть так, как показано ниже.
Здесь два контейнера выполняются на двух различных наборах процессорных ядер. Контейнерам разрешено использовать столько времени этих ядер, сколько возможно, но при этом они не могут задействовать те ядра, которые им не назначены.
Результатом такого шага является тот факт, что задержки в 99 перцентиле оказываются гораздо стабильнее, чем раньше. Вот пример троттлинга на продакшн-кластере баз данных (каждая линия — это контейнер), и того, что с ним происходит при включении cpusets. Как и ожидалось — троттлинг после этого исчезает.
Троттлинг исчезает из-за того, что контейнеры получают возможностью свободно использовать все выделенные им ядра. А ещё интереснее то, что задержки в 99 перцентиле тоже улучшаются, так как контейнеры получают возможность обрабатывать запросы с постоянной скоростью. В данном случае, благодаря избавлению от сильного троттлинга, 99 перцентиль задержек упал примерно на 50%.
Тут стоит отметить то, что у применения cpusets есть и негативные эффекты. В частности, это вызывает небольшое увеличение задержек в 50 перцентиле, так как процессы больше не могут пользоваться ядрами, которые им не назначены. В результате оказывается, что задержки в 50 и 99 перцентилях сближаются, что, как правило, считается позитивным явлением. Подробнее об этом мы поговорим ниже.
Назначение процессоров контейнерам
Для того чтобы использовать cpusets, контейнеры необходимо привязать к процессорным ядрам. Для правильного назначения ядер контейнерам нужны некоторые знания в сфере архитектур современных процессоров. Если сделать это неправильно — может серьёзно пострадать производительность системы.
Типичные процессорные подсистемы современных компьютеров устроены так:
Физическая машина может иметь несколько процессорных сокетов.
У каждого сокета имеется независимый L3-кеш.
У каждого процессора имеется несколько ядер.
Каждое ядро обладает независимыми L1/L2-кешами.
Каждое ядро может иметь гиперпотоки.
Гиперпотоки часто рассматривают как самостоятельные ядра, но выделение 2 гиперпотоков вместо 1 может привести к увеличению производительности лишь в 1,3 раза.
Всё это значит, что правильный выбор ядер — это по-настоящему важно. Последним препятствием на пути подбора подходящих ядер является тот факт, что нумерация ядер непоследовательна и иногда даже недетерминирована. Например, вот как может выглядеть схема процессорной подсистемы некоего компьютера.
В данном случае запуск контейнера запланирован на разных процессорных сокетах и на разных ядрах. Это ведёт к ухудшению производительности. Мы сталкивались с тем, что неправильное распределение контейнеров по сокетам ухудшало, вплоть до 500%, задержки в 99 перцентиле. Для того чтобы не допускать таких ошибок, планировщик должен взять из ядра точные сведения о топологии аппаратного обеспечения и воспользоваться этими данными при распределении ядер между контейнерами. Исходная информация такого рода находится в /proc/cpuinfo
.
Используя эту информацию, мы можем назначать контейнерам ядра, физически близкие друг к другу.
Недостатки и ограничения cpusets
Хотя cpusets решает проблему длинного хвоста распределения задержек, у этого механизма есть некоторые ограничения, при его использовании приходится идти на кое-какие компромиссы.
Не допускается выделение процессу части ядра. Это — не проблема в случае с процессами баз данных, так как обычно они достаточно велики, а значит — округление количества нужных им ядер в большую или меньшую сторону вполне допустимо. Но это означает, что количество контейнеров, работающих на сервере, не может превышать количества ядер. Это требование проблематично соблюсти в случае с некоторыми рабочими нагрузками.
Процессы системного уровня способны отнимать процессорное время у процессов, которым назначены конкретные ядра. Например, сервисы, работающие на хост-системе через systemd, рабочие процессы ядра, да и прочие подобные сущности должны, несмотря ни на что, где-то выполняться. Их, в теории, можно назначать ограниченному набору ядер. Но это может оказаться непростым делом из-за того, что необходимое им процессорное время пропорционально нагрузке на систему. Один из способов обхода этой проблемы заключается в применении к подмножеству контейнеров планирования процессов в реальном времени. Мы подумываем о том, чтобы написать отдельный материал об этом.
Необходимость «дефрагментации». Со временем доступные последовательности ядер оказываются «фрагментированными». То есть — для того чтобы процессы получали бы в своё распоряжение непрерывные последовательности ядер, приходится перемещать процессы между ядрами. Делать это можно в ходе выполнения кода, но перенос программы с одного сокета на другой означает, что ему внезапно придётся работать не с локальной, а с удалённой памятью. С этим можно бороться, но это, опять же, тема для отдельного материала, посвящённого NUMA.
Отсутствие возможности использования ресурсов, которые не назначены процессу. Иногда может быть желательным использование ресурсов хоста, не назначенных контейнеру. Делается это ради ускорения работы контейнера. Мы говорили о применении cpusets для создания конфигураций, в которых контейнеры жёстко привязываются к ядрам. Но одно и то же ядро можно назначить нескольким контейнерам (речь идёт о cgroups), ещё можно комбинировать механизмы cpusets и квоты. Это позволяет процессам свободнее обходиться с ресурсами, но это тоже тема для другого материала.
Итоги
Переход на cpusets для рабочих нагрузок, хранящих состояние, стал для Uber значительным шагом вперёд на пути улучшения своих информационных систем. Это позволило нам добиться гораздо более стабильных задержек на уровне баз данных. Мы смогли освободить около 11% процессорных ядер — тех, которые раньше выделяли с запасом, рассчитывая на обработку пиков трафика в условиях троттлинга. Так как применение cpusets не даёт процессам использовать ядра, которые им не назначены, контейнеры одного и того же размера теперь ведут себя на разных хостах одинаково, что, опять же, ведёт к большему единообразию наших систем в плане производительности.
Платформа для развёртывания рабочих нагрузок, хранящих состояние, является внутренней разработкой Uber, но cpusets, посредством политики static, поддерживает и Kubernetes.
Здесь можно найти описание того, как мы тестировали системы, в которых применялись квоты процессорных ресурсов и cpusets.
О, а приходите к нам работать? ????
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
syusifov
"Речь идёт о 65000 таких хостов, о 2,4 миллионах процессорных ядер и о 200000 контейнеров" - намек на офигенный архитектурный бардак с последующей офигенной катастрофой.
harios
А сегодня, дети, мы создадим сами себе проблемы и будем героически их превозмогать!(с)
CrzyDocTI
в среднем по 12 ядер на контейнер - бардак уже есть
stitrace
Что необычного в том, что контейнер, скажем с БД, займёт 12 ядер?
CrzyDocTI
БД в контейнере уже странно. А основный посыл был в том что 12 это среднее - значит основная масса скорее всего занимает меньше, а часть значительно больше. То что занимает значительно больше - зачем опять же в контейнерах?
stitrace
БД в контейнере странно? Думаю, уже не актуальный стереотип. Bitnami считает так же, но судить не возьмусь, это мое мнение и я не вижу причин почему бы и нет. Значительно больше может занимать опять же БД.
stitrace
Высоконагруженный веб сервер, например. Да сотни их, примеров. Я не понимаю, почему это должно быть ошибкой архитектуры.