From 26f9605c06c274427511f398993995d92df9ffc5 Mon Sep 17 00:00:00 2001 From: Light1YT Date: Sat, 27 Jun 2026 00:13:41 +0500 Subject: [PATCH] feat(devops): self-hosted OSRM routing engine for site-finder (#39) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stand up an internal-only OSRM container so /analyze can later use real road/walking distances to POI instead of straight-line ST_Distance. Infra only — the /analyze integration is a separate follow-up (A2/A3). - docker-compose.prod.yml: osrm service (osrm/osrm-backend, --algorithm mld, mem_limit 1.5g, default network, no public port — backend reaches it at http://osrm:5000). TCP /dev/tcp healthcheck (image has no curl). backend does NOT depend_on osrm, so a crash-loop before the graph is built won't block deploy. - scripts/build_osrm.sh: idempotent MLD build/refresh (extract→partition→customize, not contract). Defaults to Свердл-clip из Ural-FO via osmium (lighter RAM); CLIP=0 zero-clip and EKB_TIGHT=1 / WALK=1 options documented. Monthly-refresh note. - docs/osrm-routing.md: region/pbf decision, RAM table, VPS run-commands, verify curl. - .gitignore data/osrm/* (don't commit ~1.5GB build output); .gitkeep holds the dir. Refs #39 --- .gitignore | 6 ++ data/osrm/.gitkeep | 0 docker-compose.prod.yml | 41 ++++++++++ docs/osrm-routing.md | 126 +++++++++++++++++++++++++++++++ scripts/build_osrm.sh | 162 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 335 insertions(+) create mode 100644 data/osrm/.gitkeep create mode 100644 docs/osrm-routing.md create mode 100755 scripts/build_osrm.sh diff --git a/.gitignore b/.gitignore index 902d9193..28507375 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/data/osrm/.gitkeep b/data/osrm/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 35b179df..c490c82e 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -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/.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/.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 ' **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 '> /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 свободного диска). diff --git a/scripts/build_osrm.sh b/scripts/build_osrm.sh new file mode 100755 index 00000000..e07bb08d --- /dev/null +++ b/scripts/build_osrm.sh @@ -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/.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) — основной, строится всегда → .osrm*. +# Foot (walking) — опциональный Phase-2, строится при WALK=1 → -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 = ещё и пеший профиль -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 пишет .osrm. Имя входного pbf специально равно +# целевому базовому имени (.osm.pbf → .osrm; -foot.osm.pbf +# → -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'" -- 2.45.3