轉載:https://www.cnblogs.com/zhuyeshen/p/12061495.html
一、背景
在服務器開發過程中,難免需要重啟服務加載新的代碼或配置,如果能夠保證server重啟的過程中服務不間斷,那重啟對於業務的影響可以降為0。最近調研了一下nginx平滑重啟,覺得很有意思,記錄下來供有興趣的同學查閱。
二、重啟流程
- 重啟意味着新舊接替,在交接任務的過程中勢必會存在新舊server並存的情形,因此,重啟的流程大致為:
- 啟動新的server
- 新舊server並存,兩者共同處理請求,提供服務
- 舊的server處理完所有的請求之后優雅退出
- 這里,最主要的問題在於如何保證新舊server可以並存,如果重啟前后的server端口一致,如何保證兩者可以監聽同一端口。
三、nginx實現
為了驗證nginx平滑重啟,筆者首先嘗試nginx啟動的情形下再次開啟一個新的server實例,結果如圖:
很明顯,重新開啟server實例是行不通的,原因在於新舊server使用了同一個端口80,在未開始socket reuseport選項復用端口時,bind系統調用會出錯。nginx默認bind重試5次,失敗后直接退出。而nginx需要監聽IPV4地址0.0.0.0和IPV6地址[::],故圖中打印出10條emerg日志。
接下來就開始嘗試平滑重啟命令了,一共兩條命令:
1
2
|
kill
-USR2 `
cat
/var/run/nginx
.pid`
kill
-QUIT `
cat
/var/run/nginx
.pid.oldbin`
|
第一條命令是發送信號USR2給舊的master進程,進程的pid存放在/var/run/nginx.pid文件中,其中nginx.pid文件路徑由nginx.conf配置。
第二條命令是發送信號QUIT給舊的master進程,進程的pid存放在/var/run/nginx.pid.oldbin文件中,隨后舊的master進程退出。
那么問題來了,為什么舊的master進程的pid存在於兩個pid文件之中?事實上,在發送信號USR2給舊的master進程之后,舊的master進程將pid重命名,原先的nginx.pid文件rename成nginx.pid.oldbin。這樣新的master進行就可以使用nginx.pid這個文件名了。
先執行第一條命令,結果如圖:
不錯,新舊master和worker進程並存了。 再來第二條命令,結果如圖:
如你所見,舊的master進程8527和其worker進程全部退出,只剩下新的master進程12740。
不由得產生困惑,為什么手動開啟一個新的實例行不通,使用信號重啟就可以達到。先看下nginx log文件:
除了之前的錯誤日志,還多了一條notice,意思就是繼承了sockets,fd值為6,7。 隨着日志翻看nginx源碼,定位到nginx.c/ngx_exec_new_binary函數之中,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
ngx_pid_t
ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv)
{
...
ctx.path = argv[0];
ctx.name =
"new binary process"
;
ctx.argv = argv;
n = 2;
env
= ngx_set_environment(cycle, &n);
...
var = ngx_alloc(sizeof(NGINX_VAR)
+ cycle->listening.nelts * (NGX_INT32_LEN + 1) + 2,
cycle->log);
...
p = ngx_cpymem(var, NGINX_VAR
"="
, sizeof(NGINX_VAR));
ls
= cycle->listening.elts;
for
(i = 0; i < cycle->listening.nelts; i++) {
p = ngx_sprintf(p,
"%ud;"
,
ls
[i].fd);
}
*p =
'\0'
;
env
[n++] = var;
...
env
[n] = NULL;
...
ctx.envp = (char *const *)
env
;
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
if
(ngx_rename_file(ccf->pid.data, ccf->oldpid.data) == NGX_FILE_ERROR) {
...
return
NGX_INVALID_PID;
}
pid = ngx_execute(cycle, &ctx);
if
(pid == NGX_INVALID_PID) {
if
(ngx_rename_file(ccf->oldpid.data, ccf->pid.data)
== NGX_FILE_ERROR)
{
...
}
}
...
return
pid;
}
|
函數的流程為
- 將舊的master進程監聽的所有fd,拷貝至新master進程的env環境變量NGINX_VAR。
- rename重命名pid文件
- ngx_execute函數fork子進程,execve執行命令行啟動新的server。
- 在server啟動流程之中,涉及到環境變量NGINX_VAR的解析,ngx_connection.c/ngx_add_inherited_sockets具體代碼為:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
static ngx_int_t
ngx_add_inherited_sockets(ngx_cycle_t *cycle)
{
...
inherited = (u_char *) getenv(NGINX_VAR);
if
(inherited == NULL) {
return
NGX_OK;
}
if
(ngx_array_init(&cycle->listening, cycle->pool, 10,
sizeof(ngx_listening_t))
!= NGX_OK)
{
return
NGX_ERROR;
}
for
(p = inherited,
v
= p; *p; p++) {
if
(*p ==
':'
|| *p ==
';'
) {
s = ngx_atoi(
v
, p -
v
);
...
v
= p + 1;
ls
= ngx_array_push(&cycle->listening);
if
(
ls
== NULL) {
return
NGX_ERROR;
}
ngx_memzero(
ls
, sizeof(ngx_listening_t));
ls
->fd = (ngx_socket_t) s;
}
}
...
ngx_inherited = 1;
return
ngx_set_inherited_sockets(cycle);
}
|
函數流程為:
解析環境變量NGINX_VAR的值,獲取fd存入數組
fd對應的socket設為ngx_inherited,保存這些socket的信息。
也就是說,新的server壓根就沒重新bind端口listen,這些fd狀態和值都是新的master進程fork時帶過來的,新的master進程監聽處理繼承來的文件描述符即可,這里比較關鍵的一點在於listen socket文件描述符通過ENV傳遞。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。