设计反向代理:为什么Golang比Java(Spring Boot)性能更好? - Sajid

21-09-11 banq

假设您有三个独立的服务,它们是:- mycart.mycoolapp.com- mypayment.mycoolapp.com- mycoolproducts.mycoolapp.com

您的客户使用这三个功能。因此,您可以创建一个反向代理。您的客户端可以连接到此反向代理,例如,mycool-reverse-proxy.com。

现在,假设您的客户想要获得所有酷产品的列表。因此,它请求代理mycool-reverse-proxy.com/products。现在代理可以在您请求产品服务的 API 路径中看到。所以它将这个请求重定向到产品服务。

反向代理的全部意义在于抽象您的后端逻辑。因此,客户端不需要知道您拥有多少服务、它们的地址或它们在哪里。

很酷,不是吗?是的!

反向代理在这里做什么?它位于您的客户端和您的服务之间并路由请求。

你可能认为反向代理是一个负载均衡器。但事实并非如此。他们有完全不同类型的责任,他们的意思也不同。

现在,让我们快速计算一下!

例如,客户端将调用服务 A 10 个请求,服务 B 20 次,服务 C 5 次。但是由于反向代理位于客户端和服务之间,所有请求都将通过它,不是吗?

是的当然。所有 35 个请求都将通过它。现在,随着您的业务增长,您将获得更多负载。因此,您可以扩展您的服务。但正如我们所看到的,反向代理经历了所有服务的总负载。因此,您将进行水平/垂直缩放。您添加更多机器。

因此,在开发反向代理时,您将选择一种语言,例如 Java、Node、Golang 等。这种语言是否对反向代理有任何影响,或者反向代理中这些语言之间是否存在任何性能差异?让我们来了解一下。

在本文中,我将只专注于分析 Spring Boot(这是一个基于 Java 的框架)与 Golang 中反向代理的性能。

 

Java线程

Java 是一种基于 JVM 的语言。当您在 Java 中创建线程时,JVM 会将这个线程映射到操作系统级别的线程。您的 Java 线程和 OS 线程基本上是 1:1 映射。操作系统级线程具有固定大小的堆栈。它不可能无限增长。在64 位机器中,大小为1 MB。因此,如果您的内存为 1 GB,您可以拥有大约1000 个线程。由于 JVM 以1:1 映射将线程映射到操作系统级线程,因此您可以使用基于 JVM 的语言(例如 Java)创建大约1000 个线程。因此,Java 线程很重,具有固定的堆栈大小,并且您可以在1 GB内存中拥有大约1K线程。

 

什么是 Goroutine?

goroutine 是由 Go 运行时管理的轻量级线程。它与我们之前看到的 Java 线程有点不同。Goroutine 调度器调度 goroutine 执行。但有趣的事实是 Goroutines 不是 1:1 映射到操作系统级线程的。多个 Goroutine 被映射到一个单一的 OS 线程中。所以它被多路复用到操作系统线程。关于 Goroutine 的另一个有趣的事实是 Go 没有固定大小的堆栈。相反,它可以根据数据增长或缩小。所以 Goroutine 利用了这个特性。平均而言,新 Goroutine 堆栈的默认大小约为2 KB,并且可以根据需要增加或缩小。所以,在 1 GB 的内存中,1024 * 1024 / 2 = 5,24,288如果 Goroutines.

 

Java 和 Golang 的上下文切换比较

如上所述,JVM 使用操作系统级线程。所以当线程执行操作系统级调度程序调度时,执行线程。所以上下文切换发生在操作系统级别。当内核进行上下文切换时,它必须做大量的工作。因此,上下文切换发生时需要一些时间(微秒级)。

另一方面,Golang 有自己的 Goroutine 调度器,专门为此任务优化构建。Goroutine 调度器将 Goroutine 映射到内核中的 OS 级线程和进程。并且它是上下文切换的最佳选择,以减少这样做的时间。

 

Golang 和 Java(Spring Boot) 在反向代理中的性能比较

因此,在讨论了所有理论问题之后,就到了这一点。由于 Spring boot 在 Tomcat 服务器中运行,因此每个请求都由单独的线程处理。因此,当请求到来时,会分配一个线程来处理此请求。由于它在底层使用 JVM,我们看到这些线程是操作系统级线程。

另一方面,如果您使用 Goroutine 来处理每个请求,即当一个请求到来时,您分配一个 Goroutine 来处理该请求,它将比 Spring Boot 执行得更好。因为我们在上一段中已经看到 Goroutine 相对于线程有一定的优势。

我们还看到,在 1GB RAM 中,Goroutine 的数量会比线程多(5,24,288 对 1000)。因此,您可以使用 Golang 服务处理更多请求。

由于反向代理通常会承受系统的所有负载,因此那里总会有大量请求。如果你用轻量级的 Goroutine 处理它,你可以利用 Goroutines 的所有优点来获得高吞吐量和更好的性能,并同时处理更多请求。

 

Goroutines 的缺点

尽管 Goroutines 有很多积极的方面,但也有一些缺点。在 Spring Boot 中会有固定数量的线程池。因此,当请求到来时,从池中取出一个线程,当工作完成时,该线程再次保留在池中。它由Tomcat服务器处理。

然而,它不会在 Golang 世界中自动处理。因此,无论您是使用著名的 Golangnet/http包设计传输层,还是使用Gin-gonic 之类的框架,默认情况下都没有 Goroutine 池。所以,你必须手动处理它。

但是你可能想知道为什么我需要一个池?这是为什么?

部署代码后,无论是在服务器中还是在 Kubernetes Pod 中,总会有一个操作系统。操作系统有一个术语叫做ulimit。ulimit 是一个文件描述符,也是访问 I/O 资源的指示器。当我们从我们的代码向外界发出网络请求时,会打开一个 TCP 连接,然后在握手后发出请求。ulimit 表示在负责建立 TCP 连接的操作系统中可以有多少文件描述符。您拥有的 ulimit 越多,您可以创建的 TCP 连接就越多。

Linux 操作系统的 ulimit 值约为2¹⁶ = 65,536。在 Mac 系统中,默认值为252。但是你总是可以增加它ulimit -n number_of_ulimit_you_want。

而这也是 Goroutine 的失败点之一。

反向代理会发生什么?我们看到一个请求到达代理,然后根据请求,反向代理将请求重定向到任何下游服务。为此,反向代理从自身发出出站请求。并且要发出出站请求,需要 TCP 连接,这基本上是网络 I/O。文件描述符处理它。ulimit 表示操作系统可以拥有多少个文件描述符。

您可以在 1GB 内存中启动近 5,24,288 个 Goroutine。现在在反向代理中,如果您使用 Golang 实现它并且您没有任何 Goroutine 池,将会发生的情况是,您可以收到大量请求并且您的服务器不会陷入困境。但是由于反向代理会将请求重定向到所有不同的下游,因此它将发出所有出站请求。结果,将打开如此多的 TCP 连接,并且所有这些连接都是网络 I/O。所以文件描述符将处理所有这些。因此,如果对您的其他服务的出站请求数量超过您拥有的 ulimit 数量,那么您将收到太多打开文件错误。

所以,这就是为什么你应该有一个基于你拥有的 ulimit 的 Goroutine 池,这样你就不会陷入上述错误。

 

实际比较

所以……我做了一个实验,用 Spring boot 编写了一个产品服务。然后我开发了两个反向代理。一个是用 Spring boot 编写的,另一个是用 Golang 编写的(我用 Gin-gonic 作为路由器)。然后我使用JMeter对整个系统进行负载测试。这是结果

说明:

  • Number of Requests是具有相同标签的样本数。
  • 平均值是一组结果的平均时间。
  • Min是具有相同标签的样本的最短时间。
  • Max是具有相同标签的样本的最长时间
  • Throughput 吞吐量以每秒/分钟/小时的请求数来衡量。选择时间单位以使显示的速率至少为 1.0。吞吐量保存到CSV文件时,以requests/second表示,即30.0 requests/minute保存为0.5。
  • Received KB/sec是以每秒千字节为单位的吞吐量。时间以毫秒为单位。
  • Standard Deviation标准差是数据集可变性的度量。JMeter 计算总体标准偏差(STDEVP 函数模拟)
  • Avg Bytes — 具有相同标签的样本的响应字节的算术平均值。

可以看出,Golang 的性能比 Spring Boot 好。Spring Boot 系统偏差很大。

 

我们来看看Golang代理和Spring Boot代理的CPU和内存使用情况。

在我的 hexacore 机器上,我运行了用 Spring boot 编写的产品服务。然后首先我启动了spring boot代理,然后拿了矩阵。之后,我运行 Go 代理并获取矩阵。它们如下

Golang使用的CPU和内存比SpringBoot代理少。根据前面的理论讨论,这是可以预期的。

因此,可以说,在服务器中部署代理时,就CPU和内存等资源而言,Golang的性能比Spring Boot要好。因此,在伸缩的情况下,就像水平伸缩一样,需要更多的SpringBoot代理来满足巨大的负载。然而,由于自动缩放组通常在进行缩放时监视实例的CPU和内存使用情况,因此它需要更少的Go代理。

 

猜你喜欢