Search code examples
phpsymfonyredisamazon-elasticache

Configure symfony cache to use AWS Redis read replicas


I started up a Redis cluster in AWS, with two nodes: the primary, and a replica. The cluster has two entrypoints: primary entrypoint, and reader entrypoint

In my Symfony app I'm using the primary entrypoint in the .env configuration

REDIS_HOST=xxx-redis.yyy.ng.0001.zone.cache.amazonaws.com
REDIS_PORT=6379

and cache configuration under the framework.cache in the cache.yaml like this:

app: cache.adapter.redis
default_redis_provider: redis://%env(REDIS_HOST)%:%env(int:REDIS_PORT)%

The app is working well and cache is being used, but I noticed that only the primary node is being used, the replica does not register any hit.

I googled a lot for configuration examples on how to add the read only entrypoint to the symfony redis configuration, but found nothing.

So, I hope somebody has had the same issue and figured out the solution. I think that maybe doing a custom cache adapter, or something like that maybe?


Solution

  • To utilize replication, you can provide multiple URL's inside Redis DNS as stated inside this documentation. Use AWS provided main Primary and Reader endpoints, not the ones from individual nodes. So you only need 2 URL's, no

    default_redis_provider: 'redis:?host[xxx-redis.yyy.ng.0001.zone.cache.amazonaws.com]&host[xxx-redis.yyy-ro.ng.0001.zone.cache.amazonaws.com]'
    

    Please note that there is no // after redis:. All this information can be read and debugged inside /vendor/symfony/cache/Traits/RedisTrait.php createConnection method.

    Now the fun part if using Predis package:

    It requires adding ?role=master and optionally ?role=slave into host URL.

    So your YAML config would look like this:

    default_redis_provider: 'redis:?host[xxx-redis.amazonaws.com%3Frole%3Dmaster]&host[xxx-redis-ro.amazonaws.com%3Frole%3Dslave]'
    

    • Keep in mind you additionally need to escape %
    • To bypass espacing you can do host[xxx-redis.amazonaws.com][role]=master
    • If writing previous point in multi-line format using >- there will be white spaces added to parameters:
    default_redis_provider: >-
        ?host[xxx-redis.amazonaws.com][role]=master
        &host[xxx-redis-ro.amazonaws.com][role]=slave
    

    becomes ?host[xxx-redis.amazonaws.com][role]=master &host[xxx-redis-ro.amazonaws.com][role]=slave


    For this I have made my own impelementation of RedisAdapter:

    framework:
        cache:
            prefix_seed: '%env(ENVIRONMENT)%'
            pools:
                external.cache:
                    adapter: cache.adapter.redis_tag_aware
                    provider: App\Component\Cache\Adapter\RedisAdapter
    
    services:
        App\Component\Cache\Adapter\RedisAdapter:
            factory: [null, 'createReplicaConnection']
            arguments:
                $primaryHost: '%app.cache.redis.host.primary%'
                $readerHost: '%app.cache.redis.host.reader%'
    
    use Predis\Client;
    use Symfony\Component\Cache\Adapter\RedisAdapter as BaseRedisAdapter;
    
    class RedisAdapter extends BaseRedisAdapter
    {
        public static function createReplicaConnection(
            string|array $primaryHost,
            null|string|array $readerHost = null,
            array $options = [],
        ): \Redis|\RedisArray|\RedisCluster|ClientInterface|Relay {
            $data = [
                'host' => [],
                'class' => Client::class,
                'replication' => 'predis',
            ];
    
            $primaryHost = array_merge(
                ['role' => 'master', 'alias' => 'primary'],
                is_string($primaryHost) ? ['host' => $primaryHost] : $primaryHost,
            );
    
            $data['host'][$primaryHost['host']] = $primaryHost;
    
            if ($readerHost !== null) {
                $readerHost = array_merge(
                    ['role' => 'slave', 'alias' => 'reader'],
                    is_string($readerHost) ? ['host' => $readerHost] : $readerHost,
                );
                $data['host'][$readerHost['host']] = $readerHost;
            }
    
            return static::createConnection('redis:?' . http_build_query($data), $options);
        }
    

    Symfony 7.1 has issues with Predis: https://github.com/symfony/symfony/issues/49238