异地远程同步memcache

近来遇到一个需求,是要在两地机房间进行缓存的同步

实现方案如下:

在两地机房cache在本地执行后,插入本地的维护两个不同方向的队列,利用http将队列中的数据打包传送到异地,解包后处理。完成同步。

1. 为避免大流量的缓存同步请求造成队列拥堵,对缓存的写操作进行分类,在cache基类里封装是否同步的开关,避免不需要同步的cache进入队列造成数据异常。

2. 队列需要的是顺序执行,要求有较快的插入,弹出以及取长度(便于计算同步效率)效率,对排序和随机插入没有要求,但是对数据要求要有持久性,便于同步脚本出问题或者机器网络故障进行数据恢复。因此选择了redis的List作为队列存储。

3. 原本最先考虑的是利用Gearman进行同步。其特点是实现简单,gearman本身封装了任务队列,只需要在代理机器上运行gearmand服务器端,在两地各个memcache集群机器上启动worker,通过分布式计算,实现负载均衡。但是此方案对做代理的server端稳定性要求高,存在server端与worker端成功建立连接后crash,worker端实现自动切换问题,并且引入gearmand这个架构增加运维成本,因此在技术实现上没有采取这种方案。

4. 后来看到agentzh大牛的openresty项目,实现了很多效率很高的nginx模块,利用Nginx事件模型来进行非阻塞I/O通信。可以利用HttpLuaModule进行nginx模块开发,直接利用httpMemc/srcacheModule进行memcache操作或lua-resty-memcached模块直接用Lua进行memcache操作。QPS大大提高。

5.  在本地服务器上启动一个类似守护进程,将本地执行成功后的cache命令压入队列,1000个分成一个数据包,curl发送到异地nginx。这里采用的是redis的Hash(哈希表)结构。

6. 为了可维护性和可扩展性,最终方案选定由HttpLuaModule和Lua-resty-memcached模块实现memcache相关操作。用lua实现set和delete操作开始是采用的是subrequest方式也就是ngx.location.capture_multi来实现,这个好处是响应是按照请求顺序返回,后面程序等待所有结果返回后才开始处理。后来考虑到memcache操作memcache集群返回值并不要求完全统一,采用了异步的Lua轻量级线程ngx.thread.spawn,这样可实现“先到先服务”。例如,当delete操作有一台机器返回HTTP.OK是可认为操作成功即将该命令和key值压入异地同步队列而无需等所有机器响应全部返回。

7. 实际过程中,发现使用的是PECL的memcache扩展,一致性hash算法Lua-resty-memcached的默认算法不同,需要实现Lua版本的一致性算法。这样在set的时候可以根据计算key值hash制定机器进行set。为了简单delete采取了轮询删除的方式。

8. 当然,如果自己实现一致性hash,可以利用fastcgi方式调用php脚本利用PECL的memcache扩展实现set以保证一致性。这样需要得到curl的post数据,上文1000个cache数据,而PHP实现的$_REQUEST 方法会自动解析,导致只有$_REQUEST数组中只会有最后一组数据。可采用file_get_contents("php://input")得到原始的POST数据。

部分实现代码如下:

1> 本地处理cache成功后并插入当前队列

local router = require "meilishuo.router"
local memcached = require "resty.memcached"
local redis = require "resty.redis"
local config = ngx.shared.config
local insert = table.insert

--ngx.req.read_body()
local param = ngx.req.get_body_data()
local request_args = router:router_mem(param)

local method = "/" .. request_args.method
local threads = {}
local total = config:get("total");

local fifo = redis:new();

--调用resty.memcached 完成delete操作
local function query_memcache(no, key) 
    local cacheHandle = memcached:new();

    local host, port =  config:get("cache_" .. no .. "_host"), config:get("cache_" .. no .. "_port")
    local ok, err = cacheHandle:connect(host, port);
    if not ok then
        ngx.say("cache server " .. host .. ":" .. port .. " went away")
    end 

    local res, err = cacheHandle:delete(key)
    if res then
        ngx.say("found cache in server" .. host .. ":" .. port)
        response = 'OK'
    elseif err == 'NOT_FOUND' then
        ngx.say("cache not found in server " .. host .. ":" .. port)
        response = 'NOT_FOUND'
    elseif not res then
        response = 'ERROR'
        ngx.say("cache server" .. host .. ":" .. port .. "write error")
        ngx.exit(ngx.HTTP_OK)
    end

    cacheHandle:set_keepalive(200, 300 * config:get("total"));
    --ngx.say(response)
    return response
end

--本地操作delete cache成功后,执行插入队列
local function insert_redis(config, fifo, param) 
    local host, port = config:get("fifo_host"), config:get("fifo_port");
    local ok, err = fifo:connect(host, port);
    if not ok then
        ngx.log(ngx.ERR, "fifo server " .. host .. ":" .. port .. " went away")
    end 
    local fifokey = "FIFO:MEM"
    res = fifo:lpush(fifokey, param)

    --to the connection pool
    fifo:set_keepalive(200, 200 * config:get("total"));
end

if (request_args.method == 'delete') then
    for no = 1, total, 1 do
        --ngx.thread.spawn线程并发
        local co = ngx.thread.spawn(query_memcache, no, request_args.key)
        insert(threads, co)
    end
    
    for i = 1, #threads do
        local ok, res = ngx.thread.wait(threads[i])
        if not ok then
            ngx.say(i, ": failed to run: ", res)
        else
            ngx.say(i, ": status: ", res)
            if (res == 'OK') then 
                insert_redis(config, fifo, param)
            end
        end
    end 
elseif (request_args.method == 'set') then
    local host, port = config:get("fifo_host"), config:get("fifo_port");
    local ok, err = fifo:connect(host, port);
    if not ok then
        ngx.log(ngx.ERR, "fifo server " .. host .. ":" .. port .. " went away")
    end
    local fifokey = "FIFO:MEM:SET"
    res = fifo:lpush(fifokey, param)

    --to the connection pool
    fifo:set_keepalive(200, 200 * config:get("total"));
end

2> fastcgi setcache脚本

<?php
/**
 * set cache 脚本
 */
header('Content-type: text/html;charset=utf8');

//得到原始的POST数据
$data = file_get_contents("php://input");
//得到GET数据
$stamp = $_GET['stamp'];

$cacheObj = Cache::instance();

$logHandle = new log('cache_content', 'normal');

//初始化redis 查看cache队列处理status
$redis = new Redis();
$redis_key = "FIFO:status";
$host = "192.168.60.4";
$port = 6579;
$timeout = 5;
$result = $redis->pconnect($host, $port, $timeout);
if (empty($result)) {
    $str = "redis server went away!";
    $logHandle->w_log(print_r($str, TRUE));    
    exit;
}

//得到本次操作前 process status
$result = $redis->hgetall($redis_key);
$processing = $result['processing'];
$processed = $result['processed'];
if ($stamp <= $processed) {
    echo "$stamp has been processed!\n";
}
if ($stamp <= $processing) {
    echo "$stamp has been processing!\n";
}
$redis->hset($redis_key, 'processing', $stamp);

$response = split_and_set($data, '||', $cacheObj, $logHandle);
if (empty($response)) {
    $log = new zx_log('cache_content_error', 'normal');
    $str = "stamp: $stamp error when operate, content is: $data\n";
    $logHandle->w_log(print_r($str, TRUE));
}

//本次操作后设置redis
$redis->hset($redis_key, 'processed', $stamp);

//处理cache值并set
function split_and_set($str, $delimiter = '||', $cacheObj, $logHandle) {
    $resp = array();
    $data = explode($delimiter, $str);
    if (empty($data)) {
        return FALSE;
    }
    foreach($data as $key => $value) {
        $temp = explode('&', $value);
        array_shift($temp);
        if (empty($temp[0]) || empty($temp[1]) || empty($temp[2])) {
            continue;
        }
        $resp[$key]['key'] = substr($temp[0], strlen('key='));
        $resp[$key]['expire'] = substr($temp[1], strlen('expire='));
        $arg = substr($temp[2], strlen('arg='));
        $resp[$key]['value'] = unserialize(base64_decode($arg));

        $logHandle->w_log(print_r($resp[$key], TRUE));
        $cacheObj->set($resp[$key]['key'], $resp[$key]['value'], $resp[$key]['expire']);
    } 
    return TRUE;
}

3> lua实现一致性hash  这个直接从github上拿了一份别人的 地址:https://github.com/alacner/flexihash

--Flexihash - A simple consistent hashing implementation for Lua.

--local nix = require "nix"

module('Flexihash', package.seeall)

Flexihash_Crc32Hasher = {
    hash = function(string) return ngx.crc32_short(string) end
}

Flexihash_Md5Hasher = {
    hash = function(string) return string.sub(ngx.md5(string), 0, 8) end -- 8 hexits = 32bit
}

local function array_keys_values(tbl)
    local keys, values = {}, {}
    for k,v in pairs(tbl) do
        table.insert(keys, k)
        table.insert(values, v)
    end
    return keys, values
end

local function __toString(this)
end

--[[
-- Sorts the internal mapping (positions to targets) by position
--]]
local function _sortPositionTargets(this)
    -- sort by key (position) if not already
    if not this._positionToTargetSorted then
        this._sortedPositions = array_keys_values(this._positionToTarget)
        table.sort(this._sortedPositions)
        this._positionToTargetSorted = true
    end
end

--[[
-- Add a target.
-- @param string target
--]]
local function addTarget(this, target)
    if this._targetToPositions[target] then
        return false, "Target '" .. target .."' already exists."
    end

    this._targetToPositions[target] = {}

    -- hash the target into multiple positions
    for i = 0, this._replicas-1 do
        local position = this._hasher(target .. i)
        this._positionToTarget[position] = target -- lookup
        table.insert(this._targetToPositions[target], position) -- target removal
    end

    this._positionToTargetSorted = false;
    this._targetCount = this._targetCount + 1
    return this
end

--[[
-- Add a list of targets.
--@param table targets
--]]
local function addTargets(this, targets)
    for k,target in pairs(targets) do
        addTarget(this, target)
    end
    return this
end

--[[
-- Remove a target.
-- @param string target
--]]
local function removeTarget(this, target)
    if not this._targetToPositions[target] then
        return false, "Target '" .. target .. "' does not exist."
    end

    for k,position in pairs(this._targetToPositions[target]) do
        if this._positionToTarget[position] then
            this._positionToTarget[position] = nil
        end
    end

    this._targetToPositions[target] = nil
    this._targetCount = this._targetCount - 1

    return this
end

--[[
-- A list of all potential targets
-- @return array
--]]
local function getAllTargets(this)
    local targets = {}
    for target,v in pairs(this._targetToPositions) do
        table.insert(targets, target)
    end
    return targets
end

--[[
-- Get a list of targets for the resource, in order of precedence.
-- Up to $requestedCount targets are returned, less if there are fewer in total.
--
-- @param string resource
-- @param int requestedCount The length of the list to return
-- @return table List of targets
--]]
local function lookupList(this, resource, requestedCount)
    if tonumber(requestedCount) == 0 then
        return {}, 'Invalid count requested'
    end

    -- handle no targets
    if this._targetCount == 0 then
        return {}
    end

    -- optimize single target
    if this._targetCount == 1 then
        local keys, values = array_keys_values(this._positionToTarget)
        return {values[1]}
    end

    -- hash resource to a position
    local resourcePosition = this._hasher(resource)

    local results, _results = {}, {}
    local collect = false;

    this._sortPositionTargets(this)

    -- search values above the resourcePosition
    for i,key in ipairs(this._sortedPositions) do
        -- start collecting targets after passing resource position
        if (not collect) and key > resourcePosition then
            collect = true
        end

        local value = this._positionToTarget[key]

        -- only collect the first instance of any target
        if collect and (not _results[value]) then
            table.insert(results, value)
            _results[value] = true
        end
        -- return when enough results, or list exhausted
        if #results == requestedCount or #results == this._targetCount then
            return results
        end
    end

    -- loop to start - search values below the resourcePosition
    for i,key in ipairs(this._sortedPositions) do
        local value = this._positionToTarget[key]

        if not _results[value] then
            table.insert(results, value)
            _results[value] = true
        end
        -- return when enough results, or list exhausted
        if #results == requestedCount or #results == this._targetCount then
            return results
        end
    end

    -- return results after iterating through both "parts"
    return results
end

--[[
-- Looks up the target for the given resource.
-- @param string resource
-- @return string
--]]
local function lookup(this, resource)
    local targets = this.lookupList(this, resource, 1)
    if #targets == 0 then
        return false, 'No targets exist'
    end
    return targets[1]
end

function New(...)
    local hasher, replicas = ...

    if type(hasher) ~= 'function' then
        hasher = hasher or Flexihash_Crc32Hasher.hash
    end

    replicas = replicas or 64

    local this = {
        _replicas = replicas, --The number of positions to hash each target to.
        _hasher = hasher, --The hash algorithm, encapsulated in a Flexihash_Hasher implementation.
        _targetCount = 0, --Internal counter for current number of targets.
        _positionToTarget = {}, --Internal map of positions (hash outputs) to targets @var array { position => target, ... }
        _targetToPositions = {}, --Internal map of targets to lists of positions that target is hashed to. @var array { target => [ position, position, ... ], ... }
        _sortedPositions = {},
        _positionToTargetSorted = false, --Whether the internal map of positions to targets is already sorted.
        _sortPositionTargets = _sortPositionTargets,
        addTarget = addTarget,
        addTargets = addTargets,
        removeTarget = removeTarget,
        getAllTargets = getAllTargets,
        lookupList = lookupList,
        lookup = lookup
    }

    return this
end

 

 

 

 
posted @ 2013-02-21 23:54  风之子_2012  阅读(1765)  评论(0编辑  收藏  举报