使用 HTTP 协议和微服务通讯
当你的应用需要一个来自服务端的立即响应才能继续执行的时候,使用 HTTP 协议来交互将是不二的选择。
当你需要一个立即响应的时候,HTTP 协议通讯将是不二的选择。
在下面的例子中,PublisherService 类实现了使用 HTTP Post 方法来和后端的 Faraday 服务模块进行通讯。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
class PublisherService < HttpService
attr_reader :author, :title, :slug, :category, :body
def initialize(author:, title:, slug:, category:, body:)
@author = author
@title = title
@slug = slug
@category = category
@body = body
end
def call
post["published_url"]
end
private
def conn
Faraday.new(url: Cms::PUBLIC_WEBSITE_URL)
end
def post
resp = conn.post '/articles/publish', payload
if resp.success?
JSON.parse resp.body
else
raise ServiceResponseError
end
end
def payload
{author: author, title: title, slug: slug, category: category, body: body}
end
end
|
这段代码简单来说就是构造了一个需要发送给后端的数据,然后通过 HTTP Post 发送到后端,并且处理从后端的返回的数据。但后端返回了正确的数据,程序将解释这个数据,否则程序将抛出一个异常。在后面我们将对这个代码进行详细地解释。
在代码中,后端服务程序的地址保存在常量 Cms::PUBLIC_WEBSITE_URL中,这个常量的值是通过初始化代码设置的。这样做的好处就是允许我们使用环境变量,根据部署环境的不同(比如开发环境或者生产环境)来给它配置不同的值。
?
1
|
Cms::PUBLIC_WEBSITE_URL = ENV['PUBLIC_WEBSITE_URL'] || 'http://localhost:3000'
|
测试我们的服务
现在让我们来测试 PublisherService 类,看看它是否正常工作。
在这个测试中,由于我们是在开发环境中做测试,所以并不能保证后端服务一直可用,因此我们将使用 WebMock 模块来模拟到后端的 HTTP 请求,并返回需要的数据。
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
RSpec.describe PublisherService, type: :model do
let(:article) {
OpenStruct.new({
author: 'Carlos Valderrama',
title: 'My Hair Secrets',
slug: 'my-hair-secrets',
category: 'Soccer',
body: "# My Hair Secrets\nHow hair was the secret to my soccer success."
})
}
describe 'call the publisher service' do
subject {
PublisherService.new(
author: article.author,
title: article.title,
slug: article.slug,
category: article.category,
body: article.body
)
}
let(:post_url) {
"#{Cms::PUBLIC_WEBSITE_URL}/articles/publish"
}
let(:payload) {
{published_url: 'http://www.website.com/article/my-hair-secrets'}.to_json
}
it 'parses response for published url' do
stub_request(:post, post_url).to_return(body: payload)
expect(subject.call).to eq('http://www.website.com/article/my-hair-secrets')
end
it 'raises exception on failure' do
stub_request(:post, post_url).to_return(status: 500)
expect{subject.call}.to raise_error(PublisherService::ServiceResponseError)
end
end
end
|
处理调用失败
在系统使用过程中,有一件事情是绝对不可避免的,那就是对于服务端的调用可能失败(服务暂时不可用或者网络通信超市),我们的代码应该要能够正确处理这些异常。
当远端服务不可用的时候,系统应该如何响应完全取决于开发者。在我们的 CMS 应用中,当远端服务不可用的时候,用户仍然可以创建和编辑文章,只是不能发布任何文章。
在上面的测试例子中,代码包含了对 HTTP Status Code 500 (服务段出现异常)的处理。当测试代码收到 500 Status Code 的时候,代码将抛出 PublisherService::ServiceResponseError 这个异常。 ServiceResponseError 这个异常类继承自 Error 类,目前这个类并没有对外提供任何有用的信息,仅仅表示发生了一个错误。下面是这个类的相关代码。
?
1
2
3
4
5
6
7
8
9
|
class HttpService
class Error < RuntimeError
end
class ServiceResponseError < Error
end
end
|
在 Martin Fowler 的一篇文章中,提出了另外一种处理服务不可用的方法(在他的文章中,他把这种方法叫做 CircuitBreaker 模式)。简单来说,这个模式的任务就是通过某种方式检测远端服务是否运作正常。如果运作不正常,它将阻止对响应远端服务的调用。
我们也可以通过让我们的应用感知远端服务的状态并且做出适当的反应来让我们的应用更强壮。这种系统行为的改变,我们既可以通过类似 CircuitBreaker 的模式来自动实现,也可以通过用户手动关闭系统的某些功能来实现。
在我们的例子中,如果我们可以在现实 Publish 按钮之前检查一下远端 Publish 服务是否可用,那么我们就可以直接避免对不可用服务的调用。
使用队列进行通信
HTTP 并非是与其他服务通信的唯一方式。队列是不同的服务之间传递异步消息的很好的选择。如果对于要做的事情不需要消息接收者立刻反馈,那就非常适合这种方式(例如发送邮件)。
队列是不同的服务之间传递异步消息的很好的选择。
我们的 CMS 应用中,文章发布后,订阅文章的主题的用户会被通知到(通过邮件,或者网站通知或者推送消息),告知他们有感兴趣的文章被发布。我们的程序并不需要 Notifier 服务的反馈,只需要把消息发给它就行了。
使用 Rails 的队列
之前的一篇文章,我介绍了如何使用ActiveJob,Rails 自带的,用来处理这种后台或者异步处理的任务。
ActiveJob 要求接收代码也需要运行在 Rails 环境,不过它确实是一种很好的选择,简单易用。
使用 RabbitMQ
RabbitMQ 是 Rails(以及 Ruby)之外的另一个选择,可以作为不同的服务之间的一个通用的消息处理系统。通过 RabbitMQ 也可以处理远程方法调用(RPC),不过更多的是使用 RabbitMQ 向其他服务方式异步消息。这里有很好的 Ruby 的使用教程。
下面的类用于向 Notifier 服务发送消息,通知有新文章发布。
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
class NotifierService
attr_reader :category, :title, :published_url
def initialize(category, title, published_url)
@category = category
@title = title
@published_url = published_url
end
def call
publish payload
end
private
def publish(data)
channel.default_exchange.publish(data, routing_key: queue.name)
connection.close
end
def payload
{category: category, title: title, published_url: published_url}.to_json
end
def connection
@conn ||= begin
conn = Bunny.new
conn.start
end
end
def channel
@channel ||= connection.create_channel
end
def queue
@queue ||= channel.queue 'notifier'
end
end
|
代码可以这样调用:
1
|
NotifierService.new("Soccer", "My Hair Secrets", "http://localhost:3000/article/my-hair-secrets").call
|
总结
微服务并不可怕,不过确实需要仔细的处理。它会带来很多好处。我的建议是从一个有着清晰边界的小系统开始,这样你可以很容易的划分服务。
微服务并不可怕,不过确实需要仔细的处理。
更多的服务意味着更多的开发运维工作(你不再只是部署一个单独的程序,而是需要部署多个小服务),这时你也许有兴趣看一下我写的如何部署到 Docker 容器。
http://www.oschina.net/translate/architecting-rails-apps-as-microservices
(责任编辑:IT) |