如何在Ruby中編寫微服務?

來源:互聯網
上載者:User

標籤:

【編者按】本文作者為 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.newconnection.startchannel = connection.create_channelqueue = channel.queue ‘mails‘, durable: truejson = { ... }.to_jsonqueue.publish jsonconnection.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!  endend

我們必須明確從哪個隊列讀取訊息(即“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_pathENVIRONMENT = 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  endend

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

#!/usr/bin/env rubyrequire_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::INFOHttplog.options[:log_headers] = trueSneakers::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: POSThttps://mandrillapp.com:443/api/1.0/messages/send-template.jsonD, ... [httplog] Data: {"template_name":"invoice", ...}D, ... [httplog] Connecting: mandrillapp.com:443D, ... [httplog] Status: 200D, ... [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_userWorkingDirectory=/var/www/mailer_production/releases/16Environment=PORT=5000Environment=PATH=/home/deploy/.rvm/gems/ruby-2.2.3/gems/bundler-1.11.2:...Environment=ENVIRONMENT=productionExecStart=/bin/bash -lc ‘bin/mailer‘Restart=alwaysStandardInput=nullStandardOutput=syslogStandardError=syslogSyslogIdentifier=%nKillMode=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

如何在Ruby中編寫微服務?

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.