不知不覺,我們已經(jīng)講到第五篇了,不知道聽到這里,你對(duì)于秒殺系統(tǒng)的構(gòu)建有沒有形成一些框架性的認(rèn)識(shí),這里我再帶你簡(jiǎn)單回憶下前面的主線。
前面的四篇文章里,我介紹的內(nèi)容多少都和優(yōu)化有關(guān):第一篇介紹了一些指導(dǎo)原則;第二篇和第三篇從動(dòng)靜分離和熱點(diǎn)數(shù)據(jù)兩個(gè)維度,介紹了如何有針對(duì)性地對(duì)數(shù)據(jù)進(jìn)行區(qū)分和優(yōu)化處理;第四篇介紹了在保證實(shí)現(xiàn)基本業(yè)務(wù)功能的前提下,盡量減少和過濾一些無效請(qǐng)求的思路。
這幾篇文章既是在講根據(jù)指導(dǎo)原則實(shí)現(xiàn)的具體案例,也是在講如何實(shí)現(xiàn)能夠讓整個(gè)系統(tǒng)更“快”。我想說的是,優(yōu)化本身有很多手段,也是一個(gè)復(fù)雜的系統(tǒng)工程。今天,我就來結(jié)合秒殺這一場(chǎng)景,重點(diǎn)給你介紹下服務(wù)端的一些優(yōu)化技巧。
影響性能的因素
你想要提升性能,首先肯定要知道哪些因素對(duì)于系統(tǒng)性能的影響最大,然后再針對(duì)這些具體的因素想辦法做優(yōu)化,是不是這個(gè)邏輯?
那么,哪些因素對(duì)性能有影響呢?在回答這個(gè)問題之前,我們先定義一下“性能”,服務(wù)設(shè)備不同對(duì)性能的定義也是不一樣的,例如 CPU 主要看主頻、磁盤主要看 IOPS(Input/Output Operations Per Second,即每秒進(jìn)行讀寫操作的次數(shù))。
而今天我們討論的主要是系統(tǒng)服務(wù)端性能,一般用 QPS(Query Per Second,每秒請(qǐng)求數(shù))來衡量,還有一個(gè)影響和 QPS 也息息相關(guān),那就是響應(yīng)時(shí)間(Response Time,RT),它可以理解為服務(wù)器處理響應(yīng)的耗時(shí)。
正常情況下響應(yīng)時(shí)間(RT)越短,一秒鐘處理的請(qǐng)求數(shù)(QPS)自然也就會(huì)越多,這在單線程處理的情況下看起來是線性的關(guān)系,即我們只要把每個(gè)請(qǐng)求的響應(yīng)時(shí)間降到最低,那么性能就會(huì)最高。
但是你可能想到響應(yīng)時(shí)間總有一個(gè)極限,不可能無限下降,所以又出現(xiàn)了另外一個(gè)維度,即通過多線程,來處理請(qǐng)求。這樣理論上就變成了“總 QPS =(1000ms / 響應(yīng)時(shí)間)× 線程數(shù)量”,這樣性能就和兩個(gè)因素相關(guān)了,一個(gè)是一次響應(yīng)的服務(wù)端耗時(shí),一個(gè)是處理請(qǐng)求的線程數(shù)。
接下來,我們一起看看這個(gè)兩個(gè)因素到底會(huì)造成什么樣的影響。
首先,我們先來看看響應(yīng)時(shí)間和 QPS 有啥關(guān)系。
對(duì)于大部分的 Web 系統(tǒng)而言,響應(yīng)時(shí)間一般都是由 CPU 執(zhí)行時(shí)間和線程等待時(shí)間(比如 RPC、IO 等待、Sleep、Wait 等)組成,即服務(wù)器在處理一個(gè)請(qǐng)求時(shí),一部分是 CPU 本身在做運(yùn)算,還有一部分是在各種等待。
理解了服務(wù)器處理請(qǐng)求的邏輯,估計(jì)你會(huì)說為什么我們不去減少這種等待時(shí)間。很遺憾,根據(jù)我們實(shí)際的測(cè)試發(fā)現(xiàn),減少線程等待時(shí)間對(duì)提升性能的影響沒有我們想象得那么大,它并不是線性的提升關(guān)系,這點(diǎn)在很多代理服務(wù)器(Proxy)上可以做驗(yàn)證。
如果代理服務(wù)器本身沒有 CPU 消耗,我們?cè)诿看谓o代理服務(wù)器代理的請(qǐng)求加個(gè)延時(shí),即增加響應(yīng)時(shí)間,但是這對(duì)代理服務(wù)器本身的吞吐量并沒有多大的影響,因?yàn)榇矸?wù)器本身的資源并沒有被消耗,可以通過增加代理服務(wù)器的處理線程數(shù),來彌補(bǔ)響應(yīng)時(shí)間對(duì)代理服務(wù)器的 QPS 的影響。
其實(shí),真正對(duì)性能有影響的是 CPU 的執(zhí)行時(shí)間。這也很好理解,因?yàn)?CPU 的執(zhí)行真正消耗了服務(wù)器的資源。經(jīng)過實(shí)際的測(cè)試,如果減少 CPU 一半的執(zhí)行時(shí)間,就可以增加一倍的 QPS。
也就是說,我們應(yīng)該致力于減少 CPU 的執(zhí)行時(shí)間。
其次,我們?cè)賮砜纯淳€程數(shù)對(duì) QPS 的影響。
單看“總 QPS”的計(jì)算公式,你會(huì)覺得線程數(shù)越多 QPS 也就會(huì)越高,但這會(huì)一直正確嗎?顯然不是,線程數(shù)不是越多越好,因?yàn)榫€程本身也消耗資源,也受到其他因素的制約。例如,線程越多系統(tǒng)的線程切換成本就會(huì)越高,而且每個(gè)線程也都會(huì)耗費(fèi)一定內(nèi)存。
那么,設(shè)置什么樣的線程數(shù)最合理呢?其實(shí)很多多線程的場(chǎng)景都有一個(gè)默認(rèn)配置,即“線程數(shù) = 2 * CPU 核數(shù) + 1”。除去這個(gè)配置,還有一個(gè)根據(jù)最佳實(shí)踐得出來的公式:
線程數(shù) = [(線程等待時(shí)間 + 線程 CPU 時(shí)間) / 線程 CPU 時(shí)間] × CPU 數(shù)量
當(dāng)然,最好的辦法是通過性能測(cè)試來發(fā)現(xiàn)最佳的線程數(shù)。
換句話說,要提升性能我們就要減少 CPU 的執(zhí)行時(shí)間,另外就是要設(shè)置一個(gè)合理的并發(fā)線程數(shù),通過這兩方面來顯著提升服務(wù)器的性能。
現(xiàn)在,你知道了如何來快速提升性能,那接下來你估計(jì)會(huì)問,我應(yīng)該怎么發(fā)現(xiàn)系統(tǒng)哪里最消耗 CPU 資源呢?
如何發(fā)現(xiàn)瓶頸
就服務(wù)器而言,會(huì)出現(xiàn)瓶頸的地方有很多,例如 CPU、內(nèi)存、磁盤以及網(wǎng)絡(luò)等都可能會(huì)導(dǎo)致瓶頸。此外,不同的系統(tǒng)對(duì)瓶頸的關(guān)注度也不一樣,例如對(duì)緩存系統(tǒng)而言,制約它的是內(nèi)存,而對(duì)存儲(chǔ)型系統(tǒng)來說 I/O 更容易是瓶頸。
我們定位的場(chǎng)景是秒殺,它的瓶頸更多地發(fā)生在 CPU 上。
那么,如何發(fā)現(xiàn) CPU 的瓶頸呢?其實(shí)有很多 CPU 診斷工具可以發(fā)現(xiàn) CPU 的消耗,最常用的就是 JProfiler 和 Yourkit 這兩個(gè)工具,它們可以列出整個(gè)請(qǐng)求中每個(gè)函數(shù)的 CPU 執(zhí)行時(shí)間,可以發(fā)現(xiàn)哪個(gè)函數(shù)消耗的 CPU 時(shí)間最多,以便你有針對(duì)性地做優(yōu)化。
當(dāng)然還有一些辦法也可以近似地統(tǒng)計(jì) CPU 的耗時(shí),例如通過 jstack 定時(shí)地打印調(diào)用棧,如果某些函數(shù)調(diào)用頻繁或者耗時(shí)較多,那么那些函數(shù)就會(huì)多次出現(xiàn)在系統(tǒng)調(diào)用棧里,這樣相當(dāng)于采樣的方式也能夠發(fā)現(xiàn)耗時(shí)較多的函數(shù)。
雖說秒殺系統(tǒng)的瓶頸大部分在 CPU,但這并不表示其他方面就一定不出現(xiàn)瓶頸。例如,如果海量請(qǐng)求涌過來,你的頁(yè)面又比較大,那么網(wǎng)絡(luò)就有可能出現(xiàn)瓶頸。
怎樣簡(jiǎn)單地判斷 CPU 是不是瓶頸呢?一個(gè)辦法就是看當(dāng) QPS 達(dá)到極限時(shí),你的服務(wù)器的 CPU 使用率是不是超過了 95%,如果沒有超過,那么表示 CPU 還有提升的空間,要么是有鎖限制,要么是有過多的本地 I/O 等待發(fā)生。
現(xiàn)在你知道了優(yōu)化哪些因素,又發(fā)現(xiàn)了瓶頸,那么接下來就要關(guān)注如何優(yōu)化了。
如何優(yōu)化系統(tǒng)
對(duì) Java 系統(tǒng)來說,可以優(yōu)化的地方很多,這里我重點(diǎn)說一下比較有效的幾種手段,供你參考,它們是:減少編碼、減少序列化、Java 極致優(yōu)化、并發(fā)讀優(yōu)化。接下來,我們分別來看一下。
1. 減少編碼
Java 的編碼運(yùn)行比較慢,這是 Java 的一大硬傷。在很多場(chǎng)景下,只要涉及字符串的操作(如輸入輸出操作、I/O 操作)都比較耗 CPU 資源,不管它是磁盤 I/O 還是網(wǎng)絡(luò) I/O,因?yàn)槎夹枰獙⒆址D(zhuǎn)換成字節(jié),而這個(gè)轉(zhuǎn)換必須編碼。
每個(gè)字符的編碼都需要查表,而這種查表的操作非常耗資源,所以減少字符到字節(jié)或者相反的轉(zhuǎn)換、減少字符編碼會(huì)非常有成效。減少編碼就可以大大提升性能。
那么如何才能減少編碼呢?例如,網(wǎng)頁(yè)輸出是可以直接進(jìn)行流輸出的,即用 resp.getOutputStream() 函數(shù)寫數(shù)據(jù),把一些靜態(tài)的數(shù)據(jù)提前轉(zhuǎn)化成字節(jié),等到真正往外寫的時(shí)候再直接用 OutputStream() 函數(shù)寫,就可以減少靜態(tài)數(shù)據(jù)的編碼轉(zhuǎn)換。
我在《深入分析 Java Web 技術(shù)內(nèi)幕》一書中介紹的“Velocity 優(yōu)化實(shí)踐”一章的內(nèi)容,就是基于把靜態(tài)的字符串提前編碼成字節(jié)并緩存,然后直接輸出字節(jié)內(nèi)容到頁(yè)面,從而大大減少編碼的性能消耗的,網(wǎng)頁(yè)輸出的性能比沒有提前進(jìn)行字符到字節(jié)轉(zhuǎn)換時(shí)提升了 30% 左右。
2. 減少序列化
序列化也是 Java 性能的一大天敵,減少 Java 中的序列化操作也能大大提升性能。又因?yàn)樾蛄谢呛途幋a同時(shí)發(fā)生的,所以減少序列化也就減少了編碼。
序列化大部分是在 RPC 中發(fā)生的,因此避免或者減少 RPC 就可以減少序列化,當(dāng)然當(dāng)前的序列化協(xié)議也已經(jīng)做了很多優(yōu)化來提升性能。有一種新的方案,就是可以將多個(gè)關(guān)聯(lián)性比較強(qiáng)的應(yīng)用進(jìn)行“合并部署”,而減少不同應(yīng)用之間的 RPC 也可以減少序列化的消耗。
所謂“合并部署”,就是把兩個(gè)原本在不同機(jī)器上的不同應(yīng)用合并部署到一臺(tái)機(jī)器上,當(dāng)然不僅僅是部署在一臺(tái)機(jī)器上,還要在同一個(gè) Tomcat 容器中,且不能走本機(jī)的 Socket,這樣才能避免序列化的產(chǎn)生。
另外針對(duì)秒殺場(chǎng)景,我們還可以做得更極致一些,接下來我們來看第 3 點(diǎn):Java 極致優(yōu)化。
3. Java 極致優(yōu)化
Java 和通用的 Web 服務(wù)器(如 Nginx 或 Apache 服務(wù)器)相比,在處理大并發(fā)的 HTTP 請(qǐng)求時(shí)要弱一點(diǎn),所以一般我們都會(huì)對(duì)大流量的 Web 系統(tǒng)做靜態(tài)化改造,讓大部分請(qǐng)求和數(shù)據(jù)直接在 Nginx 服務(wù)器或者 Web 代理服務(wù)器(如 Varnish、Squid 等)上直接返回(這樣可以減少數(shù)據(jù)的序列化與反序列化),而 Java 層只需處理少量數(shù)據(jù)的動(dòng)態(tài)請(qǐng)求。針對(duì)這些請(qǐng)求,我們可以使用以下手段進(jìn)行優(yōu)化:
直接使用 Servlet 處理請(qǐng)求。避免使用傳統(tǒng)的 MVC 框架,這樣可以繞過一大堆復(fù)雜且用處不大的處理邏輯,節(jié)省 1ms 時(shí)間(具體取決于你對(duì) MVC 框架的依賴程度)。
直接輸出流數(shù)據(jù)。使用 resp.getOutputStream() 而不是 resp.getWriter() 函數(shù),可以省掉一些不變字符數(shù)據(jù)的編碼,從而提升性能;數(shù)據(jù)輸出時(shí)推薦使用 JSON 而不是模板引擎(一般都是解釋執(zhí)行)來輸出頁(yè)面。
4. 并發(fā)讀優(yōu)化
也許有讀者會(huì)覺得這個(gè)問題很容易解決,無非就是放到 Tair 緩存里面。集中式緩存為了保證命中率一般都會(huì)采用一致性 Hash,所以同一個(gè) key 會(huì)落到同一臺(tái)機(jī)器上。雖然單臺(tái)緩存機(jī)器也能支撐 30w/s 的請(qǐng)求,但還是遠(yuǎn)不足以應(yīng)對(duì)像“大秒”這種級(jí)別的熱點(diǎn)商品。那么,該如何徹底解決單點(diǎn)的瓶頸呢?
答案是采用應(yīng)用層的 LocalCache,即在秒殺系統(tǒng)的單機(jī)上緩存商品相關(guān)的數(shù)據(jù)。
那么,又如何緩存(Cache)數(shù)據(jù)呢?你需要?jiǎng)澐殖蓜?dòng)態(tài)數(shù)據(jù)和靜態(tài)數(shù)據(jù)分別進(jìn)行處理:
像商品中的“標(biāo)題”和“描述”這些本身不變的數(shù)據(jù),會(huì)在秒殺開始之前全量推送到秒殺機(jī)器上,并一直緩存到秒殺結(jié)束;
像庫(kù)存這類動(dòng)態(tài)數(shù)據(jù),會(huì)采用“被動(dòng)失效”的方式緩存一定時(shí)間(一般是數(shù)秒),失效后再去緩存拉取最新的數(shù)據(jù)。
你可能還會(huì)有疑問:像庫(kù)存這種頻繁更新的數(shù)據(jù),一旦數(shù)據(jù)不一致,會(huì)不會(huì)導(dǎo)致超賣?
這就要用到前面介紹的讀數(shù)據(jù)的分層校驗(yàn)原則了,讀的場(chǎng)景可以允許一定的臟數(shù)據(jù),因?yàn)檫@里的誤判只會(huì)導(dǎo)致少量原本無庫(kù)存的下單請(qǐng)求被誤認(rèn)為有庫(kù)存,可以等到真正寫數(shù)據(jù)時(shí)再保證最終的一致性,通過在數(shù)據(jù)的高可用性和一致性之間的平衡,來解決高并發(fā)的數(shù)據(jù)讀取問題。
總結(jié)一下
性能優(yōu)化的過程首先要從發(fā)現(xiàn)短板開始,除了我今天介紹的一些優(yōu)化措施外,你還可以在減少數(shù)據(jù)、數(shù)據(jù)分級(jí)(動(dòng)靜分離),以及減少中間環(huán)節(jié)、增加預(yù)處理等這些環(huán)節(jié)上做優(yōu)化。
首先是“發(fā)現(xiàn)短板”,比如考慮以下因素的一些限制:光速(光速:C = 30 萬千米 / 秒;光纖:V = C/1.5=20 萬千米 / 秒,即數(shù)據(jù)傳輸是有物理距離的限制的)、網(wǎng)速(2017 年 11 月知名測(cè)速網(wǎng)站 Ookla 發(fā)布報(bào)告,全國(guó)平均上網(wǎng)帶寬達(dá)到 61.24 Mbps,千兆帶寬下 10KB 數(shù)據(jù)的極限 QPS 為 1.25 萬 QPS=1000Mbps/8/10KB)、網(wǎng)絡(luò)結(jié)構(gòu)(交換機(jī) / 網(wǎng)卡的限制)、TCP/IP、虛擬機(jī)(內(nèi)存 /CPU/IO 等資源的限制)和應(yīng)用本身的一些瓶頸等。
其次是減少數(shù)據(jù)。事實(shí)上,有兩個(gè)地方特別影響性能,一是服務(wù)端在處理數(shù)據(jù)時(shí)不可避免地存在字符到字節(jié)的相互轉(zhuǎn)化,二是 HTTP 請(qǐng)求時(shí)要做 Gzip 壓縮,還有網(wǎng)絡(luò)傳輸?shù)暮臅r(shí),這些都和數(shù)據(jù)大小密切相關(guān)。
再次,就是數(shù)據(jù)分級(jí),也就是要保證首屏為先、重要信息為先,次要信息則異步加載,以這種方式提升用戶獲取數(shù)據(jù)的體驗(yàn)。
最后就是要減少中間環(huán)節(jié),減少字符到字節(jié)的轉(zhuǎn)換,增加預(yù)處理(提前做字符到字節(jié)的轉(zhuǎn)換)去掉不需要的操作。
此外,要做好優(yōu)化,你還需要做好應(yīng)用基線,比如性能基線(何時(shí)性能突然下降)、成本基線(去年雙 11 用了多少臺(tái)機(jī)器)、鏈路基線(我們的系統(tǒng)發(fā)生了哪些變化),你可以通過這些基線持續(xù)關(guān)注系統(tǒng)的性能,做到在代碼上提升編碼質(zhì)量,在業(yè)務(wù)上改掉不合理的調(diào)用,在架構(gòu)和調(diào)用鏈路上不斷的改進(jìn)。