QGIS的docker管理

This commit is contained in:
wzy-warehouse
2026-06-22 11:01:15 +08:00
parent 154f0a968e
commit d402668a5c
5 changed files with 290 additions and 24 deletions
+85 -9
View File
@@ -66,7 +66,65 @@ docker run -d ^
| `-v 输出目录:输出目录` | 文件输出目录读写挂载,保持主机与容器路径一致 |
| `sleep infinity` | 保持容器运行,等待 `docker exec` 调用 |
## 4. 安装中文字体(手动
## 4. 预拷贝静态数据到容器本地 FS(必须,性能关键
WSL2 9P 文件系统随机读取极慢(GPKG 62MB 耗时 6-10s/模板,模板 ZIP 也慢),拷贝到容器内 `/data/` 后读取仅需 ~0.5s。
### 方式一:Python 脚本(推荐)
```bash
python app/script/copy_data_to_container.py
```
输出示例:
```
=== 预拷贝静态数据到容器 qgis-server ===
[GPKG] 主机目录: F:\project\xian\xian_algorithm_new\app\data\gpkg
文件数: 23, 总大小: 62.3 MB
容器目标: qgis-server:/data/gpkg
拷贝完成: 3.2s, 容器内 23 个文件
[模板] 主机目录: F:\project\xian\xian_algorithm_new\app\data\template
文件数: 34, 总大小: 45.1 MB
容器目标: qgis-server:/data/template
拷贝完成: 2.1s, 容器内 34 个文件
=== 总耗时: 5.4s ===
```
其他用法:
```bash
python app/script/copy_data_to_container.py --dry-run # 仅查看信息
python app/script/copy_data_to_container.py --only gpkg # 只拷贝 GPKG
python app/script/copy_data_to_container.py --only template # 只拷贝模板
```
### 方式二:手动 docker cp
```bash
# 清理旧文件
docker exec qgis-server rm -rf /data/gpkg /data/template
# 拷贝 GPKG
docker cp F:\project\xian\xian_algorithm_new\app\data\gpkg qgis-server:/data/gpkg
# 拷贝模板
docker cp F:\project\xian\xian_algorithm_new\app\data\template qgis-server:/data/template
# 验证
docker exec qgis-server ls -la /data/gpkg
docker exec qgis-server ls -la /data/template/rainfall
docker exec qgis-server ls -la /data/template/earthquake
```
### 何时需要重新执行
- GPKG 或模板文件有更新时
- 容器重建后(`/data` 目录丢失)
- 首次部署时
## 5. 安装中文字体(手动)
QGIS 模板使用了 SimHei(黑体)、SimSun(宋体)、Microsoft YaHei(微软雅黑)等 Windows 中文字体,
Docker 镜像默认不包含这些字体,会导致中文全部乱码。**字体需手动安装,代码不会自动安装。**
@@ -122,7 +180,7 @@ docker run -d ... \
...
```
## 5. 验证容器
## 6. 验证容器
```bash
# 检查容器状态
@@ -135,7 +193,7 @@ docker exec qgis-server python3 -c "from qgis.core import QgsApplication; print(
docker exec qgis-server ls /app/app/services/qgis/
```
## 6. 配置文件
## 7. 配置文件
所有 Docker 相关配置集中在 `settings.toml``[default]` 段:
@@ -158,6 +216,8 @@ QGIS_DOCKER_KEEP_ALIVE = "sleep infinity" # 容器保活命令
# ---- 专题图参数 ----
QGIS_GPKG_DIR = "app/data/gpkg" # GPKG 目录(相对于项目根)
QGIS_DOCKER_GPKG_DIR = "/data/gpkg" # 容器内 GPKG 本地路径(预拷贝后读取)
QGIS_DOCKER_TEMPLATE_DIR = "/data/template" # 容器内模板本地路径(预拷贝后读取)
QGIS_EXPORT_DPI = 200 # 导出 DPI
QGIS_PARALLEL_WORKERS = 4 # 并行 docker exec 子进程数
QGIS_MAX_CONCURRENT = 2 # 最大并发请求数
@@ -184,7 +244,7 @@ export QGIS_DOCKER_PYTHON="/usr/bin/python3"
| `DB_HOST` | `"47.92.216.173"` | `"10.22.245.138"` |
| `DB_PORT` | `7654` | `54321` |
## 7. 目录结构
## 8. 目录结构
```
项目根目录/
@@ -200,6 +260,8 @@ export QGIS_DOCKER_PYTHON="/usr/bin/python3"
│ │ └── map_exporter.py # 图片导出
│ └── api/
│ └── qgis_map_export.py # FastAPI 专题图导出接口
├── script/
│ └── copy_data_to_container.py # GPKG + 模板预拷贝脚本
├── config.py # Dynaconf 配置入口
├── settings.toml # 全部配置
├── requirements.txt # Python 依赖
@@ -207,12 +269,12 @@ export QGIS_DOCKER_PYTHON="/usr/bin/python3"
└── tmp/ # 临时文件目录(容器内映射为 /app/tmp/)
```
## 8. 临时文件
## 9. 临时文件
- 主机端临时 JSON(批量产图配置):写入 `{项目根}/tmp/`,容器内可通过 `/app/tmp/` 访问
- 容器端临时 .qgz(修改后的模板):写入容器内 `/tmp/`,由 runner 自动清理
## 9. 故障排查
## 10. 故障排查
```bash
# 容器未运行
@@ -242,14 +304,28 @@ zh = [f for f in db.families() if any(k in f for k in ['SimHei','YaHei','SimSun'
print('中文字体:', zh if zh else '未安装!')
"
# 检查 GPKG 文件
docker exec qgis-server ls /app/app/data/gpkg/
# 检查 GPKG 文件(容器本地路径,性能关键)
docker exec qgis-server ls /app/app/data/gpkg/ # 挂载路径(慢)
docker exec qgis-server ls /data/gpkg/ # 本地路径(快,需预拷贝)
# 检查模板文件(容器本地路径)
docker exec qgis-server ls /data/template/rainfall/ # 本地路径(快)
docker exec qgis-server ls /data/template/earthquake/
# 检查临时文件目录
docker exec qgis-server ls /app/tmp/
```
## 10. 工作流程
### 静态数据相关问题
| 问题 | 原因 | 解决 |
|------|------|------|
| 产图慢(>60s) | GPKG/模板从挂载目录读取(9P 慢) | 执行 `python app/script/copy_data_to_container.py` |
| 日志显示 `gpkg_dir=/app/...` | 未预拷贝或 `QGIS_DOCKER_GPKG_DIR` 为空 | 检查 settings.toml 配置 |
| 容器重建后变慢 | `/data` 目录丢失 | 重新执行拷贝脚本 |
| `docker cp` 权限错误 | 容器未运行 | `docker start qgis-server` |
## 11. 工作流程
```
用户请求 POST /qgis/export/map
+9 -4
View File
@@ -175,10 +175,15 @@ def _build_qgis_config(batch_folder: str) -> dict:
is_docker = False
if is_docker:
# GPKG 目录:容器内路径 = 项目挂载目录 + GPKG 子目录
# GPKG 目录:优先使用容器内本地路径(预拷贝后绕过 WSL2 9P)
# 需先执行 python script/copy_gpkg_to_container.py
gpkg_dir = getattr(settings, "QGIS_DOCKER_GPKG_DIR", "") or ""
if not gpkg_dir:
# fallback: 使用挂载路径(性能差)
project_dir = get_docker_project_dir()
gpkg_subdir = getattr(settings, "QGIS_GPKG_DIR", "app/data/gpkg")
gpkg_dir = f"{project_dir}/{gpkg_subdir}"
logger.warning(f"GPKG 将从挂载目录读取(慢),建议执行 copy_gpkg_to_container.py")
# batch_folder:主机路径 → 容器路径
host_fs = get_host_file_store().rstrip("/")
container_fs = get_container_file_store().rstrip("/")
@@ -365,7 +370,7 @@ def _generate_batch_maps(models: list, config: dict, batch_key: str,
import json, math, concurrent.futures, subprocess, tempfile, threading
from app.services.qgis.qgis_env import (
get_docker_project_dir, get_container_python_path, build_docker_exec_cmd,
map_host_to_container, map_container_to_host,
map_host_to_container, map_container_to_host, map_template_to_container,
)
max_workers = getattr(settings, "QGIS_PARALLEL_WORKERS", 4)
@@ -397,9 +402,9 @@ def _generate_batch_maps(models: list, config: dict, batch_key: str,
cm = dict(m)
if "outFile" in cm:
cm["outFile"] = map_host_to_container(cm["outFile"])
# 模板 path 也需要映射:Windows主机路径 → 容器内路径
# 模板 path 映射到容器本地路径(预拷贝后绕过 9P)
if "path" in cm:
cm["path"] = map_host_to_container(cm["path"])
cm["path"] = map_template_to_container(cm["path"])
container_models.append(cm)
request = json.dumps({"config": config, "models": container_models}, ensure_ascii=False)
+159
View File
@@ -0,0 +1,159 @@
"""
将主机端 GPKG 和模板文件预拷贝到 Docker 容器本地文件系统。
WSL2 9P 文件系统随机读取极慢(GPKG 62MB 耗时 6-10s,模板 ZIP 也慢),
拷贝到容器内 /data/ 后读取仅需 ~0.5s。
用法:
python app/script/copy_data_to_container.py [--container qgis-server] [--dry-run] [--only gpkg|template]
前置条件:
Docker 容器已启动(docker start qgis-server
"""
import argparse
import os
import subprocess
import sys
import time
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
def _load_config():
"""从 settings.toml 读取配置"""
try:
from config import settings
return {
"gpkg_subdir": getattr(settings, "QGIS_GPKG_DIR", "app/data/gpkg"),
"template_subdir": "app/data/template",
"container": getattr(settings, "QGIS_DOCKER_CONTAINER", "qgis-server"),
"container_gpkg": getattr(settings, "QGIS_DOCKER_GPKG_DIR", "/data/gpkg"),
"container_template": getattr(settings, "QGIS_DOCKER_TEMPLATE_DIR", "/data/template"),
}
except ImportError:
pass
# fallback: 解析 TOML
cfg = {
"gpkg_subdir": "app/data/gpkg",
"template_subdir": "app/data/template",
"container": "qgis-server",
"container_gpkg": "/data/gpkg",
"container_template": "/data/template",
}
toml_path = PROJECT_ROOT / "settings.toml"
if toml_path.exists():
for line in toml_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
for key, prefix in [
("gpkg_subdir", "QGIS_GPKG_DIR"),
("container", "QGIS_DOCKER_CONTAINER"),
("container_gpkg", "QGIS_DOCKER_GPKG_DIR"),
("container_template", "QGIS_DOCKER_TEMPLATE_DIR"),
]:
if line.startswith(prefix) and "=" in line:
cfg[key] = line.split("=", 1)[1].strip().strip('"').strip("'")
return cfg
def _run(cmd, timeout=10, **kwargs):
"""subprocess.run 封装,统一 UTF-8 编码,避免 Windows GBK 报错"""
result = subprocess.run(
cmd, capture_output=True, timeout=timeout,
encoding="utf-8", errors="replace", **kwargs,
)
return result
def _check_container(container: str):
"""检查容器是否运行"""
result = _run(["docker", "inspect", "--format={{.State.Running}}", container], timeout=5)
if (result.stdout or "").strip() != "true":
raise RuntimeError(f"容器 {container} 未运行,请先 docker start {container}")
def _copy_dir_to_container(host_dir: Path, container: str, container_dest: str, label: str, dry_run: bool):
"""拷贝单个目录到容器"""
if not host_dir.is_dir():
print(f" [{label}] 目录不存在,跳过: {host_dir}")
return 0
files = list(host_dir.rglob("*"))
files = [f for f in files if f.is_file()]
if not files:
print(f" [{label}] 目录为空,跳过: {host_dir}")
return 0
total_size = sum(f.stat().st_size for f in files)
print(f" [{label}] 主机目录: {host_dir}")
print(f" 文件数: {len(files)}, 总大小: {total_size / 1024 / 1024:.1f} MB")
print(f" 容器目标: {container}:{container_dest}")
if dry_run:
print(f" [dry-run] 跳过")
return 0
# 确保目标目录存在
parent_dir = container_dest.rsplit("/", 1)[0]
_run(["docker", "exec", container, "mkdir", "-p", parent_dir])
# 清理旧目录
_run(["docker", "exec", container, "rm", "-rf", container_dest])
# docker cp
t0 = time.time()
result = _run(["docker", "cp", str(host_dir), f"{container}:{container_dest}"], timeout=120)
elapsed = time.time() - t0
if result.returncode != 0:
raise RuntimeError(f"[{label}] docker cp 失败: {result.stderr}")
# 验证
verify = _run(["docker", "exec", container, "find", container_dest, "-type", "f"])
stdout = (verify.stdout or "").strip()
count = len([l for l in stdout.splitlines() if l.strip()])
print(f" 拷贝完成: {elapsed:.1f}s, 容器内 {count} 个文件")
return elapsed
def copy_to_container(container: str = None, dry_run: bool = False, only: str = None):
"""将 GPKG 和模板拷贝到容器本地文件系统"""
cfg = _load_config()
if container is None:
container = cfg["container"]
_check_container(container)
t_total = time.time()
print(f"=== 预拷贝静态数据到容器 {container} ===\n")
if only != "template":
host_gpkg = PROJECT_ROOT / cfg["gpkg_subdir"]
_copy_dir_to_container(host_gpkg, container, cfg["container_gpkg"], "GPKG", dry_run)
print()
if only != "gpkg":
host_template = PROJECT_ROOT / cfg["template_subdir"]
_copy_dir_to_container(host_template, container, cfg["container_template"], "模板", dry_run)
elapsed = time.time() - t_total
print(f"\n=== 总耗时: {elapsed:.1f}s ===")
def main():
parser = argparse.ArgumentParser(description="将 GPKG 和模板预拷贝到 Docker 容器本地 FS")
parser.add_argument("--container", default=None, help="Docker 容器名称")
parser.add_argument("--dry-run", action="store_true", help="仅显示信息,不实际拷贝")
parser.add_argument("--only", choices=["gpkg", "template"], help="只拷贝指定类型")
args = parser.parse_args()
try:
copy_to_container(container=args.container, dry_run=args.dry_run, only=args.only)
except Exception as e:
print(f"错误: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
+27
View File
@@ -167,6 +167,33 @@ def map_container_to_host(path: str) -> str:
return path
def map_template_to_container(host_path: str) -> str:
"""将主机端模板路径映射到容器内本地路径(预拷贝后绕过 9P)。
主机: F:/project/xian/xian_algorithm_new/app/data/template/rainfall/xxx.qgz
容器: /data/template/rainfall/xxx.qgz
"""
try:
from config import settings
container_tpl = getattr(settings, "QGIS_DOCKER_TEMPLATE_DIR", "") or ""
except Exception:
container_tpl = ""
if not container_tpl:
# fallback: 用通用映射(走 9P,慢)
return map_host_to_container(host_path)
normalized = host_path.replace("\\", "/").lower()
# 找 "app/data/template/" 后面的部分
marker = "app/data/template/"
idx = normalized.find(marker)
if idx >= 0:
relative = host_path.replace("\\", "/")[idx + len(marker):]
return f"{container_tpl.rstrip('/')}/{relative}"
return map_host_to_container(host_path)
def build_docker_volume_mounts() -> list:
"""
构建 Docker 卷挂载参数列表(用于 docker run)。
+7 -8
View File
@@ -8,9 +8,13 @@ RAIN_STATION_GRID_DIR = "/xian/rainfall/grid/images/:id"
REDIS_RAIN_STATION_GRID_KEY = "xian:rainfall:rain_station_grid"
REDIS_RAIN_STATION_IDENTIFIER_KEY = "xian:rainfall:rain_station_identifier"
PREDICT_PROBABILITY_THRESHOLD = 50
# 静态底图 GeoPackage 目录(相对于项目根目录)
# 静态底图 GeoPackage 目录
QGIS_GPKG_DIR = "app/data/gpkg"
# 专题图输出子目录(相对于 FILE_STORE_DIR
# 容器内 GPKG 本地路径
QGIS_DOCKER_GPKG_DIR = "/data/gpkg"
# 容器内模板本地路径
QGIS_DOCKER_TEMPLATE_DIR = "/data/template"
# 专题图输出子目录
QGIS_OUTPUT_DIR = "xian/qgis/map/:eventType/:inferenceId"
# 专题图默认参数
QGIS_DEFAULTS_MAP_LAYOUT = "A4"
@@ -21,7 +25,7 @@ QGIS_DEFAULTS_MAP_UNIT = "制图单位:西安市应急管理局"
QGIS_EXPORT_DPI = 200
# 并行 docker exec 子进程数
QGIS_PARALLEL_WORKERS = 4
# 最大并发请求数(防止多人同时触发资源耗尽)
# 最大并发请求数
QGIS_MAX_CONCURRENT = 2
# ============================================================
# Docker QGIS 配置
@@ -91,11 +95,6 @@ REDIS_DB = 0
# 文件路径配置
# ============================================================
FILE_STORE_DIR = "G:/files"
# ============================================================
# 专题图输出子目录
# ============================================================
QGIS_OUTPUT_DIR = "xian/qgis/map/:eventType/:inferenceId"
QGIS_DEFAULTS_MAP_UNIT = "制图单位:西安市应急管理局"
# ============================================================
# 生产环境