Ouais, ça fait un peu slogan d’une pub américaine (mal doublée en français) pour un merveilleux objet du télé-achat qu’il faut absolument acheter… mais quand même, memcache c’est drôlement bien.

Pourquoi utiliser un cache?

La question est un peu débile, tout le monde comprend bien l’intérêt d’utiliser un cache… mais il faut cependant bien choisir les données a y mettre et (surtout) utiliser le(s) bon(s) système(s).

Quand on construit une page web dynamiquement, c’est un peu crétin de la générer 20 fois si les données n’ont pas changé ou si une mise a jour hebdomadaire suffit. On peut alors, en ajoutant les headers HTTP qui vont bien, forcer l’utilisation du cache du navigateur, pour éviter qu’une même personne ne régénère 10 fois la même page a coups de F5 (cf. RFC 2616). On peut aussi générer la page une première fois (lors de la première requête) puis enregistrer le résultat pour le resservir ensuite aux autres. On peut même faire les deux en même temps et utiliser des
outils-tout-fait t-as-rien-a-faire et ça marche en 5 minutes (cf. JP cache par exemple pour une page en php). Il est également possible d’utiliser Apache et Squid pour faire des choses sympas, ou même encore utiliser le cache de MySQL si on est fou et/ou si on sait ce que l’on fait.

Dans la majorité des cas, les données (souvent sérialisées et compressées) sont stockées dans un fichier. C’est bien mais c’est pas super rapide, surtout quand on doit partager le cache entre plusieurs serveurs et qu’on utilise donc NFS par exemple, ou que l’on doit partager des données entre plusieurs
applications. Une solution est donc d’utiliser memcache.

Memcached, c’est quoi ?

Comme ils le disent eux-même, “memcached is a high-performance, distributed memory object caching system, generic in nature, but intended for use in speeding up dynamic web applications by alleviating database load.”. Donc en gros, memcache un système de cache qui stocke les données en ram pour être rapide. On fait donc tourner un daemon sur notre serveur, on se connecte dessus (tcp) et lui envoie et/ou demande des données, c’est extrêmement simple. Il faut cependant bien garder en tête que les données stockées en mémoire partagée sont perdu lors d’un reboot, on sera donc intelligent et on y mettra pas n’importe quoi. L’idéal est bien de l’utiliser en complément d’un autre système de cache (file system) et d’y stocker uniquement des données pouvant être volatiles (des resultsets par exemple).

Installer et configurer memcached

Télécharge et compile les dernières sources disponibles : http://www.danga.com/memcached/download.bml
ou utilise apt-get si tu as un vrai os.

panjhy@sushi:~$ sudo apt-get install memcached

Edite le fichier /etc/memcached.conf, modifie le port et l’ip a écouter, et la mémoire a allouer si besoin est (il est également recommandé de créer un user spécifique pour lancer le daemon).

Lance memcached en utilisant la ligne de commande suivante :

panjhy@sushi:~$ sudo /etc/init.d/memcached start

Vérifie que tout s’est bien passé, qu’aucune erreur n’a été loggée (fichier de logs par default : /var/log/memcached).

Configurer php

Télécharge l’extension PECL a l’adresse suivante : http://pecl.php.net/package/memcache

Décompresse l’archive et compile :

panjhy@sushi:/tmp$  wget http://pecl.php.net/get/memcache-2.1.0.tgz
panjhy@sushi:/tmp$  tar zxvf memcache-2.1.0.tgz
panjhy@sushi:/tmp$  cd memcache-2.1.0
panjhy@sushi:/tmp/memcache-2.1.0$  phpize
panjhy@sushi:/tmp/memcache-2.1.0$  ./configure
panjhy@sushi:/tmp/memcache-2.1.0$  sudo make
panjhy@sushi:/tmp/memcache-2.1.0$  sudo make install

(note: on peut aussi installer une extension PECL via PEAR, c’est plu simple quand ca marche)

Edite ton php.ini et ajoute l’extension memcache dans la section extensions :

extension=memcache.so

Redémarre apache (si php est en module, sinon kill tes process cgi) et c’est fini :)

panjhy@sushi:/tmp/memcache-2.1.0$ sudo /etc/init.d/apache2 restart

Jete un oeil voir si tout c’est bien passe :

panjhy@sushi:/usr/local/lib/php$ php -r "phpinfo();" | grep memcache
memcache
memcache support => enabled
memcache.allow_failover => 1 => 1
memcache.chunk_size => 8192 => 8192
memcache.default_port => 11211 => 11211
memcache.max_failover_attempts => 20 => 20
sushi:/usr/local/lib/php# 

Utiliser mencache avec php

Toute la doc se trouve par la :
http://php.net/manual/en/ref.memcache.php

Les 3 fonctions vitales sont :

  • Memcache::conect qui permet de se connecter a memcached. On préférera quand même utiliser Memcache::addServer car, comme on peut le lire dans la doc, sur une architecture 32 bits on ne peut pas assigner plus de 4Go par process, on pourra par contre lancer plusieurs daemon.
  • Memcache::set permet de placer un item en mémoire. Le troisième paramètre est le TTL (durée de vie de l’item en cache). Je trouve bien plus logique de fixer le TTL au moment où on met quelque chose en cache que de checker si l’item a expiré quand on le récupère.
  • Memcache::get permet de récupérer un item a partir du cache, retourne false si l’item n’est plus valide.

Voici une petite classe pour l’utiliser simplement :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<?php
class mcd
{
        private $_memcache      = NULL;                 // (object)     memcache instance
        private $_status        = NULL;                 // (bool)       cache enable
 
        private $_params        = array(
                'use_pconnect'  => false,                    // (bool)       use persistent connexion
                'use_zlib'      => false,                    // (bool)       use zlib to store conpressed data
                'timeout'       => 1,                        // (int)        timeout (secondes)
                'default_ttl'   => 86400                     // (int)        default item ttl (secondes)
        );
 
        private $_servers       = array(
                array(
                        'host'          => 'localhost',      // (srting)     hostname or ip
                        'port'          => '11211',          // (int)        tcp port
                        'weight'        => 50                // (int)        server's weight
                ),
        //      array(
        //              'host'          => 'mcd.plumbr.com',
        //              'port'          => '11211',
        //              'weight'        => 10
        //      ),      
        );
 
        /**
         * New instance of memcache
         *
         * @param array $params         parameters
         * @param array $servers        servers pool
         */
        public function __construct($params = array(), $servers = NULL) {
                $this->_status = (true === defined('CACHE_ENABLE') and true === CACHE_ENABLE ? true : false);
                $this->_params = array_merge($this->_params, $params);
 
                $this->_params['use_zlib'] = (true === $this->_params['use_zlib'] ? MEMCACHE_COMPRESSED : 0);
 
                if(NULL !== $servers) $this->_servers = $servers;
 
                if(true === $this->_status) {
                        try {
                                $this->_memcache = new Memcache;
                                assert('true === is_object($this->_memcache)');
                                $this->_connect();
                        } catch(Exception $e) {
                                trigger_error(var_export($e, true), E_USER_WARNING);
                        }
                }
        }
 
        /**
         * (void) Connect to the memcache pool servers
         */
        private function _connect() {
                foreach($this->_servers as $server) {
                        if(false === array_key_exists('host', $server) or false === array_key_exists('port', $server))
                                throw new Exception('Bad server config :/');
 
                        if(false === $this->_memcache->addServer(
                                $server['host'],
                                $server['port'],
                                $this->_params['use_pconnect'],
                                (true === array_key_exists('weight', $server) ? $server['weight'] : 50),
                                $this->_params['timeout']
                        )) throw new Exception('Cannot connect to memcached '.$server['host'].':'.$server['port']);
                }
        }
 
        /**
         * Return the value cached item
         *
         * @param mixed $data   item value
         * @param string $name  item key
         * @param int $ttl              item cache ttl
         * @return mixed                return false if an erro rhas occured
         */
        public function set(&$data, $name, $ttl = NULL) {
                assert('true === is_string($name)');
                if(false === $this->_status) return false;
 
                if(NULL === $ttl) $ttl = $this->_params['default_ttl'];
 
                if(false === $this->_memcache->set($name, $data, $this->_params['use_zlib'], $ttl)) {
                        trigger_error('Cannot set '.$name.' into memcache server', E_USER_WARNING);
                        return false;
                }
 
                return true;
        }
 
        /**
         * Retrieve item from the server
         *
         * @param string $name  item's name
         * @return mixed                return false if an error has occured
         */
        public function get($name) {
                assert('true === is_string($name)');
 
                return (false === $this->_status ? false : $this->_memcache->get($name));
        }
 
        /**
         * (void) Flush all existing items
         */
        public function flush() {
                $this->_memcache->flush();
        }
 
        /**
         * Destructor: close the connexions to memcached
         */
        public function __destruct() {
                if($this->_memcache) $this->_memcache->close();
        }
}
 
error_reporting(E_ALL);
ini_set('display_errors', 1);
define('CACHE_ENABLE' , true);
 
$data = array('foo' => time());                      // data to store
 
$cache = new mcd(array('use_zlib' => true));
 
$cache->set($data, 'foo', 2);                        // store $data
var_dump($cache->get('foo'));                        // retreive $data
sleep(3);
var_dump($cache->get('foo'));                        // test cache expiration: have to return false
 
$cache->set($data, 'foo');
var_dump($cache->get('foo'));
$cache->flush();                                     // flush entire cached items
var_dump($cache->get('foo'));                        // test cache flush: have to return false
?>