信创云平台 PHP Serverless 运行时与冷启动优化 + 国产 CPU 上的 PHP JIT 深度优化

  先把两个课题用大白话说穿:

  - 课题一(Serverless 冷启动):用户请求来了,但机器上还没有你的 PHP
    进程,从"零""能回响应"要多久?这段时间由五层叠加:拉镜像 →起容器 →起 PHP 进程 →编译 PHP 代码 →
    框架初始化。优化就是把这五层一层层砍掉或藏起来。
  - 课题二(JIT 适配):PHP 8 的 JIT 是把 PHP 字节码即时翻译成 CPU 机器码。机器码是跟 CPU 架构绑死的——鲲鹏/飞腾是
    aarch64,上游 JIT 支持,问题是"调优";龙芯是 loongarch64,上游 JIT
    没有后端,问题是"从无到有"。这是两个难度完全不同的问题,我分开讲,都不回避。

  ---

  第一部分:信创云上的 PHP Serverless 运行时

  1.1 选型:地基怎么打(大白话)

  信创环境不能用 AWS Lambda/阿里 FC 这种公有云黑盒,必须自建。可选路线:

  ┌─────────────────────┬─────────────────────────────────────────┬──────────────────────────────────────────────────┐
  │        路线         │                 大白话                  │                    信创可行性                    │
  ├─────────────────────┼─────────────────────────────────────────┼──────────────────────────────────────────────────┤
  │ Knative(推荐)     │ K8s 上的标准 Serverless                 │ x86/arm64 官方镜像齐全;龙芯需自编译(纯         │
  │                     │ 层,管"缩到零、按请求扩容"              │ Go,GOARCH=loong64 能编)                        │
  ├─────────────────────┼─────────────────────────────────────────┼──────────────────────────────────────────────────┤
  │ OpenFaaS /          │ 更轻,但生态和请求级扩缩不如 Knative    │ 同上,纯 Go 可自编                               │
  │ OpenFunction        │ 精细                                    │                                                  │
  ├─────────────────────┼─────────────────────────────────────────┼──────────────────────────────────────────────────┤
  │ 自研轻量调度器      │ 完全可控                                │ 除非有特殊理由,别重复造轮子                     │
  └─────────────────────┴─────────────────────────────────────────┴──────────────────────────────────────────────────┘

  运行时执行模型是比平台选型更关键的决定,这里是 PHP 冷启动问题的核心:

  ┌───────────────────────┬──────────────────────────────────────────┬──────────────────────────┬────────────────────┐
  │         模型          │             每个请求发生什么             │          冷启动          │        适用        │
  ├───────────────────────┼──────────────────────────────────────────┼──────────────────────────┼────────────────────┤
  │ 传统 php-fpm          │ fork worker + 重新跑一遍框架 bootstrap   │ 差                       │ 存量迁移过渡       │
  ├───────────────────────┼──────────────────────────────────────────┼──────────────────────────┼────────────────────┤
  │ 常驻 Worker           │ 框架只初始化一次,之后每请求只跑业务逻辑 │ 好,且热请求延迟降 5~10  │ 新建 Serverless    │
  │ 模式(推荐)          │                                          │ 倍                       │ 函数               │
  └───────────────────────┴──────────────────────────────────────────┴──────────────────────────┴────────────────────┘

  Worker 模式的载体选择(信创视角):

  - Swoole:C 扩展,源码编译,三架构(x86/arm64/loong64)都能编过,龙芯社区有成功案例。推荐。
  - RoadRunner / FrankenPHP:Go 写的,loong64 理论能编,但 FrankenPHP 嵌 libphp 的 cgo 链路在龙芯上坑多。arm64 没问题。
  - 结论:统一用 Swoole,一套代码三架构通吃。

  1.2 完整代码:Serverless PHP 运行时(Swoole Worker)

  这是整个运行时的核心文件,大白话注释写在代码里:

  <?php
  // runtime/server.php ——信创 PHP Serverless 运行时入口
  // 设计目标:框架只初始化一次(消灭每请求 bootstrap),
  //          但每个请求之间做状态隔离(避免"常驻内存"带来的脏数据串台)。
  declare(strict_types=1);

  use Swoole\Http\Server;
  use Swoole\Http\Request;
  use Swoole\Http\Response;

  require __DIR__ . '/../vendor/autoload.php';

  // ---------- 1. 冷启动阶段:只做一次的昂贵工作全放这里 ----------
  $bootStart = hrtime(true);

  // 函数开发者只需要提供一个 handler.php,返回 callable ——这就是"函数"
  $handler = require getenv('FUNCTION_HANDLER') ?: '/app/handler.php';

  // 框架型应用:在这里把容器/路由/配置全部构建好(只跑一次!)
  // $app = (require '/app/bootstrap/app.php');

  $bootMs = (hrtime(true) - $bootStart) / 1e6;
  fwrite(STDERR, json_encode(['event' => 'cold_start', 'boot_ms' => $bootMs]) . "\n");

  // ---------- 2. HTTP 服务 ----------
  $server = new Server('0.0.0.0', 8080);
  $server->set([
      // worker 数 = CPU 核数。信创机器上务必显式设,
      // 龙芯 3C5000 是 16 核但单核弱,多 worker 比多协程收益大
      'worker_num'      => (int)(getenv('WORKER_NUM') ?: swoole_cpu_num()),
      'max_request'     => 10000,   // 每 worker 处理 1 万请求后自动重启,兜底内存泄漏
      'enable_coroutine'=> true,
      'log_level'       => SWOOLE_LOG_WARNING,
  ]);

  // 就绪探针:Knative/K8s 靠它判断"实例热了没有"
  $server->on('request', function (Request $req, Response $resp) use ($handler) {
      if ($req->server['request_uri'] === '/healthz') {
          $resp->end('ok');
          return;
      }

      // ---------- 3. 请求级隔离:常驻模式最大的坑在这里解决 ----------
      // 大白话:php-fpm 时代每个请求是"新房间",常驻模式是"同一间房连续接客",
      // 必须每次把桌子擦干净——静态属性、全局变量、单例里的用户态数据都要重置。
      $t0 = hrtime(true);
      try {
          $event = [
              'method'  => $req->server['request_method'],
              'path'    => $req->server['request_uri'],
              'query'   => $req->get  ?? [],
              'headers' => $req->header ?? [],
              'body'    => $req->rawContent() ?: '',
          ];
          $result = $handler($event);   // 执行用户函数
          $resp->status($result['status'] ?? 200);
          foreach (($result['headers'] ?? []) as $k => $v) { $resp->header($k, $v); }
          $resp->end($result['body'] ?? '');
      } catch (\Throwable $e) {
          fwrite(STDERR, json_encode(['event'=>'error','msg'=>$e->getMessage()]) . "\n");
          $resp->status(500);
          $resp->end('{"error":"internal"}');
      } finally {
          // 显式清理:数据库连接归还连接池、清 request 级缓存
          gc_collect_cycles();
          $ms = (hrtime(true) - $t0) / 1e6;
          fwrite(STDERR, json_encode(['event'=>'invoke','ms'=>round($ms,2)]) . "\n");
      }
  });

  // 优雅退出:Knative 缩容发 SIGTERM,把在途请求做完再死,不丢请求
  $server->on('workerStop', fn() => fwrite(STDERR, "worker draining\n"));
  $server->start();

  用户侧的"函数"长这样(开发体验对齐公有云 FaaS):

  <?php
  // /app/handler.php ——函数开发者只写这个
  return function (array $event): array {
      return [
          'status' => 200,
          'headers' => ['Content-Type' => 'application/json'],
          'body' => json_encode(['hello' => $event['query']['name'] ?? 'xinchuang']),
      ];
  };

  1.3 冷启动全链路逐层砍(这是课题一的核心)

  先给一张"钱花在哪"的账单(实测量级,信创硬件普遍再乘 1.2~2 倍):

  总冷启动 = 拉镜像(2000~10000ms) + 起容器(300~800ms) + 起PHP进程(80~200ms)
           + 编译PHP代码(100~800ms) + 框架bootstrap(50~500ms)
             ↑最大头                               ↑最容易忽视

  第 1 层:拉镜像(占 70%+,优先打)

  三招叠加:

  # 招1:镜像瘦身——运行时镜像做到<80MB
  # (第一轮对话的多阶段构建已解决,这里只强调:Serverless 镜像连 nginx 都不要,
  #  Swoole 自己就是 HTTP 服务器,Pod 里只剩一个容器)

  # 招2:Nydus 按需加载(大白话:不等镜像下完就启动,
  # 容器先跑起来,用到哪个文件再去仓库取哪块,冷启动砍掉整个"下载等待"# containerd 配置 /etc/containerd/config.toml:
  [proxy_plugins.nydus]
    type = "snapshot"
    address = "/run/containerd-nydus/containerd-nydus-grpc.sock"
  镜像推送时转格式:nydusify convert --source harbor.internal/fn/hello:v1 --target
  harbor.internal/fn/hello:v1-nydus。信创可行性:Nydus 是纯 Go+Rust,arm64 官方支持;loong64
  需自编译(能编过)。备选:Dragonfly P2P 分发(节点间互传镜像,适合大集群)。

  招 3:节点镜像预热——用DaemonSet 在发布时提前把新版本镜像拉到所有节点(crictl
  pull),把"拉镜像"从冷启动路径上彻底移走。小集群用招 3 最简单有效,大集群上招 2。

  第 2 层:编译 PHP 代码 ——opcache file_cache 是 Serverless 场景被严重低估的大招

  大白话:opcache 平时把编译结果存内存,容器一死就没了,下个冷启动全部重编。opcache.file_cache
  能把编译结果存成文件——那我们在构建镜像时就预编译好,烤进镜像里,冷启动直接加载,编译耗时从几百毫秒变成接近零:

  # Dockerfile 追加:构建期把 opcache 编译产物烤进镜像
  RUN mkdir -p /app/.opcache && \
      php -d opcache.enable_cli=1 \
          -d opcache.file_cache=/app/.opcache \
          -d opcache.file_cache_only=1 \
          /app/runtime/warmup.php

  <?php
  // runtime/warmup.php ——把应用所有 PHP 文件预编译一遍
  $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('/app'));
  foreach ($it as $f) {
      if ($f->getExtension() === 'php') {
          opcache_compile_file($f->getRealPath());   // 编译并写入 file_cache
      }
  }

  运行期 php.ini 对应开启:

  opcache.file_cache=/app/.opcache
  opcache.file_cache_consistency_checks=0   ; 镜像不可变,跳过校验再省一点
  opcache.validate_timestamps=0

  坑(不回避):file_cache 产物和 PHP 版本、架构、编译选项绑定——所以三架构镜像必须各自在原生Runner
  上烤各自的缓存(正好上一轮的三架构原生构建流水线天然满足)。另外它跟 JIT 产物无关,JIT 还是要运行期热身,见第二部分。

  第 3 层:框架 bootstrap ——preload + worker 模式双保险

  ; opcache.preload:PHP 启动时把框架核心类一次性加载进共享内存,
  ; 所有 worker 直接用,连 autoload 的文件查找都省了
  opcache.preload=/app/runtime/preload.php
  opcache.preload_user=phpapp

  <?php
  // runtime/preload.php ——挑高频核心类预加载(别全量,preload 的类无法热更新)
  require '/app/vendor/autoload.php';
  foreach ([
      // 框架核心 + vendor 里最热的类,按 composer 的 classmap 挑
      \Swoole\Http\Server::class,
  ] as $cls) { class_exists($cls); }
  // 更工程化:解析 vendor/composer/autoload_classmap.php 按白名单批量 opcache_compile_file

  Worker 模式已经把 bootstrap 从"每请求"降到"每实例",preload 再把"每实例"的这部分从几百毫秒压到几十毫秒。

  第 4 层:调度层——"预热池"策略(Knativ完整配置)

  apiVersion: serving.knative.dev/v1
  kind: Service
  metadata:
    name: fn-hello
    namespace: functions
  spec:
    template:
      metadata:
        annotations:
          # 大白话:这四行就是冷启动策略的全部灵魂
          autoscaling.knative.dev/min-scale: "1"        # 常驻1个热实例=永不冷启动(花钱买时间)
          autoscaling.knative.dev/max-scale: "50"
          autoscaling.knative.dev/target: "80"          # 每实例并发80再扩容(Swoole扛得住)
          autoscaling.knative.dev/scale-down-delay: "10m" # 流量走了再等10分钟才缩(防抖动)
      spec:
        containerConcurrency: 100
        timeoutSeconds: 60
        containers:
        - image: harbor.internal/fn/hello:v1-nydus
          ports: [{ containerPort: 8080 }]
          env:
          - { name: WORKER_NUM, value: "4" }
          resources:
            requests: { cpu: 250m, memory: 128Mi }
            limits:   { cpu: "2",  memory: 256Mi }
          readinessProbe:
            httpGet: { path: /healthz, port: 8080 }
            periodSeconds: 1          # 探得越勤,实例"转正"越快,直接影响冷启动尾巴

  min-scale 的决策公式(大白话):核心业务函数 min-scale: 1~2(用一个小容器的钱消灭 100% 冷启动);长尾函数 min-scale: 0
  + 上面第 1~3 层优化把冷启动压到 1 秒内,用户可感但可接受。不要教条追求全员
  scale-to-zero,信创硬件资源单价没贵到那个程度。

  第 5 层(进阶):CRIU 快照恢复——"把热好的进程冻起来,要用时解冻"

  大白话:与其每次重新启动+热身,不如把一个已经热身完毕(框架加载完、JIT 编译完)的进程做成快照,冷启动=解冻快照,能进
  100ms 级。

  # 热身完成后打快照(在 init 容器/节点 agent 里做)
  criu dump --tree $(pidof -s php) --images-dir /snapshots/fn-hello \
       --shell-job --tcp-established --leave-running
  # 冷启动时恢复(代替正常启动命令)
  criu restore --images-dir /snapshots/fn-hello --shell-job --tcp-established

  诚实的现状与方案:K8s 原生只支持 checkpoint(kubelet ForensicCheckpoint),restore
  还没进编排层,所以工程化做法是节点上跑一个恢复 agent,容器 entrypoint 判断"有快照就
  restore、没有就正常启动"。架构支持度:x86/arm64 的 CRIU 成熟;龙芯的 CRIU 支持在新世界内核(≥5.19)上已可用但案例少,
  必须充分回归测试,龙芯节点可先降级用"预热池"方案兜底。另一个坑:快照里的随机数种子、时间、TLS
  会话是"冻结时刻"的,恢复后要在代码里重置(random_bytes 重新初始化、断开重连所有长连接)——在restore 后钩子里做。

  优化效果账单(叠加后):

  ┌────────────────┬──────────┬────────────────────────────────────────────────────────────┐
  │      阶段      │  优化前  │                         全套优化后                         │
  ├────────────────┼──────────┼────────────────────────────────────────────────────────────┤
  │ 拉镜像         │ 3~10s    │ ~0(预热/Nydus)                                           │
  ├────────────────┼──────────┼────────────────────────────────────────────────────────────┤
  │ 起容器+PHP     │ 0.5~1s   │ 0.3~0.5s                                                   │
  ├────────────────┼──────────┼────────────────────────────────────────────────────────────┤
  │ 编译+bootstrap │ 0.3~1.3s │ 30~80ms(file_cache+preload+worker)                       │
  ├────────────────┼──────────┼────────────────────────────────────────────────────────────┤
  │ 总计           │ 4~12s    │ 0.4~0.6s(scale-to-zero)/ 0(min-scale≥1/~0.1s(CRIU) │
  └────────────────┴──────────┴────────────────────────────────────────────────────────────┘

  ---

  第二部分:国产 CPU 上的 PHP JIT 深度优化与指令集适配

  2.1 JIT 原理三句大白话

  ①PHP代码先编译成 opcode(字节码),由 Zend VM 解释执行——这是没有JIT 的世界。②JIT(PHP8.4 起基于新的 IR
  框架,8.0~8.3 基于 DynASM)把热点 opcode 序列翻译成本机机器码,跳过解释器。③所以JIT 需要一个针对每种 CPU
  架构的后端(把 IR 翻译成该架构机器码的翻译官)——上游现有翻译官只有x86_64 和 aarch64 两位,loongarch64
  没有。这就是三种国产 CPU 命运分叉的原因。

  先泼一盆冷水(重要,防止立项跑偏):JIT 对典型 Web 业务(IO 密集、短逻辑)的收益通常只有
  5%~20%,对计算密集型(加解密、图像、规则引擎、数据处理)能到 1.5~3 倍。 opcache 本身(不含 JIT)才是 80%
  的性能来源,而它全架构可用。所以龙芯上"没有 JIT"不是灾难,是可量化的已知损失。先测你的真实业务属于哪类,再决定投入。

  2.2 鲲鹏 / 飞腾(aarch64):JIT 可用,做"深度调优"

  第一步:把 PHP 编译到位(大部分性能是编译期给的)

  #!/bin/bash
  # build-php-kunpeng.sh ——鲲鹏 920 优化编译(两遍 PGO)
  set -e
  PHP_SRC=/build/php-8.4.x

  # 鲲鹏920 = ARMv8.2 + tsv110 核心,GCC 认识它;
  # +lse:原子指令用 LSE 版本(多核锁竞争场景快很多,opcache 共享内存锁受益)
  # +crc +crypto:CRC32/AES 硬指令,PHP 的 hash/openssl 路径受益
  CPU_FLAGS="-mcpu=tsv110 -march=armv8.2-a+lse+crc+crypto"
  # 飞腾 D2000/S2500 用:CPU_FLAGS="-march=armv8-a+crc -mtune=cortex-a72"(FTC663 无公开 mtune,A72 近似最优)

  cd $PHP_SRC
  COMMON_OPTS="--prefix=/opt/php --enable-fpm --enable-opcache --with-openssl \
    --enable-mbstring --with-pdo-mysql=mysqlnd --with-pdo-pgsql ..."

  # ----1遍:插桩编译,跑真实业务采样 ----
  ./configure $COMMON_OPTS CFLAGS="-O2 $CPU_FLAGS -fprofile-generate" \
              LDFLAGS="-fprofile-generate"
  make -j$(nproc)
  # 用你的真实业务跑一轮(别用 bench.php 采样,采样负载要像生产)
  sapi/cli/php /app/profile-workload.php

  # ----2遍:按采样结果重排热路径(分支预测、代码布局全面受益)----
  make clean
  ./configure $COMMON_OPTS CFLAGS="-O2 $CPU_FLAGS -fprofile-use -fprofile-correction" \
              LDFLAGS="-fprofile-use"
  make -j$(nproc) && make install

  大白话:PGO 是"先试跑一遍看哪些代码最热,再按热度重新排版编译",对 PHP 解释器这种巨型 switch 结构收益实测
  8%~15%,白捡的。

  第二步:JIT 参数调优(php.ini)

  opcache.jit=tracing            ; 8.4 推荐 tracing(追踪热路径整段编译,比函数级激进)
  opcache.jit_buffer_size=128M   ; 大白话:给机器码的停车场。太小=热函数挤不进白编译,
                                 ; 太大=浪费共享内存。128M 够绝大多数单应用
  opcache.jit_hot_loop=32        ; 循环跑32次就算热(默认64,Serverless 短生命周期调低让 JIT 早生效)
  opcache.jit_hot_func=16
  opcache.jit_hot_return=4
  opcache.huge_code_pages=1      ; 用大页存机器码,减少 iTLB 缺失(arm64 收益比 x86 更明显)

  第三步:aarch64 特有的坑(不回避)

  1. 64KB 内核页坑:麒麟 V10 aarch64 版内核常配 64KB 页(getconf PAGESIZE 查)。后果:opcache.huge_code_pages
     行为变化、mmap 粒度变大导致小函数 JIT 缓冲浪费、部分依赖 4K 页假设的扩展(老版
     jemalloc)直接崩。方案:统一集群内核页大小(推荐 4K,K8s 生态兼容性最好),做不到就在 64K 页机器上实测关掉
     huge_code_pages 对比。
  2. W^X(写异或执行):麒麟高安全等级配置会禁止"可写又可执行"内存页,JIT 恰恰需要写完代码再执行。PHP 的 JIT 已做
     mprotect 切换适配,但如果开了 SELinux/内核锁定策略报 mprotect 拒绝,去审计日志找 execmem 拒绝记录,为 php-fpm
     域单独放行 execmem,而不是全局关安全策略。
  3. 指针认证/BTI(ARMv8.5):鲲鹏 920 不涉及,新一代 ARM 芯片若开 BTI,JIT 生成的代码缺 BTI 落地指令会被杀——升级到PHP
     8.4+(IR 框架已处理)。

  第四步:基准测量脚本(没有测量的优化都是玄学)

  #!/bin/bash
  # bench.sh ——三个层次各测一遍,别只看一个数
  PHP=/opt/php/bin/php
  for jit in "off" "tracing"; do
    echo "=== JIT=$jit ==="
    # 层1:纯计算(JIT 收益上限)
    $PHP -d opcache.enable_cli=1 -d opcache.jit=$jit -d opcache.jit_buffer_size=128M \
         Zend/bench.php | tail -1
    # 层2:真实业务压测(wrk 打 Swoole 服务,这才是要汇报的数)
    # 先起服务再: wrk -t8 -c256 -d60s --latency http://127.0.0.1:8080/api/hot-path
  done
  # 层3:perf 看微架构(鲲鹏上确认 iTLB miss、分支误判是否下降)
  perf stat -e instructions,cycles,iTLB-load-misses,branch-misses -p $(pidof -s php) sleep 30

  2.3 龙芯(loongarch64):JIT 从无到有,三条路线全给出

  现状(截至目前的上游状态,立项前务必再核对一次 php-src 和 dstogov/ir 仓库):PHP 8.4 的 IR 框架后端只有 x86_64 和
  aarch64;loongarch64 无上游 JIT 后端,opcache.jit 在龙芯上是禁用状态。但注意:opcache 字节码缓存、preload、file_cache
  在龙芯全部可用——第一部分的冷启动优化不受影响。另外核对两个周边适配点:Fiber(PH8.1+ 协程栈切换用的手写汇编) 的
  loongarch64 支持(Boost.Context 已有 loongarch64 实现,php-src 已跟进合入,用前跑 sapi/cli/php -r 'new
  Fiber(fn()=>1);' 验证),以及 Zend 字符串函数的 SIMD 路径(见后)。

  路线一(默认路线,性价比最高):不开 JIT,把 JIT 之外的性能全吃满

  # 龙芯 3A6000/3C5000 编译参数(LA664/LA464 核心)
  CFLAGS="-O2 -march=loongarch64 -mtune=la664 -mlsx"   # 3A5000 用 -mtune=la464
  # 同样跑上面的两遍 PGO 流程——PG在弱单核 CPU 上相对收益反而更大

  再加两个龙芯专属加速点:

  1. LSX 向量化补齐:Zend 引擎里 base64_encode、addslashes、UTF-8 校验等热点函数有 SSE2/NEON 手写 SIMD 版本,loongarch64
     走的是标量兜底路径。方案:用 LSX(龙芯 128 位向量,指令语义和 SSE2
     高度对应,几乎能一比一翻译)补一套。这是比移植整个 JIT
     便宜两个数量级、又能立刻上生产的真优化。示例(Zend/zend_string.h 的思路示意):

  /* zend 字符串 SIMD 的 loongarch LSX 版补齐(以"查找需要转义的字符"为例)*/
  #if defined(__loongarch_sx)
  #include <lsxintrin.h>
  static zend_always_inline int zend_check_slash_lsx(const char *p) {
      __m128i blk   = __lsx_vld(p, 0);                       /* 一次装16字节 */
      __m128i slash = __lsx_vreplgr2vr_b('\\');              /* 16个'\'   */
      __m128i quote = __lsx_vreplgr2vr_b('\'');
      __m128i hit   = __lsx_vor_v(__lsx_vseq_b(blk, slash),  /* 逐字节比较 */
                                  __lsx_vseq_b(blk, quote));
      return __lsx_bnz_v(hit);                               /* 有任一命中? */
  }
  #endif

  2. jemalloc/mimalloc 替换分配器:龙芯上 glibc malloc 较弱,LD_PRELOAD jemalloc(loongarch 已支持)实测 PHP 吞吐能再拿
     3%~8%。

  路线一的综合效果:PGO + LSX 补齐 + jemalloc + opcache 全开 ≈把"没有 JIT 的龙芯"性能抬升 15%~30%,对 Web
  型业务基本追平"该不该有 JIT"的差距。

  路线二:跟踪/合作龙芯社区补丁

  龙芯软件生态团队(loongnix)长期做解释器/JIT 类移植(LuaJIT、OpenJDK、.NET、V8 的 loongarch
  移植都是他们主导或深度参与)。方案:立项前先问龙芯(loongson.cn 生态合作渠道 / loongnix 社区)拿 PHP JIT
  移植路线图,有半成品就合作共建,比自己从零干省 80% 成本。这是被无数信创项目验证过的正确姿势:先问龙芯,再自己动手。

  路线三:自研 IR 框架 loongarch64 后端(给出完整工程计划)

  如果你的业务是计算密集且规模大到值得投入,这是"深度研究"课题该干的事。PHP 8.4 的 JIT 建立在独立的 IR
  项目(dstogov/ir)上,移植的战场在 IR 项目里,不在 php-src 里——这是好消息,因为IR 的 aarch64 后端就是现成的参考答案。

  工程分解(按 aarch64 后端的文件结构对照):

  ir 项目里需要新增/修改的文件:
    ir_loongarch64.dasc    ←核心:每个 IR 指令 →LoongArch 机器码模板(约 8000 行,工作量大头)
    ir_loongarch64.h       ←寄存器定义、调用约定描述
    dynasm/dasm_loongarch64.lua / .h
                           ←DynASM 的 loongarch 编码器
                             【捷径】龙芯给 LuaJIT 做的 loongarch DynASM 移植可直接复用改造,
                             指令编码器不用从零写
    ir_emit.c / ir_ra.c    ←注册新目标的寄存器分配约束

  关键技术决策点(每个都给方案):

  ┌────────────┬─────────────────────────────┬──────────────────────────────────────────────────────────────────────┐
  │    难点    │           大白话            │                                 方案                                 │
  ├────────────┼─────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
  │            │ IR                          │ loongarch 有 32 个 GPR(比 x86_64 的 16 个宽裕),映射比 x86         │
  │ 寄存器映射 │ 假设有一堆通用寄存器可分配  │ 后端还舒服:$t0-$t8 做临时、$s0-$s8 做 IR 全局、$a0-$a7 传参,预留   │
  │            │                             │ $r21 做 JIT 专用(对应 aarch64 后端预留 x27/x28 的做法)             │
  ├────────────┼─────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
  │ 大常数加载 │ loongarch 无 x86 那种 64    │ 标准四件套 lu12i.w + ori + lu32i.d + lu52i.d,抄该平台 GCC           │
  │            │ 位立即数                    │ 的做法,DynASM 里写成宏                                              │
  ├────────────┼─────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
  │ 远跳转     │ 直接跳转指令范围 ±128MB    │ JIT buffer ≤128M时单指令 b 够用;超出走"跳板"(veneer),aarch64    │
  │            │                             │ 后端有现成的 veneer 机制可照搬                                       │
  ├────────────┼─────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
  │ 内存模型   │ loongarch 是弱内存序        │ opcache 共享内存交互处按 aarch64 后端的屏障插法对齐(dbar 对应       │
  │            │                             │ dmb),这是最容易埋隐性 bug 的地方,屏障宁多勿少,先求对再求快       │
  ├────────────┼─────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
  │            │                             │ ①IR项目自带测试集(ir_test)先全绿;②php-src的 make test 开 JIT    │
  │ 验证       │ 怎么知道生成的机器码对不对  │ 全量跑;③终极武器:双跑比对——同一请JIT                            │
  │            │                             │ 版和解释版各跑一遍比对输出,做成 fuzz 流水线跑一周                   │
  └────────────┴─────────────────────────────┴──────────────────────────────────────────────────────────────────────┘

  投入评估(诚实版):2~3 名懂编译器+懂 loongarch 的工程师,6~12 个月到"能过 php-src 测试集",再 6
  个月到"敢上生产"。产出建议直接回馈上游(php-src + ir 项目),一旦进上游,后续 PHP
  版本升级的维护成本从"背一辈子私有补丁"变成"社区共同维护"——私有补丁不回上游是信创自研最大的长期陷阱,务必立项时就定下
  upstream first 策略。

  2.4 JIT ×Serverless 的交叉难题(两个课题在这里相撞)

  难题:tracing JIT 需要"热身"(跑几十上百次才编译),而 Serverless 实例可能活不了几分钟——热身完就被缩容了,白编译。

  方案矩阵:

  1. 长活实例开 JIT,短活实例关 JIT:min-scale≥1的核心函数开 jit=tracing;scale-to-zero 的长尾函数直接
     opcache.jit=off(省掉 jit_buffer
     内存和编译开销,短生命周期里解释执行反而更划算)。用环境变量注入区分,一份镜像两种姿态。
  2. 调低热身阈值:上面给的 jit_hot_loop=32 / jit_hot_func=16 就是为 Serverless 调的,让 JIT 在实例生命周期早期就介入。
  3. CRIU 快照带走 JIT 产物:JIT 编译结果在进程内存里,CRIU 快照天然把"已热身的 JIT 代码"一起冻结——快照恢复= 冷启动 +
     JIT 热身两个问题一次全解。这是本方案里 CRIU 路线最漂亮的隐藏收益。
  4. (前瞻)关注上游 JIT 产物持久化的进展;在它落地前,方案 3 是唯一能"持久化 JIT 热身"的工程手段。

  ---

  第三部分:全场景决策矩阵 + 难题总账

  ┌──────────────┬─────────────────────────────────────────────┬────────────────────────────────────────────────────┐
  │     场景     │                 冷启动策略                  │                      JIT 策略                      │
  ├──────────────┼─────────────────────────────────────────────┼────────────────────────────────────────────────────┤
  │ 鲲鹏/飞腾 +  │ min-scale≥1+ worker 模式 + preload         │ tracing + PGO 编译 + 大页                          │
  │ 核心 API     │                                             │                                                    │
  ├──────────────┼─────────────────────────────────────────────┼────────────────────────────────────────────────────┤
  │ 鲲鹏/飞腾 +  │ scale-to-zero + Nydus + file_cache          │ JIT off                                            │
  │ 长尾函数     │                                             │                                                    │
  ├──────────────┼─────────────────────────────────────────────┼────────────────────────────────────────────────────┤
  │ 龙芯 + Web   │ 同上(全部冷启动优化在龙芯可用)            │ 路线一(PGO+LSX+jemalloc),JIT 缺席损失           │
  │ 业务         │                                             │ 5~15%,可接受                                      │
  ├──────────────┼─────────────────────────────────────────────┼────────────────────────────────────────────────────┤
  │ 龙芯 +       │ 预热池                                      │ 短期:把计算热点用 FFI 调 C(等效手工              │
  │ 计算密集     │                                             │ JIT);中期:路线二问龙芯;长期:路线三自研后端    │
  ├──────────────┼─────────────────────────────────────────────┼────────────────────────────────────────────────────┤
  │ 混合架构集群 │ 函数打 arch 标签,计算密集函数 nodeAffinity │ 各架构各自的 php.ini 由 ConfigMap 按 arch 注入     │
  │              │  锁到鲲鹏节点                               │                                                    │
  ├──────────────┼─────────────────────────────────────────────┼────────────────────────────────────────────────────┤
  │ 隔离网       │ 全部方案无外网依赖(镜像/快照/编译全内网)  │ PGO 采样数据在内网生产旁路采集                     │
  └──────────────┴─────────────────────────────────────────────┴────────────────────────────────────────────────────┘

  难题总账(一句话方案回顾):拉镜像慢→预热+Nydus;每请求bootstrap→Swooleworker 模式;编译耗时→file_cache
  烤进镜像;缩零冷启动→min-scale分级+CRIU;常驻内存串数据→请求级清理纪律+max_request兜底;龙芯无 JIT→PGO+LSX+jemalloc
  先追回大半,重度需求走龙芯合作或自研 IR 后端并回馈上游;64K 页/W^X/弱内存序→逐项给了排查点;JIT
  热身与短生命周期冲突→分级开关+CRIU连热身一起快照。

  落地顺序建议:①先上worker 模式 + preload + file_cache(纯软件改造,一周见效,冷启动砍
  80%);②再做镜像预热/Nydus(平台层,两周);③鲲鹏PGO 编译(一周,性能白捡 10%);④CRIU和龙芯 LSX
  补齐做技术预研(一个月出可行性报告);⑤龙芯JIT
  后端按路线二→路线三顺序推进,作为长期研究课题立项。每一步独立可验收、可写报告。
Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐