面試:Redis 是單線程,是怎么解決高并發(fā)問題的
你好,我是強壯的病貓,在這里分享面經(jīng)。這不,最近又面了一家公司,又是被虐,有幾道題貓哥一時語塞,今天分享給你,以后碰到這類問題時可以試試反虐。
首先,得說下,無論哪一次面試,貓哥必然會被問到兩個問題:
2-5 分鐘的自我介紹。如果是外企或跨國企業(yè)或大廠,如果你能用英文流暢的自我介紹,必然是加分項,朋友們,離開校園后,英語的學習可別放棄。
你印象最深刻的一次問題解決經(jīng)歷,或者說你最有成就感的一次經(jīng)歷。這個通過你的描述,看看你對技術(shù)的興趣,看看你解決問題的方法論,當然還有口語表達能力。
這兩個問題還沒有哪次面試不遇到的,要面試的同學,可要好好準備,多排練下,人生如戲,全靠演技。
然后,說下這次遇到的幾個問題:
1. uWSGI 生產(chǎn)環(huán)境的配置有兩種方式一種是 socket,一種是 http,兩種方式有什么區(qū)別?為什么你用 socket 而不用 http?
我當時直接說這個不太清楚為什么 socket 更好。一臉懵逼,懊惱自己當時只顧著這樣用卻不多想一下為什么要這樣用。
參考回答:
通常情況下,Nginx 與 uWSGI 一起工作,Nginx 處理靜態(tài)文件,將動態(tài)的接口請求轉(zhuǎn)發(fā)給 uWSGI。這就是涉及 Nginx 與 uWSGI 以何種協(xié)議進行通信,Nginx 的 uwsgi_pass 選項告訴它使用特殊的 uWSGI 協(xié)議,而這種協(xié)議就是 uWSGI 的套接字使用的默認協(xié)議。
uwsgi.ini 示例:
- [uwsgi]
- master = true
- chdir= /root/KeJiTuan/rearEnd
- socket = :8000
- #http = :8000
- socket = %(chdir)/uwsgi.socket
- wsgi-file = rearEnd/wsgi.py
- processes = 1
- threads = 4
- virtualenv = /root/KeJiTuan/env
- static-map = /static=/root/KeJiTuan/frontEnd/dist/static
- stats = %(chdir)/uwsgi.status
- pidfile = %(chdir)/uwsgi.pid
- daemonize = %(chdir)/uwsgi.log%
如果你使用 http 選項配置 uWSGI,這樣 uWSGI 本身就可以對外提供 http 服務,不會做任何有用的事情,這樣的話,就需要將 NGINX 配置為使用 HTTP 與 uWSGI 對話,并且 NGINX 將不得不重寫標頭以表示它正在代理,并且最終會做更多的工作,因此性能不如 socket 方式。
也就是說,配置為 socket 其實用的就是 TCP 協(xié)議,配置為 http 用的就是 HTTP 協(xié)議,TCP 是傳輸層協(xié)議,更底層,程序處理的報文更小,性能更快,而 HTTP 是建立在 TCP 之上的應用層協(xié)議,需要處理更多的報文封裝與解碼。
因此生產(chǎn)環(huán)境 uWSGI 首選 socket 配置。
2. redis 是單線程,是怎么解決高并發(fā)問題的?
這個我當時是這樣回答的:單線程想高并發(fā),就是用到了類似 nginx 的事件循環(huán)之類的技術(shù)。
參考回答:
redis 是基于內(nèi)存的,內(nèi)存的讀寫速度非常快(純內(nèi)存); 數(shù)據(jù)存在內(nèi)存中,數(shù)據(jù)結(jié)構(gòu)用 HashMap,HashMap 的優(yōu)勢就是查找和操作的時間復雜度都是 O(1)。
redis是單線程的,省去了很多上下文切換線程的時間(避免線程切換的資源消耗)。
redis 使用 I/O 多路復用技術(shù),可以處理高并發(fā)的連接(非阻塞I/O)。(如果你懂 I/O 多路復用,可以展開講一講,展示你鉆研的深度)
寫到這里,貓哥自己也產(chǎn)生了疑問,什么是事件循環(huán),什么是 I/O 多路復用,兩者有什么關(guān)系?于是找了找學習資料,整理如下,如有反對意見,請文末留言討論。
事件循環(huán)是一種編程范式,通常,我們寫服務器處理模型的程序時,有以下幾種模型:
(1)每收到一個請求,創(chuàng)建一個新的進程,來處理該請求;(2)每收到一個請求,創(chuàng)建一個新的線程,來處理該請求;(3)每收到一個請求,放入一個事件列表,讓主進程通過非阻塞 I/O 方式來處理請求;
第三種,就是事件驅(qū)動的方式,比如 Python 中的 協(xié)程就是事件循環(huán),也大多數(shù)網(wǎng)絡服務器采用的方式比如 Nginx。
比如說 javascript 吧,一大特點就是單線程,那為什你沒有覺得瀏覽器中的 javascript 慢呢?肯定沒有,對吧,因為 javascript 在處理 DOM 時也用到了事件循環(huán)。
單線程就意味著,所有任務需要排隊,前一個任務結(jié)束,才會執(zhí)行后一個任務。如果前一個任務耗時很長,后一個任務就不得不一直等著。但是如果任務是計算型任務,CPU 忙不過來,等就等了,如果是 I/O 型任務,主線程完全可以不管 I/O 設備,而是掛起處于等待中的任務,先運行排在后面的任務。等到 I/O 設備返回了結(jié)果,把掛起的任務繼續(xù)執(zhí)行下去。
也就是說主線程之外,有一個任務隊列,只要異步任務(異步 I/O)有了結(jié)果,就在任務隊列中放置一個事件,主線程中任務執(zhí)行完就會去任務隊列取出有結(jié)果的異步任務執(zhí)行,具體過程如下圖所示:
因為整個過程是不斷循環(huán)的,這種運行機制又稱事件循環(huán)。到這里,相信你已經(jīng)對事件循環(huán)有一個比較清晰的印象了。
那什么是 I/O 多路復用?這里借用下知乎的高贊回答:
作者:柴小喵 鏈接:https://www.zhihu.com/question/28594409/answer/52835876 來源:知乎。
下面舉一個例子,模擬一個 tcp 服務器處理 30 個客戶 socket。假設你是一個老師,讓 30 個學生解答一道題目,然后檢查學生做的是否正確,你有下面幾個選擇:1. 第一種選擇:按順序逐個檢查,先檢查 A,然后是 B,之后是 C、D。。。這中間如果有一個學生卡住,全班都會被耽誤。這種模式就好比,你用循環(huán)挨個處理 socket,根本不具有并發(fā)能力。2. 第二種選擇:你創(chuàng)建 30 個分身,每個分身檢查一個學生的答案是否正確。這種類似于為每一個用戶創(chuàng)建一個進程或者線程處理連接。3. 第三種選擇,你站在講臺上等,誰解答完誰舉手。這時 C、D 舉手,表示他們解答問題完畢,你下去依次檢查 C、D 的答案,然后繼續(xù)回到講臺上等。此時 E、A 又舉手,然后去處理 E 和 A。。。這種就是 I/O 復用模型,Linux 下的 select、poll 和 epoll 就是干這個的。將用戶 socket 對應的 fd 注冊進 epoll,然后 epoll 幫你監(jiān)聽哪些 socket 上有消息到達,這樣就避免了大量的無用操作。此時的 socket 應該采用非阻塞模式。這樣,整個過程只在調(diào)用 select、poll、epoll 這些調(diào)用的時候才會阻塞,收發(fā)客戶消息是不會阻塞的,整個進程或者線程就被充分利用起來,這就是事件驅(qū)動。
也就是說 select、poll、epoll 都是 I/O 多路復用的機制,區(qū)別如下
說到這里,你應該明白了,事件循環(huán)是一種編程范式,很多場景都可以這樣來設計代碼,而 I/O 多路復用是一種 I/O 模型,是操作系統(tǒng)提供的一種機制,與進程、線程的概念是等價的,也就是說現(xiàn)代操作系統(tǒng)提供三種并發(fā)機制:
- 多進程
- 多線程
- I/O 多路復用
而 I/O 多路復用中的 epoll 用到了事件驅(qū)動,使得連接沒有上限,提升了并發(fā)性能。
3. HTTP 中的 Keep-Alive 起什么作用,是怎么實現(xiàn)的?
參考回答:
HTTP 是建立在 TCP 之上的,每次建立連接,都要經(jīng)歷三次握手,每次斷開鏈接都要四次揮手,建立和斷開連接的成本都很高。
Keep-Alive 是一個通用消息頭,允許消息發(fā)送者暗示連接的狀態(tài),還可以用來設置超時時長和最大請求數(shù)。
- HTTP/1.1 200 OK
- Connection: keep-alive
- Content-Encoding: gzip
- Content-Type: text/html; charset=utf-8
- Date: Thu, 11 Aug 2016 15:23:13 GMT
- Keep-Alive: timeout=5, max=1000
- Last-Modified: Mon, 25 Jul 2016 04:32:39 GMT
- Server: Apache
Keep-Alive 使客戶端到服務器端的連接持續(xù)有效,當出現(xiàn)對服務器的后繼請求時,Keep-Alive 功能避免了建立或者重新建立連接?,F(xiàn)在的 Web 服務器,基本上都支持 HTTP Keep-Alive,Keep-Alive 帶來以下優(yōu)勢:
- 較少的CPU和內(nèi)存的使用(由于同時打開的連接的減少了)
- 允許請求和應答的 HTTP 流水線
- 降低擁塞控制 (TCP連接減少了)
- 減少了后續(xù)請求的延遲(無需再進行握手)
- 報告錯誤無需關(guān)閉 TCP 連接
劣勢:
保持連接會讓某些不必要的連接也占用服務器的資源,比如單個文件被不斷請求的服務(例如圖片存放網(wǎng)站),Keep-Alive 可能會極大的影響性能,因為它在文件被請求之后還保持了不必要的連接很長時間。
HTTP Keep-Alive 是怎么實現(xiàn)的?
客戶端發(fā)送 connection:Keep-Alive 頭給服務端,且服務端也接受這個Keep-Alive 的話,兩邊對上暗號,這個連接就可以復用了,一個 HTTP 處理完之后,另外一個 HTTP 數(shù)據(jù)直接從這個連接走了。
當要斷開連接時可以加入 Connection: close 關(guān)閉連接,當然也可以設置Keep-Alive 模式的屬性,例如 Keep-Alive: timeout=5, max=100,表示這個TCP通道可以保持 5 秒,max=100,表示這個長連接最多接收 100 次請求就斷開。
但是如果開啟了 Keep-Alive模式,那么客戶端如何知道某一次的響應結(jié)束了呢?
以下有兩個方法:
如果是靜態(tài)的響應數(shù)據(jù),可以通過判斷響應頭部中的 Content-Length 字段,判斷數(shù)據(jù)達到這個大小就知道數(shù)據(jù)傳輸結(jié)束了。
但是返回的數(shù)據(jù)是動態(tài)變化的,服務器不能第一時間知道數(shù)據(jù)長度,這樣就沒有 Content-Length 關(guān)鍵字了。這種情況下,服務器是分塊傳輸數(shù)據(jù)的,Transfer-Encoding:chunk,這時候就要根據(jù)傳輸?shù)臄?shù)據(jù)塊 chunk 來判斷,數(shù)據(jù)傳輸結(jié)束的時候,最后的一個數(shù)據(jù)塊 chunk 的長度是 0。
最后的話
面完后,貓哥就把自己回答的不是很好的問題記下來,然后去搜索一番,總結(jié)出來希望能幫到你,貓哥后續(xù)會不定期分享面試經(jīng)驗,如果有收獲,不妨關(guān)注、在看、點贊支持一波。
本文轉(zhuǎn)載自微信公眾號「Python七號」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系Python七號公眾號。