如何在Ruby中編寫微服務?

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

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

人人都在討論微服務,但我至今也沒見過幾篇有關用Ruby編寫微服務的、像樣的教程。這多是由於許多Ruby開發人員仍然最喜歡Rails架構(這沒什麼很差,Rails自己也沒什麼很差,可是Ruby能夠作到的事還有不少呢。)程序員

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

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

{  
  'provider': 'mandrill',  
  'template': 'invoice',  
  'from': 'support@company.com',  
  'to': 'user@example.com',  
  'replacements': {    
  'salutation': 'Jack',    
  'year': '2016'
  }
}

它的任務是替換掉模板中的某些變量,而後把發票郵件發送至user@example.com。(咱們用mandrill做爲郵件API的供應商,使人憂傷的是,mandrill即將要中止服務了。)api

這個例子很是適合使用微服務,由於它很小,並且只關注某個功能點,接口也定義得很清晰。所以,當咱們在工做中決定要重寫郵件基礎結構時,咱們就會這樣作。數組

若是咱們有一個微服務,咱們須要找到一個方法,向它發送一些信息。也就是傳遞消息隊列的方法。有許許多多可選的消息系統,你能夠隨便選擇一個本身喜歡的。咱們這裏選取的是RabbitMQ,由於:瀏覽器

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

  • 它已與多種語言綁定,所以很是適合多語言環境。我喜歡用Ruby來編寫應用(也以爲它比其餘的語言更好),但我並不認爲目前Ruby適用於全部的問題,也不認爲未來會是這樣。所以,咱們也有可能須要用Elixir編寫一個發送郵件的應用(寫起來也不會很困難)。bash

  • 它很是靈活,能夠適應各類工做流 – 能夠適應簡單的在後臺處理消息隊列的工做流(這是本文的重點討論對象),也能夠適應複雜的消息交換工做流(甚至是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":"user@example.com","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

相關文章
相關標籤/搜索