Optimizing Performance!
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWhat about performance? Is our server optimized? Could our deploy somehow make our code faster? Why actually... yes!
Google for Symfony performance to find an article in the docs all about this.
Optimized Autoloader
Scroll down a little: I want to start at the section about Composer's Class Map. It turns out... autoloading classes - which happens hundreds of times on each request... is kinda slow! Composer even has its own documentation about optimizing the autoloader. Fortunately, making it fast is super simple: we just need to pass a few extra flags to composer install
.
Open up the Ansible documentation for the composer
module. It has an option called optimize_autoloader
which actually defaults to true! In other words, thanks to the module, we're already passing the --optimize
flag as well as the --no-dev
flag.
The only missing option is --classmap-authoritative
, which gives a minor performance boost. But hey, I love free performance!
Open up after-symlink-shared.yml
and find the Composer install task. Add arguments
set to --classmap-authoritative
. I'll also set optimize_autoloader
to true... but that's the default anyways:
// ... lines 2 - 6 | |
- name: Install Composer dependencies | |
composer: | |
command: install | |
arguments: --classmap-authoritative | |
optimize_autoloader: yes | |
working_dir: '{{ ansistrano_release_path.stdout }}' | |
// ... lines 13 - 50 |
Oh, and there is one other way to optimize the autoloader: with a --apcu-autoloader
flag. This is meant to be used instead of --classmap-authoritative
... but I'm not sure if the performance will be much different. If you really care, you can test it, and let me know.
Guarantee OPCache
Back on the performance docs, at the top, the first thing it mentions is using a byte code cache... so OPcache. If you ignore everything else I say, at least make sure you have this installed. We already do. But, to be sure, we can open playbook.yml
and - under the extensions - add php7.1-opcache
:
- hosts: webserver | |
// ... lines 3 - 35 | |
tasks: | |
// ... lines 37 - 68 | |
- name: Install PHP packages | |
become: true | |
apt: | |
name: "{{ item }}" | |
state: latest | |
with_items: | |
// ... lines 75 - 79 | |
- php7.1-opcache | |
// ... lines 81 - 183 |
opcache.max_accelerated_files
Ok, what other performance goodies are there? Ah yes, opcache.max_accelerated_files
. This defines how many files OPcache will store. Since Symfony uses a lot of files, we recommend setting this to a higher value.
On our server, the default is 10,000:
php -i | grep max_accelerated_files
But the docs recommend 20,000. So let's change it!
We already have some code that changes the date.timezone
php.ini setting:
- hosts: webserver | |
// ... lines 3 - 35 | |
tasks: | |
// ... lines 37 - 84 | |
- name: Set date.timezone for CLI | |
become: true | |
ini_file: | |
path: /etc/php/7.1/cli/php.ini | |
section: Date | |
option: date.timezone | |
value: UTC | |
- name: Set date.timezone for FPM | |
become: true | |
ini_file: | |
path: /etc/php/7.1/fpm/php.ini | |
section: Date | |
option: date.timezone | |
value: UTC | |
notify: Restart PHP-FPM | |
// ... lines 101 - 183 |
In that case, we modified both the cli and fpm config files. But because this is just for performance, let's only worry about FPM. Copy the previous task and create a new one called: Increase OPcache limit
of accelerated files:
- hosts: webserver | |
// ... lines 3 - 35 | |
tasks: | |
// ... lines 37 - 92 | |
- name: Set date.timezone for FPM | |
// ... lines 94 - 101 | |
- name: Increase OPcache limit of accelerated files | |
become: true | |
ini_file: | |
path: /etc/php/7.1/fpm/php.ini | |
section: opcache | |
// ... lines 107 - 108 | |
notify: Restart PHP-FPM | |
// ... lines 110 - 192 |
The section
will be opcache
. Why? On the server, open up the php.ini
file and hit /
to search for max_accelerated_files
:
sudo vim /etc/php/7.1/fpm/php.ini
This is the setting we want to modify. And if you scroll up... yep! It's under a section called [opcache]
:
# /etc/php/7.1/fpm/php.ini
# ...
[opcache]
# ...
; The maximum number of keys (scripts) in the OPcache hash table.
; Only numbers between 200 and 1000000 are allowed.
;opcache.max_accelerated_files=1000
Tell the ini_file
module to set the option
opcache.max_accelerated_files
to a value of 20000:
- hosts: webserver | |
// ... lines 3 - 35 | |
tasks: | |
// ... lines 37 - 101 | |
- name: Increase OPcache limit of accelerated files | |
become: true | |
ini_file: | |
path: /etc/php/7.1/fpm/php.ini | |
section: opcache | |
option: opcache.max_accelerated_files | |
value: 20000 | |
notify: Restart PHP-FPM | |
// ... lines 110 - 192 |
The Mysterious realpath_cache_size
There is just one last recommendation I want to implement: increasing realpath_cache_size
and realpath_cache_ttl
. Let's change them first... and explain later.
Go back to the php.ini
file on the server and move all the way to the top. The standard PHP configuration all lives under a section called PHP
:
# /etc/php/7.1/fpm/php.ini
[PHP]
;;;;;;;;;;;;;;;;;;;
; About php.ini ;
;;;;;;;;;;;;;;;;;;;
# ...
If you looked closely enough, you would find out that the two "realpath" options indeed live here:
# /etc/php/7.1/fpm/php.ini
[PHP]
# ...
; Determines the size of the realpath cache to be used by PHP. This value should
; be increased on systems where PHP opens many files to reflect the quantity of
; the file operations performed.
; http://php.net/realpath-cache-size
;realpath_cache_size = 4096k
; Duration of time, in seconds for which to cache realpath information for a given
; file or directory. For systems with rarely changing files, consider increasing this
; value.
; http://php.net/realpath-cache-ttl
;realpath_cache_ttl = 120
Copy the previous task and paste. Oh, and I'll fix my silly typo. Name the new task "Configure the PHP realpath cache". This time, we want to modify two values. So, for option
, use the handy {{ item.option }}
. And for value
, {{ item.value }}
:
- hosts: webserver | |
// ... lines 3 - 35 | |
tasks: | |
// ... lines 37 - 101 | |
- name: Increase OPcache limit of accelerated files | |
// ... lines 103 - 110 | |
- name: Configure the PHP realpath cache | |
become: true | |
ini_file: | |
path: /etc/php/7.1/fpm/php.ini | |
section: PHP | |
option: '{{ item.option }}' | |
value: '{{ item.value }}' | |
notify: Restart PHP-FPM | |
// ... lines 119 - 204 |
Hook this up by using with_items
. Instead of simple strings, set the first item to an array with option: realpath_cache_size
and value
, which should be 4096K
. Copy that and change the second line: realpath_cache_ttl
to 600:
- hosts: webserver | |
// ... lines 3 - 35 | |
tasks: | |
// ... lines 37 - 110 | |
- name: Configure the PHP realpath cache | |
become: true | |
ini_file: | |
path: /etc/php/7.1/fpm/php.ini | |
section: PHP | |
option: '{{ item.option }}' | |
value: '{{ item.value }}' | |
notify: Restart PHP-FPM | |
with_items: | |
- { option: 'realpath_cache_size', value: '4096K' } | |
- { option: 'realpath_cache_ttl', value: '600' } | |
// ... lines 122 - 204 |
All about the realpath_cache
We did make one small change to our deploy playbook, but let's just trust it works. The more interesting changes were to playbook.yml
. So let's re-provision the servers:
ansible-playbook ansible/playbook.yml -i ansible/hosts.ini --ask-vault-pass -l aws
While that works, I want to explain all this realpath_cache
stuff... because I don't think many of us really know how it works. Actually, Benjamin Eberlei even wrote a blog post about these settings. Read it to go deeper.
But here's the tl;dr: each time you require or include a file - which happens many times on each request - the "real path" to that file is cached. This is useful for symlinks: if a file lives at a symlinked location, then PHP figures out the "real" path to that file, then caches a map from the original, symlinked path, to the final, real path. That's the "Real Path Cache".
But even if you're not using symlinks, the cache is great, because it prevents IO operations: PHP does not even need to check if the path is a symlink, or get other information.
The point is: the realpath cache rocks and makes your site faster. And that's exactly why we're making sure that the cache is big enough for the number of files that are used in a Symfony app.
The realpath_cache_ttl
is where things get really interesting. We're using a symlink strategy in our deploy. And some sources will tell you that this strategy plays really poorly with the "realpath cache". Why? Well, just think about: suppose your web/app.php
file requires app/AppKernel.php
. Internally. app.php
will ask:
Hey Realpath Cache! What is the real path to
/var/www/project/current/app/AppKernel.php
?
If we've recently deployed, then that path may already exist in the "realpath cache"... but still point to the old release directory! In other words, the "realpath cache" will continue to think that a bunch of our files still truly live in the old release directory! This will happen until all the cache entries hit their TTL.
So here's the big question: if the "realpath cache" is such a problem... then why are we increasing the TTL to an even higher value!?
Because... I lied... a little. Let me show you why the "realpath cache" is not a problem. On your server, open up /etc/nginx/sites-available/mootube.example.com.conf
. Search for "real". Ah yes:
# /etc/nginx/sites-available/mootube.example.com.conf
# ...
location ~ ^/(app_dev|config)\.php(/|$) {
# ...
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
# ...
}
This line helps pass information to PHP. The key is the $realpath_root
part. Thanks to this, by the time PHP executes, our code - like web/app.php
already knows that it is in a releases
directory. That means that when it tries to require a file - like app/AppKernel.php
, it actually says:
Hey Realpath Cache! What is the real path to
/var/www/project/releases/2017XXXXX/app/AppKernel.php
?
The symlink directory - current/
is never included in the "realpath cache"... because our own code thinks that it lives in the resolved, "releases" directory. This is a long way of explaining that the "realpath cache" just works... as long as you have this line.
Check on the provision. Perfect! It just finished updating the php.ini
file. Go check that out on the server and look for the changes. Yep! max_accelerated_files
looks perfect... and so do the realpath
settings:
# /etc/php/7.1/fpm/php.ini
# ...
opcache.max_accelerated_files = 20000
# ...
realpath_cache_size = 4096K
# ...
realpath_cache_ttl = 600
# ...
Next! Let's talk about rolling back a deploy. Oh, we of course never make mistakes... but... ya know... let's talk about rolling back a deploy anyways... in case someone else messes up.