HI END


一种永不妥协,追求极致与完美的精神与态度。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

大型Rails与VoIP系统架构与部署实践(转)

Posted on 2012-09-01 11:28  HI END  阅读(266)  评论(0编辑  收藏  举报

来源:http://www.itivy.com/ivy/archive/2011/12/2/big-cluster-rails-voip-experience.html

复杂的系统最初都是从简单开始的。本篇是我们团队关于Rails系统重构、测试与部署系列文章的最后一篇。在此与大家分享一下我们在系统部署与维护方面的一些经验,希望大家批评指正。

回顾

2008年初,我加入一个Rails团队——Idapted。Idapted是国内最早的Rails团队之一,带头人Jonathan Palley颇有创新和冒险精神,他一个人写了几乎全部早期的系统原型,包括但不限于Rails后台、VoIP客户端和Flex前端等等。

我刚刚加入团队时,面对眼花缭乱的技术简直不知所措。对我来说一切都是全新的。我作为一个系统管理员,职责是管理三台服务器:一台在国内,主要是用作HTTP代理;两台在国外,分别是SVN和Rails应用。当然,当时只一个Rails系统,与数据库都在一台服务器上。

业务简介

在详细介绍之前,我先来简单说明一下公司的业务,以便于读者更好地理解与之相关的技术。我们主要的业务是做一对一的在线英语口语培训。老师在美国,学生主要在中国。学生学习的过程一般分为三个阶段:预习、一对一连线和复习。预习和复习阶段主要是使用Flex呈现课件内容(那时候还没有HTML5),连线阶段也使用Flash,只不过增加了VoIP应用。在美国,老师使用基于SIP的VoIP客户端;而在中国,学生则可以通过电话或手机、Skype、Google Talk等与老师连线实时对话,通话过程全程录音。老师除了在通话过程中实时纠正学生的发音和语法外,还会在在录音的相关位置做上标记和反馈,学生就可以在复习时掌握这些内容。

架构

业务决定架构。最初我们也把Rails部署到国内的服务器上,但这样美国的老师访问起来就很慢。所以后来我们就把Rails移到美国,而国内的服务器就只作HTTP反向代理;另外美国的VoIP环境比国内要好,让它靠近Rails服务是理所当然的。

随着Rails项目的代码越来越多,我们决定将系统拆分成三个部分:Admin、Trainer与Student,分别负责管理员功能、老师平台及学生。VoIP后台也由开源的Asterisk换成了当时比较年轻的FreeSWITCH。同时我们也把系统迁移到了新的美国服务器上:Database + VoIP + Rails。

工具

Rails系统使用的是典型的Nginx + Mongrel + MySQL,部署使用Capistrano。为了更方便的部署系统我基于Capistrano写了一个小工具,可以通过类似cap admin/trainer/student deploy方式部署系统。后来随着系统被拆分的越来越多,我们又发现了一个好的工具Webistrano

为了方便测试,我们在北京的办公室放了两台PC服务器,使用Xen虚拟化技术,虚拟机与生产系统的服务器一一对应,并使用真实数据进行测试,保证测试环境与生产环境的表现完全一样。同时我们也把SVN服务器移到了办公室,这样提交代码就快多了。

迭代

接下来随着业务的发展,我们对Rails系统进行了更进一步的拆分,最终产生了数十个Rails应用,《From 1 to 30: How to Refactor 1 Monolithic Application into 30 Independently Maintainable Applications》便是我们在RailsConf 2010的演讲主题。

在拆分过程中我们也尝试了好多不同的架构和部署方案,有成功的喜悦也有失败的惨痛经历。其中一件事情让我印象深刻:拆分后,各Rails App之间的通信就主要靠共享只读数据库和HTTP Rest Service(大部分使用 Rails 中的ActiveResource 实现),而当时我们也正在尝试基于Phusion Passenger的部署方式。因为我们的系统和Passenger都存在BUG,因此整个经常莫名奇妙地失去反应。最后我们得出的教训就是:当代码和运行环境都不够稳定的时候,定位问题往往需要花费大量的时间,而这是可以避免的。

总体架构

后来,在Idapted被Eleutian Technology收购以后,我们把代码仓库从SVN迁移到了github。同时也把服务器迁移到了一个新的数据中心。最后的架构如下图:

技术架构

下面来详细谈一下我们系统的架构和使用的技术,以下基本都是来自我们的实际经验。至于运行环境,所有Rails都是运行在Ubuntu Linux上,我们只使用LTS版,如8.04 和10.04。Rails2的应用使用Ruby Enterprise Edition 1.7,Rails3则使用Ruby 1.9。

反向代理

最后我们选择了Nginx + Unicorn的方式。Nginx现在几乎已经成为事实上的标准了,而Unicorn更是非常优雅,它不仅稳定高效,而且可以很方便的添加和减少进程。不管起多少个进程,都只占用一个TCP端口,非常方便与Nginx联合部署。

为避免Javascript跨域请求安全性问题,以及使所有的Rails App看起来协调统一,我们将所有App部署在同一个域名下不同的子目录中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
upstream app1 {
    app1.lan:3010;
    app2.lan:3010
}
  
upstream app2 {
    app1.lan:3020;
}
upstream app3 {
    app1.lan:3030;
}
  
location /app1 {
    include /usr/local/nginx/conf/proxy_headers.conf;
    proxy_pass http://app1;
}
location /app2 {
    include /usr/local/nginx/conf/proxy_headers.conf;
    proxy_pass http://app2;
}
location /app3 {
    include /usr/local/nginx/conf/proxy_headers.conf;
    proxy_pass http://app3;
}

 

静态内容与文件服务器

我们使用Squid在国内服务器对普通的静态内容做缓存。另外,我们建立了自己的文件服务器用于存放用户上传文件,大部分是录音(录音也是使用统一的上传文件接口)。文件服务器上的文件会实时备份到Amason S3。

对于文件服务器的缓存,我们使用了Nginx的cache + sendfile功能。如下图,国内服务器N1收到GET请求后(1),使用 http_proxy 请求国外服务器上的 Rails 应用进行鉴权(2)。鉴权通过后Rails返回sendfile HTTP头(3)。N1则在本地缓存中查找对应的文件,如果存在则直接返回文件(4-1),如果不存在,它会再发出一个http_proxy GET请求(4-2)到国外服务器上的 Nginx(N2),N2返回文件到N1,N1将文件发送给用户同时缓存在本地。

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
location /file {
    include /usr/local/nginx/conf/proxy_headers.conf;
    proxy_pass http://file-rails-server:60020;
}
  
location /file-internal {
    internal;
    proxy_set_header   X-Real-IP  $remote_addr;
    proxy_store        on;
    proxy_store_access user:rw  group:rw  all:r;
    proxy_temp_path    /tmp/nginx_temp;
  
    alias              /home/app/shared/file-internal;
    proxy_set_header   X-debug1  $request_filename;
    proxy_set_header   X-Uri $uri;
    proxy_set_header   Host  www.idapted.com;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    if (!-f $request_filename) {
        proxy_pass http://file-nginx-server;
    }
}

 

国外服务器上的情形和这个类似。由于服务器空间有限,录音文件又会占用很大的空间,因此我们会定期删除服务器上长时间未访问的文件,如果偶尔有人访问的话,则通过我们自己编写的一个idp_proxy实时从S3上取回。

VoIP 架构

如前所述,VoIP平台使用的是FreeSWITCH,采用 CentOS 5.5双机冷备份,并在一台服务器上启动多个FreeSWITCH实例,以支持Skype、Google Talk等。详细的内容请参考这篇Blog

Erlang

除Rails外,我们在好多地方也使用了Erlang。Erlang是一种函数式语言,除了我们喜欢它的简洁和优雅,更重要的是,FreeSWITCH有一个原生的Erlang接口,它最初就是用来处理电信业务的,用它控制VoIP再适合不过。

在我们系统中,Erlang开发的程序类似于一种中间件,它位于FreeSWITCH及Rails系统之间,通过原生接口与FreeSWITCH通信,与Rails通信则通过HTTP REST接口及共享数据库。另外,我们的VoIP客户端也使用一个基于TCP的简单协议与Erlang通信。

监控与分析

监控总是最关键的一点。如果你不知道你的服务器在干什么,你就不知道该怎么做。我们使用monit、munin及nagios进行监控,通过短信、Email/IM等方式接收事件通知。

另外,我们也自己开发了一些监控与分析系统,如VoIP行为的关联分析,通过对系统日志的分析及协议的跟踪,来帮助我们分析通话质量问题及断线原因。

其他技术

Flex

所有Flex代码都在服务器上编译,避免由不同开发者不同版本的编译器编译的模块加载时出现问题。

虚拟化

由于Xen的发展不明朗,我们在新的生产环境中使用了LXC及KVM虚拟化技术。两者只是在不同的服务器上进行简单的负荷分担,在虚拟化方面我们没有进行更多地研究。

我们还使用了其它常用及不常用的技术,如memcacheiwatch等, 在这里就不费笔墨一一赘述了。

小结

系统架构是一门很深的学问,部署和维护也同样重要。我最初也没有太多经验,所有经验都是在不断的开发,维护中不断学习和积累起来的。当然,从失败和错误中学习是最快最好的方法。以下是我们总结的几点经验:

谨慎使用最新的技术

敏捷往往与激进联系起来,Rails和FreeSWITCH发展很快,我们总是使用最新的版本,保证我们总是能使用那些最新的特性。当然,前面已经说过,当环境和代码都不稳定时,查找Bug的难度要增大好多倍。所以我们在操作系统及HTTP服务器的选择上又比较保守,这样就保证在敏捷的同时又最大限度的相对稳定。

测试环境尽量与生产环境一致

许多错误在测试阶段没有发现,都是由于测试环境与生产环境不一致引起的。因此,我们会花相当多的时间保证测试环境与生产环境在软件版本上保持一致,甚至,我们还使用虚拟化技术使网络拓扑结构保持一致,比如用虚拟机模拟物理服务器使其在数量上保持一致,并使用真实数据进行测试。

结对操作

我们不仅在开发阶段实施结对编程,在重要的部署维护阶段也是双人结对操作。人都是会犯错误的,我就曾经在单独操作时点错了按钮而几乎犯下大错。因此,我们通过引入双人结对操作的方式,最大限度地降低人为因素在部署维护阶段的影响,对重要操作的定义也更为严格。

从错误中学习,让开发人员也参与进来

我们会把系统故障及处理过程记下来,做成Case供大家学习,并在每周一次的Code Review时间与开发人员交流。每个人都知道系统架构与维护并不完全是架构师和管理员的责任,能够开发出易于部署和维护的系统更是开发人员的责任,好的系统是整个团队紧密合作的结晶。

让管理层理解系统维护的重要性

作为系统维护人员,最郁闷的可能就是系统不出问题。这可不是开玩笑。一般来说,系统不出问题是由于系统维护工作做的好;但管理层也许会误认为管理人员整天没事做。所以,处理好工作与老板的关系是一门艺术,也是团队成功的关键。