GO日记——fasthttp client为什么快

翻译自 https://weekly-geekly.github.io/articles/443378/index.html

我们使用fasthttp的例子编写了一个高性能的http客户端。Alexander Valyalkin(VertaMedia)
Fasthttp库是标准Golang软件包中net / http的加速替代品。
怎么安排?它为什么这么快?

我是Alexander Valyalkin。我在VertaMedia公司工作。我为我们的需求设计了一个fasthttp。它包括http客户端实现和http服务器实现。Fasthttp的工作速度比标准Go包的net / http快得多。

fasthttp是什么?

在这里插入图片描述
Fasthttp是http服务器和客户端的快速实现。地址:https://github.com/valyala/fasthttp

性能压测

在这里插入图片描述
我想很多人都听说过fasthttp服务器,它非常快。但很少有人听说过fasthttp客户端。Fasthttp服务器参与了techempower基准测试 - 一个用于http服务器着名基准测试。Fasthttp服务器参与第12轮和第13轮。第13轮尚未出现(2016年 - Ed。)。
在这里插入图片描述
第12轮测试的结果之一,其中fasthttp几乎位于顶部。这些数字显示了他在给定测试中每秒发出的请求数。在此测试中,请求提供给hello world的页面。fasthttp返回hello world的页面非常快。
在这里插入图片描述
下一轮的初步结果尚未公布(2016年 - 编辑)。4个fasthttp实现占据了基准测试的第一个位置,它不仅提供了hello world的返回,而且可以根据模板形成了一个html页面。

Fasthttp server性能很强,那么Fasthttp client呢?

关于fasthttp客户端,很少有人知道。但事实上,他也很酷。在本报告中,我将向您介绍fasthttp客户端的内部结构及其开发原因。
在这里插入图片描述
实际上,fasthttp中有几个客户端:Client,HostClient和PipelineClient。然后我会告诉你更多关于它们的信息。

fasthttp.Client

在这里插入图片描述
Fasthttp.Client是典型的通用http客户端。有了它,您可以向任何Internet站点发出请求,获得响应。它的特点:它工作得很快,你可以限制每个主机的开放连接数,这与net / http包不同。该文档位于https://godoc.org/github.com/valyala/fasthttp#Client。

fasthttp.HostClient

在这里插入图片描述
Fasthttp.HostClient是一个专门的客户端,只与一台服务器通信。它通常用于访问HTTP API:REST API,JSON API。它还可用于代理从Internet到多个服务器上的内部DataCenter的流量。文档在这里:https://godoc.org/github.com/valyala/fasthttp#HostClient

就像Fasthttp.Client一样,使用Fasthttp.HostClient,您可以限制每个后端服务器的打开连接数。net / http中不存在此功能,并且free nginx中也不存在此功能。据我所知,此功能仅适用于付费的nginx。

fasthttp.PipelineClient

在这里插入图片描述
Fasthttp.PipelineClient是一个专用客户端,允许您管理服务器或有限数量的服务器的管道请求。它可以用于通过HTTP协议访问API,您需要尽可能快地执行大量请求。Fasthttp.PipelineClient的限制是它可能遭受Head of Line阻塞。这是我们向服务器发送许多请求而不等待对每个请求的响应的时候。某些请求的服务器被阻止。因此,在他之后的所有其他请求将等到此服务器处理慢速请求。只有在您确定服务器会立即响应您的请求时,才应使用Fasthttp.PipelineClient。文档在这里:https://godoc.org/github.com/valyala/fasthttp#PipelineClient

fasthttp.HostClient 内部实现

现在我将开始讨论每个客户端的内部实现。我将从Fasthttp.HostClient开始,因为几乎所有其他客户端都基于它。
在这里插入图片描述

这是Go上伪代码中最简单的HTTP客户端实现。连接,获取此URL的http响应。我们正在连接到这个主机。我们收到联系。在此代码中,因此它小于卷,所有错误检查都将丢失。事实上,这是不可能的。您应该始终检查错误。我们创建连接。使用延迟关闭连接。通过URL向此连接发送请求。我们收到响应,我们回复此响应。这个HTTP客户端的实现有什么问题?
在这里插入图片描述
第一个问题是在此实现中,为每个请求设置连接。此实现不支持HTTP KeepAlive。如何解决这个问题呢?您可以为每个服务器使用连接池。您不能将连接池用于所有服务器,因为下一个请求不清楚要发送到哪个服务器。每个服务器都应该有自己的连接池。并使用HTTP KeepAlive。这意味着在Header中没有必要指定连接关闭。在HTTP / 1.1中,默认情况下支持HTTP KeepAlive,并且应从Header中删除Connection Close。以下是客户端伪代码中的实现,并支持连接池。每个主机都有一组连接池。第一个函数connPoolForHost,从给定的URL返回给定主机的连接池。然后我们从这个连接池中取出连接,计划使用Defer将此连接发送回池,向此连接发送KeepAlive请求,返回响应。响应后,执行Defer并返回连接池。因此,启用了HTTP KeepAlive支持,一切都开始更快。因为我们不会浪费时间为每个请求创建连接。

但解决方案也存在问题。如果查看函数签名,可以看到它为每个请求返回一个响应对象。这意味着每次需要为此对象分配内存时,请初始化并返回它。这对性能不利。如果您对Get函数进行了大量此类调用,那可能会很糟糕。
在这里插入图片描述

因此,可以通过将指针对象放在此函数的参数中的响应对象上来解决此问题,因为它可以在Fasthttp中解决。因此,该调用代码可以多次重用该响应对象。在幻灯片上,执行这个想法。在Get函数中,我们传递对响应对象的引用,该函数填充此响应。最后一行填充此对象。
在这里插入图片描述

以下是代码中的外观。接收通道的函数,该通道传输需要轮询的URL列表。让我们在这个通道上组织一个环。创建一个响应对象并在循环中重用它。我们导致Get,我们将指针传递给object,我们处理这个响应。处理完毕后,将其重置为原始状态。因此,我们避免了内存分配并加快了代码的速度。
在这里插入图片描述

第三个问题是连接关闭。Connection close是一个HTTP标头,可以同时出现在请求和响应中。如果我们得到这样的标题,那么应该关闭此连接。因此,在客户端的实现中,必须提供关闭连接。如果您发送了带有标头连接的请求,则在收到响应后,您需要关闭此连接。如果您发送没有连接的请求,并且返回关闭连接的响应,则表示您还需要在收到响应后关闭此连接。
在这里插入图片描述

这是此实现的伪代码。收到响应后,我们会检查header中是否将Connected设置为close 。如果已设置,只需关闭连接即可。如果未设置,请将连接返回池中。如果您不这样做,那么如果服务器在返回响应后关闭连接,那么您的连接池将包含服务器已关闭的断开连接,您将尝试在其中写入内容并且您将遇到错误。
在这里插入图片描述

影响HTTP客户端的第四个问题是速度慢的服务器或缓慢的非运营网络。服务器可能因各种原因停止响应您的请求。例如,服务器已损坏或客户端与服务器之间的网络已停止工作。因此,所有调用前面描述的Get函数的gorutine都将被阻止,等待来自服务器的响应无限期。例如,如果您实现接受传入连接的http代理并为每个连接调用Get函数,则将创建大量gorutine,它们将全部挂在您的服务器上,直到服务器崩溃直到内存耗尽。
[外链图片转存失败(img-Ws7lKFjh-1565491354888)(leanote://file/getImage?fileId=5d4bbedbae67871dc8000011)]

如何解决这个问题呢?有这样一个天真的解决方案,这是第一次浮现在脑海中 - 只需把它变成一座独立的山。然后将一个空通道传递给gorutine,这将在Get之后关闭。这个gorutine发布后,在这个通道上等待一段时间(超时)。在这种情况下,如果它花了你一些时间并且这个Get失败,那么在超时时将退出此函数。如果执行此Get,则通道将关闭并退出。但这个决定是错误的,因为它将问题从病态的头部转移到健康的头部。尽管如此,无论使用何种超时,都会创建和挂起gorutines。导致Get超时的gorutine数量将受到限制,但是将会在超时时间内创建无限数量的Gorutine。
[外链图片转存失败(img-fMAfvgbS-1565491354888)(leanote://file/getImage?fileId=5d4bbeecae67871dc8000012)]

如何解决这个问题呢?有一个第一个解决方案 - 这是限制函数Get中阻塞的gorutine的数量。这可以使用众所周知的模式来完成,例如使用有限长度的缓冲通道,其将计算执行Get功能的gorutine的数量。如果这个量的gorutine超过某种限制 - 这个通道的容量,那么我们将退出到默认分支。这意味着Get所做的所有Gortonins都忙于我们,默认情况下,分支只需要返回一个没有空闲资源的错误。在我们创建一个山之前,我们尝试将一些空结构写入此通道。如果这不起作用,那么我们已经超过了gorutine的数量。如果它有效,那么我们创建这个gorutine,在Get完成后,我们从这个通道中读取一个值。
[外链图片转存失败(img-R3D0TYKr-1565491354891)(leanote://file/getImage?fileId=5d4bbf1eae67871dc8000013)]

补充第一个解决方案的第二个解决方案是设置与服务器连接的超时。如果服务器长时间没有响应或网络中断,这将解锁get函数。

如果网络在解决方案#1中不起作用,那么一切都会挂起。在使用cuncurrency对有限数量的gorutine进行评分后,getimeout函数将始终返回错误。为了使其正常工作,您需要第二个解决方案(解决方案#2),该解决方案设置从连接读取和写入的超时。如果网络或服务器停止工作,这有助于解锁阻止的gorutine。
[外链图片转存失败(img-Wkg7MoZa-1565491354892)(leanote://file/getImage?fileId=5d4bbf28ae67871dc8000014)]

解决方案#1有数据竞争。如果我们有Get阻塞,则传递指针的响应对象将busy。但是这个函数获取超时可以超时。在这种情况下,我们退出此功能,响应将挂起并随着时间的推移将被覆盖。因此,事实证明数据竞争。由于退出函数后我们有一个响应,它仍然在gorutine的某个地方使用。

通过创建响应副本并将响应副本传递到山来解决该问题。在执行Get之后,我们将此响应的副本复制到我们原始响应的副本,该响应在此处传输。因此,数据竞争得以解决。此响应副本会短时间存在并返回池中。我们重用响应。仅在超时时,响应的副本可能不适合池中。超时后,池中的响应将丢失。
在这里插入图片描述

在超时期间服务器没有返回响应后,是否需要关闭连接?答案是不。相反,是的,如果你想做服务器。因为当您向服务器发送请求时,等待某人一段时间,服务器在此期间没有响应 - 它不能处理请求。例如,您关闭此连接,但这并不意味着服务器将立即停止执行此请求。服务器将继续执行。在尝试向您返回响应后,服务器将发现不需要执行此请求。您关闭了连接,尝试再次创建新请求,再次超时,再次关闭,创建了一个新请求。您将有一个服务器负载上升。因此,您的服务适用于您的请求的DoS。这是http请求级别的DoS。如果您的服务器速度很慢并且您不想禁用它们,那么您不需要在超时后关闭连接。您需要等待一段时间,保留连接以获取此服务器的弥补。让他试着给你响应。在此期间,使用其他空闲连接。以前告诉的所有内容都是Fasthttp.Client实现的所有阶段以及在Fasthttp.Client实现过程中出现的问题。这些问题在Fasthttp.HostClient中得到解决。使用其他空闲连接。以前告诉的所有内容都是Fasthttp.Client实现的所有阶段以及在Fasthttp.Client实现过程中出现的问题。这些问题在Fasthttp.HostClient中得到解决。使用其他空闲连接。以前告诉的所有内容都是Fasthttp.Client实现的所有阶段以及在Fasthttp.Client实现过程中出现的问题。这些问题在Fasthttp.HostClient中得到解决。

我们现在有快速的客户吗?并不是的。我们需要了解连接池的实现方式。

在这里插入图片描述

连接池的简单实现看起来像这样。您需要建立连接的服务器地址。有一个空闲连接列表和一个锁定来同步访问此列表。
在这里插入图片描述

以下是从连接池获取连接的功能。我们看到了我们的收藏清单。如果那里有东西,那么我们得到一个空闲连接并返回它。如果没有,则创建与此服务器的新连接并返回它。这有什么不对?
在这里插入图片描述

函数connPool.Put归还一个空闲连接。

由于超时。在Fasthttp.Client中,您可以指定打开的未使用连接的最长生命周期。在此时间过后,未使用的连接将自动关闭并从此池中抛出。

较旧的连接会随着时间的推移而不再使用,并会自动关闭并从池中删除。

当从池中获取连接,并且事实证明其服务器已关闭,并且您尝试在那里写入内容时,会进行第二次尝试 - 它获得新连接并再次尝试发送对该连接的请求。但这只是当这个请求是幂等的 - 也就是说,一个可以多次执行而没有副作用的请求 - 是GET或HEAD请求。例如,在标准的net / http中,他们刚刚添加了对已关闭连接的检查。他们做了一个更聪明的检查。当他们尝试从池中向连接发送新请求时,如果至少有一个字节进入此连接,则检查它们。如果你去,然后返回错误。如果没有,则从池中获取新连接。
在这里插入图片描述

池有什么问题?它的大小不受限制。与net / http中的实现相同。如果您编写的客户端从数百万的gorutine打破到慢速服务器,那么客户端将尝试创建与该服务器的百万连接。在标准的net / http包中,最大连接数没有限制。对于用于通过HTTP访问API的客户端,建议限制此连接池的大小。否则,您的客户端可能会停机,因为将使用所有资源:流,对象,连接,gorutines和内存。它也可能导致服务器的DoS,因为它们会有很多连接,这些连接要么没有使用,要么效率低下,因为服务器无法容纳这么多连接。
[外链图片转存失败(img-yUdqWTe3-1565491354897)(leanote://file/getImage?fileId=5d4bbf80ae67871dc800001a)]

我们限制连接池。这里没有代码,因为它太大而无法放在一张幻灯片上。有兴趣的人可以在github.com上看到这个功能的实现。地址:https://github.com/valyala/fasthttp/blob/master/workerpool.go
在这里插入图片描述

第二个问题。许多请求在某个时间点发送给客户端。之后会出现下降并返回之前的请求数量。例如,同时发出10,000个请求,然后每单位时间返回1,000个请求的数量。此连接池将增长到10,000连接。这些连接将无休止地挂在那里。这个问题出现在1.7版的标准net / http客户端中。因此,我们需要解决这个问题。
在这里插入图片描述

通过限制未使用的连接的寿命来解决该问题。如果有一段时间没有通过连接发送单个请求,那么它就会被关闭并从池中抛出。

我们有一个快速而酷的客户端?在某种角度不确定。我们仍然有创建连接的函数 - dialHost。

在这里插入图片描述

我们来看看它的实现。一个天真的实现看起来像这样。只是您需要连接的地址。我们称之为标准的net.Dial函数。它返回一个连接。这个实现有什么问题?
在这里插入图片描述

默认情况下,net.Dial为每个调用发出一个dns请求。这可能会导致DNS子系统的资源利用率提高。如果API客户端连接到不支持KeepAlive连接的服务器,则它们会关闭连接。KeepAlive支持您,但不支持服务器。在此响应之后,服务器关闭连接。事实证明,每个请求都会调用net.Dial。这样的要求大约每秒1万。你在dns中每秒有10万次解决。这会加载DNS子系统。
在这里插入图片描述

如何解决这个问题呢?在Go代码中创建一个缓存,即一个host的map,并且不要将dns解析为每个net.Dial。连接到就绪IP地址。
在这里插入图片描述

第二个问题是服务器上的负载不均匀,如果您的域名后面隐藏了多个服务器。例如,像Round Robin DNS。如果您在DNS中缓存一个IP地址一段时间,那么在此期间所有请求将转到一个服务器。虽然你可能有几个。需要解决这个问题。它通过枚举隐藏在给定域名后面的所有可用IP来解决。这也是在Fasthttp.Client中完成的。
在这里插入图片描述

第三个问题是net.Dial也可能由于网络问题或您尝试连接的服务器而无限期挂起。在这种情况下,你的gorutine将挂在Get方法。这也可以提高资源利用率。
在这里插入图片描述

解决方案是添加超时。或者使用标准包网络中的超时拨号。但据我所知,它实施不正确。也许现在他们已经修好了,但早些时候就像我告诉你的那样实施了。
在这里插入图片描述

这就是它的实现方式。而不是Get,有一个Dial功能。它是在某种gorutine中进行的。如果Dial挂了,结果证明了gorutines累积了。悬挂的这种gorutine的数量可以无限增长。这是DialTimeout的标准实现。也许他们已经修好了。
在这里插入图片描述

此外,HostClient具有以下功能。

HostClient能够在您指定的服务器列表上分配负载。因此,实现了原始的LoadBalance。

HostClient也可以跳过不工作的服务器。如果在某个时间点某些服务器停止工作,则HostClient将在尝试访问此服务器时检测到此情况。在下一个连接中,他将不会访问此服务器。从而实现了负载均衡。您丢失了最小数量的请求。

错误的host有两个原因。

第一个原因是我们无法建立与服务器的连接。挂在表盘上。在这种情况下,事实证明,我们被困在这个拨号上。获得,冻结,将等待一段时间。在他等待的同时,所有其他请求将转到其他服务器。因此,更多的请求将通过其他主机而不是通过此主机。

第二种选择是服务器开始缓慢响应。他在Get中获得的时间比其他服务器多。在这种情况下,发送到此服务器的请求数量会少于其他服务器。

如果刚刚返回错误,则尝试连接到循环中的下一个服务器。

由于Golang具有非常酷的实现,因此SSL支持非常容易。在决策中使用和连接都很方便。

fasthttp.Client内部实现

转到fasthttp.Client。事实上,与HostClient相比,一切都要简单得多,因为fasthttp.Client是在HostClient的基础上实现的。
在这里插入图片描述

这是客户端实现Get函数的原始伪代码我们为每个已知主机都有一个HostClient列表。此函数从给定角度返回给定主机的所需HostClient。然后我们在这个HostClient中调用函数Get。以下是基于HostClient的整个客户端实现。
在这里插入图片描述

此函数可以为我们的URL中出现的任何新尾部创建新的HostClient。如果您使用网络爬虫,那么您的客户端可以访问数百万个站点。结果,每个站点都会获得一百万个HostClient,并且所有内存都已结束。这就是它在标准net/ http中的情况,也许这个问题已经解决了。为了防止这种情况发生,您应该定期清理HostClient,它很长时间没有被解决。fasthttp也是如此。

fasthttp.PipelineClient内部实现

与Client和HostClient不同,PipelineClient的实现略有不同。PipelineClient中没有连接池。PipelineClient可以选择需要在主机上建立的连接数。PipelineClient将尝试通过此连接数量推送所有请求。因此,没有连接池。PipelineClient立即建立连接并将传入请求分散到可用连接中。
在这里插入图片描述

对于PipelineClient,对于每个连接,将启动两个gorutines。PipelineConnClient.writer - 将请求写入连接,而不等待响应。PipelineConnClient.reader - 读取此连接的响应,并将它们与通过PipelineConnClient.writer发送的请求进行匹配。PipelineConnClient.reader返回对调用此Get函数的代码的响应。
在这里插入图片描述

在幻灯片上,PipelineClient的PipelineClient.Get函数的示例性实现。在管道工作结构中,有一个需要引用的URL,有一个指向响应的指针,有通道完成,表示响应已准备就绪。

以下是Get实现。我们创建并填充结构。我们将它发送到通道,由PipelineConnClient.writer读取,所有请求都写入连接。当我对此请求的响应发出时,我们正在等待通道w.done,它由PipelineConnClient.reader关闭。

性能比较

在接下来的2张幻灯片上比较net / http客户端性能和fasthttp.Client。
在这里插入图片描述

这些幻灯片上显示的基准测试显示在fasthttp中。您可以自己运行,测试和测试它们。fasthttp。,fasthttp ,. 分配。。
在这里插入图片描述

资料来源:https://habr.com/ru/post/443378/

猜你喜欢

转载自blog.csdn.net/qq_32198277/article/details/99172183