第 8 章:亲手运行一个受限任务
第一步:准备数据和计算程序
用文本编辑器创建 input/sales.csv,内容如下:
item,amount
book,120
course,80
notebook,50再创建 input/analyze.py:
import csv
import json
from decimal import Decimal
from pathlib import Path
with Path('/input/sales.csv').open(newline='') as f:
rows = list(csv.DictReader(f))
total = sum((Decimal(r['amount']) for r in rows), Decimal('0'))
report = {'rows': len(rows), 'total': str(total)}
Path('/output/report.json').write_text(
json.dumps(report, ensure_ascii=False), encoding='utf-8'
)
print('Report saved.')Decimal 用来按十进制计算金额。示例只处理已知的小文件;真实系统还应检查文件大小、字段名、金额格式和异常数据,不能无界读取陌生文件。
第二步:创建控制器
在实验目录创建 run_demo.py。下面的代码直接调用 Docker,不会安装 Python 依赖。它显式指定一个容器名,因此超时后能找到并停止同一个容器。
import json
import os
from pathlib import Path
import subprocess
import uuid
base = Path(__file__).resolve().parent
out = base / 'output'
out.mkdir(exist_ok=True)
report = out / 'report.json'
if report.exists():
raise SystemExit('请先把旧 report.json 移走,再运行。')
name = 'sandbox-demo-' + uuid.uuid4().hex[:12]
uid = os.getuid()
gid = os.getgid()
cmd = [
'docker', 'run', '--rm', '--pull=never', '--name', name,
'--network=none', '--read-only',
'--memory=128m', '--memory-swap=128m', '--cpus=0.5',
'--pids-limit=32', '--cap-drop=ALL',
'--security-opt=no-new-privileges:true',
'--user', f'{uid}:{gid}',
'--tmpfs', '/tmp:rw,noexec,nosuid,size=16m,mode=1777',
'--mount', f'type=bind,src={base / "input"},dst=/input,readonly',
'--mount', f'type=bind,src={out},dst=/output',
'python:3.12-slim', 'python', '-B', '/input/analyze.py'
]
try:
result = subprocess.run(cmd, timeout=30, check=False)
if result.returncode != 0:
raise SystemExit(f'任务失败,退出码:{result.returncode}')
if report.is_symlink() or not report.is_file():
raise SystemExit('没有找到普通报告文件')
if report.stat().st_size > 4096:
raise SystemExit('报告过大')
data = json.loads(report.read_text(encoding='utf-8'))
if data != {'rows': 3, 'total': '250'}:
raise SystemExit(f'报告与预期不符:{data}')
print('验证通过:3 笔数据,总额 250。')
except subprocess.TimeoutExpired:
raise SystemExit('任务超过 30 秒,正在清理容器。')
finally:
cleanup = subprocess.run(
['docker', 'rm', '-f', name],
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
text=True, timeout=15
)
if cleanup.returncode and 'No such container' not in cleanup.stderr:
print('请检查容器是否已清理:', name)在普通 Linux 环境中,使用当前用户的 UID/GID 有助于保证绑定目录可写。Docker Desktop 会处理宿主机文件共享,但企业策略、SELinux、rootless 模式和用户映射可能影响访问;报错时先查看具体原因,不要直接取消所有权限限制。包含逗号的目录路径会影响 –mount 参数解析,本实验请使用简单路径。
第三步:运行并观察
python3 run_demo.py
cat output/report.json预期先看到 Report saved.,再看到验证通过,报告包含 rows 为 3、total 为字符串 250。JSON 键顺序不重要。你已经完成一次“控制器提交任务—受限环境执行—控制器校验产物”的流程。
这里的绑定输出目录会真实修改宿主机的实验目录。只给一个空的专用 output 目录,是为了让这些写入有明确范围。不要给已有的重要文档目录作为输出。
每个限制做了什么
–network=none 关闭容器的常规外部网络连接;–read-only 让容器根文件系统只读;输入挂载的 readonly 保护输入目录免遭容器修改。输出挂载允许程序写报告。
–memory 与 –memory-swap 在支持的 Linux 配置中限制内存和交换使用;实际支持以 Docker 和内核为准。–cpus 限制 CPU 配额,–pids-limit 限制进程数量。Python 控制器的 timeout 给 Docker 客户端设置等待上限,并在 finally 中显式清理容器。[3]
这些限制没有给宿主机 output 目录设置硬磁盘配额,也没有实现多租户隔离、恶意产物检查或全局费用限制。它是理解原理的教学起点。