国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 系統 > Linux > 正文

深入了解Linux端口重用這一特性

2024-08-27 23:54:46
字體:
來源:轉載
供稿:網友
  開篇我先考大家一個小問題,如果你的服務器上已經有個進程在 listen 6000 這個端口號了。那么該服務器上其它進程是否還能 bind 和 listen 該端口呢?
 
  我相信一定會有一部分同學會答說是不能的。因為很多人都遇到過“Address already in use”這個錯誤。而這個錯誤產生的原因就是端口已經被占用。
 
  但其實在 Linux 3.9 以上的內核版本里,是允許多個進程綁定到同一個端口號上。這就是我們今天要說的 REUSEPORT 新特性。
 
  本文中我們將闡述 REUSEPORT 是為了解決什么問題而產生的。如果有多個進程復用同一個端口,當用戶請求到達時內核是如何選一個進程進行響應的。學習完本文,你將深刻掌握這一提升服務器端性能的利器!
 
  一、 REUSEPORT 要解決的問題
  我覺得理解一個技術點的很重要的前提是要弄明白這個問題產生的背景,弄明白了背景再理解起技術點來就會容易許多。
 
  關于 REUSEPORT 特性產生的背景其實在 linux 的 commit 中提供的足夠詳細了(參見:https://github.com/torvalds/linux/commit/da5e36308d9f7151845018369148201a5d28b46d)。
 
  我就在這個 commit 中的信息的基礎上給大家展開了說一說。
 
  大家有過服務器端開發的經驗的同學都知道,一般一個服務是固定監聽某一個端口的。比如 Nginx 服務一般固定監聽 80 或 8080,Mysql 服務固定監聽 3306 等等。
 
  在網民數量還不夠多,終端設備也還沒有爆炸的年代里,一直是在使用的是端口不可重復被監聽的模式。但是到了 2010 年之后,Web 互聯網已經發展到了一個高潮,移動端的設備也開始迎來了大發展。這個時候端口不能重用的性能瓶頸就暴露出來了。
 
  應對海量流量的主要措施就是應用多進程模型。在端口不可被重復 bind 和 listen 的年代里,提供海量服務的多進程 Server 提供一般是采用如下兩種進程模型來工作。
 
  第一種是專門搞一個或多個進程服務 accept 新連接,接收請求,然后將請求轉給其它的 worker 進程來處理。
 
 
 
  這種多進程模型有兩個問題,首先第一個是 dispatcher 進程并不處理任務,需要轉交給 worker 進程來處理和響應。這會導致一次額外的進程上下文切換的開銷。第二個問題是如果流量特別大的時候 dispatcher 進程很容易成為制約整個服務 qps 提升的瓶頸。
 
  還有另一種多進程模型是多個進程復用一個 listen 狀態的 socket,多個進程同時從一個 socket 中 accept 請求來處理。Nginx 就采用的是這種模型。
 
 
 
  這種進程模型解決了第一個模型的問題。但是又帶來了新的問題。當 socket 收到一條連接的時候,不能把所有的 worker 進程都招呼起來。需要用鎖來保證唯一性,這樣就會有鎖競爭的問題。
 
  二、REUSEPORT 的誕生
  為了更高效地讓多個用戶態的進程接收和響應客戶端的請求。Linux 在 2013 年的 3.9 版本中提供了 REUSEPORT 新特性。
 
 
 
  內核詳細Commit代碼參見https://github.com/torvalds/linux/commit/da5e36308d9f7151845018369148201a5d28b46d 和https://github.com/torvalds/linux/commit/055dc21a1d1d219608cd4baac7d0683fb2cbbe8a
 
  該特性允許同一機器上的多個進程同時創建不同的 socket 來 bind 和 listen 在相同的端口上。然后在內核層面實現多個用戶進程的負載均衡。
 
  我們來看下內核是如何支持 reuseport 這個特性的。
 
  2.1 SO_REUSEPORT 設置
  想給自己的服務開啟 REUSEPORT 很簡單,就是給自己 server 里 listen 用的 socket 上加這么一句。(這里以 c 為 demo,其它語言可能會有差異,但基本上差不多)
 
  復制
  setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, ...);
   這行代碼在內核中對應的處理步驟就是把內核 socket 的 sk_reuseport 字段設置為相應的值,開啟的話是 1。
 
  復制
  //file: net/core/sock.c
  int sock_setsockopt(struct socket *sock, int level, int optname,
        char __user *optval, unsigned int optlen)
  {
   ...
   switch (optname) {
    ...
    case SO_REUSEPORT:
     sk->sk_reuseport = valbool;
    ...
   }
  }
   2.2 bind 時的處理
  內核在 inet_bind 時會調用到 inet_csk_get_port 函數。我們來看看 bind 時對 reuseport 的處理過程。來看源碼:
 
  復制
  //file: net/ipv4/inet_connection_sock.c
  int inet_csk_get_port(struct sock *sk, unsigned short snum)
  {
   ...
   //在綁定表(bhash)中查找,
   head = &hashinfo->bhash[inet_bhashfn(net, snum,
     hashinfo->bhash_size)];
   inet_bind_bucket_for_each(tb, &head->chain)
    //找到了,在一個命名空間下而且端口號一致,表示該端口已經綁定
    if (net_eq(ib_net(tb), net) && tb->port == snum)
     goto tb_found;
   ...
  }
   內核通過拉鏈哈希表的方式來管理所有的 bind 的 socket。其中 inet_bhashfn 是計算哈希值的函數。
 
 
 
  當計算找到哈希槽位以后,通過 inet_bind_bucket_for_each 來遍歷所有的 bind 狀態的 socket,目的是為了判斷是否沖突。
 
  net_eq(ib_net(tb), net) 這個條件表示網絡命名空間匹配,tb->port == snum 表示端口號匹配。這兩個條件加起來,就是說在同一個命名空間下,該端口已經被綁定過了。我們接著看 tb_found 里會干啥。
 
  復制
  //file: net/ipv4/inet_connection_sock.c
  int inet_csk_get_port(struct sock *sk, unsigned short snum)
  {
   ...
   if (((tb->fastreuse > 0 &&
         sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
        (tb->fastreuseport > 0 &&
         sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
       smallest_size == -1) {
    goto success;
   } else {
    //綁定沖突
    ......
   }
   我們看 tb->fastreuseport > 0 和 sk->sk_reuseport 這兩個條件。
 
  這兩個條件的意思是已經 bind 的 socket 和正在 bind 的 socket 都開啟了 SO_REUSEPORT 特性。符合條件的話,將會跳轉到 success 進行綁定成功的處理。也就是說,這個端口可以重復綁定使用!
 
  uid_eq(tb->fastuid, uid) 這個條件目的是安全性,必須要求相同的用戶進程下的 socket 才可以復用端口。避免跨用戶啟動相同端口來竊取另外用戶服務上的流量。
 
  2.3 accept 響應新連接
  當有多個進程都 bind 和 listen 了同一個端口的時候。有客戶端連接請求到來的時候就涉及到選擇哪個 socket(進程)進行處理的問題。我們再簡單看一下,響應連接時的處理過程。
 
  內核仍然是通過 hash + 拉鏈的方式來保存所有的 listen 狀態的 socket。
 
 
 
  查找 listen 狀態的 socket 的時候需要查找該哈希表。我們進入響應握手請求的時候進入的一個關鍵函數 __inet_lookup_listener 來看。
 
  復制
  //file: net/ipv4/inet_hashtables.c
  struct sock *__inet_lookup_listener(struct net *net,
          struct inet_hashinfo *hashinfo,
          const __be32 saddr, __be16 sport,
          const __be32 daddr, const unsigned short hnum,
          const int dif)
  {
   //所有 listen socket 都在這個 listening_hash 中
   struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];
 
  begin:
   result = NULL;
   hiscore = 0;
   sk_nulls_for_each_rcu(sk, node, &ilb->head) {
    score = compute_score(sk, net, hnum, daddr, dif);
    if (score > hiscore) {
     result = sk;
     hiscore = score;
     reuseport = sk->sk_reuseport;
     if (reuseport) {
      phash = inet_ehashfn(net, daddr, hnum,
             saddr, sport);
      matches = 1;
     }
    } else if (score == hiscore && reuseport) {
     matches++;
     if (((u64)phash * matches) >> 32 == 0)
      result = sk;
     phash = next_pseudo_random32(phash);
    }
   }
   ...
   return result;
  }
   其中 sk_nulls_for_each_rcu 是在遍歷所有 hash 值相同的 listen 狀態的 socket。注意看 compute_score 這個函數,這里是計算匹配分。當有多個 socket 都命中的時候,匹配分高的優先命中。我們來看一下這個函數里的一個細節。
 
  復制
  //file: net/ipv4/inet_hashtables.c
  static inline int compute_score(struct sock *sk, ...)
  {
   int score = -1;
   struct inet_sock *inet = inet_sk(sk);
 
   if (net_eq(sock_net(sk), net) && inet->inet_num == hnum &&
     !ipv6_only_sock(sk)) {
    //如果服務綁定的是 0.0.0.0,那么 rcv_saddr 為假
    __be32 rcv_saddr = inet->inet_rcv_saddr;
    score = sk->sk_family == PF_INET ? 2 : 1;
    if (rcv_saddr) {
     if (rcv_saddr != daddr)
      return -1;
     score += 4;
    }
    ...
   }
   return score;
  }
   那么匹配分解決的是什么問題呢?為了描述的更清楚,我們假設某臺服務器有兩個 ip 地址,分別是 10.0.0.2 和 10.0.0.3。我們啟動了如下三個服務器進程。
 
  復制
  A 進程:./test-server 10.0.0.2 6000
  B 進程:./test-server 0.0.0.0 6000
  C 進程:./test-server 127.0.0.1 6000
   那么你的客戶端如果指定是連接 10.0.0.2:6000,那么 A 進程會優先執行。因為當匹配到 A 進程的 socket 的時候,需要看一下握手包中的目的 ip 和這個地址是否匹配,確實匹配那得分就是 4 分,最高分。
 
  如果你指定連接的是 10.0.0.3,那么 A 進程就無法被匹配到。這個時候 B 進程監聽時指定的是 0.0.0.0(rcv_saddr 為 false),則不需要進行目的地址的比對,得分為 2。由于沒有更高分,所以這次命中的是 B 進程。
 
  C 進程只有你在本機訪問,且指定 ip 使用 127.0.0.1 才能命中,得分也是為 4 分。外部服務器或者是在本機使用其它 ip 都無法訪問的到。
 
  如果當多個 socket 的匹配分一致,通過調用 next_pseudo_random32 進行隨機的選擇。在內核態做了負載均衡的事情,選定一個具體的 socket,避免了多個進程在同一個 socket 上的鎖競爭。



  

(編輯:武林網)

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 湘乡市| 娱乐| 晋州市| 永善县| 梅州市| 武穴市| 延吉市| 宜春市| 德保县| 上思县| 汉源县| 砀山县| 昌图县| 怀安县| 山丹县| 江北区| 大足县| 宜阳县| 二连浩特市| 兴国县| 永清县| 西乌珠穆沁旗| 敦化市| 马尔康县| 望奎县| 金溪县| 淅川县| 湟源县| 天长市| 湖北省| 大埔县| 绥化市| 崇信县| 凭祥市| 汾西县| 晋中市| 天台县| 治多县| 襄垣县| 玉田县| 保定市|