打好這些計(jì)算機(jī)基礎(chǔ)體系,大廠Offer任你挑
前言
金九銀十,又是一年校招季。
經(jīng)歷過,才深知不易。最近,和作為校招面試官的同事聊了聊,問他們是如何去考察一個(gè)學(xué)生的,我簡(jiǎn)單歸為以下幾點(diǎn):
聰明、反應(yīng)快,這點(diǎn)自不必說,聰明意味著學(xué)習(xí)能力、適應(yīng)力強(qiáng),能夠快速勝任工作。
算法不錯(cuò),代碼基本功好,這點(diǎn)其實(shí)考察的是算法能力和代碼是否寫得優(yōu)雅。
基礎(chǔ)過硬,技術(shù)崗面試最核心的還是考察「技術(shù)儲(chǔ)備」,包括了語(yǔ)言基本功,操作系統(tǒng)、網(wǎng)絡(luò)、體系結(jié)構(gòu)、系統(tǒng)設(shè)計(jì)。
語(yǔ)言組織和表達(dá)能力,這點(diǎn)很重要,很多同學(xué)懂得某個(gè)知識(shí)點(diǎn),卻很難用簡(jiǎn)潔準(zhǔn)確的語(yǔ)言表述出來。
想必有很多同學(xué)在刷題、刷面經(jīng),不過我想說“面經(jīng)雖好,不要貪杯哦~”,面經(jīng)可以刷,看看面試官都是怎么提問的,但不要寄希望于原題。
因?yàn)槊嬖囘^程中的問題往往是一環(huán)扣一環(huán)的,這意味著你需要有足夠的技術(shù)深度,將知識(shí)由點(diǎn)連接成面,而不是停留在相互孤立的知識(shí)點(diǎn)上。
所以還是建議系統(tǒng)性的看書,如果覺得時(shí)間不夠,可以關(guān)注書里的重點(diǎn)章節(jié)。
那么回到技術(shù)面試上,除了算法和網(wǎng)絡(luò)、操作系統(tǒng)這種基礎(chǔ)之外,還有一類系統(tǒng)設(shè)計(jì)和優(yōu)化的問題。這類問題需要你有一個(gè)全局的技術(shù)視野,以及熟悉一些常用的系統(tǒng)優(yōu)化方法論,也就是工程上的一些 Best Practice,而不至于自己臨時(shí)拍腦袋瞎設(shè)計(jì)。
在互聯(lián)網(wǎng)公司,經(jīng)常面臨一個(gè)“三高”問題:
高并發(fā)
高性能
高可用
這篇文章將總結(jié)一下后臺(tái)服務(wù)器開發(fā)中有哪些常用的解決“三高”問題的方法和思想。
希望這些知識(shí),能夠給你一絲啟發(fā)和幫助,助力你收割 各大公司 Offer~
先上本文思維導(dǎo)圖:
如何解決三高
正文
一、緩存
什么是緩存?看看維基百科怎么說:
In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere.
在計(jì)算機(jī)中,緩存是存儲(chǔ)數(shù)據(jù)的硬件或軟件組件,以便可以更快地滿足將來對(duì)該數(shù)據(jù)的請(qǐng)求。存儲(chǔ)在緩存中的數(shù)據(jù)可能是之前計(jì)算結(jié)果,也可能是存儲(chǔ)在其他位置的數(shù)據(jù)副本。
緩存本質(zhì)來說是使用空間換時(shí)間的思想,它在計(jì)算機(jī)世界中無處不在, 比如 CPU 就自帶 L1、L2、L3 Cache,這個(gè)一般應(yīng)用開發(fā)可能關(guān)注較少。但是在一些實(shí)時(shí)系統(tǒng)、大規(guī)模計(jì)算模擬、圖像處理等追求極致性能的領(lǐng)域,就特別注重編寫緩存友好的代碼。
什么是緩存友好?簡(jiǎn)單來說,就是代碼在訪問數(shù)據(jù)的時(shí)候,盡量使用緩存命中率高的方式。這個(gè)后面可以單獨(dú)寫一篇 CPU 緩存系統(tǒng)以及如何編寫緩存友好代碼的文章。
1.1 緩存為什么有效?
緩存之所以能夠大幅提高系統(tǒng)的性能,關(guān)鍵在于數(shù)據(jù)的訪問具有局部性,也就是二八定律:「百分之八十的數(shù)據(jù)訪問是集中在 20% 的數(shù)據(jù)上」。這部分?jǐn)?shù)據(jù)也被叫做熱點(diǎn)數(shù)據(jù)。
緩存一般使用內(nèi)存作為存儲(chǔ),內(nèi)存讀寫速度快于磁盤,但容量有限,十分寶貴,不可能將所有數(shù)據(jù)都緩存起來。
如果應(yīng)用訪問數(shù)據(jù)沒有熱點(diǎn),不遵循二八定律,即大部分?jǐn)?shù)據(jù)訪問并沒有集中在小部分?jǐn)?shù)據(jù)上,那么緩存就沒有意義,因?yàn)榇蟛糠謹(jǐn)?shù)據(jù)還沒有被再次訪問就已經(jīng)被擠出緩存了。每次訪問都會(huì)回源到數(shù)據(jù)庫(kù)查詢,那么反而會(huì)降低數(shù)據(jù)訪問效率。
1.2 緩存分類
1. 本地緩存:
使用進(jìn)程內(nèi)成員變量或者靜態(tài)變量,適合簡(jiǎn)單的場(chǎng)景,不需要考慮緩存一致性、過期時(shí)間、清空策略等問題。
可以直接使用語(yǔ)言標(biāo)準(zhǔn)庫(kù)內(nèi)的容器來做存儲(chǔ)。例如:
本地緩存2. 分布式緩存:
當(dāng)緩存的數(shù)據(jù)量增大以后,單機(jī)不足以承載緩存服務(wù)時(shí),就要考慮對(duì)緩存服務(wù)做水平擴(kuò)展,引入緩存集群。
將數(shù)據(jù)分片后分散存儲(chǔ)在不同機(jī)器中,如何決定每個(gè)數(shù)據(jù)分片存放在哪臺(tái)機(jī)器呢?一般是采用一致性 Hash 算法,它能夠保證在緩存集群動(dòng)態(tài)調(diào)整,不斷增加或者減少機(jī)器后,客戶端訪問時(shí)依然能夠根據(jù) key 訪問到數(shù)據(jù)。
一致性 Hash 算法也是值得用一篇文章來講的,如果暫時(shí)還不懂的話可以去搜一下。
常用的組件有 Memcache、 Redis Cluster 等,第二個(gè)是在高性能內(nèi)存存儲(chǔ) Redis 的基礎(chǔ)上,提供分布式存儲(chǔ)的解決方案。
1.3 緩存使用指南
1. 適合緩存的場(chǎng)景:
讀多寫少:
比如電商里的商品詳情頁(yè)面,訪問頻率很高,但是一般寫入只在店家上架商品和修改信息的時(shí)候發(fā)生。如果把熱點(diǎn)商品的信息緩存起來,這將攔截掉很多對(duì)數(shù)據(jù)庫(kù)的訪問,提高系統(tǒng)整體的吞吐量。
因?yàn)橐话銛?shù)據(jù)庫(kù)的 QPS 由于有「ACID」約束、并且數(shù)據(jù)是持久化在硬盤的,所以比 Redis 這類基于內(nèi)存的 NoSQL 存儲(chǔ)低不少。常常是一個(gè)系統(tǒng)的瓶頸,如果我們把大部分的查詢都在 Redis 緩存中命中了,那么系統(tǒng)整體的 QPS 也就上去了。
計(jì)算耗時(shí)大,且實(shí)時(shí)性不高:
比如王者榮耀里的全區(qū)排行榜,一般一周更新一次,并且計(jì)算的數(shù)據(jù)量也比較大,所以計(jì)算后緩存起來,請(qǐng)求排行榜直接從緩存中取出,就不用實(shí)時(shí)計(jì)算了。
2. 不適合緩存的場(chǎng)景:
寫多讀少,頻繁更新。
對(duì)數(shù)據(jù)一致性要求嚴(yán)格: 因?yàn)榫彺鏁?huì)有更新策略,所以很難做到和數(shù)據(jù)庫(kù)實(shí)時(shí)同步。
數(shù)據(jù)訪問完全隨機(jī): 因?yàn)檫@樣會(huì)導(dǎo)致緩存的命中率極低。
1.4 緩存更新的策略
如何更新緩存其實(shí)已經(jīng)有總結(jié)得非常好的「最佳實(shí)踐」,我們按照套路來,大概率不會(huì)犯錯(cuò)。
主要分為兩類 Cache-Aside 和 Cache-As-SoR。 SoR 即「System Of Record,記錄系統(tǒng)」,表示數(shù)據(jù)源,一般就是指數(shù)據(jù)庫(kù)。
1、Cache-Aside:
Cache-Aside架構(gòu)圖
這應(yīng)該是最容易想到的模式了,獲取數(shù)據(jù)時(shí)先從緩存讀,如果 cache hit 則直接返回,沒命中就從數(shù)據(jù)源獲取,然后更新緩存。
寫數(shù)據(jù)的時(shí)候則先更新數(shù)據(jù)源,然后設(shè)置緩存失效,下一次獲取數(shù)據(jù)的時(shí)候必然 cache miss,然后觸發(fā)回源。
直接看偽代碼:
Cache-Aside 代碼示范
可以看到這種方式對(duì)于緩存的使用者是不透明的,需要使用者手動(dòng)維護(hù)緩存。
2、Cache-As-SoR:
Cache-As-SoR架構(gòu)圖
從字面上來看,就是把 Cache 當(dāng)作 SoR,也就是數(shù)據(jù)源,所以一切讀寫操作都是針對(duì) Cache 的,由 Cache 內(nèi)部自己維護(hù)和數(shù)據(jù)源的一致性。
這樣對(duì)于使用者來說就和直接操作 SoR 沒有區(qū)別了,完全感知不到 Cache 的存在。
CPU 內(nèi)部的 L1、L2、L3 Cache 就是這種方式,作為數(shù)據(jù)的使用方應(yīng)用程序,是完全感知不到在內(nèi)存和我們之間還存在幾層的 Cache,但是我們之前又提到編寫 “緩存友好”的代碼,不是透明的嗎?這是不是沖突呢?
其實(shí)不然,緩存友好是指我們通過學(xué)習(xí)了解緩存內(nèi)部實(shí)現(xiàn)、更新策略之后,通過調(diào)整數(shù)據(jù)訪問順序提高緩存的命中率。
Cache-As-SoR 又分為以下三種方式:
Read Through:這種方式和 Cache-Aside 非常相似,都是在查詢時(shí)發(fā)生 cache miss 去更新緩存,但是區(qū)別在于 Cache-Aside 需要調(diào)用方手動(dòng)更新緩存,而 Cache-As-SoR 則是由緩存內(nèi)部實(shí)現(xiàn)自己負(fù)責(zé),對(duì)應(yīng)用層透明。
Write Through:直寫式,就是在將數(shù)據(jù)寫入緩存的同時(shí),緩存也去更新后面的數(shù)據(jù)源,并且必須等到數(shù)據(jù)源被更新成功后才可返回。這樣保證了緩存和數(shù)據(jù)庫(kù)里的數(shù)據(jù)一致性。
Write Back:回寫式,數(shù)據(jù)寫入緩存即可返回,緩存內(nèi)部會(huì)異步的去更新數(shù)據(jù)源,這樣好處是寫操作特別快,因?yàn)橹恍枰戮彺妗2⑶揖彺鎯?nèi)部可以合并對(duì)相同數(shù)據(jù)項(xiàng)的多次更新,但是帶來的問題就是數(shù)據(jù)不一致,可能發(fā)生寫丟失。
二、預(yù)處理和延后處理
預(yù)先延后,這其實(shí)是一個(gè)事物的兩面,不管是預(yù)先還是延后核心思想都是將本來該在實(shí)時(shí)鏈路上處理的事情剝離,要么提前要么延后處理。降低實(shí)時(shí)鏈路的路徑長(zhǎng)度, 這樣能有效提高系統(tǒng)性能。
2.1 預(yù)處理
舉個(gè)我們團(tuán)隊(duì)實(shí)際中遇到的問題:
前兩個(gè)月支付寶聯(lián)合杭州市政府發(fā)放消費(fèi)劵,但是要求只有杭州市常駐居民才能領(lǐng)取,那么需要在搶卷請(qǐng)求進(jìn)入后臺(tái)的時(shí)候就判斷一下用戶是否是杭州常駐居民。
而判斷用戶是否是常駐居民這個(gè)是另外一個(gè)微服務(wù)接口,如果直接實(shí)時(shí)的去調(diào)用那個(gè)接口,短時(shí)的高并發(fā)很有可能把這個(gè)服務(wù)也拖掛,最終導(dǎo)致整個(gè)系統(tǒng)不可用,并且 RPC 本身也是比較耗時(shí)的,所以就考慮在這里進(jìn)行優(yōu)化。
那么該怎么做呢?很簡(jiǎn)單的一個(gè)思路,提前將杭州所有常駐居民的 user_id 存到緩存中, 比如可以直接存到 Redis。大概就是千萬量級(jí),這樣,當(dāng)請(qǐng)求到來的時(shí)候我們直接通過緩存可以快速判斷是否來自杭州常駐居民。如果不是則直接在這里返回前端。
這里通過預(yù)先處理減少了實(shí)時(shí)鏈路上的 RPC 調(diào)用,既減少了系統(tǒng)的外部依賴,也極大的提高了系統(tǒng)的吞吐量。
預(yù)處理在 CPU 和操作系統(tǒng)中也廣泛使用,比如 CPU 基于歷史訪存信息,將內(nèi)存中的指令和數(shù)據(jù)預(yù)取到 Cache 中,這樣可以大大提高Cache 命中率。 還比如在 Linux 文件系統(tǒng)中,預(yù)讀算法會(huì)預(yù)測(cè)即將訪問的 page,然后批量加載比當(dāng)前讀請(qǐng)求更多的數(shù)據(jù)緩存在 page cache 中,這樣當(dāng)下次讀請(qǐng)求到來時(shí)可以直接從 cache 中返回,大大減少了訪問磁盤的時(shí)間。
2.2 延后處理
還是支付寶,上栗子:
集五?;顒?dòng)
這是支付寶春節(jié)集五?;顒?dòng)開獎(jiǎng)當(dāng)晚,不過,作為非酋的我一般是不屑于參與這種活動(dòng)的。
大家發(fā)現(xiàn)沒有,這類活動(dòng)中獎(jiǎng)獎(jiǎng)金一般會(huì)顯示 「稍后到賬」,為什么呢?那當(dāng)然是到賬這個(gè)操作不簡(jiǎn)單!
到賬即轉(zhuǎn)賬,A 賬戶給 B 賬戶轉(zhuǎn)錢,A 減錢, B 就必須要同時(shí)加上錢,也就是說不能 A 減了錢但 B 沒有加上,這就會(huì)導(dǎo)致資金損失。資金安全是支付業(yè)務(wù)的生命線,這可不行。
這兩個(gè)動(dòng)作必須一起成功或是一起都不成功,不能只成功一半,這是保證數(shù)據(jù)一致性。 保證兩個(gè)操作同時(shí)成功或者失敗就需要用到事務(wù)。
如果去實(shí)時(shí)的做到賬,那么大概率數(shù)據(jù)庫(kù)的 TPS(每秒處理的事務(wù)數(shù)) 會(huì)是瓶頸。通過產(chǎn)品提示,將到賬操作延后處理,解決了數(shù)據(jù)庫(kù) TPS 瓶頸。
延后處理還有一個(gè)非常著名的例子,COW(Copy On Write,寫時(shí)復(fù)制)。 Linux 創(chuàng)建進(jìn)程的系統(tǒng)調(diào)用 fork,fork 產(chǎn)生的子進(jìn)程只會(huì)創(chuàng)建虛擬地址空間,而不會(huì)分配真正的物理內(nèi)存,子進(jìn)程共享父進(jìn)程的物理空間,只有當(dāng)某個(gè)進(jìn)程需要寫入的時(shí)候,才會(huì)真正分配物理頁(yè),拷貝該物理頁(yè),通過 COW 減少了很多不必要的數(shù)據(jù)拷貝。
三、池化
后臺(tái)開發(fā)過程中你一定離不開各種 「池子」: 內(nèi)存池、連接池、線程池、對(duì)象池……
內(nèi)存、連接、線程這些都是資源,創(chuàng)建線程、分配內(nèi)存、數(shù)據(jù)庫(kù)連接這些操作都有一個(gè)特征, 那就是創(chuàng)建和銷毀過程都會(huì)涉及到很多系統(tǒng)調(diào)用或者網(wǎng)絡(luò) IO。 每次都在請(qǐng)求中去申請(qǐng)創(chuàng)建這些資源,就會(huì)增加請(qǐng)求處理耗時(shí),但是如果我們用一個(gè) 容器(池) 把它們保存起來,下次需要的時(shí)候,直接拿出來使用,避免重復(fù)創(chuàng)建和銷毀浪費(fèi)的時(shí)間。
3.1 內(nèi)存池
在 C/C++ 中,經(jīng)常使用 malloc、new 等 API 動(dòng)態(tài)申請(qǐng)內(nèi)存。由于申請(qǐng)的內(nèi)存塊大小不一,如果頻繁的申請(qǐng)、釋放會(huì)導(dǎo)致大量的內(nèi)存碎片,并且這些 API 底層依賴系統(tǒng)調(diào)用,會(huì)有額外的開銷。
內(nèi)存池就是在使用內(nèi)存前,先向系統(tǒng)申請(qǐng)一塊空間留做備用,使用者需要內(nèi)池時(shí)向內(nèi)存池申請(qǐng),用完后還回來。
內(nèi)存池的思想非常簡(jiǎn)單,實(shí)現(xiàn)卻不簡(jiǎn)單,難點(diǎn)在于以下幾點(diǎn):
如何快速分配內(nèi)存
降低內(nèi)存碎片率
維護(hù)內(nèi)存池所需的額外空間盡量少
如果不考慮效率,我們完全可以將內(nèi)存分為不同大小的塊,然后用鏈表連接起來,分配的時(shí)候找到大小最合適的返回,釋放的時(shí)候直接添加進(jìn)鏈表。如:
空閑鏈表
當(dāng)然這只是玩具級(jí)別的實(shí)現(xiàn),業(yè)界有性能非常好的實(shí)現(xiàn)了,我們可以直接拿來學(xué)習(xí)和使用。
比如 Google 的 「tcmalloc」 和 Facebook 的 「jemalloc」。
限于篇幅我們不在這里詳細(xì)講解它們的實(shí)現(xiàn)原理,如果感興趣可以搜來看看,也推薦去看看被譽(yù)為神書的 CSAPP(《深入理解計(jì)算機(jī)系統(tǒng)》)第 10 章,那里也講到了動(dòng)態(tài)內(nèi)存分配算法。
3.2 線程池
線程是干嘛的?線程就是我們程序執(zhí)行的實(shí)體。在服務(wù)器開發(fā)領(lǐng)域,我們經(jīng)常會(huì)為每個(gè)請(qǐng)求分配一個(gè)線程去處理,但是線程的創(chuàng)建銷毀、調(diào)度都會(huì)帶來額外的開銷,線程太多也會(huì)導(dǎo)致系統(tǒng)整體性能下降。在這種場(chǎng)景下,我們通常會(huì)提前創(chuàng)建若干個(gè)線程,通過線程池來進(jìn)行管理。當(dāng)請(qǐng)求到來時(shí),只需從線程池選一個(gè)線程去執(zhí)行處理任務(wù)即可。
線程池常常和隊(duì)列一起使用來實(shí)現(xiàn)任務(wù)調(diào)度,主線程收到請(qǐng)求后將創(chuàng)建對(duì)應(yīng)的任務(wù),然后放到隊(duì)列里,線程池中的工作線程等待隊(duì)列里的任務(wù)。
線程池實(shí)現(xiàn)上一般有四個(gè)核心組成部分:
管理器(Manager): 用于創(chuàng)建并管理線程池。
工作線程(Worker): 執(zhí)行任務(wù)的線程。
任務(wù)接口(Task): 每個(gè)具體的任務(wù)必須實(shí)現(xiàn)任務(wù)接口,工作線程將調(diào)用該接口來完成具體的任務(wù)。
任務(wù)隊(duì)列(TaskQueue): 存放還未執(zhí)行的任務(wù)。
線程池模型
線程池在 C、C++ 中沒有具體的實(shí)現(xiàn),需要應(yīng)用開發(fā)者手動(dòng)實(shí)現(xiàn)上訴幾個(gè)部分。
在 Java 中 「ThreadPoolExecutor」 類就是線程池的實(shí)現(xiàn)。后續(xù)我也會(huì)寫文章分析 C++ 如何寫一個(gè)簡(jiǎn)單的線程池以及 Java 中線程池是如何實(shí)現(xiàn)的。
3.3 連接池
顧名思義,連接池是創(chuàng)建和管理連接的。
大家最熟悉的莫過于數(shù)據(jù)庫(kù)連接池,這里我們簡(jiǎn)單分析下如果不用數(shù)據(jù)庫(kù)連接池,一次 SQL 查詢請(qǐng)求會(huì)經(jīng)過哪些步驟:
和 MySQL server 建立 TCP 連接:
三次握手
MySQL 權(quán)限認(rèn)證:
Server 向 Client 發(fā)送 密鑰
Client 使用密鑰加密用戶名、密碼等信息,將加密后的報(bào)文發(fā)送給 Server
Server 根據(jù) Client 請(qǐng)求包,驗(yàn)證是否是合法用戶,然后給 Client 發(fā)送認(rèn)證結(jié)果
Client 發(fā)送 SQL 語(yǔ)句
Server 返回語(yǔ)句執(zhí)行結(jié)果
MySQL 關(guān)閉
TCP 連接斷開
四次揮手
可以看出不使用連接池的話,為了執(zhí)行一條 SQL,會(huì)花很多時(shí)間在安全認(rèn)證、網(wǎng)絡(luò)IO上。
如果使用連接池,執(zhí)行一條 SQL 就省去了建立連接和斷開連接所需的額外開銷。
還能想起哪里用到了連接池的思想嗎?我認(rèn)為 HTTP 長(zhǎng)鏈接也算一個(gè)變相的鏈接池,雖然它本質(zhì)上只有一個(gè)連接,但是思想?yún)s和連接池不謀而合,都是為了復(fù)用同一個(gè)連接發(fā)送多個(gè) HTTP 請(qǐng)求,避免建立和斷開連接的開銷。
池化實(shí)際上是預(yù)處理和延后處理的一種應(yīng)用場(chǎng)景,通過池子將各類資源的創(chuàng)建提前和銷毀延后。
四、同步變異步
對(duì)于處理耗時(shí)的任務(wù),如果采用同步的方式,那么會(huì)增加任務(wù)耗時(shí),降低系統(tǒng)并發(fā)度。
可以通過將同步任務(wù)變?yōu)楫惒竭M(jìn)行優(yōu)化。
舉個(gè)例子,比如我們?nèi)?KFC 點(diǎn)餐,遇到排隊(duì)的人很多,當(dāng)點(diǎn)完餐后,大多情況下我們會(huì)隔幾分鐘就去問好了沒,反復(fù)去問了好幾次才拿到,在這期間我們也沒法干活了,這時(shí)候我們是這樣的:
同步寫法
這個(gè)就叫同步輪訓(xùn), 這樣效率顯然太低了。
服務(wù)員被問煩了,就在點(diǎn)完餐后給我們一個(gè)號(hào)碼牌,每次準(zhǔn)備好了就會(huì)在服務(wù)臺(tái)叫號(hào),這樣我們就可以在被叫到的時(shí)候再去取餐,中途可以繼續(xù)干自己的事。
這就叫異步,在很多編程語(yǔ)言中有異步編程的庫(kù),比如 C++ std::future、Python asyncio 等,但是異步編程往往需要回調(diào)函數(shù)(Callback function),如果回調(diào)函數(shù)的層級(jí)太深,這就是回調(diào)地獄(Callback hell)?;卣{(diào)地獄如何優(yōu)化又是一個(gè)龐大的話題。。。。
這個(gè)例子相當(dāng)于函數(shù)調(diào)用的異步化,還有的是情況是處理流程異步化,這個(gè)會(huì)在接下來消息隊(duì)列中講到。
五、消息隊(duì)列
消息隊(duì)列示意圖
這是一個(gè)非常簡(jiǎn)化的消息隊(duì)列模型,上游生產(chǎn)者將消息通過隊(duì)列發(fā)送給下游消費(fèi)者。在這之間,消息隊(duì)列可以發(fā)揮很多作用,比如:
5.1 服務(wù)解耦
有些服務(wù)被其它很多服務(wù)依賴,比如一個(gè)論壇網(wǎng)站,當(dāng)用戶成功發(fā)布一條帖子有一系列的流程要做,有積分服務(wù)計(jì)算積分,推送服務(wù)向發(fā)布者的粉絲推送一條消息….. 對(duì)于這類需求,常見的實(shí)現(xiàn)方式是直接調(diào)用:
直接調(diào)用
這樣如果需要新增一個(gè)數(shù)據(jù)分析的服務(wù),那么又得改動(dòng)發(fā)布服務(wù),這違背了依賴倒置原則,即上層服務(wù)不應(yīng)該依賴下層服務(wù),那么怎么辦呢?
發(fā)布訂閱模式
引入消息隊(duì)列作為中間層,當(dāng)帖子發(fā)布完成后,發(fā)送一個(gè)事件到消息隊(duì)列里,而關(guān)心帖子發(fā)布成功這件事的下游服務(wù)就可以訂閱這個(gè)事件,這樣即使后續(xù)繼續(xù)增加新的下游服務(wù),只需要訂閱該事件即可,完全不用改動(dòng)發(fā)布服務(wù),完成系統(tǒng)解耦。
5.2 異步處理
有些業(yè)務(wù)涉及到的處理流程非常多,但是很多步驟并不要求實(shí)時(shí)性。那么我們就可以通過消息隊(duì)列異步處理。比如淘寶下單,一般包括了風(fēng)控、鎖庫(kù)存、生成訂單、短信/郵件通知等步驟。但是核心的就風(fēng)控和鎖庫(kù)存, 只要風(fēng)控和扣減庫(kù)存成功,那么就可以返回結(jié)果通知用戶成功下單了。后續(xù)的生成訂單,短信通知都可以通過消息隊(duì)列發(fā)送給下游服務(wù)異步處理。大大提高了系統(tǒng)響應(yīng)速度。
這就是處理流程異步化。
5.3 流量削峰
一般像秒殺、抽獎(jiǎng)、搶卷這種活動(dòng)都伴隨著短時(shí)間海量的請(qǐng)求, 一般超過后端的處理能力,那么我們就可以在接入層將請(qǐng)求放到消息隊(duì)列里,后端根據(jù)自己的處理能力不斷從隊(duì)列里取出請(qǐng)求進(jìn)行業(yè)務(wù)處理。
就像最近長(zhǎng)江汛期,上游短時(shí)間大量的洪水匯聚直奔下游,但是通過三峽大壩將這些水緩存起來,然后勻速的向下游釋放,起到了很好的削峰作用。
起到了平均流量的作用。
5.4 總結(jié)
消息隊(duì)列的核心思想就是把同步的操作變成異步處理,異步處理會(huì)帶來相應(yīng)的好處,比如:
服務(wù)解耦
提高系統(tǒng)的并發(fā)度,將非核心操作異步處理,不會(huì)阻塞住主流程
但是軟件開發(fā)沒有銀彈,所有的方案選擇都是一種 trade-off。 同樣,異步處理也不全是好處,也會(huì)導(dǎo)致一些問題:
降低了數(shù)據(jù)一致性,從強(qiáng)一致性變?yōu)樽罱K一致性
有消息丟失的風(fēng)險(xiǎn),比如宕機(jī),需要有容災(zāi)機(jī)制
六、批量處理
在涉及到網(wǎng)絡(luò)連接、IO等情況時(shí),將操作批量進(jìn)行處理能夠有效提高系統(tǒng)的傳輸速率和吞吐量。
在前后端通信中,通過合并一些頻繁請(qǐng)求的小資源可以獲得更快的加載速度。
比如我們后臺(tái) RPC 框架,經(jīng)常有更新數(shù)據(jù)的需求,而有的數(shù)據(jù)更新的接口往往只接受一項(xiàng),這個(gè)時(shí)候我們往往會(huì)優(yōu)化下更新接口,
使其能夠接受批量更新的請(qǐng)求,這樣可以將批量的數(shù)據(jù)一次性發(fā)送,大大縮短網(wǎng)絡(luò) RPC 調(diào)用耗時(shí)。
七、數(shù)據(jù)庫(kù)
我們常把后臺(tái)開發(fā)調(diào)侃為「CRUD」,數(shù)據(jù)庫(kù)在整個(gè)應(yīng)用開發(fā)過程中的重要性不言而喻。
而且很多時(shí)候系統(tǒng)的瓶頸也往往處在數(shù)據(jù)庫(kù)這里,慢的原因也有很多,比如可能是沒用索引、沒用對(duì)索引、讀寫鎖沖突等等。
那么如何使用數(shù)據(jù)才能又快又好呢?下面這幾點(diǎn)需要重點(diǎn)關(guān)注:
7.1 索引
索引可能是我們平時(shí)在使用數(shù)據(jù)庫(kù)過程中接觸得最多的優(yōu)化方式。索引好比圖書館里的書籍索引號(hào),想象一下,如果我讓你去一個(gè)沒有書籍索引號(hào)的圖書館找《人生》這本書,你是什么樣的感受?當(dāng)然是懷疑人生,同理,你應(yīng)該可以理解當(dāng)你查詢數(shù)據(jù),卻不用索引的時(shí)候數(shù)據(jù)庫(kù)該有多崩潰了吧。
數(shù)據(jù)庫(kù)表的索引就像圖書館里的書籍索引號(hào)一樣,可以提高我們檢索數(shù)據(jù)的效率。索引能提高查找效率,可是你有沒有想過為什么呢?這是因?yàn)樗饕话愣允且粋€(gè)排序列表,排序意味著可以基于二分思想進(jìn)行查找,將查詢時(shí)間復(fù)雜度做到 O(log(N)),快速的支持等值查詢和范圍查詢。
二叉搜索樹查詢效率無疑是最高的,因?yàn)槠骄鶃碚f每次比較都能縮小一半的搜索范圍,但是一般在數(shù)據(jù)庫(kù)索引的實(shí)現(xiàn)上卻會(huì)選擇 B 樹或 B+ 樹而不用二叉搜索樹,為什么呢?
這就涉及到數(shù)據(jù)庫(kù)的存儲(chǔ)介質(zhì)了,數(shù)據(jù)庫(kù)的數(shù)據(jù)和索引都是存放在磁盤,并且是 InnoDB 引擎是以頁(yè)為基本單位管理磁盤的,一頁(yè)一般為 16 KB。AVL 或紅黑樹搜索效率雖然非常高,但是同樣數(shù)據(jù)項(xiàng),它也會(huì)比 B、B+ 樹更高,高就意味著平均來說會(huì)訪問更多的節(jié)點(diǎn),即磁盤IO次數(shù)!
根據(jù) Google 工程師 Jeff Dean 的統(tǒng)計(jì),訪問內(nèi)存數(shù)據(jù)耗時(shí)大概在 100 ns,訪問磁盤則是 10,000,000 ns。
所以表面上來看我們使用 B、B+ 樹沒有 二叉查找樹效率高,但是實(shí)際上由于 B、B+ 樹降低了樹高,減少了磁盤 IO 次數(shù),反而大大提升了速度。
這也告訴我們,沒有絕對(duì)的快和慢,系統(tǒng)分析要抓主要矛盾,先分析出決定系統(tǒng)瓶頸的到底是什么,然后才是針對(duì)瓶頸的優(yōu)化。
其實(shí)關(guān)于索引想寫的也還有很多,但還是受限于篇幅,以后再單獨(dú)寫。
先把我認(rèn)為索引必知必會(huì)的知識(shí)列出來,大家可以查漏補(bǔ)缺:
主鍵索引和普通索引,以及它們之間的區(qū)別
最左前綴匹配原則
索引下推
覆蓋索引、聯(lián)合索引
7.2 讀寫分離
一般業(yè)務(wù)剛上線的時(shí)候,直接使用單機(jī)數(shù)據(jù)庫(kù)就夠了,但是隨著用戶量上來之后,系統(tǒng)就面臨著大量的寫操作和讀操作,單機(jī)數(shù)據(jù)庫(kù)處理能力有限,容易成為系統(tǒng)瓶頸。
由于存在讀寫鎖沖突,并且很多大型互聯(lián)網(wǎng)業(yè)務(wù)往往讀多寫少,讀操作會(huì)首先成為數(shù)據(jù)庫(kù)瓶頸,我們希望消除讀寫鎖沖突從而提升數(shù)據(jù)庫(kù)整體的讀寫能力。
那么就需要采用讀寫分離的數(shù)據(jù)庫(kù)集群方式,一主多從,主庫(kù)會(huì)同步數(shù)據(jù)到從庫(kù)。寫操作都到主庫(kù),讀操作都去從庫(kù)。
讀寫分離
讀寫分離到之后就避免了讀寫鎖爭(zhēng)用,這里解釋一下,什么叫讀寫鎖爭(zhēng)用:
MySQL 中有兩種鎖:
排它鎖( X 鎖): 事務(wù) T 對(duì)數(shù)據(jù) A 加上 X 鎖時(shí),只允許事務(wù) T 讀取和修改數(shù)據(jù) A。
共享鎖( S 鎖): 事務(wù) T 對(duì)數(shù)據(jù) A 加上 S 鎖時(shí),其他事務(wù)只能再對(duì)數(shù)據(jù) A 加 S 鎖,而不能加 X 鎖,直到 T 釋放 A 上的 S 鎖。
讀寫分離解決問題的同時(shí)也會(huì)帶來新問題,比如主庫(kù)和從庫(kù)數(shù)據(jù)不一致
MySQL 的主從同步依賴于 binlog,binlog(二進(jìn)制日志)是 MySQL Server 層維護(hù)的一種二進(jìn)制日志,是獨(dú)立于具體的存儲(chǔ)引擎。它主要存儲(chǔ)對(duì)數(shù)據(jù)庫(kù)更新(insert、delete、update)的 SQL 語(yǔ)句,由于記錄了完整的 SQL 更新信息,所以 binlog 是可以用來數(shù)據(jù)恢復(fù)和主從同步復(fù)制的。
從庫(kù)從主庫(kù)拉取 binlog 然后依次執(zhí)行其中的 SQL 即可達(dá)到復(fù)制主庫(kù)的目的,由于從庫(kù)拉取 binlog 存在網(wǎng)絡(luò)延遲等,所以主從數(shù)據(jù)存在延遲問題。
那么這里就要看業(yè)務(wù)是否允許短時(shí)間內(nèi)的數(shù)據(jù)不一致,如果不能容忍,那么可以通過如果讀從庫(kù)沒獲取到數(shù)據(jù)就去主庫(kù)讀一次來解決。
7.3 分庫(kù)分表
如果用戶越來越多,寫請(qǐng)求暴漲,對(duì)于上面的單 Master 節(jié)點(diǎn)肯定扛不住,那么該怎么辦呢?多加幾個(gè) Master?不行,這樣會(huì)帶來更多的數(shù)據(jù)不一致的問題,增加系統(tǒng)的復(fù)雜度。那該怎么辦?就只能對(duì)庫(kù)表進(jìn)行拆分了。
常見的拆分類型有垂直拆分和水平拆分。
考慮拼夕夕電商系統(tǒng),一般有 訂單表、用戶表、支付表、商品表、商家表等, 最初這些表都在一個(gè)數(shù)據(jù)庫(kù)里。
后來隨著砍一刀帶來的海量用戶,拼夕夕后臺(tái)扛不住了! 于是緊急從阿貍粑粑那里挖來了幾個(gè) P8、P9 大佬對(duì)系統(tǒng)進(jìn)行重構(gòu)。
P9 大佬第一步先對(duì)數(shù)據(jù)庫(kù)進(jìn)行垂直分庫(kù),
根據(jù)業(yè)務(wù)關(guān)聯(lián)性強(qiáng)弱,將它們分到不同的數(shù)據(jù)庫(kù), 比如訂單庫(kù),商家?guī)?、支付?kù)、用戶庫(kù)。第二步是對(duì)一些大表進(jìn)行垂直分表,將一個(gè)表按照字段分成多表,每個(gè)表存儲(chǔ)其中一部分字段。 比如商品詳情表可能最初包含了幾十個(gè)字段,但是往往最多訪問的是商品名稱、價(jià)格、產(chǎn)地、圖片、介紹等信息,所以我們將不常訪問的字段單獨(dú)拆成一個(gè)表。
由于垂直分庫(kù)已經(jīng)按照業(yè)務(wù)關(guān)聯(lián)切分到了最小粒度,數(shù)據(jù)量任然非常大,P9 大佬開始水平分庫(kù),比如可以把訂單庫(kù)分為訂單1庫(kù)、訂單2庫(kù)、訂單3庫(kù)…… 那么如何決定某個(gè)訂單放在哪個(gè)訂單庫(kù)呢?可以考慮對(duì)主鍵通過哈希算法計(jì)算放在哪個(gè)庫(kù)。
分完庫(kù),單表數(shù)據(jù)量任然很大,查詢起來非常慢,P9 大佬決定按日或者按月將訂單分表,叫做日表、月表。
分庫(kù)分表同時(shí)會(huì)帶來一些問題,比如平時(shí)單庫(kù)單表使用的主鍵自增特性將作廢,因?yàn)槟硞€(gè)分區(qū)庫(kù)表生成的主鍵無法保證全局唯一,這就需要引入全局 UUID 服務(wù)了。
經(jīng)過一番大刀闊斧的重構(gòu),拼夕夕恢復(fù)了往日的活力,大家又可以愉快的在上面互相砍一刀了。
(分庫(kù)分表會(huì)引入很多問題,并沒有一一介紹,這里只是為了講解什么是分庫(kù)分表)
八、具體技法
8.1 零拷貝
高性能的服務(wù)器應(yīng)當(dāng)避免不必要數(shù)據(jù)復(fù)制,特別是在用戶空間和內(nèi)核空間之間的數(shù)據(jù)復(fù)制。 比如 HTTP 靜態(tài)服務(wù)器發(fā)送靜態(tài)文件的時(shí)候,一般我們會(huì)這樣寫:
發(fā)送文件
如果了解 Linux IO 的話就知道這個(gè)過程包含了內(nèi)核空間和用戶空間之間的多次拷貝:
IO示意圖
內(nèi)核空間和用戶空間之間數(shù)據(jù)拷貝需要 CPU 親自完成,但是對(duì)于這類數(shù)據(jù)不需要在用戶空間進(jìn)行處理的程序來說,這樣的兩次拷貝顯然是浪費(fèi)。什么叫 「不需要在用戶空間進(jìn)行處理」?
比如 FTP 或者 HTTP 靜態(tài)服務(wù)器,它們的作用只是將文件從磁盤發(fā)送到網(wǎng)絡(luò),不需要在中途對(duì)數(shù)據(jù)進(jìn)行編解碼之類的計(jì)算操作。
如果能夠直接將數(shù)據(jù)在內(nèi)核緩存之間移動(dòng),那么除了減少拷貝次數(shù)以外,還能避免內(nèi)核態(tài)和用戶態(tài)之間的上下文切換。
而這正是零拷貝(Zero copy)干的事,主要就是利用各種零拷貝技術(shù),減少不必要的數(shù)據(jù)拷貝,將 CPU 從數(shù)據(jù)拷貝這樣簡(jiǎn)單的任務(wù)解脫出來,讓 CPU 專注于別的任務(wù)。
常用的零拷貝技術(shù):
mmap
mmap通過內(nèi)存映射,將文件映射到內(nèi)核緩沖區(qū),同時(shí),用戶空間可以共享內(nèi)核空間的數(shù)據(jù)。這樣,在進(jìn)行網(wǎng)絡(luò)傳輸時(shí),就可以減少內(nèi)核空間到用戶空間的拷貝次數(shù)。
mmap
sendfile
sendfile是 Linux2.1 版本提供的,數(shù)據(jù)不經(jīng)過用戶態(tài),直接從頁(yè)緩存拷貝到 socket 緩存,同時(shí)由于和用戶態(tài)完全無關(guān),就減少了一次上下文切換。
在 Linux 2.4 版本,對(duì) sendfile 進(jìn)行了優(yōu)化,直接通過 DMA 將磁盤文件數(shù)據(jù)讀取到 socket 緩存,真正實(shí)現(xiàn)了 ”0” 拷貝。前面 mmap 和 2.1 版本的 sendfile 實(shí)際上只是消除了用戶空間和內(nèi)核空間之間拷貝,而頁(yè)緩存和 socket 緩存之間的拷貝依然存在。
8.2 無鎖化
在多線程環(huán)境下,為了避免 競(jìng)態(tài)條件(race condition), 我們通常會(huì)采用加鎖來進(jìn)行并發(fā)控制,鎖的代價(jià)也是比較高的,鎖會(huì)導(dǎo)致上線文切換,甚至被掛起直到鎖被釋放。
基于硬件提供的原子操作 CAS(Compare And Swap) 實(shí)現(xiàn)一些高性能無鎖的數(shù)據(jù)結(jié)構(gòu),比如無鎖隊(duì)列,可以在保證并發(fā)安全的情況下,提供更高的性能。
首先需要理解什么是 CAS,CAS 有三個(gè)操作數(shù),內(nèi)存里當(dāng)前值M,預(yù)期值 E,修改的新值 N,CAS 的語(yǔ)義就是:
如果當(dāng)前值等于預(yù)期值,則將內(nèi)存修改為新值,否則不做任何操作。
用 C 語(yǔ)言來表達(dá)就是:
CAS
注意,上面 CAS 函數(shù)實(shí)際上是一條原子指令,那么是如何用的呢?
假設(shè)我需要實(shí)現(xiàn)這樣一個(gè)功能:
對(duì)一個(gè)全局變量 global 在兩個(gè)不同線程分別對(duì)它加 100 次,這里多線程訪問一個(gè)全局變量存在 race condition,所以我們需要采用線程同步操作,下面我分別用鎖和CAS的方法來實(shí)現(xiàn)這個(gè)功能。
CAS和鎖示范
通過使用原子操作大大降低了鎖沖突的可能性,提高了程序的性能。
除了 CAS,還有一些硬件原子指令:
Fetch-And-Add,對(duì)變量原子性 + 1
Test-And-Set,這是各種鎖算法的核心,在 AT&T/GNU 匯編語(yǔ)法下,叫 xchg 指令,我會(huì)單獨(dú)寫一篇如何使用 xchg 實(shí)現(xiàn)各種鎖。
8.3 序列化與反序列化
先看看維基百科怎么定義的序列化:
In computing, serialization (US spelling) or serialisation (UK spelling) is the process of translating a data structure or object state into a format that can be stored (for example, in a file or memory data buffer) or transmitted (for example, across a computer network) and reconstructed later (possibly in a different computer environment). When the resulting series of bits is reread according to the serialization format, it can be used to create a semantically identical clone of the original object. For many complex objects, such as those that make extensive use of references, this process is not straightforward. Serialization of object-oriented objects does not include any of their associated methods with which they were previously linked.
我相信你大概率沒有看完上面的英文描述,其實(shí)我也不愛看英文資料,總覺得很慢,但是計(jì)算機(jī)領(lǐng)域一手的學(xué)習(xí)資料都是美帝那邊的,所以沒辦法,必須逼自己去試著讀一些英文的資料。
實(shí)際上也沒有那么難,熟悉常用的幾百個(gè)專業(yè)名詞,句子都是非常簡(jiǎn)單的一些從句。沒看的話,再倒回去看看?
這里我就不做翻譯了,主要是水平太低,估計(jì)做到「信達(dá)雅」的信都很難。
扯遠(yuǎn)了,還是回到序列化來。
所有的編程一定是圍繞數(shù)據(jù)展開的,而數(shù)據(jù)呈現(xiàn)形式往往是結(jié)構(gòu)化的,比如結(jié)構(gòu)體(Struct)、類(Class)。 但是當(dāng)我們 通過網(wǎng)絡(luò)、磁盤等傳輸、存儲(chǔ)數(shù)據(jù)的時(shí)候卻要求是二進(jìn)制流。 比如 TCP 連接,它提供給上層應(yīng)用的是面向連接的可靠字節(jié)流服務(wù)。那么如何將這些結(jié)構(gòu)體和類轉(zhuǎn)化為可存儲(chǔ)和可傳輸?shù)淖止?jié)流呢?這就是序列化要干的事情,反之,從字節(jié)流如何恢復(fù)為結(jié)構(gòu)化的數(shù)據(jù)就是反序列化。
序列化解決了對(duì)象持久化和跨網(wǎng)絡(luò)數(shù)據(jù)交換的問題。
序列化一般按照序列化后的結(jié)果是否可讀,可分為以下兩類:
文本類型:
如 JSON、XML,這些類型可讀性非常好,是自解釋的。也常常用在前后端數(shù)據(jù)交互上,因?yàn)榻涌谡{(diào)試,可讀性高非常方便。但是缺點(diǎn)就是信息密度低,序列化后占用空間大。
二進(jìn)制類型
如 Protocol Buffer、Thrift等,這些類型采用二進(jìn)制編碼,數(shù)據(jù)組織得更加緊湊,信息密度高,占用空間小,但是帶來的問題就是基本不可讀。
還有 Java 、Go 這類語(yǔ)言內(nèi)置了序列化方式,比如在 Java 里實(shí)現(xiàn)了 Serializable 接口即表示該對(duì)象可序列化。
說到這讓我想起了大一寫的的兩個(gè)程序,一個(gè)是用剛 C 語(yǔ)言寫的公交管理系統(tǒng),當(dāng)時(shí)需要將公交線路、站點(diǎn)信息持久化保存,當(dāng)時(shí)的方案就是每個(gè)公交線路寫在一行,用 "|"分割信息,比如:
5|6:00-22:00|大學(xué)城|南山站|北京站
123|6:30-23:00|南湖大道|茶山劉|世界
第一列就是線路編號(hào)、第二項(xiàng)是發(fā)車時(shí)間、后面就是途徑的站點(diǎn)。是不是非常原始?實(shí)際上這也是一種序列化方式,只是效率很低,也不通用。而且存在一個(gè)問題就是如果信息中包含 “|”怎么辦?當(dāng)然是用轉(zhuǎn)義。
第二個(gè)程序是用 Java 寫的網(wǎng)絡(luò)五子棋,當(dāng)時(shí)需要通過網(wǎng)絡(luò)傳輸表示棋子位置的對(duì)象,查了一圈最后發(fā)現(xiàn)只需要實(shí)現(xiàn) Serializable 接口,自己什么都不用干,就能自己完成對(duì)象的序列化,然后通過網(wǎng)絡(luò)傳輸后反序列化。當(dāng)時(shí)哪懂得這就叫序列化,只覺得牛逼、神奇!
最后完成了一個(gè)可以網(wǎng)絡(luò)五子棋,拉著隔壁室友一起玩。。。真的是成就感滿滿哈哈哈。
說來在編程方面,已經(jīng)很久沒有這樣的成就感了。
總結(jié)
這篇文章主要是粗淺的介紹了一些系統(tǒng)設(shè)計(jì)、系統(tǒng)優(yōu)化的套路和最佳實(shí)踐。
不知道你發(fā)現(xiàn)沒有,從緩存到消息隊(duì)列、CAS……,很多看起來很牛逼的架構(gòu)設(shè)計(jì)其實(shí)都來源于操作系統(tǒng)、體系結(jié)構(gòu)。
所以我非常熱衷學(xué)習(xí)一些底層的基礎(chǔ)知識(shí),這些看似古老的技術(shù)是經(jīng)過時(shí)間洗禮留下來的好東西。現(xiàn)在很多的新技術(shù)、框架看似非常厲害,實(shí)則不少都是新瓶裝舊酒,每幾年又會(huì)被淘汰一批。
絮叨
大家好,我是小林,一個(gè)專為大家圖解的工具人,如果覺得文章對(duì)你有幫助,歡迎分享給你的朋友,我們下次見!
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!