TileService

Асинхронный тайл-сервер для GeoPackage и GeoTIFF файлов. Раздаёт тайлы по ZXY-схеме, кэширует в Redis, поддерживает горизонтальное масштабирование и мониторинг в реальном времени.

Полностью автономный сервис. Не зависит от внешних БД или фреймворков. Единственные зависимости — Redis (кэш) и файлы на диске.

Архитектура

TileService состоит из 3 контейнеров:

🌐
Клиент
HTTP / WS
Nginx
:80
reverse proxy + tile cache
proxy
FastAPI
:8000
REST API + Web UI
cache / stats
Redis
:6379
tile cache + stats
📁
/data/
GPKG / TIFF файлы
  • Nginx — reverse proxy, 5 ГБ дисковый кэш тайлов, WebSocket upgrade, gzip
  • FastAPI — обработка запросов, пулы соединений, чтение тайлов, Redis-кэш
  • Redis — кэш тайлов/метаданных, атомарные счётчики, реестр инстансов

Жизненный цикл запроса

1
Запрос
GET /tiles/z/x/y?file=...
2
Nginx Cache
Дисковый кэш
X-Cache: HIT
3
Redis Cache
In-memory
X-Tile-Cache: HIT
4
Reader
GPKG / TIFF
connection pool
5
Ответ
PNG / JPEG / WebP
256×256 px

Двухуровневый кэш: Nginx (диск, 5 ГБ, 24 ч) → Redis (память, TTL настраивается). При повторном запросе тайл отдаётся из Nginx за <1 мс.

Поддерживаемые форматы

GeoPackage (.gpkg)

Нативные тайлы из таблицы tiles (OGC GeoPackage Tile Encoding). Читаются напрямую через aiosqlite — пул соединений для каждого файла. Поддерживаются все zoom-уровни, определённые в файле.

GeoTIFF (.tiff, .tif)

Динамическая нарезка через rasterio + GDAL. Поддержка overviews (COG), автоматический расчёт нативного zoom по GSD. На каждом запросе вычисляется bbox тайла, заданная область вырезается и масштабируется до 256×256.

Быстрый старт

# Клонировать и запустить
git clone git@gitlab.levkona.ru:geo/tile-service.git
cd tile-service

# Настроить переменные
cp .env.example .env
# Указать ALLOWED_DIRS — директории с файлами GPKG/TIFF

# Запустить
docker compose up -d --build

# Проверить
curl http://localhost:8082/health
curl http://localhost:8082/files

После запуска доступны:

  • Просмотр — интерактивная карта с Leaflet
  • Мониторинг — дашборд состояния в реальном времени
  • Swagger — интерактивная API-документация

API — Тайлы

Получить тайл

GET /tiles/{z}/{x}/{y}?file={path}

Возвращает тайл-изображение (PNG, JPEG или WebP) по координатам ZXY.

ПараметрТипОписание
zint (path)Zoom level
xint (path)X-координата тайла
yint (path)Y-координата тайла
filestring (query)Абсолютный путь к файлу GPKG/TIFF

Заголовки ответа

ЗаголовокЗначение
X-Tile-CacheHIT — из Redis, MISS — из файла
X-Cache-StatusHIT / MISS — Nginx proxy cache
Cache-Controlpublic, max-age=86400
curl -o tile.png "http://localhost:8082/tiles/14/9876/5432?file=/data/map.gpkg"
const resp = await fetch('/tiles/14/9876/5432?file=/data/map.gpkg');
const blob = await resp.blob();
// использовать как img.src через URL.createObjectURL(blob)
import httpx

resp = httpx.get("http://localhost:8082/tiles/14/9876/5432", params={"file": "/data/map.gpkg"})
with open("tile.png", "wb") as f:
    f.write(resp.content)

Leaflet интеграция

L.tileLayer('/tiles/{z}/{x}/{y}?file=/data/map.gpkg', {
    minZoom: 8,
    maxZoom: 21,
}).addTo(map);

Метаданные файла

GET /metadata?file={path}

Возвращает метаданные файла: CRS, zoom-уровни, экстент, центр.

ПараметрТипОписание
filestring (query)Абсолютный путь к файлу

Пример ответа (GPKG)

{
  "path": "/data/map.gpkg",
  "name": "map.gpkg",
  "crs": {"epsg": 3857, "wkt": "..."},
  "rectangle": {"north": 56.0, "south": 55.0, "east": 38.0, "west": 37.0},
  "center": {"lat": 55.5, "lon": 37.5},
  "minimum_level": 8,
  "maximum_level": 21,
  "native_levels": [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21],
  "tile_url_template": "/tiles/{z}/{x}/{y}?file=/data/map.gpkg"
}

Пример ответа (TIFF)

{
  "path": "/data/ortho.tiff",
  "name": "ortho.tiff",
  "crs": {"epsg": 32637, "wkt": "..."},
  "rectangle": {"north": 55.9, "south": 55.7, "east": 37.8, "west": 37.5},
  "center": {"lat": 55.8, "lon": 37.65},
  "minimum_level": 10,
  "maximum_level": 20,
  "native_zoom": 18,
  "resolution_m": 0.05,
  "bands": 3,
  "dtype": "uint8",
  "tile_url_template": "/tiles/{z}/{x}/{y}?file=/data/ortho.tiff"
}

Охват (GeoJSON)

GET /coverage?file={path}

Возвращает экстент файла как GeoJSON Feature (bbox-полигон). Удобно для визуализации на карте.

{
  "type": "Feature",
  "geometry": {
    "type": "Polygon",
    "coordinates": [[[37.0,55.0],[38.0,55.0],[38.0,56.0],[37.0,56.0],[37.0,55.0]]]
  },
  "properties": {"file": "/data/map.gpkg"}
}

Список файлов

GET /files

Возвращает список всех доступных файлов из разрешённых директорий (ALLOWED_DIRS).

[
  {
    "path": "/data/map.gpkg",
    "name": "map.gpkg",
    "extension": ".gpkg",
    "size_bytes": 2910748672
  },
  {
    "path": "/data/ortho.tiff",
    "name": "ortho.tiff",
    "extension": ".tiff",
    "size_bytes": 524288000
  }
]

API — Мониторинг

Статистика (JSON)

GET /api/stats

Полная статистика: uptime, RPS, latency, Redis, пулы соединений, клиенты, инстансы.

{
  "global": {
    "uptime_seconds": 3600,
    "total_requests": 12450,
    "rps": 3.4,
    "p50_ms": 12,
    "p95_ms": 45
  },
  "cache": {
    "hit_rate": "87%",
    "hits": 10832,
    "misses": 1618,
    "redis_keys": 5420,
    "redis_used_memory_human": "42.5M"
  },
  "instances": [
    {"id": "abc123def456", "uptime_local": 3600, "pools": 2}
  ],
  "connections": [...],
  "pool_summary": {"total_connections": 20, "total_in_use": 3, "open_files": 2},
  "clients": [
    {"ip": "10.0.0.1", "request_count": 500, "last_request_at": 1711756800, "last_path": "/tiles/14/..."}
  ],
  "allowed_dirs": ["/data"]
}

WebSocket мониторинг

WS /ws/stats

Пушит статистику каждые 2 секунды. Формат ответа идентичен GET /api/stats. Используется дашбордом мониторинга.

// JavaScript
const ws = new WebSocket('ws://localhost:8082/ws/stats');
ws.onmessage = (event) => {
    const stats = JSON.parse(event.data);
    console.log('RPS:', stats.global.rps);
};

Health Check

GET /health

Проверка работоспособности. Возвращает {"status": "ok"}. Используется Docker healthcheck и Kubernetes liveness/readiness probes.

Кэширование

Redis кэш (L1)

Все тайлы, метаданные и список файлов кэшируются в Redis. TTL настраивается через переменные окружения:

ПеременнаяПо умолчаниюОписание
TILE_CACHE_TTL86400 (24 ч)TTL тайлов
META_CACHE_TTL3600 (1 ч)TTL метаданных
FILE_LIST_CACHE_TTL300 (5 мин)TTL списка файлов

Ключи Redis:

  • tile:{path}:{z}:{x}:{y} — бинарные данные тайла
  • meta:{path} — JSON метаданных
  • file_list — JSON списка файлов
  • ts:hits / ts:misses — атомарные счётчики
  • ts:instance:{id} — heartbeat инстанса
  • ts:requests — stream запросов (для latency)

Nginx кэш (L2)

Nginx кэширует ответы /tiles/ на диске (до 5 ГБ, 24 ч). Дополнительные оптимизации:

  • proxy_cache_lock on — предотвращает thundering herd
  • proxy_cache_use_stale — отдаёт старый кэш при ошибках бэкенда
  • Заголовок X-Cache-StatusHIT / MISS / EXPIRED

Масштабирование

Горизонтальное масштабирование

TileService поддерживает запуск нескольких инстансов за одним Nginx:

# Docker Compose
docker compose up -d --scale app=4

# Kubernetes HPA (автоматически)
kubectl autoscale deployment tile-app --min=2 --max=10 --cpu-percent=70

Вся статистика (счётчики, latency, heartbeat) хранится в Redis — инстансы stateless и взаимозаменяемы.

Реестр инстансов

Каждый инстанс при старте генерирует уникальный INSTANCE_ID и каждые 10 секунд публикует heartbeat в Redis (ts:instance:{id}). Дашборд мониторинга агрегирует данные всех живых инстансов.

Деплой

Docker Compose

# Полный запуск
docker compose up -d --build

# Пересборка одного сервиса
docker compose up -d --build app

# Логи
docker compose logs -f app

# Масштабирование
docker compose up -d --scale app=3

# Остановка
docker compose down

Переменные окружения

ПеременнаяПо умолчаниюОписание
ALLOWED_DIRS/dataРазрешённые директории (через запятую)
REDIS_URLredis://redis:6379/0Подключение к Redis
POOL_SIZE10Размер пула соединений на файл
IDLE_TIMEOUT300Таймаут idle соединений (сек)
IDLE_CHECK_INTERVAL30Интервал проверки idle (сек)
TILE_CACHE_TTL86400TTL тайлов в Redis (сек)
META_CACHE_TTL3600TTL метаданных (сек)
FILE_LIST_CACHE_TTL300TTL списка файлов (сек)
HEARTBEAT_INTERVAL10Интервал heartbeat (сек)
INSTANCE_TTL30TTL записи инстанса в Redis (сек)
LOG_LEVELinfoУровень логирования

Kubernetes

Манифесты в директории k8s/:

# Применить все манифесты
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/

# Проверить
kubectl -n tile-service get pods
kubectl -n tile-service get hpa

HPA настроен на автомасштабирование по CPU (target 70%, min 2, max 10 реплик).

Обработка ошибок

КодКогда
200Успешный ответ
404Тайл не найден / файл не существует / путь вне ALLOWED_DIRS
422Некорректные параметры (отсутствует file, неверный ZXY)
500Ошибка чтения файла / внутренняя ошибка
Путь к файлу должен быть абсолютным и находиться в одной из директорий ALLOWED_DIRS. Попытки path traversal (../) блокируются.

Swagger / ReDoc

Интерактивная API-документация доступна по ссылкам: