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?
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'));
?>