This commit is contained in:
parent
4846197d54
commit
4fe20a9cbd
5 changed files with 335 additions and 0 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -82,5 +82,11 @@ sf_anton_snapshot.db-*
|
|||
*.csv.zip
|
||||
!backend/db/init/*.sql
|
||||
|
||||
# OSRM routing graph build output (#39) — ~1.5GB .osrm* + intermediate .pbf.
|
||||
# Строится offline на VPS через scripts/build_osrm.sh, монтируется в osrm-service.
|
||||
# НЕ коммитим артефакты; .gitkeep держит каталог.
|
||||
data/osrm/*
|
||||
!data/osrm/.gitkeep
|
||||
|
||||
# Log cruft at repo root
|
||||
debug.log
|
||||
|
|
|
|||
0
data/osrm/.gitkeep
Normal file
0
data/osrm/.gitkeep
Normal file
|
|
@ -55,6 +55,47 @@ services:
|
|||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
# OSRM routing engine (#39 — site-finder /analyze road/walking distances to POI
|
||||
# вместо straight-line ST_Distance). INFRA-only здесь: интеграция в /analyze —
|
||||
# отдельный follow-up (A2/A3). Backend ходит к нему по http://osrm:5000 через
|
||||
# default-сеть. НЕ публикуется наружу (нет ports:) — internal-only, Caddy его
|
||||
# не проксирует.
|
||||
#
|
||||
# Region: Свердловская обл (oblast-clip из Ural-FO extract). Префикс .osrm-файлов
|
||||
# задаётся OSRM_REGION (default `sverdlovsk`). Файлы /data/<region>.osrm* кладёт
|
||||
# scripts/build_osrm.sh в ./data/osrm/ (host) → /data (container). Артефакты
|
||||
# .gitignored (не коммитим ~1.5GB build output) — на VPS строятся offline.
|
||||
#
|
||||
# --algorithm mld → pipeline extract→partition→customize (НЕ contract). max-table-size
|
||||
# поднят для будущих many-to-many table-запросов (изохроны A2).
|
||||
#
|
||||
# ВАЖНО: пока scripts/build_osrm.sh не отработал, /data/<region>.osrm нет →
|
||||
# osrm-routed падает и контейнер crash-loop'ит (restart: unless-stopped). Это
|
||||
# БЕЗВРЕДНО для деплоя: backend НЕ depends_on osrm, поэтому `compose up -d` в
|
||||
# deploy.yml не блокируется. Оркестратор строит граф offline и поднимает osrm
|
||||
# отдельно (docs/osrm-routing.md).
|
||||
osrm:
|
||||
image: osrm/osrm-backend:latest
|
||||
restart: unless-stopped
|
||||
command: osrm-routed --algorithm mld --max-table-size 8000 /data/${OSRM_REGION:-sverdlovsk}.osrm
|
||||
volumes:
|
||||
- ./data/osrm:/data
|
||||
mem_limit: 1.5g
|
||||
# TCP-probe порта 5000 через bash /dev/tcp. osrm/osrm-backend (Debian-slim)
|
||||
# НЕ содержит curl/wget — HTTP-healthcheck падал бы на missing binary (ср.
|
||||
# glitchtip #644). bash в образе есть → /dev/tcp надёжно проверяет, что
|
||||
# osrm-routed слушает (= граф загружен в RAM, движок принимает запросы).
|
||||
# start_period щедрый: загрузка MLD-графа Свердл в RAM занимает время.
|
||||
# Полный route-probe (driving) — в scripts/build_osrm.sh и в PR-инструкции
|
||||
# для оркестратора (curl с хоста VPS, см. docs/osrm-routing.md).
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "timeout 3 bash -c '</dev/tcp/127.0.0.1/5000' || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 40s
|
||||
networks: [default]
|
||||
|
||||
backend:
|
||||
image: ghcr.io/lekss361/gendesign-backend:${IMAGE_TAG:-latest}
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
126
docs/osrm-routing.md
Normal file
126
docs/osrm-routing.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# OSRM routing engine (self-hosted) — #39
|
||||
|
||||
Self-hosted [OSRM](https://project-osrm.org/) для site-finder `/analyze`: реальные
|
||||
дорожные / пешие расстояния до POI вместо straight-line `ST_Distance(geography)`.
|
||||
|
||||
> **Scope этого PR — INFRA ONLY.** Здесь стоит контейнер + build-скрипт + healthcheck.
|
||||
> Интеграция в `/analyze` (замена `ST_Distance` на OSRM `route`/`table`) — отдельный
|
||||
> follow-up (A2 изохроны, A3 per-category routing-decay). Backend пока к osrm НЕ ходит.
|
||||
|
||||
## Архитектура
|
||||
|
||||
```
|
||||
backend ──http://osrm:5000──▶ osrm (osrm-routed --algorithm mld)
|
||||
│ volume ./data/osrm:/data
|
||||
└─ /data/sverdlovsk.osrm* (MLD-граф, ~ХХХ MB)
|
||||
```
|
||||
|
||||
- Compose service `osrm` в `docker-compose.prod.yml`, сеть `default` (project `gendesign`).
|
||||
- **Наружу НЕ публикуется** — нет `ports:`. Достижим только из backend по `http://osrm:5000`.
|
||||
Caddy его не проксирует (routing — internal API, не должен быть публичным).
|
||||
- `mem_limit: 1.5g`, `restart: unless-stopped`.
|
||||
- Граф (`/data/*.osrm*`) НЕ в git (`.gitignore` → `data/osrm/*`). Строится offline на VPS.
|
||||
|
||||
## Build / refresh графа
|
||||
|
||||
Скрипт: [`scripts/build_osrm.sh`](../scripts/build_osrm.sh). Запускается **вручную**
|
||||
на VPS (НЕ часть `deploy.yml` — download+build тяжёлые).
|
||||
|
||||
```bash
|
||||
cd /opt/gendesign
|
||||
bash scripts/build_osrm.sh # car-профиль, Свердл-clip (default)
|
||||
```
|
||||
|
||||
### Источник pbf и решение по региону
|
||||
|
||||
Geofabrik **не** публикует Свердловскую область отдельно — есть только
|
||||
[Уральский ФО](https://download.geofabrik.de/russia/ural-fed-district-latest.osm.pbf)
|
||||
(≈600 MB). Скрипт по умолчанию (`CLIP=1`) скачивает Ural-FO и **обрезает по bbox
|
||||
Свердл. обл.** через `osmium extract` → легче граф, меньше RAM в runtime.
|
||||
|
||||
| Режим | Команда | pbf | Граф в RAM (warm) | Когда |
|
||||
|---|---|---|---|---|
|
||||
| **Свердл-clip** (default) | `bash scripts/build_osrm.sh` | clip из Ural-FO | ~0.8–1.2 GB | рекоменд., extensibility на обл |
|
||||
| ЕКБ-tight | `EKB_TIGHT=1 bash scripts/build_osrm.sh` | узкий bbox ЕКБ | ~0.4–0.6 GB | если RAM на VPS критична |
|
||||
| Ural-FO zero-clip | `CLIP=0 bash scripts/build_osrm.sh` | весь Ural-FO | ~1.5 GB+ | если нужен запас на соседние регионы |
|
||||
|
||||
bbox Свердл (с запасом): `lon 57.2..66.2, lat 56.0..62.0`. ЕКБ-tight: `lon 59.0..62.5, lat 56.2..58.2`.
|
||||
|
||||
### Pipeline (MLD, не CH)
|
||||
|
||||
`--algorithm mld` в compose требует pipeline **extract → partition → customize**
|
||||
(скрипт делает это через тот же `osrm/osrm-backend` образ). `osrm-contract` —
|
||||
это CH-алгоритм, он несовместим с `--algorithm mld`, поэтому НЕ используется.
|
||||
|
||||
### Walking-профиль (Phase-2, опционально)
|
||||
|
||||
Issue хочет оба профиля (car + foot) eventually. Этот PR **car-first**. Пеший граф:
|
||||
|
||||
```bash
|
||||
WALK=1 bash scripts/build_osrm.sh # дополнительно соберёт sverdlovsk-foot.osrm*
|
||||
```
|
||||
|
||||
Для пешего routing нужен второй compose-service (он не добавлен в этом PR, чтобы не
|
||||
плодить idle-контейнер пока /analyze не интегрирован). Блок для будущего:
|
||||
|
||||
```yaml
|
||||
osrm-walk:
|
||||
image: osrm/osrm-backend:latest
|
||||
restart: unless-stopped
|
||||
command: osrm-routed --algorithm mld --max-table-size 8000 /data/${OSRM_REGION:-sverdlovsk}-foot.osrm
|
||||
volumes: [./data/osrm:/data]
|
||||
mem_limit: 1g
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "timeout 3 bash -c '</dev/tcp/127.0.0.1/5000' || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 40s
|
||||
networks: [default]
|
||||
```
|
||||
|
||||
Backend ходил бы к нему по `http://osrm-walk:5000`.
|
||||
|
||||
### Monthly refresh
|
||||
|
||||
osm.pbf на Geofabrik обновляется ежедневно. Граф освежать ~ежемесячно:
|
||||
|
||||
```cron
|
||||
0 4 1 * * cd /opt/gendesign && bash scripts/build_osrm.sh >> /var/log/osrm-build.log 2>&1 \
|
||||
&& docker compose -p gendesign -f docker-compose.prod.yml up -d --force-recreate --no-deps osrm
|
||||
```
|
||||
|
||||
Скрипт idempotent: `wget -N` качает только если на сервере новее, граф перестраивается overwrite.
|
||||
|
||||
## Развёртывание (что выполняет оркестратор на VPS)
|
||||
|
||||
```bash
|
||||
cd /opt/gendesign
|
||||
git fetch origin main && git reset --hard origin/main # подтянуть compose + скрипт
|
||||
|
||||
# 1. Build графа (~30–50 мин, ~600MB download + ~3GB peak RAM на extract)
|
||||
bash scripts/build_osrm.sh
|
||||
|
||||
# 2. Поднять контейнер
|
||||
docker compose -p gendesign -f docker-compose.prod.yml up -d osrm
|
||||
|
||||
# 3. Verify (curl ВНУТРИ образа нет — используем sidecar curl в той же сети)
|
||||
docker run --rm --network gendesign_default curlimages/curl:8 -fsS \
|
||||
'http://osrm:5000/route/v1/driving/60.6,56.84;60.65,56.86'
|
||||
# ожидаем JSON: {"code":"Ok","routes":[{...}], ...}
|
||||
```
|
||||
|
||||
## Healthcheck
|
||||
|
||||
Compose-healthcheck — TCP-probe порта 5000 через `bash /dev/tcp` (образ
|
||||
`osrm/osrm-backend` НЕ содержит curl/wget, HTTP-probe падал бы на missing binary).
|
||||
Полный route-probe — verify-команда выше (sidecar curl).
|
||||
|
||||
## Риски / open
|
||||
|
||||
- **RAM на Beget VPS**: +~1 GB warm для Свердл-clip. Перед `up -d osrm` проверить
|
||||
свободную RAM (`free -h`). Если жмёт — `EKB_TIGHT=1` (~0.4–0.6 GB) или upgrade plan.
|
||||
- **Build на VPS медленный** (50+ мин). Альтернатива: собрать локально и `scp`
|
||||
`data/osrm/*.osrm*` в `/opt/gendesign/data/osrm/`.
|
||||
- **Размер pbf неточен**: Ural-FO ≈600 MB на скачку; «1.5GB» из issue — оценка по
|
||||
всему ФО + intermediate-файлы на диске во время build (нужно ~5 GB свободного диска).
|
||||
162
scripts/build_osrm.sh
Executable file
162
scripts/build_osrm.sh
Executable file
|
|
@ -0,0 +1,162 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build / refresh OSRM routing graph для site-finder (#39).
|
||||
#
|
||||
# Готовит .osrm-файлы (MLD pipeline) из OpenStreetMap-экстракта Свердловской
|
||||
# области и кладёт их в ./data/osrm/, откуда их монтирует docker-compose.prod.yml
|
||||
# (service `osrm`, volume ./data/osrm:/data → osrm-routed читает /data/<region>.osrm).
|
||||
#
|
||||
# === Где запускать ===
|
||||
# На VPS из /opt/gendesign: bash scripts/build_osrm.sh
|
||||
# (требует docker + ~5GB свободного диска + ~3GB свободной RAM на время extract).
|
||||
#
|
||||
# === ВАЖНО: это offline-задача, НЕ часть deploy.yml ===
|
||||
# Download ~600MB-1.5GB + build 30-50 мин. Запускается ВРУЧНУЮ оркестратором,
|
||||
# НЕ на каждом push. deploy.yml только поднимает уже готовый osrm-контейнер.
|
||||
#
|
||||
# === Idempotent / re-runnable ===
|
||||
# Повторный запуск перекачивает свежий pbf и пересобирает граф (overwrite).
|
||||
# Безопасно гонять ежемесячно (osm.pbf на Geofabrik обновляется ежедневно).
|
||||
# # monthly refresh: cron на VPS, напр. 0 4 1 * * bash /opt/gendesign/scripts/build_osrm.sh >> /var/log/osrm-build.log 2>&1
|
||||
# После refresh: docker compose -p gendesign -f docker-compose.prod.yml up -d --force-recreate --no-deps osrm
|
||||
#
|
||||
# === Регион / источник pbf ===
|
||||
# Geofabrik НЕ публикует Свердловскую область отдельным extract'ом — только
|
||||
# «Уральский ФО» (ural-fed-district). Два режима (CLIP=1 по умолчанию):
|
||||
#
|
||||
# CLIP=1 (default, РЕКОМЕНДОВАНО): качаем Ural-FO pbf и обрезаем по bbox
|
||||
# Свердл. обл. через `osmium extract`. Результат легче → меньше RAM/диск
|
||||
# в build и в runtime (osrm-routed warm ~0.8-1.2GB вместо ~1.5GB+ на
|
||||
# весь Ural-FO). Минус: нужен extra-шаг osmium (запускаем в docker).
|
||||
#
|
||||
# CLIP=0: используем весь Ural-FO pbf как есть (zero-clip, проще, но тяжелее —
|
||||
# граф Курган/Тюмень/ХМАО/ЯНАО грузится в RAM без пользы для Свердл).
|
||||
# Бери, если RAM не жмёт и нужен запас на соседние регионы.
|
||||
#
|
||||
# bbox Свердловской обл (грубый, с запасом): lon 57.2..66.2, lat 56.0..62.0.
|
||||
# Можно сузить до ЕКБ-агломерации если RAM критична (lon 59.0..62.5, lat 56.2..58.2)
|
||||
# через EKB_TIGHT=1.
|
||||
#
|
||||
# === Профили ===
|
||||
# Car (driving) — основной, строится всегда → <region>.osrm*.
|
||||
# Foot (walking) — опциональный Phase-2, строится при WALK=1 → <region>-foot.osrm*.
|
||||
# Для walking нужен второй compose-service (osrm-walk) — см. docs/osrm-routing.md.
|
||||
# Этот PR car-first; walk выключен по умолчанию (issue хочет оба eventually).
|
||||
#
|
||||
# === Проверка после build ===
|
||||
# docker compose -p gendesign -f docker-compose.prod.yml up -d osrm
|
||||
# docker compose -p gendesign -f docker-compose.prod.yml exec osrm \
|
||||
# sh -c 'echo OK' # контейнер жив
|
||||
# # route-probe С ХОСТА (внутри образа нет curl) — временно прокинуть порт:
|
||||
# docker run --rm --network gendesign_default curlimages/curl:8 -fsS \
|
||||
# 'http://osrm:5000/route/v1/driving/60.6,56.84;60.65,56.86'
|
||||
# ↑ ожидаем JSON {"code":"Ok","routes":[...]}.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Config (override через env) -------------------------------------------
|
||||
OSRM_IMAGE="${OSRM_IMAGE:-osrm/osrm-backend:latest}"
|
||||
OSMIUM_IMAGE="${OSMIUM_IMAGE:-stefda/osmium-tool:latest}"
|
||||
REGION="${OSRM_REGION:-sverdlovsk}" # имя выходных .osrm* (= OSRM_REGION в compose)
|
||||
PBF_URL="${PBF_URL:-https://download.geofabrik.de/russia/ural-fed-district-latest.osm.pbf}"
|
||||
CLIP="${CLIP:-1}" # 1 = обрезать по bbox Свердл, 0 = весь Ural-FO
|
||||
EKB_TIGHT="${EKB_TIGHT:-0}" # 1 = узкий bbox только ЕКБ-агломерация
|
||||
WALK="${WALK:-0}" # 1 = ещё и пеший профиль <region>-foot.osrm*
|
||||
CAR_PROFILE="${CAR_PROFILE:-/opt/car.lua}" # встроенные профили в osrm-backend образе
|
||||
FOOT_PROFILE="${FOOT_PROFILE:-/opt/foot.lua}"
|
||||
|
||||
# bbox: left,bottom,right,top (lon_min,lat_min,lon_max,lat_max)
|
||||
if [ "$EKB_TIGHT" = "1" ]; then
|
||||
BBOX="${BBOX:-59.0,56.2,62.5,58.2}" # ЕКБ-агломерация (минимум RAM)
|
||||
else
|
||||
BBOX="${BBOX:-57.2,56.0,66.2,62.0}" # вся Свердловская обл (с запасом)
|
||||
fi
|
||||
|
||||
# Каталог host (= ./data/osrm в compose). Скрипт зовём из /opt/gendesign.
|
||||
DATA_DIR="${DATA_DIR:-$(pwd)/data/osrm}"
|
||||
mkdir -p "$DATA_DIR"
|
||||
|
||||
RAW_PBF="$DATA_DIR/${REGION}-raw.osm.pbf" # сырой Ural-FO (или clip-источник)
|
||||
WORK_PBF="$DATA_DIR/${REGION}.osm.pbf" # то, что реально пойдёт в osrm-extract
|
||||
|
||||
echo "=== OSRM build ==="
|
||||
echo " region : $REGION"
|
||||
echo " pbf source : $PBF_URL"
|
||||
echo " clip to bbox : CLIP=$CLIP BBOX=$BBOX (EKB_TIGHT=$EKB_TIGHT)"
|
||||
echo " walking profile : WALK=$WALK"
|
||||
echo " data dir (host) : $DATA_DIR"
|
||||
echo ""
|
||||
|
||||
# --- 1. Download ------------------------------------------------------------
|
||||
# -N: качаем только если на сервере новее (idempotent monthly refresh).
|
||||
echo "=== 1. Download pbf (Ural-FO ≈600MB) ==="
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
wget -N -O "$RAW_PBF" "$PBF_URL"
|
||||
else
|
||||
curl -fSL --time-cond "$RAW_PBF" -o "$RAW_PBF" "$PBF_URL"
|
||||
fi
|
||||
|
||||
# --- 2. Clip (optional) -----------------------------------------------------
|
||||
if [ "$CLIP" = "1" ]; then
|
||||
echo "=== 2. Clip to Свердл bbox через osmium ($BBOX) ==="
|
||||
docker run --rm -v "$DATA_DIR:/data" "$OSMIUM_IMAGE" \
|
||||
osmium extract -b "$BBOX" "/data/$(basename "$RAW_PBF")" \
|
||||
-o "/data/$(basename "$WORK_PBF")" --overwrite
|
||||
else
|
||||
echo "=== 2. No clip (CLIP=0) — используем Ural-FO целиком ==="
|
||||
cp -f "$RAW_PBF" "$WORK_PBF"
|
||||
fi
|
||||
|
||||
# --- helper: запуск osrm-backend образа на /data --------------------------
|
||||
osrm() {
|
||||
docker run --rm -v "$DATA_DIR:/data" "$OSRM_IMAGE" "$@"
|
||||
}
|
||||
|
||||
# --- 3. MLD pipeline (car) --------------------------------------------------
|
||||
# MLD = extract → partition → customize. НЕ osrm-contract (то CH-алгоритм,
|
||||
# несовместим с --algorithm mld в compose).
|
||||
#
|
||||
# osrm-extract пишет <pbf-без-.osm.pbf>.osrm. Имя входного pbf специально равно
|
||||
# целевому базовому имени (<region>.osm.pbf → <region>.osrm; <region>-foot.osm.pbf
|
||||
# → <region>-foot.osrm), поэтому partition/customize работают по тому же базовому
|
||||
# имени и переименование не требуется.
|
||||
build_profile() {
|
||||
local profile="$1" src_pbf="$2"
|
||||
local base extracted
|
||||
base="$(basename "${src_pbf%.osm.pbf}")"
|
||||
extracted="/data/${base}.osrm"
|
||||
echo "--- extract ($profile → ${base}.osrm) ---"
|
||||
osrm osrm-extract -p "$profile" "/data/$(basename "$src_pbf")"
|
||||
echo "--- partition (${base}.osrm) ---"
|
||||
osrm osrm-partition "$extracted"
|
||||
echo "--- customize (${base}.osrm) ---"
|
||||
osrm osrm-customize "$extracted"
|
||||
}
|
||||
|
||||
echo "=== 3. Build car profile → ${REGION}.osrm* ==="
|
||||
build_profile "$CAR_PROFILE" "$WORK_PBF"
|
||||
|
||||
# --- 4. MLD pipeline (foot, optional) --------------------------------------
|
||||
if [ "$WALK" = "1" ]; then
|
||||
echo "=== 4. Build foot profile → ${REGION}-foot.osrm* ==="
|
||||
# Отдельная копия pbf под пеший граф (osrm-extract overwrite'ит базу).
|
||||
WALK_PBF="$DATA_DIR/${REGION}-foot.osm.pbf"
|
||||
cp -f "$WORK_PBF" "$WALK_PBF"
|
||||
build_profile "$FOOT_PROFILE" "$WALK_PBF"
|
||||
else
|
||||
echo "=== 4. Skip foot profile (WALK=0) — car-first, см. docs/osrm-routing.md ==="
|
||||
fi
|
||||
|
||||
# --- 5. Cleanup heavy intermediates ----------------------------------------
|
||||
echo "=== 5. Cleanup промежуточных .pbf (граф .osrm* уже собран) ==="
|
||||
rm -f "$RAW_PBF" "$WORK_PBF" "${DATA_DIR}/${REGION}-foot.osm.pbf" 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "=== DONE ==="
|
||||
echo "Артефакты в $DATA_DIR:"
|
||||
ls -lh "$DATA_DIR"/*.osrm* 2>/dev/null | awk '{print " "$5"\t"$9}' || true
|
||||
echo ""
|
||||
echo "Дальше:"
|
||||
echo " docker compose -p gendesign -f docker-compose.prod.yml up -d osrm"
|
||||
echo " # verify (curl с хоста — внутри образа curl нет):"
|
||||
echo " docker run --rm --network gendesign_default curlimages/curl:8 -fsS \\"
|
||||
echo " 'http://osrm:5000/route/v1/driving/60.6,56.84;60.65,56.86'"
|
||||
Loading…
Add table
Reference in a new issue