TileService
Асинхронный тайл-сервер для GeoPackage и GeoTIFF файлов. Раздаёт тайлы по ZXY-схеме, кэширует в Redis, поддерживает горизонтальное масштабирование и мониторинг в реальном времени.
Архитектура
TileService состоит из 3 контейнеров:
- Nginx — reverse proxy, 5 ГБ дисковый кэш тайлов, WebSocket upgrade, gzip
- FastAPI — обработка запросов, пулы соединений, чтение тайлов, Redis-кэш
- Redis — кэш тайлов/метаданных, атомарные счётчики, реестр инстансов
Жизненный цикл запроса
GET /tiles/z/x/y?file=...X-Cache: HITX-Tile-Cache: HITconnection pool
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 — Тайлы
Получить тайл
/tiles/{z}/{x}/{y}?file={path}
Возвращает тайл-изображение (PNG, JPEG или WebP) по координатам ZXY.
| Параметр | Тип | Описание |
|---|---|---|
z | int (path) | Zoom level |
x | int (path) | X-координата тайла |
y | int (path) | Y-координата тайла |
file | string (query) | Абсолютный путь к файлу GPKG/TIFF |
Заголовки ответа
| Заголовок | Значение |
|---|---|
X-Tile-Cache | HIT — из Redis, MISS — из файла |
X-Cache-Status | HIT / MISS — Nginx proxy cache |
Cache-Control | public, 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);Метаданные файла
/metadata?file={path}
Возвращает метаданные файла: CRS, zoom-уровни, экстент, центр.
| Параметр | Тип | Описание |
|---|---|---|
file | string (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)
/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"}
}Список файлов
/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)
/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/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
/health
Проверка работоспособности. Возвращает {"status": "ok"}.
Используется Docker healthcheck и Kubernetes liveness/readiness probes.
Кэширование
Redis кэш (L1)
Все тайлы, метаданные и список файлов кэшируются в Redis. TTL настраивается через переменные окружения:
| Переменная | По умолчанию | Описание |
|---|---|---|
TILE_CACHE_TTL | 86400 (24 ч) | TTL тайлов |
META_CACHE_TTL | 3600 (1 ч) | TTL метаданных |
FILE_LIST_CACHE_TTL | 300 (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 herdproxy_cache_use_stale— отдаёт старый кэш при ошибках бэкенда- Заголовок
X-Cache-Status—HIT/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_URL | redis://redis:6379/0 | Подключение к Redis |
POOL_SIZE | 10 | Размер пула соединений на файл |
IDLE_TIMEOUT | 300 | Таймаут idle соединений (сек) |
IDLE_CHECK_INTERVAL | 30 | Интервал проверки idle (сек) |
TILE_CACHE_TTL | 86400 | TTL тайлов в Redis (сек) |
META_CACHE_TTL | 3600 | TTL метаданных (сек) |
FILE_LIST_CACHE_TTL | 300 | TTL списка файлов (сек) |
HEARTBEAT_INTERVAL | 10 | Интервал heartbeat (сек) |
INSTANCE_TTL | 30 | TTL записи инстанса в Redis (сек) |
LOG_LEVEL | info | Уровень логирования |
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-документация доступна по ссылкам:
- Swagger UI —
/docs - ReDoc —
/redoc - OpenAPI JSON —
/openapi.json