2025, Oct 19 10:16

Почему gdal2tiles.py зависает в Docker и как починить таймауты

Почему gdal2tiles.py зависает в Docker при запуске из Python и игнорирует таймауты: переполнен stdout/stderr. Решение — не перехватывать вывод subprocess.

Когда конвейер на Python внутри Docker-образа на базе Conda запускает утилиты GDAL, логично ожидать, что таймаут остановит любой шаг, если он завис. Но gdal2tiles.py порой «замирает» настолько глубоко, что таймауты subprocess так и не срабатывают. Ниже — практический разбор, почему это случается и как исправить ситуацию, не меняя привычный рабочий процесс.

Повторение проблемы: двухшаговый процесс GDAL зависает на gdal2tiles.py

Скрипт сначала раскрашивает GeoTIFF с помощью gdaldem color-relief, затем генерирует тайлы через gdal2tiles.py. Первый шаг стабильно проходит успешно; второй зависает без какого‑либо вывода ошибок и игнорирует таймаут.

try:
    logging.info("starting gdaldem color-relief")
    palette_data = build_palette_map(prod_cfg['cmap'], prod_cfg['vmin'], prod_cfg['vmax'])
    with open(palette_path, 'w') as fh:
        fh.write(palette_data)

    dem_cmd = ['gdaldem', 'color-relief', src_tiff, palette_path, tinted_tiff, '-alpha']
    subprocess.run(dem_cmd, check=True, capture_output=True, text=True, timeout=60)
    logging.info(f"colorized to {tinted_tiff}")

    logging.info(f"tiling {tinted_tiff} with gdal2tiles.py")
    tiles_cmd = [
        'gdal2tiles.py',
        '--profile=raster',
        '--zoom=5-12',
        '--webp-quality=90',
        tinted_tiff,
        tiles_dir
    ]
    subprocess.run(tiles_cmd, check=True, capture_output=True, text=True, timeout=180)

    logging.info(f"done -> {tiles_dir}")
    return tiles_dir

Окружение и параметры уже были проверены. Корректный gdal2tiles.py найден через which внутри окружения Conda (/opt/conda/envs/radar-env/bin/gdal2tiles.py). Удаление --processes не помогло, и проблема сохранялась независимо от настроек формата тайлов. Замена subprocess.run на Popen с communicate(timeout=) всё равно не приводила к возникновению TimeoutExpired.

Что на самом деле происходит

gdal2tiles.py может генерировать очень много вывода. Когда этот поток перехватывается в Python (capture_output=True), он идёт в буферизованные каналы. Если буферы заполняются, дочерний процесс блокируется при попытке записать ещё, а родительский процесс ждёт его завершения. Ошибка не появляется, и таймаут на стороне Python не успевает сработать, потому что процесс застрял в I/O‑взаимоблокировке.

Это объясняет, почему gdaldem color-relief выполняется успешно, а gdal2tiles.py — нет. Первый инструмент пишет сравнительно мало; второй при генерации множества тайлов бывает крайне многословным и легко насыщает буферы в неинтерактивном запуске контейнера.

Решение: перестаньте перехватывать вывод

Решение простое и эффективное: позвольте gdal2tiles.py писать напрямую в stdout/stderr контейнера вместо перехвата. Достаточно убрать capture_output=True — взаимоблокировка исчезнет, логи будут идти потоком, процесс завершится штатно, а при необходимости таймаут в Python начнёт работать как ожидается.

try:
    logging.info("starting gdaldem color-relief")
    palette_data = build_palette_map(prod_cfg['cmap'], prod_cfg['vmin'], prod_cfg['vmax'])
    with open(palette_path, 'w') as fh:
        fh.write(palette_data)

    dem_cmd = ['gdaldem', 'color-relief', src_tiff, palette_path, tinted_tiff, '-alpha']
    subprocess.run(dem_cmd, check=True, text=True, timeout=60)
    logging.info(f"colorized to {tinted_tiff}")

    logging.info(f"tiling {tinted_tiff} with gdal2tiles.py")
    tiles_cmd = [
        'gdal2tiles.py',
        '--profile=raster',
        '--zoom=5-12',
        '--webp-quality=90',
        tinted_tiff,
        tiles_dir
    ]
    subprocess.run(tiles_cmd, check=True, text=True, timeout=180)

    logging.info(f"done -> {tiles_dir}")
    return tiles_dir

Почему это важно

Геопространственные инструменты по своей природе «говорливы». В контейнерных, неинтерактивных запусках перехват этого вывода может ненароком создать жёсткую взаимоблокировку, которая выглядит как зависание приложения и сводит на нет вашу логику таймаутов. Потоковый вывод в терминал обходит узкое место и даёт живую картину прогресса — например, сообщения вроде “Generating Base Tiles”, что полезно, когда нужно видеть активность, а не тишину.

Вывод

Если gdal2tiles.py зависает внутри Dockerизированного процесса Python и игнорирует ваши таймауты, не перехватывайте его stdout/stderr. Удалите capture_output=True — логи будут стримиться. Эта небольшая правка обычно разблокирует долгую генерацию тайлов и сохраняет остальной конвейер без изменений, включая возможность фиксировать сбои через check=True и применять таймауты, когда они действительно наступают.

Эта статья основана на вопросе на StackOverflow от mrotskcud и ответе Abhay Jain.