配置参考
OxPHP 完全通过环境变量进行配置。没有需要管理的配置文件,而且每一项设置都有默认值,因此零配置部署开箱即用。
布尔值
标记为布尔类型的变量接受一组固定的规范取值,大小写不敏感并会去除首尾空白:
- 真值:
on、true、1、yes - 假值:
off、false、0、no
任何落在该集合之外的非空值——例如 ture 这样的拼写错误——会在启动时快速失败,并给出指明该变量的错误信息。这能在流量到来之前捕获配置错误,而不是悄无声息地把标志翻到错误的方向。
未设置的变量或空赋值(FOO=)会回退到文档记载的默认值。空值被特意当作未设置处理:Docker Compose / Kubernetes 中形如 FOO=${FOO} 的替换在宿主变量缺失时会产生 FOO=,而这不应导致服务器拒绝启动。
服务器
| Variable | Default | Description |
|---|---|---|
LISTEN_ADDR |
0.0.0.0:80 |
主 HTTP 服务器的地址与端口 |
DOCUMENT_ROOT |
/var/www/html/public |
提供文件和 PHP 脚本服务的根目录 |
ENTRY_FILE |
(未设置) | 单一规范入口脚本。未设置 = 直接文件映射。*.php = 前端控制器。非 .php = 静态回退(SPA)。当 WORKER_MODE_ENABLED=true 时 = 工作进程引导。相对于 DOCUMENT_ROOT 解析(允许相对路径和 ..;绝对路径按原样使用)。参见路由 |
WORKER_MODE_ENABLED |
false |
启用持久化工作进程模式。要求 ENTRY_FILE 指向一个 .php 脚本。布尔——参见布尔值 |
MAX_CONNECTIONS |
10000 |
最大并发 TCP 连接数。也是默认 QUEUE_MAX_WAITING 的上限来源(取其一半),因此格式错误的值是启动错误而不是静默回退。调低它并不会移动 QUEUE_CAPACITY 的默认值——后者只按工作进程数计算——请让它保持高于 PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING,参见为接受循环保留余量 |
TOKIO_WORKERS |
CPU / 2(最小 1) | 异步 I/O 线程。1 = 单线程,N > 1 = 固定线程数,未设置 = 自动(CPU / 2,最小 1) |
PHP 工作进程
| Variable | Default | Description |
|---|---|---|
EXECUTOR |
sapi |
PHP 执行器后端。sapi 用于执行 PHP,stub 用于不带 PHP 的基准测试 |
PHP_WORKERS |
CPU / 2(最小 1) | 工作进程池大小。N = 固定池,MIN:MAX = 动态伸缩,0 = 自动 |
PHP_WORKERS_IDLE_SECONDS |
30 |
动态工作进程被回收前保持空闲的秒数(仅动态模式)。回收发生在工作进程手头没有任何在途请求的时刻,因此它正在服务的请求不会被中途切断——而持有一个永不结束的请求(例如一个打开的流)的工作进程永远等不到这样的时刻,完全不会被回收 |
QUEUE_CAPACITY |
初始工作进程数 × 128 | PHP 队列中最大待处理请求数。到达已满队列的请求会等待空位(参见 QUEUE_WAIT_TIMEOUT_MS)。对于动态池(MIN:MAX),初始工作进程数 = 最小数量。0 = 自动 |
QUEUE_WAIT_TIMEOUT_MS |
1000 |
请求在被 529 拒绝之前,最多可以花多长时间等待一个 PHP 工作进程。这是在到达时盖上的单一截止时间,覆盖请求可能面临的两段等待:等队列空位,以及在队列里等工作进程。工作进程在截止时间过后才轮到的请求,会在取出时被拒绝而不是被执行,因此该预算约束的是整段等待,而不只是准入那一半——它不约束处理器随后运行多久。同时最多有 QUEUE_MAX_WAITING 个请求在等待;超过之后请求会被立即拒绝。0 = 队列一满就立即拒绝。当应用会通过 HTTP 回调这台服务器自身时(内层调用要等外层调用释放它的工作进程才能被放行,这段等待是白费的),或当前面的负载均衡器有更短的超时设置时,请调低它或设为 0。等待途中关闭连接的客户端会立即把位置让出来——HTTP/1.1 和 HTTP/2 都是如此——所以一个超时即断开的负载均衡器不会用它已经放弃的尝试填满等待集合。服务器看不到的是那些不关闭连接就不再等待的客户端——这样的客户端会一直占着位置,直到被放行或预算耗尽;如果一个工作进程先空了出来,它的脚本就为不存在的人跑了一遍——所以把预算压在前置组件的超时之下仍然值得做 |
QUEUE_MAX_WAITING |
初始工作进程数 × 128,上限 MAX_CONNECTIONS / 2 |
同时停放着等待队列空位的最大请求数。超过之后请求会被立即拒绝而不是等待。每个等待者在等待期间都占着一条连接和一份已完整缓冲的请求体,所以这是对占用资源的约束,而不是对终将有回报的等待的约束。默认值上的 MAX_CONNECTIONS / 2 上限只约束积压中的这一部分;排队和运行中的请求同样占着连接,所以服务器实际留给接受与拒绝的余量,取决于 PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING 与 MAX_CONNECTIONS 的对比——见下文。请按你自己的处理器延迟来设定它——同样见下文。0 = 自动,且绝不低于 1 |
QUEUE_MAX_WAITING_BYTES |
67108864(64 MiB) |
停放中的请求合计最多可持有的请求体字节数。请求体会把总量推过该值的请求会被立即以 529 拒绝而不是等待;不携带请求体的请求绝不会因此被拒绝。QUEUE_MAX_WAITING 以请求数约束同一个集合,但对它们的大小不置一词——请求体在请求到达队列之前就已完整缓冲,因此只有数量上限时,停放集合可以持有 QUEUE_MAX_WAITING × 10 MiB。对应当吸收大请求体突发而不是卸除它们的上传密集型应用,调高它;在内存受限的容器上,调低它。0 = 自动 |
确定等待集合的大小
有多少请求能有意义地等待,取决于进程池消化它们的速度。有 W 个工作进程、处理器耗时 T 毫秒时,进程池每毫秒放行 W / T 个请求,所以 B 毫秒的预算大约能放进 W × B / T 个等待者。超出这个数的每个请求都会等完整个预算然后照样被拒绝,全程占着一条连接和一份已缓冲的请求体。
默认值算不出这个数——服务速率在启动时是未知的——所以它刻意取得宽松。这适合快的处理器:进程池能在预算内轻松排空很深的积压;对慢的处理器则大得离谱:8 个工作进程、默认 1 秒预算加 200 毫秒的处理器,只有约 40 个请求能被及时放行,而默认值却允许停放多达 1024 个。
QUEUE_MAX_WAITING=40把它设在 W × B / T 附近,会让多出来的部分立即得到 529,而不是一秒之后才得到。缩短 QUEUE_WAIT_TIMEOUT_MS 从另一个因子达到同样的效果。两者互相权衡;在慢处理器上,更小的预算通常是更好的杠杆,因为它同时约束了客户端等到拒绝要花多久。
这个集合还被第二重约束限定:字节。每个等待者在等待期间都把请求体保存在内存里,所以按请求数计的上限把它们占用的内存交给了流量来决定:同样是 1024 个等待者,在无请求体的 GET 上不花什么内存,在上传上却能花掉数 GB。QUEUE_MAX_WAITING_BYTES 直接约束这个总和——超过之后,携带请求体的请求会被立刻拒绝而不是停放,无请求体的请求照常等待。应当吸收突发而不是卸除的上传密集型应用需要更大的值;有硬性内存限制的容器需要更小的值。两种上限造成的拒绝在 oxphp_admission_refused_total 中分开计数(waiting_full 对应数量上限,waiting_bytes 对应内存上限),所以这个指标会直接点名该去拧哪个旋钮。
为接受循环保留余量
一个请求从到达到被应答,无论中间在做什么,都占着它的连接——以及 MAX_CONNECTIONS 许可中的一个。这覆盖三类彼此独立的群体,因为队列槽位在工作进程把请求取走的那一刻、脚本运行之前就已释放:
- 在工作进程中运行:至少
PHP_WORKERS个,工作进程模式下更多,那里一个线程多路复用多个纤程; - 排队中:至多
QUEUE_CAPACITY个; - 在准入处停放:至多
QUEUE_MAX_WAITING个。
只有第三项有从 MAX_CONNECTIONS 推导出的上限。请让 PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING 保持在 MAX_CONNECTIONS 之下。越过之后,失败模式会变得更糟:接受循环在开始服务一个连接之前要先取得一个连接许可,所以一旦 PHP 路径占满了全部许可,它就会停摆,此时到来的客户端得到的不是 529,而是完全没有响应——负载均衡器无法把这与一个死掉的实例区分开,而 INTERNAL_ADDR 上的健康探针依然是绿的,因为它不经过这两个限制中的任何一个。
服务器会在启动时检查这一点,并在总和达到预算时发出警告,点名每一个值;oxphp config --check 报告同样的内容,但不改变其结论或退出码。把消除警告当作必要条件而不是充分条件:空闲的 keep-alive 连接、静态文件请求和进行中的握手同样占用许可,而它们没有一个能在启动时被数出来。
调低 MAX_CONNECTIONS 是掉进这个警告最常见的路径,因为 QUEUE_CAPACITY 的默认值按工作进程数计算,不会跟着降下来——7 个工作进程加 MAX_CONNECTIONS=1000 得到 7 + 896 + 500 对上 1000 的预算。注意该去拧哪个旋钮:在 QUEUE_MAX_WAITING 保持默认时,单独调高 MAX_CONNECTIONS 并不能消除警告,因为那个默认值是 MAX_CONNECTIONS 的一半,会跟着一起涨——在 7 个工作进程时总和会稳定在 1799,条件要到 MAX_CONNECTIONS=1800 以上才解除。请调低 QUEUE_CAPACITY,或显式设置 QUEUE_MAX_WAITING 之后再提高预算。
对于动态池(MIN:MAX),运行项按最小值计——这也是队列默认值的计算基数——所以扩到最大规模的池实际持有的比总和显示的更多。
这个比较以连接数计,而预算由请求消耗,所以以 HTTP/2 为主的部署合理地可以高于它:一条连接最多承载 H2_MAX_CONCURRENT_STREAMS 个请求,同样的积压只需一小部分连接就能承载。在那里出现这个警告是意料之中。一个使用默认预算的大进程池同样会触及它——自动定大小的 39 个工作进程的池给出 39 + 4992 + 4992 对上 10 000——那一个值得处理而不是忽略。
静态工作进程与动态工作进程
将 PHP_WORKERS 设为单个数字以使用固定池:
PHP_WORKERS=8 # Fixed 8 workers
PHP_WORKERS=0 # Auto-detect: CPU / 2 (min 1)将 PHP_WORKERS 设为 MIN:MAX 以启用自动伸缩:
PHP_WORKERS=2:16 # Scale between 2 and 16 workers
PHP_WORKERS=4:0 # 4 minimum, auto-detect maximum (CPU × 2)
PHP_WORKERS=0:16 # auto-detect minimum (CPU / 4, min 1), 16 maximum在动态模式下,当所有工作进程都繁忙时 OxPHP 会扩容工作进程,当工作进程空闲时间超过 PHP_WORKERS_IDLE_SECONDS 时会缩容。
工作进程模式
| Variable | Default | Description |
|---|---|---|
WORKER_MAX_MEMORY_MIB |
0 |
每个工作进程被回收前的最大内存(MiB)。0 = 无限制 |
设置 WORKER_MODE_ENABLED=true 并让 ENTRY_FILE 指向你的工作进程引导脚本(例如 ENTRY_FILE=worker.php 或 ENTRY_FILE=../worker.php)。此后 PHP 进程会跨请求保持存活,把引导状态(自动加载器、数据库连接)保留在内存中。当工作进程超过 WORKER_MAX_MEMORY_MIB 时会被自动回收,或者在应用调用 Worker::scheduleExit() 时按需回收。早期版本中的 WORKER_MAX_REQUESTS 旋钮已弃用并被忽略——两者都不要设置,或者迁移到 Worker::scheduleExit()。
已弃用:INDEX_FILE 与 WORKER_FILE
为向后兼容,遗留的 INDEX_FILE 和 WORKER_FILE 变量仍会被解析。设置后,它们会在启动时输出一行 WARN 日志,并映射到新模型上:
| Legacy | Equivalent today |
|---|---|
INDEX_FILE=index.php |
ENTRY_FILE=index.php |
INDEX_FILE=index.html |
ENTRY_FILE=index.html |
WORKER_FILE=/path/worker.php |
WORKER_MODE_ENABLED=true ENTRY_FILE=/path/worker.php |
如果新旧同时设置,则以 ENTRY_FILE / WORKER_MODE_ENABLED 为准。可以在方便时再迁移;已弃用的形式将在未来版本中移除。
SAPI / PHP
| Variable | Default | Description |
|---|---|---|
SUPERGLOBALS_ENABLED |
true |
在脚本执行前填充 PHP 超全局变量($_GET、$_POST、$_COOKIE、$_FILES、$_SERVER、php://input)。设为假值可跳过填充——此时请求数据只能通过对象 API(oxphp_http_request())获取。对于直接使用对象 API、希望避免在每个请求上构建超全局变量开销的应用很有用 |
超时
| Variable | Default | Description |
|---|---|---|
HEADER_TIMEOUT_SECONDS |
5 |
连接建立后接收 HTTP 头部的最大秒数(Slowloris 防护) |
DRAIN_TIMEOUT_SECONDS |
25 |
优雅关闭期间等待进行中连接的最大秒数 |
PHP 执行时间受 PHP 自身的 max_execution_time ini 指令(以及运行时的 set_time_limit())限制,而非某个 OxPHP 环境变量。
限流
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT |
0(关闭) |
每个 IP 在每个时间窗口内的最大请求数。0 禁用限流 |
RATE_WINDOW_SECONDS |
60 |
限流窗口时长(秒) |
安全
| Variable | Default | Description |
|---|---|---|
FRAME_OPTIONS |
SAMEORIGIN |
点击劫持防护。SAMEORIGIN 只允许同源页面进行框架嵌入,DENY 阻止所有框架嵌入,off 禁用(当你通过自己的 CSP 管理框架嵌入时使用)。任何其他值都会回退到默认的 SAMEORIGIN 并给出启动警告。在每个响应上同时设置 X-Frame-Options 和 Content-Security-Policy: frame-ancestors。发出的头部值、服务器头部如何让位于应用设置的头部,以及取值建议,参见下文点击劫持防护 |
TRUSTED_PROXIES |
(未设置) | 可信反向代理网络(逗号分隔的 CIDR 或 private)。设置后,OxPHP 会使用最右非可信算法从 Forwarded(RFC 7239)或 X-Forwarded-For 头部中提取真实客户端 IP。还会处理 X-Forwarded-Proto 和 X-Forwarded-Host,用于 $_SERVER['HTTPS']、REQUEST_SCHEME、SERVER_NAME 和 SERVER_PORT。未设置 = 功能禁用 |
PHP_DENY_PATHS |
(未设置) | 逗号分隔的 glob 模式,其 .php 文件绝不能通过直接 URI 执行(例如 /uploads/**,/cache/**,/admin/legacy.php)。模式可以针对整个目录或单个文件。适用于直接映射模式——传统模式和 SPA 模式;在框架模式和工作进程模式下会被忽略并给出启动警告,因为这两种模式永远不会直接执行任意 .php 文件。也涵盖通过目录索引解析到达的脚本(/uploads/ → uploads/index.php)。对于直接的 .php URI,匹配发生在磁盘 I/O 之前,因此被拒绝的路径无论文件是否存在都产生相同的响应(不存在存在性预言)。遗留名称 PHP_DENY_DIRS 作为已弃用的别名被接受,并会输出启动 WARN。参见 PHP 执行拒绝列表 |
PHP_DENY_FALLBACK |
404 |
命中 PHP_DENY_PATHS 时返回什么。可以是一个 HTTP 状态码 400–599(与 ERROR_PAGES_DIR 配合可提供自定义 HTML),或者是一个以 / 开头、指向 DOCUMENT_ROOT 内 PHP 回退脚本的 URI 路径。回退脚本会在 $_SERVER 中收到 OXPHP_DENIED_PATH 和 OXPHP_DENIED_PATTERN。在启动时校验:脚本必须存在、规范化后位于 DOCUMENT_ROOT 内,且自身不能匹配 PHP_DENY_PATHS(防止循环) |
SYMLINK_ALLOW_PATHS |
(未设置) | 逗号分隔的绝对路径列表,在这些路径之下允许符号链接逃逸出 DOCUMENT_ROOT。每一项都必须已在磁盘上存在;相对路径和不存在的路径会中止启动。未设置 = 不允许任何符号链接逃逸。参见符号链接允许路径 |
特殊值 private 会展开为所有 RFC-1918 私有网络、回环地址和链路本地地址(IPv4 与 IPv6):10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8、169.254.0.0/16、::1/128、fc00::/7、fe80::/10。
点击劫持防护
点击劫持是一种攻击:一个恶意页面把你的网站嵌进一个不可见的 <iframe>,诱骗用户去点击他们看不见的东西——一个“是的,删除我的账户”按钮、一次一键购买、一个 OAuth 的“授权”提示。防御手段是告诉浏览器谁(如果有的话)可以把你的页面嵌进框架。FRAME_OPTIONS 控制的就是这件事。
有两个头部管辖框架嵌入——遗留的 X-Frame-Options(所有浏览器都理解)和现代的 Content-Security-Policy: frame-ancestors(两者同时存在时取代前者)——因此 OxPHP 会把两个都发出,让策略在新旧浏览器上同样成立。单个 FRAME_OPTIONS 值映射为一对相互匹配的头部:
FRAME_OPTIONS |
X-Frame-Options |
Content-Security-Policy |
谁可以嵌入你的页面 |
|---|---|---|---|
SAMEORIGIN(默认) |
SAMEORIGIN |
frame-ancestors 'self' |
只有同源页面 |
DENY |
DENY |
frame-ancestors 'none' |
任何人都不行,连你自己的页面也不行 |
off |
(不发送) | (不发送) | 任何人——服务器不施加框架嵌入限制 |
取值建议。SAMEORIGIN 是默认值:它阻止点击劫持真正依赖的跨源框架嵌入,同时仍允许你自己的页面互相嵌套——许多应用有正当的这种需求(管理后台预览、仪表盘小组件、托管在同一源上的支付组件)。当你的站点没有任何东西打算被框架嵌入、连自己也不例外时,选择 DENY,姿态最严格。只有当你通过应用自己发出的完整 Content-Security-Policy 管理框架嵌入时才选择 off——见下文。
**嵌入外部源。**两个 X-Frame-Options 值都无法点名某个被允许的源(ALLOW-FROM 已从标准中移除)。要允许指定的第三方嵌入你的页面,请设置 FRAME_OPTIONS=off,并让应用发出自己的、带显式 frame-ancestors 列表的 Content-Security-Policy,例如 header("Content-Security-Policy: frame-ancestors 'self' https://partner.example.com");。
**应用头部优先。**服务器头部是回退项,只在响应没有携带对应头部时才应用:通过 PHP header() 设置了自己的 X-Frame-Options 或 Content-Security-Policy 的应用,其头部会原封不动地保留。两个框架嵌入头部被当作同一个策略对待,因此服务器永远不会与应用相矛盾:
- 如果你的应用设置了
X-Frame-Options,OxPHP 会跳过它的frame-ancestors回退(服务器的 CSP 会在现代浏览器中覆盖你应用的选择)。 - 如果你的应用设置了包含
frame-ancestors指令的Content-Security-Policy,OxPHP 会跳过它的X-Frame-Options回退(更严格的服务器X-Frame-Options会在忽略 CSP 的旧浏览器中过度拦截)。
同样的优先级也适用于 X-Content-Type-Options——OxPHP 在每个响应上把它设为 nosniff:应用设置的值会原样保留。注意 nosniff 是唯一起作用的值——应用用其他任何值覆盖它,都会悄悄禁用 MIME 嗅探防护。
TLS
| Variable | Default | Description |
|---|---|---|
TLS_CERT |
(未设置) | PEM 编码的 TLS 证书路径。必须同时设置 TLS_CERT 和 TLS_KEY 才能启用 TLS |
TLS_KEY |
(未设置) | PEM 编码的 TLS 私钥路径 |
TLS_MIN_VERSION |
1.2 |
接受的最低 TLS 协议版本:1.2 或 1.3。即使未启用 TLS,也会在启动时(以及通过 oxphp config --check)校验——任何其他值,包括非 UTF-8 字节,都是硬性启动错误。空值被当作未设置处理 |
HTTP/2
| Variable | Default | Description |
|---|---|---|
H2_MAX_CONCURRENT_STREAMS |
PHP_WORKERS_MAX × 4(最小 32) |
每个 HTTP/2 连接同时打开的最大流数 |
H2_MAX_PENDING_RESET |
20 |
在关闭连接前排队的最大 RST_STREAM 帧数(Rapid Reset 防护) |
H2_MAX_HEADER_LIST_BYTES |
65536 |
每个请求解码后头部的最大总字节数 |
H2_KEEPALIVE_INTERVAL_SECS |
20 |
HTTP/2 PING 帧之间的秒数;0 禁用 |
H2_KEEPALIVE_TIMEOUT_SECS |
10 |
在关闭连接前等待 PING 回复的秒数 |
静态文件
| Variable | Default | Description |
|---|---|---|
STATIC_MAX_AGE |
30d |
静态文件的 Cache-Control: max-age。接受:30s、5m、2h、30d、1w、1y、纯秒数(3600),或用 off 禁用该头部。取代已弃用的 STATIC_CACHE_TTL。 |
STATIC_REVALIDATE |
off |
布尔——参见布尔值。设为真值以对内存内容缓存启用 mtime 重新校验:每个文件的修改时间最多每 3 秒重新检查一次(按文件计,而非按请求计),过期条目会被自动逐出,因此变更会在 3 秒内变得可见。取代已弃用的 STATIC_CACHE(其中 off 的含义正好相反)。 |
COMPRESSION_LEVEL |
4 |
Brotli 压缩质量(0–11)。0 禁用压缩 |
日志
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
info |
日志详细程度:trace、debug、info、warn、error |
ACCESS_LOG |
(未设置) | 按请求的访问日志:all = 每个请求,error = 仅 4xx/5xx,未设置 = 关闭 |
ACCESS_LOG 接受 all 或 error。不设置它即可完全禁用访问日志。
可观测性
| Variable | Default | Description |
|---|---|---|
INTERNAL_ADDR |
(未设置) | 内部服务器(/health、/metrics、/config)的地址。未设置时不会启动内部服务器。仅端口的值(:9090 或 9090)会绑定到 127.0.0.1;绑定显式的 0.0.0.0:9090 可将其暴露到主机之外 |
INTERNAL_ALLOW_IPS |
(未设置) | 内部服务器的逗号分隔 CIDR/IP 允许列表。列表之外的对端在访问 /metrics、/config 和插件路径时会收到 403;健康探针(/health、/healthz、/readyz、/startupz 及其完整形式)保持可访问。未设置/为空 = 允许所有。回环地址不是隐式包含的——列出 127.0.0.1/32 以保留 localhost 访问。格式错误的列表会中止启动 |
ERROR_PAGES_DIR |
(未设置) | 包含以 {status}.html 命名的自定义错误页面(例如 404.html、503.html)的目录 |
MAX_QUERY_BODY |
524288 |
内部查询端点的最大请求体大小(字节)(512 KiB) |
TRACE_CONTEXT |
false |
布尔——参见布尔值。为真时,启用 W3C Trace Context 传播:读取 traceparent/tracestate 头部并通过 $_SERVER 转发给 PHP |
OpenTelemetry
| Variable | Default | Description |
|---|---|---|
OTEL_ENABLED |
false |
启用 OpenTelemetry span 导出。自动设置 TRACE_CONTEXT=true。布尔——参见布尔值 |
OTEL_EXPORTER_OTLP_PROTOCOL |
grpc |
导出协议:grpc 或 http/protobuf |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4317(gRPC)或 http://localhost:4318(HTTP) |
OTLP 采集器端点 |
OTEL_EXPORTER_OTLP_TIMEOUT |
10000 |
导出超时(毫秒) |
OTEL_EXPORTER_OTLP_HEADERS |
(未设置) | 认证头部:key=value,key2=value2 |
OTEL_SERVICE_NAME |
oxphp |
导出 span 中的服务名 |
OTEL_SERVICE_VERSION |
(未设置) | 服务版本属性 |
OTEL_RESOURCE_ATTRIBUTES |
(未设置) | 额外的资源属性:env=prod,region=us-east-1 |
OTEL_TRACES_SAMPLER |
parentbased_traceidratio |
采样策略:always_on、always_off、traceidratio、parentbased_always_on、parentbased_always_off、parentbased_traceidratio |
OTEL_TRACES_SAMPLER_ARG |
1.0 |
基于比例的采样器的采样比率(0.0–1.0) |
无效或超出范围的 OTEL_TRACES_SAMPLER_ARG 值会被钳制到 [0.0, 1.0] 并以 warn 级别记录日志。未知的 OTEL_TRACES_SAMPLER 值会回退到 parentbased_traceidratio 并记录日志。
APM
| Variable | Default | Description |
|---|---|---|
OTEL_APM_ENABLED |
false |
启用 APM:自动埋点、错误捕获以及 PHP 追踪 SDK。要求 OTEL_ENABLED=true。布尔——参见布尔值 |
OTEL_APM_SLOW_QUERY_MS |
100 |
慢查询阈值(毫秒)。超过此值的数据库查询会获得 oxphp.db.slow=true span 属性 |
OTEL_APM_DB_CAPTURE_PARAMS_ENABLED |
false |
在 db.params span 属性中记录绑定参数。若参数可能包含敏感数据,请在生产环境中禁用。布尔——参见布尔值 |
OTEL_APM_STACKTRACE_MAX_BYTES |
8192 |
exception.stacktrace 属性的最大大小(字节)。超过上限时,堆栈跟踪会从尾部截断并带 …(truncated) 标记。0 禁用截断 |
OTEL_APM_MESSAGE_MAX_BYTES |
4096 |
exception.message 属性的最大大小(字节)(默认值与 New Relic 的每属性值上限一致)。超过上限时,消息会从尾部截断并带 …(truncated) 标记。0 禁用截断 |
启用 APM 时,OxPHP 会自动挂钩 34 个内部 PHP 函数(PDO、mysqli、cURL、Redis、Memcached、文件 I/O)以创建子 span。无论 APM 是否启用,oxphp_apm_*() PHP 函数都会被注册——禁用时,它们是安全的空操作。
异步工作进程
| Variable | Default | Description |
|---|---|---|
ASYNC_WORKERS |
0(禁用) |
专用异步工作进程线程的数量。当为 0 时,异步函数(oxphp_async 等)仍会被注册,但调用时会抛出 OxPHP\Async\AsyncException。设为正值以启用后台任务执行 |
ASYNC_QUEUE_CAPACITY |
ASYNC_WORKERS × 64 |
异步队列中最大待处理任务数。0 = 自动(workers × 64) |
ASYNC_MAX_FIBERS |
256 |
每个工作进程并发异步任务纤程的上限。进程全局的进行中上限(排队 + 运行)为 ASYNC_MAX_FIBERS × ASYNC_WORKERS;超过它的派发会立即以 OxPHP\Async\AsyncException 被拒绝,因此扇出组合不会死锁 |
异步工作进程池处理从 PHP 派发的即发即忘后台任务。它独立于 PHP 工作进程池,标准请求处理并不需要它。
这三个变量中任何一个出现格式错误的值(例如 ASYNC_WORKERS=8x)都是启动错误——回退到默认值会悄无声息地禁用或错误配置该池。恰好为空的值被当作未设置处理。
运行时钩子
| Variable | Default | Description |
|---|---|---|
RUNTIME_HOOKS |
(关闭) | 按需启用:用挂起纤程的实现替换阻塞式的 PHP 内置函数。1/true/all 启用所有钩子类别;逗号分隔的列表启用指定类别(例如 RUNTIME_HOOKS=sleep,streams) |
| 类别 | 钩住什么 |
|---|---|
sleep |
原生 sleep() 和 usleep() 会像 oxphp_sleep()/oxphp_usleep() 一样挂起当前纤程 |
streams |
两种等待会挂起当前纤程而不是钉住工作线程:对 tcp:// 套接字流的阻塞读取,以及 stream_select()。读取覆盖在单个套接字上阻塞的客户端——fsockopen()、stream_socket_client()、HTTP 流封装器、mysqlnd(PDO_MySQL、mysqli)、phpredis;stream_select() 覆盖同时等待多个套接字的循环。两者都无需改动代码。以其他方式等待的客户端不受影响(见下文) |
钩子在工作进程模式的请求纤程和异步任务纤程内部生效。在纤程之外(传统/框架/SPA 请求上下文、CLI),保留原生行为,包括参数校验错误。启用 sleep 钩子后,直接调用 sleep() 的第三方代码不再钉住工作线程——无需改动任何代码。在被钩住的 sleep 期间取消异步任务,会以 OxPHP\Async\AsyncException 展开它;被钩住的 sleep() 总是返回 0(原生内置函数的信号中断返回值不会出现)。
streams 钩子覆盖的范围
streams 钩子让其变得协作式的,是对 PHP 套接字流的阻塞读取,以及 stream_select() 内部的等待。在指望它之前,先确认你的客户端是用这两种方式之一等待的。几个常见的客户端不是,对它们来说什么都不会变:
- ext/curl。
curl_exec()和curl_multi_*自己跟套接字打交道,位于 PHP 流之下,所以钩子永远看不到它们。这覆盖了所有基于 curl 的 HTTP 客户端——包括使用默认 handler 的 Guzzle。curl 客户端可以改为指向 stream 封装器 handler,那条路径会经过 PHP 流。 socket_select()。 那是 ext/sockets,一套作用于原始描述符的不同 API,没有被钩住。stream_select()被钩住了。同样的道理适用于stream_socket_accept()内部的等待,它也没有被钩住。- 来自
socket_export_stream()的流。 它们携带一张不同的 ops 表,是 PHP 的私有实现,无法从扩展打补丁。 unix://、udp://和udg://流。 同样的原因:它们的 ops 表是 PHP 私有的。ssl://和tls://,一旦加密激活。 在stream_socket_enable_crypto()成功之前,SSL 流的读取委托给普通套接字读取,因此确实会挂起;从握手起就不会了。- 建立连接与 DNS 解析。 保持连接打开的客户端在每次查询上受益;连接建立本身不受益。
- 通过
localhost访问的 MySQL。 MySQL 客户端把localhost读作“使用 unix socket”,那不是tcp://流——请在 DSN 里写127.0.0.1。这对 PDO_MySQL 和 mysqli 同样适用,而且很容易被忽略,因为一切照常工作,只是没有收益。 - 写入。 只有读取会等待。只要一个纤程拥有描述符且没有别的东西把数据读走,读就绪状态就会保持不变;而发送缓冲区里的空间是由对端授予和收回的,所以因可写而被唤醒的纤程,可能在写入执行时发现窗口又关上了,此后 PHP 无论如何都会让线程阻塞满它的整个超时。因此,填满套接字缓冲区的写入,其行为与没有钩子时完全一样。实践中这代价很小:花时间的是等待回复,而不是把查询递给内核。(写入仍会被查看,但只为一件事:这条连接当前承载的是哪个纤程的往来交换——见下文关于共享连接的说明。在没有其他纤程使用的连接上——通常情况下的每一条连接都是——写入走原生路径,不受触碰。)
在这个范围之内,钩子保持原生契约:套接字超时(stream_set_timeout()、default_socket_timeout)不变地适用,超时的读取仍通过 stream_get_meta_data() 报告 timed_out,流的身份不受触碰,所以 socket_import_stream() 之类的继续工作。超时有一个限制:截止时间每个调度器 tick 只检查一次,所以它最早在下一个 tick 触发——工作进程模式下最好是 100 µs,异步池 50 µs,负载之下更长,因为一个 tick 会持续到工作进程正在运行的东西结束为止。异步池在无事发生时会暂停更久,但不会睡过它持有的截止时间:读或写的截止时间会把暂停缩短到正好落在自己身上,所以等得更久不会让超时更晚。在 default_socket_timeout 为 60 秒的情况下,绝大多数部署观察不到这一点。
stream_select() 的钩法是:先等待三个数组点名的描述符,然后把调用交给 PHP、超时设为零,所以你能观察到的一切仍由 PHP 决定:返回计数、把数组改写为只剩就绪的流、警告,以及参数错误。只要数组里有钩子不会替其站岗的东西,等待就会被跳过——调用就像没有钩子一样运行:已有缓冲数据的读取流(stream_select() 会直接从缓冲区应答,不看描述符)、根本没有描述符的流、不是活跃流的元素(比如留在数组里的已关闭流——PHP 用错误应答,而非等待)、内核根本不会监视其就绪状态的描述符(普通文件是常见情形,它在你问的那一刻就算就绪)、以及达到或超过 FD_SETSIZE 的描述符——PHP 自己的 select() 直接拒绝它。最后一条是真实的天花板而非形式条款:忙碌的工作进程可以持有超过 1024 个打开的描述符,点名其中之一的 stream_select() 无论有没有钩子都会以同样的方式失败。
并发纤程之间共享的连接
在并发纤程之间共享一条连接,对 OxPHP 守护的客户端来说是安全的,也不会带来收益。 这是工作进程模式应用的常态而非边缘情况:WordPress、Laravel 和 Symfony 在工作进程启动时打开一次数据库和缓存客户端,把同样的实例交给每个请求;除非重写它们的数据访问层,否则无法要求它们为每个纤程开一条连接。
客户端协议是一串往来交换——写一条命令、读它的应答——连接上没有任何东西标记一次交换在哪里结束。停在读取上的纤程停在一次交换的中间,第二个纤程的命令落在那里会破坏协议。两个客户端对此的失败方式不同,且都不体面:mysqlnd 跟踪自己的连接状态,在发送任何东西之前就拒绝该命令;phpredis 没有这种检查,两个纤程会读到彼此的应答——一个请求的数据被返回给另一个请求,且不抛任何错误。
因此,纤程在使用连接之前会先认领它,而且是在必须发生的两个层面上:套接字 ops 层,保证字节有序;以及 PDO 和 mysqli 的客户端入口层,因为 mysqlnd 的拒绝发生在任何 I/O 之前,套接字层的任何手段都来不及阻止它。另一个纤程碰到同一条连接时,在客户端层会等待它被让出——那里持有的只是连接的身份;在套接字层则不等待而是被拒绝,就像套接字超时被拒绝那样,因为挂起在别人的流的操作里的纤程,握着的是其属主随时可能释放的指针。phpredis 也在客户端层被逐个方法地守护,正是出于这个原因。客户端层认领的是连接本身而不是持有它的 PHP 对象,所以经由多个 PDO 对象访问的持久连接算作同一条;用 PDO::connect() 打开的连接——它返回驱动自己的子类而不是 PDO——也和其他连接一样被覆盖。
因此,钩子的收益应当理解为属于纤程为自己打开的连接:自己发 HTTP 或数据库调用的异步任务、自己开客户端的请求。共享连接得到的,是等待期间把工作线程还回来,让其他请求去跑不在这条连接上的工作;它自己的往来交换一个接一个地运行,和钩子关闭时完全一样。有四条边界值得了解:
- 查询过一次的纤程会把连接保留到它的请求结束,因为请求的结束是第一个确定越过了一次交换结尾的时刻。它停在任何别的东西上时同样保留着连接,所以一个已经查询过、又在等待自己那份需要同一条连接的工作的请求,是在等待它自己;两者靠下面那条界限分开,而不是继续推进。
- 等待总是有界的:以
max_execution_time和default_socket_timeout中较小者为界。两者都不设时这个界是 30 秒,因为服务器 SAPI 采用引擎的默认值——前者 30、后者 60;只有max_execution_time为 0 时才是default_socket_timeout的 60。max_execution_time按请求当前的值读取,因为set_time_limit()就是请求声明自己可以运行多久的方式;default_socket_timeout按进程启动时的值读取,因为它是套接字操作的默认截止时间,一个请求为自己的某次调用把它收窄——库常这么干,还常忘了恢复——不应缩短同一工作进程上后续请求的这个界限。过了这个界,调用回落到无守护的行为,并在服务器日志里说明原因:对 PDO 和 mysqli,它被交给客户端,客户端对交换中途下发命令的拒绝,正是应用早已在处理的那个错误;对 phpredis——它没有这种拒绝,反而会读到别人的应答——则抛出RedisException,什么也不发送。套接字层的冲突没有自己的界,因为它从不等待:操作像超时一样立刻失败,stream_get_meta_data()报告timed_out。两个纤程各自握着对方等待的东西时,因此在这个界上分开,而不是永远互相等待。 - 有些情况被有意地不覆盖。 跨请求保留的语句或结果对象——没有任何被认领的调用先于它:在先前请求里 prepare 的语句上调用
PDOStatement::execute(),其行为与完全没有认领时一样。在另一个纤程交换中途对持久连接构造第二个句柄:PDO 在交出池化连接前会检查它是否存活,这个检查在交换中途会失败,PDO 的回应是丢弃该连接。在原始套接字上手写的协议——写一条命令、挂起在别的东西上、稍后再读应答——因为套接字层只在持有者停在应答本身上时才拒绝,在这两个时刻之间,另一个纤程会把连接接管过去;上面三个客户端靠客户端层认领得到覆盖,手写协议没有等价物。还有任何完全在纤程之外触及连接的东西,比如引擎的循环回收器在请求之间运行的析构函数——没有任何认领能看到它。 - 在另一个请求停在读取上时关闭共享连接,会终结那个请求,以
500和一条点名事由的日志行收场。认领能把两次交换隔开;它不能让连接活着,而停着的请求的应答在一条已不存在的连接上——所以诚实的回答是终结它,而不是把释放后的内存现在装着的东西交回去。哪些调用能造成这种局面取决于客户端,其中只有一个会等待:mysqli::close()和mysqli_close()是被认领的调用,会等待持有者、等完之后再关闭;Redis::close()同样被认领,但抛出RedisException而不是关闭。其余的都立即关闭——原始流上的fclose(),还有重要的 PDO:它根本没有close()方法,连接靠丢弃句柄的最后一个引用来释放(unset($pdo)、重新赋值、让它离开作用域),而那条路径在引擎自己的对象析构里运行,那里不查询任何认领。所以重新赋值共享 PDO 句柄的重连助手会终结停在旧连接上的请求,得不到mysqli等价物那样的有界等待。请求在关闭发生时就被立刻告知,而不是等到它自己的读取截止——对 mysqlnd 那将是mysqlnd.net_read_timeout,默认长达一天。为了强制重连而关闭共享连接的应用(WordPress 的$wpdb->check_connection()是最常见的)应当预期:那一刻停在连接上的请求会失败,而不是返回错误的数据。
还有一条边界:把钩子跑在用户态纤程调度器(AMPHP、Revolt)之下会回落到阻塞式 I/O。这类调度器启动的纤程运行在它自己的上下文上,OxPHP 的调度器无法恢复它,所以钩子检测到这种情况后走原生路径,而不是把任何一个调度器搞坏。这说的是请求内部由用户态调度器启动的纤程。请求自己的纤程由 OxPHP 驱动,在工作进程模式下它是一个真正的 Fiber——请求内部的 Fiber::getCurrent() 会返回它——所以只需要区分并发请求的库无需任何回退就能工作。同一个事实也是 Revolt 拒绝在工作进程模式请求内部运行其事件循环的原因。
启用与开销
1、true 和 all 启用所有类别,streams 也在内。已经为 sleep 钩子设置了 RUNTIME_HOOKS=1 的部署,升级后无需任何改动就会开始钩住套接字;如果那不是你想要的,请显式列出类别(RUNTIME_HOOKS=sleep)。启用 streams 还会在启动时于内存中修补 PHP 流 ops 表的一个条目;那一页会被按原样放回,但在无法确定其原始保护属性的平台上,它会保持可写——一点硬化上的小损失,会在服务器日志中报告。
开销,实测:在没有其他停放者的工作进程上,每次套接字往返约 3–5 µs;有 64 个纤程停在描述符上时约 5–6 µs;200 个时约 7–11 µs。就绪状态通过内核在两次等待之间保留的集合解析,所以这个数字追踪的是有多少描述符变为就绪,而不是有多少在等待。空闲的工作进程会在那些描述符上等待,而不是睡一个固定间隔、在下一个 tick 才注意到就绪——这一点非常重要:盲睡的代价大约是每次往返 2 ms。
宽的 stream_select() 带有窄情形没有的开销,因为调用点名的每个描述符都要在等待前注册、等待后移除。把等待本身剔除后测量——每个描述符都已可读,调用立即返回,只剩开销——1 个描述符时约 6 µs 对无钩子的 5 µs,64 个时 56 µs 对 9 µs,200 个时 130–150 µs 对 16–21 µs:大约每个描述符 0.65 µs。
请把它理解为每次调用的固定成本,而不是同一份工作的变慢。真正会等待的 stream_select()——select 循环本来就是为此写的——让它相形见绌:一毫秒的等待就让 200 描述符的数字降到整个调用的十分之一左右,而它买回的是工作线程——未钩住的调用会把线程整段等待都占着。有两种形态是例外,对它们这个类别最好保持关闭:跨很多描述符、几乎从不等待的调用,因为没有线程时间可回收;以及请求本身就是事件循环的情况,那里线程反正没有别的可跑。在单条连接上等待的客户端——mysqlnd、phpredis、HTTP 流封装器——位于表格顶端,开销在一微秒上下。
RUNTIME_HOOKS 与 oxphp_sleep() 的关系
钩子不取代第一方原语——它们覆盖不同的情形:
- 在你自己写的代码里用
oxphp_sleep()/oxphp_usleep()。它们在工作进程模式下无条件挂起纤程,不依赖任何环境标志,而且oxphp_sleep()接受小数秒(oxphp_sleep(0.25))——原生sleep()(整数秒)表达不了的精度。 - 为你无法编辑的代码启用
RUNTIME_HOOKS=sleep——直接调用原生sleep()/usleep()的框架或第三方库。它默认关闭,只在纤程内生效,并保持原生契约(被钩住的sleep()仍接受int并返回0)。
让你自己的处理器依赖 RUNTIME_HOOKS 来获得协作性,等于把它们的协作性绑在一项部署设置而不是代码上;在那里请优先使用显式的 oxphp_sleep()。
共享状态
进程内并发原语(OxPHP\Shared\Counter、Map、Channel、Mutex、Once、Pool、Atomic、Flag、Registry)。API 概览参见共享状态。
| Variable | Default | Description |
|---|---|---|
SHARED_ENABLED |
true |
布尔——参见布尔值。整个 OxPHP\Shared\* 子系统的总开关 |
SHARED_MAX_ENTRIES |
100000 |
所有共享条目合计的全局上限。超过后插入会以 CapacityException 失败 |
SHARED_MAX_BYTES |
1073741824(1 GiB) |
所有共享条目估算内存的全局上限 |
SHARED_SOFT_LIMIT_RATIO |
0.7 |
当使用量越过 SHARED_MAX_BYTES / SHARED_MAX_ENTRIES 的这一比例时,开始丢弃最低优先级的工作 |
SHARED_METRICS_ENABLED |
true |
布尔。切换 oxphp_shared_* Prometheus 暴露 |
SHARED_INTROSPECTION_ENABLED |
true |
布尔。切换内部服务器上的 /__ox_shared/* 内省 API |
SHARED_INTROSPECTION_PREVIEW_ENABLED |
true |
布尔。切换内省响应中的值预览(当预览可能泄露敏感数据时禁用) |
SHARED_CYCLE_DETECT_DEPTH |
16 |
环检测期间的 BFS 深度。对合法的深层图可调高 |
SHARED_CYCLE_DETECT_EDGES |
10000 |
环检测期间遍历的边数。对合法的稠密图可调高 |
SHARED_MAX_VALUE_SIZE |
1048576(1 MiB) |
每个值的大小上限。插入更大的值会快速失败 |
SHARED_MAX_CHANNEL_BYTES |
67108864(64 MiB) |
每个 channel 的负载(请求体)总量上限 |
SHARED_POISON_STRICT |
false |
布尔。为真时,Mutex/Once 闭包内部的 panic 会永久性地污染该原语,而非尽力恢复 |
SHARED_LOCK_DIAGNOSTICS |
off |
锁竞争诊断:off、count 或 trace |
SHARED_LOCK_POLL_INTERVAL_MS |
100 |
锁诊断采样器使用的轮询间隔 |
SHARED_PREVIEW_STRING_LIMIT |
256 |
/__ox_shared/preview 预览中每个字符串的截断长度,以字节计(在字符边界处截断) |
SHARED_PREVIEW_ARRAY_LIMIT |
20 |
/entry?id=… 预览中采样的条目数 |
性能分析
发出 xhprof / speedscope 追踪的采样性能分析器。输出格式和查看器集成参见性能分析。
| Variable | Default | Description |
|---|---|---|
PROFILER_ENABLED |
false |
布尔——参见布尔值。总开关。所有其他 PROFILER_* 变量在启动时仍会被解析,以便拼写错误立即暴露 |
PROFILER_SAMPLE_RATE |
0.0 |
请求被采样的概率(0.0–1.0)。范围之外的值会被钳制 |
PROFILER_INTERNAL |
false |
布尔。为真时,对内部服务器(/health、/metrics、插件端点)的请求也有资格被采样 |
PROFILER_AUTH_TOKEN |
(未设置) | 可选的 bearer 令牌。设置后,oxphp_profiler_* PHP 函数要求请求携带该令牌才能启用按需性能分析 |
PROFILER_MAX_SPANS |
50000 |
每个请求的性能分析 span 上限。超过上限的性能分析会被截断 |
PROFILER_MAX_DEPTH |
256 |
每个采样捕获的最大调用栈深度。硬上限为 65535 |
PROFILER_OUTPUT_DIR |
/tmp/oxphp-profiles |
磁盘上性能分析文件的目录 |
PROFILER_OUTPUT_FORMATS |
xhprof,speedscope |
逗号分隔的要写入磁盘的输出格式列表 |
PROFILER_DISK_MAX_PER_SEC |
10 |
每秒写入磁盘的性能分析文件的限流 |
PROFILER_RETENTION_COUNT |
100 |
PROFILER_OUTPUT_DIR 中保留的最大性能分析文件数。较旧的文件会被清理 |
PROFILER_EXPORT_URL |
(未设置) | 用于 POST 性能分析的远程端点。设置后,除非 PROFILER_OUTPUT_FORMATS 为空,否则仍会写入磁盘 |
PROFILER_EXPORT_FORMAT |
xhprof |
PROFILER_EXPORT_URL POST 的传输格式 |
PROFILER_EXPORT_AUTH_TOKEN |
(未设置) | 随每次导出请求一起发送的可选 bearer 令牌 |
PROFILER_EXPORT_XHGUI |
(自动检测) | 布尔。强制对导出负载进行 XHGui 兼容封装。未设置 = 当 PROFILER_EXPORT_URL 路径以 /run/import 结尾时自动检测(不匹配 host/query 提示) |
PROFILER_EXPORT_BUGGREGATOR |
(自动检测) | 布尔。强制使用 Buggregator 信封。未设置 = 当 PROFILER_EXPORT_URL 路径以 /api/profiler/store 结尾时自动检测。该信封始终发出 xhprof,因此对它而言 PROFILER_EXPORT_FORMAT 会被忽略(非 xhprof 值会警告,非致命)。与 PROFILER_EXPORT_XHGUI 互斥——同时启用两者是启动错误 |
PROFILER_EXPORT_APP_NAME |
(未设置) | 用于项目分组的 Buggregator app_name |
PROFILER_EXPORT_TAGS |
(未设置) | Buggregator tags,格式为 key=value,key2=value2;格式错误的 token、空键或重复键都是启动错误 |
示例配置
开发
LISTEN_ADDR=127.0.0.1:8080
DOCUMENT_ROOT=./public
LOG_LEVEL=debug
ACCESS_LOG=all
PHP_WORKERS=1
INTERNAL_ADDR=127.0.0.1:9090生产(框架模式)
LISTEN_ADDR=0.0.0.0:80
DOCUMENT_ROOT=/var/www/html/public
ENTRY_FILE=index.php
PHP_WORKERS=8
QUEUE_CAPACITY=1024
LOG_LEVEL=warn
ACCESS_LOG=error
MAX_CONNECTIONS=10000
INTERNAL_ADDR=127.0.0.1:9090
RATE_LIMIT=100
RATE_WINDOW_SECONDS=60
TRUSTED_PROXIES=private
HEADER_TIMEOUT_SECONDS=5
DRAIN_TIMEOUT_SECONDS=25
COMPRESSION_LEVEL=4
STATIC_MAX_AGE=30d生产(工作进程模式)
LISTEN_ADDR=0.0.0.0:80
DOCUMENT_ROOT=/var/www/html/public
WORKER_MODE_ENABLED=true
ENTRY_FILE=../worker.php
PHP_WORKERS=8
WORKER_MAX_MEMORY_MIB=128
QUEUE_CAPACITY=1024
LOG_LEVEL=warn
ACCESS_LOG=error
INTERNAL_ADDR=127.0.0.1:9090TLS
LISTEN_ADDR=0.0.0.0:443
TLS_CERT=/etc/ssl/oxphp/cert.pem
TLS_KEY=/etc/ssl/oxphp/key.pem
DOCUMENT_ROOT=/var/www/html/public
ENTRY_FILE=index.php查看当前生效的配置
当内部服务器运行时,查询 /config 端点以查看已解析的配置:
curl -s http://localhost:9090/config | jq .{
"listen_addr": "0.0.0.0:80",
"document_root": "/var/www/html/public",
"entry_file": "/var/www/html/public/index.php",
"log_level": "warn",
"executor_type": "sapi",
"php_workers": "8",
"tokio_workers": 4,
"queue_capacity": 1024,
"queue_wait_timeout_ms": 1000,
"queue_max_waiting": 1024,
"queue_max_waiting_bytes": 67108864,
"max_connections": 10000,
"drain_timeout_seconds": 30,
"header_timeout_seconds": 5,
"rate_limit": 100,
"rate_window_seconds": 60,
"tls_enabled": true,
"compression_level": 4,
"access_log": "all",
"max_query_body": 524288,
"worker_mode_enabled": false,
"worker_max_memory_mib": 0,
"static_max_age": 2592000,
"static_revalidate": false,
"async_workers": 0,
"async_queue_capacity": 0,
"async_max_fibers": 256,
"async_in_flight_cap": 0,
"trace_context": true,
"superglobals_enabled": true,
"trusted_proxies": false,
"plugins": {
"otel": {
"enabled": true,
"protocol": "grpc",
"service_name": "oxphp"
},
"apm": {
"enabled": true,
"slow_query_ms": 100,
"db_capture_params": false,
"hooks_registered": 34
}
}
}对外提供的 /config 响应会清除内部 Config 表示所携带的少数几个键:TLS 证书和密钥路径永远不会被发出(tls_enabled 表示 TLS 是否处于激活状态),internal_addr 和 error_pages_dir 也会被移除——这些部署拓扑和文件系统路径有利于攻击者,且指标抓取器并不需要它们。