架構師訓練營第 1 期 - 第 07 周總結
系統性能的主要技術指標
性能測試
性能測試是針對典型、重要或關鍵的網路接口進行
是性能優化的前提和基礎
透過性能測試,獲得性能指標,從而對系統性能有一個整體的認知
是性能優化結果的檢查和度量標準
不同視角有不同的標準,不同的性能指標,也有不同的優化手段
主要視角
主觀視角
用戶感受到的性能
客觀視角
性能指標衡量的性能
兩個視角並不總是統一的
一般情況:響應時間快,用戶感受也快
有可能狀況
服務端響應快,但用戶端渲染慢、處理慢、展示慢
顯示文字及圖片順序不同,也可能影響用戶主觀感受
有時在請求時,客戶端的顯示(空白頁面或進度條)也會影響感受
性能優化的主要最終目的
讓用戶感受到性能好
架構師做性能優化時
提高客觀的性能指標
提高用戶主觀的感受
性能測試指標
響應時間
客戶端視角
從發出請求開始到收到最後響應數據所需的時間
服務端視角
從收到請求開始到返回響應數據所需的時間
是系統最重要的性能指標
直觀反映系統的 "快慢"
併發數
某一瞬時,系統 "同時" 處理 "請求" 的數目
反映系統的負載特性
網站各種用戶數
併發用戶數
即併發數
同時提交請求的用戶數
在線用戶數
當前登錄系統的用戶數
只瀏覽頁面,沒有發送請求的用戶
系統用戶數
可能訪問(註冊)系統的總用戶數
系統用戶數中的部分會成為在線用戶數,在線用戶數的部分會成為併發用戶數
會消耗系統資源的是併發用戶數
吞吐量
單位時間內,系統處理的請求數量
展現系統的處理能力
衡量指標
請求數 / 秒
頁面數 / 秒
訪問人數 / 天
處理業務數 / 小時
TPS:事務數 / 秒 (transaction per second)
HPS:HTTP請求數 / 秒 (HTTP per second)
QPS:查詢數 / 秒 (Queries per second)
TPS 與併發數的關係
若響應時間是 1 sec,併發數即為吞吐量
吞吐量 = ( 1000 / 響應時間ms) x 併發數
性能計數器
描述服務器或操作系統性能狀況的一類數據指標
System Load
對象與線程數、進程數
內存使用量
CPU 使用量
磁盤與網路 I/O
系統監控的重要參數
須對這些指標設定警報閥值,及時發現處理系統異常
性能測試方法
性能測試三階段 - 透過不斷增加併發請求壓力
性能測試 (第一階段)
以系統設計規劃的性能指標為預期目標
對系統施加併發的訪問壓力,驗證系統在資源可接受的範圍內,是否能達到預期目標
負載測試 (第二階段)
達到規劃目標後,對不斷地增加併發請求,增加系統壓力
直到系統的某項或多項性能達到安全臨界值
壓測系統最大的處理能力
壓力測試 (第三階段)
在超過安全負載的情況下,繼續對系統施加壓力
直到系統崩潰或不能在處理任何請求
獲得系統最大壓力承受能力
穩定性測試
系統在特定軟、硬體及網路環境條件下,加載一定業務壓力,進行較長時間測試
需模擬生產環境
模擬用戶使用場景
請求壓力不均勻
測試也須不均勻的對系統施加壓力
性能測試曲線
TPS - 系統資源
不斷增加併發線程及併發數
a -> b:近似線性增加
b -> c:較 a ->b 平緩,c 點為最大值
c -> d:下降,超過系統安全值,到 d 點系統崩潰
增加 -> 平緩 -> 下降 原因
剛開始系統有足夠資源
足夠 CPU
足夠內存
足夠線程
過了 b 點,系統資源開始不足
線程等待 CPU 調度
響應時間變慢,所以變平緩,但還是增加的趨勢
過了 c 點,系統資源消耗到極限
再增加壓力,處理能力反而下降
如內存不足,以硬盤虛擬內存
處理能力消耗在內存置換上
在成本和收益的考量下
架構師須決定系統要運行在哪個區間
成本收益的平衡點在哪裡,就是架構設計的部署決策
a -> b:服務器部署較多(拉低每一台的TPS),成本高
b -> c:服務器較少,成本較低,但系統容許偏差能力比較低
c -> d:服務器最少,成本最低,但系統也最危險
響應時間 - 併發用戶數
不斷增加併發線程及併發數
全鏈路壓測的挑戰
全鏈路壓測
測試整個系統是否達到性能目標
模擬真實系統運行,測試所有用戶可能訪問的各種接口的組合
通常是大促銷之前用於壓測整個系統
如淘寶天貓的雙十一
在特定的業務場景下,將相關鏈路完整的串聯起來同時施壓
盡可能模擬出真實用戶的行為
目的
找出系統性能的瓶頸
探測系統整體的真實處理能力
指導在大促銷前進行容量規劃及性能優化
挑戰
壓測相關的業務眾多、牽涉到整條練路上所有的基礎設施及中間件
須確保壓測流量全方位、無死角
需考慮壓測數據如何構造
數據模型如何與真實貼近
壓測直接在線上的真實環境進行模擬,要保證對線上真實的業務數據沒有影響
製作極高併發的巨大數據流量
解決方法
數據構造
無法手工構造數據
無法兼顧所有場景
無法保證模擬跟真實環境一樣的數據
利用真實線上用戶產生的數據
在真實線上 Dump 出真實數據
通過日誌或網路探針取下所有用戶提交的request
對 dump 出來的數據進行
清洗過濾
移除用戶敏感數據
支付信息
身分訊息
密碼訊息
財務訊息
隔離訂正
放入基礎數據池中
成為待壓測的基本數據
產生真正的壓測數據
結合 Bi (商業智能)數據中心
進行預測
按比例放大
測試壓測數據可用
利用限流調用至真實環境上測試將來壓測的數據是否可用
流量平台
拉取壓測數據,真正進行測試
數據隔離 (壓測數據與真實數據隔離)
邏輯隔離
直接把壓測數據與真實數據寫在一起
但對壓測數據做特殊標識
可能會汙染線上真實數據,破壞線上數據安全性
虛擬隔離
在寫入數據的地方做 Mock,不真正寫入
mock 會對壓測結果產生干擾
若瓶頸為資料寫入的地方
物理隔離
對壓測數據做特殊標識
對壓測流量的寫,寫入隔離的位置
包含存儲、緩存、搜索引擎等
流量構造 (以天貓全鏈路壓測為例)
Master + Slave 結構
Master (流量控制台)
管理slave 節點
平台運轉控制
命令發送
數據收集
決策
Slave 節點
部署在全球各地的 CDN 節點
壓測引擎
具體的請求發送
模擬從全球各地過來的用戶請求
平穩輸出每秒 1000萬的用戶請求,保持過億個無線用戶長鏈接
全鏈路壓測平台化
平台化原因
各種促銷越來越多
需要壓測的時間、場景越來越多
各個不同產品線也有壓測需求
需要壓測時,向資源池申請
壓測引擎
多少個CDN服務器
多少流量
壓測那些接口
將全鏈路壓測常態化
系統性能優化的分層思想
性能優化兩個基本原則
不能優化一個沒有測試過的軟體
須先對軟體測試,取得性能指標
發現有性能瓶頸、性能問題、性能不符合期望,才能對它進行優化
用泛泛的、概念性的、主觀的性能優化,並不能解決問題
需要測試結果支撐,再進行優化
最後再利用性能測試,了解優化結果是否達到目標
不能優化一個不瞭解的軟體
了解軟件的功能
了解架構
了解內部的關鍵設計、細節如何實現
性能測試的主要指標
響應時間
併發數
吞吐量
性能計數器
性能優化的一般方法
性能測試,獲取性能指標
高併發模擬用戶請求
壓測系統
分析指標,發現性能與資源的瓶頸
各項指標
太高可能會成為瓶頸
太低可能存在資源浪費
架構與代碼分析,找出性能與資源瓶頸的關鍵所在
對架構與代碼或其他地方進行優化
優化關鍵技術點
平衡資源利用
優化代碼架構
優化外部服務器
優化系統中間件
優化操作系統
性能測試,了解優化效果
對各項指標,優化提升的多少
進入性能優化閉環
若優化後沒有達到目標,則迭代循環下去
系統性能優化的分層思想 (略)
機房與骨幹網路性能優化
服務器與硬件性能優化
操作系統性能優化
虛擬機性能優化
基礎組件性能優化
軟件架構性能優化
軟件代碼性能優化
系統性能優化的分層思想
機房與骨幹網路性能優化
異地多活得多機房架構
大型互聯網系統的標配
多活 : 多個活躍的機房對外提供服務
目的
高可用
提高服務性能
縮短物理距離與網路傳輸時間
用戶就近訪問最近的機房
專線網路與自主CDN建設
CDN自主可控,可以進行專項的優化
優化成效比軟件代碼高
此優化提升性能是毫秒級
軟件代碼提升性能是奈秒級
服務器與硬件性能優化
使用更優的 CPU、磁盤、內存、網卡
性能優化的成效也比軟件優化高幾個數量級
如硬盤由機械式轉成 SSD,可能就會提升百倍的速度
Spark作業過程中需要傳送大量數據
根據分析,發現大量時間消耗在網路傳輸
可以用壓縮、解壓縮來優化
但消耗 CPU 時間
硬件優化
超過 1G 網卡能承受的量
圖中有切頭的圖形,表示最高僅能傳輸到切頭的量
使用 10G 網卡後,明顯減低傳輸時間 ( 23x sec -> 15x sec)
沒有觸及可傳輸的最高點
操作系統性能優化
優化前
發現大量的CPU操作為 sys 類型,消耗大量計算資源
分析發現起因是部分 linux 版本,default 打開 tranparent huge page 導致
優化後
關閉操作系統 tranparent huge page
明顯降低操作系統使用占比 (21x sec -> 15x sec)
虛擬機性能優化
如 Java虛擬機有不同的 garbage collection算法,也會影響系統性能
基礎組件性能優化
不同組件、不同版本間也會有性能差異
軟件架構性能優化
三板斧
緩存
主要優化 "讀" 操作
從內存讀取數據,減少響應時間
減少數據庫訪問,降低存儲設備負載壓力
緩存結果對象,減少 CPU 計算
異步
主要優化 "寫" 操作
即時響應,更好的用戶體驗
控制消費速度,合適的負載壓力
削峰填谷
集群
用更多的服務器,提供更多的處理能力
集群的技術目標: 對使用者而言,如何使很多台服務器看起來像一台服務器
軟件代碼性能優化
遵循面向對象的設計原則與設計模式編程
清晰的
易於復用
易於擴展
易於維護
併發編程,多線程與索
資源復用,線程池與對象池
異步編程,生產者消費者
使用合適的數據結構
數組
鏈表
hash 表
樹
計算機如何處理成百上千的併發請求
程序運行時的架構
程序是靜態的
程序運行起來以後稱之為 "進程"
執行步驟
操作系統將程序代碼 (二進制可執行代碼) 載入內存
操作系統分配
進程內存空間
進程數據結構
堆 (heap) 內存
棧 (stack) 內存
將程序的第一行指令交給 CPU 後,程序開始運行
操作系統多任務運行環境
計算機 CPU 核心數有限,如何同時處理多個用戶請求
利用進程分時執行
進程是邏輯上在 CPU 同時運行
每個進程在 CPU 上執行一個時間段後,由操作系統切換其他進程在 CPU 上執行
只要這個時間段夠短,用戶會感覺所有進程同時執行
進程的運行期狀態
運行
進程真正在 CPU上執行
處於運行狀態的進程樹小於等於 CPU 的數目
就緒
進程已獲得除了 CPU 時間外所有需要的資源
等待 CPU 調用執行
也稱為 "等待運行狀態"
阻塞
進程正在等待某一事件發生,而暫時停止運行
等待 I/O,等待鎖 .....
這時即使把 CPU 時間分給進程,進程也無法執行
也稱為 "等待或睡眠狀態"
進程 V.S. 線程
系統進行 "進程" 間CPU的切換,代價非常大
系統不在 "進程" 間調度切換
系統在 "線程" 間調度切換
服務器應用通常是 "單進程" "多線程"的方式
"進程" 內創建很多 "線程"
一個 "線程" 處理一個用戶請求
"進程" 從操作系統獲得基本的內存空間
所有 "線程" 共享 "進程" 的內存地址空間
每個 "線程" 也擁有自己私有的內存地址範圍,其他 "線程" 不能訪問
"線程" 運行的各狀態與 "進程" 相同
每個 "線程" 都有自己的用戶 "棧"
當執行相同代碼時,代碼執行時的變量值儲存在 "線程" 自己的堆棧中
所以每個用戶請求的執行,不會相互影響
每個 "線程" 共享進程 "堆" 的空間
線程安全
當某些線程 "同時" 修改 "共同" 的內存堆 (進程內共享的內存)時,會發生數據不一致
因為修改分兩個步驟 - 讀 與 寫
如庫存僅有一件商品
兩個線程同時讀到,所以均以為可以賣出
兩個線程賣出後均將庫存減一後存回
雖然最後庫存為零,但卻一件商品兩賣了
多線程共享變量,並同時對其變更時,就會產生線程安全問題
臨界區
多個線程訪問共享資源的這段代碼稱為臨界區
臨界區只允許一個線程進入執行
解決線程安全問題的主要方法是使用 "鎖"
將臨界區的代碼加鎖
獲得鎖的線程才能執行臨界區代碼
沒有獲得鎖的線程必須等待鎖的釋放
鎖被執行臨界去的現成釋放後,其他等待這個鎖的線程再爭奪這把鎖
阻塞(等待)導致高併發系統崩潰
鎖 (I/O) 引起線程阻塞
阻塞導致線程不能執行也不能釋放資源
進而導致資源耗盡
線程也佔用系統資源
最終系統崩潰
避免阻塞引起的崩潰
限流
控制進入計算機的請求數,進而減少創建的線程數
但沒進入計算機的請求,對用戶來說也是系統不可用
部分用戶不可用
降級
關閉部分功能程序的運行,進早釋放線程
關閉有問題的功能執行
反應式
異步
無臨界區 (Actor 模型)
通過消息傳遞的方式
鎖原語 CAS 與各類鎖
鎖原語 CAS
原語: Primitive
CAS : Compare And Set
鎖本身也有線程安全問題須考慮
先讀: 先看鎖有沒有被鎖定
再寫: 如果沒有鎖定,則將鎖鎖定
在多線程的系統,只要發生先讀再寫都會發生線程安全問題
需要從 CPU 指令的 level 來看
而不僅僅由是不是一個高階語言的敘述來看
通常簡單的 i = i + 1也會有讀寫兩個動作
讀 : 由內存讀取 i
寫 : 將 1 寫回內存
CAS (V, E, N)
V : 需要更新的變量
E : 預期值 (讀取時的舊值)
N : 需要設定的新值
在設定新值時
若 V 的值還是等於在讀取時的舊值 E,則設定 V 為 N 值
若 V 的值不等於舊值 E ,則甚麼都不做
CAS 是一種鎖原語
由硬體或操作系統提供
在執行 CAS的過程中,不會被中斷
CAS 的兩個動作 compare and set 中間不會被中斷
中間不會發生線程切換
透過CAS,可以保證鎖的線程安全
當兩個線程同時讀到鎖沒有被鎖定 (獲取相同的 E)
其中線程 A 先執行 CAS
讀(compare)與寫(set)不能被中斷
執行後 V == N
線程 A 進入臨界區
另一個線程 B 再執行 CAS時
此時V != E,所以不做任何事 (執行不成功)
線程 B 沒有獲得鎖,所以沒有進入臨界區
從而保證僅有線程 A 進入臨界區
問題
無法解決 ABA 問題
原本 A 被改為 B,又改回 A
其他線程並不知道有ABA的改變
解決
再加一個 timestamp,同時滿足 V == E及timestamp才能獲得鎖
Java 的 CAS 原語實例
使用 synchronize 或 lock 變量加鎖
鎖的狀態可能為
正常
沒有鎖
偏向鎖 (biased lock)
一段臨界區被一個線程訪問,該線程由正常狀態,自動獲取鎖,轉程偏向鎖狀態
輕量級鎖
當鎖是偏向鎖時,又被另外一個或多個線程訪問,偏向鎖變升級程輕量級鎖
這些線程通過 CAS 自旋(spin)的形式嘗試獲得鎖
不停的執行 CAS
自旋的線程不會阻塞
重量級鎖
當線程自旋達到一定次數時,還沒獲得鎖,則再升級程重量級鎖
此時線程會阻塞,並加入鎖等待隊列中
當其他線程釋放鎖後,系統會從對列中喚醒一個線程獲得鎖,執行臨界區代碼
Java 對象中由 Mark Word 紀錄目前鎖的狀態
透過 CAS 改變 Mark Word 的值,改變鎖的狀態
mark word 在對象中僅有一個,所以對象的不同方法或對象本身都同時互斥
若 對象有 f(...),g(...)方法都是synchronize,則共用同一個鎖
有一個線程進入 f(...)方法,其他線程無法進入 g(...) 方法
多個方法間也都是臨界區
多 CPU 情況下的鎖
CAS 作用於單 CPU 核
單一 CPU 內的 compare and set 兩個動作不可中斷
解決多 CPU 的原子操作
總線鎖
使用處理器 LOCK# 信號
一個處理器在內存總線上發出此信號時,該處理器獨佔內存
其他處理器對內存的請求將被阻塞
可保證鎖的完整性
代價很高,一個鎖的獲取,導致所有的 CPU 都無法訪問內存
緩存鎖
先將鎖操作寫入緩存(Cache)中
當從緩存寫回內存時,通過緩存一致性協議,保證寫入的原子性
緩存一致性協議會阻止兩個處理器不同緩存修改相同的內存區域數據
當一個被鎖定的緩存已寫入內存,另外一個處理器要回寫已被鎖定的緩存數據時,會無效
不會阻塞其他處理器的請求
其他鎖的概念
公平鎖與非公平鎖
公平鎖
多個線程按照申請鎖的順序來獲取鎖
如 Java 中的重量級鎖
非公平鎖
由多個線程隨機同時爭取,無順序性
可能有些線程會爭取不到鎖,造成飢餓現象
如 Java 中的輕量級鎖 (CAS 自旋)
可重入鎖
當某線程已經獲得某個鎖後,再去申請相同的鎖一次,而不會出現死鎖
Example
synchornize 方法 A 中再調用 synchornize 方法 B
synchornize 方法 A 遞迴調用
獨占性分類
獨享鎖/互斥鎖
該鎖一次只能被一個線程持有
共享鎖
該鎖可被多個線程持有
通常設有最大允許的共享線程數
讀寫鎖
多個讀線程之間不互斥
寫線程則要求與任何線程互斥
有寫時,不能讀,也不能寫
悲觀鎖 / 樂觀鎖
悲觀鎖
對併發的操作,一律加鎖之後再行操作
樂觀鎖
當併發的操作中,在真的需要修改數據時,才去進行相關的鎖操作
分段鎖
細化鎖的粒度,將數據分段
每一段各自有自己的鎖,修改不同段的數據不會互相影響
如 JDK 的 ConcurrentHashMap
自旋鎖
採用循環的方式去嘗試獲得鎖
好處是減少線程上下文的切換消耗
壞處是循環會消耗 CPU
異步併發分布式編程框架 akka
Akka 編程框架提供
Actor 編程模型
實現多線程併發
不使用鎖、不會產生阻塞、等待
異步編程
支持分布式
Akka Vision
Simpler concurrency (scale up)
更簡單的實現垂直伸縮
更好地利用 CPU 核心
scale up
Processes (even less **)
Threads (4096 / 1GB)
1GB 內存約可以啟動 4096 個 threads
Actors (2.7 million / 1GB)
actors are small
actor 也是一個併發的基本單位
actor 更輕量、靈活,更容易實現併發
actor 更容易創建和消耗
Simpler distribution (scale out)
更簡單的實現水平伸縮
Simpler fault-tolerance (self healing)
更簡單的容錯能力
可以自癒
All of that with a single unified programming model
Akka toolkit
Run on the JVM
can be used from Java and Scala language
can be integrated with common infrastructure
e.g. Spring, etc
Akka Core concept: Actor
Cal Hewitt (1973) : Fundamental unit of computation
Behavior - react on messages it receives
每一次只處理一個訊息
State - shielded from the rest of the word, no need for synchronization
僅改變自身 Actor的內部狀態,不會修改共享的狀態
不需要進行加鎖、同步
不會產生阻塞
Communication - interact with other actors exclusively via message
除了互相發送訊息外,不互相交互
Sender (另外一個 Actor) 發送訊息給 Actor
呼叫 ActorRef 的函式
呼叫完即返回,無需等待 Actor 將其處理完
ActorRef (Actor的引用) 的函式
將剛剛的呼叫包裝成 message
發送給相對應 Actor 的Mailbox
隨即返回
Dispatcher
是系統的線程池,監控所有Actor
若Actor 的mailbox有message 需要處理
Dispatcher 由 mailbox 取出 message
喚醒相對應的 actor 處理
Actor
編寫相對應的函式處理
若Sender 需要知道處理的結果,則 Actor在發一個結果訊息給 Sender 即可
Message
Receive Message
Hello 繼承 Actor 父類
編寫 onReceive(...) 方法接收 message
Send Message
Send 僅需擁有 Hello 的 Actor 引用 (ActorRef)
呼叫 hello.tell(...)時,會將 message 發送至 hello 的 mailbox
發送完後,立即返回
ActorRef Path
Local
Akka://Master/user/master
所有繼承關係形成path
可用於垂直伸縮
Remote
Akka.tcp://Master@sr148:2052/user/master
遠程部署
支持分布式,可用於水平伸縮
Akka ActorSystem
ActorSystem 有自己的根 (root Guardian)
繼承關係形成樹狀結構
上級(父類) Actor 也是 下級 (子類) Actor的監控者
上級處理下級出現的各種異常
Create ActorSystem
創建一個根
Create a top-level actor (父actor)
再利用 system 創建一個 actor
ActorRef
通過路徑的方式創建
也可以直接 new 創建
Create a child actor
由當前的父 actor,創建子 actor
Handle Message
可以將命令分發給父 actor
父 actor 在交給自己若干的子 actor 操作
子 actor 亦可以在往下一級 子actor 發送
Router for cluster
router 相當於負載均衡器
創建多個相同的 actor 組成集群
這些 actor 處理相同的消息,執行相同的處理邏輯
如負載均衡般,均勻的分法給集群中的 actor
Embrace Failure
Supervision
Like in real life, parents supervise their children (manage children's failure)
depending on the parent's supervisor strategy
failing actor can get stopped, restarted or resumed
Benefits
you don't have to deal with concurrency detail
不用處理異步阻塞、等待、鎖各種問題
僅需設計好 actor 與 actor 之間的消息
yu can manage failures easily
Distribution is just a deployment decision
設計好 Actor,遠程啟動即可
僅僅只是一個部署決策
該在本地部署,還是遠端部署
Example
Bar Service
Spec
drinks : Akkarita, MaiPlay, PinaScalada
actors : guests, waiters, head waiter, barkeepers
messages : order, drink served, complaint, etc.
failures : guest drunk, waiter frustrated
Message flow
Akka 在金融業的流程
流程圖天然就可以變成 Actor 之間訊息發送和接收的一個設計
版权声明: 本文为 InfoQ 作者【Panda】的原创文章。
原文链接:【http://xie.infoq.cn/article/f2304b1d2ab9912a3f7ec45e0】。未经作者许可,禁止转载。
评论