原文来自于:http://www.infoq.com/cn/articles/fengche-co-architecturehtml
风车这个项目开始于 2011 年 11 月份,以前叫作 Pragmatic.ly。从第一天开始咱们基本上就定了大体的框架结构,在今天回头看,基本上整个架构都没有什么变化,能够算是个很成熟和很适合时代的方案,☺。前端
最近一两年,做为技术人员,咱们都能很明显的感受到前端技术的飞速发展,好比 HTML5 支持,移动端优先、响应式界面设计以及层出不穷的各类客户端框架。而全部这些,都是基于一点:浏览器的高速发展。Chrome、Firefox、Safari、Opera 甚至于 IE,最近几年发展的都很快,不夸张的说,这些浏览器已经再也不是浏览器,而是成为开放平台,有各自的扩展插件机制。这些极大地改变了网站开发的方式,网站开始应用化。html5
风车便是如此,设计得很是接近桌面应用,好比下面这些特色:node
而在这个设计的背后,就是其自己的技术栈。git
风车在客户端使用的是 Spine.JS,后端使用的是 Ruby on Rails。实时消息同步用的是 Pusher。(三个里面有两个由于莫名其妙的缘由打不开... -.-)程序员
Spine.js 是一个轻量级的 MVC JavaScript 库,由《Javascript Web Applications》的做者 Alex MacCaw 基于 Backbone.js 改良。Spine 库使用 CoffeeScript 编写,总体代码量仅一千行左右,比起 Angular.js, Ember.js 这些框架来讲少的多,很是容易学习和上手。github
Rails 目前咱们在使用的仍是 3.2 版本,基本上是用来作 API 服务器,只管数据,不作逻辑。上周活动有些朋友也问到基本只作 API 服务器,为啥不选用更轻量的方案如 Sinatra,Grape 之类。一是从开发上来讲,Rails 默认这一套用起来比较舒服,咱们除了 API 以外还有一些第三方应用的集成和管理性功能,因此总体建站更方便。二是咱们目前还没遇到大的性能上问题,因此不必去更换。若是下一阶段真有须要了,会把 RESTful API 专门独立出来。web
Pusher 是一个基于 WebSocket 的实时消息推送服务,集成到应用中也很是方便。即便在不支持 WebSocket 的浏览器里(对,没错,说的就是 IE),也提供默认的备用方式,可选择 Flash Socket 或者 SockJS。总体体验来讲,Pusher 算是一个很不错的解决方法,轻、快,给咱们节省了大量开发时间,只须要关注产品的核心价值。不过若是你的应用对于实时性要求很是严格,好比交易系统,可能 Pusher 的稳定性还不够符合你要求,由于你懂的一些网络缘由。数据库
当浏览器刷新页面的时候,会向服务端发起一个请求。服务端收到这个请求后,会返回一个不带数据的纯 HTML 空模板。而后客户端渲染模板后,再次经过 RESTful API 向服务端请求项目的真实数据(JSON 格式),再由客户端对数据作处理并呈现,获得用户真正看到的页面。以后,会跟 Pusher 服务器创建一条 WebSocket 的长链接,接收推送信息。当服务端有任何更新的时候,会发送消息到 Pusher 服务器,再由 Pusher 服务器传输到客户端浏览器,页面同时也获得更新。以上,就是一个简单的过程。后端
上面也介绍过了,风车的前端用的是 Spine.JS 和 jQuery。在移动端稍微有点不一样,是 Spine.JS Mobile 和 Zepto。在目前这个时候,我计算了一下,压缩后的 JS 和 CSS,包括全部第三方的库,已经将近 270 KB。这里感谢一下七牛云存储,风车的这些静态文件因他们的 CDN 服务,能很快的下载到用户端,加速页面的加载。
Spine.JS
Spine 上面已经介绍了很多,这里再介绍一个我很喜欢的特性:_Asynchronous interfaces_。当咱们决定把逻辑从服务端移到客户端的时候,就是要提升用户的整个使用体验,要能很是迅速的对用户行为作出响应。因此,当用户作了一个操做更新数据的时候,不要再显示个 loading spinner,让用户去等待数据更新完毕,而是应该马上给出页面的变化。这就是异步 UI,经过解耦客户端 UI 交互跟服务端数据同步,保证了交互的流畅性。
CoffeeScript + Eco
这里真的很佩服 DHH,当时力排众难执拗地在 Rails 3 里面默认加入了 CoffeeScript,让 CoffeeScript 迅速地流行起来。Coffee 是那种一用就能上瘾的东西,咱们几乎全部的 JS 代码都是用 Coffee 写的,最后编译出来的纯 JS 代码也很具备可读性。即便是让人诟病的调试复杂度,对于熟悉代码结构和 Coffee 的人来讲也历来不是问题,更不用说如今还提供了 Source Maps 的支持。Eco 是 "Embedded CoffeeScript templates",语法跟 ERB 很像,做为一个 Ruby 开发人员无法不喜欢,:)
Model-View-Controller
从 MVC 框架来讲,Controller 层,主要负责接受请求并处理请求,对应到客户端,请求就是事件,因此 Controller 负责对 DOM 事件的处理和 Router 事件的处理。基于此,风车前端有两种类型的 Controller,一种是跟页面 DOM 打交道的,一种是跟路由打交道的。基于 DOM 的 Controller 是按照页面的结构设计,每一个 Controller 对应于一个单独的 DOM 模块,好比风车里面的侧边栏对应一个 Controller,侧边栏里面的任务列表部分和团队成员部分又分别对应一个 Controller,等等。这些 DOM Controller 会监视 DOM 上事件的发生,以及 DOM 对应的数据的更新。而基于 Router 的 Controller 是按照 URL 来设计,用来监视页面 URL 的变化,好比每一项任务都对应于一个单独的 URL,那么点击行为会致使 URL 的变化,这个变化会被 Router Controller 捕捉到,执行相应的操做,整个过程跟 Dom 没有任何关系。
Model 层,负责全部跟数据有关的处理。绝大多数时候,数据是 URL 对应,因此 Model 层绝大多数时间只须要跟 Router Controller 交互。Router Controller 从 Model 准备好数据后,会触发一个事件,交由 DOM Controller 去渲染相应的页面。
除了 MVC 三层以外,风车在设计上还使用了不少 HTML5 的特性,除了以前介绍的 WebSocket 以外,还有 History、Web Notification、Drag & Drop 和 LocalStorage。
HTML5 History pushState
History 是 Router Controller 的实现基础。咱们都已经很习惯了浏览器页面的前进后退来访问历史页面。而不少富客户端应用由于 URL 没有发生改变,就很难支持这点。HTML5 History 就是为了解决这个问题,而在 JavaScript 端提供的一个实现。当咱们访问下一个页面的时候,会 push 这个 URL 到栈里,当咱们按后退时,会 pop 出这个 URL。Router Controller 须要定义一系列要响应的 URL,这样一旦匹配,就会截获页面跳转,转而去执行相应的代码,渲染出对应数据。因此在风车里,每次页面的修改都是对应一个惟一的 URL,这样除了能够前进后退外,还有个额外的好处是刷新页面后,老是能够回到刷新前的 URL。固然,须要在客户端和服务端用同一套路由。
HTML5 桌面通知
Web Notification 是桌面端的通知,当一个跟用户相关的事件发生时,好比团队里有人分配了一个任务给你,或有人在讨论里 @ 了你,就会收到一个通知。目前 Chrome 和 Safari 已经直接支持 Web Notification,Firefox 最新版已经支持,老版本须要安装一个插件支持,而 IE 在 10 以上才支持,并且必须加入 pinned site 列表里。具体的能够参考我以前在风车官方博客里面写的这篇详细介绍 HTML5 Web Notification 的文章。
HTML5 拖放
Drag & Drop 以前已经有不少 JS 的实现了,好比 jQuery UI 里面就有 DnD 的支持,HTML5 在规范制定的时候,也把 DnD 加入了进来,作了标准化。风车里面是用的 HTML5 的规范,主要用于把任务拖到某个任务列表里,某个成员里和拖到同个列表不一样的位置。
有限离线支持
上面也介绍了,Spine 里面有个特性是异步 UI,可是若是咱们跟服务器之间的链接出现短暂的问题,好比网络断了,那么就会在客户端更新了,可是服务端却没有同步到,这样用户一刷新,就会发现丢数据了。咱们针对这个状况,通常发现同步失败,就先把未同步的数据放到 LocalStorage 里面,每隔一段时间重试一下,直到同步成功。因此,在页面加载后,即便在离线状况下,短暂时间使用风车也不是问题。可是,由于咱们目前实现的颇有限,也没作版本控制系统,因此在极端状况下,好比团队里不一样人对同一个任务作出了更新,后同步的数据会覆盖掉先同步的。由于这种状况不多发生,因此目前咱们没花大时间去改进它。
风车的整个服务端是基于 Ruby on Rails,前面也已经给了一些介绍。我在以前的重构 Rails 项目之最佳实践介绍了一些能够用来写出更好的代码结构的方法。在风车中,咱们也在标准的 MVC 三层以外,加了 Service 层和 Presenter 层。
Service Layer
为了保持 Controller 结构的尽量简单,对于一些复杂的又不只属于某个 Model 的请求处理逻辑,咱们在 Controller 和 Model 之间引入了新的一层:Service 层。举个例子,风车目前集成了 GitHub、GitLab 和 BitBucket 的 hook。当用户向远端推送提交的时候,GitHub/BitBucket 会向风车的服务器发起一个请求,包含这些提交的信息。在风车这边收到这些请求的时候,首先须要去作特征判断,查明是来自于哪个服务。而后分析推判断送消息,是否绑定到具体的任务。最后根据消息,判断是否要更新状态和建立讨论。整个处理的逻辑比较复杂又相对独立,涉及多个 Model 而且有多种不一样的策略,很是适合抽象成一个 Service。另外的一些场景好比 Analytics Service,Password Service,Email Handler Service 等。
Presenter Layer
熟悉 Rails 开发的朋友通常都知道,View 层应该仅仅是用来显示数据,咱们应该避免在 View 里面有逻辑。可是,不少时候会不可避免的会有一些很难维护的 View。去年 RubyConf China 2013,来自台湾的讲师 xdite 就介绍了如何写出可维护的 View,详见这里。不过对于风车而言,大量的 View 是在客户端,服务端不多,只是提供了一些数据准备,因此没用多少技巧。只是由于数据准备涉及到了多个 Model 的数据以及来自于 Redis 数据库里面的数据,因此咱们独立出了 Presenter Object,用来管理这些逻辑,而不是放在 View 里面。
Observer
Observer 通常而言是监控数据的,当数据建立、更新或者删除时,能够执行一些相应的操做,好比用户注册后能够发送注册邮件。可是风车里面的 Observer 有些许不一样,除了监控数据之外,咱们还监控 Controller,来了解数据变化相对应的请求信息,好比操做用户是谁。这里主要借鉴了 Rails 自带的 Caching Sweeper,后面咱们会专门写一篇文章来介绍。
Sidekiq
Sidekiq 是一个简单强大的消息队列系统,目前能够说是 Ruby 世界里后台处理的首选。同类的选择还有 resque、delayed_job 等,可是 Sidekiq 之因此能迅速成为首选是基于两个特色,一是基于 Actor 模式的并行处理机制,二是基于 Redis 的 pubsub 模型,因此能用更少的内存资源来得到同样的处理能力。在风车里只要是能延迟的操做咱们就会全放到后台执行,好比发送通知、数据统计、建立初始数据等等。这样子,咱们就能让每一个请求在最短的时间内完成,提升整个系统的吞吐量。Sidekiq 在收到消息后会在后台处理,即便失败了也会重试,更加可靠。
Percona
风车的主数据库仍是使用了关系数据库 Percona,是基于 MySQL 的一个分支,可是由于使用了 Percona 公司本身研发的 XtraDB 存储引擎,具备更好的性能。另一点好处是 Percona 号称是最接近官方 MySQL Enterprise 发行版的版本,能够彻底与 MySQL 兼容,我能够很方便的作切换而不用修改代码。
Redis
Redis 在风车里面主要是两个用途:1. 用做 Caching Store,存储 View Cache 和 Record Cache 2. 加速数据访问,在内存中存储一些会频繁读写的数据,减小对 Percona 数据库的访问,好比 UID 映射表、统计信息之类。使用 Redis 而不是 Memcached 的缘由是首先 Redis 的数据是持久的,不会由于重启而丢失,由于咱们有一些没法马上重建的数据,好比用户的在线状态,第二点是 Redis 能够用来存储一些复杂数据结构,好比 List 和 Set,对于统计信息来讲很是合适。今年 Redis 3.0 有望正式发布,到时有 Redis Cluster 的支持,能够期待带来更好的性能。
以上可是风车总体的技术架构,不复杂,可是很是实用。目前咱们只使用了 Linode 上的单台机器,由于业务逻辑主要在前端,后端以 API 为主,因此性能问题并不突出。可是仍是得认可健壮性有所缺少,一旦后台某点发生故障,好比数据库或者 Redis,都会影响到服务的正常运行。下面来谈谈后台架构的一些可扩展性,来源于我以前的工做,但并无上风车实践。
Rails
Rails 一直被人诟病的是其的性能,最近也常常能看到很多相似 XX 应用从 Rails 迁移到 Node.js 后得到 YY 倍的性能之类的报道。上期 Teahour 跟朴灵也聊到,Node.js 是从框架上用事件驱动和非阻塞来保证高性能,减小程序员犯错。一样的效果,Ruby 也能作到,可是对程序员自己的要求会更高。几种可能的扩展方式。
Percona
目前在数据库方便的优化很少,咱们也只是在优化索引和尽可能避免连表查询。不过风车这个应用性质决定了不会是大数据,尤为是咱们有些数据还不经过 Percona 存储。惭愧的是目前风车只是作到了数据的备份,可是尚未作 Cluster、主从、读写分离这些,不过都是会在遇到瓶颈时去尝试的。跳出关系型数据库,也许能够尝试一些文档数据库如 Mongo,应该也蛮合适的。
Redis
Redis 对于风车最大的做用是用来存储分析数据,节省掉数据库的访问和运算。由于 Redis 在 2.X 版本的时候没有 Cluster 实现,目前要作扩展只能在上层经过一致性哈希来得到有限的支持。3.0 目前已经发布 beta 版,内建的 Cluster 功能还在测试中,值得期待。另外对于 Redis 的主从复制,针对不一样的应用场景会有不一样的问题,好比 Redis 持久化策略、主从间同步策略等。在百万级别以上的数据上,Redis 就必需要调优了,同时,每次重启的时候也很痛苦,重建库要花很多时间。因此,有可能的话,对数据进行分片,尽可能只让新鲜数据或者经常使用数据留在内存里,陈旧数据能够存储到磁盘上。
Sidekiq
Sidekiq 自己是基于线程的单进程运行模式,使用 Redis 作为消息队列。因此,Sidekiq 的并行能力很容易提高,只要多起几个进程就能够了,前提是数据库和 Redis 都扛得住,固然还有内存这些硬件资源。
Pusher
这个就是使用在线服务的好处,花点钱把性能问题留给他们吧,价格也算合理,:)
目前来讲,我相信这套架构还能让咱们撑很多时间,也能够很方便的作水平扩展。对于一个技术驱动型团队,这是咱们的优点,也是对待事情应有的态度。感谢全部风车使用到的开源软件和在线服务,才能让咱们这么"小"的一个团队,能有更好的时间专一在产品的核心价值上,节省时间去作"大"事。做为一个团队协做工具,风车也想帮助大家更好的工做。若是你懂得时间的价值,那么你应该使用风车来管理你的项目。风车,让协做更简单,让协做更高效。
有想法吗?如今就试试吧!