1. 程式人生 > >《億級流量網站架構核心技術》總結

《億級流量網站架構核心技術》總結

nginx後端節點健康檢查

主要有三種實現方式:
1. 本身自帶的ngx_http_proxy_module模組和ngx_http_upstream_module模組,屬於惰性檢測。
ngx_http_proxy_module:proxy_connect_timeout(與後端伺服器建立連線的超時時間)、proxy_read_timeout(從後端伺服器讀取響應的超時)和proxy_next_upstream(何種情況下一個失敗的請求應該被髮送到下一臺後端伺服器)
ngx_http_upstream_module:max_fails(嘗試失敗次數)、fail_timeout(多長時間內失敗,並且在這個時間內不再請求)。
2. nginx_upstream_check_module模組(淘寶開發的模組),屬於主動檢測。可以間隔檢測後臺伺服器的健康狀態,並對伺服器的狀態進行標記。還提供了路徑可以實時檢視各伺服器的健康狀態。
3. 直接使用淘寶開源的Tengine,自帶了主動檢測健康狀態的模組,主動檢測。

tomcat處理請求的流程

  1. Connector埠監聽到請求,根據http協議解析該次請求。
  2. 解析的http報文,經裝飾模式轉化為servlet api對應的HttpServletRequest與HttpServletReponse。
  3. 經層層容器engine、host、context最終到過我們所寫的業務servlet的service方法。
  4. 業務方法service,處理相關的業務邏輯,寫入相應的響應的至response,並返回tomat的容器元件。
  5. tomcat該處理執行緒關閉響應流Response並將響應內容返回客戶端。
  6. tomcat該處理執行緒被釋放,然後用於下次請求的處理。

servlet3.0的非同步特性

  1. 優勢:同步的servlet執行緒會阻塞到處理完成返回響應才釋放;非同步的servlet執行緒會將實際的業務處理轉交給工作執行緒,然後馬上釋放servlet執行緒或放回池中,所以相對於同步servlet能夠提升吞吐量。但效能是沒有改善的。
  2. 步驟:
    • ServletRequest.startAsync開啟非同步化,獲取AsyncContext的例項。AsyncContext提供方法讓ServletRequest和ServletResponse物件引用,隨後釋放servlet執行緒,但沒有關閉相應response流,所以我們可以在業務程式碼中繼續進行處理。
    • 隨後將請求轉交給業務執行緒池中的一個執行緒進行處理,處理完之後,呼叫AsyncContext .complete(),將響應寫回引用的AsyncContext的響應流中,並關閉響應流,完成此次請求處理。
  3. 注意:AsyncContext並不是真正的非同步輸出,而是同步輸出,但是解放伺服器端的執行緒使用。使用AsyncContext的時候,對於瀏覽器來說,他們是同步在等待輸出的,但是對於伺服器端來說,處理此請求的執行緒並沒有卡在那裡等待,則是把當前的處理轉為執行緒池處理了。
  4. 在SpringMVC中,controller通過返回一個Callable或者DefferredResult,此時servlet容器執行緒已經釋放,SpringMVC是通過藉助TaskExecutor執行緒池呼叫池中一個執行緒來對實際業務進行處理。

Guava框架

相對於Apache Commons Collection提供的集合工具類,Goolgle 的Guava提供了更強大的集合工具類,除了集合的操作,還包含檔案、字串等其他的操作工具類。
1. 對集合建立檢視,設定只讀操作。
2. 對集合中的資料進行轉換、過濾等。
3. 對引數進行驗證(非空、長度等)。
4. 對不同集合進行交集、差集、並集。
5. 檔案的讀取操作。
6. 對字串的分割、合併等操作。
7. Multimap對map集合的高階操作等。
……………..

分散式系統設計冪等性

  1. 冪等性:是指一次和多次請求某一個資源對系統造成的影響是一致的。http請求中,get、put、delete都是冪等的,但post不是冪等。
  2. 保證冪等性的方法:
    • 去重表,每次介面呼叫之前都根據請求地址+引數生成一個唯一鍵,作為去重表的ID,然後儲存。不報錯則說明成功,報錯則說明重複請求了。
    • 狀態機冪等,比如訂單狀態的處理,一般情況下存在有限狀態機,如果狀態機已經處於下一個狀態,這時候來了一個上一個狀態變更的請求,理論上是不能夠變更。保證了有限狀態機的冪等。
    • token機制,防止頁面重複提交。一般是表單資料提交前先向伺服器申請token,token放入快取並有過期時間,然後提交資料到後臺並校驗攜帶過去的token是否一致,同時刪除token。

protobuf二進位制傳輸格式

  1. 定義:protobuf是Google開源的二進位制傳輸格式。
  2. 使用:按照一定的語法定義結構化的訊息格式,然後送給命令列工具,工具會自動生成相關的類,並且可以很輕鬆的呼叫相關方法來序列化和反序列化。
  3. 效能對比:protobuf序列化後的大小是json的十分之一、xml的二十分之一、是java自帶二進位制序列化的十分之一,所以效能很高。還有facebook開源的thrift,效能要更高。
  4. 適合場景:
    • 需要和其他系統進行互動,對訊息大小很敏感。
    • 小資料的場合。大資料並不適合。

java堆外記憶體

  1. java本地儲存物件的幾種方式:堆記憶體、堆外記憶體和磁碟。堆外記憶體可以通過-XX:MaxDirectMemorySize設定,不設定的話預設跟堆記憶體一樣。來自於java.nio。
  2. 相對於堆記憶體的優點:
    • 避免了垃圾回收GC的工作。
    • 減少IO時的記憶體複製,不需要堆記憶體Buffer拷貝一份到直接記憶體中,然後才寫入Socket中。
  3. 建立(以Netty為例):Netty使用的堆外記憶體是Java NIO的DirectByteBuffer類。首先向Bits類申請額度,根據一個全域性變數totalCapacity(記錄堆外記憶體的總大小),判斷是否有足夠限額分配,批准的話呼叫sun.misc.Unsafe分配記憶體,返回記憶體基地址。額度不夠則先進行GC後再分配,如果垃圾回收了100ms記憶體還不夠,則拋OOM異常。
  4. 回收:只有在進行full GC的時候才會回收對外記憶體,可以主動呼叫System.gc來觸發,但有不確定性。所以一般主動從DirectByteBuffer中取出sun.misc.Cleaner,然後呼叫其clean()方法即可。
  5. 適用場景:不適合儲存很複雜的物件,一般簡單的物件或者扁平化的結構比較適合。

快取雪崩,快取穿透,快取併發,快取預熱,快取演算法

  1. 快取雪崩:可能是因為資料未載入到快取中,或者快取同一時間大面積的失效,從而導致所有請求都去查資料庫,導致資料庫CPU和記憶體負載過高,甚至宕機。解決思路:
    • 加鎖計數(即限制併發的數量,可以用semphore)或者起一定數量的佇列來避免快取失效時大量請求併發到資料庫。但這種方式會降低吞吐量。
    • 分析使用者行為,然後失效時間均勻分佈。或者在失效時間的基礎上再加1~5分鐘的隨機數。
    • 如果是某臺快取伺服器宕機,則考慮做主備。
  2. 快取穿透:指使用者查詢資料,在資料庫沒有,自然在快取中也不會有。這樣就導致使用者查詢的時候,在快取中找不到,每次都要去資料庫中查詢。解決思路:
    • 如果查詢資料庫也為空,直接設定一個預設值存放到快取,這樣第二次到緩衝中獲取就有值了,而不會繼續訪問資料庫。設定一個過期時間或者當有值的時候將快取中的值替換掉即可。
    • 可以給key設定一些格式規則,然後查詢之前先過濾掉不符合規則的Key。
  3. 快取併發:如果網站併發訪問高,一個快取如果失效,可能出現多個程序同時查詢DB,同時設定快取的情況,如果併發確實很大,這也可能造成DB壓力過大,還有快取頻繁更新的問題。解決思路:
    • 對快取查詢加鎖,如果KEY不存在,就加鎖,然後查DB入快取,然後解鎖;其他程序如果發現有鎖就等待,然後等解鎖後返回資料或者進入DB查詢。
  4. 快取預熱:目的就是在系統上線前,將資料載入到快取中。解決思路:
    • 資料量不大的話,在系統啟動的時候直接載入。
    • 自己寫個簡單的快取預熱程式。
  5. 快取演算法:
    • FIFO演算法:First in First out,先進先出。原則:一個數據最先進入快取中,則應該最早淘汰掉。也就是說,當快取滿的時候,應當把最先進入快取的資料給淘汰掉。
    • LFU演算法:Least Frequently Used,最不經常使用演算法。
    • LRU演算法:Least Recently Used,近期最少使用演算法。
    • LRU和LFU的區別。LFU演算法是根據在一段時間裡資料項被使用的次數選擇出最少使用的資料項,即根據使用次數的差異來決定。而LRU是根據使用時間的差異來決定的。

redis的持久化機制

  1. redis主要提供了兩種持久化機制:RDB和AOF。
  2. RDB:預設開啟,會按照配置的指定時間將記憶體中的資料快照到磁碟中,建立一個dump.rdb檔案,redis啟動時再回復到記憶體中。redis會單獨建立fork()一個子程序,將資料寫入到臨時檔案中,持久化的過程結束了,再用這個臨時檔案替換上次的快照檔案,然後子程序退出。需要注意的是,每次快照持久化都是將記憶體資料完整寫入磁碟一次,並不是增量的只同步變更資料,所以如果資料量大的話,而且寫操作頻繁,必然會引起大量的磁碟I/O操作,會嚴重影響效能,並且最後一次持久化後的資料可能會丟失。
  3. AOF:以日誌的形式記錄每個寫操作(讀操作不記錄),只需追加檔案但不可以改寫檔案,redis啟動時會根據日誌從頭到尾全部執行一遍以完成資料的恢復工作。包括flushDB也會執行。主要有兩種方式觸發:有寫操作就寫、每秒定時寫(也會丟資料)。因為AOF採用追加的方式,所以檔案會越來越大,針對這個問題,新增了重寫機制,就是當日志文件大到一定程度的時候,會fork出一條新程序來遍歷程序記憶體中的資料,每條記錄對應一條set語句,寫到臨時檔案中,然後再替換到舊的日誌檔案(類似rdb的操作方式)。預設觸發是當aof檔案大小是上次重寫後大小的一倍且檔案大於64M時觸發。
  4. 當兩種方式同時開啟時,資料恢復redis會優先選擇AOF恢復。一般情況下,只要使用預設開啟的RDB即可,因為相對於AOF,RDB便於進行資料庫備份,並且恢復資料集的速度也要快很多。
  5. 開啟持久化快取機制,對效能會有一定的影響,特別是當設定的記憶體滿了的時候,更是下降到幾百reqs/s。所以如果只是用來做快取的話,可以關掉持久化。

執行緒池ExecutorService的關閉

  1. 當jvm關閉或重啟的時候,要注意主動關閉執行緒池,要不然有可能會導致java程序一直殘留在OS中。
  2. 關閉流程:
    • shutdown,起到通知的作用,不能繼續向執行緒池追加新的任務。
    • awaitTermination,在指定的時間內所有的任務結束的時候,返回true,否則返回false,返回false意味著還有執行緒未完成。
    • shutdownNow,向所有執行中的執行緒發出interrupted以終止執行緒的執行。

HttpClient4.3連線池

  1. 使用基本步驟:
    • 建立連線管理器PoolingHttpClientConnectionManager。
    • 設定總併發連線數maxTotal和單路由連線數maxPerRoute。
    • 設定socket的配置。
    • 設定http connection的配置。
    • 設定request請求的配置。
    • 設定重試策略,可以重寫。
    • 建立httpClient,設定上面步驟建立的管理器、請求相關配置、重試策略等。
      • 根據不同的屬性不同,此時會建立不同的Executor執行器。HttpClient使用了責任鏈模式,所有Executor都實現了ClientExecChain介面的execute()方法,每個Executor都持有下一個要執行的Executor的引用,這樣就會形成一個Executor的執行鏈條,請求在這個鏈條上傳遞。
    • 建立一個get請求,並重新設定請求引數,覆蓋預設。
    • 執行請求。
    • 釋放連接回到連線池、關閉連線、關閉連線管理器。
  2. 基本結構:
    • 連線池的實體PoolEntry<HttpRoute, ManagedHttpConnection>
      • 包含ManagedHttpClientConnection連線,其實就是一個httpClient連線,真正連線後會bind繫結一個socket,用於傳輸http報文。
      • 連線的route路由資訊。
      • 連線存活時間相隔資訊。
    • LinkedList<PoolEntry> avaiable,存放可用的連線。使用完後所有可重用的連接回被放到available連結串列頭部,之後再獲取連線時優先從available連結串列頭部迭代可用的連線。之所以使用LinkedList是利用了其佇列的特性,即可以在隊首和隊尾分別插入、刪除。入available連結串列時都是addFirst()放入頭部,獲取時都是從頭部依次迭代可用的連線,這樣可以獲取到最新放入連結串列的連線,其離過期時間更遠(這種策略可以儘量保證獲取到的連線沒有過期,而從隊尾獲取連線是可以做到在連線過期前儘量使用,但獲取到過期連線的風險就大了),刪除available連結串列中連線時是從隊尾開始,即先刪除最可能快要過期的連線。
    • HashSet<PoolEntry> leased,存放被租用的連線。maxTotal限制的是外層httpConnPool中leased集合和available佇列的總和的大小,leased和available的大小沒有單獨限制。
    • LinkedList<PoolEntryFuture> pending,存放等待獲取連線的執行緒的Future。當從池中獲取連線時,如果available連結串列沒有現成可用的連線,且當前路由或連線池已經達到了最大數量的限制,也不能建立連線了,此時不會阻塞整個連線池,而是將當前執行緒用於獲取連線的Future放入pending連結串列的末尾,之後當前執行緒呼叫await(),釋放持有的鎖,並等待被喚醒。當有連線被release()釋放回連線池時,會從pending連結串列頭獲取future,並喚醒其執行緒繼續獲取連線,做到了先進先出。
  3. 注意事項
    • 引起TCP連線過高的問題:因為連線池用完之後的close,並不會真的釋放掉tcp而是放回池中,所以當不小心new了多個httpClient的話,併發量一高,就有可能導致tcp連結高居不下,要等到超時了才會自己釋放。因為HttpClient在多執行緒下是執行緒安全的,在多執行緒的環境中應該只是用一個全域性單例的HttpClient,並且使用MultiThreadHttpConnectionManager來管理Connection。
    • 要注意設定每個路由的最大連線數(路由指的是某個地址),預設值是2。
    • 引發經常拋連線超時異常的問題:有可能是某個路由的最大連線數設定得不對,還有就是讀取資料的超時時間設定過大。

協程(纖程)Fiber

  1. 協程概念:一種使用者態的輕量級執行緒,其實就是單執行緒,指定執行整個函式中到一部分然後就先出去執行別的,等條件滿足時,協程下次更新幀到了再繼續往下執行。優點是無需執行緒上下文切換的開銷,充分開發了單CPU的能力,資源佔用低,適合高併發I/O。缺點也很明顯,就是沒辦法利用多CPU的優勢。
  2. 框架:Quasar,排程器使用ForkJoinPool來排程這些fiber。Fiber排程器FiberScheduler是一個高效的、work-stealing、多執行緒的排程器。
  3. 場景:服務A平時需要呼叫其他服務,但其他服務在併發高的時候延遲很嚴重。
    • 一開始可以用httpClient連線池+執行緒池來處理,但如果呼叫服務的時候延遲太高或者超時,則會導致服務A的吞吐量會特別差。原因主要是一般一個連結由一個執行緒來處理,是阻塞的,所以線上程池數有限的情況下,吞吐量肯定上不去。並且當所有執行緒都I/O阻塞的時候,會很浪費CPU資源,並且CPU會一直做無用的上下文切換。
    • 這時候可以考慮協程來替換。

ForkJoinPool執行緒池

  1. Fork/Join框架是Java7提供了的一個用於並行執行任務的框架,是一個把大任務分割成若干個小任務,最終彙總每個小任務結果後得到大任務結果的框架。
  2. 工作竊取(work-stealing)演算法是Fork/Join框架最重要的特性。一般一個執行緒會對應一個任務佇列,當處理較快的執行緒處理完自己的任務之後,就會竊取另外一個處理比較慢的執行緒對應的任務,這時候會存在兩個執行緒同時處理一個佇列的情況,所以任務佇列一般使用雙端佇列,被竊取任務執行緒永遠從雙端佇列的頭部拿任務執行,而竊取任務的執行緒永遠從雙端佇列的尾部拿任務執行。優點是充分利用執行緒進行平行計算,並減少了執行緒間的競爭。

NIO原理

  1. 由一個專門的執行緒來處理所有的IO事件,並負責分發。
  2. 事件驅動機制:事件到的時候觸發,而不是同步的去監視事件。非同步事件驅動模型中,把會導致阻塞的操作轉化為一個非同步操作,主執行緒負責發起這個非同步操作,並處理這個非同步操作的結果。nginx就是使用的這種模型。
  3. 執行緒通訊:執行緒之間通過wait、notify等方式通訊。保證每次上下文切換都是有意義的。減少無謂的執行緒切換。
  4. 服務端和客戶端各自維護一個管理通道的物件,我們稱之為selector,該物件能檢測一個或多個通道 (channel) 上的事件。我們以服務端為例,如果服務端的selector上註冊了讀事件,某時刻客戶端給服務端傳送了一些資料,阻塞I/O這時會呼叫read()方法阻塞地讀取資料,而NIO的服務端會在selector中新增一個讀事件。服務端的處理執行緒會輪詢地訪問selector,如果訪問selector時發現有感興趣的事件到達,則處理這些事件,如果沒有感興趣的事件到達,則處理執行緒會一直阻塞直到感興趣的事件到達為止。