Laravel Octane 高性能指南(一)

原文:zh.annas-archive.org/md5/53ac6eb53ccf1baf1816775e25495357

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书全面概述了使用 Laravel Octane 设计和构建高性能应用程序所需的一切,包括为什么应该这样做以及如何操作。本书还涵盖了 Laravel Octane 使用的不同工具,例如 Swoole 和 RoadRunner,并列出并解释了各种功能和区分元素。但最重要的是,它将使您理解何时以及为什么使用 Swoole 或 RoadRunner。

本书面向对象

本书面向那些已经了解 Laravel 框架基础(如路由机制、控制器、服务和模型)的 Laravel 开发者。本书旨在为那些希望以更可扩展的方式设计应用程序并提高其性能的开发者编写。

本书涵盖内容

第一章理解 Laravel 网络应用程序架构,是 Laravel Octane 介绍新功能的地方,但更重要的是,它引入了一种新的思考方式,即如何设计高性能的 Laravel 应用程序。

第二章配置 RoadRunner 应用程序服务器,展示了 Laravel Octane 基于 RoadRunner 应用程序服务器的场景。RoadRunner 是一个非常快速且有效的应用程序服务器,易于安装。由于其简单性,它使用户能够轻松且直接地接近 Laravel Octane。

第三章配置 Swoole 应用程序服务器,展示了 Laravel Octane 基于 Swoole 应用程序服务器的场景。以应用程序服务器的形式运行 Swoole 允许开发者使用一些高级功能,例如管理多个工作进程和并发任务,更有效地引导应用程序,快速缓存以及在工作进程之间共享数据。由于 Laravel Octane 使用 Swoole,因此了解使用这些应用程序服务器带来的机会非常重要。

第四章构建 Laravel Octane 应用程序,通过使用 Swoole 应用程序服务器提供的功能,通过实际示例教会我们如何构建应用程序。

第五章使用异步方法降低延迟和管理数据,教会我们一旦开发者加快了框架的引导速度并消除了请求端的瓶颈,他们就必须在其他应用程序部分降低延迟。本章介绍了降低执行任务和管理数据延迟的有用技术。

第六章在您的应用程序中使用队列来实现异步方法,教导我们除了应用程序服务器提供的各种工具外,我们还可以向我们的应用程序架构中添加额外的组件。为了提高解决方案的可扩展性,与应用程序服务器一起使用的主要工具之一是队列机制。本章展示了向我们的应用程序添加队列机制的好处。

第七章配置 Laravel Octane 应用程序以用于生产环境,讨论了如何进入生产环境,涵盖了特定环境的配置、将应用程序部署到应用程序服务器以及微调 nginx 作为 Swoole 反向代理的配置。

为了充分利用本书

您需要在您的计算机上安装 PHP 8.0 或更高版本。所有代码示例都已使用 macOS 和 GNU/Linux 上的 PHP 8.1 进行测试。

本书中涵盖的软件/硬件操作系统要求
Laravel 9Windows, macOS, 或 Linux
PHP 8+

如果您不想在本地机器上安装 PHP,并且熟悉 Docker 设置,您可以使用 Laravel Sail 提供的 Docker 镜像。本书解释了本地安装和通过 Docker 镜像安装应用程序服务器和模块(Swoole/OpenSwoole)的设置。

如果您使用的是本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/High-Performance-with-Laravel-Octane。如果代码有更新,它将在 GitHub 仓库中更新。

下载彩色图像

我们还提供了一份包含本书中使用的截图和图表的彩色图像 PDF 文件。您可以从这里下载:packt.link/ZTNyn

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“从 Symfony 世界来看,Laravel 包括Symfony/routing等包来管理路由,以及http-foundationhttp-kernel来管理 HTTP 通信。”

代码块设置如下:

        "nyholm/psr7": "¹.5",
        "spiral/roadrunner": "v2.0"

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

command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=swoole --host=0.0.0.0 --port=80 --watch

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

npm install --save-dev chokidar

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“一旦一切正常,您将在网页上看到表已创建!。这意味着行已正确创建。”

小贴士或重要提示

看起来像这样。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对此书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在此书中发现错误,我们将不胜感激,如果您能向我们报告,请访问www.packtpub.com/support/errata并填写表格。

盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件发送至 copyright@packt.com 并提供材料的链接。

如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

分享您的想法

读完《使用 Octane 进行高性能 Laravel 开发》后,我们非常乐意听到您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载此书的免费 PDF 副本

感谢您购买此书!

您喜欢在路上阅读,但无法随身携带您的印刷书籍?您的电子书购买是否与您选择的设备不兼容?

别担心,现在每购买一本 Packt 图书,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠远不止于此,您还可以获得独家折扣、时事通讯和丰富的免费内容,每天直接发送到您的收件箱。

按照以下简单步骤获取福利:

  1. 扫描二维码或访问以下链接

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/B17728_QR_Free_PDF.jpg

packt.link/free-ebook/9781801819404

  1. 提交您的购买证明

  2. 就这些!我们将直接将您的免费 PDF 和其他福利发送到您的电子邮件。

第一部分:架构

本部分的目标是使您熟悉 PHP 的应用服务器架构,并展示应用服务器如何跨工作进程共享资源以服务 HTTP 请求。

本部分包括以下章节:

  • 第一章, 理解 Laravel 网络应用程序架构

第一章:理解 Laravel Web 应用程序架构

本书是为希望以更可扩展的方式设计和构建他们的 Laravel Web 应用程序,并使其更高效性能的 Laravel 开发者而写的。

本书旨在为您提供有关如何改进 Web 应用程序软件架构的知识、建议和解释,从典型的 PHP Web 应用程序架构到更可扩展和高效架构。

它提供了使用 Laravel Octane 设计和构建高性能应用程序所需的所有内容的 360 度概览。我们将看到为什么 Laravel Octane 适合设计和构建高性能应用程序。这本书还涵盖了 Laravel Octane 使用的不同工具,例如 Open SwooleRoadRunner,并列出并描述了各种功能和差异化元素。但最重要的是,它能够让你理解为什么以及何时使用 Open Swoole 或 RoadRunner。

但在开始之前,为什么使用 Laravel Octane?

Laravel Octane 是一个工具,允许我们访问我们刚才提到的两个应用程序服务器暴露的一些功能和特性。

Laravel Octane 的一个好处是显著提高了客户端(如网页浏览器)对 HTTP 请求的响应时间。当我们开发 Laravel 应用程序时,我们使用框架实现的一个重要软件层。这个软件层需要时间和资源来启动。即使我们只谈论少量资源和很短的时间,这种对每个请求的重复操作,尤其是在存在许多请求的环境中,也可能成为一个问题。或者更确切地说,它的优化可以带来巨大的好处。

通过应用程序服务器,Laravel Octane 正是这样做:优化了框架启动的过程,这通常发生在每个单独的请求中。我们将详细看到这是如何实现的;本质上,框架需要的对象和一切都被启动并分配到应用程序服务器的开始处,然后实例被提供给各个工作者。工作者是启动来服务请求的进程。

另一个有趣的原因是评估在您的 Laravel Web 应用程序中采用 Laravel Octane,是因为通过使用像 Swoole 这样的应用程序服务器,您可以访问 Swoole 实现的功能。

功能,例如,缓存驱动程序的先进机制、在各个工作者之间共享信息的共享存储以及并行执行任务。

这对于典型的 PHP 开发者来说是一个全新的概念,他们通常没有在 PHP 核心中立即可用的功能来并行化进程。

本章将向您介绍 Laravel 生态系统,并探讨 Laravel Octane 是什么。

本章旨在向您介绍应用服务器方法,在这种方法中,更多的工作者协同工作以管理多个请求。了解底层的操作行为使开发者能够避免一些错误,尤其是在工作者之间共享资源(对象和全局状态)时。这很重要,因为经典的 PHP 方法是使用一个专门的线程来管理一个请求。

在本章中,我们将涵盖以下主题:

  • 探索 Laravel 生态系统

  • 理解请求生命周期

  • 了解应用服务器

技术要求

为了运行本书中展示的代码和工具,您必须在您的机器上安装 PHP 引擎。建议安装较新的 PHP 版本(至少 8.0,2020 年 11 月发布)。

此外,为了方便安装额外的工具,如果你使用 macOS,建议安装 Homebrew。在 GNU/Linux 系统中,将足够使用所使用的发行版的包管理器,而在 Windows 系统中,建议使用虚拟环境,例如 Docker。

在本章中,将展示一些命令和源代码,只是为了分享一些概念。在随后的章节中,特别是关于 RoadRunner 的第二章和关于 Open Swoole 的第三章,将逐步介绍每个包和工具的安装。

有些人,无论使用什么操作系统,都更喜欢通过使用 Docker 来维护一个“干净”的安装,无论主机操作系统是什么。在接下来的章节中,将处理操作系统依赖工具的安装,不同的方法将根据所使用的系统进行突出显示。

当前章节中描述的示例的源代码和配置文件可在以下位置找到:github.com/PacktPublishing/High-Performance-with-Laravel-Octane/tree/main/octane-ch01

探索 Laravel 生态系统

Laravel 是 PHP 生态系统中的一个优秀框架,它帮助开发者快速、可靠地构建 Web 应用程序。

它包括一些 PHP 生态系统的优秀工具作为依赖项,例如 Symfony 包,以及一些其他坚实且成熟的包,如用于日志记录的 Monolog,用于访问文件和存储的 Flysystem,以及用于管理 Markdown 格式的 CommonMark

从 Symfony 世界来看,Laravel 包括用于管理路由的 Symfony/routing 包,以及用于管理 HTTP 通信的 http-foundationhttp-kernel 包。

所有这些只是为了说明 Laravel 使用了 PHP 生态系统的最佳部分,将它们组合在一起,并为开发者提供工具、辅助函数、类和方法,以简化所有工具的使用。

此外,Laravel 不仅仅是一个框架。Laravel 是一个生态系统。

Laravel 还提供与框架集成的应用程序和服务。

例如,Laravel 提供以下内容:

  • 收银员:用于与 StripePaddle 集成,处理支付和订阅流程。

  • BreezeJetstreamSanctumSocialite:用于管理授权、身份验证、社交登录集成流程和公开受保护的 API。

  • 黄昏害虫:用于测试。

  • Echo:用于实时广播事件。

  • EnvoyerForgeVapor:用于服务器或无服务器管理和部署流程管理。

  • Mix:通过完全集成 Laravel 前端的 webpack 配置编译 JavaScriptCSS

  • Horizon:基于 Redis 的队列监控的 Web 用户界面。

  • Nova:Laravel 应用程序的管理员面板构建器。

  • Sail:基于 Docker 的本地开发环境。

  • Scout:一个全文搜索引擎,由 AlgoliaMeilisearch 或简单的 MySQLPostgreSQL 数据库提供支持。

  • Spark:用于管理应用程序中的计费/订阅的样板解决方案。

  • Telescope:用于显示调试和洞察的 UI 模块。

  • Valet:为运行 PHP 应用程序配置的 macOS 特定应用程序包。它依赖于 nginxPHPDnsmasq

  • Octane:用于提高性能和优化资源。

在这本书中,我们将分析此列表中的最后一个工具:Laravel Octane。

我们将介绍 Laravel 生态系统内其他工具的使用,例如 Sail(用于简化完整开发环境的安装过程),以及 Valet(用于正确设置本地环境以运行 Web 服务器和 PHP)。此外,Laravel Octane 依赖于本书中将深入探讨的重要软件。Laravel Octane 有严格的要求:它需要额外的软件,如 Swoole 或 RoadRunner。

但一步一个脚印。

在我们深入探讨工具及其配置之前,了解一些管理 HTTP 请求 的基本机制是很重要的。

HTTP

HTTP 是一种定义了在网络上获取资源(如 HTML 文档(网页)和资产)的规则、消息和方法的协议。客户端(需要资源的人)和服务器(提供资源的人)通过交换消息进行通信。客户端发送请求,服务器发送响应。

本书的一个目标是通过做不同的事情来赋予你提升你网络应用程序性能的能力,从设计应用程序架构开始,选择并使用正确的工具,编写代码,最后发布应用程序。

我们将要分析和使用的工具将完成大部分工作,但我认为理解其背后的动态对于有一个良好的意识了解各种工具如何工作,以便您能够以最佳方式配置、集成和使用它们,这一点非常重要。

在我们深入探讨 Laravel Octane 的工作原理之前,让我通过解释 HTTP 请求的生命周期来向您展示服务器通常如何处理 HTTP 请求。

理解 HTTP 请求生命周期

在执行 HTTP 请求的过程中涉及了许多组件。组件如下:

  • 客户端:这是请求开始和响应结束的地方(例如,浏览器)。

  • 网络:请求和响应通过这个连接服务器和客户端。

  • 代理(Proxy):这是一个可选组件,可以在请求到达 Web 服务器之前执行一些任务,例如缓存、重写和/或修改请求,并将请求转发到正确的 Web 服务器。

  • Web 服务器:它接收请求并负责选择正确的资源。

  • PHP:语言,或者更普遍地说,在服务器端语言的情况下,使用的特定语言引擎。在这种情况下,使用 PHP 解释器。PHP 解释器可以通过两种主要方式激活:作为 Web 服务器模块或作为单独的进程。在后一种情况下,使用一种称为**FastCGI 进程管理器(FPM)**的技术。我们将在稍后更详细地了解这种机制的工作原理。目前,了解 Web 服务器如何以某种方式调用服务器端语言解释器是有用的。通过这样做,我们的服务器能够解释语言。如果被调用的资源是具有特定 PHP 语法的 PHP 类型文件,则请求的资源文件将由 PHP 引擎解释。输出以响应的形式发送回 Web 服务器、网络,然后是浏览器。

  • 框架:如果应用程序是用 PHP 编写的并且使用了框架,作为开发者,你可以访问类、方法和辅助工具来更快地构建你的应用程序。

组件在 HTTP 请求流中按顺序调用。HTTP 请求从浏览器开始,然后通过网络(可选地通过代理),直到到达调用 PHP 引擎并引导框架的 Web 服务器。

从性能的角度来看,如果你想要带来一些改进,你必须采取一些行动或根据这个架构的元素实现一些解决方案。

例如,在浏览器端,你可以处理浏览器中的缓存资源或优化你的 JavaScript 代码。在网络方面,一个解决方案可能是资源优化,例如,减少资源的重量或引入如 CDN 等架构元素。在 Web 服务器的情况下,一个有效的第一级改进可能是避免加载 PHP 引擎来处理静态资源(非 PHP 文件)。

所有这些微调将在最终章节中解决,我们将处理生产元素的配置和优化。本书的大部分内容涵盖了框架的优化。例如,在第二章和第三章中,讨论了使用 Octane 与 Swoole 和 RoadRunner 等工具,这些工具能够更高效、更有效地加载资源(共享对象和结构)。框架侧的性能改进点还包括通过使用队列系统引入异步方法(第六章和第七章)。

既然你已经了解了 HTTP 请求中涉及的组件,让我们看看 HTTP 请求的结构。

HTTP 请求的结构

要详细了解典型 HTTP 请求中发生的情况,我们首先分析在请求期间浏览器发送到服务器的数据。请求主要特征是方法(GETPOSTPUT等)、URL 和 HTTP 头。

URL 在浏览器的地址栏中可见,而头由浏览器自动处理,对用户不可见。

以下描述了 HTTP 请求的结构:

  • GET方法:读取和检索资源

  • POST方法:创建新的资源

  • PUT方法:替换资源

  • PATCH方法:编辑资源

  • DELETE方法:删除资源

  • URL标识资源。我们将在下一节中解释 URL 结构(处理 HTTP 请求)。包括允许服务器理解如何处理资源的信息。这些信息可以包括认证信息、资源所需的格式等。体负载是附加数据,例如,当表单提交到服务器时发送的数据。

既然你已经了解了 HTTP 请求的结构,让我们看看这样的请求是如何处理的。

处理 HTTP 请求

一个 URL 由协议、主机名、端口、路径和参数组成。一个典型的 URL 如下所示:

<``protocol>://<hostname>:<port>/<path>?<parameters>

例如,一个 URL 可以是以下形式:

https://127.0.0.1:8000/home?cache=12345

构成 HTTP 请求的每一部分都由处理 HTTP 请求的各种软件专门使用:

  • 协议由浏览器用来确定通信加密(通过 HTTPS 加密或通过 HTTP 非加密)。

  • DNS 使用主机名解析主机名到 IP 地址,而 Web 服务器则用于涉及正确的虚拟主机。

  • 端口由服务器的操作系统用来访问正确的进程。

  • 路径被 Web 服务器用来调用正确的资源,并且用于框架激活路由机制。

  • 应用程序使用参数来控制逻辑的行为(服务器端用于查询参数,客户端用于锚点参数)。例如,查询参数在 URL 中的?字符之后定义,而锚点参数在 URL 中的#字符之后定义:https://127.0.0.1:8000/?queryparam=1#anchorparam

首先,定义了协议(通常是 HTTP 或 HTTPS)。接下来,指定了主机名,这对于确定要联系哪个服务器是有用的。然后,有一个通常不指定的部分,即端口号;通常,HTTP 的端口号是80,HTTPS 的端口号是443。还包括标识我们请求的服务器资源的路径。最后,还有两个其他可选部分处理参数。第一个是关于服务器端参数(查询字符串),第二个是关于客户端或 JavaScript 参数(带有锚点的参数)。

除了 URL 之外,请求的另一个特征元素是 HTTP 头部,这对于请求到达的服务器来说非常重要,以便更好地理解如何处理请求。

HTTP 头部基于一些信息和浏览状态由浏览器自动处理。通常,头部指定了资源的格式和其他信息;例如,它们指定了 MIME 类型、用户代理等等。

如果请求的资源受到保护,它们还指定了任何访问令牌。用于管理状态的头元素也以 cookie 和会话引用的形式存在于头部。这些信息对服务器理解并关联连续请求是有用的。

为什么理解一个请求是如何组成的如此重要?因为在分析关于性能的优化元素时,URL 的结构和组成头部的部分决定了网络架构(浏览器、网络、Web 服务器、服务器端语言和框架)中不同元素的行为。

例如,像主机名这样的元素对 DNS(网络)来说是有用的,可以将主机名解析为 IP 地址。了解这一点对于决定是否进行缓存,例如,对于名称解析来说是有用的。

每个涉及的元素都有其自身的特性,可以通过优化来获得更好的性能。

典型的对经典 PHP 应用程序的请求的一个特征元素是每个请求都是相互独立的。这意味着如果您的 PHP 脚本实例化了一个对象,这个操作会随着每个请求而重复。如果您的脚本只被调用几次且脚本很简单,这几乎没有影响。

让我们考虑一个场景,其中我们有一个基于框架的应用程序,该应用程序必须处理大量的并发请求。

一个基于框架的应用程序拥有许多可供使用的对象,这些对象必须在启动时实例化和配置。在经典的 PHP 案例中,框架的启动对应于一个请求。

另一方面,Laravel Octane 引入了一种新的启动应用程序的方式。

在经典的 Laravel 网络应用程序中,只需要一个网络服务器(如 nginx)或 Laravel 在开发者在本地计算机上进行开发时提供的内部网络服务器。一个经典网络服务器可以在没有任何类型的资源共享的情况下处理请求,除非这些资源是外部资源,如数据库或缓存管理器。

与经典网络服务器发生的情况相反,应用程序服务器有启动和管理多个工作进程执行的任务。每个工作进程将通过重用应用程序的对象和逻辑的一部分来处理多个请求。

这有一个好处,即你的应用程序的实际启动和设置各种对象是在从工作进程接收到的第一个请求上发生的,而不是在每个单独的请求上。

HTTP 请求和 Laravel

从 Laravel 应用程序的角度来看,直接参与 HTTPS 请求的部分通常是路由和控制器。

通过 Laravel 应用程序处理请求通常意味着需要在控制器中实现路由部分和实现管理请求的逻辑。路由允许我们在 Laravel 应用程序中针对 URL 中的特定路径定义要执行的代码。例如,我们可能想要定义,特定类(如 HomeController::home())中的方法代码必须在具有 /home 路径的请求中调用。在经典的 Laravel 定义中,我们会在 routes/web.php 文件中编写如下内容:

Route::get('/home', [HomeController::class, 'home'])->name("home");

现在我们必须在 HomeController 类(我们必须创建)中实现管理请求的逻辑,并实现 home 方法。因此,在一个新的 app/Http/Controllers/HomeController.php 文件中,你必须实现扩展基本控制器类的 HomeController 类:

<?php
namespace App\Http\Controllers;
class HomeController extends Controller
{
    public function home(): string
    {
        return "this is the Home page";
    }
}

既然你已经了解了网络服务器如何处理请求,那么让我们更多地了解 Laravel Octane 集成的应用程序服务器。

了解 Laravel Octane 的应用程序服务器

在 PHP 生态系统中,我们有几个应用程序服务器。

处理服务器配置、启动和执行的 Laravel Octane 集成了其中两个:Swoole 和 RoadRunner。

我们将在稍后详细讨论这些两个应用程序服务器的安装、配置和使用。

目前,对于我们来说,知道一旦安装了应用程序服务器,Laravel Octane 将负责它们的管理就足够了。Laravel Octane 还将通过以下命令负责它们的正确启动:

php artisan octane:start

当 Laravel Octane 安装时,会添加 octane:serve 命令。

换句话说,Laravel Octane 对应用程序服务器(如 RoadRunner 或 Swoole)有很强的依赖性。

在启动时,Laravel Octane 通过 Swoole 或 RoadRunner 激活了一些工作进程,如下面的图所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_1.1_B17728.jpg

图 1.1:工作进程的激活

工作进程是什么?

在 Octane 中,工作进程是一个负责处理与其关联的请求的进程。工作进程有责任启动框架和初始化框架对象。

这从性能角度来看具有极其积极的影响。框架是在分配给工作进程的第一个请求上实例化的。分配给该工作进程的第二个(以及随后的)请求将重用已经实例化的对象。这种副作用是,工作进程在请求之间共享全局对象和静态变量的实例。这意味着不同的控制器调用可以访问请求之间共享的数据结构。

更复杂的是,分配给同一工作进程的请求共享一个全局状态,但不同的工作进程是独立的,并且它们的作用域相互独立。因此,我们可以这样说,并非所有请求都共享相同的全局状态。当请求与同一工作进程相关联时,请求才共享全局状态。来自两个不同工作进程的两个请求之间没有任何共享。

为了最小化副作用,Laravel Octane 负责在请求之间管理框架直接拥有的类/对象的重置。

然而,Octane 无法管理和重置应用程序直接拥有的类。

因此,在使用 Octane 时,需要注意的主要是变量和对象的作用域和生命周期。

为了更好地理解这一点,我将给你一个非常基础的例子。

共享变量的示例

这个例子在 routes/web.php 文件中创建了一个 path / 的路由,并返回一个可读的时间戳。为了简化解释,我们将直接将逻辑写入路由文件,而不是调用并将逻辑委派给控制器:

$myStartTime = microtime(true);
Route::get('/', function () use($myStartTime) {
    return DateTime::createFromFormat('U.u', $myStartTime)
    ->format("r (u)");
});

routes/web.php 路由文件(web.php 已经存储在 Laravel 根文件夹项目的 routes 目录中),实例化了一个 $myStartTime 变量,并将其分配为以毫秒表示的当前时间。然后,该变量通过 use 子句由 route/management 函数继承。

在与 route/ 相关的函数的性能中,返回 $myStartTime 变量的内容,然后显示。

在 Laravel 应用程序的经典行为中,每次调用/执行时,变量都会被重新生成和重新初始化(每次都带有新值)。

要以经典模式启动 Laravel 应用程序,只需运行以下命令:

php artisan serve

一旦启动网络服务器,请通过浏览器访问以下 URL:http://127.0.0.1:8000

通过不断刷新页面,每次都会显示不同的值;基本上,每次请求都会显示时间戳。

与使用 Laravel 提供的开发网络服务器不同,您将使用 Laravel Octane 并得到不同的结果。在每次页面刷新(网页重新加载)时,您都会看到相同的值。该值相对于第一个请求的时间戳。这意味着变量是在第一个请求中初始化的,然后该值在请求之间被重复使用。

如果您尝试多次刷新,在某些情况下,您可能会看到新值。

如果发生这种情况,这意味着请求是由第二个(或新的)工作者处理的。这意味着这种行为相当不可预测,因为 Octane 充当负载均衡器。当网络请求到来时,应用程序服务器将决定将请求分配给哪个工作者(那些可用的工作者之一)。

此外,另一个可能导致生成新值的情况是当您达到单个工作者管理的请求最大数量时。我们将在后面看到如何定义最大请求数量,通常,我们将在第二章和第三章中进行 Laravel Octane 配置的深入探讨。

当变量在应用程序服务器重启之前在工作者之间共享的行为仅适用于全局变量或存储在应用程序服务容器中的对象。局部变量(其作用域仅限于函数或方法的变量)不受影响。

例如,在前面显示的代码中,我将在由路由机制调用的函数中声明一个 $myLocalStartTime 变量。$myLocalStartTime 变量的作用域及其生命周期仅限于 Closure 函数:

$myStartTime = microtime(true);
Route::get('/', function () use($myStartTime) {
    $myLocalStartTime = microtime(true);
    return DateTime::createFromFormat('U.u', $myStartTime)
    ->format("r (u)") . " - " .
    DateTime::createFromFormat('U.u', $myLocalStartTime)
    ->format("r (u)");
});

使用以下命令执行经典 Laravel 网络服务器:

php artisan serve

您将看到每个新请求时这两个值都会改变。您可以通过打开浏览器到 http://127.0.0.1:8000 来看到这一点。

使用以下命令以服务器模式启动 Octane:

php artisan octane:start

在浏览器中(http://127.0.0.1:8000),您将看到两个不同的日期/时间,带有毫秒数。如果您刷新页面,您将只看到第二个的变化($myLocalStartTime)。

当您基于 Octane 构建应用程序时,您必须注意这种行为。

为了更好地理解这种行为,我们可以创建一个具有静态属性的类作为另一个例子。

创建具有静态属性的类

为了使这个例子尽可能简单,我在 routes/web.php 文件中创建了一个 MyClass 类。

我将添加新的路由,这些路由调用MyClass对象的add()方法,然后调用并返回由get()方法检索到的静态属性的值。

routes/web.php中添加以下类:

class MyClass
{
    public static $number = 0;
    public function __construct()
    {
        print "Construct\n";
    }
    public function __destruct()
    {
        print "Deconstruct\n";
    }
    public function add()
    {
        self::$number++;
    }
    public function get()
    {
        return self::$number;
    }
}

然后,在routes/web.php文件中,声明新的路由如下:

Route::get('/static-class', function (MyClass $myclass) {
    $myclass->add();
    return $myclass->get();
});

接下来,你可以使用以下命令以经典方式启动 Laravel:

php artisan serve

现在,如果你多次访问 URL http://127.0.0.1:8000/static-class,将显示值1。这是因为,传统上,对于每个请求,MyClass对象都是从零开始实例化的。

使用以下命令启动 Laravel Octane:

php artisan octane:serve

如果你多次访问 URL http://127.0.0.1:8000/static-class,你将看到第一次请求的值为1,第二次为2,第三次为3,依此类推。这是因为,使用 Octane,MyClass对象为每个请求实例化,但静态值保存在内存中。

对于非静态属性,我们可以看到以下差异:

class MyClass
{
    public static $numberStatic = 0;
    public $number = 0;
    public function __construct()
    {
        print "Construct\n";
    }
    public function __destruct()
    {
        print "Deconstruct\n";
    }
    public function add()
    {
        self::$numberStatic++;
        $this->number++;
    }
    public function get()
    {
        return self::$numberStatic . " - " . $this->number;
    }
}

在调用页面五次之后,浏览器中显示的结果如下:

Construct Deconstruct 51

这相当简单,但最终有助于理解静态变量在底层的行为。

静态变量的使用并不罕见。只需想想单例对象或 Laravel 的应用容器。

为了避免意外行为——就像在这个特定示例中静态变量的情况,但更普遍地,与全局对象(Laravel 广泛使用它们)相关——必须注意显式重新初始化。在这种情况下,静态变量在构造函数中初始化。我的建议是在构造函数中显式初始化属性。这是因为在这种情况下,开发者有责任负责全局状态(对象和变量)的重新初始化。

class MyClass
{
    public static $numberStatic = 0;
    public $number = 0;
    public function __construct()
    {
        print "Construct\n";
        self::$numberStatic = 0;
    }
    public function __destruct()
    {
        print "Deconstruct\n";
    }
    public function add()
    {
        self::$numberStatic++;
        $this->number++;
    }
    public function get()
    {
        return self::$numberStatic . " - " . $this->number;
    }
}

我们已经看到了一些关于如果你要安装和使用 Laravel Octane 时对代码影响的非常基础的示例。前面展示的示例故意设计得非常简单,但目的是易于理解。在我们将在实际场景中使用 Octane 的章节中,我们将涵盖更现实的示例。

现在我们将分析对性能的影响。所以,通过安装 Octane,我们在性能方面可能会有什么样的改进?

理解 Laravel Octane 中的性能测量

我们已经说过,在你的应用程序中引入 Laravel Octane 可以提升性能,主要是因为框架使用的对象和类的各种实例不再在每个 HTTP 请求中初始化,而是在应用程序服务器启动时初始化。因此,对于每个 HTTP 请求,框架对象被重用。重用框架对象可以节省在处理 HTTP 请求中的时间。

虽然在逻辑和可理解层面上,这可能在性能方面产生积极影响,但本部分的目标是通过尝试恢复一些指标和值来获取这种性能提升的实用反馈。

为了提供一个关于请求的好处和改进响应时间性能的大致指示,让我们尝试进行一个简单的性能测试。

为了做到这一点,我们将安装一个工具来生成和执行一些 HTTP 并发请求。这类工具有很多,其中之一是 wrk (github.com/wg/wrk)。

如果你有一个 macOS 环境,你可以使用 brew 命令(由 Homebrew 提供)来安装 wrk 工具。要安装该工具,请使用如下所示的 brew install

brew install wrk

使用 wrk,你可以生成一定时间内的并发请求。

我们将进行两次测试以进行比较:一次是在 nginx 上的经典 Web 应用程序上进行测试(http://octane.test),另一次是在 Laravel Octane 上的应用程序服务器上提供服务(http://octane.test:8000)。

两个 URL 的解析如下所示:

  • http://octane.test/ 使用本地地址 127.0.0.1 解析,并将回复 nginx

  • http://octane.test:8000/ 使用本地地址 127.0.0.1 解析,端口 8000 由 Swoole 绑定

wrk 执行将使用 4 个线程,打开 20 个连接,并持续 10 秒的测试。

因此,为了测试 NGINX,使用以下参数启动 wrk 命令:

wrk -t4 -c20 -d10s http://octane.test

你将看到以下输出:

Running 10s test @ http://octane.test
  4 threads and 20 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    51.78ms   61.33ms 473.05ms   88.54%
    Req/Sec   141.79     68.87   313.00     66.50%
  5612 requests in 10.05s, 8.47MB read
  Non-2xx or 3xx responses: 2
Requests/sec:    558.17
Transfer/sec:    863.14KB

要测试 Laravel Octane(RoadRunner),请使用以下命令:

wrk -t4 -c20 -d10s http://octane.test:8000

你将看到以下输出:

Running 10s test @ http://octane.test:8000
  4 threads and 20 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   134.58ms  178.24ms   1.09s    79.75%
    Req/Sec   222.72    192.63     1.02k    73.72%
  7196 requests in 10.02s, 8.06MB read
Requests/sec:    718.51
Transfer/sec:    823.76KB

这个测试非常基础,因为没有涉及特殊的服务器端逻辑或查询数据库,但运行这个测试有助于理解 Laravel(应用程序容器、请求等)启动基本对象时的原始差异,并感知它们的味道。

差异并不大(7,196 个请求与 5,612 个请求),大约 22%,但考虑到如果你添加了新的包和库(每个请求都要启动更多的代码),这种差异会增长。

考虑到 RoadRunner 和 Swoole 还提供了其他一些用于提高性能的工具,例如启用并发和执行并发任务。这些附加工具将在第 2 章和 3 章中展示。

为了更好地解释为什么 Laravel Octane 允许你实现这种改进,让我演示一下服务提供者是如何和何时实例化并加载到服务容器中的。

通常情况下,在一个经典的 Laravel 应用程序服务中,提供者会在每个请求中加载。

app/Providers 目录中创建一个新的服务提供者 MyServiceProvider

<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class MyServiceProvider extends ServiceProvider
{
    public function __construct($app)
    {
        echo "NEW     - " . __METHOD__ . PHP_EOL;
        parent::__construct($app);
    }
    public function register()
    {
        echo "REGISTER     - " . __METHOD__ . PHP_EOL;
    }
    public function boot()
    {
        echo "BOOT     - " . __METHOD__ . PHP_EOL;
    }
}

新的服务提供者简单地在服务提供者创建、注册和启动时显示一条消息。

服务提供者的生命周期从三个阶段开始:创建、注册和启动。

register()boot() 方法需要用于 依赖解析。首先,每个服务提供者都会被注册。一旦它们都被注册,它们就可以启动。如果一个服务提供者在 boot 方法中需要另一个服务,你可以确信它已经准备好使用,因为它已经被注册了。

然后,你必须注册服务提供者,所以需要在 config/app.php 文件中的 providers 数组中添加 App\Providers\MyServiceProvider::class

在一个经典的 Laravel 网络应用程序中,对于每一个 HTTP 请求,MyServiceProvider 服务提供者都会被实例化,并且每次都会调用 constructregisterboot 方法,显示以下输出:

NEW          - App\Providers\MyServiceProvider::__construct
REGISTER     - App\Providers\MyServiceProvider::register
BOOT         - App\Providers\MyServiceProvider::boot

使用 Laravel Octane,会发生一些不同的事情。

为了更好地理解,我们将使用两个参数启动 Laravel Octane 服务器:

  • workers:应该可用于处理请求的工作进程数量。我们将把这个数字设置为 2

  • max-requests:在重新加载服务器之前要处理的请求数量。我们将把这个数字设置为每个工作进程的最大限制 5

要以两个工作进程启动 Octane 服务器,并在处理五个请求后重新加载服务器,我们输入以下命令:

php artisan octane:start --workers=2 --max-requests=5

启动 Octane 后,尝试使用浏览器访问以下 URL 进行多个请求:http://127.0.0.1:8000

以下是输出结果:

NEW          - App\Providers\MyServiceProvider::__construct
REGISTER     - App\Providers\MyServiceProvider::register
BOOT         - App\Providers\MyServiceProvider::boot
  200    GET / ...................................................... 113.62 ms
NEW          - App\Providers\MyServiceProvider::__construct
REGISTER     - App\Providers\MyServiceProvider::register
BOOT         - App\Providers\MyServiceProvider::boot
  200    GET / ....................................................... 85.49 ms
  200    GET / ........................................................ 7.57 ms
  200    GET / ........................................................ 6.96 ms
  200    GET / ........................................................ 6.40 ms
  200    GET / ........................................................ 7.27 ms
  200    GET / ........................................................ 3.97 ms
  200    GET / ........................................................ 5.17 ms
  200    GET / ........................................................ 8.41 ms
worker stopped
  200    GET / ........................................................ 4.84 ms
worker stopped

前两个请求大约需要 100 次调用 register()boot() 方法。

因此,我们可以看到前两个请求(因为我们有两个工作进程,所以是两个)比后续请求(从第三个到第十个请求,响应时间低于 10 毫秒)慢一些(113.62 毫秒和 85.49 毫秒)。

另一个需要提到的重要事情是,registerboot 方法在前两个请求中会被调用,直到第十个请求(两个工作进程乘以最多五个请求)。后续请求会重复这种行为。

因此,在您的网络应用程序中安装 Laravel Octane 允许您提高应用程序的响应时间。

所有这些都不需要涉及某些工具,例如 Swoole 和 RoadRunner 等应用服务器提供的并发管理工具。

摘要

现在,我们已经了解了 Laravel Octane 的行为、一些优点和一些副作用,我们可以继续下一章,通过安装和配置两个 Laravel Octane 兼容的应用服务器之一:RoadRunner 应用服务器。

我们将回顾本章中提到的某些指令。本章的目标是提供一些有用的总结元素,以解决本书其余部分中更具体和详细的案例。

第二部分:应用服务器

Laravel Octane 使用应用服务器,如 RoadRunner 或 Swoole。在应用服务器而不是经典网络服务器上运行应用程序,允许开发者使用一些高级功能,例如更有效地管理多个工作进程、并发任务以及以更高效的方式引导应用程序;快速缓存,以及在工作进程之间共享数据。本部分包括以下章节:

  • 第二章配置 RoadRunner 应用服务器

  • 第三章, 配置 Swoole 应用程序服务器

第二章:配置 RoadRunner 应用服务器

当在 Laravel 中开发一个 Web 应用程序时,我们习惯于使用 Web 服务器通过网络交付我们的 Web 应用程序。

Web 服务器通过 HTTPHTTPS 协议公开应用程序,并实现与通过 HTTP 协议交付资源紧密相关的功能。

应用服务器在处理不同协议方面是一个结构化和更复杂的软件组件;它可以处理 HTTP,以及更底层的协议,如 TCP,或其他协议,如 WebSocket

此外,应用服务器可以实现一个结构化的工作员结构。这意味着执行应用程序逻辑的应用服务器将执行委托给工作员。工作员是一个负责执行给定任务的隔离线程。

工作员管理允许通过应用服务器运行的应用程序访问并发性和并行任务执行等功能。

要能够管理各种工作员,应用服务器还必须能够实现跨工作员的负载分配功能,并且还必须能够实现适当的平衡(使用负载均衡器)。

在 PHP 生态系统中有很多应用服务器,其中两个是 RoadRunnerSwoole。它们与 Laravel 生态系统相关,因为它们直接由 Laravel Octane 支持。

这两种解决方案具有不同的功能;然而,它们都允许 Laravel Octane 启动不同的工作员,这些工作员将接管 HTTP 请求的解析。

通过 Laravel Octane 可访问的附加功能,仅在 Swoole(而不是 RoadRunner)中可用,包括执行多个并发函数的能力,通过特殊表以优化的方式管理共享数据,以及以计划重复的模式启动函数。我们将在 第三章 中介绍 Swoole 提供的附加功能,配置 Swoole 应用服务器

在可用的功能方面,RoadRunner 可能是应用服务器中最简单的一个,它也是最容易安装的。

因此,为了熟悉 Laravel Octane 的配置,我们将从使用 RoadRunner 开始。

本章的目标是向您展示如何设置基本的 Laravel 应用程序,添加 Laravel Octane,使用 RoadRunner 启动 Octane,并对其进行配置。

理解设置和配置是允许您控制应用程序行为的第一个步骤。

在本章中,我们将涵盖以下主题:

  • 设置基本的 Laravel 应用程序

  • 安装 RoadRunner

  • 安装 Laravel Octane

  • 启动 Laravel Octane

  • Laravel Octane 和 RoadRunner 高级配置

技术要求

本章将涵盖框架和应用服务器设置(安装和配置)。

假设您已经安装了 PHP 和 Composer。我们建议您使用 PHP(至少版本 8.0)并将 Composer 更新到最新版本。

通常,我们有两种主要方法来安装语言和开发工具。第一种是在您的机器操作系统中直接安装工具。第二种是在隔离环境中安装工具,例如虚拟机或 Docker。

如果您想使用 Docker 跟随书中的说明和示例,假设您已经在您的机器上安装了 Docker Desktop。

对于 Docker,我们将提供您必要的说明,以便使用 PHP 和 Composer 运行一个镜像。

采用这种方法,您将能够以相同的方式运行命令和遵循示例,无论您是否有 Docker 或想原生运行 PHP 和 Composer。

我们将从控制台应用程序(或终端模拟器)启动命令,因此预期您非常熟悉此类应用程序(终端、iTerm2、MacOS 的 Warp、Windows 的 Windows Terminal、Terminator、xterm、GNOME 终端、GNU/Linux 的 Konsole,或者适用于所有操作系统的 Alacritty)。

在终端模拟器中,您需要一个 shell 环境,通常是 Bash 或 ZSH(Z shell)。我们将使用 shell 配置来设置一些环境变量,例如 PATH 变量。PATH 变量指定了搜索命令的目录。

源代码

您可以在本书的官方 GitHub 仓库中找到本章使用的示例源代码:github.com/PacktPublishing/High-Performance-with-Laravel-Octane/tree/main/octane-ch02

设置基本的 Laravel 应用程序

本章的目标是配置 Laravel Octane 与 RoadRunner 应用程序服务器。为此,我们必须安装 RoadRunner 应用程序服务器。然而,在安装之前,我们必须首先创建一个新的 Laravel 应用程序,然后添加并安装 Laravel Octane 包。简而言之,为了演示如何安装 RoadRunner,我们将执行以下操作:

  1. 从头创建一个新的 Laravel 应用程序。

  2. 将 Laravel Octane 包添加到新的 Laravel 应用程序中。

  3. 安装 Laravel Octane,执行 Laravel Octane 包提供的特定命令。命令执行将创建一个基本配置,这对于我们开始使用 Laravel Octane 非常有用。我们将在后面的 安装 Laravel Octane 部分展示如何安装 Laravel Octane。

获取 Laravel 安装程序

要从头开始安装 Laravel,您可以使用 Laravel 安装程序。要全局安装 Laravel 安装程序,在您的终端模拟器中输入以下命令:

composer global require laravel/installer

一旦安装了 Laravel,请确保您的 PATH 环境变量包括存储全局 composer 包的目录,通常是在您的主目录中的 .composer/vendor/bin/

为了使PATH变量持久化并确保在操作系统重启后正确加载,您可以将其添加到您的 shell 配置文件中。例如,如果您正在使用 Zshell,请在您的.zshrc文件中添加此行:

export PATH=$PATH:~/.composer/vendor/bin/

确保您的 shell 配置正确重新加载并且您正在使用 Zshell,请输入以下命令:

source ~/.zshrc

如果您有疑问,请重新启动控制台应用程序(您用于启动命令的应用程序)。

要检查一切是否正常,请尝试通过命令行使用-V选项执行 Laravel 安装器工具:

laravel -V

如果您收到类似Laravel Installer 4.2.11的输出,那么一切正常;否则,您可能会看到类似command not found的错误。在这种情况下,我的建议是检查以下内容:

  • laravel命令存在于~/.composer/vendor/bin/

  • laravel命令是可执行的

  • PATH变量包括~/.composer/vendor/bin/目录

要检查 Laravel 安装器命令是否存在并可执行,您可以使用经典的ls命令进行检查:

ls -l ~/.composer/vendor/bin/laravel

要查看权限是否包含x字符,您将看到类似-rwxr-xr-x的输出。

如果命令存在于正确的位置但没有可执行权限,您可以使用chmod命令修复它,为所有者(u)添加可执行(+x)权限:

chmod u+x ~/.composer/vendor/bin/laravel

如果命令存在并且具有正确的权限,请检查PATH变量是否正确并且包括~/.composer/vendor/bin/路径。

如果PATH变量不包括正确的路径,请检查您是否已将其添加到PATH变量中,如果PATH变量包括正确的路径,请确保已重新加载 shell 环境或至少重启您的终端模拟器。

我想对这种检查多说几句。这种检查很有用,并且随着我们添加新的命令,它将继续有用。命令的存在、其权限和其可达性是可以在遇到运行新安装的命令时节省时间的检查。

现在,让我向您展示如何在添加 Laravel Octane 之前安装 Laravel 应用程序。

从头开始安装新的 Laravel Web 应用程序

要创建一个新的基本 Laravel 应用程序,我们可以使用 Laravel 安装器:

laravel new octane-ch2

如果您没有 Laravel 安装器,您可以使用composer命令安装 Laravel 应用程序:

composer create-project laravel/laravel octane-ch2

在基本使用中,这些命令(laravel newcomposer create-project)相当相似。它们执行以下操作:

  • 克隆laravel/laravel仓库

  • .env.example创建.env文件

  • 安装composer.json中找到的所有依赖项

  • 生成优化的自动加载文件

  • 通过执行php artisan package:discover命令注册或发现任何新的支持包

  • 发布laravel-assets文件

  • .env文件中生成应用程序密钥

我建议你使用 Laravel 命令,因为它有一些额外的选项和参数,允许你启用一些很酷的功能,例如添加 Jetstream 框架,选择 Livewire 栈或 Inertia 作为 Jetstream,以及启用 Jetstream 的团队管理。所有这些选项,对于我们目前的目标来说都不是必需的,因此出于这个原因,使用第一个或第二个命令的结果是相同的。

因此,现在你可以进入新的 octane-ch2 目录来检查你的新 Laravel 应用程序。

要启动内部 Web 服务器,你可以使用 Laravel 提供的 artisan 命令:

php artisan serve

如果你打开浏览器访问 http://127.0.0.1:8000,你可以看到 Laravel 的默认主页。

现在我们已经将 Laravel 应用程序启动并运行,是时候设置 RoadRunner 应用服务器了。

安装 RoadRunner

RoadRunner 是一个成熟且稳定的 PHP 应用服务器,因此你可以在生产环境中使用它。它是用 Go 编程语言编写的,这意味着在底层,RoadRunner 使用 Go 提供的 goroutines 和多线程功能。由于其 Go 实现,RoadRunner 在最常用的操作系统上运行,如 macOS、Windows、Linux、FreeBSD 和 ARM。

再次感谢其 Go 实现版本,RoadRunner 以二进制文件的形式发布,因此安装过程非常简单。

RoadRunner 是一个开源项目,因此你可以访问源代码、二进制文件和文档:

我们可以通过多种方式获取 RoadRunner。

为了快速开始,我将使用 Composer 方法。Composer 方法需要两个步骤:

  1. 安装 RoadRunner CLI。

  2. 通过 RoadRunner CLI 获取 RoadRunner 二进制文件。

因此,作为第一步,让我根据官方文档安装 RoadRunner CLI,官方文档可在 roadrunner.dev/docs/intro-install 找到:

composer require spiral/roadrunner:v2.0 nyholm/psr7

如你所见,我们打算添加两个包:

  • RoadRunner CLI 版本 2

  • Nyholm 对 PSR7 的实现

Nyholm

Nyholm 是一个实现 PSR7 标准的开源 PHP 包。源代码在这里:github.com/Nyholm/psr7

最后,composer 命令将两行添加到你的 composer.json 文件的 require 部分:

        "nyholm/psr7": "¹.5",
        "spiral/roadrunner": "v2.0"

提到的 PSR7 是一个定义 PHP 接口以表示 HTTP 消息和 URI 的标准。这样,如果你打算使用一个库来管理 HTTP 消息和 URI,并且该库实现了 PSR7 标准,你就知道你有一些具有标准化签名的标准方法来管理请求。例如,你知道你有 getMethod() 来获取 HTTP 方法,其值是一个字符串(因为它是根据标准定义的)。

通过 Composer 安装 RoadRunner CLI 后,你将在 vendor/bin/ 目录中找到 rr 可执行文件。

要检查它是否存在,请使用此命令:

ls -l vendor/bin/rr

你将看到一个文件,大约 3 KB,具有可执行权限(由 x 符号表示)。

这个可执行文件是 RoadRunner CLI,它允许你安装 RoadRunner 应用程序服务器可执行文件。要获取可执行文件,你可以使用 get-binary 选项执行 RoadRunner CLI:

vendor/bin/rr get-binary

命令执行生成的输出将显示软件包版本、操作系统和架构,如图下所示:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_2.1_B17728.jpg

图 2.1:获取 RoadRunner 可执行文件

你可能会对此有些混淆,因为 RoadRunner CLI 可执行文件被命名为 rr,就像 RoadRunner 可执行文件一样。你应该知道的是,RoadRunner CLI 存储在 vendor/bin 目录中,而 RoadRunner 应用程序服务器可执行文件存储在项目根目录中。此外,CLI 大约 3 KB,而应用程序服务器大约 50 MB。

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_2.2_B17728.jpg

图 2.2:两个 rr 可执行文件,CLI 和应用程序服务器

此外,你可以使用显示版本选项运行两个可执行文件:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_2.3_B17728.jpg

图 2.3:rr 版本

现在我们已经安装了 rr 可执行文件(RoadRunner),我们可以开始使用它了。

执行 RoadRunner 应用程序服务器(不使用 Octane)

要使用基本示例执行 RoadRunner 应用程序服务器,我们需要做以下事情:

  • 创建一个配置文件

  • 创建一个应用程序服务器在 HTTP 请求击中应用程序服务器时调用的 PHP 脚本

  • 启动应用程序服务器

默认情况下,RoadRunner 的配置文件是 .rr.yaml。它包含许多配置指令和参数。

一个最小配置文件需要一些东西:

  • 为每个工作实例启动的命令 (server.command)

  • 绑定和监听新 HTTP 连接的地址和端口 (http.address)

  • 要启动的工作进程数量 (http.pool.num_workers)

  • 日志级别 (logs.level)

这里展示了考虑上述因素的一个配置文件示例:

version: '2.7'
server:
  command: "php test-rr.php"
http:
  address: "0.0.0.0:8080"
  pool:
    num_workers: 2
logs:
  level: info

使用此配置文件,test-rr.php 是为工作进程启动的脚本,8080 是监听连接的端口,使用 2 个工作进程和 info 作为日志级别。

实现工作者逻辑的脚本文件是test-rr.php

<?php
include 'vendor/autoload.php';
use Nyholm\Psr7;
use Spiral\RoadRunner;
$worker = RoadRunner\Worker::create();
$psrFactory = new Psr7\Factory\Psr17Factory();
$worker = new RoadRunner\Http\PSR7Worker($worker, $psrFactory, $psrFactory, $psrFactory);
// creating a unique identifier specific for the worker
$id = uniqid('', true);
echo "STARTING ${id}";
while ($req = $worker->waitRequest()) {
    try {
        $rsp = new Psr7\Response();
        $rsp->getBody()->write("Hello ${id}");
        echo "RESPONSE SENT from ${id}";
        $worker->respond($rsp);
    } catch (\Throwable $e) {
        $worker->getWorker()->error((string) $e);
        echo 'ERROR ' . $e->getMessage();
    }
}

脚本执行以下操作:

  • 包含vendor/autoload.php

  • 使用 RoadRunner 提供的类实例化worker对象(RoadRunner\Http\PSR7Worker

  • 为显示流量如何平衡和委派给两个工作者生成一个唯一的 ID($id = uniqid('', true)

  • 等待新的连接($worker->waitRequest()

  • 当新的连接请求到达时,生成新的响应($worker->respond()

使用配置文件和前面的工作者脚本,您可以使用serve选项启动应用程序服务器:

./rr serve

使用此配置,您将看到由服务器启动的一个服务器和两个工作者。

现在,您可以通过curl命令来启动服务。curl命令是一种向特定 URL 发送 HTTP 请求的命令。

在另一个终端模拟器(或另一个标签页)的实例中,启动以下命令:

curl localhost:8080

通过执行curl四次,我们将向端口8080的应用程序服务器发送四个不同的请求。

在终端模拟器中,如果您启动应用程序服务器,您将看到应用程序服务器的日志:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_2.4_B17728.jpg

图 2.4:应用程序服务器的 INFO 日志

最重要的是,在第一次请求后的前两次请求中,经过的时间至少减少了十倍。

如果您查看elapsed值,您将看到第一次请求需要 20 毫秒来执行,而后续请求大约需要几百微秒(1 毫秒等于 1,000 微秒)。

响应时间(以毫秒计的绝对值)可能取决于多个因素(负载、资源、内存、CPU)。请查看相对值以及响应时间在后续请求中的减少程度。响应时间从几毫秒显著减少到几微秒。

因此,我们说,得益于基于 RoadRunner 实现的工作者架构,我们可以提高性能,尤其是在第一次请求之后的请求中。

但我们如何在 Laravel 应用程序中包含和使用 RoadRunner 呢?

之前的示例在纯 PHP 环境中使用了 RoadRunner 提供的对象和方法。现在我们必须弄清楚如何将这些功能/改进包括在 Laravel 中,特别是与框架启动相关的一切。

这就是 Octane 的目标。它允许我们在隐藏集成复杂性、启动过程和配置的同时使用 RoadRunner 的功能。

安装 Laravel Octane

创建了脚本文件(test-rr.php)和配置文件(.rr.yaml),以便理解 RoadRunner 的操作动态。现在,让我们专注于 Laravel Octane 的安装。让我们从通过laravel new命令安装 Laravel 应用程序和通过运行composer require然后运行rr get-binaries来安装 RoadRunner 可执行文件继续讨论。让我简要回顾一下:

# installing Laravel application
laravel new octane-ch2b
# entering into the directory
cd octane-ch2b
# installing RoadRunner CLI
composer require spiral/roadrunner:v2.0 nyholm/psr7
# Obtaining Roadrunner Application Server executable, via CLI
vendor/bin/rr get-binary

现在您可以安装 Laravel Octane:

composer require laravel/octane

然后,您可以使用octane:install命令正确配置 Laravel Octane:

php artisan octane:install

使用最新命令,您必须决定是使用 RoadRunner 还是 Swoole。为了本章的目的,选择 RoadRunner。我们将在下一章介绍 Swoole。

octane:install执行的任务如下:

  • 避免在 Git 仓库中提交/push RoadRunner 文件:检查并最终修复包含rr(RoadRunner 可执行文件)和.rr.yaml(RoadRunner 配置文件)的gitignore文件。

  • 确保项目中已安装 RoadRunner 包。如果没有,它将执行composer require命令。

  • 确保 RoadRunner 二进制文件已安装到项目中。如果没有,它将执行./vendor/bin/rr get-binary以下载 RoadRunner 应用程序服务器。

  • 确保 RoadRunner 二进制文件可执行(chmod 755)。

  • 检查一些要求,例如版本 2.x,如果 RoadRunner 应用程序服务器已经安装。

  • .env文件中设置OCTANE_SERVER环境变量(如果尚未存在)。

最后的octane:install命令将创建一个config/octane.php文件,并将一个新的配置键添加到.env文件中。新的键名为OCTANE_SERVER,其值设置为roadrunner

此值在config/octane.php文件中使用:

return [
    /*
    |--------------------------------------------------------------------------
    | Octane Server
    |--------------------------------------------------------------------------
    |
    | This value determines the default "server" that will
      be used by Octane
    | when starting, restarting, or stopping your server
      via the CLI. You
    | are free to change this to the supported server of
      your choosing.
    |
    | Supported: "roadrunner", "swoole"
    |
    */
    'server' => env('OCTANE_SERVER', 'roadrunner'),

因此,通过环境变量,您可以控制您想要使用哪个应用程序服务器。

现在我们已经安装了 Laravel Octane,是时候启动它了。

启动 Laravel Octane

要启动 Laravel Octane,请运行以下命令:

php artisan octane:start

一旦 Laravel Octane 启动,您可以在浏览器中访问http://127.0.0.1:8000

您的浏览器将显示经典的 Laravel 欢迎页面。Laravel 和 Laravel Octane 的欢迎页面在视觉上没有差异。最大的区别是您的应用程序通过 HTTP 服务的方式。

您可以使用一些参数来控制 Octane 的执行:

  • --host:默认127.0.0.1,服务器应绑定的 IP 地址

  • --port:默认8000,服务器应可用的端口

  • --workers:默认自动,应可用于处理请求的工作进程数量

  • --max-requests:默认500,在重新加载服务器之前要处理的请求数量

例如,您可以使用两个工作进程启动 Octane:

php artisan octane:start --workers=2

因此,现在,在http://localhost:8000上打开页面超过两次(两个是工作进程的数量)。您可以通过浏览器打开页面或通过启动curl

curl localhost:8000

您可以看到一些我们已知的内容,因为之前已经测试了安装了 Laravel 的 RoadRunner。前两次请求(工作进程的数量为两个)比后续请求慢。

以下输出与 Laravel Octane 服务器显示的日志相关:

  200    GET / .................................. 76.60 ms
  200    GET / .................................. 60.39 ms
  200    GET / ................................... 3.46 ms
  200    GET / ................................... 2.70 ms
  200    GET / ................................... 2.66 ms
  200    GET / ................................... 3.66 ms

如果您要启动服务器,请定义在启动服务器之前要处理的请求的最大数量(对于每个工作进程):

php artisan octane:start --workers=2 --max-requests=3

您可以看到类似的输出,但经过六次请求(两个工作进程的最大请求次数为三次),您将看到消息工作进程已停止,并且停止工作进程后的响应时间与第一次和第二次请求相同:

  200    GET / .................................. 86.56 ms
  200    GET / .................................. 52.30 ms
  200    GET / ................................... 2.38 ms
  200    GET / ................................... 2.73 ms
  200    GET / ................................... 2.57 ms
worker stopped
  200    GET / ................................... 2.75 ms
worker stopped
  200    GET / .................................. 63.95 ms
  200    GET / .................................. 60.83 ms
  200    GET / ................................... 1.75 ms
  200    GET / ................................... 2.74 ms

为什么重启服务器很重要?为了确保我们防止由于对象(服务器和工作进程)的长期生命周期导致的任何内存泄漏问题,重置状态是一种常见的做法。如果您不打算在命令行中定义 max-requests 参数,Laravel Octane 会自动将其设置为 500

在经典的 Web 服务器场景(没有 Laravel Octane)中,与您的应用程序相关的所有对象的生命周期,尤其是框架自动实例化和管理的对象的生命周期,都被限制在每个单独的请求中。在每次请求中,框架工作所需的所有对象都会被实例化,并在将响应发送回客户端时销毁对象。这也解释了为什么在具有 Web 服务器的经典框架中,响应时间比已初始化的工作者的响应时间长。

现在 Laravel Octane 已启动,我们可以查看其配置。

Laravel Octane 和 RoadRunner 高级配置

如前所述,我们可以在启动 Laravel Octane 期间控制一些参数。这是因为您想更改一些选项,例如工作进程的数量或端口,以及,如以下示例所示,如果您想激活 HTTPS 协议。

在底层,Octane 从命令行收集参数和一些 Octane 配置,并启动 RoadRunner 进程(它启动了 rr 命令)。

在 Octane 源代码中,有一个名为 StartRoadRunnerCommand.php 的文件,该文件实现了一个 Laravel artisan 命令,其代码如下:

$server = tap(new Process(array_filter([
    $roadRunnerBinary,
    '-c', $this->configPath(),
    '-o', 'version=2.7',
    '-o', 'http.address='.$this->option('host').':
      '.$this->option('port'),
    '-o', 'server.command='.(new PhpExecutableFinder)-
     >find().' '.base_path(config('octane.roadrunner
     .command', 'vendor/bin/roadrunner-worker')),
    '-o', 'http.pool.num_workers='.$this->workerCount(),
    '-o', 'http.pool.max_jobs='.$this->option(
      'max-requests'),
    '-o', 'rpc.listen=tcp://'.$this->option('host').':
     '.$this->rpcPort(),
    '-o', 'http.pool.supervisor.exec_ttl='
     .$this->maxExecutionTime(),
    '-o', 'http.static.dir='.base_path('public'),
    '-o', 'http.middleware='.config(
     'octane.roadrunner.http_middleware', 'static'),
    '-o', 'logs.mode=production',
    '-o', app()->environment('local') ? 'logs.level=debug'
     : 'logs.level=warn',
    '-o', 'logs.output=stdout',
    '-o', 'logs.encoding=json',
    'serve',
]), base_path(), [
    'APP_ENV' => app()->environment(),
    'APP_BASE_PATH' => base_path(),
    'LARAVEL_OCTANE' => 1,
]))->start();

查看此源代码有助于您了解用于启动 RoadRunner 可执行文件的参数。

使用 -c 选项($this->configPath()),将加载一个额外的配置文件。这意味着如果 Octane 管理的基本选项与您的期望不匹配,您可以在 .rr.yaml 配置文件中定义它们。

Octane 管理的基本参数(如前所述)包括主机名、端口、工作进程数量、最大请求次数、管理器的最大执行时间、HTTP 中间件和日志级别。

RoadRunner 配置文件允许你加载特殊和高级配置。一个经典的例子是允许本地 RoadRunner 实例监听并接收 HTTPS 请求。

为什么需要在开发环境中本地提供 HTTPS?你可能需要激活 HTTPS 协议,因为一些浏览器功能仅在页面通过 HTTPS 或 localhost 提供服务时才可用。这些功能包括地理位置、设备运动、设备方向、音频录制、通知等。

通常,在本地开发过程中,我们习惯于通过 localhost 提供服务页面。在这种情况下,没有必要通过 HTTPS 提供服务。然而,如果我们想将页面暴露给本地网络,以便通过连接到本地网络的移动设备测试我们的 Web 应用程序,我们必须确保服务可以通过有效的本地网络地址访问,因此 localhost 是不够的。在这种情况下(对于那些特殊的浏览器功能),需要 HTTPS。

或者另一种场景,即你本地提供的页面包含在网页中(通过 iFrame 或作为资产),而主页面是通过 HTTPS 提供的。在这种情况下,在 HTTPS 环境中包含资产或包含通过 HTTP 提供的页面会在浏览器中引发安全异常。

如果你想要配置 Octane 以服务 HTTPS 请求,你必须执行以下操作:

  • 安装一个允许你创建和管理证书的工具,例如mkcert。由于 HTTPS 的设计和实现,该协议需要公钥/私钥证书才能工作。

  • 为 localhost 或你想要的地址创建证书。

  • 查看 CA 证书和密钥存储位置。

为了更好地理解需要什么,让我们看看 RoadRunner 的 HTTPS 配置:

version: "2.7"
http:
  # host and port separated by semicolon
  address: 127.0.0.1:8000
  ssl:
    # host and port separated by semicolon (default :443)
    address: :8893
    redirect: false
    # Path to the cert file. This option is required for
    # SSL working.
    # This option is required.
    cert: "./localhost.pem"
    # Path to the cert key file.
    # This option is required.
    key: "./localhost-key.pem"
    # Path to the root certificate authority file.
    # This option is optional.
    root_ca: "/Users/roberto/Library/Application\
              Support/mkcert/rootCA.pem"

两个必填字段和一个可选文件如下:

  • Cert:证书文件

  • Key:证书密钥文件

  • Root_ca:根证书颁发机构文件

使用前两个文件时,HTTPS 可以工作,但你的浏览器会发出警告(因为没有有效的证书,因为证书是自签名的)。只填写前两个参数,证书会被评估为自签名的,通常情况下,浏览器不会认为这样的证书是可信的。

使用第三个文件时,浏览器允许你通过 HTTPS 浏览而没有任何警告(证书是有效的)。

因此,首先,你必须安装mkcert。mkcert 的 Git 仓库是github.com/FiloSottile/mkcert

mkcert 是一个适用于所有平台的开源工具。

安装 mkcert 并为 macOS 创建证书的说明如下:

brew install mkcert
mkcert -install
mkcert localhost

如果你使用 Windows,可以使用 Chocolatey 软件包管理器(chocolatey.org/)并使用以下命令:

choco install mkcert

对于 GNU/Linux,你可以使用你发行版提供的软件包管理器。

现在,项目目录中有两个新文件:localhost-key.pemlocalhost.pem

注意

我强烈建议将这些两个文件列入 .gitignore 文件中,以防止它们被推送到你的 Git 仓库(如果使用的话)。

你可以在你的 .rr.yaml 文件中使用第一个作为 key 参数,第二个作为 cert 参数。

要填充 root_ca 参数,你必须通过 mkcert 命令(使用 CAROOT 选项)查看 CA 文件存储的位置:

mkcert -CAROOT

此命令将显示存储 CA 文件的位置。

要查看 CA 文件名,请运行以下命令:

ls "$(mkcert -CAROOT)"

你可以用 rootCA.pem 文件的完整路径填充 root_ca 参数。

注意

如果你使用的是 Firefox,并且仍然收到自签名证书警告,请安装 certutil(使用 Homebrew,certutil 包含在 nss 包中,因此执行 brew install nss),然后再次执行 mkcert -install(并重新启动 Firefox 浏览器)。

现在,你可以使用以下命令启动 Octane:

php artisan octane:start

address 参数定义的 URL 上打开你的浏览器。根据最后一个示例中使用的参数(.rr.yaml 文件中的 RoadRunner 配置),你应该打开浏览器并打开此 URL:https://127.0.0.1:8893。(注意 https:// 是协议而不是 http://

因此,现在你已经熟悉了如何使用 Laravel Octane 安装 RoadRunner,启动 Octane 服务器,以及访问高级配置。

摘要

在本章中,我们探讨了使用 RoadRunner 应用程序服务器安装和配置 Laravel Octane。我们查看了我们从使用 RoadRunner 获得的好处以及如何启用高级功能,如 HTTPS 协议。

在下一章中,我们将看到如何使用 Swoole 做同样的事情。我们将看到 Swoole 相比 RoadRunner 的额外功能,并在 第四章构建 Laravel Octane 应用程序 中,我们将开始查看使用 Octane 服务运行的 Web 应用程序的代码,该服务现在正在运行。

第三章:配置 Swoole 应用服务器

使用 Laravel Octane,您可以选择另一种类型的应用服务器。除了 RoadRunner 之外,我们还可以配置和使用 Swoole。这两个工具都允许您实现应用服务器。显然,这两个工具之间存在不同的元素,有时决定使用哪一个可能会很困难。

正如我们所见,RoadRunner 是一个独立的可执行文件,这意味着其安装,如在第二章配置 RoadRunner 应用服务器中所示,相当简单,不会影响 PHP 引擎的核心。这意味着它很可能对引擎核心中的其他工具没有副作用。这些工具(如 Xdebug)通常是诊断、监控和调试工具。

我想给出的建议是,在分析和选择应用服务器的过程中,评估可能对开发过程有用的各种其他工具(如 Xdebug),并评估它们与 Swoole 的兼容性。

尽管管理 Swoole 的复杂性更高,但 Swoole 提供了诸如内存缓存管理(性能上的好处)、Swoole Table(用于在不同进程之间共享信息的数据存储,便于进程间的信息共享和更好的协作),以及能够在特定间隔启动异步函数和进程的能力(从而实现函数的异步和并行执行)等额外和高级功能。

在本章中,我们将了解如何使用 Laravel Octane 安装和配置 Swoole,以及如何最好地使用 Swoole 提供的特定功能。在本章中,我们将探讨 Swoole 的功能,例如执行并发任务、执行间隔任务、使用 Swoole 提供的高性能缓存和存储机制,以及访问关于工作者使用情况的指标。所有这些功能都可通过 Swoole 应用服务器通过 Octane 提供。

尤其是以下内容:

  • 使用 Laravel Sail 配置 Laravel Octane 与 Swoole

  • 安装 Open Swoole

  • 探索 Swoole 功能

技术要求

本章将涵盖 Swoole 和 Open Swoole 应用服务器的设置(安装和配置)。

与我们为 RoadRunner 所做的不同,在这种情况下,我们必须安装一个 PHP 扩展社区库PECL)扩展,以便 PHP 能够与 Swoole 一起操作。

什么是 PECL?

PECL 是 PHP 扩展的仓库。PECL 扩展是用 C 语言编写的,并且必须编译后才能与 PHP 引擎一起使用。

由于安装 PECL 模块及其所有依赖项的编译、配置和设置复杂,我们将使用容器方法——因此,我们不会在我们的个人操作系统中安装编译 PHP 扩展所需的所有必要工具,而是使用Docker

这使我们能够在真实的操作系统内托管一个运行中的操作系统(容器)。在容器中拥有一个隔离的操作系统目的是为了包含 PHP 开发环境所需的所有内容。

这意味着如果我们安装依赖项和工具,我们将在一个隔离的环境中完成,而不会影响我们的真实操作系统。

在上一章中,我们没有使用这种方法,因为要求更简单。然而,许多人使用容器方法来处理每个开发环境,即使是简单的那些。

随着开发环境开始需要额外的依赖项,它可能变得难以管理。在进化的情况下,这可能会变得尤其难以管理:试着想象同时管理多个版本的 PHP,这可能会需要依赖项的额外版本。为了在一致的环境中隔离和限制所有这些,建议使用容器方法。为此,建议安装Docker Desktop (www.docker.com/products/docker-desktop/)。

一旦我们安装了 Docker Desktop,我们将配置一个带有所需扩展的特定 PHP 镜像。

我们将要安装的所有新包都将存储在这个镜像中。当我们删除开发环境时,只需删除使用的镜像即可。我们操作系统中安装的唯一工具将是 Docker Desktop。

因此,要安装 Docker Desktop,只需下载并按照适用于您操作系统的安装向导进行即可。在 macOS 的情况下,请参考参考芯片的类型(Intel 或 Apple)。

如果你没有深入了解 Docker,不要担心——我们将使用 Laravel 生态系统中的另一个强大工具:Laravel Sail

Laravel Sail 是一个命令行界面,它公开了用于管理 Docker 的命令,特别是针对 Laravel。Laravel Sail 简化了 Docker 镜像的使用和配置,使开发者能够专注于代码。

我们将使用 Laravel Sail 命令来创建开发环境,但底层将产生 Docker 命令和现成的 Docker 配置。

源代码

你可以在这个章节中使用的示例的源代码在本书的官方 GitHub 仓库中:github.com/PacktPublishing/High-Performance-with-Laravel-Octane/tree/main/octane-ch03

使用 Laravel Sail 设置 Laravel Octane 与 Swoole

为了有一个使用 Swoole 作为应用服务器并通过 Docker 容器运行的运行环境,你必须遵循一些步骤:

  1. 设置 Laravel Sail

  2. 安装 Laravel Octane

  3. 设置 Laravel Octane 和 Swoole

设置 Laravel Sail

首先,创建你的 Laravel 应用:

laravel new octane-ch03
cd octane-ch03

或者,如果你已经有了你的 Laravel 应用,你可以使用composer show命令来检查 Laravel Sail 是否已安装。此命令还会显示有关包的一些附加信息:

composer show laravel/sail

如果 Laravel Sail 未安装,请运行composer require laravel/sail --dev

一旦安装了 Sail,你必须创建一个docker-compose.yml文件。要创建docker-compose.yml文件,你可以使用sail命令,sail:install

php artisan sail:install

sail:install命令将为你创建 Docker 文件。sail:install过程将询问你想要启用哪个服务。为了开始,你可以选择默认项(mysql):

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_3.1_B17728.jpg

图 3.1 – Laravel Sail 服务

回答sail:install命令中的问题以确定要包含哪些服务。Sail 的 Docker 配置从一组现成的模板(占位符)开始,sail:install命令包含了必要的模板。如果你对包含哪些模板以及它们是如何实现的感兴趣,请查看这里:github.com/laravel/sail/tree/1.x/stubs

如果你查看这些模板,你会看到它们使用了环境变量,例如${APP_PORT:-80}。这意味着你可以通过环境变量来控制配置,这些变量可以通过.env文件进行配置。.env文件由 Laravel 的安装自动生成。如果由于某种原因.env文件不存在,你可以从.env.example文件(例如,当你克隆一个使用 Laravel Octane 的现有仓库时,可能.env文件包含在.gitignore文件中)复制.env文件。在示例中,如果你想自定义 Web 服务器接收请求的端口(APP_PORT),只需将参数添加到.env文件中:

APP_PORT=81

在这种情况下,将使用端口81来服务你的 Laravel 应用,默认端口为80,如${APP_PORT:-80}所示。

注意

使用 Swoole 功能部分的示例中,将使用APP_PORT设置为81的 Laravel Sail,因此所有示例都将引用主机http://127.0.0.1:81

如果你已经修改了.env文件,你现在可以从你的 Laravel 项目目录中启动命令:

./vendor/bin/sail up

这将启动 Docker 容器。第一次执行可能需要一些时间(几分钟),因为将下载预配置的包含 nginx、PHP 和 MySQL 的镜像。

一旦命令执行完成,你可以访问http://127.0.0.1:81页面并看到你的 Laravel 欢迎页面:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_3.2_B17728.jpg

图 3.2 – Laravel 欢迎页面

与此容器方法相比,主要区别在于用于渲染页面的工具(nginx、PHP)包含在 Docker 图像中,而不是在您的主操作系统上。使用此方法,您甚至可能没有在主操作系统上安装 PHP 和 nginx 引擎。Laravel Sail 配置指示 Docker 使用容器来运行 PHP 和所有需要的工具,并将本地文件系统中的本地源代码(您的 Laravel 项目的根目录)指向本地文件系统。

现在我们已经将 Laravel Sail 与您的 Laravel 应用程序一起安装好了,我们必须添加 Laravel Octane 的相关内容,以便使用 Sail 提供的 Laravel 图像中已经包含的 Swoole 包。

让我们从安装 Laravel Octane 开始。

安装 Laravel Octane

我们将通过 Laravel Sail 提供的容器来安装 Laravel Octane。

因此,当 sail up 仍在运行(作为服务器运行)时,使用 composer require 命令启动 Octane 包。使用 sail 启动命令将在您的容器内执行您的命令:

./vendor/bin/sail composer require laravel/octane

设置 Laravel Octane 和 Swoole

为了调整启动服务器的命令,您必须使用以下命令发布 Laravel Sail 文件:

./vendor/bin/sail artisan sail:publish

此命令将从根项目目录中的 docker 目录中的包中复制 Docker 配置文件。

它创建了一个 docker 目录。在 docker 目录内,创建了多个目录,每个目录对应一个 PHP 版本:7.48.08.1

docker/8.1 目录中,您有以下内容:

  • 一个 Dockerfile。

  • php.ini 文件。

  • 用于启动容器的 start-container 脚本。此脚本引用了带有引导配置的 supervisor.conf 文件。

  • 包含监督脚本配置的 supervisor.conf 文件。

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_3.3_B17728.jpg

图 3.3 – Docker 配置文件(sail:publish 执行后)

supervisord.conf 文件很重要,因为它包含了容器中 web 服务器的引导命令。默认情况下,supervisord.conf 文件包含命令指令:

command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80

现在,我们有了 Laravel Octane,所以不再使用经典的 artisan serve,而要改为使用 artisan octane:start;因此,在 docker/supervisor.conf 文件中,您必须调整命令行:

command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=swoole --host=0.0.0.0 --port=80

如果您看的话,您会看到,使用 artisan octane:start 时,带有 --server=swoole 参数的 Swoole 服务器也被定义了。

注意

如果您对端口 8081 感到困惑,只是为了澄清:容器中的应用服务器将监听内部端口 80。通过 APP_PORT=81,我们指示 Docker 将来自端口 81 的外部连接映射到内部端口 80

当你更改一些 Docker 配置文件时,你必须重建镜像以便容器使用更改的文件。要重建镜像,请使用 build 选项:

./vendor/bin/sail build --no-cache

这个命令需要一段时间才能完成,但一旦完成,你就可以执行 sail up 命令:

./vendor/bin/sail up

当 Octane 准备就绪时,你会看到 INFO Server running… 消息:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_3.4_B17728.jpg

图 3.4 – Octane 服务器正在运行

如果你打开你的浏览器,你可以访问 http://localhost:81。你的控制台中的消息表明服务器正在监听端口 80,但如我们之前提到的,监听端口 80 的进程是 Docker 容器内的内部进程。Docker 容器外部的进程(你的浏览器)必须引用暴露的端口(根据 APP_PORT 配置,为 81)。

在你的浏览器中,你会看到默认的 Laravel 欢迎页面。你可以从响应的 HTTP 服务器头中看出这个页面是由 Octane 和 Swoole 服务的。查看这个的一个方法是使用带有 -I 选项的 curl 命令来显示头信息,并使用 grep 命令过滤所需的头信息:

curl -I 127.0.0.1:81 -s | grep ^Server

输出将如下所示:

Server: swoole-http-server

这意味着 Laravel 应用程序是由 Octane 和 Swoole 应用服务器服务的。

因此,我们可以开始使用一些 Swoole 功能 – 但在那之前,让我们先安装 Open Swoole。

安装 Open Swoole

Laravel Sail 默认使用包含 Swoole 模块的 PHP 镜像。Swoole 以 PECL 模块的形式分发,你可以在这里找到它:pecl.php.net/package/swoole。源代码在这里:github.com/swoole/swoole-src

一些开发者从 Swoole 的源代码中分叉,创建了 Open Swoole 项目来解决安全问题。

分叉的原因在此处报告:news-web.php.net/php.pecl.dev/17446

因此,如果你想将 Swoole 作为 Laravel Octane 的引擎,你可以决定使用 Open Swoole 实现。如果你想使用 Open Swoole,安装和配置与 Swoole 相同;Open Swoole 也以 PECL 模块的形式分发。

Laravel Octane 支持两者。

为了演示目的,我将在操作系统(无 Docker)中直接为新的 Laravel 项目安装 Open Swoole。

# installing new Laravel application
laravel new octane-ch03-openswoole
# entering into the new directory
cd octane-ch03-openswoole
# installing Pecl module
pecl install openswoole
# installing Octane package
composer require laravel/octane
# installing Laravel Octane files
php artisan octane:install
# launching the OpenSwoole server
php artisan octane:start

要检查 HTTP 响应是 Open Swoole 服务器创建的,在另一个终端会话中,启动以下 curl 命令:

curl -I 127.0.0.1:8000 -s | grep ^Server

这是输出结果:

Server: OpenSwoole 4.11.1

因此,Open Swoole 是 Swoole 项目的分支。我们将称之为 Swoole;你可以决定安装哪一个。在 探索 Swoole 功能 部分讨论的功能都由 Swoole 和 Open Swoole 支持。

在探索 Swoole 功能之前,我们应该安装一个额外的包来提高开发者体验。

在编辑代码之前

我们将使用 Swoole 功能,实现一些示例代码。当你更改(或编辑)你的代码,并且 Laravel Octane 已经加载了工作进程时,你必须重新加载工作进程。手动来说,你可以使用 Octane 命令。如果你使用 Laravel Sail(因此是 Docker),你必须在该容器中运行命令。命令如下:

php artisan octane:reload --server=swoole

如果你在一个容器中运行,你必须使用sail命令:

vendor/bin/sail php artisan octane:reload --server=swoole

如果你想要避免每次编辑或更改代码时手动重新加载工作进程,并且你希望 Octane 自动监视文件更改,你必须做以下操作:

  • 安装watch模式

  • 修改supervisord配置文件以使用--watch选项启动 Octane

  • 重建镜像以反映更改

  • 再次执行Sail

因此,首先,让我们安装chokidar

npm install --save-dev chokidar

docker/8.1/supervisord.conf中,将--watch选项添加到octane:start命令指令中:

command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=swoole --host=0.0.0.0 --port=80 --watch

然后,重建 Docker 镜像以确保配置更新生效,然后启动 Laravel Sail 服务:

vendor/bin/sail build
vendor/bin/sail up

现在,当你编辑代码(在你的 Laravel 应用程序的 PHP 文件中)时,工作进程将自动重新加载。在 Octane 的输出消息中,你会看到以下内容:

INFO  Application change detected. Restarting workers…

现在我们有了自动重新加载功能,我们可以探索 Swoole 的功能。

探索 Swoole 功能

Swoole 拥有许多我们可以在 Laravel Octane 应用程序中使用的功能,以提高我们应用程序的性能和速度。在本章中,我们将探讨这些功能,然后在随后的章节中,我们将使用这些功能。我们将探讨的 Swoole 功能如下:

  • 并发任务

  • 间隔命令执行

  • 缓存

  • 表格

  • 指标

并发任务

使用 Swoole,可以并行执行多个任务。为了演示这一点,我们将实现两个执行需要一些时间的函数。

为了模拟这两个函数是耗时的,我们将使用sleep()函数,该函数会暂停执行一定数量的秒数。

这两个函数返回字符串:第一个返回“Hello”,第二个返回“World”。

我们将通过sleep()函数将执行时间设置为 2 秒。

在两个函数的经典顺序执行场景中,总耗时将是 4 秒加上由于开销而产生的毫秒数。

我们将使用hrtime()函数跟踪执行时间。

注意

当你需要跟踪一系列指令的执行时间时,建议使用hrtime()函数,因为它是一个返回单调时间戳的函数。单调时间戳是基于一个参考点(因此是相对的)计算的时间,并且不受系统日期更改(如自动时钟调整[NTP 或夏令时更新])的影响。

我们还将使用两个匿名函数,因为在第二个示例(并发执行示例)中这将非常有用,以便能够更容易地进行比较。

在我们查看代码之前,我们将为了简单和专注,直接在routes/web.php文件中实现示例。您可以使用这段代码,特别是Octane::concurrently(),在您的控制器或其他 Laravel 应用程序的部分。

注意

为了访问这些示例,我们将使用 Laravel Sail 和 Swoole 的配置,将APP_PORT设置为81。如果您打算使用您本地的 Open Swoole 安装,请使用127.0.0.1:8000而不是127.0.0.1:81

示例显示了顺序执行:

Route::get('/serial-task', function () {
    $start = hrtime(true);
    [$fn1, $fn2] = [
        function () {
            sleep(2);
            return 'Hello';
        },
        function () {
            sleep(2);
            return 'World';
        },
    ];
    $result1 = $fn1();
    $result2 = $fn2();
    $end = hrtime(true);
    return "{$result1} {$result2} in ".($end - $start) /
      1000000000 .' seconds';
});

如果您使用浏览器访问http://127.0.0.1:81/serial-task页面,您应该在您的页面上看到以下输出:

Hello World in 4.001601125 seconds

这两个函数是顺序执行的,这意味着执行时间是第一个函数的执行时间加上第二个函数的执行时间。

如果您使用Octane::concurrently()方法(将您的函数作为Closure数组传递)调用这两个函数,您可以在并行中执行这些函数:

use Laravel\Octane\Facades\Octane;
Route::get('/concurrent-task', function () {
    $start = hrtime(true);
    [$result1, $result2] = Octane::concurrently([
        function () {
            sleep(2);
            return 'Hello';
        },
        function () {
            sleep(2);
            return 'World';
        },
    ]);
    $end = hrtime(true);
    return "{$result1} {$result2} in ".($end - $start) /
      1000000000 .' seconds';
});

如果您打开浏览器到http://127.0.0.1:81/concurrent-task,您将看到以下信息:

Hello World in 2.035140709 seconds

另一点需要注意的是,对于并发函数,执行时间取决于函数内部发生的情况。例如,如果您想并行执行两个或更多函数,这些函数的执行时间不可预测,因为它们依赖于第三方因素,如网络服务的响应时间或数据库的工作负载,或者当正在解析大文件时,您可能无法对执行顺序或持续时间做出任何假设。

在下一个示例中,我们有两个简单的函数:第一个函数执行时间更长,因此尽管它是第一个函数,但它是在第二个函数之后完成的。这对于习惯于处理并行任务的人来说是显而易见的,但对于习惯于使用强同步语言(如没有 Swoole 或其他添加异步功能的工具的 PHP)的人来说可能不太明显:

use Laravel\Octane\Facades\Octane;
Route::get('/who-is-the-first', function () {
    $start = hrtime(true);
    [$result1, $result2] = Octane::concurrently([
        function () {
            sleep(2);
            Log::info('Concurrent function: First');
            return 'Hello';
        },
        function () {
            sleep(1);
            Log::info('Concurrent function: Second');
            return 'World';
        },
    ]);
    $end = hrtime(true);
    return "{$result1} {$result2} in ".($end - $start) /
      1000000000 .' seconds';
});

结果是在日志文件中先打印第二条语句,然后再打印第一条。如果您查看storage/logs/laravel.log文件,您将看到以下内容:

local.INFO: Concurrent function: Second
local.INFO: Concurrent function: First

这意味着由Octane::concurrently调用的函数的执行将在大致相同的时间开始,但它们完成的精确时刻取决于函数执行所需的时间。

为什么这一点需要记住?

因为如果两个函数完全独立于彼此,那么一切可能都会顺利。另一方面,如果函数使用相同的资源(例如,读写同一个数据库表),我们需要考虑两个操作之间的依赖关系。例如,一个函数可能会更改表中的数据,而另一个函数可能会读取它。读取数据的时间是相关的:考虑数据是在写入之前读取还是写入之后读取。在这种情况下,我们可能会有两种完全不同的行为。

不论如何,我们将在下一章更深入地探讨concurrently方法,在那里我们将使用 Octane 在更真实的场景中——例如,使用concurrently从多个数据库查询和多个 API 调用中检索数据。

间隔命令执行

有时候,你必须每 X 秒执行一次函数。例如,你可能想每 10 秒执行一次函数。使用 Swoole,你可以使用Octane::tick()函数,其中你可以提供一个名称(作为第一个参数)和定义一个Closure的函数作为第二个参数。

调用tick()函数的最佳位置是在你的服务提供者之一的boot()方法中。通常,我使用在app/Providers目录中默认创建的AppServiceProvider,这是当你使用laravel new命令设置新的 Laravel 应用程序时为你创建的。

app/Providers/AppServiceProvider.php中的boot()方法中,使用一个非常简单的功能调用Octane::tick()函数,该功能使用时间戳记录一条消息。除非你在.env文件中有特殊配置,否则日志消息将被跟踪在storage/logs/laravel.log文件中:

    public function boot()
    {
        Octane::tick('simple-ticker', fn () =>
        Log::info('OCTANE TICK.', ['timestamp' => now()]))
        ->seconds(10)
        ->immediate();
    }

在前面的代码片段中,我们使用了OctaneLog类,所以请记住在AppServiceProvider.php文件的顶部包含它们:

use Laravel\Octane\Facades\Octane;
use Illuminate\Support\Facades\Log;

Octane::tick()方法返回一个实现了几个方法的InvokeTickCallable对象:

  • __invoke(): 一个特殊的方法,用于调用tick()监听器;它是负责执行传递给tick()方法的第二个参数中函数的方法。

  • seconds(): 一个方法,用于指示监听器应该自动调用(通过__invoke()方法)。它接受一个以秒为单位的整数参数。

  • immediate(): 一个方法,表示监听器应该在第一次 tick 时被调用(因此,尽可能快地)。

注意

当你启动octane:start命令时,会调用应用程序服务中的tick()方法。如果你使用 Laravel Sail,当运行sail up时,应用程序服务会被加载和启动,仅仅因为最终 Sail 启动了supervisord,而supervisord的配置中包含了octane:start命令。

一旦在storage/logs/laravel.log中启动了 Octane,你就可以看到tick()函数记录的消息:

[2022-07-29 08:23:11] local.INFO: OCTANE TICK. {"timestamp":"2022-07-29 08:23:11"}
[2022-07-29 08:23:22] local.INFO: OCTANE TICK. {"timestamp":"2022-07-29 08:23:21"}
[2022-07-29 08:23:31] local.INFO: OCTANE TICK. {"timestamp":"2022-07-29 08:23:31"} [2022-07-26 20:45:19] local.INFO: OCTANE TICK. {"timestamp":"2022-07-26 20:45:19"}

在此代码片段中,Laravel 日志显示tick()方法的执行。

注意

对于实时显示日志,你可以使用tail -f命令——例如,tail -f storage/logs/laravel.log

缓存

在基于工作者的系统中管理缓存机制,暂时保存一些数据,其中每个工作者都有自己的内存空间,可能并不那么简单。

Laravel Octane 提供了一个机制来非永久地保存不同工作者之间共享的数据。这意味着如果一个工作者需要存储一个值并使其对其他工作者的后续执行可用,这是可能的,可以通过缓存机制来实现。Laravel Octane 中的缓存机制是通过 Swoole Table 实现的,我们将在稍后详细了解。

在这个特定的情况下,我们将使用 Laravel 直接暴露的Cache类。Laravel 的缓存机制允许我们使用不同的驱动程序,例如,例如,数据库(MySQL、Postgresql 或 SQLite)、Memcache、Redis 或其他驱动程序。因为我们安装了 Laravel Octane 并使用了 Swoole,我们可以使用一个新的 Octane 特定驱动程序。如果我们想使用 Laravel 的缓存机制,我们将使用带有经典store方法的Cache类来存储值。在示例中,我们将在 Octane 驱动程序中存储一个名为last-random-number的键,并将其与一个随机数关联。在示例中,我们将通过之前看到的 Octane tick 函数调用负责在缓存中存储值的函数,并将间隔设置为 10 秒。我们将看到每 10 秒生成一个新的带有随机数的缓存值。我们还将实现一个新的路由,/get-number,我们将读取此值并在网页上显示它。

要使用 Octane 提供程序获取缓存实例,你可以使用Cache::store('octane')。一旦你有了实例,你可以使用put()方法来存储新值。

app/Providers/AppServiceProvider.php文件中,在boot()方法中,添加以下内容:

        Octane::tick('cache-last-random-number',
            function () {
                $number = rand(1, 1000);
                Cache::store('octane')->put(
                       'last-random-number', $number);
                Log::info("New number in cache: ${number}",
                         ['timestamp' => now()]);
                return;
            }
        )
        ->seconds(10)
        ->immediate();

确保你在文件开头包含正确的类。此代码片段需要的类如下:

use Illuminate\Support\Facades\Cache;
use Laravel\Octane\Facades\Octane;
use Illuminate\Support\Facades\Log;

如果你想检查,你可以在storage/logs/laravel.log文件中查看tick()函数打印的日志消息。

现在,我们可以在routes/web.php文件中创建一个新的路由,从缓存中获取值(last-random-number):

use Illuminate\Support\Facades\Cache;
Route::get('/get-random-number', function () {
    $number = Cache::store('octane')->get(
      'last-random-number', 0);
    return $number;
});

如果你打开浏览器并访问你的/get-random-number路径(在我的情况下,http://127.0.0.1:81/get-random-number因为我使用 Laravel Sail 并且我在.env文件中设置了APP_PORT=81),你可以看到随机数。如果你刷新页面,你将在 10 秒内看到相同的数字,或者更一般地说,在tick()函数(在服务提供者文件中)设置的间隔时间内。

注意

Octane 缓存中的存储不是永久的;它是易变的。这意味着每次服务器重启时,你可能会丢失这些值。

使用 Octane 缓存,我们还有一些其他不错的方法,例如 incrementdecrement,例如:

Route::get('/increment-number', function () {
    $number =
      Cache::store('octane')->increment('my-number');
    return $number;
});
Route::get('/decrement-number', function () {
    $number =
      Cache::store('octane')->decrement('my-number');
    return $number;
});
Route::get('/get-number', function () {
    $number = Cache::store('octane')->get('my-number', 0);
    return $number;
});

现在,打开你的浏览器,多次加载 /increment-number 路径,然后加载 /get-number;你将看到增加的值。

其他用于管理多个值的实用函数包括 putMany(),它用于保存一个项目数组,以及 many(),它一次检索多个项目:

Route::get('/save-many', function () {
    Cache::store('octane')->putMany([
        'my-number' => 42,
        'my-string' => 'Hello World!',
        'my-array' => ['Kiwi', 'Strawberry', 'Lemon'],
    ]);
    return "Items saved!";
});
Route::get('/get-many', function () {
    $array = Cache::store('octane')->many([
        'my-number',
        'my-string',
        'my-array',
    ]);
    return $array;
});

如果你首先在 /save-many 路径上打开浏览器,然后打开 /get-many,你将在页面上看到以下内容:

{"my-number":42,"my-string":"Hello World!","my-array":["Kiwi","Strawberry","Lemon"]}

如果你使用 putMany() 方法将一个值作为数组的项目保存,你可以使用数组键作为缓存键来检索它:

Route::get('/get-one-from-many/{key?}', function ($key = "my-number") {
    return Cache::store('octane')->get($key);
});

如果你打开 /get-one-from-many/my-string 页面,你将看到以下内容:

Hello World!

这意味着从缓存中检索了具有 "my-string" 键的值。

最后,我们使用 Laravel 缓存机制,以 Swoole 作为后端提供者。

为什么我们应该使用 Swoole 作为后端缓存提供者?它相对于数据库等其他提供者有什么优势?我们已经看到,性能肯定是它的主要价值之一。使用这种类型的缓存允许我们以非常高效的方式在工作进程之间共享信息。Swoole 的官方文档报告,它支持每秒 2 百万的读写速率。

Laravel Octane 缓存是通过 Swoole 表实现的。在下一节中,我们将探讨 Swoole 表。

Swoole 表

如果你想以比缓存更结构化的方式在多个工作进程之间共享数据,你可以使用 Swoole 表。如前所述,Octane 缓存使用 Swoole 表,所以让我更详细地解释一下 Swoole 表。

首先,如果你想使用 Swoole 表,你必须定义表的架构。架构在 config/octane.php 文件的 tables 部分中定义。

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_3.5_B17728.jpg

图 3.5 – config/octane.php 文件中表的配置

默认情况下,当你执行 octane:install 命令时,会为你创建一个 config/octane.php 文件,并且已经定义了一个表:一个名为 example 的表,最多有 1,000 行 (example:1000),包含 2 个字段。第一个字段名为 name,是一个最大长度为 500 个字符的字符串,第二个字段名为 votes,类型为整数 (int)。

以同样的方式,你可以配置你的表。字段类型如下:

  • int: 用于整数

  • float: 用于浮点数

  • string: 用于字符串;你可以定义最大字符数

一旦你在配置文件中定义了表,你就可以设置表的值。

例如,我将创建一个最大 100 行的 my-table 表,并包含一些字段。在 config/octane.php 文件的 tables 部分中,我们将创建以下内容:

  • 一个名为 my-table 的表,最多有 100 行

  • 一个 string 类型的 UUID 字段(最大长度为 36 个字符)

  • 一个 string 类型的名称字段(最大长度为 1000 个字符)

  • 一个 int 类型的年龄字段

  • 一个 float 类型的值字段

因此,配置如下:

        'my-table:100' => [
            'uuid' => 'string:36',
            'name' => 'string:1000',
            'age' => 'int',
            'value' => 'float',
        ],

当服务器启动时,Octane 将为我们创建内存中的表。

现在,我们将创建两个路由:第一个路由用于向表中填充一些数据,第二个路由用于从表中检索并显示信息。

第一个路由 /table-create 将执行以下操作:

  • 获取 my-table 表的实例。my-table 是在 config/octane.php 中配置的表。

  • 创建 90 行,为 uuidnameagevalue 字段填充数据。为了填充值,我们将使用 fake() 辅助函数。这个辅助函数允许你为 UUID、名称、整数数字、小数数字等生成随机值。

routes/web.php 文件中的代码如下:

Route::get('/table-create', function () {
    // Getting the table instance
    $table = Octane::table('my-table');
    // looping 1..90 creating rows with fake() helper
    for ($i=1; $i <= 90; $i++) {
        $table->set($i,
        [
            'uuid' => fake()->uuid(),
            'name' => fake()->name(),
            'age' => fake()->numberBetween(18, 99),
            'value' => fake()->randomFloat(2, 0, 1000)
        ]);
    }
    return "Table created!";
});

该片段在调用 /table-create URL 时创建表。

如果你打开 http://127.0.0.1:81/table-create,你会看到一个带有一些行的表被创建。

在你的页面上,你可能会有这样的错误:

Swoole\Table::set(): failed to set('91'), unable to allocate memory

确保配置文件中表的尺寸大于你创建的行数。当我们与数据库表一起工作时,我们不需要设置最大行数;在这种情况下,对于这种类型的表(在跨工作进程共享的内存表中),我们必须注意行数。

一切正常后,你将在网页上看到表已创建!这意味着行已正确创建。

为了验证这一点,我们将创建一个新的路由,/table-get,我们执行以下操作:

  • 获取 my-table 表的实例(与我们在 /table-create 中使用的相同表)

  • 获取索引为 1 的行

  • 返回行(关联数组,其中项是具有 uuidnameagevalue 字段的行字段)

routes/web.php 文件中,定义新的路由:

Route::get('/table-get', function () {
    $table = Octane::table('my-table');
    $row = $table->get(1);
    return $row;
});

打开 https://127.0.0.1:81/table-get(在打开 /table-create 之后,因为你必须先创建行才能访问它们),你应该看到如下内容:

{"uuid":"6e7c7eb6-9ecf-3cf8-8de9-5034f4c44ab5","name":"Hugh Larson IV","age":81,"value":945.67}

你将看到内容方面有所不同,因为行是使用 fake() 辅助函数生成的,但在结构方面你会看到类似之处。

你也可以使用 foreach() 遍历 Swoole Table,你还可以在 Table 对象上使用 count() 函数(用于计数表中的元素):

Route::get('/table-get-all', function () {
    $table = Octane::table('my-table');
    $rows=[];
    foreach ($table as $key => $value) {
        $rows[$key] = $table->get($key);
    }
    // adding as first row the table rows count
    $rows[0] = count($table);
    return $rows;
});

在前面的示例中,对于行计数,你可以看到最后,我们可以访问 Swoole 对象并使用它实现的函数和方法。使用 Swoole 对象的另一个例子是访问服务器对象,如下一节中检索指标时所示。

指标

Swoole 提供了一种方法来检索关于应用服务器和工作进程资源使用情况的某些指标。如果你想要跟踪某些方面的使用情况——例如,时间、处理请求数量、活跃连接数、内存使用量、任务数量等,这非常有用。要检索指标,首先,你必须访问 Swoole 类实例。感谢 Laravel 提供的服务容器,你可以从容器中解析和访问对象。Swoole 服务器对象由 Octane 存储在容器中,因此,通过App::make,你可以访问 Swoole 服务器类实例。在routes/web.php文件中,你可以创建一个新的路由,在那里你可以做以下操作:

  • 你可以通过App::make检索 Swoole 服务器。

  • 一旦你可以访问该对象,你就可以使用它们的方法,例如,比如stats

  • Swoole 服务器Stats对象作为响应返回:

use Illuminate\Support\Facades\App;
Route::get('/metrics', function () {
    $server = App::make(Swoole\Http\Server::class);
    return $server->stats();
});

在新的/metrics页面上,你可以看到 Swoole 服务器提供的所有指标。

对于所有方法,你可以直接看到 Swoole 提供的服务器文档:openswoole.com/docs/modules/swoole-server-stats

摘要

在本章中,我们探讨了如何安装和配置 Swoole 环境。然后,我们还分析了该应用服务器暴露的各种功能。

我们通过简单的示例了解了这些功能,以便我们可以专注于单个特性。在下一章中,我们将探讨在更真实的应用服务器环境中使用 Laravel Octane。

第三部分:Laravel Octane——全面游览

这一部分的目标是展示如何最好地使用 Laravel Octane 和 Swoole 应用服务器提供的功能,特别是针对优化数据访问。本部分展示了具有缓存和并行查询的数据查询管理示例。

本部分包括以下章节:

  • 第四章构建 Laravel Octane 应用

  • 第五章使用异步方法降低延迟和管理数据

第四章:构建 Laravel Octane 应用程序

在前几章中,我们专注于安装、配置和使用 Laravel Octane 提供的一些功能。我们探讨了 Swoole 和 RoadRunner 这两种由 Laravel Octane 支持的应用程序服务器之间的区别。

在本章中,我们将重点关注 Laravel Octane 的每个功能,以发现其潜力并了解如何单独使用它。

本章的目标是在现实环境中分析 Laravel Octane 的功能。

为了做到这一点,我们将构建一个示例仪表板应用程序,涵盖多个方面,例如配置应用程序、创建数据库表架构以及生成初始数据。

然后,我们将继续实现仪表板交付的特定路由,并在控制器中实现数据检索逻辑以及在模型中的查询。

然后,我们将在示例仪表板应用程序中创建一个页面,我们将从多个查询中收集信息。当我们必须实现用于检索数据的查询时,通常我们关注逻辑和过滤、排序和选择数据的方法。然而,在本章中,我们将保持逻辑尽可能简单,以便您能够关注其他方面,例如通过执行并行任务来高效地加载数据,并且我们将应用一些策略以尽可能减少响应时间(并行运行任务减少了整体响应执行时间)。

在设计应用程序架构时,我们还需要考虑可能出错的事情。

在前一章的示例中,我们通过考虑所谓的快乐路径来分析每个功能。快乐路径是用户为了实现预期结果而采取的默认场景,不会遇到任何错误。

在设计真实应用程序时,我们还必须考虑那些不包括在快乐路径中的所有情况。例如,在并发执行重查询的情况下,我们需要考虑执行可能返回意外结果的情况,例如空结果集,或者当查询执行引发异常时。我们需要考虑这个单个异常也可能对其他并发执行产生影响。这看起来更像是一个更真实的生活场景(由于某些异常而可能出错),在本章中,我们还将尝试管理错误。

因此,我们将尝试模拟一个典型的数据消耗型应用程序,其中用户的请求响应控制器必须尽可能快地执行操作,即使在面对高请求负载的情况下。

本章的主要目标是指导您通过使用多个查询、在渲染仪表板页面时的并发执行以及尝试在应用程序中应用 Octane 功能来大幅减少应用程序的响应时间。我们将逐步介绍路由、控制器、模型、查询、迁移、填充和视图模板。我们将涉及 Octane 提供的一些机制,例如 Octane 路由、分块数据加载、并行任务(用于查询和 HTTP 请求)、错误和异常管理以及 Octane 缓存。

在本章中,我们将涵盖以下内容:

  • 安装和设置应用程序

  • 导入初始数据(以及如何高效地完成它的建议)

  • 并行从数据库查询多份数据

  • 优化路由

  • 更多集成第三方 API 的示例

  • 使用 Octane Cache 提高速度

技术要求

我们将假设您拥有 PHP 8.0 或更高版本(8.1 或 8.2)以及 Composer 工具。如果您想使用 Laravel Sail (laravel.com/docs/9.x/sail),则需要 Docker Desktop 应用程序 (www.docker.com/products/docker-desktop)。

我们还将快速回顾 Octane 的设置,以便于我们的实际示例。因此,我们将安装所有需要的工具。

当前章节中描述的示例的源代码和配置文件在此处可用:github.com/PacktPublishing/High-Performance-with-Laravel-Octane/tree/main/octane-ch04

安装和设置仪表板应用程序

为了展示 Laravel Octane 的强大功能,我们将构建一个仪表板页面来以不同的方式显示过滤的事件数据。我们将尽可能保持简单,以避免关注业务功能,并且我们将继续关注如何在保持应用程序可靠和错误免费的同时应用提高性能的技术。

安装您的 Laravel 应用程序

第一章中所示,理解 Laravel Web 应用程序架构,您可以通过以下 Laravel 命令从头开始安装 Laravel 应用程序:

composer global requires laravel/installer

一旦安装了 Laravel 命令,您可以使用以下命令创建应用程序:

laravel new octane-ch04

laravel new命令创建包含您的应用程序的目录,因此下一步是进入新目录以开始自定义应用程序:

cd octane-ch04

添加数据库

现在我们已经创建了应用程序,我们必须安装和设置数据库,因为我们的示例应用程序需要数据库来存储和检索示例数据。因此,为了安装和设置数据库,我们将执行以下操作:

  1. 安装 MySQL 数据库服务器

  2. 在 Laravel 中执行迁移(将模式定义应用到数据库表中)

  3. 安装一个应用程序来管理和检查数据库的表和数据

安装数据库服务

安装数据库服务器有三种方式:通过官方安装程序、通过您的本地包管理器或通过 Docker/Laravel Sail。

第一种方法是使用 MySQL 提供的官方安装程序。您可以从官方网站下载并执行适用于您特定操作系统的安装程序:dev.mysql.com/downloads/installer/

下载安装程序后,您可以执行它。

另一种方法是使用您的系统包管理器。如果您有 macOS,我的建议是使用 Homebrew(见 第一章理解 Laravel 网络应用程序架构)并执行以下命令:

brew install mysql

如果您使用 GNU/Linux,您可以使用您的 GNU/Linux 发行版提供的包管理器。例如,对于 Ubuntu,您可以执行以下操作:

sudo apt install mysql-server

如果您不想在本地操作系统上安装或添加 MySQL 服务器,您可以使用在 Docker 容器中运行的 Docker 镜像。为此,我们可以使用 Laravel Sail 工具。如果您熟悉 Docker 镜像,使用 Docker 镜像可以简化第三方软件(如数据库)的安装。Laravel Sail 简化了管理 Docker 镜像的过程。

确保将 Laravel Sail 添加到您的应用程序中。在项目目录中,将 Laravel Sail 包添加到您的项目中:

composer require laravel/sail --dev

然后,执行 Laravel Sail 提供的新命令以添加 Docker 的 Sail 配置:

php artisan sail:install

执行前面的命令将需要您通过 Laravel Sail 选择要激活的服务。目前的目标是激活 MySQL 服务,因此选择第一个选项。选择 MySQL 服务后,MySQL Docker 镜像将自动下载:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_4.01_B17728.jpg

图 4.1:安装 Laravel Sail

安装 Laravel Sail 以及下载 MySQL Docker 镜像,将 docker-compose.yml 文件添加到您的项目目录中,并将 PHPUnit 配置更改为使用新的数据库实例。因此,安装 Laravel Sail 帮助您进行 Docker 配置(根据 sail:install 命令提出的问题的答案创建具有预设配置的 docker-compose.yml 文件),以及 PHPUnit 的配置(创建正确的 PHPUnit 配置以使用新的数据库实例)。

docker-compose.yml 文件将包含以下内容:

  • 为您的网络应用程序提供主要服务

  • 为 MySQL 服务器提供附加服务

  • 为服务提供正确的配置,以便使用 .env 文件中的相同环境变量

如果您已经在本地操作系统中运行了一些服务,并且想要避免一些冲突(使用相同端口的多个服务),您可以通过docker-compose.yml控制 Docker 容器使用的某些参数,在.env文件中设置以下变量:

  • VITE_PORT:这是 Vite 用于服务前端部分(JavaScript 和 CSS)的端口。默认值为5173;如果您已经在本地运行了 Vite,则可以使用端口5174以避免冲突。

  • APP_PORT:这是 web 服务器使用的端口。默认情况下,本地 web 服务器使用的端口是端口 80,但如果您已经有一个本地 web 服务器正在运行,您可以在.env文件中使用8080设置(APP_PORT=8080)。

  • FORWARD_DB_PORT:这是 Laravel Sail 用于公开 MySQL 服务的端口。默认情况下,MySQL 使用的端口是3306,但如果它已被占用,您可以通过FORWARD_DB_PORT=3307设置端口。

一旦.env配置对您来说良好,您就可以通过 Laravel Sail 启动 Docker 容器。

要启动 Laravel Sail 并启动 Docker 容器,请使用以下命令:

./vendor/bin/sail up -d

-d选项允许您在后台执行 Laravel Sail,这在您想要重复使用 shell 来启动其他命令时非常有用。

要检查您的数据库是否正常运行,您可以通过sail执行php artisan db:show命令:

./vendor/bin/sail php artisan db:show

第一次执行db:show命令时,会安装一个额外的包——Doctrine artisan命令。一旦您运行了db:show命令,您将看到以下内容:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_4.02_B17728.jpg

图 4.2:通过 Sail 执行 db:show 命令

现在,您的数据库正在运行,因此您可以创建表。我们将执行迁移以创建数据库表。数据库表将包含您的数据——例如,事件。

迁移文件是一个您可以定义数据库表结构的文件。在迁移文件中,您可以列出表的列并定义列的类型(字符串、整数、日期、时间等)。

执行迁移

Laravel 框架提供了针对标准功能(如用户和凭证管理)的内置迁移。这就是为什么在将框架安装到database/migrations目录后,您可以在框架中找到已提供的迁移文件:创建users表、password resets表、failed jobs表和personal access``tokens表的迁移。

迁移文件存储在database/migrations目录中。

要在 Docker 容器中执行迁移,您可以通过命令行执行migrate命令:

./vendor/bin/sail php artisan migrate

这就是您会看到的:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_4.03_B17728.jpg

图 4.3:执行迁移

如果你没有使用 Laravel Sail,并且你使用的是本地操作系统上安装的 MySQL 服务器(使用 Homebrew 或你的操作系统打包器或 MySQL 服务器官方安装程序),你可以使用 php artisan migrate 命令而不需要 sail 命令:

php artisan migrate

数据库模式和表是通过迁移创建的。现在我们可以安装 MySQL 客户端来访问数据库。

安装 MySQL 客户端

要访问数据库的结构和数据,建议安装 MySQL 客户端。MySQL 客户端允许你访问结构、模式和数据,并允许你执行 SQL 查询以提取数据。

你可以选择可用的工具之一;有些是开源工具,有些是付费工具。以下是一些用于管理 MySQL 结构和数据的工具:

如果你选择 Sequel Ace 或其他工具,你必须根据 .env 文件设置正确的参数进行初始连接。

例如,Sequel Ace 的初始屏幕会要求你提供主机名、凭证、数据库名和端口号:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_4.04_B17728.jpg

图 4.4:Sequel Ace 登录界面

图 4*.4* 所示,以下是这些值:

  • 127.0.0.1

  • .env 文件中的 DB_USERNAME 参数

  • .env 文件中的 DB_PASSWORD 参数

  • .env 文件中的 DB_DATABASE 参数

  • 如果你使用 Laravel Sail,则使用 FORWARD_DB_PORT 参数,如果不使用本地 Docker 容器,则使用 DB_PORT 参数

安装 MySQL 客户端后,我们将继续讨论 Sail 与本地工具的比较。

Sail 与本地工具的比较

我们探讨了两种使用 PHP、服务和工具的方法:使用 Docker 容器(Laravel Sail)和使用本地安装。

一旦设置好 Sail,如果你想通过 Sail 启动命令,你必须将你的命令前缀为 ./vendor/bin/sail。例如,如果你想列出已安装的 PHP 模块,以下命令将列出本地操作系统中安装的所有 PHP 模块:

php -m

如果你使用 php -m 命令与 sail 工具一起,如下所示,将显示 Docker 容器中安装的 PHP 模块:

./vendor/bin/sail php -m

Laravel Sail 镜像已经为你安装并配置了 Swoole 扩展,因此现在你可以将 Octane 添加到你的应用程序中。

将 Octane 添加到你的应用程序中

要将 Laravel Octane 添加到你的应用程序中,你必须执行以下操作:

  1. 添加 Octane 包

  2. 创建 Octane 配置文件

信息

我们已经在 第三章 中介绍了使用 Laravel Sail 和 Swoole 的 Octane 设置,即 使用 Swoole 应用服务器。现在让我们快速回顾一下当前章节提供的示例所需的 Octane 配置步骤。

因此,首先,在项目目录中,我们将使用 composer require 命令添加 Laravel Octane 包:

./vendor/bin/sail composer require laravel/octane

然后,我们将使用 octane:install 命令创建 Octane 配置文件:

./vendor/bin/sail php artisan octane:install

现在我们已经安装了 Laravel Octane,我们必须配置 Laravel 以启动 Swoole 应用服务器。

将 Swoole 作为应用服务器激活

如果你正在使用 Laravel Sail,你必须激活 Swoole 来运行你的 Laravel 应用程序。默认的 Laravel Sail 配置启动了经典的 php artisan serve 工具。因此,目标是编辑定义 artisan serve 命令的配置文件,并将其替换为 octane:start 命令。为此,你需要将配置文件从 vendor 目录复制到一个你可以编辑它的目录。Laravel Sail 提供了一个发布命令,通过 sail:publish 命令来复制并生成配置文件:

./vendor/bin/sail artisan sail:publish

publish 命令生成 Docker 目录和 supervisord.conf 文件。supervisord.conf 文件负责启动网络服务以接受 HTTP 请求并生成 HTTP 响应。在 Laravel Sail 中,运行网络服务的命令放置在 supervisord.conf 文件中。然后,在项目目录中的 docker/8.1/supervisord.conf 文件(放置在项目目录中),为了启动 Laravel Octane 而不是经典的网络服务器,将 artisan serve 命令替换为带有所有正确参数的 artisan octane:start

# command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80
command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan octane:start --server=swoole --host=0.0.0.0 --port=80

使用 Laravel Sail,当你更改任何 Docker 配置文件时,你必须重新构建镜像:

./vendor/bin/sail build --no-cache

然后,重新启动 Laravel Sail:

./vendor/bin/sail stop
./vendor/bin/sail up -d

如果你打开浏览器到 http://127.0.0.1:8080/,你会看到由 Swoole 运行的 Laravel 应用程序。

验证你的配置

一旦设置好工具和服务,我的建议是要注意工具使用的配置。使用 PHP 命令,你可以有一些选项来检查已安装的模块(例如,检查模块是否正确加载,例如检查 Swoole 模块是否加载),以及查看 PHP 当前配置的选项。

要检查模块是否已安装,你可以使用带有 -m 选项的 PHP 命令:

./vendor/bin/sail php -m

要检查 Swoole 是否正确加载,你可以过滤出以 Swoole 为名称的行(不区分大小写)。要过滤行,你可以使用 grep 命令。grep 命令只显示符合特定标准的行:

./vendor/bin/sail php -m | grep -i swoole

如果你想要列出所有 PHP 配置,你可以使用带有 -i 选项的 PHP 命令:

./vendor/bin/sail php -i

如果你想要更改配置中的某些内容,你可能想查看配置(.ini)文件的位置。要查看 .ini 文件的位置,只需过滤 ini 字符串:

./vendor/bin/sail php -i | grep ini

你将看到类似以下内容:

Configuration File (php.ini) Path => /etc/php/8.1/cli
Loaded Configuration File => /etc/php/8.1/cli/php.ini
Scan this dir for additional .ini files => /etc/php/8.1/cli/conf.d
Additional .ini files parsed => /etc/php/8.1/cli/conf.d/10-mysqlnd.ini,

使用 php -i 命令,你可以获取有关 php.ini 文件位置的详细信息。如果你使用 Laravel Sail,你可以执行以下命令:

./vendor/bin/sail php -i | grep ini

你将看到有一个特定的 .ini 文件用于 Swoole:

/etc/php/8.1/cli/conf.d/25-swoole.ini

如果你想要访问该文件以检查或编辑它,你可以通过 shell 命令跳转到运行中的容器:

./vendor/bin/sail shell

使用此命令,它将显示运行容器的 shell 提示符,你可以在那里查看文件内容:

less /etc/php/8.1/cli/conf.d/25-swoole.ini

命令将显示 25-swoole.ini 配置文件的内容。文件内容如下:

extension=swoole.so

如果你想要禁用 Swoole,你可以在 extension 指令的开头添加 ; 字符,如下所示:

; extension=swoole.so

在开头使用 ; 字符,则扩展不会加载。

总结安装和设置

在继续实施之前,让我总结一下之前的步骤:

  1. 我们安装了我们的 Laravel 应用程序。

  2. 我们添加了一个数据库服务。

  3. 我们配置了一个 MySQL 客户端以访问 MySQL 服务器。

  4. 我们添加了 Octane 包和配置。

  5. 我们添加了 Swoole 作为应用程序服务器。

  6. 我们检查了配置。

因此,现在我们可以开始使用一些 Octane 功能,例如以并行和异步的方式执行重任务。

创建仪表板应用程序

在一个应用程序中,你可以在多个表中存储多种类型的数据。

通常,在产品列表页面上,你必须执行一个查询来检索产品列表。

或者,在一个仪表板中,也许你可以显示多个图表或表格来显示数据库中的某些数据。如果你想在同一页面上显示更多图表,你必须对多个表执行多个查询。

你可能一次执行一个查询;这意味着检索用于组成仪表板的所有有用信息的总时间是所有涉及查询的执行时间的总和。

同时运行多个查询将减少检索所有信息的总时间。

为了演示这一点,我们将创建一个 events 表,我们将存储一些带有用户时间戳的事件。

创建一个事件表

当你在 Laravel 中创建一个表时,你必须使用迁移文件。迁移文件包含创建表和所有字段的逻辑。它包含定义你表结构的所有指令。为了管理使用存储在表中的数据的逻辑,你可能还需要其他一些东西,例如 modelseeder 类。

model 类允许开发者访问数据,并提供了一些保存、删除、加载和查询数据的方法。

seeder 类用于用初始值或示例值填充表。

要创建model类、seeder类和迁移文件,您可以使用带有m(创建迁移文件)和s(创建seeder类)参数的make:model命令:

php artisan make:model Event -ms

使用make:model命令和m以及s参数,将创建三个文件:

  • 迁移文件创建在database/migration/目录下,文件名以时间戳为前缀,以create_events_table为后缀,例如,2022_08_22_210043_create_events_table.php

  • app/Models/Event.php中的model

  • app/database/seeders/EventSeeder.php中的seeder类文件

自定义迁移文件

make:model命令创建了一个用于创建表的模板文件,其中包含基本的字段,如idtimestamps。开发人员必须添加特定于应用程序的字段。在仪表板应用程序中,我们将添加以下字段:

  • user_id: 对于与users表的外部引用,一个用户可以关联到多个事件

  • type: 事件类型可以是INFOWARNINGALERT

  • description: 事件描述文本

  • value: 从110的整数

  • date: 事件日期和时间

创建表的迁移文件示例如下:

<?php
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('events',
                       function (Blueprint $table) {
            $table->id();
            $table->foreignIdFor(User::class)->index();
            $table->string('type', 30);
            $table->string('description', 250);
            $table->integer('value');
            $table->dateTime('date');
            $table->timestamps();
        });
    }
    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('events');
    }
};

您可以在up()方法中列出您想要添加到表中的字段。在代码中,我们正在添加用户表的 foreign ID、类型、描述、值和日期。down()方法通常用于删除表。up()方法在开发人员想要执行迁移时调用,而down()方法在开发人员想要回滚迁移时调用。

播种数据

使用seeder文件,您可以创建初始数据以填充表。出于测试目的,您可以用假数据填充表。Laravel 为您提供了创建假数据的出色辅助工具,fake()

fake()辅助工具

对于生成假数据,fake()辅助工具使用Faker库。该库的首页在fakerphp.github.io/

现在,我们将为用户和事件创建假数据。

要创建假用户,您可以创建app/database/seeders/UserSeeder.php文件。

在示例中,我们将执行以下操作:

  • 通过fake()->firstName()生成随机姓名

  • 通过fake()->email()生成随机电子邮件

  • 使用Hash::make(fake()->password())生成随机散列密码

我们将生成 1,000 个用户,因此我们将使用for循环。

您必须在UserSeeder类的run()方法中生成数据并调用User::insert()来生成数据:

<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $data = [];
        $passwordEnc = Hash::make(fake()->password());
        for ($i = 0; $i < 1000; $i++) {
            $data[] =
            [
                'name' => fake()->firstName(),
                'email' => fake()->unique()->email(),
                'password' => $passwordEnc,
            ];
        }
        foreach (array_chunk($data, 100) as $chunk) {
            User::insert($chunk);
        }
    }
}

使用UserSeeder类,我们将创建 1,000 个用户。然后,一旦我们在user表中有了用户,我们将创建 100,000 个事件:

<?php
namespace Database\Seeders;
use App\Models\Event;
use Illuminate\Database\Seeder;
use Illuminate\Support\Arr;
class EventSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $data = [];
        for ($i = 0; $i < 100_000; $i++) {
            $data[] = [
                'user_id' => random_int(1, 1000),
                'type' => Arr::random(
                    [
                        'ALERT', 'WARNING', 'INFO',
                    ]
                ),
                'description' => fake()->realText(),
                'value' => random_int(1, 10),
                'date' => fake()->dateTimeThisYear(),
            ];
        }
        foreach (array_chunk($data, 100) as $chunk) {
            Event::insert($chunk);
        }
    }
}

要创建假事件,我们需要使用fake()辅助工具填写事件字段。为events表填写的字段如下:

  • user_id: 我们将从11000生成一个随机数

  • type:我们将使用 Laravel 的Arr:random()辅助函数从以下值中选择一个:'ALERT''WARNING''INFO'

  • description:来自fake()辅助函数的随机文本

  • value:从110的随机整数

  • date:由fake()辅助函数提供的日期函数,用于从当前年份生成一天,dateTimeThisYear()

就像我们对users表所做的那样,我们正在使用分块方法来尝试提高数据生成器的执行速度。对于大型数组,分块方法允许代码更高效,因为它涉及将数组分成块并处理这些块,而不是逐条记录。这减少了数据库中的插入次数。

提高种子操作的速度

生成大量数据需要考虑操作的成本,即花费的时间

在创建初始用户数据时,用于数据种子(通过UserSeeder类)的两个最昂贵的操作如下:

  • Hash::make()只需要很短的时间,因为它非常占用 CPU。如果你多次重复这个操作,最终它需要几秒钟才能执行。

  • array_chunk可以帮助你减少对insert()方法的调用次数。考虑一下,insert()方法可以接受一个项目数组(插入多行)。使用数组作为参数调用insert()比逐行调用insert()要快得多。在底层(数据库级别)的每次insert()执行都需要为插入准备事务操作,将行插入表中,调整表的所有索引和所有元数据,并关闭事务。换句话说,每次insert()操作都有一些开销时间,当你想要多次调用它时必须考虑。这意味着每次insert()操作都有额外的开销成本来确保操作的一致性。减少这种操作的次数可以减少额外操作的总时间。

因此,为了提高数据创建(种子)的性能,我们可以做一些假设并实现以下方法:

  • 要创建多个用户,所有用户使用相同的密码是可以接受的。我们不需要实现一个登录过程,只需要一个用户列表。

  • 我们可以创建一个用户数组,然后使用分块方法来插入数据块(对于 1,000 个用户,我们插入 10 个包含 100 个用户的块)。

因此,在创建用户的前一个代码片段中,我们使用了这两种优化方法:减少哈希调用次数和使用array_chunk

在某些场景中,你必须将大量数据插入和加载到数据库中。在这种情况下,我的建议是使用数据库提供的某些特定功能来加载数据,而不是尝试优化你的代码。

例如,如果你有大量数据要加载或从另一个数据库传输,在 MySQL 的情况下,有两种工具。

第一个选项是使用 INTO OUTFILE 选项:

select * from events INTO OUTFILE '/var/lib/mysql-files/export-events.txt';

在做之前,你必须确保 MySQL 允许你执行此操作。

因为我们将在一个目录中导出大量数据,我们必须在 MySQL 配置中将此目录列为允许的。

my.cnf 文件(MySQL 的配置文件)中,务必确保存在 secure-file-priv 指令。此指令的值将是一个你可以导出和导入文件的目录。

如果你使用 Laravel Sail,secure-file-priv 已经设置为一个目录:

secure-file-priv=/var/lib/mysql-files

在 Homebrew 的情况下,配置文件位于以下位置:/opt/homebrew/etc/my.cnf

例如,my.cnf 文件可能有以下结构:

[mysqld]
bind-address = 127.0.0.1
mysqlx-bind-address = 127.0.0.1
secure-file-priv = "/Users/roberto"

在这种情况下,导出数据和文件的目录是 "/Users/roberto"

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_4.05_B17728.jpg

图 4.5:MySQL 的 secure-file-priv 指令

这个指令存在是出于安全原因,所以在进行此编辑之前,请进行评估。在生产环境中,我禁用该指令(将其设置为空字符串)。在本地开发环境中,此配置可能是可接受的,或者至少只在需要时激活此选项。

在此配置更改后,你必须重新加载 MySQL 服务器。在 Homebrew 的情况下,使用以下命令:

brew services restart mysql

现在,你可以执行一个 artisan 命令(php artisan db)来访问数据库。你不需要指定数据库名称、用户名或密码,因为该命令使用 Laravel 配置(.env 中的 DB_ 参数):

php artisan db

在启动 artisan db 命令后显示的 MySQL 提示符中,你可以使用 SELECT 语法导出数据,例如:

select * from events INTO OUTFILE '/Users/roberto/export-events.txt';

你会看到导出成千上万条记录只需几毫秒。

如果你使用 Laravel Sail,像往常一样,你必须通过 sail 命令启动 php artisan

./vendor/bin/sail php artisan db

在 MySQL Docker 提示符中使用以下命令:

select * from events INTO OUTFILE '/var/lib/mysql-files/export-events.txt';

如果你想要加载之前导出的 SELECT 语句的文件,你可以使用 LOAD DATA

LOAD DATA INFILE '/Users/roberto/export-events.txt' INTO TABLE events;

再次,你会看到这个命令将花费几毫秒来导入成千上万条记录:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_4.06_B17728.jpg

图 4.6:使用 LOAD DATA 可以加速加载数据的过程

因此,最终你有多种方法可以提升加载数据的过程。我建议当你使用 MySQL 时使用 LOAD DATA,并且你可以通过 SELECT 获取导出的数据。另一种情况是,作为开发者,你从其他人那里收到一个巨大的数据文件,并且你可以同意文件格式。或者,如果你已经知道你将需要多次加载大量数据以进行测试,你可以评估一次性创建一个巨大的文件(例如,使用 fake() 辅助函数),然后每次你想对 MySQL 数据库进行初始化时都使用该文件。

执行迁移

现在,在实现检索数据的查询之前,我们必须运行迁移和种子。

因此,在前面的章节中,我们介绍了如何创建种子文件和迁移文件。

要控制哪些种子需要被加载和执行,你必须在 database/seeders/DatabaseSeeder.php 文件中的 run() 方法中列出种子。你必须这样列出种子:

        $this->call([
            UserSeeder::class,
            EventSeeder::class,
        ]);

要使用一条命令创建表和加载数据,请使用以下命令:

php artisan migrate --seed

如果你已经执行了迁移,并且想要从头开始重新创建它们,可以使用 migrate:refresh

php artisan migrate:refresh --seed

或者,你可以使用 migrate:fresh 命令,它删除表而不是执行回滚:

php artisan migrate:fresh --seed

注意

migrate:refresh 命令将执行你的迁移中的所有 down() 函数。通常,在 down() 方法中,会调用 dropIfExists() 方法(用于删除表),因此你的表将在从头开始创建之前被清理,你的数据也将丢失。

现在你已经创建了表和数据,我们将通过控制器中的查询来加载数据。让我们看看如何操作。

路由机制

作为一项实际练习,我们想要构建一个仪表板。仪表板从我们的 events 表中收集一些信息。我们必须运行多个查询来收集一些数据以渲染仪表板视图。

在示例中,我们将执行以下操作:

  • 定义两个路由,用于 /dashboard/dashboard-concurrent。第一个用于顺序查询,第二个用于并发查询。

  • 定义一个名为 DashboardController 的控制器,包含两个方法 - index()(用于顺序查询)和 indexConcurrent()(用于并发查询)。

  • 定义四个查询:一个用于计算 events 表中的行数,以及三个查询用于检索描述字段中包含特定术语(在示例中我们寻找包含术语 something 的字符串)的最后五个事件,对于每种事件类型('INFO''WARNING''ALERT')。

  • 定义一个视图来显示查询的结果。

使用 Octane 路由

Octane 提供了路由机制的实现。

Octane 提供的路由机制(Octane::route())比经典 Laravel 路由机制(Route::get())更轻量。Octane 路由机制更快,因为它跳过了 Laravel 路由提供的所有完整功能,如中间件。中间件是在路由被调用时添加功能的一种方式,但它需要时间来调用和管理这个软件层。

要调用 Octane 路由,你可以使用 Octane::route() 方法。route() 方法有三个参数。第一个参数是 HTTP 方法(例如 'GET', 'POST', 等),第二个参数是路径(例如 ‘/dashboard’),第三个参数是一个返回 Response 对象的函数。

现在我们已经了解了 Route::get()Octane::route() 之间的语法差异,我们可以通过将 Route::get() 替换为 Octane::route() 来修改最后的代码片段:

use Laravel\Octane\Facades\Octane;
use Illuminate\Http\Response;
use App\Http\Controllers\DashboardController;
Octane::route('GET', '/dashboard', function() {
    return new Response(
      (new DashboardController)->index());
});
Octane::route('GET', '/dashboard-concurrent', function() {
    return new Response(
      (new DashboardController)->indexConcurrent());
});

如果你想测试 Octane 路由机制比 Laravel 路由机制快多少,创建两个路由:第一个由 Octane 服务,第二个由 Laravel 路由服务。你会看到响应非常快,因为应用程序继承了来自所有 Octane 框架加载机制的所有好处,Octane::route 也优化了路由部分。代码创建了两个路由,/a/b/a 路由由 Octane 路由机制管理,/b 路由由经典路由机制管理:

Octane::route('GET', '/a', function () {
    return new Response(view('welcome'));
});
Route::get('/b', function () {
    return new Response(view('welcome'));
});

如果你通过浏览器调用并检查响应时间来比较这两个请求,你会看到 /a 路由比 /b 路由快(在我的本地机器上,快 50%),这是因为 Octane::route()

现在路由已经设置好了,我们可以专注于控制器。

创建控制器

现在我们将创建一个控制器,名为 DashboardController,包含两个方法:index()indexConcurrent()

app/Http/Controllers/ 目录下,创建一个 DashboardController.php 文件,内容如下:

<?php
namespace App\Http\Controllers;
class DashboardController extends Controller
{
    public function index()
    {
        return view('welcome');
    }
    public function indexConcurrent()
    {
        return view('welcome');
    }
}

我们刚刚创建了控制器的这些方法,所以它们只是加载视图。现在我们将在方法中添加一些逻辑,在模型文件中创建一个查询,并从控制器中调用它。

创建查询

为了允许控制器加载数据,我们将实现 events 表。为此,我们将使用 Laravel 提供的查询作用域机制。查询作用域允许你在模型中定义逻辑并在你的应用程序中重用它。

我们将要实现的查询作用域将被放置在 Event 模型类中的 scopeOfType() 方法中。scopeOfType() 方法允许你扩展 Event 模型的功能并添加一个新的方法,ofType()

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Event extends Model
{
    use HasFactory;
    /**
     * This is a simulation of a
     * complex query that is time-consuming
     *
     * @param  mixed  $query
     * @param  string  $type
     * @return mixed
     */
    public function scopeOfType($query, $type)
    {
        sleep(1);
        return $query->where('type', $type)
        ->where('description', 'LIKE', '%something%')
        ->orderBy('date')->limit(5);
    }
}

Event 模型文件位于 app/Models 目录下。文件名为 Event.php

查询返回定义为参数的事件类型($type),并选择描述中包含单词 something 的行(通过 '``LIKE' 操作符)。

最后,我们将按日期排序数据(orderBy)并限制为五条记录(limit)。

为了突出我们即将实施的优化的好处,我将添加一个 1 秒的 sleep 函数来模拟耗时操作。

DashboardController 文件

现在,我们可以再次打开 DashboardController 文件并实现调用四个查询的逻辑——第一个用于计算事件数量:

Event::count();

第二个是使用 ofType 函数通过定义的查询检索具有 '``INFO' 类型的事件:

Event::ofType('INFO')->get();

第三个是用于检索 '``WARNING' 事件:

Event::ofType('WARNING')->get();

最后一个是用于检索 '``ALERT' 事件:

Event::ofType('ALERT')->get();

让我们在控制器 index() 方法中将所有这些放在一起,以顺序调用查询:

use App\Models\Event;
// …
public function index()
{
    $time = hrtime(true);
    $count = Event::count();
    $eventsInfo = Event::ofType('INFO')->get();
    $eventsWarning = Event::ofType('WARNING')->get();
    $eventsAlert = Event::ofType('ALERT')->get();
    $time = (hrtime(true) - $time) / 1_000_000;
    return view('dashboard.index',
        compact('count', 'eventsInfo', 'eventsWarning',
                'eventsAlert', 'time')
    );
}

hrtime() 方法用于测量所有四个查询的执行时间。

然后,在所有查询执行完毕后,调用 dashboard.index 视图。

现在,以同样的方式,我们将创建 indexConcurrent() 方法,其中查询通过 Octane::concurrently() 方法并行执行。

Octane::concurrently() 方法有两个参数。第一个是任务数组。一个任务是一个匿名函数。匿名函数可以返回一个值。concurrently() 方法返回一个值数组(任务数组的返回值)。第二个参数是 concurrently() 等待任务完成的毫秒数。如果一个任务花费的时间超过第二个参数(毫秒),concurrently() 函数将抛出 TaskTimeoutException 异常。

indexConcurrent() 方法的实现位于 DashboardController 类中:

    public function indexConcurrent()
    {
        $time = hrtime(true);
        try {
            [$count,$eventsInfo,$eventsWarning,$eventsAlert] =
            Octane::concurrently([
                fn () => Event::count(),
                fn () => Event::ofType('INFO')->get(),
                fn () => Event::ofType('WARNING')->get(),
                fn () => Event::ofType('ALERT')->get(),
            ]);
        } catch (TaskTimeoutException $e) {
            return "Error: " . $e->getMessage();
        }
        $time = (hrtime(true) - $time) / 1_000_000;
        return view('dashboard.index',
            compact('count', 'eventsInfo', 'eventsWarning',
                    'eventsAlert', 'time')
        );
    }

要正确使用 TaskTimeoutException,你必须导入该类:

use Laravel\Octane\Exceptions\TaskTimeoutException;

最后,你需要实现以渲染页面的是视图。

渲染视图

在控制器中,每个方法的最后一条指令是返回视图:

return view('dashboard.index',
            compact('count', 'eventsInfo', 'eventsWarning',
                    'eventsAlert', 'time')
        );

view() 函数加载 resources/views/dashboard/index.blade.php 文件(dashboard.index)。为了从控制器向视图共享数据,我们将向 view() 函数发送一些参数,例如 $count$eventsInfo$eventsWarning$eventsAlert$time

视图是一个使用 Blade 语法显示变量(如 $count$eventsInfo$eventsWarning$eventsAlert$time)的 HTML 模板:

<x-layout>
    <div>
        Count : {{ $count }}
    </div>
    <div>
        Time : {{ $time }} milliseconds
    </div>
    @foreach ($eventsInfo as $e)
    <div>
        {{ $e->type }} ({{ $e->date }}): {{ $e->description }}
    </div>
    @endforeach
    @foreach ($eventsWarning as $e)
    <div>
        {{ $e->type }} ({{ $e->date }}): {{ $e->description }}
    </div>
    @endforeach
    @foreach ($eventsAlert as $e)
    <div>
        {{ $e->type }} ({{ $e->date }}): {{ $e->description }}
    </div>
    @endforeach
</x-layout>

视图继承布局(通过 x-layout 指令),因此你可以创建 resources/views/components/layout.blade.php 文件:

<html>
    <head>
        <title>{{ $title ?? 'Laravel Octane Example' }}
        </title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width,
          initial-scale=1.0">
    </head>
    <body>
        <h1>Laravel Octane Example</h1>
        <hr/>
        {{ $slot }}
    </body>
</html>

现在你已经拥有了数据库中的数据,model 类中的查询,以及通过模型加载数据并发送数据到视图的控制器,以及视图模板文件。

我们还有两个路由:第一个是 /dashboard,使用顺序查询,第二个是 /dashboard-concurrent,使用并行查询。

仅以此为例,查询被强制设置为 1 秒(在模型方法中)。

如果你打开浏览器访问 http://127.0.0.1:8000/dashboard,你会看到每个请求需要超过 3 秒钟(每个查询需要 1 秒)。这是每个查询执行时间的总和。

如果你打开浏览器访问 http://127.0.0.1:8000/dashboard-concurrent,你会看到每个请求需要 1 秒钟来执行。这是最昂贵的查询的最大执行时间。

这意味着你必须在控制器中调用多个查询来检索数据。为了渲染页面,你可以使用 Octane::concurrently() 方法。

Octane::concurrently() 方法在其他场景中也很有用(不仅仅是加载数据库中的数据),例如执行并发 HTTP 请求。因此,在下一节中,我们将使用 Octane::concurrently() 方法从 HTTP 调用中检索数据(而不是从数据库中检索数据)。让我们看看如何操作。

并行执行 HTTP 请求

想象一下这样的场景:在你的应用程序中需要添加一个新的网页,为了渲染这个网页,你必须调用多个 API,因为同一个页面需要从多个来源获取多个数据片段(产品列表、新闻列表、链接列表等)。在需要从多个 API 调用获取数据的单个网页场景中,你可以同时执行 HTTP 请求以减少页面的响应时间。

在这个例子中,为了简化说明,我们将避免使用控制器和视图。我们将从 API 收集 JSON 响应,然后我们将合并这些响应为一个 JSON 响应。我们需要关注的重要方面是调用第三方 HTTP 服务的 HTTP 请求机制,因为我们的目标是了解如何并发地执行 HTTPS 调用。

为了模拟 HTTP 服务,我们将创建两个新的路由:

  • api/sentence:一个 API 端点,回复包含随机句子的 JSON

  • api/name:一个 API 端点,回复包含随机名字的 JSON

两个端点 API 都实现了一个 1 秒的 sleep() 函数,以便客户端(调用端点的人)等待答案。这是一种模拟慢速 API 并查看我们可以从并行 HTTP 请求中获得的好处的方法。

routes/web.php 文件中,你可以添加实现 API 的两个路由:

Octane::route('GET', '/api/sentence', function () {
    sleep(1);
    return response()->json([
        'text' => fake()->sentence()
    ]);
});
Octane::route('GET', '/api/name', function () {
    sleep(1);
    return response()->json([
        'name' => fake()->name()
    ]);
});

现在,使用 Http::get() 方法执行 HTTP 请求,你可以实现从两个 API 顺序检索数据的逻辑:

Octane::route('GET', '/httpcall/sequence', function () {
    $time = hrtime(true);
    $sentenceJson =
      Http::get('http://127.0.0.1:8000/api/sentence')->
      json();
    $nameJson =
      Http::get('http://127.0.0.1:8000/api/name')->json();
    $time = hrtime(true) - $time;
    return response()->json(
        array_merge(
            $sentenceJson,
            $nameJson,
            ["time_ms" => $time / 1_000_000]
        )
        );
});

使用 Octane::concurrently(),你现在可以调用两个 Http::get() 方法,使用 HTTP 请求作为 Closure(匿名函数),就像我们为数据库查询所做的那样:

Octane::route('GET', '/httpcall/parallel', function () {
    $time = hrtime(true);
    [$sentenceJson, $nameJson] = Octane::concurrently([
        fn() =>
          Http::get('http://127.0.0.1:8000/api/sentence')->
          json(),
        fn() =>
          Http::get('http://127.0.0.1:8000/api/sequence')->
          json()
    ]
    );
    $time = hrtime(true) - $time;
    return response()->json(
        array_merge(
            $sentenceJson,
            $nameJson,
            ["time_ms" => $time / 1_000_000]
        )
        );
});

如果你打开浏览器访问 http://127.0.0.1:8000/httpcall/sequence,你会看到响应时间超过 2,000 毫秒(两个 sleep 函数的执行时间之和,以及一些用于执行 HTTP 连接的毫秒)。

如果你打开你的浏览器到http://127.0.0.1:8000/httpcall/parallel,你会看到响应时间超过 1,000 毫秒(两个 HTTP 请求是并行执行的)。

使用Octane::concurrently()可以帮助你在进行这些示例(如数据库查询或获取外部资源)时节省一些总响应时间。

管理 HTTP 错误

在并行执行 HTTP 调用时,你必须预期有时外部服务可能会以错误(例如,HTTP 状态码500)回答。为了在源代码中更好地管理错误,我们还必须正确处理从 API 获取空响应的情况,这通常会导致包含错误的响应(例如,API 返回状态码500)。

在这里,我们演示我们将实现一个返回500作为 HTTP 状态码(内部服务器错误消息)的 API:

Octane::route('GET', '/api/error', function () {
    return response(
        status: 500
    );
});

然后,我们可以在我们的并发 HTTP 调用中的一个调用 API 错误路由。如果我们没有管理错误,我们将收到如下错误:

https://github.com/OpenDocCN/freelearn-php-zh/raw/master/docs/hiperf-lrv-octane/img/Figure_4.07_B17728.jpg

图 4.7:浏览器中的未管理错误

因此,我们可以通过管理以下内容来改进我们的代码:

  • 来自并发 HTTP 调用执行的异常

  • 使用Null合并运算符的空响应值

  • 将数组初始化为空数组

routes/web.php文件中,我们可以改进 API 调用并使其更可靠:

Route::get('/httpcall/parallel-witherror', function () {
    $time = hrtime(true);
    $sentenceJson = [];
    $nameJson = [];
    try {
        [$sentenceJson, $nameJson] = Octane::concurrently([
            fn () => Http::get(
              'http://127.0.0.1:8000/api/sentence')->json()
              ?? [],
            fn () => Http::get(
              'http://127.0.0.1:8000/api/error')->json() ??
              [],
        ]
        );
    } catch (Exception $e) {
        // The error: $e->getMessage();
    }
    $time = hrtime(true) - $time;
    return response()->json(
        array_merge(
            $sentenceJson,
            $nameJson,
            ['time_ms' => $time / 1_000_000]
        )
    );
});

这样,如果抛出异常或我们收到 HTTP 错误作为响应,我们的软件将管理这些场景。

建议,即使你专注于性能方面,你也不必忽视应用程序的行为和管理不愉快的路径。

现在我们已经了解了如何并行执行任务,我们可以专注于缓存响应以避免在每次请求时调用外部资源(数据库或 Web 服务)。

理解缓存机制

Laravel 为开发者提供了一个强大的缓存机制。

缓存机制可以与数据库、Memcached、Redis 或 DynamoDB 等提供者一起使用。

Laravel 的缓存机制允许数据被快速高效地存储以供以后检索。

在从数据库或 Web 服务检索数据可能是一个耗时操作的情况下,这非常有用。在信息检索后,将检索到的信息存储在缓存机制中,可以使未来的信息检索更容易和更快。

因此,基本上,缓存机制暴露了两个基本功能:信息的缓存和从缓存中检索信息。

为了在每次使用缓存项时正确检索信息,使用存储键是合适的。这样,就可以通过特定的键缓存大量信息。

Laravel 的缓存机制通过特殊的 remember() 函数允许检索与特定键相关联的信息。如果由于存储时间超时或键未缓存,该信息已过时,那么 remember() 方法允许调用一个匿名函数,该函数的任务是从外部资源获取数据,这可能是一个数据库或网络服务。一旦检索到原始数据,remember() 函数将自动返回数据,同时也会负责使用用户定义的键将其缓存。

下面是使用 remember() 函数的一个示例:

use Illuminate\Support\Facades\Cache;
$secondsTimeToLive = 5;
$cacheKey= 'cache-key';
$value = Cache::remember($cacheKey, $secondsTimeToLive, function () {
    return Http::get('http://127.0.0.1:8000/api/sentence')
      ->json() ?? [];
});

在前一个示例中应用于每个 HTTP 请求的 remember() 功能可以通过匿名函数实现:

$getHttpCached = function ($url) {
        $data = Cache::store('octane')->remember(
                'key-'.$url, 20, function () use ($url) {
            return Http::get(
              'http://127.0.0.1:8000/api/'.$url)->json() ??
              [];
        });
        return $data;
    };

然后,匿名函数可以被 Octane::concurrently() 函数为每个并发任务调用:

[$sentenceJson, $nameJson] = Octane::concurrently([
            fn () => $getHttpCached('sentence'),
            fn () => $getHttpCached('name'),
        ]
        );

因此,routes/web.php 文件中路由的最终代码如下:

Octane::route('GET','/httpcall/parallel-caching', function () {
    $getHttpCached = function ($url) {
        $data = Cache::store('octane')->remember(
                'key-'.$url, 20, function () use ($url) {
            return Http::get(
              'http://127.0.0.1:8000/api/'.$url)->json() ??
              [];
        });
        return $data;
    };
    $time = hrtime(true);
    $sentenceJson = [];
    $nameJson = [];
    try {
        [$sentenceJson, $nameJson] = Octane::concurrently([
            fn () => $getHttpCached('sentence'),
            fn () => $getHttpCached('name'),
        ]
        );
    } catch (Exception $e) {
        // The error: $e->getMessage();
    }
    $time = hrtime(true) - $time;
    return response()->json(
        array_merge(
            $sentenceJson,
            $nameJson,
            ['time_ms' => $time / 1_000_000]
        )
    );
});

以下是一些关于代码的考虑:

  • 我们使用了比 Laravel 路由更快的 Octane 路由。

  • 匿名函数的 $url 参数用于创建缓存键,并通过 Http::get() 调用正确的 API。

  • 我们使用 Octane 作为驱动程序使用缓存,Cache::store('octane')

  • 我们使用了 remember() 函数进行缓存。

  • 我们将缓存项的生存时间设置为 20 秒。这意味着在 20 秒后,缓存项将被生成,匿名函数提供的代码将被调用。

这段代码通过缓存显著提高了响应时间。

然而,代码可以更加优化。

我们缓存了每个 HTTP 请求的结果。但是,我们也可以缓存由 Octane::concurrently 提供的结果。所以,我们不必缓存每个 HTTP 请求,而是可以缓存来自 Octane::concurrently() 的结果。这使我们能够通过避免执行已缓存的 Octane::concurrently() 来节省更多时间。

在这种情况下,我们可以将 Octane::concurrently() 移到由 remember() 调用的匿名函数体中:

Octane::route('GET', '/httpcall/caching', function () {
    $time = hrtime(true);
    $sentenceJson = [];
    $nameJson = [];
    try {
        [$sentenceJson, $nameJson] =
        Cache::store('octane')->remember('key-checking',
                                          20, function () {
            return Octane::concurrently([
                fn () => Http::get(
                  'http://127.0.0.1:8000/api/sentence')->
                  json(),
                fn () => Http::get(
                 'http://127.0.0.1:8000/api/name')->json(),
            ]);
        });
    } catch (Exception $e) {
        // The error: $e->getMessage();
    }
    $time = hrtime(true) - $time;
    return response()->json(
        array_merge(
            $sentenceJson,
            $nameJson,
            ['time_ms' => $time / 1_000_000]
        )
    );
});

在这种情况下,从请求日志中,你可以看到 API 只在第一次调用,然后从缓存中检索数据,执行时间减少:

  200    GET /api/sentence ........ 18.57 mb 17.36 ms
  200    GET /api/name ............ 18.57 mb 17.36 ms
  200    GET /httpcall/caching .... 17.43 mb 59.82 ms
  200    GET /httpcall/caching ..... 17.64 mb 3.38 ms
  200    GET /httpcall/caching ..... 17.64 mb 2.36 ms
  200    GET /httpcall/caching ..... 17.64 mb 3.80 ms
  200    GET /httpcall/caching ..... 17.64 mb 3.30 ms

第一次调用缓存路由大约需要 60 毫秒;后续请求要快得多(大约 3 毫秒)

如果你尝试通过按顺序调用 HTTP 请求而不使用缓存来进行相同的测试,你将看到更高的响应时间值。你还会看到 API 每次都会被调用,这使得应用程序的速度和可靠性依赖于第三方系统,因为可靠性和速度取决于第三方系统(提供 API 的系统)创建响应的方式。

例如,通过按顺序调用 HTTP 请求,不使用缓存——即使 API 由 Octane 提供(因此速度更快)——你将获得以下结果:

  200    GET /api/sentence ........ 18.57 mb 15.22 ms
  200    GET /api/name ............. 18.68 mb 0.64 ms
  200    GET /httpcall/sequence ... 18.79 mb 60.81 ms
  200    GET /api/sentence ......... 18.69 mb 3.26 ms
  200    GET /api/name ............. 18.69 mb 1.68 ms
  200    GET /httpcall/sequence ... 18.94 mb 15.55 ms
  200    GET /api/sentence ......... 18.70 mb 1.30 ms
  200    GET /api/name ............. 18.70 mb 1.09 ms
  200    GET /httpcall/sequence .... 18.97 mb 9.52 ms
  200    GET /api/sentence ......... 18.71 mb 1.32 ms
  200    GET /api/name ............. 18.71 mb 1.05 ms
  200    GET /httpcall/sequence .... 19.00 mb 9.28 ms

虽然你可能认为这并不是一个很大的改进,或者这些值是机器相关的,但对于单个请求的微小改进(我们的响应时间从 10-15 毫秒降低到 2-3 毫秒)可能产生重大影响,尤其是在生产环境中,如果你有大量的并发请求。单个请求的每次微小改进的好处,在具有许多并发用户的生产环境中,会通过请求的数量而倍增。

现在我们对缓存有了更多了解,我们可以通过添加事件检索的缓存来重构仪表板。

重构仪表板

我们将创建一个新的路由,/dashboard-concurrent-cached,与 Octane 路由一起使用,并且我们将调用一个新的 DashboardController 方法,indexConcurrentCached()

// Importing Octane class
use Laravel\Octane\Facades\Octane;
// Importing Response class
use Illuminate\Http\Response;
// Importing the DashboardController class
use App\Http\Controllers\DashboardController;
Octane::route('GET', '/dashboard-concurrent-cached', function () {
    return new Response((new DashboardController)->
     indexConcurrentCached());
});

在控制器 app/Http/Controllers/DashboardController.php 文件中,你可以添加新的方法:

public function indexConcurrentCached()
{
    $time = hrtime(true);
    try {
        [$count,$eventsInfo,$eventsWarning,$eventsAlert] =
        Cache::store('octane')->remember(
            key: 'key-event-cache',
            ttl: 20,
            callback: function () {
                return Octane::concurrently([
                    fn () => Event::count(),
                    fn () => Event::ofType('INFO')->get(),
                    fn () => Event::ofType('WARNING')->
                             get(),
                    fn () => Event::ofType('ALERT')->get(),
                ]);
            }
        );
    } catch (Exception $e) {
        return 'Error: '.$e->getMessage();
    }
    $time = (hrtime(true) - $time) / 1_000_000;
    return view('dashboard.index',
        compact('count', 'eventsInfo', 'eventsWarning',
                'eventsAlert', 'time')
    );
}

在新方法中,我们执行以下操作:

  • 调用 remember() 方法将值存储在缓存中

  • 执行 Octane:concurrently 来并行化查询

  • 使用 'key-event-cache' 作为缓存项的键名

  • 使用 20 秒作为缓存生存时间(20 秒后,查询将被执行并从数据库检索新值)

  • 使用与 /dashboard 路由相同的查询和相同的 blade 视图(为了进行良好的比较)

现在,如果你没有使用自动加载器(如第二章配置 RoadRunner 应用程序服务器)中解释的那样),你可以使用 php artisan octane:reload 重新启动你的 Octane 工作进程,然后访问以下内容:

  • http://127.0.0.1:8000/dashboard 用于加载使用顺序查询且没有缓存机制的页面

  • http://127.0.0.1:8000/dashboard-concurrent-cached 用于加载使用并行查询和缓存机制的页面

现在我们已经实现了逻辑并打开了页面,我们将分析结果。

结果

你可以看到的结果是令人印象深刻的,因为从响应时间超过 200 毫秒,你现在将有一个响应时间仅为 3 或 4 毫秒。

较长的响应来自 /dashboard,其中实现了没有缓存的顺序查询。最快的响应来自 /dashboard-concurrent-cached,它使用 Octane::concurrently() 来执行查询,并将结果缓存 20 秒:

  200    GET /dashboard ...... 19.15 mb 261.34 ms
  200    GET /dashboard ...... 19.36 mb 218.45 ms
  200    GET /dashboard ...... 19.36 mb 223.23 ms
  200    GET /dashboard ...... 19.36 mb 222.72 ms
  200    GET /dashboard-concurrent-cached .............................. 19.80 mb 112.64 ms
  200    GET /dashboard-concurrent-cached ................................ 19.81 mb 3.93 ms
  200    GET /dashboard-concurrent-cached ................................ 19.81 mb 3.69 ms
  200    GET /dashboard-concurrent-cached ................................ 19.81 mb 4.28 ms
  200    GET /dashboard-concurrent-cached ................................ 19.81 mb 4.62 ms

当你在 Octane Cache 中缓存数据时,你也应该注意缓存配置。错误的配置可能会在你的应用程序中引发一些错误。

缓存配置

当你在实际场景中开始使用 Octane Cache 时可能会遇到的典型异常是这样的:

Value [a:4:{i:0;i:100000;i:...] is too large for [value] column

解决上述错误信息的方法是更改缓存配置,通过增加用于存储缓存值的字节数。在 config/octane.php 文件中,您可以配置缓存用于行数和分配给缓存的字节数。

默认情况下,配置如下:

    'cache' => [
        'rows' => 1000,
        'bytes' => 10000,
    ],

如果你在浏览器中遇到 Value is too large 异常,你可能需要增加 config/octane.php 文件中的字节数:

    'cache' => [
        'rows' => 1000,
        'bytes' => 100000,
    ],

现在,使用 Octane 功能,您可以提高应用程序的响应时间以及一些方面。

摘要

在本章中,我们构建了一个非常简单的应用程序,使我们能够涵盖构建 Laravel 应用程序的多方面内容,例如导入初始数据、优化路由机制、通过 HTTP 请求集成第三方数据,以及通过 Octane Cache 使用缓存机制。我们还使用了 Laravel Octane 的一些功能,以减少页面加载响应时间,这得益于以下原因:

  • Octane::route 用于优化路由解析过程

  • Octane::concurrently 用于优化和启动并行任务

  • Octane Cache 用于向我们的应用程序添加基于 Swoole 的缓存

我们学习了如何并发执行查询和 API 调用,并使用缓存机制在请求间重用内容。

在下一章中,我们将探讨一些其他方面,这些方面并非严格由 Octane 提供,但可能会影响你的 Octane 优化过程。

我们还将应用不同的缓存策略,使用 Octane 提供的计划任务和其他优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值