月度归档:2014年09月

多Region应用系统设计和部署

有一个Web应用系统,需要跨Region部署,每个Region依赖的底层服务各不相同,但是要求是入口统一、单点登录(SSO)。一些较大的业务平台(商务领航)、监控平台、云计算平台(aws)应该都有类似需求。整体结构如下图所示:
multi-regions

用户通过mydomain.com入口进入系统,登陆后可选择进入不同Region,使用不同的后端服务(Service1,Service2...),并且可以随时通过页面选择进入其他Region。除非用户明确切换Region,否则每次请求总是落到相同Region上。

在设计方面,要实现上述系统,需关心以下几点:
1. 单点登陆。在没有可靠的集中式session服务器存在的情况下,将用户认证信息放在Cookie中是个较好的选择,去掉应用系统的状态,方便扩展,应用可以以集群的方式部署在任一Region里。这里意味着,当用户在任一Region登陆成功后,应用系统要向浏览器写认证Cookie,此后,用户无论在Region内部或者Region间跳转都可以不必重新登陆。

2. 底层服务去耦合。应用系统作为业务聚合的地方,需与底层服务尽量松耦合。一般底层服务以RESTful API的形式对外暴露接口,应用系统对其的依赖可以删减至最少,只需将其服务地址(IP+端口号)加入配置项即可。相同Region中的Tomcat集群使用相同配置文件,不同Region使用不同的配置文件。

3. 反向代理标记。架构图中可以看出前端有个Nginx作为反向代理,将请求代理到某个Region下Tomcat集群中的一个节点。Nginx实际上是个透明代理,应用系统必须通过一种方式将用户所选择的Region信息告知Nginx,才能保证每次用户请求落到相同Region上。最简单的做法当然是由应用系统将当前Region的id写入到REGION Cookie里,Nginx根据此Cookie值进行反向代理。也可以选择不侵入系统,应用系统只需给出第一推动力,比如给个参数,此后Nginx会一直将该参数传递下去,实现上述Cookie实现的功能。

在部署方面,主要涉及应用系统部署和Nginx部署:
1. 应用系统。应用系统只要做到了无状态,就可以很简单的以Tomcat集群方式对外提供服务。注意每个集群下的节点配置文件不同,主要是RegionId, 底层服务地址等有所区别。

2. Nginx。各个Tomcat集群中的节点负载均衡交给Nginx upstream模块实现,一个集群对应一段upstream配置,均衡策略根据需要选择。例如:

1
2
3
4
5
6
7
8
9
10
upstream region1 {
  server 10.10.1.1;
  server 10.10.1.2;
  server 10.10.1.3;
}
 
upstream region2 {
  server 10.10.2.1;
  server 10.10.2.2;
}

多Region的配置方面,要实现Region间切换,Nginx需支持根据regionid参数反向代理,要实现Region内停留,需支持根据Cookie值反向代理。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#省略部分配置
location / {
  if ($args ~ regionid=1){
    proxy_pass http://region1;
    break;
  }
  if ($args ~ regionid=2){
    proxy_pass http://region2;
    break;
  }
  if ($http_cookie ~* "REGION=1" ){
    proxy_pass http://region1;
    break;
  }
  if ($http_cookie ~* "REGION=2" ){
    proxy_pass http://region2;
    break;
  }
  proxy_pass http://region1;
 
  #省略部分配置
}

上述配置的含义是,当url中携带显式的regionid参数时,根据参数值代理到相应Region,该参数只有用户选择切换Region时才会携带。当url中不带regionid参数,则根据REGION Cookie的值进行反向代理。否则,Nginx默认将请求代理到默认Region上。

--EOF--

『幻夜』

huanye作为『白夜行』的姊妹篇,两者叙事主线,节奏把握极其类似,因为有了『白夜行』的先入为主,所以读『幻夜』少了点酣畅淋漓的阅读体验。不过仍值得约等于5星的评价。

作为一本独立存在的推理小说,东野圭吾在架构故事的时候似乎做到了面面俱到,故事的真相也在一次次的伏笔中得以揭开,然而,有一个问题是我掩卷之前一直期待却未得到解答的,就是新海美冬策划这一系列的原始动机。我知道她的目的是通过一次又一次的构陷以巧取豪夺的方式得到自己想要的东西,名誉,地位,金钱等等,抛开故弄玄虚为了吸引读者这个原因不提,因为我觉得东野圭吾不会这么low,新海美冬改变身份这一设定是否过于累赘和多此一举呢?假设,加藤亘警官和水原雅也的调查属实,真正的新海美冬在地震中死去,之后该身份一直以来都是由white night服装店老板娘假扮,那么如上所说,老板娘假扮新海美冬缺少动机。新海美冬是个很普通的人,普通家庭普通长相,假扮她除了得到一个身份外得不到其他更多地东西了,凭借老板娘的魅力和手段,有没有这个身份根本无关紧要。

如果新海美冬的话属实,从头到尾美冬一直是美冬,只是因为崇拜老板娘而有意向她学习,包括整容换成她的容颜,从心理学上说有根有据,但是解释不通新海美冬为何要杀害曾我孝道。即便她和曾我孝道见了面,曾我孝道疑惑现在新海美冬和照片中长得不一样,这要解释完全不成问题,毕竟女大十八变,跟何况还整过容。所以从这点来看,新海美冬对加藤亘说的话为假。

不像『白夜行』结局,东野圭吾给了一个充分的理由揭开唐泽雪穗和桐原亮司不择手段策划杀人的原因,『幻夜』从伊始至结尾,新海美冬就不停地在干损人利己的事情,一副蛇蝎美人的人物形象。有人理解为『幻夜』是『白夜行』的续集,因为从时间上衔接得起来,唐泽雪穗开了一家服装店,经营不善,后来趁着阪神大地震,换了一个新海美冬的身份以原有的那套寄居蟹手段东山再起,这倒是能在一定程度上解释了之前提到的新海美冬的原始动机,但是无法解释为什么唐泽雪穗能潦倒得这么彻底,她在『白夜行』中积累的人脉和资源,包括筱冢家族,不可能在短短几年时间里全都消失不见。

所以以我之见,『白夜行』和『幻夜』还是得当成两个独立的故事来看待,只是恰好两个女主角的城府和手段类似而已。也就是说,我更倾向于将『幻夜』中新海美冬身份被冒充缺少原始动机当成本书的唯一缺陷。

另外,水原雅也差桐原亮司好几条街,最后他杀美冬不成,反而和加藤亘同归于尽了,这个逻辑略奇葩。他真的应该和冈田有子待一块的,如果说新海美冬夺去了他的灵魂,那么冈田有子能够将自己的灵魂分给他一半。

--EOF--

一次Java内存泄露问题排查

近期随着管理平台用户增加,后台多次出现OutOfMemoryError错误,尝试着把堆内存从2G加到4G(-Xms4096m -Xmx4096m)也不顶用,隔段时间就OOM,看来是出现内存泄露了。于是在Tomcat启动脚本中加入JVM参数-XX:+HeapDumpOnOutOfMemoryError,使其OOM时把堆内存dump成文件,便于分析。今天,程序又一次OOM,我拿到了这个4G大的java_pid32693.hprof文件。打开 Eclipse Memory Analyzer(MAT),导入dump文件,内存泄露问题显而易见:
 Eclipse Memory Analyzer

根据提示,有一个ConcurrentHashMap数据结构占用了所有4G内存中的99.11%,该Map名为asyncContentMap,后面是它的package路径。

回忆了一下,这个貌似是很早以前实现过的一个实例状态自动刷新功能,管理平台有一个实例列表页面,当实例状态变化时,无需用户刷新页面,状态栏自动变更。为了前端通用,放弃Websocket、socket.io,用了最简单的ajax实现,浏览器端和管理平台间用long polling实现状态推送。下图就是整个状态自动刷新功能的示意图,管理平台接到请求后,使用Servlet 3.0的异步Servlet挂住浏览器端连接,并将请求上下文(AsyncContext)放入队列。另一方面,通过worker线程池处理请求队列里的请求,worker线程做的事情就是依次轮询后端服务(Service1,Service2...Service N)接口,判断其实例状态有无变化,如果状态变化,则取出前端请求上下文,返回响应。用户就能从浏览器得知实例状态变更,之后,再发起一个HTTP long polling请求,重复上述流程。当然,long polling有个超时时间,由管理平台控制,当对后端服务连续polling了一定时间都没有状态变化时,也要将响应返回给浏览器,使之进入下一个long polling周期。
Long Polling

要实现上述描述的功能,管理平台这边需维护两个状态,分别对应两个线程安全的数据结构。一个是请求队列LinkedBlockingQueue asyncContextList,worker线程池会从请求队列中取出浏览器请求;另一个是映射表ConcurrentHashMap asyncContentMap,用于存放异步请求和改请求附带的一些元信息,比如上一次polling后端Service的时间戳、响应值等等。根据MAT的提示asyncContentMap出现内存泄露,那么可以肯定是有些异步请求已经完成响应,但是忘了从映射表里把记录清除。检查了一下程序,的确如此,加入remove操作后问题解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
AsyncContext asyncContext = Container.asyncContextList.take();
AsyncContentDeliver asyncDeliver = Container.asyncContentMap.get(asyncContext);
 
//略去其余部分
 
if (respEntry.getResponseBody().equals(lastResponseBody)) {
  // 与上一次polling相比,实例状态未发生改变,异步请求重新入队。
  Container.asyncContextList.put(asyncContext);
  continue;
} else {// 实例状态发生了改变,进行响应。
  try {
    asyncContext.getResponse().setCharacterEncoding("UTF-8");
    asyncContext.getResponse().getWriter().print(respEntry.getResponseBody());
    asyncContext.complete();
    Container.asyncContentMap.remove(asyncContext); // BugFix!
  } catch (IOException e) {
    e.printStackTrace();
    Container.asyncContextList.put(asyncContext);
  }
}

总结这次Bug修复,深深感受到一点:编程语言始终解决不了业务逻辑层面上的内存泄露问题,资源的分配和释放程序员必须谨记在心。

--EOF--