自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

聊聊NAT引發(fā)的性能瓶頸

網(wǎng)絡(luò) 通信技術(shù)
筆者最近解決了一個(gè)非常曲折的問(wèn)題,從抓包開(kāi)始一路排查到不同內(nèi)核版本間的細(xì)微差異,最后才完美解釋了所有的現(xiàn)象。在這里將整個(gè)過(guò)程寫(xiě)成博文記錄下來(lái),希望能夠?qū)ψx者有所幫助。(篇幅可能會(huì)有點(diǎn)長(zhǎng),耐心看完,絕對(duì)物有所值~)

[[351617]]

讀者最近解決了一個(gè)非常曲折的問(wèn)題,從抓包開(kāi)始一路排查到不同內(nèi)核版本間的細(xì)微差異,最后才完美解釋了所有的現(xiàn)象。在這里將整個(gè)過(guò)程寫(xiě)成博文記錄下來(lái),希望能夠?qū)ψx者有所幫助。(篇幅可能會(huì)有點(diǎn)長(zhǎng),耐心看完,絕對(duì)物有所值~)

環(huán)境介紹

先來(lái)介紹一下出問(wèn)題的環(huán)境吧,調(diào)用拓?fù)淙缦聢D所示:

調(diào)用拓?fù)鋱D

 

合作方的多臺(tái)機(jī)器用NAT將多個(gè)源ip映射成同一個(gè)出口ip 20.1.1.1,而我們內(nèi)網(wǎng)將多個(gè)Nginx映射成同一個(gè)目的ip 30.1.1.1。這樣,在防火墻和LVS之間,所有的請(qǐng)求始終是通過(guò)(20.1.1.1,30.1.1.1)這樣一個(gè)ip地址對(duì)進(jìn)行訪問(wèn)。

同時(shí)還固定了一個(gè)參數(shù),那就是目的端口號(hào)始終是443。

 

短連接-HTTP1.0

由于對(duì)方是采用短連接和Nginx進(jìn)行交互的,而且采用的協(xié)議是HTTP-1.0。所以我們的Nginx在每個(gè)請(qǐng)求完成后,會(huì)主動(dòng)關(guān)閉連接,從而造成有大量的TIME_WAIT。

 

值得注意的是,TIME_WAIT取決于Server端和Client端誰(shuí)先關(guān)閉這個(gè)Socket。所以Nginx作為Server端先關(guān)閉的話,也必然會(huì)產(chǎn)生TIME_WAIT。

 

內(nèi)核參數(shù)配置

內(nèi)核參數(shù)配置如下所示:

  1. cat /proc/sys/net/ipv4/tcp_tw_reuse 0 
  2. cat /proc/sys/net/ipv4/tcp_tw_recycle 0 
  3. cat /proc/sys/net/ipv4/tcp_timestamps 1 

其中tcp_tw_recycle設(shè)置為0。這樣,可以有效解決tcp_timestamps和tcp_tw_recycle在NAT情況下導(dǎo)致的連接失敗問(wèn)題。具體見(jiàn)筆者之前的博客:

  1. https://my.oschina.net/alchemystar/blog/3119992 

Bug現(xiàn)場(chǎng)

好了,介紹完環(huán)境,我們就可以正式描述Bug現(xiàn)場(chǎng)了。

Client端大量創(chuàng)建連接異常,而Server端無(wú)法感知

表象是合作方的應(yīng)用出現(xiàn)大量的創(chuàng)建連接異常,而Server端確沒(méi)有任何關(guān)于這些異常的任何異常日志,仿佛就從來(lái)沒(méi)有出現(xiàn)過(guò)這些請(qǐng)求一樣。

 

LVS監(jiān)控曲線

出現(xiàn)問(wèn)題后,筆者翻了下LVS對(duì)應(yīng)的監(jiān)控曲線,其中有個(gè)曲線的變現(xiàn)非常的詭異。如下圖所示:

 

什么情況?看上去像建立不了連接了?但是雖然業(yè)務(wù)有大量的報(bào)錯(cuò),依舊有很高的訪問(wèn)量,看日志的話,每秒請(qǐng)求應(yīng)該在550向上!和這個(gè)曲線上面每秒只有30個(gè)新建連接是矛盾的!

每天發(fā)生的時(shí)間點(diǎn)非常接近

觀察了幾天后。發(fā)現(xiàn),每天都在10點(diǎn)左右開(kāi)始發(fā)生報(bào)錯(cuò),同時(shí)在12點(diǎn)左右就慢慢恢復(fù)。

 

感覺(jué)就像每天10點(diǎn)在做活動(dòng),導(dǎo)致流量超過(guò)了系統(tǒng)瓶頸,進(jìn)而暴露出問(wèn)題。而11:40之后,流量慢慢下降,系統(tǒng)才慢慢恢復(fù)。難道LVS這點(diǎn)量都撐不住?才550TPS啊?就崩潰了?

難道是網(wǎng)絡(luò)問(wèn)題?

難道就是傳說(shuō)中的網(wǎng)絡(luò)問(wèn)題?看了下監(jiān)控,流量確實(shí)增加,不過(guò)只占了將近1/8的帶寬,離打爆網(wǎng)絡(luò)還遠(yuǎn)著呢。

 

進(jìn)行抓包

不管三七二十一,先抓包吧!

抓包結(jié)果

在這里筆者給出一個(gè)典型的抓包結(jié)果:

 

序號(hào) 時(shí)間 源地址 目的地址 源端口號(hào) 目的端口號(hào) 信息
1 09:57:30.60 30.1.1.1 20.1.1.1 443 33735 [FIN,ACK]Seq=507,Ack=2195,TSval=1164446830
2 09:57:30.64 20.1.1.1 30.1.1.1 33735 443 [FIN,ACK]Seq=2195,Ack=508,TSval=2149756058
3 09:57:30.64 30.1.1.1 20.1.1.1 443 33735 [ACK]Seq=508,Ack=2196,TSval=1164446863
4 09:59:22.06 20.1.1.1 30.1.1.1 33735 443 [SYN]Seq=0,TSVal=21495149222
5 09:59:22.06 30.1.1.1 20.1.1.1 443 33735 [ACK]Seq=1,Ack=1487349876,TSval=1164558280
6 09:59:22.08 20.1.1.1 30.1.1.1 33735 443 [RST]Seq=1487349876

上面抓包結(jié)果如下圖所示,一開(kāi)始33735->443這個(gè)Socket四次揮手。在將近兩分鐘后又使用了同一個(gè)33735端口和443建立連接,443給33735回了一個(gè)莫名其妙的Ack,導(dǎo)致33735發(fā)了RST!

 

現(xiàn)象是怎么產(chǎn)生的?

首先最可疑的是為什么發(fā)送了一個(gè)莫名其妙的Ack回來(lái)?筆者想到,這個(gè)Ack是WireShark給我計(jì)算出來(lái)的。為了我們方便,WireShark會(huì)根據(jù)Seq=0而調(diào)整Ack的值。事實(shí)上,真正的Seq是個(gè)隨機(jī)數(shù)!有沒(méi)有可能是WireShark在某些情況下計(jì)算錯(cuò)誤?

還是看看最原始的未經(jīng)過(guò)加工的數(shù)據(jù)吧,于是筆者將wireshark的

  1. Relative sequence numbers 

給取消了。取消后的抓包結(jié)果立馬就有意思了!調(diào)整過(guò)后抓包結(jié)果如下所示:

 

序號(hào) 時(shí)間 源地址 目的地址 源端口號(hào) 目的端口號(hào) 信息
1 09:57:30.60 30.1.1.1 20.1.1.1 443 33735 [FIN,ACK]Seq=909296387,Ack=1556577962,TSval=1164446830
2 09:57:30.64 20.1.1.1 30.1.1.1 33735 443 [FIN,ACK]Seq=1556577962,Ack=909296388,TSval=2149756058
3 09:57:30.64 30.1.1.1 20.1.1.1 443 33735 [ACK]Seq=909296388,Ack=1556577963,TSval=1164446863
4 09:59:22.06 20.1.1.1 30.1.1.1 33735 443 [SYN]Seq=69228087,TSVal=21495149222
5 09:59:22.06 30.1.1.1 20.1.1.1 443 33735 [ACK]Seq=909296388,Ack=1556577963,TSval=1164558280
6 09:59:22.08 20.1.1.1 30.1.1.1 33735 443 [RST]Seq=1556577963

看表中,四次揮手里面的Seq和Ack對(duì)應(yīng)的值和三次回收中那個(gè)錯(cuò)誤的ACK完全一致!也就是說(shuō),四次回收后,五元組并沒(méi)有消失,而是在111.5s內(nèi)還存活著!按照TCPIP狀態(tài)轉(zhuǎn)移圖,只有TIME_WAIT狀態(tài)才會(huì)如此。

 

我們可以看看Linux關(guān)于TIME_WAIT處理的內(nèi)核源碼:

  1. switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) { 
  2. // 如果是TCP_TW_SYN,那么允許此SYN分節(jié)重建連接 
  3. // 即允許TIM_WAIT狀態(tài)躍遷到SYN_RECV 
  4. case TCP_TW_SYN: { 
  5.  struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev), 
  6.       &tcp_hashinfo, 
  7.       iph->saddr, th->source, 
  8.       iph->daddr, th->dest, 
  9.       inet_iif(skb)); 
  10.  if (sk2) { 
  11.   inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row); 
  12.   inet_twsk_put(inet_twsk(sk)); 
  13.   sk = sk2; 
  14.   goto process; 
  15.  } 
  16.  /* Fall through to ACK */ 
  17. // 如果是TCP_TW_ACK,那么,返回記憶中的ACK,這和我們的現(xiàn)象一致 
  18. case TCP_TW_ACK: 
  19.  tcp_v4_timewait_ack(sk, skb); 
  20.  break; 
  21. // 如果是TCP_TW_RST直接發(fā)送RESET包 
  22. case TCP_TW_RST: 
  23.  tcp_v4_send_reset(sk, skb); 
  24.  inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row); 
  25.  inet_twsk_put(inet_twsk(sk)); 
  26.  goto discard_it; 
  27. // 如果是TCP_TW_SUCCESS則直接丟棄此包,不做任何響應(yīng) 
  28. case TCP_TW_SUCCESS:; 
  29. goto discard_it; 

上面的代碼有兩個(gè)分支,值得我們注意,一個(gè)是TCP_TW_ACK,在這個(gè)分支下,返回TIME_WAIT記憶中的ACK和我們的抓包現(xiàn)象一模一樣。還有一個(gè)TCP_TW_SYN,它表明了在 TIME_WAIT狀態(tài)下,可以立馬重用此五元組,跳過(guò)2MSL而達(dá)到SYN_RECV狀態(tài)!

 

狀態(tài)的遷移就在于tcp_timewait_state_process這個(gè)函數(shù),我們著重看下想要觀察的分支:

  1. enum tcp_tw_status 
  2. tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb, 
  3.       const struct tcphdr *th) 
  4.  bool paws_reject = false
  5.  ...... 
  6.  paws_reject = tcp_paws_reject(&tmp_opt, th->rst); 
  7.  if (!paws_reject && 
  8.      (TCP_SKB_CB(skb)->seq == tcptw->tw_rcv_nxt && 
  9.       (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq || th->rst))) { 
  10.   ...... 
  11.   // 重復(fù)的ACK,discard此包 
  12.   return TCP_TW_SUCCESS; 
  13.  } 
  14.  // 如果是SYN分節(jié),而且通過(guò)了paws校驗(yàn) 
  15.  if (th->syn && !th->rst && !th->ack && !paws_reject && 
  16.      (after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt) || 
  17.       (tmp_opt.saw_tstamp && 
  18.        (s32)(tcptw->tw_ts_recent - tmp_opt.rcv_tsval) < 0))) { 
  19.   ...... 
  20.   // 返回TCP_TW_SYN,允許重用TIME_WAIT五元組重新建立連接 
  21.   return TCP_TW_SYN; 
  22.  } 
  23.  // 如果沒(méi)有通過(guò)paws校驗(yàn),則增加統(tǒng)計(jì)參數(shù) 
  24.  if (paws_reject) 
  25.   NET_INC_STATS_BH(twsk_net(tw), LINUX_MIB_PAWSESTABREJECTED); 
  26.  if (!th->rst) { 
  27.   // 如果沒(méi)有通過(guò)paws校驗(yàn),而且這個(gè)分節(jié)包含ack,則將TIMEWAIT持續(xù)時(shí)間重新延長(zhǎng) 
  28.   // 我們抓包結(jié)果的結(jié)果沒(méi)有ACK,只有SYN,所以并不會(huì)延長(zhǎng) 
  29.   if (paws_reject || th->ack) 
  30.    inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN, 
  31.         TCP_TIMEWAIT_LEN); 
  32.   // 返回TCP_TW_ACK,也即TCP重傳ACK到對(duì)面 
  33.   return TCP_TW_ACK; 
  34.  } 

根據(jù)上面源碼,PAWS(Protect Againest Wrapped Sequence numbers防止回繞)校驗(yàn)機(jī)制如果生效而拒絕此分節(jié)的話,LINUX_MIB_PAWSESTABREJECTED這個(gè)統(tǒng)計(jì)參數(shù)會(huì)增加,對(duì)應(yīng)于Linux中的命令即是:

  1. netstat -s | grep reject 
  2. 216576 packets rejects in established connections because of timestamp 

這么上去后端的Nginx一統(tǒng)計(jì),果然有大量的報(bào)錯(cuò)。而且根據(jù)筆者的觀察,這個(gè)統(tǒng)計(jì)參數(shù)急速增加的時(shí)間段就是出問(wèn)題的時(shí)間段,也就是每天早上10:00-12:00左右。每次大概會(huì)增加1W多個(gè)統(tǒng)計(jì)參數(shù)。那么什么時(shí)候PAWS會(huì)不通過(guò)呢,我們直接看下tcp_paws_reject的源碼吧:

  1. static inline int tcp_paws_reject(const struct tcp_options_received *rx_opt, 
  2.       int rst) 
  3.  if (tcp_paws_check(rx_opt, 0)) 
  4.   return 0; 
  5.  // 如果是rst,則放松要求,60s沒(méi)有收到對(duì)端報(bào)文,認(rèn)為PAWS檢測(cè)通過(guò)  
  6.  if (rst && get_seconds() >= rx_opt->ts_recent_stamp + TCP_PAWS_MSL) 
  7.   return 0; 
  8.  return 1; 
  9.  
  10. static inline int tcp_paws_check(const struct tcp_options_received *rx_opt, 
  11.      int paws_win) 
  12.  
  13.  // 如果ts_recent中記錄的上次報(bào)文(SYN)的時(shí)間戳,小于當(dāng)前報(bào)文的時(shí)間戳(TSval),表明paws檢測(cè)通過(guò) 
  14.  // paws_win = 0 
  15.  if ((s32)(rx_opt->ts_recent - rx_opt->rcv_tsval) <= paws_win) 
  16.   return 1; 
  17.  // 否則,上一次獲得ts_recent時(shí)間戳的時(shí)刻的24天之后,為真表明已經(jīng)有超過(guò)24天沒(méi)有接收到對(duì)端的報(bào)文了,認(rèn)為PAWS檢測(cè)通過(guò) 
  18.  if (unlikely(get_seconds() >= rx_opt->ts_recent_stamp + TCP_PAWS_24DAYS)) 
  19.   return 1; 
  20.  
  21.  return 0; 

在抓包的過(guò)程中,我們明顯發(fā)現(xiàn),在四次揮手時(shí)候,記錄的tsval是2149756058,而下一次syn三次握手的時(shí)候是21495149222,反而比之前的小了!

序號(hào) 時(shí)間 源地址 目的地址 源端口號(hào) 目的端口號(hào) 信息
2 09:57:30.64 20.1.1.1 30.1.1.1 33735 443 [FIN,ACK]TSval=2149756058
4 09:59:22.06 20.1.1.1 30.1.1.1 33735 443 [SYN]TSVal=21495149222

所以PAWS校驗(yàn)不過(guò)。那么為什么會(huì)這個(gè)SYN時(shí)間戳比之前揮手的時(shí)間戳還小呢?那當(dāng)然是NAT的鍋嘍,NAT把多臺(tái)機(jī)器的ip虛擬成同一個(gè)ip。但是多臺(tái)機(jī)器的時(shí)間戳(也即從啟動(dòng)開(kāi)始到現(xiàn)在的時(shí)間,非墻上時(shí)間),如下圖所示:

 

但是還有一個(gè)疑問(wèn),筆者記得TIME_WAIT也即2MSL在Linux的代碼里面是定義為了60s。為何抓包的結(jié)果卻存活了將近2分鐘之久呢?

TIME_WAIT的持續(xù)時(shí)間

于是筆者開(kāi)始閱讀器關(guān)于TIME_WAIT定時(shí)器的源碼,具體可見(jiàn)筆者的另一篇博客:

  1. 從Linux源碼看TIME_WAIT狀態(tài)的持續(xù)時(shí)間 
  2. https://my.oschina.net/alchemystar/blog/4690516 

結(jié)論如下

在TIME_WAIT很多的狀態(tài)下,TIME_WAIT能夠存活112.5s,將近兩分鐘的時(shí)間,和我們的抓包結(jié)果一致。當(dāng)然了,這個(gè)計(jì)算只是針對(duì)Linux 2.6和3.10內(nèi)核而言,而對(duì)紅帽維護(hù)的3.10.1127內(nèi)核版本則會(huì)有另外的變化,這個(gè)變化導(dǎo)致了一個(gè)令筆者感到非常奇異的現(xiàn)象,這個(gè)在后面會(huì)提到。

 

問(wèn)題發(fā)生條件

如上面所解釋?zhuān)挥性赟erver端TIME_WAIT還沒(méi)有消失時(shí)候,重用這個(gè)Socket的時(shí)候,遇上了反序的時(shí)間戳SYN,就會(huì)發(fā)生這種問(wèn)題。由于NAT前面的所有機(jī)器時(shí)間戳都不相同,所以有很大概率會(huì)導(dǎo)致時(shí)間戳反序!

那么什么時(shí)候重用TIME_WAIT狀態(tài)的Socket呢

筆者知道,防火墻的端口號(hào)選擇邏輯是RoundRobin的,也即從2048開(kāi)始一直增長(zhǎng)到65535,再回繞到2048,如下圖所示:

 

為什么壓測(cè)的時(shí)候不出現(xiàn)問(wèn)題

但我們?cè)诰€下壓測(cè)的時(shí)候,明顯速率遠(yuǎn)超560tps,那為何確沒(méi)有這樣的問(wèn)題出現(xiàn)呢。很簡(jiǎn)單,是因?yàn)? TCP_SYN_SUCCESS這個(gè)分支,由于我們的壓測(cè)機(jī)沒(méi)有過(guò)NAT,那么時(shí)間戳始終保持單IP下的單調(diào)遞增,即便>560TPS之后,走的也是TCP_SYN_SUCCESS,將TIME_WAIT Socket重用為SYN_RECV,自然不會(huì)出現(xiàn)這樣的問(wèn)題,如下圖所示:

 

如何解釋LVS的監(jiān)控曲線?

等等,564TPS?這個(gè)和LVS陡然下跌的TPS基本相同!難道在端口號(hào)復(fù)用之后LVS就不會(huì)新建連接(其實(shí)是LVS中的session表項(xiàng))?從而導(dǎo)致統(tǒng)計(jì)參數(shù)并不增加?

于是筆者直接去擼了一下LVS的源碼:

  1. tcp_conn_schedule 
  2.   |->ip_vs_schedule 
  3.    /* 如果新建conn表項(xiàng)成功,則對(duì)已有連接數(shù)++ */ 
  4.    |->ip_vs_conn_stats 
  5. 而在我們的入口函數(shù)ip\_vs\_in中 
  6. static unsigned int 
  7. ip_vs_in(unsigned int hooknum, struct sk_buff *skb, 
  8.   const struct net_device *in, const struct net_device *out
  9.   int (*okfn) (struct sk_buff *)) 
  10.  ...... 
  11.  // 如果能找到對(duì)應(yīng)的五元組 
  12.  cp = pp->conn_in_get(af, skb, pp, &iph, iph.len, 0, &res_dir); 
  13.  
  14.  if (likely(cp)) { 
  15.   /* For full-nat/local-client packets, it could be a response */ 
  16.   if (res_dir == IP_VS_CIDX_F_IN2OUT) { 
  17.    return handle_response(af, skb, pp, cp, iph.len); 
  18.   } 
  19.  } else { 
  20.   /* create a new connection */ 
  21.   int v; 
  22.   // 找不到對(duì)應(yīng)的五元組,則新建連接,同時(shí)conn++ 
  23.   if (!pp->conn_schedule(af, skb, pp, &v, &cp)) 
  24.    return v; 
  25.  } 
  26.  ...... 

很明顯的,如果當(dāng)前五元組表項(xiàng)存在,則直接復(fù)用表項(xiàng),而不存在,才創(chuàng)建新的表項(xiàng),同時(shí)conn++。而表項(xiàng)需要在LVS的Fintimeout時(shí)間超過(guò)后才消失(在筆者的環(huán)境里面是120s)。這樣,在端口號(hào)復(fù)用的時(shí)候,因?yàn)?lt;112.5s,所以LVS會(huì)直接復(fù)用表項(xiàng),而統(tǒng)計(jì)參數(shù)不會(huì)有任何變化,從而導(dǎo)致了下面這個(gè)曲線。

 

當(dāng)流量慢慢變小,無(wú)法達(dá)到重用端口號(hào)的條件的時(shí)候,曲線又會(huì)垂直上升。和筆者的推測(cè)一致。也就是說(shuō)在五元組固定四元的情況下>529tps(63487/120)的時(shí)候,在此固定業(yè)務(wù)下的新建連接數(shù)不會(huì)增加。

而圖中僅存的560-529=>21+個(gè)連接創(chuàng)建,是由另一個(gè)業(yè)務(wù)的vip引起,在這個(gè)vip上,由于量很小,沒(méi)有端口復(fù)用。但是LVS統(tǒng)計(jì)的是總數(shù)量,所以在端口號(hào)開(kāi)始復(fù)用之后,始終會(huì)有少量的新建連接存在。

值得注意的是,端口號(hào)復(fù)用之后,LVS轉(zhuǎn)發(fā)的時(shí)候就會(huì)直接使用這個(gè)映射表項(xiàng),所以相同的五元組到LVS后會(huì)轉(zhuǎn)發(fā)給相同的Nginx,而不會(huì)進(jìn)行WRR(Weight Round Robin)負(fù)載均衡,表現(xiàn)出了一定的"親和性"。如下圖所示:

 

NAT下固定ip地址對(duì)的性能瓶頸

好了,現(xiàn)在可以下結(jié)論了。在ip源和目的地址固定,目的端口號(hào)也固定的情況下,五元組的可變量只有ip源端口號(hào)了。而源端口號(hào)最多是65535個(gè),如果計(jì)算保留端口號(hào)(0-2048)的話(假設(shè)防火墻保留2048個(gè)),那么最多可使用63487個(gè)端口。

由于每使用一個(gè)端口號(hào),在高負(fù)載的情況下,都會(huì)產(chǎn)生一個(gè)112.5s才消失的TIME_WAIT。那么在63487/112.5也就是564TPS(使用短連接)的情況下,就會(huì)復(fù)用TIME_WAIT下的Socket。再加上PAWS校驗(yàn),就會(huì)造成大量的連接創(chuàng)建異常!

 

這個(gè)論斷和筆者觀察到的應(yīng)用報(bào)錯(cuò)以及LVS監(jiān)控曲線一致。

LVS曲線異常事件和報(bào)錯(cuò)時(shí)間接近

因?yàn)長(zhǎng)VS是在529TPS時(shí)候開(kāi)始垂直下降,而端口號(hào)復(fù)用是在564TPS的時(shí)候開(kāi)始,兩者所需TPS非常接近,所以一般LVS出現(xiàn)曲線異常的時(shí)候,基本就是開(kāi)始報(bào)錯(cuò)的時(shí)候!但是LVS曲線異常只能表明復(fù)用表項(xiàng),并不能表明一定有問(wèn)題,因?yàn)榭梢酝ㄟ^(guò)調(diào)節(jié)某些內(nèi)核參數(shù)使得在端口號(hào)復(fù)用的時(shí)候不報(bào)錯(cuò)!

 

在端口號(hào)復(fù)用情況下,lvs本身的新建連接數(shù)無(wú)法代表真實(shí)TPS。

嘗試修復(fù)

設(shè)置tcp_tw_max_bucket

首先,筆者嘗試限制Nginx所在Linux中最大TIME_WAIT數(shù)量

  1. echo '5000'  > /proc/sys/net/ipv4/tcp_tw_max_bucket 

這基于一個(gè)很簡(jiǎn)單的想法,TIME_WAIT狀態(tài)越少,那么命中TIME_WAIT狀態(tài)Socket的概率肯定越小。設(shè)置了之后,確實(shí)報(bào)錯(cuò)量確實(shí)減少了好多。但由于TPS超越極限之后端口號(hào)不停的回繞,導(dǎo)致還是一直在報(bào)錯(cuò),不會(huì)有根本性好轉(zhuǎn)。

 

如果將tcp_tw_max_bucket設(shè)置為0,那么按理論上來(lái)說(shuō)不會(huì)出問(wèn)題了。但是無(wú)疑將TCP精心設(shè)計(jì)的TIME_WAIT這個(gè)狀態(tài)給廢棄了,筆者覺(jué)得這樣做過(guò)于冒險(xiǎn),于是沒(méi)有進(jìn)行嘗試。

嘗試擴(kuò)展源地址

這個(gè)問(wèn)題本質(zhì)是由于五元組在限定了4元,只有源端口號(hào)可變的情況下,端口號(hào)只有 2048-65535可用。那么我們放開(kāi)源地址的限定,例如將源IP增加到3個(gè),無(wú)疑可以將TPS擴(kuò)大三倍。

 

同理,將目的地址給擴(kuò)容,也能達(dá)到類(lèi)似的效果。

但據(jù)網(wǎng)工反映,合作方通過(guò)他們的防火墻出來(lái)之后就只有一個(gè)IP,而一個(gè)IP在我們的防火墻上并不能映射成多個(gè)IP,多以在不變更它們網(wǎng)絡(luò)設(shè)置的情況下無(wú)法擴(kuò)展源地址。而擴(kuò)容目的地址,也需要對(duì)合作方網(wǎng)絡(luò)設(shè)置進(jìn)行修改。本著不讓合作方改動(dòng)的服務(wù)精神,筆者開(kāi)始嘗試其它方案。

擴(kuò)容Nginx?沒(méi)效果

在一開(kāi)始筆者沒(méi)有搞明白LVS那個(gè)詭異的曲線的時(shí)候,筆者并不知道在端口復(fù)用的情況下,LVS會(huì)表現(xiàn)出"親和性"。于是想著,如果擴(kuò)容Nginx后,根據(jù)負(fù)載均衡原則,正好落到有這個(gè)TIME_WAIT五元組的概率會(huì)降低,所以嘗試著另擴(kuò)容了一倍的Nginx。但由于之前所說(shuō)的LVS在端口號(hào)復(fù)用下的親和性,反而加大了TIME_WAIT段!

 

擴(kuò)容Nginx的奇異現(xiàn)象

在筆者想明白LVS的"親和性"之后,對(duì)擴(kuò)容Nginx會(huì)導(dǎo)致更多的報(bào)錯(cuò)已經(jīng)有了心理預(yù)期,不過(guò)被現(xiàn)實(shí)啪啪啪打臉!報(bào)錯(cuò)量和之前基本一樣。更奇怪的是,筆者發(fā)現(xiàn)非活躍連接數(shù)監(jiān)控(即非ESTABLISHED)狀態(tài),會(huì)在端口號(hào)復(fù)用之后,呈現(xiàn)出一種負(fù)載不均衡的現(xiàn)象,如下圖所示。

 

筆者上去新擴(kuò)容的Nginx看了一下,發(fā)現(xiàn)新Nginx只有很少量的由于PAWS引起的報(bào)錯(cuò),增長(zhǎng)速率很慢,基本1個(gè)小時(shí)只有100多。而舊Nginx一個(gè)小時(shí)就有1W多!

那么按照這個(gè)錯(cuò)誤比例分布,就很好理解為什么形成這樣的曲線了。因?yàn)長(zhǎng)VS的親和性,在端口號(hào)復(fù)用時(shí)刻,落到舊Nginx上會(huì)大概率失敗,從而在Fintimeout到期后,重新選擇一個(gè)負(fù)載均衡的時(shí)候,如果落到新Nginx上,按照統(tǒng)計(jì)參數(shù)來(lái)看基本都會(huì)成功,但如果還是落到舊Nginx上則基本還會(huì)失敗,如此往復(fù)。就天然的形成了一個(gè)優(yōu)先選擇的過(guò)程,從而造成了這個(gè)曲線。

 

當(dāng)然實(shí)際的過(guò)程會(huì)比這個(gè)復(fù)雜一點(diǎn),多一些步驟,但大體是這個(gè)思路。

而在端口復(fù)用結(jié)束后,不管落到哪個(gè)Nginx上都會(huì)成功,所以負(fù)載均衡又會(huì)慢慢趨于均衡。

為什么新擴(kuò)容的Nginx表現(xiàn)異常優(yōu)異呢?

新擴(kuò)容的Nginx表現(xiàn)異常優(yōu)異,在這個(gè)TPS下沒(méi)有問(wèn)題,那到底是為什么呢?筆者想了一天都沒(méi)想明白。睡了一覺(jué)之后,對(duì)比了兩者的內(nèi)核參數(shù),突然豁然開(kāi)朗。原來(lái)新擴(kuò)容的Nginx所在的內(nèi)核版本變了,變成了3.10!

筆者連忙對(duì)比起了原來(lái)的2.6內(nèi)核和3.10的內(nèi)核版本變化,但毫無(wú)所得。。。思維有陷入了停滯

Linux官方3.10和紅帽的3.10.1127分支差異

等等,我們線上的內(nèi)核版本是3.10.1127,并不是官方的內(nèi)核,難道代碼有所不同?于是筆者立馬下載了3.10.1127的源碼。這一比對(duì),終于讓筆者找到了原因所在,看如下代碼!

  1. void inet_twdr_twkill_work(struct work_struct *work
  2.  struct inet_timewait_death_row *twdr = 
  3.   container_of(work, struct inet_timewait_death_row, twkill_work); 
  4.  bool rearm_timer = false
  5.  int i; 
  6.  
  7.  BUILD_BUG_ON((INET_TWDR_TWKILL_SLOTS - 1) > 
  8.    (sizeof(twdr->thread_slots) * 8)); 
  9.  
  10.  while (twdr->thread_slots) { 
  11.   spin_lock_bh(&twdr->death_lock); 
  12.   for (i = 0; i < INET_TWDR_TWKILL_SLOTS; i++) { 
  13.    if (!(twdr->thread_slots & (1 << i))) 
  14.     continue
  15.  
  16.    while (inet_twdr_do_twkill_work(twdr, i) != 0) { 
  17.     // 如果這次沒(méi)處理完,將rearm_timer設(shè)置為true 
  18.     rearm_timer = true
  19.     if (need_resched()) { 
  20.      spin_unlock_bh(&twdr->death_lock); 
  21.      schedule(); 
  22.      spin_lock_bh(&twdr->death_lock); 
  23.     } 
  24.    } 
  25.  
  26.    twdr->thread_slots &= ~(1 << i); 
  27.   } 
  28.   spin_unlock_bh(&twdr->death_lock); 
  29.  } 
  30.  // 在這邊多了一個(gè)rearm_timer,并將定時(shí)器設(shè)置為1s之后 
  31.  // 這樣,原來(lái)需要額外等待的7.5s現(xiàn)在收斂為額外等待1s 
  32.  if (rearm_timer) 
  33.   mod_timer(&twdr->tw_timer, jiffies + HZ); 

如代碼所示,3.10.1127對(duì)TIME_WAIT的時(shí)間輪處理做了加速,讓原來(lái)需要額外等待的7.5s收斂為額外等待的1s。經(jīng)過(guò)校正后的時(shí)間輪如下所示:

 

那么TIME_WAIT的存活時(shí)間就從112.5s下降到60.5s(計(jì)算公式8.5*7+1)。

那么,在這個(gè)狀態(tài)下,我們的端口復(fù)用臨界TPS就達(dá)到了(65535-2048)/60.5=1049tps,由于線上業(yè)務(wù)量并沒(méi)有達(dá)到這一tps。所以對(duì)于新擴(kuò)容的Nginx,并不會(huì)造成TIME_WAIT下的端口復(fù)用。所以錯(cuò)誤量并沒(méi)有變多!當(dāng)然,由于舊Nginx的存在,錯(cuò)誤量也沒(méi)有變少。

但是,由于那個(gè)神奇的選擇性負(fù)載均衡的存在,在端口復(fù)用時(shí)間越長(zhǎng),每秒鐘的報(bào)錯(cuò)量會(huì)越少!直到LVS的表項(xiàng)全部指到新Nginx集群,就不會(huì)再有報(bào)錯(cuò)了!

TPS漲到1049tps依舊會(huì)報(bào)錯(cuò)

當(dāng)然了,根據(jù)上面的計(jì)算,在TPS繼續(xù)上漲到1049后,依舊會(huì)產(chǎn)生錯(cuò)誤。新版本內(nèi)核只不過(guò)拉高了臨界值,所以筆者還是要尋求更加徹底的解決方案。

順便吐槽一句

Linux TCP的實(shí)現(xiàn)對(duì)TIME_WAIT的處理用時(shí)間輪在筆者看來(lái)并不是什么高明的處理方式。

Linux本身對(duì)于Timer的處理本身就提供了紅黑樹(shù)這樣的方案。放著這樣好的方案不用,偏偏去實(shí)現(xiàn)一個(gè)精度不高還很復(fù)雜的時(shí)間輪。

所幸在Linux 4.x版本中,擯棄了時(shí)間輪,直接使用Linux本身的紅黑樹(shù)方案。感覺(jué)自然多了!

本文轉(zhuǎn)載自微信公眾號(hào)「解Bug之路」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系解Bug之路公眾號(hào)。

 

責(zé)任編輯:武曉燕 來(lái)源: 解Bug之路
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)