跳转到内容

热重载

katana 无需重启即可根据两个来源更新运行状态。保存配置文件后,katana 会重新加载它并应用差异;面板修改节点或其用户后,katana 会在下一次轮询时获取变化。大多数变化在几秒内生效,有些会断开节点上的现有连接。只有单独修改的 [dns],以及续期后的证书或 geodata 文件,要等重启或之后的某次重建才会生效。

本页面向需要修改运行中节点配置的运维人员,以及需要预判面板变化会对已连接用户造成什么影响的人。内容基于 katana v3.0.1。配置项本身见配置文件。

来源 katana 何时察觉 详见
配置文件 文件写入后约 500 ms 文件变化
面板 节点的下一次轮询,每 update_periodic 秒一次(默认 60) 面板变化

katana 没有重载信号,也没有重载命令。它不处理 SIGHUP,因此该信号按默认行为立即结束进程,不会像 SIGTERM 或 SIGINT 关闭时那样发送最后一次流量上报。

katana 启动时监视的是配置文件所在的目录,而不是文件本身。如果使用相对路径,例如默认的 -c config.toml,这个目录就是工作目录。编辑器保存文件时常常是先写一个新文件,再把它重命名覆盖旧文件;监视目录可以同时捕获这两种保存方式。监视不是递归的。

该目录中的任何事件都会触发一次重载,包括对证书等无关文件的写入。在该目录中打开文件也算,因此对该目录中的文件执行 cat 或 katana --test 同样会触发重载。随后 katana 会:

  1. 等待 500 ms,并丢弃这段时间内到达的所有事件,使一次保存产生的一连串写入只触发一次重载;
  2. 重新读取并解析配置文件;
  3. 将结果与正在运行的配置比较,只应用有差异的部分。

如果重载发现解析后的配置没有变化,就什么也不应用,因此只改注释或空白的保存没有任何代价。被 katana 拒绝的重载会让正在运行的配置保持原样,因此下一个事件会再次比较同一个文件,并再次拒绝它(见第 2 步)。

katana 自己读取配置文件,也是被监视目录中的一个事件。因此,一旦第一个事件到达,katana 在整个运行期间都会每隔约 500 ms 重新读取并比较一次文件,无论你是否保存过。对于未变化的文件,代价只是一次读取和一次比较。但这也意味着:

  • 无法解析的文件会以大约每秒两次的频率输出下文的 config reload failed 日志,直到你修正为止;
  • 被拒绝的出站池会以同样的频率输出 reload: bad outbounds;
  • 包含无法构建的节点的文件会以同样的频率输出 reload: node <name>: <error>; keeping current config。

如果配置路径是指向另一个目录的符号链接,就不会出现这种情况,因为此时的读取发生在目标文件所在的目录。

如果文件无法解析,katana 会记录错误并保留正在运行的配置。什么都不会被应用,即使文件中有效的部分也不会:

ERROR katana::runtime: config reload failed, keeping current: config parse error: TOML parse error at line 14, column 1
|
14 | listen_ipp = "0.0.0.0"
| ^^^^^^^^^^
unknown field `listen_ipp`, expected one of `listen_ip`, `send_ip`, `update_periodic`, `disable_upload_traffic`, `disable_get_rule`, `disable_sniffing`, `cert`

文件不存在或不是 UTF-8 时,也会出现同样的 config reload failed, keeping current 日志,只是原因不同。文件一旦能再次解析,katana 就会应用它:保存修正本身就是一个事件,上文所述的反复读取也会发现它。

flowchart TB
  E["配置目录中出现事件"] --> W["等待 500 ms,丢弃排队的事件"]
  W --> P{"文件能否解析?"}
  P -- 否 --> K["保留正在运行的配置"]
  P -- 是 --> O{"outbound 列表是否变化?"}
  O -- 是 --> B{"新出站池能否构建?"}
  B -- 否 --> K
  B -- 是 --> C{"新增节点和有变化的面板客户端能否构建?"}
  O -- 否 --> C
  C -- 否 --> K
  C -- 是 --> L["应用日志级别"]
  L --> S["把新池交给每个节点"]
  S --> R["停止被移除的节点"]
  R --> U["更新有变化的节点"]
  U --> N["启动新节点"]
  1. 出站池。 如果任何 [[outbound]] 条目有变化,katana 会先构建完整的新池:内置的 direct、block、freedom 和 blackhole 处理器、每个 [[outbound]] 条目,以及它们共用的 [dns] 解析器。如果池构建失败,katana 会拒绝整次重载并记录 reload: bad outbounds, keeping current config: …。日志级别、节点以及其他一切都保持原样。

  2. 节点检查。 在应用任何内容之前,katana 会构建文件新增的每个节点,包括身份发生变化的节点:构建其面板客户端,以及路由器(如果第 1 步构建了新池,则基于新池编译)。对于 [[node]] 表有变化的每个运行中节点,katana 会构建新的面板客户端。其中任何一项失败,katana 都会拒绝整次重载,并记录是哪个节点导致的:

    ERROR katana::runtime: reload: node newv2board@https://panel.example.com#1/v2rayy: unknown node_type "V2rayy"; keeping current config

    规则无法编译的新节点会记录 reload: node <name>: build router: …; keeping current config。此时什么都不会被应用:不应用日志级别,不替换出站池,也不移除、修改或添加任何节点。每个节点都照旧运行。

  3. 日志级别。 如果 [log].level 有变化,katana 会替换日志过滤器并记录 reload: log level → <level>。删除该配置项会设为 info。新过滤器会取代启动时从 RUST_LOG 取得的过滤器。

  4. 替换出站池。 如果构建了新池,katana 会把它交给每个节点并记录 reload: outbound pool rebuilt。每个节点都会基于新池重新编译路由器并重建监听器,无论其规则是否用到了发生变化的出站。

  5. 被移除的节点。 正在运行的节点,如果其身份在文件中已不存在,就会被停止:监听器关闭,其上的所有连接结束,并向面板发送最后一次流量上报(除非开启了 disable_upload_traffic),在 SSPanel 上还会发送最后一次审计上报。katana 会等这些上报完成后再继续。每个请求受面板超时(api.timeout,默认 5 秒)限制,因此面板响应缓慢时,重载的其余部分最多可能被推迟两倍于此的时间。如果最后一次流量上报失败,katana 不会重试。

  6. 有变化的节点。 身份不变、但 [[node]] 表有差异的节点会收到新设置,katana 记录 reload: reconfigured node <name>。节点自行应用这些设置,见各项修改的效果。

  7. 新节点。 第 2 步构建的每个节点都会被启动:联系面板、获取用户并绑定端口,并一直重试直到上线(见无法上线的节点)。

katana 先构建新节点,再移除任何节点,并在移除之后才启动新节点。当一次保存改变了某个节点的身份,或用另一个使用相同端口的节点替换了它时,旧节点会先释放端口,新节点再绑定。

katana 根据身份把新文件中的每个 [[node]] 与正在运行的节点对应起来。身份由五个值组成:

值 比较方式
panel_type 不区分大小写:NewV2board 与 newv2board 相同。NewV2board 与 V2board 不同,尽管两者使用同一种面板客户端
api.host 按原文精确比较:https://panel.example.com 与 https://panel.example.com/ 不同
api.node_id 按数值比较
api.key 按原文精确比较
katana 向面板请求的节点类型 仅限 NewV2board 和 V2board:当 api.node_type 是 V2ray 类的值(V2ray、Vmess 或 Vless)且 api.enable_vless = true 时为 vless,否则为小写的 api.node_type。SSPanel 没有这个值

这些值决定该条目服务于哪个面板节点。NewV2board 和 V2board 的 UniProxy API 按节点 ID 和所请求的节点类型查找节点,因此同一个 ID 以 vless 请求和以 vmess 请求是两个面板节点,各有自己的用户和流量。SSPanel 只按节点 ID 查找节点。

修改这五个值中的任何一个,katana 都会停止旧节点(最后一次上报仍按旧值发送),并按新值启动一个全新的节点。新节点的流量计数器从零开始,因此一个面板节点的流量永远不会上报给另一个面板节点;它还会重新从面板获取一切。例如在 NewV2board 上、enable_vless 关闭时,把 node_type 从 V2ray 改为 Vmess 会启动全新的节点,因为小写后的值变了;而从 V2ray 改为 v2ray 则不会。在 Trojan 节点上开启 enable_vless 不会改变身份。

[[node]] 表中的其他内容都是同一节点的设置。文件中 [[node]] 表的顺序无关紧要,可以随意调整。

日志按面板类型、主机、节点 ID 以及(在 NewV2board 和 V2board 上)节点类型来标识节点,从不使用其密钥:reload: removing node newv2board@https://panel.example.com#1/v2ray,在 SSPanel 上则是 reload: removing node sspanel@https://panel.example.com#1。本页把这个名称写作 <name>。

无论节点是 katana 启动时启动的,还是由重载添加的,它都会一直重试直到上线。每次尝试都会丢弃之前尝试得到的 ETag,获取节点设置,获取用户列表,并绑定端口。某一步失败时,节点会记录错误并等待:

ERROR katana::manager::node: node 1: node_info failed: …; retrying in 1s
日志行中的原因 失败的环节
node_info failed: … 请求节点设置
panel returned no node info 面板以 304 Not Modified 应答,没有返回节点设置
panel returned port 0 节点设置中的端口为 0。仅见于 SSPanel:在 NewV2board 和 V2board 上,端口为 0 会显示为 node_info failed: newV2board: server port must be > 0
user_list failed: … 请求用户列表
panel returned no user list 面板以 304 Not Modified 应答,没有返回用户列表
initial start failed: … 构建或绑定监听器,例如端口被占用或证书文件缺失

第一次等待 1 秒,每次失败后翻倍,最长 60 秒,且不超过 update_periodic(至少 1 秒)。使用默认的 update_periodic = 60 时,等待时间依次为 1、2、4、8、16、32、60、60 … 秒。

节点等待期间:

  • 对其自身 [[node]] 表的修改会被保存,下一次尝试立即以新设置开始;
  • 新的出站池会被保存,供下一次尝试使用,但不会让下一次尝试提前开始;
  • SIGINT 或 SIGTERM,或从文件中删除该节点,会停止重试。

“监听器重建”指 katana 关闭节点的监听器,结束其上的所有连接(包括仍在握手中的连接),然后用新设置重新绑定端口。流量计数在重建后保留,已经中继的字节仍会上报。

文件中的修改 何时生效 现有连接
[log].level 立即 保留
添加、删除或修改任何 [[outbound]] 新池构建成功后立即 所有节点上都断开
只改 [dns],[[outbound]] 未变 不应用,也不检查。在下一次修改 [[outbound]] 时或重启时生效。有问题的 [dns] 会导致下一次 [[outbound]] 修改整体失败 保留
添加 [[node]] 立即 尚无连接
删除 [[node]] 最后一次上报后立即 该节点上断开
身份值:panel_type(大小写以外的变化)、api.host、api.node_id、api.key,以及在 NewV2board 和 V2board 上 katana 请求的节点类型 立即:节点停止后重新启动 该节点上断开
api.enable_vless NewV2board 和 V2board 上 node_type 为 V2ray 或 Vmess 时:属于身份变化,节点停止后重新启动。其他情况(包括 SSPanel):立即,通过新的面板客户端和监听器重建生效 该节点上断开
api.node_type NewV2board 和 V2board:katana 请求的节点类型变化时属于身份变化;只改大小写时与下一行相同。SSPanel:立即,通过新的面板客户端和一次立即轮询生效,如果协议变了,协调阶梯会重建监听器 NewV2board 和 V2board:断开,只改大小写时除外。SSPanel:保留,除非协议变了
api.vless_flow、api.disable_custom_config、api.speed_limit、api.rule_list_path、api.timeout,或 panel_type 的大小写 立即,通过新的面板客户端生效。修改 speed_limit 会原地刷新用户表,已经打开的流仍按旧速率。修改 rule_list_path 会让新的审计规则立即作用于新的流 保留,除非重新读取的节点设置改变了协议或传输层
controller.listen_ip 立即:监听器重建 该节点上断开
[node.controller.cert] 中的任何配置项 立即:监听器重建,并重新读取证书和私钥文件 该节点上断开
controller.disable_sniffing 立即:监听器重建 该节点上断开
[node.route] 中的任何配置项或规则 立即:节点编译新路由器,然后重建监听器。无法编译的路由会被拒绝,见下文 该节点上断开
[node.hysteria] 中的任何配置项:port、obfs、obfs_password、credential、udp、udp_idle_timeout、[node.hysteria.masquerade] 立即:监听器重建。设置或清除 port 会让 Hysteria 2 节点在本地描述和面板描述之间切换。obfs 和 obfs_password 只在设置了 port 时有效;port = 0 时使用面板的混淆设置 该节点上断开
controller.update_periodic 立即:轮询计时器重新开始,下一次轮询在一个新周期之后 保留
controller.disable_upload_traffic 下一次流量上报时 保留
controller.disable_get_rule 下一次轮询时。开启后,已加载的审计规则仍会保留 保留
controller.send_ip、api.device_limit katana 接受这些配置项,但不使用。device_limit 作为 [node.api] 中的配置项,修改它仍会构建新的面板客户端 保留

节点对自身的修改要么整体应用,要么完全不应用。它先构建修改所需的组件:如果 [node.api] 中任何内容或 panel_type 的大小写有变化,就构建新的面板客户端;如果 [node.route] 有变化,就构建新的路由器。在 NewV2board 上,新客户端会保留审计规则所依据的路由,直到它自己读取节点设置为止。新客户端不持有任何 ETag,因此会完整读取节点设置和用户列表。修改 listen_ip、证书设置、disable_sniffing、enable_vless、[node.hysteria] 或 [node.route] 还会强制重建监听器。

当节点获得新的面板客户端或需要重建监听器,且它已经上线时,会立即执行一次轮询周期:获取节点设置和用户,通过协调阶梯应用它们,刷新审计规则,并发送流量上报和审计上报。常规的轮询计时器不会重置。仍在尝试上线的节点会保存这次修改,在下一次尝试时使用。其他配置项,例如 update_periodic、disable_upload_traffic 和 disable_get_rule,只会被保存,在用到的地方读取。

route rebuild failed, keeping current 日志则来自出站池的变化:节点未改动的规则无法基于新的 [[outbound]] 列表编译,例如你删除了一个规则仍在引用的 [[outbound]]。节点继续使用旧规则和旧出站路由,但仍然会重建监听器。katana --test 同样能拒绝这种错误。

在同一次保存中重命名一个出站并更新引用它的规则,是可行的。节点先收到新池,用它编译旧规则失败,记录一次 route rebuild failed,随后在收到自身的新设置时编译新规则并记录 router rebuilt。其监听器会重建两次。

配置中引用的某些文件,katana 只在构建相应组件时读取,察觉不到其内容的变化。以相同路径保存配置不算变化。

文件 何时重新读取
cert_file、key_file 节点的监听器重建时
[node.route] 的 geoip、geosite 如果某条规则使用了 geoip 或 geosite 分类,则在节点路由器编译时读取:节点启动时、修改 [node.route] 时,以及修改任何 [[outbound]] 时
[dns].ca_file 构建出站池时:启动时以及修改 [[outbound]] 时
api.rule_list_path disable_get_rule 关闭期间。newV2board:每次轮询时。SSPanel:面板下发规则列表的每次轮询时;返回 304 Not Modified 时跳过重新读取。修改 [node.api] 会立即重新读取它,因为新的面板客户端会向面板完整请求

最常见的情况是证书续期。在监听器重建之前,katana 会一直使用旧证书,而续期时配置文件无需任何改动,所以请计划在每次续期后重启。见续期证书。

每隔 update_periodic 秒,每个节点都会向面板轮询节点设置和用户列表,然后应用差异。同一次轮询还会刷新审计规则,并发送流量上报和审计上报。

如果面板请求失败,节点会记录一条警告,例如 node 1: node_info: …,并保留上次收到的设置或用户。轮询失败从不会拆除任何东西。如果面板返回端口 0,节点会记录 node 1: refreshed port is 0, keeping the last one。它保留上次收到的节点设置,但仍会应用该次轮询得到的用户列表。

节点按顺序执行第一个匹配的步骤,也就是在应用变化的前提下断开连接最少的那一步:

flowchart TB
  P["轮询:节点设置和用户"] --> Z{"用户列表为空?"}
  Z -- 是 --> T["关闭监听器"]
  Z -- 否 --> X{"没有监听器,或传输层、协议有变化?"}
  X -- 是 --> RB["监听器重建"]
  X -- 否 --> US{"用户或节点限速有变化?"}
  US -- 是 --> RF["原地刷新用户表"]
  US -- 否 --> NO["无需操作"]
面板变化 效果 现有连接
用户列表变为空 监听器关闭,端口释放 全部结束
用户列表为空后又有了用户 重新构建监听器 没有可保留的连接
传输层设置:端口、network、host、path、service name、authority、TLS 开关、headers、REALITY、PROXY protocol、Hysteria 混淆类型或密码 监听器重建 全部结束
协议设置:节点类型、VLESS 开关、VLESS flow、加密方式、server key 监听器重建 全部结束
添加、删除或修改用户,或节点限速有变化 原地替换用户表 保留,被删除用户的连接除外
无变化 不做任何事 保留

在之前某次重建中绑定失败的监听器被视为“没有监听器”,因此下一次轮询会再次尝试。因证书文件缺失或端口被占用而失去监听器的节点,会在你修复原因后的第一次轮询时自行恢复。

刷新时,新用户表在旧表旁边构建,完全构建好之后才切换。如果新用户表无法构建,节点会记录 proxy refresh build failed, keeping current: … 并继续为旧用户提供服务。但它仍会把新列表记为已应用,因此在面板的用户列表或节点设置再次变化之前不会重试。

切换成功后:

  • 未变化的用户保留其连接,流量计数继续累加。
  • 被删除的用户被停用。其现有连接结束,使用其凭据的新连接会被拒绝。某个凭据改归另一个用户时,对原用户而言视为被删除。
  • 限速有变化的用户(直接修改或经由节点限速)保留其连接。已经打开的流仍按旧速率,刷新后新打开的流按新速率。
  • 新用户在切换完成后即可连接。

用户记录的任何改动都算变化,包括密码、UUID 或限速。在 Hysteria 2 节点上,刷新只替换认证器:UDP socket 保持绑定,留下的用户的 QUIC 连接也保持不断。

限速如何组合,见限速。被删除用户的流量如何处理,见流量上报。

重载做的检查比 katana --test 少。它会拒绝无法解析的文件、无法构建的出站池,以及新增节点或有变化节点的面板客户端无法构建的文件。运行中的节点会拒绝无法编译的 [node.route] 修改。错误的 Hysteria 设置仍会被应用:监听器重建以 rebuild failed 失败,节点在每次轮询时重试。无法连上面板或无法绑定端口的节点会自行不断重试。请先检查文件,再一步到位地替换:

  1. 在原配置旁边复制一份,修改副本:

    终端窗口
    cp /etc/katana/config.toml /etc/katana/config.toml.new

    副本位于被监视的目录中,因此每次保存副本都会触发一次 config.toml 的重载。该重载发现没有变化,什么也不做。

  2. 检查副本:

    终端窗口
    katana --test -c /etc/katana/config.toml.new

    有效的文件会输出 Configuration OK 并以状态码 0 退出,否则输出 configuration error: … 并以状态码 1 退出。

  3. 用副本覆盖正在使用的文件:

    终端窗口
    mv /etc/katana/config.toml.new /etc/katana/config.toml

    在同一目录内重命名会一步完成文件替换,因此 katana 永远不会读到写了一半的配置。

  4. 跟踪 katana 的日志,查找 reload: 开头的行。例如使用部署中的模板 unit 时,运行 journalctl -u katana@xboard -f。

--test 会构建出站池、每个节点的面板客户端和路由器,以及每个 Hysteria 2 节点自身的设置。它不联系面板、不绑定端口,也不读取由面板控制 TLS 的节点的证书文件,因此这类节点上错误的 cert_file 路径能通过检查,却会在监听器重建时失败。参数列表见 CLI。

相关的修改请一起保存。每次涉及 [[outbound]]、身份值、enable_vless、listen_ip、证书设置、disable_sniffing、[node.hysteria] 或 [node.route] 的保存都会再次断开连接。

katana 把 [dns] 解析器与出站池一起构建,因此单独修改 [dns] 要等到下一次修改 [[outbound]] 时才会应用。要立即应用,请保存文件后重启 katana。重启会停止所有节点,发送每个节点的最后一次上报,然后从文件重新启动。将 katana 作为服务运行的方法见部署。

katana 在构建监听器时读取证书和私钥,因此续期后的证书要到下一次重建才会生效。可靠的做法是在每次续期后重启 katana,例如在 ACME 客户端的 deploy hook 中执行。把 cert_file 和 key_file 指向新路径也可以,因为修改它们会重建监听器,但同样会断开该节点的连接。

在 reload: 开头的日志行中,<name> 为 <panel type>@<host>#<id>,在 NewV2board 和 V2board 上后面还跟着 /<node type>。

日志行 含义
config reload failed, keeping current: … 文件无法解析或无法读取。什么都没有应用
reload: bad outbounds, keeping current config: … 新出站池构建失败。什么都没有应用
reload: node <name>: <error>; keeping current config 文件新增的某个节点,或某个有变化节点的新面板客户端,无法构建。什么都没有应用
reload: log level → <level> [log].level 有变化。即使新值因 invalid log level 被拒绝,katana 也会输出这一行
reload: outbound pool rebuilt 每个节点都收到了新池,并重建其路由器和监听器
reload: removing node <name> 某个节点被删除,或其身份发生了变化
reload: added node <name> 构建并启动了一个新节点,或以新身份出现的节点。它仍需连上面板并绑定端口,并会一直重试直到成功
reload: reconfigured node <name> 某个节点收到了新设置。其效果见上文的表格
node <id>: config edit refused, keeping the running one: … 节点的新路由器或新面板客户端无法构建。这次修改中的任何内容都没有应用
node <id>: router rebuilt 节点编译了路由规则
node <id>: route rebuild failed, keeping current: … 出站池变化后,节点的规则无法基于新池编译。节点继续使用之前的规则路由,但仍会重建监听器
node <id>: <reason>; retrying in <N>s 一次上线尝试失败。见无法上线的节点
node <id>: listening on <ip>:<port> 启动或重建后监听器已就绪
node <id>: rebuild failed: … 新监听器无法构建或绑定。在下一次轮询之前,该节点没有监听器
node <id>: refreshed port is 0, keeping the last one 面板下发了端口 0。节点保留上次的节点设置,并应用该次轮询得到的用户列表
node <id>: build router: … 仅在启动时:某个节点的规则编译失败,因此未启动。重载时,同样的错误出现在 reload: node <name>: …; keeping current config 日志行中
node <id>: unknown panel_type "<value>" 仅在启动时:某个节点的面板客户端无法构建,因此未启动。未知的 api.node_type 会记录 node <id>: unknown node_type "<value>"。重载时,这些错误出现在 reload: node <name>: …; keeping current config 日志行中
invalid log level "<level>": … 新的 [log].level 不是有效的过滤器。继续使用之前的过滤器

重载本身和它所更新的节点会并发输出日志,因此这些日志行的顺序可能不固定。