Nginx vs Envoy vs MOSN 平滑升級原理解析
前言
本文是對 Nginx、Envoy 及 MOSN 的平滑升級原理區別的分析,適合對 Nginx 實現原理比較感興趣的同學閱讀,需要具備一定的網路程式設計知識。
平滑升級的本質就是 listener fd 的遷移,雖然 Nginx、Envoy、MOSN 都提供了平滑升級支援,但是鑑於它們程式模型的差異,反映在實現上還是有些區別的。這裡來探討下它們其中的區別,並著重介紹 Nginx 的實現。
Nginx
相信有很多人認為 Nginx 的 reload 操作就能完成平滑升級,其實這是個典型的理解錯誤。實際上 reload 操作僅僅是平滑重啟,並沒有真正的升級新的二進位制檔案,也就是說其執行的依然是老的二進位制檔案。
Nginx 自身也並沒有提供平滑升級的命令選項,其只能靠手動觸發訊號來完成。具體正確的操作步驟可以參考這裡:Upgrading Executable on the Fly,這裡只分析下其實現原理。
Nginx 的平滑升級是通過 fork
+ execve
這種經典的處理方式來實現的。準備升級時,Old Master 程式收到訊號然後 fork
出一個子程式,注意此時這個子程式執行的依然是老的映象檔案。緊接著這個子程式會通過 execve
呼叫執行新的二進位制檔案來替換掉自己,成為 New Master。
那麼問題來了:New Master 啟動時按理說會執行 bind
+ listen
等操作來初始化監聽,而這時候 Old Master 還沒有退出,埠未釋放,執行 execve
時理論上應該會報:Address already in use
錯誤,但是實際上這裡卻沒有任何問題,這是為什麼?
因為 Nginx 在 execve
的時候壓根就沒有重新 bind
+ listen
,而是直接把 listener fd 新增到 epoll
的事件表。因為這個 New Master 本來就是從 Old Master 繼承而來,自然就繼承了 Old Master 的 listener fd,但是這裡依然有一個問題:該怎麼通知 New Master 呢?
環境變數。execve
在執行的時候可以傳入環境變數。實際上 Old Master 在 fork
之前會將所有 listener fd 新增到 NGINX
環境變數:
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);
...
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);
return pid;
}
Nginx 在啟動的時候,會解析 NGINX
環境變數:
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);
}
一旦檢測到是繼承而來的 socket,那就說明已經開啟了,不會再繼續 bind
+ listen
了:
ngx_int_t
ngx_open_listening_sockets(ngx_cycle_t *cycle)
{
...
/* TODO: configurable try number */
for (tries = 5; tries; tries--) {
failed = 0;
/* for each listening socket */
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
...
if (ls[i].inherited) {
/* TODO: close on exit */
/* TODO: nonblocking */
/* TODO: deferred accept */
continue;
}
...
ngx_log_debug2(NGX_LOG_DEBUG_CORE, log, 0,
"bind() %V #%d ", &ls[i].addr_text, s);
if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) {
...
}
...
}
}
if (failed) {
ngx_log_error(NGX_LOG_EMERG, log, 0, "still could not bind()");
return NGX_ERROR;
}
return NGX_OK;
}
Envoy
Envoy 使用的是單程式多執行緒模型,其侷限就是無法通過環境變數來傳遞 listener fd。因此 Envoy 採用的是 UDS(unix domain sockets)方案。當 New Envoy 啟動完成後,會通過 UDS 向 Old Envoy 請求 listener fd 副本,拿到 listener fd 之後開始接管新來的連線,並通知 Old Envoy 終止執行。
file descriptor 是可以通過
sendmsg/recvmsg
來傳遞的。
MOSN
MOSN 開源地址:https://github.com/mosn/mosn
MOSN 的方案和 Envoy 類似,都是通過 UDS 來傳遞 listener fd。但是其比 Envoy 更厲害的地方在於它可以把老的連線從 Old MOSN 上遷移到 New MOSN 上。也就是說把一個連線從程式 A 遷移到程式 B,而保持連線不斷!!!厲不厲害?聽起來很簡單,但是實現起來卻沒那麼容易,比如資料已經被拷貝到了應用層,但是還沒有被處理,怎麼辦?這裡面有很多細節需要處理。它子所以能做到這種層面,靠的也是核心的 sendmsg/recvmsg
技術。
SCM_RIGHTS - Send or receive a set of open file descriptors from another process. The data portion contains an integer array of the file descriptors. The passed file descriptors behave as though they have been created with dup(2). http://linux.die.net/man/7/unix
這裡有一個 Go 實現的小 Demo: tcp 連結遷移。
對比
Nginx 的實現是相容性最強的,因為 Envoy 和 MOSN 都依賴 sendmsg/recvmsg
系統呼叫,需要核心 3.5+ 支援。MOSN 的難度最高,算得上是真正的無損升級,而 Nginx 和 Envoy 對於老的連線,僅僅是實現 graceful shutdown,嚴格來說是有損的。這對於 HTTP(通過 Connection: close
) 和 gRPC(GoAway Frame) 協議支援很友好,但是遇到自定義的 TCP 協議就抓瞎了。如果遇到客戶端沒有處理 close
異常,很容易發生 socket fd 洩露問題。
本文作者 ms2008,轉載自Nginx vs Envoy vs Mosn 平滑升級原理解析。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- nginx平滑升級Nginx
- nginx實現平滑升級Nginx
- Nginx如何進行平滑升級Nginx
- nginx 命令和訊號及平滑升級Nginx
- 生產環境nginx平滑升級演示Nginx
- Nginx VS Traefik ComparisonNginx
- Nginx range filter模組數字錯誤漏洞修復 (Nginx平滑升級)NginxFilter
- MOSN熱升級邏輯淺析
- 1分鐘搞定 Nginx 版本的平滑升級與回滾Nginx
- JuiceFS CSI:Mount Pod 的平滑升級及其實現原理UI
- Nginx升級Nginx
- 生鮮電商戰事升級:叮咚買菜VS每日優鮮VS社群團購(附下載)
- Playwright VS Selenium VS Puppeteer VS Cypress
- 升級csproj檔案為vs2017工程格式(SDK樣式)
- java實現“資料平滑升級”Java
- DNS 解析器效能比較:CloudFlare vs Google vs Quad9DNSCloudGo
- vs 2017 vs code
- Airflow vs. Luigi vs. Argo vs. MLFlow vs. KubeFlowAIUIGo
- Linux平滑編譯升級php至5.5.0Linux編譯PHP
- Axum vs Actix vs Rocket
- RDBMS VS XML VS NoSQLXMLSQL
- nginx升級與回退Nginx
- Nginx如何升級OpensslNginx
- 如何解除安裝VS 2017之前版本比如VS 2013、VS2015、 VS vNext?
- [譯]await VS return VS return awaitAI
- The SQL vs NoSQL Difference: MySQL vs MongoDBMySqlMongoDB
- HashSet vs. TreeSet vs. LinkedHashSet
- Redux vs Mobx系列(-):immutable vs mutableRedux
- spring vs yii2 vs LaravelSpringLaravel
- coca 搭配 in vs on vs at | page1
- coca 搭配 in vs on vs at | page3
- JavaScript 的 4 種陣列遍歷方法: for VS forEach() VS for/in VS for/ofJavaScript陣列
- 從 Nginx 遷移到 Envoy ProxyNginx
- nginx 版本升級 轉載Nginx
- Nginx配置以及熱升級Nginx
- VS Code 輕量級外掛推薦
- ABAP vs Java, 蛙泳 vs 自由泳Java
- When to use var vs let vs const in JavaScriptJavaScript