处理 PHP 导出为 Word 文档耗时过长问题记录
一
问题描述:在一个 HTML 文本中有大量的网络形式 svg 图片需要下载并且转换为 png 格式,会导致接口执行时间太长,超过 PHP 限制的执行时长因此返回 504。
二
首先快速解决问题,使用ini_set('max_execution_time', '300');
暂时将 PHP 的执行时长延长,并且相应的改写服务器的配置延长执行时间,保证用户能够获取到文件。
然后再来查找原因:
先记录耗时程序运行时间方便排查
- 制作一份测试用的 HTML 文件,其中包含总计 1200+ 张 svg 图片
记录程序执行时长:
【总转换时间】[42.074449300766] 【下载总时间】[154.7038500309]
- 平均每张图片的转换执行时间是 0.01s~0.1s 左右
上面的排查记录得出结论
最大的瓶颈是下载图片,数量过多的图片导致下载耗时极长,而基础的 PHP 无法执行大量的并发请求
尝试使用 GuzzleHttp 的并发写法,使用 16 个进程同时请求,网络请求的时长可以降低到 30s
$requests = function ($total) use ($client, $urlArr) { for ($i = 0; $i < $total; $i++) { // 官方文档提供的写法,似乎实际上并未执行并发请求,使用下面的写法才能并发运行 // foreach ($urlArr as $key => $url) { // yield new Request('GET', $urlArr[$urlArrKey[$i]]); yield function() use ($client, $urlArr) { return $client->getAsync($urlArr[$i]); }; } }; $pool = new Pool($client, $requests(count($urlArr)), [ 'concurrency' => 16, 'fulfilled' => function ($response, $index) { }, 'rejected' => function ($reason, $index) { }, ]);
[2024-03-26 10:56:46]【总转换时间】[42.540313959122] [2024-03-26 10:56:46]【下载总时间】[71.826416015625]
关于并发线程数量的设置问题:
- 参考:https://www.cnblogs.com/dennyzhangdd/p/6909771.html
计算公式:
Nthreads = Ncpu*Ucpu*(1+w/c)
- Ncpu 为 CPU 核心数,Ucpu 为使用率(0~1)
- w 为等待时间,c 为计算时间
- 执行
nproc
(在Linux和Unix系统上)或sysctl -n hw.ncpu
(在macOS上)命令来获取机器的 CPU 核心数 - 代入上面测试的结果,Ncpu 为 4,Ucpu 为 1,w/c 在执行网络请求操作是近似于 1,所以 Nthreads 的值约为 8
- 简单结论:通常 IO 密集型程序设置为 2*CPU 核心数;计算密集型设置为 CPU 核心数
- 经过上面的并发改写,下载耗时被降低到了 30s 左右,经过尝试发现此时即使提升线程数也不会对结果有提升,30s 大概是只使用 PHP 实现并发下载的最小时长
另一个瓶颈是转换图片的耗时
- 由于转换图片的操作被写在请求操作成功后的回调函数内,GuzzleHttp 框架的并行是使用
curl_muti_exec
函数实现的。在回调函数中的这些操作其本质还是串行执行的,所以是否能想办法将这些操作也并行执行 - PHP 中可以使用
PCNTL
或POSIX
拓展才能让 PHP 实现多线程操作,但是这些拓展需要对 PHP 程序进行改动,没有办法在不进行大改的情况下使用 PHP 执行高效的并发操作 - PHP 的多线程操作性能无法保证,而且可维护性或安全性很难操作
- 由于转换图片的操作被写在请求操作成功后的回调函数内,GuzzleHttp 框架的并行是使用
放弃只使用 PHP 解决的思路,将这种耗时的操作使用其他方法编写脚本在外部执行。Go 有优秀的协程特性正好适合执行这些需要并行运行的程序
- 使用 goquery 库处理 html 内容读取其中图片的链接
- http 库和 sync 库执行并发请求所有图片数据
- canvas2d 库将其中的 svg 图片转换为 png 图片
- 利用 Go 的协程特性来并发的进行下载和转换操作
根据上面的步骤编写 Go 程序,然后在 php 中使用
shell_exec
函数在外部运行程序- 将 HTML 写入到文件
public/tmp/{{uuid}}.html
中 - 执行 Go 程序读取文件,处理完成后写入新文件中
- PHP 读取生成的文件并转为 Word 文档然后返回
- 将 HTML 写入到文件
使用上述方案处理之后程序的运行时间缩短到了 5s,执行并发网络请求的时长约为 2s,转换的时长约 2s
三
一些问题记录:
- Go 构建的程序理论上可以随处执行,但是执行网络请求的库对系统有部分依赖,我在 m2 mac 下直接编译后的程序放在 Docker 的 Alpine 容器(php-fpm 的容器)中运行无法运行,使用这个答案提供命令
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
进行编译,成功在容器中运行 - svg 的缺失字体问题比较严重,暂时没有想到办法解决。由于 svg 编写的公式严重依赖于很多 Windows 下的特殊字体,在 MacOS 或 Linux 下字体的缺失会导致转换错误或者转换出来的图片乱码,暂时使用字符替换将大部分的字体转为
Times New Roman
,但治标不治本,还需要后续研究如何处理