47
0
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

OSM на планете целиком: железо, импорт и почему это занимает недели

Время чтения 13 минут
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
47
0
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

Привет! Меня зовут Дмитрий Стариков, я ведущий веб-разработчик с опытом более 10 лет. Участвовал в создании электронных архивов для Национальной электронной библиотеки, Архивно-библиотечного фонда ВДНХ, порталов «Память народа» и «Росархив». Сейчас развиваю платформу закупок по №223-ФЗ и интеграционные сервисы в одном из крупнейших федеральных операторов электронных торгов.

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

OSM на планете целиком: железо, импорт и почему это занимает недели

Железо: сколько на самом деле нужно

Изначально мне выдали машину: 4 ядра, 16 ГБ ОЗУ, 500 ГБ диска. Методом тыка быстро выяснилось, что это катастрофически мало — планета на таком даже не импортировалась. Я попросил больше, накинули немного. Со второй попытки у меня было 32 ГБ ОЗУ и 500 + 2000 ГБ диска. На этом я и проработал: медленно, но планета в итоге загрузилась и работала.

База планеты после импорта и всех индексов весила около 1728 ГБ на момент начала 2024 года (OSM растет, сейчас больше), и это без учета тайлов. Диск надо брать с запасом: при импорте он дополнительно расходуется под временные данные.

Отдельная засада с диском. Заодно развею миф, который часто гуляет по таким статьям: Postgres ничего не преаллоцирует и спокойно использует выросшую файловую систему. У меня проблема была проще: база лежала на отдельном смонтированном томе, который при добавлении диска я просто не расширил, и месту неоткуда было взяться. Переформатировать ничего не пришлось, но базу потребовалось вручную перенести на бо́льший том:


# черновая копия, база еще работает
sudo rsync -av /var/lib/postgresql /mnt/tiles/db
sudo systemctl stop postgresql
# докопировать изменившееся
sudo rsync -av /var/lib/postgresql /mnt/tiles/db
sudo mv /var/lib/postgresql/14/main /var/lib/postgresql/14/main.bak
# дальше data_directory в postgresql.conf указывает на новый путь

 

Настройка PostgreSQL под нагрузку

Дефолтный конфиг Postgres под такую нагрузку не годится совсем. Большинство параметров масштабируются от объема ОЗУ, поэтому удобнее держать в голове правила, а не абсолютные числа:

Рабочие значения для сервера с 32 ГБ:

shared_buffers = 8GB

work_mem = 256MB

maintenance_work_mem = 4GB

effective_cache_size = 20GB

max_wal_size = 10GB

hash_mem_multiplier = 4.0 # множитель work_mem для хеш-операций

hash_mem_multiplier я добавил позже, на этапе оптимизации рендера: геозапросы активно используют хеш-джойны. Но тут же признаю́ свою ошибку. В первой версии у меня было work_mem = 1GB при hash_mem_multiplier = 8.0. Важная оговорка: work_mem выделяется на каждую сортировку или хеш в каждом запросе, а hash_mem_multiplier его умножает — с моими значениями одна хеш-операция могла занять до 8 ГБ. На 32 ГБ при восьми потоках renderd — это на грани OOM, и зависания рендера на высоких зумах, о которых пойдет речь ниже, скорее всего, с этим связаны. Если память поджимает, то безопаснее держать work_mem в районе 256–512 МБ, а множитель — в пределах 2.0–4.0.

Импорт планеты

Дамп планеты (≈70 ГБ на момент моих работ, сейчас заметно больше) скачиваем с докачкой, чтобы пережить обрыв связи:
wget -c https://planet.openstreetmap.org/pbf/planet-latest.osm.pbf

Команда импорта в Postgres:


osm2pgsql --slim -d gis --hstore --multi-geometry 
  --number-processes 1 
  --tag-transform-script openstreetmap-carto.lua 
  --style openstreetmap-carto.style 
  -C 14000 
  planet-latest.osm.pbf

 

Чего в этой команде не хватает и что я узнал уже после: --flat-nodes /path/nodes.cache. Флаг выносит кеш узлов из базы в отдельный файл (около 90 ГБ для планеты), за счет чего slim-таблицы худеют на сотни гигабайт, а импорт заметно ускоряется. Для планеты он считается практически обязательным, для небольших регионов, наоборот, не нужен. Часть моих пяти суток импорта и 1,7 ТБ базы — это расплата за его отсутствие.

Почему --number-processes 1? Потому что с двумя и четырьмя процессами импорт падал с segmentation fault. Точную причину я так и не установил: времени докапываться не было, в один поток заработало — на этом остановился. Ниже не вердикт, а гипотезы по убыванию правдоподобия:

  • OOM Killer убивает дочерний процесс. Linux при нехватке памяти выбирает самый «жирный» процесс и убивает его. Если он валит воркер osm2pgsql, родитель получает неожиданное завершение дочернего и падает. По контексту (памяти хронически не хватало) ставлю на это.
  • Конфликт в shared memory. В multi-process режиме воркеры координируются через общую память, и при нехватке ОЗУ или кривом shmmax это может повреждать данные и давать segfault.
  • Баг конкретной версии osm2pgsql на параллельном импорте с активными hstore и multi-geometry разом. Лечится проверкой changelog и обновлением.

Если будете разбираться: dmesg -T | grep -i "killed process" сразу после падения и coredumpctl для трейса дадут однозначный ответ. Кроме того, у меня были жесткие бюджетные ограничения, а на современном железе с 64+ ГБ ОЗУ параллельный импорт может сработать и без описанной проблемы.

Если упираетесь в память на импорте, помогает снизить -C (размер кеша osm2pgsql в МБ) и shared_buffers в конфиге Postgres: меньше давление на RAM, импорт идет медленнее, но стабильнее.

В один поток планета импортировалась примерно за пять суток (основное хранилище — HDD, на SSD будет быстрее). Почти всё это время держит сам osm2pgsql, разбирая 70 ГБ дампа и собирая геометрию.

Индексы: ускорение в 150 раз

Самый большой прирост дали индексы. До них рендер тайла на зуме 8–9 мог занимать минуты: Mapnik слал запрос в PostGIS, тот делал последовательное сканирование (seq scan), то есть перебирал все строки таблицы. Для planet_osm_polygon весом в сотни гигабайт это катастрофа.

Геоиндексы GIST ищут объекты в пространстве по дереву, а не перебором. А partial indexes с условием берут в индекс только нужные рендеру строки, поэтому он меньше и быстрее:


-- реки
CREATE INDEX planet_osm_line_river ON planet_osm_line
  USING GIST (way) WHERE waterway = 'river';
 
-- населенные пункты с названиями (для подписей)
CREATE INDEX planet_osm_point_place ON planet_osm_point
  USING GIST (way) WHERE place IS NOT NULL AND name IS NOT NULL;
 
-- крупные полигоны для низких зумов
CREATE INDEX planet_osm_polygon_way_area_z6 ON planet_osm_polygon
  USING GIST (way) WHERE way_area > 5980000;
-- ... всего около 15 индексов

 

Важная деталь, которую я понял не сразу: эти индексы — это не ручная магия, которую нужно продумывать самому, а готовый файл indexes.sql в osm-carto. На голом сервере его запускают руками после импорта (psql -d gis -f indexes.sql), и шаг легко пропустить: после osm2pgsql неочевидно, что нужно делать что-то еще, и карта «вроде работает», просто рисует тайл минутами. Мне это знание пришло далеко не сразу.

После всех индексов база выросла с ≈1728 до ≈1770 ГБ, а рендер ускорился примерно в 150 раз (не один запрос, а именно рендер тайла целиком, требующий десятки запросов к разным слоям) — разница между «тайл рисуется почти три минуты» и «тайл рисуется секунду». Замеры не претендуют на строгий бенчмарк: сравнивался рендер одного и того же набора тайлов до и после создания индексов (после очистки кеша, разумеется).

Пререндер высоких зумов

Низкие зумы пререндерятся за минуты, на высоких счет идет на дни. Вот реальные суммарные времена рендера планеты — делите на число потоков, чтобы получить реальное время:

Зум  

Суммарное время

0–3 Секунды
4 ≈596 с
5 ≈2183 с
6 ≈9109 с
7 ≈68 325 с
8 ≈273 380 с (около 76 ч суммарно, ≈9,5 ч на 8 потоках)

На высоких зумах рендер регулярно зависал — скорее всего, из-за нехватки памяти на отдельных тяжелых запросах: восемь потоков renderd, в каждом запросе несколько джойнов, и щедрые work_mem с hash_mem_multiplier из раздела про настройку Postgres в сумме легко выедали всю ОЗУ. На тот момент я выбрал стратегию рендера чанками со смещением по оси X (то есть создается весь набор метатайлов по вертикали, но по горизонтали он ограничен), просматривая после каждого чанка случайные тайлы в браузере:

render_list --all -f -x 0   -X 100 -n 8 -t /mnt/tiles -s /run/renderd/renderd.sock -z 9 -Z 9
render_list --all -f -x 100 -X 200 -n 8 -t /mnt/tiles -s /run/renderd/renderd.sock -z 9 -Z 9
# ... удобно обернуть в цикл на bash

 

Завис — стоп, рестарт сервисов, продолжаем с того чанка, где встали. Кончился диск посреди ночи — чистка, запрос на расширение тома, продолжаем. Это нормальный рабочий цикл, а не форс-мажор, хотя порой от него опускались руки. И именно он, а не отдельная команда, и есть настоящая стоимость такого проекта. Команды удобно автоматизировать при помощи bash-скриптов или makefile, потому что это то, что приходится делать постоянно.

Файловая система: inode кончаются раньше места

В первой части я упоминал, что renderd хранит тайлы метатайлами по 8 × 8 в том числе ради файловой системы. Здесь стоит раскрыть, почему это важно. 

У ext4 число inode (индексных дескрипторов, по одному на каждый файл) фиксируется при создании файловой системы и потом не меняется. По умолчанию mkfs.ext4 закладывает один inode на каждые 16 КБ объема — на томе в 2 ТБ это около 134 млн inode. 

Звучит внушительно, но один только зум 16 — это больше 4 млрд тайлов: без метатайлов, сокращающих число файлов в 64 раза, inode на планете кончились бы задолго до места на диске. 

Следить за этим просто: df -i показывает занятость inode по томам так же, как df -h показывает место (ключи можно совмещать: df -ih выведет то же в человеко-читаемом формате). Если создаете том под тайлы с нуля, запас можно заложить заранее: mkfs.ext4 -i 4096 даст вчетверо больше inode, чем дефолт, ценой небольшой потери полезного объема. А в XFS inode выделяются динамически, и этой проблемы там нет вовсе.

Эксплуатация: куда копать дальше

Я разобрал, как собрать сервер и как его подготовить к продакшен-использованию. Есть еще отдельные большие темы, вот короткие отправные точки для изучения:

  • Обновление данных и expiry. Я не использовал обновления из-за исторического контекста проекта — нам не были важны современные данные и их актуализация. Если же данные меняются, диффы на базу накатывает osm2pgsql-replication, но уже нарендеренные тайлы на диске сами не обновятся — карта будет постепенно устаревать. Решает это expiry: osm2pgsql --expire-tiles 12-14 при диффе выдает список координат затронутых тайлов, а скрипт помечает их (удаляет метатайлы), и renderd перерисовывает при следующем запросе уже не всю планету (если не запустили с ключом force), а только изменившиеся квадратики. Для статичных карт это неактуально, для динамичного сервиса — обязательно.
  • Безопасность. Дефолтная установка Postgres из пакетов слушает только локальные подключения, но во многих туториалах и Docker-образах в pg_hba.conf прописано host all all 0.0.0.0/0, то есть база открыта всем. Замените на конкретную подсеть (например, 192.168.0.0/16), или спрячьте за SSH-туннель, или же проксируйте на определенный Docker-контейнер, а доступ к самим тайлам закрывайте аутентификацией на уровне Apache или nginx перед ним.
  • Мониторинг. Минимум, за чем стоит смотреть: df -h (диск во время пререндера кончается первым), df -i (inode), htop/free -h (память), журнал renderd (journalctl -u renderd) и статистика mod_tile — модуль умеет отдавать счетчики запросов и рендера на служебном URL.

Что в итоге

Стек OSM + PostGIS + renderd работает, но требует терпения, ресурсов и понимания связей между сервисами. Если коротко:

  • Три обязательных вещи: индексы, тюнинг Postgres и внешние данные. Каждая по отдельности кажется необязательной, но без них рендер либо падает, либо рисует тайл минутами и отваливается по тайм-ауту.
  • Главный неочевидный расход места — глобальные водные полигоны, а не импортируемый регион.
  • Закладывайте время на нелинейность. Падения, рестарты, кончившийся ночью диск, перенос тома — это не форс-мажор, а реальность процесса, и именно на нее уходит больше всего сил.

И главный урок — не про технологию. Большая часть проблем, которые я описал, оказалась связана не с OSM как таковым, а с попыткой вместить планетарный набор данных в ограниченные ресурсы. Позже на сервере вдвое увеличили память: работать стало значительно приятнее, но я передал проект преемнику, поэтому могу сказать только то, что 64 ГБ — это нормальный минимум памяти, если задумали такой проект. Именно поэтому главный совет прост: если собираетесь работать с планетой целиком, сначала заложите достаточный запас по памяти и диску, а уже потом начинайте оптимизировать.

Пощупать тюнинг и индексы на маленьком регионе можно в демо — make index-benchmark и make render-benchmark показывают эффект GIST-индекса и разницу холодного и теплого тайла.

Ссылки

Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники