更新日志
服务器、PHP API 和镜像的每一处重要变更——每个版本精选亮点。完整、未删减的历史记录见 CHANGELOG.md。
工作进程模式:按请求计算的 max_execution_time — 工作进程在承载其他请求的同时接手的请求,现在拥有自己的时限,从被接手时开始计时,set_time_limit() 也只影响调用它的那个请求。此前依靠共享计时器存活的 SSE 流或长轮询必须自行调用 set_time_limit(0);发布镜像不附带 php.ini,因此该时限为 30 秒。
RUNTIME_HOOKS=1、true 和 all 包含 net — 升级后,建立连接、TLS 握手与读取、等待缓冲区空间的写入,以及 unix://、udp:// 和 udg:// 读取都会挂起纤程,无需你做任何修改。runtime_hooks 现在报告 ["sleep","streams","net"],在纤程之间共享的流无论使用何种传输方式都会受到保护。RUNTIME_HOOKS=sleep,streams 可保留旧的类别集合。
Shareable 仅保留给 Shared 类型 — PHP 类不能再实现 OxPHP\Shared\Shareable,也不能再继承 Shared 类型(它们现已声明为 final);两者都会在声明时引发致命错误。此前把这样的对象传递给另一个线程会导致服务器崩溃。现在对 Shared 类型或 Shareable 调用 PHPUnit 的 createMock() 会失败;请改用你自己的标记接口。
移除 Shared\* 可观测性别名 — oxphp_shared_channel_pending 和 oxphp_shared_pool_size 从 /metrics 中移除,pending/size 也从 /__ox_shared/entry 的 type_specific 中移除,此前提示这些别名的启动 WARN 也随之移除。仪表盘和告警必须改用 oxphp_shared_channel_count、oxphp_shared_pool_count 和 count。
TRUSTED_PROXIES 读取 IP:port 形式的条目 — 位于会向 X-Forwarded-For 写入 IP:port 的负载均衡器(Azure Application Gateway)之后时,REMOTE_ADDR 现在是客户端地址,REMOTE_PORT 是其端口。如果此类网关前面的 CDN 也会追加条目,该地址现在会是 CDN 自己的地址;请把 CDN 的地址段也列入(Cloudflare 则使用 cloudflare 关键字)。
位于发布符号链接之后的 DOCUMENT_ROOT — 根目录在启动时解析一次,每个从磁盘读取的静态文件和每个 PHP 脚本在打开时都会对照它进行检查,因此一旦切换 /app/current,它们都会以 404 应答,直到服务器重启。请让 DOCUMENT_ROOT 指向一个在各次发布之间不变的路径,或者把重启作为切换的一部分。
服务器无法打开的脚本 — 在工作进程模式之外,已删除的脚本会以 404 应答,即使 OPcache 在 validate_timestamps 关闭的情况下仍持有它,因此原地删除并重新创建文件的部署(git checkout、rsync --delete)会出现一段 404 窗口期;请把新文件写在旧文件旁边,再通过重命名覆盖旧文件。无法读取的脚本会以 500 应答,并记录 Cannot open script。
通过符号链接访问的脚本按其目标运行 — 在工作进程模式之外,经由指向另一个目录的链接访问到的脚本会以该目录作为工作目录运行,PHP 是否将其视为 phar 取决于解析后的文件名,而不是 URL。请基于 __DIR__ 构建相对路径。
fiber.stack_size 必须大于 zend.reserved_stack_size — 在工作进程模式和 ASYNC_WORKERS 任务中,纤程栈限制现在会为 PHP 的保留区(release 构建中为 48 KiB)留出空间,因此等于或小于该值的 fiber.stack_size 会让每个请求都以 Maximum call stack size of 0 bytes 失败,深度递归也会比以前早 44 KiB 停止。
性能分析器禁用时 Profile\start() 不做任何事 — 在 PROFILER_ENABLED 关闭时,它不再让 Profile\is_active() 返回 true,因此启动性能剖析并断言 is_active() 的测试需要启用性能分析器。启用 OTEL_APM_ENABLED 时,该调用过去会丢弃的 APM span 现在会保留在追踪中。
moveTo() 之后的 UploadedFile::type() 读取目标文件 — 类型检测不再在移动之前运行,因此如果文件在首次调用 type() 时已被再次移动、删除或改写,它报告的是该位置当前的内容,或者报告 application/octet-stream 并附带一条警告。如果移动后的文件不会原地保留,或位于流包装器之后,请在 moveTo() 之前调用 type()。
.oxphpdeny 路径拒绝规则 — 位于 DOCUMENT_ROOT 顶层、采用 .gitignore 语法的文件会阻止其中列出的路径被提供,适用于所有路由模式,静态文件与 PHP 一视同仁,并在任何磁盘访问之前生效。被拒绝的路径以 PHP_DENY_FALLBACK 应答,> 规则则把请求交给入口脚本。该文件在启动时读取一次;拒绝次数计入 oxphp_path_deny_total。
RUNTIME_HOOKS=net — 通过 PHP 流传输层建立的连接、TLS 握手与读取、等待发送缓冲区空间的写入,以及 unix://、udp://、udg:// 和 socket pair 流上的读取,都会挂起纤程,而不是占住工作线程。需要同时列出 streams,且仅在 Linux 上运行。不涵盖:libcurl、libpq、libmemcached、DNS 以及持久连接的建立。
TRUSTED_PROXIES 的 cloudflare 关键字 — 关键字、CIDR 和裸 IP 现在可以混合写在同一个列表中(private,100.64.0.0/10 此前会导致启动错误)。cloudflare 信任 Cloudflare 的边缘地址段,并在越过你自己的代理之后的第一跳是 Cloudflare 边缘节点时,从 CF-Connecting-IP 读取客户端地址;private,cloudflare 适用于 Cloudflare → 负载均衡器 → OxPHP。
追踪和访问日志中的 User-Agent — 根服务器 span 携带 user_agent.original,每条访问日志条目携带 user_agent 字段,截断至 512 字节并替换无效的 UTF-8,因此仅凭追踪就能区分爬虫和浏览器。
请求被卸除的原因 — 过载日志行现在将拒绝细分为 refused_queue_full、refused_wait_timeout、refused_waiting_full 和 refused_waiting_bytes,被拒绝请求的根 span 携带 oxphp.shed_reason,因此无需 INTERNAL_ADDR 监听器就能看出该调高哪个限制。
/metrics 中的 OTLP 导出失败 — oxphp_otel_export_failures_total、oxphp_otel_export_failed_spans_total 和 oxphp_otel_export_retries_total 分别统计失败的导出、这些导出携带的 span 以及快速失败后的重试。失败的导出还会以 OTLP span export failed 记录日志,并附带批次大小和原因。
启动时的 CPU 计数 — 一行 CPU count for default sizing 日志会报告 cpus、cpu_source(host、affinity、cgroup quota 或 unknown)、host_cpus,以及由此得出的 PHP_WORKERS 和 TOKIO_WORKERS。在标称 CPU 数更少的套餐上出现 cpu_source=host(如 gVisor 下的 DigitalOcean App Platform),意味着这两个变量都应显式设置。
Debian(glibc)nightly 镜像 — :nightly-trixie、:php8.4-nightly-trixie 和 :php8.5-nightly-trixie 在 php:<minor>-zts-trixie 上运行同一个服务器,在一个 Yii3 应用上每个响应的 CPU 用量比 Alpine 低 14–19%。这些镜像中 www-data 的 UID 为 33,扩展在 trixie 阶段中构建;带标签的正式版本仍基于 Alpine。
更廉价的 PHP 函数调用 — 为卡住请求分类而统计每一次调用(包括内部函数调用)的引擎观察者已被移除。在一个 Yii3 应用上,每个请求的 CPU 用量在经典模式下下降 5%,在工作进程模式下下降 7%;每个请求进行 15 000 次短调用的脚本少用 23–25%。
APM:phpredis 的 hGet、hSet、lPush 和 rPush — 开启 OTEL_APM_ENABLED 时,这四个方法根本不经过 Redis 就返回 NULL:写入丢失,读取返回空值,既没有错误也没有 span。钩子现在与 PHP 一样,以不区分大小写的方式匹配方法名。
PHP 8.5:内存耗尽错误之后的 memory_limit — 工作进程模式或异步任务中的深栈 OOM 会让工作进程完全不再执行任何内存限制,因此下一个失控的请求会不断增长,直到内核杀死服务器及其中的每个工作进程。现在该限制会在请求结束时恢复,仍超出限制的工作进程会被回收。
调用栈溢出不再导致段错误 — 在工作进程模式或异步任务中,达到 PHP 栈限制的递归会以通常的 Error 结束,而不会让服务器崩溃;此前只需在最后一个允许的层级上调用 realpath() 就足以触发崩溃。该限制现在就是 PHP 为每个纤程设置的限制。
工作进程模式:Fiber 内的致命错误 — 请求启动的纤程中的致命错误,或垃圾回收器运行的析构函数中的致命错误,会读取已释放的调用帧,并可能让进程连同所有连接一起崩溃。现在该请求以 500 结束,工作进程继续提供服务。
工作进程模式:超时结束正确的请求 — 每个线程唯一的计时器会结束当时正在运行的任意请求,并把它本应针对的请求计为处理器失败,三次之后便回收工作进程。现在每个请求都有自己的截止时间;在挂起期间到期的截止时间会以 504 结束该请求,且不计入回收。
工作进程模式:引导之后的 max_input_time — 在 max_execution_time = 0 且设置了 max_input_time 的情况下,为工作进程引导而启动的计时器会在 max_input_time 秒后结束某个请求(在 PHP 自带的 php.ini 模板下为 60 秒)。现在工作进程会在开始提供服务之前停止该计时器。
异步任务与池线程的截止时间 — 池线程唯一的计时器会在进程运行约 max_execution_time 之后到期一次,以 Maximum execution time of 0 seconds 结束当时正在运行的任意任务。现在池线程不持有截止时间,因此 set_time_limit() 不再约束任务:请用 oxphp_async_await() 的超时来约束它。
工作进程退出时关闭持久连接 — 工作进程模式下的每次回收和动态池的每次缩容,都会让 PDO::ATTR_PERSISTENT、mysqli p: 和 pfsockopen() 连接一直保持打开直到进程重启,不断累积,直到数据库拒绝新的客户端。现在它们会在线程的最后一个请求之后关闭。
工作进程模式:被取消的内部调用遗留的内存 — 在内部函数中途结束的请求(例如 usort() 的比较器执行到一半时)会在工作进程的整个生命期内泄漏该函数分配的内存。现在,经过这样一个请求后增长超过 4 MiB 的工作进程会在下一个空闲时刻被回收,并发出一条警告。
Shared 可调用对象中的致命错误 — Mutex::withLock()、Once::getOrInit()、Registry 工厂或 Pool::with() 中的致命错误,会在进程的整个生命期内让互斥锁保持锁定、Once 处于待定状态、键卡在创建中,或让池少一个槽位。现在这些都会在请求结束时归还。
RUNTIME_HOOKS=streams 搭配 APM — APM 对 PDO、mysqli 和 Redis 的包装器跳过了连接保护,因此共享同一连接的纤程可能读到彼此的 Redis 应答,或者遇到 Cannot execute queries while other unbuffered queries are active。包装器现在会调用受保护的处理器。
RUNTIME_HOOKS=streams:请求之间的析构函数 — 由工作进程循环的循环回收运行的析构函数,可能会在共享连接上写入另一个请求的交互并读取其应答。现在,若连接的持有者正挂起在该连接上,对它的套接字等待会立即失败(如同超时),且不发送任何内容。
持久化 PDO 构造按数据源等待 — 卡在一个从不应答的数据库上的构造,会阻塞该工作进程上所有其他持久化构造,直到它们的等待耗尽,随后它们会在未串行化的情况下继续执行。现在它们只等待指向同一 DSN 的构造。
在共享的持久连接上释放 PDO 句柄 — 在 RUNTIME_HOOKS=streams 下,一个请求释放其句柄会回滚另一个请求的事务,而在 pdo_mysql 下还可能在读取中途释放结果集并导致段错误。现在只有连接的当前持有者才会执行这一清理,来自其他纤程的 PDO::rollBack() 不会发送任何内容。
计划内的回收不是崩溃 — 因 WORKER_MAX_MEMORY_MIB 或 Worker::scheduleExit() 而回收的工作进程会以 INFO 级别记录为 Worker mode thread recycled 并附带原因,而不再是一条看起来像崩溃的 Dead workers detected WARN。
通过 http/protobuf 的 OTLP 导出 — 通过该协议从未成功导出过任何一个 span:导出线程会 panic,每个 span 都会记录 Spans are being emitted even after Shutdown。现在 span 会送达 OTEL_EXPORTER_OTLP_ENDPOINT 之下的 /v1/traces,该变量是一个基础 URL;如果其中已带有 /v1/traces,请将其去掉。
基于 gRPC 的 OTLP:建立连接与失效连接 — OTEL_EXPORTER_OTLP_TIMEOUT 现在也约束建立连接的过程,并且在 Linux 上,不再接收数据的连接会被替换,而不是在大约 15 分钟内让每次导出都失败。在因空闲而被断开的连接上快速失败的导出会再发送一次。已设置但为空的 OTEL_EXPORTER_OTLP_ENDPOINT 现在会在启动时被拒绝。
oxphp_worker_stuck_total 的分类 — 不含任何调用的 PHP 循环会被读作 c_call,而卡住请求的首次读数可能是相对于更早一个请求的读数测得的。监督器现在直接询问引擎本身,所用的正是传递 max_execution_time 的那个中断。
没有 fileinfo 时的 UploadedFile::moveTo() — 在没有 mime_content_type() 的情况下,或在把警告转为异常的错误处理器之下,moveTo() 会在移动之前抛出异常,上传的文件则会在请求结束时被删除。现在它会先移动,并把类型检测留给 type()。
异步任务结束时的析构函数异常 — 在任务拆除期间被释放的对象,其析构函数抛出的异常会悄无声息地消失;现在它会被报告为未捕获异常,并写入错误日志。
性能分析器的 index.json 记录 user_agent — 文档中描述的该字段此前始终为 null。
文档:SSE 指南 — 工作进程模式示例此前用 BRPOP 读取事件,因此每个事件只会送达一个客户端;现在它读取 Redis Stream,并从 Last-Event-ID 处恢复。指南不再声称 while (!connection_aborted()) 循环会通过其条件结束,其示例也会调用 ignore_user_abort(true) 和 set_time_limit(0)。
静态文件:路由之后被重新指向的符号链接 — DOCUMENT_ROOT 内的某个链接在一次请求中通过验证,之后被重新指向(比如指向 /etc/passwd),在下一次请求中就会被跟随。现在文件会在打开之后,对照内核为该文件描述符报告的位置进行检查;仍在内存缓存中的文件保留其已验证过的字节内容。
PHP 脚本:路由之后被重新指向的符号链接 — 在工作进程模式之外,一旦 PHP 的 realpath 缓存释放了旧目标,这样的链接就可能运行攻击者的脚本、输出一个文本文件,或让工作进程挂在 FIFO 上。现在工作进程自己打开脚本、检查文件描述符,再把该文件交给 PHP;被替换的链接会以 404 应答。代价是在极简脚本上约 3% 的吞吐量。
PHP 8.5:跨请求的 error_get_last() — 在工作进程模式和异步任务中,停在挂起点的请求可能读到另一个请求的致命错误回溯,连同其函数被调用时的参数:密码、令牌、客户记录。现在每个请求保留自己的回溯,并在结束时将其释放。
TRUSTED_PROXIES:无法解析的转发条目 — 最右侧非可信规则会跳过无法解析的条目,继续读入客户端写入的内容,因此在写入 IP:port 的网关之后,客户端可以自选地址。现在遇到无法解析的条目或无法读取的请求头行时,该规则会停止。
拆分的 Cookie 请求头 — 按 HTTP/2 允许的方式拆分到多个字段行中的 Cookie,现在会在任何组件读取它们之前按 RFC 9113 合并。此前位于后续行中的插件 __oxp_* cookie 会到达应用,而从第一行剥离这样一个 cookie 会丢掉其余行中应用自己的 cookie。
SHARED_ENABLED=false 现在会禁用 Shared\* — 该设置此前会被解析但随即被忽略;现在不会注册任何 OxPHP\Shared\* 类,构造第一个时就会抛出 Error: Class "OxPHP\Shared\Counter" not found。它还会以 OX_SHARED_ENABLED 和裸的 ENABLED 被读取,因此升级前请检查这三个变量;代码可以用 class_exists() 判断自己处于哪种情况。
工作进程模式:客户端挂断不再结束请求 — 处理器会运行到结束,让其 finally 块和清理代码得以执行,就像 ignore_user_abort 始终开启一样;上限只有 max_execution_time。调用 connection_aborted() 可以提前停止。流式响应仍会在下一次写入时结束。
ignore_user_abort(true) 在所有模式下都运行到结束 — 与 PHP-FPM 下一样,输出被丢弃,connection_aborted() 报告断开;依靠下一次写入来停止的流循环必须检查 connection_aborted()。php.ini 或工作进程引导中的 ignore_user_abort = On 现在对每个流都生效。
池丢失或卡死时 /health 以 503 应答 — 响应体新增 pool_stalled,并报告 "status": "degraded"。指向聚合 /health 的 Kubernetes 存活探针现在会重启 Pod;请改用 /health/liveness、/health/readiness 和 /health/startup。
ACCESS_LOG 不再被 LOG_LEVEL 否决 — ACCESS_LOG=all 搭配 LOG_LEVEL=warn 此前什么都不写;现在每个请求写一行,请据此规划日志量。RUST_LOG=warn,access_log=off 仍可单独关闭访问日志。
payload() 返回表单请求体和 JSON 标量 — 表单请求体现在在 SUPERGLOBALS_ENABLED=false 下也会被解析,内容为 "hi"、42 或 false 的 JSON 请求体会返回该值,而不是 null 或 $default。null 现在仅表示空请求体、未解析的 Content-Type、无效 JSON 或字面量 null。
客户端挂断计为取消 — oxphp_request_cancelled_total{reason="client_abort"} 开始上升,而 admission_refused_total{reason="wait_timeout"} 相应下降,客户端挂断时的 Connection error ERROR 日志行现在降为 debug;基于这两者的告警应重新审视。
性能分析运行按触发方式归档,使用自己的运行 id — oxphp_profiler_runs_total{source} 现在细分为 header、cookie、query、sample 和新增的 sdk;运行 id 的形式为 <ts_ms>-<req_id_prefix>-<rand4>,因此用请求 id 访问 /__profiler/runs/{id} 会得到 404——请在 /__profiler/runs 中按 request_id 查找。
PROFILER_MAX_DEPTH 开始生效 — 它此前在任何地方都不起作用。默认值现在为 0(不限制);大于 32 的值会被截断并在启动时发出 WARN,超出该深度的调用不会出现在性能剖析中。
COMPRESSION_LEVEL 已弃用 — 请使用 COMPRESSION_BROTLI_LEVEL;旧名称仍然可用,但启动时会发出 WARN。非零值不再意味着仅使用 Brotli,因为现在也会提供 Zstandard 和 gzip。/config 报告 brotli_level、gzip_level 和 zstd_level,而不是 compression_level。
SHARED_SOFT_LIMIT_RATIO 和 SHARED_POISON_STRICT 被忽略 — 两者从未被读取;现在都会在启动时记录 WARN,limits.soft_ratio 和 diagnostics.poison_strict 已从 /__ox_shared/summary 中移除。请对 oxphp_shared_capacity_saturation 设置告警;终态的 Once 使用 Once\FailureMode::Poison。
oxphp_queue_wait_us 读数变低 — 它现在在工作进程取走请求时就停止计时,每个请求约少 0.1 毫秒;亚毫秒级的告警阈值会更不容易触发。oxphp_request_duration_us 不变。
Zstandard 压缩 — 动态响应以 zstd(级别 6)发送给接受它的客户端:体积小于 Brotli,CPU 开销不到其一半。COMPRESSION_ENCODINGS(默认 br,zstd,gzip,或 off)选择编码,配合 COMPRESSION_BROTLI_LEVEL(5)、COMPRESSION_ZSTD_LEVEL(6)和 COMPRESSION_GZIP_LEVEL(6)。
为不支持 Brotli 的客户端提供 gzip — 明文 HTTP 下的 Chromium 和大多数 CLI 工具此前拿到的是未压缩的响应体。Accept-Encoding 现在按 RFC 9110 依 q 值排序,q=0 被视为拒绝;因此,小于 3 MiB 的可压缩文件会以完整的 200 回应 Range 请求。
预压缩静态缓存 — 被缓存的静态文件会在后台一次性生成一份最高质量的 Brotli 副本,比逐请求输出小 8–12%,且在同一缓存预算之内。
池停滞可见性 — oxphp_pool_stalled、oxphp_queue_depth、oxphp_queue_capacity 和 oxphp_admission_slots_available 揭示已不再从队列取走工作的池,停滞期间发出 WARN,工作恢复流动后发出 INFO(工作进程模式)。
日志中的过载 — 以 529 卸除的请求现在每个过载时段产生一条 WARN,最多每分钟重复一次;在一分钟内没有拒绝后,会发出一条带总数的 INFO。
报告 MAX_CONNECTIONS 耗尽 — 停放的 accept 循环会记录一条点名 MAX_CONNECTIONS 的 WARN,并在恢复时记录 INFO;oxphp_accept_stalled 和 oxphp_accept_stalls_total 导出这一状态。
oxphp_admission_wait_wasted_total — 统计期间没有任何工作进程开始处理任何请求的 wait_timeout 拒绝,这表明 QUEUE_WAIT_TIMEOUT_MS 过长,或应用在回调自己的服务器。
oxphp_worker_request_fibers_active — 每个工作进程的请求纤程数:256 表示该工作进程已停止接收工作,空闲时不回落到 0 的计数则意味着纤程泄漏(工作进程模式)。
RUNTIME_HOOKS 可见 — /config 和 oxphp_server_info() 将 runtime_hooks 报告为已安装类别的解析后列表,例如 ["sleep","streams"],因此拼错的变量会显示为 []。
自适应队列等待预算 — QUEUE_WAIT_TIMEOUT_MS 现在是一个上限:当四分之一或更多已开始的工作落到已离开的客户端身上时,服务器会将等待时间减半(下限为 10 毫秒或上限的 1/64),并在队列排空后恢复。oxphp_admission_wait_budget_us、oxphp_admission_wait_budget_ceiling_us 和 oxphp_abandoned_work_total 揭示这一机制。
Brotli 逐请求质量为 5 — 从 4 上调,4 的输出比 gzip 更大且更耗 CPU;超过 4 KB 的 Brotli 响应体现在在阻塞池上压缩。COMPRESSION_BROTLI_LEVEL=4 可恢复旧行为。
连接结束日志级别按错误类型区分 — 客户端关闭连接为 debug,服务器强制的超时保持 warn,其他情况保持 error,而不再由消息中是否出现 "timeout" 一词来决定。
工作进程模式:会话在请求之间泄漏 — 在另一个请求挂起时被接纳的请求会在其前任的会话中开始,并且在 session_write_close() 之后可能沿用其 id,并在 Set-Cookie 中收到它。现在每个请求开始前都会写入并关闭会话,并移除 $_SESSION。
持久连接回答了错误的请求 — 在工作进程模式之外,客户端中止可能会在 pfsockopen() 或持久化 Predis 上发出命令与读取应答之间结束请求,使连接失去同步直到重启。现在与 PHP-FPM 下一样,只在下一次写入时才对已离开的客户端采取行动。
工作进程模式:中止后页面组成发生变化 — 被中止的请求会跳过 finally 块,并且在操作码边界被捕获时会禁用工作进程上的所有析构函数,使递归保护一直处于设置状态,页面悄无声息地缺失区块。现在请求会运行到结束。
工作进程模式:逐请求的 ini 值 — 通过 ini_set()、ignore_user_abort() 或 error_reporting() 所做的更改不再泄漏到与之并行运行的请求,也不再比繁忙的工作进程活得更久;它们随挂起的请求一起迁移,并在请求结束时恢复。请求也不再以其纤程上前一个请求遗留的 error_reporting 级别开始。
RUNTIME_HOOKS=streams 下的持久化 PDO — 重叠的 PDO::ATTR_PERSISTENT 构造不再关闭正被某个请求使用的连接(导致致命错误或段错误),在另一个请求的存活 ping 期间执行查询也不再以 2006 MySQL server has gone away 断开连接。
工作进程模式:致命错误不再让工作进程永远空转 — 启用性能分析器或 #[Trace] 时,同一工作进程上的两次致命错误可能让观察者链以 100% CPU 循环,而池看起来却是空闲的。在从另一次致命错误恢复期间引发的致命错误,也不再丢弃该工作进程上的所有请求。
工作进程模式:带 __destruct 的未捕获异常 — 带析构函数的异常类会把 500 变成计入回收的致命错误(31 个请求让 8 个工作进程退役),禁用该工作进程上的所有析构函数,并泄漏该异常。
工作进程模式:客户端挂断不再回收工作进程 — 在 ignore_user_abort() 之后或取件期间的中止被当作致命错误,使池三个一组地轮换。现在断路器计入的每个请求都会记录一条 WARN,断路器导致的退出也以 WARN 记录。
工作进程模式:因写入而结束的流 — 这样的流不再在工作进程的整个生命期内泄漏其栈帧中的变量,并且在该清理过程中析构函数里发生的致命错误现在会计入错误断路器。
QUEUE_WAIT_TIMEOUT_MS 约束排队中的请求 — 预算此前只在工作进程取走请求时检查,因此停滞的池永远不会应答排队中的请求;现在它们会在截止时间到达时收到带 Retry-After: 3 的 529。
已离开的客户端不再被排队或拒绝 — 工作进程模式不再运行客户端在排队期间已离开的请求,而在客户端离开后才耗尽的预算会以 499 作为取消来应答,而不是 529。client_abort 现在统计在工作进程取走之前就已离开的客户端。
/health/readiness 反映池的健康状况 — 执行器检查此前总是通过;没有存活工作线程的池,或在工作进程模式下卡死一分钟的池,现在会以 503 应答。过载仍然不会。
oxphp_apm_trace() 会运行其回调 — 它此前返回 null 而不调用回调。回调现在在子 span 中运行,接收 span id 并返回其值,返回类型现为 mixed。
Error 类消息丢失 — TypeError、ValueError 以及 Error 体系中的其余类型到达 span 和 Once 毒化原因时,没有携带其消息和代码。
JIT 下的 mixed 返回类型 — oxphp_async_await() 和 Shared\Map::get() 等函数声明了格式错误的 mixed 类型,因此在 tracing JIT 下的热循环会泄漏每一个返回值,Reflection 的 getReturnType() 会段错误。
oxphp_worker_memory_bytes 报告内存 — 它此前永远读作 0;现在它报告与 WORKER_MAX_MEMORY_MIB 所检查的相同的 PHP 堆,在每个请求结束时采样。
工作进程模式:性能剖析在请求之间混杂 — 同一线程上挂起的请求共享性能分析器和 APM span 状态,产生归属到错误请求的性能剖析和 #[Trace]/#[SlowThreshold] span;现在每个请求携带自己的状态。
性能分析器截断与 OXPROF — 达到 PROFILER_MAX_SPANS 的运行会在 index.json、/__profiler/stats 和 oxphp_profiler_truncated_total 中被标记为 truncated,文档中描述的 OXPROF cookie 现在会启动一次运行。
Shared\Mutex::withLock() 的等待可以被终止 — 等待锁的请求现在每 100 毫秒让位于 max_execution_time、关闭和任务取消,因此相反顺序的加锁不再把两个工作进程一直占住直到重启。
RUNTIME_HOOKS=streams 与后加载的扩展 — 从排序在 extension.ini 之后的 ini 文件中启用的 mysqli 和 phpredis 现在与其他扩展一样受到保护;RUNTIME_HOOKS 也改为在启动时读取一次,而不是每个请求调用一次未加锁的 getenv()。
压缩的边界情况 — 1 MiB 到 3 MiB 之间的静态文件会被压缩;大小写混合的 Content-Type 值会被压缩;可压缩响应的 identity 副本携带 Vary: Accept-Encoding;非 UTF-8 的压缩设置会在启动时失败。
Shared\Counter、Atomic 和 Flag 的容量错误 — 它们的构造函数现在在超过 SHARED_MAX_ENTRIES 或 SHARED_MAX_BYTES 时抛出 Shared\CapacityException,与其他所有 Shared 类型一致。
文档更正 — SHARED_LOCK_DIAGNOSTICS 的取值为 off、warn(默认)和 strict;不再宣称按请求数回收工作进程;工作进程循环在动态池退役时也会退出;504 来自 max_execution_time;此外还有优雅关闭的界限、运行时镜像的 ini 布局以及 /config 的键顺序。
rustls 0.23.45 — 修复一个 TLS 1.3 握手缺陷(RUSTSEC-2026-0285):它会跨越加密边界接受明文握手消息,影响 TLS 监听器以及 OTLP 和性能分析器导出器;握手记录仍保持经过认证。
FRAME_OPTIONS 默认值改为 SAMEORIGIN — 跨源框架嵌入仍被阻止,同源嵌入(管理后台预览、仪表盘小组件)开箱即用;依赖内置 DENY 的部署现在必须显式设置它。
工作进程模式:请求内不允许事件循环 — 工作进程模式的请求现在是一个纤程,而 Revolt 拒绝在纤程内运行其循环:在处理器里调用 Revolt\EventLoop::run() 会抛出异常。请把它移到工作进程引导阶段;defer()、delay() 和 getSuspension() 照常工作。
工作进程模式:请求状态随请求终结 — ini_set()、set_time_limit() 和 error_reporting() 在请求之间回滚到引导基线;filter_input() 只为发问的请求作答;跨请求保留的 php://input 句柄读起来是已关闭的资源,而不是另一个请求的请求体。
query() 和 cookie() 会解码它们的值 — 曾用 rawurldecode() 补偿的代码现在会解码两次,必须去掉多余的调用。发送为空的 cookie 读作空字符串而不是 null,query() 最多解析 1000 个参数。
严格的 MAX_CONNECTIONS / QUEUE_CAPACITY 解析 — 格式错误的值会中止启动,而不是悄悄回落到默认值;QUEUE_CAPACITY=0 现在表示自动(workers × 128),而不是零容量队列。
队列等待预算 — 到达已满队列的请求现在会等待空位,最长 QUEUE_WAIT_TIMEOUT_MS(默认 1 秒),而不是立即 529;到达时盖上的一个截止时间同时覆盖等空位和在队列中的等待。QUEUE_MAX_WAITING 和 QUEUE_MAX_WAITING_BYTES 分别以请求数和请求体字节数约束等待集合。
RUNTIME_HOOKS=streams — 阻塞的 tcp:// 套接字读取和 stream_select() 会挂起纤程而不是工作线程,无需改动代码即可覆盖 mysqlnd(PDO_MySQL、mysqli)、phpredis 和 HTTP 流封装器。并发纤程之间共享的连接按往来交换逐次认领,因此请求不会读到彼此的应答。
oxphp_admission_refused_total — 在到达工作进程之前被卸除的请求现在会留下服务器侧的痕迹,reason 标签把四种过载原因(529)与 shutting_down(503)和 pool_unavailable(500)区分开。
根 span 异常自动捕获 — 以 5xx 应答的未处理异常或致命错误,会在全部四种服务模式下把一个 exception 事件(type、message、stacktrace、file、line)附加到请求的根 span 上——不需要属性标注,也不需要调用 oxphp_apm_error()。
数据库 span 属性 — PDO 和 mysqli 的 span 携带 db.statement(字面值脱敏为 ?)、db.operation、db.system、server.address/port、db.name 和 oxphp.db.slow;mysqli_stmt::prepare 作为第 34 个函数加入被挂钩集合。
oxphp_queue_wait_us 量程达到一秒 — 四个新分桶(100 毫秒到 1 秒)把接近默认预算的等待量化出来,而不是并入 +Inf;被拒绝的请求不计入该直方图。
工作进程回收只统计真正毁坏工作进程的失败 — 只有致命错误、内存耗尽和栈溢出计入连续三次失败的回收;未捕获的异常和被取消的请求是中性的。这个断路器现在在同步派发路径上也会触发——此前它在那里从不生效。
config --check 容量建议 — 当 PHP_WORKERS + QUEUE_CAPACITY + QUEUE_MAX_WAITING 达到 MAX_CONNECTIONS 时发出警告(但不判失败)——越过这一点后,过载的回应将是毫无响应而不是 529。
文档目录扁平化 — 仓库文档从 docs/en/ 上移到 docs/;俄语和中文翻译迁至 oxphp.dev,法语、波兰语和日语已在那里。
工作进程模式:$_POST 和 $_FILES 得到填充 — 此前工作进程服务的每个请求里两者都是空的,HTML 表单和文件上传因此悄无声息地不可用;读取 php://input 的 JSON API 不受影响——这也是问题一直未被发现的原因。
filter_input() 跨请求泄漏 — filter 扩展自己的输入副本会在工作进程的整个生命期内累积,用另一个客户端的查询值、会话 cookie 和请求体字段回答别的请求;现在它会在每个请求开始时被交还。
Sleep 定时器突发 — 单次调度器轮询中到期的定时器超过 32 个时,多出来的休眠者会被永远停放;大突发现在按 32 个一批唤醒。
关闭共享连接 — 在另一个请求停着读取的连接上调用 fclose()、$mysqli->close() 或重连助手,会把那个请求恢复到已释放的内存里;现在它会立即以 500 失败,并点名发生了什么。
致命错误不再清空共享状态 — 展开被中止的请求时会双重释放 include 变量,悄悄重置工作进程模式应用存放跨请求状态的数组和对象。
异步池节奏 — 任务交接不再受固定 1 毫秒空闲定时器的节拍限制:Shared\Channel 传输从每项 0.69 毫秒降到 0.14 毫秒,空闲唤醒从 777 次/秒降到 92 次/秒。
暂停的请求保留自己的开始时间 — $request->startTime() 和 oxphp_server_info()[request_time] 在一次挂起之后不再读到 0 或某个并发请求的值。
请求体读取警告到达正确的请求 — post_max_size 和上传限制的警告现在在发送该请求体的请求内部触发,错误处理器因此能看到它的超全局变量;抛出异常的处理器会以 500 应答,而不是什么都不带地关闭连接。
默认降权 — 在存在 www-data 账户的环境下以 root 启动时,服务器会绑定其监听套接字,并在开始处理任何流量之前降权到 www-data;官方镜像不再以 root 运行。传入 --user=root 可保留旧行为。
Fail-closed 的 TLS 配置 — 配置不完整的一对(有 TLS_CERT 却没有 TLS_KEY,或反之)或非 UTF-8 的证书/密钥值,现在会中止启动,而不是悄悄以明文 HTTP 提供服务,并会指明出问题的变量。
严格的 ASYNC_* / SUPERGLOBALS 解析 — ASYNC_WORKERS、ASYNC_QUEUE_CAPACITY 或 ASYNC_MAX_FIBERS 中的无效内容会导致启动错误,而不是悄悄回落到默认值,并且 oxphp run 不再忽略无效的 SUPERGLOBALS_ENABLED。
没有 Content-Type 的 QUERY 返回 400,而非 415 — 唯一由服务器生成的 415 已移除;针对此情况的自定义 ERROR_PAGES_DIR/415.html 必须重命名为 400.html。
性能分析器导出信封的判定 — PROFILER_EXPORT_XHGUI=false 现在即使在 /run/import URL 上也会生效,且信封自动检测仅依据每个工具的规范路径(xhgui /run/import、Buggregator /api/profiler/store),不再依据模糊的 "xhgui" 子串。
Buggregator 性能分析器导出 — 性能分析器的 HTTP push 会把 xhprof 数据包装进 Buggregator 的 POST /api/profiler/store 所期望的信封中;PROFILER_EXPORT_BUGGREGATOR 用于开关此功能,tags 和 app_name 通过环境变量设置。
通过 TLS 的 OTLP 追踪导出 — https:// 的 OTEL_EXPORTER_OTLP_ENDPOINT 现在在 gRPC 和 http/protobuf 上都会加密导出,并通过 rustls 对照系统信任库进行校验;运行时镜像必须自带 CA 包(官方镜像已包含)。
APM span 上的完整异常数据 — 异常 span 事件现在除了 exception.type 之外还携带 exception.message 和 exception.stacktrace,因此 New Relic Errors Inbox、Jaeger 和 Tempo 能渲染出消息和堆栈;受 OTEL_APM_MESSAGE_MAX_BYTES / OTEL_APM_STACKTRACE_MAX_BYTES 限制。
长连接的优雅排空 — 收到 SIGTERM 时,服务器会发送 GOAWAY,关闭空闲的 keep-alive 连接,并及时结束打开的 SSE/flush 流;DRAIN_TIMEOUT_SECONDS 默认值从 30 → 25 下调,以适配 Kubernetes 的 30 秒宽限期。
TLS_MIN_VERSION — 可接受的最低协议版本:1.2(默认)或 1.3。其他任何值——包括像 "TLSv1.2" 这样的异类语法——都会导致硬性启动错误,即使 TLS 被禁用也会校验,并在 /config 中以 tls_min_version 上报。
低开销的 STATIC_REVALIDATE — 每个文件最多每 3 秒执行一次 stat();静态命中会以最多一次系统调用从内存提供,磁盘上的改动会在 3 秒内可见。默认仍为关闭。
更清晰的 TLS 启动错误 — 路径拼写错误时会以 "TLS_CERT: cannot read /etc/ssl/cert.pem: No such file or directory" 报错,而不是一条裸的操作系统错误;PEM 解析错误和密钥不匹配错误也会加上同样的前缀。
工作进程模式取消定位 — 客户端中止或请求超时可能会取消工作进程上最近接受的那个请求,而不是实际应被取消的请求,从而中止一个不相关的处理中请求。
finish_request() 与多路复用流 — 一旦工作进程上有任何请求调用了 oxphp_finish_request(),多路复用到该工作进程上的其他所有流都会悄悄丢弃其后续的 oxphp_stream_flush() 输出。
超时的 await 会取消任务 — 被放弃的任务会被强制取消:已停放的任务恢复后进入取消流程,CPU 密集型任务会在操作码边界被中断。此前它会一直运行到请求结束。
嵌套的 oxphp_async() — 一个异步任务可以派发它自己的任务,并在 await_all / await_any / await_race 上挂起,而不会阻塞其工作线程。“禁止嵌套 async”的限制已移除。
处理中任务上限 — ASYNC_MAX_FIBERS(默认 256)限制排队中加运行中的任务数;超过上限的派发会立即以 AsyncException 拒绝,因此扇出不会死锁。
内部服务器允许列表 — INTERNAL_ALLOW_IPS:用于 /metrics 和 /config 的 CIDR 允许列表;健康检查端点对编排器保持可达。
性能分析器采样排除 — PROFILER_EXCLUDE_PATHS 会把框架自身流量(/_profiler、/_wdt)排除在统计采样之外;显式触发仍会进行性能分析。
Channel 发送方唤醒 — 停放在已满 Shared\Channel 上的纤程发送等待者,会在 recv_blocking 腾出一个槽位时被唤醒——sendTimeout 在饱和状态下不再被误触发。
异步边界情况 — 优雅关闭时的崩溃;对已关闭 promise 的 await 卡住而非拒绝;await_all 在提前退出时留下 promise 仍在运行。
工作进程路由约定 — 静态资源从磁盘提供,其余一切都交给工作进程的 ENTRY_FILE——任意 .php 文件、目录索引和根 index.php 回退不再按请求逐一解析。
诚实的 CGI PATH_INFO — 在 Framework 模式下,PATH_INFO 仅在显式的 /index.php/extra 请求时才设置;PHP_SELF 报告前端控制器。请基于 REQUEST_URI 路由。
Range 请求(RFC 9110)— 静态文件的 206 Partial Content:视频拖动定位、断点续传下载、PDF 部分加载;配合强校验器支持 If-Range。
HTTP/2 连接限制 — 五个环境变量限制每连接的资源占用;解析后的值会在启动时记录到日志。
强静态 ETag — "<size>-<mtime>" 取代 W/"…"——这是 If-Range 所必需的;压缩表示形式仍保留弱标签。
应用头部优先 — 由 PHP 设置的 X-Content-Type-Options、X-Frame-Options 和 CSP 不再被覆盖——服务器提供的值只是在缺失时才插入的回退。
流式传输与压缩 — 用 flush() 流式输出的响应会以未压缩的形式直接透传,而不再被 brotli 层完整缓冲直到脚本结束。
HTTP/2 DoS 加固 — Rapid Reset(CVE-2023-44487)、HPACK 炸弹和窗口停滞攻击都受到限制;死连接通过 PING/PONG keepalive 回收。
访问日志中的 remote_ip — JSON 字段 remote_addr 更名为 remote_ip,且携带不含端口的 IP——与 nginx/Apache/Caddy 保持一致。
PHP 风格的 CLI — oxphp run script.php 以 CLI 语义执行一个文件,底层使用完整引擎;#!/usr/bin/env oxphp 脚本可直接运行;接受 -d ini 覆盖。
--user 降权 — 以 root 绑定特权端口,然后在任何请求处理线程创建之前,不可逆地降权到非特权用户。
尊重转发端口 — 在 TRUSTED_PROXIES 之后,SERVER_PORT 会尊重 X-Forwarded-Port;当代理提供时,REMOTE_PORT 会从 RFC 7239 的 for= 填充。
装饰器崩溃 — 函数装饰器路径中的两处 SIGSEGV(已释放的实例缓存;跨扩容的悬空 zval)已修复;嵌套溢出现在会抛出 StackOverflowException。
性能分析器属性尊重参数 — #[SlowThreshold(ms:)]、#[MemoryThreshold(kb:)] 和 #[Mark(label:)] 不再退化为共享的默认值;APM span 事件能够抵达 OpenTelemetry。
上传对象 API 已接通 — Request::file() / files() 现在会为每种 $_FILES 形态返回 UploadedFile 对象,而不是 null。
三分式等待 API — Channel 和 Mutex 把每个等待都拆分为 try* / 裸动词 / *Timeout(int $ms),返回值类型化的 SendResult / RecvResult,而不是 mixed|null。
await_any 即 Promise.any — 第一个兑现者胜出;全部拒绝则抛出 AggregateAsyncException。旧的“第一个敲定”的行为现在是 oxphp_async_await_race()。
诚实的取消状态码 — 超时 → 504,优雅排空 → 503 + Retry-After,客户端断开 → 499(仅记录日志)。监督进程杀死和用户态取消仍为 500。
没有兼容垫片的重命名 — Async\Exception → AsyncException,Shared\Exception → SharedException;Worker 去掉 get* 前缀;Counter 变得精简(add/set);Flag 与 Atomic 对齐;Once 新增 getOrInit()/status()。
Shared\Atomic + Ordering — 一个通用的 int64 原子类型:load/store/swap/CAS/fetch*,带有显式的内存排序,与 std::sync::atomic 一一对应。
Shared\Registry — 以名称为键的进程全局句柄:每个工作进程都收敛到同一个条目;工厂最多运行一次。
单一超时来源 — REQUEST_TIMEOUT_SECONDS 和 oxphp_request_heartbeat() 已移除——max_execution_time 和 set_time_limit() 保留。
SSE 中的 connection_aborted() — 在流中途断开后返回 true,因此 while (!connection_aborted()) 循环能干净地退出。
Shared\* 热路径 — 每次操作的开销落在原始原子加载的 criterion 噪声范围之内;在 8 线程下相比此前的形态几何平均约提升 4.7×。
严格的布尔环境变量解析 — 只接受 on/true/1/yes 和 off/false/0/no;像 ture 这样的拼写错误会拒绝启动,而不是悄悄回落到默认值。
ENTRY_FILE + WORKER_MODE_ENABLED — 一个变量按扩展名选择路由模式;一个显式开关启用持久化工作进程。INDEX_FILE / WORKER_FILE 成为已弃用的别名。
OxPHP\Server\Worker 类 — 一个运行时自省句柄:id、请求计数、内存、RSS——外加通过 scheduleExit() 实现的应用驱动回收。
静态缓存重命名 — STATIC_CACHE_TTL → STATIC_MAX_AGE;STATIC_CACHE → STATIC_REVALIDATE,极性翻转。默认值不变。
WORKER_MAX_REQUESTS — 会被解析并忽略,同时在启动时发出 WARN——请迁移到 WORKER_MAX_MEMORY_MIB 或 scheduleExit()。
PHP 8.5 镜像 — :php8.5、:php8.5-alpine 和固定补丁版本的标签与 8.4 一同发布;latest 会保持在 8.4,直到观察期结束。
跨请求的分块流式传输 — 桥接层的每请求上下文在多个工作进程请求之间发生泄漏,导致首次 flush 之后丢失分块的 Transfer-Encoding。
用户纤程内的 Shared\* — Fiber::start() 内部的 Channel::recv()/send() 不再走 oxphp-fiber 挂起路径并以 fiber_await rc=1 崩溃。
插件 API 的具名参数 — 插件定义的类和函数现在会公布真实的参数名——具名参数调用可以正常工作。
异步闭包中的 Shared 句柄 — 被 oxphp_async() 闭包捕获的 Channel/Map/Pool 不再在接收线程上以 null 到达。
Async 命名空间 — 异步类已移动到 OxPHP\Async\ 之下;这些函数现在随 plugin-async 一起发布。
七种 Shared\* 原语 — Counter、Flag、Once、Mutex、Channel、Map、Pool——跨工作进程共享的原子状态,带有注册表可观测性和死锁检测器。
按请求的性能分析器 — xhprof / speedscope / pprof / collapsed 输出;cookie、header、query 和统计触发;带 xhgui 信封的 HTTP push。
APM 自动埋点 — plugin-apm 提供 PHP 追踪 SDK 和错误捕获;异步和 APM 子系统已迁移到 Rust 插件中。
生产集成 — TRUSTED_PROXIES(RFC 7239)、Kubernetes /readyz + /livez、安全头部、按 IP 限流——全部通过环境变量配置。
Pool 热路径 — 在 Docker 中无竞争的获取/释放约 0.9 µs,带有每线程亲和性。
纤程多路复用 — 单个工作线程内的并发 I/O;使用 W3C Trace Context 的分布式追踪和 OpenTelemetry 导出。
oxphp_async() promise — 在专用线程池上并行执行 PHP。
HTTP 对象 API — 带惰性访问器的 OxPHP\Http\Request,外加基于属性的装饰器系统。
端口与背压 — 默认端口 8080 → 80(启用 TLS 时为 443);背压响应 503 → 529。
CGI 正确性 — SERVER_PROTOCOL 反映真实的 HTTP 版本;REQUEST_TIME 为请求开始时间;IPv6 Host 解析;请求超时返回 408。
异步 HTTP 核心 — 一个支持优雅关闭的 Hyper + Tokio 服务器,一个带完整超全局变量的自定义 oxphp SAPI,一个带有界队列和 529 背压的 ZTS 工作进程池。
三种路由模式 — Traditional、Framework 和 SPA;静态文件带内存缓存、ETag/304、brotli、TLS。
工作进程模式 — 通过 oxphp_worker() 实现持久化工作进程、提前响应完成、SSE 流式传输、协作式超时。
可观测性 — /metrics、/health、/config、结构化 JSON 访问日志、请求 ID。
Docker 优先 — 多阶段 Alpine 构建;GHCR 上的 amd64/arm64 镜像。