高并发HTTP请求客户端 – python语言编写

  • Post author:
  • Post category:python


20200902 –

(这篇文章其实应该放在高并发、服务器性能一类的专栏下,但是暂时没有,不过后续还会研究利用C编程来进行这部分的内容,就放在这个下面)



0. 引言

之前的时候接触过高并发相关的内容,当时接触了类似epool这种编程模型,或者更高级的libev这种事件编程;但是这些都是面向服务端的内容,类似C10K这种经典问题。而对于客户端来说,虽然有很多不同语言或者不同类型的压测工具,但很少有介绍自己来编写这部分内容的。通过百度搜索也可以看到相应的结果。

百度搜索结果

也可能是搜索的关键词不对,反正从百度的搜索结果来看,基本上没有什么有效的结果。但是,

直接在谷歌上搜索,是可以找到相应的博文的

。所以,本篇文章从搜索得到的结果来入手,学习如何满足这部分需求。



1. 需求分析

利用python语言编写HTTP客户端,通过类似多线程/多进程,不管是什么手段,实现高并发的并发的连接来请求服务器。虽然有很多压测工具可以使用,但是这里并不是为了测试服务器的能力,而是能模拟出大量HTTP请求,而且说不定还要控制一些内部的逻辑,但是这里不进行深入探讨,只介绍如何模拟出针对某网页的高并发HTTP请求。先来列举一下自己当前能想到的改进方式。



1.1 改进方式(多线程)

如果要达到这样的高并发目的,那么必然不能使用阻塞式的通信方式,必须能够在等待时间过程中,同时能够进行其他的任务,这种需求当然可以利用多线程来实现,多个线程同时请求,并等待结果结果返回退出。



1.2 改进方式(复用TCP连接)

前面多线程的没有高效利用HTTP复用TCP连接的过程,当然可以通过控制复用tcp连接来实现,例如request的session部分。



1.3 改进方式(HTTP流水线)

在HTTP1.1中,可以使用HTTP长连接的方式,同时可以采用流水线技术。

使用HTTP 流水线技术(HTTP pipelining,也有翻译为管道化连接),它是指,在一个TCP连接内,多个HTTP请求可以并行,下一个HTTP请求在上一个HTTP请求的应答完成之前就发起[1]



2. 谷歌的搜索结果记录

在谷歌上搜索到了很多相关的结果(限定使用python语言),下面进行具体介绍。



2.1

Python and fast HTTP clients

本篇文章从基础的角度进行了讲解,算是一篇入门比较好的文章,逐一讲解了各种优化方案,下面具体来介绍。大致上是翻译该篇文章,同时记录一些自己的思考。



2.1.1 利用长连接(TCP复用)

前面提到,HTTP1.1之后可以使用长连接,也就是客户端可以长时间连接,复用该部分TCP连接。平时在进行爬虫任务的时候主要使用

requests

库,而对于

requests

库来说,复用的过程就是使用其提供的Session对象。需要注意的式,如果不是用Session的话,在请求完成之后,该TCP连接就断开了。

import requests

session = requests.Seession()
session.get("https://www.baidu.com")
session.get("https://www.baidu.com")

在文章中提到,这种方式提供了以下这些好处:

  • 通过减少并发连接,降低了CPU和内存利用
  • 降低TCP握手阶段的延迟
  • 当有异常发生时,不用关闭TCP连接

同时前文中提到HTTP支持流水线技术,但是

requests

库并不支持这种方式。



2.1.2 并行化

在前面的例子中,

requests

的缺点就是他是同步的,每次进行了

get

请求都必须等待这个函数结束,可以通过并行化的方式,也就是多线程的方式来实现。文章中提供了两种方式来讲解。

from concurrent import futures
import requests
with futures.ThreadPoolExecutor(max_workers=4) as executor:
    futures = [
        executor.submit(
            lambda: requests.get("http://example.org"))
        for _ in range(8)
    ]
results = [
    f.result().status_code
    for f in futures
]
print("Results: %s" % results)
from requests_futures import sessions
session = sessions.FuturesSession()
futures = [
    session.get("http://example.org")
    for _ in range(8)
]
results = [
    f.result().status_code
    for f in futures
]
print("Results: %s" % results)



2.1.3 异步化

前面的方式中提到,

requests

本身是阻塞的,即使是多线程也有自己的缺点,比如线程消耗等;而在python3.5之后,就提供了一种异步的编程方式

asyncio

,同时

aiohttp

库在其之上提供了异步的HTTP客户端,

其提供了类似HTTP流水线的方式来并行在多个连接上发送请求

(在aiohttp官网上,明确指出,

aiohttp不支持HTTP流水线

)。关于这部分具体的内容后面进行展开,先看代码。

import aiohttp
import asyncio
async def get(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return response
loop = asyncio.get_event_loop()
coroutines = [get("http://example.com") for _ in range(8)]
results = loop.run_until_complete(asyncio.gather(*coroutines))
print("Results: %s" % results)

上述的两个库就是本部分要学习的核心了,python3支持的异步IO,以及在其之上的aiohttp库;我昨天的时候也尝试学习这两个内容,就光python asyncio就有非常的大篇幅学习。

这里不对python异步编程的部分进行展开,网上可以找到大量的文章来说明这部分问题,本次学习中主要阅读了文章[2]。这里记录一下阅读之后的主要感觉,首先async在python是一种特殊的生成器,这也是之前的python中要使用该特性是以生成器的方式来调用;然后,这种概念叫做协程,本质上一个程序如果没有显示的调用线程的内容,async就是一个单进程单线程的程序;最后,每次调用await之后,就会将该部分内容返回调用该程序的部分,然后先继续执行。当然,最后等之前异步的操作执行完成后,使用权还是会回去。

有一个主要的监视器来监视他们是否做好了,做好了就把使用权给过去。这可能也是为什么在执行大量的HTTP请求之后,CPU占用率特别高的原因。




2.2 其他的搜索结果

在写这篇文章的时候,还有几篇其他的文章作为了主要参考,这里因为时间问题暂时不再进行具体的描述;先记录文章,后续再进行分析。但是实际上,核心都是利用python的async和aiohttp。

https://pawelmhm.github.io/asyncio/python/aiohttp/2016/04/22/asyncio-aiohttp.html

https://www.artificialworlds.net/blog/2017/06/12/making-100-million-requests-with-python-aiohttp/

https://medium.com/@cgarciae/making-an-infinite-number-of-requests-with-python-aiohttp-pypeln-3a552b97dc95

这三篇文章都是围绕aiohttp编程,实际上没有什么大的变动,但是其中有一些可能API有些变动,其次就是内存的占用,因为这个东西每次都是for循环生成大量的生成器,所以导致非常慢,第三个文章是对前面的改动的,但是我测试了之后也没有得到非常好的结果。

https://github.com/flyakite/100-million-requests-aiohttp

这个就是前面三个网址中,第二个的内容,我就是使用的这个角度来进行自己的测试。

为了有一定的参考价值,利用nginx进行测试,然后发现开启40个进程同时跑这个工具,然后CPU每次都占满了,最大的的时候能到大致4w5的并发数把。但是使用另外一个压测工具wrk的时候,能够达到400K的性能,甚至最后我再进行调优的时候,能够达到了1050K的性能。

可以发现python的这个异步性能还是比不了wrk的这种异步事件编程方式。

关于这边内容,后续还会具体展开,毕竟,为了调试这个东西也是花费了很长时间。


Speeding Up Python with Concurrency, Parallelism, and asyncio


这篇文章是关于多线程/进程等一系列其他内容的研究,然后从结合了asyncio等内容的调优,有时间一定好好看看。



参考

[1]

HTTP的长连接和短连接


[2]

Async IO in Python: A Complete Walkthrough



版权声明:本文为u011698800原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。