Laravel-Octane-高性能指南-全-
Laravel Octane 高性能指南(全)
原文:
zh.annas-archive.org/md5/53ac6eb53ccf1baf1816775e25495357译者:飞龙
前言
本书全面概述了使用 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 9 | Windows, 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-foundation和http-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 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止于此,您还可以获得独家折扣、时事通讯和丰富的免费内容,每天直接发送到您的收件箱。
按照以下简单步骤获取福利:
- 扫描二维码或访问以下链接

packt.link/free-ebook/9781801819404
-
提交您的购买证明
-
就这些!我们将直接将您的免费 PDF 和其他福利发送到您的电子邮件。
第一部分:架构
本部分的目标是使您熟悉 PHP 的应用服务器架构,并展示应用服务器如何跨工作进程共享资源以服务 HTTP 请求。
本部分包括以下章节:
- 第一章, 理解 Laravel 网络应用程序架构
第一章:理解 Laravel Web 应用程序架构
本书是为希望以更可扩展的方式设计和构建他们的 Laravel Web 应用程序,并使其更高效性能的 Laravel 开发者而写的。
本书旨在为您提供有关如何改进 Web 应用程序软件架构的知识、建议和解释,从典型的 PHP Web 应用程序架构到更可扩展和高效架构。
它提供了使用 Laravel Octane 设计和构建高性能应用程序所需的所有内容的 360 度概览。我们将看到为什么 Laravel Octane 适合设计和构建高性能应用程序。这本书还涵盖了 Laravel Octane 使用的不同工具,例如 Open Swoole 和 RoadRunner,并列出并描述了各种功能和差异化元素。但最重要的是,它能够让你理解为什么以及何时使用 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-foundation 和 http-kernel 包。
所有这些只是为了说明 Laravel 使用了 PHP 生态系统的最佳部分,将它们组合在一起,并为开发者提供工具、辅助函数、类和方法,以简化所有工具的使用。
此外,Laravel 不仅仅是一个框架。Laravel 是一个生态系统。
Laravel 还提供与框架集成的应用程序和服务。
例如,Laravel 提供以下内容:
-
收银员:用于与 Stripe 和 Paddle 集成,处理支付和订阅流程。
-
Breeze、Jetstream、Sanctum 和 Socialite:用于管理授权、身份验证、社交登录集成流程和公开受保护的 API。
-
黄昏 和 害虫:用于测试。
-
Echo:用于实时广播事件。
-
Envoyer、Forge 和 Vapor:用于服务器或无服务器管理和部署流程管理。
-
Mix:通过完全集成 Laravel 前端的 webpack 配置编译 JavaScript 和 CSS。
-
Horizon:基于 Redis 的队列监控的 Web 用户界面。
-
Nova:Laravel 应用程序的管理员面板构建器。
-
Sail:基于 Docker 的本地开发环境。
-
Scout:一个全文搜索引擎,由 Algolia、Meilisearch 或简单的 MySQL 或 PostgreSQL 数据库提供支持。
-
Spark:用于管理应用程序中的计费/订阅的样板解决方案。
-
Telescope:用于显示调试和洞察的 UI 模块。
-
Valet:为运行 PHP 应用程序配置的 macOS 特定应用程序包。它依赖于 nginx、PHP 和 Dnsmasq。
-
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 请求中发生的情况,我们首先分析在请求期间浏览器发送到服务器的数据。请求主要特征是方法(GET、POST、PUT等)、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 激活了一些工作进程,如下面的图所示:

图 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 5 – 1
这相当简单,但最终有助于理解静态变量在底层的行为。
静态变量的使用并不罕见。只需想想单例对象或 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 服务提供者都会被实例化,并且每次都会调用 construct、register 和 boot 方法,显示以下输出:
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 毫秒)。
另一个需要提到的重要事情是,register 和 boot 方法在前两个请求中会被调用,直到第十个请求(两个工作进程乘以最多五个请求)。后续请求会重复这种行为。
因此,在您的网络应用程序中安装 Laravel Octane 允许您提高应用程序的响应时间。
所有这些都不需要涉及某些工具,例如 Swoole 和 RoadRunner 等应用服务器提供的并发管理工具。
摘要
现在,我们已经了解了 Laravel Octane 的行为、一些优点和一些副作用,我们可以继续下一章,通过安装和配置两个 Laravel Octane 兼容的应用服务器之一:RoadRunner 应用服务器。
我们将回顾本章中提到的某些指令。本章的目标是提供一些有用的总结元素,以解决本书其余部分中更具体和详细的案例。
第二部分:应用服务器
Laravel Octane 使用应用服务器,如 RoadRunner 或 Swoole。在应用服务器而不是经典网络服务器上运行应用程序,允许开发者使用一些高级功能,例如更有效地管理多个工作进程、并发任务以及以更高效的方式引导应用程序;快速缓存,以及在工作进程之间共享数据。本部分包括以下章节:
-
第二章,配置 RoadRunner 应用服务器
-
第三章, 配置 Swoole 应用程序服务器
第二章:配置 RoadRunner 应用服务器
当在 Laravel 中开发一个 Web 应用程序时,我们习惯于使用 Web 服务器通过网络交付我们的 Web 应用程序。
Web 服务器通过 HTTP 或 HTTPS 协议公开应用程序,并实现与通过 HTTP 协议交付资源紧密相关的功能。
应用服务器在处理不同协议方面是一个结构化和更复杂的软件组件;它可以处理 HTTP,以及更底层的协议,如 TCP,或其他协议,如 WebSocket。
此外,应用服务器可以实现一个结构化的工作员结构。这意味着执行应用程序逻辑的应用服务器将执行委托给工作员。工作员是一个负责执行给定任务的隔离线程。
工作员管理允许通过应用服务器运行的应用程序访问并发性和并行任务执行等功能。
要能够管理各种工作员,应用服务器还必须能够实现跨工作员的负载分配功能,并且还必须能够实现适当的平衡(使用负载均衡器)。
在 PHP 生态系统中有很多应用服务器,其中两个是 RoadRunner 和 Swoole。它们与 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,我们将执行以下操作:
-
从头创建一个新的 Laravel 应用程序。
-
将 Laravel Octane 包添加到新的 Laravel 应用程序中。
-
安装 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 new 和 composer 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 方法需要两个步骤:
-
安装 RoadRunner CLI。
-
通过 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
命令执行生成的输出将显示软件包版本、操作系统和架构,如图下所示:

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

图 2.2:两个 rr 可执行文件,CLI 和应用程序服务器
此外,你可以使用显示版本选项运行两个可执行文件:

图 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的应用程序服务器发送四个不同的请求。
在终端模拟器中,如果您启动应用程序服务器,您将看到应用程序服务器的日志:

图 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.pem和localhost.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 容器运行的运行环境,你必须遵循一些步骤:
-
设置 Laravel Sail
-
安装 Laravel Octane
-
设置 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):

图 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 欢迎页面:

图 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.4、8.0、8.1。
在 docker/8.1 目录中,您有以下内容:
-
一个 Dockerfile。
-
php.ini文件。 -
用于启动容器的
start-container脚本。此脚本引用了带有引导配置的supervisor.conf文件。 -
包含监督脚本配置的
supervisor.conf文件。

图 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 服务器也被定义了。
注意
如果您对端口 80 和 81 感到困惑,只是为了澄清:容器中的应用服务器将监听内部端口 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… 消息:

图 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();
}
在前面的代码片段中,我们使用了Octane和Log类,所以请记住在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 缓存,我们还有一些其他不错的方法,例如 increment 和 decrement,例如:
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 部分中定义。

图 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 行,为
uuid、name、age和value字段填充数据。为了填充值,我们将使用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 的行
-
返回行(关联数组,其中项是具有
uuid、name、age和value字段的行字段)
在 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
添加数据库
现在我们已经创建了应用程序,我们必须安装和设置数据库,因为我们的示例应用程序需要数据库来存储和检索示例数据。因此,为了安装和设置数据库,我们将执行以下操作:
-
安装 MySQL 数据库服务器
-
在 Laravel 中执行迁移(将模式定义应用到数据库表中)
-
安装一个应用程序来管理和检查数据库的表和数据
安装数据库服务
安装数据库服务器有三种方式:通过官方安装程序、通过您的本地包管理器或通过 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 镜像将自动下载:

图 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命令,您将看到以下内容:

图 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
这就是您会看到的:

图 4.3:执行迁移
如果你没有使用 Laravel Sail,并且你使用的是本地操作系统上安装的 MySQL 服务器(使用 Homebrew 或你的操作系统打包器或 MySQL 服务器官方安装程序),你可以使用 php artisan migrate 命令而不需要 sail 命令:
php artisan migrate
数据库模式和表是通过迁移创建的。现在我们可以安装 MySQL 客户端来访问数据库。
安装 MySQL 客户端
要访问数据库的结构和数据,建议安装 MySQL 客户端。MySQL 客户端允许你访问结构、模式和数据,并允许你执行 SQL 查询以提取数据。
你可以选择可用的工具之一;有些是开源工具,有些是付费工具。以下是一些用于管理 MySQL 结构和数据的工具:
-
Sequel Ace 是开源的,适用于 macOS:
github.com/Sequel-Ace/Sequel-Ace -
MySQL Workbench 是官方的,适用于所有平台:
www.mysql.com/products/workbench/ -
TablePlus 可用于 Windows 和 macOS,并支持许多数据库:
tableplus.com/
如果你选择 Sequel Ace 或其他工具,你必须根据 .env 文件设置正确的参数进行初始连接。
例如,Sequel Ace 的初始屏幕会要求你提供主机名、凭证、数据库名和端口号:

图 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 添加到你的应用程序中,你必须执行以下操作:
-
添加 Octane 包
-
创建 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
在开头使用 ; 字符,则扩展不会加载。
总结安装和设置
在继续实施之前,让我总结一下之前的步骤:
-
我们安装了我们的 Laravel 应用程序。
-
我们添加了一个数据库服务。
-
我们配置了一个 MySQL 客户端以访问 MySQL 服务器。
-
我们添加了 Octane 包和配置。
-
我们添加了 Swoole 作为应用程序服务器。
-
我们检查了配置。
因此,现在我们可以开始使用一些 Octane 功能,例如以并行和异步的方式执行重任务。
创建仪表板应用程序
在一个应用程序中,你可以在多个表中存储多种类型的数据。
通常,在产品列表页面上,你必须执行一个查询来检索产品列表。
或者,在一个仪表板中,也许你可以显示多个图表或表格来显示数据库中的某些数据。如果你想在同一页面上显示更多图表,你必须对多个表执行多个查询。
你可能一次执行一个查询;这意味着检索用于组成仪表板的所有有用信息的总时间是所有涉及查询的执行时间的总和。
同时运行多个查询将减少检索所有信息的总时间。
为了演示这一点,我们将创建一个 events 表,我们将存储一些带有用户时间戳的事件。
创建一个事件表
当你在 Laravel 中创建一个表时,你必须使用迁移文件。迁移文件包含创建表和所有字段的逻辑。它包含定义你表结构的所有指令。为了管理使用存储在表中的数据的逻辑,你可能还需要其他一些东西,例如 model 和 seeder 类。
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命令创建了一个用于创建表的模板文件,其中包含基本的字段,如id和timestamps。开发人员必须添加特定于应用程序的字段。在仪表板应用程序中,我们将添加以下字段:
-
user_id: 对于与users表的外部引用,一个用户可以关联到多个事件 -
type: 事件类型可以是INFO、WARNING或ALERT -
description: 事件描述文本 -
value: 从1到10的整数 -
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: 我们将从1到1000生成一个随机数 -
type:我们将使用 Laravel 的Arr:random()辅助函数从以下值中选择一个:'ALERT'、'WARNING'或'INFO' -
description:来自fake()辅助函数的随机文本 -
value:从1到10的随机整数 -
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":

图 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;
再次,你会看到这个命令将花费几毫秒来导入成千上万条记录:

图 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 错误路由。如果我们没有管理错误,我们将收到如下错误:

图 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 提供的计划任务和其他优化。
第五章:使用异步方法减少延迟和管理数据
在上一章中,我们创建了一个新的 Laravel Octane 应用程序,并应用了 Laravel Octane 提供的某些功能来提高性能和减少应用程序的响应时间。
本章,我们将尝试优化更多的事情,例如数据库访问、更改和改进缓存策略。为了提高查询速度,我们将解释来自索引列的好处。对于缓存,我们还将查看仅缓存的方法。
例如,在上一章中,我们并行执行了查询。现在,我们将优化查询,因为并行化快速的事情比并行化慢的事情更好。查询优化允许代码更快地从数据库中检索数据,减少从任何来源(文件、数据库或网络)读取数据时通常发生的延迟。
然后,我们将展示如何使用缓存机制来加快查询过程。我们将采用一种仅缓存的方法,这意味着代码将始终从缓存中检索数据。有一个任务执行查询并将结果存储在缓存中。因此,预缓存机制完全独立于需要数据的代码。因此,我们将这种方法称为异步的,因为需要数据的运行代码无需等待检索数据并填充缓存的过程。
本章的目标是减少 HTTP 请求响应时间。为了减少响应问题,我们将看到如何通过查询优化和将缓存检索时间与缓存填充时间分离的缓存机制来实现信息检索。
本章我们将涵盖以下主题:
-
使用索引优化查询
-
使缓存机制异步
技术要求
我们假设你已经从上一章设置了应用程序。
你需要设置 Laravel Octane 应用程序,包括本章所需的事件迁移和事件生成器。当前章节的要求是安装 PHP 8,或者如果你想使用容器方法,你必须安装 Docker Desktop([www.docker.com/products/docker-desktop/](https://www.docker.com/products/docker-desktop/))或类似工具来运行 Docker 镜像。
源代码
你可以在本书的官方 GitHub 仓库中找到本章使用的示例源代码:https://github.com/PacktPublishing/High-Performance-with-Laravel-Octane/tree/main/octane-ch05。
使用索引优化查询
在上一章中,我们并行进行了查询。
如果并行查询变慢了怎么办?大多数时候,实现优化意味着从多个方面进行操作。在前一章中,我们看到了如何并行化查询。正如我们所看到的,这种方法带来了巨大的好处,但我们还可以做更多的事情。我们想要实现的是进一步减少检索数据时每个并行任务的延迟。
为了做到这一点,我们现在要优化前一章中并行化的每个查询,并探索每个查询背后的推理。
我们将分析查询的特征以及涉及行选择阶段和排序阶段的字段。
让我们从以下示例查询开始:
return $query->where('type', $type)
->where('description', 'LIKE', '%something%')
->orderBy('date')->limit(5);
我们可以看到,在查询中,我们在一些列上执行了一些操作(按类型过滤、按描述过滤等)。
为了使查询更快,我们将在涉及查询的列上创建索引。数据库中的索引是数据库引擎在执行查询时使用的一种数据结构。
为了用类比来解释索引,就像你想要在字典中查找一个单词。从第一页开始,逐页向下滚动,直到找到你想要的术语。找到术语所需的时间取决于单词的数量和单词的位置。想想在成千上万的单词中找到一个以字母z开头的单词是什么感觉。
数据库中的索引就像字典中的索引一样,每个字母都有一个页码。使用索引,对某个术语的访问会更加直接。如今,各种数据库都有一个非常复杂且性能优异的索引系统,所以与实际情况相比,上述类比是直接的。然而,它仍然使我们能够理解,在用于搜索或排序的字段上存在索引对于性能是多么关键。
在 Laravel 中,如果你想创建索引,你可以在迁移文件中这样做。迁移文件是一个文件,你可以定义你的数据库表结构。在迁移文件中,你可以列出你的表列并定义列的类型(字符串、整数、日期、时间等)。
在第四章“构建 Laravel Octane 应用程序”中,我们已经创建了events表的结构(用于我们示例的表)。现在的目标是分析哪些列可以从创建索引中受益,我们将看到如何在迁移文件中创建索引。
在前一章创建的迁移文件(在database/migrations/目录中),我们创建了一个具有一些字段的表:
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();
});
其中一些字段被用于过滤行。
例如,在app/Models中的模型文件中,我们实现了以下查询:
return $query->where('type', $type)
->where('description', 'LIKE', '%something%')
->orderBy('date')->limit(5);
这意味着用于过滤的字段是'type'和'description'。用于排序的字段是'date'字段。
因此,我们将在一个迁移中创建三个索引(一个用于 'type' 列,一个用于 'description' 列,一个用于 'date' 列)。
在创建索引之前,让我们先看看仪表板控制器的响应时间,以便有一个基线值,这样我们就可以稍后检查在节省时间方面的改进。仪表板控制器是查询通过 ofType() 方法调用的地方:
$count = Event::count();
$eventsInfo = Event::ofType('INFO')->get();
$eventsWarning = Event::ofType('WARNING')->get();
$eventsAlert = Event::ofType('ALERT')->get();
要显示包含所有这些查询的仪表板控制器的响应时间,你可以通过以下命令启动 Laravel Octane:
php artisan octane:start
然后,你可以通过你的网络浏览器在 http://127.0.0.1:8000/dashboard 访问它,并在控制台查看响应时间。

图 5.1:未使用索引的仪表板控制器的响应时间
如你所见,响应时间超过 200 毫秒。
现在,我们将创建索引,我们将看到新的响应时间。
创建索引
我们可以使用 make:migration 命令创建一个新的迁移:
php artisan make:migration create_event_indexes
然后,在 database/migrations/ 目录中创建的 yyyy_mm_dd_hhMMss_create_event_indexes.php 文件中,使用 up() 方法,我们将使用 index() 方法为每个列创建索引:
Schema::table('events', function (Blueprint $table) {
$table->index('type', 'event_type_index');
$table->index('description',
'event_description_index');
$table->index('date', 'event_date_index');
});
index() 方法的第一个参数是列名;第二个参数是索引名。索引名在例如你想在 down() 方法中删除列时很有用。down() 方法用于回滚情况:
Schema::table('events', function (Blueprint $table) {
$table->dropIndex('event_type_index');
$table->dropIndex('event_description_index');
$table->dropIndex('event_date_index');
});
要应用新创建的索引,你必须通过 migrate 命令运行迁移:
php artisan migrate
如果你想检查一切是否正常,你可以使用 db:table 命令并查看是否列出了新的索引:
php artisan db:table events
如果索引已创建,你将在 索引 部分看到它们列出来:

图 5.2:执行 db:table 可以显示新的索引
现在索引已创建,我们将分析已经实现的查询,该查询使用以下字段进行过滤和排序:type、description 和 date。我们将使用的查询是在 Event 模型(在 app/Models/Event.php 文件中)的 scopeOfType() 方法中实现的查询:
public function scopeOfType($query, $type)
{
return $query->where('type', $type)
->where('description', 'LIKE', 'something%')
->orderBy('date')->limit(5);
}
在创建索引后,再次使用你的网络浏览器,通过 http://127.0.0.1:8000/dashboard 访问仪表板控制器,并在控制台查看结果:

图 5.3:带有数据库索引的仪表板控制器的响应时间
如果您想从索引使用的优势中获得更多分析指标,您可以使用数据库直接提供的某些工具。例如,在 MySQL 的情况下,您可以通过 MySQL 命令提示符(使用 php artisan db,如下一几行所述)执行查询,并检索 Last_query_cost 值。您将获得一个表示查询执行成本的值,基于查询执行的操作数量。
为了比较最后查询的成本,我们将首先执行带索引的查询,然后不带索引。在示例中,我们将提取 Last_query_cost 指标。
这就是这样做的:
-
确保您正在使用您迁移的最新版本:
php artisan migrate -
然后,使用
db命令打开 MySQL 命令行:php artisan db
db 命令根据 Laravel 配置(数据库名称、用户名、密码和表名)执行 MySQL 客户端。
-
在 MySQL 命令提示符中,您可以在
events表上执行查询:SELECT * FROM events WHERE description LIKE 'something%'; -
一旦完成查询,结果将显示出来。
-
然后,执行以下命令:
SHOW STATUS LIKE 'Last_query_cost'; -
您现在将看到表示查询成本(依赖于查询在数据上执行的操作数量)的指标:
mysql> SHOW STATUS LIKE 'Last_query_cost';+-----------------+----------+| Variable_name | Value |+-----------------+----------+| Last_query_cost | 5.209000 |+-----------------+----------+1 row in set (0.00 sec) -
如果您现在尝试删除索引,迁移将发生回滚(因为我们的迁移最新步骤是通过回滚命令创建索引):
php artisan migrate:rollback --step=1 -
然后,在 MySQL 命令提示符中,再次执行以下命令:
SELECT * FROM events WHERE description LIKE 'something%'; -
然后,执行以下命令:
SHOW STATUS LIKE 'Last_query_cost';
您将看到以下内容:
mysql> SHOW STATUS LIKE 'Last_query_cost';
+-----------------+-------------+
| Variable_name | Value |
+-----------------+-------------+
| Last_query_cost | 1018.949000 |
+-----------------+-------------+
1 row in set (0.01 sec)
如您所见,没有索引,查询成本更高。
如果您执行更复杂的查询,如您的 Event 模型中的查询,例如使用类型和描述字段进行过滤并按日期排序,则没有索引,情况会更糟。
让我们尝试执行以下查询:
SELECT * FROM events WHERE type='ALERT' AND description LIKE 'something%' ORDER BY date;
然后,在执行查询后,您要求 MySQL 显示不带索引的 Last_query_cost 指标:
mysql> SHOW STATUS LIKE 'Last_query_cost';
+-----------------+--------------+
| Variable_name | Value |
+-----------------+--------------+
| Last_query_cost | 10645.949000 |
+-----------------+--------------+
然后,您要求 MySQL 显示带有索引的 Last_query_cost 指标:
mysql> SHOW STATUS LIKE 'Last_query_cost';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| Last_query_cost | 16.209000 |
+-----------------+-----------+
如您所见,差异是巨大的。
我们现在已经比较了带索引和不带索引的查询。通过获得的知识,我们可以通过微调我们使用的索引类型来提高查询的响应时间。
在查询中,我们正在过滤以 description 列开头的行。在查询中,我们正在过滤所有以单词 something 开头的描述。但如果我们想过滤所有包含单词 something 的 description 列的行呢?在我们的前一章中,为了选择包含特定单词的所有描述,我们使用了 LIKE 操作符和通配符:
description LIKE '%something%'
然而,如果我们想优化查询以减少其响应时间,特别是对于文本,我们还有另一个强大的数据库功能用于过滤和搜索文本——全文索引。
创建全文索引
我们将要做的就是将description列的标准索引更改为全文索引,然后我们将看到它的性能表现。这是我们的做法:
-
创建一个新的迁移:
php artisan make:migration create_event_fulltext_index
在yyyy_mm_dd_hhMMss_create_event_fulltext_index.php文件中,在database/migrations/目录下的up()方法中,我们将删除先前的索引并创建一个新的全文索引。
- 在
up()方法中,我们必须删除description列上的先前索引,然后通过fullText()方法创建全文索引:
Schema::table('events', function (Blueprint $table) {
$table->dropIndex('event_description_index');
$table->fullText('description', 'event_description_fulltext_index');
});
关于哪种方法更快,用于过滤以特定术语('something%')开头的描述列的LIKE运算符比全文搜索更快,但它仅覆盖了以特定单词开头的列的过滤。全文搜索的执行速度比使用通配符进行搜索的LIKE方法更快,而且功能更强大,尤其是当你想要搜索一个或多个术语时。让我们尝试在查询中使用whereFullText()方法:
public function scopeOfType($query, $type)
{
return $query->where('type', $type)
//->where('description', 'LIKE', '%something%')
->whereFullText('description', 'something other')
->orderBy('date')->limit(5);
}
whereFullText()方法使用两个参数;第一个参数是description参数,它是要过滤的列名,第二个参数是要搜索的字符串。
现在我们已经实现了全文搜索,我们可以添加另一个改进——缓存查询的结果。
优化查询和缓存
到目前为止,我们已经看到了如何通过并行执行、使用缓存以及在搜索字段上应用索引来优化查询。
然而,如果我们通过缓存结果来优化查询,那么第一个查询,或者缓存结果已过时或被删除的查询,将不得不处理从数据库加载数据以更新和刷新缓存的成本。
在我们即将讨论的缓存策略中,我们将尝试防止缓存刷新时间影响我们应用程序的响应时间。
通过将缓存策略更改为仅使用来自缓存的值(仅缓存的方法)来请求查询,并创建一个负责检索结果及其缓存的进程,可以优化这种场景。此进程异步运行,并且与请求生成的查询解耦。
这种方法可以提高性能,因为它倾向于消除慢查询,并且应该会处理数据检索,因为缓存的定期刷新是通过外部命令完成的。我们可以添加每n秒执行一次并检索新数据的外部命令,然后填充缓存。所有请求都从缓存中获取数据。要执行间隔命令,我们可以使用另一个 Octane 功能,即tick()方法。让我们看看如何。
使缓存机制异步
在第三章,配置 Swoole 应用程序服务器中,我们探讨了Octane::tick()方法。
tick()方法允许你每n秒执行一次函数。
缓存策略可以通过将数据加载委托给特定函数来审查。这个特定函数负责从数据库(而不是从缓存)中检索数据,一旦数据被检索,该函数将结果存储在缓存中。该函数通过 Octane::tick() 方法调用并执行——例如,可能每 60 秒,从数据库检索新鲜数据并填充缓存。所有请求都从缓存中检索数据。
使用异步缓存策略,所有请求都从缓存中检索数据。
缓存通过通过 tick() 调用的任务刷新。
为了实现异步缓存策略,我们正在进行以下操作:
-
在应用程序服务提供者中实现
tick()函数 -
将结果存储在 Octane 缓存中
-
实现读取缓存的控制器
-
实现路由
在应用程序服务提供者中实现 tick() 函数
为了在框架引导时启动缓存任务,我们可以在 App Service Provider 中设置 tick() 方法。App Service Provider 是在框架实例化时创建的文件。因此,在 app/Providers/AppServiceProvider.php 文件中的 boot() 方法中,您必须实现 Octane::tick() 函数:
// including these classes
use Illuminate\Support\Facades\Log;
use Laravel\Octane\Facades\Octane;
use App\Models\Event;
use Illuminate\Support\Facades\Cache;
// in the boot() method
Octane::tick('caching-query', function () {
Log::info('caching-query.', ['timestamp' => now()]);
$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;
$result = ['count' => $count,
'eventsInfo'=> $eventsInfo,
'eventsWarning' => $eventsWarning,
'eventsAlert'=> $eventsAlert,
];
Cache::store('octane')->put('cached-result-tick', $result);
})
->seconds(60)
->immediate();
在 tick() 函数中,我们将执行所有查询,然后将结果存储在 Octane 缓存中(指定 octane 存储):Cache::store('octane')->put()。
两个基本方法是 seconds(),其中我们可以定义节奏,即秒间隔,和 immediate(),它设置 tick() 函数的立即执行。
每 60 秒,查询会自动执行,并将结果存储在缓存中。
实现读取缓存的控制器
现在我们已经实现了填充缓存的 tick() 事件,我们可以专注于控制器,在那里我们可以从缓存中加载数据。
从缓存中检索数据的方法是 Cache::store('octane')->get()。使用 Cache::store('octane') 方法,您将检索 Octane 提供的 Cache 实例。使用 get() 方法,您将检索缓存中存储的值。以下是从 app/Http/Controllers/DashboardController.php 文件中检索缓存值的代码:
use Illuminate\Support\Facades\Cache;
use Exception;
public function indexTickCached()
{
$time = hrtime(true);
try {
$result = Cache::store('octane')->get(
'cached-result-tick');
} catch (Exception $e) {
return 'Error: '.$e->getMessage();
}
$time = (hrtime(true) - $time) / 1_000_000;
$result['time'] = $time;
return view('dashboard.index', $result);
}
在控制器中,正如你所见,存在一种异步方法,因为没有更多依赖于数据库的挂起操作,我们将从数据库的加载委托给外部函数。控制器中的唯一依赖是缓存。
实现路由
在 routes/web.php 文件中,您可以添加一个新的路由:
use Laravel\Octane\Facades\Octane;
use App\Http\Controllers\DashboardController;
use Illuminate\Http\Response;
Octane::route('GET', '/dashboard-tick-cached', function () {
return new Response((new DashboardController)->
indexTickCached());
});
如前几章所示,我们可以通过使用 Octane::route() 来优化路由加载,这有助于减少响应时间。正如你所见,我们出于性能原因使用 Octane::route(),然后将路径设置为 /dashboard-tick-cached 并调用 indexTickCached() 方法。
展示结果
如果我们打开浏览器到初始仪表板路由,那里的查询没有被优化也没有被缓存,然后打开浏览器到新的路由,那里的查询被缓存,我们可以看到在响应时间方面有巨大的差异:
200 GET /dashboard ............ 19.71 mb 66.60 ms
200 GET /dashboard ............ 19.83 mb 42.31 ms
200 GET /dashboard ............ 19.83 mb 37.31 ms
200 GET /dashboard ............ 19.83 mb 30.07 ms
200 GET /dashboard ............ 19.83 mb 42.25 ms
200 GET /dashboard-tick-cached . 19.89 mb 7.51 ms
200 GET /dashboard-tick-cached . 19.89 mb 4.32 ms
200 GET /dashboard-tick-cached . 19.89 mb 4.96 ms
正如你所见,仪表板路径的响应时间为 30-40 毫秒。dashboard-tick-cached路由大约为 5 毫秒。
这是一项重大的改进,再次强调,当你考虑性能时,你必须从这种改进对数千个请求的影响来思考。
这就带我们结束了本章的内容。
摘要
在本章中,我们看到了如何通过结合缓存机制、路由优化、异步方法和查询优化来提高信息检索的效率。缓存机制与异步方法的结合帮助我们减少每个请求(即使缓存已过时)的数据检索响应时间。查询优化减少了检索新鲜数据以填充缓存所花费的时间。路由优化有助于在框架解析路由时节省更多毫秒,从而减少响应时间。
在下一章中,我们将尝试解决需要执行耗时任务的情况——即那些需要一些时间才能完成但无法使用缓存机制的作业。
缓存机制在检索信息时可能是有益的。另一方面,如果我们需要执行诸如写入、发送或转换数据之类的任务,我们很可能需要使用其他工具。在下一章中,我们将看到如何操作。
第四部分:加速
这部分展示了如何配置工具以支持 Laravel Octane 在高效架构中。例如队列以及如何为生产环境设置系统,这些将通过实际示例进行解释。本部分包括以下章节:
-
第六章,在应用程序中使用队列实现异步方法
-
第七章,配置 Laravel Octane 应用程序以适应生产环境
第六章:在您的应用程序中使用队列实现异步方法
在前一章中,我们看到了在创建 HTTP 请求时将一些任务委托给控制器中的外部函数,从性能角度来看可以产生积极的影响。然而,前一章分析的情况仅限于一个范围有限的主题:查询数据库和填充缓存。这种策略也被称为 仅缓存策略。这个过程只需要从缓存中检索数据。
这种方法适用于需要从数据源检索信息的情况。通常,应用程序比这更复杂,例如在执行需要修改和处理数据的特定任务时。
想象一个场景,其中有一个启动备份过程的请求。通常,备份过程需要一段时间才能完成。实现同步方法意味着服务请求的控制器(谁处理请求)会一直保持客户端(网页)等待,直到过程完成。这种解决方案的两个缺点是用户会在浏览器中看到一个漫长的等待加载器,并且解决方案可能会陷入 请求超时 的场景。
对于异步实现,通常需要使用额外的工具来管理任务执行请求的列表。为了允许异步实现,我们需要一个作为队列的机制,其中有一个生产者需要执行工作(生产者产生工作请求并填充队列)和一个消费者从队列中提取工作请求并执行工作,一次一个。通常,队列系统会添加一些功能来监控队列、管理队列(清空队列)、管理优先级和管理多个通道或队列。
Laravel 提供了一种机制来实现所有队列逻辑管理,例如将任务放入队列、从队列中提取工作、管理失败的任务以及通知用户关于执行情况。
为了存储任务列表,Laravel 允许开发者选择可用的队列后端之一:数据库、Redis、Amazon SQS 或 beanstalkd。
本章旨在帮助你了解什么是队列机制,如何在 Laravel 中使用它以及如何配置它——因为使用队列的异步方法不仅减少了响应时间,还实现了不同的用户体验,尤其是在处理耗时任务时。
在本章中,我们将介绍以下内容:
-
介绍 Laravel 中的队列机制
-
安装和配置队列
-
管理队列
-
使用 Redis 管理队列并监控它们
技术要求
感谢前几章的内容,我们假设你已经安装了带有 Laravel Octane 的基本 Laravel 应用程序。对于当前章节,你可以使用 Octane 与 Swoole、Open Swoole 或 RoadRunner 一起使用。
当前章节中描述的示例源代码在此处可用:github.com/PacktPublishing/High-Performance-with-Laravel-Octane/tree/main/octane-ch06。
在 Laravel 中引入队列机制
我们将实现一个简单的用例,以便阐明异步方面以及队列机制如何提高我们网络应用最终用户的用户体验。
对于每个请求,都会执行一个耗时任务。为了模拟耗时任务,我们将调用 sleep() 以持续 3 秒。sleep() 函数,暂停执行一定时间,旨在模拟可能需要一些时间来实现的任务。在实际情况下,sleep() 函数将被替换为可能需要一定时间来完成的复杂业务逻辑。
使用同步方法,请求将保持对浏览器的响应 3 秒。作为用户,你会请求页面,等待 3 秒,然后页面将显示。页面将包含操作已完成的消息——因此你可以确信过程在 3 秒内正确执行,但你必须等待答案。
使用异步方法,路由机制负责请求;在队列中创建一个作业来调用逻辑,包括调用 sleep(3) 函数来模拟耗时操作,由 ProcessSomething::handle() 函数提供。作业创建在队列中后,将生成并发送给客户端响应。
用户将在几毫秒内收到响应,无需等待任务完成。你知道任务已被推入队列,并且一些工作进程将执行该作业。
要执行异步方法,我们将进行以下操作:
-
安装队列机制,在数据库中创建一个表来存储排队作业。
-
创建实现作业逻辑的类。
-
在作业类的
handle()方法中实现耗时逻辑。 -
创建一个用于以经典同步方式调用
handle()方法的路由。 -
创建另一个用于通过队列机制异步调用作业的路由。
-
分析调用两个路由的结果。
首先,我们将创建一个数据结构,以便队列机制能够存储作业列表。我们可以使用 MySQL 数据库或 Redis。为了简化理解,我们最初将使用基于 MySQL 数据库的队列机制(因为它更基础且更简单)。随后,我们将使用更先进的基于 Redis 的系统。
Redis
Redis 是一个开源数据存储,用于存储数据、缓存值和队列数据/消息。主要在内存中工作,其主要特性之一是速度。
现在,我们将配置 Laravel 应用程序中的基于数据库的队列机制。
安装和配置队列
要在 Laravel 项目目录的终端中创建存储队列作业的数据结构,我们可以执行以下命令:
php artisan queue:table
该命令由 Laravel 提供,无需安装额外的包。
queue:table 命令创建一个用于创建 jobs 表的新迁移文件。
文件创建在 database/migrations/ 目录中。

图 6.1:创建作业表的迁移文件
迁移将创建以下内容:
-
一个名为
'jobs'的新表 -
'id': 用于唯一标识符 -
'queue': 队列名称,有助于通过命令行控制队列 -
'payload': 以 JSON 格式包含信息的数据,这些信息由队列的消费者用于管理和启动任务 -
'attempts': 执行作业的尝试次数 -
'reserved_at': 消费者接管任务的时间戳 -
'available_at': 当任务可被消费时 -
'created_at': 作业在队列中创建的时间
JSON 负载(在负载字段中)的一个示例如下:
{
"displayName" : "App\\Jobs\\ProcessSomething",
"failOnTimeout" : false,
"retryUntil" : null,
"data" : {
"command" :
"O:25:\"App\\Jobs\\ProcessSomething\":0:{}",
"commandName" : "App\\Jobs\\ProcessSomething"
},
"maxExceptions" : null,
"maxTries" : null,
"uuid" : "e8b0c6c7-29ce-4108-a74c-08c70bb679a6",
"timeout" : null,
"backoff" : null,
"job" : "Illuminate\\Queue\\CallQueuedHandler@call"
}
JSON 负载有一些属性:
-
failOnTimeout: 布尔字段,指示作业在超时时是否应该失败 -
retryUntil: 指示作业应该超时的时间戳(整数字段) -
maxExceptions: 在异常后尝试作业的次数(整数字段) -
maxTries: 尝试作业的次数(整数字段) -
uuid: 作业的 UUID(字符串字段) -
timeout: 作业可以运行的秒数(整数字段) -
backoff: 在重试遇到未捕获异常的作业之前等待的秒数 - 可以是一个整数数组,用于跟踪每次尝试的秒数(由于错误,作业可能需要尝试多次) -
job: 队列作业类的名称(字符串字段)
一旦我们使用 php artisan queue:table 命令创建了表的架构定义,我们就可以通过 migrate 命令在数据库中创建 jobs 表。
要在数据库中创建表,你可以启动 migrate 命令:
php artisan migrate
要检查是否已创建正确的表,你可以使用 db:table 命令:
php artisan db:table jobs
现在所有数据结构都已就绪,我们必须创建文件以实现作业的逻辑。
管理队列
在 Laravel 中管理作业时,按照惯例,我们为每个作业有一个类。作业类必须实现 handle() 方法。当作业需要执行时,handle() 方法由框架调用。
要创建管理作业的类,请参阅以下内容:
php artisan make:job ProcessSomething
使用 make:job 命令,创建一个新文件 app/Jobs/ProcessSomething.php,其中包含 ProcessSomething 类和一些准备填充逻辑的方法。主要方法包括构造函数和用于管理作业的方法 handle()。
我们将逻辑实现到handle()方法中。在app/Jobs/ProcessSomething.php文件中,插入以下内容:
public function handle()
{
Log::info('Job processed START');
sleep(3);
Log::info('Job processed END');
}
作为耗时操作的示例,我们将通过sleep()函数暂停handle()方法的执行 3 秒钟。这意味着线程将被挂起 3 秒钟。我们将记录方法执行的开始和结束以跟踪更多信息。您可以在默认配置下在storage/logs/laravel.log文件中找到日志。
经典的同步方法
通常,使用同步方法,如果你调用方法,响应需要 3 秒钟。
在routes/web.php文件中,我们将添加一个新的/time路由用于调度(请求同步执行)工作:
Route::get('/time-consuming-request-sync', function () {
$start = hrtime(true);
ProcessSomething::dispatchSync();
$time = hrtime(true) - $start;
return view('result', [
'title' => url()->current(),
'description' => 'the task has been complete with
dispatchSync()',
'time' => $time,
]);
});
调用静态的dispatchSync()方法允许您同步调用handle()方法(通过队列机制)。这是我们在 PHP 中调用方法时的经典场景。
为了渲染视图,我们必须实现结果视图,一个基本的 blade 模板,用于渲染view()函数中设置的标题、描述和时间。
创建resources/views/result.blade.php blade 文件:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,
initial-scale=1">
<title>Laravel</title>
</head>
<body class="antialiased">
<div class="relative flex items-top justify-center
min-h-screen bg-gray-100 dark:bg-gray-900
sm:items-center py-4 sm:pt-0">
<div class="max-w-6xl mx-auto sm:px-6 lg:px-8">
<div class="mt-8 bg-white dark:bg-gray-800
overflow-hidden shadow sm:rounded-lg">
<div class="grid grid-cols-1">
<div class="p-6">
<div class="flex items-center">
<div class="ml-4 text-lg leading-7
font-semibold">{{ $title}}</div>
</div>
<div class="ml-12">
<div class="mt-2 text-gray-900
dark:text-gray-900 text-2xl">
{{ $description }}
</div>
<div class="mt-2 text-gray-900
dark:text-gray-900 text-2xl">
{{ $time / 1_000_000 }} milliseconds
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
blade 文件将在显示$description和$time参数的页面上显示结果。如果您通过您的网络浏览器在http://localhost:8000/time-consuming-request-sync请求页面,您必须至少等待 3 秒钟,页面才能完全渲染。此图显示时间值略大于 3,000 毫秒:

图 6.2:同步工作执行
这意味着你的工作需要 3 秒钟,Laravel 会等待发送响应,直到工作完成。
这可能没有什么特别之处,因为作为 PHP 开发者,我们已经习惯了管理同步工作。即使我们不管理队列,PHP 引擎也是同步驱动方法和函数的。其他一些语言有函数的异步调用——所以现在,让我们看看如何将执行调度到另一个可以异步执行工作的进程。
异步方法
而不是使用同步方法,我们将通过队列调度工作。
从队列中提取工作,需要启动一个特定的进程,以便消费者接管各种任务。要开始此进程,您可以执行artisan命令:
php artisan queue:work
命令检查jobs表中是否有任何工作,并在工作完成后消费队列,删除该行。
从生产者端,在路由逻辑中,我们可以调用可用的dispatch()函数:
Route::get('/time-consuming-request-async', function () {
$start = hrtime(true);
dispatch(new ProcessSomething());
// OR you can use ProcessSomething::dispatch();
$time = hrtime(true) - $start;
return view('result', [
'title' => url()->current(),
'description' => 'the task has been queued',
'time' => $time,
]);
});
dispatch() 函数需要一个作业类的实例作为参数。dispatch() 函数会将作业实例发送到队列并释放对调用者的控制,无需等待作业的完整执行。最后,响应立即创建,无需等待作业完成,而不是典型的在作业完成后创建响应的行为。
我们只是将 ProcessSomething 类的实例发送到 dispatch() 方法。惯例是,消费者 在处理作业时会执行 handle() 方法。
现在,您可以打开浏览器并调用 URL,http://localhost:8000/time-consuming-request-async:

图 6.3:异步执行
为了允许浏览器接收响应,我们必须确保我们启动了两个命令:
-
php artisan octane:start:用于启动 Octane 服务器,监听端口8000 -
php artisan queue:work:用于启动 消费者 服务,用于执行作业
如果您想查看队列的状态,您可以通过命令行执行:
php artisan queue:monitor default
queue:monitor 命令将显示每个队列的队列状态和队列中的作业:

图 6.4:队列监控工具
如果队列中有待处理的作业,您将在方括号中看到等待作业的数量:

图 6.5:带有一些等待作业的队列
在示例中,队列中有 76 个作业。
如果您使用数据库作为管理队列的后端,我建议通过查询 SQL 的作业表来直接提高您的信心。
您可以使用 artisan db 命令访问数据库:
php artisan db
然后,您可以执行 SQL 查询;例如,您可以计算每个队列有多少行:
select count(id) as jobs_count, queue
from jobs
group by queue;
SQL 命令计算 jobs 表中标识符(计数)的数量,按 queue 字段分组行。

图 6.6:在作业表上执行查询
如果队列中有多个任务,请确保您正在运行消费者:
php artisan queue:work
如果您想要有多个消费者并行执行您的任务,您可以多次启动 queue:work。如果您启动 queue:work 两次,您将有两个消费者从队列中提取作业。
如果你必须管理耗时任务,使用队列不仅是一种实现异步任务管理的方法。它是一种控制并行级别和限制你的架构可以处理或处理的并发耗时任务数量的方式。通过同步管理耗时任务,如果有大量请求,你可能会在 web 服务器上达到大量的并发请求,并且由于高资源使用,你的系统可能会崩溃。
将任务委托给特定的工人意味着你保持了用于处理请求的工人的负载较轻,你可以在专用实例或虚拟机上启动消费者进程。你也可以增加消费者进程的数量。
管理多个队列
你可以使用多个队列——例如,first 和 second。
当你需要将任务分配到队列时,你可以使用 onQueue() 方法:
ProcessSomething::dispatch()->onQueue("first");
你可以控制两个队列:
php artisan queue:monitor first,second
然后,你可以启动第一个队列的消费者:
php artisan queue:work --queue=first
然后对于名为 "second" 的队列:
php artisan queue:work --queue=second
例如,你可以为 "first" 队列启动两个消费者,而为 "second" 队列只启动一个,给 "first" 队列更高的优先级(因为它有两个专门的消费者而不是一个)。为了实现这一点,在不同的 shell 环境中,你可以启动以下命令:
php artisan queue:work --queue=first
php artisan queue:work --queue=first
php artisan queue:work --queue=second
如果你需要清空队列并删除所有挂起的任务,你可以使用 queue:clear 命令:
php artisan queue:clear database --queue=first
在这里,database 是连接的名称(我们现在正在使用数据库;在下一节中,我们将使用另一种类型的连接),我们也可以通过 --queue 参数定义队列。你也可以指定多个队列:
php artisan queue:clear database --queue=first,second
连接是可选的;如果我们没有在命令行上指定 database(连接),则将使用 .env 文件中的 QUEUE_CONNECTION 环境参数。
现在我们已经看到了如何使用数据库作为后端创建和管理队列,让我们尝试配置 Redis 作为后端。
使用 Redis 管理队列并进行监控
使用 database 作为连接对刚开始使用队列并且已经为存储应用程序数据设置了数据库的人来说很方便。为什么使用 Redis 而不是数据库?因为 Redis 在管理队列方面比数据库有更多的优化,并且你可以使用 Laravel Horizon 来监控队列。Laravel Horizon 提供了一个用于监控你的队列及其使用指标的网页仪表板。
在使用 Redis 管理队列的第一步,首先,让我们安装 Redis 服务。
安装 Redis
安装 Redis 意味着你已经向你的堆栈中添加了软件和服务。如果你是 macOS 用户,你可以通过 Homebrew 安装它:
brew install redis
brew services start redis
第一个命令安装软件;第二个命令启动服务。
如果你有一个 GNU/Linux 发行版,你可以使用你的包管理器;Redis 包含在大多数 GNU/Linux 发行版中。
或者,你可以使用 Sail(如前所述,在第三章,配置 Swoole 应用程序服务器,在设置 Laravel Sail部分,在安装 Swoole 时)。
执行以下sail:install命令:
php artisan sail:install
确保你也选择了 Redis 服务(选项编号3)。

图 6.7:Laravel Sail 的 Redis 配置
在需要多个服务(例如,MySQL 和 Redis)的情况下,你可以选择0,3(逗号分隔)。
一旦 Redis 服务启动,我们就可以开始配置队列机制,使用 Redis 作为连接。
配置 Redis
在 Laravel 项目目录中放置的环境配置文件(.env文件),我们必须调整一些参数:
QUEUE_CONNECTION=redis
REDIS_CLIENT=predis
REDIS_HOST=localhost
REDIS_PASSWORD=null
REDIS_PORT=6379
使用QUEUE_CONNECTION,你定义要使用的连接(redis)。
使用REDIS_CLIENT,你可以定义 Laravel 用于连接 Redis 服务的客户端。默认是phpredis(它使用 PECL 模块),或者你可以使用predis,它使用 PHP 包:github.com/predis/predis。phpredis模块是用 C 编写的,所以可能比predis实现(它是纯 PHP)更快。另一方面,predis实现有很多功能,社区和开发团队都非常支持。
如果你想要更改默认队列的名称(通常是“default”),你可以在.env文件中添加REDIS_QUEUE参数:
REDIS_QUEUE=yourdefaultqueue
关于 Laravel 与 Redis 服务之间的连接,如果你不熟悉 Redis,我的建议是先使用predis包,因为它需要添加一个包:
composer require predis/predis
如果你正在安装predis/predis包,你必须将REDIS_CLIENT参数设置为predis:
REDIS_CLIENT=predis
所有这些配置都与.env文件相关。
如果你使用 Predis 包正确完成配置,必须在 Laravel 引导配置中设置别名。为此,在config/app.php文件中,在'aliases'部分,为 Redis 添加一个特定的条目:
'aliases' => Facade::defaultAliases()->merge([
// 'ExampleClass' => App\Example\ExampleClass::class,
'Redis' => Illuminate\Support\Facades\Redis::class,
])->toArray(),
设置别名对于 Laravel 正确访问 Redis 对象非常有用,并帮助 Laravel 正确解析对 Redis 对象的引用,因为我们可能会与 Predis 包提供的Redis对象以及phpredis模块提供的 Redis 对象发生名称冲突。如果你忘记设置此配置,执行 Laravel 应用程序时不会收到错误,但你可能会遇到一些意外的应用程序行为。例如,当你想要清除特定连接的队列时,选定的队列没有被移除。
如果您切换连接,例如,从数据库切换到 Redis,您不需要在您的应用程序代码中做任何更改。Laravel 队列机制提供了一个抽象层,隐藏了每个连接特有的所有不同实现(在底层,管理数据库连接使用的是与 Redis 连接不同的实现)。
如果您使用的是代码(在routes/web.php文件中),请参阅以下内容:
Route::get('/time-consuming-request-async', function () {
$start = hrtime(true);
ProcessSomething::dispatch()->onQueue("first");
$time = hrtime(true) - $start;
return view('result', [
'title' => url()->current(),
'description' => 'the task has been queued',
'time' => $time,
]);
});
我们正在配置的连接上使用"first"队列。
要查看配置的连接,您可以使用about命令:
php artisan about --only=drivers
命令显示您的 Laravel 应用程序用于队列的驱动器信息:

图 6.8:驱动器配置
命令显示对于队列,我们正在使用 Redis 作为队列的后端连接。
现在,打开http://127.0.0.1:8000/time-consuming-request-async页面,您将看到将任务推迟到 Redis 队列比数据库更快。例如,在我的本地机器上,调度方法的时间少于 1 毫秒。在我们的示例中,使用数据库连接,相同的代码需要 7 毫秒:

图 6.9:在 Redis 队列上调度一个作业
通过队列机制,我们提高了应用程序的响应性,使用户能够立即收到关于任务的响应。然后,通过 Redis 连接,我们减少了排队作业所需的时间。
多亏了 Redis 连接,我们还可以使用 Laravel Horizon 来监控队列。
使用 Laravel Horizon 监控队列
我们将把 Horizon 添加到我们的应用程序中。这意味着您可以通过末尾的/horizon路径访问 Horizon 仪表板。
要安装 Laravel Horizon,您必须安装以下包:
composer require laravel/horizon
然后,您需要发布 Horizon 所需的文件:配置文件(config/horizon.php)、资产(在public/vendor/horizon目录中),以及服务提供者(在app/Providers/HorizonServiceProvider.php文件中):
php artisan horizon:install
horizon:install将所有必需的文件复制到正确的目录中。
使用以下命令启动 Laravel Octane:
php artisan octane:start
使用默认的 Horizon 配置,您可以通过http://127.0.0.1:8000/horizon访问:

图 6.10:Laravel Horizon 仪表板
状态指示 Horizon 进程的监督器是否正在运行。为了正确收集所有指标,启动监督器:
php artisan horizon
然后,如果您多次加载http://127.0.0.1:8000/time-consuming-request-async页面,队列中会创建多个作业。启动消费者:
php artisan queue:work --queue=first
您将有一个正在运行的消费者,准备从队列中执行作业。
因此,你有了 Octane、Horizon 和队列工作者的运行。多次加载页面,然后转到 http://127.0.0.1:8000/horizon/dashboard 上的仪表板。你将看到仪表板页面充满了指标:

图 6.11:详细说明指标的 Horizon 仪表板页面
这些指标是通过 Horizon 实现的 /horizon/api/stats 端点检索和计算的。
如果你查看 API 的响应,你可以以编程方式检索在仪表板 UI 中可以看到的相同信息。
备注
在这种场景下,如果你有一个 UI 通过轮询(每 3 秒)调用多个端点(API),如果你使用 Octane 提供的 API,你可以继承使用 Octane 所带来的所有好处。Octane 通过其所有优化降低了延迟。
对于统计 API,JSON 响应具有以下结构:
{
"failedJobs": 0,
"jobsPerMinute": 1,
"pausedMasters": 0,
"periods": {
"failedJobs": 10080,
"recentJobs": 60
},
"processes": 1,
"queueWithMaxRuntime": "first",
"queueWithMaxThroughput": "first",
"recentJobs": 0,
"status": "running",
"wait": {
"redis:default": 0
}
}
在这里,你可以检索以下内容:
-
最近失败的作业数量,
"failedJobs"。 -
自上次快照以来每分钟处理的作业数量,
"jobsPerMinute"。 -
当前暂停的主监督器的数量,
"pausedMasters"。 -
Horizon 管理近期和失败作业的配置(以分钟为单位)。这些值以秒为单位表示,配置在
config/horizon.php文件的trim部分中定义。 -
进程数量,
"processes"。 -
具有最长运行时间的队列名称为
"queueWithMaxRuntime"。 -
具有最高吞吐量的队列名称为
"queueWithMaxThroughput"。 -
近期作业的数量,
"recentJobs"。 -
监督器的状态(通过
php artisan horizon运行的进程),"status"。 -
每个队列的清除时间,
"wait"(以秒为单位)。
Horizon 仪表板是监控所有运行队列的状态和统计信息的便捷方式。通过 Horizon,你不能控制队列;对于管理队列,你可以使用这里解释的命令:
-
queue:monitor:用于监控队列的状态和等待作业的数量 -
queue:clear:用于删除连接或队列中的所有作业 -
queue:flush:用于删除所有失败作业 -
queue:forget:用于删除特定的失败作业(以避免重试失败作业的执行) -
queue:retry:重试之前失败的作业的执行 -
queue:work:处理队列(或通过–queue参数指定的队列)
多亏了所有这些命令,你可以控制队列的状态和健康。
使用 Horizon,你可以监控队列的执行和状态。
现在,我们有一个使用 Redis 作为后端的运行和监控队列的架构。
摘要
使用异步方法,我们可以推迟某些任务的执行,并更快速地响应用户请求。换句话说,工作被排队,稍后处理。这种行为的差异在于经典方法中工作立即执行,但好处是应用程序的 UI 控制始终对用户可用。因此,应用程序的用户体验更流畅,用户在此期间还可以使用您的应用程序做其他事情。除了提高用户体验外,异步方法更具可扩展性,因为您可以以细粒度控制将负责工作的进程。您还可以通过php artisan queue:work命令执行多个工作进程——如果您的硬件架构有更多用于运行后端进程的虚拟机,您可以在多个虚拟机上运行消费者进程。
为了在本章中实现异步架构,我们介绍了队列机制;我们展示了以下内容:
-
如何首先使用数据库作为后端连接,然后在更强大的后端 Redis 上安装和设置队列
-
同步方式和异步方式执行工作的差异
-
使用队列在系统响应性和对用户体验的影响方面的好处
-
安装 Horizon 以监控队列使用情况
在下一章中,我们将看到如何准备应用程序以及如何设置工具以在生产环境中部署 Laravel Octane 应用程序。
第七章:为生产环境配置 Laravel Octane 应用程序
在前面的章节和本书中,我们看到了如何使用 Laravel Octane 并实现一些优化来提高我们应用程序的性能和响应速度。我们主要在本地开发环境中操作。此外,我们在应用层面进行了工作。
本章将讨论生产环境中的系统配置、设置和微调。了解生产环境的特定配置非常有用,尤其是在本地环境的配置通常与生产环境的配置不同,因为可用的设置和服务不同的情况下。
在实践中,我们将涵盖以下内容:
-
典型的生产架构
-
如何优化参考架构
-
哪些具体的 Laravel 操作可以从性能角度带来好处
-
通过Makefile进行部署策略
技术要求
我们假设读者有一个可用的生产环境。通常,这种环境可以位于虚拟机上,例如一个(或多个)Amazon EC2 实例,或者由云 PaaS 解决方案提供的环境,如Platfrom.sh,或者由云服务器/服务提供商提供的环境,如Digital Ocean或Vultr。
我们不会讨论这些环境的特定安装;然而,我们将给出一个典型架构的指示,该架构结合了在(一个或多个)GNU/Linux 类型的虚拟机上使用 web 服务器(如 nginx)的使用。
当前章节中描述的示例的源代码和配置文件在此处可用:github.com/PacktPublishing/High-Performance-with-Laravel-Octane/tree/main/octane-ch07。
介绍生产架构
在本地环境开发场景(如前几章所述),使用php artisan octane:start命令启动 Laravel Octane 是可以的。然而,在要求与本地环境不同的生产环境中,配置不同的架构可能是有帮助的。
通常,在本地开发环境中,你需要自动化和配置,这些配置对于开发新功能很有用,例如自动服务重新加载。相比之下,在生产环境中,配置必须允许达到最佳的性能和可靠性水平。因此,在这个生产场景中,选择不同的、专门针对生产的架构是常见的。
我们将要讨论的生产环境架构设计涉及使用 Web 服务器来最初排序 HTTP 请求。Web 服务器的任务将是区分不需要 PHP 引擎参与的静态资产请求和需要服务器端处理的请求。通常,需要服务器端处理的页面请求可能会随后跟随着对 CSS 文件、JavaScript 文件和图像文件等静态资产的请求。对于这些类型的文件,不需要 PHP 的参与,因此 Web 服务器的任务将是尽可能快地立即提供这些类型的资产。另一方面,对于需要 PHP 执行的请求,Web 服务器将请求转发到 Laravel Octane 提供的服务。

图 7.1:典型的生产架构
要配置生产环境,必须执行几个步骤:
-
将静态资产放置在特定的目录中(我们可以将资产类型组织到多个子文件夹中)。
-
配置 nginx(
nginx.conf文件)以充当代理。 -
配置 nginx 将页面请求转发到 Laravel Octane。
-
将 nginx 设置为监听 HTTPS 协议。
-
将 nginx 与 Laravel Octane 之间的通信设置为标准 HTTP。
-
Laravel 生成的所有 URL 都必须设置为 HTTPS 服务(用于绝对 URL 生成)。
让我们从管理公共资产开始。
管理公共静态资产
如果你查看 Laravel 应用程序的目录结构,你会注意到有一个名为 public 的目录,其目的是包含静态文件。
公共目录还包含由npm run build命令生成的 CSS 和 JS 文件。如果你的应用程序需要 CSS 样式,你可能会在resources/css/app.css文件中设置它(或对于 JavaScript 文件在resources/js目录中)。一旦运行npm run build,CSS 和 JS 文件就会被构建,输出文件存储在public/build目录中。
为了在 HTTP 响应中提供静态文件,没有必要涉及 PHP 引擎。避免涉及 PHP 引擎来提供静态文件可以最小化 CPU 和内存等资源的使用,从而也减少了响应时间。
静态文件通常是具有特定扩展名(如jpg、jpeg、gif、css、png、js、ico或html)的文件。对于静态文件,我们可能需要特定的配置——例如,关闭日志、定义过期日期或添加头信息。如果你需要为资产设置特定的设置,建议在nginx.conf文件(或你的域名对应的文件)中的服务器部分为 nginx 设置特定的配置:
location ~* \.(jpg|jpeg|gif|css|png|js|ico|html)$ {
access_log off;
expires max;
log_not_found off;
add_header X-Debug-Config STATICASSET;
}
对于带有资产扩展名的文件,我添加了一个名为 X-Debug-Config 的头(HTTP 响应中的头),用于调试目的。配置设置为检查 HTTP 响应头中是否存在 STATICASSET 值。
设置 nginx 为代理
在生产环境中,我们将使用 nginx 来响应 HTTP 请求。我们将配置 nginx 作为代理,因为对于 PHP 文件,nginx 将将请求转发到 Laravel Octane 服务。
PHP 文件的 nginx 配置如下所示:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
add_header X-Debug-Config2 FASTCGI;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
这个典型的 nginx 配置用于使用 PHP,通过 FastCGI 使用 FPM 模块。如 第一章 中所述,理解 Laravel Web 应用程序架构,nginx 无法运行 PHP 脚本。为了做到这一点,nginx 可以配置为将请求转发到 FPM 模块。FPM 模块将运行 PHP 脚本,并将响应发送回 nginx。所有对 PHP 文件的请求都转发到 FPM 套接字通信通道(fastcgi_pass)。
如果你使用 Laravel Octane,你不需要使用 FastCGI 选项来执行 PHP 脚本,因为 PHP 脚本将通过应用程序服务器(Swoole 或 RoadRunner)由 Laravel Octane 服务执行。
如果你拥有 Laravel Octane,你可以通过 php artisan octane:start 命令启动 Octane 服务,然后指示 nginx 作为代理,将所有 PHP 请求转发到 Laravel Octane 服务。
要将 nginx 设置为代理,你可以在你的 nginx 配置文件中设置此配置:
location /index.php {
try_files /not_exists @octane;
}
location / {
try_files $uri $uri/ @octane;
}
location @octane {
add_header X-Debug-Config2 POWEREDBYOCTANE;
set $suffix "";
if ($uri = /index.php) {
set $suffix ?$query_string;
}
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header SERVER_PORT $server_port;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For
$proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_pass http://127.0.0.1:8000$suffix;
}
try_files 行对于分割 nginx 在如何管理静态资源(现有文件)和动态资源(不存在文件)方面的行为非常重要。
如果请求是针对 /test.png 并且文件存在于公共目录中(由于根指令),文件将由 nginx 加载并服务。
如果请求是针对 /test,它将落入 try_files 标记为 @octane 的最新场景。
然后,有一个专门的部分来管理所有标记为 @octane 的请求。所有这些请求都通过 proxy_pass 指令发送到 Laravel Octane。
如你所见,所有 FastCGI 指令都被注释掉了,或者被移除了。现在,我们正在使用 proxy_pass 指令。proxy_pass 指令指向 Laravel Octane 服务运行的 IP 地址和端口。
要使代理配置生效,你必须设置一些额外的参数,例如 proxy_http_version 1.1; 以设置协议版本和其他用于控制头部的选项。
一旦你编辑了你的 nginx 配置文件,你必须重新启动 nginx 进程以应用你的更改。在重新启动 nginx 之前,我建议检查更改后的配置文件的语法。
使用 nginx -t 命令可以显示你的新配置是否存在任何问题:
# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
一旦收到成功消息,您可以安全地重新启动 nginx。如何重新启动服务取决于您的发行版。在 Ubuntu 的情况下,您可以使用service命令:
service nginx restart
通常,如果您的 Linux 发行版不提供service命令,您可以使用systemctl命令:
sudo systemctl restart nginx
使用sudo命令是因为您需要具有 root 权限。
重新加载 nginx 后,你必须启动artisan命令来启动 Laravel Octane。
启动 Octane
您可以手动执行octane:start命令进行快速配置测试。然后,我们将看到如何在系统启动时自动启动 Octane。
要启动 Octane,您应该使用octane:start命令:
php artisan octane:start --server=swoole
此命令启动 Octane 并在本地主机上接受连接。
假设您需要在不同的机器上执行 Octane(一台机器用于运行 nginx,另一台机器用于运行 Laravel Octane):您应该允许 Octane 通过--host参数接受所有传入请求:
php artisan octane:start –host=0.0.0.0 –server=swoole
0.0.0.0地址意味着 Octane 将接受来自所有 IP 地址的传入请求(而不仅仅是本地地址)。
通过 Supervisor 启动 Octane
如果您需要在系统启动时启动和监控进程,我建议使用 Supervisor 软件。
有一个名为 Supervisor 的优秀的开源软件,您可以通过以下命令通过 GNU/Linux 包管理器进行安装:
apt-get install supervisor
一旦安装了 Supervisor,您可以为 Laravel Octane 设置启动脚本文件。
Supervisor
如果您对 Supervisor 对其他项目/命令(不仅仅是 Laravel Octane)感兴趣,您可以在官方网站上获取更多信息:supervisord.org/。
由 supervisor 管理的启动脚本存储在conf.d目录中,该目录位于/etc/supervisor/目录内。
创建一个新文件,/etc/supervisor/conf.d/laravel-octane.conf,并使用以下配置:
[program:laravel-octane]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/mydomain/htdocs/artisan octane:sta– --server=swoo– --max-requests=10– --workers– --task-workers=– --port=8000
autostart=true
autorestart=true
user=deploy
redirect_stderr=true
stdout_logfile=/var/www/mydomain/htdocs/storage/logs/laravel-octane-worker.log
在这里,您需要设置一些指令:
-
process_name:这是用于标记进程的名称 -
command:这是使用octane:start启动 Octane 服务的命令行 -
autostart和autorestart:这是设置如何以及何时启动进程(自动启动)
用于启动进程的用户(我使用的是特定的一个,例如deploy)通常是www-data,按照惯例,这是运行 Web 服务器进程(nginx 或 Apache)所使用的用户。
-
redirect_stderr:如果错误消息需要重定向到日志,则使用此选项。 -
stdout_logfile:这是用于日志的文件名。请确保默认情况下为 Octane 管理的每个请求创建一行。
如果您对更复杂的配置感兴趣,Supervisor 支持许多参数。所有参数都在此处有文档说明:supervisord.org/configuration.html。
一旦你设置了配置文件,你可以更新 Supervisor 配置,添加 Laravel Octane 配置:
supervisorctl update
然后,你重新启动 Supervisor:
supervisorctl restart all
或者,你可以专门重新启动 Octane 服务:
supervisorctl restart laravel-octane:laravel-octane_00
这样,Laravel Octane 将根据命令行上定义的参数(Supervisor 配置中的命令指令)启动,并且在重启或崩溃的情况下重新启动 Laravel Octane。
现在,Laravel Octane 已经自动启动。如果你更改应用程序的源代码或部署 Laravel 应用程序的新版本,你必须重新加载工作进程,以便将所有更改应用到所有工作进程中。
重新加载工作进程
每次你对应用程序逻辑进行一些更改时,你必须使用以下命令重新启动应用程序:
php artisan octane:reload
octane:reload 命令重新启动由 Octane 实例化的工作进程,并通过这种方式重新加载代码。

图 7.2:使用 php artisan octane:reload 重新加载工作进程
如果你想了解 Laravel Octane 服务的状态(运行或不运行),你可以使用 octane:status 命令:
php artisan octane:status
如果一切正常,octane:status 会显示 Octane 服务器正在运行 的消息。否则,它会显示 Octane 服务器 未运行:

图 7.3:检查 Laravel Octane 的状态
监听事件
Laravel Octane 内部定义了一些事件,用于通知和管理发生某些情况时的情况,例如工作进程启动、收到请求等。在一个框架中,如果你可以产生一些事件,作为开发者,你需要一个机制来监听事件发生时的情况。Laravel 框架为开发者提供了一个定义和创建事件以及监听(监听器)的机制。Laravel 中的监听器可以执行开发者定义的函数。
因此,在这种情况下,使用 Laravel 提供的事件监听机制,你可以监听一些特定的 Laravel Octane 事件。
查看 Laravel Octane 配置文件 (config/octane.php) 中的 listener 部分,你会看到定义的事件如下:WorkerStarting、RequestReceived、RequestHandled、RequestTerminated、TaskReceived、TaskTerminated、TickReceived、TickTerminated、OperationTerminated、WorkerErrorOccurred 和 WorkerStopping。
例如,如果你想实现 RequestReceived 事件的监听器,你可以使用 make:listener 命令创建监听器类:
php artisan make:listener RequestReceivedNotification --event=RequestReceived
make:listener 命令基于 RequestReceived 事件创建 RequestReceivedNotification 类文件。
该文件在 app/Listeners/RequestReceivedNotification.php 中创建,包含构造函数和 handle() 方法,因此你可以在 handle() 方法中添加你的逻辑来跟踪一些活动;例如,我将记录请求来源的 IP 地址:
<?php
namespace App\Listeners;
use Illuminate\Support\Facades\Log;
use Laravel\Octane\Events\RequestReceived;
class RequestReceivedNotification
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param \Laravel\Octane\Events\RequestReceived;
$event
* @return void
*/
public function handle(RequestReceived $event)
{
Log::info('Request Received by
'.$event->request->ip());
}
}
要启用监听器,你必须将类添加到 config/octane.php 文件中:
RequestReceived::class => [
...Octane::prepareApplicationForNextOperation(),
...Octane::prepareApplicationForNextRequest(),
// Add the RequestReceivedNotification class
RequestReceivedNotification::class
//
],
以同样的方式,你也可以添加你想要跟踪的其他事件。
现在我们已经为生产环境配置了 Web 服务器,我们可以浏览 Laravel 的生产环境配置。
为 Laravel 应用程序准备生产环境
现在,我们将专注于 Laravel 的特定生产环境配置。我们将了解如何安装生产 PHP/Laravel 软件包,避免安装开发和调试软件包。我们将了解如何优化缓存配置、路由设置和视图优化。最后,我们将了解如何禁止调试选项。调试选项在开发中非常有用,但可能会减慢 Laravel 应用程序在生产环境中的执行速度。
安装生产环境所需的软件包
在 composer.json 文件中,列出了两种类型的软件包:require 定义了在生产环境中运行应用程序所需的软件包,而 require-dev 定义了在开发环境中运行应用程序所需的软件包。
默认情况下,composer 会安装两个列表中的所有软件包。
如果你只想安装 require 列表中的软件包(生产环境),在安装生产环境软件包时可以使用 –-no-dev 选项:
composer install --optimize-autoloader --no-dev
除了 --no-dev 选项外,我们还使用 optimize-autoloader。该选项生成一个数据结构(索引),用于映射类名和要加载的文件名。
optimize-autoloader 选项减少了引导时间。
缓存配置、路由和视图
为了快速加载一些关键组件,如配置文件、路由配置和视图 blade 模板,你可以使用缓存命令:
php artisan config:cache
php artisan route:cache
php artisan view:cache
每次你在生产环境中更改 Laravel 应用程序中的某些文件时,都必须执行这些命令。

图 7.4:缓存配置、路由和视图
禁用调试模式
在开发 Laravel 应用程序时,配置通常设置为调试模式。控制调试模式的参数存储在 .env 文件中,参数如下:
APP_DEBUG=true
要检查应用程序的状态,你可以使用以下命令:
php artisan about
about 命令显示有关 调试模式 的信息:

图 7.5:显示调试模式状态的 about 命令
要更改设置,你可以将 APP_DEBUG 参数的值更改为 false:
APP_DEBUG=false
将APP_DEBUG配置设置为false允许您跳过代码中所有用于调试目的的配置(收集和显示详细信息)。
由于您更改了配置并且配置被缓存,建议清除配置以查看应用程序中的更改:
php artisan config:clear
您还应该减少日志消息的数量。日志消息对于调试目的非常有用,但跟踪日志可能是一项昂贵的操作,可能会减慢应用程序的运行速度。
例如,在.env配置文件中,通常可以找到以下内容:
LOG_LEVEL=debug
调试的级别,从最详细的(带有更多日志)到仅跟踪错误,如下所示:debug、info、notice、warning、error、critical、alert和emergency。我的建议是在生产环境中将日志级别(LOG_LEVEL)更改为error,以便仅跟踪关键信息,因为减少日志消息的数量可以使应用程序运行更快,并可以减少所需的存储量。
一般建议为生产环境设置特定的配置,因此建议创建一个文件,.env.prod,在其中可以存储您的生产配置。然后,在每个部署时将.env.prod文件复制到生产环境中的.env文件。
部署方法
您可以从众多存在的部署策略中选择一个。
当选择部署策略时,我推荐的规则是避免在生产环境中安装用于构建阶段的宝贵工具。因为目标是限制生产环境的责任,仅限于交付准备就绪的部署资产,执行构建过程的责任不应由生产环境承担。这就是为什么可以在生产环境中避免安装构建工具。
例如,我建议避免在生产环境中执行 JavaScript 和 CSS 的构建、运行测试或“linting”过程,或执行静态代码分析。
换句话说,有必要直接将优化后的代码和现成的配置转移到生产服务器。
这意味着如果应该有旨在为生产准备文件和配置的构建操作,这些操作应在专用环境或 CI/CD 运行器中执行。
生产环境中的构建机制和文件传输命令可能很复杂。因此,建议在脚本工具中正式化此命令列表。
我使用Makefile是因为这允许我定义要执行的各个步骤,但更重要的是,确定步骤之间的依赖关系。
通常,我会定义一个编译资源的步骤,一个测试步骤,一个运行phpcs(代码检查)的步骤,一个执行静态代码分析的步骤,以及一个将文件传输到生产环境的步骤。如果需要,还有直接在生产服务器上执行命令的特定步骤。这些细粒度的步骤数量使我能够保持每个步骤的简单性。
有几种复制和将文件传输到生产环境的方法;建议找到最适合你的方法;通常,我使用安全外壳(SSH)协议和工具。
然而,再次强调,最重要的是理解,无论你使用什么方式传输文件或使用什么工具,你应该传输准备由生产环境服务的文件和配置。
Makefile
Makefile是make工具用来执行命令的文件。其格式相当简单;每个命令组被分组到步骤中。当你通过make命令启动文件时,你可以指定要执行的步骤。
在Makefile中,所有命令都被列出,要执行的命令需要一个参数,例如SSH_USER或WEB_DIR。我的建议是在外部Makefile中定义这些参数。然后,你可以忽略(在 Git 仓库中)用于参数的Makefile,并提交包含步骤的Makefile。
在你的 Laravel 项目根目录中,创建一个名为Makefile.param的文件:
SSH_ALIAS=some.vm
SSH_USER=deploy
WEB_DIR=/var/www/mydomain/htdocs
WEB_USER=www-data
WEB_GROUP=www-data
# DRY_RUN=--dry-run
DRY_RUN=
参数的含义相当直观;你可以定义 SSH 别名和 SSH 用户,通过 SSH 访问远程机器,然后你可以指定存储你的 Laravel 应用程序的远程目录(在服务器上,WEB_DIR)。然后,你可以设置服务器上用于运行进程的用户(WEB_USER和WEB_GROUP)。然后,通过DRY_RUN,你可以查看(并复制)要复制到服务器上的文件。
对于在服务器上复制文件,我们将使用rsync。rsync是一个仅复制已更改文件的优秀工具。
要创建一个Makefile,创建一个名为Makefile.deploy的文件,并填充以下内容:
.PHONY: help all remotedu rsynca copyenvprod fixgroupuser newdeploy migratestatus migrate migrateseed migraterefresh buildfrontend optimize composerinstallnodev restartworkers octanestatus installdevdeps deploy
include Makefile.param.prod
all: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) |
sort | awk 'BEGIN {FS = ":.*?## "};
{printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
remotedu: ## Execute DU command in htdocs dir, just for diagnostic purpose
ssh ${SSH_ALIAS} "cd ${WEB_DIR}; du -h"
rsynca: ## execute Rsync from current dir and remote htdocs ${WEB_DIR}
rsync ${DRY_RUN} -rlcgoDvzi -e "ssh" --delete .
${SSH_ALIAS}:${WEB_DIR} --exclude-from
'exclude-list.txt'
copyenvprod:
scp .env.prod ${SSH_ALIAS}:${WEB_DIR}/.env
fixgroupuser: ## Add the right group(www) to the deploy user (ssh user)
ssh -t ${SSH_ALIAS} "sudo usermod -a -G ${WEB_GROUP}
${SSH_USER}"
fixownership: ## fix the ownership for user ${WEB_USER} into ${WEB_DIR}/storage
ssh -t ${SSH_ALIAS} "sudo chown -R
${WEB_USER}:${WEB_GROUP} ${WEB_DIR}/storage; ls -lao
${WEB_DIR}/storage"
newdeploy: buildfrontend rsynca copyenvprod fixgroupuser fixownership migrate ##first deploy
migratestatus: ## list the migration status
ssh ${SSH_ALIAS} "cd ${WEB_DIR}; php artisan
migrate:status --env=prod"
migrate: ## Execute migrate command for DB schema
ssh ${SSH_ALIAS} "cd ${WEB_DIR}; php artisan migrate
--env=prod"
migrateseed: ## Execute migrate command for DB schema
ssh ${SSH_ALIAS} "cd ${WEB_DIR}; php artisan
migrate:refresh --seed --env=prod"
migraterefresh: ## Execute migrate command for DB schema
ssh ${SSH_ALIAS} "cd ${WEB_DIR}; php artisan
migrate:refresh"
buildfrontend: ## execute npm task to compile frontend assets (js and css...)
npm run build
optimize: ## Optimize application in production
ssh ${SSH_ALIAS} "cd ${WEB_DIR}; php artisan
config:cache; php artisan route:cache; php artisan
view:cache"
composerinstallnodev:
composer install --optimize-autoloader --no-dev
restartworkers:
ssh ${SSH_ALIAS} "cd ${WEB_DIR}; php artisan octane:reload"
octanestatus:
ssh ${SSH_ALIAS} "cd ${WEB_DIR}; php artisan octane:status"
installdevdeps:
composer install
npm run dev
php artisan config:clear
deploy: buildfrontend composerinstallnodev rsynca copyenvprod migrate restartworkers installdevdeps
最后一行是deploy步骤。deploy步骤不包含要执行的命令,而是一系列需要依次调用的步骤。
执行的第一步将是buildfrontend。这一步包括npm run build命令。该命令将构建前端资源。
deploy步骤中包含的下一步是composerinstallnodev。composerinstallnodev执行的命令将为生产环境安装包(跳过开发包):
composer install --optimize-autoloader --no-dev
然后,是复制服务器上文件的步骤:rsynca。这一步包括一个命令:
rsync ${DRY_RUN} -rlcgovzi -e "ssh" --delete . ${SSH_ALIAS}:${WEB_DIR} --exclude-from 'exclude-list.txt'
在命令中,使用了一些变量(用从Makefile.param.prod文件加载的值替换),这些变量。
由于包含以下指令,Makefile.param.prod文件在Makefile的开始处被加载:
include Makefile.param.prod
rsync 命令使用一系列选项:
-
-r:递归到目录中。 -
-l:将符号链接作为符号链接复制。 -
-c:仅复制已更改的文件。通过比较源文件和目标文件的校验和来检测更改,使用-c选项。 -
-g:在复制过程中保留组(所有者)。 -
-o:在复制过程中保留用户(所有者)。 -
-v:详细输出。 -
-z:在网络传输期间压缩文件。 -
-i:显示摘要。
--exclude-from 'exclude-list.txt' 选项将 exclude-list.txt 文件中列出的所有文件从复制中排除。
exclude 文件包含以下内容:
.git
.sass-cache/
Makefile*
exclude-list.txt
nohup.out
composer.phar
node_modules
.env*
.idea
.editorconfig
.babelrc
.gitattributes
logs
*.log
npm-debug.log*
node_modules
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
storage/logs/*
storage/app/*
storage/testing/*
exports/*
app/config/prod/*
app/config/stage/*
resources/assets
public/files
public/chunks
public/storage
bootstrap/cache/config.php
.phpunit.result.cache
exclude-list.txt 文件列出了在生产环境中无用的所有文件和目录。通常,排除的文件/目录是包目录、存储目录、缓存文件等。
随意添加您想要排除并不要添加到生产环境中的应用程序文件。
copyenvprod 步骤将本地的 .env.prod(您可以在其中存储生产环境的特定配置)复制到远程的 .env:
scp .env.prod ${SSH_ALIAS}:${WEB_DIR}/.env
scp 命令允许您通过 SSH 协议复制文件。
记得将 .env.prod 文件包含在 .gitignore 文件中(以排除它从 git 提交中)。
migrate 步骤在远程服务器上执行迁移(归功于 SSH 命令):
ssh ${SSH_ALIAS} "cd ${WEB_DIR}; php artisan migrate --env=prod"
然后,执行 optimize 步骤。optimize 步骤将在目标机器上启动用于路由、视图和配置的缓存 Laravel 命令:
ssh ${SSH_ALIAS} "cd ${WEB_DIR}; php artisan config:cache; php artisan route:cache; php artisan view:cache"
然后,如果你记得的话,每次你在生产环境中更改文件时,都应该重新启动工作进程以将更改应用到正在运行中的 Laravel Octane 应用程序,因此 restartworkers 步骤将会启动:
ssh ${SSH_ALIAS} "cd ${WEB_DIR}; php artisan octane:reload"
从本地开发环境执行 Makefile 不建议。通常,您可以从 CI/CD 管道启动 Makefile。我们从一个专门的环境(CI/CD)执行 Makefile 以实现关注点的分离;本地环境用于开发,CI/CD 环境用于程序化构建和执行代码质量工具,生产环境用于交付资源和应用程序。
无论如何,如果您没有使用 CI/CD 管道,并且是从本地源代码副本启动 Makefile,您必须恢复开发包(对于生产,部署 composerinstallnodev 步骤,这将仅安装生产包)。为了恢复开发包,存在 installdevdeps 步骤,它将执行标准的 composer install 命令然后清除配置:
composer install
php artisan config:clear
如您所见,Makefile 的配置很简单。因为 Makefile 对缩进有严格的规定(缩进用于理解一个步骤的开始和结束位置),唯一的建议是使用制表符进行行缩进。一旦 Makefile 准备好,就可以执行它。
要运行名为Makefile.deploy的Makefile,您可以运行以下命令:
make -f Makefile.deploy deploy
make命令接受要执行的步骤名称作为输入。可选地,您也可以通过-f参数指定要解析的Makefile。
使用make deploy命令,应用程序已完全部署到服务器上。Makefile还包括一些用于环境微调的命令,例如修复某些目录的权限、清除缓存等。
摘要
通过这本书,我们探索了一些创建可靠且快速 Laravel 应用程序的方法。我们使用了 Laravel Octane 提供的功能,并与 Swoole 和 RoadRunner 集成。
我们涉及了一些 Swoole 的特殊功能,特别是针对缓存、并发任务执行和计划任务执行。
我们探索了一些实际示例,并采取了不同的方法来设计应用程序,将任务委托给外部执行器(通过队列)。
我们通过应用多种技术逐步提高了应用程序的性能。
在本章中,我们解释了架构、生产中使用的工具(nginx)的配置,以及将 Laravel Octane 应用程序部署到生产环境的脚本实现。
希望您喜欢阅读这本书。


浙公网安备 33010602011771号