PHP performance III -- Running nginx

Sunday, May 31. 2009
Comments

Since part one and two were uber-successful, here's an update on my Zend Framework PHP performance situation. I've also had this post sitting around since beginning of May and I figured if I don't post it now, I never will.

Disclaimer: All numbers (aka pseudo benchmarks) were not taken on a full moon and are (of course) very relative to our server hardware (e.g. DELL 1950, 8 GB RAM) and environment. The application we run is Zend Framework-based and currently handles between 150,000 and 200,000 visitors per day.

Why switch at all?

In January of this year (2009), we started investigating the 2.2 branch of the Apache webserver. Because we used Apache 1.3 for forever, we never had the need to upgrade to Apache 2.0, or 2.2. After all, you're probably familiar with the don't fix it, if it's not broken-approach.

Late last year we ran into a couple (maybe) rather FreeBSD-specific issues with PHP and its opcode cache APC. I am by no means an expert on the entire situation, but from reading mailing lists and investigating on the server, this seemed to be expected behavior — in a nutshell: Apache 1.3 and a large opcode cache on a newer versions of FreeBSD (7) were bound to fail with larger amounts of traffic.

We tried bumping up a few settings (pv entries), but we just ran into the same issue again and again.

Because the architecture of Apache 2.2 and 1.3 is so different from one another (and upgrading to 2.2 was the proposed solution), I went on to explore this upgrade to Apache 2.2. And once I completed the switch to Apache 2.2, my issues went away.

So far, so good!

Performance?

On the performance side we experienced rather mediocre results.

While we benched that a static file could be read at around 300 requests per second (that is a pretty standard Apache 2.2 install, sans a couple unnecessary modules), PHP (mod_php) performed at a fraction of that, averaging between 20 and 23 requests per second.

Myth: Hardware is cheap(, developer time is not)!

Before some people yell at me for trying to optimize my web server, one needs to take the costs of scaling (to a 100 requests per seconds) into account.

One of those servers currently runs at 2,600.00 USD. The price tag adds up to an additional 10,400.00 USD in order to scale to a 100 (lousy) requests per seconds. Chances are of course, that the hardware is slightly less expensive since DELL gives great rebates — but the 8 GB of (server) RAM and the SAS disks by themselves are melting budgets away.

And on top of all hardware costs, you need add setup, maintenance and running costs (rack space, electricity) for an additional four servers — suddenly, developer time is cheap. ;-)

So what do we do? Nginx to the rescue?!


Continue reading "PHP performance III -- Running nginx"

Cannot send headers; headers already sent in , line 0

Thursday, April 9. 2009
Comments

Yesterday, I started upgrading a test server to Apache 2.2 and PHP 5.2.9 and ran into this is a problem, which then bugged me for a good day. So here's the run down:

My test.php:

<?php
ob_start();
system('file -i -b /path/file.txt');
$contents = ob_get_contents();
ob_end_clean();
var_dump(headers_sent($file, $line), $file, $line);

My test2.php:

<?php
$contents = shell_exec('file -i -b /path/file.txt');
$contents = trim($contents);
var_dump(headers_sent($file, $line), $file, $line);

test.php claims that headers were send, while test2.php shows the expected behavior. The difference is not only system() vs. shell_exec(). But the ob_start(). Apparently headers_sent() returns true right after system() was called. We've used output buffering to trap whatever came back from system and this works on PHP 5.2.6 and 5.2.8, but not on 5.2.9.

Since I was after the response of the shell call, using shell_exec() is what should have been used to begin with — still. This is pretty weird.

Update: I've opened #47973 - Yay, bug fixed!

Defined tags for this entry: ,

Avoiding common pitfalls with Zend_Test

Saturday, March 28. 2009
Comments

Sometimes I think I'm particularly stupid when it comes to learning new things. Well, that, or maybe I'm just the only one complaining enough. ;-)

I feel like I've wasted a great deal of time last week with basics that were nowhere to be found or required debugging to no end. The following is the outcome, a rather random list of things to watch out for when you're starting on Zend_Test.

A general understanding of testing and PHPUnit is more than helpful.

How do you debug?

Did your test not redirect, or did your query assertions go wrong?

Generally, there are two ways:

<?php
// (...)
function testIfTheSunCameUp()
{
    var_dump($this->response, $this->request);
}

Not so pretty, right?

In general I don't understand why it will say, "failed asserting status code 404", and will not tell you what it got instead. It offers you a backtrace so you can go into the class and add var_dump(), but that's hardly useful.

One of the things I love about PHPT is that a failed test case is so descriptive. There's really no debugging a failed test because the process of debugging generally involves gathering information where PHPT hands it over to you right away. But maybe that's a PHPUnit thing.

And of course:

phpunit --verbose AllTests.php

My grudge here is that I'm a total PHPUnit newbie. Before Zend_Test I avoided PHPUnit where I could because I felt that it is a beast — one that's especially hard to tame.

Update: Someone else noticed too, please vote on ZF-6013.

Control your environment

We offer a REST-API to partners and that API is basically MVC and lots of context switching depending on GET, POST, PUT, HEAD, DELETE and a couple parameters. I like to think of it as true REST. ;-)

Since the unit tests are run on the cli, not even GET is set. So make sure to add this to your test:

<?php
// (...)
$this->request->setMethod('GET');

Keep in mind that $_SERVER is not set and if you really happen to rely on anything from that ($this->getServer()), you need to explicitly set it or figure out another smart way to deal with it. In my case, I'm overriding some of these $_SERVER variables in my bootstrap with a config setting when I'm using the "testing" environment.

Update: I've opend ZF-6162, please vote.

The basic AllTests.php

AllTests.php is used to tell phpunit what to do, a simple setup would look like this:

tests/AllTests.php

tests/controllers/AllTests.php

tests/controllers/IndexControllerTest.php

tests/controllers/AllTests.php:

<?php
require_once dirname(__FILE__) . '/IndexControllerTest.php';

class ControllersAllTests
{
    public static function main()
    {
        PHPUnit_TextUI_TestRunner::run(self::suite());
    }

    public static function suite()
    {
        $suite = new PHPUnit_Framework_TestSuite('Controllers');
        $suite->addSuiteCase('IndexControllerTestCase');
        return $suite;
    }
}

tests/AllTests.php:

<?php
(...)
class AllTests
{
    public static function suite()
    {
        $suite = new PHPUnit_Framework_TestSuite('AllTests');
        $suite->addSuite(ControllersAllTests::suite());
        return $suite;
    }
}

Zend_Session_Namespace

Avoid locking session namespaces, they will stab you in the eye. See ZF-6072.

Zend_Dom_Query

I'd suggest you remove all @-operators from Zend_Dom_Query since it doesn't really tell you what is wrong with the response otherwise. I've opened ZF-6142 so this gets fixed.

Slightly related to Zend_Dom_Query is a bug I noticed when you're response is empty. It's a rather trivial issue where null is not the same as an empty string. I've opened ZF-6143 and included a patch.

Bootstrapping the correct way

The most simple way to bootstrap is to create a small class to initialize your application. The class takes an environment parameter to __construct() which allows you to load a different config section or other nifty stuff. I called mine Lagged_App() and the code came originally from Andries Seuten's example ZF application but it evolved over the past years.

I wouldn't recommend his tutorial today (because things changed since 2007), but at the time it was pretty easy and straight forward. Today's bar is the quickstart section in the official manual. But since the quickstart suggests includes, I spent some time cleaning up my by going through said quickstart guide and I posted a quick and dirty example (which I labeled Lagged_Application)) in my subversion repository on Google Code.

A test case is a test case

I'm using the following "base" test case for all my controller tests:

<?php
class Lagged_PHPUnit_ControllerTestCase
    extends Zend_Test_PHPUnit_ControllerTestCase
{
    public function setUp()
    {
        Zend_Session::$_unitTestEnabled = true;

        $bootstrap       = new Bootstrap('testing');
        $this->bootstrap = array($bootstrap, 'start');
        parent::setUp();
    }
}

Clean up

One of the most fundamental things your mother tried to teach you.

This is in most of my tearDown()'s:

public function tearDown()
{
    $this->resetRequest()->resetResponse();
    $this->setQuery(array());
    $this->setPost(array());
}

Avoid direct PHP calls

I'd avoid the following:

  1. Skipping views with echo in the controller (bad, bad, bad ;-)).
  2. Use the Zend_Action_Helper redirector instead of header().
  3. Use Zend_Session etc. vs. session_*() (Don't forget Zend_Session::$_unitTestEnabled = true;).
  4. Don't dispatch always. ;-)
Defined tags for this entry: , , , , ,

Managing software deployments of your PHP applications I

Saturday, January 31. 2009
Comments

Disclaimer: I've been doing mostly PHP and Zend Framework based projects in the past two years, but the information from this article is general and should be applicable to most setups — even to non PHP-based projects (to a certain extent).

Inspired by Padraic's posting spree the other week, here's another attempt to provide you with some hands-on usefulness. I'm all open for all feedback, and sorry for the length!

What is deployment and how do you manage deployments?

Wikipedia says:

Software deployment is all of the activities that make a software system available for use.

... and goes on:

The general deployment process consists of several interrelated activities with possible transitions between them. These activities can occur at the producer site or at the consumer site or both. Because every software system is unique, the precise processes or procedures within each activity can hardly be defined.

In general, there are different approaches to software deployment. Most people are probably not aware of a deployment process at all. They edit files and push it live. In most cases, live (sometimes referred to as production environment) is the webhosting account — for consistency, the environments setup for larger projects also includes a development and staging environment.

Taking the facts into account, we can summarize those efforts into three cases:

  1. Deploym..? I'm a skilled surgeon and shell ninja! I like to edit all my files online!
  2. I have a local WAMP, MAMP or LAMP and then FTP the files online.
  3. We have a defined process and use SVN, PEAR, phing or similar.

Why should I manage deployments?

There are a few reasons as to why it's good for youTM to come up with a release schedule to manage your deployments.

  1. Establishing a release schedule allows you to project the time necessary to implement and test new features and items.
  2. Planning in advance also helps to meet the plan.
  3. A schedule enables code testing (and other QA measures) before it's live. For example, assuming the schedule says to release a new version every two weeks, the net time (ten business days) could be divided up into eight business days for development, and two business days for testing. (Adjust as needed!) Take note — the schedule excludes weekends!
  4. A release schedule helps the development team to avoid all those extra last-minute changes which break things and cause grey hair without feeling bad or guilty. The established practice and process has to be used by all people involved, and developers are not to blame if someone else forgets that.

Continue reading "Managing software deployments of your PHP applications I"

ZendFramework version requirements

Tuesday, December 9. 2008
Comments

As some people noticed, with the release of ZendFramework 1.7, the version requirement was bumped up from 5.1.6 to 5.2.4.

If you really care which ancient version of PHP you should install (vs. 5.2.6, 5.2.7 or 5.2.8 next week ;-)) -- here is a breakdown per component:

Zend_Acl: 5.0.0 
Zend_Amf: 5.0.0 
Zend_Auth: 5.0.0 
Zend_Cache: 5.0.0 
Zend_Captcha: 5.1.0 
Zend_Config: 5.0.0 
Zend_Console: 5.0.0 
Zend_Controller: 5.0.0 
Zend_Currency: 5.0.0 
Zend_Date: 5.1.0 
Zend_Db: 5.1.0 
Zend_Debug: 5.0.0 
Zend_Dojo: 5.0.0 
Zend_Dom: 5.0.0 
Zend_Exception: 4.0.0 
Zend_Feed: 5.1.1 
Zend_File: 5.2.1 
Zend_Filter: 5.1.0 
Zend_Form: 5.0.0 
Zend_Gdata: 5.1.0 
Zend_Http: 5.1.0 
Zend_InfoCard: 5.0.0 
Zend_Json: 5.0.0 
Zend_Layout: 5.0.0 
Zend_Ldap: 5.1.0 
Zend_Loader: 5.1.2 
Zend_Locale: 5.0.0 
Zend_Log: 5.0.0 
Zend_Mail: 5.1.0 
Zend_Measure: 5.0.0 
Zend_Memory: 5.0.0 
Zend_Mime: 5.0.0 
Zend_OpenId: 5.2.0 
Zend_Paginator: 5.0.0 
Zend_Pdf: 5.0.0 
Zend_ProgressBar: 5.0.0 
Zend_Registry: 5.0.0 
Zend_Request: 5.0.0 
Zend_Rest: 5.0.0 
Zend_Search: 5.0.0 
Zend_Server: 5.0.0 
Zend_Service: 5.0.0 
Zend_Session: 5.0.0 
Zend_Soap: 5.0.0 
Zend_Test: 5.0.0 
Zend_Text: 5.0.0 
Zend_TimeSync: 5.0.0 
Zend_Translate: 5.0.0 
Zend_Uri: 5.0.0 
Zend_Validate: 5.1.0 
Zend_Version: 5.0.0 
Zend_View: 5.0.0 
Zend_Wildfire: 5.0.0 
Zend_XmlRpc: 5.0.0

Zend_File is leading the list with 5.2.1 -- according to the mailinglist, the requirement is due to some tests (and essentially PHPUnit) which require 5.2.4. I generated the list using PHP_CompatInfo. I'll share the code later, since it'll go into a bigger project.

Defined tags for this entry: , ,