作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Francisco Temudo的头像

Francisco Temudo

弗朗西斯科15年以上的软件开发经验(包括PHP) & Java)最近开始专注于全栈web开发(Ruby) & JS.)

Expertise

Previously At

Siemens
Share

What Are Microservices?

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

微服务正在取代传统的单片后端服务器

在微服务架构中, 所描述的单片后端被一套分布式服务取代. 这种设计允许更好地分离责任, easier maintenance, 在为每个服务选择技术方面具有更大的灵活性, 更容易扩展和容错. 与此同时,复杂的分布式系统也面临着一系列挑战. 他们更有可能不得不应对竞争条件, 而且它们更难调试,因为问题不容易定位到单个服务上, 而是分布在许多地方. 如果在构建这样的系统时没有努力遵循最佳实践, 你可能会发现自己被不知道如何扑灭的火焰包围着. 必须特别注意服务的有效负载契约, 因为一个服务的变化可能会影响到它的所有客户端, 以及所有后端服务套件.

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

如何建立一个微服务架构

目前有很多方法可以设置微服务, 在本指南中,我们将重点关注代理体系结构.

A Broker Architecture

Broker architecture, 中间有一个经纪人(B), 以及围绕它的四个微服务, call them N, S, E, W.  请求/响应路径从体系结构外部的输入开始, 然后沿着路径N, B, E, B, S, B, W, B, E, B, N, 最后作为输出退出.

代理体系结构是让服务之间进行通信的一种方式

代理体系结构是让服务之间进行通信的一种方式. 在其中,所有服务都围绕消息传递服务器和代理,并且所有服务都连接到该服务器. 服务向代理发送消息, 然后谁知道他需要哪个或哪些其他服务来转发这些消息. 这样,服务就不需要保留其他服务的信息. Instead, 它们依赖代理来处理所有的消息传递, 这让他们被隔离,只专注于他们的特定领域. 代理也可以在其接收器关闭时存储消息, 允许发送方和接收方不被强制同时启动, 从而导致更大程度的孤立. Of course, 这种解决方案也有缺点,因为所有通信都必须经过代理,因此代理可能很快成为瓶颈, 它也可能成为后端单点故障. 然而,有一些方法可以缓解这些问题. 一种方法是让代理的多个实例并行运行, 哪一种系统容错性更好. 另一种方法是使用 other architectures. 替代体系结构与我们将在本指南中实现的体系结构的不同之处在于不使用代理, 或者使用不同的代理体系结构, 或者使用不同的消息传递协议(如HTTP).

服务间通信

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

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

ZeroMQ提供了一个协议抽象层,用于处理随机传输的多部分异步消息. 使用ZeroMQ在服务和代理之间传递消息的优势不在本指南的讨论范围之内, 我们就不一一讲了, 但如果你想了解更多, check out following Quora article. 如果你有兴趣找到其他方法让你的服务相互通信, 我建议你看一下 Broker vs. Brokerless article 看看还能取得什么成就.

构建微服务套件

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

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

Getting Started

首先,让我们确保您拥有运行代理和服务所需的一切. 首先,从下载和安装开始 Node.js, ZeroMQ and Git on your machine. If you are using OSX, 他们每个人都有自己的软件包, 而且大多数Linux发行版也有各自的软件包, 所以你应该对这个没有问题. Windows用户可以使用上面提供的下载链接.

Running the Broker

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

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

$ git clone git@github.com:达达/ zmq-broker-bootstrap.git

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

zmq-broker-bootstrap

现在安装包依赖项:

$ npm install

经纪人现在已经准备好了. 要运行broker,请运行以下命令:

$ bin/zss-broker run

中找到每个环境的配置文件 config/ directory. 这是默认的开发配置:

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

The backend parameter defines the ip:port 代理的后端和前端地址. 后端地址是代理接收来自服务的请求和回复服务的位置, 前端地址是它接收和发送服务客户端的位置. 也可以通过更改日志级别来设置日志级别 log.consolePlugin.level. Possible values are trace, debug, info, warn and error,它们决定了代理进程将输出的日志信息的数量.

Running the Service

在您建立了您的经纪人之后,是时候开发您的第一个 Ruby microservice. 首先打开一个新的控制台窗口. 然后,创建一个存储服务的目录,然后进入该目录. 在本指南中,我们将使用 ZMQ SOA套件的Ruby客户端和服务. There is a 引导“Hello world”服务 可用,所以让我们使用它来运行我们的第一个微服务.

转到services目录,克隆引导库:

$ git clone git@github.com:达达/ zmq-service-suite-ruby-bootstrap.git

进入新建目录:

zmq-service-suite-ruby-bootstrap

现在安装所有依赖项:

$ bundle install

启动服务,执行如下命令:

$ bin/zss-service run

Great. 您已经启动并运行了第一个服务.

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

2015-12-15 16:45:05 | INFO | BROKER -异步BROKER正在等待消息...

2015-12-15 16:45:14 | DEBUG | BACKEND - received from: hello-word#aaa65374-8585-410a- a4d -c8a5b024553b rid: 76f50741-913a-43b9-94b0-36d8f7bd75b1
2015-12-15 16:45:14 | DEBUG |后端-路由从:hello-word#aaa65374-8585-410a- a4d -c8a5b024553b rid: 76f50741-913a-43b9-94b0-36d8f7bd75b1到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 -回复:hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 76f50741-913a-43b9-94b0-36d8f7bd75b1 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 |后端-路由从:hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 3b3a0416-73fa-4fd2-9306-dad18bc0502a到SMI.HEARTBEAT request...
2015-12-15 16:45:15 | DEBUG | BACKEND -回复:hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: 3b3a0416-73fa-4fd2-9306-dad18bc0502a 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 |后端-路由从:hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: b3044c24-c823-4394-8204-1e872f30e909到SMI.HEARTBEAT request...
2015-12-15 16:45:16 | DEBUG | BACKEND -回复:hello-word#aaa65374-8585-410a-a41d-c8a5b024553b rid: b3044c24-c823-4394-8204-1e872f30e909 status: 200

该日志意味着代理已经确认了新服务的存在,并且正在从中接收心跳消息. Every second, 服务向代理发送心跳消息, 因此它知道服务实例已启动.

从服务中消费

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

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

$ bin/zss-client

你应该看到这样的内容:

15-49-15 16:49:54 | INFO | ZSS::CLIENT - Request 90a88081-3485-45b6-91b3-b0609d64592a发送给HELLO- word:*#HELLO/WORLD with 1.0s timeout
15-49-15 16:49:54 | INFO | ZSS::CLIENT -收到响应90a88081-3485-45b6-91b3-b0609d64592a,状态为200
"Hello World"

如果你去到你的服务正在运行的控制台窗口,你应该看到这个:

启动了hello-word守护进程...
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 -处理HELLO- word:*#HELLO/WORLD的请求
15:49 -15 16:49:54 | INFO | ZSS::SERVICE - Reply with status: 200

Good. 您刚刚启动并使用了“Hello World”微服务. 然而,这并不是我们的初衷. 我们想要构建我们的服务. Let’s get to it, then.

Building Your Service

首先,让我们停止“Hello World”服务. 转到服务的控制台窗口并按下 Ctrl+C to stop the service. 接下来,我们需要将“Hello World”服务转换为“Person”服务.

Code Structure

让我们先看一下项目的代码树. It looks like this:

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

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

The application.yml file from the config 目录看起来像这样:

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” Service

那么,让我们继续我们的“Person”服务. 让我们从配置数据库连接开始. Open the file 配置/初始化/ 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
    数据库:zss-tutorial-development

现在已经添加了数据库配置,接下来必须创建数据库. At this time, 除非您使用默认的PostgreSQL数据库,否则没有办法自动执行此操作, 在这种情况下,你可以简单地运行:

$ rake db:create

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

Next is the migration. 为此,只需创建该文件 db/migrate called 000_creates_persons.rb:

$ 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

Next, run it:

$ rake db:migrate
= = 0 CreatesPersons:迁移  ================================================
——create_table(人):
警告:调用' #timestamp '时没有为' null '指定选项. 在Rails 5中,此行为将变为' null: false '。. 您应该手动指定' null: true ',以防止更改现有迁移的行为. (从block in change在/Users/francisco/Code/microservices-tutorial/db/migrate/000_creates_persons中调用.rb:6)
   -> 0.0012s
== 0 CreatesPersons: migrate (0 ..0013s) =======================================

现在我们已经创建了表,让我们为它创建一个模型. Create the file lib/daos/person.rb:

$ touch lib/daos/person.rb

Edit it like this:

module DAO
  class Person < ActiveRecord::Base
  end
end

There is your model. 现在需要为“Person”创建DTO模型,以便将其返回给客户机. Create the file lib/dtos/person.rb:

$ touch lib/dtos/person.rb

Edit it like this:

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

接下来,您必须创建一个Mapper来将“Person”DAO转换为“Person”DTO. Create the file lib /仓库/映射器/人.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

Here, Mapper::Base 要求你实现 self.to_dao and self.to_dto. 如果您不希望这样做,您可以实现 self.map 而不是重写 Mapper::Base.map which calls to_dao or to_dto,这取决于它接收的属性是DAO还是DTO.

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

然后让我们创建存储库. Create the file lib/repositories/person.rb:

$ touch lib/dtos/person.rb

Edit it like this:

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

Edit it like this:

class PersonService < BaseService

  . attr_reader: person_repo

  def initialize
    @person_repo =存储库::个人.new
  end

  defget payload, headers
    persons = person_repo.get()
    if persons.empty?
      raise ZSS::Error.new(404,“这里没有人”)
    else
      persons.map &:serialize
    end
  end

end

“Person”服务在其初始化器中初始化存储库. “Person”服务的所有公共实例方法都有有效负载和头,如果不需要,可以忽略它们. Both are Hashie::Mash 实例,它们存储发送到端点的变量, 作为属性或标头, 它们的响应模仿HTTP响应,因为每个响应都有一个状态码,客户端可以使用它来查找发送到服务的请求的结果, 以及服务的响应有效负载. 响应代码与您期望的HTTP服务器相同. 例如,一个成功的请求将返回一个200状态码以及响应有效负载. 如果出现服务错误, 那么状态码将是500, 如果发送给服务器的参数有问题, 状态码将是400. 该服务可以使用大多数HTTP状态码及其有效负载进行应答. So, 例如,如果您希望您的服务告诉其客户端何时不允许访问某个端点, 您可以通过响应403代码来实现这一点. 如果您回头看看上面的服务代码,您可以看到响应代码的另一个示例. In the get endpoint, 当没有找到人员时,我们将返回状态码404以及可选的“No people here”消息, 就像HTTP服务器在没有可用资源时返回404一样. 如果存储库确实返回人, 然后服务序列化dto并将它们返回给客户端. 每个DTO都有一个默认的序列化器,它返回一个JSON对象,其中键和相应的值定义为两者之一 attr_reader or attr_accessible in the DTO definition. 当然,您可以通过在DTO类中定义序列化方法来重写序列化器.

现在我们已经定义了一个服务,我们需要注册它. This is the final step. Open the file 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:)

      return service
    end

  end
end

正如你可能注意到的,有一个小小的变化 add_route call. 我们删除了字符串" HELLO/WORLD ". 这是因为只有当服务谓词与实现它的方法不匹配时才需要字符串. 在我们的示例中,当使用GET谓词调用person服务时,要调用的方法是 get,所以我们可以省略字符串.

The 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:)

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

return service

Now, there is only one last step before we can launch our “Person” service; we need to create an executable script for it. 我们已经有了一个HelloService. So, open the file bin/zss-service,将“hello-word”替换为“person”,并保存文件. 回到控制台并运行:

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

That’s it. 您刚刚第一次启动“个人”服务. Now let’s test it. Open the bin/zss-client file, change the sid 变量为“person”,并更改来自的客户端调用 hello_world() to 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::错误)
	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:“method_missing”
	from bin/zss-client:12:in `
'

如你所见,你染上了 ZSS::Error. 这是因为当服务没有找到人员并且服务的数据库中还没有人员时,我们会引发一个错误.

让我们来处理这个错误. Open zss-client and edit it like this:

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"

Excellent. 现在让我们向表中添加一些人,看看服务是否将他们返回给我们的客户机. 要做到这一点,只需打开一个服务控制台:

$ rake service:console

Add some people:

$ rake service:console
[1] pry(main)> DAO::Person.create name: 'John'
=> #
[2] pry(main)> DAO::Person.create name: 'Mary'
=> #
[3] pry(main)> DAO::Person.create name: 'Francis'
=> #
[4] pry(main)> exit

现在,再次运行客户端.

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

There you have it.

Final Considerations

浏览本指南中提供的代码, 你可能会认为有很多步骤是不必要的, 例如创建存储库或dto, and you’d be right. 要拥有一个有效的“Person”服务,您所需要的只是您的服务类和DAO, 你可以直接从服务类调用它. Nevertheless, 遵循本文中描述的模式是一种很好的实践, 因为它允许您将服务逻辑与数据存储操作分离. 服务应该只关注它们的逻辑, 存储库应该处理与数据存储的所有交互. dto决定服务的有效负载和序列化, 而dao只关心从存储中获取数据. 本指南中描述的约定和技术被称为存储库模式, 你可以看看下面的图片.

The repository pattern. 最左边的方框是“客户业务逻辑”,,并从中间框中查询, 哪个堆栈由“数据映射器”、“存储库”和“查询对象”组成,但用虚线分隔. 持久化和查询都是通过标记为“业务实体”的外部框的连接进行侧连接的." Finally, the rightmost box, "data source,有一个指向“数据映射器”的箭头,和带有“查询对象”的双向箭头."

最后,我想请任何觉得这个有用的人为 SOA service suite,以任何方式扩大和加强它. 欢迎您的所有分叉和拉取请求.

我希望这将帮助您开始使用微服务. 如果您想检查服务代码,请单击a 完整版本可在GitHub上获得.

关于总博客的进一步阅读:

就这一主题咨询作者或专家.
Schedule a call
Francisco Temudo的头像
Francisco Temudo

Located in Lisbon, Portugal

Member since December 7, 2015

About the author

弗朗西斯科15年以上的软件开发经验(包括PHP) & Java)最近开始专注于全栈web开发(Ruby) & JS.)

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

Siemens

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.