前陣子在某台伺服器發現無法正常的解析網域,導致在呼叫 API 的時候一直失敗,這是一個非常特殊的案例,實際案例解析後其實就是 udp 協定的缺陷
問題:
- 該主機無法對外解析 Domain
釐清:
- 從該主機可正常交握 TCP/IP
- 從該主機可正常運行
- 該主機無其他異常
- 該主機指定 DNS Server 皆正常 (中華電信/Google/阿里巴巴)
- 本機 hosts 解析正常
- 使用 dig 出現錯誤訊息
$ dig www.google.com.tw dig isc_socket_bind
分析:
從上述狀況了解到只有 dns 解析出問題,而 dns 是使用 udp 53 協定進行溝通,排除了 dns server 的問題,開始分析本機是否哪裡異常。
- 首先確認本機是否有主動開啟 udp service 被利用漏洞攻擊。
$ ss -tunl tcp LISTEN 0 128 127.0.0.1:9000 *:* tcp LISTEN 0 50 *:3306 *:* tcp LISTEN 0 128 *:80 *:* tcp LISTEN 0 128 *:22 *:* tcp LISTEN 0 128 *:443 *:*
從上述確認為正常的 LNMP 架構,並且沒有主動 Listen udp
- 確認防火牆是否有正常控管 rules;除 80, 443 以外不該對外開放。
- 確認被動的 udp session
$ ss -tunp | grep udp udp 0 0 1x9.8x.2x7.1x6:53678 168.95.1.1:53 ESTABLISHED 29519/php-fpm udp 0 0 1x9.8x.2x7.1x6:34094 168.95.1.1:53 ESTABLISHED 29520/php-fpm udp 0 0 1x9.8x.2x7.1x6:41134 168.95.1.1:53 ESTABLISHED 29518/php-fpm udp 0 0 1x9.8x.2x7.1x6:37038 168.95.1.1:53 ESTABLISHED 7725/php-fpm udp 0 0 1x9.8x.2x7.1x6:58542 168.95.1.1:53 ESTABLISHED 7736/php-fpm udp 0 0 1x9.8x.2x7.1x6:56622 168.95.1.1:53 ESTABLISHED 29516/php-fpm udp 0 0 1x9.8x.2x7.1x6:49838 168.95.1.1:53 ESTABLISHED 29528/php-fpm udp 0 0 1x9.8x.2x7.1x6:43438 168.95.1.1:53 ESTABLISHED 1619/php-fpm udp 0 0 1x9.8x.2x7.1x6:34222 168.95.1.1:53 ESTABLISHED 29531/php-fpm udp 0 0 1x9.8x.2x7.1x6:56878 168.95.1.1:53 ESTABLISHED 1556/php-fpm udp 0 0 1x9.8x.2x7.1x6:51502 168.95.1.1:53 ESTABLISHED 29526/php-fpm udp 552 0 1x9.8x.2x7.1x6:32942 8.8.8.8:53 ESTABLISHED 7721/php-fpm udp 0 0 1x9.8x.2x7.1x6:42414 168.95.1.1:53 ESTABLISHED 29523/php-fpm udp 0 0 1x9.8x.2x7.1x6:35886 168.95.1.1:53 ESTABLISHED 7731/php-fpm udp 0 0 1x9.8x.2x7.1x6:42030 168.95.1.1:53 ESTABLISHED 7740/php-fpm udp 0 0 1x9.8x.2x7.1x6:45998 168.95.1.1:53 ESTABLISHED 29531/php-fpm udp 0 0 1x9.8x.2x7.1x6:34734 168.95.1.1:53 ESTABLISHED 7723/php-fpm udp 0 0 1x9.8x.2x7.1x6:45102 168.95.1.1:53 ESTABLISHED 29523/php-fpm udp 0 0 1x9.8x.2x7.1x6:59694 168.95.1.1:53 ESTABLISHED 7735/php-fpm udp 0 0 1x9.8x.2x7.1x6:53294 168.95.1.1:53 ESTABLISHED 7723/php-fpm udp 0 0 1x9.8x.2x7.1x6:57134 168.95.1.1:53 ESTABLISHED 29531/php-fpm udp 552 0 1x9.8x.2x7.1x6:56366 168.95.1.1:53 ESTABLISHED 29517/php-fpm udp 0 0 1x9.8x.2x7.1x6:58926 168.95.1.1:53 ESTABLISHED 29523/php-fpm udp 0 0 1x9.8x.2x7.1x6:39854 168.95.1.1:53 ESTABLISHED 4915/php-fpm udp 552 0 1x9.8x.2x7.1x6:39086 8.8.8.8:53 ESTABLISHED 4917/php-fpm udp 552 0 1x9.8x.2x7.1x6:52910 168.95.1.1:53 ESTABLISHED 29533/php-fpm udp 552 0 1x9.8x.2x7.1x6:43694 168.95.1.1:53 ESTABLISHED 7729/php-fpm udp 552 0 1x9.8x.2x7.1x6:55214 168.95.1.1:53 ESTABLISHED 7733/php-fpm udp 552 0 1x9.8x.2x7.1x6:41902 8.8.8.8:53 ESTABLISHED 1555/php-fpm udp 0 0 1x9.8x.2x7.1x6:48686 168.95.1.1:53 ESTABLISHED 29523/php-fpm udp 0 0 1x9.8x.2x7.1x6:42798 168.95.1.1:53 ESTABLISHED 1618/php-fpm udp 0 0 1x9.8x.2x7.1x6:37678 168.95.1.1:53 ESTABLISHED 7730/php-fpm
從這邊看到問題的來源了,製造者是 php-fpm,而且殘留的數量很可觀
$ ss -tun | grep udp | grep php-fpm | wc -l 6312
從程式架構上去了解發現會主動去 call API 進行溝通,而 API URL 是使用網域網址,而問題的產生點就是該主機位於香港,第一順位的 dns 是使用中華電信的 168.95.1.1,光是 dns 就選擇了跨國的 dns,這是一項錯誤的示範
而為什麼選擇了跨國的 dns 會造成這樣的現象?主因是 UDP 協定的缺陷,讓我們重溫一下 UDP協定
UDP 本身是一個非可靠、非連線型的傳輸協定,發送端和接收端不需要建立連線,也不會進行驗證,對於發送端只需要丟出一個封包,而接收端如果收到就回應,沒收到他也不管你
針對 UDP 這種特性,小弟都稱他為 射後不理的協定 潮爽der,所以通常 udp 協定都拿來做為近端的溝通,並且速度優於 TCP 非常多。
但這個特性又和這個案例有什麼關聯?!因為 udp 適合用於近端網路,但此案例 dns server 設為跨國網路,這就不只近端了已經是非常遠並且複雜的環境。
當 php-fpm 執行 php 呼叫 API 網域,如果網路環境不佳可能出現以下狀況:
- DNS 太遠了回應太慢,導致發送端將 udp session close
- UDP 在傳輸的過程中因為太慢殘留太久被中繼 Route / Gateway 直接回收了,導致 DNS 根本沒有收到這個 udp 連線
然而 udp session 就一直留在發送端主機的一直傻傻地等待 DNS Server 回應,或是被放棄的 udp session,就造成了一個死掉的 UDP session
你說,不能設定 timeout 嗎!?
UDP 本身並沒有 timeout 可言,因為無法驗證連線,即使有 timeout 也不可靠,所以你看到的 udp 都是 ESTABLISHED。
UDP 的詳細狀態解析
好,繼續解析目前殘留在發送端的 udp session 狀態
在 Linux 中 /proc/net/udp 這裡會列出存在的 udp session,ss 工具也是從這邊蒐集資料的
$ cat /proc/net/udp sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops 0: 3A226F1A:C8DC 0B50100A:0035 01 00000000:00000228 00:00000000 00000000 497 0 160696905 2 ffff880134b3f040 0 0: 3A226F1A:D6DC 0B50100A:0035 01 00000000:00000000 00:00000000 00000000 497 0 156340394 2 ffff8800816db140 0 0: 3A226F1A:BE5C 0B50100A:0035 01 00000000:00000000 00:00000000 00000000 497 0 156073421 2 ffff88020bb0b500 0 1: 3A226F1A:E1DD 0C50100A:0035 01 00000000:00000228 00:00000000 00000000 497 0 165299140 2 ffff8800d42363c0 0 2: 3A226F1A:B75E 0B50100A:0035 01 00000000:00000228 00:00000000 00000000 497 0 152373920 2 ffff880216be7740 0 3: 3A226F1A:E6DF 0B50100A:0035 01 00000000:00000000 00:00000000 00000000 497 0 159659260 2 ffff88014df260c0 0
- sl:kernel hash slot 的 socket
- local_address:本機發送的 IP:Port
- rem_address:遠端主機的 IP:Port
- st:socket 的狀態,不過在UDP協議中似乎沒有甚麼用處
- tx_queue:在 kernel 中傳出的 udp 封包占用的記憶體用量
- rx_queue:在 kernel 中進入的 udp 封包占用的記憶體用量
- tr tm->when retrnsmt:未被UDP協定使用
- uid:創建此 session 的 uid (497為nginx)
- timeout:未被UDP協定使用
- inode:此 socket 占用 inode 的位置
- ref:計算 socket 數量
- pointer:struct sock 在記憶體中的位址
- drops:socket 被丟棄的數量
從以上可以看到殘留的 udp session 有些是傳出,有些是進入的 udp session,可能的狀況是出去時延遲了,而回來也延遲了,所以導致兩種狀況都有。
探討問題的解決
針對像這種 UDP 協議的缺陷,而 DNS 又是必要的服務,如果無法避免像這樣的網路環境,小弟目前有幾種的建議方式解決
- 在近端網路架設一台 DNS Cache Server,專門提供近端伺服器進行解析減少 udp 來往的問題。
- 在 Server 中使用 dnsmasq 來 cache 解析過的網址,減少 udp query。
- 設定 crontab 定期檢查會產生 udp session 的服務,超過即釋放 buffer 裡面的 udp session
針對此案例小弟是使用方案3,並且寫了一隻 udp_max_session 去檢查 php-fpm 所產生的 udp session,當到達一定的數量(100),就進行釋放
詳細可參考 github/shazi7804
當然這個案例是我個人目前的了解以及解決辦法,也希望大家如果有遇過相同的問題可以提供更好的解決方式或與我討論!