无感部署 Blue-Green部署

2024/2/19 无感部署springbootnginx

要实现无感部署,即在更新应用程序时不影响用户访问接口,可以考虑使用以下方法:

1、使用负载均衡器:在部署应用程序时,通过负载均衡器将流量分发到多个实例上。这样,当更新一个实例时,其他实例仍然可以继续提供服务,从而保证用户访问接口的稳定性。

2、Blue-Green 部署:使用Blue-Green 部署策略,即同时部署两个版本的应用程序,一个作为主要版本(Blue),一个作为备用版本(Green)。当需要更新应用程序时,先将新版本部署为备用版本,然后逐渐将流量切换到新版本,最终将旧版本下线。这样可以保证用户在更新过程中不受影响。

3、Canary 部署:使用Canary 部署策略,即逐步将新版本应用程序部署到生产环境中的一小部分节点,然后逐渐扩大部署范围。这样可以在更新过程中逐步验证新版本的稳定性,避免一次性影响所有用户。

通过以上方法,您可以实现无感部署,确保在更新应用程序时不影响用户访问接口。同时,建议在部署过程中进行充分的测试和监控,以确保更新过程的稳定性和可用性。

# 环境

  • 服务器:CentOS 7.9.2009
  • docker-compose版本:3.9
  • docker版本:20.10.12
  • 根目录在/docker/mes

# docker-compose.yml

version: "3.9"
services:
  fcat-mes-main-blue:
    image: adoptopenjdk/openjdk8-openj9:alpine-slim
    container_name: fcat-mes-main-blue
    restart: always
    ports:
      - "16219:11001"
    volumes:
      - ./fcat-mes-main-blue/app:/app
      - /etc/localtime:/etc/localtime
    environment:
      - TZ=Asia/Shanghai
    command: [
          'java',
          '-Xmx600m',
          '-jar',
          '/app/fcat-mes-main.jar'
         ]
    deploy:
      resources:
        limits:
          memory: 800M
  fcat-mes-main-green:
    image: adoptopenjdk/openjdk8-openj9:alpine-slim
    container_name: fcat-mes-main-green
    restart: always
    ports:
      - "16220:11001"
    volumes:
      - ./fcat-mes-main-green/app:/app
      - /etc/localtime:/etc/localtime
    environment:
      - TZ=Asia/Shanghai
    command: [
          'java',
          '-Xmx600m',
          '-jar',
          '/app/fcat-mes-main.jar'
         ]
    deploy:
      resources:
        limits:
          memory: 800M
  fcat-nginx:
    image: nginx:1.21.0
    container_name: fcat-nginx
    restart: always
    privileged: true
    environment:
      - TZ=Asia/Shanghai 
    ports:
      - 16221:16221
    volumes:
      - /etc/localtime:/etc/localtime
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/conf/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/log:/var/log/nginx
      - ./nginx/web:/web
      - ./logs:/logs
    deploy:
      resources:
        limits:
          memory: 1024M
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

# nginx/conf.d/main.conf

server {
    listen       16221;
    listen  [::]:16221;
    charset utf-8,gbk;
    client_max_body_size 10m;  # 设置最大上传文件大小为10MB
 
    location /dev-api/ {
		proxy_set_header Host $http_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_pass   http://mes_main_service/;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

upstream  mes_main_service{
    server fcat-mes-main-blue:11001;
    server fcat-mes-main-green:11001 weight=100;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# nginx/conf.d/blue.conf_bak

server {
    listen       16221;
    listen  [::]:16221;
    charset utf-8,gbk;
    client_max_body_size 10m;  # 设置最大上传文件大小为10MB
 
    location /dev-api/ {
		proxy_set_header Host $http_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_pass   http://mes_main_service/;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

upstream  mes_main_service{
    server fcat-mes-main-blue:11001 weight=100;
    server fcat-mes-main-green:11001 ;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# nginx/conf.d/green.conf_bak

server {
    listen       16221;
    listen  [::]:16221;
    charset utf-8,gbk;
    client_max_body_size 10m;  # 设置最大上传文件大小为10MB
 
    location /dev-api/ {
		proxy_set_header Host $http_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_pass   http://mes_main_service/;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

upstream  mes_main_service{
    server fcat-mes-main-blue:11001;
    server fcat-mes-main-green:11001 weight=100;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 运行服务

docker-compose up -d
1
curl http://192.168.9.126:16219/test/version
{
"version": "v1"
}

curl http://192.168.9.126:16220/test/version
{
"version": "v2"
}

curl http://192.168.9.126:16221/dev-api/test/version
{
"version": "v2"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# bluegreen.txt

green
1

# deploy.sh

#!/bin/bash
# 记录当前使用蓝色版本还是绿色版本
filename="bluegreen.txt"
# 读取当前使用版本
content=$(cat "$filename")
# 蓝色版本
blue_app_name="fcat-mes-main-blue"
# 绿色版本
green_app_name="fcat-mes-main-green"
# jar应用名称
jar_name="fcat-mes-main"
# 指定要匹配的字符串
search_string="Application Start Success"

echo "当前部署版本:$content" 
case $content in
  "blue")
	rm -rf nginx/conf.d/main.conf
	cp  nginx/conf.d/green.conf_bak  nginx/conf.d/main.conf 
	rm -rf  $green_app_name/app/$jar_name.jar_bak  
	cp  $green_app_name/app/$jar_name.jar $green_app_name/app/$jar_name.jar_bak  
	rm -rf $green_app_name/app/$jar_name.jar  
	cp $jar_name.jar  $green_app_name/app/
	echo "" > output.log
	docker-compose restart $green_app_name   
	timeout 60  docker-compose logs -f --tail=0 $green_app_name  | while IFS= read -r line; do
	    echo "$line"
		if echo "$line" | grep -q "$search_string"; then
			pkill -P $$  # 杀死timeout进程
			break
		fi
	done > output.log 2>&1
	if grep -q "$search_string" output.log; then
		echo "$search_string"
		docker-compose  exec fcat-nginx nginx -s reload
		echo "green" > bluegreen.txt
	else
		echo "Error detected in logs. Rolling back..."
		# 在这里编写回滚操作的代码
		rm -rf nginx/conf.d/main.conf
		cp nginx/conf.d/blue.conf_bak  nginx/conf.d/main.conf
		rm -rf $green_app_name/app/$jar_name.jar
		cp  $green_app_name/app/$jar_name.jar_bak $green_app_name/app/$jar_name.jar  
		docker-compose restart $green_app_name
		
	fi
  ;;
  "green")
	rm -rf nginx/conf.d/main.conf
	cp nginx/conf.d/blue.conf_bak  nginx/conf.d/main.conf
	rm -rf $blue_app_name/app/$jar_name.jar_bak  
	cp  $blue_app_name/app/$jar_name.jar $blue_app_name/app/$jar_name.jar_bak  
	rm -rf $blue_app_name/app/$jar_name.jar   
	cp $jar_name.jar  $blue_app_name/app/
	echo "" > output.log
	docker-compose restart $blue_app_name  
	timeout 60  docker-compose logs -f  --tail=0 $blue_app_name  | while IFS= read -r line; do
	    echo "$line"
		if echo "$line" | grep -q "$search_string"; then
			echo "Found the search string. Exiting..."
			pkill -P $$  # 杀死timeout进程
			break
		fi
	done  > output.log 2>&1
	if grep -q "$search_string" output.log; then
		echo "$search_string"
		docker-compose  exec fcat-nginx nginx -s reload
		echo "blue" > bluegreen.txt
	else
		echo "Error detected in logs. Rolling back..."
		# 在这里编写回滚操作的代码
		rm -rf nginx/conf.d/main.conf
		cp nginx/conf.d/green.conf_bak  nginx/conf.d/main.conf
		rm -rf $blue_app_name/app/$jar_name.jar
		cp  $blue_app_name/app/$jar_name.jar_bak $blue_app_name/app/$jar_name.jar  
		docker-compose restart $blue_app_name
	fi 
  ;;
  "*")
      echo "bluegreen.txt 内容不对"
  ;;
esac



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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

# deploy-ifms.sh

#!/bin/bash
cd /docker/mes/

# 记录当前使用蓝色版本还是绿色版本
filename="ifms-bluegreen.txt"
# 读取当前使用版本
content=$(cat "$filename")
# 蓝色版本
blue_app_name="ifms-admin-blue"
# 绿色版本
green_app_name="ifms-admin-green"
# jar应用名称
jar_name="ifms-admin"
# 指定要匹配的字符串
search_string="Application Start Success"
error_string="Application Start Failed"
# 抓取的日志记录文件
log_file="ifms-output.log"
# nginx配置文件,不需要.conf后缀
nginx_file="mes"

wait_minutes=120
# 公司的ID
companyId=1

echo "当前部署版本:$content" 

# 参数:json_data='{"companyId": "11", "status": "1", "msg": "中文哈哈"}'
send_mail(){
	local json_data=$1
	echo "$json_data"
	# HTTP 接口地址和参数
	url="http://www.tsuantone.top:16020/system/mail/deploy1" 
	# 发送 POST 请求,指定 Content-Type 为 application/json
	response=$(curl -s -X POST -H "Content-Type: application/json" -d "$json_data" "$url")
	# 输出响应结果
	echo "Response: $response"
}

escape_string() {
  local string="$1"
  escaped_string=$(echo "$string" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\n/\\n/g; s/\t/\\t/g')
  echo "$escaped_string"
}

case $content in
  "blue")
	rm -rf nginx/conf.d/$nginx_file.conf
	cp  nginx/conf.d/$nginx_file-green.conf_bak  nginx/conf.d/$nginx_file.conf 
	rm -rf  $green_app_name/app/$jar_name.jar_bak  
	cp  $green_app_name/app/$jar_name.jar $green_app_name/app/$jar_name.jar_bak  
	rm -rf $green_app_name/app/$jar_name.jar  
	cp $jar_name.jar  $green_app_name/app/
	echo "" > $log_file
	cd /docker/mes/
	docker-compose restart $green_app_name   
	timeout $wait_minutes  docker-compose logs -f --tail=0 $green_app_name  | while IFS= read -r line; do
	    echo "$line"
		if echo "$line" | grep -q "$search_string"; then
			pkill -P $$  # 杀死timeout进程
			break
		fi
	done > $log_file 2>&1
	content_log=$(cat $log_file)
	# 去掉shell终端的颜色显示
	content_log=$(echo "$content_log" | sed -r "s/\x1B\[[0-9;]*[mGK]//g")
	#content_log=$(escape_string "$original_string")
	echo "$content_log"
	if grep -q "$search_string" $log_file; then
		echo "$search_string"
		json_data=$(jq -n --arg company_id "$companyId" --arg status "1" --arg msg "$content_log" '{companyId: $company_id, status: $status, msg: $msg}')
		send_mail "$json_data" 
		cd /docker/mes/
		docker-compose  restart fcat-nginx
		echo "green" > $filename
	else
		echo "$error_string"
		json_data=$(jq -n --arg company_id "$companyId" --arg status "0" --arg msg "$content_log" '{companyId: $company_id, status: $status, msg: $msg}')
		send_mail "$json_data" 
		# 在这里编写回滚操作的代码
		rm -rf nginx/conf.d/$nginx_file.conf
		cp nginx/conf.d/$nginx_file-blue.conf_bak  nginx/conf.d/$nginx_file.conf
		rm -rf $green_app_name/app/$jar_name.jar
		cp  $green_app_name/app/$jar_name.jar_bak $green_app_name/app/$jar_name.jar  
		docker-compose restart $green_app_name
		
	fi
  ;;
  "green")
	rm -rf nginx/conf.d/$nginx_file.conf
	cp nginx/conf.d/$nginx_file-blue.conf_bak  nginx/conf.d/$nginx_file.conf
	rm -rf $blue_app_name/app/$jar_name.jar_bak  
	cp  $blue_app_name/app/$jar_name.jar $blue_app_name/app/$jar_name.jar_bak  
	rm -rf $blue_app_name/app/$jar_name.jar   
	cp $jar_name.jar  $blue_app_name/app/
	echo "" > $log_file
	docker-compose restart $blue_app_name  
	timeout $wait_minutes  docker-compose logs -f  --tail=0 $blue_app_name  | while IFS= read -r line; do
	    echo "$line"
		if echo "$line" | grep -q "$search_string"; then
			pkill -P $$  # 杀死timeout进程
			break
		fi
	done  > $log_file 2>&1
	content_log=$(cat $log_file)
	# 去掉shell终端的颜色显示
	content_log=$(echo "$content_log" | sed -r "s/\x1B\[[0-9;]*[mGK]//g")
	#content_log=$(escape_string "$original_string")
	echo "$content_log"
	if grep -q "$search_string" $log_file; then
		echo "$search_string"
		json_data=$(jq -n --arg company_id "$companyId" --arg status "1" --arg msg "$content_log" '{companyId: $company_id, status: $status, msg: $msg}')
		send_mail "$json_data" 
		cd /docker/mes/
		docker-compose  restart fcat-nginx
		echo "blue" > $filename
	else
		echo "$error_string"
		json_data=$(jq -n --arg company_id "$companyId" --arg status "0" --arg msg "$content_log" '{companyId: $company_id, status: $status, msg: $msg}')
		send_mail "$json_data"
		# 在这里编写回滚操作的代码
		rm -rf nginx/conf.d/$nginx_file.conf
		cp nginx/conf.d/$nginx_file-green.conf_bak  nginx/conf.d/$nginx_file.conf
		rm -rf $blue_app_name/app/$jar_name.jar
		cp  $blue_app_name/app/$jar_name.jar_bak $blue_app_name/app/$jar_name.jar  
		docker-compose restart $blue_app_name
	fi 
  ;;
  "*")
      echo "bluegreen.txt 内容不对"
  ;;
esac


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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134

# deploy-fcat.sh

#!/bin/bash

# 记录当前使用蓝色版本还是绿色版本
filename="ruoyi-admin-bluegreen.txt"
# 读取当前使用版本
content=$(cat "$filename")
# 蓝色版本
blue_app_name="ruoyi-admin-blue"
# 绿色版本
green_app_name="ruoyi-admin-green"
# jar应用名称
jar_name="ruoyi-admin"
jar_path="/root/ruoyi-vue/ruoyi-admin/target/"
# 指定要匹配的字符串
search_string="Application Start Success"
error_string="Application Start Failed"
# 抓取的日志记录文件
log_file="ruoyi-admin-output.log"
# nginx配置文件,不需要.conf后缀
nginx_file="ruoyi"
# 检测启动日志等待时间
wait_minutes=120
# 切换nginx的蓝绿版本,需要留时间处理请求中的接口返回
wait_inteface_deal_minutes=30
# 名称
name="FCAT"

echo "当前部署版本:$content" 

# 参数:json_data='{"name": "FCAT", "status": "1", "msg": "中文哈哈"}'
send_mail(){
	local json_data=$1
	echo "$json_data"
	# HTTP 接口地址和参数
	url="https://localhost/prod-api/system/mail/deploy" 
	# 发送 POST 请求,指定 Content-Type 为 application/json
	response=$(curl -s -X POST -H "Content-Type: application/json" -d "$json_data" -k "$url")
	# 输出响应结果
	echo "Response: $response"
}

escape_string() {
  local string="$1"
  escaped_string=$(echo "$string" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\n/\\n/g; s/\t/\\t/g')
  echo "$escaped_string"
}

case $content in
  "blue")
	rm -rf nginx/conf.d/$nginx_file.conf
	cp  nginx/conf.d/$nginx_file-green.conf_bak  nginx/conf.d/$nginx_file.conf 
	rm -rf  $green_app_name/jar/$jar_name.jar_bak  
	cp  $green_app_name/jar/$jar_name.jar $green_app_name/jar/$jar_name.jar_bak  
	rm -rf $green_app_name/jar/$jar_name.jar  
	cp $jar_path$jar_name.jar  $green_app_name/jar/
	echo "" > $log_file
	docker-compose restart $green_app_name   
	timeout $wait_minutes  docker-compose logs -f --tail=0 $green_app_name  | while IFS= read -r line; do
	    echo "$line"
		if echo "$line" | grep -q "$search_string"; then
			pkill -P $$  # 杀死timeout进程
			break
		fi
	done > $log_file 2>&1
	content_log=$(cat $log_file)
	# 去掉shell终端的颜色显示
	content_log=$(echo "$content_log" | sed -r "s/\x1B\[[0-9;]*[mGK]//g")
	#content_log=$(escape_string "$original_string")
	echo "$content_log"
	if grep -q "$search_string" $log_file; then
		echo "$search_string"
		docker-compose  exec fcat-nginx nginx -s reload
		sleep $wait_inteface_deal_minutes
		json_data=$(jq -n --arg name "$name" --arg status "1" --arg msg "$content_log" '{name: $name, status: $status, msg: $msg}')
		send_mail "$json_data" 
		echo "green" > $filename
		docker-compose stop $blue_app_name
	else
		echo "$error_string"
		json_data=$(jq -n --arg name "$name" --arg status "0" --arg msg "$content_log" '{name: $name, status: $status, msg: $msg}')
		send_mail "$json_data" 
		# 在这里编写回滚操作的代码
		rm -rf nginx/conf.d/$nginx_file.conf
		cp nginx/conf.d/$nginx_file-blue.conf_bak  nginx/conf.d/$nginx_file.conf
		rm -rf $green_app_name/jar/$jar_name.jar
		cp  $green_app_name/jar/$jar_name.jar_bak $green_app_name/jar/$jar_name.jar  
		docker-compose restart $green_app_name
		
	fi
  ;;
  "green")
	rm -rf nginx/conf.d/$nginx_file.conf
	cp nginx/conf.d/$nginx_file-blue.conf_bak  nginx/conf.d/$nginx_file.conf
	rm -rf $blue_app_name/jar/$jar_name.jar_bak  
	cp  $blue_app_name/jar/$jar_name.jar $blue_app_name/jar/$jar_name.jar_bak  
	rm -rf $blue_app_name/jar/$jar_name.jar   
	cp $jar_path$jar_name.jar  $blue_app_name/jar/
	echo "" > $log_file
	docker-compose restart $blue_app_name  
	timeout $wait_minutes  docker-compose logs -f  --tail=0 $blue_app_name  | while IFS= read -r line; do
	    echo "$line"
		if echo "$line" | grep -q "$search_string"; then
			pkill -P $$  # 杀死timeout进程
			break
		fi
	done  > $log_file 2>&1
	content_log=$(cat $log_file)
	# 去掉shell终端的颜色显示
	content_log=$(echo "$content_log" | sed -r "s/\x1B\[[0-9;]*[mGK]//g")
	#content_log=$(escape_string "$original_string")
	echo "$content_log"
	if grep -q "$search_string" $log_file; then
		echo "$search_string"
		docker-compose  exec fcat-nginx nginx -s reload
		sleep $wait_inteface_deal_minutes
		json_data=$(jq -n --arg name "$name" --arg status "1" --arg msg "$content_log" '{name: $name, status: $status, msg: $msg}')
		send_mail "$json_data" 
		echo "blue" > $filename
		docker-compose stop $green_app_name
	else
		echo "$error_string"
		json_data=$(jq -n --arg name "$name" --arg status "0" --arg msg "$content_log" '{name: $name, status: $status, msg: $msg}')
		send_mail "$json_data"
		# 在这里编写回滚操作的代码
		rm -rf nginx/conf.d/$nginx_file.conf
		cp nginx/conf.d/$nginx_file-green.conf_bak  nginx/conf.d/$nginx_file.conf
		rm -rf $blue_app_name/jar/$jar_name.jar
		cp  $blue_app_name/jar/$jar_name.jar_bak $blue_app_name/jar/$jar_name.jar  
		docker-compose restart $blue_app_name
	fi 
  ;;
  "*")
      echo "$filename内容不对"
  ;;
esac


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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

# 上传jar包到服务器

根目录在/docker/mes/fcat-mes-main.jar

sh deploy.sh 
1
curl http://192.168.9.126:16219/test/version
{
"version": "v3"
}

curl http://192.168.9.126:16220/test/version
{
"version": "v2"
}

curl http://192.168.9.126:16221/dev-api/test/version
{
"version": "v3"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Last Updated: 2024/2/28 17:47:22