<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          解Bug之路-Nginx 502 Bad Gateway

          共 5463字,需瀏覽 11分鐘

           ·

          2021-05-27 23:32

          解Bug之路-Nginx 502 Bad Gateway

          前言

          事實(shí)證明,讀過Linux內(nèi)核源碼確實(shí)有很大的好處,尤其在處理問題的時(shí)刻。當(dāng)你看到報(bào)錯(cuò)的那一瞬間,就能把現(xiàn)象/原因/以及解決方案一股腦的在腦中閃現(xiàn)。甚至一些邊邊角角的現(xiàn)象都能很快的反應(yīng)過來是為何。筆者讀過一些Linux TCP協(xié)議棧的源碼,就在解決下面這個(gè)問題的時(shí)候有一種非常流暢的感覺。

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

          首先,這個(gè)問題其實(shí)并不難解決,但是這個(gè)問題引發(fā)的現(xiàn)象倒是挺有意思。先描述一下現(xiàn)象吧,
          筆者要對(duì)自研的dubbo協(xié)議隧道網(wǎng)關(guān)進(jìn)行壓測(cè)(這個(gè)網(wǎng)關(guān)的設(shè)計(jì)也挺有意思,準(zhǔn)備放到后面的博客里面)。先看下壓測(cè)的拓?fù)浒?

          為了壓測(cè)筆者gateway的單機(jī)性能,兩端僅僅各保留一臺(tái)網(wǎng)關(guān),即gateway1和gateway2。壓到一定程度就開始報(bào)錯(cuò),導(dǎo)致壓測(cè)停止。很自然的就想到,網(wǎng)關(guān)扛不住了。

          網(wǎng)關(guān)的情況

          去Gateway2的機(jī)器上看了一下,沒有任何報(bào)錯(cuò)。而Gateway1則有大量的502報(bào)錯(cuò)。502是Bad Gateway,Nginx的經(jīng)典報(bào)錯(cuò),首先想到的就是Gateway2不堪重負(fù)被Nginx在Upstream中踢掉。

          那么,就先看看Gateway2的負(fù)載情況把,查了下監(jiān)控,發(fā)現(xiàn)Gateway2在4核8G的機(jī)器上只用了一個(gè)核,完全看不出來有瓶頸的樣子,難道是IO有問題?看了下小的可憐的網(wǎng)卡流量打消了這個(gè)猜想。

          Nginx所在機(jī)器CPU利用率接近100%

          這時(shí)候,發(fā)現(xiàn)一個(gè)有意思的現(xiàn)象,Nginx確用滿了CPU!

          再次壓測(cè),去Nginx所在機(jī)器上top了一下,發(fā)現(xiàn)Nginx的4個(gè)Worker分別占了一個(gè)核把CPU吃滿-_-!

          什么,號(hào)稱性能強(qiáng)悍的Nginx竟然這么弱,說好的事件驅(qū)動(dòng)\epoll邊沿觸發(fā)\純C打造的呢?一定是用的姿勢(shì)不對(duì)!

          去掉Nginx直接通信毫無壓力

          既然猜測(cè)是Nginx的瓶頸,就把Nginx去掉吧。Gateway1和Gateway2直連,壓測(cè)TPS里面就飆升了,而且Gateway2的CPU最多也就吃了2個(gè)核,毫無壓力。

          去Nginx上看下日志

          由于Nginx機(jī)器權(quán)限并不在筆者手上,所以一開始沒有關(guān)注其日志,現(xiàn)在就聯(lián)系一下對(duì)應(yīng)的運(yùn)維去看一下吧。在accesslog里面發(fā)現(xiàn)了大量的502報(bào)錯(cuò),確實(shí)是Nginx的。又看了下錯(cuò)誤日志,發(fā)現(xiàn)有大量的

          Cannot assign requested address

          由于筆者讀過TCP源碼,一瞬間就反應(yīng)過來,是端口號(hào)耗盡了!由于Nginx upstream和后端Backend默認(rèn)是短連接,所以在大量請(qǐng)求流量進(jìn)來的時(shí)候回產(chǎn)生大量TIME_WAIT的連接。

          而這些TIME_WAIT是占據(jù)端口號(hào)的,而且基本要1分鐘左右才能被Kernel回收。

          cat /proc/sys/net/ipv4/ip_local_port_range
          32768 61000

          也就是說,只要一分鐘之內(nèi)產(chǎn)生28232(61000-32768)個(gè)TIME_WAIT的socket就會(huì)造成端口號(hào)耗盡,也即470.5TPS(28232/60),只是一個(gè)很容易達(dá)到的壓測(cè)值。事實(shí)上這個(gè)限制是Client端的,Server端沒有這樣的限制,因?yàn)镾erver端口號(hào)只有一個(gè)8080這樣的有名端口號(hào)。而在
          upstream中Nginx扮演的就是Client,而Gateway2就扮演的是Nginx

          為什么Nginx的CPU是100%

          而筆者也很快想明白了Nginx為什么吃滿了機(jī)器的CPU,問題就出來端口號(hào)的搜索過程。

          讓我們看下最耗性能的一段函數(shù):

          int __inet_hash_connect(...)
          {
          // 注意,這邊是static變量
          static u32 hint;
          // hint有助于不從0開始搜索,而是從下一個(gè)待分配的端口號(hào)搜索
          u32 offset = hint + port_offset;
          .....
          inet_get_local_port_range(&low, &high);
          // 這邊remaining就是61000 - 32768
          remaining = (high - low) + 1
          ......
          for (i = 1; i <= remaining; i++) {
          port = low + (i + offset) % remaining;
          /* port是否占用check */
          ....
          goto ok;
          }
          .......
          ok:
          hint += i;
          ......
          }

          看上面那段代碼,如果一直沒有端口號(hào)可用的話,則需要循環(huán)remaining次才能宣告端口號(hào)耗盡,也就是28232次。而如果按照正常的情況,因?yàn)橛衕int的存在,所以每次搜索從下一個(gè)待分配的端口號(hào)開始計(jì)算,以個(gè)位數(shù)的搜索就能找到端口號(hào)。如下圖所示:

          所以當(dāng)端口號(hào)耗盡后,Nginx的Worker進(jìn)程就沉浸在上述for循環(huán)中不可自拔,把CPU吃滿。

          為什么Gateway1調(diào)用Nginx沒有問題

          很簡(jiǎn)單,因?yàn)楣P者在Gateway1調(diào)用Nginx的時(shí)候設(shè)置了Keepalived,所以采用的是長(zhǎng)連接,就沒有這個(gè)端口號(hào)耗盡的限制。

          Nginx 后面有多臺(tái)機(jī)器的話

          由于是因?yàn)槎丝谔?hào)搜索導(dǎo)致CPU 100%,而且但凡有可用端口號(hào),因?yàn)閔int的原因,搜索次數(shù)可能就是1和28232的區(qū)別。

          因?yàn)槎丝谔?hào)限制是針對(duì)某個(gè)特定的遠(yuǎn)端server:port的。
          所以,只要Nginx的Backend有多臺(tái)機(jī)器,甚至同一個(gè)機(jī)器上的多個(gè)不同端口號(hào),只要不超過臨界點(diǎn),Nginx就不會(huì)有任何壓力。

          把端口號(hào)范圍調(diào)大

          比較無腦的方案當(dāng)然是把端口號(hào)范圍調(diào)大,這樣就能抗更多的TIME_WAIT。同時(shí)將tcp_max_tw_bucket調(diào)小,tcp_max_tw_bucket是kernel中最多存在的TIME_WAIT數(shù)量,只要port范圍 - tcp_max_tw_bucket大于一定的值,那么就始終有port端口可用,這樣就可以避免再次到調(diào)大臨界值得時(shí)候繼續(xù)擊穿臨界點(diǎn)。

          cat /proc/sys/net/ipv4/ip_local_port_range
          22768 61000
          cat /proc/sys/net/ipv4/tcp_max_tw_buckets
          20000

          開啟tcp_tw_reuse

          這個(gè)問題Linux其實(shí)早就有了解決方案,那就是tcp_tw_reuse這個(gè)參數(shù)。

          echo '1' > /proc/sys/net/ipv4/tcp_tw_reuse

          事實(shí)上TIME_WAIT過多的原因是其回收時(shí)間竟然需要1min,這個(gè)1min其實(shí)是TCP協(xié)議中規(guī)定的2MSL時(shí)間,而Linux中就固定為1min。

          #define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
          * state, about 60 seconds */

          2MSL的原因就是排除網(wǎng)絡(luò)上還殘留的包對(duì)新的同樣的五元組的Socket產(chǎn)生影響,也就是說在2MSL(1min)之內(nèi)重用這個(gè)五元組會(huì)有風(fēng)險(xiǎn)。為了解決這個(gè)問題,Linux就采取了一些列措施防止這樣的情況,使得在大部分情況下1s之內(nèi)的TIME_WAIT就可以重用。下面這段代碼,就是檢測(cè)此TIME_WAIT是否重用。

          __inet_hash_connect
          |->__inet_check_established
          static int __inet_check_established(......)
          {
          ......
          /* Check TIME-WAIT sockets first. */
          sk_nulls_for_each(sk2, node, &head->twchain) {
          tw = inet_twsk(sk2);
          // 如果在time_wait中找到一個(gè)match的port,就判斷是否可重用
          if (INET_TW_MATCH(sk2, net, hash, acookie,
          saddr, daddr, ports, dif)) {
          if (twsk_unique(sk, sk2, twp))
          goto unique;
          else
          goto not_unique;
          }
          }
          ......
          }

          而其中的核心函數(shù)就是twsk_unique,它的判斷邏輯如下:

          int tcp_twsk_unique(......)
          {
          ......
          if (tcptw->tw_ts_recent_stamp &&
          (twp == NULL || (sysctl_tcp_tw_reuse &&
          get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
          // 對(duì)write_seq設(shè)置為snd_nxt+65536+2
          // 這樣能夠確保在數(shù)據(jù)傳輸速率<=80Mbit/s的情況下不會(huì)被回繞
          tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2
          ......
          return 1;
          }
          return 0;
          }

          上面這段代碼邏輯如下所示:

          在開啟了tcp_timestamp以及tcp_tw_reuse的情況下,在Connect搜索port時(shí)只要比之前用這個(gè)port的TIME_WAIT狀態(tài)的Socket記錄的最近時(shí)間戳>1s,就可以重用此port,即將之前的1分鐘縮短到1s。同時(shí)為了防止?jié)撛诘男蛄刑?hào)沖突,直接將write_seq加上在65537,這樣,在單Socket傳輸速率小于80Mbit/s的情況下,不會(huì)造成序列號(hào)重疊(沖突)。
          同時(shí)這個(gè)tw_ts_recent_stamp設(shè)置的時(shí)機(jī)如下圖所示:

          所以如果Socket進(jìn)入TIME_WAIT狀態(tài)后,如果一直有對(duì)應(yīng)的包發(fā)過來,那么會(huì)影響此TIME_WAIT對(duì)應(yīng)的port是否可用的時(shí)間。
          開啟了這個(gè)參數(shù)之后,由于從1min縮短到1s,那么Nginx單臺(tái)對(duì)單Upstream可承受的TPS就從原來的470.5TPS(28232/60)一躍提升為28232TPS,增長(zhǎng)了60倍。
          如果還嫌性能不夠,可以配上上面的端口號(hào)范圍調(diào)大以及tcp_max_tw_bucket調(diào)小繼續(xù)提升tps,不過tcp_max_tw_bucket調(diào)小可能會(huì)有序列號(hào)重疊的風(fēng)險(xiǎn),畢竟Socket不經(jīng)過2MSL階段就被重用了。

          不要開啟tcp_tw_recycle

          開啟tcp_tw_recyle這個(gè)參數(shù)會(huì)在NAT環(huán)境下造成很大的影響,建議不開啟,具體見筆者的另一篇博客:

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

          Nginx upstream改成長(zhǎng)連接

          事實(shí)上,上面的一系列問題都是由于Nginx對(duì)Backend是長(zhǎng)連接導(dǎo)致。
          Nginx從 1.1.4 開始,實(shí)現(xiàn)了對(duì)后端機(jī)器的長(zhǎng)連接支持功能。在Upstream中這樣配置可以開啟長(zhǎng)連接的功能:

          upstream backend {
          server 127.0.0.1:8080;
          keepalive 32; # 后端長(zhǎng)連接數(shù)量
          keepalive_timeout 30s; # 設(shè)置后端連接的最大idle時(shí)間為30s
          }

          這樣前端和后端都是長(zhǎng)連接,大家又可以愉快的玩耍了。

          由此產(chǎn)生的風(fēng)險(xiǎn)點(diǎn)

          由于對(duì)單個(gè)遠(yuǎn)端ip:port耗盡會(huì)導(dǎo)致CPU吃滿這種現(xiàn)象。所以在Nginx在配置Upstream時(shí)候需要格外小心。假設(shè)一種情況,PE擴(kuò)容了一臺(tái)Nginx,為防止有問題,就先配一臺(tái)Backend看看情況,這時(shí)候如果量比較大的話擊穿臨界點(diǎn)就會(huì)造成大量報(bào)錯(cuò)(而應(yīng)用本身確毫無壓力,畢竟臨界值是470.5TPS(28232/60)),甚至在同Nginx上的非此域名的請(qǐng)求也會(huì)因?yàn)镃PU被耗盡而得不到響應(yīng)。多配幾臺(tái)Backend/開啟tcp_tw_reuse或許是不錯(cuò)的選擇。

          總結(jié)

          應(yīng)用再強(qiáng)大也還是承載在內(nèi)核之上,始終逃不出Linux內(nèi)核的樊籠。所以對(duì)于Linux內(nèi)核本身參數(shù)的調(diào)優(yōu)還是非常有意義的。如果讀過一些內(nèi)核源碼,無疑對(duì)我們排查線上問題有著很大的助力,同時(shí)也能指導(dǎo)我們避過一些坑!

          瀏覽 69
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  狠狠干AV| 青榴在线 | 天天日狠狠的 | 一级毛片视频免费软件 | 国产男女猛烈无遮挡在线喷水 |