# Manticore Buddy: challenges and solutions

嘿，大家好！🤗 我们希望你们已经查看了我们的 [Buddy Intro](https://manticoresearch.com/blog/manticoresearch-buddy-intro/) 并且对它的运作方式有良好的理解。我们想分享开发它的旅程和经验，以及我们遇到的挑战。

在 **Manticore Software**，我们遇到了两个主要挑战：

- 在不修改 C++ 的情况下，为 **Manticore Search** 扩展非性能关键功能。
- 让增强功能和新功能实现的贡献更容易。

我们决心找到解决方案。所以，让我们深入探讨开发 **Buddy** 的旅程以及我们遇到的问题。准备好了吗？我们开始吧！

## 旅程的开始

我们的旅程始于仔细审视问题。尽管 **Manticore Search** 是一款卓越的搜索数据库产品，但由于代码库是用 **C++** 编写的，我们在以期望的速度发布新功能时遇到了困难。编写 **C++** 代码需要与数据结构、字节进行低级交互，需要深入了解机器的工作原理，编译、调试，以及找到最佳的编写方式，这需要大量时间，但能带来更快的程序执行速度。这大大延迟了开发过程。虽然 **C++** 是性能方面的绝佳选择，但开发耗时较长。我们希望快速推进，发布更多功能，并且保持一致性。

我们提出了一个想法，为我们的主要 **searchd** 进程创建一个配套程序，它可以处理来自 **Manticore Search** 的失败查询，并将结果返回给原始客户端。我们没有花很长时间决定使用哪种语言，最终选择了 **PHP**，原因有以下几点：

- 核心团队的大多数人熟悉它，因此让它运行起来所需的时间更少。
- **PHP** 很快，尤其是最新版本（8+），即使我们不需要 Buddy 的高性能执行，它也比 **Python** 或 **JavaScript** 更快，因此它符合我们的需求。
- **PHP** 不仅速度快，而且简单，减少了未来生态系统贡献所需的专业水平。

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

我们仍然使用 C++ 开发对速度至关重要的功能。C++ 适合需要速度的任务。对于不需要太多速度的任务，**Buddy** 是最佳选择。

这就是 **Buddy** 的诞生。为了在 Manticore Search 的 C++ 端实现这一点，我们实现了一个独立的循环，并使用 **CURL** 扩展在 **searchd** 守护进程和 **Buddy** PHP 进程之间进行通信。我们开发了我们内部的协议，这是一个简单的 **JSON**，用于将查询路由到 **Buddy**；它处理这些查询并发送给我们适当的响应，以便传递回原始客户端。

## 实现通信协议

在启动新项目时，保持灵活性并不过度规划非常重要。在我们的情况下，我们首先使用 **PHP** 中的 [sockets](https://www.php.net/manual/en/book.sockets.php) 扩展实现了一个基本的通信。虽然它运行良好，但不具备可扩展性。我们的目标是将 **Manticore Search** 与 **Buddy** 连接起来，这种简单的实现使我们能够验证这个想法。

我们没有重新发明轮子，而是研究了使系统更具可扩展性和非阻塞性的选项。我们最初考虑了 **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](https://www.php.net/manual/en/language.fibers.php) 来模拟异步方法。我们将在下一节中更详细地讨论这个问题。

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

为了处理高负载，Manticore Search 的团队需要一个能够处理比 **ReactPHP** 更多请求的解决方案。虽然 **ReactPHP** 适用于实现异步 HTTP 服务器和处理一些并发，但它的可扩展性不足。经过快速搜索，我们选择了 **[parallel](https://www.php.net/manual/en/book.parallel.php)** 扩展而不是 **pthreads**，因为它的可维护性和可靠性。

但是什么是**并行**？它涉及并行化，创建独立的**运行时**，这些运行时代表并行运行的线程。这些线程可以通过**Parallel**提供的通道进行通信，允许我们从主**ReactPHP**循环进程向另一个**运行时**中并行且分离的线程发送数据，而无需重新发明轮子。

这种方法是我们处理高并发并保持响应时间低的银弹。为了实现这一点，我们在**Handler**级别实现了一个**任务**组件，以在并行线程中运行任务。这样，主进程不会被阻塞，使我们能够轻松处理大量并发请求。

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

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

## 向 manticore-executor 问好

我们进行了研究，寻找一种无痛的方式将新工具交付给客户，并发现了一个很好的方法 - [将**PHP**和Buddy编译成一个静态二进制文件](https://github.com/crazywhalecc/static-php-cli/blob/master/README-en.md)。这涉及将**PHP**注入其源代码中并创建一个可以运行的二进制文件。然而，我们遇到了一个障碍，因为我们想要混合不同的许可证 - **PHP 3.01**和**GPL 2.0** - 这是不可行的。因此，我们选择预构建**PHP**，静态链接它，并将其命名为manticore-executor。

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

为什么我们使用GCC？这是编译PHP及其扩展所必需的。问题是，我们需要静态构建的GCC来静态链接，这并不直接，并且需要大量工作。因此，我们寻找了替代方案。

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

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

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

因此，我们使用了它，并设置了操作以使用**Alpine**镜像在**Docker**中构建它。这种方法的优点是，它也使我们更容易为ARM构建，因为**Docker**有`buildx`命令，允许我们使用QEMU在准备就绪的构建模式中，以相同的流程在同台机器上构建AMD和ARM架构！查看我们的构建流程[此处](https://github.com/manticoresoftware/executor/blob/main/.github/workflows/release.yml)。

**Github Actions**自动化了所有支持操作系统的软件包构建和部署。对于用户来说，安装很简单：只需运行`apt-get install manticore-executor`或`yum install manticore-executor`，您就会拥有一个带有所有必要预装包的**PHP**版本，可以运行任何Manticore分发的**PHP**项目。简单！

## 我们如何分发源代码

在Manticore Search，我们面临了将由多个源代码文件组成的PHP应用程序提供给用户的挑战。我们有许多文件分布在不同的文件夹中，依赖项需要通过Composer安装，使安装过程变得复杂。

正如你所记得的，我们开发了一个自定义的PHP版本，称为[manticore-executor](https://github.com/manticoresoftware/executor)，可以从仓库轻松安装。然而，这仍然没有解决将整个PHP应用程序提供给用户的问题。

我们找到了**PHAR**的解决方案，它允许我们构建一个可以作为包添加到仓库的单个文件。这简化了安装过程。然而，确保所有依赖项正确包含在最终的**PHAR**存档中是棘手的。为了解决这个问题，我们创建并分离了一个外部构建系统，我们还用于我们的[manticore-backup](https://github.com/manticoresoftware/manticoresearch-backup)工具。

为了使包可执行，我们决定使用带有我们的**Manticore-Executor**包的Bash和Shebang脚本。此脚本检查系统临时文件夹中修改的**PHAR**的日期，并在该位置提取**PHAR**数据，允许多次启动并保持在新版本安装时的性能和更新。有关我们如何实现这一点的更多信息，你可以参考我们在**GitHub**上的[phar_builder](https://github.com/manticoresoftware/phar_builder/blob/main/templates/sh)项目。

## 学到的经验教训

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

## 最终成果

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

我们希望您喜欢阅读我们关于 [Manticore 6.0.0](https://manticoresearch.com/blog/manticore-search-6-0-0/) 发布前的旅程。请务必关注我们的下一篇文章，我们将探讨 **Buddy** 中新的可插拔设计及其易于贡献的生态系统，这将使整个社区受益。这确实是一个令人兴奋的时刻，我们迫不及待地想与您分享更多内容。
