Зачем вообще свой сервер
Я программист, и карты обычно не моя зона ответственности. Но однажды пришел запрос: нужен картографический сервис для внутреннего проекта. Почему не 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 ТБ и рендер, который приходится гонять чанками неделями. Об этом расскажу во второй части статьи.
Ссылки
- switch2osm — пошаговое руководство по сборке tile-сервера
- Развертывание на Ubuntu (linuxbabe)
- Документация osm2pgsql
- Дампы регионов — Geofabrik
- Уровни зума OSM
- PBF формат