红帽白皮书新鲜出炉!点击获取,让你的云战略更胜一筹! 了解详情
写点什么

从简单到复杂:大型 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:005991

评论 1 条评论

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

技术分析| WebRTC开源服务器商业化过程中遇到的问题及挑战

anyRTC开发者

开源 音视频 WebRTC 服务器 实时通信

Java 面试八股文之数据库篇(三)

Dobbykim

代码质量管理:SonarQube + Jenkins Pipeline配置

看山

DevOps 10月月更

Vue进阶(幺肆叁):如何用绝对定位(position:absolute)完美定位布局及其注意事项

No Silver Bullet

Vue 绝对定位 10月月更

【LeetCode】加一Java题解

Albert

算法 LeetCode 10月月更

Shopee ClickHouse 冷热数据分离存储架构与实践

Shopee技术团队

数据库 后端 Clickhouse 存储 S3

OpenMLDB: 一文了解窗口倾斜优化技术细节

第四范式开发者社区

机器学习 数据库 开源技术 OpenMLDB

架构营模块一作业

GTiger

架构实战营

持续测试、持续集成、持续交付、持续部署和DevOps

FunTester

持续集成 持续交付 持续测试 FunTester 持续构建

数字货币合约交易APP系统开发介绍(案例)

👊 【Spring技术特性】采用protostuff和kryo高性能序列化框架实现RestTemplate的序列化组件

洛神灬殇

spring 序列化协议 序列化机制 10月月更

数字货币合约交易系统开发内容(源码)

官方线索|#1024小鹏汽车科技日#如约而至!关于未来出行,你有什么想象?

搬砖人

1024我在现场

理解 std::declval 和 decltype

hedzr

算法 元编程 C++11 c++17 纯虚函数

【Flutter 专题】23 图解自定义 Dialog 对话框

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 10月月更

对自己深度学习方向的论文有idea,可是工程实践能力跟不上,实验搞不定怎么办?

Giant

自然语言处理 机器学习 深度学习 算法 论文

ZooKeeper分布式配置——看这篇就够了

牧小农

zookeeper 分布式配置

模块一的命题作业

月影之臣

架构实战营

期货合约系统APP开发简介(搭建)

永续合约软件系统开发源码搭建

模块一

迪马

现成秒合约交易APP系统开发模板

pygame 二次 hello world 项目感知

梦想橡皮擦

10月月更

从零到熟悉,带你掌握Python len() 函数的使用

华为云开发者联盟

Python 数据结构 函数 内置函数 len()

架构3期模块一作业

渐行渐远

架构实战营

有了这个告警系统,DBA提前预警不是难题

华为云开发者联盟

数据库 监控 GaussDB(DWS) 智能监控 告警系统

百度商业托管页系统高可用建设方法和实践

百度Geek说

架构 高可用

数字货币期权交易软件系统开发内容(源码搭建)

人脸识别主板能应用哪些产品设备?

双赞工控

安卓主板 工控主板 主板定制

永续合约APP系统开发简介(搭建)

ARouter 在多 module 项目中实战

逆锋起笔

android arouter 路由框架 阿里arouter

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