📜 ⬆️ ⬇️

FastCGI application in Perl. Part Three

In a previous article , we demonstrated how to demonize the FastCGI application. The resulting daemon successfully processes requests, but it has one major drawback - it does not know how to process several requests at the same time.

Moreover, in terms of handling several simultaneous requests, the situation with the daemon is even worse than with a regular CGI application. If the CGI application can be started by the web server in the required number of instances (for example, one instance for each incoming request), then the daemon running in one single instance will be forced to put the received requests in a queue. Until the previous request is completed, all other requests will have to wait.

In order for a FastCGI application to service several requests at the same time, it must be able to create its own copies. In this case, simultaneously received requests will be processed in parallel by several instances of the application.
')
How to make copies?

In fact, making copies of the process is not a tricky business. Managing a host of created copies is the task. We will transform our demon into agent Smith using the FCGI :: ProcManager module .

The FCGI :: ProcManager module performs three main tasks:

1) Creates working copies of the daemon - handlers or workers (or servers, in the terminology of the module itself)
2) Controls the status of handlers in the process.
3) Controls the behavior of handlers in the case of external intervention.

In addition to the handler processes, FCGI :: ProcManager launches another process manager. The process manager does not serve client requests; its task is to manage handlers.

Before embedding parallelization, I want to draw your attention to what point. In the previous article it was said that we would need to divide the process of demonization into two parts; otherwise, we may have an unpleasant surprise. Now I will explain what the salt is.

Consider a section of code from the previous article (in abbreviated form):

 # Demonization {
     ... here I have cut ...

     POSIX :: setuid (65534) or die "Can't set uid: $!";

     reopen_std ();
 #}

 my $ socket = FCGI :: OpenSocket (": 9000", 5);

 my $ request = FCGI :: Request (\ * STDIN, \ * STDOUT, \ * STDERR, \% ENV, $ socket);

 while ($ request-> Accept ()> = 0) {

The reopen_std command breaks the connection between the standard descriptors and the console. This means, in particular, that all error messages that may occur after executing this command (for example, in the OpenSocket function) will be sent to nowhere and the application will simply die in silence. This will be the most unpleasant surprise of which I spoke - it seems that the application has started up normally, but it does not suddenly appear in the list of processes.

The situation will get worse after paralleling is done. The handler that caused the error will be killed, but in its place the manager will immediately launch another one. There will again be an error in him, he will be killed again and so on in a circle. Outwardly, all this will look quite harmless - the daemon started, did not output any errors, the list of processes clearly shows the presence of a specified number of handlers. However, the daemon is not responding to requests and, even worse, after a while you suddenly notice that the system has become mercilessly slowed down, the processor is 100% busy and the load average is inexorably increasing.

The system is a surprise! - will be busy scheduling continuously and at a frantic speed of dying and newly launched processes. It will take a lot of care to notice that the pids of the handlers are constantly changing, figure out what this means and take action.

To avoid such a surprise, the call to the reopen_std function should be separated from the rest of the code from the demonization block. You must place a call to this function immediately before the request processing cycle.

Take all the same section of code and make changes:

 # Demonization {
     ... here I have cut ...

     POSIX :: setuid (65534) or die "Can't set uid: $!";
 #}

 my $ socket = FCGI :: OpenSocket (": 9000", 5);

 my $ request = FCGI :: Request (\ * STDIN, \ * STDOUT, \ * STDERR, \% ENV, $ socket);

 # Demonization {
     reopen_std ();
 #}

 while ($ request-> Accept ()> = 0) {

As you can see, at the same time, the OpenSocket and Request commands will turn out to be “inside” the demonization process. In other words, the demonization process will be divided into two parts, which would be impossible if we used the finished module for demonization.

Now, any error that occurred before the reopen_std command was issued will be displayed on the console. Accordingly, we can immediately see that something goes wrong.

Well, now let's take the daemon code from the previous article and embed paralleling in it:

 #! / usr / bin / perl

 # To heighten the order
 use strict;
 use warnings;

 # This module implements the FastCGI protocol
 use FCGI;

 # This module is for talking with concepts on the operating system :)
 use POSIX;

 # Parallelization {
     # This module provides parallel query processing.
     use FCGI :: ProcManager qw (pm_manage pm_pre_dispatch pm_post_dispatch);
 #}

 # Fork
 # getting rid of the parent
 fork_proc () && exit 0;

 # Start a new session
 # our demon will be the ancestor of the new session
 POSIX :: setsid () or die "Can't set sid: $!";

 # Go to root directory
 # so as not to interfere with unmounting a file system
 chdir '/' or die "Can't chdir: $!";

 # Change user to nobody
 # we are paranoid, huh?
 POSIX :: setuid (65534) or die "Can't set uid: $!";

 # Open the socket
 # our demon will listen to port 9000
 # query queue length - 5 pieces
 my $ socket = FCGI :: OpenSocket (": 9000", 5);

 # Getting to listen
 # daemon will intercept standard handles
 my $ request = FCGI :: Request (\ * STDIN, \ * STDOUT, \ * STDERR, \% ENV, $ socket);

 # Parallelization
     # Run handlers
     # the specified number of handlers will be launched (in this case 2)
     pm_manage (n_processes => 2);
 #}

 # Specificity {
     # There should be a code specific to each specific processor
     # for example, opening a connection to the base
 #}

 # Reopen standard handles on / dev / null
 # no longer talk to user
 reopen_std ();

 my $ count = 1;

 # Endless cycle
 # for each accepted request, one cycle is performed.
 while ($ request-> Accept ()> = 0) {
     # Parallelization
         # Managing handlers
         # reacts to external interference
         pm_pre_dispatch ();
     #}
   
     # Inside the loop, all the required actions are performed
     print "Content-Type: text / plain \ r \ n \ r \ n";
     print "$$:". $ count ++;

     # Parallelization
         # Managing handlers
         # reacts to external interference
         pm_post_dispatch ();
     #}
 };

 # Fork
 sub fork_proc {
     my $ pid;
   
     FORK: {
         if (defined ($ pid = fork)) {
             return $ pid;
         }
         elsif ($! = ~ / No more process /) {
             sleep 5;
             redo FORK;
         }
         else {
             die "Can't fork: $!";
         };
     };
 };

 # Reopen standard handles on / dev / null
 sub reopen_std {   
     open (STDIN, "+> / dev / null") or die "Can't open STDIN: $!";
     open (STDOUT, "+> & STDIN") or die "Can't open STDOUT: $!";
     open (STDERR, "+> & STDIN") or die "Can't open STDERR: $!";
 };

What are some of the characteristics?

First of all, pay attention to the location of the pm_manage command.

On the one hand, commands common to all FastCGI applications (such as creating a socket and starting a wiretap) must be executed BEFORE running handlers. You cannot start handlers, and then it will bind on one socket by several processes, this will lead to an error.

On the other hand, commands specific to each specific handler (such as opening a connection to the database) should be placed AFTER the handlers are started. You can not create a connection to the database, and then share it by process, this will lead to unnecessary problems.

Well and, once again I remind you, the reopen_std command must be located after all the preparatory commands, immediately before the start of the cycle.

The loop should begin and end with the pm_pre_dispatch and pm_post_dispatch commands, respectively. These two commands control the behavior of handlers in the event of external intervention. External intervention means receiving a signal by the FastCGI application, for example, from the kill command. Without them, handlers will not respond to signals in the right way.

Run the demon. When started, the daemon will output the following to the console:

# ./test.pl
FastCGI: manager (pid 1858): initialized
FastCGI: manager (pid 1858): server (pid 1859) started
FastCGI: server (pid 1859): initialized
FastCGI: manager (pid 1858): server (pid 1860) started
FastCGI: server (pid 1860): initialized

Here we see messages that the process manager (pid 1858) and two handlers (pid'y 1859 and 1860) were launched.

Let's see the list of processes:

# ps -aux | grep perl
nobody 1858 0.0 0.2 5852 3816 ?? Is 21:09 0: 00,00 perl-fcgi-pm (perl5.8.8)
nobody 1859 0.0 0.2 5852 3848 ?? I 21:09 0: 00,00 perl-fcgi (perl5.8.8)
nobody 1860 0.0 0.2 5852 3848 ?? I 21:09 0: 00,00 perl-fcgi (perl5.8.8)

Here we see that the process manager differs from handlers with the simple suffix "pm".

To control a FastCGI application, signals must be sent to a process manager, not handlers. The process manager resolves two signals, HUP and TERM. He does it like this:

When a HUP signal is received, the process manager sends a TERM signal to all handlers. Handlers die, process manager launches new ones. Thus, the solution of problems with hung handlers.

When a TERM signal is received, the process manager sends a TERM signal to all handlers, waits for them to die, then dies itself. If the handlers do not wish to die voluntarily, the process manager sends them a KILL signal, from which they can no longer get out.

An interesting point: in both cases, the handlers do not die immediately, allowing the processed requests to be processed to the end. Having received the TERM signal, the handler completes the useful work, gives the client an answer, and only then dies.

This behavior can be changed by adding another parameter to the Request function call - FCGI :: FAIL_ACCEPT_ON_INTR (this is a constant exported by the FCGI module):

my $ request = FCGI :: Request (\ * STDIN, \ * STDOUT, \ * STDERR, \% ENV, $ socket, FCGI :: FAIL_ACCEPT_ON_INTR);

After adding this parameter, handlers will die immediately as soon as the signal was received. This parameter is convenient to use at the debugging stage, in order not to wait until all the handlers finish their work.

That's where the Perl FastCGI application creation trilogy is over :)

( original article )

Source: https://habr.com/ru/post/53540/


All Articles