Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/jeremiah-shaulov/php-web-node
PHP-FPM implementation that allows to preserve resources (global variables) between requests, so DB connections pool is possible
https://github.com/jeremiah-shaulov/php-web-node
Last synced: about 2 months ago
JSON representation
PHP-FPM implementation that allows to preserve resources (global variables) between requests, so DB connections pool is possible
- Host: GitHub
- URL: https://github.com/jeremiah-shaulov/php-web-node
- Owner: jeremiah-shaulov
- License: mit
- Created: 2020-07-08T01:56:25.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2020-07-26T01:05:07.000Z (over 4 years ago)
- Last Synced: 2024-09-07T01:56:46.802Z (4 months ago)
- Language: PHP
- Homepage:
- Size: 177 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# php-web-node
PhpWebNode is PHP-FPM implementation written in PHP that allows to preserve resources (global variables) between requests, so database connections pool is possible. Applications based on this library can be used instead of PHP-FPM. PhpWebNode acts like FastCGI server, to which web server (like Apache) will send HTTP requests. It will spawn child processes that will handle the requests synchronously. Several (up to 'pm.max_children') children will run in parallel, and each child will sequentially process incoming requests, preserving global (and static) variables.## How fast is php-web-node
It's written in PHP, so i expected that my application will slow down a little comparing to PHP-FPM. I was surprised that the application became a little faster. Actually php-web-node removes need to reinitialize resources, and reconnect to database.
## What's supported
Most of PHP features that i know, except `$_SESSION` are supported. Most existing PHP scripts will work as with PHP-FPM, except principal distinction explained below, in "Step 3. Update PHP scripts".
PhpWebNode implements complete FastCGI protocol, including connection multiplexing, however as far as i know, currently none of popular web servers support this feature. [More info](https://stackoverflow.com/questions/25556168/nginx-fastcgi-uses-management-records-if-not-then-what).
## Installation
Create a directory for your application, `cd` to it, and issue:
```
composer require jeremiah-shaulov/php-web-node
```## How to use php-web-node
To use php-web-node we need to pass 3 steps:
1. Create master application
2. Set up web server to use our application
3. Update PHP scripts that we want to serve with php-web-node### Step 1. Create master application
We need a master application that will work as PHP-FPM service. Let's call it server.php:
```php
'/run/php-web-node/main.sock', // or: '127.0.0.1:10000', '[::1]:10000'
'listen.owner' => 'www-data',
'listen.group' => 'johnny',
'listen.mode' => 0700,
'listen.backlog' => 0,
'user' => 'johnny',
'group' => null,
'pm.max_requests' => 1000,
'pm.max_children' => 2,
'pm.process_idle_timeout' => 30,
'request_terminate_timeout' => 10,
]
);
$server->serve();
```The `Server` constructor takes array with configuration parameters. Their meaning is the same as in PHP-FPM, see [here](https://www.php.net/manual/en/install.fpm.configuration.php). Only parameters shown in the above example are supported.
Change `johnny` to your user name, or create dedicated user to run the application from it.
Now we need to start this script from console.
```
sudo php server.php
```This script requires superuser rights, because it's going to create 'listen' socket, and it's parent directories. Then it switches user to one specified in the given configuration.
If we want to daemonize this service, we can either implement daemonization in the script, or we can use external software. For example in Ubuntu we can:
```
sudo daemon --name=php-web-node --respawn --stdout=/tmp/php-web-node.log --stderr=/tmp/php-web-node-err.log -- php server.php
```### Step 2. Set up web server
If you have a web server like Apache or Nginx that is already configured to work with PHP-FPM, you only need to change the socket node name (or port) to that used in our server.php. In the example above it's `/run/php-web-node/main.sock`. Here's the simplest Apache example:
```
ServerName wntest.com
DocumentRoot /var/www/wntest.com
SetHandler "proxy:unix:/run/php-web-node/main.sock|fcgi://localhost"
# or IPv4: SetHandler "proxy:fcgi://127.0.0.1:10000"
# or IPv6: SetHandler "proxy:fcgi://[::1]:10000"
```
The DocumentRoot directory must exist, so create `/var/www/wntest.com`, or different directory on your choice, and put some test file in it, like `index.php`. Later you will be able to access it through `ServerName` URL. In example above, it's `wntest.com`, so the full URL will be `http://wntest.com/index.php`, or maybe `http://wntest.com/`. Also include `wntest.com` in your `/etc/hosts` file, like:
```
127.0.0.1 wntest.com
```### Step 3. Update PHP scripts
There's important difference between PHP-FPM and php-web-node. Let's say we have such `index.php` script:
```php
'/run/php-web-node/main.sock',
'listen.owner' => 'www-data',
'listen.group' => 'johnny',
'listen.mode' => 0700,
'listen.backlog' => 0,
'user' => 'johnny',
'group' => null,
'pm.max_requests' => 1000,
'pm.max_children' => 10,
'pm.process_idle_timeout' => 30,
'request_terminate_timeout' => 10,
]
);
$server->onerror
( function($msg)
{ echo $msg, "\n";
}
);
$server->onrequestcomplete
( function($pool_id, $messages, $time_took) use(&$n_requests, &$requests_time_took)
{ $n_requests++;
$requests_time_took += $time_took;
}
);
$server->set_interval
( function() use(&$n_requests, &$requests_time_took)
{ echo "$n_requests requests", ($n_requests==0 ? "" : " (".round($requests_time_took/$n_requests, 3)." sec avg)"), "\n";
$n_requests = 0;
$requests_time_took = 0.0;
},
6
);
$server->serve();
```The `$server->serve()` function starts the server main loop, that runs forever, so this function doesn't return or throw exceptions.
This application prints each 6 sec (10 times a minute) how many requests were completed, and average request time. This time is measured since a child process took a request job, and till a complete response was received from the child. Real request time is longer.
## Process pools
As we already mentioned, php-web-node manages child processes just like PHP-FPM, and each child process has it's own persistant resources. An example of resource is a database connection. So a PDO object stored in a global variable will not be reinitialized. If we set 'pm.max_children' to 10, we will get database connections pool with up to 10 slots.
What if we want that half of HTTP requests connect to one database, and a half to another? By default HTTP requests will be directed to random child processes, so each child will sometimes process requests that connect to database A, and sometimes to database B. So we will get 20 persistent connections from 10 child processes to 2 database servers.
PhpWebNode allows us to examine each incoming HTTP request in master application before it's directed to a child process, and the master application can choose to what group of children to direct it. Each group of child processes is called a process pool. We can have as many process pools as we want, and as many child processes in each pool as we want.
By default there's only 1 pool called `''` (empty string). The 'pm.max_children' setting is maximal number of processes in each pool.
To catch and examine incoming HTTP requests we can set `$server->onrequest()` callback.
```php
public function onrequest(callable $onrequest_func=null, int $catch_input_limit=0)
````$onrequest_func` callback receives a `Request` object, that has the following fields:
- `$request->server` - the `$_SERVER` of the request.
- `$request->get` - the `$_GET` of the request.
- `$request->post` - the `$_POST` of the request. PhpWebNode will read and buffer not less than `$catch_input_limit` bytes of request POST body before calling `$onrequest_func`. If the body is longer, `$request->post` will contain only complete parameters read so far. If Content-Type of the body is not one of `application/x-www-form-urlencoded` or `multipart/form-data`, the `$request->post` will be empty array.
- `$request->input` - the `file_get_contents('php://input')` of the request. If POST body was longer than `$catch_input_limit`, it will be incomplete. If Content-Type was `multipart/form-data`, this will be empty string.
- `$request->input_complete` - true if `$request->input` is the complete POST body.
- `$request->content_type` - lowercased substring of $_SERVER['CONTENT_TYPE'] of the request before first semicolon.`$onrequest_func` allows you to decide to which pool to forward the incoming HTTP request by returning pool Id or name (string).
```php
'/run/php-web-node/main.sock',
'listen.owner' => 'www-data',
'listen.group' => 'johnny',
'listen.mode' => 0700,
'listen.backlog' => 0,
'user' => 'johnny',
'group' => null,
'pm.max_requests' => 1000,
'pm.max_children' => 5,
'pm.process_idle_timeout' => 30,
'request_terminate_timeout' => 10,
]
);
$server->onrequest
( function(Request $request)
{ $db_id = $request->get['db-id'] ?? null;
return $db_id=='a' ? 'A' : 'B';
},
8*1024
);
$server->serve();
```It's important to return only fixed number of value alternatives. In the example above we return 2 alternatives: 'A' and 'B', so we will get 2 pools with 5 ('pm.max_children') processes in each pool. If you cease returning some value from `$onrequest_func` callback, the corresponding pool will be eventually freed.
Child process can check it's pool Id by calling `PhpWebNode\get_pool_id()`.
Another thing that `$onrequest_func` callback can do, is throwing exception to cancel the request without forwarding it to a child process.
```php
$server->onrequest
( function(Request $request)
{ if (empty($request->get['page-id']))
{ http_response_code(404);
PhpWebNode\header('Expires: '.gmdate('r', time()+60*60));
throw new Exception("Page doesn't exist"); // cancel the request
}
}
);
```## PHP MySQL connections pool implementation
As we saw above, process pool can act like database connections pool.
Please, keep in mind, that there's no way in PHP to reset a MySQL connection using [mysql_reset_connection()](https://dev.mysql.com/doc/refman/8.0/en/mysql-reset-connection.html), at least i'm not aware of such. Therefore in the beginning of each request we'll need to clean up what we can, and we can rollback ongoing transaction if it was not committed by previous request.
Also you need to know that depending on what queries you execute, memory consumption on MySQL end can decline with every query. Eventually this can make MySQL server unresponsive. So there's limit on how many times we can reuse our connection, and we need to reconnect periodically anyway. Even reusing 10 times each connection, will dramatically release network pressure in your system.
In my experiments, with PHP-FPM i saw 2500 open sockets all the time, where almost all of them were in TIME_WAIT state.
```
sudo netstat -putnw | wc -l
```With php-web-node reusing each connection 10 times, this number reduced to 700.
Example of client script that implements DB connections pool:
```php
exec("ROLLBACK");
}return $pdo;
}PhpWebNode\set_request_handler
( __FILE__,
function()
{ $pdo = get_pdo();
$cid = $pdo->query("SELECT Connection_id()")->fetchColumn();
echo "Connection ID = $cid";
}
);
```And as usual, in master application we specify pool parameters:
```php
'/run/php-web-node/main.sock',
'listen.owner' => 'www-data',
'listen.group' => 'johnny',
'listen.mode' => 0700,
'listen.backlog' => 0,
'user' => 'johnny',
'group' => null,
'pm.max_requests' => 1000,
'pm.max_children' => 5,
'pm.process_idle_timeout' => 30,
'request_terminate_timeout' => 10,
]
);
$server->serve();
```The pool will have up to `pm.max_children` concurrent database connections. If one of them remains idle for more than `pm.process_idle_timeout` seconds, it will be closed. Each `pm.max_requests` requests child process will be retired, so we could set `pm.max_requests` to 10, and database connection would reconnect each 10 requests, but it's better to set `pm.max_requests` to a big value because this will save CPU spent on stopping child process, and forking it again.