Тайловый сервер на основе Python, Mapnik и Bottle

В прошлом я писал серию статей о настройке тайлового сервера на основе apache2, renderd и модуля mod_tile. Недостаток этой связки, с моей точки зрения, заключается во-первых в необходимости использования apache, а во-вторых в том, что renderd и mod_tile отсутствуют в репозиториях Debian. Из-за второго недостатка нет уверенности в том, что при очередном обновлении её удастся успешно собрать и продолжить использовать.

Несколько месяцев назад мне попалась статья Дениса Рыкова Основы работы динамических TMS-сервисов, в которой приводится пример тайлового сервера, написанного на Python с использованием веб-фреймворка Bottle и библиотеки Mapnik. Статья привлекла моё внимание ещё потому, что в последних проектах я начал пользоваться фреймворком Bottle. Этот фреймворк обладает минимальным количеством зависимостей и может всё, что мне нужно от веб-фреймворков (да, мои потребности не велики), не навязывая при этом собственный стиль разработки, как это происходит, например, с Django.

Я решил воспользоваться этой статьёй для того, чтобы избавить себя от возможных проблем с используемой мной связкой на основе apache2, renderd и mod_tile.

1. Тайловый сервер

Для начала установим библиотеку Mapnik и веб-фреймворк Bottle:

# apt-get install python-mapnik2 python-bottle

Сам тайловый сервер разместим в файле /usr/local/share/tiles/main.py:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

from bottle import get, response, run, debug, app, HTTPError
import mapnik2 as mapnik
import os

from settings import TILE_WIDTH, TILE_HEIGHT, BBOX_MINX, BBOX_MINY, BBOX_MAXX, BBOX_MAXY, MAPNIK_CONFIGS, CACHE_PATH

# Сохранение тайла в файловую систему в целях кэширования
def save_tile(filename, body):
    path, name = os.path.split(filename)
    try:
        os.makedirs(path)
    except:
        pass
    file = open(filename, 'w')
    file.write(body)
    file.close() 
 
@get('/<service>/1.0.0/<layer>/<z:int>/<x:int>/<y:int>.png')
def tms(service, layer, z, x, y):
    # Считаем охват тайла на выбранном масштабном уровне
    stepx = (BBOX_MAXX - 1.0 - BBOX_MINX) / (2 ** z)
    stepy = (BBOX_MAXY - 1.0 - BBOX_MINY) / (2 ** z)

    # Выбираем охват тайла из словаря extents согласно типу запрашиваемого сервиса (tms или xyz)
    if service == 'tms':
        box = mapnik.Box2d(BBOX_MINX + x * stepx,
                           BBOX_MINY + y * stepy,
                           BBOX_MINX + (x + 1) * stepx,
                           BBOX_MINY + (y + 1) * stepy)
    elif service == 'xyz':
        box = mapnik.Box2d(BBOX_MINX + x * stepx,
                           BBOX_MAXY - (y + 1) * stepy,
                           BBOX_MINX + (x + 1) * stepx,
                           BBOX_MAXY - y * stepy)
 
    # Указываем путь, где находится файл с настройками Mapnik
    map = mapnik.Map(TILE_WIDTH, TILE_HEIGHT)
    mapnik.load_map(map, os.path.join(MAPNIK_CONFIGS, layer + '.xml'))
 
    # Нацеливаем карту на выбранный охват
    map.zoom_to_box(box)
 
    # Отрисовываем тайл
    image = mapnik.Image(map.width, map.height)
    mapnik.render(map, image)
    body = image.tostring('png')

    # Сохраняем тайл в файловой системе
    filename = os.path.join(CACHE_PATH, service, '1.0.0', layer, str(z), str(x), str(y) + '.png')
    save_tile(filename, body)

    # Передаём ответ клиенту
    response.content_type = 'image/png'
    return body

if __name__ == '__main__':
    debug(True)
    run(host = '0.0.0.0', port = 8080)
else:
    application = app()

В отдельном файле /usr/local/share/tiles/settings.py разместим настройки этого приложения:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

# Размеры тайла
TILE_WIDTH = 256
TILE_HEIGHT = 256

# Размеры карты
BBOX_MINX = -20037508.0
BBOX_MINY = -20037508.0
BBOX_MAXX = 20037508.0
BBOX_MAXY = 20037508.0

# Каталог с xml-файлами с описаниями слоёв
MAPNIK_CONFIGS = '/etc/mapnik-osm-data/'
# Каталог для хранения кэша тайлов
CACHE_PATH = '/var/cache/tiles'

Таким образом, в запросе можно указывать имя слоя, тайл из которого нужно вернуть. Имя слоя преобразуется в имя XML-файла, из которого извлекается его описание. Например, слою OpenStreetMap будет соответствовать запрос http://tiles.domain.tld/xyz/1.0.0/osm/0/0/0.png, при обслуживании которого будет использован файл /etc/mapnik-osm-data/osm.xml.

Создадим каталог для кэша тайлов:

# mkdir /var/cache/tiles

Проставим права доступа к нему для веб-сервера:

# chown www-data:www-data /var/cache/tiles

2. Сервер приложений uwsgi

Установим uwsgi и плагин к нему uwsgi-plugin-python, если они ещё не установлены:

# apt-get install uwsgi uwsgi-plugin-python

Создадим файл конфигурации /etc/uwsgi/apps-available/tiles.ini:

[uwsgi]

procname = uwsgi-tiles
procname-master = uwsgi-tiles-master

chdir = /usr/local/share/tiles
#module = app:application
mount = /=main:application
plugin = python27
master = true
processes = 8

Добавим файл конфигурации в каталог включенных приложений:

# cd /etc/uwsgi/apps-enabled
# ln -s /etc/uwsgi/apps-available/tiles.ini .

Запустим новое приложение:

# /etc/init.d/uwsgi start tiles

3. Веб-сервер nginx

Установим nginx, если он ещё не установлен:

# apt-get install nginx-full

Для доступа к файлам в кэше и запросе файла у тайлового сервера при их отсутствии в кэше создадим файл конфигурации. Например, если этот веб-сервер будет использоваться только для отрисовки тайлов, можно создать файл /etc/nginx/sites-available/default со следующим содержимым:

server {
  listen 0.0.0.0:80;
#  server_name tiles.domain.tld;

  root /var/www;

  location @tiles {
    uwsgi_pass unix:/var/run/uwsgi/app/tiles/socket;
    include uwsgi_params;
  }

  location /tms/ {
    root /var/cache/tiles/;
    try_files $uri @tiles;
  }

  location /xyz/ {
    root /var/cache/tiles/;
    try_files $uri @tiles;
  }
}

Включим использование этого файла веб-сервером:

# cd /etc/nginx/sites-enabled
# ln -s /etc/nginx/sites-available/tiles .

Перезапустим nginx, чтобы его новые настройки вступили в силу:

# /etc/init.d/nginx restart

4. Очистка кэша

Осталось только настроить периодическую чистку кэша от тайлов, к которым давно не было обращений. Это позволит достичь более высокой эффективности кэша в пересчёте на его объём. Сделать это просто - можно воспользоваться обычыми для Unix инструментами - планировщиком задач cron и командой find. Добавим в файл /etc/crontab такую строчку:

30    8 * * *   www-data     find /var/cache/tiles -type f -atime 14 -delete ; find /var/cache/tiles -type d -empty -delete

Из каталога кэша тайлов будут удаляться файлы, к которым не обращались более 14 дней, а также пустые каталоги.

Хочу отметить, что рассматриваемый тайловый сервер - скорее заготовка, т.к. он, например, практически не осуществляет обработку возможных исключительных ситуаций - отсутствие файла конфигурации, отсутсвие прав доступа к каталогу тайлов и т.п. Такие исключительные ситуации в конечном итоге попадают в фреймворк Bottle, который при их возникновении просто сообщает о внутренней ошибке сервера с кодом 500.

Написать автору