Thursday Nov 19, 2009

PHP: session.gc_maxlifetime vs. session.cookie_lifetime

PHP and sessions: Very simple to use, but not as simple to understand as we might want to think.

session.gc_maxlifetime

This value (default 1440 seconds) defines how long an unused PHP session will be kept alive. For example: A user logs in, browses through your application or web site, for hours, for days. No problem. As long as the time between his clicks never exceed 1440 seconds. It's a timeout value.

PHP's session garbage collector runs with a probability defined by session.gc_probability divided by session.gc_divisor. By default this is 1/100, which means that above timeout value is checked with a probability of 1 in 100.

session.cookie_lifetime

This value (default 0, which means until the browser's next restart) defines how long (in seconds) a session cookie will live. Sounds similar to session.gc_maxlifetime, but it's a completely different approach. This value indirectly defines the "absolute" maximum lifetime of a session, whether the user is active or not. If this value is set to 60, every session ends after an hour.

Wednesday Nov 18, 2009

PHP's MySQLi extension: Storing and retrieving blobs

There are a lot of tutorial out there describing how to use PHP's classic MySQL extension to store and retrieve blobs. There are also many tutorials how to use PHP's MySQLi extension to use prepared statements to fight SQL injections in your web application. But there are no tutorials about using MySQLi with any blob data at all.

Until today... ;)

Preparing the database

Okay, first I need a table to store my blobs. In this example I'll store images in my database because images usually look better in a tutorial than some random raw data.

mysql> CREATE TABLE images (
       id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
       image MEDIUMBLOB NOT NULL,
       PRIMARY KEY (id)
       );
Query OK, 0 rows affected (0.02 sec)

In general you don't want to store images in a relational database. But that's another discussion for another day.

Storing the blob

To make a long story short, here's the code to store a blob using MySQLi:

<?php
	$mysqli=mysqli_connect('localhost','user','password','db');

	if (!$mysqli)
		die("Can't connect to MySQL: ".mysqli_connect_error());

	$stmt = $mysqli->prepare("INSERT INTO images (image) VALUES(?)");
	$null = NULL;
	$stmt->bind_param("b", $null);

	$stmt->send_long_data(0, file_get_contents("osaka.jpg"));

	$stmt->execute();
?>

If you already used MySQLi, most of the above should look familiar to you. I highlighted two pieces of code, which I think are worth looking at:

  1. The $null variable is needed, because bind_param() always wants a variable reference for a given parameters. In this case the "b" (as in blob) parameter. So $null is just a dummy, to make the syntax work.

  2. In the next step I need to "fill" my blob parameter with the actual data. This is done by send_long_data(). The first parameter of this method indicates which parameter to associate the data with. Parameters are numbered beginning with 0. The second parameter of send_long_data() contains the actual data to be stored.

While using send_long_data(), please make sure that the blob isn't bigger than MySQL's max_allowed_packet:

mysql> SHOW VARIABLES LIKE 'max_allowed_packet';
+--------------------+----------+
| Variable_name      | Value    |
+--------------------+----------+
| max_allowed_packet | 16776192 | 
+--------------------+----------+
1 row in set (0.00 sec)

If your data exceeds max_allowed_packet, you probably don't get any errors returned from send_long_data() or execute(). The saved blob is just corrupt!

Simply raise the value max_allowed_packet to whatever you'll need. If you're not able to change MySQL's configuration, you'll need to send the data in smaller chunks:

	$fp = fopen("osaka.jpg", "r");
	while (!feof($fp)) 
	{
 	   $stmt->send_long_data(0, fread($fp, 16776192));
	}

Usually the default value of 16M should be a good start.

Retrieving the blob

Getting the blob data out of the database is quite simple and follows the usual way of MySQLi:

<?php
	$mysqli=mysqli_connect('localhost','user','password','db');

	if (!$mysqli)
		die("Can't connect to MySQL: ".mysqli_connect_error());

	$id=1;  
	$stmt = $mysqli->prepare("SELECT image FROM images WHERE id=?"); 
	$stmt->bind_param("i", $id);

	$stmt->execute();
	$stmt->store_result();

	$stmt->bind_result($image);
	$stmt->fetch();

	header("Content-Type: image/jpeg");
	echo $image; 
?>

Connect to the database, prepare the SQL statement, bind the parameter(s), execute the statement, bind the result to a variable, and fetch the actual data from the database. In this case there is no need to worry about max_allowed_packet. MySQLi will do all the work:

3925128491.jpg

By the way...

If you want to insert a blob from the command line using MySQL monitor, you can use LOAD_FILE() to fetch the data from a file:

mysql> INSERT INTO images (image) VALUES( LOAD_FILE("/home/oswald/osaka.jpg") );

Be aware that also in this case max_allowed_packet limits the amount of data you're able to send to the database:

mysql> SHOW VARIABLES LIKE 'max_allowed_packet';
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| max_allowed_packet | 7168  | 
+--------------------+-------+
1 row in set (0.00 sec)

mysql> INSERT INTO images (image) VALUES( LOAD_FILE("/home/oswald/osaka.jpg") );
ERROR 1048 (23000): Column 'image' cannot be null
mysql> SET @@max_allowed_packet=16777216;
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW VARIABLES LIKE 'max_allowed_packet';
+--------------------+----------+
| Variable_name      | Value    |
+--------------------+----------+
| max_allowed_packet | 16777216 | 
+--------------------+----------+
1 row in set (0.00 sec)

mysql> INSERT INTO images (image) VALUES( LOAD_FILE("/home/oswald/osaka.jpg") );
Query OK, 1 row affected (0.03 sec)

        
    

Friday Nov 13, 2009

Little-known PHP commands: scandir()

Always messed around with a combo of opendir(), readdir(), and closedir() if you wanted to read the contents of a directory? Since PHP 5 there is a new sheriff in town: scandir():

<?php   
	$files=scandir("/etc/php5");
	print_r($files);
?>

Outputs:

Array
(
    [0] => .
    [1] => ..
    [2] => apache2
    [3] => conf.d
)

Okay, you still need to traverse an array, but it's much easier to use than the traditional way.

Performance Tuning the Sun GlassFish Web Stack

My colleague Brian Overstreet wrote a must-read paper about tuning different components of the Sun GlassFish Web Stack focusing on Apache, MySQL, and PHP: Performance Tuning the Sun GlassFish Web Stack.

Thursday Nov 05, 2009

Importing a VDI in VirtualBox

If you're used to be a VMware user and try to switch to the Open-Source side of the Force by using VirtualBox, you may run into difficulties if you try to import an existing VDI file into VirtualBox. Actually it's quite easy, if you know how.

The main difference between VMware and VirtualBox is that VMware captures a whole virtual machine in an image, whereas VirtualBox only supports images of a hard disk. So in VirtualBox's world, you first need to create a new virtual machine, before using an existing VirtualBox image.

  1. First copy your VDI file into VirtualBox's virtual hard disks repository. On Mac OS X it's $HOME/Library/VirtualBox/HardDisks/.

  2. Start VirtualBox and create a new virtual machine (according to the OS you expect to live on the VirtualBox image):

    virtualbox1.jpg
  3. When you're asked for a hard disk image, select Use existing hard disk and click on the small icon on the right:

    virtualbox2.jpg
  4. Which will brings you to the Virtual Media Manager. Click on Add and select the VDI file from step 1.

    virtualbox3.jpg
  5. After leaving the Virtual Media Manager, you'll be back in your virtual machine wizard. Now you can select your new VDI as existing hard disk and finalize the creation process.

    virtualbox4.jpg
  6. Back in the main window, you're now able to start your new virtual machine:

    virtualbox5.jpg

It's quite easy, if you know how.

Wednesday Nov 04, 2009

Store PHP sessions in memcached

My last week blog topic was very much marked by Apache load balancing. Well, I promised to leave this topic alone for a while, but there is one related topic that is worth spending a minute on.

The Theory

If your web application is distributed across multiple servers you'll quickly run in sessions problems because each backend server (aka worker) usually stores its session informations locally. Now, if subsequent HTTP requests are handled by different workers, every time a new sessions is created or, even worse, sessions getting mixed up.

To overcome this problem there are two solutions:

  1. Use a session-aware load balancer that binds a user session to the same worker.
  2. Or keep all session data in a central storage.

Both solutions have the similar drawback: if a worker goes down, all session data of this worker are lost. If the central storage goes down, all sessions are lost. But consider the following: you'll probably have tons of workers, and since every computer is supposed to fail after a specific period of time, the probability of a worker failure is much higher than for a single storage server. It depends on what do you want: A system that runs all the time with small failures or a system that fails completely from time to time?

And finally, losing session data sounds worse than it actually is: usually the users only have to login again to restore their session data. That's sad, but it's not the end of the world. Okay, your system may get into trouble if thousands of users try to re-login at the same time, but that's another problem.

The Solution

My favorite solution is the second one: keep all session data in a central place. And in this scenario I'll use Apache/PHP as my "application server" and memcached as central storage for my session data. If you read and still remember the title of this post, you're probably not surprised.

phpmemcached.jpg

On the left: my load balancer, in the middle my worker farm, and on the right: my single and central memcached server. By the way: You can also have multiple memcached servers, but for this blog post I'll keep it simple.

The Requirements

First, let's check if PHP was build with memcached support:

serverA ~% php -m | egrep memcache
memcache

...on each worker node: serverA to serverD.

Second, I check if memcached is running on serverM:

serverM ~% ps -efa | egrep memcached
oswald  1543     1   0 15:21:17 ?         0:00 /home/oswald/webstack1.5/lib/memcached -d ...

Perfecto.

The Configuration

Now I need to change the PHP configuration on each worker node: Open php.ini on serverA to serverD and search for these lines:

[Session]
; Handler used to store/retrieve data.
session.save_handler = files

And change the configuration like this:

[Session]
; Handler used to store/retrieve data.
session.save_handler = files
session.save_handler = memcache
session.save_path = "tcp://serverM:11211"

Make sure that the settings are the same on all your workers.

That's all. Yes, that's the basic configuration. PHP's sessions will now get stored on the memcached node serverM. No more magic needed.

The Proof

But as we say in Germany: "Prudence is the mother of the china cabinet." Before we can grab the beer, we should make sure everything works as we expect it to.

I put this code in a file named session.php in the document root directory of all my worker nodes:

<?php   
	session_start();
	if(isset($_SESSION['zaphod']))
	{       
		echo "Zaphod is ".$_SESSION['zaphod']."!\\n";
	}       
	else    
	{       
		echo "Session ID: ".session_id()."\\n"; 
		echo "Session Name: ".session_name()."\\n";
		echo "Setting 'zaphod' to 'cool'\\n";
		$_SESSION['zaphod']='cool';
	}       
?>

From the outside I use lynx to access this file:

% lynx -source 'http://serverA/session.php'
Session ID: df58bc9465f27aa20218c11caba6750f
Session Name: PHPSESSID
Setting 'zaphod' to 'cool'

A new session with the ID df58bc9465f27aa20218c11caba6750f was created and PHP uses the session name PHPSESSID to identify the session parameter. And the session variable zaphod was set to the value cool.

Now I add the session information PHPSESSID=df58bc9465f27aa20218c11caba6750f to my URL and rerun the new lynx command:

% lynx -source 'http://serverA/session.php?PHPSESSID=df58bc9465f27aa20218c11caba6750f'
Zaphod is cool!

Yes, I got the expected output: Zaphod is cool! Proving the session data is available on serverA. But that's not a big surprise, what's about the other nodes? I replace serverA with serverB in my URL:

% lynx -source 'http://serverB/session.php?PHPSESSID=df58bc9465f27aa20218c11caba6750f'
Zaphod is cool!

Bingo, serverB also has the same session data as serverA.

And for serverC? It's also the same:

% lynx -source 'http://serverC/session.php?PHPSESSID=df58bc9465f27aa20218c11caba6750f'
Zaphod is cool!

And so on... for each worker node the session data will be the same.

A dream came true.

About

Kai 'Oswald' Seidler writes about his life as co-founder of Apache Friends, creator of XAMPP, and technology evangelist for web tier products at Sun Microsystems.

Search

Archives
« November 2009 »
SunMonTueWedThuFriSat
1
2
3
6
7
8
9
10
11
12
14
15
16
17
20
21
22
23
24
25
26
27
28
29
30
     
       
Today