本文转载请联系小菜雪编程公众号。C10K问题在互联网普及之前的早期,一个服务器有100个用户同时在线被认为是一个非常大的应用,不存在工程上的挑战。随着Web2.0时代的到来,用户群呈指数级增长,服务器需要更强的并发处理能力来应对海量用户。这时,著名的C10K问题诞生了——如何让单个服务器同时支持10000个客户端连接?原来的服务器应用程序编程模型是基于进程/线程的:当一个新的客户端连接时,服务器会分配一个进程或线程来处理这个新的连接。这意味着要解决C10K问题,操作系统需要同时运行10000个进程或线程。进程和线程是操作系统中最昂贵的资源之一。每一个新的连接都会开启一个新的进程/线程,这会造成资源的极大浪费。而且,受限于硬件资源,系统可以同时运行的进程/线程数是有上限的。也就是说,在进程/线程模型中,每个服务器所能处理的客户端连接数是非常有限的。为了支撑海量业务,只能通过简单粗暴的服务器堆叠方式来实现。但这样的人山人海战术既不稳定也不经济。为了在单个进程/线程中同时处理多个网络连接,select、poll、epoll等IO多路复用技术应运而生。在IO多路复用模型中,进程/线程不再阻塞在某个连接上,而是同时监听多个连接,只处理那些有新数据到达的活跃连接。为什么需要协程?纯IO多路复用编程模型不如阻塞式编程模型直观,给工程项目带来很多不便。最典型的例子就是JavaScript中的回调编程模型。各种回调函数在程序中飞来飞去。这不是一种直观的思维方式。为了实现像阻塞一样直观的编程模型,协程(用户态线程)的概念被提出。协程在进程/线程的基础上实现了多个执行上下文。由epoll等IO多路复用技术实现的事件循环负责驱动协程的调度和执行。协程可以看作是对IO多路复用技术的更高层次的封装。虽然与原始IO多路复用相比有一定的性能开销,但与进程/线程模型相比它很突出。协程占用的资源比进程/线程少,切换成本也比较低。因此,协程在高并发应用中有着无限的潜力。但是,协程独特的运行机制让初学者吃了不少苦头,错漏百出。接下来,我们通过一些简单的例子来探索协程的应用,了解协程的作用,揭示在高并发应用的设计和部署中常见的误区。由于asyncio是Python协程发展的主要趋势,所以本例以asyncio为讲解对象。第一个协程应用协程应用由事件循环驱动,socket必须是非阻塞模式,否则事件循环会阻塞。所以,一旦你使用了协程,你就不得不和很多类库说拜拜了。以MySQL数据库操作为例,如果我们使用asyncio,则需要使用aiomysql包来连接数据库。如果要开发web应用,可以使用aiohttp包,可以通过pip命令安装:$pipinstallaiohttp这个例子实现了一个完整的web服务器,虽然它只有返回当前时间的功能:fromaiohttpimportwebfromdatetimeimportdatetimeasyncdefhandle(request):returnweb.Response(text=datetime.now().strftime('%Y-%m-%d%H:%M:%S'))app=web.Application()app.add_routes([web.get('/',handle),])if__name__=='__main__':web.run_app(app)第4行,实现处理函数,获取当前时间并返回;第7行,创建应用对象,并将处理函数注册到路由中;第13行,运行Web应用程序,默认端口为8080;当有新的请求到来时,aiohttp会创建一个新的协程来处理这个请求,并负责执行相应的处理函数。因此,处理函数必须是一个有效的协程函数,以async关键字开头。运行程序后,我们可以通过它获取当前时间。在命令行可以使用curl命令发起请求:$curlhttp://127.0.0.1:8080/2020-08-0615:50:34压力测试开发高并发应用需要评估处理能力应用程序。我们可以在短时间内发起大量请求,衡量应用的吞吐量。但是,手再快,每秒也只能发起几个请求。怎么做?我们需要用到一些压力测试工具,比如Apache工具集中的ab。如何安装和使用ab不在本文讨论范围内,请参考本文:Web压测(https://network.fasionchan.com/zh_CN/latest/performance/web-pressure-test.html)。事不宜迟,我们先以100为并发数,压10000个请看结果:$ab-n10000-c100http://127.0.0.1:8080/ThisisApacheBench,Version2.3<$Revision:17060009$,http://www.zeustech.net/LicensedtoTheApacheSoftwareFoundation,http://www.apache.org/Benchmarking127.0.0.1(bepatient)Completed1000requestsCompleted2000requestsCompleted3000requestsCompleted4000requestsCompleted5000requestsCompleted6000requestsCompleted7000requestsCompleted8000requestsCompleted9000requestsCompleted10000requestsFinished10000requestsServerSoftware:Python/3.8ServerHostname:127.0.0.1ServerPort:8080DocumentPath:/DocumentLength:19bytesConcurrencyLevel:100Timetakenfortests:5.972secondsCompleterequests:10000Failedrequests:0Totaltransferred:1700000bytesHTMLtransferred:190000bytesRequestspersecond:1674.43[#/sec](mean)Timeperrequest:59.722[ms](mean)Timeperrequest:0.597[ms](mean,ascrossall7bytesrequest/2KTransferrequest/2Kbytes.request8/second)receivedConnectionTimes(ms)minmean[+/-sd]medianmaxConnect:021.5115Processing:43585.05789Waiting:29476.34785Total:43604.85890Percentageoftherequestsservedwithincertaintime(ms)50%5866%5975%6080%6190%6595%6998%7299%8510long请求总计(specifiedthatis%0request),总共发送了多少个请求;-c选项指定并发数,即同时发送多少个请求;从ab输出的报告可以知道,10000个请求全部成功,共耗时5.972秒,处理速度可以达到1674.43现在,我们尝试提供并发数,看看处理速度有没有提升:$ab-n10000-c100http://127.0.0.1:8080/1000并发下,10000个请求用时5.771秒,处理速度为1732.87,略有提升但不明显。这一点也不奇怪。例子中大部分处理逻辑都是计算性的,夸大并发数几乎没有意义。协程擅长什么协程擅长处理IO类型的应用逻辑。例如,当一个协程正在等待数据库的响应时,事件循环会唤醒另一个准备好的协程来执行,从而提高吞吐量。为了降低复杂度,我们通过在程序中休眠来模拟等待数据库的效果。importasynciofromaiohttpimportwebfromdatetimeimportdatetimeasyncdefhandle(request):#休眠一秒asyncio.sleep(1)returnweb.Response(text=datetime.now().strftime('%Y-%m-%d%H:%M:%S'))app=web.Application()app.add_routes([web.get('/',handle),])if__name__=='__main__':web.run_app(app)并发请求总数耗时(秒)处理speed(Requests/second)10010000102.31097.745001000022.129451.8910001000012.780782.50可以看出,随着并发数的增加,处理速度也有显着提升,且趋势接近线性。
