让OpenSwoole和AWS SDK友好相处的指南

83 阅读2分钟

我有一些内容存储在与S3兼容的对象存储中,并希望能够(a)推送到该存储,(b)从该存储中提供项目。

很简单:使用Flysystem的AWS S3适配器,把它指向我的存储,就可以了。

除了一个小插曲。我正在使用OpenSwoole。

问题所在

到底是什么问题呢?

默认情况下,AWS适配器使用AWS PHP SDK,而后者又使用Guzzle。Guzzle有一个用于HTTP处理程序的可插拔适配器系统,但默认情况下,当cURL扩展存在并支持多执行时,会使用其CurlMultiHandler 。这是一个合理的选择,并在大多数情况下提供最佳性能。

在内部,当处理程序准备进行一些请求时,它会调用curl_multi_init() ,然后将该函数返回的句柄记忆化。这使得处理程序可以并行地运行许多请求,并等待它们各自完成,即使不是在异步环境中运行,也能获得异步功能。

当使用OpenSwoole时,这种状态会成为一个问题,特别是服务,它可能被实例化一次,并在许多请求中多次重复使用,直到服务器被关闭。 更具体地说,当OpenSwoole中启用了coroutine支持时,它就成为一个问题。

OpenSwoole已经为cURL提供了一段时间的循环程序支持。 然而,当涉及到cURL的多执行程序支持时,它一次只允许一个多执行程序句柄。 这就是我的问题的具体来源。最终的结果是锁的问题,这导致了异常,从而导致了错误的响应。

(天啊,要调试并找到这些问题的根本原因是很困难的!)。

解决方案

幸好Guzzle允许你指定你自己的处理程序,而vanillaCurlHandler

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Handler\CurlHandler;

$client = new Client([
    'handler' => HandlerStack::create(new CurlHandler()),
]);

下一个障碍是让AWS S3 SDK使用这个处理程序。 幸运的是,S3客户端构造函数有一个http_handler 选项,允许你传递一个HTTP客户端处理程序实例。我可以重新使用SDK提供的现有GuzzleHandler ,把我的客户端实例传递给它:

use Aws\Handler\GuzzleV6\GuzzleHandler;
use Aws\S3\S3Client;

$storage = new S3Client([
    // .. connection options such as endpoint, region, and credentials
    'http_handler' => new GuzzleHandler($client),
]);

虽然命名空间是GuzzleV6 ,但该命名空间中的GuzzleHandler 也适用于Guzzle v7。

然后我可以把它传给Flysystem,我就可以开始了。

但是那些异步功能呢?

但是,切换到vanillaCurlHandler 不是意味着我失去了异步功能吗?

OpenSwoole coroutine支持的好处是,当cURL钩子可用时,你基本上可以通过vanilla cURL功能获得多执行的并行化好处。 因此,我概述的方法既能解决我遇到的运行时错误**,又**能提高性能。 我喜欢这样轻松的胜利!

奖励回合:PSR-7集成

与OpenSwoole + AWS SDK的问题无关,我还有一个问题想解决。 虽然我喜欢Flysystem,但有一个地方,直接使用AWS SDK的S3是一个非常好的胜利:直接服务文件。

在使用Flysystem时,我使用它的mimeType()fileSize() API来获取响应的文件元数据,然后将文件复制到内存中(即php://temp )的PSR-7StreamInterface 。重复调用意味着我为同一个文件多次查询API,降低了性能。而且缓冲到内存流有可能出现内存外错误。

我尝试的一个替代方法是将文件从存储空间复制到本地文件系统;这将使我能够使用PSR-7的标准文件系统流,它的性能相当好,而且不需要大量的内存。 然而,拥有对象存储的一个要点是,我可以减少我所使用的本地文件系统存储量。

因此,对于这个特定的用例,我转而直接使用AWS S3 SDK,并调用其getObject() 。该方法返回一个数组/对象混杂物,提供对象元数据,包括MIME类型和内容长度,还包括一个PSR-7StreamInterface 的主体。结合起来,你就可以在响应中直接流回:

$result = $s3Client->getObject([
    'Bucket' => $bucket,
    'Key'    => $filename,
]);

/** @var Psr\Http\ResponseFactoryInterface $responseFactory */
return $responseFactory->createResponse(200)
    ->withHeader('Content-Type', $result['ContentType'])
    ->withHeader('Content-Length', $result['ContentLength'])
    ->withBody($result['Body']);

这种新方法将响应时间缩短了66%(现在约400k的文件在200ms内返回),并将内存使用量减少到cURL使用的标准缓冲区大小。 同样,一个简单的胜利