如何在swoole提供服务应用程序上处理实体管理器

102 阅读3分钟

你们中的一些人可能知道,我有一个宠物项目,我喜欢时不时地在其中工作。这个项目是一个名为Shlink的自我托管的URL缩短器。

Shlink是使用expressive作为HTTP调度任务的基础框架而建立的。不久前,一个expressive模块被发布,正式支持用swoole为expressive应用提供服务,我决定将其纳入Shlink。

swoole是如何工作的?

Swoole是一个异步非阻塞I/O框架,其工作方式类似于node.js,但适用于PHP应用。

它的设计方式是,应用程序在两次请求之间保持在内存中,消除了引导步骤,使应用程序的服务速度大大加快。

Swoole可以根据你的要求同时提供多个请求。为此,它使用了工作者。每个工作者一次可以提供一个请求,每个工作者都有一个单独的应用程序实例加载在内存中(这是我最近学到的东西)。

正因为如此,你有时必须改变一下你设计代码的方式。例如,如果一个服务需要是有状态的(它不应该,但狗屎发生了),你将不得不记住,状态将在请求之间持续存在,你将希望以某种方式重置它以避免意外的副作用。

这对EntityManager有什么影响?

在Shlink中保持这种内部状态的主要服务之一是学说EntityManager

由于它最初被设计为用于经典的Web服务器+CGI上下文,假设它在每次请求时都会被重新创建,因此有一些考虑因素需要考虑。

  • 它实现了工作单元的模式。它在内部保持对所有已经创建/改变的实体的跟踪,直到你决定冲洗它。
  • 如果出现故障,EntityManager 可能会被关闭,这使得它在那一刻之后无法使用。
  • 如果数据库连接过期,EntityManager 将不会优雅地恢复。相反,它将会失败,因为它不知道连接不能使用,使你陷入前述的困境。

如何解决这个问题

我通过尝试和错误了解到这些问题,通过在应用程序上使用它并得到失败的结果。现在,我想分享一下我是如何解决这些问题的。

我面临的第一个问题是,在关闭EntityManager ,使应用程序无法使用。为了解决这个问题,我创建了一个装饰的实例(命名,一个实现EntityManagerInterface 的对象,它反过来又包装了一个实际的EntityManager ),这确保了被包装的实例在某个时候被关闭时能透明地重新创建。

然而,这造成了一些副作用,我最终意识到,最好是让EntityManager 必须从外部重新打开,而且每次请求只能打开一次(这更接近于它在经典设置中的行为方式)。

这就是我想出的Shlink目前使用的ReopeningEntityManager (从v1.20.0开始)。

<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\Common\Doctrine;

use Doctrine\ORM\Decorator\EntityManagerDecorator;

class ReopeningEntityManager extends EntityManagerDecorator
{
    /** @var callable */
    private $createEm;

    public function __construct(callable $createEm)
    {
        parent::__construct($createEm());
        $this->createEm = $createEm;
    }

    public function open(): void
    {
        if (! $this->wrapped->isOpen()) {
            $this->wrapped = ($this->createEm)();
        }
    }
}

你可以在shlink-common中找到实际的代码。

它只是期望有一个工厂来创建实际的EntityManager ,并暴露出一个open 的公共方法,如果关闭的话,就会重新创建被包裹的实例。

这个方法是由PSR-15中间件调用的,该中间件需要在中间件流水线的早期注册。它确保该方法在栈上的下一个中间件被调用之前被调用。

<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\Common\Middleware;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Common\Doctrine\ReopeningEntityManager;

class CloseDbConnectionMiddleware implements MiddlewareInterface
{
    /** @var ReopeningEntityManager */
    private $em;

    public function __construct(ReopeningEntityManager $em)
    {
        $this->em = $em;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $this->em->open();

        try {
            return $handler->handle($request);
        } finally {
            $this->em->getConnection()->close();
            $this->em->clear();
        }
    }
}

同样,代码可以在shlink-common中找到。

这个中间件还做了两件事(这次是在调用堆栈上的下一个中间件之后,并确保在抛出异常时也能做到)。

  • 它首先关闭了数据库连接。通过这样做,我们确保EntityManager 在下次需要与数据库交互时将重新连接,因此,我们避免了 "连接过期 "问题。
  • 然后,它自己清除了EntityManager 。这就避免了内存泄漏,同时也防止了任何孤儿/未刷新的实体 "跨越 "到下一个请求,解决了第一个问题。

进一步的考虑

到目前为止,这种方法一直运行良好,但重要的是要假设EntityManager 并不是真正为这种非阻塞式的环境设计的。

你需要知道,在EntityManager 完成其工作之前,利用它的工作者将不能为任何请求服务。正因为如此,你可能想增加swoole工作者的数量。

另外,在每个请求中关闭DB连接并不是最理想的方法。如果有一个连接池,能够在过期时重新创建连接,并尽可能地保持连接的开放,以加快操作速度,那就更完美了。然而,EntityManager 不是为此而设计的,所以最好是关闭它。

你还需要一个callable 来传递给ReopeningEntityManager 。你解决这个问题的方式取决于你的实现和你使用的依赖关系。

在我的例子中,在Shlink中我使用了ServiceManager ,所以我有一个委托工厂,它接收实际的工厂并将其传递给ReopeningEntityManager

<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\Common\Doctrine;

use Psr\Container\ContainerInterface;

class ReopeningEntityManagerDelegator
{
    public function __invoke(ContainerInterface $container, string $name, callable $createEm): ReopeningEntityManager
    {
        return new ReopeningEntityManager($createEm);
    }
}

你可以在shlink-common中找到实际的代码。

总结

我想把这篇文章也写成对自己的总结,因为我不得不多次询问和调查,而且我找不到一个解释一切的真理来源。

我希望,如果你最终来到这里,这篇文章对你有好处。