跳转到内容

NextPDF Symfony 生产环境部署

该 bundle 面向长时间运行的 PHP 运行时。文档对象是非共享的,字体注册表在预热后会被锁定,图像缓存会在请求之间重置。请以流式方式传输大型 PDF,并将繁重作业卸载到 Messenger worker。

长时间运行的运行时会让容器在多次请求之间保持存活,因此每次请求的状态绝不能泄漏。FrankenPHP、RoadRunner 和 Messenger worker 都以这种模式运行。该 bundle 的 services.php 定义了下述生命周期,并已对照服务定义逐项核对:

  • Document — 非共享。 nextpdf.document(以及 PdfDocumentInterface / Document 别名)每次解析都会得到一个全新的实例。在 PSR-11 下,容器对同一个 id 的每次 get() 都可以合法返回不同的值(PSR-11 §1.1.2)。应在每次请求时解析一个文档。绝不要跨请求持有同一个文档。
  • FontRegistry — 共享且锁定。 该注册表是进程级生命周期内的单例。在 warmup() 之后(当 preload_fonts 非空时),编译器 pass 会调用 lock()。该锁可防止运行时变更,从而避免跨请求的字体状态污染。
  • ImageRegistry — 共享,每次请求重置。 这个有界的最近最少使用(LRU)图像缓存是共享的,但已标记 kernel.reset 标签,方法为 reset,因此在遵循 kernel.reset 的运行时下,Symfony 会在请求之间清除它。
  • EInvoice 契约 — 非共享。 当存在 Premium 实现时,embedder、validator、profile 和 schematron 服务都会注册为非共享。每次调用的解析器上下文绝不会跨请求泄漏。

注入 PdfFactory——它是共享且无状态的配置持有者——并在每次请求时调用 create()

public function __construct(private readonly PdfFactory $pdf) {}
public function action(): Response
{
$doc = $this->pdf->create(); // fresh, disposable
// ... build ...
return PdfResponse::inline($doc, 'document.pdf');
}

不要将 Documentnextpdf.document 注入本身共享且会跨请求存活的服务。应改为在请求作用域的方法内部解析它。

PdfResponse::streamDownload()streamInline() 返回一个 StreamedResponse。它的回调会以 64 KB 分块发送 PDF 主体,并在每个块之后刷新。这样可以为大型文档限制响应缓冲区上限。下述两项权衡均已对照 PdfResponse 进行核对:

  • 流式变体会有意省略 Content-Length(响应对象无法预先知道主体大小)。下载进度条和某些 proxy 更倾向于已知长度。当文档足够小、可以保存在内存中且希望提供内容长度时,请使用非流式的 download()inline()
  • 流式变体会发送与缓冲变体相同的安全头,以及相同的 Cache-Control: private, max-age=0, must-revalidate

对于数兆字节级的报表和批量导出,应选择流式传输。对于小型且对延迟敏感的响应,应选择缓冲变体。

当请求必须快速返回,或渲染属于 CPU 密集型任务时,应将生成工作卸载到 Messenger。

  1. 为每种文档类型实现 PdfBuilderInterface
  2. 将这些 builder 注册到 container.service_locator 中,并接入 GeneratePdfHandler$builderLocator
  3. GeneratePdfMessage 路由到一个持久化传输。
  4. 以有界的生命周期运行 worker。

定期回收 worker,避免第三方依赖泄漏的内存分配无限增长:

Terminal window
php bin/console messenger:consume async \
--limit=200 \
--memory-limit=256M \
--time-limit=3600

该 bundle 的 messenger.timeoutmessenger.retries 配置键记录了每条消息预期的超时时间和重试预算。请通过 Symfony 的重试策略和 worker 标志强制执行匹配的行为。

GeneratePdfMessage 会在构造时校验输出路径。随后 GeneratePdfHandler 会在执行时、写入磁盘之前再次校验该路径。这种两阶段检查对异步作业至关重要。消息在派发与消费之间可能会停留在队列中,因此 handler 不会盲目信任已入队的路径。作为纵深防御,请将 worker 的文件系统权限限制在预期输出目录内。

其中 FontRegistryImageRegistry 服务接受可选的 Psr\Log\LoggerInterface(通过 nullOnInvalid() 绑定)。当应用提供 logger 时,这些注册表可以通过它输出诊断信息。在 PSR-3 logger 契约(PSR-3)下,该 logger 是一个可选且可替换的协作者。若需要请求级别可见性,请在你的应用代码中围绕 PdfFactory::create() 和 Messenger handler 记录日志。在事故排查期间,请使用 messenger:consume -vv

  • 在应用的 composer.json 中固定单一 nextpdf/core 主版本 (该 bundle 支持 ^3.0 || ^5.2)。
  • 确保部署的 PHP 镜像已启用 ext-mbstringext-zlib(否则该 bundle 会在启动时快速失败)。
  • 将你的文档使用的字体预先填入 preload_fonts,让注册表在启动时预热并锁定,而不是等到首次请求时才预热。
  • 如果你需要在多次部署之间复用已缓存的工件,请将 cache_path 指向可写且持久的位置。否则使用 %kernel.cache_dir% 默认值即可。
  • 在部署期间运行 php bin/console cache:warmup,以便在流量到来之前构建好已编译的容器(包括可选扩展的探测)。
  • 生产环境中的异步工作应使用持久化的 Messenger 传输(而非 sync),并使用 --limit / --memory-limit / --time-limit 来回收 worker。
  • 带缓冲 proxy 后面的流式响应 —— 会缓冲完整主体的 proxy 会抵消内存优势。请将该 proxy 配置为流式传输 PDF 响应,或在该位置改用缓冲响应。
  • kernel.reset 未被遵循 —— 在不调用 kernel.reset 的运行时下,图像缓存受 image_cache_mb 限定,但不会在请求之间清除;请据此设置容量上限。
  • 跨请求持有文档 —— 在先前请求中捕获的 Document 会携带过期状态。请始终在每次请求时通过 PdfFactory 解析它。

每一行都对应本页面作出的一项规范性声明,并固定到受控 SDO 语料库中的完整 64 位十六进制 reference_id。来源信息(即语料库清单和检索传输)位于 _sidecars/rag-citations.yaml 中。

规范条款参考 ID(reference_id)声明
PSR-11psr_11_container#1.1.2.p3.b非共享服务:每次解析得到不同的值
PSR-3psr_3_logger#x3.p17可选的 logger 协作者
  • /integrations/symfony/configuration/ —— 服务生命周期与参数。
  • /integrations/symfony/security-and-operations/ —— 响应头、路径校验、密钥处理。
  • /integrations/symfony/troubleshooting/ —— 启动与运行时诊断。
  • /integrations/symfony/quickstart/ —— 最简异步配置。