After the release of PHP7, it became possible to write long-lived applications at a relatively low price. Projects such as prooph
, broadway
, tactician
, messenger
became available to tactician
, whose authors take on the solution of the most frequent problems. But what if you make a small step forward, delving into the question?
Let's try to make out the fate of another bike that allows you to implement the Publish / Subscribe application.
To begin with, we will try to briefly review current trends in the PHP world, as well as superficially touch on asynchronous work.
For a long time, PHP was used mainly in the request / response workflow. From the point of view of developers, this is quite convenient, because there is no need to worry about memory leaks, monitor connections.
All requests will be executed in isolation from each other, the occupied resources will be released, and connections, for example, to the database will be closed at the end of the process.
As an example, you can take the usual CRUD application written on the basis of the symfony framework. In order to read from the database and return JSON, you must perform a number of steps (to save space and time, eliminate the steps to generate / execute opcodes):
As in the case of PHP (using accelerators), the framework actively uses caching (some of the tasks will not be executed at the next request), as well as deferred initialization. Starting with version 7.4, preload will be available, which will further optimize the initialization of the application.
Nevertheless, it will not be possible to completely remove all overhead costs for initialization.
The solution to the problem looks quite simple: if you launch the application every time is too expensive, then you need to initialize it once and then just pass requests to it, controlling their execution.
In the PHP ecosystem there are projects such as php-pm and RoadRunner . Both conceptually do the same thing:
If any child process dies, the supervisor creates it again and adds it to the pool. We made a daemon from our application with a single purpose: to remove the initialization overhead, significantly increasing the speed of processing requests. This is the most painless way to improve performance, but not the only one.
Note:
A lot of examples from the series “Take ReactPHP and accelerate Laravel N times.” It is important to understand the difference between demonization (and, consequently, saving time on bootstrapping the application) and multitasking.
When using php-pm or roadrunner, your code does not become non-blocking. You just save time on initialization.
Comparing php-pm, roadrunner and ReactPHP / Amp / Swoole is incorrect by definition.
Interacting with I / O in PHP is by default executed in blocking mode. This means that if we execute a request to update the information in the table, the execution thread will pause waiting for a response from the database. The more such calls during the processing of the request, the longer the server resources are idle. After all, in the course of processing the request, we need to go several times to the database, write something to the log, and return the result to the client, in the end - also a blocking operation.
Imagine that you are a call center operator and you have to call 50 customers in an hour.
You dial the first number, and there it is occupied (the subscriber discusses by phone the latest series of the Game of Thrones and what the series has rolled into).
And here you sit and try to reach him before the victory. Time passes, the change comes to an end. Having lost 40 minutes to try to reach the first subscriber, you missed the opportunity to contact others and naturally received from the boss.
But you can do otherwise: do not wait until the first subscriber is free and as soon as they heard the beeps, hang up and start dialing the next number. You can return to the first one a little later.
With this approach, the chances of ringing the maximum number of people greatly increase, and the speed of your work does not rest on the slowest task.
Code that does not block the thread of execution (does not use blocking I / O calls, as well as functions like sleep()
), is called asynchronous.
Let's return to our symfony CRUD application. It is almost impossible to force it to work in asynchronous mode due to the abundance of using blocking functions: all work with configs, caches, logging, response rendering, interaction with the database.
But these are all conventions, let's try to throw out Symfony and use Amp , which provides the implementation of the Event Loop (including a number of binders), Promises and Coroutines as a cherry on the cake, to solve our problem.
Promise is one of the ways to organize asynchronous code. For example, we need to make a call to any http resource.
We create the request object and pass it to the transport, which returns us a Promise containing the current state. There are three possible states:
Each Promise has one method ( Promise from Amp understands the example) - onResolve()
, to which a callback function with two arguments is passed.
$promise->onResolve( static function(?/Throwable $throwable, $result): void { if(null !== $throwable) { /** */ return; } /** */ } );
After we received the Promise, the question arises: who will monitor its status and inform us about the status change?
To do this, use the Event Loop.
In essence, an Event Loop is a scheduler that controls execution. As soon as the task is completed (no matter how), the callable will be called, which we transferred to Promise.
As for the nuances, I would recommend reading the article from Nikita Popov: Cooperative multitasking using coroutines . It will help to bring some clarity about what is happening and where does the generators.
Armed with new knowledge, let's try to return to our task of rendering JSON.
An example of processing an incoming http request using amphp / http-server .
As soon as we receive a request, an asynchronous reading from the database is performed (we get Promise), and upon its completion, the cherished JSON will be given to the user, formed on the basis of the received data.
If we need to listen to one port from several processes, we can look towards amphp / cluster
The main difference is that a single process can serve several requests at a time because the thread is not blocked. The client will receive his answer when the reading from the database is completed, but for now there is no answer, you can take care of the next request.
Disclaimer
Asynchronous PHP is considered in the context of the exotic and is not considered something healthy / normal. Most will be waiting for a laugh in the style of "take GO / Kotlin, fool", etc. I would not say that these people are wrong, but ...
There are a number of projects that help in writing non-blocking PHP code. Within the framework of this article, I will not begin to fully analyze all the pros and cons, and I will try only to superficially examine each of them.
Asynchronous framework, written in contrast to the rest of C and delivered as an extension to PHP. It has, perhaps, the best performance indicators for the current moment.
There is a realization of channels, korutin and other tasty pieces, but he has 1 big minus - documentation. Although it is partly in English, it is not very detailed in my opinion, and the api itself is not very obvious.
As for the community, it is also not all simple and straightforward. Personally, I do not know of a single living person who uses Swoole in battle. Perhaps I will overcome my fears and migrate to it, but this will not happen in the near future.
It is also possible to add to the minuses that it is also difficult to contribute to the project (with the help of a pull request) with any changes if you don’t know C at the proper level.
If it loses in speed to its competitor (we are talking about Swoole), then it is not very noticeable and the difference in a number of scenarios can be neglected.
It has integration with ReactPHP, which in turn expands the number of implementation points of infrastructure. To save space, I will describe the drawbacks with ReactPHP.
The advantages include a fairly large community and a huge number of examples. Cons are beginning to manifest in the process of use - this is the concept of Promise.
If you need to perform several asynchronous operations, the code turns into an endless then call wrapper (here's an example of a simple connection to RabbiqMQ without creating exchange / queue and their binding).
With some modification by a file (considered normal), you can get a coruntine implementation that will help get rid of Promise hell.
Without the recoilphp / recoil project, using ReactPHP, in my opinion, is not possible in any sane application.
Also, among other things, it seems that its development has slowed down very much. Not enough, for example, normal operation with PostgreSQL.
In my opinion the best of the options that exist at the current time.
In addition to the usual Promise, there is an implementation of Coroutine, which greatly simplifies the development process and the code looks most familiar to PHP programmers.
Developers are constantly complementing and improving the project, with feedback there are also no problems.
Unfortunately, with all the advantages of the framework, the community is relatively small, but there are implementations, for example, working with PostgreSQL, as well as all the basic things (file system, http client, DNS, etc).
I still do not quite understand the fate of the ext-async project, but the guys keep up with it. What will come of this in version 3, time will tell.
So, we have a little dismantled the theoretical part, it's time to move on to practice and fill the bumps.
First, let's formalize the requirements a bit:
message
can be divided into 2 types)command
: indicates the need to perform a task. Does not return a result (at least in the case of asynchronous communication);event
: reports any state change (for example, as a result of a command).Any message is essentially a simple structure and is separated only by semantics. The naming of messages is extremely important from the point of view of understanding the type and purpose (although in the example this moment is ignored).
According to the list of requirements, a simple implementation of the Publish / Subscribe pattern is best suited.
To ensure distributed execution, we use RabbitMQ as the message broker.
The prototype was written using ReactPHP , Bunny, and DoctrineDBAL .
The attentive reader may have noticed that Dbal uses blocking calls pdo / mysqli inside, but at the current stage it was not particularly important, as it was necessary to understand what should happen in the end.
One of the problems was the lack of libraries to work with PostgreSQL. There are some sketches, but this is not enough to complete the work (see below).
After a brief survey, ReactPHP was removed in favor of Amp, since it is relatively simple and is developing very actively.
But with all the advantages of Amp there was 1 problem: there is no ready driver for RabbitMQ ( Bunny supports only ReactPHP).
In theory, Amp allows you to use Promise from a competitor. It would seem that everything should be simple, but the ReactPHP Event Loop is used to work with sockets in the library.
At one point in time, obviously, there cannot be two different Event Loops running, so I could not use the adapt () function.
Unfortunately, the quality of the code in bunny left much to be desired and it was not possible to adequately replace one implementation with another. In order not to stop the work, it was decided to rewrite the library a little so that it would work with Amp and not lead to blocking the flow of execution.
This adaptation looked very scary, all the time I was extremely ashamed of her, but most importantly - she worked. Well, since there is nothing more permanent than temporary, the adapter is waiting for a person who is not too lazy to implement the driver.
And such a person was found. The PHPinnacle project, among other things, provides an adapter implementation sharpened by using Amp.
The author's name is Anton Shabovt, who will talk about asynchronous php within PHP Russia and about developing drivers for PHP fwdays .
The second feature of the work is interaction with the database. Under the conditions of “traditional” PHP, everything is simple: we have a connection and all requests are executed sequentially.
In the case of asynchronous execution, we should be able to execute several requests at a time (for example, 3 transactions). In order to be able to do this, a connection pool is needed.
The mechanism of operation is quite simple:
Firstly, it allows us to start several transactions at one time, and, secondly, it speeds up work due to the presence of already open connections. Amp has a component amphp / postgres . It takes over work with connections: it monitors their number, lifetime and all this without blocking the flow of execution.
By the way, when using, for example, ReactPHP, you have to implement this yourself if you want to work with the database.
For effective and, most importantly, proper operation of the application, you need to implement something like mutexes. We can identify 3 scenarios for their use:
Mutexes are needed to solve race condition related problems. After all, we do not know (and cannot know) in which order our tasks will be performed, but nevertheless we must ensure the integrity of the data.
Monolog is used for logging, but with some reservations: we cannot use the built-in handlers, as they will lead to locks.
To write to stdOut, you can take amphp / log , or write a simple sending of messages to some Graylog.
Since at one point in time we can handle many tasks and when writing logs it is necessary to understand the context in which the data is written. During the experiments, it was decided to do trace_id
( Distributed tracing ). The bottom line is that the entire call chain must be accompanied by a pass-through identifier that can be tracked. Additionally, at the time of receiving the message, a package_id
generated, which indicates exactly the received message.
Thus, with the use of both identifiers, we can easily track to what a particular entry refers. The thing is that in traditional PHP all the records we get in the log mainly in the order in which they were recorded. In the case of asynchronous execution, there is no pattern in the order of records.
Another of the nuances of asynchronous development is control over the disabling of our daemon. If you just kill the process, then all the running tasks will not be completed, and the data will be lost. In the usual approach, there is also such a problem, but it is not so great, because only one task is being performed at the same time.
To complete the execution correctly, we need to:
Contrary to popular belief, in modern PHP it’s not so easy to face situations where a memory leak occurs. It is necessary to do something completely wrong.
However, once faced with this, but because of the banal inattention. During implementation, heartbeat made it so that every 40 seconds a new timer was added to poll the connection. It is not difficult to guess that after a while memory usage began to creep up and quite quickly.
Also, among other things, I wrote a simple watcher, which will optionally run every 10 minutes and call gc_collect_cycles () and gc_mem_caches () .
But the forced launch of the garbage collector is not something necessary and important.
In order to constantly see the memory usage, a standard MemoryUsageProcessor was added to the logging .
If the thought arises that the Event Loop is blocked by something, it can also be easily checked: just connect LoopBlockWatcher .
But you need to make sure that this observer does not start in the production environment. .
: 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
;delivery()
). , RabbitMQ .whenPong
— Pong
. . @EventListener
;, — . , , , . php-service-bus , , .
2 : , ( ) . , , (, ).
Ping
, Pong
. .
, RabbitMQ:
tools/ping
, php-service-bus , Message based .
Ping\Pong, — , , Hello, world
.
- , , , Saga pattern (Process manager) .
, , .
Source: https://habr.com/ru/post/451916/
All Articles