本文概述
什么是微服务?
微服务是软件设计中的最新趋势之一, 在这种趋势中, 多个独立的服务之间相互通信并拥有自己的流程和资源。这种方法不同于典型的客户端-服务器应用程序设计。常见的客户端-服务器应用程序由一个或多个客户端, 一个包含所有域数据和逻辑的整体后端以及一个允许客户端访问后端及其功能的API组成。
![整体后端与微服务架构](http://www.srcmini02.com/wp-content/uploads/2020/04/toptal-blog-image-1455191196592-5c2d8d453ffc77ee5de598fd1c707b72.jpg)
微服务正在取代经典的单片后端服务器
在微服务架构中, 所描述的单片后端由一组分布式服务代替。这种设计可以更好地划分职责, 更轻松地维护, 为每种服务选择技术时具有更大的灵活性, 以及更轻松的可伸缩性和容错能力。同时, 复杂的分布式系统也面临着一系列挑战。它们有更大的机会来处理竞争条件, 并且它们更难调试, 因为问题不容易确定到单个服务, 而是分布在多个服务上。如果在构建这样的系统时没有努力遵循最佳实践, 你可能会发现自己被不知道如何扑灭的大火所包围。服务的有效负载合同必须格外小心, 因为一项服务的更改可能会影响其所有客户, 进而影响所有后端的服务套件。
所有这些注意事项都很重要, 但是假设你已经考虑了一下。现在, 你想要找到一种自行构建微服务后端的方法。因此, 让我们深入探讨这一点。
如何建立微服务架构
当前, 你可以通过多种方式设置微服务, 在本指南中, 我们将重点介绍代理架构。
代理架构
![代理架构](http://www.srcmini02.com/wp-content/uploads/2020/04/toptal-blog-image-1455191270658-dbb74315f95ddd25b4c1df724905f24a.jpg)
代理体系结构是使服务之间相互通信的方法之一
代理体系结构是使服务之间相互通信的方法之一。在其中, 所有服务都围绕着消息传递服务器, 代理, 并且所有服务都已连接到该服务器。服务将消息发送到代理, 代理然后知道代理需要转发这些消息的其他服务。这样, 服务不需要保留有关其他服务的信息。取而代之的是, 他们依靠代理来处理所有消息传递, 这使得它们可以被隔离并且仅专注于其特定域。代理还可以在接收方关闭时存储消息, 从而使发送方和接收方不会被迫同时打开, 从而实现更大的隔离。当然, 此解决方案存在缺陷, 因为代理程序可能很快成为瓶颈, 因为所有通信都必须经过它们, 并且它也可能成为后端的单点故障。但是, 有几种方法可以缓解这些问题。一种方法是让代理的多个实例并行运行, 这将允许更好的系统容错能力。另一种方法是使用其他体系结构。替代体系结构与我们将在本指南中实现的体系结构有所不同, 它不使用代理, 也不使用其他代理体系结构, 或者使用其他消息协议(例如HTTP)。
服务之间的沟通
在本指南中, 我们将使用ZeroMQ处理服务与代理之间的通信。
![ZeroMQ协议](http://www.srcmini02.com/wp-content/uploads/2020/04/toptal-blog-image-1455191297162-56519debeefb632ef1dc2caf56caa93f.jpg)
ZeroMQ提供了协议抽象层, 用于处理随机传输上的多部分异步消息。使用ZeroMQ在服务和代理之间进行消息传递的优点超出了本指南的范围, 因此我们不会在此处进行详细介绍, 但是如果你想了解更多有关它们的信息, 请查看Quora文章。如果你有兴趣寻找使服务彼此对话的其他方法, 建议你阅读”代理与无代理”一文, 以了解还可以实现什么。
构建微服务套件
本文将指导你完成创建微服务套件所需的所有步骤。我们的系统将由代理和服务组成。我们还将使用一个小的客户端脚本来测试对服务套件的调用, 但是请记住, 客户端代码可以在任何地方轻松使用。
因此, 让我们开始构建。
入门
首先, 请确保你具备运行代理和服务所需的一切。首先, 首先在计算机上下载并安装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参数定义代理的后端和前端的ip:port地址。后端地址是代理从服务接收请求并对其进行回复的位置, 前端地址是其接收并发送给服务客户端的位置。你还可以通过更改log.consolePlugin.level来设置日志记录级别。可能的值为trace, debug, info, warn和error`, 它们确定代理输出的日志记录信息量。
运行服务
![运行服务](http://www.srcmini02.com/wp-content/uploads/2020/04/toptal-blog-image-1455201470878-6b7972c2c7013498039bc7b91ff52ba5.jpg)
建立代理之后, 就该开发你的第一个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”服务。
代码结构
让我们开始看一下项目的代码树。看起来像这样:
![代码结构](http://www.srcmini02.com/wp-content/uploads/2020/04/toptal-blog-image-1455191353197-726278f8bf4d289cc0b6993143b60cc0.jpg)
- 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, 则lib / daos目录是你存储ActiveModel对象的地方, 或者你最终可能会创建的任何其他数据访问对象(例如Sequel模型)。
- 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
此设置仅设置代理的后端和前端地址以及日志记录级别。
到目前为止, 如果所有这些听起来令人困惑, 请不要担心, 因为随着我们的前进, 它将变得更加清晰。
“人”服务
因此, 让我们继续”个人”服务。让我们开始配置数据库连接。打开文件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
有你的模特。现在, 你需要为”人员”创建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_dao和self.to_dto。如果你不希望这样做, 则可以改为实现self.map并重写Mapper :: Base.map, 该Mapper :: Base.map调用to_dao或to_dto, 具体取决于它接收的属性是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
“人员”服务在其初始化程序中初始化存储库。 “个人”服务的所有公共实例方法都具有有效负载和标头, 如果不需要它们, 可以将其省略。两者都是Hashie :: Mash实例, 它们存储发送到端点的变量(作为属性或标头), 并且它们的响应模仿HTTP响应, 因为每个响应都有一个状态码, 客户端可以使用该状态码来查找发送到端点的请求的结果。服务以及服务的响应有效负载。响应代码与你从HTTP服务器获得的期望代码相同。例如, 成功的请求将返回200状态代码以及响应有效负载。如果发生某些服务错误, 则状态码将为500, 如果发送到服务器的参数有问题, 则状态码将为400。服务可以使用大多数HTTP状态码及其有效载荷进行回复。因此, 例如, 如果你希望你的服务告知客户何时不允许其访问某个端点, 则可以通过响应403代码来实现。如果你回顾以上我们的服务代码, 则可以看到响应代码的另一个示例。在get端点中, 当找不到人时, 我们将返回状态代码404以及可选的” No people here”消息, 就像HTTP服务器将在没有可用资源的情况下返回404一样。如果存储库确实确实返回了人员, 则该服务将DTO序列化并将其返回给客户端。每个DTO都有一个默认的序列化程序, 该序列化程序返回一个JSON对象, 其键和相应的值在DTO定义中定义为attr_reader或attr_accessible。当然, 你可以通过在DTO类中定义序列化方法来覆盖序列化器。
现在我们已经定义了服务, 我们需要注册它。这是最后一步。打开文件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
现在, 只有最后一步, 我们才能启动”人员”服务;我们需要为其创建一个可执行脚本。我们已经为” 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'
而已。你是第一次启动”人员”服务。现在让我们对其进行测试。打开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, 这是正确的。你需要具有正常运行的”人员”服务的全部是服务类和DAO, 你可以直接从服务类中调用它们。但是, 遵循本文中描述的模式是一种很好的做法, 因为它使你可以将服务逻辑与数据存储操作分开。服务应仅专注于其逻辑, 存储库应处理与数据存储的所有交互。 DTO确定服务的有效负载和序列化, 而DAO仅关注从存储中获取数据。本指南中描述的约定和技术称为存储库模式, 你可以在下图中检出。
![储存库模式](http://www.srcmini02.com/wp-content/uploads/2020/04/toptal-blog-image-1455191384034-d934f9fbefc0e4bd05b9ddf9f63f6ef4.jpg)
最后, 我想请任何发现这一点有用的人以任何方式扩展和增强SOA服务套件。欢迎你提出所有叉子和拉动要求。
我希望这将帮助你开始使用微服务。如果你想签出服务代码, 可以在GitHub上找到完整版本。
评论前必须登录!
注册