parallel-fs-ops.template.yml (2,8 КБ)
Давайте поговорим о проблеме масштабирования, которая становится болезненно очевидной, когда на сайте Discourse накапливается большая библиотека загрузок.
То, на что раньше уходили минуты для выполнения команды chown по огромному каталогу загрузок, теперь занимает секунды!
Предпосылки
При пересборке Discourse могут выполняться рекурсивные операции, такие как:
chown -R ...
chmod -R ...
Более конкретно, эта строка в templates/web.template.yml:
- chown -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp
Эти команды обходят файловую систему последовательно, по одному элементу.
Для небольшой установки это вполне разумно. Но когда shared/uploads содержит сотни тысяч — или миллионы — файлов, рекурсивные операции по смене владельца и разрешений могут доминировать в процессе развёртывания. Могут быть доступны ресурсы CPU, хранилища и сети, но один процесс обходит всё дерево по одному inode за раз.
Для сообществ с большим объёмом загрузок результатом могут стать:
- Чрезвычайно длительные пересборки
- Более длительные окна обслуживания
- Задержки в развёртывании и обновлениях безопасности
- Низкая загрузка быстрого или распределённого хранилища
- Ощущение зависания развёртывания при обработке огромного дерева файлов
- Особенно плохая производительность на NFS, JuiceFS, CephFS и других сетевых файловых системах
Раздражает то, что многие из этих файлов независимы. Их разрешения можно обрабатывать параллельно.
Решение: Шаблон параллельных операций с файловой системой
Я создал шаблон pups, который прозрачно заменяет рекурсивные операции chmod и chown параллельными конвейерами find и xargs.
Обёртки объявляют о себе каждый раз, когда перехватывают рекурсивную операцию:
echo "[parallel-fs-ops] chmod -R override active: $*" >&2
и:
echo "[parallel-fs-ops] chown -R override active: $*" >&2
Этот префикс в квадратных скобках облегчает обнаружение оптимизации в журнале развёртывания.
Как выглядит развёртывание
В начале развёртывания шаблон подтверждает, какие бинарные файлы будут обрабатывать последующие операции с файловой системой:
[parallel-fs-ops] chmod -> /usr/local/bin/chmod
[parallel-fs-ops] chown -> /usr/local/bin/chown
Когда более поздний шаблон выполняет рекурсивное изменение разрешений, в выводе развёртывания появляется строка, похожая на:
[parallel-fs-ops] chmod -R override active: -R 0755 /var/www/discourse/public
Рекурсивное изменение владельца производит:
[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp
Для установки с большим объёмом загрузок вы можете увидеть что-то вроде:
[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp
Точные пути и аргументы зависят от используемых шаблонов, но важной частью является видимый маркер:
[parallel-fs-ops]
Без шаблона развёртывание может казаться зависшим на долгое время во время рекурсивной операции с файловой системой. С шаблоном журнал сообщает вам, что:
- Обёртка была установлена правильно.
- Рекурсивная операция была обнаружена.
- Параллельная реализация активна.
- Обрабатываемые исходные аргументы видны.
Это особенно ценно при устранении неполадок, поскольку позволяет отличить медленное параллельное обход файла от зависшей сборки.
После завершения операции развёртывание продолжается с обычным выводом pups. Сама обёртка не печатает по одной строке на файл, поэтому даже дерево, содержащее миллионы загрузок, не затопит журнал развёртывания.
Шаблон
run:
- file:
path: /usr/local/bin/chmod
chmod: "+x"
contents: |
#!/bin/bash
if [[ "$*" =~ (^|[[:space:]])-R([[:space:]]|$) ]]; then
echo "[parallel-fs-ops] chmod -R override active: $*" >&2
args=()
for arg in "$@"; do
[[ "$arg" != "-R" ]] && args+=("$arg")
done
mode="${args[0]}"
targets=("${args[@]:1}")
[[ ${#targets[@]} -eq 0 ]] && targets=(".")
find "${targets[@]}" -print0 |
xargs -0 -n 32 -P 128 /bin/chmod "$mode"
else
exec /bin/chmod "$@"
fi
- file:
path: /usr/local/bin/chown
chmod: "+x"
contents: |
#!/bin/bash
if [[ "$*" =~ (^|[[:space:]])-R([[:space:]]|$) ]]; then
echo "[parallel-fs-ops] chown -R override active: $*" >&2
args=()
for arg in "$@"; do
[[ "$arg" != "-R" ]] && args+=("$arg")
done
owner="${args[0]}"
targets=("${args[@]:1}")
[[ ${#targets[@]} -eq 0 ]] && targets=(".")
find "${targets[@]}" -print0 |
xargs -0 -n 32 -P 128 /bin/chown "$owner"
else
exec /bin/chown "$@"
fi
- exec:
cmd: |
echo "[parallel-fs-ops] chmod -> $(command -v chmod)"
echo "[parallel-fs-ops] chown -> $(command -v chown)"
Шаблон устанавливает обёртки в /usr/local/bin, который обычно находится перед /bin в PATH.
Когда запрашивается обычная, нерекурсивная операция, обёртка напрямую передаёт управление стандартной утилите:
exec /bin/chmod "$@"
Когда присутствует -R, она удаляет флаг рекурсии, безопасно перечисляет цели с использованием нулевых разделителей и обрабатывает пакеты параллельно:
find "${targets[@]}" -print0 |
xargs -0 -n 32 -P 128 /bin/chmod "$mode"
Это также работает, когда pups вызывает команды через /bin/sh. Shebang Bash в обёртке учитывается при запуске исполняемого файла, даже если вызывающая оболочка — Dash.
Почему это важно при большом количестве загрузок
Сообщества с большим объёмом загрузок — это именно те места, где поведение развёртывания должно масштабироваться изящно.
В долго работающем форуме могут содержаться:
- Изображения, встроенные в постах за годы
- Аватары и фоны профилей
- Оригинальные и оптимизированные варианты изображений
- Вложения видео и аудио
- Документы и архивы
- Защищённые загрузки
- Медиафайлы, управляемые плагинами
- Дерево загрузок для мультисайтов
Объём кода приложения может оставаться относительно стабильным, в то время как количество объектов файловой системы, загруженных пользователями, продолжает расти. Обход файловой системы — а не компиляция или создание контейнеров — в конечном итоге может стать доминирующей стоимостью развёртывания.
Это необычная проблема масштабирования: чем успешнее и содержательнее становится сообщество, тем дороже может стать рутинная операционная работа.
Почему нужен шаблон
Изменение .bashrc или установка BASH_ENV не решают эту проблему надёжно. pups выполняет команды run через /bin/sh, а Dash не загружает конфигурацию Bash и не понимает функции, специфичные для Bash.
Шаблон предоставляет повторяемый способ установить обёртки достаточно рано, чтобы последующие рекурсивные операции — включая те, что идут из внешних шаблонов — решались через параллельную реализацию:
templates:
- "templates/postgres.template.yml"
- "templates/redis.template.yml"
- "templates/web.template.yml"
- "containers/parallel-fs-ops.template.yml"
Настраиваемые параметры
-P 128 подходит только для хранилищ, способных поддерживать такую параллельность. Это не универсальное значение по умолчанию.
В зависимости от системы разумный уровень параллельности может быть:
-P 4для более медленных локальных дисков-P 8или-P 16для хостов общего назначения-P 32для быстрого хранилища на SSD- Более высокие значения для распределённых файловых систем, которые выигрывают от параллельных операций с метаданными
Слишком большая параллельность может перегрузить файловую систему, насытить серверы метаданных или ухудшить производительность развёртывания. Поэтому размер пакета и уровень параллельности должны быть настраиваемыми.
Этот шаблон — практическое решение, но более широкое предложение звучит так:
Может ли Discourse официально поддерживать настраиваемую параллельность для крупных рекурсивных операций с файловой системой во время развёртывания?
Реализация в upstream могла бы:
- Параллелить только известные большие деревья каталогов
- Избегать ненужного обхода неизменённых деревьев загрузок
- Сделать параллельность настраиваемой
- Определять локальные и сетевые файловые системы
- Сохранять полную семантику аргументов
chmodиchown - Выводить периодический прогресс для очень больших деревьев
- Записывать время выполнения, чтобы администраторы могли выявить узкие места развёртывания
Важное предупреждение
Обёртка выше сфокусирована на рекурсивных формах команд, используемых в нашем процессе сборки. Это не полная пере реализация всех возможных комбинаций опций chmod или chown.
Её следует протестировать на точных командах, генерируемых шаблонами сайта, перед использованием в продакшене. Операторам следует начинать с консервативного уровня параллельности и измерять влияние на их хранилище.
Но основная проблема реальна: последовательные рекурсивные операции с метаданными плохо масштабируются, когда сообщество накопило огромное дерево загрузок.
Удачи, и я ценю любые комментарии или предложения (даже если, возможно, я дублировал чей-то другой труд, я буду признателен за ссылки на это тоже)!
С уважением!