non-stop deploy

non-stop deploy

개발한 서비스를 무중단 배포( non-stop deploy ) 하는일은 필자가 생각하기에 매우 중요한 요소중 하나라고 생각된다.
우선 필자의 목적은 재배포 ( redeploy )할시 그와동시에 client request에 대한 error 를 뱉지않는것을 목표로 잡았다.
docker swarm으로 services들을 구성하니 container replicas ( docker service update시 rolling )으로 당연히 non-stop deploy가 이루어진줄 알았다.
혹시나하는 마음으로 docker service update( redeploy )시 api call을 일정한 타임주기로 꾸준히 날려보았다.
그런데 error가 …. 역시 직접 테스트해보기전까진 모른다.
필자가 시도해본방법을 정리해보려한다.

docker HEALTHCHECK

처음 api call error를 마주하는순간 ‘아 ~ HEALTHCHECK command를 Dockerfile에 안넣어서 그렇구나’ 생각하여 health check api를 만들고 HEALTHCHECK command를 삽입하였다.

1
2
3
4
5
6
7
8
9
10
FROM ...
...
HEALTHCHECK --interval=3s --timeout=3s --retries=5 CMD curl -f http://localhost:4000/api/smpl/healthcheck || exit 1
# --interval = container가 시작된후 검사( health check api call )의 interval 주기
# --timeout = 검사의 실행시간 ( health check api call 후 timeout 시간내에 응답이없으면 실패로 간주 )
# --retries = 재시도 횟수( 연속적인 실패시 not healty로 간주 )
# = docker container가 시작된후 health check api call을 interval( 3초 ) 마다 실행.
# 실행에 대한 결과가 5초내에 이루어지지않을경우 실패로간주, 연속적으로 5번 실패시 not healthy로 간주
...
CMD ...

이렇게 dockerize 환경구성후 1.0 version을 배포, 2.0 version을 만들어 배포하는중 0.3초주기로 api를 call 무한으로 날려보았다.

1
2
3
4
5
6
7
8
9
10
11
12
...
{ version: '1.0', reqIdxVal: '1481' }
{ version: '1.0', reqIdxVal: '1482' }
{ version: '1.0', reqIdxVal: '1483' }
{ version: '1.0', reqIdxVal: '1484' }
{ version: '1.0', reqIdxVal: '1485' }
{ version: '1.0', reqIdxVal: '1486' }
{ version: '1.0', reqIdxVal: '1487' }
{ version: '1.0', reqIdxVal: '1488' }
{ version: '1.0', reqIdxVal: '1489' }
{ RequestError: Error: connect ECONNREFUSED 10.10.10.11:4000
...

에러가 발생하였다.
‘필자는 HEALTHCHECK 가 왜 아무소용이없지? 뭘 잘못했나 ? ‘ 라는 생각이 먼저들었으나 docker container를 확인해보니 못보던 정보가 생겨났다.

1
2
3
4
5
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3c0824ed0191 setyourmindpark:5000/setyourmindpark_backend:180301213341 "pm2-docker bin/www.…" 5 minutes ago Up 5 minutes (healthy) 4000/tcp setyourmindpark_service_backend.3.rf4ejsaqz2sp5t89euz7if6ly
2660ee52065c setyourmindpark:5000/setyourmindpark_backend:180301213341 "pm2-docker bin/www.…" 5 minutes ago Up 5 minutes (healthy) 4000/tcp setyourmindpark_service_backend.2.z6jx7lati4wqttw7rnu8plk78
deb1020a9b31 setyourmindpark:5000/setyourmindpark_backend:180301213341 "pm2-docker bin/www.…" 6 minutes ago Up 6 minutes (healthy) 4000/tcp setyourmindpark_service_backend.1.wrhf6tbpe8x5lj7wierju493m

https://docs.docker.com/engine/reference/builder/#healthcheck
STATUS COLUMN에 healty라는 정보가 새롭게 생겼으며 service update시 rolling중에는 (health: starting) 과 같이 나타난다.
docker swarm 내부적인 load balancer가 해당 service의 traffic을 처리하는데에있어 healthcheck를 사용하지않는듯하다.
위에서 필자가 시도한 HEALTHCHECK command 역시 개발자에게 healty 또는 unhealthy 정보를 확인할수있는 지표만을 제공하는듯하다. ( 혹시 제가 잘못알고있을수도있으니 의견부탁드립니다. )

nginx load balancing

docker HEALTHCHECK 방법이 실패하고 ‘그럼 nginx를 쓰면되지’ 라는생각과 주저없이 nginx 세팅에 들어갔다.
필자는 왜 그렇게 생각한지 모르지만 nginx를 load balancer로 사용시 당연히 health check는 기본으로 동작하면서 되는줄알았다. ( 정말 왜그렇게 생각했는지 모르겠다 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ vi /etc/nginx/default.conf
upstream target-server {
least_conn;
server 10.10.10.11:4000 max_fails=3 fail_timeout=30s;
# max_fails = 쵀대 실패횟수
# fail_timeout = 최대 요청실패 임계시간
# = 30초동안 3번의 요청 실패시 dead로 판단
}
server {
listen 80;
charset utf-8;
access_log /etc/nginx/log/access.log;
error_log /etc/nginx/log/error.log;
location / {
proxy_redirect off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_pass http://target-server;
}
}

여기서 ‘load balancer로 쓰는데 backend server가 1대야 ?’ 라고 생각하실분들이 계실것같아 설명을 하자면 docker service replicas( 5개의 container )로 구성되어있어 동작하는 backend는 5개( docker swarm load balancer )로 backend server는 1대로 구성하였다.
docker swarm 포스트의 이미지를 보면 이해가 쉬울것이다.

1
2
3
4
5
6
7
8
9
...
{ version: '1.0', reqIdxVal: '1862' }
{ version: '1.0', reqIdxVal: '1863' }
{ version: '1.0', reqIdxVal: '1864' }
{ version: '1.0', reqIdxVal: '1865' }
{ version: '1.0', reqIdxVal: '1866' }
{ version: '1.0', reqIdxVal: '1867' }
{ StatusCodeError: 502 - "<html>\r\n<head><title>502 Bad Gateway</title></head>\r\n<body bgcolor=\"white\">\r\n<center><h1>502 Bad Gateway</h1></center>
...

역시에러가 발생하였다.( 당연히될줄알았는데.. )

haproxy load balancing

필자의 생각으론 nginx 같은 앞단의 load balancer를 쓰면서 proxy target server의 health check를 해줄 녀석이 필요하다고 판단했다.
그렇게 알게된것이 haproxy 이며 ‘이제정말 되겠지’ 라는 마음으로 공부하며 적용해보았다.

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
$ vi/etc/haproxy/haproxy.cfg
defaults
mode http
option httplog
option dontlognull
option redispatch
option forwardfor
option http-server-close
retries 3
maxconn 20480
timeout connect 5s
timeout server 50s
timeout client 50s
timeout http-keep-alive 3000
frontend http_in
bind *:80
reqadd X-Forwarded-Proto:\ http
default_backend server
backend server
mode http
balance roundrobin
default-server inter 3s rise 5 fall 5
# default-server = health check 조건
# inter = interval 주기 ( 3초 )
# rise = 요청횟수 ( 3번 )
# fail = 실패횟수
# = 3초 주기로 5번요청 성공시 healthy, 5번 실패시 dead로 판단
server s1 10.10.10.11:4000 check

=> 3초 주기로 5번요청 성공시 정상작동간주, 트래픽을 연결한다.
api call response 결과는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{ version: '1.0', reqIdxVal: '980' }
{ version: '1.0', reqIdxVal: '981' }
{ version: '1.0', reqIdxVal: '982' }
{ version: '1.0', reqIdxVal: '983' }
{ version: '1.0', reqIdxVal: '984' }
{ version: '1.0', reqIdxVal: '985' }
{ version: '2.0', reqIdxVal: '986' }
{ version: '2.0', reqIdxVal: '987' }
{ version: '1.0', reqIdxVal: '988' }
{ version: '2.0', reqIdxVal: '989' }
{ version: '2.0', reqIdxVal: '990' }
{ version: '1.0', reqIdxVal: '991' }
{ version: '2.0', reqIdxVal: '992' }
{ version: '2.0', reqIdxVal: '993' }
{ version: '2.0', reqIdxVal: '994' }
{ version: '2.0', reqIdxVal: '995' }
{ version: '2.0', reqIdxVal: '996' }

무중단 배포( non-stop deploy ) 가 이루어졌다.


환경

docker version = Docker version 17.12.0-ce, build c97c6d6
nginx version = nginx version: nginx/1.12.2
haproxy version = HA-Proxy version 1.8.4-1~bpo8+1
test backend replicas container = 5개

참고사이트

https://seokjun.kim/haproxy-and-nginx-load-balancing/