如何在Ruby中編寫微服務? 頂 原

【編者按】本文作者爲 Pierpaolo Frasa,文章通過詳細的案例,介紹了在Ruby中編寫微服務時所需注意的方方面面。系國內 ITOM 管理平臺 OneAPM 編譯呈現。

最近,大家都認爲應當採用微服務架構。但是,又有多少相關教程呢?我們來看看這篇關於用Ruby編寫微服務的文章吧。

人人都在討論微服務,但我至今也沒見過幾篇有關用Ruby編寫微服務的、像樣的教程。這可能是因爲許多Ruby開發人員仍然最喜歡Rails架構(這沒什麼不好,Rails本身也沒什麼不好,但是Ruby可以做到的事還有很多呢。)

所以,我想出一份力。讓我們先來看看如何在Ruby中編寫和部署微服務。

想象一下這個場景:我們需要編寫一個微服務,其職責是發郵件。它收到的信息如下:

  {  
  'provider': 'mandrill',  
  'template': 'invoice',  
  'from': '[email protected]',  
  'to': '[email protected]',  
  'replacements': {    
  'salutation': 'Jack',    
  'year': '2016'
  }
}

它的任務是替換掉模板中的某些變量,然後把發票郵件發送至[email protected]。(我們用mandrill作爲郵件API的供應商,令人憂傷的是,mandrill即將要停止服務了。)

這個例子非常適合使用微服務,因爲它很小,而且只關注某個功能點,接口也定義得很清晰。因此,當我們在工作中決定要重寫郵件基礎結構時,我們就會這樣做。

如果我們有一個微服務,我們需要找到一個方法,向它發送一些信息。也就是傳遞消息隊列的方法。有許許多多可選的消息系統,你可以隨便選擇一個自己喜歡的。我們這裏選取的是RabbitMQ,因爲:

  • 它很普及,而且是按照標準(AMQP)來編碼的。

  • 它已與多種語言綁定,因此非常適合多語言環境。我喜歡用Ruby來編寫應用(也覺得它比其他的語言更好),但我並不認爲目前Ruby適用於所有的問題,也不認爲將來會是這樣。因此,我們也有可能需要用Elixir編寫一個發送郵件的應用(寫起來也不會很困難)。

  • 它非常靈活,可以適應各種工作流 – 可以適應簡單的在後臺處理消息隊列的工作流(這是本文的重點討論對象),也可以適應複雜的消息交換工作流(甚至是RPC)。網站上有許多的例子。

  • 通過瀏覽器即可訪問它的管理員面板,這面板非常有用。

  • 它擁有有許多託管解決方案(你可以在你最喜歡的包管理器中找資源,從而進行開發)。

  • 它是用Erlang編寫的,Erlang的程序員們很好地處理了併發問題。

RabbitMQ 把消息放入隊列中非常簡單,就像下面這樣:

require 'bunny'
require 'json'

connection = Bunny.new
connection.start
channel = connection.create_channel
queue = channel.queue 'mails', durable: true

json = { ... }.to_json
queue.publish json
connection.close

bunny是RabbitMQ的標準gem,當我們不傳任何項給Bunny.new時,它會假設RabbitMQ有標準的證書,是在localhost:5672上運行的。然後我們(經過一系列設置)連接到一個名爲“mails”的消息隊列。如果這個隊列還不存在,系統會創建這個隊列;如果已存在,系統會直接連接。接着我們可以直接對這個隊列發佈任何消息(例如,我們上面的發票消息)。在這裏我們使用JSON,但事實上,你可以使用任何你喜歡的格式(BSON、Protocol Buffers,或者隨便啥),RabbitMQ並不關心。

現在,我們已經解決了producer端,但我們仍然需要一個應用接受並處理消息。我們使用的是snearkers。sneakers是圍繞RabbitMQ的一個壓縮gem。如果你想要做一些後臺處理,它會把你最可能要用到的RabbitMQ的子集暴露給你,但是底層還是RabbitMQ的。有了sneakers(sneakers是受到sidekiq啓發而來的),我們可以設置一個“worker”去處理我們的消息發送請求:

require 'sneakers'
require 'json'
require 'mandrill_api/provider'

class Mailer
  include Sneakers::Worker
  from_queue 'mails'
  
  def work(message)
    puts "RECEIVED: #{message}"
    option = JSON.parse(message)
    MandrillApi::Provider.new.deliver(options)
    ack!
  end
end

我們必須明確從哪個隊列讀取消息(即“mails”),以及consume消息的work方法,我們先解析消息(之前我們已經說過用JSON格式–但是再說明一次,你可以選擇任何格式,RabbitMQ或者sneakers並不關心格式問題)。接着我們把消息散列傳給一些內部的實際工作的類。最後,我們必須通知系統消息已收到,否則RabbitMQ就會把消息重新放回隊列中。如果你想拒絕某條消息,或者做別的操作,snearkers的wiki中有方法。爲了掌握情況,我們還在裏面加入了日誌功能(稍後我們會解釋爲什麼日誌爲標準輸出)。

但是一個程序不能只有一個類。所以我們需要建起一個項目結構–這個對於Rails開發人員來說是比較陌生的,因爲通常我們只需要運行rails new,然後所有的東西都設置好了。在此處我想多擴展一下。我們的項目樹完成以後差不多是這樣的:

.
├── Gemfile
├── Gemfile.lock
├── Procfile
├── README.md
├── bin
│   └── mailer
├── config
│   ├── deploy/...
│   ├── deploy.rb
│   ├── settings.yml
│   └── setup.rb
├── examples
│   └── mail.rb
├── lib
│   ├── mailer.rb
│   └── mandrill_api/...
└── spec
    ├── acceptance/...
    ├── acceptance_helper.rb
    ├── lib/...
    └── spec_helper.rb

這當中有一部分是可以自我說明的,例如Gemfile(\.lock)?以及readme。我們也不用過多的解釋spec文件夾,只需要知道,照慣例我們在這個目錄下放了兩個helper文件,一個(spec_helper.rb)用於進行快速單元測試,另一個(acceptance_helper.rb)用於驗收測試。驗收測試需要設置更多東西(例如,模擬真實的HTTP請求)。lib文件夾也跟我們的主題不太相關,我們可以看到裏面有一個lib/mailer.rb(這就是我們上面定義的worker類),剩下的一個文件是專門針對個性服務的。examples/mail.rb文件是示例郵件的編隊代碼,如同上文中的一樣。我們可以隨時用它發起手動測試。現在我想着重討論一下config/setup.rb文件。這是我們通常在一開始就會加載的文件(即使是在spec_helper.rb)。所以我們並不需要它做太多事情(否則你的測試就會變得很慢)。在我們的例子中,它是這樣的:

require 'bundler/setup'

lib_path = File.expand_path '../../lib', __FILE__
$LOAD_PATH.unshift lib_path

ENVIRONMENT = ENV['ENVIRONMENT'] || 'development'

require 'yaml'
settings_file = File.expand_path '../settings.yml', __FILE__
SETTINGS = YAML.load_file(settings_file)[ENVIRONMENT]

if %w(development test).include? ENVIRONMENT
  require 'byebug'
end

這裏最重要的就是設定加載路徑。首先,我們引入bundler/setup,由此我們可以通過gem的名稱來引入各個gem。接着,我們把服務的lib文件夾加入加載路徑。這意味着我們可以做很多事,例如引入mandrill_api/provider,它可以從<project_root>/ lib/mandrill_api/provider中找到。我們之所以這樣做,是因爲大家都不喜歡相對路徑。請注意,我們沒有在Rails中使用自動加載。我們也沒有調用Bundler.require,因爲這樣會引入Gemfile當中的所有gem。這意味着你得自己明確調用你需要的依賴項(gem或者是lib文件)(我覺得這樣挺好的)。

另外,我挺喜歡Rails的多環境。在上面的例子中,我們是通過UNIX環境變量ENVIRONMENT來加載的。我們還需要進行一些設置(例如RabbitMQ連接選項,或者是我們服務所使用的某些API的密鑰)。這些應當依賴於環境,所以我們加載了一個YAML文件,然後把它變成了全局變量。

最後,這樣的代碼可以保證在開發和測試的過程中,只要提前引入,你隨時可以加入byebug(Ruby 2.x的debug工具)。如果你擔心速度問題的話(它確實需要花點時間),你可以把它拿掉,需要的時候再放進來,或者是加入一個猴子補丁:

if %w(development test).include? ENVIRONMENT
  class Object
    def byebug
      require 'byebug'
      super
    end
  end
end

現在,我們有了一個worker類,和一個大致的項目結構。我們只需要通知sneakers運行worker即可,這是我們在bin/mailer裏所做的:

#!/usr/bin/env ruby
require_relative '../config/setup'
require 'sneakers/runner'
require 'logger'
require 'mailer'
require 'httplog'

Sneakers.configure(
  amqp: SETTINGS['amqp_url'],
  daemonize: false,
  log: STDOUT
)
Sneakers.logger.level = Logger::INFO
Httplog.options[:log_headers] = true

Sneakers::Runner.new([Mailer]).run

請注意這是可執行的(看看開頭的#!),所以我們無需ruby命令,可以直接運行。首先,我們加載設置文件(在這得使用一個相對路徑),接着加載其他的需要的東西,包括我們的郵件worker類。

這裏比較重要的是配置sneakers:amqp參數會接受一個針對RabbitMQ連接的URL,這可以從設置中加載而來。我們可以通知sneakers在前臺運行,並記錄日誌爲標準輸出。接着,我們給sneakers一個worker類的數組,讓sneakers運行這個數組。同樣我們也需要一個帶有日誌的庫,這樣我們可以動態觀察情況。httplog gem會記錄下所有向外發送的請求,這對於與外部API通信來說非常有用(在這我們也讓它記錄下HTTP headers,但這不是默認設置)。

現在運行bin/mailer ,就會變成下面這樣:

... WARN: Loading runner configuration...
... INFO: New configuration:
#<Sneakers::Configuration:0x007f96229f5f28 ...>
... INFO: Heartbeat interval used (in seconds): 2

但是實際的輸出其實要冗長的多!

如果你讓它繼續運行,然後在另一個終端窗口中運行我們上面的編隊腳本,就會得到下面的結果:

... RECEIVED: {"provider":"mandrill","template":"invoice", ...}
D, ... [httplog] Sending: POST
https://mandrillapp.com:443/api/1.0/messages/send-template.json
D, ... [httplog] Data: {"template_name":"invoice", ...}
D, ... [httplog] Connecting: mandrillapp.com:443
D, ... [httplog] Status: 200
D, ... [httplog] Response:
[{"email":"[email protected]","status":"sent", ...}]
D, ... [httplog] Benchmark: 1.698229061003076 seconds

(這裏也是簡化版本!)

這裏的信息量相當大,特別是開始的部分,當然,此後你可以根據需要去掉部分日誌。

以上給出了基本的項目結構,此外還要做什麼呢?呃,還有個困難的部分:部署。

在部署微服務(或者,總體來說,部署任何應用程序)時,要注意許多事項,包括:

  • 你會想把它做成守護進程(即讓它在後臺運行)。我們可以在上面設置sneakers的時候就做好這點,但我傾向於不那樣做——開發過程中,我希望能看到日誌輸出,並且可以用CTRL+C來殺死進程。

  • 你會想要一份合理的日誌。所謂合理,是指確保日誌文件最後不會填滿硬盤,或者變得巨大無比以至於需要花一輩子的時間去檢索它(例如:循環日誌)。

  • 你會希望在你因爲某個原因重啓服務器,或者程序莫名程序崩潰時,它都能重新啓動。

  • 你會希望有一些標準化的命令,在你需要的時候用來啓動/停止/重啓程序。

你可以在Ruby中靠自己做到這些,但我覺得有更好的方案:利用一些現成的東西來處理這些任務,即你的操作系統(sidekiq的創造者Mike Perhammm也同意我的看法)。對我們來說,這就意味着使用systemd,因爲這就是在我們的服務器(以及大部分如今的Linux系統)上運行的程序,但我不想在這引發口水戰。Upstart或者daemontools可能也可以。

“部署微服務時,你得考慮很多事情。”來自@Tainnor 點擊前往Tweet

要用systemd來運行我們的微服務,需要創建一些配置文件。這可以手工完成,但我更願意使用一款叫做foreman的工具來做。有了foreman,我們可以指定所有需要在Procfile中運行的進程:

mailer: bin/mailer

這裏我們只有一個進程,但你可以指定多個。我們指定了一個叫“mailer”的進程,它將運行bin/mailer這個可執行文件。foreman的好處體現在,它可以把這一配置文件導出到許多初始化系統中,包括systemd。例如,從這個簡單的Procfile,它能創建出很多文件;正如我剛纔所說,我們可以在Profile中指定多個進程,多個這樣的文件可以指定一個依賴層級。層級的頂短時一個mailer.target文件,它依賴於一個mailer-mailer.target文件(而如果我們的Procfile當中有多個進程,mailer.target則會依賴於多個子target文件)。mailer-mailer.target文件又依賴於mailer-mailer-1.service(這類文件也可以有多個,我們只需要將線程併發度的值明確設定爲大於1即可)。最後的文件看起來是這樣的:

[Unit]
PartOf=-.target

[Service]
User=mailer_user
WorkingDirectory=/var/www/mailer_production/releases/16
Environment=PORT=5000
Environment=PATH=
/home/deploy/.rvm/gems/ruby-2.2.3/gems/bundler-1.11.2:...
Environment=ENVIRONMENT=production
ExecStart=/bin/bash -lc 'bin/mailer'
Restart=always
StandardInput=null
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=%n
KillMode=process

具體細節並不重要。但是從上面的代碼可以看出,我們明確了用戶、工作路徑、開始運行服務的命令,也明確了每次遇到失效都應當重啓,以及記錄日誌並添加到系統日誌中。我們也設定了一些環境變量,包括PATH。稍後我會再談到這個。

有了這個,我們之前想要的系統行爲都實現了。現在它可以在後臺運行了,並且每次遇到失效都會重啓。你也可以通過運行sudo systemctl enable mailer.target讓它在系統啓動時就開始運行。至於標準輸出的日誌,會重新被寫入系統日誌。對於systemd來說,也就是journald,一個二進制的日誌記錄器(因此轉儲的問題就不再存在)。我們可以通過以下的方式來檢查我們的日誌輸出:

$ sudo journalctl -xu mailer-mailer-1.service
-- Logs begin at Thu 2015-12-24 01:59:54 CET, end at ... --
Feb 23 10:00:07 ... RECEIVED: {"from": ...}
...

你可以賦予journalctl 更多的選項,例如,根據日期進行篩選。

爲了讓foreman生成systemd文件,我們必須在部署中設置導出流程。不知道你是否用過Capistrano 2或Capistrano 3或者別的類似的工具(例如mina)。下面你會看到你可能需要的殼命令。最難的部分任務是如何正確設置環境變量。爲了確保foreman可以在啓動腳本中寫出剛纔的變量,我們可以從所部署的項目根目錄中運行下面的代碼,從而把它們先放進一個.env文件:

$ echo "PATH=$(bundle show bundler):$PATH" >> .env
$ echo "ENVIRONMENT=production" >> .env

(在此我省略了PORT變量——這個變量是foreman自動生成的。我們的服務也不需要它。)

接着我們告訴foreman,在讀取我們剛剛創建的.env文件的這些變量時,把它們導出到systemd。

$ sudo -E env "PATH=$PATH" bundle exec foreman\
  export systemd /etc/systemd/system\
  -a mailer -u mailer_user -e .env

這條命令挺長的,但歸根結底就是在運行foreman export systemd,同時指定了文件應該被放置到的目錄(據我所知/etc/systemd/system是其標準目錄)、運行該命令的用戶、以及加載文件的環境。

然後我們重新加載所有的東西:

$ sudo systemctl daemon-reload
$ sudo systemctl reload-or-restart mailer.target

接下來,我們啓用該服務,讓它在服務器啓動之後保持運行:

$ sudo systemctl enable mailer.target

此後,我們的服務就可以在服務器上啓動並保持運行,並準備接受發來的所有消息了。

筆者在本文中涵蓋了很多方面,但我希望能讓你們看到編寫和部署微服務背後的全景。顯然,如果你真想自己掌握這些內容,還得深入研究。但我想我已經告訴了你,有哪些技術可以研究。

我們幾個月前寫了一個類似的郵件服務,到目前爲止,我們對結果都挺滿意。郵件服務是相對獨立的,有一個明確定義的API,並且經過獨立的嚴格測試,因此我們相信它能達到我們的預期。而其健全的重啓機制對我們來說也像個交易熔斷器——有些sidekiq工作程序偶爾會出bug,於是我們只好通過添加monit來解決問題——可以充分使用操作系統自帶的工具,感覺好極了。

本文系 OneAPM 工程師編譯整理。 OneAPM 能爲您提供端到端的 Ruby 應用性能解決方案,我們支持所有常見的 Ruby 框架及應用服務器,助您快速發現系統瓶頸,定位異常根本原因。分鐘級部署,即刻體驗,Ruby 監控從來沒有如此簡單。想閱讀更多技術文章,請訪問 OneAPM 官方技術博客

本文轉自 OneAPM 官方博客

原文地址:https://dzone.com/articles/writing-a-microservice-in-ruby

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章