本内容是对知名性能评测博主 Anton Putra .NET (C#) vs. Fiber (Go): Performance (Latency - Throughput - Saturation - Availability) 内容的翻译与整理, 有适当删减, 相关指标和结论以原作为准
在本视频中,我们将对比 C# 与 .NET 框架和 Golang 的表现。在第一个测试中,我们将专注于各自的最小框架实现。我们将测量 CPU 使用率、内存使用率、每秒可处理的请求数量,以及最重要的——最终用户延迟。
我们会运行第一个测试直到其中一个应用程序失败,然后再继续运行一段时间,以找出另一个应用程序的崩溃临界点。
在第二个测试中,我们将模拟一个常见的使用场景:从本地文件系统读取一个文件(在本例中是图片),然后将其上传到 S3 存储桶。同时,我们会将有关该图片的一些元数据(比如使用时间戳、文件名等)保存到一个关系型数据库中,例如 Postgres。此外,我们还会测量上传图片到 S3 的延迟以及将数据插入数据库的查询延迟。
基于之前的基准测试和反馈,我还会测量每个应用程序创建的数据库连接数。
我们还将测量在云环境中启动应用程序所需的时间。这不仅包括启动时间,还包括拉取镜像和通过健康检查所需的时间。这将直接影响在云端的自动扩展能力。
对于 C# 我们将使用最新的 .NET 8 框架,这是由微软构建并开源的跨平台框架,它支持构建各种类型的应用程序,从简单的 Web 应用与移动应用,到微服务和机器学习模型。
在本视频中,我们将使用 .NET Core 框架搭配 Minimal API,以构建该语言与框架中最快的应用程序。
根据微软的说法,这种设置在性能上应该优于 Golang。然而,他们的对比使用的是 Go 平台上的 Gin 框架。
另一方面,对于Golang我们将使用 Fiber 框架,这是该语言中最快的 HTTP 框架之一。但有些人并不喜欢 Fiber,因为它并不完全兼容标准库,因此并不是所有的中间件、可观测性工具以及其他相关组件都能直接与 Fiber 一起使用。
现在,让我们来看一下具体的测试。我有一个家用实验室(Home Lab
),使用 VMware Hypervisor 创建了一个多节点的 Kubernetes 集群。
我已经在 Kubernetes 中设置了一些监控组件:使用 Prometheus 服务器来抓取指标,Grafana 用于可视化这些指标并创建仪表盘,cAdvisor 用来抓取每个 Kubernetes 节点的指标,提供每个 Pod 的 CPU、内存与网络使用情况。我还部署了 kube-state-metrics,我们将用它来测量启动时间。
首先,我们将应用程序部署到 Kubernetes 集群。我有一个客户端程序,可以配置来生成负载并测量每个请求的延迟。
测量延迟的最佳方式是使用外部客户端,因为这能模拟任何最终用户的真实体验。当客户端开始发送请求时,Prometheus 会抓取这些指标,并在仪表盘中展示。
在第二个测试中,我们将使用应用程序中的另一个端点 /api/images
。每当客户端发送请求时,应用程序会从本地文件系统读取一个文件并上传到 S3 存储桶。我们不会使用 AWS S3,而是使用 MinIO,它是一个兼容 S3 的对象存储,同样部署在 Kubernetes 中。
在客户端上传图片之后,我们会将一些图片的元数据(比如创建时间戳、文件名等)写入一个关系型数据库——Postgres,这也部署在 Kubernetes 中。
你可以在我的 GitHub 公共仓库中找到每个应用程序的源代码,以及用于部署监控组件的 Terraform 脚本、Helm 图表和 YAML 文件。
在这个测试中,我们还将使用 Prometheus 客户端对每个应用程序进行指标注入,用来测量一些特定函数的调用时间。例如,我们将跟踪上传图片所需的时间,以及将数据写入数据库所需的时间。
因此,在第二个测试中,我们将从外部客户端和应用程序本身中收集指标。
接下来,我们来比较两个应用程序的镜像大小。对于 .NET 应用程序,我们将使用多阶段 Docker 构建,并在最终阶段使用 distroless 镜像。这样可以减少最终镜像的体积。你也可以使用 Alpine 作为最终阶段的基础镜像,但那样会增加 1 到 2 兆字节的大小。
对于 Go 应用程序,我们也将使用多阶段构建,并在最终阶段使用 distroless 镜像,这个镜像由 Google 提供和构建。
现在,让我们查看最终的镜像大小。Go 的镜像比 .NET 的小两倍以上。实际上,你甚至可以将 Go 的镜像压缩到大约 39MB (使用go build -ldflags "-s -w"
)。更小的镜像意味着 Kubernetes 在拉取镜像和启动应用程序时所需时间更短。
接下来,我们来测量启动时间。这包括应用程序的启动时间、Kubernetes 拉取镜像的时间以及 Pod 通过健康检查的时间。
这并不是一个非常科学的方法,因为网络速度会极大地影响拉取镜像的时间。但你仍然可以获得一个大致的概念。我们可以使用 kube-state-metrics 来测量 Pod 就绪所需的时间。相关仪表盘也可以在我的代码仓库中找到。
好了,让我们创建两个 Pod,看看它们需要多长时间启动。
由于镜像更小,Golang 第一个启动(耗时7s),而 C# 紧随其后(耗时12s)。我多次运行了这个测试,并在每次之前都从 Kubernetes 节点中删除了镜像。如果你使用 Karpenter、自动扩缩容工具或云端的 Spot 节点,需要考虑 Kubernetes 每次都必须重新拉取镜像。否则,两者的启动时间差异不大。
好了,我们开始第一次测试。首先,我会将这些应用程序部署到 Kubernetes,并在没有任何负载的情况下运行大约 20 分钟。
你可能会注意到 C# 的 CPU 使用率略高,但真正的区别在于内存使用。在空闲状态下,C# 和 .NET 框架的内存消耗远高于 Golang。
现在我们开始测试。首先,我们运行 10 个客户端,持续约 5 分钟,大约每个应用程序会接收到 20 个请求每秒。你会立即注意到 C# 的 CPU 使用率飙升,而且从客户端侧测量的整体延迟明显更高。但内存使用几乎没有变化,因为我们只是返回了硬编码的 JSON 数据。
接下来,我们增加到 50 个客户端,大约是每秒 100 个请求。CPU 使用的差异变得更大,而延迟的差异保持一致。
现在,我们将客户端数量增加到 200。整体延迟有所下降,但两者之间的相对比例保持不变。此时,Golang 更高效,CPU 使用更少。
我们继续,将客户端数量增加到 400,测试 5 分钟。
然后,我们将客户端数量降到 5,观察应用程序的适应能力。
接着,尝试 10 个客户端。
从现在开始,我们将从 550 个客户端开始,每隔 5 分钟持续增加,直到某个应用程序失败。
当请求数达到每秒约 1200 时,.NET 应用程序开始丢弃部分请求。
请求达到每秒约 1300 时,.NET 应用程序开始出现故障,并伴随延迟激增。我们会继续运行几分钟。
最后,Golang 开始丢弃一些请求,但它的延迟仍保持在几毫秒的范围内。
整个测试持续了大约 2.5 小时。现在让我打开各项图表,查看整个测试期间的表现。
首先是 CPU 使用率图表。
接着是内存使用图表。
然后是每秒请求数图表。
最后是整体延迟图表。
这就是第一个测试的全部内容,我们对比了框架本身:.NET Minimal API 与 Golang Fiber 框架,两者在 Kubernetes 中并行运行。
现在我们开始第二个测试,先从 10 个客户端开始。你可以立刻看出 CPU 和内存使用显著上升。同时我们还会测量延迟,看看读取文件并上传到 S3 所需的时间。目前来看,Golang 在低负载下表现更高效。我们还会测量保存文件元数据到关系型数据库的延迟。
目前看起来,Golang 的性能更好,使用更少的 CPU 和内存完成任务。
接下来我们模拟一个突发流量,将客户端数量暂时增加到 50,持续 5 分钟。
S3 的延迟几乎不变,但数据库延迟的差异增大。
好了,我们将客户端数量降回 10。
接下来的一个小时左右,我将逐步将客户端数量从 100 增加到 200,以观察哪一个应用程序表现更好。
你还可以看到 .NET 增加了连接池大小,而 Golang 保持相对稳定。我本可以手动设置连接池大小,但我的目标是测试这些框架和库在默认设置下的表现,因为很多人就是这么使用的。
好了,我再运行几分钟的测试。到目前为止,这大概就是两个应用程序所能处理的最大请求量了。你可以看到每秒请求数保持平稳。
现在让我打开整个测试期间的 CPU 使用图表。
接下来是内存使用图表。
然后是数据库延迟图表。
这是每个应用程序的连接池大小图。顺便一提,Postgres 默认可以打开 100 个并发连接——至少这是我用 Helm 图表部署数据库时的默认设置。
现在是客户端延迟图表。
在每秒请求数指标中,你可以看到 Golang 实际上能处理更多请求。
最后是 S3 延迟图表。