如何在 Ruby 中建立微服務架構:分步指南
已發表: 2022-03-11什麼是微服務?
微服務是軟件設計的最新趨勢之一,其中多個獨立服務相互通信並擁有自己的流程和資源。 這種方法不同於典型的客戶端-服務器應用程序設計。 通常的客戶端-服務器應用程序由一個或多個客戶端、一個包含所有域數據和邏輯的單一後端以及一個允許客戶端訪問後端及其功能的 API 組成。
在微服務架構中,所描述的單體後端被一套分佈式服務所取代。 這種設計允許更好的職責分離、更容易的維護、為每個服務選擇技術的更大靈活性以及更容易的可擴展性和容錯性。 同時,複雜的分佈式系統也面臨著一系列挑戰。 它們有更大的機會必須處理競爭條件,並且它們更難調試,因為問題不容易被確定為單個服務,而是分佈在許多服務中。 如果在構建這樣的系統時沒有努力遵循最佳實踐,您可能會發現自己被不知道如何撲滅的火災所包圍。 必須特別注意服務的有效負載合同,因為一項服務的更改可能會影響其所有客戶端,從而影響所有後端服務套件。
所有這些考慮因素都很重要,但讓我們假設您已經考慮過了。 現在你想要的是找到一種方法來自己構建微服務後端。 因此,讓我們深入研究一下。
如何建立微服務架構
目前有很多方法可以設置微服務,在本指南中,我們將重點介紹代理架構。
代理架構
代理架構是讓您的服務在它們之間進行通信的一種方式。 在其中,所有服務都圍繞著消息傳遞服務器、代理,並且所有服務都連接到它。 服務向代理髮送消息,代理知道他需要哪些其他服務或哪些服務來轉發這些消息。 這樣,服務就不需要保留有關其他服務的信息。 相反,他們依靠代理來處理所有的消息傳遞,它允許他們被隔離並只專注於他們的特定域。 代理還可以在其接收者關閉時存儲消息,從而允許發送者和接收者不會被迫同時啟動,從而允許更大的隔離。 當然,這種解決方案也有缺點,因為代理很快就會成為瓶頸,因為所有通信都必須通過它們,而且它也可能成為後端的單點故障。 但是,有一些方法可以緩解這些問題。 一種方法是讓代理的多個實例並行運行,這將允許更好的系統容錯。 另一種方法是使用其他架構。 替代架構與我們將在本指南中實現的架構不同,不使用代理,或者使用不同的代理架構,或者使用不同的消息傳遞協議,例如 HTTP。
服務之間的通信
在本指南中,我們將使用 ZeroMQ 來處理服務和代理之間的通信。
ZeroMQ 提供了一個協議抽象層,它通過隨機傳輸處理多部分異步消息。 使用 ZeroMQ 在服務和代理之間進行消息傳遞的優勢超出了本指南的範圍,因此我們不會在這裡詳細介紹,但如果您想了解更多關於它們的信息,請查看 Quora 文章。 如果您有興趣尋找其他方式讓您的服務相互通信,我建議您查看 Broker vs. Brokerless 文章,看看還有什麼可以實現的。
構建微服務套件
本文將指導您完成創建微服務套件所需的所有步驟。 我們的系統將由經紀人和服務組成。 我們還將使用一個小型客戶端腳本來測試對服務套件的調用,但請記住,客戶端代碼可以在任何地方輕鬆使用。
那麼,讓我們開始構建吧。
入門
首先,讓我們確保您擁有運行代理和服務所需的一切。 首先,首先在您的機器上下載並安裝 Node.js、ZeroMQ 和 Git。 如果您使用的是 OSX,則每個都有自製軟件包,並且大多數 Linux 發行版也有每個軟件包,因此您應該對此沒有問題。 Windows 用戶可以簡單地使用上面提供的下載鏈接。
運行代理
安裝完所有必需的依賴項後,讓我們的代理運行。 在本指南中,我們使用代理的 Node.js 實現,它是 ZMQ 面向服務套件的一部分。 你可以在 GitHub 上找到它的代碼和文檔。 要運行代理,首先將代理引導程序克隆到您的計算機。 此存儲庫是使用上述代理庫的引導程序。 注意,這一步不是必需的,因為原始庫本身是可運行的,但兩者之間的區別在於,您可以在引導存儲庫中更改默認配置。
因此,首先,使用以下 Git 命令將項目下載到您的機器上:
$ git clone [email protected]:dadah/zmq-broker-bootstrap.git
完成後,移動到創建的目錄:
$ cd zmq-broker-bootstrap
現在安裝包依賴項:
$ npm install
經紀人現在準備好了。 要運行您的代理,請運行以下命令:
$ bin/zss-broker run
您可以在config/
目錄中找到每個環境的配置文件。 這是默認的開發配置:
{ "broker": { "backend": "tcp://127.0.0.1:7776", "frontend": "tcp://127.0.0.1:7777" }, "log": { "consolePlugin": { "level": "debug" } } }
backend
參數定義了 broker 的後端和前端的ip:port
地址。 後端地址是代理接收請求和回复服務的地方,前端地址是它接收和發送給服務客戶端的地方。 您還可以通過更改log.consolePlugin.level
來設置日誌記錄級別。 可能的值是trace
、 debug
、 info
、 warn
和error
,它們確定代理進程將輸出的日誌信息量。
運行服務
建立代理後,是時候開發您的第一個 Ruby 微服務了。 首先打開一個新的控制台窗口。 然後,創建一個將存儲您的服務的目錄,然後轉到該目錄。 在本指南中,我們使用 ZMQ SOA Suite 的 Ruby 客戶端和服務。 有一個可用的引導“Hello world”服務,所以讓我們使用它來運行我們的第一個微服務。
轉到您的服務目錄並克隆引導存儲庫:
$ git clone [email protected]:dadah/zmq-service-suite-ruby-bootstrap.git
轉到新創建的目錄:
$ cd zmq-service-suite-ruby-bootstrap
現在安裝所有依賴項:
$ bundle install
要啟動服務,請運行以下命令:
$ bin/zss-service run
偉大的。 您的第一個服務已啟動並運行。
如果您轉到讓代理運行的控制台窗口,您可以看到以下輸出:
2015-12-15 16:45:05 | INFO | BROKER - Async Broker is waiting for messages... 2015-12-15 16:45:14 | DEBUG | BACKEND - received from: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 76f50741-913a-43b9-94b0-36d8f7bd75b1 2015-12-15 16:45:14 | DEBUG | BACKEND - routing from: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 76f50741-913a-43b9-94b0-36d8f7bd75b1 to SMI.UP request... 2015-12-15 16:45:14 | INFO | SMI - SMI register for sid: HELLO-WORD instance: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b! 2015-12-15 16:45:14 | DEBUG | BACKEND - reply to: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 76f50741-913a-43b9-94b0-36d8f7bd75b1 with status: 200 2015-12-15 16:45:15 | DEBUG | BACKEND - received from: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 3b3a0416-73fa-4fd2-9306-dad18bc0502a 2015-12-15 16:45:15 | DEBUG | BACKEND - routing from: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 3b3a0416-73fa-4fd2-9306-dad18bc0502a to SMI.HEARTBEAT request... 2015-12-15 16:45:15 | DEBUG | BACKEND - reply to: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 3b3a0416-73fa-4fd2-9306-dad18bc0502a with status: 200 2015-12-15 16:45:16 | DEBUG | BACKEND - received from: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: b3044c24-c823-4394-8204-1e872f30e909 2015-12-15 16:45:16 | DEBUG | BACKEND - routing from: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: b3044c24-c823-4394-8204-1e872f30e909 to SMI.HEARTBEAT request... 2015-12-15 16:45:16 | DEBUG | BACKEND - reply to: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: b3044c24-c823-4394-8204-1e872f30e909 with status: 200
此日誌表示代理已確認新服務的存在並正在接收來自它的心跳消息。 每一秒,服務都會向代理髮送心跳消息,因此它知道服務的實例已啟動。
從服務中消費
那麼現在我們有一個服務正在運行,我們如何使用它呢?
在引導存儲庫中,有一個虛擬客戶端可用於測試“Hello World”服務。 只需打開一個新的控制台窗口或選項卡,然後轉到您的服務目錄。 到達那里後,運行以下命令:
$ bin/zss-client
您應該看到如下內容:
15-49-15 16:49:54 | INFO | ZSS::CLIENT - Request 90a88081-3485-45b6-91b3-b0609d64592a sent to HELLO-WORD:*#HELLO/WORLD with 1.0s timeout 15-49-15 16:49:54 | INFO | ZSS::CLIENT - Received response to 90a88081-3485-45b6-91b3-b0609d64592a with status 200 "Hello World"
如果您轉到運行服務的控制台窗口,您應該會看到:
Started hello-word daemon... 15-45-15 16:45:14 | INFO | ZSS::SERVICE - Starting SID: 'HELLO-WORD' ID: 'hello-word#aaa65374-8585-410a-a41d-c8a5b024553b' Env: 'development' Broker: 'tcp://127.0.0.1:7776' 15-49-15 16:49:54 | INFO | ZSS::SERVICE - Handle request for HELLO-WORD:*#HELLO/WORLD 15-49-15 16:49:54 | INFO | ZSS::SERVICE - Reply with status: 200
好的。 您剛剛啟動並使用了您的“Hello World”微服務。 然而,這不是我們打算做的。 我們想要構建我們的服務。 那麼,讓我們開始吧。
建立你的服務
首先,讓我們停止“Hello World”服務。 轉到服務的控制台窗口,然後按Ctrl+C
停止服務。 接下來我們需要將我們的“Hello World”服務變成“Person”服務。
代碼結構
讓我們首先看一下項目的代碼樹。 它看起來像這樣:
-
bin
目錄是您存儲啟動服務的腳本的位置。 -
config
目錄存放所有的配置文件。-
boot.rb
文件是您可以添加所有服務依賴項的地方。 如果你打開它,你會注意到那裡已經列出了許多依賴項。 如果您需要添加更多,這就是您應該做的地方。 -
application.yml
文件存儲您的所有應用程序設置。 我們稍後會看一下這個文件。 - 在
config/initializers
目錄中,您可以添加初始化腳本。 例如,您可以在此處添加 ActiveRecord 或 Redis 連接的設置。 您添加到此目錄的腳本將在服務啟動時運行。
-
- 在
db/migrate
目錄中,您可以存儲 ActiveRecord 或 Sequel 遷移(如果有)。 如果不這樣做,您可以完全刪除此目錄。 -
lib
目錄是您的主要應用程序代碼所在的位置。-
settings.rb
文件只是加載application.yml
文件並使其在整個服務範圍內可用,以便您可以在任何地方訪問您的配置。 例如,Settings.broker.backend
返回您在上面的 YML 文件中定義的代理後端地址。 - 文件
service_register.rb
是您註冊服務和服務路由的地方。 我們稍後會解釋。 -
hello_world_service.rb
文件定義了“Hello World”服務的端點。 - 如果您使用 ActiveRecord,或者您最終可能創建的任何其他數據訪問對象,例如 Sequel 模型,
lib/daos
目錄是您存儲 ActiveModel 對象的地方。 -
lib/dtos
目錄存儲您的數據傳輸對象。 這些對像是最終發送回服務客戶端的對象。 -
lib/repositories
目錄存儲您的存儲庫。 存儲庫是允許服務訪問數據的對象,並且是唯一允許處理 DAO 的對象。 因此,如果一個服務想要一組“Hello World”實例,它會向存儲庫請求它們。 反過來,存儲庫使用適當的 DAO 從數據庫中獲取相關數據。 然後將數據映射到合適的“HelloWorld”DTO 或“HelloWorld”DTO 集合,然後返回給服務。 -
lib/repositories/mappers
目錄是您存儲映射器的位置。 映射器是將 DAO 轉換為 DTO 的對象,反之亦然。
-
config
目錄中的application.yml
文件如下所示:
defaults: &defaults broker: backend: tcp://127.0.0.1:7776 frontend: tcp://127.0.0.1:7777 logging: console: level: info development: <<: *defaults test: <<: *defaults production: <<: *defaults
此設置僅設置代理的後端和前端地址以及日誌記錄級別。
如果到目前為止所有這些聽起來都令人困惑,請不要擔心,因為隨著我們繼續前進,它會變得更加清晰。
“人”服務
所以,讓我們繼續我們的“Person”服務。 讓我們從配置數據庫連接開始。 打開文件config/initializers/active_record.rb
並取消註釋那裡唯一的行。 然後,將以下條目添加到application.yml
中的開發配置中,使其如下所示:

defaults: &defaults broker: backend: tcp://127.0.0.1:7776 frontend: tcp://127.0.0.1:7777 logging: console: level: info database: adapter: postgresql database: zss-tutorial-development
現在您已經添加了數據庫配置,您必須創建數據庫。 目前,除非您使用默認的 PostgreSQL 數據庫,否則無法自動執行此操作,在這種情況下,您可以簡單地運行:
$ rake db:create
如果您更喜歡其他數據庫,則必須將適當的 gem 添加到 gemfile 中,然後捆綁安裝項目。
接下來是遷移。 為此,只需創建名為000_creates_persons.rb
的文件db/migrate
:
$ touch db/migrate/000_creates_persons_table.rb
打開文件並像使用常規 Rails 遷移一樣創建遷移:
class CreatesPersons < ActiveRecord::Migration def change create_table :persons do |t| t.name t.timestamps end end end
接下來,運行它:
$ rake db:migrate == 0 CreatesPersons: migrating ================================================ -- create_table(:persons) DEPRECATION WARNING: `#timestamp` was called without specifying an option for `null`. In Rails 5, this behavior will change to `null: false`. You should manually specify `null: true` to prevent the behavior of your existing migrations from changing. (called from block in change at /Users/francisco/Code/microservices-tutorial/db/migrate/000_creates_persons.rb:6) -> 0.0012s == 0 CreatesPersons: migrated (0.0013s) =======================================
現在我們已經創建了表,讓我們為它創建一個模型。 創建文件lib/daos/person.rb
:
$ touch lib/daos/person.rb
像這樣編輯它:
module DAO class Person < ActiveRecord::Base end end
有你的模型。 現在您需要為“Person”創建一個 DTO 模型,以便您可以將其返回給客戶端。 創建文件lib/dtos/person.rb
:
$ touch lib/dtos/person.rb
像這樣編輯它:
module DTO class Person < Base attr_reader :id, :name end end
接下來,您必須創建一個映射器來將“Person”DAO 轉換為“Person”DTO。 創建文件lib/repositories/mappers/person.rb
,並像這樣編輯它:
module Mapper class Person < Mapper::Base def self.to_dao dto_instance DAO::Person.new id: dto_instance.id, name: dto_instance.name end def self.to_dto dao_instance DTO::Person.new id: dao_instance.id, name: dao_instance.name end end end
在這裡, Mapper::Base
要求您實現self.to_dao
和self.to_dto
。 如果您不想這樣做,您可以實現self.map
並覆蓋調用to_dao
或to_dto
的Mapper::Base.map
,具體取決於它接收的屬性是 DAO 還是 DTO。
現在您有一個 DAO 來訪問您的數據庫,一個 DTO 將其發送到客戶端,以及一個 Mapper 將一個轉換為另一個。 您現在可以在存儲庫中使用這三個類來創建邏輯,使您能夠從數據庫中獲取人員並返回相應的 DTO 集合。
然後讓我們創建存儲庫。 創建文件lib/repositories/person.rb
:
$ touch lib/dtos/person.rb
像這樣編輯它:
module Repository class Person < Repository::Base def get DAO::Person.all.map do |person| Mapper::Person.map(person) end end end end
這個存儲庫只有實例方法get
,它只是從數據庫中獲取所有人員並將他們映射到人員 DTO 的集合中——非常簡單。 現在讓我們把這一切放在一起。 現在剩下的就是創建調用此存儲庫的服務和端點。 為此,讓我們創建文件lib/person_service.rb
:
$ touch lib/person_service.rb
像這樣編輯它:
class PersonService < BaseService attr_reader :person_repo def initialize @person_repo = Repository::Person.new end def get payload, headers persons = person_repo.get() if persons.empty? raise ZSS::Error.new(404, "No people here") else persons.map &:serialize end end end
“Person”服務在其初始化程序中初始化存儲庫。 “Person”服務的所有公共實例方法都有有效負載和標頭,如果不需要它們可以省略。 兩者都是Hashie::Mash
實例,它們將發送到端點的變量存儲為屬性或標頭,並且它們的回复模仿 HTTP 響應,因為每個響應都有一個狀態代碼,客戶端可以使用該狀態代碼來找出發送到端點的請求的結果服務,以及服務的響應負載。 響應代碼與您對 HTTP 服務器的期望相同。 例如,成功的請求將返回 200 狀態代碼以及響應負載。 如果發生某些服務錯誤,則狀態碼將為 500,如果發送給服務器的參數有問題,則狀態碼將為 400。服務可以使用大多數 HTTP 狀態碼及其負載進行回复。 因此,例如,如果您希望您的服務在不允許訪問某個端點時通知其客戶端,您可以通過響應 403 代碼來實現。 如果您回頭查看上面的服務代碼,您會看到另一個響應代碼示例。 在get
端點中,當找不到人時,我們將返回狀態代碼 404 以及可選的“No people here”消息,就像 HTTP 服務器在沒有可用資源時返回 404 一樣。 如果存儲庫確實返回人員,則服務將 DTO 序列化並將它們返回給客戶端。 每個 DTO 都有一個默認的序列化程序,它返回一個 JSON 對象,其中的鍵和對應的值在 DTO 定義中定義為attr_reader
或attr_accessible
。 當然,您可以通過在 DTO 類中定義您的 serialize 方法來覆蓋序列化程序。
現在我們已經定義了一個服務,我們需要註冊它。 這是最後一步。 打開文件lib/service_register.rb
並將所有出現的“HelloWorld”替換為“Person”,因此文件最終看起來像這樣:
module ZSS class ServiceRegister def self.get_service config = Hashie::Mash.new( backend: Settings.broker.backend ) service = ZSS::Service.new(:person, config) personInstance = PersonService.new service.add_route(personInstance, :get) return service end end end
您可能已經註意到, add_route
調用有一個小的變化。 我們刪除了字符串“HELLO/WORLD”。 這是因為僅當服務動詞與實現它的方法不匹配時才需要該字符串。 在我們的例子中,當使用 GET 動詞調用人員服務時,要調用的方法是get
,因此我們可以省略字符串。
ServiceRegister
類是您必須定義方法self.get_service
的地方。 此方法初始化服務並將其連接到代理的後端。 然後,它將該服務上的路由與一個或多個服務定義中的方法進行匹配。 例如,在以下情況下,它會創建服務並將其綁定到代理:
config = Hashie::Mash.new( backend: Settings.broker.backend ) service = ZSS::Service.new(:person, config)
然後它實例化一個服務處理程序:
personInstance = PersonService.new
接下來,將服務處理程序綁定到服務:
service.add_route(personInstance, :get)
最後,它必須返回服務實例。
return service
現在,在我們啟動“Person”服務之前,只有最後一步; 我們需要為它創建一個可執行腳本。 我們已經為“HelloService”找到了一個。 因此,打開文件bin/zss-service
,將“hello-word”替換為“person”,然後保存文件。 返回控制台並運行:
$ bin/zss-service run Starting person: PID: ./log LOGS: ./log Started person daemon... 15-29-15 19:29:54 | INFO | ZSS::SERVICE - Starting SID: 'PERSON' ID: 'person#d3ca7e1f-e229-4502-ac2d-0c01d8c285f8' Env: 'development' Broker: 'tcp://127.0.0.1:7776'
而已。 您剛剛第一次啟動了“Person”服務。 現在讓我們測試一下。 打開bin/zss-client
文件,將sid
變量更改為“person”,並將客戶端調用從hello_world()
更改為get()
。 完成後,在新窗口中運行客戶端:
$ bin/zss-client /Users/francisco/.rvm/gems/ruby-2.1.2/gems/zss-0.3.4/lib/zss/client.rb:41:in `new': No people here (ZSS::Error) from /Users/francisco/.rvm/gems/ruby-2.1.2/gems/zss-0.3.4/lib/zss/client.rb:41:in `call' from /Users/francisco/.rvm/gems/ruby-2.1.2/gems/zss-0.3.4/lib/zss/client.rb:55:in `method_missing' from bin/zss-client:12:in `<main>'
如您所見,您已經捕獲了ZSS::Error
。 這是因為當服務找不到人並且我們的服務數據庫中還沒有人時,我們會引發錯誤。
那麼讓我們處理這個錯誤。 打開zss-client
並像這樣編輯它:
begin client = ZSS::Client.new(sid, config) p client.get() rescue ZSS::Client => e if e.code == 404 p e.message else raise e end end
現在我們在錯誤代碼為 404 時打印錯誤消息,如果是不同的錯誤則引發錯誤。 讓我們再次運行我們的客戶端來看看它的實際效果:
$ bin/zss-client "No people here"
優秀。 現在讓我們將一些人添加到我們的表中,看看他們是否由服務返回給我們的客戶。 為此,只需打開一個服務控制台:
$ rake service:console
添加一些人:
$ rake service:console [1] pry(main)> DAO::Person.create name: 'John' => #<DAO::Person:0x007fe51bbe9d00 id: 1, name: "John", created_at: 2015-12-16 13:22:37 UTC, updated_at: 2015-12-16 13:22:37 UTC> [2] pry(main)> DAO::Person.create name: 'Mary' => #<DAO::Person:0x007fe51c1dafe8 id: 2, name: "Mary", created_at: 2015-12-16 13:22:42 UTC, updated_at: 2015-12-16 13:22:42 UTC> [3] pry(main)> DAO::Person.create name: 'Francis' => #<DAO::Person:0x007fe51bc11698 id: 3, name: "Francis", created_at: 2015-12-16 13:22:53 UTC, updated_at: 2015-12-16 13:22:53 UTC> [4] pry(main)> exit
現在,再次運行您的客戶端。
$ bin/zss-client [{"id"=>1, "name"=>"John"}, {"id"=>2, "name"=>"Mary"}, {"id"=>3, "name"=>"Francis"}]
你有它。
最後的考慮
瀏覽本指南中提供的代碼,您可能會認為有很多不必要的步驟,例如創建存儲庫或 DTO,您是對的。 擁有一個正常運行的“Person”服務所需要的只是你的服務類和你的 DAO,你可以直接從服務類調用它們。 不過,遵循本文中描述的模式是一種很好的做法,因為它允許您將服務邏輯與數據存儲操作分開。 服務應該只關注它們的邏輯,而存儲庫應該處理與數據存儲的所有交互。 DTO 確定服務的有效負載和序列化,而 DAO 只關心從存儲中獲取數據。 本指南中描述的約定和技術稱為存儲庫模式,您可以在下圖中查看。
最後,我想請任何發現這有用的人為 SOA 服務套件做出貢獻,以任何方式擴展和增強它。 歡迎您的所有分叉和拉取請求。
我希望這將幫助您開始使用微服務。 如果您想查看服務代碼,可以在 GitHub 上找到完整版本。