<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>DeShong.net &#187; Scalability</title>
	<atom:link href="http://www.deshong.net/category/tech/scalability/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.deshong.net</link>
	<description></description>
	<lastBuildDate>Mon, 29 Mar 2010 21:12:45 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0-RC1-15022</generator>
		<item>
		<title>&#8220;Rickroll To Go&#8230;&#8221; ZendCon Session audio posted!</title>
		<link>http://www.deshong.net/2009/07/rickroll-zendcon-session/</link>
		<comments>http://www.deshong.net/2009/07/rickroll-zendcon-session/#comments</comments>
		<pubDate>Thu, 23 Jul 2009 20:23:38 +0000</pubDate>
		<dc:creator>Brian DeShong</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Scalability]]></category>

		<guid isPermaLink="false">http://www.deshong.net/?p=239</guid>
		<description><![CDATA[My ZendCon 2008 talk, “Rickroll To Go With WURFL, PHP, and Other Open Source Tools”, was just released at Zend DevZone as ZendCon Sessions episode #23! If you&#8217;re just now finding my blog from there, welcome! And thanks to Eli White, Community Relations Manager for Zend, for selecting it for posting. You can get all [...]]]></description>
			<content:encoded><![CDATA[<p>My ZendCon 2008 talk, <a href="http://www.deshong.net/2008/09/slides-zendcon-2008-rickroll-to-go/">“Rickroll To Go With WURFL, PHP, and Other Open Source Tools”</a>, was just released at <a href="http://devzone.zend.com/public/view">Zend DevZone</a> as <a href="http://devzone.zend.com/article/4871-The-ZendCon-Sessions-Episode-23-Rickroll-To-Go-With-PHP-WURFL-and-Other-Open-Source-Tools">ZendCon Sessions episode #23</a>!</p>
<p>If you&#8217;re just now finding my blog from there, welcome!  And thanks to <a href="http://eliw.com/">Eli White</a>, Community Relations Manager for <a href="http://zend.com/">Zend</a>, for selecting it for posting.</p>
<p>You can get all of the relevant info using the links below:</p>
<p><a href="http://www.deshong.net/2008/09/slides-zendcon-2008-rickroll-to-go/" target="_new">Slides and videos of the presentation materials</a><br />
<a href="http://devzone.zend.com/article/4871-The-ZendCon-Sessions-Episode-23-Rickroll-To-Go-With-PHP-WURFL-and-Other-Open-Source-Tools" target="_new">ZendCon Sessions page with audio</a><br />
<a href="http://devzone.zend.com/content/audio/zendcon_sessions/zendcon_sessions_podcast_023.mp3" target="_new">MP3 audio of the presentation</a><br />
<a href="http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewPodcast?id=310085132" target="_new">iTunes DevZone podcast</a></p>
<p>Enjoy, and thanks for listening!  Find me on <a href="http://twitter.com/bdeshong">Twitter</a> or <a href="mailto:brianNOSPAM@deshong.net">email me</a> if you&#8217;d like to discuss the materials.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.deshong.net/2009/07/rickroll-zendcon-session/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
<enclosure url="http://devzone.zend.com/content/audio/zendcon_sessions/zendcon_sessions_podcast_023.mp3" length="43557296" type="audio/mpeg" />
		</item>
		<item>
		<title>MySQL replication and the sync_binlog option</title>
		<link>http://www.deshong.net/2009/05/mysql-sync-binlog/</link>
		<comments>http://www.deshong.net/2009/05/mysql-sync-binlog/#comments</comments>
		<pubDate>Thu, 28 May 2009 01:16:13 +0000</pubDate>
		<dc:creator>Brian DeShong</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Scalability]]></category>

		<guid isPermaLink="false">http://www.deshong.net/?p=217</guid>
		<description><![CDATA[Recently I&#8217;ve been focusing on MySQL replication for a project at work. On this particular project, I&#8217;m acting in a Solutions Architect role and have been since about September of 2008. Because of my background in systems administration, I tend to get myself into situations where I become the Schematic-side sys admin on projects. This [...]]]></description>
			<content:encoded><![CDATA[<p>Recently I&#8217;ve been focusing on MySQL replication for a project at <a href="http://www.schematic.com/">work</a>.  On this particular project, I&#8217;m acting in a <a href="http://en.wikipedia.org/wiki/Solutions_Architect">Solutions Architect</a> role and have been since about September of 2008.</p>
<p>Because of my background in systems administration, I tend to get myself into situations where I become the Schematic-side sys admin on projects.  This involves things like deployment processes, getting development, staging, and production environments setup, and now, setting up <a href="http://dev.mysql.com/doc/refman/5.0/en/replication.html">MySQL replication</a>.  This is probably because a) I&#8217;m probably bad at delegating these things to others, b) I&#8217;m kinda&#8217; good at it, and c) let&#8217;s be honest, I&#8217;m a control freak, so I like knowing the servers hosting my apps are setup in a meticulous manner.</p>
<p>In short, we&#8217;re running MySQL 5.0.45 on RedHat Enterprise Linux 5 (I know, I know&#8230;RedHat and MySQL 5.0&#8230;boo&#8230;but it&#8217;s okay).  We&#8217;re required to replicate our production database to a secondary machine for backup purposes.  This way, if our production server dies, we can manually failover to the slave (once we enable writes to it, of course), then swap the two back once the production server is back up.</p>
<p>All in all, this site is rather low-traffic at around 20,000 dynamic page views per day.  Factoring in US-based users in an 11 hour time period, that&#8217;s about 1,800 request per hour, or right around 0.5 page views per second.  We&#8217;ve got a single server in production that&#8217;s acting as our webserver and database server; it&#8217;s got a RAID array in it that was all setup by the group hosting the application (so I don&#8217;t know tons about it, but it&#8217;s new, quality hardware).</p>
<p>I&#8217;m using my master database for reads and writes in Production.  Again, the slave is really only required for live backup-type purposes.</p>
<p>Now, I&#8217;m no expert on MySQL replication, but I&#8217;ve learned a lot these past few weeks.  So I&#8217;m going to share one big caveat here.  Please correct me as you see fit!</p>
<p>MySQL&#8217;s got a <a href="http://dev.mysql.com/doc/refman/5.0/en/replication-options-binary-log.html#sysvar_sync_binlog">sync_binlog</a> configuration option.  You typically set it in my.cnf, and its value is an integer from 0-n.  This value determines how many <a href="http://dev.mysql.com/doc/refman/5.0/en/binary-log.html">binary log</a> writes need to occur before its contents are flushed out of the buffer and onto disk.  With it set to zero, your operating system just determines when the buffer is flushed to disk.</p>
<p>I have a database migration process that copies table structures and their data from PostgreSQL into MySQL, then basically migrates that data into the appropriate tables in the new MySQL instance.  It involves the transformation of a lot of data.  It&#8217;s a sizable, complete data set for 8+ year old system that&#8217;s not the prettiest, best normalized data model in the world.</p>
<p>Per recommendations in <a href="http://www.amazon.com/High-Performance-MySQL-Jeremy-Zawodny/dp/0596003064/ref=sr_1_2?ie=UTF8&#038;s=books&#038;qid=1243472126&#038;sr=8-2">High Performance MySQL</a>, <strong>I had my <code>sync_binlog</code> value set to 1 in Production</strong>.</p>
<p><strong>When I was performing a test migration to Production recently, the process took about 3 hours.</strong>  Wow.  Thanks, MySQL!  It normally takes about 90 minutes in Staging, if that.</p>
<p>In digging around Google and MySQL.com, I found that a non-zero value for <code>sync_binlog</code> causes more disk seeks to flush the binary log to disk.  The benefit of having it set to <code>1</code> is so that every transaction can be written to the binary log, which is then flushed to disk upon commit.  Then, if your server happens to die, the last completed transaction will always be present in the binary log on disk, so you never have to worry about, say, missing a transaction replay on your slaves.  However, this results in a lot more disk activity on your master.</p>
<p>I set <code>sync_binlog</code> to <code>0</code> and re-ran my migration.  It ran in 90 minutes &#8212; <strong>that&#8217;s a 50% performance gain</strong>!  Now, if you do the math, this makes sense.  It&#8217;s one less disk seek and write per-transaction, so this result totally makes sense.  Hooray for numbers, right?</p>
<p>I&#8217;m willing to gamble the integrity of data on my slave for the 50% performance increase.  <small>(remind me of this post in 6 months when I&#8217;m kicking myself over this for some reason, okay?)</small></p>
<p>With no binary logging enabled (i.e. in our dev environment), this process takes about 20 minutes.  This makes sense &#8212; far less disk writes during the process.</p>
<p>Another way to workaround this would be to keep your binary logs on a physically separate disk.  However, I don&#8217;t have that luxury at this point, so that&#8217;s not an option for me.  If I had my druthers, this is how I&#8217;d handle the problem, but&#8230;no dice for now.</p>
<p>Anyways, my main point: if you are willing to gamble with every single transaction being replicated to your slave in the event of a crash, perhaps you can set <code>sync_binlog</code> to <code>0</code>.  If you&#8217;ve got a separate disk to devote to your binary log, by all means, set it to <code>1</code>!  There are other concerns around this are related to battery-backed disk cache, which you can read a bit more about in <a href="http://www.mysqlperformanceblog.com/2006/05/27/jeremy-cole-on-mysql-replication/">Jeremy Cole&#8217;s post</a> on MySQL replication.  You can also see some handy <a href="http://www.mysqlperformanceblog.com/2006/05/19/group-commit-and-xa/">benchmarks</a> that compare MySQL with and without binary logging.</p>
<p>Finally, I&#8217;ll admit this is a bit of a knee-jerk reaction post.  I&#8217;ve done a bunch of research on this, but it&#8217;s not all quite fleshed out in my mind yet.  I get the whole cause and effect in theory, but I haven&#8217;t dug into MySQL source or other materials to <em>really</em> understand what&#8217;s going on behind the scenes.</p>
<p>MySQL replication is a tricky thing.  It&#8217;s great when it works, but understand that there are overhead tradeoffs in using it!  I&#8217;m sure I&#8217;ll learn more in the weeks and months following our launch, so I look forward to sharing more of my successes and/or pains on this.  Comments, feedback, and flames such as &#8220;OMG, you&#8217;re so wrong Brian!&#8221; and &#8220;Brian is a n00b!&#8221; are welcome.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.deshong.net/2009/05/mysql-sync-binlog/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Slides: ZendCon 2008, &#8220;Rickroll To Go&#8230;&#8221;</title>
		<link>http://www.deshong.net/2008/09/slides-zendcon-2008-rickroll-to-go/</link>
		<comments>http://www.deshong.net/2008/09/slides-zendcon-2008-rickroll-to-go/#comments</comments>
		<pubDate>Tue, 16 Sep 2008 17:21:16 +0000</pubDate>
		<dc:creator>Brian DeShong</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Scalability]]></category>
		<category><![CDATA[zendcon08 rickroll php wurfl ffmpeg]]></category>

		<guid isPermaLink="false">http://www.deshong.net/?p=89</guid>
		<description><![CDATA[It&#8217;s the first day of ZendCon 2008! I&#8217;m giving my new talk, &#8220;Rickroll To Go With WURFL, PHP, and Other Open Source Tools&#8221; today at 4:00 PM PST. The slides are below in a variety of formats: PDF (no transitions) PDF (one transition per page) Quicktime movie If you&#8217;re at ZendCon and reading this, be [...]]]></description>
			<content:encoded><![CDATA[<p>It&#8217;s the first day of ZendCon 2008!  I&#8217;m giving my new talk, <a href="http://www.zendcon.com/ZendCon08/public/schedule/detail/123">&#8220;Rickroll To Go With WURFL, PHP, and Other Open Source Tools&#8221;</a> today at 4:00 PM PST.</p>
<p>The slides are below in a variety of formats:</p>
<p><a href="http://brian.deshong.net/talks/2008/zendcon/rickroll_to_go/rickroll_to_go.pdf">PDF (no transitions)</a><br />
<a href="http://brian.deshong.net/talks/2008/zendcon/rickroll_to_go/rickroll_to_go_transitions.pdf">PDF (one transition per page)</a><br />
<a href="http://brian.deshong.net/talks/2008/zendcon/rickroll_to_go/rickroll_to_go.mov">Quicktime movie</a></p>
<p>If you&#8217;re at ZendCon and reading this, be sure to drop on by at 4:00 PM &#8212; it&#8217;s sure to be a ball.  Enjoy!</p>
]]></content:encoded>
			<wfw:commentRss>http://www.deshong.net/2008/09/slides-zendcon-2008-rickroll-to-go/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
<enclosure url="http://brian.deshong.net/talks/2008/zendcon/rickroll_to_go/rickroll_to_go.mov" length="22015503" type="video/quicktime" />
		</item>
		<item>
		<title>Slides from php&#124;tek 2008</title>
		<link>http://www.deshong.net/2008/05/slides-from-phptek-2008/</link>
		<comments>http://www.deshong.net/2008/05/slides-from-phptek-2008/#comments</comments>
		<pubDate>Fri, 23 May 2008 06:03:03 +0000</pubDate>
		<dc:creator>Brian DeShong</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Scalability]]></category>

		<guid isPermaLink="false">http://www.deshong.net/?p=75</guid>
		<description><![CDATA[For those of you interested, I&#8217;ve made the PDFs of my slides from php&#124;tek 2008 available below: The Grown-Up Company&#8217;s Guide to Development Robust Batch Processing with PHP If you have any questions or comments on the materials, feel free to email me (brian at deshong dot net).]]></description>
			<content:encoded><![CDATA[<p>For those of you interested, I&#8217;ve made the PDFs of my slides from <a href="http://tek.phparch.com/">php|tek 2008</a> available below:</p>
<ul>
<li><a href="http://brian.deshong.net/talks/2008/phptek/grown_up_companys_guide_to_development.pdf">The Grown-Up Company&#8217;s Guide to Development</a></li>
<li><a href="http://brian.deshong.net/talks/2008/phptek/robust_batch_processing_with_php.pdf">Robust Batch Processing with PHP</a></li>
</ul>
<p>If you have any questions or comments on the materials, feel free to email me (brian at deshong dot net).</p>
]]></content:encoded>
			<wfw:commentRss>http://www.deshong.net/2008/05/slides-from-phptek-2008/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Speaking at Atlanta PHP, 3/6/2008!</title>
		<link>http://www.deshong.net/2008/03/speaking-at-atlanta-php-362008/</link>
		<comments>http://www.deshong.net/2008/03/speaking-at-atlanta-php-362008/#comments</comments>
		<pubDate>Wed, 05 Mar 2008 02:46:11 +0000</pubDate>
		<dc:creator>Brian DeShong</dc:creator>
				<category><![CDATA[Georgia]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Scalability]]></category>

		<guid isPermaLink="false">http://www.deshong.net/?p=69</guid>
		<description><![CDATA[For you Atlantans that may read my blog, I&#8217;ll be speaking at this week&#8217;s Atlanta PHP meetup. Specifically, I&#8217;ll be presenting on &#8220;Robust Batch Processing with PHP,&#8221; which I&#8217;m also slated to present at php&#124;tek 2008 in May. Also, special thanks to all Atlanta PHP attendees for allowing me to use them as my guinea [...]]]></description>
			<content:encoded><![CDATA[<p>
For you Atlantans that may read my blog, I&#8217;ll be speaking at this week&#8217;s <a href="http://www.atlantaphp.org/">Atlanta PHP</a> meetup.  Specifically, I&#8217;ll be presenting on <a href="http://www.deshong.net/?p=64">&#8220;Robust Batch Processing with PHP,&#8221;</a> which I&#8217;m also slated to present at <a href="http://tek.phparch.com/">php|tek 2008</a> in May.
</p>
<p>
Also, special thanks to all Atlanta PHP attendees for allowing me to use them as my guinea pigs before taking new talks out to conferences.  You regulars know that I&#8217;ve done this with <a href="http://www.deshong.net/?p=31">other talks</a> <a href="http://www.atlantaphp.org/archive/71">in the past.</a>  You&#8217;re a very gracious, brave bunch, so thanks, everyone!  <img src='http://www.deshong.net/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley' />
</p>
<p>
See you all there!</p>
]]></content:encoded>
			<wfw:commentRss>http://www.deshong.net/2008/03/speaking-at-atlanta-php-362008/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Robust Batch Processing with PHP (part 1/2)</title>
		<link>http://www.deshong.net/2007/11/robust-batch-processing-with-php-part-12/</link>
		<comments>http://www.deshong.net/2007/11/robust-batch-processing-with-php-part-12/#comments</comments>
		<pubDate>Thu, 22 Nov 2007 04:53:20 +0000</pubDate>
		<dc:creator>Brian DeShong</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Scalability]]></category>

		<guid isPermaLink="false">http://www.deshong.net/?p=63</guid>
		<description><![CDATA[I submitted a proposal for php&#124;tek 2008 entitled &#8220;Robust Batch Processing with PHP.&#8221; Granted, the schedule has not been posted yet, so I don&#8217;t know if my talk has even been accepted, but I wanted to formulate some thoughts around the topic for a long-overdue blog post. So, first things first: what is batch processing? [...]]]></description>
			<content:encoded><![CDATA[<p>I submitted a proposal for <a href="http://tek.phparch.com/">php|tek 2008</a> entitled &#8220;Robust Batch Processing with PHP.&#8221;  Granted, the schedule has not been posted yet, so I don&#8217;t know if my talk has even been accepted, but I wanted to formulate some thoughts around the topic for a long-overdue blog post.</p>
<p><b>So, first things first: what is batch processing?</b></p>
<p>Let&#8217;s look at some <a href="http://en.wikipedia.org/">Wikipedia definitions on the topic</a>:</p>
<p><i>&#8220;Batch processing is execution of a series of programs (&#8220;jobs&#8221;) on a computer without human interaction.&#8221;</i></p>
<p>&#8230;and&#8230;</p>
<p><i>&#8220;Batch jobs are set up so they can be run to completion without human interaction, so all input data is preselected through scripts or commandline parameters. This is in contrast to interactive programs which prompt the user for such input.&#8221;</i></p>
<p>So, no human interaction, which means they&#8217;re generally running out of a scheduler, such as crond.</p>
<p>In this post, I&#8217;m going to talk about batch processing in relation to web-based applications.  Given this, what are some examples of common batch processing used in web-based applications?  Here are a few:</p>
<ul>
<li>Sending of emails
<li>Video transcoding
<li>Generating image thumbnails
<li>Communication with third-party services
<li>Processing post-authorization of credit/debit card and online check transactions
</ul>
<p>&#8230;just to name a few.  Typically, tasks such as these would be done with PHP scripts run from the command line, either interactively or from a scheduler such as cron or at.  In some cases, you may even go so far as to set aside a dedicated machine (or machines) to perform these operations (this is my preferred method).</p>
<p><b>&#8220;Why would I want to do any of these things in batch?&#8221; you ask?  Here are some reasons:</b></p>
<ul>
<li>To keep front end webservers doing what they do best: serving requests!
<li>To allow for graceful handling of failure in the form of retrying the operation against a third-party vendor.  For example, if your credit card processing vendor is down for maintenance, but you want to post-authorize credit card payments, you want to wait a little bit and try again.  This is easiest done in a batch process.  The alternative would be to, say, charge the user during their actual HTTP request and retry over and over until the post-authorization request completed.  This is a lousy user experience.
<li>Sending emails from front end webservers is just silly and wasteful.  Why make SMTP connections from these webservers?  Send emails in batch on the back end so you can handle failure cases, hard and soft bounces of messages and so on.
</ul>
<p>Batch processing is tricky because it&#8217;s non-interactive.  Jobs such as the examples above will run at least once a day, and more often than not, they&#8217;ll run every few minutes or hours.  Maybe you only post-authorize credit cards every six hours, but you would definitely transcode videos around or send emails all throughout a day in order to keep your site &#8220;living.&#8221;</p>
<p><b>What are some of the challenges with batch processing?</b></p>
<ul>
<li>Developers need to be made aware of problems
<li>Processing needs to be retried if any sort of failure occurred
<li>Detailed logs of job executions must be kept so developers can investigate failures and successes; you should leave a full audit trail to anyone can track down the lifecycle of processing
<li>These batch jobs should be easy for developers to develop.  Imagine duplicating logging code across all of your batch processes &#8212; you don&#8217;t want to repeat yourself!
</ul>
<p>Point here is that if any of your processes is failing, your developers should be made aware of it immediately, or at least sometime shortly after the failure.  How do we handle these requirements?</p>
<p><b>Define error levels</b></p>
<p>First, what are the different types of errors that we have?  Well, in my experience, they&#8217;re similar to the Syslog priority levels.  These are made available in PHP for use with <a href="http://php.net/trigger_error">trigger_error()</a> using some <a href="http://us2.php.net/manual/en/ref.errorfunc.php#errorfunc.constants">pre-defined constants</a>.</p>
<p>Out of these pre-defined constants, you have these main levels:</p>
<ul>
<li>debug
<li>info
<li>notice
<li>warning
<li>error
<li>fatal
</ul>
<p>What do we do with errors of these levels?  <b>Let&#8217;s say that developers should only be emailed for anything warning or above.</b>  Anything else should just be written to the log.</p>
<p><b>Making developers aware of failures</b></p>
<p>When you think of the best way to notify developers of problems during processing, what comes to mind first?  &#8230; &#8230; &#8230; what was that?  Email?  Yes, email.  So, those warning-level error messages just just spoke about&#8230;all of those should be emailed to the developer at the completion of the process.</p>
<p>Now, it&#8217;s not the only option, but it may be the most obvious.  If your transaction to post-authorize a credit card fails, your developers should be made aware of it right away so someone can contact the vendor, or, say, identify firewall issues in your environment.  Similarly, if your video transcoding server(s) is/are down, videos can&#8217;t be transcoded &#8212; someone needs to be made aware of that!  Let&#8217;s email them.</p>
<p>Let&#8217;s take this rough example code:</p>
<pre>
$config = array('foo' => 'bar', 'baz' => 'bop'); // Config options
$batch = Batch::getInstance($config);
$vendor = Some_Billing_Processor::factory();
$accounts = Foo::getAccountsForPostAuth();

foreach ($accounts as $account) {
    $accountId = $account->getId();
    $amountToBill = Foo::getPreAuthorizationAmount($account);

    try {
        if ($vendor->postAuthorize($account)) {
            $batch->info("[$accountId] billed $amountToBill");
        } else {
            $batch->warning("[$accountId] failed to bill $amountToBill");
            // Record failure so processing can be retried
        }
    } catch (Vendor_Exception $e) {
        $batch->error(
            "[$accountId] caught exception during vendor communication");
        // Record failure so processing can be retried
    }
}
</pre>
<p>In this case, we&#8217;ve raised a warning for the failed post-auth, and we raised an error if an exception was caught (i.e. inability to connect to the vendor&#8217;s service).  The info() call won&#8217;t result in an email to the developer, though, but that message will be logged.  Bottom line&#8230;if we can&#8217;t take the customer&#8217;s money, a developer needs to address the situation soon.</p>
<p>In the cases of the warning and error, these log entries will be emailed to the error email recipient(s) upon completion of the script.</p>
<p>Another alternative here would be to never email warnings when they occur, but write a separate script that parses logs for warnings, rolls them up into one message body, and emails the developers every few hours.  This keeps the email traffic down, and ultimately keeps your developers from thinking that their back end scripts &#8220;cry wolf&#8221; by being too chatty.</p>
<p><b>What about those logs you speak of?</b></p>
<p>Everyone&#8217;s been in a situation where they have a single directory full of log files.  These can be either foo.log, foo.log.1, or even foo.log.20071114 &#8230;or any number of naming conventions.  Even worse is a <i>single</i> log file for a process that just grows and grows.  Log rotation is an easy fix for these scenarios.</p>
<p>Personally, I feel that this is bad practice.  I tend to prefer date-based directory names for storing log files.  In my opinion, planning for this from the start of your project is far better than having to react in a knee-jerk fashion later on once you&#8217;ve filled a directory or reached some sort of maximum file size limit on your filesystem.  Consider this directory and the files in it:</p>
<pre>
/var/log/cc_auth
    pre_auth.log.20071114
    post_auth.log.20071114
    pre_auth.log.20071115
    post_auth.log.20071115
    pre_auth.log.20071116
    post_auth.log.20071116
    pre_auth.log.20071117
    post_auth.log.20071117
    pre_auth.log.20071118
    post_auth.log.20071118
</pre>
<p>Messy, right?  In this example, you end up with a few downsides:</p>
<ul>
<li>A lot of files in each directory
<li>Potential to hit Unix max files per directory limit (on ext2 and some older/other filesystems)
<li>Date-based filenames are cumbersome to type (or even auto-complete in your Unix shell)
</ul>
<p>Personally, I prefer a structure using date-based directories like so:</p>
<pre>
/var/log/cc_auth/2007/11/14
    pre_auth.log
    post_auth.log
/var/log/cc_auth/2007/11/15
    pre_auth.log
    post_auth.log
/var/log/cc_auth/2007/11/16
    pre_auth.log
    post_auth.log
/var/log/cc_auth/2007/11/17
    pre_auth.log
    post_auth.log
/var/log/cc_auth/2007/11/18
    pre_auth.log
    post_auth.log
</pre>
<p>In this situation, you&#8217;ve got a clean structure laid out in directories on disk.  Now, you could make an argument that you use more <a href="http://en.wikipedia.org/wiki/Inode">inodes</a>, but that&#8217;s a weak argument.  Point here being&#8230;nice and pretty, right?</p>
<p><b>What are some other useful things to log during batch job execution?</b></p>
<p>The more useful data you can log, the better (within reason, of course).  Here are some handy examples:</p>
<ul>
<li>PID
<li>Start time of job
<li>End time of job
<li>Elapsed time
<li>Number of notices, warnings, errors, etc.
</ul>
<p>To illustrate, here&#8217;s a log entry for a script running on a batch job class that I built <a href="http://www.schematic.com/">at work</a>:</p>
<pre>
(5527) ------------------------------------
(5527)   Hostname: articuno (batch)
(5527)     Script: /data/baz/deploy/batch/Foo/Bar/some_script.php
(5527)   Log File: /data/baz/log/Foo/Bar/2007/11/13/some_script.log
(5527)      Start: 2007-11-13 02:39:02 GMT
(5527) ------------------------------------
(5527) [2007-11-13 02:39:02] [info] locked 1 items for copyright scanning
(5527) [2007-11-13 02:39:02] [info] [3EC7539BF9F0C72EE040050AEE042902] performing copyright scan; entity type id = 3; name = High sound TR TONE.mp3; scanning file = /foo/bar.mp3; mime type = audio/mpeg
(5527) [2007-11-13 02:39:18] [info] [3EC7539BF9F0C72EE040050AEE042902] entity is not copyrighted
(5527) [2007-11-13 02:39:18] [info] [3EC7539BF9F0C72EE040050AEE042902] removing entity from pending state
(5527) [2007-11-13 02:39:18] [info] [3EC7539BF9F0C72EE040050AEE042902] copied files in temporary storage to public storage
(5527) [2007-11-13 02:39:18] [info] [3EC7539BF9F0C72EE040050AEE042902] deleted all files in temporary storage
(5527) [2007-11-13 02:39:18] [info] [3EC7539BF9F0C72EE040050AEE042902] set permissions on entity's public storage directory
(5527) [2007-11-13 02:39:18] [info] [3EC7539BF9F0C72EE040050AEE042902] set copyright scan outcome
(5527) [2007-11-13 02:39:18] [info] [3EC7539BF9F0C72EE040050AEE042902] removed entity from upload queue
(5527) [2007-11-13 02:39:18] [info] [3EC7539BF9F0C72EE040050AEE042902] queued cdn purge of entity's urls
(5527) [2007-11-13 02:39:18] [info] released lock for process articuno (batch):5527
(5527) [2007-11-13 02:39:18] [info] found 0 copyrighted entities
(5527) ------------------------------------
(5527)       End: 2007-11-13 02:39:18 GMT
(5527)   Elapsed: 16.472630s
(5527) ------------------------------------
</pre>
<p>At first glance, there are a bunch of things that are really clear from this log entry:</p>
<ul>
<li>The process ID is 5527
<li>The entire execution took about 16.5 seconds
<li>We see what is being processed, and an entry for every action taken along with its success (or failure)
</ul>
<p>Now, this is a pretty useful, but successful, log entry.  Let&#8217;s take a look at a failure case, shall we?</p>
<pre>
(906) ------------------------------------
(906)   Hostname: sentret (video01)
(906)     Script: /data/baz/deploy/batch/Foo/Bar/transcode_videos.php
(906)   Log File: /data/baz/log/Foo/Bar/2007/11/07/transcode_videos.log
(906)      Start: 2007-11-07 15:58:02 GMT
(906) ------------------------------------
(906) [2007-11-07 15:58:02] [info] locked 2 items of type 4
(906) [2007-11-07 15:58:02] [info] [3D3A7D11E19B8906E040050AEE04323B] Starting flv transcode for 3gp and thumbnails
(906) [2007-11-07 15:59:04] [info] [3D3A7D11E19B8906E040050AEE04323B] Finished
(906) [2007-11-07 15:59:04] [info] [3D3A7D11E19B8906E040050AEE04323B] Starting preview flv transcode for entity page
(906) [2007-11-07 15:59:31] [notice] [3D3A7D11E19B8906E040050AEE04323B] error transcoding video to flash; skipping video; path = /foo/Croud.flv; message = Encoding process encountered an error
(906) [2007-11-07 15:59:31] [info] [3E48BC06162F4A8CE040050AEE042BCC] Starting flv transcode for 3gp and thumbnails
(906) [2007-11-07 15:59:56] [notice] [3E48BC06162F4A8CE040050AEE042BCC] error transcoding video to flash; skipping video; path = /foo/DBA_27108.gif; message = Could not create the flix handle - flixd unreachable (not running); flix result = -9
(906) [2007-11-07 15:59:56] [error] [3E48BC06162F4A8CE040050AEE042BCC] error connecting to the flix engine; skipping video; path = /foo/DBA_27108.gif; message = Could not create the flix handle - flixd unreachable (not running); flix result = -9
(906) [2007-11-07 15:59:57] [info] released lock for process sentret (video01):906
(906) ------------------------------------
(906)       End: 2007-11-07 15:59:57 GMT
(906)   Elapsed: 114.864283s
(906) ------------------------------------
</pre>
<p>In this case, we see that an error occurred.  The developers would receive an email reading:</p>
<pre>

(906) ------------------------------------
(906)   Hostname: sentret (video01)
(906)     Script: /data/baz/deploy/batch/Foo/Bar/transcode_videos.php
(906)   Log File: /data/baz/log/Foo/Bar/2007/11/07/transcode_videos.log
(906)      Start: 2007-11-07 15:58:02 GMT
(906) ------------------------------------
(906) [2007-11-07 15:59:56] [error] [3E48BC06162F4A8CE040050AEE042BCC] error connecting to the flix engine; skipping video; path = /foo/DBA_27108.gif; message = Could not create the flix handle - flixd unreachable (not running); flix result = -9
(906) ------------------------------------
(906)       End: 2007-11-07 15:59:57 GMT
(906)   Elapsed: 114.864283s
(906) ------------------------------------
</pre>
<p>We maintain a setting for &#8220;minimum email log level,&#8221; which defaults to warnings.  So, that&#8217;s what allows us to email anything at warning-level or higher to the developers that can address the situation.  Alternatively, we could set that level to email developers on anything at notice-level or above.  It&#8217;s all configurable in the batch framework.</p>
<p>Similarly, we define a default exception handler and an error handler to trap uncaught exceptions and errors from PHP.  Having an exception handler, for example, allows us to catch all exceptions, log their being uncaught, and email the developers to let them know of the problem.  Likewise, PHP notices or warnings are logged and emailed if applicable, too.</p>
<p>We&#8217;ve definitely achieved our goal of making developers aware of problems!</p>
<p><b>So, this all looks great, Brian, but how can I get my hands on it?</b></p>
<p>Well, at this time, I&#8217;m not at liberty to release any of this code.  Perhaps it&#8217;s worth submitting a Zend Framework proposal to keep it in Userland, or even a PEAR2 module.</p>
<p>Even still, let&#8217;s assume that we&#8217;ll want the following:</p>
<ul>
<li>Parsing of command line options (short and long)
<li>Lock file support
<li>Email recipient(s) on errors (you could even, say, send SMS messages!)
<li>Flexible logging in date-based directories or files, or any arbitrary structure
<li>Ability to define levels at which emails are generated
<li>Easy way to use batch functionality in any batch script
</ul>
<p>On the database side of things, let&#8217;s consider these requirements:</p>
<ul>
<li>Ability to delay retry of processing for a specified amount of time
<li>Ability to retry up to X times, then cease retries
</ul>
<p>I&#8217;ve had this post brewing for a long time now, so I&#8217;m going to deem this one &#8220;part one of two&#8221; and address some of the points above in a second post on the topic.  The database portion alone is pretty lengthy.  I also haven&#8217;t heard back on php|tek acceptance at this point, but if I get accepted, I&#8217;ll definitely be bringing some more cohesion to this topic.</p>
<p>If you have any questions or comments, just ask!  I&#8217;m also going to send a PEAR2 proposal post-Thanksgiving, so heads up!</p>
]]></content:encoded>
			<wfw:commentRss>http://www.deshong.net/2007/11/robust-batch-processing-with-php-part-12/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>ZendCon 07: &#8220;Mobilizing and Sharing: How Zend Framework Builds Community for Nokia MOSH&#8221;</title>
		<link>http://www.deshong.net/2007/10/zendcon-07-mobilzing-and-sharing-how-zend-framework-builds-community-for-nokia-mosh/</link>
		<comments>http://www.deshong.net/2007/10/zendcon-07-mobilzing-and-sharing-how-zend-framework-builds-community-for-nokia-mosh/#comments</comments>
		<pubDate>Thu, 11 Oct 2007 00:43:34 +0000</pubDate>
		<dc:creator>Brian DeShong</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Scalability]]></category>

		<guid isPermaLink="false">http://www.deshong.net/?p=58</guid>
		<description><![CDATA[On October 9, 2007, Ben Ramsey and I spoke on how we used Zend Framework in building Nokia MOSH at Schematic. I also touched on some of the architectural details as well. The talk went great! A link to a PDF of the slides is below: Grab a PDF of the slides]]></description>
			<content:encoded><![CDATA[<p>On October 9, 2007, <a href="http://benramsey.com/">Ben Ramsey</a> and I spoke on how we used Zend Framework in building Nokia MOSH at Schematic.  I also touched on some of the architectural details as well.</p>
<p>The talk went great!  A link to a PDF of the slides is below:</p>
<p><a href="http://brian.deshong.net/zendcon07/zf_and_mosh.pdf">Grab a PDF of the slides</a></p>
]]></content:encoded>
			<wfw:commentRss>http://www.deshong.net/2007/10/zendcon-07-mobilzing-and-sharing-how-zend-framework-builds-community-for-nokia-mosh/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Example: Who&#8217;s Online with PHP and Memcached</title>
		<link>http://www.deshong.net/2007/09/example-whos-online-with-php-and-memcached/</link>
		<comments>http://www.deshong.net/2007/09/example-whos-online-with-php-and-memcached/#comments</comments>
		<pubDate>Sat, 22 Sep 2007 17:01:07 +0000</pubDate>
		<dc:creator>Brian DeShong</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Scalability]]></category>

		<guid isPermaLink="false">http://www.deshong.net/?p=54</guid>
		<description><![CDATA[I figured it best to give an example to back up my last post entitled &#8220;Who&#8217;s Online with PHP and Memcached.&#8221; First, let&#8217;s look at the WhosOnline class itself. This class is meant to be a Singleton, so you have to access it with WhosOnline::getInstance(). Also, DISCLAIMER: I wrote this code in about 20-30 minutes. [...]]]></description>
			<content:encoded><![CDATA[<p>I figured it best to give an example to back up my last post entitled &#8220;Who&#8217;s Online with PHP and Memcached.&#8221;</p>
<p>First, let&#8217;s look at the WhosOnline class itself.  This class is meant to be a <a href="http://en.wikipedia.org/wiki/Singleton">Singleton</a>, so you have to access it with WhosOnline::getInstance().</p>
<p>Also, DISCLAIMER: I wrote this code in about 20-30 minutes.  There may be little odds and ends-type problems with it, but please post comments if you&#8217;ve got feedback!</p>
<pre>
/**
 * Class for accessing Who's Online data via Memcached.
 *
 * @author Brian DeShong
 */
class WhosOnline
{
    const RECORDING_DELAY_SECONDS = 120;
    private static $_instances = array();
    private $_mc;

    /**
     * Protected constructor to force use as a singleton.
     *
     * @param Memcache $mc Memcache object.
     */
    protected function __construct(Memcache $mc)
    {
        $this->_mc = $mc;
    }

    /**
     * Classic Singleton getInstance() method.  Allows for multiple
     * WhosOnline instances, though.  For example, maybe you want to use one
     * Memcached pool for users online in your forums, and another for users
     * online in your online dating application.  Coupling a different
     * Memcache object with a different $uniqueId allows this.
     *
     * @param Memcache $mc Memcache object.
     * @param string $uniqueId Unique ID of the object; optional.
     * @return WhosOnline
     */
    public static function getInstance(Memcache $mc, $uniqueId = 'default')
    {
        if (!isset(self::$_instances[$uniqueId])) {
            self::$_instances[$uniqueId] = new self($mc);
        }

        return self::$_instances[$uniqueId];
    }

    /**
     * Determines if current user's online status needs to be recorded or
     * updated.
     *
     * @return bool
     * @todo This method shouldn't reach out to $_SESSION.
     */
    public function needToRecordOnline()
    {
        return
            !isset($_SESSION['lastOnlineRecorded']) ||
            (isset($_SESSION['lastOnlineRecorded']) &#038;&#038;
             $_SESSION['lastOnlineRecorded'] <
                 time() - self::RECORDING_DELAY_SECONDS);
    }

    /**
     * Records given user ID as being online and records last activity
     * timestamp.
     *
     * @param int $userId User ID.
     * @return bool
     */
    public function recordOnline($userId)
    {
        if (!self::setUserOnline($userId)) {
            return false;
        }

        $_SESSION['lastOnlineRecorded'] = time();
        return true;
    }
    /**
     * Gets array of all users online.  Array is keyed by user ID with activity
     * timestamp as the value.
     *
     * @return array
     */
    public function getUsersOnline()
    {
        $usersOnline = $this->_mc->get('usersOnline');

        return ($usersOnline !== false ? $usersOnline : array());
    }

    /**
     * Sets an array of user IDs with their activity timestamps.
     *
     * @param array $usersOnline Array of user IDs online.
     * @return bool
     */
    public function setUsersOnline(array $usersOnline)
    {
        return
            $this->_mc->set('usersOnline', $usersOnline) &#038;&#038;
            $this->_mc->set('numUsersOnline', count($usersOnline));
    }

    /**
     * Sets given user ID as being online.
     *
     * @param int $userId User ID.
     * @return bool
     */
    protected function setUserOnline($userId)
    {
        $usersOnline = $this->getUsersOnline();
        $usersOnline[$userId] = time();
        return $this->setUsersOnline($usersOnline);
    }
}
</pre>
<p>Note the primary methods:</p>
<ul>
<li>WhosOnline::getInstance()
<li>WhosOnline::needToRecordOnline
<li>WhosOnline::recordOnline()
<li>WhosOnline::getUsersOnline()
<li>WhosOnline::setUsersOnline()
</ul>
<p>The main reason we leave setUsersOnline() public is so that it can be accessed via a back end script to cleanup the entire array of user IDs online.</p>
<p>Next, our example file using this class:</p>
<p><strong>wol_test.php</strong></p>
<pre>
// Startup the session and assign a user ID.  Typically you would do this at
// authentication time.
session_start();

if (!isset($_SESSION['user_id'])) {
    $_SESSION['user_id'] = uniqid();
}

// Connect to Memcached and grab the Who's Online object.
$mc = new Memcache();
$mc->connect('localhost', 11211);
$who = WhosOnline::getInstance($mc);

// If user needs to be recorded as online, do so.
if ($who->needToRecordOnline()) {
    $who->recordOnline($_SESSION['user_id']);
}

// Grab users online to display; typically you would never do this on the
// front end, though.
$usersOnline = $who->getUsersOnline();
?>
Your session data:
&lt;pre&gt;
&lt;?php echo print_r($_SESSION, true); ?&gt;
&lt;/pre&gt;

Users online: &lt;?php echo count($usersOnline); ?&gt;
&lt;pre&gt;
&lt;?php echo print_r($usersOnline, true); ?&gt;
&lt;/pre&gt;
</pre>
<p>I placed the example wol_test.php file in my DocumentRoot and ran it through ApacheBench a few times, like so:</p>
<pre>
ab -c 10 -t 1000 http://localhost/wol_test.php
</pre>
<p>This causes the wol_test.php page to be requested 1,000 times at a level of 10 concurrent requests.  I did this a few times and ended up with over 3,000 users in my array of users online.  Based on a manual get from Memcached like so:</p>
<pre>
get usersOnline
VALUE usersOnline 1 126727
</pre>
<p>&#8230;we see that with over 3,000 users online, it only takes up 126,727 bytes in Memcached.  Remember, the PECL extension for Memcache serializes any non-scalar values before storing them, so you have a cost associated with the serializing and unserializing of the array.  Doing the math here, a 1MB serialized array will hold 30,838 users online.  You&#8217;ll be able to squeeze more out of it if you have integer user IDs; I&#8217;m using uniqid() here just for example purposes.</p>
<p>But is this is a good idea?  Retrieving 1MB, or even 127k from Memcached every so often isn&#8217;t cheap.  Remember, you are:</p>
<ol>
<li>Retrieving string with serialized array of users online from Memcached
<li>Unserializing the string
<li>Adding user or updating their activity timestamp
<li>Serializing the array again
<li>Storing string back to Memcached
</ol>
<p>&#8230;this isn&#8217;t cheap.  This is probably going to be more sluggish than you&#8217;re willing to acceept, and I doubt it&#8217;d scale well as you crept up into thousands of users online.  I&#8217;m here with over 4,000 users in my array, and it performs well, but it&#8217;s also on a page with nothing else &#8212; once you tack on database queries and all sorts of other junk to render a page, you may be looking at a page that renders in over .5 seconds.</p>
<p>In a situation like this, you could consider splitting Who&#8217;s Online data up into multiple values in Memcached.  Basically, you can write your application code to use, say, 10 &#8220;buckets&#8221; of users online.  You would randomly select one of the 10 buckets to add/modify the user.  The key in a situation like this is to have a back end process to merge all of the arrays together, iterate over them removing stale users, and evenly distributing them back into Memcached.</p>
<p>I&#8217;ve started coding an example of this, but don&#8217;t really have the will to finish it right now.  <img src='http://www.deshong.net/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley' />   Maybe later.</p>
<p>Lastly, let&#8217;s look at the back end batch process that keeps the array of users online tidy; typically this script would run as a cronjob:</p>
<p><strong>whos_online_cleanup.php</strong></p>
<pre>
require_once './wol.php';

$now = time();
$mc = new Memcache();
$mc->connect('localhost', 11211);
$who = WhosOnline::getInstance($mc);

$usersOnline = $who->getUsersOnline();

if (empty($usersOnline)) {
    print "no users online; exiting\n\n";
    exit();
}

print "num users online: " . count($usersOnline) . "\n\n";
print "processing users...\n";

$numUsersRemoved = 0;

foreach ($usersOnline as $userId => $timestamp) {
    if ($timestamp < $now - 300) {
        print "removing $userId; last seen " .
            ($now - $timestamp) . " seconds ago\n";
        unset($usersOnline[$userId]);
        $numUsersRemoved++;
    }
}

print "num users removed: $numUsersRemoved\n";
print "current num users online: " . count($usersOnline) . "\n";
print "saving users online...";
print ($who->setUsersOnline($usersOnline) ? 'done!' : '** FAILED **');
exit();
</pre>
<p>Here&#8217;s some example output from it:</p>
<pre>
brian@henery [/web/pages]$ php ./whos_online_cleanup.php
num users online: 56

processing users...
removing 46f549b786d68; last seen 496 seconds ago
removing 46f549b786e7c; last seen 480 seconds ago
removing 46f549b786ef2; last seen 476 seconds ago
removing 46f549b7871b5; last seen 445 seconds ago
removing 46f549b787213; last seen 480 seconds ago
removing 46f549b78a105; last seen 482 seconds ago
removing 46f549b789071; last seen 389 seconds ago
removing 46f549b7927eb; last seen 467 seconds ago
removing 46f549b79372a; last seen 437 seconds ago
removing 46f549b798931; last seen 487 seconds ago
removing 46f549b79b50b; last seen 423 seconds ago
removing 46f549b79bc0a; last seen 381 seconds ago
removing 46f549b79d000; last seen 379 seconds ago
removing 46f549b79dfa6; last seen 398 seconds ago
removing 46f549b79fb99; last seen 472 seconds ago
removing 46f549b7a3975; last seen 502 seconds ago
removing 46f549b7a8278; last seen 373 seconds ago
removing 46f549b7ab905; last seen 407 seconds ago
removing 46f549b7d635e; last seen 389 seconds ago
removing 46f549b7d63b0; last seen 500 seconds ago
removing 46f549b7d63d8; last seen 453 seconds ago
removing 46f549b7da797; last seen 309 seconds ago
removing 46f549b7dbb9a; last seen 491 seconds ago
removing 46f549b7dce8a; last seen 396 seconds ago
removing 46f549b7dddb0; last seen 361 seconds ago
removing 46f549b7e3da6; last seen 353 seconds ago
num users removed: 26
current num users online: 30
saving users online...done!
</pre>
<p>So, you can just cron this like so:</p>
<pre>
* * * * * /usr/local/bin/php /some/path/to/whos_online_cleanup.php > /dev/null 2>&#038;1
</pre>
<p>&#8230;and feel free to redirect STDOUT to a log file if you&#8217;d like.</p>
<p>Some quick stats.  With over 1,000 users in the array, running the cleanup script takes under .2 seconds:</p>
<pre>
brian@henery [/web/pages]$ time php ./whos_online_cleanup.php
num users online: 1221

...[snip]...

num users removed: 470
current num users online: 751
saving users online...done!
real    0m0.195s
user    0m0.040s
sys     0m0.040s
</pre>
<p>&#8230;so it&#8217;s pretty speedy.  It&#8217;s worth noting that all of this is being done on my Mac Mini Core Solo with 2 GB RAM running PHP 5.2.4 and Apache 2.2.x on OS X 10.4.10.  Oh, and with just over 5,000 users in the array, the script runs in .58 seconds.</p>
<p>So&#8230;pretty straightforward, right?  What do you think?  Surely there&#8217;s room for improvement&#8230;</p>
]]></content:encoded>
			<wfw:commentRss>http://www.deshong.net/2007/09/example-whos-online-with-php-and-memcached/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Who&#8217;s Online with PHP and Memcached</title>
		<link>http://www.deshong.net/2007/09/whos-online-with-php-and-memcached/</link>
		<comments>http://www.deshong.net/2007/09/whos-online-with-php-and-memcached/#comments</comments>
		<pubDate>Fri, 21 Sep 2007 13:55:24 +0000</pubDate>
		<dc:creator>Brian DeShong</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Scalability]]></category>

		<guid isPermaLink="false">http://www.deshong.net/?p=46</guid>
		<description><![CDATA[Whenever you Google around for things like &#8220;Who&#8217;s Online php&#8221;, you&#8217;ll find that a lot of the solutions are centered around using a database. However, is this really necessary? For a site with, say, 50,000 concurrent users making, say, one page request every eight seconds, this could be a lot of database traffic if you&#8217;re [...]]]></description>
			<content:encoded><![CDATA[<p>Whenever you Google around for things like &#8220;Who&#8217;s Online php&#8221;, you&#8217;ll find that a lot of the solutions are centered around using a database.  However, is this really necessary?  For a site with, say, 50,000 concurrent users making, say, one page request every eight seconds, this could be a lot of database traffic if you&#8217;re recording the user&#8217;s activity on every request.</p>
<p>One goal here: get Who&#8217;s Online functionality off of the database.  We&#8217;ll explore a possible solution with Memcached that I&#8217;ve personally implemented, and thus far, it&#8217;s been working great.</p>
<p>The first thing to consider: how real-time does something like &#8220;Who&#8217;s Online&#8221; need to be?  Is having it be accurate to, say, users that have been online within the last two minutes acceptable?  Next, do we really need to know when a user made any action on the site, or can we consider them online every so often?  Not recording activity on each page request significantly reduces the amount of recording going on.</p>
<p>Next, we have to keep in mind that Memcached values can be up to one megabyte in size.  If we&#8217;re going to have hundreds of thousands of users online, it&#8217;s possible to exceed the 1M limit.  Let&#8217;s ignore this for now; we&#8217;ll address it later.  Let&#8217;s assume that your site is relatively small and won&#8217;t have more than a few hundred or thousand users online at any given time.</p>
<p>For this type of scenario, you can store a single array in Memcache.  A decent structure is like so:</p>
<pre>
array(
    '12345' => [unix timestamp],
    '12346' => [unix timestamp],
    '[user id]' => [unix timestamp],
    ...,
    ...);
</pre>
<p>Your user ID value can be whatever you&#8217;d like, as long as it&#8217;s unique.  For example, your most common IDs will (should!) be numeric, but a unique username or GUID-based ID will work fine.</p>
<p>Next, you store the timestamp of the user&#8217;s last activity.  A key point here is&#8230;how accurate does this data need to be?  Is it sufficient to know who was online in, say, the last two minutes?  If so, let&#8217;s define &#8220;being online:&#8221;</p>
<p><b>Online</b>: an authenticated user who viewed a page on the website within a given period of time.</p>
<p>In our case, let&#8217;s say that the period of time is two minutes.  You can code your application as follows:</p>
<p><b>If user is logged in</b></p>
<ol>
<li>Was user&#8217;s online state recorded within the last 2 minutes (a timestamp for this can be recorded in a session or cookie value)?
<ul>
<li>YES: do nothing
<li>NO:
<ol>
<li>Retrieve array of online user data
<li>Update timestamp of user ID&#8217;s last activity
<li>Save array of online user data back to Memcache
<li>Store online recording time (the current timestamp) to user session or cookie
</ol>
</ul>
</ol>
<p>Now, you need a backend process (or some sort of process) to clean up this array of online user data.  For example, users that have not performed an activity in the past five minutes should be removed from this array.  If a user has not been back within a given period of time, they should no longer be considered as online.  This process could run out of cron, say, once a minute or every few minutes.</p>
<p>Your process for this back end script would be like so:</p>
<ol>
<li>Retrieve array of online user IDs
<li>Iterate over all user IDs, checking their last activity timestamp
<ol>
<li>If user&#8217;s last activity is more than X seconds old (say, 5 minutes), remove them from the array
<li>If user&#8217;s last activitiy is within the past X seconds, they can remain in the array
</ol>
<li>Store array of user IDs back to Memcached
<li>For convenience, you may also consider storing the number of users in the array in a separate Memcached value; this makes displaying a Who&#8217;s Online counter nice and cheap
</ol>
<p>I&#8217;ve implemented this exact process on a website of a decent size, and it&#8217;s been working great for a few months now.  In our case, we&#8217;ve seen peaks of up to 140 users online at a time.</p>
<p>Maybe there are some holes in it, though?  I&#8217;m not an expert on all of the inner-workings of Memcached, so maybe there&#8217;s some sort of a race condition here.</p>
<p>This is, however, a simple way to implemented Who&#8217;s Online functionality without taxing a database.  Remember, just because a Memcached value can be 1MB, doesn&#8217;t mean that it should be.  If you find that the size of your array is large enough that the retrieval from Memcached takes a while, consider splitting it up over a few cache keys to keep the retrievals cheap.  Pulling 1MB down the wire every so often ain&#8217;t cheap!</p>
<p><strong>UPDATE: <a href="/?p=54">See a follow-up post with example code here!</a></strong></p>
]]></content:encoded>
			<wfw:commentRss>http://www.deshong.net/2007/09/whos-online-with-php-and-memcached/feed/</wfw:commentRss>
		<slash:comments>8</slash:comments>
		</item>
		<item>
		<title>Thanks, Atlanta PHP!</title>
		<link>http://www.deshong.net/2007/04/thanks-atlphp/</link>
		<comments>http://www.deshong.net/2007/04/thanks-atlphp/#comments</comments>
		<pubDate>Fri, 06 Apr 2007 02:44:18 +0000</pubDate>
		<dc:creator>Brian DeShong</dc:creator>
				<category><![CDATA[Georgia]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Scalability]]></category>

		<guid isPermaLink="false">http://www.deshong.net/?p=31</guid>
		<description><![CDATA[This evening, I spoke at Atlanta PHP on &#8220;Designing for Scalability&#8221; (slides coming soon!). It was a great crowd with some great questions, and most notably, was my first public speaking in the PHP community. I&#8217;ll be aiming to do more of these things in the coming months, so you may see me out on [...]]]></description>
			<content:encoded><![CDATA[<p>This evening, I spoke at <a href="http://www.atlphp.org/">Atlanta PHP</a> on &#8220;Designing for Scalability&#8221; (slides coming soon!).</p>
<p>It was a great crowd with some great questions, and most notably, was my first public speaking in the PHP community.  I&#8217;ll be aiming to do more of these things in the coming months, so you may see me out on the conference scene soon.</p>
<p>Thanks again to the group!  Please don&#8217;t hesitate to email at any time.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.deshong.net/2007/04/thanks-atlphp/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
