程序員常常要面臨的一個問題就是:如何提升程序性能?前端
這篇文章,咱們按部就班,從內存、磁盤I/O、網絡I/O、CPU、緩存、架構、算法等多層次遞進,串聯起高性能開發十大必須掌握的核心技術。nginx
準備好了嗎,坐穩了,發車!程序員
首先,咱們從最簡單的模型開始。web
老闆告訴你,開發一個靜態web服務器,把磁盤文件(網頁、圖片)經過網絡發出去,怎麼作?算法
你花了兩天時間,擼了一個1.0版本:數據庫
上線一天,老闆發現太慢了,大一點的圖片加載都有卡頓感。讓你優化,這個時候,你須要:編程
上面的工做線程,從磁盤讀文件、再經過網絡發送數據,數據從磁盤到網絡,兜兜轉轉須要拷貝四次,其中CPU親自搬運都須要兩次。後端
零拷貝技術,解放CPU,文件數據直接從內核發送出去,無需再拷貝到應用程序緩衝區,白白浪費資源。數組
Linux API:瀏覽器
ssize_t sendfile( int out_fd, int in_fd, off_t *offset, size_t count );
函數名字已經把函數的功能解釋的很明顯了:發送文件。指定要發送的文件描述符和網絡套接字描述符,一個函數搞定!
用上了零拷貝技術後開發了2.0版本,圖片加載速度明顯有了提高。不過老闆發現同時訪問的人變多了之後,又變慢了,又讓你繼續優化。這個時候,你須要:
前面的版本中,每一個線程都要阻塞在recv等待對方的請求,這來訪問的人多了,線程開的就多了,大量線程都在阻塞,系統運轉速度也隨之降低。
這個時候,你須要多路複用技術,使用select模型,將全部等待(accept、recv)都放在主線程裏,工做線程不須要再等待。
過了一段時間以後,網站訪問的人愈來愈多了,就連select也開始有點目不暇接,老闆繼續讓你優化性能。
這個時候,你須要升級多路複用模型爲epoll。
select有三弊,epoll有三優。
用上了epoll多路複用技術,開發了3.0版本,你的網站能同時處理不少用戶請求了。
可是貪心的老闆還不知足,不捨得升級硬件服務器,卻讓你進一步提升服務器的吞吐量。你研究後發現,以前的方案中,工做線程老是用到才建立,用完就關閉,大量請求來的時候,線程不斷建立、關閉、建立、關閉,開銷挺大的。這個時候,你須要:
咱們能夠在程序一開始啓動後就批量啓動一波工做線程,而不是在有請求來的時候纔去建立,使用一個公共的任務隊列,請求來臨時,向隊列中投遞任務,各個工做線程統一從隊列中不斷取出任務來處理,這就是線程池技術。
多線程技術的使用必定程度提高了服務器的併發能力,但同時,多個線程之間爲了數據同步,經常須要使用互斥體、信號、條件變量等手段來同步多個線程。這些重量級的同步手段每每會致使線程在用戶態/內核態屢次切換,系統調用,線程切換都是不小的開銷。
在線程池技術中,提到了一個公共的任務隊列,各個工做線程須要從中提取任務進行處理,這裏就涉及到多個工做線程對這個公共隊列的同步操做。
有沒有一些輕量級的方案來實現多線程安全的訪問數據呢?這個時候,你須要:
多線程併發編程中,遇到公共數據時就須要進行線程同步。而這裏的同步又能夠分爲阻塞型同步和非阻塞型同步。
阻塞型同步好理解,咱們經常使用的互斥體、信號、條件變量等這些操做系統提供的機制都屬於阻塞型同步,其本質都是要加「鎖」。
與之對應的非阻塞型同步就是在無鎖的狀況下實現同步,目前有三類技術方案:
三類技術方案都是經過必定的算法和技術手段來實現不用阻塞等待而實現同步,這其中又以Lock-free最爲應用普遍。
Lock-free可以普遍應用得益於目前主流的CPU都提供了原子級別的read-modify-write原語,這就是著名的CAS(Compare-And-Swap)操做。在Intel x86系列處理器上,就是cmpxchg系列指令。
// 經過CAS操做實現Lock-free do { ... } while(!CAS(ptr,old_data,new_data ))
咱們經常見到的無鎖隊列、無鎖鏈表、無鎖HashMap等數據結構,其無鎖的核心大都來源於此。在平常開發中,恰當的運用無鎖化編程技術,能夠有效地下降多線程阻塞和切換帶來的額外開銷,提高性能。
服務器上線了一段時間,發現服務常常崩潰異常,排查發現是工做線程代碼bug,一崩潰整個服務都不可用了。因而你決定把工做線程和主線程拆開到不一樣的進程中,工做線程崩潰不能影響總體的服務。這個時候出現了多進程,你須要:
提起進程間通訊,你能想到的是什麼?
以上各類進程間通訊的方式詳細介紹和比較,推薦一篇文章一文掌握進程間通訊,這裏再也不贅述。
對於本地進程間須要高頻次的大量數據交互,首推共享內存這種方案。
現代操做系統廣泛採用了基於虛擬內存的管理方案,在這種內存管理方式之下,各個進程之間進行了強制隔離。程序代碼中使用的內存地址均是一個虛擬地址,由操做系統的內存管理算法提早分配映射到對應的物理內存頁面,CPU在執行代碼指令時,對訪問到的內存地址再進行實時的轉換翻譯。
從上圖能夠看出,不一樣進程之中,雖然是同一個內存地址,最終在操做系統和CPU的配合下,實際存儲數據的內存頁面倒是不一樣的。
而共享內存這種進程間通訊方案的核心在於:若是讓同一個物理內存頁面映射到兩個進程地址空間中,雙方不是就能夠直接讀寫,而無需拷貝了嗎?
固然,共享內存只是最終的數據傳輸載體,雙方要實現通訊還得藉助信號、信號量等其餘通知機制。
用上了高性能的共享內存通訊機制,多個服務進程之間就能夠愉快的工做了,即使有工做進程出現Crash,整個服務也不至於癱瘓。
不久,老闆增長需求了,再也不知足於只能提供靜態網頁瀏覽了,須要可以實現動態交互。這一次老闆還算良心,給你加了一臺硬件服務器。
因而你用Java/PHP/Python等語言搞了一套web開發框架,單獨起了一個服務,用來提供動態網頁支持,和原來等靜態內容服務器配合工做。
這個時候你發現,靜態服務和動態服務之間常常須要通訊。
一開始你用基於HTTP的RESTful接口在服務器之間通訊,後來發現用JSON格式傳輸數據效率低下,你須要更高效的通訊方案。
這個時候你須要:
什麼是RPC技術?
RPC全稱Remote Procedure Call,遠程過程調用。咱們平時編程中,隨時都在調用函數,這些函數基本上都位於本地,也就是當前進程某一個位置的代碼塊。但若是要調用的函數不在本地,而在網絡上的某個服務器上呢?這就是遠程過程調用的來源。
從圖中能夠看出,經過網絡進行功能調用,涉及參數的打包解包、網絡的傳輸、結果的打包解包等工做。而其中對數據進行打包和解包就須要依賴序列化技術來完成。
什麼是序列化技術?
序列化簡單來講,是將內存中的對象轉換成能夠傳輸和存儲的數據,而這個過程的逆向操做就是反序列化。序列化 && 反序列化技術能夠實現將內存對象在本地和遠程計算機上搬運。比如把大象關進冰箱門分三步:
序列化技術有不少免費開源的框架,衡量一個序列化框架的指標有這麼幾個:
下面流行的三大序列化框架protobuf、thrift、avro的對比:
廠商:Google
支持語言:C++、Java、Python等
動態性支持:較差,通常須要提早編譯
是否包含RPC:否
簡介:ProtoBuf是谷歌出品的序列化框架,成熟穩定,性能強勁,不少大廠都在使用。自身只是一個序列化框架,不包含RPC功能,不過能夠與同是Google出品的GPRC框架一塊兒配套使用,做爲後端RPC服務開發的黃金搭檔。
缺點是對動態性支持較弱,不過在更新版本中這一現象有待改善。整體來講,ProtoBuf都是一款很是值得推薦的序列化框架。
廠商:Facebook
支持語言:C++、Java、Python、PHP、C#、Go、JavaScript等
動態性支持:差
是否包含RPC:是
簡介:這是一個由Facebook出品的RPC框架,自己內含二進制序列化方案,但Thrift自己的RPC和數據序列化是解耦的,你甚至能夠選擇XML、JSON等自定義的數據格式。在國內一樣有一批大廠在使用,性能方面和ProtoBuf不分伯仲。缺點和ProtoBuf同樣,對動態解析的支持不太友好。
支持語言:C、C++、Java、Python、C#等
動態性支持:好
是否包含RPC:是
簡介:這是一個源自於Hadoop生態中的序列化框架,自帶RPC框架,也可獨立使用。相比前兩位最大的優點就是支持動態數據解析。
爲何我一直在說這個動態解析功能呢?在以前的一段項目經歷中,軒轅就遇到了三種技術的選型,擺在咱們面前的就是這三種方案。須要一個C++開發的服務和一個Java開發的服務可以進行RPC。
Protobuf和Thrift都須要經過「編譯」將對應的數據協議定義文件編譯成對應的C++/Java源代碼,而後合入項目中一塊兒編譯,從而進行解析。
當時,Java項目組同窗很是強硬的拒絕了這一作法,其理由是這樣編譯出來的強業務型代碼融入他們的業務無關的框架服務,而業務是常變的,這樣作不夠優雅。
最後,通過測試,最終選擇了AVRO做爲咱們的方案。Java一側只須要動態加載對應的數據格式文件,就能對拿到的數據進行解析,而且性能上還不錯。(固然,對於C++一側仍是選擇了提早編譯的作法)
自從你的網站支持了動態能力,免不了要和數據庫打交道,但隨着用戶的增加,你發現數據庫的查詢速度愈來愈慢。
這個時候,你須要:
想一想你手上有一本數學教材,可是目錄被人給撕掉了,如今要你翻到講三角函數的那一頁,你該怎麼辦?
沒有了目錄,你只有兩種辦法,要麼一頁一頁的翻,要麼隨機翻,直到找到三角函數的那一頁。
對於數據庫也是同樣的道理,若是咱們的數據表沒有「目錄」,那要查詢知足條件的記錄行,就得全表掃描,那可就惱火了。因此爲了加快查詢速度,得給數據表也設置目錄,在數據庫領域中,這就是索引。
通常狀況下,數據表都會有多個字段,那根據不一樣的字段也就能夠設立不一樣的索引。
主鍵咱們都知道,是惟一標識一條數據記錄的字段(也存在多個字段一塊兒來惟一標識數據記錄的聯合主鍵),那與之對應的就是主鍵索引了。
彙集索引是指索引的邏輯順序與表記錄的物理存儲順序一致的索引,通常狀況下主鍵索引就符合這個定義,因此通常來講主鍵索引也是彙集索引。可是,這不是絕對的,在不一樣的數據庫中,或者在同一個數據庫下的不一樣存儲引擎中仍是有不一樣。
彙集索引的葉子節點直接存儲了數據,也是數據節點,而非彙集索引的葉子節點沒有存儲實際的數據,須要二次查詢。
索引的實現主要有三種:
其中,B+樹用的最多,其特色是樹的節點衆多,相較於二叉樹,這是一棵多叉樹,是一個扁平的胖樹,減小樹的深度有利於減小磁盤I/O次數,適宜數據庫的存儲特色。
哈希表實現的索引也叫散列索引,經過哈希函數來實現數據的定位。哈希算法的特色是速度快,常數階的時間複雜度,但缺點是隻適合準確匹配,不適合模糊匹配和範圍搜索。
位圖索引相對就少見了。想象這麼一個場景,若是某個字段的取值只有有限的少數幾種可能,好比性別、省份、血型等等,針對這樣的字段若是用B+樹做爲索引的話會出現什麼狀況?會出現大量索引值相同的葉子節點,這其實是一種存儲浪費。
位圖索引正是基於這一點進行優化,針對字段取值只有少許有限項,數據表中該列字段出現大量重複時,就是位圖索引一展身手的時機。
所謂位圖,就是Bitmap,其基本思想是對該字段每個取值創建一個二進制位圖來標記數據表的每一條記錄的該列字段是不是對應取值。
索引雖好,但也不可濫用,一方面索引最終是要存儲到磁盤上的,無疑會增長存儲開銷。另外更重要的是,數據表的增刪操做通常會伴隨對索引的更新,所以對數據庫的寫入速度也是會有必定影響。
你的網站如今訪問量愈來愈大了,同時在線人數大大增加。然而,大量用戶的請求帶來了後端程序對數據庫大量的訪問。漸漸的,數據庫的瓶頸開始出現,沒法再支持日益增加的用戶量。老闆再一次給你下達了性能提高的任務。
從物理CPU對內存數據的緩存到瀏覽器對網頁內容的緩存,緩存技術遍及於計算機世界的每個角落。
面對當前出現的數據庫瓶頸,一樣能夠用緩存技術來解決。
每次訪問數據庫都須要數據庫進行查表(固然,數據庫自身也有優化措施),反映到底層就是進行一次或屢次的磁盤I/O,但凡涉及I/O的就會慢下來。若是是一些頻繁用到但又不會常常變化的數據,何不將其緩存在內存中,沒必要每一次都要找數據庫要,從而減輕對數據庫對壓力呢?
有需求就有市場,有市場就會有產品,以memcached和Redis爲表明的內存對象緩存系統應運而生。
緩存系統有三個著名的問題:
關於這三個問題的更詳細闡述,推薦一篇文章什麼是緩存系統的三座大山。
有了緩存系統,咱們就能夠在向數據庫請求以前,先詢問緩存系統是否有咱們須要的數據,若是有且知足須要,咱們就能夠省去一次數據庫的查詢,若是沒有,咱們再向數據庫請求。
注意,這裏有一個關鍵的問題,如何判斷咱們要的數據是否是在緩存系統中呢?
進一步,咱們把這個問題抽象出來:如何快速判斷一個數據量很大的集合中是否包含咱們指定的數據?
這個時候,就是布隆過濾器大顯身手的時候了,它就是爲了解決這個問題而誕生的。那布隆過濾器是如何解決這個問題的呢?
先回到上面的問題中來,這實際上是一個查找問題,對於查找問題,最經常使用的解決方案是搜索樹和哈希表兩種方案。
由於這個問題有兩個關鍵點:快速、數據量很大。樹結構首先得排除,哈希表卻是能夠作到常數階的性能,但數據量大了之後,一方面對哈希表的容量要求巨大,另外一方面如何設計一個好的哈希算法可以作到如此大量數據的哈希映射也是一個難題。
對於容量的問題,考慮到只須要判斷對象是否存在,而並不是拿到對象,咱們能夠將哈希表的表項大小設置爲1個bit,1表示存在,0表示不存在,這樣大大縮小哈希表的容量。
而對於哈希算法的問題,若是咱們對哈希算法要求低一些,那哈希碰撞的機率就會增長。那一個哈希算法容易衝突,那就多弄幾個,多個哈希函數同時衝突的機率就小的多。
布隆過濾器就是基於這樣的設計思路:
當設置對應的key-value時,按照一組哈希算法的計算,將對應比特位置1。
但當對應的key-value刪除時,卻不能將對應的比特位置0,由於保不許其餘某個key的某個哈希算法也映射到了同一個位置。
也正是由於這樣,引出了布隆過濾器的另一個重要特色:布隆過濾器斷定存在的實際上不必定存在,但斷定不存在的則必定不存在。
大家公司網站的內容愈來愈多了,用戶對於快速全站搜索的需求日益強烈。這個時候,你須要:
對於一些簡單的查詢需求,傳統的關係型數據庫尚且能夠應付。但搜索需求一旦變得複雜起來,好比根據文章內容關鍵字、多個搜索條件但邏輯組合等狀況下,數據庫就捉襟見肘了,這個時候就須要單獨的索引系統來進行支持。
現在行業內普遍使用的ElasticSearch(簡稱ES)就是一套強大的搜索引擎。集全文檢索、數據分析、分佈式部署等優勢於一身,成爲企業級搜索技術的首選。
ES使用RESTful接口,使用JSON做爲數據傳輸格式,支持多種查詢匹配,爲各主流語言都提供了SDK,易於上手。
另外,ES經常和另外兩個開源軟件Logstash、Kibana一塊兒,造成一套日誌收集、分析、展現的完整解決方案:ELK架構。
其中,Logstash負責數據的收集、解析,ElasticSearch負責搜索,Kibana負責可視化交互,成爲很多企業級日誌分析管理的鐵三角。
不管咱們怎麼優化,一臺服務器的力量終究是有限的。公司業務發展迅猛,原來的服務器已經不堪重負,因而公司採購了多臺服務器,將原有的服務都部署了多份,以應對日益增加的業務需求。
如今,同一個服務有多個服務器在提供服務了,須要將用戶的請求均衡的分攤到各個服務器上,這個時候,你須要:
顧名思義,負載均衡意爲將負載均勻平衡分配到多個業務節點上去。
和緩存技術同樣,負載均衡技術一樣存在於計算機世界到各個角落。
按照均衡實現實體,能夠分爲軟件負載均衡(如LVS、Nginx、HAProxy)和硬件負載均衡(如A十、F5)。
按照網絡層次,能夠分爲四層負載均衡(基於網絡鏈接)和七層負載均衡(基於應用內容)。
按照均衡策略算法,能夠分爲輪詢均衡、哈希均衡、權重均衡、隨機均衡或者這幾種算法相結合的均衡。
而對於如今遇到等問題,可使用nginx來實現負載均衡,nginx支持輪詢、權重、IP哈希、最少鏈接數目、最短響應時間等多種方式的負載均衡配置。
輪詢
upstream web-server { server 192.168.1.100; server 192.168.1.101; }
權重
upstream web-server { server 192.168.1.100 weight=1; server 192.168.1.101 weight=2; }
IP哈希值
upstream web-server { ip_hash; server 192.168.1.100 weight=1; server 192.168.1.101 weight=2; }
最少鏈接數目
upstream web-server { ip_hash; server 192.168.1.100 weight=1; server 192.168.1.101 weight=2; }
最短響應時間
upstream web-server { server 192.168.1.100 weight=1; server 192.168.1.101 weight=2; fair; }
總結
高性能是一個永恆的話題,其涉及的技術和知識面其實遠不止上面列出的這些。
從物理硬件CPU、內存、硬盤、網卡到軟件層面的通訊、緩存、算法、架構每個環節的優化都是通往高性能的道路。
路漫漫其修遠兮,吾將上下而求索。
本文轉載自微信公衆號「編程技術宇宙」,能夠經過如下二維碼關注。轉載本文請聯繫編程技術宇宙公衆號。
【編輯推薦】
【責任編輯:武曉燕 TEL:(010)68476606】