每秒10次并发请求时Crypt_GPG加密签名报错,求解决方案
问题描述
使用以下PHP代码进行数据加密与签名操作:
$gpg = new Crypt_GPG(['digest-algo' => "SHA256", 'cipher-algo' => "AES256", 'compress-algo' => "zip", "homedir" => "/home/ubuntu/.gnupg", "debug" => true]); $gpg->addSignKey($clientKey, $passphase); $gpg->addEncryptKey($serverKey); $signedMessage = $gpg->encryptAndSign(json_encode($messageBody));
在每秒10次请求的并发场景下,出现如下错误日志:
Crypt_GPG DEBUG: GPG error stream ready for reading Crypt_GPG DEBUG: => about to read 65536 bytes from GPG error Crypt_GPG DEBUG: => read 80 bytes Crypt_GPG DEBUG: ERROR: gpg: signing failed: End of file Crypt_GPG DEBUG: ERROR: gpg: [stdin]: sign+encrypt failed: End of file Crypt_GPG DEBUG: GPG status stream ready for reading Crypt_GPG DEBUG: => about to read 65536 bytes from GPG status Crypt_GPG DEBUG: => read 69 bytes Crypt_GPG DEBUG: STATUS: BEGIN_ENCRYPTION 2 9 Crypt_GPG DEBUG: STATUS: FAILURE sign-encrypt 33570815 Crypt_GPG DEBUG: => subprocess returned an unexpected exit code: 2
需要解决该问题以支持至少5-10次并发请求。
解决方案
核心原因
并发场景下的End of file错误,本质是GnuPG的密钥锁竞争:多个进程同时访问同一个私钥时,GnuPG会创建锁文件防止冲突,但默认锁机制在高并发下会出现进程等待超时或读取密钥失败的情况。
1. 为每个请求分配独立GnuPG工作目录
通过为每个并发进程创建独立的GnuPG目录,避免锁冲突:
// 按进程ID生成临时GPG目录 $pid = getmypid(); $tempGpgDir = "/tmp/gnupg_{$pid}"; // 初始化目录及密钥文件(仅首次执行时) if (!is_dir($tempGpgDir)) { mkdir($tempGpgDir, 0700, true); // 复制主目录的公钥、私钥文件 copy("/home/ubuntu/.gnupg/pubring.kbx", "{$tempGpgDir}/pubring.kbx"); copy("/home/ubuntu/.gnupg/secring.gpg", "{$tempGpgDir}/secring.gpg"); // 复制GPG 2.1+的私钥存储目录 $sourcePrivateDir = "/home/ubuntu/.gnupg/private-keys-v1.d"; $targetPrivateDir = "{$tempGpgDir}/private-keys-v1.d"; if (is_dir($sourcePrivateDir)) { mkdir($targetPrivateDir, 0700); foreach (scandir($sourcePrivateDir) as $file) { if ($file !== '.' && $file !== '..') { copy("{$sourcePrivateDir}/{$file}", "{$targetPrivateDir}/{$file}"); } } } } // 使用临时目录初始化Crypt_GPG $gpg = new Crypt_GPG([ 'digest-algo' => "SHA256", 'cipher-algo' => "AES256", 'compress-algo' => "zip", "homedir" => $tempGpgDir, "debug" => true ]); $gpg->addSignKey($clientKey, $passphase); $gpg->addEncryptKey($serverKey); $signedMessage = $gpg->encryptAndSign(json_encode($messageBody)); // 请求结束后清理临时目录(可选,根据服务类型调整) array_map('unlink', glob("{$tempGpgDir}/*")); rmdir($tempGpgDir);
优化点:如果是长期运行的服务,可以预创建固定数量的GPG目录(比如10个),通过轮询分配给请求,减少重复复制密钥的开销。
2. 调整GnuPG锁超时参数
针对5-10次/秒的并发,可延长GnuPG的锁等待时间,减少冲突概率:
$gpg = new Crypt_GPG([ 'digest-algo' => "SHA256", 'cipher-algo' => "AES256", 'compress-algo' => "zip", "homedir" => "/home/ubuntu/.gnupg", "debug" => true, // 添加锁超时参数,单位毫秒 'additional_args' => ['--lock-timeout', '5000'] ]);
注意:该方法仅适用于低并发场景,极端并发下仍可能出现锁冲突。
3. 实现签名代理/进程池
通过固定数量的worker进程统一处理加密签名请求,避免多进程竞争密钥:
- 用PHP的
pcntl扩展创建10个左右的worker进程,通过管道或消息队列接收请求 - 使用Redis等消息队列将待处理任务放入队列,worker进程逐个处理
4. 复用Crypt_GPG实例(仅常驻内存服务)
在Swoole、Workerman等常驻内存的服务中,初始化一次Crypt_GPG实例并复用,减少重复加载密钥的开销:
// 服务启动时初始化(仅执行一次) $gpg = new Crypt_GPG([ 'digest-algo' => "SHA256", 'cipher-algo' => "AES256", 'compress-algo' => "zip", "homedir" => "/home/ubuntu/.gnupg", "debug" => true ]); $gpg->addSignKey($clientKey, $passphase); $gpg->addEncryptKey($serverKey); // 处理每个请求时直接复用实例 $signedMessage = $gpg->encryptAndSign(json_encode($messageBody));
该方法不适用于传统FPM环境,因为FPM每个请求是独立进程,无法跨请求复用实例。
内容的提问来源于stack exchange,提问作者Abhishek Chauhan
相关产品推荐
相关产品推荐

