Wednesday, February 8, 2012

Read/Write splitting in Doctrine 2.x via mysqlnd_ms

I'm currently working on a project that processes a fair bit & can greatly benefit from load-balancing due the hugeness of the queries being processed.


My set-up is as follows:
1x web-server (production)
1x database server (production)


mirror of the same
1x web-server (backup)
1x database server (backup) -- slave of the master via replication.


Now, let's not let this perfectly good 'backup' database server go to waste!


1. you need the remi repository
2. yum remove php-mysql
3. yum install pecl-mysqlnd_ms.x86_64 --enablerepo=remi


4. edit the /etc/php.d/mysqlnd_ms.ini:

; Enable mysqlnd_qc extension module
extension=mysqlnd_ms.so
mysqlnd_ms.enable = 1
mysqlnd_ms.force_config_usage = 1
mysqlnd_ms.ini_file = "/etc/php.d/mysqlnd_ms.json"
mysqlnd_ms.collect_statistics = 1
;mysqlnd_ms.multi_master = 0
mysqlnd_ms.disable_rw_split = 0
error_log=/tmp/php_errors.log
mysqlnd_ms.lazyConnection = 0

5. edit or create /etc/php.d/mysqlnd_ms.json
{
    "project_name": {
        "master": {
            "master_0": {
                "host": "master_server_IP",
                "port": "3306",
                "db": "your_project",
                "user": "write",
                "password": "write_password"
            }
        },
        "slave": {
                "slave_0": {
                        "host": "someIP",
                        "port": "3306",
                        "db": "your_project",
                        "user": "read_user",
                        "password": "read_password"
                },
         "slave_1": {
                        "host": "someIP",
                        "port": "3306",
                        "db": "your_project",
                        "user": "read_user",
                        "password": "read_password"
                }

        },
        "filters" : [ "roundrobin"]
    }
}


6. Note, I'm enforcing config usage, which basically means that the hostname in my application must match the group listed in my json configuration file. The nice thing about this is that now my username & password are not stored in code. mysqlnd_ms will sort that out. Just set it to match up with your /etc/php.d/mysqlnd_ms.json's project_name. (in this case literally 'project_name').



// application.ini

; Database configuration
resources.doctrine.dbal.connections.default.parameters.driver   = "pdo_mysql"
resources.doctrine.dbal.connections.default.parameters.host = "project_name"





Now, getting Doctrine to work ... *this was not intuitive*:


"Warning: PDOStatement::execute(): (mysqlnd_ms) string escaping doesn't work without established connection in /data01/www/your_project/library/Doctrine/DBAL/Connection.php on line 613"


There are two workarounds to this. I'm not really happy with either, but here they are:


Option 1: disable lazy_connections. the downside to this is that a connection is opened to EVERY SINGLE database server listed.


Option 2: perform a single select 1 query into the database during bootstrapping.

// INSIDE application/bootstrap.php -- note I'm using ZF-boilerplate.
public function _initDoctrineConfiguration()
{
   $this->bootstrap('Doctrine');
   $connection = \Zend_Registry::getInstance()->get('em')->getConnection()->getWrappedConnection();
   $connection->query("select 1");  
}


Option 2 is definitely my preferred route because only 1 connection must be opened, and it will probably go to a slave server anyways. 






Tuesday, December 13, 2011

timezoning - PHP, MySQL, System time

This post is about considering the potential impacts of timezones in php applications.
PHP Requires a timezone to be set in php.ini.

By default, MySQL reads the timezone settings from /etc/localtime.

Admins frequently set the server's localtime to wherever the server is physically located. *bad practice IMHO*.

Why does this even matter? If you are attempting to build an application that may be geo-redundant or rely on external data sources, standardizing times will become increasingly important and potentially confusing unless a policy is put in place that 'everything is coordinated around a single timezone.

Having servers configured with different timezones will result in confusion & bring in potential data duplication (e.g. importing logs from different services), or when systems shift data from a server with X timezone to a server with Y timezone.

Some thoughts:
1. sudo ln -s /usr/share/zoneinfo/UTC /etc/localtime
2. [php.ini]
date.timezone = "UTC"
3. [mysql]
default settings are OK as it reads from SYSTEM (points to /etc/localtime).

When the time comes to globalize & localize an application then all data saved into the database should be saved in UTC. When it comes time to present time-related data (depending on the end-use), convert it into the user's localtime based on their profile's configuration.

Monday, December 12, 2011

The easy way to configure a server

Over the past year or so I've been working on a variety of projects that have a few things in common... Dependencies! Some of those dependencies include things like web-servers (NGINX or Apache HTTPD), a Database (typically MySQL), sometimes I need Gearman client support (0.21+) or a GearmanD server, and frequently I want to have jobs auto checked to ensure that they stay running or restart if they fail (gearman workers, etc). Getting all of these up and running can be a royal PITA.

In order to give credit where credit is due, I got inspired from the centmin project and the derivative work centmin-mod. However, I didn't like that it just built everything in one go and required me to edit the main script to pick what I wanted to install. Furthermore, I really wanted each application's install/configuration scripts to belong in its own file in order to simplify long-term maintenance of this script.

So I decided to make my own install-script(s) where I can just run my menu.sh script as a user with elevated permissions and select which individual apps I need to install without having to worry about uninstalling things that I don't actually need as I did in centmin & centmin-mod.

I make no guarantee that this will work for others out there, but it should certainly give anyone a good headstart when it comes to getting your server built out.

Some things to keep in mind:
1. I do my development on a windows machine & my commits didn't save my line endings in a way that my linux box liked. You will probably need to perform the following commands in order to be good to go:
a. download the Thesis Planet Linux, MySQL, NGINX, PHP-FPM + extras installation script from github
b. sudo yum install dos2unix
c. extract the file (tar -xzvf tplmnp.tar.gz)
dos2unix menu.sh
dos2unix core/*.inc
dos2unix core/functions/*.inc

After that all you need to do is run
sudo sh menu.sh

And pick out what you want to install!

I based this off of a clean CentOS 6.0 x86_64 build to replicate what my fresh VM's look like after I finish installing the OS, add a user and do a yum upgrade -y.

Saturday, December 10, 2011

The importance of time accuracy when working with AWS

Recently, I noticed that Gearman tasks were strangely starting to fail on a project I've been working on for quite some time. The errors being logged were simply, "Bucket not found" from the AWS PHP SDK. Now, this is an odd error especially considering that at the very same time that this error was arising, I was looking right at my S3 bucket in CloudBerryS3 Explorer.  So I decided to put a simple test together to see what was going on.


[code]
<?php
require_once('init.php');
$cloud = new \App\Service\Core\Cloud();

$s3 = new \AmazonS3();
            $bucket = \Zend_Registry::getInstance()->get('cloud')->aws->BUCKET;

$exists = $s3->if_bucket_exists('MyBucket');
                while (!$exists)
                {
                        // Not yet? Sleep for 1 second, then check again
                        sleep(1);
                        $exists = $s3->if_bucket_exists($bucket);
echo "$bucket does not yet exist.\n";
//var_dump($s3->get_bucket_headers($bucket));
                }
if ($exists) {
echo "bucket exists.";
}
[/code]


All that was coming back was that my bucket did not exist.

After doing a bit of tinketing, I ran another command (list all of the buckets associated with this account).
 I finally got a useful error (instead of just a 'bucket doesn't exist') / with a 403 being sent (access denied):
This time i go
code => RequestTimeTooSkewed
 Message => The difference between the request time and the current time is too large.





The Virtual Machine that this is running on does have NTP running and synchronizes with the global NTP server pools; however, the Host that this VM sits on apparently has a nasty little bug that results in it switching what the current time is being reported to my VM. The 'new time' (/sbin/hwclock) being reported would just adjust ... by hour or days. Unfortunately, I simply can't afford my timestamps, logs, and SSL errors resulting from significant temporal deltas. So I'm going to have to trash this current VM in the older rec of the Joyent cloud IaaS. (My other VM's on Joyent don't have a problem as they are on their 'new cloud', just this one).

Thus begins the process of building out a new VM with my various customizations. Stay tuned, as I'm building out a nice little set of scripts that help simplify getting up and running on CentOS 6.0

Linux (CentOS 6.0+) MySQL, NGINX, PHP-FPM configuration scripts

Tuesday, October 18, 2011

NGINX + PHP-FPM + Zend_Framework Infinite 30x redirect loop

I officially started migrating from Apache to NGINX today.



Old build:Centos 5.6 (x64) + Apache + MySQL + PHP
New build: Centos 5.6 (x64) + NGINX + PHP-FPM + MySQL

Lesson learned: installing PHP-FPM + NGINX on Centos 5.6 is painful ... until I had my hands on a rather awesome NGINX + PHP-FPM + MariaDB (MySQL) installler script.


After a good number of hours diagnosing what I thought were nginx.conf configuration issues, I finally discovered (with some help from an individual from #nginx on irc.freenode.net) the root cause to today's challenge -- a looping 30x redirect was caused by 1. A lack of $_SERVER['HTTPS'] being sent by NGINX
2. The fact that my application does HTTPS enforcement depending on which module/controller/action a user is accessing. Given that the HTTPS parameter wasn't sent, the SSL validation module rightly assumed that the request was insecure & attempted to send a 30x redirect to the user. The Firefox Firebug plugin was handy here.

In the library/App/Controller/Plugin/SSL class, I have a nice little script that leverages Zend_Controller_Request_Http's isSecure() function to determine if a particular connection is secure. After doing some digging I discovered that particular function relies on the server variable $_SERVER['HTTPS'] to either be set to  'on', or not be set  at all(in the case that the user is using non-secure HTTP.


As it currently stands, here is my development ssl_default.conf (non-production, so don't assume it's secure) file.

A few things to note:
1. the fastcgi_params. I'll amend this post as I discover what else ZF relies on to properly function that Apache just provides.
 2. Leveraging PHP-FPM via  a socket & not the normal TCP port. As a side-note, NGINX seems substantially snappier even when factoring in the latency to the cloud-server than requests to a VM (on my own network) running the very same application on Apache. +1 NGINX!
3. in my non-SSL server config, I have removed the "fastcgi_param HTTPS on;" directive.


server {
listen       443;
server_name  localhost;


#SSL
 ssl                  on;
    ssl_certificate      /usr/local/nginx/conf/ssl/website.com.crt;
    ssl_certificate_key  /usr/local/nginx/conf/ssl/website.com.key.nopass;
    keepalive_timeout    70;

##EOSSL


root /var/www/website/public;
location / {
#                root   /var/www/website/public;
                index  index.php index.html index.htm;

        location ~ /\. {
                deny  all;
        }

        location ~* \.(ico|css|js|gif|jpeg|jpg|png)(\?[0-9]+)?$ {
                expires 7d;
                break;
        }

        ##Serve PHP
        location ~ \.php$ {
                fastcgi_pass   unix:/usr/local/nginx/logs/php-fpm.sock;
                ## nginx doesn't pass all the important variables needed by ZF.
                fastcgi_param HTTPS on;
                fastcgi_index  index.php;
                fastcgi_param  APPLICATION_ENV staging;
                fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;  # same path as above
                fastcgi_param  QUERY_STRING       $query_string;
                fastcgi_param  REQUEST_METHOD     $request_method;
                fastcgi_param  CONTENT_TYPE       $content_type;
                fastcgi_param  CONTENT_LENGTH     $content_length;
                fastcgi_param  HTTP_HOST          $http_host;
                fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
                fastcgi_param  REQUEST_URI        $request_uri;
                fastcgi_param  DOCUMENT_URI       $document_uri;
                fastcgi_param  DOCUMENT_ROOT      $document_root;
                fastcgi_param  SERVER_PROTOCOL    $server_protocol;

                fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
                fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

                fastcgi_param  REMOTE_ADDR        $remote_addr;
                fastcgi_param  REMOTE_PORT        $remote_port;
                fastcgi_param  SERVER_ADDR        $server_addr;
                fastcgi_param  SERVER_PORT        $server_port;
                fastcgi_param  SERVER_NAME        $server_name;
                fastcgi_param  ENVIRONMENT "development";


                # required if PHP was built with --enable-force-cgi-redirect
                fastcgi_param  REDIRECT_STATUS    200;
        }

        # if the requested file exists, return it immediately
        if (-f $request_filename) {
        break;
        }

        # all other requests go to ZF
       if (!-e $request_filename) {
        rewrite . /index.php$args last;
        }

#        location /gd {
#                auth_basic            "Restricted";
#                auth_basic_user_file  /etc/nginx/htpasswd;
#        }
}



}

Thursday, June 23, 2011

Zend Form - Adding a database driven list of items into a field's multiOptions attribute

This will be a nice short little blog post geared at answering a simple scenario that I was trying to figure out for a little bit.

The scenario:
I want to be able to set a new category to be a child of another in my category edit form. The basic business logic will be applied: exclude the same category, and anything that already has a parent assigned.

Inside the form:
// Add a new function to handle setting the multiOptions for the parentId.

  public function setParentOptions($options){
        $this->getElement('parentId')->setMultioptions($options); 
    }

Inside the controller:

// This function returns an array of objects (array[database Key] = a new object).
$parents = $service->fetchByServiceId(VME_Model_DnsSingleton::getInstance()->getServiceId());
        $parentsArr = array();
        $parentsArr[] = "Top level category";
        foreach ($parents as $key => $object) {
            if ($object->getParentId() != null | $object->getParentId() != 0) {
                if ($key != $this->getRequest()->getParam('id')) {
// Since Zend Form multiOptions expects an array array(optionValue = OptionTitle), we need to reformat the returned values from the Service layer.                
  $parentsArr[$key] = $object->getTitle();
                }
            }
        }
        $form->setParentOptions($parentsArr);

And Voila, the form now takes in the dynamic information provided at the controller layer.

Sunday, May 8, 2011

Migrating from OSCommerce to Magento 1.5.0.1 - Part 01

I'll be revising this article to accommodatecentos 6 shortly

This article is about getting Magento 1.5.0.1 up and running, configuring SSL, as well as some general recommended system requirements for Megento based on my experience.

The end deployment architecture for 1x website with ~ 5,000 products (no customers were imported to keep things clean):
1x web server (Centos 5.5 w/ 1024 MB RAM)
1x db server (Centos 5.5 w/ 256 MB RAM) -- the catalog isn't huge per-se, and it has worked pretty well so far.
SSL Certificates from GoDaddy


The not-so-good - The CentOS VM that I spun up was completely unable to connect to a MySQL server instance running on localhost. The error was a MySQL [1130] type error, which basically means that the host is not allowed to connect. I added various combinations (127.0.0.1, localhost, as well as the fully qualified domain name which happened to be what MySQL was claiming was being connected from).

The solution to this problem was to go with a different cloud provider that had an OS build that actually worked.


Step 1 - Build the web-server:
1. create a new Centos VM using your cloud provider.
2. sign in as root.
3. Update the existing OS to centos 5.5 (Yum upgrade). Reboot.
4. install packages (yum install nano httpd httpd-devel php53 php53-common php53-gd php53-xml php53-mysql php53-pdo php53mcrypt mysql mod_ssl)

Step 2 - Start configuring the web-server & set up SSL
1. get rid of the default virtual host in /etc/httpd/conf/httpd.conf (comment everything from ServerName www.exmaple.com:80 up until <IfModule mod_userdir.c>
2. create new directories in /etc/httpd/ (mkdir includes, mkdir ssl, mkdir virtualhosts)
3. copy the ssl certificate files into /etc/httpd/ssl/*
4. nano /etc/httpd/includes/ssl.conf


<IfModule mod_ssl.c>
SSLSessionCache         dbm:/var/run/ssl_scache
SSLSessionCacheTimeout  300
SSLMutex  file:/var/run/ssl_mutex
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
SSLProtocol all -SSLv2
SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:!SSLv2:+EXP:+eNULL

<Files ~ "\.(cgi|shtml|phtml|php?)$">
SSLOptions +StdEnvVars
</Files>

SetEnvIf User-Agent ".*MSIE [456].*" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0
AddType application/x-x509-ca-cert .crt
AddType application/x-pkcs7-crl    .crl
SSLCACertificatePath   /etc/httpd/ssl
#SSLCACertificateFile /etc/httpd/ssl/gd_bundle.crt

RequestHeader set X_FORWARDED_PROTO 'https' env=HTTPS
</IfModule>

5. ctrl X then yes to save
6.. add the new virtual host (/etc/httpd/virtualhosts/[yourdomain.com].conf) -- we'll tell httpd.conf to load up *.conf files in /etc/httpd/virtualhosts/)

NameVirtualHost *:80
<VirtualHost *:80>
    ServerName [domain.com]
    ServerAlias [domain.com]
# Directory to get the site from
    DocumentRoot /var/www/html/[domain.com]
    <Directory />
        Options FollowSymLinks
        AllowOverride All
    </Directory>
# Set the directory to be the same as above.
    <Directory /var/www/html/[domain.com]>
        Options Indexes FollowSymLinks MultiViews
        AllowOverride All
        Order allow,deny
        allow from all
    </Directory>
</VirtualHost>
<IfModule mod_ssl.c>
<VirtualHost [IP ADDRESS]:443>
DocumentRoot /var/www/html/[domain.com]
ServerName www.[domain.com]
SSLEngine on
SSLProtocol all -SSLv2
SSLCipherSuite HIGH:MEDIUM
SSLCertificateFile /etc/httpd/ssl/[domain.com].crt
SSLCertificateKeyFile /etc/httpd/ssl/[domain.com].key.nopass
SSLCertificateChainFile /etc/httpd/ssl/gd_bundle.crt
<Directory /var/www/html/[domain.com]>
        SSLRequireSSL
        Options Indexes FollowSymLinks MultiViews
        AllowOverride All
        Order allow,deny
        allow from all
</Directory>
</VirtualHost>

</IfModule>


7.. Save that file & now add the following at the end of /etc/httpd/conf/httpd.conf

# No need to modify. includes proxy headers for ssl
Include includes/ssl.conf

#include the virtual hosts
Include virtualhosts/*.conf

#SSL
SSLRandomSeed startup file:/dev/urandom 2048
SSLRandomSeed connect file:/dev/urandom 2048


Step 3 - Download or Copy over Magento from your old server

1.Download a copy of your magento (or source) directory (if it's on another server's we'll cover that as well... I had to move Magento over 3 different servers). and extract it to /var/www/html/[domain.com]

make sure that all of your configuration files [domain.com] match up to your real domain of course ;-)

2. either install magento if it's a new install.
If it's an old install (rm -rf /var/www/html/[domain.com]/var/) -- CRITICAL to kill all of your cached junk.

3. connect to your database server, open up the core_config_data table in the magento database that you specified. Change the address of the following fields to your new address: (web/unsecure/base_url, and web/secure/base_url).

4. Change the ownership of the entire magento directory to apache. -- This solves all kinds of permission problems related to Magento (and Magento Connect).
chown -R apache /var/www/html/[domain.com]
chgrp -R apache /var/www/html/[domain.com]


Step 4 - Configure a basic firewall
1. set up IPTables
mv /etc/init.d/iptables /etc/init.d/iptables.original
nano /etc/init.d/iptables
Paste in the results of the firewall generator [ http://easyfwgen.morizot.net/gen/]
(static ip, web server + ssl + SSH).
save the file.
chmod +x /etc/init.d/iptables
/sbin/chkconfig/iptables --level 235 on

Step 5 - Test and figure out what's broken
1. e-mail still doesn't work on Magento, so install the SMTP Mailer extension
2. Mcrypt is apparently not yet included in the php53 set of installers...

yum install php53-devel yum install libmcrypt-devel
wget http://mx2.php.net/get/php-5.3.3.tar.bz2/from/us3.php.net/mirror
tar xvjf php-5.3.3.tar.bz2
compile the code:
cd php-5.3.3/ext/mcrypt/
phpize
aclocal
./configure
make
make test
make install
Create configuration file /etc/php.d/mcrypt.ini:
nano /etc/php.d/mcrypt.ini
extension=mcrypt.so
Restart apache:
/etc/init.d/httpd restart
Create a file with phpinfo just to check if the extension was loaded:
<?php
phpinfo();
?>
 
Step 6 - Add new users, get rid of Root access over SSH
1.useradd (username)
2. passwd username
3. Add the user to the sudoers list (/etc/sudoers)
4. test SSH access & sudo capability for the user.
5. disable root login via ssh (/etc/ssh/sshd_config) to improve security a little bit. - permitRootLogin = no