跳转到内容

在生产环境中使用 Artisan

在生产环境中,应注入已配置好的 renderer(渲染器)和 PSR-3 logger,让同一个运行中的 Chrome 进程在多次渲染间复用;为包含多个元素的文档提供明确高度,并在进入渲染路径前用上游 timeout(超时)设定边界。

BrowserPool 让单个 Chrome 进程持续存活(keepAlive: true),并每渲染 100 次就重启一次,以限制内存增长——这是长生命周期 CDP client 中已知的累积模式。对于需要渲染大量文档的 worker,通常应使用长生命周期的 renderer,而不是为每个请求分别创建一个,这样才不会频繁支付 Chrome 的启动成本。

render-service.php
<?php
declare(strict_types=1);
use NextPDF\Artisan\ChromeHtmlRenderer;
use NextPDF\Artisan\ChromeRendererConfig;
use NextPDF\Artisan\Exception\ChromeNotAvailableException;
use NextPDF\Artisan\Exception\ChromeRenderException;
use Psr\Log\LoggerInterface;
final class ReportRenderer
{
private ChromeHtmlRenderer $renderer;
public function __construct(LoggerInterface $logger)
{
$config = ChromeRendererConfig::fromArray([
'chrome_binary' => getenv('CHROME_BINARY') ?: null,
'render_timeout' => 45,
'max_html_size' => 2_000_000,
'no_sandbox' => (bool) getenv('CHROME_NO_SANDBOX'),
]);
$this->renderer = new ChromeHtmlRenderer($config, $logger);
}
public function render(string $html, float $widthPt, float $heightPt = 0.0): string
{
try {
return $this->renderer->render($html, $widthPt, $heightPt)->getPdfData();
} catch (ChromeNotAvailableException $e) {
// Deployment fault: Chrome runtime missing. Page the on-call owner.
throw $e;
} catch (ChromeRenderException $e) {
// Render-time fault: timeout, crash, empty output. Retryable once.
throw $e;
}
}
public function shutdown(): void
{
$this->renderer->close();
}
}

renderer 只构造一次,随后复用。在 worker 关闭时调用 close(),可以确定性地释放 Chrome 进程,而无需等待析构函数(destructor)。这两个 catch 分支将部署层面的错误(运行环境缺失)与渲染阶段的错误(可重试)区分开。不要使用空的 catch 块。

将它以单例(singleton)方式接入容器:

$container->singleton(ReportRenderer::class, fn ($c) =>
new ReportRenderer($c->get(Psr\Log\LoggerInterface::class)));

当省略高度时,桥接会在 Chrome 中测量内容高度(取 body/document 的滚动高度与偏移高度的 max),转换为点,并加上约 0.2 英寸(约 14.4 pt)的安全缓冲。这个缓冲会吸收 Chrome 视区布局与打印布局回流之间的差异。若没有它,printToPDF 可能溢出到第二页,而 PageImporter(仅取第 0 页)会将其裁切掉。系统会强制应用 0.1 英寸的最小纸张高度。ChromeHtmlRendererTest::renderUsesAutoFitHeightByDefault::renderAutoFitBufferIsAddedNotSubtracted::renderAppliesMinimumHeightOf0Point1InchForTinyExplicitHeight 这几个测试会验证这一点。

对于固定布局的文档(发票、证书),请以点为单位传入明确高度。当高度被明确指定时不会加上缓冲,输出会精确匹配所请求的纸张尺寸(由 ::renderHonorsExplicitHeightWithoutAutoBuffer 验证)。

  • 每个 worker 构造一个 renderer,并复用它。BrowserPool 会复用运行中的浏览器,并在每 100 次渲染的边界自动重启。
  • 如果你想在 100 次渲染的边界之前获得一个全新的 Chrome 进程,请在 worker 关闭时以及大型批处理之间调用 close()
  • 析构函数会调用 close(),但显式调用 close() 更具确定性,是长时间运行进程中的首选做法。
  • 重启通知会以 notice 级别记录,同时带上渲染次数——当重启频率升高时应发出告警,这表示文档比预期更重。

注入一个 PSR-3 logger。发出的事件及其级别如下:

事件级别上下文数据
渲染开始debugsize, width, height
渲染完成debugpdfSize, contentHeight
浏览器启动infobinary
浏览器重启noticecount
浏览器关闭debugrenderCount

不会记录任何 HTML、PDF 字节或提取出的文本,这与 NIST SP 800-92 关于避免将负载写入运维日志的指引一致。请基于 start/complete 这组事件建立延迟 SLO,并基于 notice 事件建立重启频率告警。

  • Sidecar Chrome:在与 PHP worker 相同的容器中运行 Chrome,并固定 chrome_binary。配置支持 sandbox 的容器——请参阅 /integrations/artisan/chrome-renderer-setup/。
  • 无容器/CLI:Artisan 没有 DI 容器。在 CLI runner 中,请使用 EInvoiceServiceFactory 来建立 Premium 电子发票合约;请参阅 /integrations/artisan/boot-and-discovery/。
  • 资源界限:将 render_timeout 与上游请求预算以及主机端的 cgroup/ulimit 搭配使用。请参阅 /integrations/artisan/security-and-operations/ 上的威胁模型。
  • 在渲染过程中被拦截的 renderer 仍会关闭对应的 Chrome 页面(finally),pool 仍可继续使用。
  • 不支持跨 threads/processes 复用同一个 renderer;一个 renderer 拥有一个 Chrome 进程。
  • 100 次渲染后重启的值是固定的;安排批处理大小时请将其纳入考量,以获得可预期的延迟峰值。

稳定状态下的成本来自 Chrome 对输入进行布局再加上 printToPDF,而不是桥接本身的额外开销。启动成本由 keepAlive 摊平。预期每第 100 次渲染(进程重启)都会出现一次延迟峰值——请将其体现在 SLO 中,而不是当作异常事件处理。

生产环境路径正是不可信任的 HTML 进入之处。请重新阅读 /integrations/artisan/security-and-operations/。无论如何配置,网络层屏障都成立,但 no_sandbox: true 会移除 Chrome 的进程隔离,并提高对输入的信任要求。

在无容器的 worker 中,如果未安装 Premium,EInvoiceServiceFactory 会返回 null,因此开源渲染路径会照常运行;安装 Pro/Enterprise 即可在已渲染的文档上启用电子发票嵌入与验证。

  • 参阅 /integrations/artisan/quickstart/
  • 参阅 /integrations/artisan/configuration/
  • 参阅 /integrations/artisan/security-and-operations/
  • 参阅 /integrations/artisan/chrome-renderer-setup/
  • 参阅 /integrations/artisan/troubleshooting/