io操作的階段是io調用和io執行,io執行又分成數據准備和數據拷貝。
阻塞和非阻塞是io操作數據准備層面的概念,數據准備就是io設備准備好放到內核緩沖區。如果是阻塞就是阻塞等到它放好,但是不占用cpu。
非阻塞就是沒准備好就先回去,然后采用輪詢的方式,在下一次再來看看有沒有准備好。
對於異步來說,用戶進程啟動io操作直接返回就可以去做別的事,數據准備和數據拷貝過程都不會導致用戶進程阻塞。
同步是用戶進程啟動io操作就進入了等待狀態,直到操作完成。異步是用戶進程告訴進程這個io操作后就可以去做別的事情,都是由內核處理好直接發送給用戶進程緩沖區的。
這個項目里主要設計了一個線程池來處理多個瀏覽器請求並發的情況。首先,服務端的主線程使用epoll系統調用函數進行socket的監聽,接收socket和注冊讀寫事件,然后封裝成請求對象加入請求隊列。接下來就是線程池和請求隊列之間的處理機制。這個項目使用的是互斥鎖和信號量來解決多線程同步的問題,這里的互斥鎖就是用互斥量實現的鎖。服務端epoll系統調用在把請求加入請求隊列的時候就有先用一個互斥鎖把請求隊列鎖住,然后加入之后,會采用信號量post的方式來通知線程池里面的線程進行競爭處理。線程池方面,一開始是創建了一個固定大小的線程池,然后每一個線程都綁定到一個run 回環函數,等待信號量post就說明有新的請求加入了請求隊列,在這里會不停的while循環去等待這個信號量。 當有了信號量,線程池里的線程就會把請求隊列鎖住,然后去拿請求來進行處理。總的來說,就是一開始線程池里的所有線程都是相互獨立的睡眠狀態,只有當請求隊列里的信號量更新的時候才會喚醒進行競爭。
使用有序鏈表,每個節點都是一個定時器,鍵是定時時間(相對和絕對,使用絕對),值是回調函數。然后一開始使用alarm函數去觸發SIGALRM信號,一開始是先把listenfd和pipefd[0](統一事件源,信號處理函數通常使用管道來將信號傳遞給主循環,信號處理函數往管道寫端寫入信號值,主循環則從管道的讀端讀出信號值)都掛在樹上了,然后在樹上檢測到listenfd就可以把那些連接connfd也掛在樹上,同時把那些connfd對應的定時器也掛在鏈表上面。后來每次觸發定時信號SIGALRM就會使得timeout有標記,如果觸發SIGTERM信號系統就要退出了。
然后后來客戶連接有接收到數據的時候,先去處理,如果連接出錯就刪除連接對應的定時器,如果連接關閉也是刪除定時器,如果連接有數據就要調整定時器。在這里面還要處理定時信號到達,調用tick函數去處理每一個定時任務,然后處理完又要alarm(TIMEOUT)。最底層的原理就是利用alarm函數周期性觸發SIGALRM信號,該信號的信號處理函數會通過管道通知主線程,主線程就去執行定時器鏈表上的定時任務,比如關閉非活動的連接。
前面的方案是以固定的頻率調用tick,並在其中依次檢測到期的定時器,然后執行到期定時器上面的回調函數。另外一種時間堆的思路。把所有定時器的最小的一個定時器的相對超時時間最為心跳間隔。一個心跳函數被調用,超時時間最小的定時器一定會到期,我們就可以在tick函數里面處理這個定時器。然后,再次從剩余的定時器里面找出超時時間最小的一個,並將這個時間設置為下一次心跳間隔。如此反復就可以實現比較精確的定時。
之所以用epoll而不是select和poll的原因。
(2)文件描述符組織形式不同。select使用線性表描述,poll使用鏈表描述,epoll使用紅黑樹描述,並且維護了一個就緒隊列,使用epoll wait的時候,只要觀察就緒隊列有沒有數據就可以。紅黑樹在fd數量少的情況下,相比於哈希、B+樹等占用內存少,在fd數量多時查詢效率穩定。
(3)節省遍歷的時間開銷。select/poll的開銷來自內核判斷是否有文件描述符就緒的過程,前面拷貝也要遍歷。epoll不是采用遍歷的方式,當紅黑樹fd有活動產生,會自動觸發epoll回調函數通知,然后把這些fd放到就緒隊列,等待epoll_wait函數調用后被處理。
(4)select和poll只能工作在相對低效的LT模式,epoll同時支持LT和ET模式。
在fd數量小而且每個fd都很活躍的時候,建議使用select和poll。在fd數量多,而且單位事件內僅部分活躍的時候,建議使用epoll。
ET和LT的區別:體現在同一個線程請求的epoll_wait函數處理不同,ET是第一次請求才會發送,不管用戶有沒有處理完都不會再發送;LT是只要用戶沒出來就會一直發送。ET一定要設置非阻塞,ET模式只會響應一次,通常方法是再一個while循環利對其進行讀取。非阻塞讀,直到出現EAGAIN或EWOULDBLOCK這兩個錯誤標識socket是空,不用再讀了,然后停止循環如果弄成阻塞模式,此時循環讀在socket為空的時候會阻塞在哪里,導致其他的監聽事件沒辦法完成。
Reactor模式是主線程負責監聽fd上是否有可讀或可寫事件發生。如果有就通知工作線程,把該fd就放到請求隊列。讀寫數據、接收新連接、處理客戶請求都在工作線程完成。
Proactor模式是主線程不僅負責監聽fd上是否有可讀或可寫事件發生,還要接收新連接、讀寫數據等操作。工作線程僅負責業務邏輯,處理客戶請求。
上面兩個是事件處理模型,下面的半同步/半異步是具體的線程並發模型,用於怎么處理客戶邏輯和接收客戶請求。
在IO模型中,同步和異步的區別是內核向用戶通知的是就緒IO事件還是已完成的IO事件,以及該由用戶還是內核來完成IO讀取。
在線程並發模型里面,同步是指程序完全按照代碼序列順序執行,異步是指程序的執行需要由系統事件來驅動。
半同步/半異步模式:同步線程用於處理客戶邏輯,異步線程處理IO事件,監聽到客戶請求之后,將其封裝成請求對象並插入請求隊列。
具體哪個工作線程來為新的請求服務,取決於請求隊列的設計,RoundRobin算法,或者通過條件變量或信號量隨機選擇一個。
有限狀態機可以把有限個變量描述的狀態變化過程描述出來。根據不同狀態或消息類型進行相應的邏輯處理,是邏輯清晰易懂。
GET和POST的區別:
功能,參數傳遞的方式,傳輸大小,安全性。
(1)get是從服務器獲取數據,參數在url請求行發送,因此不太安全,url的傳輸大小比較小。get請求會被瀏覽器主動cache。get請求只能進行url編碼,參數只接收ascii字符。
(2)post是向服務器傳送數據。參數在請求體里面傳送,因此比較安全,傳輸大小比較大。post請求不會被瀏覽器主動緩存,除非手動設置。get請求支持任何編碼,參數類型沒有限制。
此外,從瀏覽器端來看,get產生一個TCP數據包,post產生兩個(發送過去收到100,然后接收到之后,再次發送)。
實際上,get和post都是HTTP協議中的兩種發送請求的方法。因此,get和post都是TCP連接。
(3)冪等性。
注冊和登錄的流程:先將數據庫中的數據載入服務器,然后對報文進行進行解析並提取用戶名和密碼,然后對描述進行注冊和登錄校驗,最后執行頁面跳轉機制。
登錄密碼狀態保存:可以使用session或者cookie的方式實現。
cookie就是服務器給客戶端分配的字符串身份標志。每次瀏覽器發送數據時,在HTTP報文加上這個串,服務器就知道是誰了。
session是保存在服務端的,當一個客戶發送報文過來,服務器在自己記錄的數據中去找,類似核對名單。.
一開始,客戶端發送賬號密碼過去,服務端生成一個sessionID,然后把賬號和用戶信息作為value,sessionID作為key,存到session表里面,再把sessionID通過set-cookie字段發送回去給客戶端。然后客戶端保存sessionID,下次訪問的時候帶上。服務端第一次收到就會去session表里面找這個用戶的登錄狀態、權限等信息,完成請求響應。
如果登錄的用戶名和密碼加載到本地使用map匹配,這個時候數據有10億,就會很耗時,可以怎么優化。
將所有的用戶信息加載到內存中很耗時,對於大數據最便利的方式就是哈希。
利用哈希建立多級索引,加快用戶驗證。首先,將10億的用戶信息,利用大致縮小1000倍的哈希算法進行哈希,獲得了100萬的哈希數據,每一個hash數據代表一個用戶信息級(一級);然后,分別對這100萬的數據再進行哈希,最后剩下1000個hash數據(二級)。
定時器的作用就是處理定時任務,或者處理非活躍連接(一直沒有任務的事件)。把每一個定時事件都放到一個升序鏈表上面,然后通過alarm()函數周期性觸發SIGALRM信號說明定時時間到,然后信號回調函數通過管道通知主循環。主循環收到信號后對升序鏈表的定時器進行處理:如果一定事件內沒有數據交換就關閉連接。
升序鏈表的添加要O(N),刪除只要O(1)。可以考慮最小堆、跳表的優化。
最小堆也是用定時器的剩余過期時間進行排序,最小的定時(即最快要到到期的)放在堆頂。SIGALRM信號觸發時就會執行定時器清除,如果堆頂的定時器時間過期,就會重新建堆再判斷是否過期,如此循環直到沒有過期。
最小堆的添加要O(logN),刪除要O(logN)。
為什么要用單例模式初始化日志系統?
同步方式就是實時寫入日志,會產生很多系統調用,如果某條日志很大,寫的時間很長,就會阻塞日志系統,造成性能瓶頸。
異步方式就是使用生產者消費者模式,生產夠了就寫到日志,具有較高的並發能力。
比如現在要記錄監控服務器狀態的日志,怎么把這個日志分到不同的機器?可以使用MQTT或RABITMQ等消息隊列進行消息分發。
壓測即並發量測試。
webbench實現的原理:
父進程fork很多子進程,子進程在一定時間內對服務器循環發出實際訪問請求。
父子進程通過pipe管道通信,管道是單向的,子進程寫入若干次請求訪問完畢記錄到的總信息,父進程讀取子進程的信息。
子進程到時間了就退出,父進程在子進程退出之后統計並給用戶顯示最后的測試結果,然后再退出。
提升服務器並發能力:(1)提升服務端操作系統的IO連接管理能力,基於非阻塞IO多路復用技術,讓上億的請求同時連在一台服務器上高效管理;(2)提升服務端網絡系統的IO讀寫能力,零拷貝、DMA、緩存等;(3)改善服務端系統資源競爭,包括線程數量、鎖競爭(無所隊列)、內存申請釋放等;(4)改善代碼架構、代碼邏輯、代碼對內存的操作。
上萬並發連接的具體數量。虛擬機內存配置4G運行webserver,服務器內存配置為16G運行webbench壓測軟件。利用webbench進行測試得到的具體數量為:客戶端數量不能超過10050 。
webbench -c 10050 -t 5 http://192.168.1.130:9006/ 並發10050運行5秒,產生的TCP連接數有,顯示有個failed。
LT:每秒鍾響應請求數目:153960 pages/min。每秒鍾傳輸數據量:287392 bytes/sec。Requests: 12830 susceed, 0 failed.
ET:每秒鍾響應請求數目:154692pages/min。每秒鍾傳輸數據量:288758bytes/sec。Requests: 12891susceed, 0 failed.
統計應用流量。應用流量是指某段時間內訪問的請求數量嗎?是的話,可以使用redis或mysql數據庫存儲對應連接的客戶端IP地址+時間,然后統計指定時間段內不同IP地址的數量。
數據庫連接池數量是8,線程池的線程數量也是8。
項目遇到的問題和解決方案。實際測試的時候,請求小文件時服務端調用一次writev就能將數據全部發送出去。但是請求大文件的時候,就會需要調用writev函數多次,此時出現文件顯示不全或者無法顯示的問題。是因為writev的m_iv結構體成員有問題,每次傳輸之后不會自動偏移文件指針和傳輸長度,還會按照原有的指針和長度進行發送。根據前面的基礎API分析,我們知道writev函數以順序iov[0]、iov[1]到iov[iovcnt-1]從緩沖區中聚集輸出數據。項目中,申請了兩個iov,其中iov[0]是存儲報文狀態行的緩沖區,iov[1]是資源文件指針。改進:考慮到報文消息頭比較小,一次傳輸就能完成,第一次傳輸之后就將其下次的傳輸長度設置成0,然后更新文件需要傳遞的開始地址和長度,下次就只會傳遞文件。在此基礎上,每次傳輸之后都更新下次傳輸的文件的起始位置和長度。
如何進行多線程調試。這里利用vscode編輯器+GDB進行調試,對程序中需要監測的位置打斷點,利用-exec執行對應的 GDB 調試指令。在此基礎上,讓程序全速運行到指定的斷點處,此時在調試控制台中采用info threads 可以顯示當前可調試的所有線程,其中帶*號的是正在調試的線程。此時如果需要在當前線程中單步運行的話,可以使用set scheduler-locking on命令進行上鎖,執行單步調試。在最后使用set scheduler-locking off 解除鎖定,返回原先的線程。
——————————————————————————————————————————————————————————————
數據結構:升序鏈表(我的項目使用的)、跳表、時間輪、紅黑樹、最小堆。
由於非活躍連接占用了連接資源,嚴重影響服務器的性能。
通過實現一個服務器定時器,處理這種非活躍連接,釋放連接資源。
利用alarm函數周期性地觸發SIGALRM信號,該信號的信號處理函數利用管道通知主循環執行定時器鏈表上的定時任務。
定時器在服務器端作用:
(1)多客戶端連接服務器,需要通信發送數據包,有些客戶端連接了長時間不干事,就需要斷開。
(2)某些任務當前不想執行,需要一定時間之后執行。
如何實現定時器?
(1)單線程環境下:定時事件通常與網絡事件協調處理。
舉例:redis單線程,nginx多進程但是每個進程下都是單線程。
epoll_wait(epfd,用戶態數組用於接收已經觸發的事件,從網絡協議棧最大取多少數據,沒有事件到達時最長阻塞時間)。
epoll_wait和定時事件的綁定:定時時間是離散有序的一個鏈表上。最近要觸發的定時器會作為epoll_wait的第四個參數。
紅黑樹:平衡二叉搜索樹
平衡的規則:從根節點出發到任意葉子節點的黑節點數一定相等。
增加和刪除的時候都會滿足平衡規則,提供一個搜索穩定時間復雜度。
找最左側的節點就可以找最小的節點,就可以找到最近要觸發的定時器。
O(logn):100萬個節點,比較20次;10億個節點,比較30次。
(2)多線程環境下:
有一個單獨的定時線程進行處理定時事件thread_timer。
采用時間輪結構實現定時器,跳表也可以,最小堆也可以。
時間輪知識:
時針、分針、秒針。時間精度是每一秒,時間范圍是60。單層級時間輪是循環數組,使用取余來操作。(time%60)
插入任務時間復雜度永遠是O(1)。不能執行刪除任務。單層級中,數組的大小必須要大於 支持最大的定時任務。此外,0有任務,59有任務,中間沒有,就會造成空推進的問題。
使用多層級的時間輪解決上述兩個問題。分成秒層級(60)、分層級(60)、時層級(12)。
132解決數組太大的問題,接下來關注秒針運轉。
跳表知識:
跳表是多層級有序鏈表。(有大量節點數據的時候才提高效率,本質還是二分查找,空間換時間)
跳表可以通過加鎖的方式,提供並發讀寫。每個節點都會存一個互斥鎖。(值得做跳表KV存儲引擎項目研究研究)
如果要插入8,先查找7到9之間,插入節點的時候分配三層1->8->20/1->7->8->20/...7->8->9....。
如果要刪除節點7,加上原子變量,方便我們並發讀。
跳表使用場景:rocksdb是kv數據庫,有內存數據和磁盤數據,需要提供組織kv並提供並發讀寫。
———————————————————————————————————————————————————————————————