
在计算机领域,完全重写有很大的阻力,原因有很多,包括成本、时间、可能出现的新错误和功能倒退。
相反,软件行业更喜欢我们所说的 "重构"(refactoring),即对现有代码进行改进,在很长一段时间内做出许多微小的改动,这样每次改动都可以更容易地实施,并以可控的方式衡量这些改动的结果。 简而言之,这种方法速度快、成本低、见效快,而且降低了出现问题的可能性。
但有时可能需要完全重新架构一个系统,原因可能有很多。 你需要更高的性能,你需要将代码更好地扩展到更多的计算资源,新硬件的出现导致旧代码运行不佳或根本无法运行,新的程序库、操作系统或执行环境与你的旧代码不兼容等等。
而上述许多情况都可能导致改写。

今年,我们发现自己从 2017 年开始编写的一些后端软件正是如此。 这些软件是为特定操作系统(Windows Server)设计的,这意味着我们利用了一些微软特定的操作系统功能,这使得我们的代码与 Linux 不兼容。
我们在设计软件时还考虑到了当时所能使用的某些硬件,即低核心数处理器和使用硬盘驱动器的慢速存储。 这导致我们的很多后端系统在使用硬件时都比较保守,这意味着大部分都是单线程操作和串行数据访问。
我们目前的服务器平均拥有 23.5 个 CPU 线程,而当我们讨论的大部分代码创建时,我们最大最好的服务器只有 8 个 CPU 线程。 说到存储,我们以前使用的硬盘只能处理 800 IOP's,而现在我们使用的 NVMe SSD 可处理 120 万 IOP's。
我们希望重写代码,使其与处理器无关,与操作系统无关,同时更好地利用我们最新(以及未来)的硬件,这就需要重写一些代码。 当然,我们可以重构一些旧代码,在许多较小的情况下,我们也是这么做的,但对于最重要的东西,重写才是正确的方法。
那么,如何以正确的方式重新构建软件呢?
首先,您需要对要重写的系统进行全面的代码审查。 这包括阅读所有代码,理解并记录代码执行的所有任务以及执行这些任务的原因。 这一点至关重要,否则你就会忘记在新重写的代码中加入之前迭代中包含的功能。
其次,您要找出代码中需要改进的所有不足之处。 这可能是杂乱无章或无法管理的代码,也可能是性能不佳或无法实现当前或未来目标(如将您绑定到特定操作系统)的非生产性代码。
第三,你要计划如何改进代码,以实现新程序的目标。 对我们来说,这主要包括使程序多线程化、更好地利用存储系统的 I/O 功能、不使用任何 Windows 操作系统特有的功能或 Linux 上无法使用的功能。 当然,所有这些工作的最终结果是增加了可扩展性和灵活性。
第四,最后是编写和测试代码。 我们在开发过程中进行了大量的测试,以检验我们的许多假设,这也为我们的设计过程提供了参考。 当我们了解到某些解决问题方法的能力时,我们所做的决定也随之改变。
让我们来看看其中的一些。
1:大约两年前,微软终止了对 WinCache 的支持,而 WinCache 是 PHP 的内存数据存储。 我们大量使用了它,因此我们必须建立一个替代品。 因此,我们在两年前编写了我们称之为 ramcache 的软件。 它的作用与 WinCache 相同,并重新实现了 WinCache 的所有功能。 我们还扩展了它的功能。 例如,WinCache 有 85MB 的内存限制,而我们的 ramcache 没有这种限制。 我们还使其与操作系统无关,也就是说,它可以在包括 Linux 在内的任何系统上运行。
2:网络钩子。 我们使用了大量 Webhooks,主要用于与付款相关的事件,例如在付款被拒或即将付款时向您发送电子邮件,但我们也使用 Webhooks 来处理我们认为时间紧迫的事件,例如当您在仪表板中进行更改时,必须快速传播到所有集群节点。
3:我们的数据库同步系统。 同步数据库一度导致处理器使用率高达 70%。 在我们升级到配备更快处理器的系统后,这一比例确实有所下降,但仍然非常高,而且随着客户每分钟产生的数据量越来越多,使用率也在稳步上升。 在这种情况下,我们开发了一个名为 Dispatcher 的新进程来处理这些流量,从而将处理器使用率大幅降低到 0.1%。
#@#4: Cluster management, node health monitoring & node deployment. Before our rewrite, this heavily relied on Microsoft-specific operating system features, especially the node health monitoring and node deployment features. We've now rewritten all of these to also be operating system agnostic and processor independent.#@#
让我们来看看一些净结果。 以前,我们的网络钩子(即一台服务器专门向集群中的一台或多台服务器发送一个小更新)平均需要 6 秒钟才能完成整个集群的更新。 新代码在使用网络时采用了多线程技术,将这一时间缩短到了 0.3 秒。 性能提高了 20 倍。

说到 Dispatcher,这对我们来说是一个巨大的改变,因为使我们的服务器保持同步的一切都使用了我们以前的系统。 旧系统包罗万象,甚至连名字都没有,因为它并不是一个特定的对象,代码中穿插了许多其他功能和特性,在我们的代码中几乎无处不在。
有了 Dispatcher,这一切都改变了,它为我们数据库的读写提供了一个标准化接口,并为我们的集群节点提供了一个框架,通过使用打包节点更新和为每个地理区域临时选择一个主节点作为数据库更新的收集器、处理器和分发器,以最被动(因此也是最不耗费资源)的方式提供数据。
你可以把调度程序想象成一个火车网络。 每个节点都运行着自己的列车,列车不停地在轨道上绕行,前往所有其他节点获取数据。 主节点从非主节点获取数据,对数据进行处理,然后仔细决定数据应插入数据库的哪个位置。 然后,主节点对数据进行重新打包,并提交给从非主节点拾取这些更新数据的列车。

每隔几分钟,节点就会进行一次投票,选出空闲资源最多、正常运行时间最长的节点作为该地理区域的主节点。 我们通过明确定义容器、合并冲突解决方案和围绕结构化 1 分钟时间表建立的数据库,来阻止因多个主节点同时发布更新而产生的冲突。
现实世界中的每一分钟都在数据库中进行记录,并为该分钟和地理区域附加一个主节点。 该节点且只有该节点可以执行维护和更改操作,除非各节点同意将其从该分钟中移除并指定另一个节点,从而允许另一个节点成为该分钟的主节点。 任何节点都可以读取分钟数据,但只有主节点才能执行更改和写入操作。
所有这一切的结果就是一个高性能的数据库同步系统,无论我们拥有多少服务器节点,它都能进行扩展,最重要的是,它能将处理器和存储设备的负担降到最低。
在进行所有这些代码重写和重构的同时,我们还研究了新技术,并为我们编写的代码调整了执行环境。 为此,我们将整个服务(包括所有网页)升级到了最新的 PHP v8.2.6。 在升级过程中,我们还使用了自 PHP v8 以来的最新 JIT 编译器,因为我们发现整个网站的页面加载时间得到了大幅改善,而且没有出现任何倒退。
以上就是今天的更新内容,希望大家喜欢我们的工作和插图。
感谢您的阅读,祝您度过愉快的一周。