Thursday, August 6, 2020

Nginx Extra Security & Performance Tuning Example


Advanced configuration for Nginx

There are some small changes you can make to make your website faster, and a little more secure.

Security enhancements

The original configuration will keep you safe, but it is always good to see what else can be done.

Giving less information to attackers

By default, the nginx.conf will return "403 Forbidden" errors for system files that are not supposed to be accessed directly. However, there's a good alternative, like so:

server {
    ...

    ## Begin - Index
    # for subfolders, simply adjust:
    # `location /subfolder {`
    # and the rewrite to use `/subfolder/index.php`
    location / {
        try_files $uri $uri/ @index;
    }

    location @index {
        try_files = /index.php?_url=$uri&$query_string;
    }
    ## End - Index

    ## Begin - Security
    # set error handler for these to the @index location
    error_page 418 = @index;
    # deny all direct access for these folders
    location ~* /(\.git|cache|bin|logs|backup|tests)/.*$ { return 418; }
    # deny running scripts inside core system folders
    location ~* /(system|vendor)/.*\.(txt|xml|md|html|yaml|yml|php|pl|py|cgi|twig|sh|bat)$ { return 418; }
    # deny running scripts inside user folder
    location ~* /user/.*\.(txt|md|yaml|yml|php|pl|py|cgi|twig|sh|bat)$ { return 418; }
    # deny access to specific files in the root folder
    location ~ /(LICENSE\.txt|composer\.lock|composer\.json|nginx\.conf|web\.config|htaccess\.txt|\.htaccess) { return 418; }
    ## End - Security
    ...
}

What happens here is the following:

  • Try and see if the file exists on disk, and if not, give the request to the @index location.
  • The new @index location will reroute requests to the /index.php as usual.
  • Instead of returning a "403 Forbidden" error, we now return a 418.
  • Because we set the error_page 418, any 418 will be handled by the @index location.
  • Grav's /index.php will pick up the route it is given, determine if there's a matching route,
    and if not, simply return a 404 by itself.

Normally, we route all non-existing files to Grav. However, returning any status code from nginx itself, will give a different kind of error than if it had been routed through Grav. That gives the attacker the information that those files are special and actually exist. More than that, that you explicitly don't want them to try and read those. They will try harder.

This is better, because if you reroute it to Grav, Grav will handle it like any other non-existing file.

No direct access to other .php-files

In the example nginx.conf, all requests to files ending in .php are sent to the PHP-handler. This is not necessary, as Grav only uses the /index.php to route requests. Every other location is handled internally.

Some vulnerabilities in CMS's are targeted specifically at plug-ins, themes or other third-party libraries. There is no reason to keep direct access to them open (unless you run Grav combined with other pieces of software in the same webroot!).

So, alternatively, only route /index.php to the PHP-handler, and block the rest! Like so:

server {
    ...
    ## Begin - PHP
    location = /index.php {
        # Choose either a socket or TCP/IP address
        fastcgi_pass unix:/var/run/php7.0-fpm.sock;
        # fastcgi_pass 127.0.0.1:9000;

        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
    ## End - PHP

    ## Begin - Security
    ...
    # deny access to other .php-scripts
    location ~ \.php$ { return 418; }
    ...
    ## End - Security
    ...
}

With this, any .php-file that is not /index.php, will be rerouted to and be handled by /index.php.

Performance

nginx is a very capable webserver, but it is also very capable of doing advanced caching.

Do not check for the existence of directories

Many, many examples use this line:

try_files $uri $uri/ @index;

Few people actually realise that it does, which is:

  1. Check for the existence of the file.
  2. If it does not exist, try and see if there's a directory with that name.
  3. Otherwise, fall back to @index (/index.php).

But, in reality, you only really use $uri/ (step 2) if you really are linking to a directory that will have its own index.php or index.html. Considering you're using Grav, you will only go to /, and that already goes to /index.php in step 3.

If you're not going to use it, don't keep it around, and you will take out an extra filesystem stat():

try_files $uri @index;

Caching filesystem metadata of files

The open file cache caches metadata about files. If they exist or not, what file permissions they have, if they are readable or not.. This can help a little on local filesystems, especially that are not on SSD. It shines more in environments with network storage (like NFS).

Keep in mind that this goes outside of the server{}-block, directly into the http{}-context:

open_file_cache                 max=10000 inactive=5m;
open_file_cache_valid           1m;
open_file_cache_min_uses        1;
open_file_cache_errors          on;

server {
    ...
}

In this example, we told nginx to:

  • Keep a maximum of 10k entries.
  • Delete metadata of a file from the cache if they it is not used for 5 minutes.
  • Refresh the metadata it has every minute.
  • Put the metadata of a file in the cache immediately upon accessing it the first time.
  • Cache errors like "Permission denied", "Not found", as well.

Precompressing resources

In the example nginx.conf, we enable GZip-compression. While excellent, it also means that the output is compressed on a per-request basis. This in turn means that with every request, it adds extra CPU-cycles and latency (you have to wait or the compression to be done).

There is an alternative in nginx, which is gzip_staticgzip_static will make nginx look for the same file, but with a .gz extension. So if I have main.css, it will try and see if there is already a main.css.gz present, and send that instead.

To enable this, use:

server {
    ...

    ## Begin - Index
    ...

    location / {
        try_files $uri @index;

        location /assets {
            gzip_static on;
        }
    }

    ...
    ## End - Index
    ...
}

By using a nested location /assets, you will not incur extra filesystem stat()s for the rest of Grav.
If enabled, the /assets location will contain pipelined/minified assets.

!! nginx does not automatically compress the files for you. You will have to do this yourself.

To compress the files (on UNIX-based systems), you can do the following:

cd assets
for asset in *.css *.js; do gzip -kN9 "$asset"; done

Note that these are automatically deleted when you clear your (asset) cache, and you will have to redo it after new resource files are created. There is an outstanding Pull Request to allow automatic precompression of assets upon creation of these files.

Enable FastCGI caching.

** DO NOT USE THIS KIND OF CACHING IF YOU HAVE DYNAMIC PAGE CONTENT **

The following example is safe to use with authentication, and the Admin interface. It is also safe for dynamic page content, but your dynamic content will not be dynamic anymore (as it is aggressively statically cached).

nginx has caching for FastCGI. In the example below, we will leverage this. Note that if anything on the site sets a cookie or similar dynamic content headers, the cache is invalid and will not be used at all.

First, we are going to need to make a map{} in the main http{}-context (so outside/before the server{}-block) to convert our optional session cookie into a unique identifier for the cache (so users with different sessions do not share the same cached resource, all of them will have a unique copy, but cached for their own session):

# This is to have caching enabled when site sessions are turned on.
# It makes FastCGI caching safe to use with authenticated content.
map $http_cookie $sessionkey {
    default '';
    ~grav-site-(?<hash>[0-9a-f]+)=(?<sessionid>[^\;]+) $hash$sessionid;
}

server {
    ...
}

If you renamed your site session name (setting session.name) in your Grav config, update its name in the example above! The regular expression will combine the unique identifier in the cookie name with the session id in the cookie into a new variable $sessionkey, which we can later use in the fastcgi_cache_key setting. Next, define a cache zone in the same context right under it:

fastcgi_cache_path      /path/to/cache/on/disk          levels=1:2
                        keys_zone=fastcgi:10m           max_size=200m
                        inactive=60m                    use_temp_path=off;

You should change the path of where the cache is stored on disk, but what it does is:

  1. Set the path. You can use this to cache on disks that might be faster. (Tip: Use /dev/shm/something on Linux systems to use a RAM-disk instead!).
  2. Define the directory hierarchy structure of 1 character, then 2 characters. (i.e. /path/to/cache/on/disk/c/29/...)
  3. Give the cache zone a name ("fastcgi" here) and an initial size (10 MB).
  4. Allow a maximum of 200 MB to be cached.
  5. Delete a resource from the cache if it hasn't been used for 60 minutes.
  6. Do not base the cache path off of fastcgi_temp_path.

Next, you can use this cache zone in your configuration:

server {
    ...
    ## Begin - PHP
    location = /index.php {
        ## Begin - FastCGI caching
        fastcgi_cache           fastcgi;
        fastcgi_cache_key       "$scheme$request_method$host$request_uri$sessionkey";
        fastcgi_cache_valid     200 30m;
        fastcgi_cache_valid     404 5m;
        fastcgi_cache_valid     any 1m;
        fastcgi_ignore_headers  "Cache-Control"
                                "Expires"
                                "Set-Cookie";

        fastcgi_cache_use_stale error
                                timeout
                                updating
                                http_429
                                http_500
                                http_503;

        fastcgi_cache_background_update on;
        ## End - FastCGI caching

        # Choose either a socket or TCP/IP address
        fastcgi_pass unix:/var/run/php7.0-fpm.sock;
        # fastcgi_pass 127.0.0.1:9000;

        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
    ## End - PHP
}

Now, what is happening here, is the following:

  • The cache zone named "fastcgi" will be used for this (we defined that earlier).
  • The cache key to lookup a cached resource is set to something like "httpsGETwww.example.com/".
  • Cache times:
    • 200 "OK"-responses: 30 minutes.
    • 404 "Not Found"-responses: 5 minutes.
    • Any other status code: 1 minute.
  • Ignore the Cache-ControlExpires and Set-Cookie headers (since we're in control here).
  • Keep serving our cache when the following happens:
    • There is an error.
    • There is a timeout.
    • nginx is busy updating the cache.
    • There is a 429 Too Many Requests on the backend (backend is too busy).
    • There is a 500 Internal Server Error on the backend (backend has a misconfiguration).
    • There is a 503 Gateway Timeout to the backend (backend has other problems).
  • And last but not least: Upgrade the cache in the background, don't make our clients wait.

Please note, once again, that this will not (and should not) work if you're using the Admin panel, or use any other sessions on your site. It is not recommended to use this caching with any authenticated content, for security reasons.

If you wish to test if your cache is working, you can add a simple header (remove on production):

add_header X-Cache "$upstream_cache_status - $scheme$request_method$host$request_uri$sessionkey";

You can view the header with cURL (or any other tool):

$ curl -I https://www.example.com/
...
X-Cache: MISS - httpsHEADexample.com/en - 7b2fdab20bb2fbi85l61eounburtlxavo
...
$ curl -I https://www.example.com/
...
X-Cache: HIT - httpsHEADexample.com/en - 7b2fdab20bb2fbi85l61eounburtlxavo
...
$

Again, you might want to disable the header after confirming it works.

If you feel the need to purge your cache entirely:

  1. Stop nginx.
  2. Remove the directory.
  3. Start ngnix.

The cache is automatically updated and pruned in the background, so you shouldn't need to do so.

More fine-grained control over (not) caching

If you use the admin interface, you might want to disable caching globally once the admin cookie has been set. Note that an admin cookie is set when you go to the /admin URL, and that anyone can go there by default. You don't need to login to disable caching.

You can use the following example as a basis. Put the map{}s outside your server{} and the fastcgi_*-directives with the rest of the caching directives:

# This is used by fastcgi_cache_bypass and fastcgi_no_cache.
# If you don't want certain URI's cached, add them here with a value of 1.
map $request_uri $no_cache1 {
        default                 0;
        ~^/(../|)admin          1;
}

# This is used by fastcgi_cache_bypass and fastcgi_no_cache.
# To disable caching based on cookie names, add them here with a value of 1.
map $http_cookie $no_cache2 {
    default 0;
    ~grav-site-([0-9a-f]+)-admin=([^\;]+) 1;
}

server {
    ...
    location = /index.php {
        ...
        fastcgi_cache_bypass $no_cache1 $no_cache2;
        fastcgi_no_cache     $no_cache1 $no_cache2;
        ...
    }
}


Nginx Block User Agent Configuration


Try adding something like the following directives to your config to block user agent:

# Disallow User Agent
if ($http_user_agent ~* "agent1|Cheesebot|Catall Spider|LWP::Simple|BBBike|wget|libwww-perl|python|nikto|curl|scan|java|winhttp|HTTrack|clshttp|archiver|loader|email|harvest|extract|grab|miner" ) {
	return 404;
}



HTTP Flood & DDoS Filter via Nginx


Try adding something like the following directives to your nginx config to prevent HTTP Flooding & Distributed Denial of Service (DDoS) :

http {
	limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
	limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=5r/s;

	server {
		limit_conn conn_limit_per_ip 10;
		limit_req zone=req_limit_per_ip burst=10 nodelay;
	}
}



NOTE: http://www.botsvsbrowsers.com/details/504401/index.html says the above user agent is not a known bot



Saturday, December 7, 2019

FCGI (FastCGI) PHP Apache OPcache



FCGI Overview

FCGI is one of many PHP handlers that Apache can use to interpret PHP code. The handlers are responsible for reading / interpreting PHP code then compiling it and executing the code. Selecting the best PHP handler for your server is critical to overall performance.
Once mod_fcgid is enabled, all PHP processing is done via the FastCGI protocol. mod_fcgid starts up a certain amount of persistent processes that listen for new PHP requests and process them when needed. These processes remain alive even if there are no PHP requests, so you must make sure you configure FCGID to run an appropriate amount of processes, otherwise the server might waste a lot of resources on idle PHP processes. Generally speaking, you should keep a few (2 or 3) idle FCGID processes running to quickly respond to new requests, that way you limit idle resource usage, but still allow for FCGID to spawn more processes if traffic picks up. To determine the maximum FCGID processes you will want to determine how much Memory the average PHP process uses, then divide the available server RAM by this number.
There is no benefit in setting the max process limit too high, otherwise you will risk server instability, especially if it starts to utilize swap space due to lack of RAM. Generally 20 or 30 max FCGID processes is a good place to start.

FCGI Versus suPHP Benchmarks

As you can see from the results. FCGI is generally much faster than suPHP, especially with OPcache on. Notice how suPHP performs worse with opcode caching enabled, mainly because suPHP processes are not persistent, so there is no point in caching something that will never get re-used. DO NOT USE APC or OPCACHE with SUPHP. Only use opcode caching with FCGI.
Test Results below were gathered using a LiquidWeb's 1GB SSD VPS
FCGI SUPHP OPCACHE BENCHMARKS.png

How to enable FCGI PHP handler on cPanel

The first thing to check is whether or not FCGI is listed as an available PHP handler. Run this command to see what the current handler is, and what handlers are available.
/usr/local/cpanel/bin/rebuild_phpconf --current

Available handlers: fcgi cgi none
If you do not see fcgi listed then you will need to run EasyApache and select Mod FastCGI. I also recommend upgrading PHP to at least 5.5 and making sure that Apache is updated to at least 2.4 with the EVENT MPM enabled.
/scripts/easyapache
EasyApache normally takes about 10 minutes or less to finish rebuilding the configuration, once this is done you should be able to log into WHM search for "PHP" and then click on "Configure PHP and suEXEC" and select FCGI as the handler, then save the config just like the image below.
WHM FCGI PHP HANDLER.png

mod_fcgid cpanel file locations

When you activate FCGI as the PHP handler on a cPanel server, there will be a new ifmodule placed in this includes file
vim /usr/local/apache/conf/includes/pre_virtualhost_global.conf
By default, cPanel applies these settings. Obviously this is not ideal, so use the configuration settings listed in the sections below to properly configure and [optimize] fcgi.
<IfModule mod_fcgid.c>
    FcgidMaxRequestLen 1073741824
</IfModule>

FCGI Considerations for Shared Servers

One thing to keep in mind if you plan on using FCGI on a server that has say, 21 + cPanel accounts on it. If the server has 1GB of RAM, you might set FcgidMaxProcesses to 20 to limit memory usage and prevent too many requests from locking up the server due to too much swap usage, or overloading your CPU.
This is all fine in most cases, but what happens if 20 of the sites already has a PHP process running as their own user, and someone tries to view website #21? Nothing happens, the website will hang and try and load for a long time, waiting for one of the occupied 20 FCGI processes to die off and be reborn again so it can serve user #21. In some cases this take be a minute or more. Obviously this is not a good thing, customer #21 is going to be mad their site doesn't load. 
Why does this happen? Because of the way FCGI handles processes and what to do with them once they are done handling requests. For a single website, the ideal FCGI configuration would be to create 10 processes, or however many you want, then set their life time to a large number, like 600 seconds or more. You would do this because of opcode caching, it's best to reuse cached PHP as much as possible to reduce CPU load.
For a shared server though, long running PHP processes are not ideal unless you have enough memory to give every user a single FCGI process, but if you have limited memory and 1000 cpanel accounts, that's not going to be possible since 1000 processes using 32MB each is probably more than 2GB or whatever small amount of memory your server might have.
If you have a server with limited memory and lots of cpanel accounts and you can't set FcgidMaxProcesses to a value higher than the # of users, then your only option is to significantly reduce the timeouts, lifetimes and increase scan intervals to kill off idle processes faster, and create new processes sooner.
While I can't say this would be the best configuration, I will say that it will reduce the chances of a "spinning website" or one that takes 30 seconds to load. By using 5 second intervals the worst case would be a 5 second delay, which sucks, a lot, but it's better than 30 seconds or more. For this to happen, 21 people all over the world would need visit 20 sites on your server within the same 5 seconds, then one other person would need to visit site #21 to run into the issue I described above, but it can happen.
Anyway, if this sounds like the problem you are having, try to use very low idle timeout and process lifetimes, this may reduce performance a but, but FCGI is still WAAAAY better than SuPHP.
<IfModule mod_fcgid.c>
    FcgidMaxRequestLen 1073741824
    FcgidMinProcessesPerClass 0
    FcgidMaxProcesses 20
    FcgidIdleTimeout 3
    FcgidProcessLifeTime 30
    FcgidIdleScanInterval 0
    FcgidErrorScanInterval 0
    FcgidZombieScanInterval 0
    FcgidSpawnScoreUpLimit 7000
    FcgidSpawnScore 1
    FcgidTerminationScore -1
    FcgidTimeScore 3
</IfModule>
I ran some quick tests to try and show how the IdleScanInterval and IdleTimeout settings can effect website load time. The results below were gathered on a cPanel server. I created 15 accounts and installed WordPress on each account. I then configured cPanel to use FCGI as the PHP handler and enabled PHP 5.5's built in OPcache. I limited FcgidMaxProcesses to 10 to replicate the issue. The theory was that after I curl the first 10 test sites, the other 5 sites would take significantly longer to load because the 10 FCGI processes would remain idle until the timeouts / scans decided it was time to kill the process off.
New processes can only be created once one of the existing 10 processes dies. As you can see here the most dramatic improvement happened when I lowered IdleTimeout from 300 (Orange Column) to 30 (Yellow Column). The longest delay between a page load dropped from 1 minute to 35 seconds.
Since IdleTimeout seems to be a huge factor, I lowered it again, from 30 seconds to 3 seconds (Blue Column). As you can see the delay dropped from 35 seconds to 9 seconds. Not bad, but if my website took 9 seconds to load, or even begin to display content I would not be happy. The green column looks much better, with a delay of 3 seconds. If you want to have the green column performance, you can have it! Below this section are some configurations based on various amounts of RAM which will provide good performance and will quickly kill off idle FCGI processes so that new ones can be created.
FCGI max processes website loading delay hangup.png
This is not an issue that will happen very often. The script I used is requesting 15 webpages as fast as it can. For the same situation to happen in the real world, 15 people would need to request 15 sites on your server within the same 6 seconds to notice a slight delay. On servers with a lot of traffic, this might be a common thing to have happen, but I'm testing out these settings for a 1GB VPS server, which is rather small. If I had 2GB of memory I would have raised FcgidMaxProcesses to at least 20, and none of this would be an issue.

FCGI Settings for Servers with only a few cPanel Accounts

  • Use Apache 2.4 Event
  • Use FCGI or PHP-FPM to handle PHP
  • Use PHP 5.5 or newer
  • Enable Zend OPcache for PHP

1GB VPS / cPanel Server

If the server already has PHP 5.5+, enable Zend OPcache. If it does not have PHP 5.5 then you should consider upgrading PHP. Otherwise you can still use fcgi, but you won't be able to use the built in opcache.so. You can still use Xcache or APC or whatever you prefer with PHP 5.4 so don't worry too much about upgrading to 5.5, but still, in general you want to run the latest version of PHP.
To enable opcache for PHP, modify the server's php.ini file
vim /usr/local/lib/php.ini
If cPanel is using PHP version 5.5 or later, the opcache.so extension should already exist. All you need to so is paste this at the bottom of php.ini, once you save the file, restart apache and you should see Zend OPcache listed when you run php -m
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20121212/opcache.so
opcache.memory_consumption=64
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=7963
opcache.revalidate_freq=0
opcache.fast_shutdown=1
opcache.enable_cli=1
opcache.enable=1

The next step is to make sure we are using Apache with the Event MPM. Don't worry too much about messing around with Apache settings, the ones I have below are the default setting. Don't add this to the main httpd.conf file, instead add this to pre_virtualhost_global.conf, or use the WHM includes editor.
vim /usr/local/apache/conf/includes/pre_virtualhost_global.conf
##Apache Event Default Settings (overrides main httpd.conf)
KeepAlive               On
KeepAliveTimeout        2
MaxKeepAliveRequests    500
<IfModule event.c>
ThreadsPerChild         25
ServerLimit             16
MaxRequestWorkers       400
StartServers            6
MinSpareThreads         150
MaxSpareThreads         400
MaxRequestsPerChild     0
</IfModule>

<IfModule mod_fcgid.c>
FcgidMaxRequestLen                      1073741824
FcgidOutputBufferSize                   1073741824
FcgidMinProcessesPerClass               0
FcgidMaxRequestsPerProcess              0
FcgidInitialEnv PHP_FCGI_MAX_REQUESTS   0
FcgidInitialEnv PHP_FCGI_CHILDREN       0
FcgidMaxProcesses                       20
FcgidMaxProcessesPerClass               10
FcgidFixPathinfo                        1
FcgidIdleTimeout                        3
FcgidBusyTimeout                        300
FcgidProcessLifeTime                    300
FcgidIOTimeout                          300
FcgidIdleScanInterval                   1
FcgidErrorScanInterval                  1
FcgidZombieScanInterval                 1
</IfModule>
Go ahead and stop, then start apache, don't just restart it. I find that sometimes if you change start server settings, or other directives a restart does not always apply the changes, not sure if this is a cpanel thing or what.
service httpd stop; service httpd start

2GB VPS / cPanel Server

vim /usr/local/lib/php.ini
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20121212/opcache.so
opcache.memory_consumption=64
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=7963
opcache.revalidate_freq=0
opcache.fast_shutdown=1
opcache.enable_cli=1
opcache.enable=1
vim /usr/local/apache/conf/includes/pre_virtualhost_global.conf
##Apache Event Default Settings (overrides main httpd.conf)
KeepAlive               On
KeepAliveTimeout        2
MaxKeepAliveRequests    500
<IfModule event.c>
ThreadsPerChild         25
ServerLimit             16
MaxRequestWorkers       400
StartServers            6
MinSpareThreads         150
MaxSpareThreads         400
MaxRequestsPerChild     0
</IfModule>

<IfModule mod_fcgid.c>
FcgidMaxRequestLen                      1073741824
FcgidOutputBufferSize                   1073741824
FcgidMinProcessesPerClass               0
FcgidMaxRequestsPerProcess              0
FcgidInitialEnv PHP_FCGI_MAX_REQUESTS   0
FcgidInitialEnv PHP_FCGI_CHILDREN       0
FcgidMaxProcesses                       40
FcgidMaxProcessesPerClass               20
FcgidFixPathinfo                        1
FcgidIdleTimeout                        3
FcgidBusyTimeout                        300
FcgidProcessLifeTime                    300
FcgidIOTimeout                          300
FcgidIdleScanInterval                   1
FcgidErrorScanInterval                  1
FcgidZombieScanInterval                 1
</IfModule>
service httpd stop; service httpd start

4GB VPS / cPanel Server

vim /usr/local/lib/php.ini
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20121212/opcache.so
opcache.memory_consumption=64
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=7963
opcache.revalidate_freq=0
opcache.fast_shutdown=1
opcache.enable_cli=1
opcache.enable=1
vim /usr/local/apache/conf/includes/pre_virtualhost_global.conf
##Apache Event Default Settings (overrides main httpd.conf)
KeepAlive               On
KeepAliveTimeout        2
MaxKeepAliveRequests    500
<IfModule event.c>
ThreadsPerChild         25
ServerLimit             16
MaxRequestWorkers       400
StartServers            6
MinSpareThreads         150
MaxSpareThreads         400
MaxRequestsPerChild     0
</IfModule>

<IfModule mod_fcgid.c>
FcgidMaxRequestLen                      1073741824
FcgidOutputBufferSize                   1073741824
FcgidMinProcessesPerClass               0
FcgidMaxRequestsPerProcess              0
FcgidInitialEnv PHP_FCGI_MAX_REQUESTS   0
FcgidInitialEnv PHP_FCGI_CHILDREN       0
FcgidMaxProcesses                       80
FcgidMaxProcessesPerClass               40
FcgidFixPathinfo                        1
FcgidIdleTimeout                        3
FcgidBusyTimeout                        300
FcgidProcessLifeTime                    300
FcgidIOTimeout                          300
FcgidIdleScanInterval                   1
FcgidErrorScanInterval                  1
FcgidZombieScanInterval                 1
</IfModule>
service httpd stop; service httpd start

8GB VPS / cPanel Server

vim /usr/local/lib/php.ini
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20121212/opcache.so
opcache.memory_consumption=64
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=7963
opcache.revalidate_freq=0
opcache.fast_shutdown=1
opcache.enable_cli=1
opcache.enable=1
vim /usr/local/apache/conf/includes/pre_virtualhost_global.conf
##Apache Event Default Settings (overrides main httpd.conf)
KeepAlive               On
KeepAliveTimeout        2
MaxKeepAliveRequests    500
<IfModule event.c>
ThreadsPerChild         25
ServerLimit             16
MaxRequestWorkers       400
StartServers            6
MinSpareThreads         150
MaxSpareThreads         400
MaxRequestsPerChild     0
</IfModule>

<IfModule mod_fcgid.c>
FcgidMaxRequestLen                      1073741824
FcgidOutputBufferSize                   1073741824
FcgidMinProcessesPerClass               0
FcgidMaxRequestsPerProcess              0
FcgidInitialEnv PHP_FCGI_MAX_REQUESTS   0
FcgidInitialEnv PHP_FCGI_CHILDREN       0
FcgidMaxProcesses                       160
FcgidMaxProcessesPerClass               80
FcgidFixPathinfo                        1
FcgidIdleTimeout                        3
FcgidBusyTimeout                        300
FcgidProcessLifeTime                    300
FcgidIOTimeout                          300
FcgidIdleScanInterval                   1
FcgidErrorScanInterval                  1
FcgidZombieScanInterval                 1
</IfModule>
service httpd stop; service httpd start

mod_fcgid configuration settings

FcgidMaxRequestLen

If the size of the request body exceeds this amount, the request will fail with 500 Server Error. By default this is set to 131072 Bytes. For cPanel the default is 1073741824, which is about 1GB.
<IfModule mod_fcgid.c>
    FcgidMaxRequestLen 1073741824
</IfModule>

FcgidMaxProcessesPerClass

This directive sets the maximum number of processes that can be started for each process class, turns out that class means PER USER. If you have multiple users, or have a cPanel server please DO NOT USE THIS. Use FcgidMaxProcesses which sets the global limit. If you set the per class value, it gets multiplied by the amount of users. By default this value is set to 100. If you do not have lots of RAM, lower this to around 20 or so, assuming your app uses about 30MB per process.
<IfModule mod_fcgid.c>
    FcgidMaxProcessesPerClass 100 (default)
</IfModule>

FcgidMaxProcesses

This sets the global / server limit for how many PHP processes can run. Unlike the FcgidMaxProcessesPerClass directive, which is a per user limit, this directive is for all users. This is basically MaxClients but for PHP. If you have more users than processes, you may run into an issue where some websites appear to hang up for well over a minute. Read on to find out how to get around this issue by lowering various timeout and scan interval settings.
<IfModule mod_fcgid.c>
    FcgidMaxProcesses 20
</IfModule>

FcgidMaxRequestsPerProcess

FastCGI application processes will be terminated after handling the specified number of requests. By default this is set to 0 which means there is no limit / check.
<IfModule mod_fcgid.c>
    FcgidMaxRequestsPerProcess 0
</IfModule>

FcgidConnectTimeout

This is the maximum period of time the module will wait while trying to connect to a FastCGI application on Windows. (This directive is not respected on Unix, where AF_UNIX defaults will apply.)

FcgidIOTimeout

This is the maximum period of time the module will wait while trying to read from or write to a FastCGI application. If you have a slow application you may want to raise this, however 40 seconds is plenty of time. IF you site / app takes 40 seconds to load then odds are no one will ever want to visit your site again.

FcgidIdleTimeout

Application processes which have not handled a request for this period of time will be terminated, if the number of processses for the class exceeds FcgidMinProcessesPerClass. A value of 0 disables the check. The default value is 300 seconds, which means that a FCGI process could serve 1 request, and then remain idle for up to 1 minute or more. This can be a huge waste of resources if you have lots of sites on your server.
<IfModule mod_fcgid.c>
    FcgidIdleTimeout 300 (default)
</IfModule>
If you notice that some websites are hanging up when you try to load them, it could be because all the FCGI processes are active and assigned to a user. If you have more users than FCGI processes then lowing FcgidIdleTimeout to a smaller value like 30, or even 3 will significantly reduce the chances of this happening.
<IfModule mod_fcgid.c>
    FcgidIdleTimeout 3
</IfModule>

FcgidMinProcessesPerClass

This directive sets the minimum number of processes that will be retained in a process class after finishing requests. By default the value is 3.
<IfModule mod_fcgid.c>
    FcgidMinProcessesPerClass 3
</IfModule>

FcgidProcessLifeTime

Idle application processes which have existed for greater than this time will be terminated, if the number of processses for the class exceeds FcgidMinProcessesPerClass. A value of 0 disables the check. By Default this is set to 3600, if you want to save RAM you can set this to something like 30 to kill off unused processes
<IfModule mod_fcgid.c>
    FcgidProcessLifeTime 60
</IfModule>

FcgidIdleScanInterval

This is the interval at which the module will search for processes which have exceeded FcgidIdleTimeout or FcgidProcessLifeTime. The default is 120 seconds, you can lower this if you want shorter living processes. If you notice that on occasion, some websites seem to hang for over a minute or more when you try to load the page, you will want to lower this setting. If all FCGI processes are in use, and you have more users than the process limit, lowering this to 5 or even 0 will cause processes to clear out much faster instead of hanging around. Setting this to 0 is a valid way to go. You will need to lower some other scan intervals besides this one to make sure that processes are not hanging around wasting space.
<IfModule mod_fcgid.c>
    FcgidIdleScanInterval 120 (default)
</IfModule>

FcgidTerminationScore

Lowing the FcgidTerminationScore value increases the allowed spawn rate, the value can be negative which can be useful for allowing process replacement without increasing the score. I did notice that lowering FcgidTerminationScore to 1 did seem to slightly boost performance while running Apache Benchmark, a basic Wordpress blog was requested 100 times and with a score of 2 the server pumped out 23 request/s. With a score of 1 the server was able to reach 29 request/s. Values that were lower than 0 seemed to reduce performance slightly, but every server / website will perform differently so you might want to test the differences for yourself.
<IfModule mod_fcgid.c>
FcgidTerminationScore 2 (default)
</IfModule>

FcgidTimeScore

The value set for FcgidTimeScore determines the amount subtracted from the activity score of a FCGI process each second. Higher values increase the spawn rate, usually this means faster response times, and potentially higher amounts of CPU usage under large amounts of traffic. I've found that raising FcgidTimeScore to 3 seems to help with clearing old processes out and quickly creating new processes to handle new user requests.
<IfModule mod_fcgid.c>
FcgidTimeScore 1 (default)
</IfModule>

Zend opcache

Zend OPcache Links

opcache.max_accelerated_files

opcache.max_accelerated_files - Controls how many PHP files can be held in memory at the same time. Should be set to a Prime number for better performance. { 223, 463, 983, 1979, 3907, 7963, 16229, 32531, 65407, 130987 }
To find out how many files you have, you can use a command like this.
find . -type f -print | grep php | wc -l
I set this to 7963 for my 1GB server and this value seems to work well.
opcache.max_accelerated_files = 7963

opcache.memory_consumption

opcache.memory_consumption - The default is 64MB, if you have a ton of sites or large amounts of PHP code in general you might want to raise this. For a 1GB server I set this to 64MB.
opcache.memory_consumption = 64

opcache.interned_strings_buffer

opcache.interned_strings_buffer - PHP uses string interning to improve performance. If you have the string "mahstring" 100 times in your code, PHP will store 1 variable for the string and will use a pointer to it for the other 99 times it gets used. This value is in MB. Raising this value too high causes each PHP process to use more RAM, or at least appear to. When this was set to 16MB each PHP process was around 48MB. When set to 4MB each PHP process used around 32MB RAM each. This is shared RAM so the usage may not matter much, but just keep that in mind when you tune this and use high values.
opcache.interned_strings_buffer = 4

opcache.revalidate_freq

opcache.revalidate_freq How often in seconds should the code cache expire and check if your code has changed. 0 means it checks your PHP code every single request which can hurt performance. If you don't update PHP code all that often then setting this to 120 seconds could help with performance and still get refreshed every 2 minutes.
opcache.revalidate_freq=120

Using a Custom / Local php.ini with FCGI

cd /home/$user/public-html/cgi-bin
cp /usr/local/lib/php.ini /home/$user/public_html/cgi-bin/
vim /home/$user/public_html/cgi-bin/php.fcgi

##Create the file with this##

#!/bin/sh
export PHP_FCGI_CHILDREN=0
export PHP_FCGI_MAX_REQUESTS=0
exec /usr/local/cpanel/cgi-sys/php5
Save the file, then make sure it is executable
chmod +x /home/$user/public_html/cgi-bin/php.fcgi
chown -R $user:$user /home/$user/public_html/cgi-bin/
Add this to the user's .htaccess file
vim /home/$user/public_html/.htaccess

##Add to top##

AddHandler php5-fastcgi .php
Action php5-fastcgi /cgi-bin/php.fcgi
Now it's time to modify the php.conf file, but before you do, please back up the original one.
cp -a /usr/local/apache/conf/php.conf /usr/local/apache/conf/php.conf.`date +%F.%H.%M`
vim /usr/local/apache/conf/php.conf 
##Make sure these two lines are in the conf##

Action php5-fastcgi /cgi-bin/php.fcgi
AddType application/x-httpd-php .php
Save the file and distill the apache conf and restart apache
/usr/local/cpanel/bin/apache_conf_distiller --update
/scripts/rebuildhttpdconf
/etc/init.d/httpd restart
Create a php info file to test out the changes
vim /home/$user/public_html/phpinfo.php

##Add this##
<?php
phpinfo();
?>
Make sure the php info file has the correct ownerships
chown $user:$user /home/$user/public_html/phpinfo.php
Near the top of the page you should see that the custom path / php.ini file is in use
Loaded Configuration File /home/user/public_html/cgi-bin/php.ini

Additional Information

Video: Scaling PHP in the Real World - Dustin Whittle


Links


Reference: