實現不停機發布
有一個后台項目由於並發量不高所以只部署了一台機器,但是如果要升級的話其他人就用不了了。為了解決不影響其他同事正常使用,我想做一個不停機發布的功能。
具體原理就是通過nginx負載均衡來實現,當停了一台還有另外一台可以提供服務,這樣就做到了不停機發布。
我修改nginx.conf文件,修改點如下:
location /xx {
proxy_read_timeout 600;
#proxy_pass http://localhost:9080;
proxy_pass http://xx_manage; ##這個地方配置了負載均衡
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream error timeout http_500 http_404 http_502 http_503 non_idempotent;
}
upstream xx_manage {
ip_hash; ##為了解決session問題使用了ip_hash
server 127.0.0.1:9080;
server 127.0.0.1:9082;
}
配置后,經過測試發現如果一台服務停機了可以進行自動切換。但是實際使用過程中有同事反映經常有跳轉到登錄頁面的情況。
聲明:本博客不停機發布功能比較簡單,就是通過nginx實現路由切換,由於要解決session問題(即登錄了后不要重新登錄)所以使用的是ip_hash方式。當然如果要解決session問題有很多其他方法,比如cookiebase、redis集中式session。但由於本后台系統不是頻繁訪問量的內部系統,所以采用ip_hash成本最小的方式來解決不停機發布問題,而本文是該問題解決的場景記錄。本文涉及技術不會很高大上但有一些細節要注意。我也想說的是從產品的角度(考慮成本、可用性、體驗性)來解決這個問題而不是純粹追求高大上技術。
排查自動登出的情況
問題描述
場景是這樣的,我登錄網站,從日志發現是訪問到了A服務器。當我在網站上點擊其他頁面的時候發現跳轉到登錄頁面讓我重新登錄。nginx負載均衡我配置的是ip_hash的方式按道理來說,如果我上次訪問A服務器(經過ip_hash計算落到A服務器),那么以后都會落到A服務器。現在當我點擊其他頁面跳轉登錄頁面說明點其他頁面時訪問到了B服務器,這個現象很怪異。
我的分析
- nginx層前面沒有其他負載均衡服務器意味着到達nginx請求的ip是固定的,也就是經過
ip_hash
會路由到同一各服務。 - nginx跟應用服務器之間沒有其他負載均衡服務器,即經過nginx處理后直接分發到服務器不會在路由了。
- 檢查了nginx的配置,發現有如下的配置,意味着如果A服務器執行請求出現
500\404\502
這些狀態碼,就會重定向到另一個服務,而另一個服務器是沒有session的,那么就會跳轉到重新登錄頁面。
proxy_next_upstream error timeout http_500 http_404 http_502 http_503 non_idempotent;
我的目標是,如果一台服務掛掉之后自動failover到另一台機器,但是如果某個頁面出現異常不需要重定向(另一台機器代碼也是一樣的,沒必要重定向,重定向還可以導致重新登錄的情況)。所以我需要在proxy_next_upstream
那里把這兩種情況區分開來,即要弄明白服務掛掉和業務有異常返回的狀態碼分別是什么?
我做的操作
區分業務異常和停機時nginx的返回碼
為了區分業務異常和服務掛掉nginx的返回碼的區別,我在虛擬機里啟動了nginx,並且部署了兩個springboot項目。springboot項目代碼參考 后面的參考->spring boot后台請求代碼
,這里nginx配置參考參考->虛擬機里nginx.conf配置
。
經過測試查看對應nginx日志:發現業務異常throw new RuntimeException()
nginx返回500錯誤,如果服務停機的話返回的502錯誤。
對應nginx日志如下:
502結果圖如下:
測試業務異常和停機時是否有failOver
要測試是否有failover
先在nginx.conf
上配置proxy_next_upstream
。
我們對upstream為默認的負載均衡策略和ip_hash策略分別進行測試:
情況1:默認負載均衡策略
配置的結果如下:
在瀏覽器中輸入url進行測試:
http://192.168.233.118/test500 //這個請求后台會拋出throw new RuntimeException();
分析執行結果:
-
第一次有failover 從8080 failover到8089了。
-
第二次從8089 failover到8080了。
-
第三次發現做了一個輪回都有錯,所以直接從8080 failover到 demoupstream 502
-
隔了一段時間(2分鍾)重新執行又恢復到500 failover了。
相關nginx配置參考:“ 虛擬機里nginx.conf配置”
情況2:IP_hash負載均衡方式
先把負載均衡改成ip_hash的方式:
upstream demoupstream{
ip_hash;
server 127.0.0.1:8080;
server 127.0.0.1:8089;
}
在瀏覽器中輸入http://192.168.233.118/test500
結果如下:發現做了failvoer從8080轉為80989
。
從上面兩個情況知道,業務異常有failover。那么怎么讓業務異常不再有failover呢?
讓業務異常的時候不再failvoer
根據前面了解到有failover
是由於配置了proxy_next_upstream
,所以我把proxy_next_upstream
后面的http_500
去掉了。
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
set $web "/mnt/web_manager";
location / {
# proxy_pass http://localhost:8080;
proxy_pass http://demoupstream;
root $web;
index index.html index.htm;
proxy_next_upstream error timeout http_502 non_idempotent; ## 這里沒有http_500
}
}
upstream demoupstream{
server 127.0.0.1:8080;
server 127.0.0.1:8089;
}
修改后重新在瀏覽器輸入http://192.168.233.118/test500
結果如下圖所示,發現沒有做failover了。
但是注意,雖然沒有failover但是后一次訪問跟前面一次訪問upstream sever不一樣,具體體現就是8080和8089輪流被訪問。注意:upstream配置的是默認負載均衡策略
我現在把負載均衡方式改成ip_hash
,發現不管請求多少次請求的后端服務器地址都是8080:
讓停機的場景可以failover
經過前面操作,讓業務異常不failover搞定了。停機的時候響應碼為http_502,為了讓停機的情況能夠failvoer,所以只需要配置proxy_next_upstream http_502
。
備注:upstream 路由方式為
ip_hash
我先殺掉8080進程再殺8089看看有沒有failvoer,(因為ip_hash方式下服務默認先請求到8080):
執行結果如下:
我們分析一下結果:
第一次:當把8080進程殺掉之后進行了failover,請求從8080轉到8089,因為8089服務還在所以返回的是500的返回碼。
注意:這里500是8089返回的,是當訪問8080出現異常可能是502路由到8089在執行的返回,由這里也可以看出failover是沒有經過客戶端的,而是先8080轉到8089再返回給客戶端。
第二次:我再把8089進程殺掉,請求同樣從8080轉到8089,但是由於8089服務也停了所以返回502的錯誤。
第三次:由於前面兩次知道8080和8089都502了,所以后面返回結果demoupstream 502
。
總結
要實現不停機發布,就需要做到服務器掛掉的時候failover同時當有業務異常的時候不進行failover,只需要在proxy_next_upstream那里配置http_502,http_500不要配置,如下代碼所示:
location /xx {
proxy_read_timeout 600;
#proxy_pass http://localhost:9080;
proxy_pass http://xx_manage; //這個地方配置了負載均衡
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream http_502 non_idempotent;
}
我具體做的就是把以前http_500從proxy_next_upstream去掉了,改動雖然只有幾個代碼但是整個調試過程和場景值得保留下來。
參考
nginx.conf的配置
http {
include mime.types;
default_type application/octet-stream;
client_max_body_size 100M;
log_format main '$remote_addr - $remote_user [$time_local] "requst:$request" '
'upstream_addr:$upstream_addr '
'ups_res_time:$upstream_response_time '
'request_time:$request_time '
'status:$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" x-forwarded-for: "$http_x_forwarded_for"';
access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://localhost:8081;
}
## 當請求鏈接為http://120.78.154.33/hnxx/index,那么會到這里而不是上面的location,所以是貪婪匹配
location /xx {
proxy_read_timeout 600;
#proxy_pass http://localhost:9080;
proxy_pass http://xx_manage; //這個地方配置了負載均衡
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream error timeout http_500 http_404 http_502 http_503 non_idempotent;
}
location /vipRegister {
proxy_pass http://localhost:9081;
}
}
//這是配置的負載均衡
upstream xx_manage {
ip_hash;
server 127.0.0.1:9080;
server 127.0.0.1:9082;
}
虛擬機里nginx.conf配置
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$upstream_addr'
'- $status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
set $web "/mnt/web_manager";
location / {
# proxy_pass http://localhost:8080;
proxy_pass http://demoupstream;
root $web;
index index.html index.htm;
proxy_next_upstream error timeout http_502 http_500 non_idempotent;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
upstream demoupstream{
# ip_hash;
server 127.0.0.1:8080;
server 127.0.0.1:8089;
}
}
spring boot后台請求代碼
@RestController
public class DockerController {
@RequestMapping("/")
public String index() {
System.out.println(">>>Hello Docker!>>>");
return "Hello Docker!";
}
@RequestMapping("/test500")
public String test500() {
System.out.println(">>>test500>>>");
throw new RuntimeException(">>>test500>>>"); //拋出異常服務器就會出現http_500的錯誤
}
}