原文來自於: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
這個就是使用在線服務的好處,花點錢把性能問題留給他們吧,價格也算合理,:)
目前來講,我相信這套架構還能讓咱們撐很多時間,也能夠很方便的作水平擴展。對於一個技術驅動型團隊,這是咱們的優點,也是對待事情應有的態度。感謝全部風車使用到的開源軟件和在線服務,才能讓咱們這麼"小"的一個團隊,能有更好的時間專一在產品的核心價值上,節省時間去作"大"事。做爲一個團隊協做工具,風車也想幫助大家更好的工做。若是你懂得時間的價值,那麼你應該使用風車來管理你的項目。風車,讓協做更簡單,讓協做更高效。
有想法嗎?如今就試試吧!