异步PHP和一辆自行车的故事

PHP7发行后,便可以以相对较低的成本编写长期存在的应用程序。 对于程序员来说,诸如proophbroadwayproophmessenger已经可用,其作者可以解决最常见的问题。 但是,如果您向前迈出一小步来研究这个问题,该怎么办?


让我们尝试弄清楚另一辆自行车的命运,它使您可以实现“发布/订阅”应用程序。


首先,我们将尝试简要回顾一下PHP世界中的当前趋势,并简要介绍一下异步操作。


创建的PHP死了


长期以来,PHP主要用于请求/响应工作流。 从开发人员的角度来看,这非常方便,因为无需担心内存泄漏,监视器连接。


所有查询都将彼此隔离地执行,使用完的资源将被释放,并且例如,到数据库的连接将在处理完成时关闭。


例如,您可以使用基于Symfony框架编写的常规CRUD应用程序。 为了从数据库中读取并返回JSON,必须执行许多步骤(为节省空间和时间,不包括生成/执行操作码的步骤):


  • 分析配置;
  • 容器编译;
  • 请求路由
  • 履行;
  • 渲染结果。

与PHP(使用加速器)一样,该框架会主动使用缓存(某些任务不会在下一个请求时完成)以及延迟的初始化。 从7.4版开始,将提供预加载功能 ,这将进一步优化应用程序的初始化。


但是,不可能完全消除初始化的所有间接费用。


让我们帮助PHP生存


该问题的解决方案看起来非常简单:如果每次运行应用程序都过于昂贵,则需要对其进行一次初始化,然后将请求传递给它,以控制其执行。


PHP生态系统中有一些项目,例如php-pmRoadRunner 。 两者在概念上都做相同的事情:


  • 创建一个充当监督者的父流程;
  • 创建子进程池;
  • 接收到请求后,主服务器从池中检索过程并将请求传递给它。 客户此时处于待处理状态;
  • 任务完成后,master将结果返回给客户端,并将子进程发送回池中。

如果任何子进程死亡,那么主管将再次创建该子进程并将其添加到池中。 我们从应用程序中创建了一个守护程序,其目的只有一个:消除初始化开销,显着提高了处理请求的速度。 这是提高生产率的最轻松的方法,但不是唯一的方法。


注意事项:
网络上有“使用ReactPHP并使Laravel加速N倍”系列中的许多示例。 重要的是要了解妖魔化(从而节省启动应用程序的时间)和多任务处理之间的区别。
当使用php-pm或roadrunner时,您的代码不会成为非阻塞的。 您只需节省初始化时间。
根据定义,将php-pm,roadrunner和ReactPHP / Amp / Swoole进行比较是不正确的。

PHP和I / O

默认情况下,在阻塞模式下执行与PHP中的I / O交互。 这意味着,如果我们执行更新表中信息的请求,执行流程将暂停以等待数据库的响应。 在处理请求的过程中,此类调用越多,服务器资源的空闲时间就越长。 实际上,在处理请求的过程中,我们需要多次访问数据库,将一些内容写入日志,然后将结果返回给客户端,这最终也是一个阻塞操作。


假设您是一个呼叫中心运营商,您需要在一个小时内致电50个客户。
您拨打第一个号码,那里就很忙(订户通过电话讨论《权力的游戏》的最后一系列以及该系列的内容)。
现在您正坐在那里,试图在胜利之前到达他。 随着时间的流逝,这种转变即将结束。 在失去联系第一位订户的40分钟后,您错过了与其他人联系的机会,自然而然地得到了老板的帮助。
但是您可以选择其他方法:不要等到第一个用户空闲后,听到哔哔声后,挂断电话并开始拨打下一个号码。 您可以稍后再返回第一个。
通过这种方法,呼叫最大人数的机会大大增加,并且工作速度不会停留在最慢的任务上。

不阻塞执行线程的代码(不使用阻塞的I / O调用以及sleep()类的函数)被称为异步代码。


让我们回到我们的Symfony CRUD应用程序。 由于大量使用了阻塞功能,几乎不可能使它在异步模式下工作:所有功能都与配置,缓存,日志记录,响应呈现以及与数据库的交互一起使用。


但是这些都是约定俗成的,让我们尝试抛出Symfony并使用Amp ,它提供了Event Loop(包括许多活页夹),Promises和Coroutines的实现,可以解决问题。


承诺是组织异步代码的一种方法。 例如,我们需要访问一些http资源。


我们创建一个请求对象,并将其传递给传输,Promise将其返回给我们,其中包含当前状态。 有三种可能的状态:


  • 成功:我们的请求已成功完成;
  • 错误:在执行请求期间,出现了问题(例如,服务器返回了500响应);
  • 等待中:请求处理尚未开始。

每个Promise都有一个方法(在示例中, Promise由Amp解析) onResolve() ,在其中传递带有两个参数的回调函数


 $promise->onResolve( static function(?/Throwable $throwable, $result): void { if(null !== $throwable) { /**   */ return; } /**  */ } ); 

在我们收到Promise之后,出现了一个问题:谁来监视其状态并将状态更改通知我们?


为此,使用事件循环。


本质上,事件循环是监视执行情况的调度程序。 任务完成后(无论如何),将调用我们传递给Promise的可调用对象。


至于细微差别,我建议您阅读Nikita Popov的文章: 使用协程进行协作式多任务处理 。 这将有助于使您更清楚地了解正在发生的事情以及发电机的位置。


有了新知识,让我们尝试返回到JSON渲染任务。
使用amphp / http-server处理传入的HTTP请求的示例
一旦我们收到请求,就会从数据库中执行异步读取(我们得到Promise),完​​成后,将向用户提供令人垂涎的JSON,该JSON基于接收到的数据形成。


如果我们需要从多个进程中监听一个端口,则可以考虑使用amphp / cluster

主要区别在于,由于没有阻塞执行线程,因此单个进程一次可以处理多个请求。 从数据库读取完成后,客户端将收到他的答案,并且在没有答案的情况下,您可以开始为下一个请求提供服务。


异步PHP的奇妙世界


免责声明
异步PHP在外来环境中被认为是正常的。 基本上,他们将以“带走GO / Kotlin,一个傻瓜”的风格等待笑声。 我不会说这些人是错的,但是...

有许多项目可以帮助编写非阻塞PHP代码。 在本文的框架中,我不会完全分析所有的利弊,但是我将尝试从表面上对它们进行逐一检查。


旋风

与用C语言编写的异步框架相反的异步框架,并作为PHP的扩展提供。 它可能拥有目前最好的性能指标。


有一个渠道,corutin和其他美味的东西的实现,但是他有一个很大的缺点-文档。 尽管它部分是英语的,但我认为它不是很详细,而且api本身也不是很明显。


对于社区来说,这也不是简单而明确的。 就个人而言,我不认识一个在战斗中使用Swoole的活人。 也许我会克服恐惧并移居到他那里,但这在不久的将来不会发生。


另外,如果您不了解适当水平的C语言,那么您还可以添加任何更改来为项目做出贡献(使用请求请求),这也是很困难的。


工人

如果它失去了竞争对手的速度(谈论Swoole),那么它并不是很引人注目,可以忽略许多情况下的差异。


它与ReactPHP集成在一起,从而扩大了基础设施问题的实现数量。 为了节省空间,我将与ReactPHP一起描述缺点。


ReactPHP

这些优点包括一个相当大的社区和大量示例。 缺点开始出现在使用过程中-这是Promise的概念。
如果您需要执行几个异步操作,那么代码将变成无休止的调用垃圾(这里是一个简单的RabbiqMQ连接示例,没有创建交换/队列及其绑定程序)。


通过对文件进行一些改进(考虑到规范),您可以获得协程的实现,这将有助于摆脱Promise的地狱。


我认为,如果没有recoilphp / recoil项目,在理智的应用程序中不可能使用ReactPHP。


此外,除了其他方面,人们还感觉到它的发展已经大大减慢了。 例如,使用PostgreSQL正常工作还不够。


功放

我认为,当前存在的最佳选择是。
除了通常的Promise之外,还有一个Coroutine的实现,它极大地简化了开发过程,并且使PHP程序员最熟悉该代码。


开发人员不断补充和改进项目,有反馈也没有问题。


不幸的是,尽管具有框架的所有优点,但社区却相对较小,但是同时也有一些实现,例如使用PostgreSQL以及所有基本功能(文件系统,http客户端,DNS等)。


我仍然不太了解ext-async项目的命运,但是伙计们跟上了它。 时间会证明在第三版中会发生什么。


开始使用


因此,我们对理论部分进行了一些整理,是时候继续练习并填补障碍了。


首先,我们将需求正式化:


  • 异步消息传递( message本身的概念可以分为两种类型)
    • command :指示需要完成任务。 不返回结果(至少在异步通信的情况下);
    • event :报告任何状态更改(例如,作为命令的结果)。
  • 非阻塞格式用于I / O;
  • 轻松增加处理器数量的能力;
  • 能够以任何语言编写消息处理程序。

任何消息本质上都是一个简单的结构,并且仅由语义共享。 从理解类型和目的的角度来看,消息的命名极为重要(尽管在示例中忽略了这一点)。

对于需求列表,最适合使用“ 发布/订阅”模式的简单实现。
为了确保分布式执行,我们将使用RabbitMQ作为消息代理。


原型是使用ReactPHPBunnyDoctrineDBAL编写的。
细心的读者可能会注意到Dbal在内部使用了pdo / mysqli阻止调用,但是在当前阶段这并不是特别重要,因为您必须了解最终会发生什么。


问题之一是缺少用于PostgreSQL的库。 有一些草稿,但这还不足以进行全面的工作(更多内容请参见下文)。


经过简短的研究,ReactPHP被取而代之的是Amp,因为它相对简单并且非常活跃。


RabbitMQ运输

但是,由于具有Amp的所有优点,所以存在一个问题:Amp没有RabbitMQ的驱动程序( Bunny仅支持ReactPHP)。


从理论上讲,Amp允许您使用竞争对手的Promise。 似乎一切都应该很简单,但是ReactPHP使用Event Loop来处理库中的套接字。
显然,在某个时间点无法启动两个不同的事件循环,因此我无法使用Adapt()函数。


不幸的是,兔子代码的质量尚待提高,并且不可能用另一种实现充分替代一种实现。 为了不停止工作,决定稍微重写该库,以便它与Amp一起使用,并且不会导致阻塞执行流。


一直以来,我都为之感到羞愧,这种改编看起来非常可怕,但最重要的是,它奏效了。 好吧,由于没有什么比临时的要持久的了,所以适配器一直在期待一个不太懒惰的人来处理驱动程序的实现。


发现了这样一个人。 除其他外, PHPinnacle项目提供了针对Amp量身定制的适配器的实现。


作者的名字叫Anton Shabovta,他将讨论PHP Russia框架内的异步php以及有关为PHP fwdays 开发驱动程序内容

PostgreSQL的

工作的第二个特点是与数据库的交互。 在“传统” PHP的条件下,一切都很简单:我们有一个连接,并且所有请求都按顺序执行。


在异步执行的情况下,我们必须能够同时执行多个请求(例如3个事务)。 为了能够做到这一点,需要一个连接池实现。


工作机制非常简单:


  • 我们在启动时打开N个连接(或延迟初始化,不是重点);
  • 如有必要,我们从池中获取连接,确保没有其他人可以使用它;
  • 我们执行请求,然后销毁连接或将其返回到池(首选)。

首先,它允许我们一次启动多个事务,其次,由于已经开放的连接的存在,它可以加快工作速度。 Amp有一个amphp / postgres组件。 他负责连接:监视连接的数量,生存期以及所有这些,而不会阻塞执行流程。


顺便说一句,例如在使用ReactPHP时,如果要使用数据库,则必须自己实现。


互斥体

为了使应用程序有效且最重要地正确运行,有必要实现与互斥锁类似的操作。 我们可以区分三种使用情况:


  • 在一个过程的框架内,一种简单的内存机制是合适的,没有任何多余;
  • 如果要在多个进程中提供锁定,则可以使用文件系统(当然,在非阻塞模式下);
  • 如果在多个服务器的上下文中,那么您已经需要考虑诸如Zookeeper之类的东西。

需要互斥体来解决竞争条件问题。 毕竟,我们不知道(而且我们不知道)任务将以什么顺序执行,但是我们必须确保数据的完整性。


记录/上下文

对于日志记录, Monolog已经成为标准配置,但是有一些警告:我们不能使用内置处理程序,因为它们会导致锁定。
要写入stdOut,您可以使用amphp / log ,或编写一条简单的消息发送给某些Graylog。


由于在某一时刻我们可以处理许多任务,并且在记录日志时,您需要了解在什么上下文中写入数据。 在实验过程中,决定制作trace_id分布式跟踪 )。 最重要的是,整个呼叫链必须随附可以跟踪的直通标识符。 另外,在接收到消息时, package_id生成package_id ,它确切指示接收到的消息。


因此,使用这两个标识符,我们可以轻松跟踪特定记录所指的内容。 事实是,在传统的PHP中,我们在日志中获得的所有记录主要都是按照写入的顺序进行的。 在异步执行的情况下,按条目顺序没有模式。


终止

异步开发的另一个细微差别是控制守护程序的关闭。 如果您只是终止进程,那么所有进行中的任务将无法完成,并且数据将丢失。在通常的方法中,也存在这样的问题,但是问题并不是那么严重,因为一次只能执行一项任务。


为了正确完成执行,我们需要:


  • 退订队列。 换句话说,使其无法接收新消息;
  • 完成所有剩余任务(等待兑现承诺);
  • 并且只有在那之后完成脚本。

泄漏,调试

与流行的看法相反,在现代PHP中,面对发生内存泄漏的情况并不是那么简单。 有必要做一些绝对错误的事情。


但是,一旦面对这一点,却是因为平庸的粗心。 在实施心跳期间,每40秒添加一个新计时器以查询连接。 不难猜测,一段时间后,内存的使用开始非常迅速地增长。


此外,他还编写了一个简单的观察程序,该观察程序可以选择每10分钟启动一次,并调用gc_collect_cycles()gc_mem_caches()
但是,强制启动垃圾收集器并不是必需和根本的事情。


为了不断查看内存使用情况,在日志记录中添加了标准的MemoryUsageProcessor


如果您知道事件循环正在阻塞某些东西,也可以很容易地进行检查:只需连接LoopBlockWatcher即可


但是您需要确保该观察者不会在生产环境中启动。 此功能仅在开发期间使用。


结果


: php-service-bus , Message Based .


, :


 composer create-project php-service-bus/skeleton pub-sub-example cd pub-sub-example docker-compose up --build -d 

, , .


/bin/consumer , .
/src 3 : Ping ; Pong : ; PingService : , .
PingService , 2 :


  /** @CommandHandler() */ public function handle(Ping $command, KernelContext $context): Promise { return $context->delivery(new Pong()); } /** @EventListener() */ public function whenPong(Pong $event, KernelContext $context): void { $context->logContextMessage('Pong message received'); } 

  • handle ( 1 ). @CommandHandler ;
    • Promise , RabbitMQ ( delivery() ). , RabbitMQ .
  • whenPongPong . . @EventListener ;
    , — . , , , . php-service-bus , , .

2 : , ( ) . , , (, ).


Ping , Pong . .


, RabbitMQ:


 tools/ping 

, php-service-bus , Message based .


Ping\Pong, — , , Hello, world .


, .


- , , , Saga pattern (Process manager) .



, symfony/messenger .


, , .

Source: https://habr.com/ru/post/zh-CN451916/


All Articles