億級流量架構之資源隔離思路與方法

作者 | 等不到的口琴

來源 |  urlify.cn/2QrEFv

爲什麼要資源隔離

常見的資源, 例如磁盤、網絡、CPU 等等, 都會存在競爭的問題, 在構建分佈式架構時, 可以將原本連接在一起的組件、模塊、資源拆分開來, 以便達到最大的利用效率或性能。資源隔離之後, 當某一部分組件出現故障時, 可以隔離故障, 方便定位的同時, 阻止傳播, 避免出現滾雪球以及雪崩效應。

常見的隔離方式有:

線程隔離

網絡上很多帖子, 大多是從框架開始聊的, 這兒說人話其實就是對線程進行治理, 把核心業務線程與非核心業務線程隔開, 不同的業務需要的線程數量不同, 可以設置不同的線程池, 來舉一些框架中應用的例子, 例如 Netty 中的主從多線程、Tomcat 請求隔離、Dubbo 線程模型。

Netty 主從程模型

主線程負責認證,連接,成功之後交由從線程負責連接的讀寫操作,大致如下代碼:

`EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);`

主線程是一個單線程,從線程是一個默認爲 cpu*2 個數的線程池,可以在我們的業務 handler 中做一個簡單測試:

public void channelRead(ChannelHandlerContext ctx, Object msg) {         System.out.println("thread name=" + Thread.currentThread().getName() + " server receive msg=" + msg);     }

服務端在讀取數據的時候打印一下當前的線程:

thread name=nioEventLoopGroup-3-1 server receive msg="..."

可以發現這裏使用的線程其實和處理 io 線程是同一個;

Dubbo 線程隔離模型

Dubbo 的底層通信框架其實使用的就是 Netty,但是 Dubbo 並沒有直接使用 Netty 的 io 線程來處理業務,可以簡單在生產者端輸出當前線程名稱:

thread name=DubboServerHandler-192.168.1.115:20880-thread-2,...

可以發現業務邏輯使用並不是 nioEventLoopGroup 線程,這是因爲 Dubbo 有自己的線程模型,可以看看官網提供的模型圖:

由圖可以知道, Dubbo 服務端接收到請求後, 通過調度器 (Dispatcher) 分發到不同的線程池,也簡單做一些關於調度器 (Dispatcher) 總結:

Dispatcher 調度器可以配置消息的處理線程:

通過看源碼可以知道, Dubbo 默認使用的線程池是 FixedThreadPool,線程數默認爲 200

Tomcat 請求線程隔離

Tomcat 是 Servelet 的具體實現, 在 Tomcat 請求支持四種請求處理方式分別爲: BIO、AIO、NIO、APR

BIO 模式:阻塞式 I/O 操作,表示 Tomcat 使用的是傳統 Java。I/O 操作 (即 Java.io 包及其子包)。Tomcat7 以下版本默認情況下是以 bio 模式運行的,由於每個請求都要創建一個線程來處理,線程開銷較大,不能處理高併發的場景,在幾種模式中性能也最低。

NIO 模式:
同步非阻塞 I/O 操作,是一個基於緩衝區、並能提供非阻塞 I/O 操作的 API,它擁有比傳統 I/O 操作具有更好的併發性能。關於 NIO, 可以參考我這篇博客: NIO 非阻塞網絡編程原理

在 Tomcat7 版本之後, Tomcat 把連接介入和業務處理拆分成兩個線程池來處理,即:

可以使用獨立的線程池來維護 servlet 的創建。連接器 connector 能介入的請求肯定比業務複雜的 servlet 處理的個數要多,在中間,Tomcat 加入了隊列,來等待 servlet 線程池空閒。這兩步是 Tomcat 內核完成的,在一階段無法區分具體業務或資源,所以只能在連接介入,servlet 初始化完成後我們根據自己的業務線去劃分獨立的連接池。

這樣做, 獨立的業務或資源中如果出現崩潰,不會影響其他的業務線程,從而達到資源隔離和服務降級的效果。

在使用了 servlet3 之後,系統線程隔離變得更靈活了。可以劃分核心業務隊列和非核心業務隊列:

線程隔離小總結

  1. 資源一旦出現問題,雖然是隔離狀態,想要讓資源重新可用,很難做到不重啓 jvm。

  2. 線程池內部線程如果出現 OOM、FullGC、cpu 耗盡等問題也是無法控制的

  3. 線程隔離,只能保證在分配線程這個資源上進行隔離,並不能保證整體穩定性

進程隔離

進程隔離這種思想其實並不陌生, Linux 操作系統中, 利用文件管理系統將各個進程的虛擬內存與實際的物理內存映射起來, 這樣做的好處是避免不同的進程之間相互影響, 而在分佈式系統中, 線程隔離不能完全隔離故障避免雪崩, 例如某個線程組耗盡內存導致 OOM, 那麼其他線程組勢必也會受影響, 所以進程隔離的思想是, CPU、內存等等這些資源也通過不同的虛擬機來做隔離。

具體操作是, 將業務邏輯進行拆分成多個子系統 (拆分原則可以參考: Redis 集羣拆分原則之 AKF),實現物理隔離,當某一個子系統出現問題,不會影響到其他子系統。

集羣隔離

如果系統中某個業務模塊包含像搶購、秒殺、存儲 I/O 密集度高、網絡 I/o 高、計算 I/O 高這類需求的時候,很容易在併發量高的時候因爲這種功能把整個模塊佔有的資源全部耗盡,導致響應編碼甚至節點不可用。像上圖的的拆分之後, 如果某一天購物人數瞬間暴增, 電商交易功能模塊可能受影響, 損壞後導致電商模塊其他的瀏覽查詢也無法使用, 因此就要建立集羣進行隔離, 具體來說就是繼續拆分模塊, 將功能微服務化。

解決方案

可以使用 hystrix 在微服務中隔離分佈式服務故障。他可以通過線程和信號量進行隔離。

線程池隔離與信號量隔離對比

這兒同上面的線程隔離, 不多贅述, 簡單敘述一下 hystrix 的兩種隔離方式的區別:

信號量隔離

說人話就是, 很多線程湧過來, 要去獲得信號量, 獲得了才能繼續執行, 否則先進入隊列等待或者直接 fallback 回調

最重要的是,信號量的調用是同步的,也就是說,每次調用都得阻塞調用方的線程,直到結果返回。這樣就導致了無法對訪問做超時(只能依靠調用協議超時,無法主動釋放)

官網對信號量隔離的描述建議

理解下兩點:

  1. 隔離的細粒度太高,數百個實例需要隔離,此時用線程池做隔離開銷過大

  2. 通常這種都是非網絡調用的情況下

機房隔離

機房隔離主要目的有兩個, 一方面是將不同區域的用戶數據隔離到不同的地區, 例如湖北的數據放在湖北的服務器, 浙江的放在浙江服務器, 等等, 這樣能解決數據容量大,計算密集,i/o(網絡)密集度高的問題, 相當於將流量分在了各個區域。

另一方面, 機房隔離也是爲了保證安全性, 所有數據都放在一個地方, 如果發生自然災害或者爆炸等災害時, 數據將全都丟失, 所以把服務建立整體副本(計算服務、數據存儲),在多機房內做異地多活或冷備份、是微服務數據異構的放大版本。

如果機房層面出現問題的時候,可以通過智能 dns、httpdns、負載均衡等技術快速切換, 讓區域用戶儘量不收影響。

數據讀寫隔離

通過主從模式,將 mysql、redis 等數據存儲服務集羣化,讀寫分離,那麼在寫入數據不可用的時候,也可以通過重試機制臨時通過其他節點讀取到數據。

多節點在做子網劃分的時候,除了異地多活,還可以做數據中心,所有數據在本地機房 crud 異步同步到數據中心,數據中心再去分發數據給其他機房, 那麼數據臨時在本地機房不可用的時候,就可以嘗試連接異地機房或數據中心。

靜態隔離

主要思路是將一些靜態資源分發在邊緣服務器中, 因爲日常訪問中有很多資源是不會變的, 所以沒必要每次都想從主服務器上獲取, 可以將這些數據保存在邊緣服務器上降低主服務器的壓力。

有一篇很詳細的講解參考: 全局負載均衡與 CDN 內容分發

爬蟲隔離

建立合適的規則, 將爬蟲請求轉移到另外的集羣

目前我們開發的都是 API 接口,並且多數都是開放的 API 接口。也就是說,只要有人拿到這個接口,任何人都可以通過這個 API 接口獲取數據,如果是網絡爬蟲請求速度快,獲取的數據多,不僅會對服務器造成影響, 不用多久,爬蟲方完全可以用我們 API 的接口來開發一個同樣的網站,開放平臺的 API 接口調用需要限制其頻率,以節約服務器資源和避免惡意的頻繁調用, 在大型互聯網項目中,對於 web 服務和網絡爬蟲的訪問流量能達到 5:1,甚至更高,有的系統有時候就會因爲爬蟲流量過高而導致資源耗盡,服務不可用。解決策略大致兩個方面:

一是限流,限制訪問的頻率;

二是將爬蟲請求轉發到固定地方。

爬蟲限流

想要分辨出來一個訪問是不是爬蟲,可以簡單的使用 nginx 來分析 ua 處理

UA 介紹

User Agent 簡稱 UA,就是用戶代理。通常我們用瀏覽器訪問網站,在網站的日誌中,我們的瀏覽器就是一種 UA。

禁止特定 UA 訪問, 例如最近有個網站 A 抄襲公司主站 B 的內容,除了域名不同,內容、圖片等都完全是我們主站的內容。出現這種情況,有兩種可能:

一種是:它用爬蟲抓取公司主站 B 的內容並放到自己服務器上顯示;

另一種是:通過將訪問代理至公司主站 B,而域名 A 是盜用者的,騙取流量。

無論怎樣,都要禁止這種行爲的繼續。有兩種方法解決:

1)禁止 IP

2)禁止 UA

從 nginx 日誌觀察,訪問者的代理 IP 經常變,但是訪問 UA 卻是固定的,因而可以禁止 UA。

nginx 不僅可以處理 ua 來分離流量,還可以通過更強大的 openresty 來完成更復雜的邏輯,實現一個流量網關,軟防火牆。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_dAbyFApkSFQuKmLAS08Ww