我们以fasthttp为例编写高性能的HTTP客户端。 亚历山大·瓦利亚金(VertaMedia)

Fasthttp库是标准Golang包中net / http的加速替代方案。
如何安排? 她为什么这么快?


我提请您注意Alexander Valyalkin Fasthttp客户内部报告的抄本。
Fasthttp模式可用于加快应用程序和代码的速度。



谁在乎,欢迎来猫。


我是Alexander Valyalkin。 我在VertaMedia工作。 我开发了fasthttp以满足我们的需求。 它包括http客户端和http服务器的实现。 Fasthttp比标准Go软件包中的net / http快得多。



Fasthttp是http服务器和客户端的快速实现。 位于github.com上的fasthttp



我认为很多人都听说过fasthttp服务器,它非常快。 但是很少有人听说过fasthttp客户端。 Fasthttp服务器参加了techempower的基准测试-狭窄的HTTP服务器基准测试。 Fasthttp服务器参加第12轮和第13轮。 第13轮尚未结束(2016年-大约)。



第12轮测试之一的结果,其中fasthttp几乎位于最顶端。 数字显示他每秒在此测试中进行的查询数量。 在此测试中,将请求一个返回hello world的页面。 在hello world fasthttp上非常快。



下一轮的初步结果尚未公布(2016年-约编)。 4个fasthttp实现在基准测试中排名第一,这不仅使hello world脱颖而出,而且还爬入数据库并基于模板形成html页面。



很少有人知道fasthttp客户端。 但实际上他也很酷。 在此报告中,我将向您介绍内部设备fasthttp客户端及其开发原因。



在fasthttp中实际上有几个客户端:Client,HostClient和PipelineClient。 此外,我将告诉您更多有关它们的更多信息。



Fasthttp.Client是常规的常规http客户端。 有了它,您可以向任何Internet站点发出请求,获得答案。 它的特点:它运行迅速,它可以限制每个主机的打开连接数,这与net / http包不同。 该文档位于https://godoc.org/github.com/valyala/fasthttp#Client



Fasthttp.HostClient是专用于仅与一台服务器通信的客户端。 通常,它用于访问HTTP API:REST API,JSON API。 它也可以用于代理从Internet到多台服务器上的内部DataCenter的流量代理。 该文档位于: https : //godoc.org/github.com/valyala/fasthttp#HostClient


与Fasthttp.Client一样,Fasthttp.HostClient可以限制与每个后端服务器的打开连接数。 net / http中没有此功能,而免费的nginx中也没有此功能。 据我所知,此功能仅在付费Nginx中。



Fasthttp.PipelineClient是一个专用客户端,它允许您管理对服务器或有限数量的服务器的管道请求。 它可用于通过HTTP协议访问API,在该协议中,您需要尽快执行大量请求。 Fasthttp.PipelineClient的局限性在于它可能遭受Head of Line阻塞。 这是当我们向服务器发送大量请求,而不必等待每个请求的答案时。 服务器因这些请求之一而被阻止。 因此,跟随他的所有其他请求将等待,直到该服务器处理缓慢的请求。 仅当您确定服务器将立即响应您的请求时,才应使用Fasthttp.PipelineClient。 文献资料



现在,我将开始讨论每个客户端的内部实现。 我将从Fasthttp.HostClient开始,因为几乎所有其他客户端都是基于它构建的。



这是Go上伪代码中HTTP客户端的最简单实现。 我们已经建立连接,我们在此URL上获得一个http响应。 我们正在连接到该主机。 我们获得联系。 在此代码中,它小于卷,因此缺少所有错误检查。 实际上,事实并非如此。 您应该始终检查错误。 创建一个连接。 延迟连接。 我们通过URL发送对此连接的请求。 我们收到答案,我们返回这个答案。 此HTTP客户端实现有什么问题?



第一个问题是在此实现中,为每个请求建立连接。 此实现不支持HTTP KeepAlive。 如何解决这个问题? 您可以为每个服务器使用连接池。 您不能对所有服务器使用连接池,因为下一个请求尚不清楚要发送到哪个服务器。 每个服务器必须具有自己的连接池。 我们使用HTTP KeepAlive。 这意味着“连接头”不需要指定“连接关闭”。 在HTTP / 1.1中,默认情况下支持HTTP KeepAlive,并且必须从Header中删除Connection Close。 这是带有连接池支持的客户端伪代码中的实现。 每个主机都有一组几个连接池。 第一个函数connPoolForHost从给定的URL返回给定主机的连接池。 然后,我们从该连接池中获得连接,我们计划使用Defer将该连接发送回该池,为该连接发送一个KeepAlive请求,并返回一个响应。 响应后,将执行Defer,然后连接返回到Pool。 因此,我们启用了HTTP KeepAlive支持,一切开始变得更快。 因为我们不会浪费时间为每个请求创建连接。


但是解决方案也有问题。 如果查看该函数的签名,则可以看到它为每个请求返回一个响应对象。 这意味着您需要为此对象每次分配内存,对其进行初始化并返回。 这对性能不利。 如果您对Get函数有很多这样的调用,那可能会很糟糕。



因此,可以通过将指针对象放置在此函数的参数中的响应对象上,从而解决此问题,因为在Fasthttp中已解决该问题。 这样,调用代码可以多次重用此响应对象。 在幻灯片上就是这个想法的实现。 我们将对响应对象的引用传递给Get函数-函数将填充该响应。 最后一行填充该对象。



这是代码中的外观。 接受通道的函数,该通道传递了要轮询的URL列表。 我们将在此频道上组织一个周期。 我们一次创建一个响应对象,然后循环使用它。 调用Get,将指针传递给对象,然后处理此响应。 处理完之后,将其重置为原始状态。 这样,我们避免了内存分配并加快了代码的速度。



第三个问题是连接关闭。 连接关闭-HTTP标头,可以在请求和响应中找到。 如果得到这样的标题,则应该关闭此Connection。 因此,在客户端的实现中,必须提供Connection关闭。 如果您发送的请求的标题为Connection close,则在收到响应后,您需要关闭此连接。 如果您发送的请求未关闭Connection,而返回的响应却带有Connection关闭,那么您还需要在收到响应后关闭此连接。



这是此实现的伪代码。 收到响应后,我们检查是否在此处安装了Connection close标头。 如果已安装,只需关闭连接。 如果未安装,请将连接返回到池中。 如果不这样做,那么如果服务器在返回答案后关闭了连接,则您的连接池将包含服务器关闭的断开的连接,您将尝试向其中写入内容,并且会收到错误消息。



HTTP客户端所面临的第四个问题是服务器速度慢或网络处于空闲状态。 服务器可能出于各种原因停止响应您的请求。 例如,服务器已损坏,或者客户端和服务器之间的网络已停止工作。 因此,所有调用前面描述的Get函数的goroutine将被阻塞,无限期地等待服务器的响应。 例如,如果您实现一个接受传入连接并在每个连接上调用Get函数的http代理,则会创建大量goroutine,它们将一直挂在您的服务器中,直到服务器崩溃,直到内存耗尽。



如何解决这个问题? 首先想到的是一个如此幼稚的决定-只需将此Get包装在单独的goroutine中即可。 然后在goroutine中传递一个空通道,该通道将在执行Get之后关闭。 启动此goroutine后,请在此通道上等待一段时间(超时)。 在这种情况下,如果经过了一段时间并且未执行此Get,则超时将退出该函数。 如果执行此Get,则通道将关闭并退出。 但是这一决定是错误的,因为它将问题从生病的头转移到了健康的头上。 同样,无论您使用什么超时,都会创建goroutine并将其挂起。 导致Get超时的goroutine的数量将受到限制,但是在带有超时的Get内部创建的goroutine的数量将不受限制。



如何解决这个问题? 第一种解决方案是限制Get函数中被阻止的goroutine的数量。 这可以通过使用众所周知的模式来完成,例如使用有限长度的缓冲通道,该通道将计算执行Get函数的goroutine的数量。 如果goroutine的数量超出某个限制(此通道的容量),那么我们将退出默认分支。 这意味着我们有所有执行Get的goroutine都很忙,在默认分支中,我们只需要返回Error,就没有可用资源。 在创建goroutine之前,我们尝试向该通道写入一些空结构。 如果无法解决问题,则说明我们已超出了goroutine的数量。 如果结果是事实,那么我们将创建此gorutin,并在执行Get之后,从该通道读取一个值。 因此,我们限制了Get中可以阻止的goroutine的数量。



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


如果网络在解决方案1中无法正常工作,则一切都将挂起。 在我们键入Cuncurrency并在此处挂了有限数量的goroutine之后,getimeout函数将始终返回错误。 为了使它开始正常工作,您需要第二个解决方案(解决方案2),该解决方案设置了从连接进行读取和写入的超时。 如果网络或服务器停止工作,这有助于解锁被阻止的goroutine。



解决方案1具有数据竞争。 如果“获取”被阻塞,则将占用从其传递指针的响应对象。 但是此获取超时功能可能会超时。 在这种情况下,我们退出此功能,该响应将挂起,并在一段时间后被重写。 因此,获得了数据竞争。 由于我们在退出函数后有响应,因此它仍在goroutine中的某处使用。


通过创建响应副本并将响应副本传递给goroutine可以解决该问题。 Get完成后,将响应从此响应复制到我们的原始响应中,并在此处传递。 因此,解决了数据竞争。 此响应副本将保留很短的时间,并返回到池中。 我们重用响应。 仅通过超时,响应副本可能无法放入池中。 通过超时,池中的响应丢失。



服务器在超时后未返回响应后,我是否需要关闭连接? 答案是否定的。 相反,是的,如果您要备份服务器。 因为当您将请求发送到服务器时,请等待一段时间,服务器在此期间不会响应-它无法处理请求。 例如,您关闭了此连接,但这并不意味着服务器将立即停止执行该请求。 服务器将继续执行它。 尝试向您返回响应后,服务器将检测到不需要执行此请求。 您关闭了连接,再次尝试创建新请求,再次通过超时,再次关闭,创建了新请求。 您的服务器负担将增加。 因此,您的服务取决于您的要求。 这些是http请求级别的DoS。 如果您的服务器运行缓慢,并且不想备份它们,则无需在超时后关闭连接。 您需要等待一会儿,将该服务器的连接保留为atone。 让他尝试给您答案。 同时,使用其他免费连接。 在此之前告诉我们的是Fasthttp.Client实施的所有阶段以及在Fasthttp.Client实施期间发生的问题。 在Fasthttp.HostClient中解决了这些问题。


我们现在有一个快速的客户? 不完全是 您需要查看如何实现连接池。



连接池的简单实现如下所示。 您需要在其中安装连接的某种服务器地址。 有一个空闲连接列表和一个锁,用于同步对此列表的访问。



这是从连接池获取连接的功能。 我们正在查看我们的收藏清单。 如果那里有东西,那么我们将获得免费连接并返回。 如果没有任何内容,则创建与此服务器的新连接并返回它。 怎么了


connPool.Put函数返回一个空闲连接。


在超时帐户。 在Fasthttp.Client中,您可以指定打开的未使用连接的最大生存期。 经过这段时间后,未使用的连接将自动关闭并从该池中抛出。


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


当从池中获得连接时,事实证明它的服务器已关闭,并且您尝试在其中写入内容,然后进行第二次尝试-获得新的连接,并尝试再次发送对此连接的请求。 但是,只有在请求是幂等的情况下,即,可以多次执行而对服务器没有副作用的请求才是GET或HEAD请求。 例如,刚才在标准的net / http中,我们添加了一个用于关闭连接的检查。 他们在那里做了一个棘手的检查。 当他们尝试从池中向连接发送新请求时,他们检查是否至少有一个字节发送到该连接。 如果取消,则返回错误。 如果您没有离开,那么我们会从泳池中建立一个新的连接。



游泳池怎么了? 其大小不受限制。 与net / http中的实现相同。 如果您编写的客户端从数百万个goroutine中断到运行缓慢的服务器,则该客户端将尝试创建与此服务器的百万个连接。 标准net / http程序包中的最大连接数没有限制。 对于用于通过HTTP访问API的客户端,建议限制此连接池的大小。 否则,您的客户端可能会崩溃,因为您将使用所有资源:线程,对象,连接,goroutine和内存。 同样,这可能导致服务器的DoS,因为将与服务器建立许多连接,这些连接要么未使用,要么使用效率低下,因为服务器无法容纳太多连接。



限制连接池。 该代码不在此处,因为它太大而无法放在一张幻灯片上。 那些感兴趣的人可以在github.com上看到此功能的实现。



第二个问题。 在某个时间点有很多请求到达客户端。 之后,请求数量下降,并返回到以前的请求数量。 例如,同时到达10,000个请求,则每单位时间返回的请求数为1000。 此后,连接池将增长到10000个连接。 这些连接将无休止地悬挂在那里。 1.7版之前的标准net / http客户端中存在此问题。 因此,您需要解决此问题。



通过限制未使用的连接的寿命来解决此问题。 如果一段时间内没有通过连接发送单个请求,则该请求只是关闭并从池中抛出。 因为太大,所以没有实现。



我们有一个工作又快又酷的客户? 不是那样的 我们仍然具有创建连接的功能-DialHost。



让我们看一下它的实现。 天真的实现看起来像这样。 您想要连接的地址只是简单地发送。 我们称标准功能为net.Dial。 她返回连接。 此实现有什么问题?



默认情况下,net.Dial为每个呼叫发出dns请求。 这会导致DNS子系统资源的更多使用。 如果API客户端连接到不支持KeepAlive连接的服务器,则它们将关闭连接。 KeepAlive支持您,但服务器不支持。 收到此类响应后,服务器将关闭连接。 事实证明,net.Dial在每个请求上都会被调用。 每秒大约有1万个此类请求。 您每秒有1万次以dns解析的速度。 这将加载DNS子系统。



如何解决这个问题? 在您的Go代码中创建一个短时间直接在IP中映射它的主机的缓存,并且不要在每个net.Dial上调用dns解析。 连接到现成的IP地址。



第二个问题是,如果域名后面隐藏了多个服务器,则服务器上的负载不均衡。 例如,像Round Robin DNS。 如果您在DNS中缓存一个IP地址一段时间,那么在此期间,所有请求都将发送到一台服务器。 尽管那里可能有几个。 有必要解决这个问题。 通过枚举隐藏在给定域名后面的所有可用IP来解决。 在Fasthttp.Client中也可以做到这一点。



第三个问题是由于您尝试连接的网络或服务器出现问题,net.Dial也可能无限期挂起。 在这种情况下,您的goroutine将挂在Get函数上。 这也可能导致资源使用增加。


解决方案是添加超时。或使用标准包网络中的超时拨号。但是,据我所知,它的实现不正确。也许他们已经修复了它,但是较早时已按照我告诉您的方法实施了。



这就是它的实现方式。而不是获取有拨号功能。它是在某种goroutine中执行的。如果Dial挂起,则表明goroutine已累积。挂起的goroutine的数量可能会无限期增长。这是DialTimeout的标准实现。也许他们已经解决了。



此外,HostClient具有以下功能。


HostClient可以在您指定的服务器列表上分配负载。因此,实现了基本的LoadBalance。


HostClient . , HostClient . connection . . .


Fauly host .


— . Dial. , Dial. Get, , - . , . , , .


— . Get , . , , , .


Error , Round Robin .


SSL , Golang . .



fasthttp.Client. HostClient, fasthttp.Client HostClient.



Get. HostClient . HostClient . HostClient Get. HostClient.



HostClient - , URL. web-crawling ( ), . HostClient . net/http, . , HostClient, . fasthttp.



Client HostClient, PipelineClient . PipelineClient connection pool. PipelineClient connection, . PipelineClient connection. connection pool. PipelineClient connection .



PipelineClient connection . PipelineConnClient.writer — connection, . PipelineConnClient.reader — connection , PipelineConnClient.writer. PipelineConnClient.reader , Get.



PipelineClient.Get PipelineClient. pipelineWork url, , response, channel done, response.


Get. C . channel, PipelineConnClient.writer connection. channel w.done, PipelineConnClient.reader, response request.



net/http fasthttp.Client 2 .



, , fasthttp. , , . fasthttp. , fasthttp, . allocation . .



net/http. , allocation net/nttp. .



: PipelineClient connection?


: — pending , . . request, pending , Error.


: API , fasthttp, net/http?


: . net/http . . string -, string . , net/http, . - , . fasthttp , . . net/http fasthttp , net/http POST-, response, () . fasthttp , request response . 10 request 10 response . , . fasthttp 10 request 10 response? . — . , net/http. . , net/http — .


PS .


.


— .

Source: https://habr.com/ru/post/zh-CN443378/


All Articles