写点什么

从简单到复杂:大型 Rails 与 VoIP 系统架构与部署实践

  • 2011-09-19
  • 本文字数:4364 字

    阅读完需:约 14 分钟

复杂的系统最初都是从简单开始的。本篇是我们团队关于 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 部署在同一个域名下不同的子目录中:

复制代码
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 将文件发送给用户同时缓存在本地。

配置如下:

复制代码
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 虚拟化技术。两者只是在不同的服务器上进行简单的负荷分担,在虚拟化方面我们没有进行更多地研究。

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

小结

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

谨慎使用最新的技术

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

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

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

结对操作

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

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

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

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

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

关于作者

杜金房,前 Eleutian Technology(前 Idapted)核心技术架构师,主要负责系统底层架构及 VoIP 系统开发。业余时间创办了 FreeSWITCH-CN 。曾任职烟台电信 / 网通,负责交换机及网管系统维护工作。

高超,Eleutian Technology(前 Idapted)高级网络工程师,负责系统底层架构与监控系统开发与维护。


:本文是 idapted 公司 Rails 系列技术文章的第三篇,前两篇分别为《Rails 系统重构:从单一复杂系统到多个小应用集群》《如何进行高效的 Rails 单元测试》

2011-09-19 00:006149

评论 1 条评论

发布
用户头像
杜老师的这篇分享,这么多年后看都是十分精彩的
2021-02-07 20:26
回复
没有更多了
发现更多内容

秒杀系统架构设计

guangbao

Flutter 浅尝 Flare / Lottie / SVGA 多种动画模式

阿策小和尚

28天写作 内容合集 签约计划第二季 12月日更

git出现ssh: connect to host github.com port 22: Connection refused

ilinux

日常开发的一点实践记录(合集)

为自己带盐

内容合集 签约计划第二季

架构实战营-毕业设计

娜酱

【Redis权威指南】「特性分析」Sentinel的特性分析典籍指南(1)

洛神灬殇

redis哨兵模式 redis哨兵 redis sentinel Redis 核心技术与实战 12月日更

前端开发: Vue封装复用思想的运用(其一)

三掌柜

28天写作 12月日更

给弟弟的信第3封|你幸福吗?

大菠萝

28天写作

架构实战营毕业总结

娜酱

工厂模式

李子捌

28天写作 12月日更

使用ABAP编程实现对微软Office Word文档的操作

汪子熙

数据库 死锁 28天写作 abap 12月日更

电商秒杀系统设计

potti

毕业总结

potti

Android C++系列:Linux线程(一)概念

轻口味

c++ android 28天写作 12月日更

为什么愿意奉献?(4/28)

赵新龙

28天写作

【架构实战营】毕业总结与设计

聆息

架构训练营|大作业

Frode

「架构实战营」

手把手带你玩转 Spring

4ye

Java spring 程序员 内容合集 签约计划第二季

在主流的linux发行版里安装redis

为自己带盐

redis 28天写作 签约计划第二季 12月日更

《网易公开课》也能被拿来练习python爬虫?离谱~

梦想橡皮擦

12月日更

《我和我的家乡》观后感

圣迪

架构实战营 - 毕业设计

Alex.Wu

第1周作业提交

cqyanbo

Go error 的四种处理方式

Rayjun

Go Error

3.《重学JAVA》—Hello World

杨鹏Geek

Java 25 周年 28天写作 12月日更

amazing

Nydia

[Pulsar] Broker 消息分发

Zike Yang

Apache Pulsar 12月日更

使用JDK自带的jmap和jhat监控处于运行状态的Java进程

汪子熙

Java jdk jmap 28天写作 12月日更

Android简介【Android专题1】

坚果

android 28天写作 12月日更

架构实战训练营|毕业总结

Frode

「架构实战营」

4.《重学 JAVA》—基础语法

杨鹏Geek

Java 25 周年 28天写作 12月日更

从简单到复杂:大型Rails与VoIP系统架构与部署实践_Ruby_杜金房_InfoQ精选文章