Skip to content

Instantly share code, notes, and snippets.

@mattatcha
Forked from tegansnyder/hhvm_magento_setup.md
Created June 9, 2016 18:25
Show Gist options
  • Save mattatcha/04a3c2065517a5b856fa7274c72cc09e to your computer and use it in GitHub Desktop.
Save mattatcha/04a3c2065517a5b856fa7274c72cc09e to your computer and use it in GitHub Desktop.
HHVM Magento Server Setup

I've had the opertunity to try a variety of different server configurations but never really got around to trying HHVM with Magento until recently. I thought I would share a detailed walkthrough of configuring a single instance Magento server running Nginx + Fast CGI + HHVM / PHP-FPM + Redis + Percona. For the purpose of this blog post I'm assuming you are using Fedora, CentOS, or in my case RHEL 6.5.

Please note: I'm 100% open to suggestions. If you see something I did that needs to be done a different way, please let me know. I haven't included my Perconca my.conf file yet. I will shortly. Also I plan on trying this same test with HHVM 3.3 and PHP 7.

Install the EPEL, Webtatic, and REMI repos

rpm -Uvh http://download.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm
rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
rpm -Uvh http://mirror.webtatic.com/yum/el6/latest.rpm

Install PHP 5.5.18

yum -y install php55w php55w-opcache php55w-devel php55w-mcrypt php55w-gd php55w-mbstring php55w-mysql php55w-pdo php55w-soap php55w-xmlrpc php55w-xml php55w-pdo php55w-mysqli libwebp

Install Percona

Note you may have existing mysql packages installed in your distro. If you do you will need to remove them prior to installing Percona. You can check by issuing:

rpm -qa | grep -i mysql

For instance on my server I needed to remove the following:

yum remove mysql
yum remove mysql-libs
yum remove compat-mysql51
Setup the Percona Repo

Open a VI editor to the following file.

vi /etc/yum.repos.d/Percona.repo

Add the following:

[percona]
name = CentOS $releasever - Percona
baseurl=http://repo.percona.com/centos/$releasever/os/$basearch/
enabled = 1
gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-percona
gpgcheck = 1
Grab the Percona GPG key
wget http://www.percona.com/downloads/RPM-GPG-KEY-percona
sudo mv RPM-GPG-KEY-percona /etc/pki/rpm-gpg/
Install Percona via Yum
sudo yum install -y Percona-Server-client-56 Percona-Server-server-56 Percona-Server-devel-56
Start Percona and Setup Root Pass
service mysql start
# then run
/usr/bin/mysql_secure_installation
# setup root password

Install HHVM

# needed to work around libstdc version issue
sudo yum upgrade --setopt=protected_multilib=false --skip-broken

# setup the hop5 repo
cd /etc/yum.repos.d
sudo wget http://www.hop5.in/yum/el6/hop5.repo

# show available versions of hvvm
yum list --showduplicates hhvm

# install latest verison show from list above
yum --nogpgcheck install -y hhvm-3.2.0-1.el6

Install Nginx and PHP-FPM

yum --enablerepo=remi install -y nginx php55w-fpm php55w-common

Configuring Nginx

# rename the default config as its not needed
sudo mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.old

# create a new config
vi /etc/nginx/conf.d/server.conf
server {
    server_name mydomainname.com www.mydomainname.com;
    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log info;

    # 504 is a PHP timeout and must be static
    # 502 is momentary during a PHP restart and should be treated like maintenance
    # other 50x errors are handled by Magento
    error_page 502 504 /var/www/mysite/504.html;

    listen 80;
    #listen 443 ssl;
 
    # if you are using a load balancer uncomment these lines
    # header from the hardware load balancers
    #real_ip_header X-Forwarded-For;
    # trust this header from anything inside the subnet
    #set_real_ip_from X.X.X.1/24;
    # the header is a comma-separated list; the left-most IP is the end user
    #real_ip_recursive on;

    # ensure zero calls are written to disk
    client_max_body_size          16m;
    client_body_buffer_size       2m;
    client_header_buffer_size     16k;
    large_client_header_buffers   8 8k;

    root /var/www/mysite;
    index index.php;

    fastcgi_read_timeout    90s;
    fastcgi_send_timeout    60s;
    
    # ensure zero calls are written to disk
    fastcgi_buffers 512 16k;
    fastcgi_buffer_size 512k;
    fastcgi_busy_buffers_size 512k;

    # remove the cache-busting timestamp
    location ~* (.+)\.(\d+)\.(js|css|png|jpg|jpeg|gif)$ {
        try_files $uri $1.$3;
        access_log off;
        log_not_found off;
        expires 21d;
        add_header Cache-Control "public";
    }

    # do not log static files; regexp should capture alternate cache-busting timestamps
    location ~* \.(jpg|jpeg|gif|css|png|js|ico|txt|swf|xml|svg|svgz|mp4|ogg|ogv)(\?[0-9]+)?$ {
        access_log off;
        log_not_found off;
        expires 21d;
        add_header Cache-Control "public";
    }

    # Server
    include main.conf;
    include security.conf;

}

Create a home for your website

If you don't already have a place for your website files to live you will need to create one:

sudo mkdir -p /var/www/mysite/

# while you at it create a nice static error page
echo "error page" >> /var/www/mysite/504.html

Setup Nginx for HHVM and Magento

Nginx needs to be told how to work with PHP traffic and forward it via FastCGI to HHVM. Here is a good configuration. You will notice their is some standard rewrites for Magento assets in place.

vi /etc/nginx/main.conf
rewrite_log on;
 
location / {
  index index.php;
  try_files $uri $uri/ @handler;
}
 
location @handler {
  rewrite / /index.php;
}
 
## force www in the URL
if ($host !~* ^www\.) {
  #rewrite / $scheme://www.$host$request_uri permanent;
}
 
## Forward paths like /js/index.php/x.js to relevant handler
location ~ \.php/ {
  rewrite ^(.*\.php)/ $1 last;
}
 
location /media/catalog/ {
  expires 1y;
  log_not_found off;
  access_log off;
}
 
location /skin/ {
  expires 1y;
}
 
location /js/ {
  access_log off;
}

location ~ \.php$ { ## Execute PHP scripts

  if (!-e $request_filename) { rewrite / /index.php last; } ## Catch 404s that try_files miss
  
  expires off; ## Do not cache dynamic content

  # for this tutorial we are going to use a unix socket
  # but if HHVM was running on another host we could forego unix socket
  # in favor of an IP address and port number as follows:
  #fastcgi_pass 127.0.0.1:8080;

  fastcgi_pass unix:/var/run/hhvm/sock;

  fastcgi_index index.php;
  #fastcgi_param HTTPS $fastcgi_https;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

  # if you need to explictly specify a store code for Magento do it here
  # this is useful if you are running multiple stores with different hostnames
  #fastcgi_param MAGE_RUN_CODE default;
  #fastcgi_param MAGE_RUN_TYPE store;

  include fastcgi_params; ## See /etc/nginx/fastcgi_params
 
  fastcgi_keep_conn on; #hhvm param
}

Next we need to setup our security configuration:

vi /etc/nginx/security.conf
## General Magento Security
location /app/ { deny all; }
location /includes/ { deny all; }
location /lib/ { deny all; }
location /media/downloadable/ { deny all; }
location /pkginfo/ { deny all; }
location /report/config.xml { deny all; }
location /var/ { deny all; }
 
## Disable .htaccess and other hidden files
location /\. {
  return 404;
}
 
## Disable all methods besides HEAD, GET and POST.
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
  return 444;
}

HHVM Configuration

vi /etc/hhvm/server.hdf
PidFile = /var/run/hhvm/pid

Server {
  Port = 8080
  SourceRoot = /var/www/mysite
  DefaultDocument = index.php
}

Log {
  Level = Warning
  AlwaysLogUnhandledExceptions = true
  RuntimeErrorReportingLevel = 8191
  UseLogFile = true
  UseSyslog = false
  File = /var/log/hhvm/error.log
  Access {
    * {
      File = /var/log/hhvm/access.log
      Format = %h %l %u % t \"%r\" %>s %b
    }
  }
}

Repo {
  Central {
    Path = /var/log/hhvm/.hhvm.hhbc
  }
}

#include "/usr/share/hhvm/hdf/static.mime-types.hdf"
StaticFile {
  FilesMatch {
    * {
      pattern = .*\.(dll|exe)
      headers {
        * = Content-Disposition: attachment
      }
    }
  }
  Extensions : StaticMimeTypes
}
MySQL {
  TypedResults = false
}

HHVM Fast-CGI support

HHVM will need to start with Fast-CGI support so Nginx can forward PHP request to it. We also need to edit the start up script to make HHVM use a unix socket. To do this edit the following file:

vi /etc/init.d/hhvm

I've only made a few changes to the start function start function to enable zend sorting per Daniel Sloof recommendation. I've also change the shutdown to kill the proper pid file (/var/run/hhvm/hhvm.pid). Here is the full init file:

#!/bin/bash
#
#	/etc/rc.d/init.d/hhvm
#
# Starts the hhvm daemon
#
# chkconfig: 345 26 74
# description: HHVM (aka the HipHop Virtual Machine) is an open-source virtual machine designed for executing programs written in Hack and PHP
# processname: hhvm

### BEGIN INIT INFO
# Provides: hhvm
# Required-Start: $local_fs
# Required-Stop: $local_fs
# Default-Start:  2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: start and stop hhvm
# Description: HHVM (aka the HipHop Virtual Machine) is an open-source virtual machine designed for executing programs written in Hack and PHP
### END INIT INFO

# Source function library.
. /etc/init.d/functions

start() {
	echo -n "Starting hhvm: "
        /usr/bin/hhvm --config /etc/hhvm/server.hdf --user apache --mode daemon -vServer.Type=fastcgi -vServer.FileSocket=/var/run/hhvm/sock -vEval.EnableZendSorting=1
	touch /var/lock/subsys/hhvm
}	

stop() {
	echo -n "Shutting down hhvm: "
	killproc -p /var/run/hhvm/pid
	rm -f /var/lock/subsys/hhvm
}

case "$1" in
    start)
	start
	;;
    stop)
	stop
	;;
    status)
    if [ ! -f /var/run/hhvm/pid ]; then
            echo "hhvm not is running"
    else
            echo "hhvm is running"
    fi
    ;;
    restart)
    	stop
	start
	;;
    reload|condrestart|probe)
	echo "$1 - Not supported."
	;;
    *)
	echo "Usage: hhvm {start|stop|status|reload|restart[|probe]"
	exit 1
	;;
esac
exit $?

Starting HHVM

As you can see if the init file for HHVM we started it with the user "apache". So before starting HHVM make sure the directory your files are stored is owned by that group. Yes I know I'm being lazy and probably should create a new user and group running hhvm.

sudo chown apache:apache /var/www -R

We also need to give HHVM the permissions to:

mkdir -p /var/run/hhvm
chown apache:apache /var/run/hhvm
chmod 775 /var/run/hhvm

Finally we can start Nginx PHP-FPM and HHVM.

service nginx start
service php-fpm start
service hhvm start

The famous phpinfo() function will not work on HHVM but there is a very nice HHVM equivalent. Lets download it for fun:

cd /var/www/mysite/
wget https://gist.githubusercontent.com/ck-on/67ca91f0310a695ceb65/raw/hhvminfo.php

SCREENSHOT HERE

HHVM admin

HHVM has an admin tool you can use to get stats - AdminServer. If you want to see what is available you can create the following file:

vi /etc/nginx/conf.d/admin.conf
server {
    # hhvm admin
    listen 8889;

    location ~ {
        fastcgi_pass   127.0.0.1:8888;
        include        fastcgi_params;
    }
}

Then add this block to your hhvm configuration:

vi /etc/hhvm/config.hdf
AdminServer {
  Port = 8888
  Password = mySecretPassword
}

Some additional tuning

It is also recommended to use “pm = static” mode (instead of “pm = dynamic”) if you decide to dedicate a server for PHP-FPM exclusively, as there is no need for dynamic allocation of resources to PHP-FPM. The “pm” part of the configuration is more or less the same as if you were to configure Apache.

  • parameters di
vi /etc/php-fpm.d/www.conf

# make these changes
pm = static
pm.max_children = 48
pm.start_servers = 8
pm.min_spare_servers = 8
pm.max_spare_servers = 8
pm.max_requests = 40000
request_terminate_timeout = 120
catch_workers_output = yes
security.limit_extensions = .php .html .phtml
vi /etc/php.ini
[PHP]
engine = On
short_open_tag = On
asp_tags = Off
precision = 14
y2k_compliance = On
output_buffering = 4096
zlib.output_compression = Off
implicit_flush = Off
unserialize_callback_func =
serialize_precision = 100
allow_call_time_pass_reference = Off
safe_mode = Off
safe_mode_gid = Off
safe_mode_include_dir =
safe_mode_exec_dir =
safe_mode_allowed_env_vars = PHP_
safe_mode_protected_env_vars = LD_LIBRARY_PATH
disable_functions =
disable_classes =
expose_php = On
max_execution_time = 90
max_input_time = 120
memory_limit = 512M
max_input_vars = 25000
error_reporting = E_ALL & ~E_DEPRECATED
display_errors = Off
display_startup_errors = Off
log_errors = On
log_errors_max_len = 1024
ignore_repeated_errors = Off
ignore_repeated_source = Off
report_memleaks = On
track_errors = Off
html_errors = Off
variables_order = "GPCS"
request_order = "GP"
register_globals = Off
register_long_arrays = Off
register_argc_argv = Off
auto_globals_jit = On
post_max_size = 64M
magic_quotes_gpc = Off
magic_quotes_runtime = Off
magic_quotes_sybase = Off
auto_prepend_file =
auto_append_file =
default_mimetype = "text/html"
doc_root =
user_dir =
enable_dl = Off
file_uploads = On
upload_max_filesize = 64M
allow_url_fopen = On
allow_url_include = Off
default_socket_timeout = 90

realpath_cache_size = 128k
realpath_cache_ttl = 86400


[Pdo_mysql]
pdo_mysql.cache_size = 2000

[Syslog]
define_syslog_variables  = Off

[mail function]
SMTP = localhost
smtp_port = 25
sendmail_path = /usr/sbin/sendmail -t -i
mail.add_x_header = On

[SQL]
sql.safe_mode = Off

[ODBC]
odbc.allow_persistent = On
odbc.check_persistent = On
odbc.max_persistent = -1
odbc.max_links = -1
odbc.defaultlrl = 4096
odbc.defaultbinmode = 1

[MySQL]
mysql.allow_persistent = Off
mysql.max_persistent = -1
mysql.max_links = -1
mysql.default_port =
mysql.default_socket =
mysql.default_host =
mysql.default_user =
mysql.default_password =
mysql.connect_timeout = 60
mysql.trace_mode = Off

[MySQLi]
mysqli.max_links = -1
mysqli.default_port = 3306
mysqli.default_socket =
mysqli.default_host =
mysqli.default_user =
mysqli.default_pw =
mysqli.reconnect = Off

[PostgresSQL]
pgsql.allow_persistent = On
pgsql.auto_reset_persistent = Off
pgsql.max_persistent = -1
pgsql.max_links = -1
pgsql.ignore_notice = 0
pgsql.log_notice = 0

[Sybase-CT]
sybct.allow_persistent = On
sybct.max_persistent = -1
sybct.max_links = -1
sybct.min_server_severity = 10
sybct.min_client_severity = 10

[bcmath]
bcmath.scale = 0

[Session]
session.save_handler = files
session.save_path = "/var/lib/php/session"
session.use_cookies = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
session.bug_compat_42 = Off
session.bug_compat_warn = Off
session.referer_check =
session.entropy_length = 0
session.entropy_file =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.hash_function = 0
session.hash_bits_per_character = 5
url_rewriter.tags = "a=href,area=href,frame=src,input=src,form=fakeentry"

[MSSQL]
mssql.allow_persistent = On
mssql.max_persistent = -1
mssql.max_links = -1
mssql.min_error_severity = 10
mssql.min_message_severity = 10
mssql.compatability_mode = Off
mssql.secure_connection = Off

[Tidy]
tidy.clean_output = Off

[soap]
soap.wsdl_cache_enabled=1
soap.wsdl_cache_dir="/tmp"
soap.wsdl_cache_ttl=86400

Installing Redis

Since we are going to be using Redis for our store lets make sure to install it.

yum install -y gcc
wget http://download.redis.io/redis-stable.tar.gz
tar xvzf redis-stable.tar.gz
cd redis-stable
make
make install

# give Redis a home
mkdir -p /var/redis

Redis startup scripts

We are going to be running 3 Redis instances for Magento sessions, cache, and FPC. Each redis session is on a different port. To do this we need startup scripts. Here is my startup scripts. As you can see I'm using unix sockets and allocating 500mb for sessions, 1gb for cache, and 2gb for FPC.

Sessions on port 8302
vi /etc/redis/8302.conf
daemonize yes
pidfile /var/run/redis_8302.pid
port 8302
unixsocket /var/run/redis_8302.sock
unixsocketperm 777
timeout 0
tcp-keepalive 0
loglevel notice
logfile /var/log/redis_8302.log
databases 2
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression no
rdbchecksum yes
dbfilename dump.rdb
dir /var/redis/8302
slave-serve-stale-data yes
slave-read-only yes
repl-disable-tcp-nodelay no
slave-priority 100
maxmemory-policy volatile-lru
maxmemory 500mb
appendonly no
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
aof-rewrite-incremental-fsync yes
Cache on port 8402
vi /etc/redis/8402.conf
daemonize yes
pidfile /var/run/redis_8402.pid
port 8402
unixsocket /var/run/redis_8402.sock
unixsocketperm 777
timeout 0
tcp-keepalive 0
loglevel notice
logfile /var/log/redis_8402.log
databases 2
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression no
rdbchecksum yes
dbfilename dump.rdb
dir /var/redis/8402
slave-serve-stale-data yes
slave-read-only yes
repl-disable-tcp-nodelay no
slave-priority 100
maxmemory-policy volatile-lru
maxmemory 1gb
appendonly no
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
aof-rewrite-incremental-fsync yes
FPC Cache on port 8502
vi /etc/redis/8502.conf
daemonize yes
pidfile /var/run/redis_8502.pid
unixsocket /var/run/redis_8502.sock
unixsocketperm 777
port 8502
timeout 0
tcp-keepalive 0
loglevel notice
logfile /var/log/redis_8502.log
databases 2
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression no
rdbchecksum yes
dbfilename dump.rdb
dir /var/redis/8502
slave-serve-stale-data yes
slave-read-only yes
repl-disable-tcp-nodelay no
slave-priority 100
maxmemory-policy volatile-lru
maxmemory 2gb
appendonly no
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
aof-rewrite-incremental-fsync yes

Redis Startup scripts

We need a way to start our servers. We can do this by creating startup scripts for it. Here are my 3 redis startup scripts.

vi /etc/init.d/redis_8302
#!/bin/sh
#
# redis        Startup script for Redis Server
#
# chkconfig: - 90 10
# description: Redis is an open source, advanced key-value store.
#
# processname: redis-server

REDISPORT=8302
EXEC=/usr/local/bin/redis-server
CLIEXEC=/usr/local/bin/redis-cli

PIDFILE=/var/run/redis_8302.pid
CONF="/etc/redis/8302.conf"

case "$1" in
    start)
        if [ -f $PIDFILE ]
        then
                echo "$PIDFILE exists, process is already running or crashed"
        else
                echo "Starting Redis server..."
                $EXEC $CONF
        fi
        ;;
    stop)
        if [ ! -f $PIDFILE ]
        then
                echo "$PIDFILE does not exist, process is not running"
        else
                PID=$(cat $PIDFILE)
                echo "Stopping ..."
                $CLIEXEC -p $REDISPORT shutdown
                while [ -x /proc/${PID} ]
                do
                    echo "Waiting for Redis to shutdown ..."
                    sleep 1
                done
                echo "Redis stopped"
        fi
        ;;
    *)
        echo "Please use start or stop as first argument"
        ;;
esac

exit 0
vi /etc/init.d/redis_8402
#!/bin/sh
#
# redis        Startup script for Redis Server
#
# chkconfig: - 90 10
# description: Redis is an open source, advanced key-value store.
#
# processname: redis-server

REDISPORT=8402
EXEC=/usr/local/bin/redis-server
CLIEXEC=/usr/local/bin/redis-cli

PIDFILE=/var/run/redis_8402.pid
CONF="/etc/redis/8402.conf"

case "$1" in
    start)
        if [ -f $PIDFILE ]
        then
                echo "$PIDFILE exists, process is already running or crashed"
        else
                echo "Starting Redis server..."
                $EXEC $CONF
        fi
        ;;
    stop)
        if [ ! -f $PIDFILE ]
        then
                echo "$PIDFILE does not exist, process is not running"
        else
                PID=$(cat $PIDFILE)
                echo "Stopping ..."
                $CLIEXEC -p $REDISPORT shutdown
                while [ -x /proc/${PID} ]
                do
                    echo "Waiting for Redis to shutdown ..."
                    sleep 1
                done
                echo "Redis stopped"
        fi
        ;;
    *)
        echo "Please use start or stop as first argument"
        ;;
esac

exit 0
vi /etc/init.d/redis_8502
#!/bin/sh
#
# redis        Startup script for Redis Server
#
# chkconfig: - 90 10
# description: Redis is an open source, advanced key-value store.
#
# processname: redis-server

REDISPORT=8502
EXEC=/usr/local/bin/redis-server
CLIEXEC=/usr/local/bin/redis-cli

PIDFILE=/var/run/redis_8502.pid
CONF="/etc/redis/8502.conf"

case "$1" in
    start)
        if [ -f $PIDFILE ]
        then
                echo "$PIDFILE exists, process is already running or crashed"
        else
                echo "Starting Redis server..."
                $EXEC $CONF
        fi
        ;;
    stop)
        if [ ! -f $PIDFILE ]
        then
                echo "$PIDFILE does not exist, process is not running"
        else
                PID=$(cat $PIDFILE)
                echo "Stopping ..."
                $CLIEXEC -p $REDISPORT shutdown
                while [ -x /proc/${PID} ]
                do
                    echo "Waiting for Redis to shutdown ..."
                    sleep 1
                done
                echo "Redis stopped"
        fi
        ;;
    *)
        echo "Please use start or stop as first argument"
        ;;
esac

exit 0
Set the file permissions on the startup scripts:
cd /etc/init.d/
chmod 755 redis_*

mkdir -p /var/redis/8302
mkdir -p /var/redis/8402
mkdir -p /var/redis/8502
chmod 775 /var/redis/8302
chmod 775 /var/redis/8402
chmod 775 /var/redis/8502
Starting Redis servers
sh /etc/init.d/redis_8302 start
sh /etc/init.d/redis_8402 start
sh /etc/init.d/redis_8502 start

You can verify it is running by using the redis-cli tool:

redis-cli -p 8302
redis-cli -p 8402
redis-cli -p 8502
Configuring Magento EE for Redis

I'm using Magento EE 1.14.0.1 for this test and here is how I have it configured:

<?xml version="1.0"?>
<config>
    <global>
        <install>
            <date><![CDATA[Sat, 08 Nov 2014 22:17:08 +0000]]></date>
        </install>
        <crypt>
            <key><![CDATA[my-secret-key]]></key>
        </crypt>
        <disable_local_modules>false</disable_local_modules>
        <resources>
            <db>
                <table_prefix><![CDATA[]]></table_prefix>
            </db>
            <default_setup>
                <connection>
                    <host><![CDATA[localhost]]></host>
                    <username><![CDATA[db-user-name-here]]></username>
                    <password><![CDATA[db-password-here]]></password>
                    <dbname><![CDATA[db-name-here]]></dbname>
                    <initStatements><![CDATA[SET NAMES utf8]]></initStatements>
                    <model><![CDATA[mysql4]]></model>
                    <type><![CDATA[pdo_mysql]]></type>
                    <pdoType><![CDATA[]]></pdoType>
                    <active>1</active>
                </connection>
            </default_setup>
        </resources>

        <session_save>db</session_save>

        <redis_session>
            <host>/var/run/redis_8302.sock</host>
            <port>0</port>
            <password></password>
            <timeout>3.5</timeout>
            <persistent></persistent>
            <db>0</db>
            <compression_threshold>2048</compression_threshold>
            <compression_lib>gzip</compression_lib>
            <log_level>1</log_level>
            <max_concurrency>6</max_concurrency>
            <break_after_frontend>5</break_after_frontend>
            <break_after_adminhtml>30</break_after_adminhtml>
            <first_lifetime>600</first_lifetime>
            <bot_first_lifetime>60</bot_first_lifetime>
            <bot_lifetime>7200</bot_lifetime>
            <disable_locking>0</disable_locking>
        </redis_session>

        <cache>
          <backend>Mage_Cache_Backend_Redis</backend>
          <backend_options>
            <server>/var/run/redis_8402.sock</server>
            <port>0</port>
            <persistent></persistent>
            <database>0</database>
            <password></password>
            <force_standalone>0</force_standalone>
            <connect_retries>1</connect_retries>
            <read_timeout>30</read_timeout>
            <automatic_cleaning_factor>0</automatic_cleaning_factor>
            <compress_data>1</compress_data>
            <compress_tags>1</compress_tags>
            <compress_threshold>20480</compress_threshold>
            <compression_lib>gzip</compression_lib>
          </backend_options>
        </cache>

        <full_page_cache>
          <backend>Mage_Cache_Backend_Redis</backend>
          <backend_options>
            <server>/var/run/redis_8502.sock</server>
            <port>0</port>
            <persistent></persistent>
            <database>0</database>
            <password></password>
            <force_standalone>0</force_standalone>
            <connect_retries>1</connect_retries>
            <read_timeout>30</read_timeout>
            <lifetimelimit>57600</lifetimelimit>
            <compress_data>0</compress_data>
          </backend_options>
        </full_page_cache>

    </global>
    <admin>
        <routers>
            <adminhtml>
                <args>
                    <frontName><![CDATA[admin]]></frontName>
                </args>
            </adminhtml>
        </routers>
    </admin>
</config>
Apache JMeter Benchmarking

Magento has release a beta version of performance testing scripts that are available here. I followed the instructions in the accompanying PDF document, but had some troubles when I was trying to run the JMeter script on my local OSX machine. Magento doesn't mention it in the documentation but you also need to add the JMeter plugins.

When you are ready to run the benchmark simply issue:

jmeter -n -t benchmark.jmx -Jhost=beepaux03.mmm.com -Jbase_path=/ -Jusers=100 -Jramp_period=300 -Jreport_save_path=./

Or you can use the GUI version of JMeter and get the fancy charts and graphs. You just need to enable the charts and set the paramters. I'm a rookie at JMeter so I'm sure I have lots to learn.

Here are the OSX instructions for those using homebrew:
brew install jmeter
wget http://jmeter-plugins.org/downloads/file/JMeterPlugins-Standard-1.2.0.zip
wget http://jmeter-plugins.org/downloads/file/JMeterPlugins-Extras-1.2.0.zip
unzip JMeterPlugins-Extras-1.2.0 
yes | cp -R JMeterPlugins-Extras-1.2.0/lib /usr/local/Cellar/jmeter/2.11/libexec/lib
yes | cp -R JMeterPlugins-Standard-1.2.0/lib /usr/local/Cellar/jmeter/2.11/libexec/lib

By default the distribution is as follows:

  • Browsing, adding items to a cart and abandoning the cart: 62%
  • Just browsing: 30%
  • Browsing, adding items to a cart and checking out as a guest: 4%
  • Browsing, adding items to a cart and checking out as a registered customer: 4%.

If your interested the inter-workings on the Magento JMeter script there is a detailed break down here.

Also to note: The JMeter java configuration comes with 512 Mo and very little GC tuning. First ensure you set -Xmx option value to a reasonable value regarding your test requirements. Then change MaxNewSize option in jmeter file to respect the original ratio between MaxNewSize and -Xmx.

vi /usr/local/Cellar/jmeter/2.11/libexec/bin/jmeter
# change head param to increase memory
HEAP="-Xms1G -Xmx3G"

And now for the results you have been waiting for:

HHVM / Nginx / Percona

alt text alt text

HHVM / Nginx / Redis / Percona

alt text


PHP 5.5.18 / Nginx / Percona (not using HHVM)

alt text alt text


Charted together (Not using Redis)

alt text


PHP 7 (PHP-NG) / Nginx / Percona

alt text

PHP 7 (PHP-NG) / Nginx / Redis / Percona

I was asked by Andi Gutmans if I was going to do this with PHP 7. Here is the results using the same config. I would be happy to further tune my PHP configuration if needed but it looks like PHP 7 and HHVM 3.2 and really close! Great work by both teams. alt text

Everything charted together

alt text alt text

For thoose interested here is how I built PHP 7 on RHEL 6.5:

# remove any existing PHP installations
sudo yum remove php*
Install correct version of bison

Since the distro package of bison1 isn’t the correct version, you need to build it from source.

mkdir ~/tmp
cd ~/tmp
wget http://ftp.gnu.org/gnu/bison/bison-2.4.tar.gz
tar -xzf bison-2.4.tar.gz
cd bison-2.4
./configure
make && make install
cd ../
Install pre-reqs:
yum install -y bzip2-devel curl-devel libjpeg-devel libpng-devel libXpm-devel gmp-devel libc-client-devel freetype-devel t1lib-devel.x86_64 libmcrypt-devel.x86_64 recode-devel libxml2-devel mysql-devel aspell-devel
Clone PHP 5.7 repo and build from source:
kdir ~/tmp
cd ~/tmp
git clone http://git.php.net/repository/php-src.git 
cd php-src
git branch phpng origin/phpng
git checkout phpng
./buildconf
./configure \
    --with-config-file-path=/etc \
    --enable-mbstring \
    --enable-zip \
    --enable-bcmath \
    --enable-pcntl \
    --enable-ftp \
    --enable-exif \
    --enable-calendar \
    --enable-sysvmsg \
    --enable-sysvsem \
    --enable-sysvshm \
    --enable-fpm \
    --enable-wddx \
    --enable-soap \
    --with-mcrypt \
    --with-curl \
    --with-iconv \
    --with-gmp \
    --with-pspell \
    --with-gd \
    --with-jpeg-dir=/usr \
    --with-png-dir=/usr \
    --with-zlib-dir=/usr \
    --with-xpm-dir=/usr \
    --with-freetype-dir=/usr \
    --with-t1lib=/usr \
    --enable-gd-native-ttf \
    --enable-gd-jis-conv \
    --with-openssl \
    --with-libdir=lib64 --with-mysql \
    --with-pdo-mysql \
    --with-gettext=/usr \
    --with-zlib=/usr \
    --with-bz2=/usr \
    --with-recode=/usr
Verify the install:
php -v
# PHP 7.0.0-dev (cli) (built: Nov 11 2014 10:37:43)
# Copyright (c) 1997-2014 The PHP Group
# Zend Engine v2.8.0-dev, Copyright (c) 1998-2014 Zend Technologies
Create (Copy over) the PHP-FPM startup script that comes with the PHP source.
sudo cp ~/tmp/php-src/sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm
chmod 755 /etc/init.d/php-fpm
Edit the defaults:
vi /etc/init.d/php-fpm
prefix=
exec_prefix=

php_fpm_BIN=/usr/local/sbin/php-fpm
php_fpm_CONF=/usr/local/etc/php-fpm.conf
php_fpm_PID=/var/run/php-fpm.pid
Copy over the PHP-FPM config

Make any edits you need to php-fpm.conf file. By default it listens on port 9000.

mv /usr/local/etc/php-fpm.conf.default /usr/local/etc/php-fpm.conf
PHP configuration

Here is the settings I used in my /etc/php.ini

[PHP]
engine = On
short_open_tag = On
asp_tags = Off
precision = 14
y2k_compliance = On
output_buffering = 4096
zlib.output_compression = Off
implicit_flush = Off
unserialize_callback_func =
serialize_precision = 100
allow_call_time_pass_reference = Off
safe_mode = Off
safe_mode_gid = Off
safe_mode_include_dir =
safe_mode_exec_dir =
safe_mode_allowed_env_vars = PHP_
safe_mode_protected_env_vars = LD_LIBRARY_PATH
disable_functions =
disable_classes =
expose_php = On
max_execution_time = 90
max_input_time = 120
memory_limit = 512M
max_input_vars = 25000
error_reporting = E_ALL & ~E_DEPRECATED
display_errors = Off
display_startup_errors = Off
log_errors = On
log_errors_max_len = 1024
ignore_repeated_errors = Off
ignore_repeated_source = Off
report_memleaks = On
track_errors = Off
html_errors = Off
variables_order = "GPCS"
request_order = "GP"
register_globals = Off
register_long_arrays = Off
register_argc_argv = Off
auto_globals_jit = On
post_max_size = 64M
magic_quotes_gpc = Off
magic_quotes_runtime = Off
magic_quotes_sybase = Off
auto_prepend_file =
auto_append_file =
default_mimetype = "text/html"
doc_root =
user_dir =
enable_dl = Off
file_uploads = On
upload_max_filesize = 64M
allow_url_fopen = On
allow_url_include = Off
default_socket_timeout = 90
realpath_cache_size = 128k
realpath_cache_ttl = 86400

[Pdo_mysql]
pdo_mysql.cache_size = 2000

[MySQL]
mysql.allow_persistent = Off
mysql.max_persistent = -1
mysql.max_links = -1
mysql.default_port =
mysql.default_socket =
mysql.default_host =
mysql.default_user =
mysql.default_password =
mysql.connect_timeout = 60
mysql.trace_mode = Off


[Session]
session.save_handler = files
session.save_path = "/var/lib/php/session"
session.use_cookies = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
session.bug_compat_42 = Off
session.bug_compat_warn = Off
session.referer_check =
session.entropy_length = 0
session.entropy_file =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.hash_function = 0
session.hash_bits_per_character = 5
url_rewriter.tags = "a=href,area=href,frame=src,input=src,form=fakeentry"

[soap]
soap.wsdl_cache_enabled=1
soap.wsdl_cache_dir="/tmp"
soap.wsdl_cache_ttl=86400

zend_extension=opcache.so
opcache.enable_cli=1
opcache.save_comments=0
opcache.fast_shutdown=1
opcache.validate_timestamps=1
opcache.revalidate_freq=60
opcache.use_cwd=1
opcache.max_accelerated_files=100000
opcache.max_wasted_percentage=5
opcache.memory_consumption=128
opcache.consistency_checks=0

Whats next

I will be upgrading to HHVM 3.3 and re-running the tests later in the week. I couldn't find a repo that contained HHVM 3.3 so I may build from source. Like I said if you find any inconsistancy I'm open to trying to tune further or make changes in my config.


Working around incompatabilities with HHVM

Let's say you have an extension that is Ioncube encoded. I'm looking at your Unirgy :) You want it to work but it can't use HHVM or lets say you have an issue with an incompatiable SOAP call or PHP extension. You can get around the issues by tricking out your Nginx configuration to serve pages that are of certain URI on PHP instead of HHVM. Here is an example:

location ~ \.php$ { ## Execute PHP scripts

  if (!-e $request_filename) { rewrite / /index.php last; }

  set $use_hhvm 1;
  if ($request_uri ~ ^/(index.php/admin/sales_order|index.php/urapidflowadmin)) {
    fastcgi_pass 127.0.0.1:9000;
    set $use_hhvm 0;
  }

  expires off; ## Do not cache dynamic content

  if ($use_hhvm = 1) {
    fastcgi_pass unix:/var/run/hhvm/sock;
  }

  fastcgi_index index.php;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  include fastcgi_params; ## See /etc/nginx/fastcgi_params

  fastcgi_keep_conn on; #hhvm param
}

As you can see I used a pipe "|" beween the URLS I want to use plain PHP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment