当前位置: > Linux服务器 > 服务器设置 >

Rails 微服务架构

时间:2015-12-05 20:46来源:oschina.net 作者:zicode

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

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

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

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

 

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

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

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

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

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

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

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

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

CMS system

 

Rails 应用需要支持 SOA 吗?

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

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

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

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

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

 

不同的模块规模不一样?

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

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

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

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

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



边界定义

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

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

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

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

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

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

     

    例如,下面的代码用来发布一篇文章。文章本身不会关心服务是通过方法调用还是 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
    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 来做测试,它会返回预定义的应答。

    ?
    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
    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)编写测试用例来保证其按预期工作。

    ?
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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 进行通信,原理是一样的。我们要做的是给系统的其他部分发送消息,通常还需要有回应。

    (责任编辑:IT)

------分隔线----------------------------