如何在 Ruby 中建立微服务架构:分步指南

已发表: 2022-03-11

什么是微服务?

微服务是软件设计的最新趋势之一,其中多个独立服务相互通信并拥有自己的流程和资源。 这种方法不同于典型的客户端-服务器应用程序设计。 通常的客户端-服务器应用程序由一个或多个客户端、一个包含所有域数据和逻辑的单一后端以及一个允许客户端访问后端及其功能的 API 组成。

微服务正在取代经典的单体后端服务器
鸣叫

在微服务架构中,所描述的单体后端被一套分布式服务所取代。 这种设计允许更好的职责分离、更容易的维护、为每个服务选择技术的更大灵活性以及更容易的可扩展性和容错性。 同时,复杂的分布式系统也面临着一系列挑战。 它们有更大的机会必须处理竞争条件,并且它们更难调试,因为问题不容易被确定为单个服务,而是分布在许多服务中。 如果在构建这样的系统时没有努力遵循最佳实践,您可能会发现自己被不知道如何扑灭的火灾所包围。 必须特别注意服务的有效负载合同,因为一项服务的更改可能会影响其所有客户端,从而影响所有后端服务套件。

所有这些考虑因素都很重要,但让我们假设您已经考虑过了。 现在你想要的是找到一种方法来自己构建微服务后端。 因此,让我们深入研究一下。

如何建立微服务架构

目前有很多方法可以设置微服务,在本指南中,我们将重点介绍代理架构。

代理架构

代理架构,中间有一个代理(B),周围有四个微服务,分别称为 N、S、E、W。请求/响应路径从架构外部的一个输入开始,然后沿着路径 N、B、 E、B、S、B、W、B、E、B、N,最后作为输出退出。

代理架构是让您的服务在它们之间进行通信的一种方式
鸣叫

代理架构是让您的服务在它们之间进行通信的一种方式。 在其中,所有服务都围绕着消息传递服务器、代理,并且所有服务都连接到它。 服务向代理发送消息,代理知道他需要哪些其他服务或哪些服务来转发这些消息。 这样,服务就不需要保留有关其他服务的信息。 相反,他们依靠代理来处理所有的消息传递,它允许他们被隔离并只专注于他们的特定域。 代理还可以在其接收者关闭时存储消息,从而允许发送者和接收者不会被迫同时启动,从而允许更大的隔离。 当然,这种解决方案也有缺点,因为代理很快就会成为瓶颈,因为所有通信都必须通过它们,而且它也可能成为后端的单点故障。 但是,有一些方法可以缓解这些问题。 一种方法是让代理的多个实例并行运行,这将允许更好的系统容错。 另一种方法是使用其他架构。 替代架构与我们将在本指南中实现的架构不同,不使用代理,或者使用不同的代理架构,或者使用不同的消息传递协议,例如 HTTP。

服务之间的通信

在本指南中,我们将使用 ZeroMQ 来处理服务和代理之间的通信。

ZeroMQ 堆栈。顶部是一个带有省略号的块,然后是 ZeroMQ 符号。底部块从上到下具有:传输、网络、数据链路和物理。

ZeroMQ 提供了一个协议抽象层,它通过随机传输处理多部分异步消息。 使用 ZeroMQ 在服务和代理之间进行消息传递的优势超出了本指南的范围,因此我们不会在这里讨论它们,但如果您想了解更多关于它们的信息,请查看 Quora 文章。 如果您有兴趣寻找其他方式让您的服务相互通信,我建议您查看 Broker vs. Brokerless 文章,看看还有什么可以实现的。

构建微服务套件

本文将指导您完成创建微服务套件所需的所有步骤。 我们的系统将由经纪人和服务组成。 我们还将使用一个小型客户端脚本来测试对服务套件的调用,但请记住,客户端代码可以在任何地方轻松使用。

那么,让我们开始构建吧。

入门

首先,让我们确保您拥有运行代理和服务所需的一切。 首先,首先在您的机器上下载并安装 Node.js、ZeroMQ 和 Git。 如果您使用的是 OSX,则每个都有自制软件包,并且大多数 Linux 发行版也有每个软件包,因此您应该对此没有问题。 Windows 用户可以简单地使用上面提供的下载链接。

运行代理

安装完所有必需的依赖项后,让我们的代理运行。 在本指南中,我们使用代理的 Node.js 实现,它是 ZMQ 面向服务套件的一部分。 你可以在 GitHub 上找到它的代码和文档。 要运行代理,首先将代理引导程序克隆到您的计算机。 此存储库是使用上述代理库的引导程序。 注意,这一步不是必需的,因为原始库本身是可运行的,但两者之间的区别在于,您可以在引导存储库中更改默认配置。

因此,首先,使用以下 Git 命令将项目下载到您的机器上:

 $ git clone [email protected]:dadah/zmq-broker-bootstrap.git

完成后,移动到创建的目录:

 $ cd zmq-broker-bootstrap

现在安装包依赖项:

 $ npm install

经纪人现在准备好了。 要运行您的代理,请运行以下命令:

 $ bin/zss-broker run

您可以在config/目录中找到每个环境的配置文件。 这是默认的开发配置:

 { "broker": { "backend": "tcp://127.0.0.1:7776", "frontend": "tcp://127.0.0.1:7777" }, "log": { "consolePlugin": { "level": "debug" } } }

backend参数定义了 broker 的后端和前端的ip:port地址。 后端地址是代理接收请求和回复服务的地方,前端地址是它接收和发送给服务客户端的地方。 您还可以通过更改log.consolePlugin.level来设置日志记录级别。 可能的值是tracedebuginfowarnerror ,它们确定代理进程将输出的日志信息量。

运行服务

建立代理后,是时候开发您的第一个 Ruby 微服务了。 首先打开一个新的控制台窗口。 然后,创建一个将存储您的服务的目录,然后转到该目录。 在本指南中,我们使用 ZMQ SOA Suite 的 Ruby 客户端和服务。 有一个可用的引导“Hello world”服务,所以让我们使用它来运行我们的第一个微服务。

转到您的服务目录并克隆引导存储库:

 $ git clone [email protected]:dadah/zmq-service-suite-ruby-bootstrap.git

转到新创建的目录:

 $ cd zmq-service-suite-ruby-bootstrap

现在安装所有依赖项:

 $ bundle install

要启动服务,请运行以下命令:

 $ bin/zss-service run

伟大的。 您的第一个服务已启动并运行。

如果您转到让代理运行的控制台窗口,您可以看到以下输出:

 2015-12-15 16:45:05 | INFO | BROKER - Async Broker is waiting for messages... 2015-12-15 16:45:14 | DEBUG | BACKEND - received from: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 76f50741-913a-43b9-94b0-36d8f7bd75b1 2015-12-15 16:45:14 | DEBUG | BACKEND - routing from: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 76f50741-913a-43b9-94b0-36d8f7bd75b1 to SMI.UP request... 2015-12-15 16:45:14 | INFO | SMI - SMI register for sid: HELLO-WORD instance: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b! 2015-12-15 16:45:14 | DEBUG | BACKEND - reply to: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 76f50741-913a-43b9-94b0-36d8f7bd75b1 with status: 200 2015-12-15 16:45:15 | DEBUG | BACKEND - received from: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 3b3a0416-73fa-4fd2-9306-dad18bc0502a 2015-12-15 16:45:15 | DEBUG | BACKEND - routing from: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 3b3a0416-73fa-4fd2-9306-dad18bc0502a to SMI.HEARTBEAT request... 2015-12-15 16:45:15 | DEBUG | BACKEND - reply to: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 3b3a0416-73fa-4fd2-9306-dad18bc0502a with status: 200 2015-12-15 16:45:16 | DEBUG | BACKEND - received from: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: b3044c24-c823-4394-8204-1e872f30e909 2015-12-15 16:45:16 | DEBUG | BACKEND - routing from: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: b3044c24-c823-4394-8204-1e872f30e909 to SMI.HEARTBEAT request... 2015-12-15 16:45:16 | DEBUG | BACKEND - reply to: hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: b3044c24-c823-4394-8204-1e872f30e909 with status: 200

此日志表示代理已确认新服务的存在并正在接收来自它的心跳消息。 每一秒,服务都会向代理发送心跳消息,因此它知道服务的实例已启动。

从服务中消费

那么现在我们有一个服务正在运行,我们如何使用它呢?

在引导存储库中,有一个虚拟客户端可用于测试“Hello World”服务。 只需打开一个新的控制台窗口或选项卡,然后转到您的服务目录。 到达那里后,运行以下命令:

 $ bin/zss-client

您应该看到如下内容:

 15-49-15 16:49:54 | INFO | ZSS::CLIENT - Request 90a88081-3485-45b6-91b3-b0609d64592a sent to HELLO-WORD:*#HELLO/WORLD with 1.0s timeout 15-49-15 16:49:54 | INFO | ZSS::CLIENT - Received response to 90a88081-3485-45b6-91b3-b0609d64592a with status 200 "Hello World"

如果您转到运行服务的控制台窗口,您应该会看到:

 Started hello-word daemon... 15-45-15 16:45:14 | INFO | ZSS::SERVICE - Starting SID: 'HELLO-WORD' ID: 'hello-word#aaa65374-8585-410a-a41d-c8a5b024553b' Env: 'development' Broker: 'tcp://127.0.0.1:7776' 15-49-15 16:49:54 | INFO | ZSS::SERVICE - Handle request for HELLO-WORD:*#HELLO/WORLD 15-49-15 16:49:54 | INFO | ZSS::SERVICE - Reply with status: 200

好的。 您刚刚启动并使用了您的“Hello World”微服务。 然而,这不是我们打算做的。 我们想要构建我们的服务。 那么,让我们开始吧。

建立你的服务

首先,让我们停止“Hello World”服务。 转到服务的控制台窗口,然后按Ctrl+C停止服务。 接下来我们需要将我们的“Hello World”服务变成“Person”服务。

代码结构

让我们首先看一下项目的代码树。 它看起来像这样:

我们示例 zmq-service-suite-ruby-bootstrap 项目的文件/文件夹层次结构。它在下面详细描述,但请注意最后提到的三个 .rb 文件实际上是在 lib/repositories 下,而不是在 lib 本身下。

  • bin目录是您存储启动服务的脚本的位置。
  • config目录存放所有的配置文件。
    • boot.rb文件是您可以添加所有服务依赖项的地方。 如果你打开它,你会注意到那里已经列出了许多依赖项。 如果您需要添加更多,这就是您应该做的地方。
    • application.yml文件存储您的所有应用程序设置。 我们稍后会看一下这个文件。
    • config/initializers目录中,您可以添加初始化脚本。 例如,您可以在此处添加 ActiveRecord 或 Redis 连接的设置。 您添加到此目录的脚本将在服务启动时运行。
  • db/migrate目录中,您可以存储 ActiveRecord 或 Sequel 迁移(如果有)。 如果不这样做,您可以完全删除此目录。
  • lib目录是您的主要应用程序代码所在的位置。
    • settings.rb文件只是加载application.yml文件并使其在整个服务范围内可用,以便您可以在任何地方访问您的配置。 例如, Settings.broker.backend返回您在上面的 YML 文件中定义的代理后端地址。
    • 文件service_register.rb是您注册服务和服务路由的地方。 我们稍后会解释。
    • hello_world_service.rb文件定义了“Hello World”服务的端点。
    • 如果您使用 ActiveRecord,或者您最终可能创建的任何其他数据访问对象,例如 Sequel 模型, lib/daos目录是您存储 ActiveModel 对象的地方。
    • lib/dtos目录存储您的数据传输对象。 这些对象是最终发送回服务客户端的对象。
    • lib/repositories目录存储您的存储库。 存储库是允许服务访问数据的对象,并且是唯一允许处理 DAO 的对象。 因此,如果一个服务想要一组“Hello World”实例,它会向存储库请求它们。 反过来,存储库使用适当的 DAO 从数据库中获取相关数据。 然后将数据映射到合适的“HelloWorld”DTO 或“HelloWorld”DTO 集合,然后返回给服务。
    • lib/repositories/mappers目录是您存储映射器的位置。 映射器是将 DAO 转换为 DTO 的对象,反之亦然。

config目录中的application.yml文件如下所示:

 defaults: &defaults broker: backend: tcp://127.0.0.1:7776 frontend: tcp://127.0.0.1:7777 logging: console: level: info development: <<: *defaults test: <<: *defaults production: <<: *defaults

此设置仅设置代理的后端和前端地址以及日志记录级别。

如果到目前为止所有这些听起来都令人困惑,请不要担心,因为随着我们继续前进,它会变得更加清晰。

“人”服务

所以,让我们继续我们的“Person”服务。 让我们从配置数据库连接开始。 打开文件config/initializers/active_record.rb并取消注释那里唯一的行。 然后,将以下条目添加到application.yml中的开发配置中,使其如下所示:

 defaults: &defaults broker: backend: tcp://127.0.0.1:7776 frontend: tcp://127.0.0.1:7777 logging: console: level: info database: adapter: postgresql database: zss-tutorial-development

现在您已经添加了数据库配置,您必须创建数据库。 目前,除非您使用默认的 PostgreSQL 数据库,否则无法自动执行此操作,在这种情况下,您可以简单地运行:

 $ rake db:create

如果您更喜欢其他数据库,则必须将适当的 gem 添加到 gemfile 中,然后捆绑安装项目。

接下来是迁移。 为此,只需创建名为000_creates_persons.rb的文件db/migrate

 $ touch db/migrate/000_creates_persons_table.rb

打开文件并像使用常规 Rails 迁移一样创建迁移:

 class CreatesPersons < ActiveRecord::Migration def change create_table :persons do |t| t.name t.timestamps end end end

接下来,运行它:

 $ rake db:migrate == 0 CreatesPersons: migrating ================================================ -- create_table(:persons) DEPRECATION WARNING: `#timestamp` was called without specifying an option for `null`. In Rails 5, this behavior will change to `null: false`. You should manually specify `null: true` to prevent the behavior of your existing migrations from changing. (called from block in change at /Users/francisco/Code/microservices-tutorial/db/migrate/000_creates_persons.rb:6) -> 0.0012s == 0 CreatesPersons: migrated (0.0013s) =======================================

现在我们已经创建了表,让我们为它创建一个模型。 创建文件lib/daos/person.rb

 $ touch lib/daos/person.rb

像这样编辑它:

 module DAO class Person < ActiveRecord::Base end end

有你的模型。 现在您需要为“Person”创建一个 DTO 模型,以便您可以将其返回给客户端。 创建文件lib/dtos/person.rb

 $ touch lib/dtos/person.rb

像这样编辑它:

 module DTO class Person < Base attr_reader :id, :name end end

接下来,您必须创建一个映射器来将“Person”DAO 转换为“Person”DTO。 创建文件lib/repositories/mappers/person.rb ,并像这样编辑它:

 module Mapper class Person < Mapper::Base def self.to_dao dto_instance DAO::Person.new id: dto_instance.id, name: dto_instance.name end def self.to_dto dao_instance DTO::Person.new id: dao_instance.id, name: dao_instance.name end end end

在这里, Mapper::Base要求您实现self.to_daoself.to_dto 。 如果您不想这样做,您可以实现self.map并覆盖调用to_daoto_dtoMapper::Base.map ,具体取决于它接收的属性是 DAO 还是 DTO。

现在您有一个 DAO 来访问您的数据库,一个 DTO 将其发送到客户端,以及一个 Mapper 将一个转换为另一个。 您现在可以在存储库中使用这三个类来创建逻辑,使您能够从数据库中获取人员并返回相应的 DTO 集合。

然后让我们创建存储库。 创建文件lib/repositories/person.rb

 $ touch lib/dtos/person.rb

像这样编辑它:

 module Repository class Person < Repository::Base def get DAO::Person.all.map do |person| Mapper::Person.map(person) end end end end

这个存储库只有实例方法get ,它只是从数据库中获取所有人员并将他们映射到人员 DTO 的集合中——非常简单。 现在让我们把这一切放在一起。 现在剩下的就是创建调用此存储库的服务和端点。 为此,让我们创建文件lib/person_service.rb

 $ touch lib/person_service.rb

像这样编辑它:

 class PersonService < BaseService attr_reader :person_repo def initialize @person_repo = Repository::Person.new end def get payload, headers persons = person_repo.get() if persons.empty? raise ZSS::Error.new(404, "No people here") else persons.map &:serialize end end end

“Person”服务在其初始化程序中初始化存储库。 “Person”服务的所有公共实例方法都有有效负载和标头,如果不需要它们可以省略。 两者都是Hashie::Mash实例,它们将发送到端点的变量存储为属性或标头,并且它们的回复模仿 HTTP 响应,因为每个响应都有一个状态代码,客户端可以使用该状态代码来找出发送到端点的请求的结果服务,以及服务的响应负载。 响应代码与您对 HTTP 服务器的期望相同。 例如,成功的请求将返回 200 状态代码以及响应负载。 如果发生某些服务错误,则状态码将为 500,如果发送给服务器的参数有问题,则状态码将为 400。服务可以使用大多数 HTTP 状态码及其负载进行回复。 因此,例如,如果您希望您的服务在不允许访问某个端点时通知其客户端,您可以通过响应 403 代码来实现。 如果您回头查看上面的服务代码,您会看到另一个响应代码示例。 在get端点中,当找不到人时,我们将返回状态代码 404 以及可选的“No people here”消息,就像 HTTP 服务器在没有可用资源时返回 404 一样。 如果存储库确实返回人员,则服务将 DTO 序列化并将它们返回给客户端。 每个 DTO 都有一个默认的序列化程序,它返回一个 JSON 对象,其中的键和对应的值在 DTO 定义中定义为attr_readerattr_accessible 。 当然,您可以通过在 DTO 类中定义您的 serialize 方法来覆盖序列化程序。

现在我们已经定义了一个服务,我们需要注册它。 这是最后一步。 打开文件lib/service_register.rb并将所有出现的“HelloWorld”替换为“Person”,因此文件最终看起来像这样:

 module ZSS class ServiceRegister def self.get_service config = Hashie::Mash.new( backend: Settings.broker.backend ) service = ZSS::Service.new(:person, config) personInstance = PersonService.new service.add_route(personInstance, :get) return service end end end

您可能已经注意到, add_route调用有一个小的变化。 我们删除了字符串“HELLO/WORLD”。 这是因为仅当服务动词与实现它的方法不匹配时才需要该字符串。 在我们的例子中,当使用 GET 动词调用人员服务时,要调用的方法是get ,因此我们可以省略字符串。

ServiceRegister类是您必须定义方法self.get_service的地方。 此方法初始化服务并将其连接到代理的后端。 然后,它将该服务上的路由与一个或多个服务定义中的方法进行匹配。 例如,在以下情况下,它会创建服务并将其绑定到代理:

 config = Hashie::Mash.new( backend: Settings.broker.backend ) service = ZSS::Service.new(:person, config)

然后它实例化一个服务处理程序:

 personInstance = PersonService.new

接下来,将服务处理程序绑定到服务:

 service.add_route(personInstance, :get)

最后,它必须返回服务实例。

 return service

现在,在我们启动“Person”服务之前,只有最后一步; 我们需要为它创建一个可执行脚本。 我们已经为“HelloService”找到了一个。 因此,打开文件bin/zss-service ,将“hello-word”替换为“person”,然后保存文件。 返回控制台并运行:

 $ bin/zss-service run Starting person: PID: ./log LOGS: ./log Started person daemon... 15-29-15 19:29:54 | INFO | ZSS::SERVICE - Starting SID: 'PERSON' ID: 'person#d3ca7e1f-e229-4502-ac2d-0c01d8c285f8' Env: 'development' Broker: 'tcp://127.0.0.1:7776'

而已。 您刚刚第一次启动了“Person”服务。 现在让我们测试一下。 打开bin/zss-client文件,将sid变量更改为“person”,并将客户端调用从hello_world()更改为get() 。 完成后,在新窗口中运行客户端:

 $ bin/zss-client /Users/francisco/.rvm/gems/ruby-2.1.2/gems/zss-0.3.4/lib/zss/client.rb:41:in `new': No people here (ZSS::Error) from /Users/francisco/.rvm/gems/ruby-2.1.2/gems/zss-0.3.4/lib/zss/client.rb:41:in `call' from /Users/francisco/.rvm/gems/ruby-2.1.2/gems/zss-0.3.4/lib/zss/client.rb:55:in `method_missing' from bin/zss-client:12:in `<main>'

如您所见,您已经捕获了ZSS::Error 。 这是因为当服务找不到人并且我们的服务数据库中还没有人时,我们会引发错误。

那么让我们处理这个错误。 打开zss-client并像这样编辑它:

 begin client = ZSS::Client.new(sid, config) p client.get() rescue ZSS::Client => e if e.code == 404 p e.message else raise e end end

现在我们在错误代码为 404 时打印错误消息,如果是不同的错误则引发错误。 让我们再次运行我们的客户端来看看它的实际效果:

 $ bin/zss-client "No people here"

优秀。 现在让我们将一些人添加到我们的表中,看看他们是否由服务返回给我们的客户。 为此,只需打开一个服务控制台:

 $ rake service:console

添加一些人:

 $ rake service:console [1] pry(main)> DAO::Person.create name: 'John' => #<DAO::Person:0x007fe51bbe9d00 id: 1, name: "John", created_at: 2015-12-16 13:22:37 UTC, updated_at: 2015-12-16 13:22:37 UTC> [2] pry(main)> DAO::Person.create name: 'Mary' => #<DAO::Person:0x007fe51c1dafe8 id: 2, name: "Mary", created_at: 2015-12-16 13:22:42 UTC, updated_at: 2015-12-16 13:22:42 UTC> [3] pry(main)> DAO::Person.create name: 'Francis' => #<DAO::Person:0x007fe51bc11698 id: 3, name: "Francis", created_at: 2015-12-16 13:22:53 UTC, updated_at: 2015-12-16 13:22:53 UTC> [4] pry(main)> exit

现在,再次运行您的客户端。

 $ bin/zss-client [{"id"=>1, "name"=>"John"}, {"id"=>2, "name"=>"Mary"}, {"id"=>3, "name"=>"Francis"}]

你有它。

最后的考虑

浏览本指南中提供的代码,您可能会认为有很多不必要的步骤,例如创建存储库或 DTO,您是对的。 拥有一个正常运行的“Person”服务所需要的只是你的服务类和你的 DAO,你可以直接从服务类调用它们。 不过,遵循本文中描述的模式是一种很好的做法,因为它允许您将服务逻辑与数据存储操作分开。 服务应该只关注它们的逻辑,而存储库应该处理与数据存储的所有交互。 DTO 确定服务的有效负载和序列化,而 DAO 只关心从存储中获取数据。 本指南中描述的约定和技术称为存储库模式,您可以在下图中查看。

存储库模式。最左边的框是“客户端业务逻辑”,它持续到中间框并从中查询,中间框是由“数据映射器”在“存储库”和“查询对象”上组成的堆栈,但用虚线分隔。持久化和查询都通过来自标有“业务实体”的外部框的连接进行侧连接。最后,最右边的“数据源”框有一个指向“数据映射器”的箭头和带有“查询对象”的双向箭头。

最后,我想请任何发现这有用的人为 SOA 服务套件做出贡献,以任何方式扩展和增强它。 欢迎您的所有分叉和拉取请求。

我希望这将帮助您开始使用微服务。 如果您想查看服务代码,可以在 GitHub 上找到完整版本。