天天看點

超級NB的黑科技,實作socket程序間遷移!

今天介紹一個可以拿出去吹牛的功能:實作socket句柄在程序之間遷移!

我們的伺服器上,運作着大量的server執行個體(instance)。這些instance,每個都要承載着數十萬的連接配接和非常繁忙的網絡請求。能夠把這樣的連接配接數,這樣的流量,玩弄于股掌之間,是每個網際網路程式員的夢想。

但軟體總是要更新的,每當更新的時候,就需要先停掉原來的instance,然後再啟動一個新的。在這一停一起之間,數十秒就過去了,更不要說JAVA這種啟動時間就能生個孩子的速度了。

傳統的做法,是先把這個instance從負載均衡上面摘除,然後啟動起來再加上;對于微服務來說,就要先隔離,然後啟動後再取消隔離。這些操作,對于海量應用來說,就是個噩夢。

1. 零停機更新

有沒有一種方法,能夠把一個程序所挂載的連接配接(socket),轉移到另外一個程序之上呢?這樣,我在更新的時候,就可可以先啟動一個更新版本的程序,然後把老程序的socket,one by one的給轉移過去。

實作零停機更新。

這個是可以的。Facebook就實踐過類似的技術,它們把這項技術,叫做

Socket Takeover

。千萬别用百度搜這個關鍵字,你得到的可能是一堆垃圾。

這麼牛x的技術,還這麼有用,為什麼就沒人科普呢?别問我,我也不知道,可能大家現在都在糾結怎麼研究茴香豆的茴字寫法,沒時間幹正事吧。

那今天就由

xjjdog

來介紹一下吧,順便增加一下大家以後的吹牛資本。

這個牛x的功能,是由Linux一對底層的

系統調用

函數所實作的:

sendmsg()

recvmsg()

。我們一般在發送網絡資料包的時候,一般會使用send函數,但send函數隻有在socket處于連接配接狀态時才可以使用;與之不同的是,sendmsg在任何時候都可以使用。

2. 技術要點

在c語言網絡程式設計中,首先要通過

listen

函數,來注冊監聽位址,然後再用accept函數接收新連接配接。比如:

int listen_fd = socket(addr->ss_family, SOCK_STREAM, 0);
...
bind(listen_fd, (struct sockaddr *) addr, addrlen);
...
int accept_fd = accept(fd, (struct sockaddr *) &addr, &addrlen);
複制代碼           

我們首先要做的,就是把

listen_fd

,從一個程序,傳遞到另外一個程序中去。怎麼發送呢?肯定是要通過一個通道的。在Linux上,那就是UDS,全稱

Unix Domain Sockets

2.1 Unix Domain Sockets監聽

UDS(Unix Domain Sockets)在Linux上的表現,是一個檔案。相比較于普通socket監聽在端口上,一個程序也可以監聽在一個UDS檔案上,比如

/tmp/xjjdog.sock

。由于通過這個檔案進行資料傳輸,并不需要走網卡等實體裝置,是以通過UDS傳輸資料,速度是非常快的。

但今天我們不關心它有多塊,而是關心它多有用。通過bind函數,我們同樣可以通過這個檔案接收連接配接,就像端口接收連接配接一樣。

struct sockaddr_un addr;
char *path="/tmp/xjjdog.sock";
int err, fd;
fd = socket(AF_UNIX, SOCK_STREAM, 0);
memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, path, strlen(path));
addrlen = sizeof(addr.sun_family) + strlen(path);
err = bind(fd, (struct sockaddr *) &addr, addrlen);
...
accept_fd = accept(fd, (struct sockaddr *) &addr, &addrlen);
複制代碼           
超級NB的黑科技,實作socket程式間遷移!

這樣。其他的程序,就可以通過兩種不同的方式,來連接配接我們的服務。

  1. 通過端口:進行正常的服務,輸出正常的業務資料。執行正常業務
  2. 通過UDS:開始接收

    listen_fd

    accept_fd

    們。執行不停機遷移socket業務

2.2 fd遷移技術要點

怎麼遷移呢?我們關鍵看第二步。

實際上,當新更新的服務通過UDS連接配接上來,我們就開始使用sendmsg函數,将

listen_fd

給轉移過去。

我們來看一下sendmsg這個函數的參數。

ssize_t sendmsg(
    int socket,
    const struct msghdr *message,
    int flags
);
複制代碼           

socket可以了解為我們的UDS連接配接。關鍵在于

msghdr

這個結構體。

struct msghdr {
    void            *msg_name;      /* optional address */
    socklen_t       msg_namelen;    /* size of address */
    struct          iovec *msg_iov; /* scatter/gather array */
    int             msg_iovlen;     /* # elements in msg_iov */
    void            *msg_control;   /* ancillary data, see below */
    socklen_t       msg_controllen; /* ancillary data buffer len */
    int             msg_flags;      /* flags on received message */
};
複制代碼           

其中,

msg_iov

表示要正常發送的資料,比如

HelloWord

;除此之外,還有兩個

ancillary

(附屬的) 的變量,提供了附加的功能,那就是變量

msg_control

msg_controllen

。其中,

msg_control

又指向了另外一個結構體

cmsghdr

struct cmsghdr {
    socklen_t cmsg_len;    /* data byte count, including header */
    int       cmsg_level;  /* originating protocol */
    int       cmsg_type;   /* protocol-specific type */
    /* followed by */
    unsigned char cmsg_data[];
};
複制代碼           

在這個結構體中,有一個叫做

cmsg_type

的成員變量,是我們實作socket遷移的關鍵。

它共有三個類型。

  • SCM_RIGHTS

  • SCM_CREDENTIALS

  • SCM_SECURITY

SCM_RIGHTS

就是我們所需要的,它允許我們從一個程序,發送一個檔案句柄到另外一個程序。

超級NB的黑科技,實作socket程式間遷移!
struct msghdr msg;
...
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;

//socket fd清單,設定在cmsg_data上
int *fds = (int *) CMSG_DATA(cmsg);
複制代碼           

依靠

sendmsg

函數,socket句柄就發送到另外一個程序了。

3. 接收和還原

同樣的,

recvmsg

函數,将會接收這部分資料,然後将其還原成

cmsghdr

結構體。然後我們就可以從

cmsg_data

中擷取句柄清單。

為什麼能這麼做呢?因為socket句柄,在某個程序裡,其實隻是一個引用。真正的fd句柄,其實是放在核心中的。所謂的遷移,隻不過是把一個指針,從一個程序中去掉,再加到另外一個程序中罷了。

fd句柄的屬性,有兩種情況。

  • 監聽fd,直接調用

    accept

    函數作用在fd上即可
  • 普通fd,需要将其還原成正常的socket
超級NB的黑科技,實作socket程式間遷移!

圖檔來自論文:(Zero Downtime Release: Disruption-free Load Balancing of a Multi-Billion User Website)。

對于普通fd,肯定要調用與原新連接配接到來時

相同的

代碼邏輯。是以,一個大體的遷移過程,包括:

  1. 首先遷移listener fd到新程序,并開啟監聽,以便新程序能快速接收新的請求。如果我們開啟了

    SO_REUSEADDR

    選項,新老服務甚至能夠一起進行服務
  2. 等待新程序預熱之後,停掉原程序的監聽
  3. 遷移原老程序中的大量socket,這些socket可能有數萬條,最好編碼能看到遷移進度
  4. 新程序接收到這些socket,陸續将其還原為正常的連接配接。相當于略過了accept階段,直接就擷取了socket清單
  5. 遷移完畢,老程序就空轉了,此時可以安全的停掉

4. End

繼續閱讀