軟件開發的三高指標:高並發、高性能、高可用。
高並發方面要求QPS 大於 10萬;高性能方面要求請求延遲小於 100 ms;高可用方面要高於 99.99%(4個9)
一、高並發:
高並發是現在互聯網分布式框架設計必須要考慮的因素之一,它是可以保證系統能同時並發處理很多請求,對於高並發來說,它的指標有:
1、響應時間:系統對進來的請求反應的時間,比如你打開一個頁面需要1秒,那么這1秒就是響應時間
2、吞吐量:吞吐量指每秒能處理多少請求數量
3、每秒查詢率(QPS,Queries Per Second):每秒響應請求數,和吞吐量差不多
4、並發用戶數:同時承載正常使用系統功能的用戶數量。例如一個即時通訊系統,同時在線量一定程度上代表了系統的並發用戶數
提高QPS的架構策略
Redis、MQ、多線程
1、負載均衡:
高並發首選方案就是集群化部署,一台服務器承載的QPS有限,多台服務器疊加效果就會有明顯提升。
集群化部署,就需要考慮如何將流量轉發到服務器集群,這里就需要用到負載均衡,如LVS(Linux Virtual Server)和nginx。
常用的負載均衡算法有輪詢法、隨機法、源地址哈希法、加權輪詢法、加權隨機法、最小連接法等。
業務實戰:對於千萬級流量的秒殺業務,一台LVS扛不住流量洪峰,通常需要 10 台左右,其上面用DDNS(Dynamic DNS)做域名解析負載均衡。搭配高性能網卡,單台LVS能夠提供百萬以上並發能力。
注意, LVS 負責網絡四層協議轉發,無法按 HTTP 協議中的請求路徑做負載均衡,所以還需要 Nginx
2、池化技術:
復用單個連接無法承載高並發,如果每次請求都新建連接、關閉連接,考慮到TCP的三次握手、四次揮手,需要花費大量開銷。
池化技術的核心是資源的“預分配”和“循環利用”,常用的池化技術有線程池、連接池、進程池、對象池、內存池、協程池。
連接池的幾個重要參數:最小連接數、空閑連接數、最大連接數
3、流量漏斗(風控攔截):
以上的幾種方式是正向方式提升系統QPS,我們也可以逆向思維,做減法,攔截非法請求,將核心能力留給正常業務請求。
互聯網高並發流量並不都是純凈的,也有很多惡意流量(比如黑客攻擊、惡意爬蟲、黃牛、秒殺器等),我們需要設計流量攔截器,將哪些非法的、無資格的、低優先級的流量過濾掉(風控掉),減輕系統的並發壓力。
攔截器分層:
網關和 WAF(Web Application Firewall,Web 應用防火牆)
采用封禁攻擊者來源 IP、拒絕帶有非法參數的請求、按來源 IP 限流、按用戶 ID 限流等方法
風控分析。借助大數據能力分析訂單等歷史業務數據,對同ip多個賬號下單、或者下單后支付時間過快等行為有效識別,並給賬號打標記,提供給業務團隊使用
下游的每個tomcat實例應用本地內存緩存化,將一些庫存存儲在本地一份,做前置校驗。當然,為了盡量保持數據的一致性,有定時任務,從 Redis 中定時拉取最新的庫存數據,並更新到本地內存緩存中
4、直接讀寫緩存,不可直接讀寫關系型數據庫
MySQL即使使用分庫分表,讀寫分離,完美的連接池配置等也無法抵擋qps大於10W帶來的沖擊。
我們必須使用內存緩存,緩存預熱讀多級緩存(JVM緩存,其次Redis),寫消息隊列最后寫入MySQL
5、多級緩存
Redis目前是緩存的第一首選.單機可達6-8萬的qps,在面對高並發的情況下,我們可以手動的水平擴容,以達到應對qps可能無線增長的場景。但是這種做法也存在弊端,因為redis是單線程的,並且會存在熱點問題
雖然redis內部用crc16算法做了hash打散,但是同一個key還是會落到一個單獨的機器上,就會使機器的負載增加,redis典型的存在緩存擊穿和緩存穿透兩個問題,尤其在秒殺這個場景中,如果要解決熱點問題,就變的比較棘手
這個時候多級緩存就必須要考慮了,典型的在秒殺的場景中,單sku商品在售賣開始的瞬間,qps會急劇上升.而我們這時候需要用memeryCache來擋一層,memeryCache是多線程的,比redis擁有更好的並發能力,並且它是天然可以解決熱點問題的。有了memeryCache,我們還需要localCache,本地緩存,這是一種以內存換速度的方式。本地緩存會接入用戶的第一層請求,如果它找不到,接下來走memeryCache,然后走redis,這套流程下來可以擋住百萬的qps
6、多線程
多線程並發處理提高處理速度,CountDownLatch
7、優化IO
如將多次單個的請求,優化為一次批量請求,減少網絡IO
對應MySQL就是批量插入,批量查詢
因為每次建立連接,數據交互,釋放連接都會消耗大量的資源,同時涉及到用戶態到核心態的切換
8、優雅打印日志
高並發情況下,日志打印不當會占用程序的IO,增加響應耗時。如果日志量過大,會導致磁盤打滿
①:異步打印日志,控制日志輸出的長度
②:基於白名單的日志打印,線上配置了白名單用戶請求才打印日志,避免大量的無效日志輸出
其他:
機器擴容:
大流量到來之前,對服務機器進行擴容,分化消化流量。
如Redis緩存單機可達6-8W的qps,在高並發到來之前,可以手動或配置自動伸縮擴容,以達到應對qps可能無限增長的場景。
高並發服務發散:
假設qps為10W,每個請求讀寫數據為1KB,那么10W個請求每秒鍾讀寫就達到1GB,1分鍾則60GB,這對於底層的數據存儲與訪問都是巨大的壓力。
二、高性能:
高性能指程序處理速度非常快,所占內存少,且CPU占用率低。高性能的指標經常和高並發的指標緊密相關,想要提升性能,那么就要提高系統並發能力,兩者互相捆綁在一起。
高性能指標要求:
指標 | 請求耗時 |
P50 | 50ms |
P75 | 75ms |
P90 | 90ms |
P99(百分之99的請求) | 100ms |
P50: 即中位數值。100個請求按照響應時間從小到大排列,位置為50的值,即為P50值
P95:響應耗時從小到大排列,順序處於95%位置的值即為P95值
有哪些因素會影響系統的性能?
業務代碼的邏輯設計,算法實現是否高效、架構設計
業務系統CPU、內存、磁盤等性能
下游系統的性能
業務鏈路的長度
請求/響應數據包大小
用戶網絡環境
怎么樣提高性能呢?
1、避免因為IO阻塞讓CPU閑置,導致CPU的浪費。
當系統處理大量磁盤IO操作的時候,由於CPU和內存的速度遠高於磁盤,可能導致CPU耗費太多時間等待磁盤返回處理的結果。對於這部分在IO上的開銷,稱為"iowait"。
磁盤有個性能指標:IOPS,即每秒讀寫次數,性能較好的固態硬盤,IOPS 大概在 3 萬左右。對於秒殺系統,如果單節點QPS在10萬,每次請求產生3條日志,那么日志的寫入QPS在 30W/s,磁盤根本扛不住
Linux 有一種特殊的文件系統:tmpfs(臨時文件系統),它是一種基於內存的文件系統,由操作系統管理。當我們寫磁盤的時候實際是寫到內存中,當日志文件達到我們的設置閾值,操作系統會將日志寫到磁盤中,並將tmpfs中的日志文件刪除
這種批量化、順序寫,大大提升了磁盤的吞吐性能!
2、避免多線程間增加鎖來保證同步,導致並行系統串行化
3、避免創建、銷毀、維護太多進程、線程,導致操作系統浪費資源在調度上
4、高性能緩存,如Redis。
對熱點數據從緩存中讀取來提升熱點數據的訪問性能,避免熱點數據每次都從數據庫中讀取,給數據庫帶來壓力
1、無鎖化
大多數情況下,多線程處理處理可以提高並發性能
1):串行無鎖
無鎖串行最簡單的實現方式可能就是單線程模型了,如 redis/Nginx 都采用了這種方式。
網絡編程模型中,主線程負責處理IO事件,當主線程MainReactor accept一個新連接之后從眾多的SubReactor選取一個進行注冊,通過創建一個Channel與IO線程進行綁定,此后該連接的讀寫都在同一個線程執行,無需進行同步
主從Reactor職責鏈模型:
2):結構無鎖
利用硬件支持的原子操作可以實現無鎖的數據結構,如CAS原子操作
2、零拷貝
3、序列化
當將數據寫入文件、發送到網絡時通常需要序列化技術,從其讀取時需要進行反序列化。
序列化作為傳輸數據的表示形式,與網絡框架和通信協議是解耦的。如網絡框架 taf 支持 jce、json 和自定義序列化,HTTP 協議支持 XML、JSON 和流媒體傳輸等
1)序列化分類
①:內置類型
指編程語言內置支持的類型,如 java 的 java.io.Serializable。這種類型由於與語言綁定,不具有通用性,而且一般性能不佳,一般只在局部范圍內使用
②:文本類型
一般是標准化的文本格式,如 XML、JSON。這種類型可讀性較好,且支持跨平台,具有廣泛的應用。主要缺點是比較臃腫,網絡傳輸占用帶寬大
③:二進制類型
采用二進制編碼,數據組織更加緊湊,支持多語言和多平台。常見的有 Protocol Buffer/Thrift/MessagePack/FlatBuffer 等
2)性能指標
衡量序列化/反序列化主要有三個指標:
①:序列化之后的字節大小;
②:序列化/反序列化的速度;
③:CPU 和內存消耗
其中性能最好的是FlatBuffer,其次是Protobuf
3)選型考量
①:性能
CPU 和字節占用大小是序列化的主要開銷。在基礎的 RPC 通信、存儲系統和高並發業務上應該選擇高性能高壓縮的二進制序列化。一些內部服務、請求較少 Web 的應用可以采用文本的 JSON,瀏覽器直接內置支持 JSON
②:易用性
豐富數據結構和輔助工具能提高易用性,減少業務代碼的開發量。現在很多序列化框架都支持 List、Map 等多種結構和可讀的打印
③:通用性
現代的服務往往涉及多語言、多平台,能否支持跨平台跨語言的互通是序列化選型的基本條件
④:兼容性
現代的服務都是快速迭代和升級,一個好的序列化框架應該有良好的向前兼容性,支持字段的增減和修改等
⑤:擴展性
序列化框架能否低門檻的支持自定義的格式有時候也是一個比較重要的考慮因素
4、池化
其本質是通過創建池子提高對象復用,減少重復創建、銷毀的開銷。
常見的池化技術有內存池、線程池、連接池、對象池等
1)內存池
我們都知道,在 C/C++中分別使用 malloc/free 和 new/delete 進行內存的分配,其底層調用系統調用 sbrk/brk。頻繁的調用系統調用分配釋放內存不但影響性能還容易造成內存碎片,內存池技術旨在解決這些問題。正是這些原因,C/C++中的內存操作並不是直接調用系統調用,而是已經實現了自己的一套內存管理。
malloc 的實現主要有三大實現。
①、ptmalloc:glibc 的實現。
②、tcmalloc:Google 的實現。
③、jemalloc:Facebook 的實現。
tcmalloc和jemalloc性能差不多,ptmalloc性能不如兩者,redis和mysql可以指定使用哪個malloc,我們可以根據需要選擇更適合的malloc。
內存管理的三個層次:
2)線程池
線程池使應用能更加充分利用CPU、內存、網絡、IO等系統資源。限制線程的創建數量並復用已創建的線程,從而提高系統的性能。
線程的創建需要開辟虛擬機棧、本地方法棧、程序計數器等線程私有的內存空間
線程的銷毀時需要回收這些系統資源。因此頻繁的創建和銷毀線程會浪費大量的系統資源,增加並發編程風險。
另外,在服務器負載過大的時候,如何讓新的線程等待或者友好地拒絕服務?這些都是線程本身無法解決的。所以需要通過線程池協調多個線程,並實現類似主次線程隔離、定時執行、周期執行等任務。線程池的作用包括:
①:利用線程池管理並復用線程、控制最大並發數等
②:實現任務線程隊列緩存策略和拒絕機制
③:實現某些與時間相關的功能,如定時執行、周期執行
④:隔離線程環境(分類或者分組)
分組:通過配置兩個或多個線程池,不同的任務使用不同的線程池,如較慢的任務與其他任務分隔開,避免任務間互相影響
分類:可以分為核心和非核心,核心線程池一直存在不會被回收,非核心可能對空閑一段時間后的線程進行回收,從而節省系統資源,等到需要時在按需創建放入池子中
3)連接池
常見的連接池有數據庫連接池、Redis連接池、TCP連接池等。
其主要目的是通過復用連接來減少創建和釋放連接的開銷。連接池實現通常需要考慮以下幾個問題:
①:初始化時機
啟動即初始化或惰性初始化,通常使用啟動即初始化的方式
啟動初始化可以減少一些加鎖操作和需要時可以直接使用,缺點是可能造成服務啟動緩慢或者啟動后沒有任務處理,造成資源浪費
惰性初始化是使用的時候再去創建,這種方式可能有助於減少資源占用,但是面對突發的任務請求,然后瞬間去創建一堆連接,可能會造成系統響應慢甚至響應失敗。
②:連接數目
權衡所需的連接數,連接數太少則可能造成任務處理緩慢,太多不但使任務處理慢還會過度消耗系統資源
③:連接取出
當連接池已經無可用連接時,是一直等待直到有可用連接還是分配一個新的臨時連接
④:連接歸還
當連接使用完畢且連接池未滿時,將連接放入連接池(包括 3 中創建的臨時連接),否則關閉
⑤:連接有效性檢測
長時間空閑連接和失效連接需要關閉並從連接池移除。常用的檢測方法有:使用時檢測和定期檢測
4)對象池
嚴格來說,各種池都是對象池模式的應用。
對象池跟上面其他池一樣,也是緩存一些對象從而避免大量創建同一個類型的對象,同時限制了實例的個數,如:
①:Redis 中 0-9999 整數對象就通過采用對象池進行共享
②:在游戲開發中對象池模式經常使用,如進入地圖時怪物和 NPC 的出現並不是每次都是重新創建,而是從對象池中取出
③:mdm中RedisTemplate對象緩存、uvcas中TalosProducer對象緩存
public static final Map<String, RedisTemplate<String, Object>> REDIS_TEMPLATE_MAP = new ConcurrentHashMap<>(); // 當前線程本地變量 RdmContext rdmContext = RdmContext.currentContext(); RedisProperties redisProperties = rdmContext.getRedisProperties(); String token = rdmContext.getToken(); // 看是否有緩存的redisTemplate RedisTemplate<String, Object> redisTemplate = RdmCache.REDIS_TEMPLATE_MAP.get(token); if (redisTemplate != null) { rdmContext.setRedisTemplate(redisTemplate); return; } // 創建RedisTEmplate並緩存 // 緩存起來 RdmCache.REDIS_TEMPLATE_MAP.put(token, redisTemplate);
5、並發化
1)請求並發
如果一個任務需要處理多個子任務,可以將沒有依賴關系的子任務並發化,這種場景在后台開發很常見。如一個請求需要查詢 3 個數據,分別耗時 T1、T2、T3,如果串行調用總耗時 T=T1+T2+T3。對三個任務執行並發,總耗時 T=max(T1,T 2,T3)。同理,寫操作也如此。對於同種請求,還可以同時進行批量合並,減少 RPC 調用次數
2)冗余請求
冗余請求指的是同時向后端服務發送多個同樣的請求,誰響應快就是使用誰,其他的則丟棄。這種策略縮短了客戶端的等待時間,但也使整個系統調用量猛增,一般適用於初始化或者請求少的場景
6、異步化
對於處理耗時長的任務,如果采用同步等待的方式,會嚴重降低系統的吞吐量,可以采用異步化進行解決。
1)調用異步化
在進行一個耗時的RPC調用或者任務處理時,常用的異步化方式如下:
①:Callback
異步回調通過注冊一個回調函數,然后發起異步任務,當任務執行完畢時會回調用戶注冊的回調函數,從而減少調用端等待時間。這種方式會造成代碼分散難以維護,定位問題也相對困難
②:Future
當用戶提交一個任務時會立刻先返回一個 Future,然后任務異步執行,后續可以通過 Future 獲取執行結果
//異步並發任務 Future<Response> f1 = Executor.submit(query1); //處理其他事情 doSomething(); //獲取結果 Response res1 = f1.getResult();
③:CPS
(Continuation-passing style)可以對多個異步編程進行編排,組成更復雜的異步處理,並以同步的代碼調用形式實現異步效果
CPS 將后續的處理邏輯當作參數傳遞給 Then 並可以最終捕獲異常,解決了異步回調代碼散亂和異常跟蹤難的問題
Java 中的 CompletableFuture 和 C++ PPL 基本支持這一特性。典型的調用形式如下:
void handleRequest(const Request &req) { return req.Read().Then([](Buffer &inbuf){ return handleData(inbuf); }).Then([](Buffer &outbuf){ return handleWrite(outbuf); }).Finally(){ return cleanUp(); }); }
2)流程異步化
一個業務流程往往伴隨着調用鏈路長、后置依賴多等特點,這會同時降低系統的可用性和並發處理能力
可以采用對非關鍵依賴進行異步化解決,如MQ
7、緩存
從單核 CPU 到分布式系統,從前端到后台,緩存無處不在
緩存是原始數據的一個復制集,其本質就是空間換時間,主要是為了解決高並發讀
1)緩存的使用場景
緩存是空間換時間的藝術,使用緩存能提高系統的性能。
注意不要為了所謂的提高性能不計成本的使用緩存,而是要看場景。
①:一旦生成后基本不會變化的數據
②:讀密集型或存在熱點的數據
③:計算代價大的數據
④:千人一面的數據
不適合使用緩存的場景:
①:寫多讀少,更新頻繁
②:對數據一致性要求嚴格
2)緩存的分類
①:進程級緩存
緩存的數據直接在進程地址空間內,這可能是訪問速度最快使用最簡單的緩存方式了。
主要的缺點是受制於進程空間大小,能緩存的數據量有限,進程重啟緩存數據會丟失。一般用於緩存數據量不大的場景,如JVM緩存
②:集中式緩存
緩存的數據集中在一台機器上,如共享內存。這類緩存容量主要受制於機器內存大小,而且進程重啟后數據不丟失。常用的集中式緩存中間件有單機版 redis、memcache 等
③:分布式緩存
緩存的數據分布在多台機器上,通常需要采用特定算法(如 Hash)進行數據分片,將海量的緩存數據均勻的分布在每個機器節點上。常用的組件有:Memcache(客戶端分片)、Codis(代理分片)、Redis Cluster(集群分片)
④:多級緩存
指在系統中的不同層級的進行數據緩存,以提高訪問效率和減少對后端存儲的沖擊
本地緩存:caffeine
外部緩存:Redis
3)緩存一些好的實踐
①:動靜分離
對於一個緩存對象,可能分為很多種屬性,這些屬性中有的是靜態的,有的是動態的。在緩存的時候最好采用動靜分離的方式
②:慎用大對象
如果緩存對象過大,每次讀寫開銷非常大並且可能會卡住其他請求,特別是在 redis 這種單線程的架構中。典型的情況是將一堆列表掛在某個 value 的字段上或者存儲一個沒有邊界的列表,這種情況下需要重新設計數據結構或者分割 value 再由客戶端聚合
③:過期設置
盡量設置過期時間減少臟數據和存儲占用,但要注意過期時間不能集中在某個時間段
④:超時設置
緩存作為加速數據訪問的手段,通常需要設置超時時間而且超時時間不能過長(如 100ms 左右),否則會導致整個請求超時連回源訪問的機會都沒有
⑤:緩存隔離
首先,不同的業務使用不同的 key,防止出現沖突或者互相覆蓋。其次,核心和非核心業務進行通過不同的緩存實例進行物理上的隔離
⑥:失敗降級
用緩存需要有一定的降級預案,緩存通常不是關鍵邏輯,特別是對於核心服務,如果緩存部分失效或者失敗,應該繼續回源處理,不應該直接中斷返回
⑦:容量控制
使用緩存要進行容量控制,特別是本地緩存,緩存數量太多內存緊張時會頻繁的 swap 存儲空間或 GC 操作,從而降低響應速度
⑧:業務導向
以業務為導向,不要為了緩存而緩存。對性能要求不高或請求量不大,分布式緩存甚至數據庫都足以應對時,就不需要增加本地緩存,否則可能因為引入數據節點復制和冪等處理邏輯反而得不償失
⑨:監控告警
對大對象、慢查詢、內存占用等進行監控
8、分片
分片,即將一個較大的部分分成多個較小的部分,在這里我們分為數據分片和任務分片。
對於數據分片,不同系統的拆分技術術語(如 region、shard、vnode、partition)等統稱為分片
分片可以說是一箭三雕的技術,將一個大數據集分散在更多節點上,單點的讀寫負載隨之也分散到了多個節點上,同時還提高了擴展性和可用性
數據分片,小到編程語言標准庫里的集合,大到分布式中間件,無所不在,如:
Java線程安全的ConcurrentHashMap采取分段機制,按照哈希或者取模將對象放置到某個分段中,減少鎖爭用
分布式消息中間件 Kafka 中對 topic 也分成了多個 partition,每個 partition 互相獨立可以並發讀寫
1)分片策略
進行分片時,要盡量均勻的將數據分布在所有節點上以平攤負載。
如果分布不均,會導致傾斜使得整個系統性能的下降,常見的分片策略如下:
①:區間分片
基於一段連續關鍵字的分片,保持了排序,適合進行范圍查找,減少了垮分片讀寫。區間分片的缺點是容易造成數據分布不均勻,導致熱點。如根據ID范圍進行分片
常見的還有按時間范圍分片,則最近時間段的讀寫操作通常比很久之前的時間段頻繁
②:隨機分片
按照一定的方式(如哈希取模)進行分片,這種方式數據分布比較均勻,不容易出現熱點和並發瓶頸
缺點就是失去了有序相鄰的特性,如進行范圍查詢時會向多個節點發起請求
③:組合
對區間分片和隨機分片的一種折中,采取了兩種方式的組合。通過多個鍵組成復合鍵,其中第一個鍵用於做哈希隨機,其余鍵用於進行區間排序
社交場景,如微信朋友圈、QQ 說說、微博等以用戶 id+發布時間(user_id,pub_time)的組合找到用戶某段時間的發表記錄
2)二級索引
二級索引通常用來加速特定值的查找,不能唯一標識一條記錄,使用二級索引需要兩次查找查找。
關系型數據庫和一些KV數據庫都支持二級索引,如MySQL中的非聚簇索引,ES倒排索引通過term找到文檔都是二級索引。
①:本地二級索引
二級索引存儲在與關鍵字相同的分區中,即索引和記錄在同一個分區中。
這樣對於寫操作時都在一個分區里進行,不需要跨分區操作。但是對於讀操作,需要聚合其他分區上的數據。
②:全局二級索引
按索引值本身進行分區,與關鍵字獨立。
這樣對於讀取某個索引的數據時,都在一個分區里進行,而對於寫操作,需要跨多個分區。
3)路由策略
路由策略決定如何將數據請求發送到指定的節點,包括分片調整后的路由。
路由策略通常有三種方式:客戶端路由、代理路由、集群路由
①:客戶端路由
客戶端直接操作分片邏輯,感知分片和節點的分配關系並直接連接到目標節點。
Memcache就是采用這種方式實現的分布式
②:代理層路由
客戶端的請求發送到代理層,由其轉發到對應的數據節點上。
很多分布式系統都采用了這種方式,如業界的基於redis實現的分布式存儲codis等
③:集群路由
由集群實現分片路由,客戶端連接任意節點,如果該節點存在請求的分片,則處理;否則將請求轉發到合適的節點或者告訴客戶端重定向到目標節點
如redis cluster就采用了這種方式
以上幾種方式各有優缺點,客戶端路由實現相對簡單但對業務入侵較強。
代理層路由對業務透明,但增加了一層網絡傳輸。對性能有一定影響,同時在部署上也相對復雜。
集群路由對業務透明,且比代理路由少了一層結構,節約成本,但實現更復雜,且不合理的策略會增加多次網絡傳輸
4)動態平衡
在學習平衡二叉樹和紅黑樹的時候我們都知道,由於數據的插入刪除會破壞其平衡性。為了保持樹的平衡,在插入刪除后我們會通過左旋右旋動態調整樹的高度以保持再平衡。在分布式數據存儲也同樣需要再平衡,只不過引起不平衡的因素更多了,主要有以下幾個方面:
①:讀寫負載增加,需要更多CPU
②:數據規模增加,需要更多磁盤和內存
③:數據節點故障,需要其他節點接替
業務有很多產品支持動態平衡調增,如redis cluster的resharding,HDFS/kafka的rebalance等。常見的方式如下:
①:固定分區
創建遠超節點數的分區數,為每個幾點分配多個分區。
如果新增節點,可從現有的節點上均勻移走幾個分區從而達到平衡,刪除節點反之。
②:動態分區
自動增減分區數,當分區數據增長到一定閥值時,則對進行拆分。當分區數據縮小到一定閥值時,對分區進行合並。
類似於B+樹的分裂刪除操作。很多存儲組件都采用了這種方式,如 HBase Region 的拆分合並,TDSQL 的 Set Shard。
這種方式的優點是自動適配數據量,擴展性好。使用這種分區需要注意的一點,如果初始化分區為一個,剛上線請求量就很大的話會造成單點負載高,通常采取預先初始化多個分區的方式解決,如 HBase 的預分裂。
5)分庫分表
當數據庫的單表/單機數據量很大時,會造成性能瓶頸,為了分散數據庫的壓力,提高讀寫性能,需要采取分而治之的策略進行分庫分表。通常,在以下情況下需要進行分庫分表:
①:單表的數據量達到了一定的量級(如 mysql 一般為千萬級),讀寫的性能會下降。這時索引也會很大,性能不佳,需要分解單表
②:數據庫吞吐量達到瓶頸,需要增加更多數據庫實例來分擔數據讀寫壓力
分庫分表按照特定的條件將數據分散到多個數據庫和表中,分為垂直切分和水平切分兩種模式。
①:垂直切分
按照一定規則,如業務或模塊類型,將一個數據庫中的多個表分布到不同的數據庫上
優點:
①:切分規則清晰,業務划分明確
②:可以按照業務的類型、重要程度進行成本管理,擴展也方便
③:數據維護簡單
缺點:
①:不同表分到了不同的庫中,無法使用表連接 Join。不過在實際的業務設計中,也基本不會用到 join 操作,一般都會建立映射表通過兩次查詢或者寫時構造好數據存到性能更高的存儲系統中
②:事務處理復雜,原本在事務中操作同一個庫的不同表不再支持。這時可以采用柔性事務或者其他分布式事物方案
②:水平切分
按照一定規則,如哈希或取模,將同一個表中的數據拆分到多個數據庫上
可以簡單理解為按行拆分,拆分后的表結構是一樣的
優點:
①:切分后表結構一樣,業務代碼不需要改動
②:能控制單表數據量,有利於性能提升
缺點:
①:join、count、記錄合並、排序、分頁等問題需要跨節點處理
②:相對復雜,需要實現路由策略;
綜上所述,垂直切分和水平切分各有優缺點,通常情況下這兩種模式會一起使用。
6)任務分片
任務分片將一個任務分成多個子任務並行處理,加速任務的執行,通常涉及到數據分片,如歸並排序首先將數據分成多個子序列,先對每個子序列排序,最終合成一個有序序列。
在大數據處理中,Map/Reduce 就是數據分片和任務分片的經典結合
9、存儲
1)讀寫分離
大多數業務都是讀多寫少,為了提高系統處理能力(因為寫時會加鎖無法讀),可以采用讀寫分離的方式將主節點用於寫,從節點用於讀
讀寫分離架構有以下幾個特點:
①:數據庫服務為主從架構,可以為一主一從或者一主多從
②:主節點負責寫操作,從節點負責讀操作
③:主節點將數據復制到從節點;基於基本架構,可以變種出多種讀寫分離的架構,如主-主-從、主-從-從。主從節點也可以是不同的存儲,如 mysql+redis
讀寫分離的主從架構一般采用異步復制,會存在數據復制延遲的問題,適用於對數據一致性要求不高的業務。可采用以下幾個方式盡量避免復制滯后帶來的問題
①:寫后讀一致性
即讀自己的寫,適用於用戶寫操作后要求實時看到更新。典型的場景是,用戶注冊賬號或者修改賬戶密碼后,緊接着登錄,此時如果讀請求發送到從節點,由於數據可能還沒同步完成,用戶登錄失敗,這是不可接受的。針對這種情況,可以將自己的讀請求發送到主節點上,查看其他用戶信息的請求依然發送到從節點
②:二次讀取
優先讀取從節點,如果讀取失敗或者跟蹤的更新時間小於某個閥值,則再從主節點讀取
③:關鍵業務讀寫主節點,非關鍵業務讀寫分離
④:單調讀
保證用戶的讀請求都發到同一個從節點,避免出現回滾的現象。如用戶在 M 主節點更新信息后,數據很快同步到了從節點 S1,用戶查詢時請求發往 S1,看到了更新的信息。接着用戶再一次查詢,此時請求發到數據同步沒有完成的從節點 S2,用戶看到的現象是剛才的更新的信息又消失了,即以為數據回滾了
2)動靜分離
動靜分離將經常更新的數據和更新頻率低的數據進行分離。最常見於 CDN,一個網頁通常分為靜態資源(圖片/js/css 等)和動態資源(JSP、PHP 等),采取動靜分離的方式將靜態資源緩存在 CDN 邊緣節點上,只需請求動態資源即可,減少網絡傳輸和服務負載
在數據庫和 KV 存儲上也可以采取動態分離的方式:
①:在緩存中,將一個緩存對象中的靜態字段和動態字段分開緩存
②:在數據庫中,動靜分離更像是一種垂直切分,將動態和靜態的字段分別存儲在不同的庫表中,減小數據庫鎖的粒度,同時可以分配不同的數據庫資源來合理提升利用率
3)冷熱分離
冷熱分離可以說是每個存儲產品和海量業務的必備功能,Mysql、ElasticSearch、CMEM、Grocery 等都直接或間接支持冷熱分離
將熱數據放到性能更好的存儲設備上,冷數據下沉到廉價的磁盤,從而節約成本。
如保留7天的熱數據,超過7天的數據任務其為冷數據進行遷移
4)重寫輕讀
①:關鍵寫,降低讀的關鍵性,如異步復制,保證主節點寫成功即可,從節點的讀可容忍同步延遲
②:寫重邏輯,讀輕邏輯,將計算的邏輯從讀轉移到寫。適用於讀請求的時候還要進行計算的場景,常見的如排行榜是在寫的時候構建而不是在讀請求的時候再構建
5)數據異構
數據異構主要是按照不同的維度建立索引關系以加速查詢
如京東、天貓等網上商城,一般按照訂單號進行了分庫分表。由於訂單號不在同一個表中,要查詢一個買家或者商家的訂單列表,就需要查詢所有分庫然后進行數據聚合
可以采取構建異構索引,在生成訂單的時同時創建買家和商家到訂單的索引表,這個表可以按照用戶 id 進行分庫分表
10、隊列
在系統應用中,不是所有的任務和請求必須實時處理,很多時候數據也不需要強一致性而只需保持最終一致性,有時候我們也不需要知道系統模塊間的依賴,在這些場景下隊列技術大有可為
1)應用場景
①:異步處理
業務請求的處理流程通常很多,有些流程並不需要在本次請求中立即處理,這時就可以采用異步處理
②:流量削峰
高並發系統的性能瓶頸一般在 I/O 操作上,如讀寫數據庫。面對突發的流量,可以使用消息隊列進行排隊緩沖
③:系統解耦
解決系統之間的強調用關系,去除阻塞調用,改為異步消息
④:數據同步
消息隊列可以起到數據總線的作用,特別是在跨系統進行數據同步時。如通過 RabbitMQ 在寫 Mysql 時將數據同步到 Redis,從而實現一個最終一致性的分布式緩存
⑤:柔性事務
傳統的分布式事務采用兩階段協議或者其優化變種實現,當事務執行時都需要爭搶鎖資源和等待,在高並發場景下會嚴重降低系統的性能和吞吐量,甚至出現死鎖
互聯網的核心是高並發和高可用,一般將傳統的事務問題轉換為柔性事務
柔性事務的核心流程為:
1):分布式事務發起方在執行第一個本地事務前,向 MQ 發送一條事務消息並保存到MQ服務端,MQ 消費者無法感知和消費該消息 ①②
2):事務消息發送成功后開始進行單機事務操作 ③
a. 如果本地事務執行成功,則將 MQ 服務端的事務消息更新為正常狀態 ④
b.如果本地事務執行時因為宕機或者網絡問題沒有及時向 MQ 服務端反饋,則之前的事務消息會一直保存在 MQ。MQ 服務端會對事務消息進行定期掃描,如果發現有消息保存時間超過了一定的時間閥值,則向 MQ 生產端發送檢查事務執行狀態的請求 ⑤
c.檢查本地事務結果后 ⑥,如果事務執行成功,則將之前保存的事務消息更新為正常狀態,否則告知 MQ 服務端進行丟棄
3):消費者獲取到事務消息設置為正常狀態后,則執行第二個本地事務 ⑧。如果執行失敗則通知 MQ 發送方對第一個本地事務進行回滾或正向補償
2)應用分類
①:緩沖隊列
隊列的基本功能就是緩沖排隊,如 TCP 的發送緩沖區,網絡框架通常還會再加上應用層的緩沖區。使用緩沖隊列應對突發流量時,使處理更加平滑,從而保護系統
在大數據日志系統中,通常需要在日志采集系統和日志解析系統之間增加日志緩沖隊列,以防止解析系統高負載時阻塞采集系統甚至造成日志丟棄,同時便於各自升級維護。如數據采集系統中,采用 Kafka 作為日志緩沖隊列
②:請求隊列
對用戶的請求進行排隊,網絡框架一般都有請求隊列,如 spp 在 proxy 進程和 work 進程之間有共享內存隊列,taf 在網絡線程和 Servant 線程之間也有隊列,主要用於流量控制、過載保護和超時丟棄等
③:任務隊列
將任務提交到隊列中異步執行,最常見的就是線程池的任務隊列
④:消息隊列
用於消息投遞,主要有點對點和發布訂閱兩種模式,常見的有 RabbitMQ、RocketMQ、Kafka 等
三、高可用:
可用性:指一個系統處在可用工作狀態的時間的比例
高可用:讓系統趨近於100%的高度可用
具體衡量指標:
MTBF(Mean Time Between Failure):平均故障間隔時間,平均無故障工作時間,即系統可用時長,單位為小時
MTTR(Mean Time To Repair):系統從故障到恢復正常所耗費的時間
SLA(Service-Level Agreement):服務等級協議,用於評估服務可用性等級。計算公式是 MTBF/(MTBF+MTTR)
我們常說的可用性高於99.99%(4個9),是指指標SLA高於99.99%。
可用性 | 年故障時間 | 日故障時間 |
90% (1個9) | 36.5天 | 2.4小時 |
99% (2個9) | 3.65天 | 14.4分鍾 |
99.9% (3個9) | 0.365天,8小時 | 1.44分鍾 |
99.99% (4個9) | 0.0365天,52分鍾 | 8.6秒 |
99.999% (5個9) | 0.00365天,5分鍾 | 0.86秒 |
可用性級別:
級別 |
可用性級別 |
通俗說法 |
年度停機時間 |
配套措施 |
基本可用性 |
99% |
2 個 9 |
3d-15h-39m-29s |
|
高可用性 |
99.9% |
3 個 9 |
8h-45m-56s |
大量的自動化故障工具,以及各種控制調度系統等基礎設施要做好 |
具有故障自動恢復 |
99.99% |
4 個 9 |
52m-35s |
本地多機房(像 AWS 一樣每個地方都有三個可用區) |
極高可用性 |
99.999% |
5 個 9 |
5m-15s |
遠程多機房,異地多活 |
技術架構,高可用有哪些策略:
多雲架構、異地多活、異地備份
主備切換:如Redis緩存、MySQL數據庫,主備節點會實時數據同步、備份。如果主節點不可用,自動切換到備用節點
微服務,無狀態化架構、業務集群化部署,有心跳檢測,能最短時間檢測到不可用的服務
通過熔斷、限流,解決流量過載問題,提供過載保護
重視web安全,解決攻擊和XSS問題
1、系統拆分
早前的系統都是單體系統,比如電商業務,會員、商品、訂單、物流、營銷等模塊都堆積在一個系統。每到節假日搞個大促活動,系統擴容時,一擴全擴,一掛全掛。只要一個接口出了問題,整個系統都不可用。
因此面對龐大的單體系統,我們要對其做系統拆分為微服務架構。按照DDD(領域驅動設計Domain-DrivenDesign)的思想,將一個復雜的業務拆分成若干個子系統,每個子系統負責專屬的業務功能,做好垂直化建設,各個子系統之間做好邊界隔離,降低風險蔓延。
2、解耦
軟件開發有個重要原則“高內聚、低耦合”
小到接口抽象、MVC分層,大到SOLID原則,23種設計模式。核心都是降低不同模塊間的耦合度,避免一處錯誤改動影響到整個系統
思路如,Spring AOP、事件驅動模型
3、異步
同步指一個線程在執行請求的時候,若該請求需要一段時間才能返回信息,那么這個線程將會阻塞一直等待下去,直到收到返回信息才繼續執行下去。
如果是非實時響應的動作可以采用異步來完成,線程不需要一直等待,而是繼續執行后面的邏輯
如:線程池、消息隊列
舉例:下單操作,我們關心的是訂單是否創建成功,能否進行后續的付款流程
至於其他的業務動作,如短信通知、郵件通知、生成訂單快照,超時取消任務這些非核心動作用戶並不是很關心,這些操作我們可以采用消息隊列異步執行。在下單成功在數據庫插入訂單記錄之后,發送消息到MQ,然后返回用戶成功,監聽消息的線程來完成這些操作
4、重試
重試主要體現在遠程的RPC調用,受網絡抖動、線程資源阻塞等因素影響,請求無法及時響應。
為了提升用戶體驗,調用方可以通過 重試 方式再次發送請求,嘗試獲取結果。
接口重試是一把雙刃劍,雖然客戶端收到了響應超時結果,但是我們無法確定,服務端是否已經執行完成。如果盲目地重試,可能會帶來嚴重后果。比如:銀行轉賬。
重試通常跟冪等組合使用,如果一個接口支持了 冪等,那你就可以隨便重試。
重試方案:
①:sisyphus
②:spring retry
冪等方案:
①:插入前先執行查詢操作,看是否存在,再決定是否插入。
②:增加唯一索引。
③:建防重表。
④:引入狀態機,比如付款后,訂單狀態調整為已付款,SQL 更新記錄前增加條件判斷。
⑥:增加分布式鎖。
⑦:采用 Token 機制,服務端增加 token 校驗,只有第一次請求是合法的
5、補償
通過補償,來實現數據的最終一致性
注意:補償操作有個重要前提,業務能接受短時間內的數據不一致
業務補償根據處理的方向分為兩部分:
①:正向
多個操作構成一個分布式事務,如果部分成功。部分失敗,我們會通過最大努力機制將失敗的任務推到成功狀態
②:逆向
通過反向操作,將部分成功任務恢復到初始狀態
補償實現方式:
①:本地建表方式,存儲相關數據,然后通過定時任務掃描提取,並借助反射機制觸發執行
②:也可以采用簡單的消息中間件,構建業務消息體,由下游的消費任務執行。如果失敗,可以借助 MQ 的重試機制,多次重試
6、故障轉移
故障轉移:一般指主備切換、縮短故障時間
當系統出現故障時,首要任務不是立馬查找原因,考慮到故障的復雜性,定位排查要花些時間,等問題修復好,SLA也降了好幾個檔。更好的解決方案就是:故障轉移
故障轉移:當發現故障節點的時候,不是嘗試修復它,而是立即把它隔離,同時將流量轉移到正常節點上。這樣通過故障轉移,不僅減少了MTTR提升了SLA,還為修復故障節點贏得了足夠的時間。
①:對等節點可直接轉移切換
②:節點分主備時,轉移時需要進行主備切換
如何發現故障並自動轉移?
一般采用某種故障檢測機制,比如心跳機制,備份節點定期發送心跳包,當多數節點未收到主節點的心跳包,表示主節點故障,需要進行切換
切換到哪個備節點?
一般采用paxos、raft等分布式一致性算法,在多個備份節點中選出新主節點
主備切換大致分為三步:
1)故障自動偵測(auto-detect),采用健康檢查,心跳等手段自動偵測故障節點
2)自動轉移(failover),當偵測到故障節點后,采用摘除流量、脫離集群等方式隔離故障節點,將流量轉移到正常節點
3)自動恢復(failback),當故障節點恢復正常后,自動將其加入集群中,確保集群資源與故障前一致
7、多活策略
容災備份策略並不能保證萬事大吉
在一些極端情況,如:機房斷電、機房火災、地震、山洪等不可抗力因素,所有的服務器(主、備)可能都同時出現故障,全部無法對外提供服務,導致整體業務癱瘓。
為了降低風險,保證服務的24小時可用性,我們可以采用多活策略
常見的多活方案有:同城多活、兩地三中心、三地五中心、異地雙活、異地多活等
8、隔離
隔離屬於物理層面的分割,將若干的系統低耦合設計,獨立部署,從物理上隔開。
每個子系統有自己獨立的代碼庫,獨立開發,獨立發布。一旦出現故障,也不會相互干擾。當然如果不同子系統間有相互依賴,這種情況比較特殊,需要有默認值或者異常特殊處理,這屬於業務層面解決方案。
隔離屬於分布式技術的衍生產物,我們最常見的微服務解決方案。
將一個大型的復雜系統拆分成若干個微服務系統,這些微服務子系統通常由不同的團隊開發、維護,獨立部署,服務之間通過 RPC 遠程調用。
隔離使得系統間邊界更加清晰,故障可以更加隔離開來,問題的發現與解決也更加快速,系統的可用性也更高
9、限流,提供過載保護
高並發系統,如果遇到流量洪峰,超過了當前系統的承載能力,要怎么辦
一種方案,如果照單全收,CPU、內存、Load負載飆的很高,最后處理不過來,所有請求都超時無法正常響應
另一種方案,將多余的流量舍棄掉
限流定義:
限制到達系統的並發請求數量,保證系統能夠正常響應部分用戶請求,而對於超過限制的流量,則通過拒絕服務的方式保證系統整體的可用性
限流的原理跟熔斷有點類似,都是通過判斷某個條件來確定是否執行某個策略。但是又有所區別,熔斷觸發過載保護,該節點會暫停服務,直到恢復。限流,則是只處理自己能力范圍之內的請求,超量的請求會被限流
根據作用范圍:限流分為單機版限流、分布式限流
1、單機版限流
主要借助於本機內存來實現計數器,比如通過 AtomicLong#incrementAndGet(),但是要注意之前不用的 key 定期做清理,釋放內存
純內存實現,無需和其他節點統計匯總,性能最高。但是優點也是缺點,無法做到全局統一化的限流
2、分布式限流
單機版限流僅能保護自身節點,但無法保護應用依賴的各種服務,並且在進行節點擴容、縮容時也無法准確控制整個服務的請求限制。
而分布式限流,以集群為維度,可以方便的控制這個集群的請求限制,從而保護下游依賴的各種服務資源
限流支持的多個維度:
①:整個系統一定時間內(比如每分鍾)處理多少請求
②:單個接口一定時間內處理多少流量
③:單個 IP、城市、渠道、設備 id、用戶 id 等在一定時間內發送的請求數
④:如果是開放平台,則為每個 appkey 設置獨立的訪問速率規則
限流算法主要有:
計數器限流、滑動窗口限流、令牌桶限流、漏桶限流
10、熔斷,提供過載保護
所謂過載保護,是指負載超過系統的承載能力時,系統會自動采取保護措施,確保自身不被壓垮。
熔斷,其實是對調用鏈路中某個資源出現不穩定狀態時(如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其他的資源而導致聯機錯誤。
例子:熔斷觸發條件往往跟系統節點的承載能力和服務質量有關,比如 CPU 的使用率超過 90%,請求錯誤率超過 5%,請求延遲超過 500ms, 它們中的任意一個滿足條件就會出現熔斷。
熔斷的主要方式是使用斷路器阻斷故障服務器的調用。
斷路器有三種狀態:關閉、打開、半打開
①:關閉(Closed)狀態:在這個狀態下,請求都會被轉發給后端服務。同時會記錄請求失敗的次數,當請求失敗次數在一段時間超過一定次數就會進入打開狀態
②:打開(Open)狀態:在這個狀態下,熔斷器會直接拒絕請求,返回錯誤,而不去調用后端服務。同時,會有一個定時器,時間到的時候會變成半打開狀態。目的是假設服務會在一段時間內恢復正常
③:半打開(Half Open)狀態:在這個狀態下,熔斷器會嘗試把部分請求轉發給后端服務,目的是為了探測后端服務是否恢復。如果請求失敗會進入打開狀態,成功情況下會進入關閉狀態,同時重置計數
11、降級
降級是系統保護的一種重要手段
為了使有限資源發揮最大價值,我們會臨時關閉一些非核心功能,減輕系統壓力,並將有限資源留給核心業務
比如電商大促,業務在峰值時刻,系統抵擋不住全部的流量時,系統的負載、CPU 的使用率都超過了預警水位,可以對一些非核心的功能進行降級,降低系統壓力,比如把商品評價、成交記錄等功能臨時關掉。棄車保帥,保證 創建訂單、支付 等核心功能的正常使用
總結下來:降級是通過暫時關閉某些非核心服務或者組件從而保護核心系統的可用性。
12、超時控制
在分布式環境下,服務響應慢可能比宕機危害更大,失敗只是暫時的,但調用延遲會導致占用的資源得不到釋放,在高並發情況下會造成整個系統崩潰
如何合理設置超時時間?
收集系統之間的調用日志,統計比如說 99% 的響應時間是怎樣的,然后依據這個時間來指定超時時間
超時處理策略?
①:服務超時釋放資源,響應失敗
如數據庫配置超時時間,超時則終止操作,釋放資源
jdbc配置:
connectTimeout:表示等待和MySQL建立socket連接的超時時間,默認值0,表示不超時,單位毫秒,建議30000
socketTimeout:表示客戶端和MySQL建立socket后,讀寫socket時的等待超時時間
②:由於網絡波動、節點異常的原因導致的請求超時,可以采用服務降級的方式,為請求提供兜底的數據響應,避免用戶界面處於長時間停頓
高可用設計理論:
CAP:Consistency、Availability、Partition tolerance,此理論人盡皆知,最終會在CP和AP中權衡,找到滿足BASE(Basically Available、Soft state、Eventually consistent)的平衡結果
高可用設計要素:
冗余:確保對系統操作至關重要的任何元素都有一個額外的冗余組件,這些組件可以在發生故障時接管。
監控:從正在運行的系統中收集數據,並檢測組件何時發生故障或停止響應。
故障轉移:一種手動或自動機制。如果監控顯示活動組件發生故障,該機制可以從當前活動的組件切換到冗余組件。
上述三要素邏輯也很清晰:要實現高可用,不管是否存在狀態,要先有冗余或備份;當真正出現故障的時候,要有監控手段監控到故障發生;故障發生后,可以通過故障轉移組件快速轉移到之前的冗余組件中,保證服務不中斷。
高可用方案設計需要從哪些角度討論和思考?
首先,應用側、支撐側、運維側的設計方式方法不同。
應用側高可用除了可以通過上述提到的冗余、集群、負載均衡等做到快速的故障轉移,還包括熔斷、限流、容錯、降級、應急等保障手段,框架組件的超時及重試策略、異步調用、冪等性設計來補充。
支撐側(或稱基礎設施平台)需要一整套高可用相關的監控指標,滿足故障的提前預警、快速報警、可視化監控和分析。常見指標包括請求量、請求錯誤率、平均延時、HTTP狀態,以及系統資源消耗相關指標等。
運維側中關鍵一點是DevOps,自動化發布、灰度發布、優雅發布、版本控制、健康檢查等能力,可以在業務發生故障前和發生故障時,幫助應用最大程度減小服務不可用時長。
END.