blog-post

Manticore Buddy: challenges and solutions

嘿,大家好!🤗 我们希望你们已经查看了我们的 Buddy Intro ,并对它的工作原理有了良好的理解。我们想分享我们开发它的旅程和经历,以及我们面临的挑战。

Manticore Software,我们遇到了两个主要挑战:

  • 在不修改 C++ 的情况下扩展 Manticore Search 的非性能关键特性。
  • 使贡献于增强和新特性实现变得更容易。

我们决心找到解决方案。那么,让我们深入探讨我们开发 Buddy 的旅程以及我们面临的问题。准备好了吗?我们开始吧!

旅程的开始

我们的旅程始于仔细审视这个问题。尽管 Manticore Search 是一款出色的搜索数据库产品,但由于代码库是用 C++ 编写的,我们在以所需速度发布新特性时遇到了困难。编写 C++ 代码需要与数据结构、字节进行低级交互,深入了解机器的工作原理、编译、调试以及找到最佳的编写方法,这需要大量时间,但结果是程序执行更快。这大大延迟了开发过程。尽管 C++ 是性能的绝佳选择,但开发需要时间。我们希望快速推进,持续发布更多特性。

我们想出了创建一个伴随我们主要 searchd 进程的想法,它可以处理来自 Manticore Search 的失败查询并将结果返回给原始客户端。我们很快决定使用哪种语言,并选择了 PHP,原因有几个:

  • 大部分核心团队对它很熟悉,因此花费的时间会更少。
  • PHP 很快,尤其是最新版本(8+),即使我们不需要 Buddy 的高性能执行。它比 PythonJavaScript 快,因此非常适合我们的需求。
  • PHP 不仅快速,而且简单,降低了对未来生态系统贡献所需的专业知识水平。

这就是我们选择 PHP 并开始实现基本代码以了解我们后续需要什么的原因。

我们仍然使用 C++ 来开发速度关键特性。C++ 适合需要速度的任务。对于不需要太多速度的任务,Buddy 是最佳选择。

这就是 Buddy 的创建过程。为了在 Manticore Search 的 C++ 端实现这一点,我们实现了一个单独的循环和 searchd 守护进程与 Buddy PHP 进程之间的通信,使用 CURL 扩展。我们开发了自己的内部协议,这是一个简单的 JSON,用于将查询路由到 Buddy;它处理这些查询并向我们发送适当的响应,以便返回给原始客户端。

实现通信协议

在开始一个新项目时,保持灵活性而不进行过度规划是很重要的。在我们的案例中,我们开始时使用 PHP 中的 sockets 扩展进行基本的通信实现。虽然它工作良好,但并不具备可扩展性。我们的目标是将 Manticore SearchBuddy 连接,这个简单的实现使我们能够验证这个想法。

我们没有重新发明轮子,而是研究了使系统更具可扩展性和非阻塞性的选项。我们最初考虑了 OpenSwoole,但由于许可证问题,我们无法使用它。然后我们找到了 ReactPHP,它有合适的许可证。因此,我们决定使用 ReactPHP

ReactPHP 是一个普通的 PHP 框架,允许我们以异步模式运行 TCP 服务器。

这个选择对我们来说效果很好,因为它允许我们同时处理多个请求,并轻松扩展系统。

接下来,我们重写了我们的简单 Buddy MVP,并创建了一个核心结构,使将来添加新 SQL 命令的处理变得容易。过程如下:

  • Manticore Search 从用户那里接收 SQL 查询并尝试处理它。
  • 如果 Manticore Search 能够处理查询,它会将响应返回给客户端,而不涉及 Buddy
  • 如果 Manticore Search 无法处理查询,它会将包含输入查询和任何错误的所有信息的特殊结构发送给 Buddy
  • Buddy 解析该结构并检查是否有处理程序。如果没有,它会返回 Manticore Search 将发送给客户端的相同错误。
  • 如果一切正常,并且我们有处理查询的实现,我们将过程分为两个步骤:准备包含所需数据的请求和使用处理程序逻辑处理它。请求是一个简单的结构,表示一个具有预定义变量和从输入 SQL 查询解析的输入参数的类。如果出现任何问题,它可能会失败并向客户端返回错误。
  • 然后处理程序接收请求,进行必要的工作,并将最终结果返回给 HTTP 请求。

这个系统易于维护,简单,并且可以轻松扩展新功能。然而,这种方法存在一个问题。如果我们在 Buddy 中有一些重的任务或需要等待,它可能会减慢并发请求。这是因为,尽管异步不是并行的,PHP 仍然是阻塞代码,而 ReactPHP 使用 fibers 来模拟异步方法。我们将在下一节中更详细地讨论这个问题。

PHP 中的异步问题和并发的扩展

为了处理重负载,我们的 Manticore Search 团队需要一个能够处理比 ReactPHP 更多请求的解决方案。虽然 ReactPHP 在实现异步 HTTP 服务器和处理一些并发方面有效,但它的可扩展性不足。经过快速搜索,我们选择使用 parallel 扩展,而不是 pthreads,因为它的可维护性和可靠性。

但什么是 parallel?它涉及并行化,创建独立的 Runtimes,代表并行运行的线程。这些线程可以通过 Parallel 提供的通道进行通信,使我们能够将数据从主 ReactPHP 循环进程发送到另一个 Runtime 中的并行和分离线程,而无需重新发明轮子。

这种方法是我们处理高并发水平并保持响应时间低的灵丹妙药。为了实现这一点,我们在 Handler 级别实现了一个 Task 组件,以在并行线程中运行任务。这样,主进程不会阻塞,使我们能够轻松处理许多并发请求。

最初,我们在每个请求上创建一个 Runtime,并在执行结束时销毁它。然而,这种方法在接收到许多请求时导致性能下降。为了处理更多请求,我们在第一次启动时准备了 Runtimes,并以轮询方式使用它们。这样,我们可以限制创建的最大线程数,并且不超过总核心数,这也会影响性能。

虽然我们轻松解决了性能和并发问题,但我们面临另一个挑战:部署。并非所有操作系统都支持最新的 PHP 8 版本,并且一些不常见的扩展未包含在默认安装中。但我们找到了一个解决方案,你也可以。

向 manticore-executor 打个招呼

我们进行了研究,以找到一种无痛的方式将新工具交付给客户,并发现了一种很好的方法 - 将 <strong>PHP</strong> 和 Buddy 编译成一个静态二进制文件 。这涉及将 PHP 注入其源代码并创建一个可以运行的二进制文件。然而,我们遇到了一个障碍,因为我们想混合不同的许可证 - PHP 3.01GPL 2.0 - 这是不可行的。因此,我们选择预构建 PHP,静态链接,并将其命名为 manticore-executor。

不幸的是,这个过程并不简单。我们尝试在 Ubuntu 上构建它,但遇到了一个问题 - 我们需要 OpenSSL 来建立与外部域的安全连接。然而,当使用动态 GCC 时,我们无法静态链接 OpenSSL。

我们为什么使用 GCC?这是编译 PHP 及其扩展所必需的。问题在于我们需要一个静态构建的 GCC 来进行静态链接,这并不简单,并且需要大量工作。因此,我们寻找替代方案。

幸运的是,我们发现了 MUSLAlpine,这使我们能够轻松构建一个包含所有所需扩展和库的完全静态版本的 PHP!此外,它可以在任何 Linux 发行版上运行。

Alpine Linux 是编译 C 程序的绝佳选择,因为它体积小、轻量级,适合资源有限的系统,如嵌入式设备或容器。此外,Alpine Linux 是安全的。它采用了强化的内核和少量软件包,限制了攻击面,使其对安全威胁的脆弱性降低。这对于可能容易受到安全漏洞影响的 C 程序尤其重要。

此外,Alpine Linux 采用 MUSL libc 作为其标准 C 库,这是一个轻量级且高效的 C 库,生成的代码比其他 C 库更快、更高效。

因此,我们利用它并设置了操作,以使用 Alpine 镜像并在 Docker 中构建它。这种方法的美妙之处在于,它还使我们更容易为 ARM 构建,因为 Docker 具有 buildx 命令,允许我们在准备构建的模式中利用 QEMU,并在同一台机器上为 AMD 和 ARM 架构完成相同的构建流程!查看我们的构建流程 这里

Github Actions 自动化了所有支持的操作系统的包构建和部署。对于用户来说,安装很简单:只需运行 apt-get install manticore-executoryum install manticore-executor,你就会拥有一个准备好使用的 PHP 版本,所有必要的包都预先安装,以运行任何 Manticore 交付的 PHP 项目。简单!

我们如何交付源代码

在 Manticore Search,我们面临着将由多个源代码文件组成的 PHP 应用程序提供给用户的挑战。我们有许多文件分散在不同的文件夹中,并且必须使用 Composer 安装的依赖项,使安装过程变得复杂。

正如你所记得的,我们开发了一个自定义的 PHP 版本,称为 manticore-executor ,可以轻松地从仓库中安装。然而,这仍然没有解决将整个 PHP 应用程序提供给用户的问题。

我们在 PHAR 中找到了一个解决方案,它允许我们构建一个可以作为包添加到仓库的单个文件。这简化了安装过程。然而,确保所有依赖项正确包含在最终的 PHAR 存档中是棘手的。为了解决这个问题,我们创建并分离了一个外部构建系统,我们也在我们的 manticore-backup 工具中使用它。

为了使包可执行,我们决定使用 Bash 和 Shebang 脚本与我们的 Manticore-Executor 包一起使用。该脚本检查系统临时文件夹中修改的 PHAR 的日期,并将 PHAR 数据提取到那里,允许多次启动,保持性能并及时更新新安装的版本。有关我们如何实现这一点的更多信息,你可以参考我们在 GitHub 上的 phar_builder 项目。

经验教训

  1. 在不确定项目未来成功时,从简单、基本的解决方案开始,不要使用软件设计模式。优先进行验证,然后重构并迭代更新。
  2. PHP 中并发可能是一个挑战,但使用线程和异步框架可以帮助实现高吞吐量。为了获得最佳性能,建议同时使用这两者。为线程预分配运行时间可以帮助达到期望的性能。
  3. 简化用户的运输过程。减少所需的指令数量。在我们的案例中,一个 PHAR 存档和一个包含所有扩展的二进制文件解决了这个问题。
  4. 使用最新版本的 PHP 或其他工具,以保持对最新发展的掌握并确保您的数据安全。过时的软件可能容易受到黑客攻击和安全漏洞的影响。升级提供了更好的性能、最新的功能和高效的编码过程。
  5. 寻找可以解决您问题的包,并检查它们的依赖关系。选择依赖关系最少的包,以避免依赖地狱。使用小型包作为构建块,而不是创建自定义解决方案。

最终结果

Buddy 的开发过程中,我们面临了许多挑战,但我们兴奋地克服了这些挑战。该工具完全用 PHP 编写,并作为操作系统包发布,使用户安装变得非常简单,同时也便于我们维护和自动化构建,得益于 GitHub Actions。虽然仍有改进的空间以使工具更简单,但我们的故事展示了如何构建一个易于维护和易于安装的工具,全部依靠 PHP 的力量。

我们希望您喜欢阅读我们关于发布 Manticore 6.0.0 的旅程。请继续关注我们的下一篇文章,我们将探讨 Buddy 中新的可插拔设计及其易于贡献的生态系统,这将使整个社区受益。这真是一个激动人心的时刻,我们迫不及待想与您分享更多。

安装Manticore Search

安装Manticore Search