Tomcat-connector的微調(1): acceptCount引數
對於acceptCount
這個引數,含義跟字面意思並不是特別一致(個人感覺),容易跟maxConnections
,maxThreads
等引數混淆;實際上這個引數在tomcat裡會被對映成backlog
:
static { replacements.put("acceptCount", "backlog"); replacements.put("connectionLinger", "soLinger"); replacements.put("connectionTimeout", "soTimeout"); replacements.put("rootFile", "rootfile"); }
backlog表示積壓待處理的事物,是socket的引數,在bind的時候傳入的,比如在Endpoint
裡的bind方法裡:
public void bind() throws Exception {
serverSock = ServerSocketChannel.open();
...
serverSock.socket().bind(addr,getBacklog());
...
}
這個引數跟tcp底層實現的半連線佇列和完全連線佇列有什麼關係呢?我們在tomcat預設BIO模式下模擬一下它的效果。
模擬的思路還是簡單的通過shell指令碼,建立一個長連線傳送請求,持有20秒再斷開,好有時間觀察網路狀態。注意BIO模式下預設超過75%的執行緒時會關閉keep-alive,需要把這個百分比調成100,這樣就不會關閉keep-alive了。修改後的connector如下,最後邊的三行引數是新增的:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxThreads="1"
disableKeepAlivePercentage="100"
acceptCount="2"
/>
上面的配置裡我們把tomcat的最大執行緒數設定為1個,一直開啟keep-alive,acceptCount設定為2。在linux上可以通過ss命令檢測引數是否生效:
$ ss -ant State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 2 :::7001 :::*
可以看到7001埠是LISTEN狀態,send-q
的值是2,也就是我們設定的backlog的值。如果我們不設定,tomcat預設會設定為100,java則預設是50。
然後用下面的指令碼模擬一次長連線:
$ {
echo -ne "POST /main HTTP/1.1\nhost: localhost:7001\n\n";
sleep 20
} | telnet localhost 7001
這個時候看伺服器端socket的狀況,是ESTABLISHED
,並且Recv-Q
和Send-Q
都是沒有堆積的,說明請求已經處理完
$ netstat -an | awk 'NR==2 || $4~/7001/'
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 127.0.0.1.7001 127.0.0.1.54453 ESTABLISHED
現在我們模擬多個連線:
$ for i in {1..5}; do
(
{
echo -ne "POST /main HTTP/1.1\nhost: localhost:7001\n\n";
sleep 20
} | telnet localhost 7001
)&;
done
上面發起了5個連結,伺服器端只有1個執行緒,只有第一個連線上的請求會被處理,另外4次連線,有2個連線還是完成了建立(ESTABLISHED狀態),還有2個連線則因為伺服器端的連線佇列已滿,沒有響應,傳送端處於SYN_SENT
狀態。下面列出傳送端的tcp狀態:
$ netstat -an | awk 'NR==2 || $5~/7001/'
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 127.0.0.1.51389 127.0.0.1.7001 SYN_SENT
tcp4 0 0 127.0.0.1.51388 127.0.0.1.7001 SYN_SENT
tcp4 0 0 127.0.0.1.51387 127.0.0.1.7001 ESTABLISHED
tcp4 0 0 127.0.0.1.51386 127.0.0.1.7001 ESTABLISHED
tcp4 0 0 127.0.0.1.51385 127.0.0.1.7001 ESTABLISHED
再看tomcat端的狀態:
$ netstat -an | awk 'NR==2 || $4~/7001/'
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 45 0 127.0.0.1.7001 127.0.0.1.51387 ESTABLISHED
tcp4 45 0 127.0.0.1.7001 127.0.0.1.51386 ESTABLISHED
tcp4 0 0 127.0.0.1.7001 127.0.0.1.51385 ESTABLISHED
有3個連結,除了第一條連線請求的Recv-Q
是0,另外兩個連線的Recv-Q
則有資料堆積(大小表示傳送過來的位元組長度)。注意,在ESTABLISHED
狀態下看到的Recv-Q
或Send-Q
的大小與在LISTEN
狀態下的含義不同,在LISTEN
狀態下的大小表示佇列的長度,而非資料的大小。
從上面的模擬可以看出acceptCount
引數是指伺服器端執行緒都處於busy狀態時(執行緒池已滿),還可接受的連線數,即tcp的完全連線佇列的大小。對於完全佇列的計算,在linux上是:
min(backlog,somaxconn)
即backlog
引數和proc/sys/net/core/somaxconn
這兩個值哪個小選哪個。
不過acceptCount/backlog
引數還不僅僅決定完全連線佇列的大小,對於半連線佇列也有影響。參考同事飄零的blog,在linux 2.6.20核心之後,它的計算方式大致是:
table_entries = min(min(somaxconn,backlog),tcp_max_syn_backlog)
roundup_pow_of_two(table_entries + 1)
第二行的函式roundup_pow_of_two
表示取最近的2的n次方的值,舉例來說:假設somaxconn
為128,backlog
值為50,tcp_max_syn_backlog
值為4096,則第一步計算出來的為50,然後roundup_pow_of_two(50 + 1)
,找到比51大的2的n次方的數為64,所以最終半連線佇列的長度是64。
所以對於acceptCount
這個值,需要慎重對待,如果請求量不是很大,通常tomcat預設的100也ok,但若訪問量較大的情況,建議這個值設定的大一些,比如1024或更大。如果在tomcat前邊一層對synflood攻擊的防禦沒有把握的話,最好也開啟syn cookie來防禦。