Flink網(wǎng)絡(luò)流控及反壓剖析-InfoQ
https://www.infoq.cn/article/g8DbwKBoxSk4JdBXA7fX
本文根據(jù) Apache Flink 系列直播整理而成,由 Apache Flink Contributor、OPPO 大數(shù)據(jù)平臺(tái)研發(fā)負(fù)責(zé)人張俊老師分享。主要內(nèi)容如下:
網(wǎng)絡(luò)流控的概念與背景
TCP 的流控機(jī)制
Flink TCP-based 反壓機(jī)制(before V1.5)
Flink Credit-based 反壓機(jī)制 (since V1.5)
總結(jié)與思考
網(wǎng)絡(luò)流控的概念與背景
為什么需要網(wǎng)絡(luò)流控

首先我們可以看下這張最精簡(jiǎn)的網(wǎng)絡(luò)流控的圖,Producer 的吞吐率是 2MB/s,Consumer 是 1MB/s,這個(gè)時(shí)候我們就會(huì)發(fā)現(xiàn)在網(wǎng)絡(luò)通信的時(shí)候我們的 Producer 的速度是比 Consumer 要快的,有 1MB/s 的這樣的速度差,假定我們兩端都有一個(gè) Buffer,Producer 端有一個(gè)發(fā)送用的 Send Buffer,Consumer 端有一個(gè)接收用的 Receive Buffer,在網(wǎng)絡(luò)端的吞吐率是 2MB/s,過(guò)了 5s 后我們的 Receive Buffer 可能就撐不住了,這時(shí)候會(huì)面臨兩種情況:
1.如果 Receive Buffer 是有界的,這時(shí)候新到達(dá)的數(shù)據(jù)就只能被丟棄掉了。
2.如果 Receive Buffer 是無(wú)界的,Receive Buffer 會(huì)持續(xù)的擴(kuò)張,最終會(huì)導(dǎo)致 Consumer 的內(nèi)存耗盡。
網(wǎng)絡(luò)流控的實(shí)現(xiàn):靜態(tài)限速

為了解決這個(gè)問(wèn)題,我們就需要網(wǎng)絡(luò)流控來(lái)解決上下游速度差的問(wèn)題,傳統(tǒng)的做法可以在 Producer 端實(shí)現(xiàn)一個(gè)類似 Rate Limiter 這樣的靜態(tài)限流,Producer 的發(fā)送速率是 2MB/s,但是經(jīng)過(guò)限流這一層后,往 Send Buffer 去傳數(shù)據(jù)的時(shí)候就會(huì)降到 1MB/s 了,這樣的話 Producer 端的發(fā)送速率跟 Consumer 端的處理速率就可以匹配起來(lái)了,就不會(huì)導(dǎo)致上述問(wèn)題。但是這個(gè)解決方案有兩點(diǎn)限制:
1、事先無(wú)法預(yù)估 Consumer 到底能承受多大的速率
2、 Consumer 的承受能力通常會(huì)動(dòng)態(tài)地波動(dòng)
網(wǎng)絡(luò)流控的實(shí)現(xiàn):動(dòng)態(tài)反饋/自動(dòng)反壓

針對(duì)靜態(tài)限速的問(wèn)題我們就演進(jìn)到了動(dòng)態(tài)反饋(自動(dòng)反壓)的機(jī)制,我們需要 Consumer 能夠及時(shí)的給 Producer 做一個(gè) feedback,即告知 Producer 能夠承受的速率是多少。動(dòng)態(tài)反饋分為兩種:
1、負(fù)反饋:接受速率小于發(fā)送速率時(shí)發(fā)生,告知 Producer 降低發(fā)送速率
2、正反饋:發(fā)送速率小于接收速率時(shí)發(fā)生,告知 Producer 可以把發(fā)送速率提上來(lái)
讓我們來(lái)看幾個(gè)經(jīng)典案例
案例一:Storm 反壓實(shí)現(xiàn)

上圖就是 Storm 里實(shí)現(xiàn)的反壓機(jī)制,可以看到 Storm 在每一個(gè) Bolt 都會(huì)有一個(gè)監(jiān)測(cè)反壓的線程(Backpressure Thread),這個(gè)線程一但檢測(cè)到 Bolt 里的接收隊(duì)列(recv queue)出現(xiàn)了嚴(yán)重阻塞就會(huì)把這個(gè)情況寫到 ZooKeeper 里,ZooKeeper 會(huì)一直被 Spout 監(jiān)聽,監(jiān)聽到有反壓的情況就會(huì)停止發(fā)送,通過(guò)這樣的方式匹配上下游的發(fā)送接收速率。
案例二:Spark Streaming 反壓實(shí)現(xiàn)

Spark Streaming 里也有做類似這樣的 feedback 機(jī)制,上圖 Fecher 會(huì)實(shí)時(shí)的從 Buffer、Processing 這樣的節(jié)點(diǎn)收集一些指標(biāo)然后通過(guò) Controller 把速度接收的情況再反饋到 Receiver,實(shí)現(xiàn)速率的匹配。
疑問(wèn):為什么 Flink(before V1.5)里沒(méi)有用類似的方式實(shí)現(xiàn) feedback 機(jī)制?
首先在解決這個(gè)疑問(wèn)之前我們需要先了解一下 Flink 的網(wǎng)絡(luò)傳輸是一個(gè)什么樣的架構(gòu)。

這張圖就體現(xiàn)了 Flink 在做網(wǎng)絡(luò)傳輸?shù)臅r(shí)候基本的數(shù)據(jù)的流向,發(fā)送端在發(fā)送網(wǎng)絡(luò)數(shù)據(jù)前要經(jīng)歷自己內(nèi)部的一個(gè)流程,會(huì)有一個(gè)自己的 Network Buffer,在底層用 Netty 去做通信,Netty 這一層又有屬于自己的 ChannelOutbound Buffer,因?yàn)樽罱K是要通過(guò) Socket 做網(wǎng)絡(luò)請(qǐng)求的發(fā)送,所以在 Socket 也有自己的 Send Buffer,同樣在接收端也有對(duì)應(yīng)的三級(jí) Buffer。學(xué)過(guò)計(jì)算機(jī)網(wǎng)絡(luò)的時(shí)候我們應(yīng)該了解到,TCP 是自帶流量控制的。實(shí)際上 Flink (before V1.5)就是通過(guò) TCP 的流控機(jī)制來(lái)實(shí)現(xiàn) feedback 的。
TCP 流控機(jī)制
根據(jù)下圖我們來(lái)簡(jiǎn)單的回顧一下 TCP 包的格式結(jié)構(gòu)。首先,他有 Sequence number 這樣一個(gè)機(jī)制給每個(gè)數(shù)據(jù)包做一個(gè)編號(hào),還有 ACK number 這樣一個(gè)機(jī)制來(lái)確保 TCP 的數(shù)據(jù)傳輸是可靠的,除此之外還有一個(gè)很重要的部分就是 Window Size,接收端在回復(fù)消息的時(shí)候會(huì)通過(guò) Window Size 告訴發(fā)送端還可以發(fā)送多少數(shù)據(jù)。

接下來(lái)我們來(lái)簡(jiǎn)單看一下這個(gè)過(guò)程。
TCP 流控:滑動(dòng)窗口

TCP 的流控就是基于滑動(dòng)窗口的機(jī)制,現(xiàn)在我們有一個(gè) Socket 的發(fā)送端和一個(gè) Socket 的接收端,目前我們的發(fā)送端的速率是我們接收端的 3 倍,這樣會(huì)發(fā)生什么樣的一個(gè)情況呢?假定初始的時(shí)候我們發(fā)送的 window 大小是 3,然后我們接收端的 window 大小是固定的,就是接收端的 Buffer 大小為 5。

首先,發(fā)送端會(huì)一次性發(fā) 3 個(gè) packets,將 1,2,3 發(fā)送給接收端,接收端接收到后會(huì)將這 3 個(gè) packets 放到 Buffer 里去。

接收端一次消費(fèi) 1 個(gè) packet,這時(shí)候 1 就已經(jīng)被消費(fèi)了,然后我們看到接收端的滑動(dòng)窗口會(huì)往前滑動(dòng)一格,這時(shí)候 2,3 還在 Buffer 當(dāng)中 而 4,5,6 是空出來(lái)的,所以接收端會(huì)給發(fā)送端發(fā)送 ACK = 4 ,代表發(fā)送端可以從 4 開始發(fā)送,同時(shí)會(huì)將 window 設(shè)置為 3 (Buffer 的大小 5 減去已經(jīng)存下的 2 和 3),發(fā)送端接收到回應(yīng)后也會(huì)將他的滑動(dòng)窗口向前移動(dòng)到 4,5,6。

這時(shí)候發(fā)送端將 4,5,6 發(fā)送,接收端也能成功的接收到 Buffer 中去。

到這一階段后,接收端就消費(fèi)到 2 了,同樣他的窗口也會(huì)向前滑動(dòng)一個(gè),這時(shí)候他的 Buffer 就只剩一個(gè)了,于是向發(fā)送端發(fā)送 ACK = 7、window = 1。發(fā)送端收到之后滑動(dòng)窗口也向前移,但是這個(gè)時(shí)候就不能移動(dòng) 3 格了,雖然發(fā)送端的速度允許發(fā) 3 個(gè) packets 但是 window 傳值已經(jīng)告知只能接收一個(gè),所以他的滑動(dòng)窗口就只能往前移一格到 7 ,這樣就達(dá)到了限流的效果,發(fā)送端的發(fā)送速度從 3 降到 1。


我們?cè)倏匆幌逻@種情況,這時(shí)候發(fā)送端將 7 發(fā)送后,接收端接收到,但是由于接收端的消費(fèi)出現(xiàn)問(wèn)題,一直沒(méi)有從 Buffer 中去取,這時(shí)候接收端向發(fā)送端發(fā)送 ACK = 8、window = 0 ,由于這個(gè)時(shí)候 window = 0,發(fā)送端是不能發(fā)送任何數(shù)據(jù),也就會(huì)使發(fā)送端的發(fā)送速度降為 0。這個(gè)時(shí)候發(fā)送端不發(fā)送任何數(shù)據(jù)了,接收端也不進(jìn)行任何的反饋了,那么如何知道消費(fèi)端又開始消費(fèi)了呢?



TCP 當(dāng)中有一個(gè) ZeroWindowProbe 的機(jī)制,發(fā)送端會(huì)定期的發(fā)送 1 個(gè)字節(jié)的探測(cè)消息,這時(shí)候接收端就會(huì)把 window 的大小進(jìn)行反饋。當(dāng)接收端的消費(fèi)恢復(fù)了之后,接收到探測(cè)消息就可以將 window 反饋給發(fā)送端端了從而恢復(fù)整個(gè)流程。TCP 就是通過(guò)這樣一個(gè)滑動(dòng)窗口的機(jī)制實(shí)現(xiàn) feedback。
Flink TCP-based 反壓機(jī)制(before V1.5)
示例:WindowWordCount

大體的邏輯就是從 Socket 里去接收數(shù)據(jù),每 5s 去進(jìn)行一次 WordCount,將這個(gè)代碼提交后就進(jìn)入到了編譯階段。
編譯階段:生成 JobGraph

這時(shí)候還沒(méi)有向集群去提交任務(wù),在 Client 端會(huì)將 StreamGraph 生成 JobGraph,JobGraph 就是做為向集群提交的最基本的單元。在生成 JobGrap 的時(shí)候會(huì)做一些優(yōu)化,將一些沒(méi)有 Shuffle 機(jī)制的節(jié)點(diǎn)進(jìn)行合并。有了 JobGraph 后就會(huì)向集群進(jìn)行提交,進(jìn)入運(yùn)行階段。
運(yùn)行階段:調(diào)度 ExecutionGraph

JobGraph 提交到集群后會(huì)生成 ExecutionGraph ,這時(shí)候就已經(jīng)具備基本的執(zhí)行任務(wù)的雛形了,把每個(gè)任務(wù)拆解成了不同的 SubTask,上圖 ExecutionGraph 中的 Intermediate Result Partition 就是用于發(fā)送數(shù)據(jù)的模塊,最終會(huì)將 ExecutionGraph 交給 JobManager 的調(diào)度器,將整個(gè) ExecutionGraph 調(diào)度起來(lái)。然后我們概念化這樣一張物理執(zhí)行圖,可以看到每個(gè) Task 在接收數(shù)據(jù)時(shí)都會(huì)通過(guò)這樣一個(gè) InputGate 可以認(rèn)為是負(fù)責(zé)接收數(shù)據(jù)的,再往前有這樣一個(gè) ResultPartition 負(fù)責(zé)發(fā)送數(shù)據(jù),在 ResultPartition 又會(huì)去做分區(qū)跟下游的 Task 保持一致,就形成了 ResultSubPartition 和 InputChannel 的對(duì)應(yīng)關(guān)系。這就是從邏輯層上來(lái)看的網(wǎng)絡(luò)傳輸?shù)耐ǖ溃谶@么一個(gè)概念我們可以將反壓的問(wèn)題進(jìn)行拆解。
問(wèn)題拆解:反壓傳播兩個(gè)階段

反壓的傳播實(shí)際上是分為兩個(gè)階段的,對(duì)應(yīng)著上面的執(zhí)行圖,我們一共涉及 3 個(gè) TaskManager,在每個(gè) TaskManager 里面都有相應(yīng)的 Task 在執(zhí)行,還有負(fù)責(zé)接收數(shù)據(jù)的 InputGate,發(fā)送數(shù)據(jù)的 ResultPartition,這就是一個(gè)最基本的數(shù)據(jù)傳輸?shù)耐ǖ?。在這時(shí)候假設(shè)最下游的 Task (Sink)出現(xiàn)了問(wèn)題,處理速度降了下來(lái)這時(shí)候是如何將這個(gè)壓力反向傳播回去呢?這時(shí)候就分為兩種情況:
跨 TaskManager ,反壓如何從 InputGate 傳播到 ResultPartition
TaskManager 內(nèi),反壓如何從 ResultPartition 傳播到 InputGate
跨 TaskManager 數(shù)據(jù)傳輸

前面提到,發(fā)送數(shù)據(jù)需要 ResultPartition,在每個(gè) ResultPartition 里面會(huì)有分區(qū) ResultSubPartition,中間還會(huì)有一些關(guān)于內(nèi)存管理的 Buffer。
對(duì)于一個(gè) TaskManager 來(lái)說(shuō)會(huì)有一個(gè)統(tǒng)一的 Network BufferPool 被所有的 Task 共享,在初始化時(shí)會(huì)從 Off-heap Memory 中申請(qǐng)內(nèi)存,申請(qǐng)到內(nèi)存的后續(xù)內(nèi)存管理就是同步 Network BufferPool 來(lái)進(jìn)行的,不需要依賴 JVM GC 的機(jī)制去釋放。有了 Network BufferPool 之后可以為每一個(gè) ResultSubPartition 創(chuàng)建 Local BufferPool 。
如上圖左邊的 TaskManager 的 Record Writer 寫了 <1,2> 這個(gè)兩個(gè)數(shù)據(jù)進(jìn)來(lái),因?yàn)?ResultSubPartition 初始化的時(shí)候?yàn)榭眨瑳](méi)有 Buffer 用來(lái)接收,就會(huì)向 Local BufferPool 申請(qǐng)內(nèi)存,這時(shí) Local BufferPool 也沒(méi)有足夠的內(nèi)存于是將請(qǐng)求轉(zhuǎn)到 Network BufferPool,最終將申請(qǐng)到的 Buffer 按原鏈路返還給 ResultSubPartition,<1,2> 這個(gè)兩個(gè)數(shù)據(jù)就可以被寫入了。之后會(huì)將 ResultSubPartition 的 Buffer 拷貝到 Netty 的 Buffer 當(dāng)中最終拷貝到 Socket 的 Buffer 將消息發(fā)送出去。然后接收端按照類似的機(jī)制去處理將消息消費(fèi)掉。
接下來(lái)我們來(lái)模擬上下游處理速度不匹配的場(chǎng)景,發(fā)送端的速率為 2,接收端的速率為 1,看一下反壓的過(guò)程是怎樣的。
跨 TaskManager 反壓過(guò)程

因?yàn)樗俣炔黄ヅ渚蜁?huì)導(dǎo)致一段時(shí)間后 InputChannel 的 Buffer 被用盡,于是他會(huì)向 Local BufferPool 申請(qǐng)新的 Buffer ,這時(shí)候可以看到 Local BufferPool 中的一個(gè) Buffer 就會(huì)被標(biāo)記為 Used。

發(fā)送端還在持續(xù)以不匹配的速度發(fā)送數(shù)據(jù),然后就會(huì)導(dǎo)致 InputChannel 向 Local BufferPool 申請(qǐng) Buffer 的時(shí)候發(fā)現(xiàn)沒(méi)有可用的 Buffer 了,這時(shí)候就只能向 Network BufferPool 去申請(qǐng),當(dāng)然每個(gè) Local BufferPool 都有最大的可用的 Buffer,防止一個(gè) Local BufferPool 把 Network BufferPool 耗盡。這時(shí)候看到 Network BufferPool 還是有可用的 Buffer 可以向其申請(qǐng)。

一段時(shí)間后,發(fā)現(xiàn) Network BufferPool 沒(méi)有可用的 Buffer,或是 Local BufferPool 的最大可用 Buffer 到了上限無(wú)法向 Network BufferPool 申請(qǐng),沒(méi)有辦法去讀取新的數(shù)據(jù),這時(shí) Netty AutoRead 就會(huì)被禁掉,Netty 就不會(huì)從 Socket 的 Buffer 中讀取數(shù)據(jù)了。

顯然,再過(guò)不久 Socket 的 Buffer 也被用盡,這時(shí)就會(huì)將 Window = 0 發(fā)送給發(fā)送端(前文提到的 TCP 滑動(dòng)窗口的機(jī)制)。這時(shí)發(fā)送端的 Socket 就會(huì)停止發(fā)送。

很快發(fā)送端的 Socket 的 Buffer 也被用盡,Netty 檢測(cè)到 Socket 無(wú)法寫了之后就會(huì)停止向 Socket 寫數(shù)據(jù)。

Netty 停止寫了之后,所有的數(shù)據(jù)就會(huì)阻塞在 Netty 的 Buffer 當(dāng)中了,但是 Netty 的 Buffer 是無(wú)界的,可以通過(guò) Netty 的水位機(jī)制中的 high watermark 控制他的上界。當(dāng)超過(guò)了 high watermark,Netty 就會(huì)將其 channel 置為不可寫,ResultSubPartition 在寫之前都會(huì)檢測(cè) Netty 是否可寫,發(fā)現(xiàn)不可寫就會(huì)停止向 Netty 寫數(shù)據(jù)。

這時(shí)候所有的壓力都來(lái)到了 ResultSubPartition,和接收端一樣他會(huì)不斷的向 Local BufferPool 和 Network BufferPool 申請(qǐng)內(nèi)存。

Local BufferPool 和 Network BufferPool 都用盡后整個(gè) Operator 就會(huì)停止寫數(shù)據(jù),達(dá)到跨 TaskManager 的反壓。
TaskManager 內(nèi)反壓過(guò)程
了解了跨 TaskManager 反壓過(guò)程后再來(lái)看 TaskManager 內(nèi)反壓過(guò)程就更好理解了,下游的 TaskManager 反壓導(dǎo)致本 TaskManager 的 ResultSubPartition 無(wú)法繼續(xù)寫入數(shù)據(jù),于是 Record Writer 的寫也被阻塞住了,因?yàn)?Operator 需要有輸入才能有計(jì)算后的輸出,輸入跟輸出都是在同一線程執(zhí)行, Record Writer 阻塞了,Record Reader 也停止從 InputChannel 讀數(shù)據(jù),這時(shí)上游的 TaskManager 還在不斷地發(fā)送數(shù)據(jù),最終將這個(gè) TaskManager 的 Buffer 耗盡。具體流程可以參考下圖,這就是 TaskManager 內(nèi)的反壓過(guò)程。




Flink Credit-based 反壓機(jī)制(since V1.5)
TCP-based 反壓的弊端

在介紹 Credit-based 反壓機(jī)制之前,先分析下 TCP 反壓有哪些弊端。
在一個(gè) TaskManager 中可能要執(zhí)行多個(gè) Task,如果多個(gè) Task 的數(shù)據(jù)最終都要傳輸?shù)较掠蔚耐粋€(gè) TaskManager 就會(huì)復(fù)用同一個(gè) Socket 進(jìn)行傳輸,這個(gè)時(shí)候如果單個(gè) Task 產(chǎn)生反壓,就會(huì)導(dǎo)致復(fù)用的 Socket 阻塞,其余的 Task 也無(wú)法使用傳輸,checkpoint barrier 也無(wú)法發(fā)出導(dǎo)致下游執(zhí)行 checkpoint 的延遲增大。
依賴最底層的 TCP 去做流控,會(huì)導(dǎo)致反壓傳播路徑太長(zhǎng),導(dǎo)致生效的延遲比較大。
引入 Credit-based 反壓
這個(gè)機(jī)制簡(jiǎn)單的理解起來(lái)就是在 Flink 層面實(shí)現(xiàn)類似 TCP 流控的反壓機(jī)制來(lái)解決上述的弊端,Credit 可以類比為 TCP 的 Window 機(jī)制。
Credit-based 反壓過(guò)程

如圖所示在 Flink 層面實(shí)現(xiàn)反壓機(jī)制,就是每一次 ResultSubPartition 向 InputChannel 發(fā)送消息的時(shí)候都會(huì)發(fā)送一個(gè) backlog size 告訴下游準(zhǔn)備發(fā)送多少消息,下游就會(huì)去計(jì)算有多少的 Buffer 去接收消息,算完之后如果有充足的 Buffer 就會(huì)返還給上游一個(gè) Credit 告知他可以發(fā)送消息(圖上兩個(gè) ResultSubPartition 和 InputChannel 之間是虛線是因?yàn)樽罱K還是要通過(guò) Netty 和 Socket 去通信),下面我們看一個(gè)具體示例。

假設(shè)我們上下游的速度不匹配,上游發(fā)送速率為 2,下游接收速率為 1,可以看到圖上在 ResultSubPartition 中累積了兩條消息,10 和 11, backlog 就為 2,這時(shí)就會(huì)將發(fā)送的數(shù)據(jù) <8,9> 和 backlog = 2 一同發(fā)送給下游。下游收到了之后就會(huì)去計(jì)算是否有 2 個(gè) Buffer 去接收,可以看到 InputChannel 中已經(jīng)不足了這時(shí)就會(huì)從 Local BufferPool 和 Network BufferPool 申請(qǐng),好在這個(gè)時(shí)候 Buffer 還是可以申請(qǐng)到的。

過(guò)了一段時(shí)間后由于上游的發(fā)送速率要大于下游的接受速率,下游的 TaskManager 的 Buffer 已經(jīng)到達(dá)了申請(qǐng)上限,這時(shí)候下游就會(huì)向上游返回 Credit = 0,ResultSubPartition 接收到之后就不會(huì)向 Netty 去傳輸數(shù)據(jù),上游 TaskManager 的 Buffer 也很快耗盡,達(dá)到反壓的效果,這樣在 ResultSubPartition 層就能感知到反壓,不用通過(guò) Socket 和 Netty 一層層地向上反饋,降低了反壓生效的延遲。同時(shí)也不會(huì)將 Socket 去阻塞,解決了由于一個(gè) Task 反壓導(dǎo)致 TaskManager 和 TaskManager 之間的 Socket 阻塞的問(wèn)題。
總結(jié)與思考
總結(jié)
網(wǎng)絡(luò)流控是為了在上下游速度不匹配的情況下,防止下游出現(xiàn)過(guò)載
網(wǎng)絡(luò)流控有靜態(tài)限速和動(dòng)態(tài)反壓兩種手段
Flink 1.5 之前是基于 TCP 流控 + bounded buffer 實(shí)現(xiàn)反壓
Flink 1.5 之后實(shí)現(xiàn)了自己托管的 credit - based 流控機(jī)制,在應(yīng)用層模擬 TCP 的流控機(jī)制
思考
有了動(dòng)態(tài)反壓,靜態(tài)限速是不是完全沒(méi)有作用了?

實(shí)際上動(dòng)態(tài)反壓不是萬(wàn)能的,我們流計(jì)算的結(jié)果最終是要輸出到一個(gè)外部的存儲(chǔ)(Storage),外部數(shù)據(jù)存儲(chǔ)到 Sink 端的反壓是不一定會(huì)觸發(fā)的,這要取決于外部存儲(chǔ)的實(shí)現(xiàn),像 Kafka 這樣是實(shí)現(xiàn)了限流限速的消息中間件可以通過(guò)協(xié)議將反壓反饋給 Sink 端,但是像 ES 無(wú)法將反壓進(jìn)行傳播反饋給 Sink 端,這種情況下為了防止外部存儲(chǔ)在大的數(shù)據(jù)量下被打爆,我們就可以通過(guò)靜態(tài)限速的方式在 Source 端去做限流。所以說(shuō)動(dòng)態(tài)反壓并不能完全替代靜態(tài)限速的,需要根據(jù)合適的場(chǎng)景去選擇處理方案。
