Сегодня я расскажу как довольно просто поднять и настроить свой собственный сервер карт (тайловый сервер) на основе Ubuntu Server 14.04 LTS и OpenStreetMap.
Итак начнём. Из Википедии:
OpenStreetMap (дословно «открытая карта улиц»), сокращённо OSM — некоммерческий веб-картографический проект по созданию силами сообщества участников-пользователей Интернета подробной свободной и бесплатной географической карты мира.
Есть довольно подробная официальная статья об установке и настройке tile-сервера, но есть ещё более простой и быстрый способ.
Установка OpenStreetMap
Ставим
sudo apt-get install python-software-properties
Добавляем репозиторий
sudo add-apt-repository ppa:kakrueger/openstreetmap
Обновляемся
sudo apt-get update
Ставим единственный пакет, что потянет за собой всё необходимое для работы полноценного сервера карт.
sudo apt-get install libapache2-mod-tile
По ходу инсталляции (openstreetmap-postgis-db-setup) вам будет задано несколько вопросов:
This will enable the apache config for mod_tile WARNING: This will disable the currently active default site config Do you want to enable mod_tile in the apache config?
ОтвечаемYes
This script downloads the necessary coastline and boundary data. WARNING: The data is about 400 — 500Mb in size. Do you want to download coastlines?
ОтвечаемYes
Do you want these scripts to create and setup a new postgis database ready to be used with e.g. Osm2pgsql. WARNING: This will delete an existing db. Do you want to create a postgis db?
ОтвечаемYes
If you don’t use the default name, you might need to adapt programs and scripts to use the new name. Name of the database to create:
Указываемgis
Please specify which users should have access to the newly created db. You will want the user www-data for rendering and your own user name to import data into the db. The list of users is blank separated: E.g. «www-data peter». Other users that should have access to the db:
Указываемwww-data
Установлено.
Настройка PostgreSQL и Mapnik
Вначале о самой Mapnik:
Mapnik — это программа, которую мы используем для отрисовки основного Slippy Map слоя для OSM, вместе с другими слоями вроде «cycle map» и «noname». Также, это имя, данное основному слою, что вводит некоторых в заблуждение.
Mapnik — свободный инструментарий отрисовки карты. Он написан на C++ и Python. Использует библиотеку AGG и дает возможность сглаживать объекты на карте с большой точностью. Может читать данные в формате компании ESRI, PostGIS, точечные рисунки TIFF, файлы .osm, а также поддерживает любые GDAL или OGR форматы. Пакеты доступны для большинства выпусков Linux, двоичные файлы доступны для Mac OS X и Windows.
Довольно странный нюанс в том, что нам не предложили задать пароль для доступа пользователя gis (создаётся автоматически) к одноимённой БД.
Устанавливаем пароль для пользователя gis
su postgres -c "psql -d gis"
gis=# \password gis
Теперь необходимо указать пользователя и пароль в файле конфига mapnik
.
Сделаем это простым способом — заменой с помощью утилиты rpl
:
rpl \
'<Parameter name="dbname"><![CDATA[gis]]></Parameter>' \
'<Parameter name="dbname"><![CDATA[gis]]></Parameter><Parameter name="user"><![CDATA[gis]]></Parameter><Parameter name="password"><![CDATA[1234567890]]></Parameter>' \
/etc/mapnik-osm-carto-data/osm.xml
Где «1234567890» — это пароль, что мы указали для пользователя gis
Даём возможность подключаться с локального сервера, используя конфиг /etc/postgresql/9.3/main/pg_hba.conf
Добавляем в него строчку
local all all md5
Настраиваем PostgreSQL (относительно характеристик сервера: 16 ядер, 16GB RAM, БД на HDD).
/etc/postgresql/9.3/main/postgresql.conf
Показываю только то, что менял:
listen_addresses = '*'
max_connections = 200
shared_buffers = 12GB
work_mem = 2GB
maintenance_work_mem = 8GB
synchronous_commit = off # только для импорта, потом необходимо убрать!
fsync = off # только для импорта, потом необходимо убрать!
wal_buffers = 16MB
checkpoint_segments = 1024
checkpoint_completion_target = 0.9
random_page_cost = 2.0
cpu_tuple_cost = 0.001
cpu_index_tuple_cost = 0.0005
effective_cache_size = 14GB
autovacuum = off # только для импорта, потом необходимо убрать!
И перезапускаем Постгрес для применения всех настроек
service postgresql restart
Потюним немного конфиг демона renderd
рендерера тайлов (Mapnik rendering daemon) и увеличим количество потоков renderd
до количества ядер (в моём случаи — 16)
vi /etc/renderd.conf
[renderd]
num_threads=16
Теперь необходимо перезапустить демон renderd
рендерер тайлов (Mapnik rendering daemon)
service renderd restart
Настройка Apache
Сервер карт будет работать на связке Apache + PostgreSQL.
Я не в восторге от данной связки. После основных действий мы ещё доставим nginx для более быстрой работы в связке nginx + Apache + PostgreSQL.
Настраиваем Apache.
Названия сервера для примера будет osm.net.
Обратите внимание что порт я указал 8080
.
grep -v '#' /etc/apache2/sites-available/tileserver_site.conf
<VirtualHost *:8080>
ServerAdmin [email protected]
ServerName osm.net
DocumentRoot /var/www
LogLevel info
ModTileTileDir /var/lib/mod_tile
LoadTileConfigFile /etc/renderd.conf
ModTileRequestTimeout 10
ModTileMissingRequestTimeout 30
ModTileMaxLoadOld 2
ModTileMaxLoadMissing 25
ModTileRenderdSocketName /var/run/renderd/renderd.sock
ModTileCacheDurationMax 604800
ModTileCacheDurationDirty 900
ModTileCacheDurationMinimum 10800
ModTileCacheDurationMediumZoom 13 86400
ModTileCacheDurationLowZoom 9 518400
ModTileCacheLastModifiedFactor 0.20
ModTileEnableTileThrottling Off
ModTileEnableTileThrottlingXForward 0
ModTileThrottlingTiles 10000 1
ModTileThrottlingRenders 128 0.2
<Directory />
Options FollowSymLinks
AllowOverride None
</Directory>
<Directory /var/www/>
Options -Indexes -FollowSymLinks -MultiViews
AllowOverride None
Order Allow,Deny
Allow From All
</Directory>
</VirtualHost>
Правим порт в /etc/apache2/ports.conf
на 8080
Listen 8080
Дописываем в /etc/apache2/apache2.conf
название нашего сервера, т.к. Апач без этого работать не может.
echo "ServerName osm.net" >> /etc/apache2/apache2.conf
Перезапускаем.
service apache2 restart
Импорт карты
Скачиваем с проекта Geofabrik, для примера, карту Украины в формате .pbf и вливаем его в нашу БД.
mkdir /var/osm
wget -O /var/osm/ukraine.`date +%Y-%m-%d`.osm.pbf \
https://download.geofabrik.de/europe/ukraine-latest.osm.pbf
osm2pgsql -d gis -U gis -W -c --slim -C 4000 --number-processes 16 \
/var/osm/ukraine.`date +%Y-%m-%d`.osm.pbf
На виртуальной машине с 16 ядрами, 16GB ОЗУ и HDD RAID-10 массивом это занимает около 30 минут.
На скорость импорта довольно сильно влияет диск. На SSD вливается заметно быстрее.
Обратите внимание на параметр -c
(--create
) при импорте через osm2pgsql
. С этим параметром удалится вся существующая информация с таблиц. Если вам необходимо сделать импорт ещё нескольких стран, то вместо -c
указывайте параметр -a
(--append
).
После того как сделаете импорт всех необходимых стран (или всего мира) не забудьте вернуть в default из конфига PostgreSQL /etc/postgresql/9.3/main/postgresql.conf
параметры synchronous_commit
, fsync
и autovacuum
.
Проверка работоспособности карты
Текущая комплектация поставляется примером /var/www/osm/slippymap.html
Я предлагаю проверить на данном этапе правильность настройки PostgreSQL + Apache (mod_tile) + Mapnik, перед тем, как мы перейдём к настройке nginx.
Открываем в браузере https://osm.net:8080/osm/slippymap.html
Должен отобразится простой интерфейс и карта. Важно чтобы был выбран пункт в интерфейсе «Local Tiles».
Если позумить и поскроллить, то карта будет перерисовываться, постепенно подгружая разные тайлы.
Вот например, адрес тайла центра города Киева будет выгдядеть так: https://osm.net:8080/osm/11/1197/690.png
И если вы увидели картинку — значит всё сделали правильно.
Кэш отрендеренных тайлов будет складываться в папку /var/lib/mod_tile/default/
.
И тут как-раз настало время отдавать этот кэш не через медленного монстра Apache, а через быстрый nginx.
Настройка nginx
Ставим
apt-get install nginx-light
cat /etc/nginx/nginx.conf
user www-data www-data;
worker_processes 32;
worker_priority -20;
pid /var/run/nginx.pid;
error_log /var/log/nginx/error.log warn;
events {
worker_connections 1024;
accept_mutex on;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
reset_timedout_connection on;
server_tokens off;
log_format main '$remote_addr - [$time_local] '
'$host {$upstream_cache_status} "$request" $status $bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$gzip_ratio" $upstream_response_time';
log_format ip_only '$remote_addr';
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
client_header_buffer_size 16m;
client_max_body_size 512m;
server_names_hash_max_size 4096;
server_names_hash_bucket_size 512;
variables_hash_max_size 4096;
variables_hash_bucket_size 512;
types_hash_max_size 8192;
port_in_redirect off;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_buffers 16 8k;
gzip_comp_level 5;
gzip_http_version 1.0;
gzip_proxied any;
gzip_disable "msie6";
gzip_types text/plain text/css application/x-javascript text/xml
application/xml application/xml+rss text/javascript text/json;
fastcgi_temp_path /var/cache/nginx/temp/fastcgi;
proxy_temp_path /var/cache/nginx/temp/proxy;
charset utf-8;
index index.html index.htm;
access_log off;
error_log /dev/null;
include /etc/nginx/conf.d/upstream.conf;
include /etc/nginx/sites-enabled/*;
}
cat /etc/nginx/conf.d/upstream.conf
upstream apache {
server 127.0.0.1:8080;
}
cat /etc/nginx/sites-enabled/001-osm
server {
listen 80;
server_name osm.net;
access_log /var/log/nginx/osm.net.access.log main;
error_log /var/log/nginx/osm.net.error.log;
root /var/www;
index index.html;
charset utf-8;
# OSM
location ~* ^/osm/([0-9]+)/([0-9]+)/([0-9]+)\.png$ {
proxy_pass https://apache;
include /etc/nginx/proxy_params;
access_log off;
autoindex off;
expires 1d;
add_header Cache-Control 'public';
}
# static resources
location ~* ^.+\.(ico|htm|html|txt|jpg|jpeg|css|js|png|gif|map|woff|woff2|ttf|tif|tiff|pdf)$ {
access_log off;
autoindex off;
expires 1d;
add_header Cache-Control 'public';
}
}
Перезапускаем
service nginx restart
Оптимизируем OpenStreetMap (PostGIS)
(на основе статьи PostGIS Tuning)
CREATE INDEX idx_poly_idlanduse ON planet_osm_polygon USING gist (way)
WHERE((landuse IS NOT NULL)
OR (leisure IS NOT NULL)
OR (aeroway = ANY ('{apron,aerodrome}'::text[]))
OR (amenity = ANY ('{parking,university,college,school,hospital,kindergarten,grave_yard}'::text[]))
OR (military = ANY ('{barracks,danger_area}'::text[]))
OR ("natural" = ANY ('{field,beach,desert,heath,mud,grassland,wood,sand,scrub}'::text[]))
OR (power = ANY ('{station,sub_station,generator}'::text[]))
OR (tourism = ANY ('{attraction,camp_site,caravan_site,picnic_site,zoo}'::text[]))
OR (highway = ANY ('{services,rest_area}'::text[])));
CREATE INDEX "planet_osm_polygon_nobuilding_index"
ON "planet_osm_polygon"
USING gist ("way")
WHERE "building" IS NULL;
CREATE INDEX ferry_idx ON planet_osm_line USING gist (way) WHERE (route = 'ferry'::text);
CREATE INDEX "idx_poly_aeroway" on planet_osm_polygon USING gist (way) WHERE "aeroway" IS NOT NULL ;
CREATE INDEX "idx_poly_aeroway" on planet_osm_polygon USING gist (way) WHERE "aeroway" IS NOT NULL ;
CREATE INDEX "idx_poly_historic" on planet_osm_polygon USING gist (way) WHERE "historic" IS NOT NULL ;
CREATE INDEX "idx_poly_leisure" on planet_osm_polygon USING gist (way) WHERE "leisure" IS NOT NULL ;
CREATE INDEX "idx_poly_man_made" on planet_osm_polygon USING gist (way) WHERE "man_made" IS NOT NULL ;
CREATE INDEX "idx_poly_military" on planet_osm_polygon USING gist (way) WHERE "military" IS NOT NULL ;
CREATE INDEX "idx_poly_power" on planet_osm_polygon USING gist (way) WHERE "power" IS NOT NULL ;
CREATE INDEX "idx_poly_landuse" on planet_osm_polygon USING gist (way) WHERE "landuse" IS NOT NULL ;
CREATE INDEX "idx_poly_amenity" on planet_osm_polygon USING gist (way) WHERE "amenity" IS NOT NULL ;
CREATE INDEX "idx_poly_natural" on planet_osm_polygon USING gist (way) WHERE "natural" IS NOT NULL ;
CREATE INDEX "idx_poly_highway" on planet_osm_polygon USING gist (way) WHERE "highway" IS NOT NULL ;
CREATE INDEX "idx_poly_tourism" on planet_osm_polygon USING gist (way) WHERE "tourism" IS NOT NULL ;
CREATE INDEX "idx_poly_building" on planet_osm_polygon USING gist (way) WHERE "building" IS NOT NULL ;
CREATE INDEX "idx_poly_barrier" on planet_osm_polygon USING gist (way) WHERE "barrier" IS NOT NULL ;
CREATE INDEX "idx_poly_railway" on planet_osm_polygon USING gist (way) WHERE "railway" IS NOT NULL ;
CREATE INDEX "idx_poly_aerialway" on planet_osm_polygon USING gist (way) WHERE "aerialway" IS NOT NULL ;
CREATE INDEX "idx_poly_power_source" on planet_osm_polygon USING gist (way) WHERE "power_source" IS NOT NULL ;
CREATE INDEX "idx_poly_generator:source" on planet_osm_polygon USING gist (way) WHERE "generator:source" IS NOT NULL ;
CREATE INDEX "idx_line_aerialway" on planet_osm_line USING gist (way) WHERE "aerialway" IS NOT NULL ;
CREATE INDEX "idx_line_waterway" on planet_osm_line USING gist (way) WHERE "waterway" IS NOT NULL ;
CREATE INDEX "idx_line_bridge" on planet_osm_line USING gist (way) WHERE "bridge" IS NOT NULL ;
CREATE INDEX "idx_line_tunnel" on planet_osm_line USING gist (way) WHERE "tunnel" IS NOT NULL ;
CREATE INDEX "idx_line_access" on planet_osm_line USING gist (way) WHERE "access" IS NOT NULL ;
CREATE INDEX "idx_line_railway" on planet_osm_line USING gist (way) WHERE "railway" IS NOT NULL ;
CREATE INDEX "idx_line_power" on planet_osm_line USING gist (way) WHERE "power" IS NOT NULL ;
CREATE INDEX "idx_line_name" on planet_osm_line USING gist (way) WHERE "name" IS NOT NULL ;
CREATE INDEX "idx_line_ref" on planet_osm_line USING gist (way) WHERE "ref" IS NOT NULL ;
CREATE INDEX "idx_point_aerialway" on planet_osm_point USING gist (way) WHERE "aerialway" IS NOT NULL ;
CREATE INDEX "idx_point_power_source" on planet_osm_point USING gist (way) WHERE "power_source" IS NOT NULL ;
CREATE INDEX "idx_point_shop" on planet_osm_point USING gist (way) WHERE "shop" IS NOT NULL ;
CREATE INDEX "idx_point_place" on planet_osm_point USING gist (way) WHERE "place" IS NOT NULL ;
CREATE INDEX "idx_point_barrier" on planet_osm_point USING gist (way) WHERE "barrier" IS NOT NULL ;
CREATE INDEX "idx_point_railway" on planet_osm_point USING gist (way) WHERE "railway" IS NOT NULL ;
CREATE INDEX "idx_point_amenity" on planet_osm_point USING gist (way) WHERE "amenity" IS NOT NULL ;
CREATE INDEX "idx_point_natural" on planet_osm_point USING gist (way) WHERE "natural" IS NOT NULL ;
CREATE INDEX "idx_point_highway" on planet_osm_point USING gist (way) WHERE "highway" IS NOT NULL ;
CREATE INDEX "idx_point_tourism" on planet_osm_point USING gist (way) WHERE "tourism" IS NOT NULL ;
CREATE INDEX "idx_point_power" on planet_osm_point USING gist (way) WHERE "power" IS NOT NULL ;
CREATE INDEX "idx_point_aeroway" on planet_osm_point USING gist (way) WHERE "aeroway" IS NOT NULL ;
CREATE INDEX "idx_point_historic" on planet_osm_point USING gist (way) WHERE "historic" IS NOT NULL ;
CREATE INDEX "idx_point_leisure" on planet_osm_point USING gist (way) WHERE "leisure" IS NOT NULL ;
CREATE INDEX "idx_point_man_made" on planet_osm_point USING gist (way) WHERE "man_made" IS NOT NULL ;
CREATE INDEX "idx_point_waterway" on planet_osm_point USING gist (way) WHERE "waterway" IS NOT NULL ;
CREATE INDEX "idx_point_generator:source" on planet_osm_point USING gist (way) WHERE "generator:source" IS NOT NULL ;
CREATE INDEX "idx_point_capital" on planet_osm_point USING gist (way) WHERE "capital" IS NOT NULL ;
CREATE INDEX "idx_point_lock" on planet_osm_point USING gist (way) WHERE "lock" IS NOT NULL ;
CREATE INDEX "idx_point_landuse" on planet_osm_point USING gist (way) WHERE "landuse" IS NOT NULL ;
CREATE INDEX "idx_point_military" on planet_osm_point USING gist (way) WHERE "military" IS NOT NULL ;
CREATE INDEX idx_poly_wayarea_text ON planet_osm_polygon USING gist (way)
WHERE name IS NOT NULL
AND place IS NULL
AND way_area <= 320000;
CREATE INDEX idx_poly_text_poly ON planet_osm_polygon USING gist (way)
where amenity is not null
or shop in ('supermarket','bakery','clothes','fashion','convenience','doityourself','hairdresser','department_store', 'butcher','car','car_repair','bicycle')
or leisure is not null
or landuse is not null
or tourism is not null
or "natural" is not null
or man_made in ('lighthouse','windmill')
or place='island'
or military='danger_area'
or historic in ('memorial','archaeological_site')
or highway='bus_stop';
CREATE INDEX "idx_line_cutline" on planet_osm_line USING gist (way)
where man_made='cutline';
CREATE INDEX idx_poly_buildings_lz ON planet_osm_polygon USING gist (way)
where railway='station'
or building in ('station','supermarket')
or amenity='place_of_worship';
CREATE INDEX idx_poly_buildings ON planet_osm_polygon USING gist (way)
where (building is not null
and building not in ('no','station','supermarket','planned')
and (railway is null or railway != 'station')
and (amenity is null or amenity != 'place_of_worship'))
or aeroway = 'terminal';
CREATE INDEX water_areas_idx ON planet_osm_polygon USING gist (way)
WHERE (((waterway IS NOT NULL)
OR (landuse = ANY (ARRAY['reservoir'::text, 'water'::text, 'basin'::text])))
OR ("natural" IS NOT NULL));
Сохраним эти запросы в файл /var/osm/osm.indexes.sql
и выполним их
su postgres -с "psql -d gis -U gis -W"
gis=> \i /var/osm/osm.indexes.sql
Разогрев кэша Mapnik
Для часто используемых тайлов есть смысл «разогреть» кэш Mapnik.
Для этого есть очень простая команда:
cd /var/lib/mod_tile
render_list -m default -a -z 0 -Z 10 -n 16
Где
-z 0
— начальный зум (от)
-Z 10
— конечный зум (до)
-n 16
— количество ядер
Всё тот же пример с картой Украины для 16
ядер и с зумом от 0
до 10
занимает около 20 минут.