Search code examples
phpredisphpredis

PHP redis session handler - changing number of servers


I am using phpredis as a session handler (https://github.com/phpredis/phpredis). My current connection string looks like this:

session.save_path = "tcp://10.0.1.11:7005?weight=1&timeout=0.2&persistent=1&read_timeout=0.5,
tcp://10.0.1.12:7005?weight=1&timeout=0.2&persistent=1&read_timeout=0.5"

but i need to add more redis servers and move existing sessions among them.

My new connection string will look like this:

session.save_path = "tcp://10.0.1.11:7005?weight=1&timeout=0.2&persistent=1&read_timeout=0.5,
tcp://10.0.1.12:7005?weight=1&timeout=0.2&persistent=1&read_timeout=0.5,
tcp://10.0.1.13:7005?weight=1&timeout=0.2&persistent=1&read_timeout=0.5,
tcp://10.0.1.14:7005?weight=1&timeout=0.2&persistent=1&read_timeout=0.5,
tcp://10.0.1.15:7005?weight=1&timeout=0.2&persistent=1&read_timeout=0.5"

so there will be 3 more servers and sessions distribution by key will change with number of servers.

How can i move existing sessions from old servers to new servers to have each one on right server then? Are there any existing tools for this? Has anyone had a similar problem and have a ready solution for it?


Solution

  • I have ready php script that can migrate all sessions to new servers while preserving the same layout as phpredis session handler is using. It support batching and redis->migrate() or manual redis->dump() and redis->restore(). Maybe it will be useful to someone with the same problem.

    <?php
    class RedisPoolMember {
        public Redis $redis_sock;
        public string $hostname;
        public int $port;
        public int $weight;
        public $next;
    
        public function __construct(string $hostname, int $port, int $weight) {
            $this->redis_sock = new Redis(); 
            $this->redis_sock->connect($hostname, $port);
            $this->weight = $weight;
            $this->hostname = $hostname;
            $this->port = $port;
            $this->next = null;
        }
    }
    
    class RedisPool {
        public $totalWeight = 0;
        public $count = 0;
        public $head = null;
        public $lock_status;
    
        public function add(string $hostname, int $port, int $weight) {
            
            $rpm = new RedisPoolMember($hostname, $port, $weight);
            $rpm->next = $this->head;
            $this->head = $rpm;
    
            $this->totalWeight += $weight;
            $this->count++;
        }
        
        public function get(string $key): ?RedisPoolMember {
            $pos = unpack("L", substr($key, 0, 4))[1]; // Assuming a little-endian order
            $pos %= $this->totalWeight;
    
            $rpm = $this->head;
    
            for ($i = 0; $i < $this->totalWeight;) {
                if ($pos >= $i && $pos < $i + $rpm->weight) {
                    return $rpm;
                }
                $i += $rpm->weight;
                $rpm = $rpm->next;
            }
    
            return null;
        }
    }
    
    function saveKeyPool(RedisPool &$pool, string $sessionPrefix, string $key, $value) {
        $sid = substr($key, strlen($sessionPrefix));
        $rpm = $pool->get($sid);
        $rpm->redis_sock->restore($key, $value);
    }
    
    function readKeyPool(RedisPool &$pool, string $sessionPrefix, string $key) {
        $sid = substr($key, strlen($sessionPrefix));
        $rpm = $pool->get($sid);
        return $rpm->redis_sock->dump($key);
    }
    
    function migrateKey(RedisPool &$oldPool, RedisPool &$newPool, string $sessionPrefix, string $key) {
        echo('.');
        $sid = substr($key, strlen($sessionPrefix));
        $rpmOld = $oldPool->get($sid);
        $rpmNew = $newPool->get($sid);
        //echo("Migrate key $key to {$rpmNew->hostname}:{$rpmNew->port}\n");
        $rpmOld->redis_sock->migrate($rpmNew->hostname, $rpmNew->port, $key, 0, 0, true, true);
    }
    
    function getKeyDestinationRpm(RedisPool &$newPool, string $sessionPrefix, string $key) {
        $sid = substr($key, strlen($sessionPrefix));
        $rpmNew = $newPool->get($sid);
        return $rpmNew->hostname.':'.$rpmNew->port;
    }
    
    function processBatch(Redis &$oldRedis, array &$batch) {
        foreach($batch as $server=>$keys) {
            $server = explode(':', $server);
            $oldRedis->migrate($server[0], $server[1], $keys, 0, 0, true, true);
        }
    }
    
    $sessionPrefix = 'PHPREDIS_SESSION:';
    $oldRedisServers = [['hostname'=>'10.0.1.11', 'port'=>7005], ['hostname'=>'10.0.1.12', 'port'=>7005]];
    
    $redisPoolOld = new RedisPool();
    $redisPoolOld->add('10.0.1.11', 7005, 1);
    $redisPoolOld->add('10.0.1.12', 7005, 1);
    
    
    $redisPoolNew = new RedisPool();
    $redisPoolNew->add('10.0.1.11', 7010, 1);
    $redisPoolNew->add('10.0.1.11', 7011, 1);
    $redisPoolNew->add('10.0.1.11', 7012, 1);
    $redisPoolNew->add('10.0.1.11', 7013, 1);
    $redisPoolNew->add('10.0.1.11', 7014, 1);
    $redisPoolNew->add('10.0.1.11', 7015, 1);
    $redisPoolNew->add('10.0.1.12', 7010, 1);
    $redisPoolNew->add('10.0.1.12', 7011, 1);
    $redisPoolNew->add('10.0.1.12', 7012, 1);
    $redisPoolNew->add('10.0.1.12', 7013, 1);
    $redisPoolNew->add('10.0.1.12', 7014, 1);
    $redisPoolNew->add('10.0.1.12', 7015, 1);
    
    foreach($oldRedisServers as $redisServerData) {
        $oldRedis = new Redis();
        $oldRedis->connect($redisServerData['hostname'], $redisServerData['port']);
        $count = 0;
        // Get all keys
        $it = NULL;
        do {
            // Use scan to get the next batch of keys and update the iterator
            $arr_keys = $oldRedis->scan($it, '*', 1000);
            $batch = [];
            if ($arr_keys !== FALSE) {
                foreach ($arr_keys as $str_key) {
                    //migrateKey($redisPoolOld, $redisPoolNew, $sessionPrefix, $str_key);
                    $batch[getKeyDestinationRpm($redisPoolNew, $sessionPrefix, $str_key)] []= $str_key;
                }
            }
            processBatch($oldRedis, $batch);
            $count += 1000;
            echo("\n{$redisServerData['hostname']}:{$redisServerData['port']} - $count migrated\n"); //1000 records mark
        } while ($it > 0);
    }
    
    //test functions
    //$key = 'PHPREDIS_SESSION:d09d7358fa84cf68455fee3564e126a5';
    //$value = readKeyPool($redisPoolOld, $sessionPrefix, $key);
    //saveKeyPool($redisPoolNew, $sessionPrefix, $key, $value);
    //migrateKey($redisPoolOld, $redisPoolNew, $sessionPrefix, $key);
    //var_dump(readKeyPool($redisPoolOld, $sessionPrefix, 'PHPREDIS_SESSION:d09d7358fa84cf68455fee3564e126a5'));
    ?>