Redis高可用性之Failover过渡方案
时间:2015-09-19 22:32 来源:linux.it.net.cn 作者:IT
从Redis官方路线图来看,大概会在Redis3.0左右正式支持Cluster。不过即便是乐观的估计,至少也得等几个月的时间,为了让我的应用在这段时间内能保持高可用性,我以主从服务器为基础实现了一个Failover过渡方案。
从理论上解释,一旦主服务器下线,可以在从服务器里挑选出新的主服务器,同时重新设置主从关系,并且当下线服务器重新上线后能自动加入到主从关系中去,内容如下:
<?php
class RedisFailover
{
public $config = array();
public $map = array();
const CONFIG_FILE = 'config.php';
const MAP_FILE = 'map.php';
public function __construct()
{
$config = include self::CONFIG_FILE;
foreach ((array)$config as $name => $nodes) {
foreach ($nodes as $node) {
$node = new RedisNode($node['host'], $node['port']);
if ($node->isValid()) {
$this->config[$name][] = $node;
}
}
if (empty($this->config[$name])) {
throw new Exception('Invalid config.');
}
$this->map[$name] = $this->config[$name][0];
}
if (file_exists(self::MAP_FILE)) {
$map = include self::MAP_FILE;
foreach ((array)$map as $name => $node) {
$node = new RedisNode($node['host'], $node['port']);
$this->map[$name] = $node;
}
}
}
public function run()
{
$set_nodes_master = function($nodes, $master) {
foreach ($nodes as $node) {
$node->setMaster($master->host, $master->port);
}
};
foreach ($this->config as $name => $nodes) {
$is_master_valid = false;
foreach ($nodes as $node) {
if ($node == $this->map[$name]) {
$is_master_valid = true;
break;
}
}
if ($is_master_valid) {
$set_nodes_master($nodes, $this->map[$name]);
continue;
}
foreach ($nodes as $node) {
$master = $node->getMaster();
if (empty($master)) {
continue;
}
if ($master['master_host'] != $this->map[$name]->host) {
continue;
}
if ($master['master_port'] != $this->map[$name]->port) {
continue;
}
if ($master['master_sync_in_progress']) {
continue;
}
$node->clearMaster();
$set_nodes_master($nodes, $node);
$this->map[$name] = $node;
break;
}
}
$map = array();
foreach ($this->map as $name => $node) {
$map[$name] = array(
'host' => $node->host, 'port' => $node->port
);
}
$content = '<?php return ' . var_export($map, true) . '; ?>';
file_put_contents(self::MAP_FILE, $content);
}
}
class RedisNode
{
public $host;
public $port;
const CLI = '/usr/local/bin/redis-cli';
public function __construct($host, $port)
{
$this->host = $host;
$this->port = $port;
}
public function setMaster($host, $port)
{
if ($this->host != $host || $this->port != $port) {
return $this->execute("SLAVEOF {$host} {$port}") == 'OK';
}
return false;
}
public function getMaster()
{
$result = array();
$this->execute('INFO', $rows);
foreach ($rows as $row) {
if (preg_match('/^master_/', $row)) {
list($key, $value) = explode(':', $row);
$result[$key] = $value;
}
}
return $result;
}
public function clearMaster()
{
return $this->execute('SLAVEOF NO ONE') == 'OK';
}
public function isValid()
{
return $this->execute('PING') == 'PONG';
}
public function execute($command, &$output = null)
{
return exec(
self::CLI . " -h {$this->host} -p {$this->port} {$command}", $output
);
}
}
?>
其中提到了两个文件,先说一下config.php:
<?php
return array(
'redis_foo' => array(
array('host' => '192.168.0.1', 'port' => '6379'),
array('host' => '192.168.0.2', 'port' => '6379'),
array('host' => '192.168.0.3', 'port' => '6379'),
),
);
?>
说明:每个别名对应一组服务器,在这组服务器中,有一个是主服务器,其余都是从服务器,主从关系不要在配置文件里硬编码,而应该通过SLAVEOF命令动态设定。
再说一下map.php文件,内容如下:
<?php
return array (
'redis_foo' => array (
'host' => '192.168.0.1', 'port' => '6379'
),
);
?>
说明:别名对应的是当前有效的服务器。需要注意的是这个文件是自动生成的!程序在使用Redis的时候,都配置成别名的形式,具体的host,port通过此文件映射获得。
明白了以上代码之后,运行就很简单了:
<?php
$failover = new RedisFailover();
$failover->run();
?>
说明:实际部署时,最严格的方式是以守护进程的方式来执行,不过如果要求不是很苛刻的话,CRON就够了。测试时可以手动杀掉主服务器进程,再通过INFO查看效果。
再补充一些命令行用法的相关说明,本文都是使用redis-cli来发送命令的,通常这也是最佳选择,不过如果因为某些原因不能使用redis-cli的话,也可以使用nc(netcat)命令按照Redis协议实现一个简单的客户端工具,比如说PING命令可以这样实现:
shell> (echo -en "PING\r\n"; sleep 1) | nc localhost 6379
说明:之所以需要sleep一下是因为Redis的请求响应机制是Pipelining方式的。
既然说到这里了,就再唠十块钱儿的,通常,我们可以使用telnet命令和服务交互,但是telnet有一点非常不爽的是命令行不支持上下键历史,还好可以借助rlwrap来达成这个目的,视操作系统,可以很容易的用APT或YUM来安装,运行也很简单:
shell> rlwrap telnet localhost 6379
说明:通过使用rlwrap,不仅支持上下键历史,而且连Ctrl+r搜索也一并支持了,强!
…
在Redis Cluster释出前,希望这个脚本能帮到你,其实其他的服务也可以使用类似的方案,比如MySQL,不过复杂性会加大很多,好在已经有类似MHA之类的方案了。
(责任编辑:IT)
从Redis官方路线图来看,大概会在Redis3.0左右正式支持Cluster。不过即便是乐观的估计,至少也得等几个月的时间,为了让我的应用在这段时间内能保持高可用性,我以主从服务器为基础实现了一个Failover过渡方案。 从理论上解释,一旦主服务器下线,可以在从服务器里挑选出新的主服务器,同时重新设置主从关系,并且当下线服务器重新上线后能自动加入到主从关系中去,内容如下: <?php class RedisFailover { public $config = array(); public $map = array(); const CONFIG_FILE = 'config.php'; const MAP_FILE = 'map.php'; public function __construct() { $config = include self::CONFIG_FILE; foreach ((array)$config as $name => $nodes) { foreach ($nodes as $node) { $node = new RedisNode($node['host'], $node['port']); if ($node->isValid()) { $this->config[$name][] = $node; } } if (empty($this->config[$name])) { throw new Exception('Invalid config.'); } $this->map[$name] = $this->config[$name][0]; } if (file_exists(self::MAP_FILE)) { $map = include self::MAP_FILE; foreach ((array)$map as $name => $node) { $node = new RedisNode($node['host'], $node['port']); $this->map[$name] = $node; } } } public function run() { $set_nodes_master = function($nodes, $master) { foreach ($nodes as $node) { $node->setMaster($master->host, $master->port); } }; foreach ($this->config as $name => $nodes) { $is_master_valid = false; foreach ($nodes as $node) { if ($node == $this->map[$name]) { $is_master_valid = true; break; } } if ($is_master_valid) { $set_nodes_master($nodes, $this->map[$name]); continue; } foreach ($nodes as $node) { $master = $node->getMaster(); if (empty($master)) { continue; } if ($master['master_host'] != $this->map[$name]->host) { continue; } if ($master['master_port'] != $this->map[$name]->port) { continue; } if ($master['master_sync_in_progress']) { continue; } $node->clearMaster(); $set_nodes_master($nodes, $node); $this->map[$name] = $node; break; } } $map = array(); foreach ($this->map as $name => $node) { $map[$name] = array( 'host' => $node->host, 'port' => $node->port ); } $content = '<?php return ' . var_export($map, true) . '; ?>'; file_put_contents(self::MAP_FILE, $content); } } class RedisNode { public $host; public $port; const CLI = '/usr/local/bin/redis-cli'; public function __construct($host, $port) { $this->host = $host; $this->port = $port; } public function setMaster($host, $port) { if ($this->host != $host || $this->port != $port) { return $this->execute("SLAVEOF {$host} {$port}") == 'OK'; } return false; } public function getMaster() { $result = array(); $this->execute('INFO', $rows); foreach ($rows as $row) { if (preg_match('/^master_/', $row)) { list($key, $value) = explode(':', $row); $result[$key] = $value; } } return $result; } public function clearMaster() { return $this->execute('SLAVEOF NO ONE') == 'OK'; } public function isValid() { return $this->execute('PING') == 'PONG'; } public function execute($command, &$output = null) { return exec( self::CLI . " -h {$this->host} -p {$this->port} {$command}", $output ); } } ?> 其中提到了两个文件,先说一下config.php: <?php return array( 'redis_foo' => array( array('host' => '192.168.0.1', 'port' => '6379'), array('host' => '192.168.0.2', 'port' => '6379'), array('host' => '192.168.0.3', 'port' => '6379'), ), ); ?> 说明:每个别名对应一组服务器,在这组服务器中,有一个是主服务器,其余都是从服务器,主从关系不要在配置文件里硬编码,而应该通过SLAVEOF命令动态设定。 再说一下map.php文件,内容如下: <?php return array ( 'redis_foo' => array ( 'host' => '192.168.0.1', 'port' => '6379' ), ); ?> 说明:别名对应的是当前有效的服务器。需要注意的是这个文件是自动生成的!程序在使用Redis的时候,都配置成别名的形式,具体的host,port通过此文件映射获得。 明白了以上代码之后,运行就很简单了: <?php $failover = new RedisFailover(); $failover->run(); ?> 说明:实际部署时,最严格的方式是以守护进程的方式来执行,不过如果要求不是很苛刻的话,CRON就够了。测试时可以手动杀掉主服务器进程,再通过INFO查看效果。 再补充一些命令行用法的相关说明,本文都是使用redis-cli来发送命令的,通常这也是最佳选择,不过如果因为某些原因不能使用redis-cli的话,也可以使用nc(netcat)命令按照Redis协议实现一个简单的客户端工具,比如说PING命令可以这样实现: shell> (echo -en "PING\r\n"; sleep 1) | nc localhost 6379 说明:之所以需要sleep一下是因为Redis的请求响应机制是Pipelining方式的。 既然说到这里了,就再唠十块钱儿的,通常,我们可以使用telnet命令和服务交互,但是telnet有一点非常不爽的是命令行不支持上下键历史,还好可以借助rlwrap来达成这个目的,视操作系统,可以很容易的用APT或YUM来安装,运行也很简单: shell> rlwrap telnet localhost 6379 说明:通过使用rlwrap,不仅支持上下键历史,而且连Ctrl+r搜索也一并支持了,强! … 在Redis Cluster释出前,希望这个脚本能帮到你,其实其他的服务也可以使用类似的方案,比如MySQL,不过复杂性会加大很多,好在已经有类似MHA之类的方案了。 (责任编辑:IT) |