日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

干貨 | Python后臺開發(fā)的高并發(fā)場景優(yōu)化解決方案

 風(fēng)聲之家 2019-07-10

嘉賓 | 黃思涵

來源 | AI科技大本營在線公開課

互聯(lián)網(wǎng)發(fā)展到今天,規(guī)模變得越來越大,也對所有的后端服務(wù)提出了更高的要求。在平時的工作中,我們或多或少都遇到過服務(wù)器壓力過大問題。針對該問題,本次公開課邀請到了金山辦公AI平臺研發(fā)工程師黃思涵,他分享的主題是《Python后臺開發(fā)的高并發(fā)場景優(yōu)化解決方案》,為大家講解不同業(yè)務(wù)場景下這類問題的解決思路和方案。本文是直播公開課的速記版,視頻回放請見↓↓↓

1 背景

我們看一下今天的課程內(nèi)容。首先我們來看一組數(shù)據(jù)。第一個數(shù)據(jù)是春晚紅包,相信大家在今年春晚,大家已經(jīng)用過這個功能;互動游戲數(shù)據(jù)是互動次數(shù)達(dá)到了208億次。在晚上的短短四個小時之內(nèi),去完成了這樣的請求量,說明這個互聯(lián)網(wǎng)的規(guī)模是非常大的。把它平攤到14.8億人的頭上,平均每個人也是點擊了14.8次,這當(dāng)中還沒有算上這些非網(wǎng)民的次數(shù),所以說,整個互聯(lián)網(wǎng)的規(guī)模是非常龐大的。

再接下來,我們再看一個雙十一的一個數(shù)據(jù),這個雙十一的訂單量,在去年數(shù)據(jù)是13.52億訂單量,我們再看一下,在這個屏幕,左邊這里有一個圖,從2014年,它大概是1點多億件,到2015年會有一個增長,到了四點多億件,到2016年,這邊是逐漸的增長,直到2018年增長到了一個13億次的一個訂單量,那么有了這樣的訂單量,大家可以看到互聯(lián)網(wǎng)規(guī)模的迅速發(fā)展,它是逐年在增加的,說明發(fā)展速度是非??臁?/span>

接下來,我們再看一個數(shù)據(jù),那么就是微信,也是平時大家用的比較多的一個軟件。微信的日活可以達(dá)到十個億,相應(yīng)的一個請求量,可能比這個更多。那么看這些數(shù)據(jù),它有一個什么意義?這樣的一些數(shù)據(jù),它就說明了,我們的互聯(lián)網(wǎng)的一個規(guī)模是非常龐大的。而且它的增長也是非??斓?。這就引出了今天的一個問題,那么也許大家平時在一些開發(fā)過程當(dāng)中,或者在大家自己的遇到的一個業(yè)務(wù)場景當(dāng)中,沒有前面那么多比較恐怖的數(shù)據(jù),但是,大家知道,公司要去增長,我們的業(yè)務(wù)要增長,業(yè)務(wù)的增長,就意味著用戶量會去增加,用戶量增加就導(dǎo)致了一個請求量的增加。請求量的增加對我們開發(fā)人員來講,或者對我們后臺的服務(wù)器來講的最直接的反映,就是服務(wù)器壓力的增加。那么壓力的增加就意味著對服務(wù)器來講,它的一個請求量到達(dá)了一個比較高的水平,或者說它的壓力呢,已經(jīng)不能夠繼續(xù)往上增加了,那么請求量已經(jīng)到達(dá)了一個瓶頸。

接下來,我們看一下,它會導(dǎo)致的直接的問題是什么?大家可以看到這樣一個簡單的模型。左邊是一個客戶端,右邊是一個服務(wù)端??蛻舳讼蚍?wù)端發(fā)出請求,當(dāng)服務(wù)端壓力過大的時候,對服務(wù)器來說,那么就是請求太多,來不及處理,甚至是這樣的請求呢,它還會被丟棄掉。但是,對于這個客戶端來講,或者是使用這個客戶端的一個用戶來講,它可能面臨的一個問題,就是等待時間過長,或者是出現(xiàn)錯誤,對客戶來講,客戶端來講,他并不知道,你這個后臺是面臨著這樣大的一個壓力,或者是你能處理的請求是多大的一個數(shù)量,他只能直觀感受到,我這個請求時間長,或者我這個請求有問題。這在我們的業(yè)務(wù)場景當(dāng)中都是不能接受的。

所以,今天我就將跟大家去探討一下,如何盡可能多的去處理這樣的一個客戶端的請求,對于我們來講,這個服務(wù)端的壓力大,最簡單的來講就是資源不足。我們可能第一印象就是這個資源不足,那么資源不足呢,非常簡單,就是去提升硬件配置,比如說這里CPU,或者是內(nèi)存,直接去升級硬件就可以了,這樣是一個簡單的處理方式。但大家有沒有想過,還會有一種問題,這種場景下面,就是當(dāng)我資源充足的情況下,我的一個服務(wù)器的請求依然不能夠進(jìn)行提升,或者請求還依然維持在一個比較低的水平,不能夠?qū)⑽宜械馁Y源進(jìn)行一個很好的利用。這就存在著一個浪費的問題,它的性價比呢,就是不高的。

2 優(yōu)化思路和方案

接下來,我們今天呢,主要就是針對下面的這種情況,會去給大家分享我們優(yōu)化的思路和方案。說到服務(wù)端的壓力大,最簡單的可能想到的一個原因,就是CPU。畢竟CPU在計算機(jī)當(dāng)中處于一個非常核心的位置。

查看CPU使用情況

我們先來看一下CPU。我使用TOP命令在這個主機(jī)上,去查看CPU的使用情況。在這里,我截了一個圖,這個圖中,CPU使用率是接近于百分之百,說明CPU,已經(jīng)是完全被代碼,被測試程序是完全占滿了。所以它就導(dǎo)致了請求量無法去再進(jìn)行一個提升。它也就是說明我這個資源不足了。如何解決呢?那么就是去增加資源,去升級這樣的一個CPU,這就是我講的一個比較簡單的一個場景。

大家有沒有想過另外一種情況,假如說我的CPU它的利用率是在非常低的水平,比如這里,它只有這樣一個大概1/10的一個利用率,但是在這個情況下,我的一個請求數(shù)量依然不能夠增加,我的服務(wù)器的壓力依然很大。如果在這種情況下,要去給老板說,我這個請求無法提升,我需要去增加配置,這樣的話,老板看到這樣的情況肯定不會很滿意,因為你資源明顯沒有占滿,你還要去申請這樣的一個機(jī)器。我們繼續(xù)去查看它的詳情,在這里我使用這個TOP命令,在這個之后繼續(xù)去按一個一,就可以展開四個CPU的一個詳情,這里呢,我的CPU是四核心的,看到這里有四個CPU的核心數(shù)在這里運行,大家可以看到在這里,第一個確實是占滿了第一個核心,但是呢,后面的三個是在這里,空閑的,這說明了一個什么問題呢?就是在前面的這個程序當(dāng)中,實際上它對整個CPU的一個利用,只是用了它的一部分,而沒有全部用,沒有全部去用上,那么這種情況,解決方案就變得比較簡單了。大家可以想一下,如何去解決呢?很顯然,就是要將這樣的一個CPU,其他的核心數(shù),全部用起來。另外在現(xiàn)在的CPU幾乎都是多核心的,這里我們要盡可能的多的使用我們已有的資源。這里有同學(xué)說的多進(jìn)程,所以我們就可以考慮使用多進(jìn)程,將所有的CPU去給它利用起來,盡可能的多的使用我們已有的資源。

多進(jìn)程

        

接下來,我使用Python去給大家講解一下,如何去使用多進(jìn)程,來利用這樣的一個資源。這里我貼了一段代碼,這個代碼是Python去實現(xiàn)的一個多進(jìn)程,然后來利用多核CPU完成任務(wù)。

        

這里我大概給大家講一下。在Python當(dāng)中,我們使用multiprocessing這個庫去完成這樣的一個多進(jìn)程,這里有一個-1大家要注意一下,通常我們會去留下這么一些核心數(shù),去用作其他的一些任務(wù),防止整個所有的核心數(shù)被占滿導(dǎo)致其他的任務(wù)不能夠正常運行。這里算出一個核心的CPU數(shù)。下面我是用了一個進(jìn)程池,進(jìn)程池大概是一個什么概念呢?這里用了Pool函數(shù),這個函數(shù),會創(chuàng)建出這樣一個進(jìn)程池。創(chuàng)建出了這樣的進(jìn)程池之后,下面它會在當(dāng)中去加入這樣的一些任務(wù),這個Task它實際上是寫的一個函數(shù)。在這里,就將它加入到這樣的一個進(jìn)程池當(dāng)中。

實際上,它是異步加入的,會有一個隊列,這些任務(wù)都會被在非常短的時間內(nèi)加載到下面的這樣的一個隊列當(dāng)中,由于這上面已經(jīng)有了進(jìn)程池,這個multiprocessing幫我們實現(xiàn)好了。它會去創(chuàng)建好一些進(jìn)程,創(chuàng)建好之后,它會自動去把這些加入進(jìn)來的任務(wù)呢,放到這個進(jìn)程當(dāng)中去進(jìn)行調(diào)度,然后執(zhí)行。當(dāng)前面的一些執(zhí)行完了之后,后面的議程會再次被調(diào)度進(jìn)去,這個進(jìn)程池它相對于普通的直接去創(chuàng)建進(jìn)程的方式有什么好處呢?如果我們直接去創(chuàng)建這樣進(jìn)程,比如有一個任務(wù),我就創(chuàng)建一個進(jìn)程,然后當(dāng)這個任務(wù)執(zhí)行結(jié)束之后呢,我再銷毀這個進(jìn)程,這樣創(chuàng)建和一個刪除的過程都會有這樣一個開銷,這里使有進(jìn)程池,它就會很好的去解決這樣一個問題。接下來,Close,就是去當(dāng)你不需要加入的時候,就對它進(jìn)行一個Close掉,Join在這里是進(jìn)行一個阻塞等待,這里給大家講了這一段代碼,大概做了一個什么事情。

       

下面,我就給大家看一下我是如何去測試這樣的一個多進(jìn)程,如何去進(jìn)行一個對比的。我執(zhí)行的測試流程,是這樣的一個流程:首先我會創(chuàng)建了一個隊列,這個隊列是一個全局的一個隊列,而且它在這些進(jìn)程當(dāng)中是進(jìn)行共享,當(dāng)然它是用的multiprocessing當(dāng)中的一個隊列去實現(xiàn)的。然后,下面會有一個循環(huán),當(dāng)然這個是單進(jìn)程的時候,下面會有一個循環(huán),然后循環(huán)每次從這個隊列當(dāng)中,這個隊列當(dāng)中去拿一個操作號,這個隊列它是從0到9這樣的一個數(shù)組,從0、1、2、3、4一直到9,這樣的一個序列。后面,我在這里會拿到了一個執(zhí)行號之后,然后再去執(zhí)行的一個函數(shù),執(zhí)行了這個函數(shù);下一次循環(huán),就是再次從這個隊列當(dāng)中去再拿出一個執(zhí)行號,拿出執(zhí)行號之后,再執(zhí)行一下這樣的一個函數(shù),最終執(zhí)行結(jié)束,當(dāng)隊列為空的時候這里就執(zhí)行結(jié)束了。

這里我再給大家說一下,這個函數(shù)是一個什么函數(shù)呢?是這邊定義的一個計算斐波那契數(shù)列的一個函數(shù),這個函數(shù)使用遞推實現(xiàn)的。說到這個斐波那契數(shù)列可以給大家提一下,斐波那契數(shù)列大概是長這個樣子,寫出來可能大家就知道了,第一,1、2、3、5,然后是8,這樣的一個數(shù)列。就是前兩個數(shù)相加就等于后一個數(shù),這樣的一個數(shù)列。我實現(xiàn)它的方式是實現(xiàn)遞推實現(xiàn)的,為什么使用這樣一個函數(shù)在這里呢?比較簡單的一個理由,就是它比較耗時,我沒有進(jìn)行優(yōu)化。耗時的話,我這里就可以去清楚地來對比單進(jìn)程和多進(jìn)程它們之間的一個優(yōu)化的一個那個結(jié)果,那么多進(jìn)程的時候,同樣是有一個全局的隊列。全局的隊列之后呢,下面會有多個進(jìn)程,同時在這個隊列當(dāng)中去拿這個操作號,依然是0到9操作號。拿了操作號之后,每一個進(jìn)程獨立的去拿到進(jìn)程號之后,獨立的去執(zhí)行斐波那契數(shù)列,直到這個執(zhí)行為空,這個隊列為空的時候,就算作是整體執(zhí)行結(jié)束。

這里講了我的—個測試腳本的流程之后,我們來看一下它的結(jié)果。在單核的時候,它也就是前面我執(zhí)行這個單進(jìn)程的時候,它依然是把一個核心代碼,這樣我是展開來看的,一個核心代碼。后面使用多核的時候,大家可以看到,它將所有的核心數(shù)幾乎都是再一個比較高的使用水平上面,這里呢,我的代碼呢,它并沒有去留下一個核心數(shù)來作為預(yù)留,因為我為了去測試它的效果,讓它看起來更明顯,我直接將四個核心都用上了,這里大家也可以看到它的執(zhí)行時間的結(jié)果,當(dāng)使用單線程的時候是15秒,使用多進(jìn)程的時候是在4.8秒。它看起來大致是在一個4倍的一個左右的時間,當(dāng)中要考慮這個進(jìn)程的開銷,它這個4倍是一個理論值。

這里講到多進(jìn)程,大家可能有些同學(xué)也會有有想,既然多進(jìn)程可以,那么我們是否可以用多線程實現(xiàn)呢?是的,多線程也能完成這樣的一個工作。

多線程

接下來我們看一下多線程如何去執(zhí)行,首先在這個進(jìn)程的使用進(jìn)程的時候,它的進(jìn)程時間的通信是比較不方便的,因為大家知道這個進(jìn)程是系統(tǒng)分配資源的一個最小單位,所以說呢,它這個一些變量,包括你要在進(jìn)程之間去進(jìn)行共享數(shù)據(jù),這樣的時候,就會變得不太方便。面臨一個問題,就是創(chuàng)建或撤銷的時候,它的系統(tǒng)開銷是比較大的,所以說因為它的開銷大,你不可能去創(chuàng)建大量的一個進(jìn)程在這個地方去執(zhí)行這樣的任務(wù)。還有一個,就是進(jìn)程的切換,它的耗時是比較大的,當(dāng)然它這個耗時,這里指的耗時是跟與多線程比較來講的。

              

那么多線程相對于進(jìn)程來講,它就會有如下三個特點。第一個,就是通信簡單,可以在這個多個線程之間去共享數(shù)據(jù),會比進(jìn)程之間來的比較簡單,而且它的開銷小,就意味著,可以去使用更多的線程,同時呢,它的切換也是比這個進(jìn)程更輕量,更快的。

       

那么接下來,我們看一下,如何使用這個多線程去改寫當(dāng)前的一個測試的一個代碼,那么這里是多線程的一部分關(guān)鍵代碼,這里呢,大家注意到,隊列queue 它實際上是一個全局變量,這樣的全局變量在這里就并不是使用了一個Multiprocessing里面隊列來實現(xiàn)的,而是簡單的,直接使用了這個Python當(dāng)中的列表去實現(xiàn)的,同樣的,它依然是去拿到操作號之后,執(zhí)行的斐波那契的序列,這里去起動線程的時候,在Python當(dāng)中,這里我使用了Thread這個庫,然后去創(chuàng)建線程.所以同樣的,大家看到右邊這個流程圖,它和我們多進(jìn)程的一個流程圖類似。在這里,因為它也是有一個全局的一個隊列,在這個隊列,實際上在這里,它是一個List,這個List,當(dāng)然下面所有的線程都依次的從這個列表當(dāng)中去拿到了這樣一個操作號,然后再去執(zhí)行斐波那契數(shù)列,最終呢,直到這個List為空的時候,所有的程序執(zhí)行結(jié)束。

       

我們看一下它的執(zhí)行結(jié)果,同樣的是單線程的時候,依然是一個單核CPU被使用的,利用率非常高,那么多線程的時候,大家注意一下這樣的一個結(jié)果,它所有的核心這里看起來,都使用起來的,而且它處在一個非常低的一個使用水平,這里是我們程序被優(yōu)化了嗎?大家想一下這個問題,看起來是,就如果直觀的從這樣的一個結(jié)果來看,好像是被優(yōu)化了,而且它的一個利用率還比較低,但是我們來看一下它的一個結(jié)果,在單線程的時候,它是14.52秒,在這個多線程的時候有一個,時間相對于這個單線程它還增加了,這是一個什么問題呢?這里有同學(xué)說,數(shù)量越大,效果就能體現(xiàn)出來。

全局解釋器鎖(GIL)

            

接下來,我給大家解釋一下,大家可能就會明白了,要解釋這樣的,為什么多線程反而在這里會比單線程的時間長?首先我們要去知道Python當(dāng)中的一個非常著名的問題,就是全局解釋器鎖這樣的一個問題。為什么Python當(dāng)中會有這樣的一個東西呢?這個東西叫GIL(Global Interpreter Lock的縮寫),是在很早之前,并發(fā)編程出現(xiàn)的時候,就會涉及到數(shù)據(jù)一致性的問題。Python社區(qū)最開始為了解決這個問題,提出了全局鎖。全局鎖控制線程之間,或者是并行的任務(wù)之間的數(shù)據(jù)一致性的問題。全局鎖確實很好地解決了這個問題,不過它就會帶來一些問題,就是使得任何一個時刻,只會有一個線程在執(zhí)行。這樣也就會帶來一些性能上的問題,我們經(jīng)常會聽到的Python的并發(fā)性不好這樣的一些說法,后面我給大家講一下,大家可能知道這樣的一個原因所在,是為什么,就清楚了。

接下來,我們先看一下這個數(shù)據(jù)一致性是什么樣的問題?假設(shè)我這里有兩個線程,一個是線程一,一個是線程二,如果這兩個任務(wù)我們同時是執(zhí)行的了一個同樣的任務(wù),有一個A是全局的變量,它是線程一和線程二都能夠同時訪問的一個變量。線程一,它要執(zhí)行的任務(wù),就是先給這個A賦上一個值,比如說賦上一個1,再第二步它再去,給這個值進(jìn)行一個自加,即A等于A加一;線程二,它也是執(zhí)行的這個功能,那么也是A,然后它是等于二,然后它是進(jìn)行一個自加,即A等于A加二。這里,如果兩個線程單獨執(zhí)行,那第一個線程執(zhí)行的一個結(jié)果就是A等于1,A=1+1,那它執(zhí)行的一個結(jié)果就是A就等于2;下面這個,如果是單獨去執(zhí)行的話,它執(zhí)行的一個結(jié)果呢,就等于4。這樣是沒有問題的。

但是呢,如果這兩個線程,同時去運行,而且不去控制順序的話,那么有可能,我先執(zhí)行了一個A=1,然后再執(zhí)行了A=2,這個時候,A=2;這個時候,大家去在這個線的位置,假如說它們是有一個交叉的,在執(zhí)行的上面這個,A=A+1,那么這個最后呢,在線程一執(zhí)行出來A的結(jié)果,它就等于3了。在上面這個執(zhí)行完了以后,再去執(zhí)行下面這一個線程二的結(jié)果的時候,它就變成了A=5。大家注意沒有,這里它會有兩個結(jié)果,對于一個程序來說它的一個運行時機(jī)居然決定了它的一個結(jié)果,這種方式是大家不能接受的。也就是說如果有可能它運行的線程,第一個,先運行,第二個,后運行;如果是在這個地方,它的結(jié)果是第一個,如果是這種重疊運行的時候,結(jié)果是第二個,那么這就會有一個歧義。

在這個地方,為了解決這樣一個一致性的問題,就提出了一個全局鎖的概念,這個全局鎖,那么它是如何去解決這樣的一個問題的呢?接下來,我們看一下,在下面這個位置,同樣的,先是這個線程一開始執(zhí)行,執(zhí)行到這個地方的時候,假如它碰上了一個I/O,碰到了一個I/O的時候呢,它就會進(jìn)行一個釋放鎖的動作,釋放鎖的動作之后,會被第二個線程去獲取鎖,這里有個獲取鎖的動作,它再進(jìn)行執(zhí)行,也就是說我每一個線程要去執(zhí)行的時候,我就必須先要去有個獲取鎖的動作,獲取鎖之后,再進(jìn)行執(zhí)行。這里呢,假如說線程二,它執(zhí)行的時間比較長,這里我就沒有用這個I/O來進(jìn)行舉例了,還有一種情況,Python,這個解釋器它內(nèi)部會去計算它所執(zhí)行的一個微代碼的一個數(shù)量,當(dāng)它微代碼的數(shù)量執(zhí)行的足夠多的時候,它會進(jìn)行一個釋放,將這樣一個線程強(qiáng)制釋放鎖,強(qiáng)制釋放鎖之后,下一次,比如說這里,就是線程4,它獲取到了這樣一個鎖,他再進(jìn)行一個執(zhí)行。這個地方,微代碼的數(shù)量大家可以簡單的去理解它就是一個執(zhí)行時間,當(dāng)它執(zhí)行時間長了以后,就系統(tǒng)不可能讓這樣一個線程一直在這里,帶著CPU去執(zhí)行,它就會去對它進(jìn)行強(qiáng)制釋放,當(dāng)然大家可以簡單的這樣去理解。當(dāng)這個線程4,拿到了這樣一個鎖之后,它又會執(zhí)行,所以這些線程就在這樣的切換當(dāng)中,依次的去進(jìn)行一個切換,輪流著來執(zhí)行,而這里呢,也就解釋了,下面這一句話,就是全局解釋器鎖,使得在任意一個時刻,僅有一個線程在執(zhí)行,這樣的處理方式也就解決了我們上面這樣的一個計算的問題,也就是數(shù)據(jù)一致性的問題,它保證同一時刻只有一個線程去執(zhí)行。那么同一時刻,也就只有一個線程去對這樣的一個數(shù)據(jù)進(jìn)行修改,也就不會出現(xiàn)一個不一致的情況。

這里講了全局解釋器鎖,我們再回過頭來看一下前面的那個問題,為什么我們的一個多線程,反而比這個單線程的一個執(zhí)行時間會長。大家看到,當(dāng)多線程它進(jìn)行一個計算的時候,因為大家知道,這里它會進(jìn)行切換,綠色的呢,表示是這個線程正在計算,那么它會不停的去進(jìn)行切換,因為我們前面的這個程序呢,它是一個計算密集型的,它的主要的任務(wù)就是進(jìn)行計算,而且它在這個當(dāng)中切換了,就相當(dāng)于是當(dāng)它執(zhí)行的時間足夠長之后,被這個系統(tǒng)強(qiáng)制進(jìn)行釋放。

比如第二個CPU,拿到的這樣一個鎖之后,它進(jìn)一個執(zhí)行,這里再進(jìn)行一次釋放,然后被CPU0拿到了進(jìn)行執(zhí)行,所以它這里會進(jìn)行非常多的切換,這樣的切換,它當(dāng)中會是有代價的。它這樣切換的代價帶來的結(jié)果,最終執(zhí)行的時間會是比原來單線程執(zhí)行的要高的,這里就是一個原因。

CPU密集型

這里,那么看起來,多線程它會不會是沒用呢?其實多線程它也有自己的一個使用場景,那么我們這里已經(jīng)提到了一個CPU密集型,也就是說計算密集型,它也稱之為是CPU密集型,簡單的去理解,就是在程序當(dāng)中,大部分時間花在計算上面的,比如說,前面我講的這個事例,它計算了一個斐波那契數(shù)列,它的所有的時間都是耗在計算上面的。并且這樣的一個程序呢,沒有去進(jìn)行使用遞推沒有進(jìn)行優(yōu)化的話,計算量會非常的大。這個大家下來自己去嘗試一下,去計算這樣的一個數(shù)列,然后去比較一下它的時間,就知道它的計算量。由于它的計算量都在計算上面,所以說呢,它比較適用于這個多進(jìn)程,多進(jìn)程它就可以讓真正的讓所有的CPU都去參與到這個計算當(dāng)中。

I/O密集型

            

好,那么還有一種計算的類型,就是這個I/O密集型,所謂I/O密集型,就是程序當(dāng)中,大部分的時間是花在這個數(shù)據(jù)傳輸上面的。比如我們的要在程序當(dāng)中去讀入一個比較大的一個圖片,或者是讀入一些比較大的一些支點數(shù)據(jù),或者從數(shù)據(jù)庫讀一些比較大的數(shù)據(jù)出來,那么要讀取很大的數(shù)據(jù),它就會稱之為是I/O密集型。當(dāng)然大部分時間,也是相對于這個CPU的計算時間來講的。同學(xué)們再回憶一下,前面我講的一個多線程切換的時候,當(dāng)遇到I/O的時候,它就會進(jìn)行切換,所以當(dāng)它進(jìn)行一個I/O的時候,它切換到其他的線程進(jìn)行一個執(zhí)行,那么多線程是可以對這種場景下的一個程序進(jìn)行優(yōu)化的,這里呢,就給大家講,解釋講了兩種,一個是多進(jìn)程,一個是多線程。

 協(xié)程

        

大家在平時的Python編程當(dāng)中,還會聽到一個值,叫協(xié)程。那么協(xié)程是什么?這里我再補(bǔ)充給大家說一下。協(xié)程它就是一個用戶太輕量級的這樣一個線程,它有自己的一個寄存器,而且上下文切換,比線程還要更輕量,這樣說起來可能比較抽象,所以我在這里用代碼,再給大家去演示一下協(xié)程是如何工作的。這里我有三個工作的一個函數(shù),第一個函數(shù),task0,當(dāng)中我使用了這樣的一個Sleep,去模擬I/O,Task一我又Sleep 3秒,Task2,就去Sleep  0秒,然后呢,在這個下面這個地位Join去把所有的任務(wù)加到這個協(xié)程當(dāng)中,然后去來進(jìn)行一個計算,它最終執(zhí)行的流程,是從task0開始,當(dāng)它執(zhí)行到task0的時候,遇到了這樣一個I/O,5秒這個位置,遇到I/O之后,它就會進(jìn)行切換,切換到下一個開始執(zhí)行;切換到task1的時候,它也發(fā)現(xiàn),這里也有一個I/O,那么它會繼續(xù)切換,切換到Task2的時候,這里它也會發(fā)現(xiàn),這里有一個sleep0,大家注意在這個,這個協(xié)程當(dāng)中呢,它遇到了這樣的一個sleep0的時候,雖然這里Sleep0,但它依然會觸發(fā)一次切換,所以它最后還是會切換到Task0;之后,它會繼續(xù)進(jìn)行下一輪的一個執(zhí)行,當(dāng)然這里看起來會有這么多流程。

實際上呢,它的一個切換的速度是非常快的,它的執(zhí)行速度非???。所以看起來,這樣的一個程序最終的一個結(jié)果看起來會是一個并發(fā)進(jìn)行的,所以這里最終執(zhí)行的一個結(jié)果呢,大家看一下,如果直接去串行執(zhí)行這三個任務(wù)的話,它就會是一個8點多秒,大概是在5和3的加起來的一個值。如果是協(xié)程來計算,大概在5秒的樣子,那么5秒的樣子,它也就是5、3和0,這三個取得一個最大值,因為它這里會不停地進(jìn)行切換了。最終它的值會處在,就以這個最大的值是它的一個結(jié)果,來講它也是適用于I/O密集型的程序。實際上在這個工程開發(fā)當(dāng)中,協(xié)程用的多的是在這個非阻塞,異步并發(fā)的情況下,用的是比較多的,那么比如這個有一個Web框架,它就是用協(xié)程去做的,這個服務(wù)器的壓力依然很大,就是對我們來講,這個服務(wù)器的請求的數(shù)量依然不能夠有一個提升。

評估磁盤I/O

那么接下來,我們還要從哪些方面去考慮呢?接下來就是磁盤。大家想一想,磁盤有沒有可能成為阻礙我們這個請求提升的一個因素?

要去查看一個磁盤目前的狀態(tài),這里我提供兩點思路。一種,就是使用TOP命令,可以去看到磁盤信息,這里有大家注意到,磁盤信息,這邊有一個WA,這個詞它就表示的是等待輸入輸出的一個時間一個比例,當(dāng)然這個值是我模擬出來的,它當(dāng)時處在一個非常高的水平。這就說明,目前的一個I/O讀寫的一個能力,是在一個比較有壓力的位置。還有一種情況,就是去直接查看業(yè)務(wù)代碼,大家對自己的一個業(yè)務(wù)代碼相信是非常熟悉的,那么要去查,是否對磁盤的處理能力有要求,或者是否磁盤處理的能力壓力大的情況下,就查看業(yè)務(wù)代碼也可以,這也是比較方便的一種做法。大家知道在這個業(yè)務(wù)代碼當(dāng)中,比如說這里有一種讀文件的Read函數(shù),這里我寫的是偽代碼,假如大家在這個平常的一個業(yè)務(wù)代碼當(dāng)中會發(fā)現(xiàn)有這種場景,就是去讀一些大文件,或者最常見的是讀一些圖片,或者是呢,讀一些比較大的二進(jìn)制文件,或者是一些其他什么,比如一些AI訓(xùn)練當(dāng)中有一些模型訓(xùn)練當(dāng)中一些文件,這個對這個磁盤的要求非常高。

好,接下來,我們就希望去提升這磁盤的讀寫能力,我們接下來就會去針對這個磁盤進(jìn)行優(yōu)化。當(dāng)然說到優(yōu)化,依然是從硬件開始。這里我有一個程序,這個程序它是在讀取磁盤。對它進(jìn)行優(yōu)化最簡單,直接就將這個磁盤換為SID,從機(jī)械盤換成一個固態(tài)盤。方式雖然簡單,但是對于企業(yè)來講,或者對于大家的業(yè)務(wù)來講,這是一個成本非常高的一個選擇,大家知道這個固態(tài)盤它是非常貴的,相對于機(jī)械盤來說,它是非常不劃算的,那么我們有沒有一種方式,能夠用很多的這樣的一些機(jī)械盤,比較廉價的機(jī)械盤,把它拼起來成一個大的盤,或者讓它并行的去進(jìn)行一些處理?這樣我們的速度就能夠進(jìn)行一個提升,這樣就優(yōu)化了我們的思路。

但是大家看一下,如果我放了這么多盤在這里,大家看到中間這么一個圖,我放了很多盤在這里,那么對程序來講,我到底是訪問第一個盤呢,還是訪問第二個盤呢,還是訪問第三個盤呢?這對程序來講,又會是一個很大的難題,那么這里,我就想,如果我有這么一個工具,假如在這里有一個工具,它能夠幫我把上面的這些磁盤全部屏蔽掉,讓我不知道下面有這些磁盤,它來幫我做這樣的一些事情;而這個程序,只需要去讀這樣一個工具就好了,并且我希望讀這個工具,跟我直接讀一塊盤是一樣的,那么就更好了,那么有沒有這樣的一個工具幫我們?nèi)プ鲞@樣的事情呢?

磁盤陣列

            

接下來,就是我要給大家講的一個磁盤陣列,這個磁盤陣列技術(shù)(RAID),那么這里呢,會有幾種RAID,實際上RAID對讀操作來講,它本身是一個硬件,是一個RAID卡,這個卡可以插在這個主板上面,或者是有些主板上面它直接就帶有一種RAID卡。RAID卡的作用就是將這些磁盤去連接起來,可以認(rèn)為這上面它就是一個RAID卡,把這個磁盤進(jìn)行一個連接,它當(dāng)中會去做一些事情,來把這些磁盤組織起來,具體如何去讀來一塊數(shù)據(jù),都是由它來進(jìn)行組織的。而對于我們的一個計算機(jī)來講,直接去讀這個RAID卡就可以了。

            

這里講的RAID也會有幾種類型,它對磁盤的管理,比如說這里有個RAID0,第一個,我們給大家講一下,那么RAID0,它就是使用兩個以上的磁盤并聯(lián),它的速度是在所有型號當(dāng)中最快的。這是一個什么含義呢?就是比如這里我用一個D0和D1,這是兩塊盤那么就將它并起來,這就是我們前面所講到的我們所希望完成的一個功能,就是把這兩塊磁盤并列起來,把它的數(shù)據(jù)由這個RAID卡自己去決定,這一份數(shù)據(jù)是寫在第0塊盤上,或者是第一塊盤上,這個都是由RAID自己去決定的。

但是這里大家會發(fā)現(xiàn)一個問題,它的是沒有容錯能力的,因為我的一份數(shù)據(jù),要么寫在第0號上面,要么寫在第1號上面,如果這個數(shù)據(jù)丟失了,那么它就沒了,就跟我們使用一塊盤的時候是一樣的,而且大家知道這個隨著我的這個盤,比如這里我再接上一塊盤,隨著盤的增加,它出錯的一個概率,或者說數(shù)據(jù)丟失概率也會增加。那么我們就想一下,有沒有一種方式它更快,能夠滿足我們快速的需求,而且它還能夠幫我們進(jìn)行一些備份,或者進(jìn)行一些容錯?這個就是要引出今天要講的RAID1。RAID1是如何做得呢?它就是兩組以上的磁塊互做進(jìn)項,比如我有一個D0和D1,那么有了這兩塊盤之后,它就是對于這個RAID來講,來了一份數(shù)據(jù)之后,是第一塊盤上存儲一份,第二塊盤上存一份,那么這樣的一個數(shù)據(jù)呢,相當(dāng)于,它就做了一個備份,而且它的讀寫速度也幾乎是等于這個RAID1的一個讀寫速度,幾乎是等于RAID0的,它們兩個讀寫速度幾乎一樣。

但是呢,大家知道,它的缺點是什么呢?雖然它有一個容錯能力,比如說我第一塊盤丟失了,那么第二塊盤依然存在一個完整的數(shù)據(jù),并不影響我們上層的邏輯的一個功能。但是這里,同學(xué)們注意一下,它的每一份數(shù)據(jù)都是存了兩份的,而且如果這里還有第三塊盤的時候,那么相當(dāng)于第三塊盤,它也會把樣的數(shù)據(jù)再拷貝一份到這來,這樣的RAID1對磁盤無疑是造成了一種浪費,那么這個也是一種不經(jīng)濟(jì)的行為,就是比較浪費的。

對我們來說,這兩個方案各有優(yōu)缺點,第一個沒有容錯能力,但是它一個對磁盤的空間的利用率是比較高的。如果是第二個方案,它雖然是有容錯能力,但是它對磁盤的利用率不高,現(xiàn)在有沒有一種比較折中的方案,讓我們能夠去把兩種方案進(jìn)行一個結(jié)合,如果有這種方案,這種對磁盤的一個陣列的一個解決呢,就算是比較完美的。

接下來,我再給大家講一下這個RAID5,RAID5就屬于一個這種的方案,那么這個折中的方案是如何實現(xiàn)的呢?它的一個原理是什么,至少需要3個盤,這個是必不可少的。這里我是用四塊盤來作為演示的,數(shù)據(jù)存到上面,它是不是直接存的,就是對于這種備份據(jù)它也不是直接備份的,然后,它呢,是將這些數(shù)據(jù)進(jìn)行一個奇偶校驗,校驗之后,然后存到了另外的一塊盤上,比如說這里,我對A1,A2,A3這樣三個數(shù)據(jù),對它進(jìn)行計算一個奇偶校驗,計算出的結(jié)果我放在了第四塊盤上,對于B1、B2、B3進(jìn)行一個計算,放到了中間這塊盤上面。這樣做有什么好處?假如現(xiàn)在我的一個磁盤,比如最后一個磁盤已經(jīng)壞掉了,那么這個磁盤壞掉了之后,我就依然可以對它數(shù)據(jù)進(jìn)行一個重建。現(xiàn)在壞掉了之后,當(dāng)我插上一塊新的盤之后,這個RAID它會對這個數(shù)據(jù)進(jìn)遷移,遷移之后,比如這里,我要去計算這個AP的時候,重新根據(jù)這個A1,A2,A3去進(jìn)行計算,就能夠得到這個AP,那么對于這個B3它依然可以根據(jù)前面已有的三個數(shù)據(jù)去計算出這樣的一個A3的數(shù)據(jù),所以最終得到的一個結(jié)果,就是這個第四塊盤可以被完整的一個重建出來。這個RAID5,它也有速度快,而且呢,它也可以去進(jìn)行一定的容錯,所以那么這個方案算是比較折中的一個方案,這里就給大家介紹了三個RAID的一個型號。

這里大家還會想一個問題,就是說這個磁盤雖然它的速度比較快,但是我們還能不能去更快的訪問呢?這里就涉及到一個問題,就是大家想一下,在這個計算機(jī)當(dāng)中,我們除了能夠在磁盤上面去存儲數(shù)據(jù),還能夠在哪里去存儲這樣的一些數(shù)據(jù)呢?也是說在計算機(jī)當(dāng)中,我們的存儲設(shè)備還可以有哪些呢?這里有同學(xué)說到了這個內(nèi)存。

內(nèi)存

那么接下來,我們就想一下,能不能把這樣的數(shù)據(jù)直接存儲內(nèi)存里?因為內(nèi)存的數(shù)據(jù)相對于這個磁盤來講,它是一個數(shù)量級上的提升。

              

這里就給大家講到了一個優(yōu)化的點,那么就是去運用緩存。比如我們這里有兩個場景,一個是外賣,一個是電商。假如說外賣,我們?nèi)c這個外賣信息的時候,或者查看這樣的一些商家信息的時候,每個人都去查找,而且它返回的數(shù)據(jù)都是同一份,都會需要去從這個數(shù)據(jù)庫當(dāng)中去查找這樣的一份數(shù)據(jù),而且這樣的數(shù)據(jù),它變更并不是非常的頻繁,對于電商來講,他也是,電商來講,他給我們?nèi)タ匆粋€商品的信息,那么這個商品的信息它也變換的不是非常頻繁,而且信息都是相同的,而且每個用戶去訪問得到這樣的信息也都是相同的。如果放在數(shù)據(jù)庫當(dāng)中,或者放在這種磁盤上面去訪問的時候,勢必效率會非常低,這里我們就考慮把它放在內(nèi)存當(dāng)中。

這里給大家介紹Redis。它是一個內(nèi)存數(shù)據(jù)庫,那么用Redis來做一個緩存,像這樣不是經(jīng)常更新的數(shù)據(jù),而且不是這樣經(jīng)常更改的數(shù)據(jù),將它放在這個內(nèi)村當(dāng)中,當(dāng)這個程序去訪問想要訪問數(shù)據(jù)庫的時候,先要查這個緩存,通過緩存當(dāng)中拿到數(shù)據(jù),這樣它的效率就會進(jìn)一步的提升,我這里呢,給大家講一下,如何去通過這樣的一個緩存去讀寫數(shù)據(jù),大家可以看到,第一個,如果去獲取數(shù)據(jù)的時候,上面我們是直接讀的數(shù)據(jù)庫,而下面呢,我是先去讀這個緩存,如果緩存當(dāng)中有,那么直接就返回,就得到了一個這樣的數(shù)據(jù),如果緩存當(dāng)中沒有,我就去讀這個數(shù)據(jù)庫,讀了數(shù)據(jù)庫之后,大家一定記住,下一步是要去把這個讀到的數(shù)據(jù)寫到緩存里面,方便下一次讀。下一次讀取的時候,我就知道這個緩存當(dāng)中有這個數(shù)據(jù),就不用再去查到這個數(shù)據(jù)庫,這個就是一個讀取數(shù)據(jù)庫,如果要寫入數(shù)據(jù)的時候,要如何去做呢?寫入數(shù)據(jù)的時候,就是直接寫數(shù)據(jù)庫,同時寫的時候,依然要記住去更新一下這個緩存當(dāng)中的一些數(shù)據(jù)。

軟件存儲優(yōu)化

            

這里就是緩存的一個最簡單的一個讀寫的模型,就是右邊這種形式。那么如果大家想一下在這種情況下面,它有沒有一些問題?或者有沒有一些容易出錯的地方?這里給大家提示一下,第一種呢,假如我這個數(shù)據(jù)庫,這個程序,假如我這個程序是訪問這個緩存的時候,把緩存當(dāng)中沒有數(shù)據(jù),那么它是不是要繼續(xù)去訪問這樣的一個數(shù)據(jù)庫?訪問這個數(shù)據(jù)庫,它依然是沒有數(shù)據(jù)庫,那么相當(dāng)于,我這個緩存并沒有起作用,而且它還增加了這樣的兩次訪問,這是一個問題,還有一個問題,假如我這樣的一個緩存宕機(jī)了,那么宕機(jī)了之后,緩存崩潰,宕機(jī)了之后,所有的請求都會走到后面的這樣一個數(shù)據(jù)庫去,那么這里也會存在一個問題。

接下來給大家講一下這兩個問題。它的一個比較常見的解決方式,第一個就是緩存穿透的問題,就是剛才給大家講到的,假如說我在這個緩存當(dāng)中去訪問,并沒有找到這個數(shù)據(jù),而且在這個數(shù)據(jù)庫當(dāng)中也沒有找到,那么這種情況呢,相當(dāng)于是繞過了這樣的一個緩存,如果解決的話呢,第一個去給這個查詢的時候,在數(shù)據(jù)庫當(dāng)中查詢?yōu)榭盏臅r候,那么也去給這個緩存當(dāng)中寫上這樣一份緩存,那么下次獲取的時候,但是我知道這個數(shù)據(jù)是不存在的,而且數(shù)據(jù)庫當(dāng)中也沒有,那么我就不會再去訪問這個數(shù)據(jù)庫了;還有可以去增加一些過濾器,去給它提前進(jìn)行過濾。

那么還有一個,剛剛有同學(xué)提到了,是一個緩存失效,緩存崩潰的問題,緩存崩潰,或者是緩存失效,因為緩存我們會有更新時間,假如你設(shè)置了一個同樣的更新時間,在這個統(tǒng)一時間內(nèi),這個緩存都失效了,那么他們就會大量的一個重復(fù)更新的一些請求,需要從數(shù)據(jù)庫更新到這個緩存當(dāng)中,這個請求對數(shù)據(jù)庫來講也是一個比較有壓力的實行,我們設(shè)置緩存失效時間的時候,就可以設(shè)置成一個隨機(jī)的時間?;蛘咴谶@個數(shù)據(jù)庫讀寫的時候,我們要去控制它的一個讀寫的線程,那么這里呢,就是提到緩存可能會存在的兩個問題,講到這里呢,幾乎我們已經(jīng)涉及到了一個對CPU的優(yōu)化,和對這個存儲方面的優(yōu)化。

網(wǎng)絡(luò)優(yōu)化

那么接下來,大家想一下,還有可能是哪一方面?可能會去進(jìn)行限制請求數(shù)量的增長。那么就是網(wǎng)絡(luò)問題。

首先要去了解網(wǎng)絡(luò)問題,我們可以先去查看一些,最基礎(chǔ)的可以查看一些硬件信息,比如說查看網(wǎng)絡(luò)帶寬,或者去查看一下程序在運行的時候,我們在后臺查看一下網(wǎng)絡(luò)當(dāng)前的流量是否已經(jīng)到達(dá)了非常高的值,這里給大家提一下,兩個點,這個使用的命令是IFTOP命令,如果沒有軟件的話,可以直接去裝上這樣的一個軟件,就可以了,那么下面還介紹一下這個TX和RX,這兩個呢,分別是發(fā)送和接收的一個流量,就是發(fā)送和接收的流量,大家通過這兩個值也可以看到當(dāng)前網(wǎng)絡(luò)的一個情況。

針對網(wǎng)絡(luò)的問題,依然我們最簡單的能夠想到,一個硬件問題,那么硬件問題呢,就比較好解決了,那么就去選取網(wǎng)卡,如果是自建機(jī)房,就比如用千兆網(wǎng)卡換成萬兆網(wǎng)卡,或者是變口換成端口,如果是云平臺,大家就需要在這個控制臺上去升級帶寬。

            

當(dāng)然這樣的方式是比較簡單的。還有一種呢,就是DNS域名解析的問題,大家知道這個我們訪問域名的時候,實際上是通過這個域名解析的一個服務(wù)器,當(dāng)中去拿到了這樣的一個實際的IP,并且通過這個IP去進(jìn)行訪問,那么如果說這個域名服務(wù)器它解析的速度非常的慢,這個就會導(dǎo)致我們的服務(wù)器,整個請求鏈時間也會變得慢,那么這個解決方法也是比較簡單的,就是會去更換域名服務(wù)器,這些域名服務(wù)器的提供商,我們也可以去選擇一些更優(yōu)質(zhì)的一些服務(wù)提供商去來進(jìn)行服務(wù)。

內(nèi)核參數(shù)優(yōu)化

接下來,網(wǎng)絡(luò)優(yōu)化呢,接下來給大家講一個稍微復(fù)雜一點的優(yōu)化,那么這個呢,就是一個內(nèi)核參數(shù)的一個優(yōu)化。

大家裝系統(tǒng)的時候,默認(rèn)了系統(tǒng)內(nèi)核的一些參數(shù),它不一定適合你當(dāng)前的業(yè)務(wù)場景,包括你的一些硬件配置,這些內(nèi)核參數(shù),可以去對它進(jìn)行一些調(diào)整。

這里要講這個內(nèi)核參數(shù)的優(yōu)化。我就先給大家去講一下這個關(guān)于握手連接,給大家回憶一下。在這個連接過程當(dāng)中,首先是服務(wù)器會去監(jiān)聽你的端口,綁定一個地址,監(jiān)聽你的端口;然后,由客戶端去發(fā)起一個請求,發(fā)起了請求之后,發(fā)送一個SYN包到服務(wù)端,服務(wù)端接收了線包之后,他會回一個SYN加ACK,最后客戶端接收到之后,它會再回一個ACK到服務(wù)端,這三步完成之后,整個連接就建立起來了.基于這樣的一個思路,那我們在這個系統(tǒng)當(dāng)中,實際上它會維護(hù)兩個隊列,一個隊列是半連接隊列,第一個隊列它維護(hù)的是,里面存的是一個什么樣的一些東西,存的就是這個客戶端,過來請求的時候,它一些來不及處理的這樣的一些連接,那么它會放到這樣一個隊列里,大家知道,如果說這個連接是一個一個過來,是沒有問題的,當(dāng)如果是大量的這樣的請求過來,服務(wù)器不能在瞬間去處理好這些請求的時候,它就會需要放到這樣的一個隊列里面,去進(jìn)行一個處理,接下來一個全連接隊列,就是這樣已經(jīng)建立好的一些連接,就會放到這個全連接隊列當(dāng)中。這里假設(shè)兩個隊列的原因是什么呢?因為大家想一下,這兩個隊列的長度就決定了,我們這個系統(tǒng)當(dāng)前能夠接收到的一個請求的一個上限。

              

這里我給大家講幾個比較關(guān)鍵的參數(shù),比如第一個參數(shù),當(dāng)然這個參數(shù)比較長,后續(xù)大家拿著課件之后,可以再仔細(xì)的去研究。第一個參數(shù)呢,是來控制這個SYN半連接隊列長度的,后面的1024是它的一個默認(rèn)的一個值,也就是裝好系統(tǒng)之后,默認(rèn)的是1024,當(dāng)然這個值對系統(tǒng)來講是一個比較小的一個值,我們可以通過去修改這樣的一個值,讓它的把這樣的半連接隊列去變得更長,我能夠允許這樣一個名詞一個連接的長度可以變得更長。

那么下面還有一個就是這個全連接隊列長度,就是這個程序控制的,當(dāng)然這個值,這個地方是128。它控制的就是這個全連接的長度,它的長度也就決定了,我這個系統(tǒng)當(dāng)前能夠維護(hù)多長的一個監(jiān)聽的一個隊列,這里還要給大家提一點就是,我的半連接長度,實際上受制于這個全連接的長度,也就是說,我半連接長度的這個值,哪怕這個地方是1024,但是它實際上能夠起效的一個值是128,它會選這兩個值當(dāng)中最小的一個值,去進(jìn)行一個生效,這里講了兩個連接隊列。

最后再給大家講一個,這個是一個Time_Wait,而維持Time_Wait最大數(shù)量的一個參數(shù),那么它們是為什么是在Search,Watch斷開當(dāng)中會出現(xiàn)的一個狀態(tài)?那么這個Time_Wait狀態(tài)如果過多的話,也有可能去把這個系統(tǒng)就拖死,所以呢,設(shè)置這樣的一個值也可以去幫我們進(jìn)行優(yōu)化,這里講呢,這幾個值要如何去配置呢?也稍微提一下,要配置的時候,這里先講一個sysctl-a,去查看目前已經(jīng)生效的一個參數(shù),通過sysctl-a可以去查看,但是如果直接去使用了時候,它的一個會顯示出來的值會非常的長。

所以呢,這里用Grep去過濾一下,這里我過濾了一個值。要修改也是比較簡單的,就直接去修改這樣的ETC下面的配置文件,然后使用Sysctl-p去進(jìn)行一個生效就可以了,當(dāng)然這里需要給大家提醒一下,需要去注意的,修改內(nèi)核參數(shù)的時候,需要對系統(tǒng)有充分的了解,然后再去修改,而且這個值也并不是越大越好,需要經(jīng)過一系列的測試。根據(jù)你當(dāng)前系統(tǒng)的一個運行配置,然后來測試,還有你的業(yè)務(wù)才能決定,到底什么樣的值是比較符合這樣的一個系統(tǒng)的。

              

負(fù)載均衡

那么我們接下來再看一下,因為我們已經(jīng)講到了CPU,也講了存儲,還講了這樣的一些網(wǎng)絡(luò)上的優(yōu)化。如果我還想,這個系統(tǒng)能夠再進(jìn)一步的去提升它對外服務(wù)的一個能力,還有哪些方面可以去進(jìn)行一個優(yōu)化。

在目前的業(yè)務(wù)當(dāng)中。單機(jī)是比較難去滿足這樣的一些場景的,通常我們就會去進(jìn)行一個橫向擴(kuò)展,橫向擴(kuò)展之后,它可以去優(yōu)化整體的服務(wù)能力,因為對用戶來講,或者對客戶端來講,它只要能夠訪問到這樣的一個服務(wù),它并不關(guān)心后面是一個服務(wù)器,還是一個集群對它進(jìn)服務(wù)的。但是這里也會帶來一個問題,那么就是集群會帶來一個你的程序,或者是你后端的一些設(shè)計復(fù)雜度提升。

            

這里講到了個集群。那么對用戶來講,我要如何知道,我去訪問后面,到底訪問哪一臺機(jī)器呢?那么這里就會去講到一個負(fù)載均衡的問題,對用戶來講我期望去訪問到中間這臺服務(wù)器,那么我期望有這樣一個東西在這里,讓我去訪問它,而它幫我去做一些事情,它來由中間這個服務(wù)器來決定,最終這個請求是到了那一個后端,那么這樣呢,對客戶端來講,那就是比較友好的。所以這里呢,中間這樣的一個服務(wù)器就承擔(dān)了任務(wù)分發(fā)的一個角色,那么這個中間這個一個抽象的服務(wù)器,它可以是硬件,它也可以是單獨的一個服務(wù)器,上面裝了這樣一些軟件,硬件這里我列了兩家,一個是F5,一個是A10  Networks,這兩個公司他們都是非常好的硬件提供商,他們的硬件能夠很好的去解決這樣一個業(yè)務(wù)分發(fā)的問題。

但是在這樣的硬件雖然好,它的價格是非常昂貴的。比如像這個F5的話,這樣的設(shè)備應(yīng)該都是在百萬級別的,那么這樣的一些昂貴的設(shè)備,可能在一些業(yè)務(wù),對一些業(yè)務(wù)來講,它的一個收益并不是那么的明顯,或者說對它來說性價比不是很高,這里我們就可以去用這種軟件的一個負(fù)載均衡,那么軟件的負(fù)載均衡呢,這里也給大家提供兩個思路,一個是LVS,這個是現(xiàn)在已經(jīng)被納入了這個Linux內(nèi)核的一個技術(shù),它是基于OSI網(wǎng)絡(luò)模型當(dāng)中第四層(傳輸層)來做得,那么這個nginx是基于應(yīng)用層(7層)來做的,當(dāng)然這兩個,他們也有各自的一個應(yīng)用場景,也并不是說這個層數(shù)越低越好,或者層數(shù)越高的越好。大家需要注意一下。

       

這里講到了負(fù)載均衡,它中間這個服務(wù)器會進(jìn)行任務(wù)分發(fā)。那么它是如何知道,我要把這個任務(wù)發(fā)給誰呢?這里是隨便發(fā)都可以嗎?還是說有一定的規(guī)則?這里就給大家講一下它的一個常見的這個算法,那么當(dāng)然隨便發(fā)我這里是可以是一種叫隨機(jī)的算法,最常見的有一種算法,叫輪詢算法。輪詢算法的含義就是它追求的就是一個絕對的均衡,就比如這個地方我有一個請求,要發(fā)送,這個請求發(fā)送,它是一個中間抽象的這個任務(wù)分發(fā)的一個主機(jī),那么就是將請求比如來了一個請求,發(fā)給第一個機(jī)器,那么第二個機(jī)器我發(fā)給第二個機(jī)器。第三個請求就發(fā)給第三個機(jī)器,那么這樣就是一個絕對的均衡算法,它就是進(jìn)行一個輪詢。

             

還有一個,就是最小連接數(shù)算法。它這里會,就計算,去計算每一個服務(wù)器的一個當(dāng)前的一個連接數(shù),然后呢,它會把這個請求轉(zhuǎn)發(fā)到一個最小的一個,當(dāng)前連接數(shù)最小的一個服務(wù)器上面,這個連接算法,雖然它會比上面的這種RR算法要好,但是這個LC算法實現(xiàn)難度,也會去相對來更高一點。

還有一種就是源地址HASH,這個它能夠保證我客戶端的一個IP始終訪問某一臺服務(wù)器,它的大致的思路呢,就是首先對這個客戶端的IP進(jìn)行HASH,對服務(wù)器的一個列表進(jìn)行一個取模,取模之后,最后指定到某一臺服務(wù)器上面。這樣就能保證,我同一個IP過來的請求能夠到達(dá)某一個指定的服務(wù)器上面,這里就是一個常見的負(fù)載均衡算法。

3 峰值場景優(yōu)化

當(dāng)然前面,這邊已經(jīng)給大家講了這么多種優(yōu)化的場景,其實這些場景當(dāng)中,沒有考慮到一個問題,那么就是請求波動很大的情況,那么這種情況是比較難處理的,我單獨去把它拎出來說。

              

在請求這個波動很大的就是我們所遇到的一些峰值場景,峰值場景下,比如,我們來看一下,第一個,它在一個時間上分布不均,假如說是一天的一個時間,這個數(shù)軸它是一個請求的次數(shù),單位是百萬次,大家可以看到,這里呢,平常的請求都是維持在一個很低的水平,但是到了某一個時間點,它會到達(dá)一個比較高的一個峰值,那么到達(dá)這個頂尖之后,也就對服務(wù)器產(chǎn)生的要求很大,這種場景比如說是外賣平臺,它到了這個中午的時候,可能大家點外賣的這個請求量就集中的去爆發(fā)了;或者票務(wù)網(wǎng)站,比如看演唱會的門票,到達(dá)某一時間放票的時候,或者到了某一個流量的明星,他的一個票務(wù)的時候,那么這個對網(wǎng)站來說,它的請求量就會是一個有明顯的峰值,那么針對這樣的一個場景,要如何去進(jìn)行優(yōu)化?或者能不能去進(jìn)行優(yōu)化呢?

              

我們看一下,這種情況也會分為兩種,第一種就是有規(guī)律的一個場景,比如說外賣網(wǎng)站,每天我知道到了中午就會有一個小高峰,每天中午都會有個小高峰,我就可以去運用這種容器技術(shù),或者多機(jī)集群的方式,來進(jìn)行一個定時的擴(kuò)容,每天到了中午的時候,比如這里到了預(yù)測到達(dá)的這個高峰之前,我就可以在這個點提前的對它進(jìn)行擴(kuò)容,讓這個整體的一個服務(wù)能力有一個提升,然后能夠很好地去處理這樣一個峰值情況。然后渡過了高峰之后,比如在這個地方,然后對這樣的一些容器進(jìn)行銷毀,可以去節(jié)約資源。

容器(Docker)技術(shù)

那么接下來,我給大家再提一下,一個當(dāng)前比較流行的一個容器技術(shù),以Docker為例來講一下。

它的優(yōu)勢,是在于它的一個機(jī)器的輕量,它雖然說名字叫容器,但是它只打造了一些代碼和代碼相關(guān)的一些運行環(huán)境,而且它的部署也是非??斓?,當(dāng)然部分的一些容器甚至可以做到毫秒級。這個也是與具體的業(yè)務(wù)場景相關(guān)的,平常有一些是秒級,當(dāng)然有些也可能時間更長一點,這就是它的一個部署的方便性。還有呢,它就是一個移植性非常好,鏡像打包,它打包了這個代碼和運行環(huán)境,那么它在其他平臺上去運行也是非常方便的。而不需要去依賴這個環(huán)境上面的本身的這個數(shù)字機(jī),身的一個這些環(huán)境配置的依賴。

還有一個好處是彈性伸縮,基于Docker,基于這個技術(shù)現(xiàn)在有很多的調(diào)度平臺的一些解決方案。比如Kubernetes,這樣的一個調(diào)度平臺。它是非常強(qiáng)大的,而且是背靠Google的,這樣的一個調(diào)度平臺它可以讓這個Docker的生命力變得更加的頑強(qiáng)。

Docker應(yīng)用

那么接下來,我給大家看一下如何去使用Docker的一個流程,將它和傳統(tǒng)方式進(jìn)行一個對比。

              

這里我已啟動Flask作為Web服務(wù)器。例如,當(dāng)傳統(tǒng)方式,我要去啟動一個Flask來作為Web服務(wù)器的時候,把這里可能首先就要有一臺主機(jī),我要去配置運行環(huán)境,配置運行環(huán)境之后呢,我需要在當(dāng)中去裝上一些工具或者軟件,比如Python,首先要有Python,而且,它還要有Flask這樣的一個軟件,使用PIP去安裝上?;蛘呶疫€需要一個Request這樣的一個庫,如果有一些其他功能,我甚至還需要去安裝更多的一些庫。

那么最后呢,就是啟動這樣的一個程序進(jìn)行運行。如果是使用容器的方式,我要怎么操作呢?可能在最開始,我就只需要打包好這樣的一個容器,有了這樣一個Image(鏡像)之后呢,我只需要這樣一個宿主機(jī)上面有這樣一個Docker  Engine這樣的容器引擎,然后通過這個容器引擎下載了這個鏡像之后就對它進(jìn)行啟動。

這里就容器的啟動方式,可能這里大家的感覺,兩種方式都是3步,而且看起來是差不多的。實際上,這里容器在這里它會有兩個優(yōu)勢,一個優(yōu)勢就是它比如有非常復(fù)雜的代碼,在于它的一個可移植性是非常方便的,因為我只要求,這樣的一個數(shù)據(jù)集上面有一個Docker  Engine我就可以啟動了。或者是,我需要進(jìn)行多機(jī)擴(kuò)展的時候,比如我現(xiàn)在要再創(chuàng)建一臺虛擬機(jī),我不需要去裝其他的依賴的運行環(huán)境,只需要去有這樣一個Docker  Engine在這里,那么我就可以去運行這樣的一個容器,或者運行一些其他的容器都可以。

這里,就是給大家講了一個關(guān)于規(guī)則流量下的一個處理方式。最后大家也許會問,如果實在有這樣不規(guī)則的請求,那要怎樣去處理呢?

無規(guī)律峰值

接下來我們就講一個無規(guī)律峰值的情況,那么這種無規(guī)律峰值的情況,首先當(dāng)然服務(wù)是要允許可異步的服務(wù),就可以對這個流量進(jìn)行一個削峰,將流量進(jìn)行分?jǐn)?。比如說我到這個地方有了一個流量的突增,突增之后對于這個服務(wù)器來講,它肯定是處理不了的,比如對這個地方對服務(wù)器來講,它肯定是處理不了這樣突增的請求,那么對如果時間是這樣,它的一個請求可能被丟棄,或者被延遲處理。

            

在這里,我們中間增加一個這樣的消息隊列,對它進(jìn)行一個緩沖,當(dāng)有了這樣一個流量洪峰過來之后,我用這個消息隊列把這樣一個流量給它接住,接住之后,然后再這邊再平緩的去給它把這些流量平滑的去放給它的下端,這里就類似于我們的那個防洪大壩這樣的一個概念,它可以先把這樣的洪水接住,然后再平滑的去放給下游,這樣對下游的一個壓力比較小的。

流量削峰

如何去處理這樣的一個隊列呢?這里就是削峰的一個場景。

              

假如這里我的一個客戶端,它的一個發(fā)送的能力是150萬次每秒,而對于消息隊列呢,它能夠去承載的是一個一千萬的一個請求,那么對于這個服務(wù)端呢,它是600次每秒的一個處理能力,如果這樣的150萬的能力請求,直接到后端,是沒有問題的,但是呢,我們這個請求如果到達(dá)了一個雪山的高峰,比如說這里,到達(dá)了八百萬,或者九百萬次,對這個服務(wù)端的壓力是非常大,那么它處理不了的請求可能就會丟失了。如果中間有了這樣的一個消息隊列之后,消息隊列就可以將這樣上面這樣一些多余的流量先將它接住,接住之后,由這個消息隊列通過一個平緩的方式將這個流量再發(fā)送給服務(wù)端去進(jìn)行一個處理,那么這樣呢,所有的請求就會被分?jǐn)?。大家看一下這個紅色的線是六百萬次,當(dāng)請求把這些上面的高峰都處理掉之后,大概就成了下面這個樣子,它引入這種又高又尖的這樣一個流量請求,最終會變得平緩。

4 小結(jié)

好,那么今天到這里,差不多的一個優(yōu)化的一個方案大概就講到這里,差不多結(jié)束了,我給大家稍微總給一下今天的主要內(nèi)容。

              

那么前面先給大家講了請求壓力的來源,我們要先分析是資源不足,還是資源不合理,如果不足的話,那么就去增加資源,這樣就是沒有其他更好的辦法了;如果是資源不合理,這個就是我們今天主要講的要去面對的一個問題,資源不合理的時候,就需要去,先去判斷它是如何不合理的,當(dāng)不合理當(dāng)中,我們還會有一個沒有峰值的場景呢,就會去從計算上面去優(yōu)化,從存儲上面去優(yōu)化,或者從網(wǎng)絡(luò)上面去優(yōu)化,或者從集群上面去優(yōu)化,分為這幾個方面去進(jìn)行優(yōu)化。

對于有峰值這樣比較難處理的請求,我們依然會有兩種方案去解決,也會分為規(guī)律或者是不規(guī)律的,如果是規(guī)律的呢,就使用這種容器化定時擴(kuò)容的思路;如果是可以去異步的服務(wù)呢,我們可以去使用隊列,進(jìn)行消息的削峰。

Q&A

(1) 如何不重啟服務(wù),把一臺服務(wù)器加到Nginx負(fù)載下?

那么這個NGINX這個加到負(fù)載下,Nginx它本身是可以去進(jìn)行動態(tài)加載的,這個使用命令去配置好之后,使用命令去動態(tài)加載就可以了。

(2) 這里還有緩存不存在,緩存不存在的鍵如何避免查詢,不存在的鍵攻擊?

這個問題呢,就需要去從這個緩存前面,大家可能去從前面去進(jìn)行一些處理,緩存本身這樣去做處理這樣的一個問題可能比較難,大家可以考慮在緩存前面增加一些過濾,這樣的一些方法,可以去達(dá)到這樣的一個目的,防止進(jìn)行一個惡意攻擊。這里還有,我們看一下還有什么。

(3) 云用的是什么方式存儲數(shù)據(jù)的?

這個云用的,現(xiàn)在公有云的存儲方式,這種大型的存儲用的多的一般是用對象存儲,比如像這種開源的一個存儲方案,像Ceph,這樣的一個存儲方案,大家可以去了解一下,在屏幕上寫一下。Ceph,這樣Ceph是一個現(xiàn)在非常強(qiáng)大的一個對象存儲,還有一個呢,是Swift 這樣的一個,也是一個對象存儲方案,這兩個都是開源的,這個Swift 還是基于Python去實現(xiàn)的。這里是兩個開源的對象存儲,那么像公有云用的多的,像亞馬遜的ES3,或者像我們金山云也有 KS3 這樣的,它也是基于這樣對象存儲去實現(xiàn)的。

(4) 內(nèi)核參數(shù)擁抱半連接,全連接?

半連接,如果熟悉三次握手的話,你就會比較方便的去理解這樣的一個東西,這里我大概畫一下,在第一次握手的時候它會發(fā)送一個SYN的一個號給這個服務(wù)端,發(fā)送給這個服務(wù)端,發(fā)送給服務(wù)端之后,大家知道,這個發(fā)送過來,如果是一個,那當(dāng)然沒有問題,服務(wù)端會立馬處理這樣的一個連接。如果是發(fā)送了很多,比如說這里是成千上萬,或者是幾十萬這樣的一個請求過來之后,服務(wù)端它不能夠立即去處理這樣的一個請求,所以它本身在這個內(nèi)核當(dāng)中,它會有這樣的一個隊列,它會有這樣的一個隊列當(dāng)中,它會去存放來的這樣一些請求的SYN的一些數(shù)據(jù),那么它存放之后,對于這個內(nèi)核來講,它處理的實際上是從這個隊列當(dāng)中出來的一些數(shù)據(jù),一些出來的一些SYN,它就從這個隊列當(dāng)中去拿到這樣的一些結(jié)果,那么這個就是一個我們所說的一個半連接的一個隊列,那么全連接呢,就是三次握手結(jié)束之后,對于這個客戶端和服務(wù)端已經(jīng)建立好了一個連接,比如說這里已經(jīng)建立好了連接,建立好了連接之后,這樣的一些連接的請求,它也會存放在一個隊列當(dāng)中,這個隊列就稱之為是一個全連接的隊列,不知道這樣說,這位同學(xué)可清楚嗎?

(5) 流量削峰是系統(tǒng)默認(rèn)嗎?

這個是需要你本身從這個業(yè)務(wù)層面去實現(xiàn)的這樣一個東西,就是你需要去在你的一個代碼層面去處理你的一個連接。

(6) 典型的I/O密集場景是什么?

協(xié)程不是只適用于這個I/O密集場景,協(xié)程在工程當(dāng)中,實際上協(xié)程用的多是這種非阻塞,異步并發(fā)的場景用的比較多。典型的I/O密集型場景,典型的IO密集型場景就是和計算相關(guān)的。I/O密集型場景就是去讀取數(shù)據(jù)比較多的情況,就是在你的這個程序當(dāng)中,你要去讀取數(shù)據(jù),比如說我現(xiàn)在要去對一張圖片進(jìn)行處理,如果我只是對圖片進(jìn)行一個讀取,并且保存,這樣的一個簡單請求,那么這個,它就可能是一個I/O密集型的,或者是我要讀入一個一些比較大的一些,比如說我要去讀取一個字典,比如一個字典數(shù)據(jù),這些數(shù)據(jù)去讀到這個內(nèi)存當(dāng)中,那么這都是一些I/O密集型的。

(7) 三次握手,SYN,ACK時候是什么連接?

這里可能這位同學(xué)沒明白剛剛說的半連接和全連接,半連接和全連接,它應(yīng)該是這個在為了方便去理解這個系統(tǒng)的時候,給出的一個定義,它定義的呢,就是在這個SYN,最開始發(fā)送到服務(wù)端的時候,那么這個我再說一下這里,可能有點不清楚,SYN發(fā)過來的時候,由于服務(wù)端它來不及處理,它這里會有一個隊列,去對這個進(jìn)行一個保存。這個隊列就在系統(tǒng)層面,它取了一個名字就叫半連接隊列,它跟這個本身的一個三次握手連接的一些定義是沒有關(guān)系的。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多