PHP 微服务(一)

原文:zh.annas-archive.org/md5/32377e38e7a2e12adc56f6a343e595a0

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

微服务是新生力量。它们是解决问题的一种方式,但这并不意味着它们总是最佳解决方案。微服务是一个完整而复杂的架构,您需要深入了解,才能成功地将它们应用于现实世界。

我们知道,理解和准备微服务可能需要时间,可能非常复杂,因此出于这个原因,我们写了这本书——为您提供微服务的简要指南,从 A 到 Z。这本书只是微服务的冰山一角,但我们希望它至少能成为进入这个世界的有益起点。

在本书中,我们将从解释这种架构的基础知识开始,您需要掌握的基本知识。随着您在本书中的进展,我们将增加难度。然而,我们将引导您完成每个步骤,因此在本书结束时,您将能够知道微服务架构是否是解决问题的最佳方案,以及如何应用这种架构。

享受您的微服务!

本书涵盖内容

第一章,“什么是微服务?”,将教授您微服务的所有基础知识。

第二章,“开发环境”,将带您设置开发机器,以成功构建微服务。

第三章,“应用程序设计”,将帮助您开始设计应用程序,创建项目的基础。

第四章,“测试和质量控制”,探讨了测试应用程序的重要性以及如何向应用程序添加测试。

第五章,“微服务开发”,将介绍构建微服务应用程序,解释每个步骤。

第六章,“监控”,涵盖了如何监控应用程序,以便您始终了解应用程序的行为。

第七章,“安全”,着重介绍了如何向应用程序添加额外的复杂性,使其更加安全。

第八章,“部署”,解释了如何成功部署您的应用程序。

第九章,“从单体架构到微服务”,讨论了单体应用程序如何转变为微服务的示例。

第十章,“可扩展性策略”,概述了如何创建可扩展的应用程序。

第十一章,“最佳实践和惯例”,将更新您对应用程序中应该使用的最佳实践和惯例的了解。

第十二章,“云和 DevOps”,探讨了不同的云提供商和 DevOps 世界。

本书需要什么

要跟随本书,您需要一台连接互联网的计算机,以下载所需的软件和 Docker 镜像。在某个时候,您至少需要一个文本编辑器或 IDE,我们强烈推荐 PHPStorm。

本书适合对象

本书适合有经验的 PHP 开发人员,他们希望在职业生涯中迈出一步,开始构建成功的可扩展和可维护的应用程序,所有这些都基于微服务。阅读本书后,他们将能够知道微服务是否是解决问题的最佳方案,如果是这种情况,创建一个成功的应用程序。

惯例

在本书中,您将找到一些文本样式,用于区分不同类型的信息。以下是这些样式的一些示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“下面的代码行读取链接并将其分配给BeautifulSoup函数。”

一块代码设置如下:

    version: '2'
    services:

当我们希望引起您对代码块的特定部分的注意时,相关的行或项目以粗体显示:

    version: '**2**'
    services:

任何命令行输入或输出都以以下方式编写:

**$ uname -r 
3.10.0-229.el7.x86_64** 

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,以这种方式出现在文本中:“单击工具栏上的鲸鱼图标以打开**首选项…**和其他选项。”

注意

警告或重要说明以这样的框出现。

提示

提示和技巧显示为这样。

第一章:什么是微服务?

好的项目需要好的解决方案;这就是为什么开发人员总是在寻找更好的工作方式。没有适用于所有项目的最佳解决方案,因为每个项目都有不同的需求,架构师(或开发人员)必须为该特定项目找到最佳解决方案。

微服务可能是解决问题的一个好方法;在过去几年中,像 Netflix、PayPal、eBay、亚马逊和 Spotify 这样的公司选择在他们自己的开发团队中使用微服务,因为他们认为这是他们项目的最佳解决方案。要理解他们为什么选择微服务,并了解应该在哪种项目中使用它们,有必要了解什么是微服务。

首先,了解什么是单片应用是至关重要的,但基本上,我们可以将微服务定义为扩展的面向服务的架构。换句话说,这是一种通过遵循将应用程序转变为各种小服务所需的步骤来开发应用程序的方式。每个服务将自行执行并通过请求与其他服务通信,通常使用 HTTP 上的 API。

要进一步了解什么是微服务,我们首先需要了解什么是单片应用。这是我们过去几年一直在开发的典型应用,例如在 PHP 中使用像 Symfony 这样的框架;换句话说,我们一直在开发的所有应用都被划分为不同的部分,如前端、后端和数据库,并且还使用 MVC 模式。重要的是要区分 MVC 和微服务。MVC 是一种设计模式,而微服务是一种开发应用的方式;因此,使用 MVC 开发的应用仍然可能是单片应用。人们可能会认为,如果我们将应用程序分割到不同的机器上,并将业务逻辑与模型和视图分开,那么应用程序就是基于微服务的,但这是不正确的。

然而,使用单片架构仍然有其优势。还有一些巨大的网络应用,比如 Facebook,使用它;我们只需要知道何时需要使用单片架构,何时需要使用微服务。

单片与微服务

现在,我们将讨论使用单片应用的优缺点以及微服务如何通过提供一个基本示例来改进特定项目。

想象一下像 Uber 这样的出租车平台;在这个例子中,平台将是一个小型平台,只有基本的东西,以便理解定义。有顾客、司机、城市,以及一个实时跟踪出租车位置的系统:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在单片系统中,所有这些都在一起—我们有一个与顾客和司机相关联的公共数据库,这些都与城市相关联,并且所有这些都与使用外键跟踪出租车的系统相关联。

所有这些也可以托管在不同的机器上,使用主从数据库;后端可以在不同的实例中使用负载均衡器或反向代理,前端可以使用不同的技术,如 Node.js 甚至纯 HTML。即便如此,平台仍然是一个单片应用。

让我们看一个在单片应用中可能面临的问题的例子:

  • 两位开发人员 Joe 和 John 正在基本的出租车示例项目上工作;Joe 需要更改一些关于司机的代码,而 John 需要更改一些关于顾客的代码。第一个问题是,这两位开发人员都在同一个基本代码上工作;这不是一个独占的单片问题,但如果 Joe 需要在司机模型上添加一个新字段,他可能也需要更改顾客的模型,因此 Joe 的工作不仅仅局限在司机方面;换句话说,他的工作没有被界定。这就是当我们使用单片应用时会发生的情况。

  • 乔和约翰意识到,跟踪出租车的系统必须与调用外部 API 的第三方进行通信。应用程序的负载并不是很大,但跟踪出租车的系统却有很多来自顾客和司机的请求,因此在那一方面存在瓶颈。乔和约翰必须对其进行扩展以解决跟踪系统上的问题:他们可以获得更快的机器、更多的内存和负载均衡器,但问题是他们必须对整个应用程序进行扩展。

  • 乔和约翰很高兴;他们刚刚解决了跟踪出租车系统的问题。现在他们将把更改放到生产服务器上。他们将不得不在网站应用程序的流量较低时工作;风险很高,因为他们必须部署整个应用程序,而不仅仅是他们修复的跟踪出租车系统。

  • 在几个小时内,应用程序出现了 500 错误。乔和约翰知道问题与跟踪系统有关,但整个应用程序将会崩溃,只是因为应用程序的一部分出了问题。

微服务是一个简单的、独立的实体,具有明确的目标。它是独立的,并通过约定的通道与其他微服务进行通信,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于习惯于面向对象编程的开发人员来说,微服务的概念就像是一个封装的对象在不同的机器上运行,并且与其他在不同机器上运行的对象隔离。

按照之前的例子,如果我们在跟踪出租车的系统上出现问题,就需要隔离与该部分应用程序相关的所有代码。这有点复杂,将在第九章“从单体架构到微服务”中详细解释,但总的来说,这是一个专门用于跟踪出租车的系统的数据库,因此我们需要提取出这部分,并修改代码以使其与提取出的数据库一起工作。一旦目标实现,我们将拥有一个可以被单体应用程序的其余部分调用的微服务 API(或任何其他渠道)。

这将避免之前提到的问题——乔和约翰可以独立处理他们自己的问题,因为一旦应用程序被划分为微服务,他们将处理顾客或司机微服务。如果乔需要更改代码或包含新字段,他只需要在自己的实体中进行更改,约翰将使用司机 API 从顾客部分进行通信。

可扩展性只需针对这个微服务进行,因此不需要花费金钱和资源来扩展整个应用程序,如果跟踪出租车的系统出现问题,其余应用程序将可以正常工作。

使用微服务的另一个优势是它们对语言是不可知的;换句话说,每个微服务可以使用不同的语言。一个用 PHP 编写的微服务可以与用 Python 或 Ruby 编写的其他微服务进行通信,因为它们只向其他微服务提供 API,因此它们只需共享相同的接口来相互通信。

面向服务的架构与微服务

当开发人员遇到微服务并了解面向服务的架构(SOA)软件设计风格时,他们首先要问自己的问题是 SOA 和微服务是否是同一回事,或者它们是否相关,答案有点具有争议性;取决于你问的人,答案会有所不同。

根据马丁·福勒(Martin Fowler)的说法,SOA 专注于将单体应用程序相互集成,并使用企业服务总线(ESB)来实现这一目标。

当 SOA 架构开始出现时,它们试图将不同的组件连接在一起,这是微服务的特点之一,但 SOA 的问题在于它需要许多围绕架构工作的东西,如 ESB、业务流程管理BPM)、服务存储库、注册表等等,因此这使得开发变得更加困难。此外,为了更改代码的某些部分,需要先与其他开发团队达成一致。

所有这些因素使得维护和代码演进变得困难,上市时间变长;换句话说,这种架构并不适合经常需要实时更改的应用程序。

关于 SOA 还有其他观点。有人说 SOA 和微服务是一样的,但 SOA 是理论,微服务是一个很好的实现。使用 ESB 或使用 WSDL 或 WADL 进行通信并不是必须的,但它被定义为 SOA 的标准。如下图所示,使用 SOA 和 ESB 的架构如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请求通过不同的方式到达;这与微服务的工作方式相同,但所有请求都到达 ESB,它知道应该调用哪里来获取数据。

微服务特点

接下来,我们将看一下微服务架构的关键要素:

  • 准备失败:微服务被设计为失败。在 Web 应用程序中,微服务相互通信,如果其中一个失败,其余部分应该继续工作以避免级联故障。基本上,微服务试图避免使用同步通信,而是使用异步调用、队列、基于 actor 的系统和流。这个话题将在接下来的章节中讨论。

  • Unix 哲学:微服务应该遵循 Unix 哲学。每个微服务必须设计为只做一件事,并且应该小而独立。这使我们作为开发人员能够独立调整和部署每个微服务。Unix 哲学强调构建简单、简短、清晰、模块化和可扩展的代码,这样可以方便开发人员进行维护和重用,而不仅仅是其创建者。

  • 通信层:每个微服务通过 HTTP 请求和消息与其他微服务通信,执行业务逻辑,查询数据库,与所需系统交换消息,最终返回 JSON(或 HTML/XML)响应。

  • 可扩展性:选择微服务架构的主要原因是可以轻松扩展应用程序。应用程序越大,流量越大,选择微服务的合理性就越大。微服务可以在不影响应用程序的其他部分的情况下扩展所需的部分。

成功案例

了解微服务在现实生活中有多重要的最好方法是了解一些决定发展并使用微服务的平台,考虑未来并使维护和扩展更容易、更快速、更有效:

  • Netflix:在线视频流媒体的头号应用在几年前将其架构转变为微服务。有一个关于他们决定使用微服务的原因的故事。在对评论模块进行更改时,一名开发人员忘记在行尾加上;,导致 Netflix 宕机了很多小时。该平台每天约 30%的总流量来自美国,因此 Netflix 必须向每月付费的客户提供稳定的服务。为了实现这一点,Netflix 对每个我们发出的请求进行五次请求,并且可以使用其流媒体视频 API 从 800 个不同的设备获取请求。

  • eBay:在 2007 年,eBay 决定将其架构改为微服务;他们曾在 C++和 Perl 上使用单片应用程序,后来他们转向了基于 Java 构建的服务,最终实施了微服务架构。它的主要应用程序有许多服务,每个服务都执行自己的逻辑,以供每个领域的客户使用。

  • Uber:微服务使得这家公司能够快速增长,因为它允许使用不同的语言(Node.js、Go、Scala、Java 或 Python)用于每个微服务,而且招聘工程师的过程更容易,因为他们不受语言代码的限制。

  • 亚马逊:也许亚马逊不是互联网流量之王,但它几年前就转向了微服务,成为最早使用实时微服务的公司之一。工程师们表示,使用旧的单片应用程序无法提供他们提供的所有服务,比如网络服务。

  • Spotify:对于 Spotify 来说,比对手更快是必须的。主要工程师尼古拉斯·古斯塔夫森表示,快速、自动化一切以及拥有更小的开发团队对应用程序非常重要。这就是为什么 Spotify 使用微服务的原因。

微服务的缺点

接下来,我们将看看微服务架构的缺点。当询问开发人员时,他们认为微服务的主要问题是在生产服务器上进行调试。

基于微服务的应用程序调试可能会有点繁琐,特别是当你的应用程序中有数百个微服务时,你需要找到问题出在哪里;你需要花时间寻找给你报错的微服务。每个微服务都像一个独立的机器,所以要查看特定的日志,你必须去特定的微服务。幸运的是,有一些工具可以帮助我们解决这个问题,可以从应用程序中的所有不同微服务中获取日志,并将它们汇总到一个位置。在接下来的章节中,我们将看看这些工具。

另一个缺点是需要将每个微服务都作为一个完整的服务器进行维护;换句话说,每个微服务都可以有一个或多个数据库、日志、不同的服务或库版本,甚至代码可以使用不同的语言,因此如果维护单个服务器困难,那么维护数百个服务器将浪费金钱和时间。

此外,微服务之间的通信非常重要—它们必须像时钟一样工作,因此通信对于应用程序至关重要。为了做到这一点,开发团队之间的沟通将是必要的,彼此告知他们需要什么,同时为每个微服务编写良好的文档也是必要的。

与微服务一起工作的一个好的做法是自动化一切,或者至少尽可能自动化一切。也许最重要的部分是部署。如果需要部署数百个微服务,这可能会很困难。因此,最好的方法是自动化这些任务。我们将在接下来的章节中看看如何做到这一点。

如何将开发重点放在微服务上

开发微服务是一种新的思维方式。因此,当您第一次遇到应用程序的设计和构建时,可能会感到困难。然而,如果您始终记住微服务背后最重要的思想是将应用程序分解为更小的逻辑部分的需求,那么您已经完成了一半。

一旦理解了这一点,以下核心思想将有助于您的应用程序的设计和构建过程。

始终创建小的逻辑黑匣子

作为开发人员,你总是从你将要构建的大局开始。尝试将大局分解为只做一件事情的小逻辑块。一旦多个小块准备就绪,你可以开始构建复杂的系统,确保你的应用程序的基础是坚实的。

你的每个微服务都像一个黑匣子,有一个公共接口,这是与你的软件交互的唯一方式。你需要牢记的主要建议是构建一个非常稳定的 API。你可以在不引起太多问题的情况下更改 API 调用的实现,但是如果你改变调用的方式甚至是这个调用的响应,你将陷入麻烦。在对 API 进行重大更改的情况下,确保使用某种版本控制,以便你可以支持旧版本和新版本。

网络延迟是你隐藏的敌人

服务之间的通信是通过使用网络作为连接管道的 API 调用进行的。这种消息交换需要一些时间,由于多种因素,它的时间不会总是相同。想象一下,你在一台机器上有service_a,在另一台机器上有service_b。你认为网络延迟会一直是一样的吗?如果,例如,其中一台服务器负载很高,需要一些时间来处理请求,会发生什么?为了减少时间,始终关注你的基础设施,监控一切,并在可用时使用压缩。

始终考虑可扩展性

微服务应用的主要优势之一是每个服务都可以进行扩展。通过将有状态服务的数量减少到最低,可以实现这种灵活性。有状态服务依赖于数据持久性,这使得在没有数据一致性问题的情况下移动或共享数据变得困难。

使用自动发现和自动注册技术,你可以构建一个系统,它始终知道谁将处理每个请求。

使用轻量级通信协议

没有人喜欢等待,即使是你的微服务也是如此。不要试图重新发明轮子或使用一个晦涩但很酷的通信协议,使用 HTTP 和 REST。它们为所有的 Web 开发人员所熟知,它们快速、可靠、易于实现,非常容易调试。如果需要增加安全性,实现 SSL/TSL。

使用队列来减少服务负载或进行异步执行。

作为开发人员,你希望使你的系统尽可能快。因此,增加 API 调用的执行时间只因为它在等待可以在后台完成的某些操作是没有意义的。在这些情况下,最好的方法是使用队列和作业运行程序来处理后台处理。

想象一下,你有一个通知系统,在微服务电子商务上下订单时向客户发送电子邮件。你认为客户想要等待看到支付成功页面,只是因为系统正在尝试发送电子邮件吗?在这种情况下,更好的方法是将消息加入队列,这样客户将会有一个非常快速的感谢页面。稍后,作业运行程序将提取排队的通知,并将电子邮件发送给客户。

为最坏的情况做好准备

你有一个建立在微服务之上的漂亮、新颖、好看的网站。当一切都出错的时候,你准备好了吗?如果你遇到了网络分区,你知道你的应用程序是否能从这种情况中恢复吗?现在,想象一下你有一个推荐系统,它出现了问题,你会在尝试恢复死掉的服务时给客户一个默认的推荐吗?欢迎来到分布式系统的世界,在这里,一旦出现问题,情况可能会变得更糟。始终牢记这一点,并尝试为任何情况做好准备。

每个服务都是不同的,因此保持不同的存储库和构建环境

我们正在将一个应用程序分解成小部分,我们希望对其进行扩展和独立部署,因此将源代码保留在不同的存储库中是有意义的。拥有不同的构建环境和存储库意味着每个微服务都有自己的生命周期,并且可以在不影响应用程序的其他部分的情况下进行部署。

在接下来的章节中,我们将更深入地研究所有这些想法,以及如何使用不同的驱动开发来实现它们。

使用 PHP 构建微服务的优势

要理解为什么 PHP 是构建微服务的合适编程语言,我们需要稍微了解一下它的历史,它的起源,它试图解决的问题以及语言的演变。

PHP 的简短历史

1994 年,Rasmus Lerdorf 创建了我们可以说是 PHP 的第一个版本。他用 C 语言构建了一套小型的公共网关接口CGI)来维护他的个人网页。这套脚本被称为个人主页工具,但更常被称为PHP 工具

时间过去了,Rasmus 重写并扩展了这套脚本,使其能够与 Web 表单一起工作,并具有与数据库通信的能力。这个新的实现被称为个人主页/表单解释器PHP/FI,并作为其他开发人员可以构建动态 Web 应用程序的框架。1995 年 6 月,源代码以个人主页工具PHP 工具版本 1.0的名义向公众开放,允许世界各地的开发人员使用它,修复错误并改进这套脚本。

PHP/FI 的最初想法并不是创建一种新的编程语言,Lerdorf 让它自然生长,导致了一些问题,比如函数名称或参数的不一致性。有时函数名称与 PHP 正在使用的低级库相同。

1995 年 10 月,Rasmus 发布了代码的新重写;这是第一个被认为是高级脚本接口的发布,PHP 开始成为今天的编程语言。

作为一种语言,PHP 的设计与 C 语言的结构非常相似,因此对于熟悉 C、Perl 或类似语言的开发人员来说更容易被接受。随着语言功能的增长,早期采用者的数量也开始增长。1998 年 5 月的 Netcraft 调查显示,几乎有 6 万个域名包含了 PHP 的头部信息(占当时互联网域名的 1%左右),这表明主机服务器已经安装了 PHP。

PHP 历史上的一个重要时刻是 1997 年以色列特拉维夫的 Andi Gutmans 和 Zeev Suraski 加入了这个项目。在这个时候,他们对解析器进行了另一次完全重写,并开始开发一种新的独立编程语言。这种新语言被命名为 PHP,意思是递归缩写—PHP超文本预处理器)。

PHP 3 的官方发布是在 1998 年 6 月,包括了许多功能,使该语言适用于各种项目。其中一些功能包括成熟的多数据库接口,支持多种协议和 API,以及扩展语言本身的便利性。在所有功能中,最重要的是包括了面向对象编程和更强大、一致的语言语法。

Andi Gutmans 和 Zeev Suraski 成立了 Zend Technologies,并于 1999 年开始重写 PHP 的核心,创建了 Zend 引擎。Zend Technologies 成为了最重要的 PHP 公司,也是源代码的主要贡献者。

这只是一个开始,随着时间的推移,PHP 在功能、语言稳定性、成熟度和开发者采用方面不断增长。

PHP 的里程碑

现在我们有了一些历史背景,我们可以专注于 PHP 在这些年里取得的主要里程碑。每个版本都增加了语言的稳定性,并添加了越来越多的功能。

4.x 版本

PHP 4 是第一个包含 Zend 引擎的版本。这个引擎提高了 PHP 的平均性能。除了 Zend 引擎,PHP 4 还包括对更多 Web 服务器、HTTP 会话、输出缓冲和增强安全性的支持。

5.x 版本

PHP 5 于 2004 年 7 月 13 日发布,使用了 Zend Engine II,再次提高了语言的性能。这个版本包括了对面向对象OO)编程的重要改进,使语言更加灵活和健壮。现在,用户可以选择以过程化或稳定的面向对象方式开发应用程序;他们可以兼得两者的优点。在这个版本中,还包括了连接到数据存储的最重要的扩展之一—PHP 数据对象PDO)扩展。

随着 PHP 5 在 2008 年成为最稳定的版本,许多开源项目开始在他们的新代码中终止对 PHP 4 的支持。

6.x 版本

这个版本是 PHP 最著名的失败之一。这个主要版本的开发始于 2005 年,但在 2010 年因 Unicode 实现的困难而被放弃。并非所有的工作都被抛弃,大部分功能,其中包括命名空间,都被添加到了之前的版本中。顺便说一句,版本 6 通常与技术世界中的失败联系在一起:PHP 6、Perl 6,甚至 MySQL 6 都从未发布。

7.x 版本

这是一个期待已久的发布—一个统治所有的发布,一个具有前所未有的性能水平的发布。

2015 年 12 月 3 日,发布了最后一个 Zend 引擎的 7.0.0 版本。仅通过在您的机器上更改运行版本,性能提高了 70%,内存占用非常小。

语言也在不断发展,PHP 现在具有更好的 64 位支持和安全的随机数生成器。现在你可以创建匿名类或定义返回类型等其他重大改变。

这个版本成为了其他所谓的企业语言的严肃竞争对手。

优势

PHP 是你可以用来构建你的 Web 项目的最常用的编程语言之一。如果你还不确定 PHP 是否适合你的下一个应用程序,让我们告诉你使用 PHP 的主要优势:

  • 庞大的社区:很容易参加全球各地的 ZendCon、PHP[world]或国际 PHP 大会等会议、活动和聚会。你不仅可以在活动和会议上与其他 PHP 开发者交流,还可以加入 IRC/Slack 频道或邮件列表提问和保持更新。你可以在官方网站上的php.net/cal.php找到附近地区的活动地点之一。

  • 文档齐全:关于这种语言的主要信息可以在 PHP 网站上找到(php.net/docs.php)。这个参考指南涵盖了 PHP 的每个方面,从基本的控制流到高级主题。你想读一本书吗?没问题,你很容易在 15000 多本亚马逊参考书中找到合适的书。即使你需要更多信息,快速在谷歌上搜索就会给你超过 93 亿的结果。

  • 稳定:PHP 项目经常发布版本,并同时维护几个主要版本,直到它们计划的生命周期结束EOL)。通常,发布和 EOL 之间的时间足以转移到下一个主流版本。

  • 易于部署:PHP 是最受欢迎的服务器端编程语言,在 2016 年 8 月的使用率达到 81.8%,所以市场也朝着同一个方向发展。很容易找到一个预安装了 PHP 并且可以直接使用的托管提供商,所以你只需要处理部署的问题。

有许多部署代码到生产环境的方法。例如,你可以在 Git 存储库中跟踪你的代码,然后稍后在服务器上进行 Git 拉取。你也可以通过 FTP 将文件推送到公共位置。你甚至可以使用 Jenkins、Docker、Terraform、Packer、Ansible 或其他工具构建一个持续集成/持续交付CI/CD)系统。部署的复杂性将始终与项目的复杂性相匹配。

  • 易于安装:PHP 在主要操作系统上都有预构建的软件包:它可以安装在 Linux、Mac、Windows 或 Unix 上。有时软件包可以在软件包系统内获得(例如 apt)。在其他情况下,你需要外部工具来安装它,比如 Homebrew。在最坏的情况下,你可以下载源代码并在你的机器上编译它。

  • 开源:所有 PHP 源代码都可以在 GitHub 上找到,因此任何开发人员都可以轻松地深入了解一切是如何工作的。这种开放性允许程序员参与项目,扩展语言或修复错误。PHP 使用的许可证是伯克利软件发行BSD)风格的许可证,没有与 GPL 相关的版权限制。

  • 高运行速度(PHP 7):过去,PHP 的速度不够快,但这种情况在 PHP 7 中完全改变了。这个主要版本是基于PHP Next-GenPHPNG)项目,由 Zend Technologies 领导。PHPNG 的理念是加速 PHP 应用程序,他们做得非常好。仅通过更改 PHP 版本,性能提升可以在 25%到 70%之间变化。

  • 大量可用的框架和库:PHP 生态系统在库和框架方面非常丰富。找到适合你的项目的库的常见方法是使用 PEAR 和 PECL。

关于可用的框架,你可以使用最好的之一,例如 Zend Framework、Symfony、Laravel、Phalcon、CodeIgniter,或者如果找不到符合你要求的框架,你可以从头开始构建一个。最好的部分是所有这些都是开源的,所以你可以随时扩展它们。

  • 高开发速度:PHP 具有相对较小的学习曲线,这可以帮助你从头开始编码。此外,与 C、Perl 和其他语言的语法相似性可以让你很快理解一切是如何工作的。PHP 避免了等待编译器生成我们的构建所浪费的时间。

  • 易于调试:你可能知道,PHP 是一种解释性语言。因此,在解决问题时,你有多种选择。简单的方法是放置一些var_dumpprint_r调用来查看代码的执行情况。为了更深入地查看执行情况,你可以将你的 IDE 链接到 Xdebug 或 Zend Debug,并开始跟踪你的应用程序。

  • 易于测试:没有现代编程语言能在野外生存而没有适当的测试套件,所以你必须确保你的应用程序将继续按预期运行。不用担心,PHP 社区支持你,因为你有多种工具可进行任何类型的测试。例如,你可以使用 PHPUnit 进行你的测试驱动开发TDD),或者如果你遵循行为驱动开发BDD),可以使用 Behat。

  • 你可以做任何事情:PHP 是一种现代编程语言,在现实世界中有多种应用。因此,只有天空是极限!你可以使用 GTK 扩展构建 GUI 应用程序,或者你可以在 phar 存档中创建一个包含所有所需文件的终端命令。这种语言没有限制,所以任何东西都可以构建。

缺点

像任何编程语言一样,PHP 也有一些缺点。一些最常见的缺点包括:安全问题,不适合大型应用程序和弱类型。PHP 最初是一组 CGI 的集合,并且随着多年的发展变得更加现代和健壮,因此与其他语言相比,它非常健壮和灵活。

无论如何,有经验的开发人员在构建应用程序时,如果使用最佳实践,就不会遇到这些缺点。

正如你已经看到的,PHP 的发展是巨大的。它拥有最活跃的社区之一,是为网络而生,并且具有创建任何类型项目所需的所有功能。毫无疑问,PHP 将是您表达最佳创意的正确语言。

总结

在本章中,我们探讨了微服务与单体架构和 SOA 的对比。我们了解了微服务架构的基本组件及其优缺点。在本章的后半部分,我们讨论了如何实施微服务架构以及在转向微服务之前需要考虑的先决条件。随后,我们介绍了 PHP 版本的历史以及它们的优缺点。

第二章:开发环境

在本章中,我们将开始构建基于微服务的应用程序,现在我们知道为什么微服务对于应用程序的开发是必要的,以及如果我们基于微服务构建应用程序可以享受到的优势。

我们将在本书中开发的应用程序(类似于 Pokemon GO)被称为“寻找秘密”。这个应用程序将像一个使用地理位置信息来寻找世界各地不同秘密的游戏。整个世界都隐藏着许多秘密,玩家们必须尽快找到它们。有 100 种不同类型的秘密,它们每天会在世界的不同地方生成和出现,因此玩家可以通过在不同地区四处走动并检查附近是否有任何类型的秘密来找到它们。

秘密将保存在应用程序钱包中,如果玩家发现他们已经拥有的秘密,他们将无法收集它。

玩家可以在附近与其他玩家进行决斗。决斗包括掷骰子以获得最高点数,输掉的玩家将向另一名玩家随机透露一个秘密。

在接下来的章节中,具体功能将更加详细,但在本章中,我们只需要了解应用程序的工作方式,以便对整个应用程序有一个总体概述,从而开始构建基于微服务的基本平台。

用于构建微服务基本平台的设计和架构

创建基于微服务的应用程序不像单片应用程序。因此,我们必须将功能划分为不同的服务。为此,根据其要求,按照适当的设计和结构每个微服务是很重要的。

设计负责将应用程序分成逻辑部分,并根据它们的现有关系对它们进行分组。架构负责定义哪些具体元素支持每个微服务,例如数据存储位置或服务之间的通信。

在整本书中,我们将遵循每个微服务的给定结构。在下图中,您将看到一个微服务的结构,其余的微服务类似;但是,有些部分是可选的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们所有微服务的请求都来自反向代理,因为这样可以平衡负载。此外,我们使用 NGINX 作为 PHP 构建的 API 的网关。为了减少负载并提高 PHP 和 NGINX 的性能,我们可以使用缓存层。

如果我们需要执行大型、消耗资源的任务,或者任务不需要在具体时间窗口内执行,我们的 API 可以使用队列系统。

如果我们需要存储一些数据,我们的 API 负责管理访问并将数据保存在我们的数据存储中。

注意

在本书中,我们将使用容器化,这是一种新的虚拟化方法,它会启动容器而不是完整的虚拟机。每个容器只会安装运行应用程序所需的最低资源和软件。

我们可以使用遥测(它是一个从容器获取统计数据的系统)和自动发现(它是一个帮助我们查看哪些容器正常工作的系统)来监督容器生态系统。

开始使用微服务的要求

现在您了解了为什么可以使用 PHP(特别是最新版本 7)来进行下一个项目,是时候谈谈您的微服务项目成功的其他要求了。

您可能已经意识到应用程序的可扩展性的重要性,但如何在预算内实现呢?答案是虚拟化。使用这项技术,您将浪费更少的资源。过去,同一硬件上一次只能执行一个操作系统OS),但随着虚拟化的诞生,您可以同时运行多个操作系统。您的项目最大的成就将是,您将运行更多专用于您的微服务的服务器,但使用更少的硬件。

鉴于虚拟化和容器化提供的优势,如今在基于微服务的应用程序开发中使用容器已成为默认标准。有多个容器化项目,但最常用和受支持的是 Docker。因此,这是开始使用微服务的主要要求。

以下是我们将在 Docker 环境中使用的不同工具/软件:

  • PHP 7

  • 数据存储:Percona,MySQL,PostgreSQL

  • 反向代理:NGINX,Fabio,Traefik

  • 依赖管理:composer

  • 测试工具:PHPUnit,Behat,Selenium

  • 版本控制:Git

在第五章中,微服务开发,我们将解释如何将它们添加到我们的项目中。

由于我们的主要要求是容器化套件,我们将在接下来的章节中解释如何安装和测试 Docker。

Docker 安装

Docker 可以从两个不同的渠道安装,各有优缺点:

  • 稳定渠道:顾名思义,您从该渠道安装的所有内容都经过充分测试,您将获得 Docker 引擎的最新 GA 版本。这是最可靠的平台,因此适用于生产环境。通过该渠道,发布遵循版本计划,经过长时间的测试和 beta 时间,只是为了确保一切都应该按预期工作。

  • Beta channel: 如果您需要拥有最新功能,这是您的渠道。所有安装程序都配备了 Docker 引擎的实验版本,可能会出现错误,因此不建议用于生产环境。该渠道是 Docker beta 计划的延续,您可以提供反馈,没有版本计划,因此会有更频繁的发布。

我们将开发一个稳定的生产环境,所以现在您可以忘记 beta 渠道,因为您所需的一切都在稳定版本中。

Docker 诞生于 Linux,因此最佳实现是针对该操作系统完成的。对于其他操作系统,如 Windows 或 macOS,您有两个选择:本机实现和如果无法使用本机实现,则进行工具箱安装。

在 macOS 上安装 Docker

在 macOS 上,您有两种安装 Docker 的选择,取决于您的机器是否符合最低要求。对于相对较新的机器(OS X 10.10 Yosemite 及更高版本),您可以安装使用 Hyperkit 的本机实现,这是一个轻量级的 OS X 虚拟化解决方案,构建在Hypervisor.Framework之上。如果您有一台不符合最低要求的旧机器,您可以安装 Docker 工具箱。

Docker for Mac(别名,本机实现)与 Docker 工具箱

Docker 工具箱是 macOS 上 Docker 的第一个实现,它没有深度的操作系统集成。它使用 VirtualBox 来启动一个 Linux 虚拟机,Docker 将在其中运行。在虚拟机中运行所有容器会有很多问题,最明显的是性能不佳。但是,如果您的机器不符合本机实现的要求,这是理想的选择。

Mac 上的 Docker 是一个本地 Mac 应用程序,具有本地用户界面和自动更新功能,并且与 OS X 本地虚拟化(Hypervisor.Framework)、网络和文件系统深度集成。这个版本比 Docker Toolbox 更快、更易于使用,更可靠。

最低要求
  • Mac 必须是 2010 年或更新的型号,具有英特尔硬件对内存管理单元MMU)虚拟化的支持;也就是扩展页表EPT)。

  • OS X 10.10.3 Yosemite 或更新版本

  • 至少 4GB 的 RAM

  • 在安装 Docker for Mac 之前,不能安装 VirtualBox 4.3.30 之前的版本(它与 Mac 上的 Docker 不兼容)

Mac 上的 Docker 安装过程

如果你的机器符合要求,你可以从官方页面下载 Mac 上的 Docker,即www.docker.com/products/docker

一旦你在你的机器上下载了镜像,你可以执行以下步骤:

  1. 双击下载的镜像(名为Docker.dmg)以打开安装程序。一旦镜像被挂载,你需要将 Docker 应用程序拖放到Applications文件夹中:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Docker.app在安装过程中可能会要求你输入密码,以特权模式安装和设置网络组件。

  1. 安装完成后,Docker 将出现在你的 Launchpad 和Applications文件夹中。执行该应用程序以启动 Docker。一旦 Docker 启动,你将在工具栏中看到鲸鱼图标。这将是你快速访问设置的方式。

  2. 点击工具栏上的鲸鱼图标以获取**首选项…**和其他选项:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 点击关于 Docker以查看你是否在运行最新版本。

在 Linux 上安装 Docker

Docker 生态系统是在 Linux 上开发的,因此在这个操作系统上的安装过程更容易。在接下来的页面中,我们将只涵盖在Community ENTerprise Operating SystemCentOS)/Red Hat Enterprise LinuxRHEL)(它们使用 yum 作为包管理器)和 Ubuntu(使用 apt 作为包管理器)上的安装。

CentOS/RHEL

Docker 可以在 CentOS 7 上执行,也可以在任何其他二进制兼容的 EL7 发行版上执行,但 Docker 在这些兼容的发行版上没有经过测试或支持。

最低要求

安装和执行 Docker 的最低要求是拥有 64 位操作系统和 3.10 或更高版本的内核。如果你需要知道你当前的版本,你可以打开终端并执行以下命令:

**$ uname -r 
3.10.0-229.el7.x86_64** 

请注意,建议你的操作系统保持最新,以避免任何潜在的内核错误。

使用 yum 安装 Docker

首先,你需要有一个具有 root 权限的用户;你可以以这个用户登录你的机器,或者在你选择的终端上使用sudo命令。在接下来的步骤中,我们假设你正在使用一个 root 或特权用户(如果你没有使用 root 用户,请在命令中添加 sudo)。

首先确保你所有现有的包都是最新的:

**yum update**

现在你的机器已经有了最新的可用包,你需要添加官方 Docker yum存储库:

**# tee /etc/yum.repos.d/docker.repo <<-'EOF' 
[dockerrepo] 
name=Docker Repository 
baseurl=https://yum.dockerproject.org/repo/main/centos/7/ 
enabled=1 
gpgcheck=1 
gpgkey=https://yum.dockerproject.org/gpg 
EOF**

在将 yum 存储库添加到你的 CentOS/RHEL 之后,你可以使用以下命令轻松安装 Docker 包:

**yum install docker-engine** 

你可以使用systemctl命令将 Docker 服务添加到你的操作系统的启动中(这一步是可选的):

**systemctl enable docker.service**

同样的systemctl命令可以用来启动服务:

 **systemctl start docker** 

现在你已经安装并运行了所有东西,所以你可以开始测试和玩 Docker 了。

安装后设置 - 创建 Docker 组

Docker 作为绑定到 Unix 套接字的守护程序执行。此套接字由 root 拥有,因此其他用户访问它的唯一方式是使用sudo命令。每次使用任何 Docker 命令都要使用sudo命令可能很痛苦,但您可以创建一个名为docker的 Unix 组,并将用户分配给该组。通过进行这个小改变,Docker 守护程序将启动并将 Unix 套接字的所有权分配给这个新组。

以下是创建 Docker 组的命令:

**groupadd docker**
**usermod -aG docker my_username**

执行完这些命令后,您需要注销并重新登录以刷新权限。

在 Ubuntu 上安装 Docker

Ubuntu 得到官方支持,主要建议使用 LTS(始终建议使用最新版本):

  • Ubuntu Xenial 16.04(LTS)

  • Ubuntu Trusty 14.04(LTS)

  • Ubuntu Precise 12.04(LTS)

就像之前的 Linux 安装步骤一样,我们假设您是使用 root 或特权用户来安装和设置 Docker。

最低要求

与其他 Linux 发行版一样,需要 64 位版本,并且您的内核版本至少需要 3.10。较旧的内核版本存在已知的错误,会导致数据丢失和频繁的内核崩溃。

要检查当前的内核版本,请打开您喜欢的终端并运行:

**$ uname -r
3.11.0-15-generic** 

使用 apt 安装 Docker

首先确保您的 apt 源指向 Docker 存储库,特别是如果您之前是通过 apt 安装 Docker。另外,更新您的系统:

**apt-get update** 

现在您的系统已经更新,是时候安装一些必需的软件包和新的 GPG 密钥了:

**apt-get install apt-transport-https ca-certificates 
apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys \ 58118E89F3A912897C070ADBF76221572C52609D** 

在 Ubuntu 上,很容易添加官方 Docker 存储库;您只需要在您喜欢的编辑器中创建(或编辑)/etc/apt/sources.list.d/docker.list文件。

如果您的旧存储库中有上述行,请删除所有内容并添加以下条目之一。确保与您当前的 Ubuntu 版本匹配:

 **Ubuntu Precise 12.04 (LTS):**  
    deb https://apt.dockerproject.org/repo ubuntu-precise main 
    **Ubuntu Trusty 14.04 (LTS):**
    deb https://apt.dockerproject.org/repo ubuntu-trusty main 
    **Ubuntu Xenial 16.04 (LTS):**
    deb https://apt.dockerproject.org/repo ubuntu-xenial main 

保存文件后,您需要更新apt软件包索引:

**apt-get update** 

如果您的 Ubuntu 上有以前的 Docker 存储库,则需要清除旧存储库:

**apt-get purge lxc-docker** 

在 Trusty 和 Xenial 上,建议安装linux-image-extra-*内核包,允许您使用 AFUS 存储驱动程序。要安装它们,请运行以下命令:

**apt-get update && apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual** 

在 Precise 上,Docker 需要 3.13 内核版本,因此请确保您有正确的内核;如果您的版本较旧,则必须升级。

此时,您的机器将准备好安装 Docker。可以使用单个命令,如yum

**apt-get install docker-engine** 

现在您已经安装并运行了所有内容,可以开始使用和测试 Docker。

Ubuntu 上的常见问题

如果您在使用 Docker 时看到与交换限制相关的错误,则需要在系统上启用内存和交换。可以通过 GNU GRUB 按照给定的步骤完成:

  1. 编辑/etc/default/grub文件。

  2. GRUB_CMDLINE_LINUX设置如下:

        GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"

  1. 更新 grub:
 **update-grub**

  1. 重新启动系统。
UFW 转发

Ubuntu 配备了简化防火墙UFW),如果在运行 Docker 的同一主机上启用了它,您需要进行一些调整,因为默认情况下,UFW 将丢弃任何转发流量。此外,UFW 将拒绝任何传入流量,使得无法从不同主机访问您的容器。在干净的安装中,Docker 在未启用 TLS 的情况下运行时,Docker 默认端口为 2376。让我们配置 UFW!

首先,您可以检查 UFW 是否已安装并启用:

**ufw status**

现在您确定 UFW 已安装并运行,可以使用您喜欢的编辑器编辑config文件/etc/default/ufw,并设置DEFAULT_FORWARD_POLICY

    vi /etc/default/ufw
    DEFAULT_FORWARD_POLICY="ACCEPT"

现在您可以保存并关闭config文件,在重新启动 UFW 后,您的更改将生效:

**ufw reload**

允许传入连接到 Docker 端口可以使用ufw命令完成:

**ufw allow 2375/tcp**

DNS 服务器

Ubuntu 及其衍生产品在/etc/resolv.conf文件中使用 127.0.0.1 作为默认名称服务器,因此当您使用此配置启动容器时,您将看到警告,因为 Docker 无法使用本地 DNS 名称服务器。

如果要避免这些警告,您需要指定一个 DNS 服务器供 Docker 使用,或者在NetworkManager中禁用dnsmasq。请注意,禁用dnsmasq会使 DNS 解析变慢一点。

要指定 DNS 服务器,您可以使用您喜欢的编辑器打开/etc/default/docker文件,并添加以下设置:

    DOCKER_OPTS="--dns 8.8.8.8"

8.8.8.8替换为您的本地 DNS 服务器。如果您有多个 DNS 服务器,可以添加多个--dns记录,用空格分隔。考虑以下示例:

    DOCKER_OPTS="--dns 8.8.8.8 --dns 9.9.9.9"

一旦您保存了更改,您需要重新启动 Docker 守护程序:

**service docker restart**

如果您没有本地 DNS 服务器,并且想要禁用dnsmasq,请使用您的编辑器打开/etc/NetworkManager/NetworkManager.conf文件,并注释掉以下行:

    dns=dnsmasq

保存更改并重新启动 NetworkManager 和 Docker:

**restart network-manager**
**restart docker**

安装后设置-创建 Docker 组

Docker 作为绑定到 Unix 套接字的守护程序执行。此套接字由 root 拥有,因此其他用户访问它的唯一方法是使用sudo命令。每次使用任何 docker 命令都使用sudo命令可能会很痛苦,但是您可以创建一个名为docker的 Unix 组,并将用户分配给此组。通过进行这个小改变,Docker 守护程序将启动并将 Unix 套接字的所有权分配给这个新组。

执行以下步骤创建一个 Docker 组:

**groupadd docker
usermod -aG docker my_username** 

完成这些步骤后,您需要注销并重新登录以刷新权限。

启动时启动 Docker

从 Ubuntu 15.04 开始,使用systemd系统作为其引导和服务管理器,而对于 14.10 版本和之前的版本,则使用upstart系统。

对于 15.04 及更高版本的系统,您可以通过运行给定的命令将 Docker 守护程序配置为在启动时启动:

**systemctl enable docker**

对于使用旧版本的情况,安装方法会自动配置 upstart 以在启动时启动 Docker 守护程序。

在 Windows 上安装 Docker

Docker 团队为将他们的整个生态系统带到任何操作系统做出了巨大的努力,他们没有忘记 Windows。与 macOS 一样,您在此操作系统上安装 Docker 有两个选项:工具箱和更本地的选项。

最低要求

Windows 版的 Docker 需要 64 位的 Windows 10 专业版、企业版和教育版(1511 年 11 月更新,版本 10586 或更高),并且必须启用Hyper-V包。

如果您的计算机运行的是不同版本,您可以安装需要 64 位操作系统的工具箱,至少在 Windows 7 上运行,并且在计算机上启用了虚拟化。如您所见,它的要求更轻。

由于 Docker for Windows 至少需要专业/企业/教育版本,而大多数计算机都是使用不同版本出售的,我们将解释如何使用工具箱安装 Docker。

安装 Docker 工具

Docker 工具使用 VirtualBox 来启动运行 Docker 引擎的虚拟机。安装包可以从https://www.docker.com/products/docker-toolbox下载。

一旦您获得安装程序,您只需要双击下载的可执行文件即可开始安装过程。

安装程序显示的第一个窗口允许您将调试信息发送到 Docker,以改善生态系统。允许 Docker 引擎从您的开发环境发送调试信息可以帮助社区找到错误并改善生态系统。建议在开发环境中至少启用此选项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

就像任何其他 Windows 安装程序一样,您可以选择安装的位置。在大多数情况下,默认设置对于您的开发环境来说是可以的。

默认情况下,安装程序将向您的计算机添加所有必需的软件包和一些额外的软件。在安装的这一步,您可以清除一些不必要的软件。以下是一些可选软件包:

  • Windows 的 Docker compose:在我们看来,这是必不可少的,因为我们将在我们的书中使用这个软件包。

  • Windows 的 Kitematic:这个应用程序是一个 GUI,可以轻松管理您的容器。如果您不习惯使用命令行,可以安装这个软件包。

  • Windows 的 Git:这是另一个必须安装的软件包;我们将使用 Git 来存储和管理我们的项目。

选择要安装的软件包后,是时候进行一些额外的任务了。默认选择的任务对于您的开发环境来说是合适的。

现在,您只需要在安装开始之前确认您在之前步骤中所做的所有设置。

安装可能需要几分钟才能完成,所以请耐心等待。

在安装过程中,您可能会收到有关安装 Oracle 设备的警报。这是因为工具正在使用 VirtualBox 来启动一个虚拟机来运行 Docker 引擎。安装此设备以避免将来的麻烦:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

恭喜!您已经在 Windows 机器上安装了 Docker。别浪费时间,开始测试和玩弄您的 Docker 生态系统。

如何检查您的 Docker 引擎、compose 和 machine 版本

现在您已经安装了 Docker,您只需要打开您喜欢的终端,并输入以下命令:

**$ docker --version && docker-compose --version && docker-machine --version**

快速检查 Docker 安装的示例

您应该已经运行 Docker 并执行以下命令:

**$ docker run -d -p 8080:80 --name webserver-test nginx**

上述命令将执行以下操作:

  • 使用-d在后台执行容器

  • 使用-p 8080:80将您机器的 8080 端口映射到容器的 80 端口

  • 使用--name webserver-test为您的容器分配一个名称

  • 获取 NGINX Docker 镜像并使容器运行此镜像。

现在,打开您喜欢的浏览器,导航到http://localhost:8080,您将看到一个默认的 NGINX 页面。

常见的管理任务

通过在终端上执行docker ps,您可以查看正在运行的容器。

上述命令让我们更深入地了解了在您的机器上运行的容器,它们正在使用的镜像,它们的创建时间,状态以及端口映射或分配的名称。

玩完容器后,是时候停止它了。执行docker stop webserver-test,容器将结束它的生命周期。

糟糕!您需要再次使用相同的容器。没问题,因为简单的docker start webserver-test将再次为您启动容器。

现在,是时候停止并删除容器了,因为您不再需要它。在您的终端上执行docker rm -f webserver-test就可以了。请注意,此命令将删除容器,但不会删除我们使用过的下载的镜像。对于最后一步,您可以执行docker rmi nginx

版本控制 - Git 与 SVN

版本控制是一种工具,它可以帮助您回顾以前的源代码版本,以便检查和处理它们;它与使用的语言或技术无关,并且可以在所有以纯文本开发的软件中使用版本控制。

我们可以将版本控制工具分类为以下几类:

  • 集中式版本:控制系统需要一个集中的服务器来工作,所有开发人员都需要连接到它,以便从中同步和下载更改。

  • 分布式版本:控制系统不是集中的;换句话说,每个开发人员在自己的机器上拥有整个管理版本控制系统,因此可以在本地工作,然后与公共服务器或每个开发人员同步。分布式版本控制系统DVCS)更快,因为它们在集中或共享服务器上需要更少的更改。

Subversion(SVN)是一个集中式版本控制系统,因此一些开发人员认为这是尊重整个项目工作的最佳方式,因此开发人员只需在一个地方编写和读取访问控制器。

整个代码都托管在一个地方,因此可能认为这样更容易理解 SVN 而不是 Git。事实是 SVN 命令行更简单,而且有更多的 GUI 可用于 SVN。原因很明显:SVN 自 2000 年以来就存在,而 Git 是 5 年后才出现的。

SVN 的另一个优点是版本编号系统更清晰;它使用顺序编号系统(1,2,3,4…),而 Git 使用更难以阅读和理解的 SHA-1 代码。

最后,使用 SVN 可以获得一个子目录来处理它,而无需拥有整个项目。对于小项目来说,这不是问题,但对于大项目来说可能会很困难。

Git

在本书中,我们将使用 Git 进行版本控制。我们做出这个决定是因为 Git 绝对更快,更轻量级(占用的磁盘空间比 SVN 少 30 倍)。此外,Git 已成为 Web 开发版本控制的标准,我们的目标是基于 PHP 创建一个基于微服务的应用程序,因此 Git 是该项目的一个很好的解决方案。

Git 的优点如下:

  • 分支比 SVN 更轻量级

  • Git 比 SVN 快得多

  • Git 从一开始就是一个分布式版本控制系统,因此开发人员对其本地拥有完全控制权

  • Git 在分支和合并方面提供更好的审计

在随后的章节中,我们将在我们的项目中使用 Git 命令,解释每一个并给出示例,但在那之前,让我们先看一下基本的命令:

如何创建一个新存储库:创建一个新文件夹,打开它,并在其中执行 Git 来创建一个新的 Git 存储库。

如何检出现有存储库:通过执行 Git clone /path/to/repository从存储库创建一个本地副本。如果您正在使用远程服务器(托管集中式服务器将在以下行中解释),则执行 Git clone username@host:/path/to/repository

要理解 Git 的工作流程,有必要知道有三种不同的树:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 工作目录:包含您的项目文件

  • INDEX:这起到中间区域的作用;文件将在此处,直到它们被提交

  • HEAD:指向最后一次提交

将文件添加到索引并提交它们是简单的任务。在项目中使用文件并对其进行更改后,您必须将它们添加到索引:

  • 如何将文件添加到索引:建议在将文件添加到索引之前检查文件。您可以通过执行git diff <file>来做到这一点。它会显示您添加和删除的行,以及有关您修改的文件的一些更有趣的信息。一旦您对所做的更改满意,可以执行git add <filename>来添加特定文件,或者将 Git 添加到您修改过的所有文件中。

  • 如何将文件添加到 HEAD:一旦您在索引中包含了所有必要的文件,您必须提交它们。您可以通过执行git commit -m "提交消息"来做到这一点。现在文件已经包含在您的本地副本的 HEAD 中,但它们仍未在远程存储库中。

  • 如何将更改发送到远程存储库:要将包含在您的本地副本的 HEAD 中的更改发送到远程存储库,执行git push origin <branch name>;您可以选择要包含更改的分支,例如 master。如果您没有克隆现有存储库,并且想要将本地存储库连接到远程存储库,执行git remote add origin <server>

分支用于开发隔离的功能,并且可以在将来与主分支合并。创建新存储库时的默认分支称为 master。工作流程概述将如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 如何创建新分支:一旦您在要创建新分支的分支中,执行git checkout -b new_feature

  • 如何更改分支:您可以通过执行git checkout <branch name>来浏览分支。

  • 如何删除分支:您可以通过执行git branch -d new_feature来删除分支

  • 如何使您的分支对所有人可用:在将您的分支上传到远程存储库之前,分支将不会对其他开发人员可用,执行git push origin <branch>

如果您想要将本地副本更新为远程存储库上的更改,您可以执行git fetch来检查是否有任何新的更新,然后执行git pull来获取该更新。

要将您的活动分支与不同的分支合并,执行git merge <branch>,Git 将尝试融合两个分支,但有时,如果两个或更多的开发人员更改了相同的文件,可能会出现冲突,您需要在合并之前手动解决这些冲突,然后再次将修改后的文件放入 INDEX 中。

如果失败,也许您想要丢弃本地更改并再次从存储库获取更改。您可以通过执行git checkout -- <filename>来做到这一点。如果您想要丢弃所有本地更改和提交,执行git fetch origingit reset --hard origin/master

托管

当我们在团队中工作时,也许我们希望有一个带有集中式服务器的公共存储库。请记住,Git 是一个分布式版本控制系统,不需要使用一个地方来集中存储代码,但是如果出于不同的原因想要使用它,我们将看看两个著名的地方。

托管为您提供了使用 Web 界面更好地管理存储库的方式。

GitHub

GitHub 是大多数开发人员选择托管代码的地方。它基于 Git,像 Twitter 和 Facebook 这样的公司使用这项服务来发布他们的开源项目。GitHub 在短短几年内成为最著名的源代码托管地,并且目前,许多公司在技术面试之前要求候选人提供他们的 GitHub 存储库。

这个托管对所有开发人员免费;他们可以创建无限的项目,有无限的合作者,唯一的条件是项目需要是开源和公共的。如果您想要一个私有项目,您将不得不付费。

在 GitHub 上将您的项目公开是向世界展示您的项目并利用庞大的 GitHub 社区的好机会。可以寻求帮助,因为有许多注册和活跃的开发人员。

您可以访问官方网站github.com/

BitBucket

BitBucket 是托管项目的另一个选择。它使用 Git,但您也可以使用 Mercurial。界面与 GitHub 非常相似。BitBucket 的一个巨大优势是 Atlassian 公司使其成为可能。它在其托管中包含许多开发人员的功能,例如集成其他 Atlassian 工具的可能性或一个小型的持续交付工具,允许您构建,测试和部署应用程序。

这个地方是免费的,无论您想要的项目是公共的还是私有的。唯一的限制是每个项目只允许五个合作者;如果您需要更多人参与您的项目,您将不得不付费。

官方网站bitbucket.org/

版本控制策略

当您开发应用程序时,保持代码整洁是很重要的,但当您与其他开发人员一起工作时更重要。在本节中,我们将为您介绍一些最常见的版本控制策略,您可以在项目中使用。

集中式工作

这种策略对于以前使用 SVN(老派)或类似版本控制的开发人员来说是最常见的。与 Subversion 一样,项目托管在一个具有唯一入口点的中央存储库中。除了主分支(在 SVN 中为主干)之外,这种策略不需要更多的分支。

开发人员在本地机器上克隆整个项目,对项目进行工作,然后提交更改。当他们想要发布更改时,他们执行推送。

功能分支工作流

这是从集中式工作的下一步。它也与中央存储库一起工作,但开发人员在其副本中创建一个本地功能分支,并且这个分支也会发布到中央存储库,因此所有开发人员都有机会参与该功能。分支将具有描述性名称或问题编号。

在这种策略中,主分支永远不包含错误,因此这对于持续集成是一个很大的改进。此外,为每个功能设置特定的分支是一个很好的封装,以免干扰主代码库。

Gitflow 工作流

Gitflow 工作流不会添加比功能分支工作流更多的新概念。它只为每个分支分配不同的角色。Gitflow 工作流也与中央存储库一起工作,开发人员在其中创建分支,就像功能分支工作流一样。然而,这些分支有特定的功能,例如开发、发布或功能。因此,功能分支将与特定发布合并,然后与主分支合并。这样,同一个项目可以有不同的发布版本。

这种策略适用于大型项目或需要发布的项目。

分支工作流

最后一种策略与本章中我们看过的其他策略非常不同。与从集中服务器克隆副本并在其上工作不同,这种策略为每个开发人员提供了一个分支。这意味着每个开发人员都有项目的两个副本:一个私有副本和一个服务器端副本。

一旦开发人员做出他们想要的更改,它们将被发送给项目维护者进行审查和检查,以确保它们不会破坏项目,然后它们将被合并到主存储库中。

这种策略适用于开源项目,因此开发人员不能破坏当前项目。

语义化版本控制

在我们的微服务或 API 中拥有一个版本控制系统非常重要。这使得消费者和您自己都能够拥有一个一致的版本控制系统,以便每个人都能知道发布或功能的重要性。

将版本号设置为 MAJOR.MINOR.PATCH

  1. MAJOR 在不兼容的 API 更改时会增加,因此开发人员需要废弃当前的 API 版本并使用新版本。

  2. MINOR 在添加新功能并且与当前代码兼容时会增加。因此,开发人员不需要改变整个 API 来更新当前版本。

  3. PATCH 在对当前版本进行新的错误修复时会增加。

这是语义化版本控制的摘要,但您可以在 semver.org/ 找到更多信息。

为微服务设置开发环境

使用 Docker 及其容器生态系统的最大好处之一是您不需要在计算机上安装任何其他东西。例如,如果您需要一个 MySQL 数据库,您不需要在本地开发环境上安装任何东西;只需轻松创建一个包含所需版本的容器并开始使用它。

这种开发方式更加灵活,因此我们将在整本书中都使用 Docker 容器。在本节中,我们将学习如何构建一个基本的 Docker 环境;这将是我们的基础,并且我们将在随后的章节中改进和调整这个基础以适应我们的每个微服务。

为了简化我们项目的文件夹结构,我们将在开发机器上有一些根文件夹:

  • Docker:此文件夹将包含所有 Docker 环境。

  • Source:这个文件夹将包含我们每个微服务的源代码!为微服务设置开发环境

请注意,这个结构是灵活的,可以根据您的具体要求进行更改和调整,而不会出现任何问题。

所有所需的文件都可以在我们的 GitHub 存储库上找到,位于chapter-02标签的主分支上,网址为github.com/php-microservices/docker

让我们深入了解 Docker 设置。打开您的 docker 文件夹,并创建一个名为docker-compose.yml的文件,内容如下:

    version: '2'
    services:

这两行表示我们正在使用 Docker compose 的最新语法,并且它们定义了我们每次执行docker-compose up时将会启动的服务列表。我们所有的服务都将在services声明之后添加。

自动发现服务

自动发现是一种机制,我们不指定每个微服务的端点。我们的每个服务都使用一个共享注册表,它们在其中声明自己是可用的。当一个微服务需要知道另一个微服务的位置时,它可以查询我们的自动发现注册表以了解所需的端点。

对于我们的应用程序,我们将使用自动发现机制来确保我们的微服务可以轻松扩展,如果一个节点不健康,我们将停止向其发送请求。我们选择使用 Consul(由 HashiCorp 提供),这是一个非常小的应用程序,我们可以将其添加到我们的项目中。我们的 Consul 容器的主要作用是保持一切井然有序,保持可用和健康服务的列表。

让我们通过打开您喜欢的 IDE/editor 的docker-compose.yml文件并在services:行之后添加下一段代码来开始项目:

    version: '2'
    services:
        autodiscovery:
            build: ./autodiscovery/
            mem_limit: 128m
            expose:
                - 53
                - 8300
                - 8301
                - 8302
                - 8400
                - 8500
            ports:
                - 8500:8500
            dns:
                - 127.0.0.1

在 Docker compose 文件中,语法非常容易理解,并且始终遵循相同的流程。第一行定义了一个容器类型(对于开发人员来说,它就像一个类名);在我们的情况下,它是autodiscovery,在这个容器类型内部,我们可以指定几个选项,以适应我们的要求。

通过build: ./autodiscovery/,我们告诉 Docker 在哪里可以找到一个描述我们容器详细信息的 Dockerfile。

mem_limit: 128m这句话将限制autodiscovery类型的任何容器的内存消耗不超过 128 Mb。请注意,这个指令是可选的。

每个容器都需要打开不同的端口,默认情况下,当您启动一个容器时,这些端口都是关闭的。因此,您需要指定每个容器要打开哪些端口。例如,一个带有 Web 服务器的容器将需要打开端口 80,但对于运行 MySQL 的容器,所需的端口可能是3306。在我们的情况下,我们为我们的每个autodiscovery容器打开了端口5383008301830284008500

如果您尝试在打开的端口之一上访问容器,它将无法工作。容器生态系统位于一个单独的网络中,只有在您的环境和 Docker 网络之间创建一个桥接时,您才能访问它。我们的autodiscovery容器运行 Consul,并且在端口8500上有一个很好的 Web UI。我们希望能够使用这个 UI;因此,当我们使用ports时,我们将我们本地的8500端口映射到容器的8500端口。

现在,是时候在与您的docker-compose.yml文件相同的路径下创建一个名为autodiscovery的新文件夹了。在这个新文件夹中,放置一个名为Dockerfile的文件,内容如下:

    FROM consul:v0.7.0 

Dockerfile中的这个小句子表明我们正在使用一个带有标签v0.7.0的 Docker consul镜像。这个镜像将从官方 Docker hub 获取,这是一个容器镜像的存储库。

此时,执行$ docker-compose up将启动一个 Consul 机器,请试一试。由于我们没有指定-d选项,Docker 引擎将把所有日志输出到您的终端。您可以通过简单的CTRL+C来停止您的容器。当您添加-d选项时,Docker compose 将作为守护进程运行并返回提示符;您可以执行$ docker-compose stop来停止容器。

微服务基础核心 - NGINX 和 PHP-FPM

PHP-FPM 是在我们的 Web 服务器中执行 PHP 的一种替代方法。使用 PHP-FPM 的主要好处是其小的内存占用和在高负载下的高性能。您可以找到的最好的 Web 服务器来运行您的 PHP-FPM 是 NGINX,这是一个非常轻量级的 Web 服务器和反向代理,用于最重要的项目。

由于我们的应用程序将使用自动发现模式,我们需要一种简单的方法来处理服务的注册、注销和健康检查。您可以使用的最简单和最快的应用程序之一是 ContainerPilot,这是由 Joyent 创建的一个小型微服务编排应用程序,可以与您喜欢的容器调度程序一起使用,在我们的案例中是 Docker compose。这个小应用程序作为 PID 1 执行,并在容器内部运行我们想要运行的应用程序。

我们将使用 ContainerPilot,因为它可以减轻开发人员处理自动发现的负担,所以我们需要在我们将要使用的每个容器上都安装最新版本。

让我们开始定义我们的基础php-fpm容器。打开docker-compose.yml,为php-fpm添加一个新的服务:

    microservice_base_fpm: 
      build: ./microservices/base/php-fpm/ 
    links: 
      - autodiscovery 
    expose: 
      - 9000 
    environment: 
      - BACKEND=microservice_base_nginx 
      - CONSUL=autodiscovery 

在上述代码中,我们正在定义一个新的服务,一个有趣的属性是链接。这个属性定义了我们的服务可以看到或连接哪些其他容器。在我们的例子中,我们希望将这种类型的容器链接到任何autodiscovery容器。如果没有这个明确的定义,我们的fpm容器将看不到autodiscovery服务。

现在,在您的 IDE/编辑器中创建microservices/base/php-fpm/Dockerfile文件,内容如下:

    FROM php:7-fpm 

    RUN apt-get update && apt-get -y install \ 
      git g++ libcurl4-gnutls-dev libicu-dev libmcrypt-dev 
      libpq-dev libxml2-dev 
      unzip zlib1g-dev \ 
      && git clone -b php7 
      https://github.com/phpredis/phpredis.git 
      /usr/src/php/ext/redis \
      && docker-php-ext-install curl intl json mbstring 
      mcrypt pdo pdo_pgsql 
      redis xml \ 
      && apt-get autoremove && apt-get autoclean \ 
      && rm -rf /var/lib/apt/lists/* 

    RUN echo 'date.timezone="Europe/Madrid"' >>  
      /usr/local/etc/php/conf.d/date.ini 
    RUN echo 'session.save_path = "/tmp"' >>  
      /usr/local/etc/php/conf.d/session.ini 

    ENV CONSUL_TEMPLATE_VERSION 0.16.0 
    ENV CONSUL_TEMPLATE_SHA1  
    064b0b492bb7ca3663811d297436a4bbf3226de706d2b76adade7021cd22e156 

    RUN curl --retry 7 -Lso /tmp/consul-template.zip \ 
      "https://releases.hashicorp.com/
      consul-template/${CONSUL_TEMPLATE_VERSION}/
      consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip" \ 
    && echo "${CONSUL_TEMPLATE_SHA1}  /tmp/consul-template.zip" 
    | sha256sum -c \ 
    && unzip /tmp/consul-template.zip -d /usr/local/bin \ 
    && rm /tmp/consul-template.zip 

    ENV CONTAINERPILOT_VERSION 2.4.3 
    ENV CONTAINERPILOT_SHA1 2c469a0e79a7ac801f1c032c2515dd0278134790 
    ENV CONTAINERPILOT file:///etc/containerpilot.json 

    RUN curl --retry 7 -Lso /tmp/containerpilot.tar.gz \ 
      "https://github.com/joyent/containerpilot/releases/download/
      ${CONTAINERPILOT_VERSION}/containerpilot-
      ${CONTAINERPILOT_VERSION}.tar.gz" 
      \ 
      && echo "${CONTAINERPILOT_SHA1}  /tmp/containerpilot.tar.gz" 
      | sha1sum -c \ 
      && tar zxf /tmp/containerpilot.tar.gz -C /usr/local/bin \ 
      && rm /tmp/containerpilot.tar.gz 

    COPY config/ /etc 
    COPY scripts/ /usr/local/bin 

    RUN chmod +x /usr/local/bin/reload.sh 

    CMD [ "/usr/local/bin/containerpilot", "php-fpm", "--nodaemonize"] 

在这个文件中,我们告诉 Docker 如何创建我们的php-fpm容器。第一行声明了我们想要用作容器基础的官方版本,这里是 php7 fpm。一旦镜像下载完成,第一个RUN行将添加我们将使用的所有额外的PHP包。

这两个RUN语句将添加定制的 PHP 配置;请随时根据您的需求调整这些行。

一旦所有的 PHP 任务完成,就是时候在容器上安装一个小应用程序来帮助我们处理模板–consul-template。这个应用程序用于使用我们在 Consul 服务上存储的信息动态构建配置模板。

正如我们之前所说,我们正在使用 ContainerPilot。因此,在consul-template安装之后,我们正在告诉 Docker 如何安装这个应用程序。

此时,Docker 完成了安装所有所需的软件包,并复制了一些 ContainerPilot 所需的配置和 shell 脚本。

最后一行将 ContainerPilot 作为 PID 1 启动,并分叉php-fpm

现在,让我们解释 ContainerPilot 需要的配置文件。打开您的 IDE/编辑器,并创建microservices/base/php-fpm/config/containerpilot.json文件,内容如下:

    { 
      "consul": "{{ if .CONSUL_AGENT }}localhost{{ else }}{{ .CONSUL }}
      {{ end }}:8500", 
      "preStart": "/usr/local/bin/reload.sh preStart", 
      "logging": {"level": "DEBUG"}, 
      "services": [ 
        { 
          "name": "microservice_base_fpm", 
          "port": 80, 
          "health": "/usr/local/sbin/php-fpm -t", 
          "poll": 10, 
          "ttl": 25, 
          "interfaces": ["eth1", "eth0"] 
        } 
      ], 
      "backends": [ 
        { 
          "name": "{{ .BACKEND }}", 
          "poll": 7, 
          "onChange": "/usr/local/bin/reload.sh" 
        } 
      ], 
      "coprocesses": [{{ if .CONSUL_AGENT }} 
        { 
          "command": ["/usr/local/bin/consul", "agent", 
            "-data-dir=/var/lib/consul", 
            "-config-dir=/etc/consul", 
            "-rejoin", 
            "-retry-join", "{{ .CONSUL }}", 
            "-retry-max", "10", 
          "-retry-interval", "10s"], 
          "restarts": "unlimited" 
        }
      {{ end }}] 
    } 

这个 JSON 配置文件非常容易理解。首先,它定义了我们可以在哪里找到我们的 Consul 容器,以及我们希望在 ContainerPilot 的 preStart 事件上运行哪个命令。在services中,您可以定义所有当前容器正在运行的服务。在backends中,您可以定义所有您正在监听更改的服务。在我们的案例中,我们正在监听名为microservice_base_nginx的服务的更改(BACKEND变量在docker-compose.yml中定义)。如果 Consul 上这些服务发生了变化,我们将在容器中执行onChange命令。

有关 ContainerPilot 的更多信息,您可以访问官方页面,即www.joyent.com/containerpilot

是时候创建microservices/base/php-fpm/scripts/reload.sh文件,内容如下:

    #!/bin/bash 

    SERVICE_NAME=${SERVICE_NAME:-php-fpm} 
    CONSUL=${CONSUL:-consul} 
    preStart() { 
      echo "php-fpm preStart" 
    } 

    onChange() { 
      echo "php-fpm onChange" 
    } 

    help() { 
      echo "Usage: ./reload.sh preStart  
      => first-run configuration for php-fpm" 
      echo "      ./reload.sh onChange  
      => [default] update php-fom config on 
      upstream changes" 
    } 

    until 
      cmd=$1 
      if [ -z "$cmd" ]; then 
             onChange 
      fi 
      shift 1 
      $cmd "$@" 
      [ "$?" -ne 127 ] 
    do 
      onChange 
      exit 
    done 

在这里,我们创建了一个虚拟脚本,但是你可以根据自己的需求进行调整。例如,它可以更改为run execute consul-template并在 ContainerPilot 触发脚本后重新构建 NGINX 配置。我们将在以后解释更复杂的脚本。

我们的基本php-fpm容器已经准备就绪,但是我们的基本环境如果没有 web 服务器是不完整的。我们将使用 NGINX,一个非常轻巧和强大的反向代理和 web 服务器。

我们将构建我们的 NGINX 服务器的方式与php-fpm非常相似,因此我们只会解释其中的区别。

提示

请记住,所有文件都可以在我们的 GitHub 存储库中找到。

我们将在docker-compose.yml文件中为 NGINX 添加一个新的服务定义,并将其链接到我们的autodiscovery服务以及我们的php-fpm

    microservice_base_nginx: 
      build: ./microservices/base/nginx/ 
      links: 
        - autodiscovery 
        - microservice_base_fpm 
      environment: 
        - BACKEND=microservice_base_fpm 
        - CONSUL=autodiscovery 
      ports: 
        - 8080:80 

在我们的microservices/base/nginx/config/containerpilot.json中,现在有一个新选项telemetry。此配置设置允许我们指定用于收集我们服务的统计信息的远程遥测服务。包含这种类型的服务在我们的环境中可以让我们看到我们的容器的性能如何:

    "telemetry": { 
      "port": 9090, 
      "sensors": [ 
        { 
          "name": "nginx_connections_unhandled_total", 
          "help": "Number of accepted connnections that were not handled", 
          "type": "gauge", 
          "poll": 5, 
          "check": ["/usr/local/bin/sensor.sh", "unhandled"] 
        }, 
        { 
          "name": "nginx_connections_load", 
          "help": "Ratio of active connections (less waiting) to 
          the maximum  
          worker connections", 
          "type": "gauge", 
          "poll": 5, 
          "check": ["/usr/local/bin/sensor.sh", "connections_load"] 
        }
      ]
    } 

正如您所看到的,我们使用一个定制的 bash 脚本来获取容器统计信息,我们的microservices/base/nginx/scripts/sensor.sh脚本的内容如下:

    #!/bin/bash 
    set -e 

    help() { 
      echo 'Make requests to the Nginx stub_status endpoint and 
      pull out metrics' 
      echo 'for the telemetry service. Refer to the Nginx docs 
      for details:' 
      echo 'http://nginx.org/en/docs/http/ngx_http_stub_status_module.html' 
    } 

    unhandled() { 
      local accepts=$(curl -s --fail localhost/nginx-health | awk 'FNR == 3 
      {print $1}') 
      local handled=$(curl -s --fail localhost/nginx-health | awk 'FNR == 3 
      {print $2}') 
      echo $(expr ${accepts} - ${handled}) 
    } 

    connections_load() { 
      local scraped=$(curl -s --fail localhost/nginx-health) 
      local active=$(echo ${scraped} 
      | awk '/Active connections/{print $3}') 
      local waiting=$(echo ${scraped} | awk '/Reading/{print $6}') 
      local workers=$(echo $(cat /etc/nginx/nginx.conf | perl -n -e'/
      worker_connections *(\d+)/ && print $1') ) 
      echo $(echo "scale=4; (${active} - ${waiting}) / ${workers}" | bc) 
    } 

    cmd=$1 
    if [ ! -z "$cmd" ]; then 
      shift 1 
      $cmd "$@" 
      exit 
    fi 

    help 

这个 bash 脚本获取了一些我们将使用 ContainerPilot 发送到我们的telemetry服务器的nginx统计信息。

我们的microservices/base/nginx/scripts/reload.sh比我们之前为php-fpm创建的要复杂一些:

    #!/bin/bash 

    SERVICE_NAME=${SERVICE_NAME:-nginx} 
    CONSUL=${CONSUL:-consul} 

    preStart() { 
      consul-template \ 
            -once \ 
            -dedup \ 
            -consul ${CONSUL}:8500 \ 
            -template "/etc/nginx/nginx.conf.ctmpl:/etc/nginx/nginx.conf" 
    } 

    onChange() { 
      consul-template \ 
            -once \ 
            -dedup \ 
            -consul ${CONSUL}:8500 \ 
            -template "/etc/nginx/nginx.conf.ctmpl:/etc/nginx/
            nginx.conf:nginx -s reload" 
    } 

    help() { 
      echo "Usage: ./reload.sh preStart  
      => first-run configuration for Nginx" 
      echo "      ./reload.sh onChange  => [default] update Nginx config on 
      upstream changes" 
    } 

    until 
      cmd=$1 
      if [ -z "$cmd" ]; then 
             onChange 
      fi 
      shift 1 
      $cmd "$@" 
      [ "$?" -ne 127 ] 
    do 
       onChange 
       exit 
    done 

正如您所看到的,我们使用consul-template在启动时或当 ContainerPilot 检测到我们将要监视的后端服务列表中的更改时重建我们的 NGINX 配置。这种行为允许我们停止向不健康的节点发送请求。

在这一点上,我们的基本环境已经准备就绪,我们准备用一个简单的$ docker-compose up来测试它。我们将使用所有这些部件来创建更大更复杂的服务。在接下来的章节中,我们将添加 telemetry 服务或数据存储等。

微服务框架

框架是我们可以用于软件开发的骨架。使用框架将帮助我们在应用程序中使用标准和稳健的模式,使其更稳定并为其他开发人员所熟知。PHP 有许多不同的框架可以在您的日常工作中使用。我们将看到一些最常见框架上使用的标准,以便您可以为您的项目选择最佳的。

PHP-FIG

多年来,PHP 社区一直在开发自己的项目并遵循自己的规则。自 PHP 最初的几年以来,已经发布了成千上万个不同的项目,由不同的开发人员开发,并且没有遵循任何共同的标准。

这对 PHP 开发人员来说是一个问题,首先是因为他们无法知道构建应用程序所遵循的步骤是否正确。只有他们自己的经验和互联网可以帮助开发人员猜测他们的代码是否正确编写,并且将来是否可读。

其次,开发人员觉得他们在试图重复造轮子。由于没有标准适应第三方应用程序到他们的项目中,开发人员为他们的项目制作了相同的现有应用程序。

2009 年,PHP 框架互操作性组PHP-FIG)诞生,其主要目标是为 PHP 开发创建一个统一的标准。PHP-FIG 是一个由成员组成的大社区,致力于PHP 标准推荐PSR),讨论如何最好地使用 PHP 语言。

PHP-FIG 得到大型项目的支持,如 Drupal,Magento,Joomla!,Phalcon,CakePHP,Yii,Zend 和 Symfony,这就是为什么它们提出的 PSR 被 PHP 框架实现的原因。

一些标准,如 PSR-1 和 PSR-2,涉及代码的使用和样式(使用制表符或空格,PHP 的开放标签,使用驼峰命名法或文件名),其他标准涉及自动加载(PSR-0 然后 PSR-4)。自 PHP 5.3 以来,命名空间已经包含,并且实现自动加载是最重要的事情。

自动加载可能是 PHP 中最重要的改进之一。在 PHP-FIG 之前,框架有它们自己的方法来实现自动加载,它们自己的格式化方式,初始化方式和命名方式,每个都不同,所以这是一场灾难(Java 已经通过其 bean 系统解决了这个问题)。

最后,Composer 实现了自动加载,这是由 PHP-FIG 编写的。因此,开发人员不再需要担心require_once()include_once()require()include()

您可以在www.php-fig.org/找到有关 PHP-FIG 的更多信息。

PSR-7

在本书中,我们将使用PHP 标准推荐 7PSR-7)。它涉及 HTTP 消息接口。这是 Web 开发的本质;这是与服务器通信的方式。HTTP 客户端或 Web 浏览器向服务器发送 HTTP 请求消息,服务器会回复一个 HTTP 响应消息。

这些类型的消息对普通用户是隐藏的,但开发人员需要了解结构以操纵它们。PSR-7 讨论了推荐的操纵方式,简单明了地进行操作。

我们将使用 HTTP 消息与微服务进行通信,因此有必要了解它们的工作原理和结构。

HTTP 请求具有以下结构:

 **POST** /path **HTTP/**1.1 Host: example.com
    foo=bar&baz=bat

在请求中,有所有必要的东西让服务器理解请求消息并能够回复。在第一行中,我们可以看到用于请求的方法(GETPOSTPUTDELETE),请求目标和 HTTP 协议版本,然后是一个或多个 HTTP 头部,一个空行和主体。

HTTP 响应将如下所示:

 **HTTP/**1.1 200 **OK** Content-Type: text/plain

这是响应主体。响应包含 HTTP 协议版本,例如请求和 HTTP 状态代码,后面跟着一个描述代码的文本。您将在接下来的章节中找到所有可用的状态代码。其余的行与请求类似:一个或多个 HTTP 头部,一个空行,然后是主体。

一旦我们了解了 HTTP 请求消息和 HTTP 响应消息的结构,我们将了解 PHP-FIG 关于 PSR-7 的建议。

消息

任何消息都是 HTTP 请求消息(RequestInterface)或 HTTP 响应消息(ResponseInterface)。它们扩展了 MessageInterface 并且可以直接实现。实现者应该实现 RequestInterface 和 ResponseInterface。

头部

标题字段名称不区分大小写:

    $message = $message->withHeader('foo', 'bar');
    echo $message->getHeaderLine('foo');
    // Outputs: bar
    echo $message->getHeaderLine('FoO');
    // Outputs: bar

具有多个值的标题:

    $message = $message ->withHeader('foo', 'bar') ->
    withAddedHeader('foo', 'baz');
    $header = $message->getHeaderLine('foo'); 
    // $header contains: 'bar, baz'
    $header = $message->getHeader('foo'); // ['bar', 'baz']
主机头

通常,主机头与 URI 的主机组件相同,并且在建立 TCP 连接时使用的主机相同,但可以进行修改。

RequestInterface::withUri()将替换请求主机头。

通过传递第二个参数为 true,您可以保持主机的原始状态;它将保留主机头,除非消息没有主机头。

当读取或写入数据流时,StreamInterface 用于隐藏实现细节。

流使用以下三种方法公开其功能:

    isReadable()
    isWritable()
    isSeekable()
请求目标和 URI

请求目标位于请求行的第二部分:

    origin-form
    absolute-form
    authority-form
    asterisk-form

getRequestTarget()方法将使用 URI 对象来生成原始形式(最常见的请求目标)。

使用withRequestTarget(),用户可以使用其他三个选项。例如,星号形式,如下所示:

    $request = $request
    ->withMethod('OPTIONS')
    ->withRequestTarget('*')
    ->withUri(new Uri('https://example.org/'));

HTTP 请求将如下所示:

    OPTIONS * HTTP/1.1
服务器端请求

RequestInterface 为 HTTP 请求提供了一般表示,但服务器端请求需要被处理成通用网关接口CGI)。PHP 通过其超全局变量提供了简化:

    $_COOKIE
    $_GET
    $_POST
    $_FILES
    $_SERVER

ServerRequestInterface 提供了对这些超全局变量的抽象,以减少对超全局变量的消费者的耦合。

上传的文件

$_FILES超全局变量在处理数组或文件输入时存在一些众所周知的问题。例如,使用输入名称files,提交 files[0]和 files[1],PHP 将表示如下:

    array(    
      'files' => array(        
        'name' => array(            
          0 => 'file0.txt',              
          1 => 'file1.html',        
        ),
        'type' => array(
          0 => 'text/plain',             
          1 => 'text/html',
        ),        
    /* etc. */    ), )

预期的表示如下:


    array( 
      'files' => array(
        0 => array(
        'name' => 'file0.txt',
        'type' => 'text/plain',
      /* etc. */        ),
      1 => array(
        'name' => 'file1.html',
        'type' => 'text/html',
        /* etc. */        
      ),    
    ), )

因此,客户需要了解这些问题,并编写代码来修复给定的数据。

getUploadedFiles()为消费者提供了规范化的结构。

您可以在www.php-fig.org/psr/psr-7/找到更详细的信息和我们讨论的接口和类。

中间件

中间件是一种过滤应用程序的 HTTP 请求的机制;它允许您为业务逻辑添加额外的层。它在我们想要到达的代码片段之前和之后执行,以处理输入和输出通信。中间件使用 PSR-7 的建议来修改 HTTP 请求。这就是为什么 PSR-7 和中间件是相结合的原因。

中间件的最常见示例是在身份验证中。在需要登录以获取用户权限的应用程序中,中间件将决定用户是否可以查看应用程序的特定内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在前面的图片中,我们可以看到一个典型的 PSR-7 HTTP 请求响应,带有两个中间件。通常,您会将中间件实现为 lambda(λ)。

现在,我们将看一些典型中间件实现的示例:

    use Psr\Http\Message\ResponseInterface; 
    use Psr\Http\Message\ServerRequestInterface;  
    function (ServerRequestInterface $request, ResponseInterface $response, 
    callable $next = null) 
    {    
      // Do things before the program itself and the next middleware   
      // If exists next middleware call it and get its response    
      if (!is_null($next)) {  
      $response = $next($request, $response);    }    
      // Do things after the previous middleware has finished    
      // Return response    
      return $response; 
    }

requestresponse是对象,最后一个参数$next是要调用的下一个中间件的名称。如果中间件是最后一个,可以将其留空。在代码中,我们可以看到三个不同的部分:第一部分是在下一个中间件之前修改和执行操作,第二部分是中间件调用下一个中间件(如果存在),第三部分是在上一个中间件之后修改和执行操作。

在看到$next$request$response)形式之前和之后的代码是一种良好的实践,就像中间件周围的洋葱层一样,但是必须对中间件的执行顺序保持警惕。

另一个良好的实践是将真实应用程序(通常是中间件到达的代码部分,通常是控制器函数)也视为中间件,因为它接收请求和响应,并且必须返回响应,但这次没有下一个参数,因为它是最后一个;然而,执行在此之后继续。这就是我们必须以与最后一个中间件相同的方式查看最终代码的原因。

我们将看到一个完整的示例,以了解如何在基于微服务的应用程序中使用它。正如我们在前面的图片中看到的,有两个中间件,我们将称它们为第一个和第二个,然后结束函数将被调用 endfunction:

    use Psr\Http\Message\ResponseInterface; 
    use Psr\Http\Message\ServerRequestInterface; 
    $first = function (ServerRequestInterface $request, 
    ResponseInterface $response, callable $next) 
    {
      $request = $request->withAttribute('word', 'hello');   
      $response = $next($request, $response);    
      $response = $response->withHeader('X-App-Environment', 
      'development');
      return $response; 
    } class Second {    
      public function __invoke(ServerRequestInterface $request, 
      ResponseInterface $response, callable $next) 
      {
        $response->getBody()->write('word:'. 
        $request->getAttribute('word'));        
        $response = $next($request, $response);       
        $response = $response->withStatus(200, 'OK');
        return $response;    
      } 
    }
    $second = new Second; $endfunction = function (
    ServerRequestInterface $request, ResponseInterface $response) 
    {
      $response->getBody()->write('function reached');   
      return $response; 
    }

每个框架都有自己的中间件处理程序,但每个处理程序的工作方式都非常相似。中间件处理程序用于管理堆栈,因此您可以在其上添加更多中间件,并且它们将按顺序调用。这是一个通用的中间件处理程序:

    class MiddlewareHandler {
    public function __construct()
    { //this depends the framework and you are using
        $this->middlewareStack = new Stack; 
        $this->middlewareStack[] = $first;        
        $this->middlewareStack[] = $second; 
        $this->middlewareStack[] = $endfunction;
    }
    ... }

执行轨迹将是这样的:

  1. 用户向服务器请求特定路径。例如,/

  2. 执行first中间件,添加word等于hello

  3. first中间件将执行发送到second

  4. second中间件添加句子word: hello

  5. second中间件将执行发送到end函数。

  6. end函数添加句子function reached并完成自己的工作。

  7. 执行将继续由second中间件进行,这个中间件会将 HTTP 状态设置为 200 以响应。

  8. 执行将继续由first中间件进行,这个中间件会添加一个自定义标头。

  9. 响应将返回给用户。

当然,如果在中间件执行期间出现错误,您可以使用以下常见代码进行管理:

    try { // Things to do } 
    catch (\Exception $e) { 
      // Error happened return 
      $response->withStatus(500); 
    }

响应将立即发送给用户,而不会结束整个执行。

可用的框架

有数百个可用于开发应用程序的框架,但当您需要一个用于制作微框架的框架时,有必要寻找一些特点:

  • 一个能够处理最大请求数的微框架

  • 在内存方面轻量级

  • 如果可能的话,有一个伟大的社区为该框架开发应用程序

现在我们将看一下可能是目前使用最多的五个框架。找到最好的框架不是一个独特的观点,每个开发者都有自己的观点,所以让我向您介绍以下几个:

框架每秒请求峰值内存
Phalcon 2.01,746.900.27
Slim 2.6880.240.47
Lumen 5.1412.360.95
Zend Expressive 1.0391.970.80
Silex 1.3383.660.86

来源PHP 框架基准测试。该项目试图在现实世界中测量 PHP 框架的最小开销。

Phalcon

Phalcon 是一个流行的框架,它因其速度而闻名。Phalcon 非常优化和模块化;换句话说,它只使用您需要或想要的东西,而不会添加您不会使用的额外东西。

文档非常好,但它的缺点是社区没有 Silex 或 Slim 那么大,所以第三方社区很小,有时在出现问题时很难找到快速解决方案。

Phalcon ORM 基于 C 语言。如果您正在开发基于数据库的微服务,这一点非常重要。

总之,这是最好的框架;但是,不建议初学者使用。

您可以访问官方网站phalconphp.com

Slim 框架

Slim 是目前可用的最快的微型 RESTful 框架之一。它为您提供了框架应该具有的每个功能。此外,Slim 框架有一个非常庞大的社区:您可以在互联网上找到很多资源、教程和文档,因为有很多开发者在使用它。

自版本 3 发布以来,该框架具有更好的架构,从整体架构和安全性方面来看更好。这个新版本比版本 2 慢一点,但所有引入的更改使得这个框架适用于各种规模的项目。

文档不错,但可以更好。它太短了。

这是一个适合初学者的微框架。

请参阅官方网站www.slimframework.com/

Lumen

Lumen 是由 Laravel 制作的最快的微型 RESTful 框架之一。这个微框架是专门为超快的微服务和 API 而设计的。Lumen 确实很快,但有一些微框架更快,比如 Slim、Silex 和 Phalcon。

这个框架因为它与 CodeIgniter 语法非常相似而变得很有名,而且非常容易使用,所以也许这就是为什么 Lumen 是使用微框架开始使用微服务的最佳微框架。此外,Lumen 有非常好和清晰的文档;您可以在lumen.laravel.com/docs找到它。如果您使用这个框架,您可以在很短的时间内开始工作,因为设置非常快。

Lumen 的另一个优点是您可以开始使用它,然后,如果将来需要完整的 Laravel,将框架转换和更新为 Laravel 非常容易。

请注意,Lumen 仍然强制执行应用程序结构(约定优于配置),这可能会在设计应用程序时限制您的选项。

如果您要使用 Lumen,那是因为您已经使用过 Laravel 并且喜欢它;如果您不喜欢 Laravel,Lumen 不是最好的解决方案。

您可以访问官方网站lumen.laravel.com/

Zend Expressive

Lumen 相当于 Laravel,Zend Expressive 相当于 Zend Framework。它是一个用于制作微服务的微框架,并且准备根据 PSR-7 进行专门使用,并基于中间件。

您可以在几分钟内设置它,并且在社区方面具有 Zend Framework 的所有优势。此外,作为 Zend 的产品是质量和安全的代名词。

它具有最小的核心,您可以选择要包含的组件。它具有非常好的灵活性和扩展能力。

访问官方网站zendframework.github.io/zend-expressive/

Silex(基于 Symfony)

Silex 也是一个非常好的微型 RESTful PHP 框架。它是五个最快的微框架之一,目前它是最知名的之一,因为 Silex 社区是最大的之一,他们开发了非常好的第三方,因此开发人员对其项目有许多解决方案。

社区及其与 Symfony 的连接保证了稳定的实现和许多可用资源。此外,文档真的很好,这个微框架特别适合大型项目。

Silex 和 Slim Framework 的优势非常相似;也许是竞争使它们变得更好。

请参阅官方网站silex.sensiolabs.org/

摘要

在本章中,我们讨论了本书中要构建的示例应用程序。我们还向您展示了如何使用 Docker 设置开发机器,甚至谈到了您可以使用的不同微框架。在下一章中,我们将学习如何设计我们的应用程序以及不同类型的微服务模式。

第三章:应用程序设计

在前几章中,我们看了一下我们将要构建的应用程序的简要描述。现在是时候让您深入了解整个项目了。

微服务结构

我们希望构建一个地理定位应用程序,并选择将其创建为一个游戏,以使其更有趣且更易于理解。请随意将示例调整为任何其他想法,例如嵌入地理定位的旅游应用程序。

我们的游戏将使用地理定位来发现世界各地的不同秘密(或者在特定地理区域内,如果您想要一个较小的地图)。后端系统将生成新的秘密并将它们随机放置在我们的地图上,允许用户探索他们的环境以找到它们。作为我们游戏的玩家,您将收集不同的秘密并将它们存储在您的钱包中,这是您将找到有关每个秘密的更多信息的地方。

为了使我们的游戏更有趣,我们将拥有一个战斗引擎。当您发现我们的秘密世界时,您可以与其他玩家进行战斗,以窃取他/她的秘密。战斗引擎将是一个简单的引擎–只需掷骰子,得分最高者赢得战斗。

这种类型的项目不能没有其他服务完成,例如用户/玩家管理系统等。

作为开发人员,您从规范开始,然后尝试将其分解为更小的部分。根据我们的简要描述,我们可以开始定义我们的微服务及其责任如下:

  • 用户服务:这项服务的主要责任是用户注册和管理。为了使示例简单,我们还将添加额外的功能,例如用户通知和秘密钱包管理。

  • 战斗服务:这项服务将负责用户的战斗,记录每场战斗并将秘密从失败者的钱包转移到获胜者的钱包。

  • 秘密服务:这是我们游戏的核心服务之一,因为它将负责所有秘密事务。

  • 位置服务:为了增加额外的复杂性,我们决定创建一个服务来管理与位置相关的任何任务。主要责任是知道所有东西的位置;例如,如果用户服务需要知道该区域是否有其他玩家,向该服务发送带有地理定位的消息,响应将告诉用户服务谁在该区域。

请注意,我们不仅为我们的游戏创建服务,而且我们将使用其他支持服务使一切运行顺利。

以下图表描述了我们不同服务之间的通信路径。每个服务都能够与其他服务交流,这样我们就可以组合更大更复杂的任务。以下图表描述了我们微服务之间的连接:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

微服务模式

设计模式是在现实世界应用程序开发中解决重复问题的可重用解决方案。这些解决方案已经被证明具有成功的记录,并且被广泛使用,因此将它们添加到我们的项目中将使我们的软件更加稳定和可靠。

我们正在构建一个微服务应用程序,因为我们希望它尽可能稳定和可靠,所以我们将使用一些微服务模式,例如:API 网关、服务发现和注册表,以及共享数据库或每个服务一个数据库。

API 网关

我们将为用户提供前端注册和与我们的应用程序交互,并且它将是我们微服务的主要客户端。此外,我们计划将来拥有原生移动应用程序。由于不同的客户端使用我们的应用程序可能会给我们带来麻烦,因为他们对我们的微服务的使用可能会有很大不同。

为了统一任何客户端使用我们的微服务的方式,我们将添加一个额外的层–一个 API 网关。这个 API 网关成为任何客户端(例如浏览器和原生应用程序)的单一入口点。在这一层,我们的网关可以以两种方式处理请求:一些请求只是被代理,而其他请求则被分发到多个服务。我们甚至可以将这个 API 网关用作安全层,检查客户端的每个请求是否被允许使用我们的微服务:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

资产请求

拥有 API 网关有许多好处,其中我们可以强调以下几点:

  • 我们的应用程序将有一个单一的访问点,消除了客户端需要知道每个微服务位置的问题。

  • 我们可以更好地控制我们的服务如何被使用,甚至可以为特定客户提供自定义端点。

  • 它减少了请求/往返的次数。通过一次往返,客户端可以从多个服务中检索数据。

服务发现和注册

我们的服务将需要调用其他服务。在单体应用程序中,解决方案非常简单–我们可以调用方法或使用过程调用。我们正在构建一个运行在容器中的微服务应用程序,所以没有简单的方法可以知道某个服务的位置。我们的容器基础设施非常灵活,我们需要构建一个服务发现系统。

我们的每个服务将通过查询我们的服务注册表(一个使用 Consul 存储有关所有服务信息的地方)来获取所有其他链接服务的位置。我们的注册表将知道每个服务实例的位置。下图显示了自动发现模式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了实现这一点,我们将使用不同的工具:

  • Consul:这是我们的服务注册表,具有许多功能,如集群支持等。

  • Fabio:这是一个基于 Go 构建的反向代理,与 Consul 有深度集成。我们喜欢这个代理的原因是它与 Consul 的轻松连接以及其进行蓝绿部署的能力。另一个你可以尝试的有趣工具是 Træfik。

  • NGINX:这是一个强大的 HTTP 服务器和反向代理,对于大多数 Web 开发人员来说,这是一个非常知名的工具,由于其性能和低内存占用而被选择。我们将同时使用 Fabio 和 NGINX 作为反向代理。

  • ContainerPilot:这是一个用 Go 编写的小工具。我们将使用这个软件将我们的服务注册到 Consul 中,将我们容器的统计数据发送到一个集中的遥测系统,向 Consul 发送健康检查,并检测其他服务的变化。我们将使用这个工具创建一种自动修复系统。

共享数据库或每个服务一个数据库

以某种方式,应用程序会生成我们需要存储的数据。在单体应用程序中,毫无疑问,所有数据都存储在同一个地方。问题在于当你处理微服务应用程序时,没有简单的答案。每个应用程序域都是独特的,所以没有固定的规则来解决问题;你需要分析你的数据,并决定是否要将所有数据存储在共享存储中,每个服务是否有自己的数据存储,或者混合使用。

在我们的示例应用程序中,我们将涵盖这两种方法,但让我们解释每种选项的好处。

每个服务一个数据库

在这种方法中,我们将每个微服务的持久数据保持私有,数据只能通过其 API 访问,并具有许多好处:

  • 它使服务之间的耦合度降低;你可以对战斗服务进行更改而不影响用户服务,例如。

  • 由于数据只能通过其 API 访问,它增加了我们应用程序的灵活性;我们可以使用不同的存储引擎。例如,我们可以在用户服务中使用关系数据库,在位置服务中使用 NoSQL。

当然,这种解决方案也有一些缺点,最显著的问题是共享不同服务之间的数据的困难。

共享数据库

这种方法就像在单体应用程序中的数据库一样–所有数据都存储在同一个引擎中。主要好处是将所有内容放在一个地方的简单性。

这种简单性也有一些缺点;我们在其中突出以下几个:

  • 任何数据库更改都可能破坏或影响其他服务

  • 使用相同的引擎处理所有数据会导致应用程序不够灵活

  • 如果数据存储出现问题,所有使用共享数据库的服务都会注意到这个问题。

作为开发人员,您的工作是为您需要解决的每个问题找到最佳解决方案。您需要决定如何存储应用程序数据,始终牢记每个选项的利弊。

RESTful 惯例

表述状态转移是用于与 API 通信的方法的名称。顾名思义,它是无状态的;换句话说,服务不会保留传输的数据,因此,如果您调用一个发送数据的微服务(例如,用户名和密码),微服务将不会在下次调用时记住数据。状态由客户端保留,因此客户端需要每次调用微服务时发送状态。

一个很好的例子是当用户登录并且用户能够调用特定方法时,每次都需要发送用户凭据(用户名和密码或令牌)。

Rest API 的概念不再是一个服务;相反,它就像一个资源容器,可以通过标识符(URI)进行通信。

在接下来的几行中,我们将定义一些有关 API 的有趣的惯例。了解这些提示很重要,因为在编写 API 时,您应该像在 API 上工作时一样做事情。换句话说,编写 API 就像为自己写书一样–它将被像您一样的开发人员阅读,因此完美的功能并不是唯一重要的事情,友好的交流方式也很重要。

如果您遵循一些惯例,为您和消费者创建一个 RESTful API 将会更容易让他们满意。我一直在我的 RESTful API 上使用一些建议,结果非常好。它们有助于组织您的应用程序及其未来的维护需求。此外,当 API 消费者享受与您的应用程序一起工作时,他们会感谢您。

安全

RESTful API 中的安全性很重要,但如果您的 API 将被您不认识的人使用,换句话说,如果它将对所有人开放,那么它就尤为重要。

  • 到处使用 SSL–这对于您的 API 的安全性很重要。有许多没有 SSL 连接的公共场所,可以嗅探数据包并获取其他人的凭据。

  • 使用令牌身份验证,但如果要使用令牌对用户进行身份验证,则必须使用 SSL。使用令牌可以避免每次需要识别当前用户时发送完整凭据。如果不可能,您可以使用 OAuth2。

  • 提供响应头,以限制和避免同一消费者发送过多请求。大公司的一个问题是流量,甚至有人试图对您的 API 做坏事。有一种方法可以避免这类问题是很好的。

标准

PHP 和微服务的更多标准正在逐渐出现。正如我们在上一章中看到的,有一些团体,比如 PHP-FIG,正在努力建立它们。以下是一些使您的 API 更加标准的提示:

  • 到处使用 JSON。避免使用 XML;如果有一个 RESTful API 的标准,那就是 JSON。它更紧凑,可以在 Web 语言中轻松加载。

  • 使用驼峰命名法而不是蛇形命名法;这样更容易阅读。

  • 使用 HTTP 状态码错误。每种情况都有标准状态,因此使用它们可以避免解释 API 的每个响应。

  • 在 URL 中包含版本信息,不要将其放在头部。版本信息需要在 URL 中,以确保资源在不同版本之间的浏览器可探索性。

  • 提供一种覆盖 HTTP 方法的方式。一些浏览器只允许POSTGET请求,因此允许使用X-HTTP-Method-Override头来覆盖PUTPATCHDELETE将是有益的。

消费者便利设施

您的 API 的使用者是最重要的,因此您需要提供有用、友好的方法来使开发人员的工作更轻松。开发方法时要考虑到他们:

  • 限制响应数据。开发人员不需要所有可用的数据,因此可以使用字段过滤器限制响应。

  • 使用查询参数来过滤和排序结果。这将有助于简化您的 API。

  • 记住您的 API 将被不同的开发人员使用,因此要注意您的文档——它需要非常清晰和友好。

  • POSTPATCHPUT请求上返回有用的内容。避免开发人员多次调用 API 以获取所需的数据。

  • 提供一种在响应中自动加载相关资源表示的方式。这对开发人员来说将是有帮助的,以避免多次请求相同的内容以获取所有必要的数据。可以通过在 URL 中包含过滤器来定义特定参数来实现这一点。

  • 使用链接头来进行分页,这样开发人员就不需要自己创建链接。

  • 包括促进缓存的响应头。HTTP 已经包含了一个框架,只需添加一些头部即可实现这一点。

还有很多提示,但这些足以作为 RESTful 约定的第一步。在接下来的章节中,我们将看到这些 RESTful 约定的示例,并解释如何更好地使用它们。

缓存策略

Phil Karlton

“在计算机科学中只有两件难事:缓存失效和命名事物。”

缓存是一个组件,用于临时存储数据,以便将来对该数据的请求可以更快地提供。这种临时存储用于缩短我们的数据访问时间,减少延迟,并改善 I/O。我们可以在微服务架构中使用不同类型的缓存来提高整体性能。让我们来看看这个主题。

一般的缓存策略

为了维护缓存,我们有算法提供指示,告诉我们应该如何维护缓存。最常见的算法如下:

  • 最不经常使用(LFU):此策略使用计数器来跟踪条目的访问频率,并首先删除计数最低的元素。

  • 最近最少使用(LRU):在这种情况下,最近使用的项目总是靠近缓存的顶部,当我们需要一些空间时,将删除最近未被访问的元素。

  • 最近最常使用(MRU):首先删除最近使用的项目。我们将在较老的项目更常被访问的情况下使用这种方法。

在设计应用程序所需的每个微服务时,开始考虑缓存策略的最佳时机。每当您的服务返回数据时,您需要问自己一些问题:

  • 我们是否返回了无法在任何地方存储的合理数据?

  • 如果输入相同,我们是否返回相同的结果?

  • 我们可以存储这些数据多久?

  • 我们想要如何使缓存失效?

您可以在应用程序中的任何位置添加缓存层。例如,如果您正在使用 Percona/MySQL/MariaDB 作为数据存储,可以正确启用和设置查询缓存。这个小设置将提升您的数据库性能。

即使在编码时,你也需要考虑缓存。你可以对对象和数据进行延迟加载,或者构建一个自定义的缓存层来提高整体性能。想象一下,你正在从外部存储请求和处理数据,请求的数据可能在同一次执行中重复多次。做类似下面的代码片段将减少对外部存储的调用:

    <?php 
    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]; 
       } 
    } 

请注意,我们的示例省略了大量的代码,比如命名空间或其他函数。我们只想给你一个整体的想法,这样你就可以创建自己的代码。

在这种情况下,我们将我们的数据存储在$myCache变量中,每当我们使用 ID 作为键标识符向外部存储发出请求时。下一次我们请求与之前相同 ID 的元素时,我们将从$myCache中获取元素,而不是从外部存储请求数据。请注意,只有在同一次 PHP 执行中可以重用数据时,这种策略才会成功。

在 PHP 中,你可以访问最流行的缓存服务器,比如memcachedRedis;它们都以键值格式存储数据。访问这些强大的工具将允许我们提高微服务应用程序的性能。

让我们使用Redis作为我们的缓存存储来重建我们之前的示例。在下面的代码片段中,我们将假设你的环境中有一个Redis库可用(例如 phpredis),并且有一个Redis服务器在运行:

    <?php 
    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)中。

将来的请求将从我们的缓存层获取秘密数据,而不是从我们的数据存储中获取,从而提高我们应用的性能。请注意,检索和存储秘密数据的服务是负责管理这个缓存的服务。

让我们看看我们将在微服务应用程序中使用的其他缓存策略。

HTTP 缓存

这种策略使用一些 HTTP 头来确定浏览器是否可以使用响应的本地副本,或者需要从源服务器请求新的副本。这种缓存策略是在应用程序之外管理的,所以你对它没有太多控制。

我们可以使用的一些 HTTP 头如下:

  • Expires:设置内容将过期的未来时间。当未来的这一点到达时,任何类似的请求都必须返回到原始服务器。

  • Last-modified:指定响应最后修改的时间;它可以作为您的自定义验证策略的一部分,以确保用户始终拥有新鲜内容。

  • Etag:此标头标记是 HTTP 提供的用于 Web 缓存验证的几种机制之一,它允许客户端进行条件请求。Etag 是服务器分配给资源特定版本的标识符。如果资源发生更改,Etag 也会更改,从而使我们能够快速比较两个资源表示以确定它们是否相同。

  • Pragma:这是一个旧的标头,来自 HTTP/1.0 实现。HTTP/1.1 Cache-control 实现了相同的概念。

  • Cache-control:此标头是 expires 标头的替代品;它得到了很好的支持,并允许我们实现更灵活的缓存策略。此标头的不同值可以组合以实现不同的缓存行为。

以下是可用的选项:

  • no-cache:表示必须在每个请求之前重新验证任何缓存的内容,然后才能发送给客户端。

  • no-store:表示内容无法以任何方式缓存。当响应包含敏感数据时,此选项很有用。

  • public:将内容标记为公共,可以由浏览器和任何中间缓存进行缓存。

  • private:将内容标记为私有;此内容可以由用户的浏览器存储,但不能由中间方存储。

  • max-age:设置内容在必须重新验证之前可以缓存的最长时间。此选项值以秒为单位,最长为 1 年(31,536,000 秒)。

  • s-maxage:这与 max-age 标头类似;唯一的区别是此选项仅应用于中间缓存。

  • must-revalidate:此标记表示必须严格遵守 max-age、s-maxage 或 expires 标头指示的规则。

  • proxy-revalidate:这与 s-maxage 类似,但仅适用于中间代理。

  • no-transform:此标头告诉缓存它们不得在任何情况下修改接收到的内容。

在我们的示例应用程序中,我们将拥有一个可以通过任何 Web 浏览器访问的公共 UI。使用正确的 HTTP 标头,我们可以避免对相同资产的重复请求。例如,我们的 CSS 和 JavaScript 文件不会经常更改,因此我们可以设置一个未来的到期日期,浏览器将保留它们的副本;未来的请求将使用本地副本而不是请求新副本。

您可以使用简单的规则在 NGINX 中为所有.jpg.jpeg.png.gif.ico.css.js文件添加一个到浏览器访问时间未来 123 天的到期标头:

    location ~*  .(jpg|jpeg|png|gif|ico|css|js)$ { 
        expires 123d; 
    } 

静态文件缓存

一些静态元素非常适合缓存,其中包括以下内容:

  • 标志和非自动生成的图像

  • 样式表

  • JavaScript 文件

  • 可下载内容

  • 任何媒体文件

这些元素往往很少更改,因此可以缓存更长时间。为了减轻服务器的负载,您可以使用内容交付网络CDN),以便这些很少更改的文件可以由这些外部服务器提供。

基本上,CDN 有两种类型:

  • Push CDNs:这种类型要求您推送要存储的文件。您有责任确保将正确的文件上传到 CDN,并且推送的资源可用。它主要用于上传的图像,例如用户的头像。请注意,一些 CDN 在推送后可以返回 OK 响应,但您的文件实际上还没有准备好。

  • 拉 CDN:这是懒惰的版本,您不需要将任何内容发送到 CDN。当请求通过 CDN 并且文件不在它们的存储中时,它们会从您的服务器获取资源并将其存储以供将来使用。它主要用于 CSS、图像和 JavaScript 资源。

在设计微服务应用程序时,您需要记住这一点,因为您可能允许用户上传一些文件。

您将在哪里存储这些文件?如果它们是公开的,为什么不使用 CDN 来传送这些文件,而不是从您的服务器中删除它们。

一些著名的 CDN 包括 CloudFlare、Amazon CloudFront 和 Fastly 等。它们的共同之处在于它们在世界各地都有多个数据中心,使它们可以尝试从最近的服务器为您提供文件的副本。

通过将 HTTP 与静态文件缓存策略结合起来,您将最大限度地减少服务器上的资源请求。我们不会解释其他缓存策略,比如完整页面缓存;根据我们所涵盖的内容,您已经有足够的知识来开始构建成功的微服务应用程序。

领域驱动设计

领域驱动设计(从这里开始称为 DDD)是一种在有复杂需求时进行开发的方法。这个概念并不新鲜;它是由 Eric Evans 在他 2004 年的同名书中创建的,但现在它是主流的,因为微服务在开发人员中很受欢迎,并且在大型项目中非常常见。

这是发生的,因为微服务概念(关于软件架构,将每个功能划分为服务)和 DDD 概念(关于有界上下文)之间有很好的兼容性。

在了解我们如何在微服务项目中使用 DDD 之前,有必要了解 DDD 是什么以及它是如何工作的,所以让我向您介绍这种方法的主要概念作为总结。

领域驱动设计的工作原理

Evans 引入了一些必要的概念,以了解领域驱动设计的工作原理:

  • 上下文:这是一个词或语句出现的环境,决定了它的含义。

  • 领域:这是知识(本体论)、影响或活动的领域。用户应用程序的主题领域是软件的领域。

  • 模型:这是描述领域的选定方面的抽象系统,可以用来解决与该领域相关的问题。

  • 普遍语言:这是围绕领域模型构建的语言,由所有团队成员使用,将团队的所有活动与软件连接起来。

软件领域与技术术语、编程或计算机无关。在大多数项目中,最具挑战性的部分是理解业务领域,因此 DDD 建议使用模型领域;这是在图表、代码或文字中复制的抽象、有序和选择性知识。

模型领域就像建立具有复杂功能的项目的路线图,需要遵循五个步骤才能实现。这五个步骤需要开发团队和领域专家达成一致。

  1. 头脑风暴和完善:开发团队和领域专家之间应该有一个沟通渠道。因此,项目中的所有人都应该能够与每个人交谈,因为他们都需要知道项目应该如何工作。

  2. 草稿领域模型:在对话过程中,有必要开始绘制领域模型的草稿,以便领域专家可以检查和纠正,直到他们达成一致。

  3. 早期类图:使用草稿,我们可以开始构建类图的早期版本。

  4. 简单原型:使用早期类图和领域模型的草稿,可以构建一个非常简单的原型。Evans 建议避免与领域无关的事物,以确保领域业务得到适当建模。它可以是一个非常简单的程序作为追踪。

  5. 原型反馈:领域专家与原型进行交互,以检查是否满足所有需求,然后整个团队将改进领域模型和原型。

这个过程将进行所有必要的迭代,直到领域模型正确为止:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

模型、代码和设计必须一起发展和成长。它们不能不同步。如果模型上的概念更新了,代码和设计也应该自动更新,其他方面也是如此。

模型是描述领域的选择性概念的抽象系统,它可以用来解决与该领域相关的问题。如果模型中的某个部分在代码中没有反映出来,那么它应该被移除。

最后,领域模型是项目中通用语言的基础。DDD 中的这种通用语言称为普遍语言,它应该具有以下特点:

  • 与领域相关的类名和它们的功能

  • 讨论模型中包含的领域规则的术语

  • 应用于领域模型的分析和设计模式的名称

项目所有成员,包括开发人员和领域专家,都应该使用普遍语言,因此开发人员应该能够描述所有的任务和功能。

在团队之间的所有讨论中绝对必须使用这种语言,比如会议、图表或文档,但这种语言并不是在过程的第一次迭代中诞生的,这意味着它可能需要多次重构,使模型、语言和代码保持同步。例如,如果开发人员发现领域中的一个类应该被重命名,他们不能在没有重构领域模型和普遍语言的情况下进行重构。

普遍语言、领域模型和代码应该作为一个单一的知识块一起发展。

关于 DDD 存在争议的概念。Eric Evans 说,领域专家必须使用与团队相同的语言,但有些人不喜欢这个想法。通常,领域专家不了解面向对象的概念或微服务,因为这些对非开发人员来说太抽象了。无论如何,DDD 认为,如果领域专家不理解领域模型,那是因为它存在问题。

领域模型中有图表,但 Evans 建议也使用文本,因为图表无法正确解释概念。此外,图表应该是表面的;如果你想看更多细节,可以查看代码。

一些项目受到领域模型和代码之间的连接的影响。这是因为分析和设计之间存在分歧。分析人员制定了一个独立于设计的模型,开发人员无法开发功能,因为缺少一些信息。此外,他们无法与领域专家交流。开发团队将不遵循模型,最终领域模型将不会得到更新,也不会起作用。因此,项目将无法满足要求。

总之,DDD 旨在将软件开发作为模型、设计和代码的迭代精炼过程,作为一个单一的任务块。

在微服务中使用领域驱动设计

正如我们之前所说,DDD 完全满足微服务的需求。微服务的一个常见问题是它们具有分散的数据管理;这有优势,但有时也会带来问题。

两个服务之间的概念模型将是不同的,这可能会在大型公司中造成问题。例如,用户可能会因服务而异,每个服务关于用户的属性可能会不同,而且属性语义也可能会不同。

当在一个大公司中,应用程序经历了很多年的更新,变得更加复杂时,情况变得更加复杂。每个服务可能对用户有不同的属性,通常它们不匹配。因此,使用 DDD 是解决这个问题的一个很好的方法。

与微服务一样,DDD 将复杂的领域划分为不同的上下文,建立它们之间的关系,并要求所有成员合作,以在特定领域和有界上下文中获得一种通用语言,通过迭代这个过程直到实现对问题的真正概念。

Evans 建议将每个微服务设计为 DDD 有界上下文,以便为系统内的微服务提供逻辑边界。每个微服务(或团队)将负责系统的一部分,并提供更清晰和可维护的代码。

Michael Plöd 提出了有关 DDD 如何帮助微服务的更多想法。在构建微服务方面有四个重要领域:

  • 战略设计:这基本上是有界上下文,但上下文映射和其他模式也很重要。上下文映射应显示项目的所有有界上下文及其彼此之间的关系;它还描述它们之间的契约。上下文映射对于希望转向微服务的单片应用程序非常有用。

  • 内部构建块:这指的是在设计有界上下文内部时使用战术模式,如聚合、实体或存储库。

  • 大规模结构:用于使用不断发展的顺序和责任层创建结构。这也是微服务中的一个概念。在大型项目中,将大规模结构创建到边界上下文中是有帮助的。它们应该被设计为独立演变。

  • 蒸馏:从已经成长的系统中提炼核心领域,在将单片应用程序迁移到微服务时非常有用。最重要的部分应该是识别和提取核心领域,以及识别子域、从核心中提取它并进行重构的迭代过程。

总之,微服务和 DDD 完全匹配,但需要有更大的范围并且了解超出边界上下文的内容。

事件驱动架构

事件驱动架构EDA)是一种遵循生产、检测、消费和对事件做出反应的应用程序架构模式。

可以将事件描述为状态的改变。例如,如果门关闭了,有人打开了它,门的状态就从关闭变为打开。打开门的服务必须将此更改作为事件进行,其他服务可以知道这个事件。

事件通知是异步产生、发布、检测或消费的消息,它是由事件改变的状态。重要的是要理解事件并不在应用程序中传播,它只是发生。术语“事件”有点争议,因为它通常指的是消息事件通知,而不是事件,因此了解事件和事件通知之间的区别很重要。

这种模式通常用于基于组件或微服务的应用程序,因为它们可以应用于应用程序的设计和实现。由事件驱动的应用程序具有事件创建者和事件消费者或接收器(它们必须在事件可用时立即执行操作)。

事件创建者是事件的生产者;它只知道事件已发生,没有其他信息。然后我们有事件消费者,它们负责知道事件已被触发。消费者参与处理或更改事件。

事件消费者订阅了某种中间件事件管理器,一旦它收到来自创建者事件的通知,就会将事件转发给已注册的消费者来接收。

将应用程序开发为围绕诸如 EDA 之类的架构的微服务允许这些应用程序以一种更具响应性的方式构建,因为 EDA 应用程序是按设计准备好在不可预测和异步环境中运行的。

使用 EDA 的优势如下:

  • 解耦系统:创建者服务不需要知道其他服务,其他服务也不知道创建者。因此,它允许解耦系统。

  • 交互发布/订阅:EDA 允许多对多的交互,其中服务发布有关某个事件的信息,服务可以获取该信息并对事件进行必要的处理。因此,它使许多创建者事件和消费者事件能够实时交换状态并响应信息。

  • 异步:EDA 允许服务之间的异步交互,因此它们不需要等待即时响应,而且在等待响应时不需要连接工作。

微服务中的事件驱动架构

在大型项目中,通常使用微服务将其服务划分为较小的服务。因此,它非常重要在它们之间有良好和有组织的通信。事件驱动架构可用于解决微服务之间通信的常见问题。

在基于微服务的项目中,通常每个微服务都使用 HTTP 请求相互通信。这会带来一些问题,我们现在来解释。

在我们的“寻找秘密”项目中,有一个函数用于为用户创建事件。创建新事件时,需要将事件名称和事件表单中附加的图像发送到一个服务,以从接收到的数据创建视频。生成视频后,事件将被更新并通过电子邮件发送给用户。

如果我们为每个服务进行 HTTP 请求,问题在于所有服务都需要了解其他服务。例如,生成视频的服务需要知道如何在生成视频后更新事件;换句话说,服务必须包含代码来执行此更新。

此外,一旦我们添加了许多服务,这将变得越来越困难,因为它将需要更多的通信。它将有更多的故障,并且主要问题是,如果一个微服务宕机,视频将无法生成。因此,在像这样的项目中,使用 HTTP 请求不会很好地扩展,我们应该使用不同的策略来进行微服务通信。

如果我们以不同的方式做事会怎样?换句话说,生成视频的服务不会直接更新事件,事件也不会要求视频服务生成视频。那么,我们如何使微服务进行通信?答案是使用事件驱动架构。

为此,我们需要以下内容:

  • 每个微服务的事件队列

  • 所有微服务都必须将事件发送到集中式总线(我们可以使用 AWS 来做这个)

  • 每个微服务队列都必须订阅集中式总线

  • 每个微服务都有一个后台工作程序监听事件队列,并在接收到事件时执行必要的操作

在下图中,您可以看到涉及的不同服务和流程流程,用箭头表示。下图显示了事件驱动的工作流程:

微服务中的事件驱动架构

当我们在事件服务 API1)上创建一个新事件时,事件会进入集中式总线(2),相应的工作者会从集中式总线(3)获取事件;其他工作者会忽略这个事件。事件被放置在视频生成服务的队列中,并等待服务执行(4)。

一旦视频由服务工作者生成,服务就会向集中式总线(5)发出新的事件。然而,这次会由另一个工作者(6)接管,其他工作者会像之前一样忽略这个事件。更新事件的工作者和发送电子邮件的工作者会将事件放入他们的队列中,并且会执行(7)对每个服务执行相应的操作,如果有必要的话,他们会向集中式总线发送新的事件。

这是一个事件循环,它改进了服务之间的通信的 HTTP 请求方法。使用事件驱动架构的优势如上所述:

  • 如果服务出现任何错误或异常,事件不会丢失,它会留在队列中,并且稍后会被执行。例如,如果发送电子邮件的服务出现问题,发送电子邮件的事件将被保留在队列中,等待服务再次启动。

  • 服务不需要知道如何更新其他服务。这意味着服务的逻辑可以在每个服务中被隔离。

  • 可以添加更多的微服务而不会产生影响。

  • 它将更好地扩展。

持续集成、持续交付和工具

没有代码提交策略或测试/部署工作流程,软件项目是无法成功的。当你在团队中工作时,拥有一个策略更加重要。没有什么比在一个混乱的项目上工作更令人恼火,没有规则或没有人对他们所做的工作负责。在本节中,我们将解释最常见和成功的开发实践。

持续集成 - CI

持续集成是一个软件开发实践,团队成员频繁地集成他们的工作。每当新代码被推送到共享存储库时,将触发自动构建,以尽快检测任何集成错误。主要目标是避免长时间和不可预测的集成。

什么是持续集成?

让我们用一个简短的例子更好地解释持续集成的过程是什么样的。想象一下,你的游戏示例已经准备就绪,在生产中运行良好,你有一个新的想法,一个小功能,你的应用程序的用户会喜欢。这个新功能可以在几个小时内完成。

首先,在开发机器上获取当前源代码的副本;你将使用源代码控制系统,所以你只需要从主线检出一个工作副本。

现在你已经有了源代码的工作副本,你可以做任何你需要完成的功能,添加新代码,创建新测试等等。持续集成实践假设你的大部分代码将被自动化测试覆盖。对于 PHP 来说,一个流行的单元测试套件是 PHPUnit,这是一个简单而强大的工具,我们将在后面的章节中介绍。对我们的代码进行测试将有助于我们在未来的步骤中,并确保我们的代码质量高。

现在你已经完成了新功能,是时候在你的开发环境上启动一个自动构建了。这个过程将获取源代码,检查错误,并运行自动化测试。只有当构建和所有测试都通过没有错误时,我们才能认为构建是好的,并且可以将其添加到我们的存储库中。

这个过程的结果是,我们有一个稳定的软件,它能正常工作并且包含非常少的 bug。

持续集成的好处

持续集成的主要目标是降低风险,但这并不是采用这种开发实践的唯一好处。其中,我们可以强调以下好处:

  • 减少集成时间

  • 由于我们正在推送小的更改并且每个更改都经过了一次又一次的测试,因此可以早期发现错误

  • 稳定构建的持续可用性,我们可以使用它进行新的测试,用作我们的客户演示,甚至再次部署

  • 项目质量指标的持续监控

持续集成的工具

作为开发人员,您可能会担心如何自动化这个过程。不用担心,在市场上有多种方式可以创建和管理您的 CI 流水线。我们的最佳建议是,在决定在项目中使用哪种 CI 软件之前,花一些时间测试所有的选择。一些与 PHP 轻松集成的 CI 软件包括:

  • Jenkins:这是一个非常容易安装和管理的开源项目。它的多功能性使得这个软件可能是最广泛使用的 CI 软件之一。

  • Bamboo:这是一个基于订阅的软件。Atlassian 在开发世界以其生产力和开发支持工具而闻名。如果您需要与其他 Atlassian 工具深度集成,这是一个不错的选择。

  • Travis:这是另一个基于订阅的软件,针对开源项目有免费计划。

  • PHP CI:这个新的开源工具是基于 PHP 构建的,可以安装在您的服务器上,也可以作为基于云的工具使用。

在我们的示例项目中,我们将使用 Jenkins 并启动一个 Docker 容器。与此同时,您可以使用以下简单命令开始测试 Jenkins:

**$ docker run -p 8080:8080 -p 50000:50000 jenkins** 

这个命令将创建一个包含 Jenkins 官方 Docker 镜像的容器,并将本地环境的 8080 和 50000 端口映射到容器中。如果您在浏览器中打开http://localhost:8080,您将可以访问 Jenkins UI。

持续交付

持续交付是持续集成的延续,其主要目标是能够在任何时候部署软件的任何版本,而不会出现故障。我们可以通过确保我们的代码始终可供部署,并遵循持续集成的实践,来实现这一点,从而确保我们的源代码的质量和集成水平。

通过持续交付,每当我们对代码进行更改时,这些更改都会被构建、测试,然后发布到一个阶段环境。下图显示了 CD 流水线上的基本工作流程。正如您所看到的,如果任何测试步骤失败,我们需要重新开始,直到我们的代码通过测试。通过这种方式工作,我们可以始终确保我们的项目符合最高的质量标准。

以下是持续交付工作流程的图表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

持续交付的好处

持续交付有许多好处;其中,我们强调以下几点:

  • 减少部署风险:我们将部署更小的更改,因此出错的可能性更小,而且更容易修复任何问题。即使我们应用了部署模式,比如蓝绿部署,我们的部署对我们的用户来说是不可察觉的。

  • 进度跟踪:由于并非所有开发人员和经理以相同的方式跟踪工作进展,我们现在非常快速地部署小版本;当任务完成时毫无疑问——如果它在生产环境中,那么任务就完成了。

  • 更高的质量:通过持续交付,我们以小批量工作;这使我们能够在交付生命周期中从用户那里获得反馈。我们甚至可以使用 A/B 测试来在构建完整功能之前测试想法。在我们的流水线中使用自动化测试工具使开发人员能够快速发现回归并避免发布不稳定的软件。

  • 更快的上市时间:传统软件生命周期的集成和测试阶段可能需要几周的时间,但如果我们设法自动化构建和部署、环境配置和测试流程,我们可以将时间缩短到最低并将其纳入开发人员的日常工作中。

  • 降低成本:如果我们投资于构建、测试、部署和环境自动化,就可以通过消除许多固定的相关成本来降低软件成本。

持续交付流水线的工具

如前所述,持续交付是持续集成的延续,因此我们可以使用我们之前提到的大多数 CI 工具,并用我们最喜欢的测试框架扩展我们的流水线。在 PHP 中,有许多可用的测试框架,但最知名的有以下几种:

  • phpUnit:这是最知名的用于创建单元测试的框架。每个 PHP 开发人员都需要了解这个框架,因为它将成为他们测试的基础。这是行业标准。

  • Codeception:这是为 PHP 提供的最完整的测试套件之一。使用 Codeception,你可以构建单元测试、功能测试和验收测试。

  • Behat:这是最流行的面向行为的 PHP 测试框架。你不需要编写代码,而是编写故事,框架会转换并测试它们。

  • PHPSec:这是另一个遵循面向行为测试的重要框架。

  • Selenium:这是最复杂的测试框架之一,用于自动化浏览器。有了这个框架,就可以编写用户验收测试。

在接下来的章节中,我们将使用其中一些测试框架。与此同时,尝试每一个并选择你最喜欢的框架。记住,你可以毫无问题地混合它们。

总结

在本章中,我们讨论了设计和开发应用程序的不同方式。我们涵盖了一些模式和策略,你可以轻松地整合到你的开发工作流程中,甚至谈到了最常见的开发实践。在接下来的章节中,我们将在我们的开发工作流程中应用所有这些概念。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值