跳转至

xunx · 多用户复用服务

xunx 用同一个镜像为每个用户维护一个常驻容器,再用单个网关进程按 URL 前缀转发。用户看到的是 http://网关:18960/alice/chat/,容器内部跑的仍是 xuns --base-path /alice。

xunx user-add alice
xunx serve --host 0.0.0.0 --port 18960

概念

概念 说明
用户库 SQLite,路径 {XUN_HOME}/x/xunx.db,字段 name / token / generation / paused(旧库启动时自动补列)
令牌 每个用户一个随机令牌(secrets.token_urlsafe(24)),同时作为其容器的 xuns --token=<token>;用 = 拼接传参,因此以 - 开头的令牌不会被 xuns 误当作选项
URL 前缀 固定为 /{username},容器据此生成正确的相对链接
容器名 xunx-<实例ID前8位>-<用户名>,实例 ID 为用户库路径的哈希
端口 在 --port-range 内随机挑选空闲端口,仅绑定 127.0.0.1(宿主端口 = 容器端口)
代次 generation 每次 upgrade 递增;对账时发现代次过期就重建容器
暂停 paused 每个用户一个持久标记;对账时用 docker pause/unpause(cgroup freezer)冻结或解冻容器,不删除容器,进程内存与已保存会话全部保留。user-list 的 STATE 列显示的就是这个标记(期望状态),不是容器的实时状态
对账 reconcile serve 启动时先跑一次,其后每 --interval 秒一次:接管已有容器、补起缺失容器、清理越界容器、同步暂停状态;孤儿容器清理只在首次对账执行一次

子命令

xunx user-add alice

用户名只允许字母、数字、_、-;重复添加报错。输出用户与令牌表格,令牌即访问密码。

xunx user-del alice      # 删除用户;其容器在下次对账时被清理
xunx user-list           # 列出 USER / TOKEN / BASE PATH / STATE(数据库里的期望状态)
xunx pause alice         # 冻结该用户的容器
xunx resume alice        # 解冻,容器接着上次的状态继续跑

两个命令只改写数据库里的 paused 标记,并提示「The container state changes on the next serve reconciliation」;真正的 docker pause/unpause 由运行中的 serve 在下一次对账时执行,用户不存在时报错。与 upgrade 不同,暂停不丢弃容器内数据。

xunx upgrade alice       # 或 xunx upgrade --all

只递增数据库里的 generation 并提示「Queued upgrade」,真正的重建发生在下一次 serve 对账时;容器内数据(工作区、已保存会话)会随之丢弃。

参数 默认 说明
--host 0.0.0.0 网关监听地址
--port 18960 网关端口(会从可用端口池里排除)
--port-range START-END,默认覆盖 17960-18958 容器可用端口区间;代码默认值是 range(17960, 18959),右端点开,因此 18959 不在默认池内
--image xun 使用的镜像
--env 空 与 xunc --env 同义:NAME=VALUE 赋值或通配符转发;XUN_*/_XUN_* 总是转发
--interval 5 对账间隔秒数(帮助中隐藏),必须大于 0

请求如何被转发

flowchart LR
  U["浏览器<br/>/alice/chat/"] --> G["xunx 网关<br/>:18960"]
  G --> R["用户库<br/>用户名与令牌"]
  G --> C["容器内 xuns<br/>--base-path /alice"]
  C -.->|"相对链接自带 /alice 前缀"| G
  • 路径原样转发(含 /alice 前缀与查询串),不做改写:容器内的 xuns 已经通过 --base-path 知道自己在 /alice 下。
  • 用户名不存在 → 404 User not found;上游不可用 → 502 Upstream unavailable。
  • 该用户的容器被暂停时,网关自行回 503 并带上 Retry-After: 60:前端路径(/<user>、/<user>/login、/<user>/chat/*、/<user>/docs/*)返回一份「Temporarily unavailable」提示页,其余路径返回纯文本 Service paused。
  • 支持 WebSocket(事件流),双泵转发,心跳 20 秒;逐跳头部(connection、transfer-encoding、upgrade 等)双向剥离。
  • 容器打印的是它自己的地址 http://0.0.0.0:<容器端口>/alice/chat/?session=%2F&token=<令牌>(并附 localhost:<容器端口> 等价形式,宿主端口与容器端口相同);要走网关得把主机换成 http://<网关>:18960,/alice 前缀不变。

持久化与对账

serve 重启后不会重建容器:对账时先按容器名接管正在运行(含已暂停)的容器(读取其发布端口与标签),只有令牌或代次过期时才重建;用户被删除或容器已退出时容器会被停止;prune 只清理带本实例 xunx.instance 标签的容器(首次对账时把本实例遗留的孤儿容器一并清掉),其它实例的容器不受影响。新建容器的日志会被转发到 serve 的 stderr,行首带容器名(serve 重启后接管来的容器不转发日志)。容器以 auto_remove=True 创建,docker daemon 重启会把它们一并带走,serve 只能在下次对账时重建(存在容器内的会话随之丢失)。

每轮对账都把容器状态向数据库里的标记收敛:令牌与代次决定是否重建,paused 决定冻结与否。

flowchart LR
  S([serve 启动]) --> R["running"]
  R -->|"xunx pause"| P["paused"]
  P -->|"xunx resume"| R
  R -->|"xunx upgrade:换镜像重建"| R
  P -->|"xunx upgrade:重建后仍冻结"| P
  R --> E(["用户被删除 / 容器退出"])
  P --> E

upgrade 会让容器换新镜像重建:如果这个用户处于暂停状态,重建出来的容器会立刻被重新冻结。

xunx user-list            # STATE 列是数据库里的期望状态,容器在下一次对账后与之对齐
xunx upgrade --all        # 镜像更新后,让所有用户在下次对账时用新镜像重建

对外发布

网关监听 0.0.0.0 时,令牌是唯一凭据;生产环境建议置于 HTTPS 反向代理之后,并透传 X-Forwarded-Proto,这样登录 Cookie 才会带上 Secure。uvicorn 默认只信任来自 127.0.0.1 / ::1 的 X-Forwarded-*,反代不在本机时需另外设置 FORWARDED_ALLOW_IPS。