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

Как устроен собственный сервер карт на OpenStreetMap

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

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

В двух частях я разберу, как поднять собственный тайловый сервер карт, и изложу, через что мне пришлось пройти на реальном внедрении. Для начала расскажу о стеке, при помощи которого всё это реализуется, какой компонент за что отвечает и как всё работает вместе. Во второй части поделюсь своим продакшен-кейсом и уделю внимание тонкой настройке, оптимизациям и тому, что может сломаться, когда вместо небольшого региона на сервер загружается вся планета.

К статье прилагается деморепозиторий — минимальная рабочая сборка на одном регионе, которая поднимается в Docker-контейнере за 15 минут одной командой make setup. На ней можно потрогать руками всё, о чём пойдет речь в самой статье.

Как устроен собственный сервер карт на OpenStreetMap

Зачем вообще свой сервер

Я программист, и карты обычно не моя зона ответственности. Но однажды пришел запрос: нужен картографический сервис для внутреннего проекта. Почему не Google Maps и не Яндекс Карты? Причины такие:

  • Внутренний контур. Требование внутреннего заказчика. Никаких внешних API-ключей и запросов на чужие сервера — данные и рендеринг целиком на наших серверах.
  • Контроль над данными. Можно взять дамп нужной давности, убрать лишнее (например, административные границы, которые в нашем случае только мешали) и оставить ровно то, что нужно проекту.
  • Контроль над отображением. Цвета, шрифты, какие объекты показывать и с какого зума.

То есть нужен был не «еще один провайдер карт», а платформа, которую можно править под себя. Сервис поднимался в рамках более широкого проекта исторической направленности, поэтому требования были специфичными для его нужд. Выбор пал на стандартный для этой задачи стек: OpenStreetMap + PostGIS + renderd + osm-carto.

Почему не векторные тайлы?

Если вы читаете это в 2026 году, то закономерный вопрос: зачем вообще поднимать классический стек OpenStreetMap + PostGIS + Mapnik + renderd, если существуют векторные тайлы и инструменты вроде Planetiler или Tilemaker?

Сегодня это действительно популярный подход. Планета один раз преобразуется в набор векторных тайлов (обычно MBTiles или PMTiles), после чего их можно раздавать как статические файлы через обычный nginx, а отображение и стилизация происходят уже на клиенте через MapLibre GL. Такой вариант проще масштабируется, требует меньше серверной логики и нередко обходится без PostgreSQL и рендера тайлов на лету.

Однако мой проект стартовал в начале 2024 года. На тот момент я ориентировался прежде всего на классический стек OpenStreetMap, потому что я не сталкивался серьезно с картографией и разбирался по ходу дела, а по этому стеку было достаточно документации и статей про проблемы, которые я решал. Кроме того, одной из задач была кастомизация стандартного стиля osm-carto: изменение подписей, скрытие отдельных объектов и получение карты, максимально близкой по внешнему виду к привычному рендеру OpenStreetMap.

Именно поэтому выбор был сделан в пользу традиционной схемы с PostGIS, Mapnik и renderd. Если бы я запускал аналогичный проект сегодня с нуля, то обязательно рассмотрел бы вариант с векторными тайлами как одну из основных альтернатив.

Архитектура

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

Подготовка:

 

   PBF-дамп ──► osm2pgsql ──► PostgreSQL + PostGIS

                                              │

                                     дамп выбранной территории 

                                        + внешние данные 

                                     (береговые линии, вода)

Отдача:

   браузер ──► (опционально) nginx — можно раздавать другие разделы сайта

                               │            и созданные ранее PNG

                               │

                             Apache + mod_tile

                                          │

                                       renderd

                                          │

                                        Mapnik ──► PNG-тайл

                                          │

                                  читает геоданные из PostGIS

Данные из дампа один раз импортируются в базу. Дальше браузер запрашивает тайл, Apache с модулем mod_tile передает запрос демону renderd, тот вызывает движок Mapnik. Mapnik берет геоданные из PostGIS, накладывает правила стиля и рисует картинку. Готовый тайл кешируется на диск и при повторных запросах отдается уже из кеша.

Правила стиля Mapnik читает из отдельного файла style.xml (он компилируется из набора osm-carto). На схеме я его не рисую: это не звено потока данных, а конфигурация отрисовки, которую Mapnik подгружает сам.

Глоссарий

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

OSM (OpenStreetMap) — открытая картографическая база, которую редактирует сообщество. Это не сервис и не приложение, а именно данные: координаты дорог, зданий, рек, их названия и теги. Любой может скачать их целиком или по регионам и делать с ними что хочет.

PBF (Protocolbuffer Binary Format) — формат файла с данными OSM. Это бинарный формат на основе Google Protocol Buffers: компактный и быстрый в разборе, поэтому им и раздают дампы. Вся планета (planet.osm.pbf) весит ≈70 ГБ, отдельные страны и регионы берут с ресурса Geofabrik, дампы там обновляются ежедневно.

Тайлы — карта в браузере грузится не одной большой картинкой. Она нарезана на квадратики (обычно 256 × 256 пикселей), каждый адресуется тремя числами: /{zoom}/{x}/{y}.png. Зум 0 — вся планета в одном тайле, зум 14 — это отдельные улицы, миллионы тайлов. Браузер запрашивает только видимую/запрашиваемую область — это хорошо видно во вкладке Network в браузере. Тем же приемом «нарезать на куски и отдавать по запросу» раздают и документы большого разрешения, спутниковые снимки, чтобы не отправлять весь объем сразу, к тому же без запроса пользователя.

osm2pgsql — утилита импорта PBF в PostgreSQL. Раскладывает данные по таблицам planet_osm_point, planet_osm_line, planet_osm_polygon, planet_osm_roads.

PostGIS — расширение PostgreSQL для геоданных. Добавляет типы geometry/geography, функции вроде ST_Intersects, пространственные индексы GIST. Без него координаты в Postgres хранить можно, а быстро искать по ним — нет.

hstore — расширение PostgreSQL для хранения пар «ключ-значение» в одном поле, по сути, key-value словарь в колонке. OSM активно использует теги (name:ru, amenity, highway), и hstore держит их без сотни отдельных колонок. В запросах: tags->'name:ru'. От JSONB отличается тем, что проще и быстрее на плоских парах, тогда как JSONB гибче и поддерживает вложенность; для тегов OSM hstore более чем достаточно.

Mapnik — движок, который по геоданным и правилам стиля рисует PNG. Напрямую с ним обычно не работают, он живет внутри renderd.

renderd — демон (фоновая служба) без интерактивного управления, между HTTP-запросами и Mapnik. Принимает запрос на тайл, проверяет кеш, при необходимости просит Mapnik нарисовать и сохраняет результат. Управляется через Unix-сокет.

mod_tile — модуль Apache. Принимает HTTP-запросы вида /osm/15/17571/11355.png и передает их renderd. Без него renderd снаружи не виден.

openstreetmap-carto (osm-carto) — стандартный набор правил отрисовки: цвета дорог, с какого зума показывать подписи, как выглядят парки и вода. Хранится в формате CartoCSS (.mml + .mss) и компилируется утилитой carto в style.xml для Mapnik.

Leaflet — JavaScript-библиотека, которая показывает тайлы в браузере как интерактивную карту, обрабатывает жесты и клики. С альтернативами (OpenLayers, MapLibre GL) я не работал; для большинства задач Leaflet более чем достаточно.

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

Как это собирается

1. Скачивание данных

Дамп берем с Geofabrik (страны и регионы) или planet.openstreetmap.org (вся планета). Для одного региона дамп удобно еще и обрезать по bounding box утилитой osmium (в образе он уже установлен), чтобы не тащить лишнее:


sudo apt install osmium-tool # если необходимо установить
# bbox: left,bottom,right,top (minlon,minlat,maxlon,maxlat)
osmium extract -b 157.5,52.0,160.0,54.0 far-eastern.osm.pbf -o region.osm.pbf

 

Если, наоборот, хотите объединить несколько регионов, их можно склеить тем же osmium:


osmium merge norway-latest.osm.pbf sweden-latest.osm.pbf finland-latest.osm.pbf 
  -o scandinavia.pbf

 

Альтернатива — osmconvert:


sudo apt install osmctools # если необходимо установить
osmconvert <(osmconvert norway-latest.osm.pbf  --out-o5m) 
           <(osmconvert sweden-latest.osm.pbf  --out-o5m) 
           <(osmconvert finland-latest.osm.pbf --out-o5m) 
           -o=scandinavia.pbf

 

2. Импорт в базу. osm2pgsql


osm2pgsql --slim -d gis --hstore --multi-geometry 
  --tag-transform-script openstreetmap-carto.lua 
  --style openstreetmap-carto.style 
  region.osm.pbf

 

Что значат флаги:

  • --slim — хранить промежуточные данные в БД, а не в памяти (нужно для последующих обновлений и для больших дампов, которые в память не влезут). Без этого ключа импорт быстрее, но требует больше ОЗУ.
  • --hstore — добавить колонку tags типа hstore со всеми тегами (см. глоссарий).
  • --multi-geometry — собирать мультиполигоны в один объект, иначе геометрия рисуется неправильно.

3. Обогащение внешними данными

Шаг, который в большинстве руководств упоминают вскользь, хотя без него рендер невозможен. После импорта нужно догрузить данные, которых нет в PBF, но без которых карта рисуется неправильно: береговые линии, моря, водные полигоны. Их тянет скрипт из osm-carto:


cd openstreetmap-carto/
scripts/get-external-data.py            # создает таблицы water_polygons и др.
carto project.mml > style.xml           # генерируем стиль для Mapnik
psql -d gis -c "GRANT ALL ON ALL TABLES IN SCHEMA public TO osm;"

 

После того как появились новые таблицы, права на них надо выдать заново, иначе renderd не сможет их читать. Если пропустить весь этот шаг, то рендер падает на отсутствующих таблицах, а ошибка неочевидная: osm2pgsql отработал успешно, и кажется, что всё готово.

4. Стиль: убираем лишнее, меняем язык подписей

Вся настройка отображения происходит в project.mml (внутренний формат CartoCSS, который затем переводится в style.xml). Здесь же делается кастомизация под проект.

Первой моей задачей было убрать административные границы. Находим в запросах слои с boundary = 'administrative' и дописываем заведомо ложное условие, чтобы слой возвращал пустой результат:


-- было:
WHERE boundary = 'administrative' AND admin_level IN ('2', '4')
-- стало:
WHERE boundary = 'administrative' AND admin_level IN ('2', '4') AND 1 > 2

 

Это хак, а не решение, но он позволяет быстро получить результат, не разбираясь во внутренней логике слоев. В приложенном демо та же доработка реализована через представления PostgreSQL для EN-слоя — более правильный подход. Хак можете попробовать командой make style-no-boundaries.

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

COALESCE(tags->'name:ru', name, '') AS name

После правок стиль пересобирается: carto project.mml > style.xml.

5. Настройка renderd и Apache

renderd настраивается в конфиге /etc/renderd.conf: где расположен сокет, сколько потоков, какой стиль и куда складывать отрисованные тайлы.


[renderd]
num_threads=8
tile_dir=/mnt/tiles
 
[mapnik]
plugins_dir=/usr/lib/mapnik/3.1/input
font_dir=/usr/share/fonts/truetype
font_dir_recurse=true
 
[default]
URI=/osm/
XML=/home/osm/openstreetmap-carto/style.xml
HOST=tile.example.com
MAXZOOM=14

 

MAXZOOM=14 здесь — значение под конкретный проект. Стандартный OSM-стек рендерит до зума 20, поэтому запросы вроде /osm/15/17571/11355.png из глоссария потребуют поднять этот лимит.

В конфиге mod_tile стоит увеличить тайм-ауты: по умолчанию ModTileRequestTimeout равен 3 секундам, а ModTileMissingRequestTimeout — 10. Это рассчитано на быстрый рендер, тогда как тяжелый тайл рисуется несколько секунд.

ModTileRequestTimeout 10

ModTileMissingRequestTimeout 60

6. Шрифты

Почти гарантированно при первом рендере в журнале renderd появятся ошибки про отсутствующие шрифты и карта нарисуется без подписей. Исправляется это установкой Noto-шрифтов и обновлением их кеша:


sudo apt-get install -y fonts-noto-cjk fonts-noto-hinted fonts-noto-unhinted 
  fonts-hanazono ttf-unifont fontconfig
sudo fc-cache -fv

 

Журнал renderd (journalctl -eu renderd) — это первое место, куда нужно смотреть при любых проблемах с рендером. Отдельная неочевидная ошибка Failed to send render cmd ... Inappropriate ioctl for device почти всегда означает проблему с сокетом renderd: путь к сокету в конфиге mod_tile не совпадает с путем в renderd.conf, либо у пользователя Apache (www-data) нет прав в него писать. Сверьте пути в обоих конфигах и проверьте права на каталог сокета (обычно /run/renderd). На форумах при этой ошибке также советуют export GPG_TTY=$(tty), но к renderd эта переменная отношения не имеет: если после нее «помогло», то, скорее всего, сработал сопутствующий рестарт сервиса.

Пререндер: почему карта тормозит при первом открытии

При первом запросе тайла renderd рисует его на лету — это секунды на высоких зумах. При повторных отдает из кеша мгновенно. Именно поэтому без пререндера карта на первом открытии медленная и высокие зумы рисуются долго, часть не успевает за тайм-аут, потому что ваш запрос вызывает всю цепочку со схемы, преобразуется в сложный SQL-запрос, требующий нескольких выборок разных слоев данных из огромных таблиц с условиями на ограничение по границам тайла.

Масштаб задачи задается геометрией. Зум z — это сетка 2^z × 2^z тайлов:

Зум Тайлов
0 1 (вся планета)
1 4
2 16
6 16 384
14 ≈268 млн

Уровней зума в OSM двадцать один — от 0 до 20. Если отрендерить самый высокий, двадцатый, то только на нём вы получите 1 099 511 627 776 (триллион с лишним) тайлов! Именно поэтому низкие зумы пререндерят целиком и быстро, а высокие — только для нужной области либо по запросу, потому что здесь уже нужно выбирать между скоростью и ценой хранения, обслуживания. И так как вероятность, что конкретный пользователь запросит именно пререндеренный тайл из этого триллиона, мала, то дешевле заставить его немного подождать, чем хранить огромное число ненужных тайлов и периодически обновлять их. Используемая команда render_list:

render_list --all -n 4 -t /mnt/tiles -s /run/renderd/renderd.sock -z 0 -Z 6

Флаги: 

  • --all — рендерить весь заданный диапазон (без него render_list ждет список тайлов из stdin);
  •  -z/-Z — нижний и верхний зум;
  •  -n — число потоков;
  •  -t — каталог тайлов;
  •  -s — путь к сокету renderd.

Часто еще ставят -f (force) — перерисовать, даже если тайл уже есть.

Зачем нужен «загрузчик тайлов»

renderd хранит тайлы в своем формате — метатайлы (.meta, по 8 × 8 тайлов в файле плюс метаданные), потому что это эффективнее миллионов мелких файлов на диске. Во-первых, обычно запрашиваются соседние тайлы и оптимальнее прочитать их с диска группой, чем обращаться к каждому по отдельности. Во-вторых, намного снижается количество файлов, что важно для файловой системы: число индексных дескрипторов (inode) на томе, конечно, и задается при его создании, а тайлов на высоких зумах миллиарды. Подробнее об этом лимите и его контроле расскажу во второй части.

Наружу Apache с mod_tile отдает метатайлы уже как обычные PNG, и браузер про .meta ничего не знает. Для обычной отдачи ничего больше не нужно.

Отдельный загрузчик (скрипт, который проходит по тайлам и сохраняет PNG) нужен только в одном случае: если вы хотите выгрузить тайлы как статические файлы и раздавать их через nginx или из S3, вообще без renderd. Тогда .meta превращают в дерево {z}/{x}/{y}.png.

Для этого есть готовые конвертеры (например, meta2tile), которые разбирают .meta прямо с диска, и это намного быстрее. Но для наглядности покажу примитивный скрипт: он проходит по диапазону тайлов и качает каждый по HTTP с работающего tile-сервера (renderd при этом отдает их как обычные PNG):


#!/bin/bash
# loader.sh <zoom> <xmin> <xmax> <ymin> <ymax>
zoom=$1; xmin=$2; xmax=$3; ymin=$4; ymax=$5
for x in $(seq $xmin $xmax); do
  mkdir -p "$zoom/$x"
  for y in $(seq $ymin $ymax); do
    wget -qO "$zoom/$x/$y.png" "http://tile.example.com/osm/$zoom/$x/$y.png"
  done
done

 

Запускают его по зумам, передавая на каждом свой диапазон x/y (напомню: на зуме z — это сетка 2^z × 2^z):


bash loader.sh 0 0  0  0  0      # зум 0: один тайл
bash loader.sh 6 0 63  0 63      # зум 6: сетка 64×64

 

На выходе получается обычное дерево {z}/{x}/{y}.png, совместимое с Leaflet и любым статическим веб-сервером.

Демо: всё в миниатюре

Чтобы не разворачивать всё на хост-сервере, демо собрано на готовом образе overv/openstreetmap-tile-server (весь OSM-стек в одном контейнере) и позволяет удобно и изолированно от внешнего окружения запустить проект, не засоряя основную операционную систему данными от экспериментов. Один регион — Камчатка — скачивается, нарезается, рендерится и раздается после выполнения одной команды make setup минут за 10–15 на ноутбуке. Там же пререндер, переключатель языка карты и несколько замеров производительности, которые будут подробнее рассматриваться во второй части.

Один вывод оттуда стоит держать в голове с самого начала: размер базы определяется регионом, а не зумом рендера. Ограничение зума экономит место только под метатайлы/тайлы, а база фиксируется на импорте. Причем значительную часть занимает не ваш регион, а глобальные береговые линии и водные полигоны: их get-external-data тянет одинаково хоть для города, хоть для планеты. Например, Камчатка всего 8 МБ дампа, но ≈3 ГБ базы, и почти всё это вода.

Выводы

Собрав такой сервер руками, понимаешь главное: OSM-стек — это не монолит, а набор слабосвязанных инструментов, наросших исторически. osm2pgsql, Mapnik, CartoCSS, Lua- и Python-скрипты, PostgreSQL, модуль Apache, Unix-сокеты, шелл-обвязка — каждый делает свое, общается с соседом через файл или сокет, и собрать их вместе — только половина работы. Зато когда всё собрано, то это полностью ваш сервер: свои данные, свой стиль, никаких внешних зависимостей.

На одном регионе всё поднимается за день (если не считать мелких граблей со шрифтами, правами и тайм-аутами, на которые вы наверняка тоже наступите), если знать, что делать. А вот когда «регион» — это вся планета, то начинается совсем другая история: пятисуточные импорты, OOM killer, база данных на 1,7 ТБ и рендер, который приходится гонять чанками неделями. Об этом расскажу во второй части статьи.

Ссылки

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