如何在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

时间: 2024-11-05 22:05:38

如何在Ruby中编写微服务?的相关文章

如何在BPM中使用REST服务(1):通过程序访问网页内容

这篇文章主要描述如何通过程序来访问网页内容,这是访问REST服务的基础. 在Java中,我们可以使用HttpUrlConnection类来实现,代码如下. 1 package http.base; 2 3 import java.io.BufferedReader; 4 import java.io.IOException; 5 import java.io.InputStream; 6 import java.io.InputStreamReader; 7 import java.net.Ht

如何在ubuntu中启用SSH服务

如何在ubuntu14.04 中启用SSH服务 开篇科普:  SSH 为 Secure Shell 的缩写,由 IETF 的网络工作小组(Network Working Group)所制定:SSH 为建立在应用层和传输层基础上的安全协议.SSH 是目前较可靠,专为远程登录会话和其他网络服务提供安全性的协议.利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题.SSH最初是UNIX系统上的一个程序,后来又迅速扩展到其他操作平台.SSH在正确使用时可弥补网络中的漏洞.SSH客户端适用于多种平台

面试中的微服务架构

近几年,微服务架构迅速在整个技术社区窜红,被认为是 IT 软件架构的未来方向.一线互联网公司由于具有大量的业务体量和业务场景,比如阿里.百度.网易,很早就开始入坑微服务架构. 但说起微服务,不少人还是有这样的困惑:“作为一个开发,微服务架构是不是和我关系不大?那不都是架构师的事吗?”   关于这个问题,我来谈谈自己的看法. 微服务是当下最火热的后端架构之一.不管你是一个什么级别的程序员,也不论你在一个什么体量的公司,服务化都是你迟早会遇到的难题.实践微服务的过程本身也是一个升级打怪的过程,这中间

如何在 IIS 中设置 HTTPS 服务

Windows Server2008.IIS7启用CA认证及证书制作完整过程 这篇文章介绍了如何安装证书申请工具: 如何在iis创建证书申请: 如何使用iis申请证书生成的txt文件,在工具中开始申请证书: 如何导出证书: 以及在网站中开始使用https的方法:

如何在win7中提供ftp服务

需求: 为Centos虚拟机共享文件所使用 为什么不用Linux提供服务? 因为家中配置最好的电脑是windows系统,并且虚拟机在今后的一段时间中将会run在这台电脑中,所以使用本机提供服务为最好的选择 为什么不直接使用xManager中的Xftp传输文件? 因为习惯里命令行操作,点来点去真的是很头疼. 步骤: 1. 创建一个用户ftp,此步骤可以省略,只是说一下windows7中如何添加用户,如果在后续的配置中,使用匿名访问的话,这个步骤完全可以省略. 2. 创建两个文件夹用于ftp文件的上

如何在xlwt中编写多个列的单元格?

目的,写下面的表格: ---------------- | Long Cell | ---------------- | 1 | 2 | ---------------- 如果下面这样写: sheet.write(0, 0, 'Long Cell') sheet.write(1, 0, 1) sheet.write(1, 1, 2) 结果就是: -------------------- | Long Cell | | -------------------- | 1 | 2 | --------

微服务框架和工具大全

引言:不去重新发明轮子总是更好的.本文探讨了14个已经可用并能提供使微服务的开发和部署更容易的平台.框架和功能.本文还补充了每个工具将如何有助于建立良好的微服务架构的简要概述. 本文选自<Java微服务>. 在<Java微服务>一书中,我们使用 Spring Cloud,它提供使微服务非常容易地开发所需的所有工具和平台.Spring Cloud使用 Netflix开放源码软件( OSS).让我们探讨 Netflix OSS--一个完整的软件包. Netflix开放源码软件(OSS)

微服务架构(Microservices)

说在前面 好久没写博文了,心里痒痒(也许是换工作后,有点时间了吧).最近好像谈论微服务的人比较多,也开始学习一下,但是都有E文,看起来半懂不懂的. Martinfowler的<微服务>,也算是入门必读了.有人翻译过,但是只有一半.还是自己练练手吧. 微服务 "微服务架构"一词在过去几年里广泛的传播,它用于描述一种独立部署的软件应用设计方式.这种架构方式并没有非常准确的定义,但是在业务能力.自动部署.端对端的整合.对语言及数据的分散控制上,却有着显著特征. "微服务

Spring cloud微服务安全实战

第1章 课程导学我们会对整个课程的内容做一个简要的介绍,包括章节的安排,使用的主要技术栈,实战案例的介绍以及前置知识的介绍等内容. 第2章 环境搭建开发工具的介绍及安装,介绍项目代码结构并搭建,基本的依赖和参数设置. 第3章 API安全我们从简单的API场景入手,讲述API安全相关的知识.首先我们会介绍要保证一个API安全都需要考虑哪些问题,然后我们针对这些问题介绍常见的安全机制,我们会针对每种问题和安全机制编写相应的代码,让大家对这些问题和安全机制有一个初步的认识.... 第4章 微服务网关安