问题描述:在一个 HTML 文本中有大量的网络形式 svg 图片需要下载并且转换为 png 格式,会导致接口执行时间太长,超过 PHP 限制的执行时长因此返回 504。


首先快速解决问题,使用ini_set('max_execution_time', '300');暂时将 PHP 的执行时长延长,并且相应的改写服务器的配置延长执行时间,保证用户能够获取到文件。

然后再来查找原因:

  1. 先记录耗时程序运行时间方便排查

    1. 制作一份测试用的 HTML 文件,其中包含总计 1200+ 张 svg 图片
    2. 记录程序执行时长:

      【总转换时间】[42.074449300766]
      【下载总时间】[154.7038500309]
    3. 平均每张图片的转换执行时间是 0.01s~0.1s 左右
  2. 上面的排查记录得出结论

    1. 最大的瓶颈是下载图片,数量过多的图片导致下载耗时极长,而基础的 PHP 无法执行大量的并发请求

      1. 尝试使用 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]
      2. 关于并发线程数量的设置问题:

        1. 参考:https://www.cnblogs.com/dennyzhangdd/p/6909771.html
        2. 计算公式:Nthreads = Ncpu*Ucpu*(1+w/c)

          1. Ncpu 为 CPU 核心数,Ucpu 为使用率(0~1)
          2. w 为等待时间,c 为计算时间
          3. 执行nproc(在Linux和Unix系统上)或sysctl -n hw.ncpu(在macOS上)命令来获取机器的 CPU 核心数
          4. 代入上面测试的结果,Ncpu 为 4,Ucpu 为 1,w/c 在执行网络请求操作是近似于 1,所以 Nthreads 的值约为 8
        3. 简单结论:通常 IO 密集型程序设置为 2*CPU 核心数;计算密集型设置为 CPU 核心数
    2. 经过上面的并发改写,下载耗时被降低到了 30s 左右,经过尝试发现此时即使提升线程数也不会对结果有提升,30s 大概是只使用 PHP 实现并发下载的最小时长
    3. 另一个瓶颈是转换图片的耗时

      1. 由于转换图片的操作被写在请求操作成功后的回调函数内,GuzzleHttp 框架的并行是使用 curl_muti_exec 函数实现的。在回调函数中的这些操作其本质还是串行执行的,所以是否能想办法将这些操作也并行执行
      2. PHP 中可以使用 PCNTLPOSIX 拓展才能让 PHP 实现多线程操作,但是这些拓展需要对 PHP 程序进行改动,没有办法在不进行大改的情况下使用 PHP 执行高效的并发操作
      3. PHP 的多线程操作性能无法保证,而且可维护性或安全性很难操作
  3. 放弃只使用 PHP 解决的思路,将这种耗时的操作使用其他方法编写脚本在外部执行。Go 有优秀的协程特性正好适合执行这些需要并行运行的程序

    1. 使用 goquery 库处理 html 内容读取其中图片的链接
    2. http 库和 sync 库执行并发请求所有图片数据
    3. canvas2d 库将其中的 svg 图片转换为 png 图片
    4. 利用 Go 的协程特性来并发的进行下载和转换操作
  4. 根据上面的步骤编写 Go 程序,然后在 php 中使用 shell_exec 函数在外部运行程序

    1. 将 HTML 写入到文件 public/tmp/{{uuid}}.html
    2. 执行 Go 程序读取文件,处理完成后写入新文件中
    3. PHP 读取生成的文件并转为 Word 文档然后返回

使用上述方案处理之后程序的运行时间缩短到了 5s,执行并发网络请求的时长约为 2s,转换的时长约 2s


一些问题记录:

  1. Go 构建的程序理论上可以随处执行,但是执行网络请求的库对系统有部分依赖,我在 m2 mac 下直接编译后的程序放在 Docker 的 Alpine 容器(php-fpm 的容器)中运行无法运行,使用这个答案提供命令CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go进行编译,成功在容器中运行
  2. svg 的缺失字体问题比较严重,暂时没有想到办法解决。由于 svg 编写的公式严重依赖于很多 Windows 下的特殊字体,在 MacOS 或 Linux 下字体的缺失会导致转换错误或者转换出来的图片乱码,暂时使用字符替换将大部分的字体转为Times New Roman ,但治标不治本,还需要后续研究如何处理