OPNsense sur Proxmox : remplacer sa box opérateur par un vrai firewall
Le ZenWifi XT8 est un très bon point d'accès WiFi mesh. Son firewall intégré, en…
Le caching distribué est la clé pour scaler WordPress au-delà de millions de pages vues par jour. Cet article présente des stratégies de caching battle-tested en production avec Redis et Memcached.
┌─────────────────────────────────────────────────────────────────┐
│ CDN Layer │
│ CloudFlare / CloudFront / Fastly (Edge Caching) │
│ Cache-Control: public, max-age=3600 │
└────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────────┐
│ Reverse Proxy Cache │
│ Varnish / NGINX FastCGI Cache (Full Page Cache) │
│ TTL: 1 hour (dynamic), 1 day (static) │
└────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────────┐
│ Application Cache Layer │
│ Redis Cluster / Memcached Cluster │
│ - Object Cache (WordPress transients, queries) │
│ - Session Storage │
│ - Fragment Cache (partials, widgets) │
└────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────────┐
│ Opcode Cache (OPcache) │
│ PHP 8.2+ with Opcache (compiled bytecode) │
└────────────────────────┬────────────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────────────┐
│ Database Query Cache │
│ MySQL Query Cache (deprecated) / ProxySQL │
└─────────────────────────────────────────────────────────────────┘
| Type de Contenu | CDN | Varnish | Redis | TTL | Invalidation |
|---|---|---|---|---|---|
| Pages statiques | Oui | Oui | Non | 24h | URL-based |
| Pages accueil | Oui | Oui | Oui | 1h | Time + Event |
| Articles/Pages | Oui | Oui | Oui | 6h | Post update |
| Archives | Oui | Oui | Oui | 2h | New post |
| Widgets | Non | Non | Oui | 1h | Manual |
| Queries DB | Non | Non | Oui | 5m | Smart purge |
| Sessions | Non | Non | Oui | 24h | Logout |
| Transients | Non | Non | Oui | Variable | API call |
┌──────────────────────────────────────┐
│ Application Servers (Web Tier) │
│ PHP Redis Client (phpredis) │
└─────────────┬────────────────────────┘
│
┌─────────────▼────────────────┐
│ Redis Sentinel Cluster │
│ (HA Monitoring) │
│ sentinel-1, sentinel-2, │
│ sentinel-3 │
└─────────────┬────────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
┌────▼─────┐ ┌──────▼────┐ ┌──────▼────┐
│ Master 1 │◄─────┤ Master 2 │◄─────┤ Master 3 │
│ Shard A │ │ Shard B │ │ Shard C │
│ Port:6379│ │ Port:6380 │ │ Port:6381 │
└────┬─────┘ └──────┬────┘ └──────┬────┘
│ │ │
┌────▼─────┐ ┌──────▼────┐ ┌──────▼────┐
│ Replica 1│ │ Replica 2 │ │ Replica 3 │
│ Shard A │ │ Shard B │ │ Shard C │
│ Port:6379│ │ Port:6380 │ │ Port:6381 │
└──────────┘ └───────────┘ └───────────┘
# /etc/redis/redis-6379.conf
# Redis 7.0+ Configuration for WordPress
# Network
bind 0.0.0.0
port 6379
timeout 300
tcp-keepalive 60
protected-mode yes
requirepass your_redis_strong_password
# General
daemonize yes
supervised systemd
pidfile /var/run/redis/redis-6379.pid
loglevel notice
logfile /var/log/redis/redis-6379.log
databases 16
# Snapshotting (RDB)
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump-6379.rdb
dir /var/lib/redis
# Replication
replicaof no one
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
replica-priority 100
# Security
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command CONFIG ""
# Limits
maxclients 10000
maxmemory 8gb
maxmemory-policy allkeys-lru
maxmemory-samples 5
# Append Only File (AOF)
appendonly yes
appendfilename "appendonly-6379.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
# Lua scripting
lua-time-limit 5000
# Slow log
slowlog-log-slower-than 10000
slowlog-max-len 128
# Latency monitor
latency-monitor-threshold 100
# Event notification
notify-keyspace-events "Ex"
# Advanced config
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
# Active defrag
activedefrag yes
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100
active-defrag-cycle-min 1
active-defrag-cycle-max 25
# Client output buffer limits
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
# Performance
io-threads 4
io-threads-do-reads yes
# /etc/redis/sentinel.conf
# Redis Sentinel for High Availability
port 26379
daemonize yes
pidfile /var/run/redis/redis-sentinel.pid
logfile /var/log/redis/redis-sentinel.log
dir /var/lib/redis
# Monitor master instances
sentinel monitor mymaster-shard-a 10.0.3.10 6379 2
sentinel auth-pass mymaster-shard-a your_redis_strong_password
sentinel down-after-milliseconds mymaster-shard-a 5000
sentinel parallel-syncs mymaster-shard-a 1
sentinel failover-timeout mymaster-shard-a 10000
sentinel monitor mymaster-shard-b 10.0.3.20 6380 2
sentinel auth-pass mymaster-shard-b your_redis_strong_password
sentinel down-after-milliseconds mymaster-shard-b 5000
sentinel parallel-syncs mymaster-shard-b 1
sentinel failover-timeout mymaster-shard-b 10000
sentinel monitor mymaster-shard-c 10.0.3.30 6381 2
sentinel auth-pass mymaster-shard-c your_redis_strong_password
sentinel down-after-milliseconds mymaster-shard-c 5000
sentinel parallel-syncs mymaster-shard-c 1
sentinel failover-timeout mymaster-shard-c 10000
# Notification scripts
sentinel notification-script mymaster-shard-a /usr/local/bin/redis-notify.sh
sentinel client-reconfig-script mymaster-shard-a /usr/local/bin/redis-reconfig.sh
redis = new Redis();
if ( $this->sentinel_mode ) {
$this->connect_sentinel();
} else {
$this->connect_direct();
}
}
/**
* Connect to Redis via Sentinel for HA
*/
private function connect_sentinel() {
$sentinels = [
[ 'host' => '10.0.3.101', 'port' => 26379 ],
[ 'host' => '10.0.3.102', 'port' => 26379 ],
[ 'host' => '10.0.3.103', 'port' => 26379 ],
];
$master_name = 'mymaster-shard-a';
foreach ( $sentinels as $sentinel ) {
try {
$sentinel_conn = new Redis();
$sentinel_conn->connect(
$sentinel['host'],
$sentinel['port'],
1 // timeout
);
$master_info = $sentinel_conn->rawCommand(
'SENTINEL', 'get-master-addr-by-name', $master_name
);
if ( $master_info && count( $master_info ) === 2 ) {
$this->redis->connect(
$master_info[0],
$master_info[1],
1, // timeout
null,
0,
0,
[ 'stream' => [ 'verify_peer' => false ] ]
);
$this->redis->auth( WP_REDIS_PASSWORD );
$this->redis->select( WP_REDIS_DATABASE ?? 0 );
// Set client name for monitoring
$this->redis->client( 'SETNAME', 'wordpress:' . $*SERVER['SERVER_NAME'] ?? 'unknown' );
return true;
}
} catch ( Exception $e ) {
continue;
}
}
return false;
}
/**
* Direct connection (fallback)
*/
private function connect_direct() {
try {
$this->redis->connect(
WP_REDIS_HOST ?? '127.0.0.1',
WP_REDIS_PORT ?? 6379,
1
);
if ( defined( 'WP_REDIS_PASSWORD' ) ) {
$this->redis->auth( WP_REDIS_PASSWORD );
}
$this->redis->select( WP_REDIS_DATABASE ?? 0 );
return true;
} catch ( Exception $e ) {
error_log( 'Redis connection failed: ' . $e->getMessage() );
return false;
}
}
/**
* Get cached value
*/
public function get( $key, $group = 'default', $force = false, &$found = null ) {
$derived_key = $this->build_key( $key, $group );
// Check non-persistent groups
if ( in_array( $group, $this->non_persistent_groups ) ) {
$found = isset( $this->cache[ $derived_key ] );
if ( $found ) {
$this->cache_hits++;
return $this->cache[ $derived_key ];
}
$this->cache_misses++;
return false;
}
// Check local cache first
if ( ! $force && isset( $this->cache[ $derived_key ] ) ) {
$found = true;
$this->cache_hits++;
return $this->cache[ $derived_key ];
}
// Get from Redis
try {
$value = $this->redis->get( $derived_key );
if ( $value === false ) {
$found = false;
$this->cache_misses++;
return false;
}
$found = true;
$this->cache_hits++;
$value = maybe_unserialize( $value );
$this->cache[ $derived_key ] = $value;
return $value;
} catch ( Exception $e ) {
$found = false;
$this->cache_misses++;
error_log( 'Redis GET error: ' . $e->getMessage() );
return false;
}
}
/**
* Set cached value
*/
public function set( $key, $data, $group = 'default', $expire = 0 ) {
$derived_key = $this->build_key( $key, $group );
// Non-persistent groups stay in memory only
if ( in_array( $group, $this->non_persistent_groups ) ) {
$this->cache[ $derived_key ] = $data;
return true;
}
// Store in local cache
$this->cache[ $derived_key ] = $data;
// Store in Redis
try {
$value = maybe_serialize( $data );
if ( $expire > 0 ) {
$result = $this->redis->setex( $derived_key, $expire, $value );
} else {
$result = $this->redis->set( $derived_key, $value );
}
return $result;
} catch ( Exception $e ) {
error_log( 'Redis SET error: ' . $e->getMessage() );
return false;
}
}
/**
* Delete cached value
*/
public function delete( $key, $group = 'default' ) {
$derived_key = $this->build_key( $key, $group );
unset( $this->cache[ $derived_key ] );
if ( in_array( $group, $this->non_persistent_groups ) ) {
return true;
}
try {
return (bool) $this->redis->del( $derived_key );
} catch ( Exception $e ) {
error_log( 'Redis DELETE error: ' . $e->getMessage() );
return false;
}
}
/**
* Flush entire cache
*/
public function flush() {
$this->cache = [];
try {
return $this->redis->flushDB();
} catch ( Exception $e ) {
error_log( 'Redis FLUSH error: ' . $e->getMessage() );
return false;
}
}
/**
* Build cache key with blog ID prefix for multisite
*/
private function build_key( $key, $group = 'default' ) {
if ( empty( $group ) ) {
$group = 'default';
}
$blog_prefix = '';
if ( function_exists( 'is_multisite' ) && is_multisite() ) {
$blog_id = get_current_blog_id();
$blog_prefix = $blog_id . ':';
}
return $blog_prefix . $group . ':' . $key;
}
/**
* Add non-persistent groups
*/
public function add_non_persistent_groups( $groups ) {
$groups = (array) $groups;
$this->non_persistent_groups = array_unique(
array_merge( $this->non_persistent_groups, $groups )
);
}
/**
* Get cache stats
*/
public function get_stats() {
$total = $this->cache_hits + $this->cache_misses;
$hit_rate = $total > 0 ? ( $this->cache_hits / $total ) * 100 : 0;
try {
$info = $this->redis->info();
return [
'hits' => $this->cache_hits,
'misses' => $this->cache_misses,
'hit_rate' => round( $hit_rate, 2 ),
'uptime' => $info['uptime_in_seconds'] ?? 0,
'connected_clients' => $info['connected_clients'] ?? 0,
'used_memory' => $info['used_memory_human'] ?? '0',
'used_memory_peak' => $info['used_memory_peak_human'] ?? '0',
'evicted_keys' => $info['evicted_keys'] ?? 0,
'keyspace_hits' => $info['keyspace_hits'] ?? 0,
'keyspace_misses' => $info['keyspace_misses'] ?? 0,
];
} catch ( Exception $e ) {
return [
'hits' => $this->cache_hits,
'misses' => $this->cache_misses,
'hit_rate' => round( $hit_rate, 2 ),
'error' => $e->getMessage(),
];
}
}
/**
* Implement other WordPress Object Cache API methods
*/
public function add( $key, $data, $group = 'default', $expire = 0 ) {
$derived_key = $this->build_key( $key, $group );
if ( $this->get( $key, $group ) !== false ) {
return false;
}
return $this->set( $key, $data, $group, $expire );
}
public function replace( $key, $data, $group = 'default', $expire = 0 ) {
$derived_key = $this->build_key( $key, $group );
if ( $this->get( $key, $group ) === false ) {
return false;
}
return $this->set( $key, $data, $group, $expire );
}
public function incr( $key, $offset = 1, $group = 'default' ) {
$derived_key = $this->build_key( $key, $group );
try {
$value = $this->redis->incrBy( $derived_key, $offset );
$this->cache[ $derived_key ] = $value;
return $value;
} catch ( Exception $e ) {
return false;
}
}
public function decr( $key, $offset = 1, $group = 'default' ) {
$derived_key = $this->build_key( $key, $group );
try {
$value = $this->redis->decrBy( $derived_key, $offset );
$this->cache[ $derived_key ] = $value;
return $value;
} catch ( Exception $e ) {
return false;
}
}
}
// Initialize global cache object
$GLOBALS['wp_object_cache'] = new WP_Redis_Object_Cache();
/**
* WordPress Object Cache API wrapper functions
*/
function wp_cache_init() {
// Already initialized
}
function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->add( $key, $data, $group, (int) $expire );
}
function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->set( $key, $data, $group, (int) $expire );
}
function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
global $wp_object_cache;
return $wp_object_cache->get( $key, $group, $force, $found );
}
function wp_cache_delete( $key, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->delete( $key, $group );
}
function wp_cache_flush() {
global $wp_object_cache;
return $wp_object_cache->flush();
}
function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
return $wp_object_cache->replace( $key, $data, $group, (int) $expire );
}
function wp_cache_incr( $key, $offset = 1, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->incr( $key, $offset, $group );
}
function wp_cache_decr( $key, $offset = 1, $group = '' ) {
global $wp_object_cache;
return $wp_object_cache->decr( $key, $offset, $group );
}
function wp_cache_add_global_groups( $groups ) {
global $wp_object_cache;
$wp_object_cache->add_global_groups( $groups );
}
function wp_cache_add_non_persistent_groups( $groups ) {
global $wp_object_cache;
$wp_object_cache->add_non_persistent_groups( $groups );
}
┌──────────────────────────────┐
│ Consistent Hashing Ring │
│ (Client-side distribution) │
└───────────┬──────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Memcached│ │Memcached│ │Memcached│
│ Node 1 │ │ Node 2 │ │ Node 3 │
│11211 │ │11211 │ │11211 │
│16GB RAM │ │16GB RAM │ │16GB RAM │
└─────────┘ └─────────┘ └─────────┘
# /etc/memcached.conf
# Memcached 1.6+
# Memory
-m 16384
-I 5m
# Network
-p 11211
-U 0
-l 0.0.0.0
# Connection
-c 4096
-t 8
# Logging
-v
-L
# Memory management
-M
-o modern
-o hashpower=20
-o slab_reassign
-o slab_automove
-o lru_crawler
-o lru_maintainer
# Security
# Use firewall rules instead of SASL for better performance
memcached = new Memcached( 'wordpress' );
$this->memcached->setOption( Memcached::OPT_COMPRESSION, true );
$this->memcached->setOption( Memcached::OPT_COMPRESSION_TYPE, Memcached::COMPRESSION_FASTLZ );
$this->memcached->setOption( Memcached::OPT_SERIALIZER, Memcached::SERIALIZER_IGBINARY );
$this->memcached->setOption( Memcached::OPT_DISTRIBUTION, Memcached::DISTRIBUTION_CONSISTENT );
$this->memcached->setOption( Memcached::OPT_LIBKETAMA_COMPATIBLE, true );
$this->memcached->setOption( Memcached::OPT_BINARY_PROTOCOL, true );
$this->memcached->setOption( Memcached::OPT_TCP_NODELAY, true );
$this->memcached->setOption( Memcached::OPT_CONNECT_TIMEOUT, 1000 );
$this->memcached->setOption( Memcached::OPT_RETRY_TIMEOUT, 1 );
$this->memcached->setOption( Memcached::OPT_SERVER_FAILURE_LIMIT, 2 );
$this->memcached->setOption( Memcached::OPT_REMOVE_FAILED_SERVERS, true );
// Add servers (consistent hashing)
if ( ! $this->memcached->getServerList() ) {
$this->memcached->addServers( [
[ '10.0.4.10', 11211, 100 ], // weight 100
[ '10.0.4.11', 11211, 100 ],
[ '10.0.4.12', 11211, 100 ],
] );
}
}
// Similar implementation to Redis but using Memcached methods
public function get( $key, $group = 'default', $force = false, &$found = null ) {
$derived_key = $this->build_key( $key, $group );
if ( in_array( $group, $this->non_persistent_groups ) ) {
$found = isset( $this->cache[ $derived_key ] );
if ( $found ) {
$this->cache_hits++;
return $this->cache[ $derived_key ];
}
$this->cache_misses++;
return false;
}
if ( ! $force && isset( $this->cache[ $derived_key ] ) ) {
$found = true;
$this->cache_hits++;
return $this->cache[ $derived_key ];
}
$value = $this->memcached->get( $derived_key );
$result_code = $this->memcached->getResultCode();
if ( $result_code === Memcached::RES_SUCCESS ) {
$found = true;
$this->cache_hits++;
$this->cache[ $derived_key ] = $value;
return $value;
}
$found = false;
$this->cache_misses++;
return false;
}
public function set( $key, $data, $group = 'default', $expire = 0 ) {
$derived_key = $this->build_key( $key, $group );
if ( in_array( $group, $this->non_persistent_groups ) ) {
$this->cache[ $derived_key ] = $data;
return true;
}
$this->cache[ $derived_key ] = $data;
$expire = ( $expire === 0 ) ? 0 : time() + $expire;
return $this->memcached->set( $derived_key, $data, $expire );
}
public function delete( $key, $group = 'default' ) {
$derived_key = $this->build_key( $key, $group );
unset( $this->cache[ $derived_key ] );
if ( in_array( $group, $this->non_persistent_groups ) ) {
return true;
}
return $this->memcached->delete( $derived_key );
}
private function build_key( $key, $group = 'default' ) {
if ( empty( $group ) ) {
$group = 'default';
}
$blog_prefix = '';
if ( function_exists( 'is_multisite' ) && is_multisite() ) {
$blog_id = get_current_blog_id();
$blog_prefix = $blog_id . ':';
}
return 'wp:' . $blog_prefix . $group . ':' . $key;
}
public function get_stats() {
$stats = $this->memcached->getStats();
$total_hits = 0;
$total_misses = 0;
$total_memory = 0;
$used_memory = 0;
foreach ( $stats as $server => $server_stats ) {
if ( ! $server_stats ) {
continue;
}
$total_hits += $server_stats['get_hits'] ?? 0;
$total_misses += $server_stats['get_misses'] ?? 0;
$total_memory += $server_stats['limit_maxbytes'] ?? 0;
$used_memory += $server_stats['bytes'] ?? 0;
}
$total = $total_hits + $total_misses;
$hit_rate = $total > 0 ? ( $total_hits / $total ) * 100 : 0;
return [
'hits' => $this->cache_hits,
'misses' => $this->cache_misses,
'hit_rate' => round( $hit_rate, 2 ),
'total_memory' => $this->format_bytes( $total_memory ),
'used_memory' => $this->format_bytes( $used_memory ),
'servers' => count( $stats ),
];
}
private function format_bytes( $bytes ) {
$units = [ 'B', 'KB', 'MB', 'GB', 'TB' ];
$bytes = max( $bytes, 0 );
$pow = floor( ( $bytes ? log( $bytes ) : 0 ) / log( 1024 ) );
$pow = min( $pow, count( $units ) - 1 );
$bytes /= pow( 1024, $pow );
return round( $bytes, 2 ) . ' ' . $units[ $pow ];
}
}
$value ) {
switch ( $type ) {
case 'post':
$post = get_post( $value );
$hash_parts[] = $post ? $post->post_modified : '';
break;
case 'term':
$term = get_term( $value );
$hash_parts[] = $term ? $term->count : '';
break;
case 'option':
$hash_parts[] = get_option( $value );
break;
case 'user':
$user = get_user_by( 'id', $value );
$hash_parts[] = $user ? $user->user_email : '';
break;
}
}
return md5( serialize( $hash_parts ) );
}
/**
* Invalidate fragment cache
*/
public static function invalidate( $key ) {
$cache_key = 'fragment:' . $key;
$deps_key = 'fragment_deps:' . $key;
wp_cache_delete( $cache_key, 'fragments' );
wp_cache_delete( $deps_key, 'fragments' );
}
/**
* Invalidate by pattern (requires Redis)
*/
public static function invalidate_pattern( $pattern ) {
global $wp_object_cache;
if ( ! method_exists( $wp_object_cache->redis, 'scan' ) ) {
return false;
}
$iterator = null;
$prefix = 'fragment:' . $pattern . '*';
while ( false !== ( $keys = $wp_object_cache->redis->scan( $iterator, $prefix ) ) ) {
foreach ( $keys as $key ) {
wp_cache_delete( str_replace( 'fragment:', '', $key ), 'fragments' );
}
}
return true;
}
}
/**
* Helper functions
*/
function wp_fragment_cache( $key, $ttl, $callable, $args = [] ) {
WP_Fragment_Cache::cache( $key, $ttl, $callable, $args );
}
function wp_fragment_invalidate( $key ) {
WP_Fragment_Cache::invalidate( $key );
}
/**
* Usage examples in theme templates
*/
function example_usage() {
// Simple fragment cache
wp_fragment_cache( 'popular_posts', HOUR_IN_SECONDS, function() {
$query = new WP_Query( [ 'meta_key' => 'views', 'orderby' => 'meta_value_num', 'posts_per_page' => 5 ] );
while ( $query->have_posts() ) {
$query->the_post();
get_template_part( 'template-parts/content', 'popular' );
}
wp_reset_postdata();
} );
// Smart cache with dependencies
WP_Fragment_Cache::smart_cache(
'post*' . get_the_ID() . '*related',
DAY_IN_SECONDS,
[ 'post' => get_the_ID(), 'term' => get_the_category()[0]->term_id ],
function( $post_id ) {
$related = new WP_Query( [
'post__not_in' => [ $post_id ],
'category__in' => wp_get_post_categories( $post_id ),
'posts_per_page' => 4,
] );
while ( $related->have_posts() ) {
$related->the_post();
get_template_part( 'template-parts/content', 'related' );
}
wp_reset_postdata();
},
[ get_the_ID() ]
);
}
/**
* Auto-invalidation on content updates
*/
add_action( 'save_post', function( $post_id ) {
// Invalidate post fragment
wp_fragment_invalidate( 'post*' . $post_id . '*related' );
// Invalidate category archives
$categories = wp_get_post_categories( $post_id );
foreach ( $categories as $cat_id ) {
wp_fragment_invalidate( 'category*' . $cat_id . '*posts' );
}
// Invalidate homepage
wp_fragment_invalidate( 'popular_posts' );
wp_fragment_invalidate( 'recent_posts' );
}, 10, 1 );
should_cache_query( $query ) ) {
return $posts;
}
$cache_key = $this->get_cache_key( $query );
$cached = wp_cache_get( $cache_key, 'query_results' );
if ( $cached !== false ) {
// Restore found_posts for pagination
$query->found_posts = $cached['found_posts'];
$query->max_num_pages = $cached['max_num_pages'];
return $cached['posts'];
}
return $posts;
}
/**
* Cache query results after execution
*/
public function maybe_cache_results( $posts, $query ) {
if ( ! $this->should_cache_query( $query ) ) {
return $posts;
}
$cache_key = $this->get_cache_key( $query );
$cache_data = [
'posts' => $posts,
'found_posts' => $query->found_posts,
'max_num_pages' => $query->max_num_pages,
];
wp_cache_set( $cache_key, $cache_data, 'query_results', $this->ttl );
return $posts;
}
/**
* Determine if query should be cached
*/
private function should_cache_query( $query ) {
// Don't cache if explicitly disabled
if ( ! $this->enabled || ! empty( $query->query_vars['cache_results'] ) && $query->query_vars['cache_results'] === false ) {
return false;
}
// Don't cache admin queries
if ( is_admin() && ! wp_doing_ajax() ) {
return false;
}
// Don't cache if user is logged in (unless specifically allowed)
if ( is_user_logged_in() && ! apply_filters( 'wp_query_cache_logged_in', false ) ) {
return false;
}
// Don't cache meta queries (complex to invalidate)
if ( ! empty( $query->query_vars['meta_query'] ) ) {
return false;
}
return true;
}
/**
* Generate unique cache key for query
*/
private function get_cache_key( $query ) {
$key_parts = [
'sql' => $query->request,
'query_vars' => $query->query_vars,
];
return 'query:' . md5( serialize( $key_parts ) );
}
/**
* Invalidate query caches
*/
public static function invalidate_for_post( $post_id ) {
$post = get_post( $post_id );
if ( ! $post ) {
return;
}
// Invalidate by post type
self::invalidate_by_pattern( 'post_type:' . $post->post_type );
// Invalidate by taxonomy
$taxonomies = get_object_taxonomies( $post->post_type );
foreach ( $taxonomies as $taxonomy ) {
$terms = wp_get_object_terms( $post_id, $taxonomy, [ 'fields' => 'ids' ] );
foreach ( $terms as $term_id ) {
self::invalidate_by_pattern( 'taxonomy:' . $taxonomy . ':' . $term_id );
}
}
}
/**
* Invalidate caches by pattern
*/
private static function invalidate_by_pattern( $pattern ) {
// This requires Redis SCAN or similar
// For Memcached, you'll need to track keys separately
WP_Fragment_Cache::invalidate_pattern( $pattern );
}
}
// Initialize
WP_Query_Cache::get_instance();
// Invalidate on post save
add_action( 'save_post', [ 'WP_Query_Cache', 'invalidate_for_post' ] );
add_action( 'delete_post', [ 'WP_Query_Cache', 'invalidate_for_post' ] );
# Redis Benchmark
redis-benchmark -h 10.0.3.10 -p 6379 -a password -t set,get -n 1000000 -c 50 -d 100
SET: 142,857.14 requests per second
GET: 166,666.67 requests per second
Latency (GET P99): 0.8ms
# Memcached Benchmark
memcached-benchmark -s 10.0.4.10:11211 -c 50 -n 1000000
SET: 175,438.60 requests per second
GET: 192,307.69 requests per second
Latency (GET P99): 0.5ms
| Scenario | No Cache | Redis | Memcached | Improvement |
|---|---|---|---|---|
| Homepage (20 posts) | 450ms | 42ms | 38ms | 91% |
| Archive page | 380ms | 35ms | 32ms | 92% |
| Single post | 180ms | 28ms | 25ms | 86% |
| Complex taxonomy query | 850ms | 95ms | 88ms | 90% |
| REST API /wp/v2/posts | 520ms | 68ms | 62ms | 88% |
| Layer | Hit Rate | Avg TTL | Evictions/day |
|---|---|---|---|
| Object Cache (Redis) | 94.3% | 1h | 2,145 |
| Fragment Cache | 91.7% | 6h | 856 |
| Query Cache | 88.2% | 1h | 3,421 |
| Full Page Cache (Varnish) | 96.8% | 1h | 1,234 |
#!/bin/bash
# /usr/local/bin/redis-monitor.sh
REDIS_CLI="/usr/bin/redis-cli"
REDIS_HOST="10.0.3.10"
REDIS_PORT="6379"
REDIS_PASS="your_redis_password"
# Get Redis info
INFO=$($REDIS_CLI -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASS INFO)
# Parse metrics
USED_MEMORY=$(echo "$INFO" | grep "used_memory_human:" | cut -d: -f2 | tr -d 'r')
USED_MEMORY_PEAK=$(echo "$INFO" | grep "used_memory_peak_human:" | cut -d: -f2 | tr -d 'r')
CONNECTED_CLIENTS=$(echo "$INFO" | grep "connected_clients:" | cut -d: -f2 | tr -d 'r')
KEYSPACE_HITS=$(echo "$INFO" | grep "keyspace_hits:" | cut -d: -f2 | tr -d 'r')
KEYSPACE_MISSES=$(echo "$INFO" | grep "keyspace_misses:" | cut -d: -f2 | tr -d 'r')
EVICTED_KEYS=$(echo "$INFO" | grep "evicted_keys:" | cut -d: -f2 | tr -d 'r')
# Calculate hit rate
TOTAL=$((KEYSPACE_HITS + KEYSPACE_MISSES))
if [ $TOTAL -gt 0 ]; then
HIT_RATE=$(awk "BEGIN {printf "%.2f", ($KEYSPACE_HITS / $TOTAL) * 100}")
else
HIT_RATE=0
fi
# Output for Prometheus node exporter textfile collector
cat > /var/lib/node_exporter/textfile_collector/redis.prom <
urls = array_merge( $this->urls, $urls );
}
/**
* Warm critical pages
*/
public function warm_cache() {
// Get critical URLs
$critical_urls = $this->get_critical_urls();
// Process in batches
$batches = array_chunk( $critical_urls, $this->batch_size );
foreach ( $batches as $batch ) {
$this->process_batch( $batch );
}
}
/**
* Get critical URLs to warm
*/
private function get_critical_urls() {
$urls = [];
// Homepage
$urls[] = home_url( '/' );
// Top 20 posts by views
$popular = new WP_Query( [
'post_type' => 'post',
'posts_per_page' => 20,
'meta_key' => 'views',
'orderby' => 'meta_value_num',
'order' => 'DESC',
] );
while ( $popular->have_posts() ) {
$popular->the_post();
$urls[] = get_permalink();
}
wp_reset_postdata();
// Main category archives
$categories = get_categories( [ 'number' => 10, 'orderby' => 'count', 'order' => 'DESC' ] );
foreach ( $categories as $category ) {
$urls[] = get_category_link( $category->term_id );
}
// Add custom URLs
$urls = array_merge( $urls, $this->urls );
return array_unique( $urls );
}
/**
* Process batch of URLs
*/
private function process_batch( $batch ) {
$multi = curl_multi_init();
$handles = [];
foreach ( $batch as $url ) {
$ch = curl_init( $url );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_TIMEOUT => 30,
CURLOPT_USERAGENT => 'WordPress Cache Warmer',
CURLOPT_HEADER => true,
] );
curl_multi_add_handle( $multi, $ch );
$handles[] = $ch;
}
// Execute all requests
$running = null;
do {
curl_multi_exec( $multi, $running );
curl_multi_select( $multi );
} while ( $running > 0 );
// Clean up
foreach ( $handles as $ch ) {
curl_multi_remove_handle( $multi, $ch );
curl_close( $ch );
}
curl_multi_close( $multi );
}
}
// Initialize
$cache_warmer = new WP_Cache_Warmer();
// Add custom URLs
$cache_warmer->add_urls( [
home_url( '/about' ),
home_url( '/contact' ),
home_url( '/services' ),
] );
Le caching distribué avec Redis et Memcached permet de scaler WordPress à des millions de requêtes par jour:
Redis - Recommandé si:
Memcached - Recommandé si:
La clé du succès: monitoring continu, invalidation intelligente, et tuning progressif basé sur les métriques réelles.
Cet article est vivant - corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.