天天看点

如何在Ruby中编写微服务?

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

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

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

这个例子非常适合使用微服务,因为它很小,而且只关注某个功能点,接口也定义得很清晰。因此,当我们在工作中决定要重写邮件基础结构时,我们就会这样做。

它很普及,而且是按照标准(amqp)来编码的。

它已与多种语言绑定,因此非常适合多语言环境。我喜欢用ruby来编写应用(也觉得它比其他的语言更好),但我并不认为目前ruby适用于所有的问题,也不认为将来会是这样。因此,我们也有可能需要用elixir编写一个发送邮件的应用(写起来也不会很困难)。

它非常灵活,可以适应各种工作流 – 可以适应简单的在后台处理消息队列的工作流(这是本文的重点讨论对象),也可以适应复杂的消息交换工作流(甚至是rpc)。网站上有许多的例子。

通过浏览器即可访问它的管理员面板,这面板非常有用。

<code>bunny</code>是rabbitmq的标准gem,当我们不传任何项给<code>bunny.new</code>时,它会假设rabbitmq有标准的证书,是在<code>localhost:5672</code>上运行的。然后我们(经过一系列设置)连接到一个名为“mails”的消息队列。如果这个队列还不存在,系统会创建这个队列;如果已存在,系统会直接连接。接着我们可以直接对这个队列发布任何消息(例如,我们上面的发票消息)。在这里我们使用json,但事实上,你可以使用任何你喜欢的格式(bson、protocol buffers,或者随便啥),rabbitmq并不关心。

我们必须明确从哪个队列读取消息(即“mails”),以及consume消息的<code>work</code>方法,我们先解析消息(之前我们已经说过用json格式–但是再说明一次,你可以选择任何格式,rabbitmq或者sneakers并不关心格式问题)。接着我们把消息散列传给一些内部的实际工作的类。最后,我们必须通知系统消息已收到,否则rabbitmq就会把消息重新放回队列中。如果你想拒绝某条消息,或者做别的操作,snearkers的wiki中有方法。为了掌握情况,我们还在里面加入了日志功能(稍后我们会解释为什么日志为标准输出)。

但是一个程序不能只有一个类。所以我们需要建起一个项目结构–这个对于rails开发人员来说是比较陌生的,因为通常我们只需要运行<code>rails new</code>,然后所有的东西都设置好了。在此处我想多扩展一下。我们的项目树完成以后差不多是这样的:

这当中有一部分是可以自我说明的,例如<code>gemfile(\.lock)?</code>以及readme。我们也不用过多的解释spec文件夹,只需要知道,照惯例我们在这个目录下放了两个helper文件,一个(<code>spec_helper.rb</code>)用于进行快速单元测试,另一个(<code>acceptance_helper.rb</code>)用于验收测试。验收测试需要设置更多东西(例如,模拟真实的http请求)。<code>lib</code>文件夹也跟我们的主题不太相关,我们可以看到里面有一个<code>lib/mailer.rb</code>(这就是我们上面定义的worker类),剩下的一个文件是专门针对个性服务的。<code>examples/mail.rb</code>文件是示例邮件的编队代码,如同上文中的一样。我们可以随时用它发起手动测试。现在我想着重讨论一下<code>config/setup.rb</code>文件。这是我们通常在一开始就会加载的文件(即使是在<code>spec_helper.rb</code>)。所以我们并不需要它做太多事情(否则你的测试就会变得很慢)。在我们的例子中,它是这样的:

这里最重要的就是设定加载路径。首先,我们引入<code>bundler/setup</code>,由此我们可以通过gem的名称来引入各个gem。接着,我们把服务的lib文件夹加入加载路径。这意味着我们可以做很多事,例如引入<code>mandrill_api/provider</code>,它可以从<code>&lt;project_root&gt;/ lib/mandrill_api/provider</code>中找到。我们之所以这样做,是因为大家都不喜欢相对路径。请注意,我们没有在rails中使用自动加载。我们也没有调用<code>bundler.require</code>,因为这样会引入gemfile当中的所有gem。这意味着你得自己明确调用你需要的依赖项(gem或者是lib文件)(我觉得这样挺好的)。

另外,我挺喜欢rails的多环境。在上面的例子中,我们是通过unix环境变量<code>environment</code>来加载的。我们还需要进行一些设置(例如rabbitmq连接选项,或者是我们服务所使用的某些api的密钥)。这些应当依赖于环境,所以我们加载了一个yaml文件,然后把它变成了全局变量。

最后,这样的代码可以保证在开发和测试的过程中,只要提前引入,你随时可以加入byebug(ruby 2.x的debug工具)。如果你担心速度问题的话(它确实需要花点时间),你可以把它拿掉,需要的时候再放进来,或者是加入一个猴子补丁:

现在,我们有了一个worker类,和一个大致的项目结构。我们只需要通知sneakers运行worker即可,这是我们在<code>bin/mailer</code>里所做的:

请注意这是可执行的(看看开头的#!),所以我们无需<code>ruby</code>命令,可以直接运行。首先,我们加载设置文件(在这得使用一个相对路径),接着加载其他的需要的东西,包括我们的邮件worker类。

现在运行<code>bin/mailer</code> ,就会变成下面这样:

但是实际的输出其实要冗长的多!

如果你让它继续运行,然后在另一个终端窗口中运行我们上面的编队脚本,就会得到下面的结果:

(这里也是简化版本!)

这里的信息量相当大,特别是开始的部分,当然,此后你可以根据需要去掉部分日志。

以上给出了基本的项目结构,此外还要做什么呢?呃,还有个困难的部分:部署。

在部署微服务(或者,总体来说,部署任何应用程序)时,要注意许多事项,包括:

你会想把它做成守护进程(即让它在后台运行)。我们可以在上面设置sneakers的时候就做好这点,但我倾向于不那样做——开发过程中,我希望能看到日志输出,并且可以用<code>ctrl+c</code>来杀死进程。

你会想要一份合理的日志。所谓合理,是指确保日志文件最后不会填满硬盘,或者变得巨大无比以至于需要花一辈子的时间去检索它(例如:循环日志)。

你会希望在你因为某个原因重启服务器,或者程序莫名程序崩溃时,它都能重新启动。

你会希望有一些标准化的命令,在你需要的时候用来启动/停止/重启程序。

“部署微服务时,你得考虑很多事情。”来自@tainnor

<a href="https://twitter.com/share?text=%22there%e2%80%99s%20lots%20of%20things%20you%20have%20to%20take%20care%20of%20when%20you%20deploy%20a%20microservice.%22%20via%20@tainnor&amp;url=https://blog.codeship.com/writing-microservice-in-ruby/">点击前往tweet</a>

这里我们只有一个进程,但你可以指定多个。我们指定了一个叫“mailer”的进程,它将运行<code>bin/mailer</code>这个可执行文件。foreman的好处体现在,它可以把这一配置文件导出到许多初始化系统中,包括systemd。例如,从这个简单的procfile,它能创建出很多文件;正如我刚才所说,我们可以在profile中指定多个进程,多个这样的文件可以指定一个依赖层级。层级的顶短时一个<code>mailer.target</code>文件,它依赖于一个<code>mailer-mailer.target</code>文件(而如果我们的procfile当中有多个进程,<code>mailer.target</code>则会依赖于多个子target文件)。<code>mailer-mailer.target</code>文件又依赖于<code>mailer-mailer-1.service</code>(这类文件也可以有多个,我们只需要将线程并发度的值明确设定为大于1即可)。最后的文件看起来是这样的:

具体细节并不重要。但是从上面的代码可以看出,我们明确了用户、工作路径、开始运行服务的命令,也明确了每次遇到失效都应当重启,以及记录日志并添加到系统日志中。我们也设定了一些环境变量,包括path。稍后我会再谈到这个。

有了这个,我们之前想要的系统行为都实现了。现在它可以在后台运行了,并且每次遇到失效都会重启。你也可以通过运行<code>sudo systemctl enable mailer.target</code>让它在系统启动时就开始运行。至于标准输出的日志,会重新被写入系统日志。对于systemd来说,也就是<code>journald</code>,一个二进制的日志记录器(因此转储的问题就不再存在)。我们可以通过以下的方式来检查我们的日志输出:

你可以赋予<code>journalctl</code> 更多的选项,例如,根据日期进行筛选。

(在此我省略了port变量——这个变量是foreman自动生成的。我们的服务也不需要它。)

接着我们告诉foreman,在读取我们刚刚创建的<code>.env</code>文件的这些变量时,把它们导出到systemd。

这条命令挺长的,但归根结底就是在运行<code>foreman export systemd</code>,同时指定了文件应该被放置到的目录(据我所知<code>/etc/systemd/system</code>是其标准目录)、运行该命令的用户、以及加载文件的环境。

然后我们重新加载所有的东西:

接下来,我们启用该服务,让它在服务器启动之后保持运行:

此后,我们的服务就可以在服务器上启动并保持运行,并准备接受发来的所有消息了。

笔者在本文中涵盖了很多方面,但我希望能让你们看到编写和部署微服务背后的全景。显然,如果你真想自己掌握这些内容,还得深入研究。但我想我已经告诉了你,有哪些技术可以研究。

我们几个月前写了一个类似的邮件服务,到目前为止,我们对结果都挺满意。邮件服务是相对独立的,有一个明确定义的api,并且经过独立的严格测试,因此我们相信它能达到我们的预期。而其健全的重启机制对我们来说也像个交易熔断器——有些sidekiq工作程序偶尔会出bug,于是我们只好通过添加monit来解决问题——可以充分使用操作系统自带的工具,感觉好极了。