Rails 微服务架构

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介:

Rails 应用有各种类型,规模也各有不同。有的是一个独立的庞大的应用,全部应用都在同一个位置(包括管理界面、API、前端部分以及所有需要的模块)。另一些应用则是划分成一系列的微服务,服务之间互相通信,这样可以把整个应用切分成更易管理的部分。

这种微服务的架构被称为面向服务的架构( SOA )。虽然我见到过的 Rails 应用通常都倾向于成为独立的程序,不过开发者也完全可以选择让多个 Rails 程序,以及与其他语言或者框架编写的服务一起工作来完成任务。

Rails应用通常是独立的程序,但是没有理由阻止你尝试微服务的架构。

独立的程序不意味着一定写的不好,但是写的差的独立程序被拆成微服务后大多也是很糟糕的。有多种方式可以让你写出清晰的(更容易测试的)代码,同时在需要拆分应用的时候也更轻松。

使用微服务架构的 Rails 应用的用例

本文会讨论如何实现一个 CMS 的网站。可以假设是一家大的报纸或者博客,有很多作者负责投稿,用户可以按主题订阅内容。

Martin Fowler 有一篇很不错的文章,介绍了为什么编辑和发布应该分成两个不同的系统。我们的用例与此类似,另外我们还要添加两个模块:通知和订阅。

我们的 CMS 现在有四个主要的模块:

CMS 编辑器:作者和编辑用来创建、编辑和发布文章。

公共的网站:对外提供服务,浏览已发布的文章。

通知: 通知订阅者有新发布的文章。

订阅: 管理用户账号和订阅。

image

Rails 应用需要支持 SOA 吗?

是选择独立程序还是构建成微服务?这里没有对和错之分,不过下面的问题能帮你做出决定。

如何确定是选择独立程序还是微服务?

团队的组织结构是怎样的?

是否选择支持 SOA 通常与技术无关,而是在于开发团队的组织结构。

由四个团队分别负责一个主要的模块,比所有人在整个系统上一起工作要靠谱一些。如果你只有一个团队或者少数几个开发人员,一开始就决定采用微服务架构实际上会减慢开发的速度,这是因为需要为四个不同的组件直接的通信以及部署增加开发量。

不同的模块规模不一样?

对于本文的例子,有一个问题提现的很好,对外提供服务的公共网站肯定要比作者和编辑使用的 CMS 编辑器的访问压力要大很多。

如果这些模块都部署成分离的系统,我们就可以单独的控制它们的规模,为系统中不同的部分采用不同的缓存技术。你当然还是可以坚持采用单一的系统,但是那样的话你就只能为整个系统一次性确定其规模,而不是对不同的组件分开处理。

不同的模块使用不同的技术?

对于 CMS 编辑器,你也许想使用 Single Page Application (SPA),采用 React 或者 Angular 技术。而对外的网站,会使用更传统一些的服务端渲染的 Rails 应用(为了支持 SEO)。也许通知模块更适合 Elixir,因为这个语言对并发和并行处理支持不错。

模块的分离,使得你可以为每个模块选择最适合的编程语言。

边界定义

现在最重要的事情是定义好系统中模块之间的边界。

系统中的某个部分可能是某个外部 Server 的 Client。使用方法调用还是基于 HTTP 都不重要,它只需要知道它需要与系统中的其他部分进行通信。

为此我们需要定义清晰的边界。

当一篇文章发布时,会发生两件事:

首先会把文章的发布版本发送给对外的网站,它会返回一个发布后的 URL。

然后我们把刚创建的公开的 URL、话题、标题发送到通知模块,后者会通知到所有对话题感兴趣的订阅者。这一步可以是异步的,因为通常会耗费一些时间来通知到每一个用户,并且这个通知是不会有反馈的。

例如,下面的代码用来发布一篇文章。文章本身不会关心服务是通过方法调用还是 HTTP 来调用的。

class Publisher 
  attr_reader :article, :service

  def initialize(article, service) 
    @article = article 
    @service = service 
  end

  def publish 
    mark_as_published call_service 
    article 
  end

  private

  def call_service 
    service.new( 
      author: article.author, 
      title: article.title, 
      slug: article.slug, 
      category: article.category, 
      body: article.body 
    ).call 
  end

  def mark_as_published(published_url) 
    article.published_at = Time.zone.now 
    article.published_url = published_url 
  end 
end

这种方式也可以让我们方便测试 Publisher 类的功能,我们可以使用 TestPublisherService 来做测试,它会返回预定义的应答。

require "rails_helper"

RSpec.describe Publisher, 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." 
    }) 
  }

  class TestPublisherService < PublisherService 
    def call 
      "http://www.website.com/article/#{slug}" 
    end 
  end

  describe 'publishes an article to public website' do 
    subject { Publisher.new(article, TestPublisherService) }

    it 'sets published url' do
      published_article = subject.publish
      expect(published_article.published_url).to eq('http://www.website.com/article/my-hair-secrets')
    end
    
    it 'sets published at' do
      published_article = subject.publish
      expect(published_article.published_at).to be_a(Time)
    end
  end 
end

实际上 PublisherService 的具体实现还没有完成,但是这不妨碍我们为客户端(此处是 Publisher)编写测试用例来保证其按预期工作。

class PublisherService 
  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 
    # coming soon 
  end 
end

服务间通信

服务之间需要能够互相通信。对此作为 Ruby 程序员应该是很熟悉了,即使之前没有做过微服务的程序。

调用某个对象的方法,只需要给它发送消息,例如调用 Time.send(:now) 就可以改变 Time.now。不管是通过方法调用还是基于 HTTP 进行通信,原理是一样的。我们要做的是给系统的其他部分发送消息,通常还需要有回应。

使用 HTTP 协议和微服务通讯

当你的应用需要一个来自服务端的立即响应才能继续执行的时候,使用 HTTP 协议来交互将是不二的选择。

当你需要一个立即响应的时候,HTTP 协议通讯将是不二的选择。

在下面的例子中,PublisherService 类实现了使用 HTTP Post 方法来和后端的 Faraday 服务模块进行通讯。

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中,这个常量的值是通过初始化代码设置的。这样做的好处就是允许我们使用环境变量,根据部署环境的不同(比如开发环境或者生产环境)来给它配置不同的值。

Cms::PUBLIC_WEBSITE_URL = ENV['PUBLIC_WEBSITE_URL'] || 'http://localhost:3000'

测试我们的服务

现在让我们来测试 PublisherService 类,看看它是否正常工作。

在这个测试中,由于我们是在开发环境中做测试,所以并不能保证后端服务一直可用,因此我们将使用 WebMock 模块来模拟到后端的 HTTP 请求,并返回需要的数据。

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 类,目前这个类并没有对外提供任何有用的信息,仅仅表示发生了一个错误。下面是这个类的相关代码。

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 服务发送消息,通知有新文章发布。

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

代码可以这样调用:

NotifierService.new("Soccer", "My Hair Secrets", "http://localhost:3000/article/my-hair-secrets").call

总结

微服务并不可怕,不过确实需要仔细的处理。它会带来很多好处。我的建议是从一个有着清晰边界的小系统开始,这样你可以很容易的划分服务。

微服务并不可怕,不过确实需要仔细的处理。

更多的服务意味着更多的开发运维工作(你不再只是部署一个单独的程序,而是需要部署多个小服务),这时你也许有兴趣看一下我写的如何部署到 Docker 容器。

文章转载自 开源中国社区[https://www.oschina.net]

相关实践学习
RocketMQ一站式入门使用
从源码编译、部署broker、部署namesrv,使用java客户端首发消息等一站式入门RocketMQ。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
20小时前
|
负载均衡 JavaScript Java
构建高效微服务架构:后端开发的新视角
【5月更文挑战第13天】在现代软件开发中,微服务架构已经成为一种流行趋势。它通过将应用程序拆分为一组小型、独立的服务来提高可扩展性、弹性和可维护性。本文将探讨如何构建一个高效的微服务架构,包括选择合适的技术栈、设计良好的服务接口、确保数据一致性以及实现有效的服务发现和负载均衡。
|
20小时前
|
监控 Java 开发者
构建高效微服务架构:后端开发的新趋势
【5月更文挑战第13天】随着现代应用的复杂性日益增加,传统的单体应用架构已不足以满足快速迭代和可扩展性的需求。本文将探讨如何通过微服务架构来提升后端开发的效率和系统的可靠性,涵盖微服务设计原则、技术栈选择、部署策略以及维护实践。我们将分析微服务的优势与挑战,并提供一系列实施建议,帮助开发者在构建和维护分布式系统时做出明智决策。
|
1天前
|
监控 持续交付 数据库
构建高效可靠的微服务架构:后端开发的新范式
【5月更文挑战第13天】 在当今软件开发的世界中,微服务架构已经成为了一种流行且有效的设计模式。它通过将大型复杂系统分解为一组独立的、可部署的服务来提高系统的可维护性、可扩展性和敏捷性。本文将探讨如何构建一个高效且可靠的微服务架构,包括关键的设计原则、技术选型以及可能面临的挑战。我们的目标是为后端开发者提供一套实用的指南,以便在构建现代化应用程序时做出明智的决策。
|
2天前
|
监控 API 开发者
构建高效微服务架构:后端开发的新范式
【5月更文挑战第12天】 在现代软件开发的浪潮中,微服务架构已经成为了设计复杂系统的首选模式。它通过将大型应用程序拆分成一组小而专注的服务来增强系统的可维护性和可扩展性。本文将探讨微服务架构的关键概念、优势以及如何在后端开发中实现一个高效的微服务系统。我们还将讨论一些常见的挑战和最佳实践,以帮助开发者避免陷入常见的陷阱。
15 6
|
2天前
|
存储 NoSQL MongoDB
【MongoDB 专栏】MongoDB 与微服务架构的结合
【5月更文挑战第11天】微服务架构流行趋势下,选择合适的数据库至关重要。MongoDB作为非关系型数据库,与微服务有天然契合度。其灵活的文档模型、水平扩展性、高性能及局部事务支持,满足微服务对数据模型多样性、高可用性、快速读写的需求。实践中,需注意数据划分、索引优化、监控调优和版本控制。未来,MongoDB在微服务中的应用将更广泛,新技术将提升其在微服务架构中的价值。
【MongoDB 专栏】MongoDB 与微服务架构的结合
|
3天前
|
监控 数据库 开发者
构建高效可靠的微服务架构:策略与实践
【5月更文挑战第11天】在当今软件开发的世界中,微服务架构已经成为构建可扩展、灵活且容错的系统的首选方法。本文深入探讨了设计、部署和维护微服务系统时面临的挑战,并提出了一系列实用的策略和最佳实践。我们将从服务的划分原则出发,讨论如何确保每个微服务的自治性,以及如何通过容器化和编排技术实现服务的高效运行。文章还将涉及监控、日志记录和故障恢复的策略,旨在帮助开发人员构建一个既高效又可靠的微服务环境。
|
3天前
|
Kubernetes API 开发者
构建高效微服务架构:后端开发的新范式
【5月更文挑战第11天】 在现代软件开发的快速演变中,微服务架构已成为企业追求敏捷性、可扩展性和技术多样性的关键解决方案。本文旨在探讨如何构建高效的微服务架构,并分析其对后端开发的影响。我们将通过一系列最佳实践和策略,展示如何优化服务的独立性、弹性和性能,同时确保系统的整体稳定性和安全性。文章还将介绍容器化、API网关、服务发现和分布式追踪等关键技术的应用,为后端开发者提供一份全面的微服务实施指南。
|
3天前
|
设计模式 监控 API
构建高效的微服务架构:后端开发的新范式
【5月更文挑战第11天】 在当今的软件开发领域,微服务架构已经成为一种流行的设计模式。它通过将应用程序分解为一组小型、松散耦合的服务来提供高度可扩展和灵活的解决方案。本文将探讨如何构建一个高效的微服务架构,包括选择合适的技术栈、设计原则以及应对常见挑战的策略。我们将深入讨论如何确保系统的可维护性、可靠性和性能,同时考虑到安全性和监控的需求。
|
4天前
|
监控 持续交付 Docker
使用Docker进行微服务架构的最佳实践
【5月更文挑战第10天】本文探讨了使用Docker实施微服务架构的最佳实践。首先,理解微服务架构是拆分小型独立服务的模式,借助Docker实现快速部署、高可移植性和环境一致性。Docker的优势在于服务扩展、容器编排、自动化构建与部署。最佳实践包括:定义清晰服务边界,使用Dockerfile和Docker Compose自动化构建,利用Docker Swarm或Kubernetes编排,实施服务发现和负载均衡,监控与日志记录,以及持续集成和持续部署。Docker虽重要,但需与其他技术结合以确保系统整体稳定性。
|
4天前
|
缓存 负载均衡 API
微服务架构下的API网关性能优化实践
【5月更文挑战第10天】在微服务架构中,API网关作为前端和后端服务之间的关键枢纽,其性能直接影响到整个系统的响应速度和稳定性。本文将探讨在高并发场景下,如何通过缓存策略、负载均衡、异步处理等技术手段对API网关进行性能优化,以确保用户体验和服务的可靠性。