实现不停机发布
有一个后台项目由于并发量不高所以只部署了一台机器,但是如果要升级的话其他人就用不了了。为了解决不影响其他同事正常使用,我想做一个不停机发布的功能。
具体原理就是通过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的错误
}
}