在前面的章节中,我们简要介绍了我们将构建的应用程序。现在是深入了解整个项目的时候了。
微服务结构我们想构建一个地理定位应用程序,我们选择像游戏一样创建它,这样它更有趣,更容易理解。您可以随意将该示例应用于任何其他想法,例如,嵌入地理本地化的旅游应用程序。
我们的游戏将使用地理定位来发现世界各地的不同秘密(如果你想要一张更小的地图,也可以在特定的地理区域)。后端系统将生成新的秘密,并将它们随机放置在我们的地图上,允许用户探索他们的环境以找到它们。作为我们游戏的玩家,您将收集不同的秘密并将其存储在您的钱包中,在这里您将找到关于每个秘密的更多信息。
为了让我们的游戏更有趣,我们将有一个战斗引擎。当你发现我们的秘密世界时,你可以与其他玩家战斗,窃取他/她的秘密。战斗引擎将是一个简单的-只要掷一个骰子,最高分数赢得战斗。
没有其他服务,例如用户/玩家管理系统等,此类项目无法完成。
作为开发人员,您从一个规范开始,并尝试将其分解为更小的部分。从我们的小描述中,我们可以开始定义我们的微服务及其职责,如下所示:
请注意,我们不仅为我们的游戏创建服务,还将使用其他支持服务使一切顺利进行。
下图描述了不同服务之间的通信路径。每个服务都可以与其他服务对话,这样我们就可以组合更大、更复杂的任务。下图描述了我们的微服务之间的连接:
设计模式是解决实际应用程序开发中经常出现的问题的可重用解决方案。这些解决方案有着行之有效的成功记录,并且被广泛使用,因此将它们添加到我们的项目中将使我们的软件更加稳定和可靠。
我们正在构建一个微服务应用程序,因为我们希望它尽可能稳定可靠,所以我们将使用一些微服务模式,例如:API 网关、服务发现和注册,以及共享数据库或每个服务的数据库。
我们将有一个前端供用户注册并与我们的应用程序交互,它将是我们微服务的主要客户端。此外,我们还计划在未来推出本地移动应用程序。让不同的客户使用我们的应用程序会让我们头疼,因为他们对我们的微服务的使用可能会非常不同。
为了统一任何客户机使用我们的微服务的方式,我们将添加一个额外的层——API 网关。此 API 网关成为任何客户端(例如浏览器和本机应用程序)的单一入口点。在这一层中,我们的网关可以通过两种方式处理请求:一些请求被简单地代理,另一些请求被分散到多个服务。我们甚至可以将此 API 网关用作安全层,检查客户端的每个请求是否允许使用我们的微服务:
资产的请求
拥有 API 网关有许多好处,其中我们可以强调以下几点:
我们的服务需要呼叫其他服务。在单体应用程序上,解决方案非常简单——我们可以调用方法或使用过程调用。我们正在构建一个运行在容器中的 microservices 应用程序,因此没有简单的方法知道某些服务的位置。我们的容器基础设施非常灵活,我们需要构建一个服务发现系统。
我们的每项服务都将通过查询我们的服务注册中心(我们使用 Consor 存储所有服务信息的地方)获得所有其他链接服务的位置。我们的注册表将知道每个服务实例的位置。下图显示了自动发现模式:
为此,我们将使用不同的工具:
应用程序以某种方式生成我们需要存储的数据。在单体应用程序中,毫无疑问,所有数据都存储在同一个位置。问题是当您处理 microservice 应用程序时,没有简单的响应。每个应用领域都是唯一的,因此没有解决问题的经验法则;您需要分析您的数据,并决定是将所有数据存储在共享存储中,还是每个服务都有自己的数据存储,还是混合存储。
在我们的示例应用程序中,我们将介绍这两种方法,但让我们解释每种方法的好处。
在这种方法中,我们将每个微服务的持久数据保持为该服务的私有数据,这些数据只能通过其 API 访问,并具有许多好处:
当然,此解决方案有一些缺点,最显著的问题是难以连接不同服务之间共享的数据。
这种方法的工作原理类似于单体应用程序中的数据库——所有数据都存储在同一个引擎中。主要的好处是将所有东西放在一个地方非常简单。
这种简单性有一些缺点;我们强调以下几点:
作为开发人员,您的工作是为您需要解决的每个问题找到最佳解决方案。您需要决定如何存储应用程序数据,始终牢记每个选项的优点和缺点。
宁静的习俗Representational State Transfer是用于与 API 通信的方法的名称。顾名思义,它是无国籍;换句话说,这些服务不会保持数据传输,因此,如果您调用发送数据的微服务(例如,用户名和密码),微服务在下次调用时将不会记住数据。状态由客户端保存,因此客户端需要在每次调用微服务时发送状态。
一个很好的例子是当用户登录并且用户能够调用特定方法时,因此每次都需要发送用户凭据(用户名和密码或令牌)。
RESTAPI 的概念不再是服务;相反,它就像一个可以通过标识符(URI)进行通信的资源容器。
在以下几行中,我们将定义一些关于 API 的有趣约定。了解这些技巧很重要,因为在使用 API 时,您应该按照自己的意愿进行操作。换句话说,编写 API 就像是为自己写一本书——像您这样的开发人员会阅读它,所以完美的功能不是唯一重要的事情,友好的交谈方式也很重要。
如果您遵循一些惯例以使消费者满意,那么创建 RESTful API 对您和消费者来说将更容易。我在 RESTful API 上使用了一些建议,结果非常好。它们有助于组织应用程序及其未来的维护需求。此外,当您的 API 消费者喜欢使用您的应用程序时,他们会感谢您。
RESTful API 中的安全性很重要,但是如果您的 API 将被您不认识的人使用,换句话说,如果它将对每个人都可用,那么它就特别重要。
一点一点地,PHP 和微服务的更多标准正在出现。正如我们在上一章中看到的,有一些小组,比如 PHP-FIG,试图建立它们。以下是使 API 更标准的一些技巧:
POST
和GET
请求,因此最好允许X-HTTP-Method-Override
头覆盖PUT
、PATCH
和DELETE
。API 的使用者是最重要的,因此您需要提供有用、有用和友好的方法,使开发人员的工作更轻松。发展思考这些问题的方法:
POST
、PATCH
和PUT
请求中返回有用的内容。避免开发人员多次调用 API 以获取所需数据。还有很多技巧,但是这些技巧对于 RESTful 约定的第一种方法已经足够了。在接下来的章节中,我们将看到这些 RESTful 约定的示例,并解释如何更好地使用它们。
缓存策略菲尔·卡尔顿
“计算机科学中只有两个难题:缓存失效和命名问题。”
缓存是一个临时存储数据的组件,以便将来对该数据的请求可以更快地得到处理。这种临时存储用于缩短数据访问时间、减少延迟和改进 I/O。我们可以在微服务体系结构中使用不同类型的缓存来提高总体性能。让我们来看看这个问题。
为了维护缓存,我们有一些算法,这些算法提供了指令,告诉我们应该如何维护缓存。最常见的算法如下所示:
开始考虑缓存策略的最佳时机是在设计应用程序所需的每个微服务时。每次服务返回数据时,您都需要问自己一些问题:
您可以在应用程序中的任何位置添加缓存层。例如,如果使用 Percona/MySQL/MariaDB 作为数据存储,则可以正确启用和设置查询缓存。这个小小的设置将提升您的数据库。
即使在编写代码时,也需要考虑缓存。您可以对对象和数据执行延迟加载,或者构建自定义缓存层以提高总体性能。假设您正在从外部存储器请求和处理数据,请求的数据可以在同一执行中重复多次。执行类似于以下代码的操作将减少对外部存储的调用:
class MyClass
{
protected $myCache = [];
public function getMyDataById($id)
{
if (empty($this->myCache[$id])) {
$externalData = $this->getExternalData($id);
if ($externalData !== false) {
$this->myCache[$id] = $externalData;
}
}
return $this->myCache[$id];
}
}
请注意,我们的示例省略了大块代码,例如名称空间或其他函数。我们只想为您提供总体思路,以便您可以创建自己的代码。
在这种情况下,每当我们使用 ID 作为密钥标识符向外部存储器发出请求时,我们都会将数据存储在$myCache
变量中。下次我们请求一个与前一个 ID 相同的元素时,我们将从$myCache
获取该元素,而不是从外部存储器请求数据。请注意,只有在相同的 PHP 执行中可以重用数据时,此策略才会成功。
在 PHP 中,您可以访问最流行的缓存服务器,如memcached和Redis;它们都以键值格式存储数据。访问这些功能强大的工具将使我们能够提高微服务应用程序的性能。
让我们使用Redis
作为缓存来重建前面的示例。在下面的代码中,我们假设您的环境中有一个可用的Redis
库(例如,phpredis)和一个正在运行的Redis
服务器:
class MyClass
{
protected $myCache = null;
public function __construct()
{
$this->myCache = new Redis();
$this->myCache->connect('127.0.0.1', 6379);
}
public function getMyDataById($id)
{
$externalData = $this->myCache->get($id);
if ($externalData === false) {
$externalData = $this->getExternalData($id);
if ($externalData !== false) {
$this->myCache->set($id, $externalData);
}
}
return $externalData;
}
}
在这里,我们首先连接到 Redis 服务器,并调整了getMyDataById
功能以使用我们新的 Redis 实例。这个例子可能更复杂,例如,通过添加依赖注入和在缓存中存储 JSON,以及其他无限选项。使用缓存引擎而不是构建自己的缓存引擎的好处之一是,所有这些引擎都具有许多非常酷和有用的功能。假设您只想将数据保存在缓存中 10 秒钟;使用 Redis 很容易做到这一点——只需使用$this->myCache->set($id, $externalData, 10)
更改 set 调用,十秒钟后,您的记录将从缓存中删除。
比向缓存引擎添加数据更重要的事情是使存储的数据无效或删除数据。在某些情况下,可以使用旧数据,但在其他情况下,使用旧数据可能会导致问题。如果不添加 TTL 以使数据自动过期,请确保在需要时有方法删除或使数据无效。
记住这个例子和前一个例子,我们将在我们的微服务应用程序中使用这两种策略。
作为开发人员,您不需要绑定到特定的缓存引擎;包装它,创建一个抽象,并使用该抽象,以便您可以在任何时候更改底层引擎,而无需更改所有代码。
这种通用的缓存策略可以在应用程序的任何范围内使用——您可以在微服务的代码中甚至在微服务之间使用它。在我们的应用示例中,我们将处理机密;它们的数据不会经常更改,因此我们可以在第一次访问它们时将所有这些信息存储在缓存层(Redis)中。
未来的申请将从我们的缓存层获取机密数据,而不是从我们的数据存储中获取,从而提高我们应用程序的性能。请注意,检索和存储机密数据的服务是负责管理此缓存的服务。
让我们看看我们将在 microservices 应用程序中使用的一些其他缓存策略。
此策略使用一些 HTTP 头来确定浏览器是否可以使用响应的本地副本,还是需要从源服务器请求新副本。此缓存策略在应用程序外部进行管理,因此您对其没有太多控制权。
我们可以使用的一些 HTTP 头如下所示:
以下是可用的选项:
在我们的示例应用程序中,我们将拥有一个可以通过任何 web 浏览器访问的公共 UI。使用正确的 HTTP 头,我们可以避免一次又一次地请求相同的资产。例如,我们的 CSS 和 Javascript 文件不会经常更改,因此我们可以在将来设置到期日期,浏览器将保留它们的副本;未来的请求将使用本地副本,而不是请求新副本。
您可以从 NGINX 的浏览器访问时间开始,向所有.jpg
、.jpeg
、.png
、.gif
、.ico
、.css
和.js
文件添加一个 expires 头,日期为未来 123 天,规则很简单:
location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {
expires 123d;
}
一些静态元素对缓存非常友好,其中您可以缓存以下元素:
这些元素往往很少更改,因此可以缓存更长的时间。为了减轻服务器的负载,您可以使用内容交付网络(CDN,以便这些不经常更改的文件可以由这些外部服务器提供服务。
基本上,CDN 有两种类型:
在设计 microservice 应用程序时,您需要记住这一点,因为您可能允许用户上载一些文件。
你打算把这些文件存放在哪里?如果它们是公开的,为什么不使用 CDN 来交付这些文件,而不是从您的服务器中删除它们呢。
一些著名的 CDN 包括 CloudFlare、Amazon CloudFront 和 Fastly 等。它们的共同点是,它们在世界各地都有多个数据中心,允许它们尝试从最近的服务器向您提供文件副本。
通过将 HTTP 与静态文件缓存策略相结合,可以将服务器上的资产请求减少到最低限度。我们将不解释其他缓存策略,例如全页缓存;有了我们介绍的内容,您就可以开始构建成功的微服务应用程序了。
领域驱动设计领域驱动设计(DDD从此)是一种在需求复杂时进行开发的方法。这个概念并不新鲜;它是由 Eric Evans 在 2004 年的同名书中创建的,但现在它已成为主流,因为微服务在开发人员中很受欢迎,在大型项目中也很常见。
这是因为微服务概念(关于软件体系结构,将每个功能划分为服务)和 DDD 概念(关于有界上下文)之间具有很强的兼容性。
在了解在我们的微服务项目中在何处以及如何使用 DDD 之前,有必要了解 DDD 是什么以及它是如何工作的,因此,让我向您介绍主要概念,作为此方法的总结。
Evans 介绍了了解领域驱动设计工作原理所需的一些概念:
软件领域与技术术语、编程或计算机无关。在大多数项目中,最具挑战性的部分是理解业务领域,因此 DDD 建议使用模型领域;这是一种抽象、有序和选择性的知识,以图表、代码或文字形式再现。
模型域就像构建具有复杂功能的项目的路线图,需要遵循五个步骤来实现它。这五个步骤需要得到开发团队和领域专家的同意:
在域模型正确之前,此过程将包含所需的所有迭代:
模型、代码和设计必须一起发展和成长。它们不能完全不同步。如果一个概念在模型上更新了,它也应该在代码和设计上自动更新,其余的也一样。
模型是一个抽象系统,它描述一个领域的选择性概念,可以用来解决与该领域相关的问题。如果有一部分模型没有反映在代码中,则应将其删除。
最后,域模型是项目中公共语言的基础。DDD 中的这种通用语言称为泛在语言,它应该具有以下特性:
项目的所有成员(包括开发人员和领域专家)都应该使用通用语言,因此开发人员应该能够描述所有任务和功能。
在团队之间的所有讨论中,如会议、图表或文档中,使用这种语言是绝对必要的,但这种语言并不是在流程的第一次迭代中诞生的,这意味着需要多次迭代重构才能同步模型、语言和代码。例如,如果开发人员发现域中的某个类应该重命名,那么如果不重构域模型和通用语言上的名称,他们就无法重构该类。
无处不在的语言、领域模型和代码应该作为单个知识块一起进化。
DDD 的概念存在争议。Eric Evans 说,领域专家有必要使用与团队相同的语言,但有些人不喜欢这种想法。通常,领域专家不了解面向对象的概念或微服务,因为它们对于非开发人员来说过于抽象。无论如何,DDD 说如果领域专家不理解领域模型,那是因为它有问题。
领域模型中有图表,但 Evans 建议也使用文本,因为图表不能正确解释概念。此外,图表应该是肤浅的;如果你想看到更多的细节,你有它的代码。
某些项目受域模型和代码之间的连接的影响。这是因为在分析和设计之间存在一个划分。分析员制作独立于设计的模型,开发人员无法开发功能,因为缺少一些信息。此外,他们不能与领域专家交谈。开发团队将不会遵循该模型,最终,域模型将不会更新,也不会工作。因此,本项目不符合要求。
综上所述,DDD 将软件开发作为一个迭代过程来实现,该过程将模型、设计和代码作为一个块中的单个任务进行细化。
正如我们前面所说,DDD 完美地满足了微服务的需求。微服务出现了一个常见问题,因为它们具有分散的数据管理;这有好处,但有时可能会有问题。
两种服务之间的概念模型将是不同的,这可能会给大型公司带来问题。例如,用户可以根据服务的不同而有所不同,关于用户的每个服务的属性可以不同,并且属性语义也可以不同。
在一家大公司中,当应用程序不断发展并进行了多年的更新时,情况就更加复杂了。对于用户,每个服务都可以有不同的属性,通常情况下,它们不匹配。所以,解决这个问题的一个好方法是使用 DDD。
正如微服务所做的那样,DDD 将一个复杂的域划分为不同的上下文,在它们之间建立关系,并要求所有成员协作以获得特定域和有界上下文中的通用语言,迭代此过程,直到他们获得有关问题的实际概念。
Evans 建议将每个微服务设计为 DDD 绑定的上下文,以便为系统内的微服务提供逻辑边界。每一个微服务(或在其上工作的团队)都将负责系统的这一部分,它将提供更清晰和可维护的代码。
Michael Plöd 给出了更多关于 DDD 如何帮助微服务的想法。关于构建微服务,有四个重要方面:
总而言之,微服务和 DDD 非常匹配,但有必要拥有更大的范围,理解更多的边界上下文。
事件驱动架构事件驱动架构(EDA)是一种应用程序架构模式,遵循事件的产生、检测、消耗和反应的技巧。
可以将事件描述为状态变化。例如,如果门已关闭,但有人打开了它,则门的状态将从关闭变为打开。开门服务必须像事件一样进行更改,其他服务可以知道该事件。
事件通知是异步生成、发布、检测或使用的消息,是事件更改的状态。重要的是要理解,事件不会在应用程序中移动,它只是发生而已。术语事件有点争议,因为它通常是指消息事件通知而不是事件,因此了解事件和事件通知之间的区别很重要。
这种模式通常用于基于组件或微服务的应用程序中,因为它们可以通过应用程序的设计和实现来应用。由事件驱动的应用程序具有事件创建者和事件使用者或接收器(一旦事件可用,他们就必须执行操作)。
事件创建者是事件的制作人;它只知道事件已经发生,其他什么都不知道。然后我们有事件消费者,他们是负责知道事件被触发的实体。消费者参与处理或更改事件。
事件消费者订阅了某种中间件事件管理器,该管理器在收到创建者事件发出的事件通知后,立即将事件转发给注册消费者,供其使用。
围绕体系结构(如 EDA)将应用程序开发为微服务,可以使这些应用程序的构建方式更易于响应,因为 EDA 应用程序在设计上可以在不可预测的异步环境中运行。
使用 EDA 的优点如下:
在大型项目中,微服务通常用于将其服务划分为较小的服务。因此,在他们之间进行良好且有组织的沟通是非常重要的。事件驱动体系结构可用于解决微服务之间通信的常见问题。
在基于微服务的项目中,通常每个微服务都使用 HTTP 请求相互通信。这有一些问题,我们现在将解释。
在我们的Finding secrets
项目中,有一个为用户创建事件的功能。创建新事件时,需要将事件名称和事件表单中附加的图像发送到服务,以便根据接收到的数据创建视频。视频生成后,将更新事件并通过电子邮件发送给用户。
如果我们对每个服务发出 HTTP 请求,问题是所有服务都需要了解其他服务。例如,生成视频的服务需要知道在生成视频后如何更新事件;换句话说,服务必须包含执行此更新的代码。
而且,一旦我们添加了许多服务,这将变得越来越困难,因为它们之间需要更多的通信。它会有更多的故障,主要的问题是,如果微服务关闭,视频将无法生成。因此,使用 HTTP 请求无法很好地扩展,我们应该在这样的项目中使用不同的策略来通信微服务。
如果我们用不同的方式做事呢?换句话说,生成视频的服务不会直接更新事件,事件也不会要求视频服务生成视频。那么,我们如何让微服务进行通信呢?答案是使用事件驱动的体系结构。
为此,我们需要以下几点:
在下图中,您可以看到所涉及的不同服务和流程流(用箭头表示)。下图显示了事件驱动的工作流:
当我们在事件服务 API(1上创建新事件时,该事件进入集中总线(2),相应的工作人员从集中总线(3获取该事件;其他人只是忽略了这件事。事件放置在视频生成器服务队列中,等待服务执行(4。
一旦视频由服务工作人员生成,服务将向中央总线(5)启动一个新事件。但是,这一次将由不同的工作人员执行(6),工作人员的 res 将像前面一样忽略此事件。更新事件的工作人员和发送电子邮件的工作人员将把事件放入他们的队列中,并对每个服务执行相应的操作(7,如果需要,他们将向集中总线发送新事件。
这是一个事件循环,它改进了服务之间通信的 HTTP 请求方法。使用事件驱动体系结构的优点如下所述:
没有代码提交策略或测试/部署工作流,软件项目就不可能成功。在团队中工作时,制定战略更为重要。没有什么比在一个杂乱无章的项目上工作更烦人的了,那里没有规则,也没有人对他们所做的工作负责。在本节中,我们将解释最常见和最成功的开发实践。
持续集成是一种软件开发实践,其中所有团队成员都经常集成他们的工作。每次将新代码推送到共享存储库时,都会启动一个自动构建,以尽可能快地检测任何类型的集成错误。主要目标是避免长期和不可预测的集成。
让我们用一个简单的 CI 流程示例来更好地解释它。假设您已经准备好了我们的游戏示例,并且在生产环境中运行良好,并且您对应用程序的用户会喜欢的一个小功能有了新的想法。这个新功能可以在几个小时内完成。
首先在开发机器上获取当前源代码的副本;您将使用一个源代码管理系统,所以您只需要从主线签出一个工作副本。
现在您已经有了源代码的工作副本,您可以做任何您需要的事情来完成特性、添加新代码、创建新测试等等。CI 实践假设您的代码的很大一部分将被自动化测试覆盖。PHP 中一个流行的单元测试套件是 PHPUnit,这是一个简单而强大的工具,我们将在后面的章节中介绍。测试我们的代码将有助于我们在流程的未来步骤中,并将确保代码的高质量。
现在,您已经结束了新特性,现在是在您的开发环境上启动自动构建的时候了。这个过程将获取源代码,检查错误,并运行自动测试。只有在生成和所有测试没有错误的情况下,我们才能将构建视为好的,并且可以将其添加到存储库中。
这样做的结果是,我们有一个稳定的软件,工作正常,包含很少的错误。
持续集成的主要目标是降低风险,但这并不是采用这种开发实践的唯一好处。除其他外,我们可以强调以下好处:
作为一名开发人员,您可能会担心如何自动化此过程。别担心,在市场上,您有多种方法来创建和管理 CI 管道。我们的最佳建议是,在您决定在项目中使用哪种 CI 软件之前,请花一些时间测试所有选项。一些易于与 PHP 集成的 CI 软件包括:
在我们的示例项目中,我们将使用 Jenkins 并旋转 Docker 容器。同时,您可以使用以下简单命令开始测试 Jenkins:
$ docker run -p 8080:8080 -p 50000:50000 jenkins
此命令将为 Jenkins 创建一个带有正式 Docker 图像的容器,并将 8080 和 50000 从本地环境映射到该容器。如果您在http://localhost:8080
上打开浏览器,您将可以访问詹金斯用户界面。
持续交付是持续集成的延续,其主要目标是能够在任何时间点部署软件的任何版本,而不会出现故障。我们可以通过确保代码始终可供部署来实现这一点,并且通过遵循持续集成实践,我们可以确保源代码的集成质量和级别。
通过持续交付,每次我们对代码进行更改时,都会构建、测试这些更改,然后将其发布到后台环境中。下图显示了 CD 管道上的基本工作流。如您所见,如果任何测试步骤失败,我们需要重新开始,直到代码通过测试。通过这种方式,我们可以始终确保我们的项目符合最高质量标准。
以下是连续交付工作流的示意图:
持续交付有许多好处;其中,我们强调以下几点:
如前所述,持续交付是持续集成的延续,因此我们可以使用前面提到的大多数 CI 工具,并使用我们最喜欢的测试框架扩展我们的管道。在 PHP 中,我们有大量可用的测试框架,但最著名的是:
在接下来的章节中,我们将使用其中一些测试框架。同时,让他们中的每一个都尝试一下,选择你最喜欢的框架。记住,你可以毫无问题地混合它们。
总结在本章中,我们讨论了设计和开发应用程序的不同方法。我们讨论了一些可以轻松集成到开发工作流中的模式和策略,甚至还讨论了最常见的开发实践。在接下来的章节中,我们将在开发工作流程中应用所有这些概念。