## 性能:瓶颈在传输,不在代码
先量后改。helper 冷启动 0.44s(编译 22173 行)看着像瓶颈,但
drouter-helpd 是常驻守护,请求走 Unix socket,正常路径根本不 fork;
实测各 API 已是毫秒级。app.js 语法编译 18ms、顶层执行 18ms
(393 个顶层函数 / 13401 行)—— JS 执行不是瓶颈。
真瓶颈是 app.js 690,482 字节每次重传。**决定不拆包**:JS 执行只 18ms,
拆包引入首屏 waterfall 的风险大于收益。改走三条:
1. 缓存策略按「有没有指纹」区分(不是按扩展名)
- 带 ?v=<mtime> → public, max-age=31536000, immutable
- 没指纹 → 仍 no-cache(否则部署后用户拿到旧文件)
- index.html → 永远 no-store(每次都要重新注入)
2. 编码协商 br > gzip > 原文,**逐级降级**
3. 压缩结果按 (path, mtime, size, enc) 缓存
真机实测:首次 690,482 → 200,137 字节,第二次打开 0 字节(304)。
## 逐级降级不是可选项
目标机**没装 python3-brotli**(只有 C 库 libbrotli1)。最初写的
_brotli_cached 在模块缺失时 `return data, None`,而 None 是 encoding,
调用方理解为「压不动,原样输出」—— 于是 583KB 裸传,**比优化前更慢**。
改成 (None, None) 明确表示「这条路不通」,由 _encode_body 继续试 gzip。
同时修:Accept-Encoding 探测必须认 q=0(br;q=0 是「明确不要」,
子串匹配会正好发给解不开的客户端 → 满屏乱码);编码必须进缓存键
(共用键会互相覆盖);brotli 固定 q5(q11 只省 10KB 却要 1.5-3s)。
## 宽 except 把 bug 装成了特性
兜底只接 ImportError 这类**真·预期**的缺失,不接 TypeError/ValueError
(「str 传给要 bytes 的接口」也是 TypeError,接了等于没接)。
早先的 `except Exception` 吞掉了一个 NameError(漏注入 _comp_lock),
表现是压缩功能完好但从不生效,页面照常能开、只看响应头的判据完全发现不了。
## 版本号:建立单一真源 packaging/VERSION
原来版本号在 4 个构建脚本各写死一个默认值 + openapi 里硬编码。
漏改一处就产出「文件名 1.0.9、control 里 1.0.8」的包 —— apt 按
control 认版本,dpkg -l 永远显示旧版且升不上去。
index.html 的 ETag 并入版本号:它发出去的是注入后的内容,
ETag 却按原文件算,只改 VERSION 文件时 ETag 不变,客户端换到 304
继续用旧副本,页面版本号永远停在旧版。
## logo 回概述
用 <a> 而不是 <div onclick>:Tab 能聚焦、Enter 能触发、读屏会念「链接」。
已在 dash 页时不重复 go()(go 会 stopPageTimers + 重拉数据)。
CSS 显式去 text-decoration/color(UA 样式表不声明仍生效),
:focus-visible 焦点环保留(纯键盘用户唯一的路标)。
## 顺带修掉一个已发布版本的真 bug
_vpn_env 里 os.path.join('/lib/modules', ...) 被留成了 '/nonexistent'
(反向验证脚本被 SIGTERM 打断,finally 没执行,注入留在源码里),
于是 mod_file 永远为空、**need_module 整个状态从四态里消失** ——
这正是 v1.0.8 release notes 声称修好的那个 bug(PVE 的 KVM 虚机内核
明明有 wireguard.ko.xz 却报 unsupported,前端配一句写死的
「通常说明跑在精简容器里」把排查方向带偏)。
新增 _dev/t-worktree-clean.py 扫注入哨兵,放在 sync.sh 所有判据之前 ——
基线被污染时,后面每一条红都不可信。
## 判据
新增 t-static-perf.py(42 项静态)、t-encoding-runtime.py(26 项**真跑**,
覆盖 9 种 Accept-Encoding 组合、brotli 缺失降级、两种编码不互相覆盖、
压不动直出、内部错误不被吞)、t-worktree-clean.py。
三个都做了反向验证,注入后确实变红。
反向验证本身也踩了坑:判据里 len(body) 在 body 为 None 时先崩 ——
退出码非 0 但没有 FAIL 行,看着像「跑了」,实际没人知道它验出了什么。
取长度一律过 _sz() 包一层。
全量回归 54 个判据 0 失败;真机验收 22 项全通过;
前端三查 47 视图 0 异常、26 项移动端适配全过。
81 lines
3.5 KiB
Docker
81 lines
3.5 KiB
Docker
# =============================================================================
|
||
# drouter —— Docker 镜像
|
||
#
|
||
# 定位:用于「快速体验 / 开发调试 / CI 渲染校验」。
|
||
# 真正当主路由跑,请直接装在 Debian 13 物理机或虚拟机里(需要直接操作网卡与
|
||
# nftables,容器只能以 --network host + NET_ADMIN 勉强模拟)。
|
||
#
|
||
# 构建:bash packaging/build-docker.sh [tag]
|
||
# 运行:docker run -d --name drouter \
|
||
# --network host --cap-add NET_ADMIN --cap-add NET_RAW \
|
||
# -v drouter-data:/opt/drouter/data \
|
||
# -v drouter-etc:/etc/drouter \
|
||
# drouter:1.0.8
|
||
# =============================================================================
|
||
FROM debian:trixie-slim
|
||
|
||
LABEL maintainer="火麒麟 (ajeef) <https://github.com/liuzhuohua>" \
|
||
org.opencontainers.image.title="drouter" \
|
||
org.opencontainers.image.description="Debian 13 主路由管理系统(Web 管理面板)" \
|
||
org.opencontainers.image.licenses="MIT"
|
||
|
||
ENV DEBIAN_FRONTEND=noninteractive \
|
||
PYTHONUNBUFFERED=1 \
|
||
TZ=Asia/Shanghai
|
||
|
||
# 1) 依赖。只装运行 Web 面板与渲染配置所必需的包;
|
||
# 不装 dnsmasq/radvd —— 容器里默认不启动,装了也不会跑。
|
||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||
python3 \
|
||
nftables \
|
||
iproute2 \
|
||
iputils-ping \
|
||
ethtool \
|
||
conntrack \
|
||
bridge-utils \
|
||
sqlite3 \
|
||
curl \
|
||
procps \
|
||
sudo \
|
||
openssl \
|
||
ca-certificates \
|
||
tzdata \
|
||
&& rm -rf /var/lib/apt/lists/*
|
||
|
||
# 2) 程序文件
|
||
RUN mkdir -p /opt/drouter/backend /opt/drouter/web /opt/drouter/docs \
|
||
/opt/drouter/data /opt/drouter/snapshots /opt/drouter/certs \
|
||
/opt/drouter/scripts /opt/drouter/bin \
|
||
/etc/drouter/generated /etc/drouter/themes \
|
||
/var/log/drouter /run/drouter-helper
|
||
COPY backend/*.py /opt/drouter/backend/
|
||
COPY web/ /opt/drouter/web/
|
||
# 只拷顶层 .md:docs/screenshots/ 是给 GitHub README 用的 78 张截图,
|
||
# 进镜像纯属白白撑大体积
|
||
COPY docs/*.md /opt/drouter/docs/
|
||
COPY scripts/*.sh /opt/drouter/scripts/
|
||
# 版本号(唯一真源)。页面底部与 openapi 的 version 字段都读它 ——
|
||
# 不拷的话容器里只能退回硬编码兜底值,镜像 tag 是 1.0.9 页面却显示 1.0.8。
|
||
COPY packaging/VERSION /opt/drouter/VERSION
|
||
COPY packaging/docker/docker-init.sh /opt/drouter/bin/docker-init.sh
|
||
RUN chmod +x /opt/drouter/bin/docker-init.sh /opt/drouter/scripts/*.sh
|
||
|
||
# 3) 运行用户(容器里以低权用户跑 Web,写 /etc 走 helper 白名单)
|
||
RUN useradd -r -s /usr/sbin/nologin -d /opt/drouter -M drouter \
|
||
&& printf 'drouter ALL=(root) NOPASSWD: /usr/bin/python3 /opt/drouter/backend/drouter-helper.py *\n' \
|
||
> /etc/sudoers.d/drouter \
|
||
&& chmod 0440 /etc/sudoers.d/drouter \
|
||
&& chown -R drouter:drouter /opt/drouter/data /opt/drouter/snapshots /opt/drouter/certs /var/log/drouter \
|
||
&& chmod 700 /run/drouter-helper
|
||
|
||
EXPOSE 8080 8443
|
||
VOLUME ["/opt/drouter/data", "/opt/drouter/snapshots", "/etc/drouter", "/var/log/drouter"]
|
||
|
||
# 端口要跟着实际配置走,写死 8443 会在用户改端口后一直报 unhealthy
|
||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||
CMD curl -sk "https://127.0.0.1:$(cat /etc/drouter/web-port 2>/dev/null || echo 8443)/api/health" \
|
||
|| exit 1
|
||
|
||
ENTRYPOINT ["/opt/drouter/bin/docker-init.sh"]
|
||
CMD ["/usr/bin/python3", "/opt/drouter/backend/drouter-web.py"]
|