如何在Ruby中编写微服务?

栏目: Ruby · 发布时间: 8年前

内容简介:如何在Ruby中编写微服务?

最近,大家都认为应当采用微服务架构。但是,又有多少相关教程呢?我们来看看这篇关于用 Ruby 编写微服务的文章吧。

人人都在讨论微服务,但我至今也没见过几篇有关用Ruby编写微服务的、像样的教程。这可能是因为许多Ruby开发人员仍然最喜欢Rails架构(这没什么不好,Rails本身也没什么不好,但是Ruby可以做到的事还有很多呢。)

所以,我想出一份力。让我们先来看看如何在Ruby中编写和部署微服务。

想象一下这个场景:我们需要编写一个微服务,其职责是发邮件。它收到的信息如下:

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

它的任务是替换掉模板中的某些变量,然后把发票邮件发送至user@example.com。(我们用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":"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来解决问题——可以充分使用操作系统自带的工具,感觉好极了。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

A Philosophy of Software Design

A Philosophy of Software Design

John Ousterhout / Yaknyam Press / 2018-4-6 / GBP 14.21

This book addresses the topic of software design: how to decompose complex software systems into modules (such as classes and methods) that can be implemented relatively independently. The book first ......一起来看看 《A Philosophy of Software Design》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具