如何優雅的使用 IPtables 在多租戶環境中實現 TCP 限速
我們有個服務以類似 SideCar 的方式和應用一起運行,SideCar 和應用通過 Unix Domain Socket 進行通訊。為了方便用戶,在開發的時候不必在自己的開發環境中跑一個 SideCar,我用 socat 在一臺開發環境的機器上 map UDS 到一個端口。這樣用戶在開發的時候就可以直接通過這個 TCP 端口測試服務,而不用自己開一個 SideCar 使用 UDS 了。
因為所有人都要用這一個地址做開發,所以就有互相影響的問題。雖然性能還可以,幾十萬 QPS 不成問題,但是總有憨憨拿來搞壓測,把資源跑滿,影響別人。我在使用說明文檔里用紅色大字寫了這是開發測試用的,不能壓測,還是有一些視力不好的同事會強行壓測。隔三差五我就得去解釋一番,禮貌地請同事不要再這樣做了。
最近實在累了。研究了一下直接給這個端口加上 per IP 的 rate limit,效果還不錯。方法是在 Per-IP rate limiting with iptables[1] 學習到的,這個公司是提供一個多租戶的 SaaS 服務,也有類似的問題:有一些非正常用戶 abuse 他們的服務,由于 abuse 發生在連接建立階段,還沒有進入到業務代碼,所以無法從應用的層面進行限速,解決發現就是通過 iptables 實現的。詳細的實現方法可以參考這篇文章。
iptables 本身是無狀態的,每一個進入的 packet 都單獨判斷規則。rate limit 顯然是一個有狀態的規則,所以要用到 module: hashlimit。(原文中還用到了 conntrack,他是想只針對新建連接做限制,已經建立的連接不限制速度了。因為這個應用內部就可以控制了,但是我這里是想對所有的 packet 進行限速,所以就不需要用到這個 module)
完整的命令如下:
- $ iptables --new-chain SOCAT-RATE-LIMIT
- $ iptables --append SOCAT-RATE-LIMIT \
- --match hashlimit \
- --hashlimit-mode srcip \
- --hashlimit-upto 50/sec \
- --hashlimit-burst 100 \
- --hashlimit-name conn_rate_limit \
- --jump ACCEPT
- $ iptables --append SOCAT-RATE-LIMIT --jump DROP
- $ iptables -I INPUT -p tcp --dport 1234 --jump SOCAT-RATE-LIMIT
第一行是新建一個 iptables Chain,做 rate limit;
第二行處理如果在 rate limit 限額內,就接受包;否則跳到第三行,直接將包 DROP;
最后將新的 Chain 加入到 INPUT 中,對此端口的流量進行限制。
有關 rate limit 的算法,主要是兩個參數:
- --hashlimit-upto 其實本質上是 1s 內可以進入多少 packet,50/sec 就是 20ms 一個 packet;
- 那如何在 10ms 發來 10 個 packet,后面一直沒發送,怎么辦?這個在測試情景下也比較常見,不能要求用戶一直勻速地發送。所以就要用到 --hashlimit-burst。字面意思是瞬間可以發送多少 packet,但實際上,可以理解這個參數就是可用的 credit。
兩個指標配合起來理解,就是每個 ip 剛開始都會有 burst 個 credit,每個 ip 發送來的 packet 都會占用 burst 里面的 credit,用完了之后再發來的包就會被直接 DROP。這個 credit 會以 upto 的速度一直增加,但是最多增加到 burst(初始值),之后就 use it or lost it.
舉個例子,假如 --hashlimit-upto 50/sec --hashlimit-burst 20 的話,某個 IP 以勻速每 ms 一個 packet 的速度發送,最終會有多少 packets 被接受?答案是 70. 最初的 20ms,所有的 packet 都會被接受,因為 --hashlimit-burst 是 20,所以最初的 credit 是 20. 這個用完之后就要依賴 --hashlimit--upto 50/sec 來每 20ms 獲得一個 packet credit 了。所以每 20ms 可以接受一個。
這是限速之后的效果,非常明顯: