配置与命令行
主线位置:服务主线的第一步和第二步。
Server::new读命令行和配置文件,得到一份ServerConf;bootstrap处理-t和-u。之后的add_service和run_forever只读这份配置,不再改它。守护进程化不在bootstrap里,而在run的第一步,见平滑升级与停机。
flowchart TB
opt["Opt 命令行"] --> q1{"传了 Opt?"}
q1 -->|"None"| d1["ServerConf new"]
q1 -->|"有"| q2{"有 -c?"}
q2 -->|"无"| d2["new 再合并 -d"]
q2 -->|"有"| y["读 YAML 再合并 -d"]
d1 --> conf["Arc ServerConf"]
d2 --> conf
y --> conf
conf --> boot["bootstrap"]
boot -->|"-t"| exit0["exit 0"]
boot -->|"-u"| fds["从旧进程收 fd"]
代码在 pingora-core/src/server/configuration/mod.rs(748 行,一半是测试)和 server/mod.rs(896 行)里 Server::new、new_with_opt_and_conf 两个构造函数,bootstrap 本体在 server/bootstrap_services.rs(343 行)。实验 crate 是 exp-pingora/server-lab(M2 Max、macOS 27、rustc 1.99.0,release,tokio 1.53.2)。
三个入口,三种默认值
Server::new 按「有没有 Opt」「Opt 里有没有 -c」分三路:
let conf = if let Some(opt) = opt.as_ref() {
opt.conf.as_ref().map_or_else(
|| {
// options, no conf, generated
ServerConf::new_with_opt_override(opt).ok_or_else(|| {
Error::explain(ErrorType::ReadError, "Conf generation failed")
})
},
|_| {
// options and conf loaded
ServerConf::load_yaml_with_opt_override(opt)
},
)
} else {
ServerConf::new()
.ok_or_else(|| Error::explain(ErrorType::ReadError, "Conf generation failed"))
}?;
ServerConf::new不是直接调Default,而是解析一段只有version: 1的 YAML。结构体上有#[serde(default)],没写的字段取Default::default(),所以它和Default只差version:一个是 1,一个是 0。merge_with_opt只合并一个开关:命令行有-d就把daemon置成true。-u和-t不进ServerConf,留在Server::options里给bootstrap用。- 第三个入口
new_with_opt_and_conf给自己解析配置的程序用:调用方直接给一份ServerConf,Opt里的-c只打一条 warn,被忽略。
实验 conf(src/bin/conf.rs)把这几条路都走了一遍:
Default::default().version = 0
ServerConf::new().version = 1
default == new except version: true
...
unknown key ignored, threads = 4
bad type: ReadError context: Unable to parse yaml conf ---
version: 1
threads: four
cause: threads: invalid type: string "four", expected usize at line 3 column 10
validate: ReadError context: max_blocking_threads must be greater than zero
missing conf: ReadError context: Unable to read conf file from /nonexistent/pingora.yaml cause: No such file or directory (os error 2)
with -d -u: conf.daemon=true opt.upgrade=true
new_with_opt_and_conf: threads=3 version=0
version 在整个仓库里没有任何代码读它,写 0、1 还是 2 都一样。未知的键(my_app_key)被 serde 忽略,这是 ServerConf 文档注释里说的扩展方式:同一个 YAML 里放自己的键,再用自己的结构体解析一遍。类型错了、文件不存在,Server::new 返回 ReadError,错误信息把整段 YAML 原样带出来。
问: 我的 main 写的是 Server::new(None),启动时加了 -c /etc/app.yaml,为什么配置没生效?
答: None 走的是第三路 ServerConf::new(),根本不看命令行。命令行要靠 Opt::parse_args 解析出来再传进去:Server::new(Some(Opt::parse_args()))。Opt 是一个 clap 的 Parser,-c 写错文件名会在 Server::new 返回错误;但如果根本没调 parse_args,-c 和任何别的参数都不会报错,因为没人读 std::env::args。
Opt:四个开关
Opt 有 -u/--upgrade、-d/--daemon、-t/--test、-c/--conf,外加一个隐藏的 --nocapture(注释说是为了让 cargo test 传进来的参数不报错)。svc --help 的真实输出:
Usage: svc [OPTIONS]
Options:
-u, --upgrade This is the base set of command line arguments for a pingora-based service
-d, --daemon Whether this server should run in the background
-t, --test This flag is useful for upgrading service where the user wants to make sure the new service can start before shutting down the old server process.
-c, --conf <CONF> The path to the configuration file.
-h, --help Print help
-u 的帮助文字是「This is the base set of command line arguments...」:这句本该是整个命令的描述,被 #[clap(help = ...)] 写到了 upgrade 字段上。想用 pingora 默认命令行的程序,--help 里 -u 的说明就是这一句。docs/user_guide/start_stop.md 的参数表写 -c 的默认值是「empty string」,代码里是 Option<String>,没给就是 None。
字段表:默认值和谁读它
ServerConf 的 Default 给出所有默认值;不少字段是 Option,None 的含义要到读它的地方找。下表按「谁读」整理,「读它的代码」一列都在 tag 0.9.0 上 git grep 过:
| 字段 | 默认 | None 时实际用 | 读它的代码 |
|---|---|---|---|
version | 0(new() 是 1) | — | 无 |
daemon | false | — | Server::run 开头 daemonize |
pid_file | /tmp/pingora.pid | — | daemonize |
error_log | None | stderr | daemonize(只在守护进程时用) |
user / group / working_directory | None | 不切换 | daemonize |
upgrade_sock | /tmp/pingora_upgrade.sock | — | Bootstrap::new、main_loop 的 SIGQUIT 分支 |
threads | 1 | — | run:服务自己的 threads() 优先;连接池容量 |
listener_tasks_per_fd | 1 | — | run_service → start_service |
work_stealing | true | — | create_runtime |
runtime_enable_alt_timer | false | — | runtime_opts,只对 Steal 生效,要 tokio_unstable |
grace_period_seconds | None | 300(EXIT_TIMEOUT) | run 的停机段 |
graceful_shutdown_timeout_seconds | None | 5 | run 的停机段 |
max_blocking_threads / blocking_threads_ttl_seconds | None | tokio 的 512 / 10 秒 | BlockingPoolOpts |
fast_timeout_to_tokio_threshold_seconds | Some(900) | 写 null 关掉回退 | fast_timeout::set_fast_timeout_to_tokio_threshold |
runtime_metrics_poll_time_histogram* | false / None | — | runtime_opts,要 tokio_unstable |
upstream_keepalive_pool_size | 128 | — | ConnectorOptions::from_server_conf,乘以 threads |
client_bind_to_ipv4 / _ipv6 | 空 | — | ConnectorOptions |
ca_file | None | TLS 库默认信任库 | 各 TLS 后端的连接器 |
upstream_connect_offload_* | None | 不开 | ConnectorOptions,两个都大于 0 才开 |
downstream_tls_offload_* | None | 不开 | TlsSettings::set_offload_threadpool_from_server_conf,要调用方自己调 |
upstream_debug_ssl_keylog | false | — | ConnectorOptions |
max_retries | 16 | — | pingora-proxy 的 HttpProxy::new |
upgrade_sock_connect_accept_max_retries | None | 5(MAX_RETRY) | 无 |
daemon_wait_for_ready | false | — | daemonize |
daemon_ready_timeout_seconds | None | 600 秒 | daemonize |
daemon_notify_timeout_seconds | None | 60 秒 | Bootstrap::new |
几条要单独说的:
work_stealing默认是true。 不写配置时,每个服务跑在 tokio 的多线程(工作窃取)运行时上;NoSteal 要显式写work_stealing: false。这个默认值从 0.1.0 起没变过。两种运行时的区别见运行时。threads是「每个服务」的线程数,服务之间不共享。GenBackgroundService自己把threads设成Some(1),所以后台服务不管配置写多少都是 1 个线程。代理服务里,ConnectorOptions::from_server_conf把上游连接池容量算成upstream_keepalive_pool_size × threads。- 配置在
http_proxy_service(&server.configuration, ...)那一刻被读走。HttpProxy::new从这份Arc<ServerConf>里取max_retries、threads,建连接器。Server的configuration字段是pub的Arc,之后再换掉它,已经拿走这份Arc的服务看不到;要改配置,应该在建服务之前用new_with_opt_and_conf传进去。 validate只查四个数:max_blocking_threads和两个直方图参数不能是 0,桶数不超过 1024。threads: 0能通过校验,见本页的动脑筋。docs/user_guide/conf.md的表格漏了grace_period_seconds、graceful_shutdown_timeout_seconds、listener_tasks_per_fd、max_retries、max_blocking_threads、blocking_threads_ttl_seconds、upstream_debug_ssl_keylog和upgrade_sock_connect_accept_max_retries;这些字段只能从源码里查。
upgrade_sock_connect_accept_max_retries 从加进来那天起就没人读de4cc62(2026-01-08,「Add configurable retries for upgrade sock connect/accept」,首次进入 0.7.0,没有 PR 编号)做了两件事:给 ServerConf 加了这个字段,注释说它控制 send_fds_to 重试连接、get_fds_from 重试 accept 的次数;给这两个函数加了一个 max_retry: Option<usize> 参数,None 时用原来的常量 MAX_RETRY = 5。但同一个提交里,唯一的调用方 Fds::send_to_sock 和 get_from_sock 传的都是 None,配置值没有一条路径传进去。到 0.9.0 和 0.9.0 之后的 main(84f21ee)都还是这样。CHANGELOG 0.7.0 一节写着「Add configurable retries for upgrade sock connect/accept」,按字面理解会以为配了就生效;实际上平滑升级时两边仍然各重试 5 次、每次间隔 1 秒。
bootstrap 做了什么
Server::bootstrap 只是锁住 Bootstrap 调 Bootstrap::bootstrap:
pub fn bootstrap(&mut self) {
// already bootstrapped
if self.completed {
return;
}
// ...
if self.test {
info!("Server Test passed, exiting");
std::process::exit(0);
}
// ...
#[cfg(unix)]
if let Some(pid) = self.notify_parent_pid {
notify_parent_ready_for_fds(pid, self.daemon_notify_timeout);
}
#[cfg(unix)]
match self.load_fds(self.upgrade) {
Ok(_) => {
info!("Bootstrap done");
}
Err(e) => {
// ...
error!("Bootstrap failed on error: {:?}, exiting.", e);
std::process::exit(1);
}
}
// ...
}
按顺序是三件事:
-t到此为止。 进程以 0 退出。所以-t能检查的只是Server::new能不能读通配置、bootstrap之前的代码能不能跑通;监听端口能不能绑上、服务能不能启动,它不检查。svc -c conf/grace.yaml -t的输出最后一行是Server Test passed, exiting。- 通知守护进程的父进程(只在
daemon_wait_for_ready时有 pid),见平滑升级与停机。 -u时从旧进程收监听 fd。load_fds调Fds::get_from_sock(upgrade_sock),收到的 fd 写进一张所有服务共享的表(ListenFds)。没有-u时什么都不做,表是空的,各服务自己bind。收失败直接exit(1),macOS 上一定走这条路,见平滑升级与停机。
还有一个变体 bootstrap_as_a_service:把同样的逻辑包成一个后台服务,放进依赖图,让它等别的服务就绪后再收 fd。0.9.0 之前这条路有一个 bug:服务在 bootstrap 运行之前就拿到了 fd 表的快照,永远是空的,b633683(「Fix listen fds not inherited during bootstrap_as_a_service graceful upgrade」)把表改成在 Bootstrap::new 里预先分配、收到 fd 后原地填充,首次进入 0.9.0。
守护进程化不在 bootstrap 里,0.1.0 起 daemonize 就一直在 run_forever/run 的开头。docs/user_guide/daemon.md 写的是「This process happens in the run_forever() call」,和代码一致。
threads: 0 会在哪一步出错?配置文件写 threads: 0,work_stealing 分别取 true 和 false。ServerConf::from_yaml 会拒绝它吗?如果不拒绝,进程在哪一步、以什么信息失败?
动脑筋答案
from_yaml 不拒绝:validate 不检查 threads。两种运行时都在 run 里建运行时时 panic,位置不同(release):
== threads 0 work_stealing true
thread 'main' (5579065) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pingora-runtime-0.9.0/src/lib.rs:471:14:
Worker threads cannot be set to 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
== threads 0 work_stealing false
thread 'main' (5579073) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pingora-runtime-0.9.0/src/lib.rs:611:9:
assertion failed: threads != 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
- Steal:
pingora-runtime第 471 行是.worker_threads(self.threads),panic 文字来自 tokio 的Builder::worker_threads。 - NoSteal:第 611 行是
NoStealRuntime::new里的assert!(threads != 0)。 - 两次都在
main线程,发生在bootstrap之后,所以-t发现不了这个错误。
小结
Server::new(None)不看命令行;要用-c/-d/-u/-t必须传Some(Opt::parse_args())。- 命令行只有
-d合并进ServerConf;-u、-t留在Opt里给bootstrap。 - 默认
threads: 1、work_stealing: true、宽限期 300 秒、运行时停机超时 5 秒。 version和upgrade_sock_connect_accept_max_retries没有代码读;validate不查threads: 0。bootstrap只做两件事:-t退出,-u从旧进程收监听 fd;守护进程化在run里。