ㅍㅍㅋㄷ

Docker Network 구조(3) - container 외부 통신 구조 본문

IT/Docker

Docker Network 구조(3) - container 외부 통신 구조

클쏭 2016. 7. 6. 15:41

Docker Network 구조(3) - Container 외부 통신 구조







 Docker host에 container 가 배포되면 각 container 에는 격리된 네트워크 환경(namespace)이 제공된다. 이 네트워크 환경은 오로지 각각의 container 만을 위한 네트워크 환경이다. 각 container에는 통신을 위한 인터페이스도 새롭게 할당되며, mac 주소와 prviate IP도 부여 받게 된다. 


 각 Container 들의 인터페이스는 자신들이 상주하고 있는 Docker host 와 통신을 위해 linux bridge 방식으로 binding 되어 있다. 따라서 같은 Docker host 내에 배포된 container 들 사이에는 각자 할당받은 Private IP를 이용해 자유롭게 통신이 가능하다. 


그렇다면, web 서비스를 하는 container 를 생성한다고 가정해보자.

web 서비스를 위한 container 라 한다면, http 통신을 위한 80 포트는 반드시 외부와 통신이 되어야 서비스가 가능하다. 

하지만, container는 Private IP를 부여 받고, Docker host 내 bridge 형태로 연결되어 있기 때문에 직접적으로 외부와의 통신은 불가능한 상태이다.


container 가 외부로 서비스되기 위해, 어떤 구조로 동작하는지 알아보자. 




Container Port 외부로 노출(expose) 하기


 container 를 생성하면 기본적으로 외부와 통신이 불가능한 상태이다. 따라서 외부와 통신을 위해서는 container를 외부로 노출할 Port를 지정해야한다. 노출할 port를 지정하는 방법은 container 를 생성할때 -p option을 이용하면 된다. 


root@~~# docker run -d -p 8080:80 --name web_svr01 httpd 



 위 명령대로 실행하면, 외부에서 Docker host 의 8080 포트로 요청이 들어오면 web_svr01 컨테이너의 80 포트로 해당 요청을 forwarding 하겠다는 의미이다. 아래와 같이 container 상태를 살펴 보면 8080 -> 80 포트로 forwarding 되어 있는 것을 볼 수 있다.



root@~~# docker ps -a

CONTAINER ID        IMAGE               COMMAND              CREATED             STATUS              PORTS                  NAMES

0fa48359f36d        httpd               "httpd-foreground"   12 minutes ago      Up 12 minutes       0.0.0.0:8080->80/tcp   web_svr01



  Docker host에서 netstat 명령을 통해 listening 상태를 확인해 보면 아래와 같이 8080 포트가 active 되어 있는것을 확인할 수 있다. 


root@~~# netstat -nlp | grep 8080

tcp6       0      0 :::8080                 :::*                    LISTEN      12581/docker-proxy


 그런데 이 8080 포트를 listen 하고 있는 프로세스는 다름아닌 docker-proxy 라는 프로세스이다. 

 docker-proxy 프로세스는 무엇인지 알아보자. 





docker-proxy


 이 프로세스의 목적은 그 이름처럼 docker host 로 들어온 요청을 container 로 넘기는 것 뿐이다. docker-proxy 는 kernel이 아닌 userland 에서 수행되기 때문에 kernel 과 상관없이 host가 받은 패킷을 그대로 container의 port로 넘긴다. 


 container 를 시작할때 port를 외부로 노출하도록 설정하게 되면, docker host에는 docker-proxy 라는 프로세스가 생성되게 된다. 


UID        PID  PPID  C STIME TTY          TIME CMD

root     12581  9576  0 06:31 ?        00:00:00 docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 80


 docker-proxy 프로세스는 container의 port를 노출하도록 설정한 수 만큼 추가로 프로세스가 생성된다 (run process per port). 만약 하나의 Port를 오픈하는 두개의 Container를 생성한다면 docker-proxy는 두개가 생성된다. 또한, 한개의 container 에 두개의 port 에 대해 외부로 노출하도록 설정한다면, 마찬가지로 docker-proxy 프로세스는 두개가 생성된다.


 하지만, 실제로는 docker host로 요청이 들어온 패킷이 container 로 전달되는 것은 docker-proxy 와 무관하게 docker host의 iptables 에 의해 동작된다. 


 즉, docker-proxy 프로세스를 kill 해도 외부에서 들어오는 요청이 container로 전달되는데 문제가 없다는 의미이다. (응?!)

 이부분에 대해 자세히 알아보기 전에 먼저 iptables 를 이용한 port 포워딩 설정을 살펴보자. 




iptables 를 이용한 DNAT


 먼저, docker host의 iptables 를 확인해 보자.


root@~~# iptables -t nat -L -n

Chain PREROUTING (policy ACCEPT)

target     prot opt source               destination

DOCKER     all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL


Chain INPUT (policy ACCEPT)

target     prot opt source               destination


Chain OUTPUT (policy ACCEPT)

target     prot opt source               destination

DOCKER     all  --  0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL


Chain POSTROUTING (policy ACCEPT)

target     prot opt source               destination

MASQUERADE  all  --  172.17.0.0/16        0.0.0.0/0

MASQUERADE  tcp  --  172.17.0.2           172.17.0.2           tcp dpt:80


Chain DOCKER (2 references)

target     prot opt source               destination

RETURN     all  --  0.0.0.0/0            0.0.0.0/0

DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 to:172.17.0.2:80


iptables 내역을 보면, 

먼저 Docker host에 들어온 패킷이 PREROUTING chain 을 통해 DOCKER Chain 으로 전달하고,

Docker Chain에서는 DNAT로 8080 포트로 들어온 요청을 172.17.0.2 IP를 가진 container 의 80 포트로 포워딩 되는 것을 알 수 있다. 

 

반대로, Container 에서 외부로 나갈때는 POSTROUTING 을 거쳐 MASQUERADE 되어 외부로 나간다. 


여기에서 볼 수 있는 container 관련 iptables rule 관리는 docker daemon 이 자동으로 제어하게 된다.

또한, NAT 를 위한 ip_forward 설정도 docker daemon 에서 제어하게 된다. 


root@~~# sysctl -a | grep ip_forward

net.ipv4.ip_forward = 1





docker-proxy 를 사용하는 이유


 docker host에서 container 로 패킷을 전달하는 방법이 iptables를 이용한 DNAT 방식이라면, 위에서 언급된 docker-proxy 프로세스는 과연 무슨 역할을 할까.


 docker-proxy가 존재하는 가장 큰 이유는, docker host가 iptables 의 NAT를 사용하지 못하는 상황에 대한 처리이다. 만약 정책상의 이유로 docker host의 iptables 나 ip_forward 를 enable 하지 못하는 경우에는 docker-proxy 프로세스가 패킷을 포워딩하는 역할을 대신하게 된다. 



 아래는 docker host 의 iptables 를 disable 하는 설정이다. 

 /etc/default/docker 파일을 열고(없다면 생성) 아래와 같이 DOCKER_OPTS 를 추가한 후 docker daemon 을 재시작하면 된다. 


root@~~# cat /etc/default/docker

DOCKER_OPTS="--iptables=false"

 

 iptables=false 로 설정 한후 container를 실행하면 아래와 같이 DNAT rule에 아무것도 없다. 이런 경우 docker-proxy 프로세스가 직접 패킷을 받아 container로 포워딩을 처리한다. 


root@~~# iptables -t nat -L -n

Chain PREROUTING (policy ACCEPT)

target     prot opt source               destination

DOCKER     all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL


Chain INPUT (policy ACCEPT)

target     prot opt source               destination


Chain OUTPUT (policy ACCEPT)

target     prot opt source               destination

DOCKER     all  --  0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL


Chain POSTROUTING (policy ACCEPT)

target     prot opt source               destination

MASQUERADE  all  --  172.17.0.0/16        0.0.0.0/0


Chain DOCKER (2 references)

target     prot opt source               destination

RETURN     all  --  0.0.0.0/0            0.0.0.0/0



 또한, 그것과 별개로 docker host의 internal network (172.17.0.0/16) 에서는 현재의 NAT 설정을 통해서 노출된 port(여기서는 8080) 로 포워딩이 불가능하다. 이런 경우에도 docker-proxy가 이를 대신 처리하게 된다. 만약, docker-proxy 프로세스를 kill 한후 docker host에서 localhost 로 8080 포트를 요청하면 container의 80 포트로 포워딩이 안된다. 이는 같은 host 내에 배포된 container 사이에서도 마찬가지다. 


root@~~# telnet localhost 8080

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

^C

root@~~# ps -aef | grep docker-proxy

root  21833 21725  0 05:49 docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 80

root@~~# kill -9 21833

root@~~# telnet localhost 8080

Trying 127.0.0.1...

telnet: Unable to connect to remote host: Connection refused


이런 특수한 예외 상황 때문에 docker 측에서도 docker-proxy 프로세스를 통해 이를 처리하는 것으로 보인다. 


그럼에도, 많은 사람들이 이 docker-proxy 프로세스의 필요성에 의구심을 갖는다. 

게다가, docker-proxy 프로세스가 의외로 메모리도 차지하는 편이다.


root@~~# ps -eo user,pid,rss,time,cmd --sort -rss | head -n 5

USER       PID   RSS     TIME CMD

root      9576 39340 00:00:54 /usr/bin/docker daemon --raw-logs

root     16296 11768 00:00:00 docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 80

root      9585  8724 00:00:03 docker-containerd -l /var/run/docker/libcontainerd/docker-containerd.sock --runtime docker-runc --start-timeout 2m

daemon   16345  4708 00:00:01 httpd -DFOREGROUND


 docker-proxy 프로세스 하나당 약 11 MB(11768 kb) 가까이 사용한다. 만약 docker host 한대에 여러 포트를 사용하는 container를 추가로 올릴때마다 그만큼의 메모리 사용량이 증가할 것이다.


그래서 커뮤니티 같은데 보면 docker-proxy 프로세스를 disable 할수 없는지 문의 하는 것을 볼 수 있다. ( https://github.com/docker/docker/issues/8356 )


 그래서 이 요청 사항을 받아들였는지, docker 1.7 버전 부터는 docker-proxy 대신 locahost 의 hairpin NAT 방식이 가능하도록 지원한다. 




docker-proxy disable 설정


 아래는 docker-proxy를 disable 하는 설정이다.

 /etc/default/docker 파일에 DOCKER_OPTS 을 추가한 후 docker daemon 을 재시작하면 된다. 


root@~~# cat /etc/default/docker

DOCKER_OPTS="--userland-proxy=false"


 그러면, container 에서 port를 오픈하더라도 docker-proxy 프로세스는 생성되지 않는다. 하지만 내부 network 에서도 외부 오픈된 port로 통신이 가능하도록 NAT에 추가적으로 설정 된다. 아래와 같이 iptables 의 POSTROUTING 룰에 MASQUERADE 로 local network (172.17.X.X) 에 대한 룰이 추가되는 것을 볼 수 있다. 


iptables -t nat -L -n

Chain PREROUTING (policy ACCEPT)

target     prot opt source               destination

DOCKER     all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL


Chain INPUT (policy ACCEPT)

target     prot opt source               destination


Chain OUTPUT (policy ACCEPT)

target     prot opt source               destination

DOCKER     all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL


Chain POSTROUTING (policy ACCEPT)

target     prot opt source               destination

MASQUERADE  all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match src-type LOCAL

MASQUERADE  all  --  172.17.0.0/16        0.0.0.0/0

MASQUERADE  tcp  --  172.17.0.2           172.17.0.2           tcp dpt:80


Chain DOCKER (2 references)

target     prot opt source               destination

DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 to:172.17.0.2:80




[ 참고 ]


  • http://windsock.io/tag/docker-proxy/
  • http://serverfault.com/questions/633604/what-is-the-point-of-the-docker-proxy-process-why-is-a-userspace-tcp-proxy-need
  • https://github.com/docker/docker/issues/8356
  • https://github.com/docker/docker/pull/6810
  • https://github.com/docker/docker/pull/9078


Comments