月度归档:2013年05月

一种缓存服务器宕机的应对之术

假如有一个认证服务应用,作为基础服务需要尽量保证高可用状态,应用服务器层面已经通过Nginx+keepalived实现主备热切换,但是在内部服务中,由于考虑复杂度等因素,缓存服务器和数据库服务器都是单节点的,也就是说多个认证服务应用(Tomcat)共用一个memcached缓存服务器和数据库服务器。

认证服务器的作用是接收用户请求及附带的key、StringToSign、摘要信息等参数,然后判定此请求是否合法。为了TPS能得到保障,认证服务器会将验证签名用的用户信息缓存在memcached里,避免每次从数据库中读取。

程序的流程可以很简单,分三步走:
1. 从缓存中读取数据。如果读到数据,立即返回。否则,进到第2步。
2. 从数据库中读取数据。
3. 将数据写入缓存。返回。

如果缓存服务器运行正常,这个过程没什么问题。但是一旦单节点的缓存宕掉,而服务的访问量又很高时,上面流程就会出问题了。最坏情况下,三个步骤中有两个步骤要跟缓存打交道。

因为,程序会卡在第一步从缓存读取数据,直到抛出读缓存超时异常,这个时间可短可长,但不管怎样TPS都会急剧恶化。这还没完,程序在第二步从数据库读出数据后,进行了一次写缓存的操作,一样的原因,程序会等到超时后返回。这一来性能还远不如直接从数据库中读取数据。实际上,即使不使用缓存,数据库也能撑住这些查询请求,只是TPS会下降%50左右,不会造成缓存雪崩。

其实经过简单的优化,上述过程就可以变得合理。至少,第二次写缓存操作可以省略。另外,当缓存出问题时,可以认为较短的时间内它都不会恢复。因此,我采用的策略是:

程序中维护一个上次缓存服务器故障时间戳的全局变量,同时指定一个缓存故障恢复期。每次在读写操作缓存前,都判断一下当前时间戳减去上次缓存服务器故障时间戳是否大于缓存故障恢复期,如果小于缓存故障恢复期,则认为当前缓存服务器不可用,程序直接绕过缓存从数据库中读取数据,否则认为缓存已经从故障中恢复,走正常流程。不管缓存服务器多久恢复,程序的消耗仅仅在于每经过一个缓存故障恢复期,都会尝试操作缓存一次。同时,每次捕捉到操作缓存的异常时,都更新一下上次缓存服务器故障时间戳。在我的程序里,设置了缓存故障恢复期为5分钟。基本上,这个处理已经将缓存服务器宕机带来的影响降到最低。

以下是Java代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//上次缓存服务器故障时间戳,默认为0。
public static long LAST_CACHED_EXCEPTION_TIMESTAMP = 0;
//缓存故障恢复时间
public static final long CACHED_EXCEPTION_RETRY_PERIOD = 5 * 60 * 1000;//5分钟
 
try {
    //不在缓存故障恢复期内才操作缓存。
    if (System.currentTimeMillis() - LAST_CACHED_EXCEPTION_TIMESTAMP >=
        CACHED_EXCEPTION_RETRY_PERIOD) {
        // 先尝试从cache中读取信息。
        user = getObjectFromCache(cacheKey);
    }
} catch (Exception e) {
    //更新上次缓存服务器故障时间戳。
    LAST_CACHED_EXCEPTION_TIMESTAMP = System.currentTimeMillis();
    e.printStackTrace();
}
 
if (user != null) {
    // cache中已存在该信息,返回。
    return user;  
} else {
    // cache中不存在该信息,先从数据库读,然后存入cache。
    user = getObjectFromDb();
 
    //不在缓存故障恢复期内才操作缓存。
    if (user != null
      && (System.currentTimeMillis() - LAST_CACHED_EXCEPTION_TIMESTAMP >= 
           CACHED_EXCEPTION_RETRY_PERIOD)) {
        try {
            addToCache(cacheKey, user);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
return credential;
}

--EOF--

git回滚

git-reset命令可以让本地仓库回滚到之前的任意版本。例如:

1
git reset commit-id

如果加--hard参数,会抹掉当前工作区、暂存区乃至仓库中所有在命令中commit-id之后提交的修改。这样,在本地仓库通过git-log命令就看不见被回滚掉的commit了。

假如一份代码需要多人协作,托管在远程服务器(例如GitHub)上,本地仓库回滚后还要push到服务器上,如果直接执行:

1
$ git push origin

服务器会返回:

1
2
3
4
5
6
To https://github.com/fengchj/test.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'https://github.com/fengchj/test.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.

这时候要给git-push命令加-f参数,表示强制push。如:

1
2
3
4
$ git push -f origin
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/fengchj/test.git
 + d1e6f05...087898e master -> master (forced update)

服务器上的HEAD指针也被回滚到之前的某个版本。如果此时客户端git-clone一个仓库,便看不到被回滚掉的commit。

但是假如在强制push之前,有客户端已经克隆了一个仓库,看到了本来应该被回滚掉的commit,此时即使他执行git-fetch或git-pull命令,也无法将本地仓库的HEAD指针与服务器上的同步起来,本地HEAD会领先服务器HEAD好几个commit。这时需要这个客户端执行git-reset命令,同步本地仓库的HEAD指针与服务器HEAD,使他们指向同一个commit。如:

1
2
3
$ git reset origin/master
或
$ git reset --hard origin/master

执行过后git-log命令就看不到被其他客户端回滚掉的commit了。

--EOF--

Erlang进程selective receive消息接收机制

每个Erlang进程有一个信箱(mailbox),可以看成一个消息队列,发送给此进程的消息都存放在信箱中。Erlang进程用receive表达式从信箱中接收消息。receive表达式的语法(不考虑超时)为:

1
2
3
4
5
6
receive
    Pattern1 when Guard1 -> exp11, .. ,exp1n;
    Pattern2 when Guard2 -> exp21, .. ,exp2n;
    ......
    Other -> expn1, .. ,expnn
end

Erlang进程从信箱中接收消息的流程如下:
遇见一个receive表达式,从信箱中取出一条消息(队列中的第一条消息),拿消息跟receive表达式中的第一个子句进行模式匹配,如果匹配成功,则绑定变量,执行与该子句关联的表达式,匹配结束,并将消息从信箱中删除。如果匹配失败,则拿该消息与receive中的第二个子句进行匹配,直到所有模式都匹配完毕。这个过程中如果有一个子句匹配成功,则执行与该子句关联的表达式,匹配结束,并将消息从信箱中删除;如果所有匹配都失败,那么该消息放回到save queue队列,取出信箱中的第二条消息,再依次与receive表达式中的子句进行匹配。如果队列中的所有消息都匹配过了(有些匹配成功的已经从队列中删除,匹配失败的保存在save queue里),此时不管队列中还有没有消息,Erlang进程都会阻塞住,直到队列中有新的消息到来,进程被唤醒,上述流程继续。

此外考虑如下一种一个进程中包含多个receive表达式的情况:

1
2
3
4
5
6
receive
    foo -> true
end,
receive
    bar -> true
end.

如果此时信箱中收到一条bar消息,Erlang进程取出bar消息,拿它与第一个receive表达式的foo子句进行模式匹配,因为匹配失败,所以bar消息放到save queue,进程阻塞住。直到下次来一条foo消息时,Erlang进程拿foo消息与第一个receive表达式中的foo子句匹配,等foo子句匹配成功后,程序才运行到第二个receive表达式处,将save queue中的消息放到信箱首部,从信箱中按序拿出消息,开始与第二个receive表达式进行匹配。这种receive表达式写法能保证此进程先收到foo消息,再收到bar消息。

Erlang进程这种选择性的消息接收策略,称为selective receive,如下图所示:
Selective Receive

References:
[1] Erlang Message Passing
[2] Erlang/OTP并发编程实战, Martin Logan, Eric Merritt, Richard Carlsson 著, 连城 译. 人民邮电出版社.
[3] Erlang编程指南, Francesco Cesarini, Simon Thompson 著, 杨剑 译. 机械工业出版社.
[4] Learn You Some Erlang, Selective Receives
[5] Erlang explained: Selective receive

--EOF--