Rails 微服務架構
Rails 應用有各種類型,規模也各有不同。有的是一個獨立的龐大的應用,全部應用都在同一個位置(包括管理界面、API、前端部分以及所有需要的模塊)。另一些應用則是劃分成一系列的微服務,服務之間互相通信,這樣可以把整個應用切分成更易管理的部分。
這種微服務的架構被稱為面向服務的架構( SOA )。雖然我見到過的 Rails 應用通常都傾向于成為獨立的程序,不過開發者也完全可以選擇讓多個 Rails 程序,以及與其他語言或者框架編寫的服務一起工作來完成任務。
Rails應用通常是獨立的程序,但是沒有理由阻止你嘗試微服務的架構。
</blockquote>獨立的程序不意味著一定寫的不好,但是寫的差的獨立程序被拆成微服務后大多也是很糟糕的。有多種方式可以讓你寫出清晰的(更容易測試的)代碼,同時在需要拆分應用的時候也更輕松。
使用微服務架構的 Rails 應用的用例
本文會討論如何實現一個 CMS 的網站。可以假設是一家大的報紙或者博客,有很多作者負責投稿,用戶可以按主題訂閱內容。
Martin Fowler 有一篇很不錯的文章,介紹了為什么編輯和發布應該分成兩個不同的系統。我們的用例與此類似,另外我們還要添加兩個模塊:通知和訂閱。
我們的 CMS 現在有四個主要的模塊:
CMS 編輯器:作者和編輯用來創建、編輯和發布文章。
</li>公共的網站:對外提供服務,瀏覽已發布的文章。
</li>通知: 通知訂閱者有新發布的文章。
</li>訂閱: 管理用戶賬號和訂閱。
</li> </ul>
![]()
Rails 應用需要支持 SOA 嗎?
是選擇獨立程序還是構建成微服務?這里沒有對和錯之分,不過下面的問題能幫你做出決定。
</blockquote>團隊的組織結構是怎樣的?
是否選擇支持 SOA 通常與技術無關,而是在于開發團隊的組織結構。
由四個團隊分別負責一個主要的模塊,比所有人在整個系統上一起工作要靠譜一些。如果你只有一個團隊或者少數幾個開發人員,一開始就決定采用微服務架構實際上會減慢開發的速度,這是因為需要為四個不同的組件直接的通信以及部署增加開發量。
不同的模塊規模不一樣?
對于本文的例子,有一個問題提現的很好,對外提供服務的公共網站肯定要比作者和編輯使用的 CMS 編輯器的訪問壓力要大很多。
如果這些模塊都部署成分離的系統,我們就可以單獨的控制它們的規模,為系統中不同的部分采用不同的緩存技術。你當然還是可以堅持采用單一的系統,但是那樣的話你就只能為整個系統一次性確定其規模,而不是對不同的組件分開處理。
不同的模塊使用不同的技術?
對于 CMS 編輯器,你也許想使用 Single Page Application (SPA),采用 React 或者 Angular 技術。而對外的網站,會使用更傳統一些的服務端渲染的 Rails 應用(為了支持 SEO)。也許通知模塊更適合 Elixir,因為這個語言對并發和并行處理支持不錯。
模塊的分離,使得你可以為每個模塊選擇最適合的編程語言。
邊界定義
現在最重要的事情是定義好系統中模塊之間的邊界。
系統中的某個部分可能是某個外部 Server 的 Client。使用方法調用還是基于 HTTP 都不重要,它只需要知道它需要與系統中的其他部分進行通信。
為此我們需要定義清晰的邊界。
當一篇文章發布時,會發生兩件事:
首先會把文章的發布版本發送給對外的網站,它會返回一個發布后的 URL。
</li>然后我們把剛創建的公開的 URL、話題、標題發送到通知模塊,后者會通知到所有對話題感興趣的訂閱者。這一步可以是異步的,因為通常會耗費一些時間來通知到每一個用戶,并且這個通知是不會有反饋的。
</li> </ol>例如,下面的代碼用來發布一篇文章。文章本身不會關心服務是通過方法調用還是 HTTP 來調用的。
class Publisher attr_reader :article, :servicedef initialize(article, service) @article = article @service = service end
def publish mark_as_published call_service article end
private
def call_service service.new( author: article.author, title: article.title, slug: article.slug, category: article.category, body: article.body ).call end
def mark_as_published(published_url) article.published_at = Time.zone.now article.published_url = published_url end end</pre>
這種方式也可以讓我們方便測試 Publisher 類的功能,我們可以使用 TestPublisherService 來做測試,它會返回預定義的應答。
require "rails_helper"RSpec.describe Publisher, type: :model do
let(:article) { OpenStruct.new({ author: 'Carlos Valderrama', title: 'My Hair Secrets', slug: 'my-hair-secrets', category: 'Soccer', body: "# My Hair Secrets\nHow hair was the secret to my soccer success." }) }
class TestPublisherService < PublisherService def call "; end end
describe 'publishes an article to public website' do subject { Publisher.new(article, TestPublisherService) }
it 'sets published url' do published_article = subject.publish expect(published_article.published_url).to eq('實際上 PublisherService 的具體實現還沒有完成,但是這不妨礙我們為客戶端(此處是 Publisher)編寫測試用例來保證其按預期工作。
class PublisherService attr_reader :author, :title, :slug, :category, :bodydef initialize(author:, title:, slug:, category:, body:) @author = author @title = title @slug = slug @category = category @body = body end
def call # coming soon end end</pre>
服務間通信
服務之間需要能夠互相通信。對此作為 Ruby 程序員應該是很熟悉了,即使之前沒有做過微服務的程序。
調用某個對象的方法,只需要給它發送消息,例如調用 Time.send(:now) 就可以改變 Time.now。不管是通過方法調用還是基于 HTTP 進行通信,原理是一樣的。我們要做的是給系統的其他部分發送消息,通常還需要有回應。
使用 HTTP 協議和微服務通訊
當你的應用需要一個來自服務端的立即響應才能繼續執行的時候,使用 HTTP 協議來交互將是不二的選擇。
當你需要一個立即響應的時候,HTTP 協議通訊將是不二的選擇。
</blockquote>在下面的例子中,PublisherService 類實現了使用 HTTP Post 方法來和后端的 Faraday 服務模塊進行通訊。
class PublisherService < HttpService attr_reader :author, :title, :slug, :category, :bodydef initialize(author:, title:, slug:, category:, body:) @author = author @title = title @slug = slug @category = category @body = body end
def call post["published_url"] end
private
def conn Faraday.new(url: Cms::PUBLIC_WEBSITE_URL) end
def post resp = conn.post '/articles/publish', payload
if resp.success? JSON.parse resp.body else raise ServiceResponseError end end
def payload {author: author, title: title, slug: slug, category: category, body: body} end end</pre>
這段代碼簡單來說就是構造了一個需要發送給后端的數據,然后通過 HTTP Post 發送到后端,并且處理從后端的返回的數據。但后端返回了正確的數據,程序將解釋這個數據,否則程序將拋出一個異常。在后面我們將對這個代碼進行詳細地解釋。
在代碼中,后端服務程序的地址保存在常量 Cms::PUBLIC_WEBSITE_URL中,這個常量的值是通過初始化代碼設置的。這樣做的好處就是允許我們使用環境變量,根據部署環境的不同(比如開發環境或者生產環境)來給它配置不同的值。
Cms::PUBLIC_WEBSITE_URL = ENV['PUBLIC_WEBSITE_URL'] || 'http://localhost:3000'測試我們的服務
現在讓我們來測試 PublisherService 類,看看它是否正常工作。
在這個測試中,由于我們是在開發環境中做測試,所以并不能保證后端服務一直可用,因此我們將使用 WebMock 模塊來模擬到后端的 HTTP 請求,并返回需要的數據。
RSpec.describe PublisherService, type: :model dolet(:article) { OpenStruct.new({ author: 'Carlos Valderrama', title: 'My Hair Secrets', slug: 'my-hair-secrets', category: 'Soccer', body: "# My Hair Secrets\nHow hair was the secret to my soccer success." }) }
describe 'call the publisher service' do subject { PublisherService.new( author: article.author, title: article.title, slug: article.slug, category: article.category, body: article.body ) }
let(:post_url) { "#{Cms::PUBLIC_WEBSITE_URL}/articles/publish" }
let(:payload) { {published_url: ' }
it 'parses response for published url' do stub_request(:post, post_url).to_return(body: payload) expect(subject.call).to eq('
it 'raises exception on failure' do stub_request(:post, post_url).to_return(status: 500) expect{subject.call}.to raise_error(PublisherService::ServiceResponseError) end end
end</pre>
處理調用失敗
在系統使用過程中,有一件事情是絕對不可避免的,那就是對于服務端的調用可能失敗(服務暫時不可用或者網絡通信超市),我們的代碼應該要能夠正確處理這些異常。
當遠端服務不可用的時候,系統應該如何響應完全取決于開發者。在我們的 CMS 應用中,當遠端服務不可用的時候,用戶仍然可以創建和編輯文章,只是不能發布任何文章。
在上面的測試例子中,代碼包含了對 HTTP Status Code 500 (服務段出現異常)的處理。當測試代碼收到 500 Status Code 的時候,代碼將拋出 PublisherService::ServiceResponseError 這個異常。 ServiceResponseError 這個異常類繼承自 Error 類,目前這個類并沒有對外提供任何有用的信息,僅僅表示發生了一個錯誤。下面是這個類的相關代碼。
class HttpServiceclass Error < RuntimeError end
class ServiceResponseError < Error end
end</pre>
在 Martin Fowler 的一篇文章中,提出了另外一種處理服務不可用的方法(在他的文章中,他把這種方法叫做 CircuitBreaker 模式)。簡單來說,這個模式的任務就是通過某種方式檢測遠端服務是否運作正常。如果運作不正常,它將阻止對響應遠端服務的調用。
我們也可以通過讓我們的應用感知遠端服務的狀態并且做出適當的反應來讓我們的應用更強壯。這種系統行為的改變,我們既可以通過類似 CircuitBreaker 的模式來自動實現,也可以通過用戶手動關閉系統的某些功能來實現。
在我們的例子中,如果我們可以在現實 Publish 按鈕之前檢查一下遠端 Publish 服務是否可用,那么我們就可以直接避免對不可用服務的調用。
使用隊列進行通信
HTTP 并非是與其他服務通信的唯一方式。隊列是不同的服務之間傳遞異步消息的很好的選擇。如果對于要做的事情不需要消息接收者立刻反饋,那就非常適合這種方式(例如發送郵件)。
</blockquote>我們的 CMS 應用中,文章發布后,訂閱文章的主題的用戶會被通知到(通過郵件,或者網站通知或者推送消息),告知他們有感興趣的文章被發布。我們的程序并不需要 Notifier 服務的反饋,只需要把消息發給它就行了。
使用 Rails 的隊列
之前的一篇文章,我介紹了如何使用ActiveJob,Rails 自帶的,用來處理這種后臺或者異步處理的任務。
ActiveJob 要求接收代碼也需要運行在 Rails 環境,不過它確實是一種很好的選擇,簡單易用。
使用 RabbitMQ
RabbitMQ 是 Rails(以及 Ruby)之外的另一個選擇,可以作為不同的服務之間的一個通用的消息處理系統。通過 RabbitMQ 也可以處理遠程方法調用(RPC),不過更多的是使用 RabbitMQ 向其他服務方式異步消息。這里有很好的 Ruby 的使用教程。
下面的類用于向 Notifier 服務發送消息,通知有新文章發布。
class NotifierServiceattr_reader :category, :title, :published_url
def initialize(category, title, published_url) @category = category @title = title @published_url = published_url end
def call publish payload end
private
def publish(data) channel.default_exchange.publish(data, routing_key: queue.name) connection.close end
def payload {category: category, title: title, published_url: published_url}.to_json end
def connection @conn ||= begin conn = Bunny.new conn.start end end
def channel @channel ||= connection.create_channel end
def queue @queue ||= channel.queue 'notifier' end
end</pre>
代碼可以這樣調用:
NotifierService.new("Soccer", "My Hair Secrets", "http://localhost:3000/article/my-hair-secrets").call總結
微服務并不可怕,不過確實需要仔細的處理。它會帶來很多好處。我的建議是從一個有著清晰邊界的小系統開始,這樣你可以很容易的劃分服務。
</blockquote>更多的服務意味著更多的開發運維工作(你不再只是部署一個單獨的程序,而是需要部署多個小服務),這時你也許有興趣看一下我寫的如何部署到 Docker 容器。
本文地址:http://www.oschina.net/translate/architecting-rails-apps-as-microservices
原文地址:http://blog.codeship.com/architecting-rails-apps-as-microservices/
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!