前言
本文版權(quán)屬于?天職信息,你可以用它做任何事情睡蟋,但是轉(zhuǎn)載請(qǐng)保留鏈接踏幻。
http://www.tztech.net
我們構(gòu)建的大多數(shù)系統(tǒng)都不用考慮并發(fā)性能。然而戳杀,系統(tǒng)的“速度”是用戶體驗(yàn)的第一要素该面。所以當(dāng)系統(tǒng)的反應(yīng)速度變慢的時(shí)候,我們不得不面對(duì)一系列的并發(fā)問題豺瘤∵壕耄客戶會(huì)經(jīng)常這么說:
- 這個(gè)網(wǎng)站太慢了!
- 這怎么加載這么長(zhǎng)時(shí)間坐求?
- 我剛提交的數(shù)據(jù)蚕泽,怎么失敗了?
- 我和某人同時(shí)修改某一數(shù)據(jù)桥嗤,怎么提交失敗了须妻?
- 導(dǎo)入的數(shù)據(jù)必須要同步完成。
- 我剛更新的 AccessToken 怎么就過期了(技術(shù)層面)泛领?
這些問題荒吏,歸根到底都(可能)是因?yàn)榫W(wǎng)站的并發(fā)沒有做好導(dǎo)致的低可用性。此時(shí)渊鞋,用戶的流失或客戶的失望將會(huì)是企業(yè)最大的損失绰更。
本文將會(huì)討論如何優(yōu)化網(wǎng)站的性能。
本文中提到的所有方案都是可以通過技術(shù)手段實(shí)現(xiàn)的锡宋,并且實(shí)現(xiàn)速度是較快的儡湾。本人并不認(rèn)為這種“得過且過”的方式是正確的,必要的時(shí)候請(qǐng)考慮V戳P炷啤!重構(gòu)你的系統(tǒng)或提升你的算法和數(shù)據(jù)庫(kù)優(yōu)化技能R凼住3⒇ぁ!
本文提出的解決方案可能會(huì)需要爭(zhēng)取客戶的同意才能實(shí)施
異步操作
用戶的時(shí)間是寶貴的衡奥,公司員工的時(shí)間也是寶貴的爹袁。所以對(duì)于一些費(fèi)時(shí)的操作,我們可以推薦用戶將操作異步化:比如矮固,在導(dǎo)入數(shù)據(jù)時(shí)呢簸,讓用戶先將 excel 文件上傳,然后新建一個(gè)后臺(tái)的任務(wù),在后臺(tái)進(jìn)行輪詢和插入根时。
這樣做不是沒有道理的。在生活中辰晕,我們可以讓同事取一個(gè)快遞蛤迎,取回來了之后給我打電話;點(diǎn)擊“編譯”之后含友,系統(tǒng)會(huì)在完成編譯操作之后自動(dòng)發(fā)送通知替裆;CPU有一個(gè)“中斷”的概念,用于提示外設(shè)發(fā)生了某個(gè)事件窘问;在淘寶上賣完?yáng)|西之后辆童,過兩天會(huì)有快遞給我打來電話,叫你下樓去取快遞——以上所述都是異步操作的例子惠赫。我們應(yīng)該引導(dǎo)用戶把鉴,容忍“異步操作”的存在。
異步操作——實(shí)現(xiàn)方法
異步操作有很多實(shí)現(xiàn)方式儿咱。這里介紹兩種——多線程和消息隊(duì)列庭砍。
首先,你需要找到最費(fèi)時(shí)的那幾行代碼混埠,然后想辦法將其提取為方法怠缸,然后將其滯后執(zhí)行。
多線程
我們可以用多線程來處理進(jìn)程內(nèi)的異步操作钳宪。比如:
場(chǎng)景描述:
我們需要導(dǎo)入某一個(gè) Excel 中的 10000 條數(shù)據(jù)揭北,每次插入數(shù)據(jù)的時(shí)候都要花費(fèi)0.5秒解決方案:
文件上傳后,立即新建一個(gè)線程來導(dǎo)入數(shù)據(jù)吏颖,當(dāng)前線程立即返回搔体,告知用戶“文件提交成功,正在導(dǎo)入數(shù)據(jù)”侦高。
消息隊(duì)列Message Queue, MQ
我們可以使用“發(fā)布/訂閱(pub/sub
)”模式來處理進(jìn)程間的異步操作嫉柴。比如:
場(chǎng)景描述
在用戶發(fā)送一條微博時(shí),需要將這條微博推送到關(guān)注者的時(shí)間線中解決方案
用戶發(fā)送一條微博奉呛,用戶端收到請(qǐng)求计螺,將微博推送到“發(fā)送微博”隊(duì)列(publish)
,負(fù)責(zé)計(jì)算時(shí)間線的應(yīng)用(另一個(gè)線程)會(huì)監(jiān)聽該隊(duì)列瞧壮,并處理時(shí)間線相關(guān)的計(jì)算(subscribe)
登馒。
關(guān)于消息隊(duì)列的應(yīng)用有很多。
- 在青矩項(xiàng)目中咆槽,使用消息隊(duì)列來發(fā)送系統(tǒng)通知和發(fā)送數(shù)據(jù)更新通知
- 在互聯(lián)網(wǎng)公司中陈轿,經(jīng)常使用 Kafka 隊(duì)列處理日志(GB/s 級(jí)別)
優(yōu)秀的消息隊(duì)列有:MSMQ、Kafka和RabbitMQ 睡毒。Redis也支持簡(jiǎn)單的pub/sub應(yīng)用橡伞,值得一試。
????對(duì)于 Windows版本的 Redis缆八,請(qǐng)使用 MSOpenTech/redis潜秋,請(qǐng)不要再使用
rgl/redis
數(shù)據(jù)一致性
在高并發(fā)應(yīng)用中蛔琅,我們需要保證系統(tǒng)數(shù)據(jù)的一致性,包括進(jìn)程內(nèi)數(shù)據(jù)的一致性和進(jìn)程間數(shù)據(jù)的一致性峻呛。
我們需要使用事務(wù)Transaction
來保證數(shù)據(jù)的一致性罗售。
使用雙重檢查鎖(Double Check Lock)
保證進(jìn)程內(nèi)數(shù)據(jù)的一致性
首先,需要摘取出系統(tǒng)的邊界資源钩述,然后在邊界資源更新時(shí)寨躁,對(duì)其應(yīng)用雙重檢查鎖。
場(chǎng)景描述
現(xiàn)在系統(tǒng)中有一個(gè)AccessToken牙勘,該 Token 需要2個(gè)小時(shí)刷新一次职恳。在刷新的過程中,由于并發(fā)問題谜悟,導(dǎo)致該變量中的值瞬間被刷新了兩次话肖,進(jìn)而導(dǎo)致系統(tǒng)中出現(xiàn)異常。
解決方案
在AccessToken更新時(shí)葡幸,為了防止有多個(gè)線程更新該變量最筒,可考慮在更新時(shí)使用“雙重檢查鎖”。
??注意事項(xiàng)
此技巧僅適用于更新操作較少的情況蔚叨。不恰當(dāng)?shù)氖褂每赡軙?huì)導(dǎo)致性能問題床蜘。
使用二階段提交(Two-phase Commit, 2PC)
處理進(jìn)程間數(shù)據(jù)的一致性
二階段提交算法的成立基于以下假設(shè)(來自 WikiPedia:2PC):
該分布式系統(tǒng)中,存在一個(gè)節(jié)點(diǎn)作為協(xié)調(diào)者(Coordinator)蔑水,其他節(jié)點(diǎn)作為參與者(Cohorts)邢锯。且節(jié)點(diǎn)之間可以進(jìn)行網(wǎng)絡(luò)通信。
所有節(jié)點(diǎn)都采用預(yù)寫式日志搀别,且日志被寫入后即被保持在可靠的存儲(chǔ)設(shè)備上丹擎,即使節(jié)點(diǎn)損壞不會(huì)導(dǎo)致日志數(shù)據(jù)的消失。
所有節(jié)點(diǎn)不會(huì)永久性損壞歇父,即使損壞后仍然可以恢復(fù)蒂培。
二階段提交的工作流程是這樣的(來自 WikiPedia:2PC):
第一階段(提交請(qǐng)求階段)
協(xié)調(diào)者節(jié)點(diǎn)向所有參與者節(jié)點(diǎn)詢問是否可以執(zhí)行提交操作,并開始等待各參與者節(jié)點(diǎn)的響應(yīng)榜苫。
參與者節(jié)點(diǎn)執(zhí)行詢問發(fā)起為止的所有事務(wù)操作护戳,并將Undo信息(undo logs)
和Redo信息(redo logs)
寫入日志。
各參與者節(jié)點(diǎn)響應(yīng)協(xié)調(diào)者節(jié)點(diǎn)發(fā)起的詢問垂睬。如果參與者節(jié)點(diǎn)的事務(wù)操作實(shí)際執(zhí)行成功媳荒,則它返回一個(gè)"同意"消息抗悍;如果參與者節(jié)點(diǎn)的事務(wù)操作實(shí)際執(zhí)行失敗,則它返回一個(gè)"中止"消息钳枕。
有時(shí)候缴渊,第一階段也被稱作投票階段,即各參與者投票是否要繼續(xù)接下來的提交操作鱼炒。
第二階段(提交執(zhí)行階段)
成功
當(dāng)協(xié)調(diào)者節(jié)點(diǎn)從所有參與者節(jié)點(diǎn)獲得的相應(yīng)消息都為"同意"時(shí):
- 協(xié)調(diào)者節(jié)點(diǎn)向所有參與者節(jié)點(diǎn)發(fā)出"正式提交"的請(qǐng)求疟暖。
- 參與者節(jié)點(diǎn)正式完成操作,并釋放在整個(gè)事務(wù)期間內(nèi)占用的資源田柔。
- 參與者節(jié)點(diǎn)向協(xié)調(diào)者節(jié)點(diǎn)發(fā)送"完成"消息。
- 協(xié)調(diào)者節(jié)點(diǎn)收到所有參與者節(jié)點(diǎn)反饋的"完成"消息后骨望,完成事務(wù)硬爆。
失敗
如果任一參與者節(jié)點(diǎn)在第一階段返回的響應(yīng)消息為"終止",或者 協(xié)調(diào)者節(jié)點(diǎn)在第一階段的詢問超時(shí)之前無法獲取所有參與者節(jié)點(diǎn)的響應(yīng)消息時(shí):
- 協(xié)調(diào)者節(jié)點(diǎn)向所有參與者節(jié)點(diǎn)發(fā)出"回滾操作"的請(qǐng)求擎鸠。
- 參與者節(jié)點(diǎn)利用之前寫入的Undo信息執(zhí)行回滾缀磕,并釋放在整個(gè)事務(wù)期間內(nèi)占用的資源。
- 參與者節(jié)點(diǎn)向協(xié)調(diào)者節(jié)點(diǎn)發(fā)送"回滾完成"消息劣光。
- 協(xié)調(diào)者節(jié)點(diǎn)收到所有參與者節(jié)點(diǎn)反饋的"回滾完成"消息后袜蚕,取消事務(wù)。
??有時(shí)候绢涡,第二階段也被稱作完成階段牲剃,因?yàn)闊o論結(jié)果怎樣,協(xié)調(diào)者都必須在此階段結(jié)束當(dāng)前事務(wù)雄可。
二階段提交的局限性(來自 WikiPedia:2PC)
- 二階段提交算法的最大缺點(diǎn)就在于 它的執(zhí)行過程中間凿傅,節(jié)點(diǎn)都處于阻塞狀態(tài)。即節(jié)點(diǎn)之間在等待對(duì)方的相應(yīng)消息時(shí)数苫,它將什么也做不了聪舒。特別是,當(dāng)一個(gè)節(jié)點(diǎn)在已經(jīng)占有了某項(xiàng)資源的情況下虐急,為了等待其他節(jié)點(diǎn)的響應(yīng)消息而陷入阻塞狀態(tài)時(shí)箱残,當(dāng)?shù)谌齻€(gè)節(jié)點(diǎn)嘗試訪問該節(jié)點(diǎn)占有的資源時(shí),這個(gè)節(jié)點(diǎn)也將連帶陷入阻塞狀態(tài)止吁。
- 另外被辑,協(xié)調(diào)者節(jié)點(diǎn)指示參與者節(jié)點(diǎn)進(jìn)行提交等操作時(shí),如有參與者節(jié)點(diǎn)出現(xiàn)了崩潰等情況而導(dǎo)致協(xié)調(diào)者始終無法獲取所有參與者的響應(yīng)信息赏殃,這時(shí)協(xié)調(diào)者將只能依賴協(xié)調(diào)者自身的超時(shí)機(jī)制來生效敷待。但往往超時(shí)機(jī)制生效時(shí),協(xié)調(diào)者都會(huì)指示參與者進(jìn)行回滾操作仁热。這樣的策略顯得比較保守榜揖。
除了二階段提交勾哩,我們還可以用其他的提交方法。原理與其類似举哟,但是它們?cè)谒惴ㄉ蠒?huì)或多或少地優(yōu)化性能思劳。
擴(kuò)展閱讀:
關(guān)于分布式事務(wù)、兩階段提交協(xié)議妨猩、三階提交協(xié)議
分布式系統(tǒng)的事務(wù)處理
??以上所述的提交方式在各大數(shù)據(jù)庫(kù)中(應(yīng)該)都有實(shí)現(xiàn)潜叛。然而,在一些復(fù)雜的情況中(銀行轉(zhuǎn)賬壶硅,用戶退款)威兜,我們?nèi)匀恍枰约簩?shí)現(xiàn)事務(wù)功能,并對(duì)該模塊進(jìn)行嚴(yán)格的邏輯測(cè)試和壓力測(cè)試庐椒。
分離你的業(yè)務(wù)
系統(tǒng)中所選用的技術(shù)椊范妫可能會(huì)導(dǎo)致其實(shí)現(xiàn)功能的局限性。比如:如果使用了Linux 约谈,實(shí)現(xiàn) Office 文檔的預(yù)覽會(huì)很困難笔宿,要么格式不對(duì),要么體驗(yàn)不好棱诱;如果使用了 RDBMS泼橘,就不能把所有的數(shù)據(jù)放到 DB 中,否則會(huì)導(dǎo)致 IO 很慢——這些都是技術(shù)選型與限制迈勋。我并不是在說哪個(gè)技術(shù)好哪個(gè)技術(shù)壞炬灭,在公司中,技術(shù)選型一定是基于當(dāng)前人員的配置來的粪躬,并且會(huì)有妥協(xié)担败。
隨著系統(tǒng)功能的完善,上述的“局限性”終歸需要解決镰官。我建議提前,與其將就使用當(dāng)前的技術(shù)棧,用不好的實(shí)踐實(shí)現(xiàn)這些功能泳唠,還不如在其他技術(shù)棧上使用好的實(shí)踐實(shí)現(xiàn)這些功能狈网。
使用 緩存思想 提升系統(tǒng) IO 的吞吐量 (MongoDB 為例)
與 RDBMS 相比,MongoDB IO 較快笨腥。其根本原因是拓哺,MongoDB 使用的存儲(chǔ)引擎 會(huì)在存儲(chǔ)數(shù)據(jù)時(shí)將數(shù)據(jù)存至內(nèi)存中,每 60S 或存儲(chǔ)到達(dá)2GB的時(shí)候會(huì)自動(dòng)同步至硬盤中脖母。
仿照 CPU 的架構(gòu)士鸥,我們可以做一個(gè)類似緩存的東西(L1,L2谆级,L3 Cache)來緩存新添加的數(shù)據(jù)烤礁,或者仿照 CLR 的GC算法讼积,將“熱數(shù)據(jù)”的一部分放到MongoDB的緩存中。緩存有一個(gè)“命中率”的概念脚仔。當(dāng)命中率達(dá)到80%左右的時(shí)候勤众,那么這個(gè)緩存的設(shè)計(jì)就是正確的。關(guān)于緩存命中率鲤脏,這里有一篇文章
要使用緩存们颜,我們可以這樣做
場(chǎng)景描述
現(xiàn)在需要導(dǎo)入10000條招聘信息,一定要實(shí)現(xiàn)瞬間導(dǎo)入猎醇。解決方案
用戶上傳文件 > ** 解析文件中的信息 ** > ** 放到MongoDB中** >** 后臺(tái)開線程** 窥突,將數(shù)據(jù)同步到RDBMS中(可以使用隊(duì)列來增加系統(tǒng)的容錯(cuò)性)。
如果系統(tǒng)中的一些業(yè)務(wù)可以形成閉環(huán)硫嘶,并且IO要求較高(文件波岛、附件管理相關(guān)),和其他業(yè)務(wù)關(guān)聯(lián)不大音半,可以直接將此模塊獨(dú)立出來,使用 MongoDB來提升系統(tǒng)性能贡蓖。
??在C#中曹鸠,你可以使用LINQ對(duì)MongoDB進(jìn)行查詢;你可以使用內(nèi)建的 Filter 和Update來進(jìn)行某一字段的 Update斥铺,當(dāng)然彻桃,你也可以整個(gè)對(duì)象全部更新——但筆者認(rèn)為這是不推薦的。因?yàn)?MongoDB C# 驅(qū)動(dòng)給出的 Update 語(yǔ)句已經(jīng)是強(qiáng)類型的了晾蜘,整個(gè)對(duì)象更新會(huì)讓代碼的可讀性降低邻眷。在OO設(shè)計(jì)中,每一個(gè)需要存儲(chǔ)持久化數(shù)據(jù)的方法不應(yīng)該需要保存整個(gè)實(shí)體類的——這樣做有些麻煩剔交, 也不需要這樣做肆饶。
??在Java中,同樣有優(yōu)秀的MongoDB框架岖常。雖然 Java 的驅(qū)動(dòng)本身不帶 POJO Mapper驯镊,你可以用這個(gè):Spring Data MongoDB
多說一句:筆者掃了一眼這個(gè)框架,好像還比較符合Java的設(shè)計(jì)哲學(xué)——面向?qū)ο蠼甙埃⑶以撻_源框架的GitHub上最近也有代碼正在提交板惑。值得一試
此外,還可以使用Redis來實(shí)現(xiàn)緩存機(jī)制偎快。方法類似冯乘,在此不過多贅述。
使用HTTP負(fù)載均衡(Nginx 為例)
CDN和流量分發(fā)是構(gòu)建大型網(wǎng)站應(yīng)用的常見的技術(shù)晒夹。我們可以使用Nginx來進(jìn)行流量分發(fā)(C# 和 Java)裆馒。如果是Node.js姊氓,我們可以使用PM2的機(jī)架(Cluster)模式來做負(fù)載均衡。
你可以像以下這樣設(shè)置Nginx的負(fù)載均衡:
http {
upstream myapp1 {
server srv1.example.com;
server srv2.example.com;
server srv3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://myapp1;
}
}
}
被負(fù)載均衡的應(yīng)用應(yīng)該符合 12 Factor Apps規(guī)范领追。該規(guī)范旨在:
如今他膳,軟件通常會(huì)作為一種服務(wù)來交付,它們被稱為網(wǎng)絡(luò)應(yīng)用程序绒窑,或軟件即服務(wù)(SaaS)棕孙。12-Factor 為構(gòu)建如下的 SaaS 應(yīng)用提供了方法論:
- 使用標(biāo)準(zhǔn)化流程自動(dòng)配置,從而使新的開發(fā)者花費(fèi)最少的學(xué)習(xí)成本加入這個(gè)項(xiàng)目些膨。
- 和操作系統(tǒng)之間盡可能的劃清界限蟀俊,在各個(gè)系統(tǒng)中提供最大的可移植性。
- 適合部署在現(xiàn)代的云計(jì)算平臺(tái)订雾,從而在服務(wù)器和系統(tǒng)管理方面節(jié)省資源肢预。
- 將開發(fā)環(huán)境和生產(chǎn)環(huán)境的差異降至最低,并使用持續(xù)交付實(shí)施敏捷開發(fā)洼哎。
- 可以在工具烫映、架構(gòu)和開發(fā)流程不發(fā)生明顯變化的前提下實(shí)現(xiàn)擴(kuò)展。
其中噩峦,進(jìn)程一節(jié)提到锭沟,
12-Factor 應(yīng)用的進(jìn)程必須無狀態(tài)且 無共享 。 任何需要持久化的數(shù)據(jù)都要存儲(chǔ)在 后端服務(wù) 內(nèi)识补,比如數(shù)據(jù)庫(kù)族淮。
根據(jù)該節(jié)的描述,如果我們使用了Session凭涂,需要實(shí)現(xiàn)服務(wù)間共享Session祝辣,才能對(duì)應(yīng)用進(jìn)行負(fù)載均衡的操作。
降低方法和程序的副作用(side effects)也是一種分離的方式切油。如果方法和程序的副作用過多蝙斜,那么說明此方法耦合度過緊(tough coupled),這時(shí)候就需要考慮該設(shè)計(jì)是否合理了澎胡。
結(jié)語(yǔ)
以上乍炉,所有知識(shí)都是我腦袋里能抽出來的知識(shí)了。有些討論可能會(huì)有謬誤滤馍。如果有岛琼,請(qǐng)指出,感激不盡巢株!
以上內(nèi)容每個(gè)分類都可以獨(dú)立出一個(gè)專題來講槐瑞,這里只是泛泛地談這些內(nèi)容。希望能幫助到你
?天職信息
[EOF]