這么牛的畢業(yè)生,來當CTO吧!
時光如風飄渺,眨眼間已經(jīng)在行業(yè)浸潤多年了,見過無數(shù)厲害的人物,也見過更多更多的挫B。
前幾天剛上班,就接到面試一個畢業(yè)生的任務,讓我感嘆人與人之間的差距。
他的水平,絕對的完爆工作多年的架構師。在下佩服之~
我們的話題,是關于怎么構建一個可伸縮的高可用、高可靠大型網(wǎng)站。嗯,就讓我們開始吧。
1. 要發(fā)問了
大家都知道,如今的互聯(lián)網(wǎng),數(shù)據(jù)量爆炸,服務請求飆升,即使是非常小的公司,也可能因為某個產(chǎn)品產(chǎn)生不同于往日的數(shù)十倍流量,當然這有時候是個夢想而已。
流量增加就意味著對后端服務能力的增加,如何構建每秒處理GB數(shù)據(jù)、QPS超過數(shù)十萬的大型系統(tǒng),就變成了一個挑戰(zhàn)。尤其是某些天才的秒殺創(chuàng)意,讓這個流量變的越發(fā)變態(tài)不可預料。
在有效的資源下,如何讓系統(tǒng)保持良好的反饋?以支撐老板們的夢想呢?你有什么處理方式?或者你有什么體系化的心得體會要和我分享一下的?
畢業(yè)生微微一笑:“我在這方面正好有點總結,我可以多花點時間聊聊這個”。
好吧,洗耳恭聽。
2. 服務建設的重要指標
我首先要說明的是,服務的建設要關注幾個指標。有了目標就有了方向,大體上我總結了四個”:
- 可用性。 我們要保證服務的可用性,也就是SLA指標。只有服務能夠正常響應,錯誤率保持在較低的水平,我們的服務才算正常。
- 服務性能。 可用性和性能是相輔相成的,服務性能高了,有限的資源就能夠支撐更多的請求,可用性就會提高。所以服務性能優(yōu)化是一個持續(xù)性的工作,在巨量流量下每1ms的平均性能提升,都是值得追求的。
- 可靠性。 分布式服務的組件非常多,每個組件都可能會產(chǎn)生問題,影響面也不盡相同。如何保證每個組件的運行時高可靠性,如何保證數(shù)據(jù)的一致性,都是有挑戰(zhàn)的。
- 可觀測性。 想要獲取服務優(yōu)化的指標數(shù)據(jù),就要求我們的服務,在設計開始能夠保證服務的可觀測性。宏觀上能夠識別組件類故障,微觀上能為性能優(yōu)化提供依據(jù)。在HPA等自動化伸縮場景中,遙測數(shù)據(jù)甚至是自動化決策的唯一依據(jù)。
對于一個服務來說,擴容的手段主要有兩種:
- scale-up:垂直擴展;
- scale-out:水平擴展。
垂直擴展通過增加單臺機器的配置來增加單節(jié)點的處理能力。這在某些業(yè)務場景下是非常有必要的。但我們的服務更多的是追求水平擴展,用更多的機器來支撐業(yè)務的發(fā)展。
只要服務滿足了橫向擴展的能力,滿足無狀態(tài)的特點,剩下的事情就是堆硬件了。聽起來很美好,但實際上,對整個體系架構的挑戰(zhàn)非常的大。
畢業(yè)生的一番分析像極了野雞CTO的發(fā)言,廢話連篇。我暗自點頭,鼓勵他繼續(xù)深入、細化下去,拿出點不一樣的東西。
3. 冪等性
如果接口調(diào)用失敗怎么辦?在早期的互聯(lián)網(wǎng)中,因為網(wǎng)絡原因,這樣的情況可能更嚴重。HTTP狀態(tài)碼504,就是典型的代表網(wǎng)關超時的狀態(tài)。第一次請求可能會超時失敗,第二次可能就成功了?,F(xiàn)實中需要嚴格重試的接口還是蠻多的,尤其是異步化的加入,使得重試變得更加重要。
但我們也要考慮由于快速重試所造成的重試風暴,因為超時本身可能就意味著服務器已經(jīng)不堪重負,我們沒有任何理由火上澆油。所以,重試都會有退避算法(exponential backoff),直到真正的結束請求進入異常處理流程。
可以看出,由于超時和重試機制的引入,服務的冪等變的格外重要。它不僅僅是在單臺機器上支持重復的調(diào)用,在整個分布式集群環(huán)境中同樣保證可以重入多次。
在數(shù)學上,它甚至有一個優(yōu)美的函數(shù)公式:
f(f(f(x))) = f(f(x)) = f(x)
一旦接口擁有了冪等性,就有了能夠忍受故障的能力。當我們因為偶發(fā)的網(wǎng)絡故障、機器故障造成少量的服務調(diào)用失敗時,可以通過重試和冪等很容易的最終完成調(diào)用。
對于查詢操作來說,在數(shù)據(jù)集合不變的情況下,它天然是冪等的,不需要做什么額外的處理。比較有挑戰(zhàn)的是添加和更新操作。
有不少的技術手段來保證冪等,比如使用數(shù)據(jù)庫的唯一索引,使用提前生成好的交易ID,或者使用token機制來保證唯一調(diào)用。其中,token機制被越來越多的使用,其做法是在請求之前,先請求一個唯一的tokenId,此后的調(diào)用冪等就圍繞著tokenId進行編程。
4. 健康檢查
自從k8s把健康檢查這個東西標準化之后,健康檢查就成為了一個服務的必備選項。在k8s中,分為活躍探針(liveness probe)和 就緒探針(readiness probe)。
活躍探測主要用來查明應用程序是否處于活動狀態(tài)。它只展示應用本身的狀態(tài),而不應依賴于外部其他系統(tǒng)的健康狀態(tài);就緒探測指示應用程序是否已準備好接受流量,如果應用程序?qū)嵗木途w狀態(tài)為未就緒,則不會將流量路由到該實例。
如果你使用了SpringBoot的actuator組件,通過health接口,將很容易獲取這部分功能。當容器或者注冊中心通過health接口判斷到服務出現(xiàn)了問題,會自動的把問題節(jié)點從節(jié)點列表中摘除,然后再通過一系列探測機制在服務恢復正常的時候再把它掛上去。
通過健康檢查機制,能夠避免流量被調(diào)度到錯誤的機器上去。
5. 服務自動發(fā)現(xiàn)
早期的軟件開發(fā)人員,對服務上線的機制摸的門清,不是因為他們想要這樣,而是不得不這樣做。
比如,我要擴容一臺機器,需要首先測試這臺機器的存活性,然后部署服務,最后再在負載均衡軟件比如nginx中將這臺機器配置上。通常情況下,還要看一下日志,到底有沒有流量到這臺機器上來。
借助于微服務和持續(xù)集成,我們再也不需要這么繁雜的上線流程,只需要在頁面上點一下構架、發(fā)布,服務就能夠自動上線,并被其他服務發(fā)現(xiàn)。
注冊中心在服務發(fā)現(xiàn)方面承擔了非常重要的角色。它相當于一個信息集中地,所有的服務啟動、關閉,都要上報到這里;同樣,我想要調(diào)用某些服務,也需要到同一個注冊中心去查詢。
注冊中心相當于一個中介,將這些頻繁的上下線需求和查詢需求,全部統(tǒng)一起來進行管理,現(xiàn)在已經(jīng)成為微服務的必備設施。
這些查詢需求可能是非常頻繁的,所以在調(diào)用方本地,同樣也會存儲一份副本,這樣在注冊中心出現(xiàn)問題的時候,不至于因為大腦缺氧而造成大規(guī)模故障。有了副本就有了一致性問題,有注冊中心通過Pull的方式更新信息,存在數(shù)據(jù)一致性的實效性。實效性處理的比較好的是有Push(通知)機制的組件,能夠在較快的時間感知服務的變化。
許多組件可以充當服務注冊中心,只要它有分布式存儲數(shù)據(jù)的能力和數(shù)據(jù)一致性的能力。比如Eureka、Nacos、Zookeeper、Consul、Redis,甚至數(shù)據(jù)庫,都能勝任這個角色。
6. 限流
web開發(fā)中,tomcat默認是200個線程池,當更多的請求到來,沒有新的線程能夠去處理這個請求,那這個請求將會一直等待在瀏覽器方。表現(xiàn)的形式是,瀏覽器一直在轉圈(還沒超過acceptCount),即使你請求的是一個簡單的Hello world。
我們可以把這個過程,也看作是限流。它在本質(zhì)上,是設置一個資源數(shù)量上限,超出這個上限的請求,將被緩沖,或者直接失敗。
對于高并發(fā)場景下的限流來說,它有特殊的含義:它主要是用來保護底層資源的。如果你想要調(diào)用某些服務,你需要首先獲取調(diào)用它的許可。限流一般由服務提供方來提供,對調(diào)用方能夠做事的能力進行限制。
比如,某個服務為A、B、C都提供了服務,但根據(jù)提前申請的流量預估,限制A服務的請求為1000/秒、B服務2000/秒,C服務1w/秒。在同一時刻,某些客戶端可能會出現(xiàn)被拒絕的請求,而某些客戶端能夠正常運行,限流被看作是服務端的自我保護能力。
常見的限流算法有:計數(shù)器、漏桶、令牌桶等。但計數(shù)器算法無法實現(xiàn)平滑的限流,在實際應用中使用較少。
7. 熔斷
自從施耐德發(fā)明了斷路器,這個熔斷的概念席卷了全球。從A股熔斷,到服務熔斷,大有異曲同工之妙。
熔斷的意思是:當電路閉合時,電流可以通過,當斷路器打開時,電流停止。
通常情況下,用戶的一個請求,需要后端多個服務配合才能完成工作。后端的這些服務,并不是每一個都是必須的,如果因為其中的某個服務有問題,就把用戶的整個請求給拒絕掉,那是非常不合理的。
熔斷期望某些服務,在發(fā)生問題時,返回一些默認值。整個請求依然可以正常進行下去。
比如風控。如果在某個時間風控服務不可用了,用戶其實是應該能夠正常交易的。這時候我們應該默認風控是通過的,然后把這些異常交易倒到另外一個地方,在風控恢復后再盡快趕在發(fā)貨的之前處理。
從上面的描述可以看出,有的服務,熔斷后簡單的返回些默認數(shù)據(jù)就行,比如推薦服務;但有的服務就需要有對應的異常流程支持,算是一個if else;更要命的是,有些業(yè)務不支持熔斷,那就只能Fail Fast。
一股腦的處理是沒有思考的技術手段,不是我們所推薦的。
Hystrix、resilience4j、Sentinel等組件,是Java系廣泛使用的工具。通過SpringBoot的集成,這些框架一般用起來都比較方便,可以達到配置化編程。
8. 降級
降級是一個比較模糊的說法。限流、熔斷,在一定程度上,也可以看作是降級的一種。但通常所說的降級,切入的層次更加高級一些。
降級一般考慮的是分布式系統(tǒng)的整體性,從源頭上切斷流量的來源。比如在雙11的時候,為了保證交易系統(tǒng),將會暫停一些不重要的服務,以免產(chǎn)生資源爭占。服務降級有人工參與,人為使得某些服務不可用,多屬于一種業(yè)務降級方式。
在什么地方最適合做降級呢?就是入口。比如Nginx,比如DNS等。
在某些互聯(lián)網(wǎng)應用中,會存在MVP(Minimum Viable Product)這個概念,意為最小化可行產(chǎn)品,它的SLA要求非常高。圍繞著最小可行性產(chǎn)品,會有一系列的服務拆分操作,當然某些情況甚至需要重寫。
比如,一個電商系統(tǒng),在極端情況下,只需要把商品顯示出來,把商品賣出去就行。其他一些支撐性的系統(tǒng),比如評論、推薦等,都可以臨時關掉。在物理部署和調(diào)用關系上,就要考慮這些情況。
9. 預熱
請看下面一種情況。
一個高并發(fā)環(huán)境下的DB,進程死亡后進行重啟。由于業(yè)務處在高峰期間,上游的負載均衡策略發(fā)生了重分配。剛剛啟動的DB瞬間接受了1/3的流量,然后load瘋狂飆升,直至再無響應。
原因就是:新啟動的DB,各種Cache并沒有準備完畢,系統(tǒng)狀態(tài)與正常運行時截然不同??赡芷匠?/10的量,就能夠把它帶入死亡。
同理,一個剛剛啟動的JVM進程,由于字節(jié)碼并未被JIT編譯器優(yōu)化,在剛啟動的時候,所有接口的響應時間都比較慢。如果調(diào)用它的負載均衡組件,并沒有考慮這種剛啟動的情況,1/n的流量被正常路由到這個節(jié)點,就很容易出現(xiàn)問題。
所以,我們希望負載均衡組件,能夠依據(jù)JVM進程的啟動時間,動態(tài)的慢慢加量,進行服務預熱,直到達到正常流量水平。
10. 背壓
考慮一下下面兩種場景:
- 沒有限流。請求量過高,有多少收多少,極容易造成后端服務崩潰或者內(nèi)存溢出
- 傳統(tǒng)限流。你強行規(guī)定了某個接口最大的承受能力,超出了直接拒絕,但此時后端服務是有能力處理這些請求的
如何動態(tài)的修改限流的值?這就需要一套機制。調(diào)用方需要知道被調(diào)用方的處理能力,也就是被調(diào)用方需要擁有反饋的能力。背壓,英文Back Pressure,其實是一種智能化的限流,指的是一種策略。
背壓思想,被請求方不會直接將請求端的流量直接丟掉,而是不斷的反饋自己的處理能力。請求端根據(jù)這些反饋,實時的調(diào)整自己的發(fā)送頻率。比較典型的場景,就是TCP/IP中使用滑動窗口來進行流量控制。
反應式編程(Reactive)是觀察者模式的集大成者。它們大多使用事件驅(qū)動,多是非阻塞的彈性應用,基于數(shù)據(jù)流進行彈性傳遞。在這種場景下,背壓實現(xiàn)就簡單的多。
背壓,讓系統(tǒng)更穩(wěn)定,利用率也更高,它本身擁有更高的彈性和智能。比如我們常見的HTTP 429狀態(tài)碼頭,表示的意思就是請求過多,讓客戶端緩一緩,不要那么著急,算是一個智能的告知。
11. 隔離
即使在同一個instance中,同類型的資源,有時候也要做到隔離。一個比較淺顯的比喻,就是泰坦尼克號,它有多個船艙。每個船艙都相互隔離,避免單個船艙進水造成整個船沉了。
當然,泰坦尼克號帶著騷氣的jack沉了,那是因為船艙破的太多的緣故。
在有些公司的軟件中,報表查詢服務、定時任務、普通的服務,都放在同一個tomcat中。它們使用同一套數(shù)據(jù)庫連接池,當某些報表接口的請求一上升,其他正常的服務也無法使用。這就是混用資源所造成的后果。
除了遵循CQRS來把服務拆分,一個快速的機制就是把某類服務的使用資源隔離。比如,給報表分配一個單獨的數(shù)據(jù)庫連接池,分配一個單獨的限流器,它將無法影響其他服務。
耦合除了出現(xiàn)在無狀態(tài)服務節(jié)點,同時還會出現(xiàn)在存儲節(jié)點。與其把報表服務的存儲和正常業(yè)務的存儲放在一個數(shù)據(jù)庫,不如把它們拆開,分別提供服務。
一個和尚挑水喝,兩個和尚也可以挑水喝。原因就是他們在兩個廟。
12. 異步
如果你比較過BIO和NIO的區(qū)別,就可以看到,我們的服務其實大部分時間都是在等待返回,CPU根本就沒有跑滿。當然,NIO是底層的機制,避免了線程膨脹和頻繁的上下文切換。
服務的異步化和NIO有點類似,采用之后可以避免無謂的等待。尤其是當調(diào)用路徑冗長的時候,異步不會阻塞,響應也會變的迅速。
單機時候,我們會采用NIO;而在分布式環(huán)境中,我們會采用MQ。雖然它們是不同的技術,但道理都是相通的。
異步通常涉及到編程模型的改變。同步方式,請求會一直阻塞,直到有成功,或者失敗結果的返回。雖然它的編程模型簡單,但應對突發(fā)的、時間段傾斜的流量,問題就特別大,請求很容易失敗。異步操作可以平滑的橫向擴容,也可以把瞬時壓力時間上后移。同步請求,就像拳頭打在鋼板上;異步請求,就像拳頭打在海綿上。你可以想象一下這個過程,后者肯定是富有彈性,體驗更加友好。
13. 緩存
緩存可能是軟件中使用最多的優(yōu)化技術了。比如,在最核心的CPU里,就存在著多級緩存;為了消除內(nèi)存和存儲之間的差異,各種類似Redis的緩存框架更是層出不窮。
緩存的優(yōu)化效果是非常好的,可以讓原本載入非常緩慢的頁面,瞬間秒開;也能讓本是壓力山大的數(shù)據(jù)庫,瞬間清閑下來。
緩存,本質(zhì)上是為了協(xié)調(diào)兩個速度差異非常大的組件,通過加入一個中間層,將常用的數(shù)據(jù)存放在相對高速的設備中。
在應用開發(fā)中,緩存分為本地緩存和分布式緩存。
那什么叫分布式緩存呢?它其實是一種集中管理的思想。如果我們的服務有多個節(jié)點,堆內(nèi)緩存在每個節(jié)點上都會有一份;而分布式緩存,所有的節(jié)點,共用一份緩存,既節(jié)約了空間,又減少了管理成本。
在分布式緩存領域,使用最多的就是Redis。Redis支持非常豐富的數(shù)據(jù)類型,包括字符串(string)、列表(list)、集合(set)、有序集合(zset)、哈希表(hash)等常用的數(shù)據(jù)結構。當然,它也支持一些其他的比如位圖(bitmap)一類的數(shù)據(jù)結構。
所以加下來的問題一定集中在緩存穿透、擊穿和雪崩,以及一致性上,這個我就不多聊了。
14. Plan-B
一個成熟的系統(tǒng)都有B方案,除了異地多活和容災等處置方案,Plan-B還以為著我們要為正常的服務提供異常的通道。
比如,專門運行一個最小可行性系統(tǒng),運行公司的核心業(yè)務。在大面積故障的時候,將請求全面切換到這個最小系統(tǒng)上。
Plan-B通常都是全局性的,它保證了公司最基本的服務能力,我們期望它永遠用不上。
15. 監(jiān)控報警
問題之所以成為問題,是因為它留下了證據(jù)。沒有證據(jù)的問題,你雖然看到了影響結果,但是你無法找到元兇。
而且問題通常都具有人性化,當它發(fā)現(xiàn)無法發(fā)現(xiàn)它的時候,它總會再次出現(xiàn)。就如同罪犯發(fā)現(xiàn)了漏洞,還會再次嘗試利用它。
所以,要想處理線上問題,你需要留下問題發(fā)生的證據(jù),這是重中之重。如果沒有這些東西,你的公司,絕對會陷入無盡的扯皮之中。
日志是最常見的做法。通過在程序邏輯中進行打點,配合Logback等日志框架,可以快速定位到發(fā)生問題的代碼行。我們需要看一下bug的詳細發(fā)生過程,對可能發(fā)生問題的邏輯進行詳細的日志記錄,進行更加細致的日志輸出,在發(fā)生問題的時候,就可以切換到debug進行調(diào)試。
如果是大范圍的bug,那么強烈建議直接在線上進行調(diào)試。不太推薦使用Arthas等工具動態(tài)的修改字節(jié)碼進行測試,當然也不推薦IDEA的遠程調(diào)試。相反,推薦使用類似金絲雀發(fā)布的方式,導出非常小的一部分流量,構造一個新的版本進行測試。如果你沒有金絲雀發(fā)布平臺,類似Nginx的負載均衡工具也可以通過權重做到類似的事情。
日志系統(tǒng)與監(jiān)控系統(tǒng),對硬件的需求是比較大的,尤其是你的請求體和返回體比較大的情況下,對存儲和計算資源的額要求更是高。它的硬件成本,在整個基礎設施中,占比也是比較高的。但這些證據(jù)信息,對分析問題來說,是非常有必要的。所以即使比較貴,很多公司依然會有很大的投入在這上面,包括硬件投入和人力投入。
MTTD和MTTR是兩個非常重要的指標,我們一定要加大關注。
16. 結尾
我看了一下表,這家伙很能說,預定的時間很快用完了。我揮揮手打?。骸蹦氵€會哪些東西?簡單的說一下吧!“
”也不是很多。像怎么構建一個DevOps團隊支撐我們開發(fā)、測試、線上環(huán)境,如何進行更深入的性能優(yōu)化,如何進行實際的故障排查。以及一些細節(jié)問題,比如怎么優(yōu)化操作系統(tǒng),網(wǎng)絡編程和多線程,我這些還都沒有聊?!?/p>
我說,”夠了,你已經(jīng)非常優(yōu)秀了“。
”你把自己叫作畢業(yè)生,已經(jīng)碾壓絕大多數(shù)人了。你到底是哪里的畢業(yè)生啊!“
”我是B站的,昨天剛畢業(yè)~“,他靦腆的笑了。
我盯著他的眼睛,也笑了??菽痉甏邯q再發(fā),人可兩度再少年!妙啊。