rails 應用有各種類型,規模也各有不同。有的是一個獨立的龐大的應用,全部應用都在同一個位置(包括管理界面、api、前端部分以及所有需要的子產品)。另一些應用則是劃分成一系列的微服務,服務之間互相通信,這樣可以把整個應用切分成更易管理的部分。
這種微服務的架構被稱為面向服務的架構( soa )。雖然我見到過的 rails 應用通常都傾向于成為獨立的程式,不過開發者也完全可以選擇讓多個 rails 程式,以及與其他語言或者架構編寫的服務一起工作來完成任務。
獨立的程式不意味着一定寫的不好,但是寫的差的獨立程式被拆成微服務後大多也是很糟糕的。有多種方式可以讓你寫出清晰的(更容易測試的)代碼,同時在需要拆分應用的時候也更輕松。
本文會讨論如何實作一個 cms 的網站。可以假設是一家大的報紙或者部落格,有很多作者負責投稿,使用者可以按主題訂閱内容。
martin fowler 有一篇很不錯的文章,介紹了為什麼編輯和釋出應該分成兩個不同的系統。我們的用例與此類似,另外我們還要添加兩個子產品:通知和訂閱。
我們的 cms 現在有四個主要的子產品:

是選擇獨立程式還是建構成微服務?這裡沒有對和錯之分,不過下面的問題能幫你做出決定。
是否選擇支援 soa 通常與技術無關,而是在于開發團隊的組織結構。
由四個團隊分别負責一個主要的子產品,比所有人在整個系統上一起工作要靠譜一些。如果你隻有一個團隊或者少數幾個開發人員,一開始就決定采用微服務架構實際上會減慢開發的速度,這是因為需要為四個不同的元件直接的通信以及部署增加開發量。
對于本文的例子,有一個問題提現的很好,對外提供服務的公共網站肯定要比作者和編輯使用的 cms 編輯器的通路壓力要大很多。
如果這些子產品都部署成分離的系統,我們就可以單獨的控制它們的規模,為系統中不同的部分采用不同的緩存技術。你當然還是可以堅持采用單一的系統,但是那樣的話你就隻能為整個系統一次性确定其規模,而不是對不同的元件分開處理。
對于 cms 編輯器,你也許想使用 single page application (spa),采用 react 或者 angular 技術。而對外的網站,會使用更傳統一些的服務端渲染的 rails 應用(為了支援 seo)。也許通知子產品更适合 elixir,因為這個語言對并發和并行處理支援不錯。
子產品的分離,使得你可以為每個子產品選擇最适合的程式設計語言。
現在最重要的事情是定義好系統中子產品之間的邊界。
系統中的某個部分可能是某個外部 server 的 client。使用方法調用還是基于 http 都不重要,它隻需要知道它需要與系統中的其他部分進行通信。
為此我們需要定義清晰的邊界。
當一篇文章釋出時,會發生兩件事:
例如,下面的代碼用來釋出一篇文章。文章本身不會關心服務是通過方法調用還是 http 來調用的。
這種方式也可以讓我們友善測試 publisher 類的功能,我們可以使用 testpublisherservice 來做測試,它會傳回預定義的應答。
實際上 publisherservice 的具體實作還沒有完成,但是這不妨礙我們為用戶端(此處是 publisher)編寫測試用例來保證其按預期工作。
服務之間需要能夠互相通信。對此作為 ruby 程式員應該是很熟悉了,即使之前沒有做過微服務的程式。
調用某個對象的方法,隻需要給它發送消息,例如調用 time.send(:now) 就可以改變 time.now。不管是通過方法調用還是基于 http 進行通信,原理是一樣的。我們要做的是給系統的其他部分發送消息,通常還需要有回應。
當你的應用需要一個來自服務端的立即響應才能繼續執行的時候,使用 http 協定來互動将是不二的選擇。
在下面的例子中,publisherservice 類實作了使用 http post 方法來和後端的 faraday 服務子產品進行通訊。
這段代碼簡單來說就是構造了一個需要發送給後端的資料,然後通過 http post 發送到後端,并且處理從後端的傳回的資料。但後端傳回了正确的資料,程式将解釋這個資料,否則程式将抛出一個異常。在後面我們将對這個代碼進行詳細地解釋。
在代碼中,後端服務程式的位址儲存在常量 cms::public_website_url中,這個常量的值是通過初始化代碼設定的。這樣做的好處就是允許我們使用環境變量,根據部署環境的不同(比如開發環境或者生産環境)來給它配置不同的值。
現在讓我們來測試 publisherservice 類,看看它是否正常工作。
在這個測試中,由于我們是在開發環境中做測試,是以并不能保證後端服務一直可用,是以我們将使用 webmock 子產品來模拟到後端的 http 請求,并傳回需要的資料。
在系統使用過程中,有一件事情是絕對不可避免的,那就是對于服務端的調用可能失敗(服務暫時不可用或者網絡通信超市),我們的代碼應該要能夠正确處理這些異常。
當遠端服務不可用的時候,系統應該如何響應完全取決于開發者。在我們的 cms 應用中,當遠端服務不可用的時候,使用者仍然可以建立和編輯文章,隻是不能釋出任何文章。
在上面的測試例子中,代碼包含了對 http status code 500 (服務段出現異常)的處理。當測試代碼收到 500 status code 的時候,代碼将抛出 publisherservice::serviceresponseerror 這個異常。 serviceresponseerror 這個異常類繼承自 error 類,目前這個類并沒有對外提供任何有用的資訊,僅僅表示發生了一個錯誤。下面是這個類的相關代碼。
在 martin fowler 的一篇文章中,提出了另外一種處理服務不可用的方法(在他的文章中,他把這種方法叫做 circuitbreaker 模式)。簡單來說,這個模式的任務就是通過某種方式檢測遠端服務是否運作正常。如果運作不正常,它将阻止對響應遠端服務的調用。
我們也可以通過讓我們的應用感覺遠端服務的狀态并且做出适當的反應來讓我們的應用更強壯。這種系統行為的改變,我們既可以通過類似 circuitbreaker 的模式來自動實作,也可以通過使用者手動關閉系統的某些功能來實作。
在我們的例子中,如果我們可以在現實 publish 按鈕之前檢查一下遠端 publish 服務是否可用,那麼我們就可以直接避免對不可用服務的調用。
http 并非是與其他服務通信的唯一方式。隊列是不同的服務之間傳遞異步消息的很好的選擇。如果對于要做的事情不需要消息接收者立刻回報,那就非常适合這種方式(例如發送郵件)。
我們的 cms 應用中,文章釋出後,訂閱文章的主題的使用者會被通知到(通過郵件,或者網站通知或者推送消息),告知他們有感興趣的文章被釋出。我們的程式并不需要 notifier 服務的回報,隻需要把消息發給它就行了。
之前的一篇文章,我介紹了如何使用activejob,rails 自帶的,用來處理這種背景或者異步處理的任務。
activejob 要求接收代碼也需要運作在 rails 環境,不過它确實是一種很好的選擇,簡單易用。
rabbitmq 是 rails(以及 ruby)之外的另一個選擇,可以作為不同的服務之間的一個通用的消息處理系統。通過 rabbitmq 也可以處理遠端方法調用(rpc),不過更多的是使用 rabbitmq 向其他服務方式異步消息。這裡有很好的 ruby 的使用教程。
下面的類用于向 notifier 服務發送消息,通知有新文章釋出。
代碼可以這樣調用:
微服務并不可怕,不過确實需要仔細的處理。它會帶來很多好處。我的建議是從一個有着清晰邊界的小系統開始,這樣你可以很容易的劃分服務。
更多的服務意味着更多的開發運維工作(你不再隻是部署一個單獨的程式,而是需要部署多個小服務),這時你也許有興趣看一下我寫的如何部署到 docker 容器。