IT/DevOps

패킷 덤프를 통해 확인하는 ALB와 NLB의 차이점 (1) - ALB 동작 원리

Aaron's papa 2021. 8. 5. 23:15
반응형

AWS에는 크게 세 가지 종류의 로드밸런서가 있습니다. CLB라 불리는 Classic LB, ALB라 불리는 Application LB, NLB라 불리는 Network LB 이렇게 세 가지 이죠. 그중 CLB는 사실상 deprecated 된 LB로 요즘엔 거의 사용하지 않습니다. ALB와 NLB가 용도에 맞게 각각 사용되고 있죠. 그래서 다들 궁금해합니다. 대체 ALB와 NLB의 차이점은 무엇인지, 그리고 각각 언제 사용해야 하는 건지. 그래서인지 구글링을 해보면 이 두 가지 LB의 차이점에 대해서 설명하는 글들을 많이 볼 수 있습니다. ALB와 NLB의 특징, 그리고 각각의 LB를 언제 사용하는 게 좋을지에 대한 자료들이 많이 존재하죠. 그래서 이번 글에서는 조금 색다르게 다들 알고 있는 그 차이점 말고, 직접 패킷 덤프를 생성해 가면서 이 두 LB가 어떻게 다르게 동작하고 있는지 차이점을 살펴보려고 합니다. 패킷 덤프를 통해 두 LB가 패킷을 어떻게 처리하고 있는지를 두 눈으로 살펴보면 그 차이점을 더 확실하게 알 수 있게 되기 때문입니다. 그럼 시작해 보겠습니다.


애플리케이션 서버 띄우기

우선 테스트를 위한 애플리케이션 서버를 띄워 보겠습니다. 앞서 jib 를 이용해서 생성한 도커 이미지를 EC2 인스턴스에 띄워 보겠습니다. 도커 설치가 필요하면 설치도 해줍니다.

[root@ip-172-31-11-95 ~]# yum install -y docker
Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
Resolving Dependencies
--> Running transaction check
---> Package docker.x86_64 0:20.10.4-1.amzn2 will be installed
--> Processing Dependency: runc >= 1.0.0 for package: docker-20.10.4-1.amzn2.x86_64
--> Processing Dependency: libcgroup >= 0.40.rc1-5.15 for package: docker-20.10.4-1.amzn2.x86_64
--> Processing Dependency: containerd >= 1.3.2 for package: docker-20.10.4-1.amzn2.x86_64
--> Processing Dependency: pigz for package: docker-20.10.4-1.amzn2.x86_64
... (중략) ...
[root@ip-172-31-11-95 ~]# systemctl start docker
[root@ip-172-31-11-95 ~]# docker pull sepiro2000/hello-jib:apm_enabled
apm_enabled: Pulling from sepiro2000/hello-jib
5843afab3874: Pull complete
8bc50dd1755c: Pull complete
fe9f865fe2cf: Pull complete
8e5a2ca795c6: Pull complete
3abe67bfcfac: Pull complete
a78fa1d5de9b: Pull complete
90c7c4122f60: Pull complete
a87030976dd9: Pull complete
Digest: sha256:2edcd8956d11fedaadfad7dba79920cfb83bd82060cae23496733844aaf00dae
Status: Downloaded newer image for sepiro2000/hello-jib:apm_enabled
docker.io/sepiro2000/hello-jib:apm_enabled
[root@ip-172-31-11-95 ~]# docker images
REPOSITORY             TAG           IMAGE ID       CREATED        SIZE
sepiro2000/hello-jib   apm_enabled   db4eb2926467   51 years ago   229MB
[root@ip-172-31-11-95 ~]# docker run -p 8080:8080 db4eb2926467

그리고 애플리케이션이 정상적으로 떴는지 curl 명령을 이용해서 확인해 봅니다.

[ec2-user@ip-172-31-11-95 ~]$ curl -s http://localhost:8080
hello, jib!

애플리케이션이 정상적으로 올라왔으니 이제 ALB 부터 생성해 보겠습니다.


ALB 동작 원리 살펴보기

테스트를 위해 ALB를 생성해 보겠습니다. 아래는 우리가 테스트 환경에 사용할 시스템 구성도 입니다. ALB는 80 포트의 리스너를 생성해서 트래픽을 타겟 그룹으로 넘겨줍니다. 타겟 그룹은 ALB로부터 받은 트래픽을 hello-jib-ec2 인스턴스의 8080 포트로 넘겨줍니다. 즉 앞단에서 80 포트로 들어온 트래픽을 백엔드 서버의 8080 포트로 넘겨주는 구조입니다.

테스트 시스템 구성도

생성은 아래와 같은 테라폼 코드를 사용합니다.

resource "aws_lb" "alb" {
  name     = "hello-jib-alb"
  subnets  = ["subnet-182c2954", "subnet-28fe2f43", "subnet-bb0230e7", "subnet-c2bf3db9"]
  internal = false
  security_groups = [
    aws_security_group.alb.id
  ]
  load_balancer_type = "application"
  ip_address_type    = "ipv4"
}

resource "aws_lb_target_group" "alb" {
  name                 = "hello-jib-alb"
  port                 = 8080
  protocol             = "HTTP"
  vpc_id               = "vpc-84ce69ef"
  slow_start           = 0
  deregistration_delay = 0

  health_check {
    interval            = 10
    port                = 8080
    path                = "/"
    timeout             = 3
    healthy_threshold   = 2
    unhealthy_threshold = 2
    matcher             = "200"
  }
}

resource "aws_lb_listener" "alb" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.alb.arn
  }
}

resource "aws_lb_target_group_attachment" "hello-jib" {
  target_group_arn = aws_lb_target_group.alb.arn
  target_id        = "i-0c471083e79966606"
}

resource "aws_security_group" "alb" {
  name        = "hello-jib-alb"
  description = "hello-jib-alb"
  vpc_id      = "vpc-84ce69ef"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["1.240.235.98/32"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

}

resource "aws_security_group" "ec2" {
  name        = "hello-jib-ec2"
  description = "hello-jib-ec2"
  vpc_id      = "vpc-84ce69ef"

  ingress {
    from_port = 8080
    to_port   = 8080
    protocol  = "tcp"

    security_groups = [
      aws_security_group.alb.id
    ]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

EC2가 사용할 SecurityGroup은 8080 포트를 ALB로부터 들어오는 트래픽만 받을 수 있게 해 두었습니다. 이제 terraform apply를 해서 완료되면 아래와 같이 ALB와 타켓그룹이 생성되었을 겁니다.

타겟 그룹 생성 및 서비스 상태 확인하기

이제 curl 명령을 이용해서 응답을 잘하는지 확인해 보겠습니다.

❯ curl http://hello-jib-alb-973008544.ap-northeast-2.elb.amazonaws.com
hello, jib!
❯ curl http://hello-jib-alb-973008544.ap-northeast-2.elb.amazonaws.com/divide/50
2

ALB도 동작을 잘하고 있으니 EC2 인스턴스에 ssh로 들어가서 본격적으로 tcpdump로 패킷 덤프를 생성해 보겠습니다.

[root@ip-172-31-11-95 ec2-user]# tcpdump -A -vvv -nn port 8080 -w alb_test_dump.pcap
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes

그리고 위에 처럼 curl 명령으로 몇 번 테스트 API를 날려 봅니다. 그 후 생성된 pcap 파일을 가져와서 와이어샤크로 열어 보겠습니다.

와이어샤크 화면

우리가 curl을 통해 테스트한 API들이 패킷 덤프에 보이는 것을 확인할 수 있습니다. 그런데 한 가지 이상한 건 SourceIP가 172.31.42.119 라는 것 입니다. 요청은 제가 노트북에서 날렸는데 패킷 덤프에서 확인한 SourceIP는 제 IP가 아닌 172.31.42.119 라는 처음 보는 IP 입니다. 왜 그런 걸까요? 조금 더 살펴보기 위해 패킷의 내용을 살펴보겠습니다.

HTTP Stream을 통해 패킷 내용 살펴보기

헤더 중 X-Forwarded-For 라는 헤더들에 요청자의 IP가 기록되어 있는 것을 볼 수 있습니다. 또한 요청자가 사용한 프로토콜이 X-Forwarded-Proto에 기록되어 있고 이때 사용한 포트도 X-Forwarded-Port에 기록되어 있는 것을 볼 수 있습니다. Host 헤더는 ALB의 도메인 이름이고요. 즉, ALB는 사용자의 요청을 자기가 한 번 받은 후에 그 내용을 바탕으로 패킷을 새로 만들어서 백엔드에 있는 서버로 보낸 다는 것을 알 수 있습니다. 중간에서 일종의 프록시 역할을 하는 셈이죠. 그래서 패킷 덤프 상에서의 SourceIP 가 제 노트북이 아니고 ALB의 IP로 바뀝니다.

그리고 curl 명령을 통해 API를 호출한 제 노트북에서도 패킷 덤프를 생성해 보겠습니다.

❯ sudo tcpdump -A -vvv -nn -w ./alb_test_dump_from_local.pcap host hello-jib-alb-242068031.ap-northeast-2.elb.amazonaws.com

생성된 패킷 덤프를 확인해 보겠습니다.

로콜에서의 패킷 덤프 내용

Host 에 있는 호스트 이름이 다른 이유는 중간에 ALB를 새로 만들어서 그렇습니다. ㅠ_ㅠ

EC2 인스턴스에서 생성한 패킷 덤프와는 사뭇 다릅니다. /divide/50 에 대한 응답 요청을 그대로 전달해 주는 것을 볼 수 있습니다. 당연히 응답 헤더에 대해서는 ALB가 관여할 부분이 없다 보니 당연한 결과 일 겁니다. 패킷 덤프 내용들을 바탕으로 확인한 ALB의 기능은 아래와 같습니다.

우선 사용자의 요청에 대한 패킷을 받아서 거기에 필요한 몇몇 헤더를 추가해서 새로운 패킷을 만들어 내고 그 패킷을 백엔드에 있는 서버에게 전달해 줍니다. 그리고 백엔드 서버로부터 받은 응답은 그대로 사용자에게 돌려줍니다. ALB는 마치 nginx reverse proxy 서버처럼 동작하게 됩니다.

ALB의 동작 원리

그래서 애플리케이션 입장에서는 누가 요청한 건지 요청자의 주소를 정확하게 파악하기 위해서는 X-Forwarded-For 를 비롯한 X-Forward로 시작하는 헤더들을 활용해야 합니다. 이런 동작 방식은 ALB가 Layer7 에서 동작하기 때문입니다. 이렇게 동작하면 ALB가 가지는 장점이 뭘까요? ALB는 L7에서 동작하는 프로토콜이기 때문에 HTTP/HTTPS로 인입되는 요청들의 헤더들을 변경하고 추가하는 등등의 작업을 할 수 있습니다. 그리고 앞 단에서 ALB가 모든 트래픽을 일단 받은 후에 백엔드로 넘겨주기 때문에 백엔드 서버들이 커넥션을 맺고 처리하는 작업이 줄어들게 됩니다. 이 부분에 대해서도 패킷 덤프를 통해 직접 확인해 보겠습니다. 아래와 같이 두 번 이상 API 호출을 해보겠습니다.

❯ curl http://hello-jib-alb-242068031.ap-northeast-2.elb.amazonaws.com/divide/25
4
~
❯ curl http://hello-jib-alb-242068031.ap-northeast-2.elb.amazonaws.com/divide/50
2
~
❯ curl http://hello-jib-alb-242068031.ap-northeast-2.elb.amazonaws.com/divide/10
10
~
❯ curl http://hello-jib-alb-242068031.ap-northeast-2.elb.amazonaws.com/divide/4
25

로컬에서 패킷 덤프를 떠보면 아래와 같이 다수의 SYN 패킷이 잡히는 것을 볼 수 있습니다. 기본적으로 curl 을 통해 호출한 API가 keepalive 가 되지 않고 한 번 호출하면 끊는 형태로 테스트되기 때문에 당연한 결과 일 겁니다.

다수의 SYN 패킷 확인 (1번, 12번, 23번)

하지만 ALB와 백엔드 서버 사이에서는 커넥션을 재활용할 수 있기 때문에 ALB로 들어오는 모든 SYN 패킷이 EC2로 전달되진 않습니다. 아래 EC2 인스턴스에서 생성한 패킷 덤프를 보면 SYN 패킷은 하나인데 처리한 HTTP GET 요청은 두 개 임을 볼 수 있습니다. 즉 커넥션 한 개로 클라이언트의 요청 두 개를 처리한 것입니다.

EC2 인스턴스 상에서의 패킷 덤프

이렇게 ALB와 백엔드 서버 사이에 커넥션을 재활용한다는 건 백엔드 서버의 커넥션 처리 부하를 낮춰주고 이를 통해 성능을 더 향상 시킬 수 있다는 것을 의미 합니다. 요청마다 커넥션을 만들지 않고 만들어져 있는 TCP 커넥션을 재활용 한다는 것은 성능적으로 상당한 이점을 가져옵니다. TCP 커넥션을 맺고 끊는 건 비용이 많이 드는 작업이기 때문입니다. HTTP 1.1이 keepalive 라는 기능을 넣고, HTTP 2가 스트리밍 기능을 넣은 것들만 봐도 다들 TCP 커넥션을 재활용하기 위해 얼마나 노력하는지 알 수 있습니다. 그리고 온프레미스 환경을 떠올려 보면 대부분 톰캣과 같은 WAS 서버 앞에 nginx와 같은 WEB 서버를 띄워서 nginx가 클라이언트의 커넥션 연결 요청을 앞 단에서 받아주고 HTTP 요청을 톰캣으로 넘겨주는 구조를 많이 사용하는데 그런 구조와 같은 원리라고 보시면 됩니다.

ALB의 커넥션 재활용

커넥션 재활용과 관련된 내용은 AWS 공식 문서에도 아래와 같이 언급되어 있습니다.

ALB의 커넥션 재활용 (https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html)

지금까지 ALB의 동작 원리에 대해서 살펴봤습니다. ALB는 전형적인 L7 로드밸런서이기 때문에 HTTP/HTTPS 로 서비스하는 곳이라면 당연히 사용하게 되는 로드 밸런서입니다. 특히 HTTPS 통신을 하기 위한 클라이언트와의 TLS 협상도 ALB가 해줄 수 있기 때문에 백엔드 서버는 TLS 활성화 및 인증서 관리에 대한 부담을 줄일 수 있습니다. 그럼 NLB는 ALB와는 어떻게 다르게 동작하는 걸까요? 문서들을 보면 NLB는 Layer 4에서 동작하는 로드 밸런서라는 이야기가 많이 나오는데요, L4에서 동작하는 로드 밸런서는 과연 뭐가 다른 걸까요? 이에 대해서는 분량 실패로.. 다음번 글에서 살펴보겠습니다.

ps. ALB 내용 만으로 이렇게 길어질 거라고 생각 못했는데 쓰다 보니 쓸 이야기가 점점 많아져서 이렇게 되었네요. ㅠ_ㅠ

ps2. NLB가 더 꿀잼인데..

반응형