【編者按】本文做者爲 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