我们如何将其中一个 API 响应时间缩短 87% 并使用更少的资源
首先,这不是点击诱饵标题!
对于懒惰的读者来说长话短说,我们使用 Go 语言重写了我们现有的 API。它以前是用 C# 和 .Net Core 编写的。
我必须说,重写过程和使用它很困难,而且花费的时间比我想象的要长。我们获得了一些新的经验,以及使新 API 成功的多个关键点,我们希望与您分享。
重写过程是如何开始的?
好吧,它开始是因为我试图在 并发方面提高自己 ,我认为我们的一个 API 适合它,后来我们也可以获得 并行的力量 。此外,这个 API 进行了大量的按位运算,我们认为 Go 会在其中表现得更好。
我必须承认,这是我处理过的最难的 API,并且花了大约一周的时间完成逆向工程。之后,我开始在空闲时间使用 Go 重写它。我总是把它作为一个爱好项目来对待,直到最后才向团队展示它。
重写过程是如何进行的?
在逆向工程的过程中,我发现一些代码需要在现有的 API 中进行重构。我已经完成了所有的重构然后上线了。因此,我保证新应用程序不会有它们并且代码会更干净。
保留方法名称是一个关键部分,因为在应用程序之间查找它更容易。在调试时指出错误也更容易。
起初,我不想实现 并发和并行,因为我希望两个 API 是相同的。但是,我无法帮助自己并实施了它们。
在 Go 中编写单元测试比 C# 稍微复杂一些。我花了一段时间才习惯了嘲笑的部分。在我看来,关于 Go 中模拟方法/层的文章(和最佳实践)并不多。我阅读的几乎所有文章/帖子都是关于测试不包含要模拟的方法/层的方法。我相信我应该写一些关于它的东西并分享我的经验。
幸运的是,现有的 API 有单元测试。有一次我们的首席开发人员 Yiğit 说:“如果一个应用程序有单元测试,那么将它移植到另一种语言真的很容易。”他说得很对。
测试过程是如何进行的?
完成单元测试后,我开始在两个 API 之间进行一些猴子测试。这部分非常有趣,因为我还比较了响应、响应时间和资源使用情况。
除了一些小的差异,反应是相同的。对于其余部分,我必须进行一些修复。
根据响应时间,Go 的表现绝对更好。现有的 API 在 800 毫秒到 1.2 秒左右给出响应,而 Go 在 120 毫秒到 200 毫秒左右对相同的请求给出响应。
如果仅通过比较响应和响应时间就足以让您使用新的 API,那么您可能会犯大错。在我检查了新 API 中的内存和 CPU 使用率后,我确信存在问题。我使用 Go 编写了多个 API,但没有一个使用超过 50mb 的内存,但这个 API 使用了大约 200mb。在 CPU 部分,它使用了分配给容器的所有 CPU。我不认为有问题,因为我实现了并行性。
是时候迷路了
在黑暗中独处一段时间是可以的,这就是你学习如何找到光明的方式。如果你在那里呆太久,你可能会忘记你为什么在那里,也会失明。而且,我失明了,迷失在其中。
因为通过使用通道复制数据并使用 goroutines 使用 CPU,我确信问题出在我的并发/并行实现上,并且花了很多时间阅读有关它的文章和最佳实践。尽管我的想法错了,但我学到了很多关于并发和并行的知识,因此,我不能说我浪费了我的时间。
是时候找到光了
在尝试了多种方法后,我不得不承认我失败了。这是我与后端团队分享我的项目的时间。我们的首席开发人员和我的密友 Yiğit 也为新 API 分配了时间。
我向他解释了 API 的工作原理以及我使用的技术,例如 Redis、SQL Server 等……我们一起在半小时内解决了 CPU 问题,可能更少但不会更多。API 与 Redis 建立了多个连接,当然这个 I/O 操作会消耗我们的 CPU 使用率。
为了指出新 API 中导致内存泄漏的原因,我们使用 了 pprof 包。为了通过在 Redis 中使用相同的键使现有 API 和新 API 一起工作,我制作了 JSON 编组和解组 。这就是我们指出内存泄漏问题的方式。
解决这些问题后,我编写了一个端点测试应用程序,Yiğit 从ELB中获得了最常调用的端点及其参数 。我们向现有 API 和新 API 发送了相同的请求,并将它们的响应与代码进行了比较。
一切都很酷之后,我们开始直播了!请参阅下面的新 API 响应时间和资源使用情况。
如您所见,我们实际上将响应时间减少了 87% 以上。100vcpu 和 200mbs 的内存分配给容器。现有的 API 资源值是相同的,但是,它正在扩展到最小 2 个,最大 4 个容器。使用 Go 构建的新 API 在单个容器中运行。我们同时减少了响应时间和资源使用!我们确信它以一种很好的方式影响了我们的转化率,但现在分享这些信息还为时过早。
我们没有在这里完成这个项目。我们重构了现有的 API 并删除了 Redis 查询。这就是我们想要检查 Go 在我们的案例中是否比 .Net Core 强大得多的方式。好吧,我们对结果感到惊讶。使用 .Net Core 构建的现有 API 开始在 150 毫秒到 200 毫秒之间给出响应。它再次在 2 个容器中运行,并在需要时扩展到 4 个。它的资源配置和以前一样。
我们使用了一个名为 Bombardier的工具 并再次对这两个 API 进行了基准测试!.Net Core 扩展到 4 个容器,无法处理请求。然后所有的容器都死了,并给出了 503 Service Temporarily Unavailable。然而,Go 保持不变,永远不需要扩展自己,永远不会死!
我想添加一个关于我们测试过程的有趣故事。我们想检查 Go 可以处理多少请求。我们使用 Alpine 映像,因此,由于 Linux 操作系统的限制,Go 应该处理 1024 个并发请求(您可以增加它,但我们不想这样做)。Yiğit 想在 30 秒内攻击 2048 个连接。你能想象发生了什么吗?Yiğit 分配了他路由器上的所有端口,他的互联网中断了几分钟,但 API 仍然存在。
总结
这个项目的里程碑是当我向团队寻求帮助时。拥有一支技术精湛、积极进取的团队是我在这个项目中取得成功的机会。虽然 Yiğit 没有使用 Go 构建任何项目,但他在其他语言方面的强大背景帮助我们在这个项目中取得了成功。这就是深入学习一门语言的重要性。
我在项目期间也学到了很多东西。我从不认为我花费的任何时间都是浪费,事实上,我获得了很多经验。我将在我的以下项目中使用这种经验。学过的知识!