Python解決asyncio文件描述符最大數(shù)量限制的問題
問題復(fù)現(xiàn)
Windows 平臺(tái)下,Python 版本 3.5,使用異步框架 asyncio,有時(shí)候會(huì)出現(xiàn) ValueError: too many file descriptors in select() 的報(bào)錯(cuò)信息,我們就來聊一下為什么會(huì)出現(xiàn)這種問題,以及問題的一些解決方法。
寫一個(gè)小 dome 復(fù)現(xiàn)這個(gè)問題(環(huán)境:Windows 64 位、Python 3.7):
import aiohttp
import asyncio
num = 0
async def main(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
global num
num += 1
print('%s ——> %s' % (str(num), response.status))
def tasks():
url = 'https://www.baidu.com/s?ie=UTF-8&wd=%s'
task = [main(url % i) for i in range(10000)]
return task
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks()))
在打印 500 次左右后就會(huì)出現(xiàn)以下報(bào)錯(cuò):

問題分析
好像這個(gè)報(bào)錯(cuò)和 select 有關(guān),那什么是 select 呢?要怎么解決呢?別急,我們首先來了解一下 asyncio 中的事件循環(huán),即 EventLoop。
事件循環(huán) EventLoop
事件循環(huán)是 asyncio 的核心,異步任務(wù)的運(yùn)行、任務(wù)完成之后的回調(diào)、網(wǎng)絡(luò) I/O 操作、子進(jìn)程的運(yùn)行,都是通過事件循環(huán)完成的,通俗來講,事件循環(huán)所做的就是等待事件發(fā)生,然后再將每個(gè)事件與我們已明確與所述事件類型匹配的函數(shù)進(jìn)行匹配。
下圖很好的展示了協(xié)程、事件循環(huán)之間的相互作用:

在 asyncio 中,主要提供了兩種不同事件循環(huán)的實(shí)現(xiàn)方法:
- SelectorEventLoop:基于 selectors 模塊的事件循環(huán),selectors 又是建立在底層的 I/O 復(fù)用模塊 select 之上的,selectors 提供了高度封裝和高效的 I/O 復(fù)用,也就是說 SelectorEventLoop 在底層就是使用了 select I/O 多路復(fù)用的機(jī)制。
- ProactorEventLoop:使用 IOCP 專為 Windows 構(gòu)建的事件循環(huán),IOCP 全稱 I/O Completion Port,即 I/O 完成端口。它是支持多個(gè)同時(shí)發(fā)生的異步 I/O 操作的應(yīng)用程序編程接口,它充分利用內(nèi)核對(duì)象的調(diào)度,只使用少量的幾個(gè)線程來處理和客戶端的所有通信,消除了無謂的線程上下文切換,是 Windows 下性能最好的 I/O 模型,有關(guān) IOCP 的詳細(xì)介紹可參考微軟文檔。
那么這兩種方法有什么區(qū)別呢?在 asyncio 中什么時(shí)候用什么方法呢?
我們不妨看一下 asyncio 的源碼,在 Python 3.7 中,無論在 Windows 還是 Linux 中都可以看到其默認(rèn)的設(shè)置是 SelectorEventLoop:

我們也可以分別在 Windows 平臺(tái)和 Linux 平臺(tái)打印一下 EventLoop 對(duì)象(Python 3.7),可以看到默認(rèn)都是 SelectorEventLoop:
import asyncio loop = asyncio.get_event_loop() print(loop)
- Windows:

- Linux:

事實(shí)上,在 Python 3.7 以及之前的版本中, 所有平臺(tái)默認(rèn)使用的都是 SelectorEventLoop,在 Python 3.8 以及以后的版本中,Unix 平臺(tái)默認(rèn)使用的是 SelectorEventLoop,Windows 平臺(tái)默認(rèn)使用的是 ProactorEventLoop,這個(gè)差異可以在官方文檔中看到。
- Python 3.7 文檔:https://docs.python.org/3.7/library/asyncio-eventloop.html#event-loop-implementations
- Python 3.8 文檔:https://docs.python.org/3.8/library/asyncio-eventloop.html#event-loop-implementations

說了這么多,這和 ValueError: too many file descriptors in select() 的報(bào)錯(cuò)問題有什么關(guān)系呢?select 到底是什么東西呢?
I/O 多路復(fù)用
要了解 select,我們還要了解一下什么是 I/O 多路復(fù)用(I/O multiplexing),服務(wù)器端編程經(jīng)常需要構(gòu)造高性能的 I/O 模型,常見的 I/O 模型有同步阻塞 I/O、同步非阻塞 I/O、I/O 多路復(fù)用等;當(dāng)需要同時(shí)處理多個(gè)客戶端接入請(qǐng)求時(shí),可以利用多線程或者 I/O 多路復(fù)用技術(shù)進(jìn)行處理,I/O 多路復(fù)用技術(shù)就是為了解決進(jìn)程或線程阻塞到某個(gè) I/O 系統(tǒng)調(diào)用而出現(xiàn)的技術(shù),使進(jìn)程不阻塞于某個(gè)特定的 I/O 系統(tǒng)調(diào)用。
select,poll,epoll 等都是 I/O 多路復(fù)用的一種機(jī)制,其中后兩個(gè)在 Linux 中可用,Windows 僅支持 select,I/O 多路復(fù)用通過這種機(jī)制,可以監(jiān)視多個(gè)描述符,一旦某個(gè)描述符就緒,一般是讀就緒或者寫就緒,就是在這個(gè)文件描述符進(jìn)行讀寫操作之前,能夠通知程序進(jìn)行相應(yīng)的讀寫操作。
select 的缺點(diǎn)
I/O 多路復(fù)用這個(gè)概念被提出來以后, select 是第一個(gè)實(shí)現(xiàn)這個(gè)概念的,select 被實(shí)現(xiàn)以后,很快就暴露出了很多問題,其中一個(gè)缺點(diǎn)就是 select 在 Windows 中限制了文件描述符數(shù)量為 512 個(gè),在 Linux 中限制為 1024 個(gè),那么在前面的 dome 中,使用的是 Python 3.5,這個(gè)版本的 asyncio 默認(rèn)使用了 SelectorEventLoop,底層調(diào)用的是 select,受 select 缺點(diǎn)的影響,并發(fā)量過高,就出現(xiàn)了 ValueError: too many file descriptors in select() 的報(bào)錯(cuò)信息。
解決方法
1.更換事件循環(huán)選擇器
如果你使用的是 Python 3.7 及以下的版本,那么在 Windows 平臺(tái),可以使用 ProactorEventLoop。在 Linux 平臺(tái)可以使用 PollSelector。
注意:如果你使用了 ProactorEventLoop,那么你將無法使用代理!這是 asyncio 的 bug,早在 2020 年 1 月就有人提過 issue,目前仍然可以看到類似的 issue,官方貌似也還沒辦法解決,所以,如果您必須要使用代理,則可以參考后面的解決辦法。
import selectors
import asyncio
import sys
if sys.platform == 'win32':
loop = asyncio.ProactorEventLoop()
asyncio.set_event_loop(loop)
else:
selector = selectors.PollSelector()
loop = asyncio.SelectorEventLoop(selector)
asyncio.set_event_loop(loop)
2.限制并發(fā)量
可以使用方法 asyncio.Semaphore() 來限制并發(fā)量,Semaphore 就是信號(hào)量的意思,Semaphore 管理一個(gè)內(nèi)部計(jì)數(shù)器,該計(jì)數(shù)器在每次調(diào)用 acquire() 方法時(shí)遞減,每次調(diào)用 release() 方法時(shí)遞增,計(jì)數(shù)器永遠(yuǎn)不會(huì)低于零,當(dāng)方法 acquire() 發(fā)現(xiàn)它為零時(shí),它會(huì)阻塞,等待其他線程調(diào)用 release() 方法。
通過限制并發(fā)量的方法來解決報(bào)錯(cuò)問題是個(gè)不錯(cuò)的選擇。
import aiohttp
import asyncio
num = 0
async def main(url, semaphore):
async with semaphore:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
global num
num += 1
print('%s ——> %s' % (str(num), response.status))
def tasks():
semaphore = asyncio.Semaphore(300) # 限制并發(fā)量為 300
url = 'https://www.baidu.com/s?ie=UTF-8&wd=%s'
task = [main(url % i, semaphore) for i in range(10000)] # #總共 10000 任務(wù)
return task
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks()))
3.修改最大文件描述符限制Windows
在 Windows 中,最大文件描述符限制在 C 語言的頭文件 Winsock2.h 中使用變量 FD_SETSIZE 進(jìn)行定義,如果要修改它,可以通過在包含 Winsock2.h 之前將 FD_SETSIZE 定義為另一個(gè)值來修改,如果我們使用的編程語言是 Python 的話,是不太好對(duì)這個(gè)值進(jìn)行修改的,可以參考微軟官方文檔:https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select
Linux
在 Linux 平臺(tái),可以使用 ulimit 命令來修改最大文件描述符限制:
- 查看當(dāng)前會(huì)話最大文件描述符限制(默認(rèn)1024):
ulimit -n - 臨時(shí)修改限制,只對(duì)當(dāng)前的會(huì)話有效:
ulimit -SHn 65536 - 永久修改限制,在
/etc/security/limits.conf文件里新增以下內(nèi)容:
* hard nofile 65536 * soft nofile 65536
ulimit 命令參考:
- -S使用軟 (soft) 資源限制
- -H使用硬 (hard) 資源限制
- -a所有當(dāng)前限制都被報(bào)告
- -b套接字緩存尺寸
- -c創(chuàng)建的核文件的最大尺寸
- -d一個(gè)進(jìn)程的數(shù)據(jù)區(qū)的最大尺寸
- -e最高的調(diào)度優(yōu)先級(jí) (nice)
- -f有 shell 及其子進(jìn)程可以寫的最大文件尺寸
- -i最多的可以掛起的信號(hào)數(shù)
- -k分配給此進(jìn)程的最大 kqueue 數(shù)量
- -l一個(gè)進(jìn)程可以鎖定的最大內(nèi)存尺寸
- -m最大的內(nèi)存進(jìn)駐尺寸
- -n最多的打開的文件描述符個(gè)數(shù)
- -p管道緩沖區(qū)尺寸
- -qPOSIX 信息隊(duì)列的最大字節(jié)數(shù)
- -r實(shí)時(shí)調(diào)度的最大優(yōu)先級(jí)
- -s最大棧尺寸
- -t最大的CPU時(shí)間,以秒為單位
- -u最大用戶進(jìn)程數(shù)
- -v虛擬內(nèi)存尺寸
- -x最大的文件鎖數(shù)量
- -P最大偽終端數(shù)量
- -T最大線程數(shù)量
總結(jié)
asyncio 事件循環(huán)選擇器,在 Python 3.7 以及之前的版本中,所有平臺(tái)默認(rèn)使用的都是 SelectorEventLoop,在 Python 3.8 以及以后的版本中,Unix 平臺(tái)默認(rèn)使用的是 SelectorEventLoop,Windows 平臺(tái)默認(rèn)使用的是 ProactorEventLoop。
select 在 Windows 中限制了文件描述符最大數(shù)量為 512 個(gè),在 Linux 中限制為 1024 個(gè)。
要解決 ValueError: too many file descriptors in select() 的報(bào)錯(cuò)問題,根據(jù)您的平臺(tái)和業(yè)務(wù)要求選擇合理的解決方法:
Windows
- 通過
asyncio.Semaphore()方法來限制并發(fā)量,通常設(shè)置在 300-500 比較合理,這是最優(yōu)的做法; - 更換 asyncio 的事件循環(huán)選擇器為 ProactorEventLoop,注意:這將導(dǎo)致無法使用代理!
Linux
- 通過
asyncio.Semaphore()方法來限制并發(fā)量,通常設(shè)置在 800-1000 比較合理; - 通過
ulimit命令來修改最大文件描述符限制; - 更換 asyncio 的事件循環(huán)選擇器為 PollSelector。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Python3如何使用range函數(shù)替代xrange函數(shù)
這篇文章主要介紹了Python3如何使用range函數(shù)替代xrange函數(shù),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10
python支持?jǐn)帱c(diǎn)續(xù)傳的多線程下載示例
這篇文章主要介紹了python支持?jǐn)帱c(diǎn)續(xù)傳的多線程下載示例,大家參考使用吧2014-01-01
python實(shí)現(xiàn)數(shù)通設(shè)備端口監(jiān)控示例
這篇文章主要介紹了python實(shí)現(xiàn)數(shù)通設(shè)備端口監(jiān)控示例,需要的朋友可以參考下2014-04-04
構(gòu)建Python中的分布式系統(tǒng)結(jié)合Celery與RabbitMQ
在本文中,我們深入探討了如何利用Celery和RabbitMQ構(gòu)建Python中的分布式系統(tǒng),我們首先介紹了Celery和RabbitMQ的概念及其優(yōu)勢,然后展示了如何結(jié)合它們來創(chuàng)建一個(gè)簡單但功能強(qiáng)大的分布式系統(tǒng),感興趣的朋友跟隨小編一起看看吧2024-05-05
python scatter散點(diǎn)圖用循環(huán)分類法加圖例
這篇文章主要為大家詳細(xì)介紹了python scatter散點(diǎn)圖用循環(huán)分類法加圖例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-03-03
matlab和Excel的數(shù)據(jù)交互操作(非xlsread和xlswrite)
在使用MATLAB時(shí),可能會(huì)遇到很多表格數(shù)據(jù)的處理,有時(shí)MATLAB也需要利用現(xiàn)存的表格數(shù)據(jù)實(shí)現(xiàn)操作目的,下面這篇文章主要給大家介紹了關(guān)于matlab和Excel的交互操作的相關(guān)資料,非xlsread和xlswrite,需要的朋友可以參考下2021-08-08
Python連接打印機(jī)實(shí)現(xiàn)自動(dòng)化打印的實(shí)用技巧和示例代碼
在計(jì)算機(jī)科學(xué)領(lǐng)域,打印機(jī)是一種重要的外部設(shè)備,用于將電子文檔轉(zhuǎn)換成實(shí)際的紙質(zhì)文件,下面這篇文章主要給大家介紹了關(guān)于Python連接打印機(jī)實(shí)現(xiàn)自動(dòng)化打印的實(shí)用技巧和示例代碼,需要的朋友可以參考下2024-05-05

