티스토리 뷰

배경

서비스를 실제로 사용자에게 공개하기 전에 해야 할 중요한 작업 중 하나는, 내가 만든 서버가 어느 정도의 사용자 유입을 감당할 수 있는지를 파악하는 것이다. 이를 알아낸다면 예상되는 사용자 수를 기준으로 서버를 개선하고, 안정적인 서비스를 제공할 수 있다.

나 역시 DND 프로젝트에서 snappy라는 서비스를 개발하면서, 사용자에게 서비스를 공개하기 전에 서버가 어디까지 버틸 수 있는지 확인하고 싶었다. 그래서 성능 테스트를 도입해보았고, 이번 테스트 과정에서 어떤 문제들이 있었는지, 그리고 그 문제들을 어떻게 해결해 나갔는지에 대해 작성해보려 한다.

성능 테스트

성능 테스트를 하려면 무엇이 필요할까? 우선 가장 먼저 생각나는 것은 내가 만든 서버이다. 서버는 이미 만들어 놓았기 때문에 이 부분은 문제없다.

두 번째로 필요한 것은,  성능 테스트를 위해 대량의 요청을 보낼 수 있는 도구이다. 성능 테스트의 목적은 여러 사용자가 동시에 접속했을 때 서버가 얼마나 잘 버티는지 확인하는 것이므로, 많은 요청을 보낼 수 있는 도구가 필수적이다. 성능 테스트 도구를 검색해 보면 JMeter, nGrinder 등 다양한 도구들이 있다. 나는 그중 nGrinder를 선택했다.

세 번째로, 성능 테스트 동안 서버를 모니터링할 수 있는 도구가 필요하다. 모니터링을 하지 않으면 테스트 후에 서버가 어디에서 병목이 발생했는지, 어느 자원이 부족한지 알기 어렵다. 모니터링을 통해 CPU, 메모리, 네트워크 등 서버 자원의 사용량을 분석하고, 서버 응답 속도 등의 성능 문제를 파악할 수 있다. 모니터링 도구 중에서는 여러 가지가 있지만, 나는 그중 Pinpoint라는 APM(애플리케이션 성능 관리) 도구를 선택했다.

지금 생각해보니, nGrinder와 Pinpoint 모두 네이버에서 만든 오픈소스이다. 갓 네이버..

네 번째로 필요한 것은, 앞서 언급한 세 가지 환경을 클라우드 상에서 구축하는 것이다. 처음에는 로컬에서 모두 돌려 성능 테스트를 시도했다. 그러나 한 컴퓨터에서 API 서버, 모니터링 도구, nGrinder를 동시에 실행하니, 모니터링 도구로 측정한 자원 사용량이 정확하지 않았고, 로컬 컴퓨터에서 서버 성능을 측정하는 것이 실제 배포 환경과는 다르기 때문에 성능 테스트의 신뢰성도 떨어졌다.

따라서, 위에서 언급한 세 가지 요소를 AWS 상에 구축하고, nGrinder를 통해 성능 테스트를 실행한 뒤 Pinpoint로 문제점을 모니터링한 후 성능을 개선하는 과정을 거치면 된다고 생각했다. 

그런데 여기서 또 한 가지 문제가 있었다. 성능 테스트 동안 사용되는 AWS 자원의 비용이다. 성능 테스트를 장시간 진행하다 보면, AWS 요금이 만만치 않게 나올 수 있다. 성능 테스트를 위해 많은 자원을 사용해야 하는데, 이로 인해 발생하는 비용을 어떻게 최소화할 것인가에 대한 고민이 필요했다.

성능테스트 환경 구축 version-1

성능테스트 환경을 aws상에 구축하기 위한 최소한의 자원들은 다음과 같다.

  • VPC (인터넷 게이트웨이, 라우팅 테이블, 서브넷 등등)
  • API 서버를 위한 EC2
  • 데이터베이스를 위한 RDS
  • pinpoint를 위한 EC2 
  • ngrinder를 위한 EC2(ngrider는 원래 controller와 agent를 서로 다른 컴퓨터에서 하는게 best practice지만 그렇게 할 경우 EC2개수가 너무 늘어나 한대에서 돌리기로)

그럼 위에서 AWS 프리 티어 기준으로 비용이 발생하는 곳은 어디일까? 우선 VPC는 NAT 게이트웨이를 사용하지 않는 이상 비용이 발생하지 않는다. RDS 또한 프리 티어로 사용 가능하다.

비용이 발생하는 부분은 EC2이다. 프리 티어에서는 서울 리전 기준으로 t2.micro 인스턴스를 750시간 무료로 사용할 수 있다. 하지만 성능 테스트에 필요한 EC2 인스턴스는 총 3대이고, 모두 t2.micro로 사용한다해도 750시간을 초과하게 되어 추가 요금이 발생하게 된다.

생각해보면 성능 테스트를 위해 EC2를 한 달 내내 켜두는 것은 낭비다. 성능 테스트를 진행하는 동안에만 EC2를 실행하고, 테스트가 끝나면 인스턴스를 삭제하면 된다.

그러나 이렇게 비용 절감을 위해 성능 테스트 때마다 EC2를 생성하고 삭제하는 방식은 새로운 문제를 만든다. EC2를 다시 생성할 때마다 API 서버, nGrinder, PinPoint 등을 설치해야 한다. 반복적인 작업으로 인해 시간이 많이 소모되고, 매번 AWS 콘솔에 들어가 EC2를 설정하는 것이 귀찮아진다.

문제를 요약하면 다음과 같다:
1. 비용 절약을 위해 성능 테스트마다 AWS 콘솔에서 EC2를 새로 생성해야 한다.
2. EC2를 새로 생성할 때마다 API 서버, nGrinder, Pinpoint를 설치해야 한다.

이 두 가지 문제점은 귀찮고, 시간이 많이 걸리며 반복적인 작업이라는 점에서 비효율적이다. 그렇다면, 이를 자동화할 수는 없을까? 클릭 한 번으로 성능 테스트 환경을 구축할 수 있다면 훨씬 편리해질 것이다. 이미 이번 프로젝트에서 CI/CD 파이프라인을 구축하면서 배포를 자동화해본 경험이 있기 때문에, 성능 테스트 환경 구축 역시 클릭 한 번으로 해결할 수 있을 것이라는 자신감이 생겼다.

그래서 시작했다.. 성능 테스트 자동화 환경 구축기

성능테스트 환경 구축 version-2

모든 문제를 한번에 해결해 나가는건 어려운 일이다. 그래서 나는 한가지 문제씩 해결해 나갈 예정이다.

먼저 첫 번째 문제인 성능 테스트를 할때마다 AWS 사이트에 들어가 콘솔을 통해 EC2를 만들어야 하는 문제를 이번 버전에서 해결하려 한다.

그전에 VPC, RDS도 아직 구축을 하지 않았다 이것 또한 변경될 수 있으니 콘솔이 아닌 자동으로 환경을 구축하게 할 수 없을까? 하다가 찾은것이 바로 IaC((Infrastructure as Code)이다. 

IaC((Infrastructure as Code)란?

IaC는 수동 프로세스가 아닌 코드를 통해 인프라를 관리하는 것을 말한다. 쉽게 말해 AWS 자원을 만들때 콘솔로, 즉 수동으로 만드는 것이 아닌 코드를 작성해 AWS자원을 만들 수 있고 언제든지 삭제할 수 있다. 코드로 관리되니 한번 작성한 코드를 통해서 언제든지 다시 AWS 환경을 만들 수 있다. 이를 통해 AWS 콘솔에 매번 접속하여 수동으로 EC2, VPC, RDS 등을 설정할 필요 없이, 코드로 모든 인프라를 정의하고 자동으로 생성할 수 있다.

 

가장 대표적인 IaC 도구로는 TerraformAWS CloudFormation이 있다. 이번 버전에서는 Terraform을 사용하여 성능 테스트 환경을 자동화할 계획이다. Terraform은 AWS, Azure, Google Cloud 등 다양한 클라우드 플랫폼에서 인프라를 관리할 수 있는 오픈소스 도구로, 선언적 구문을 통해 인프라의 상태를 정의하고, 그에 따라 인프라를 배포, 수정, 삭제할 수 있다.

 

먼저 terraform으로 어떻게 AWS 환경을 구축할건지는 대략적인 그림을 그려보았다. 다음 그림과 같이 구축할 예정이다.

성능 테스트 환경

먼저 VPC를 구축하는 terraform 코드이다. 

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "dnd-subnet"
  }
}

// a zone 
resource "aws_subnet" "public_subnet_a" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.0.0/22"

  availability_zone = "ap-northeast-2a"

  tags = {
    Name = "dnd-public-subnet-a"
  }
}


resource "aws_subnet" "private_subnet_a" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.128.0/20"

  availability_zone = "ap-northeast-2a"

  tags = {
    Name = "dnd-private-subnet-a"
  }
}

//b zone 
resource "aws_subnet" "public_subnet_b" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.16.0/22"

  availability_zone = "ap-northeast-2b"

  tags = {
    Name = "dnd-public-subnet-b"
  }
}


resource "aws_subnet" "private_subnet_b" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.144.0/20"

  availability_zone = "ap-northeast-2b"

  tags = {
    Name = "dnd-private-subnet-b"
  }
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "dnd-igw"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "dnd-rt-public"
  }
}

resource "aws_route_table_association" "route_table_association_public_a" {
  subnet_id      = aws_subnet.public_subnet_a.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "route_table_association_public_b" {
  subnet_id      = aws_subnet.public_subnet_b.id
  route_table_id = aws_route_table.public.id
}

resource "aws_route_table" "private_a" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "dnd-rt-private-a"
  }
}

resource "aws_route_table" "private_b" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "dnd-rt-private-b"
  }
}

resource "aws_route_table_association" "route_table_association_private_a" {
  subnet_id      = aws_subnet.private_subnet_a.id
  route_table_id = aws_route_table.private_a.id
}

resource "aws_route_table_association" "route_table_association_private_b" {
  subnet_id      = aws_subnet.private_subnet_b.id
  route_table_id = aws_route_table.private_b.id
}

 

위 코드로 VPC, IGW, public subnet, private subnet, route table을 코드로 관리할 수 있게 되었다.

 

다음으로는 RDS를 만드는 코드이다.

# RDS Subnet Group 생성
resource "aws_db_subnet_group" "db-subnet-group" {
  name = "three-tier-db-subnet-group"
  subnet_ids = [aws_subnet.private_subnet_a.id, aws_subnet.private_subnet_b.id]
}


resource "aws_db_instance" "default" {
  storage_type                = "gp2"
  allocated_storage           = 20
  db_name                     = "snappy"
  engine                      = "mysql"
  engine_version              = "8.0"
  instance_class              = "db.t3.micro"
  parameter_group_name        = "default.mysql8.0"
  db_subnet_group_name        = aws_db_subnet_group.db-subnet-group.name
  availability_zone = "ap-northeast-2a" # 가용 영역
  vpc_security_group_ids      = [aws_security_group.rds_sg.id]
  skip_final_snapshot = true
}

output "rds_endpoint" {
  value = aws_db_instance.default.endpoint
}

 

RDS는 프리티어 기준으로 돈이 안나가게 구성을 해주었다. 

 

다음으로는 EC2 구성이다. 

# EC2

resource "aws_eip" "api_server" {
  instance = aws_instance.api_server.id
  domain      = "vpc"
  tags = {
    Name = "dnd-bastion-eip"
  }
}

resource "aws_instance" "api_server" {
  ami                         = "ami-062cf18d655c0b1e8"
  instance_type               = "t2.micro"
  vpc_security_group_ids       = [aws_security_group.bastion_ec2.id]
  subnet_id                   = aws_subnet.public_subnet_a.id
  key_name                    = "my-key"
  disable_api_termination      = true
  root_block_device {
    volume_size               = "30"
    volume_type               = "gp3"
    delete_on_termination     = true
    tags = {
      Name = "dnd-bastion-ec2"
    }
  }
  tags = {
    Name = "dnd-bastion-ec2"
  }
}

resource "aws_eip" "pinpoint_server" {
  instance = aws_instance.pinpoint_server.id
  domain      = "vpc"
  tags = {
    Name = "pinpoint_server"
  }
}

resource "aws_instance" "pinpoint_server" {
  ami                         = "ami-062cf18d655c0b1e8"
  instance_type               = "t3.medium"
  vpc_security_group_ids       = [aws_security_group.pinpoint.id]
  subnet_id                   = aws_subnet.public_subnet_a.id
  key_name                    = "my-key"
  disable_api_termination      = true
  root_block_device {
    volume_size               = "30"
    volume_type               = "gp3"
    delete_on_termination     = true
    tags = {
      Name = "pinpoint_server"
    }
  }
  tags = {
    Name = "pinpoint_server"
  }
}

resource "aws_eip" "ngrider_server" {
  instance = aws_instance.ngrider_server.id
  domain      = "vpc"
  tags = {
    Name = "ngrider_server"
  }
}

resource "aws_instance" "ngrider_server" {
  ami                         = "ami-062cf18d655c0b1e8"
  instance_type               = "t3.medium"
  vpc_security_group_ids       = [aws_security_group.bastion_ec2.id]
  subnet_id                   = aws_subnet.public_subnet_a.id
  key_name                    = "my-key"
  disable_api_termination      = true
  root_block_device {
    volume_size               = "30"
    volume_type               = "gp3"
    delete_on_termination     = true
    tags = {
      Name = "ngrider_server"
    }
  }
  tags = {
    Name = "ngrider_server"
  }
}



output "api_server_eip" {
  value = aws_eip.api_server.public_ip
}

output "pinpoint_server_eip" {
  value = aws_eip.pinpoint_server.public_ip
}

output "ngrider_server_eip" {
  value = aws_eip.ngrider_server.public_ip
}

 

api 서버, PinPoint를 위한 서버, nGrinder위한 서버를 위해 3개의 EC2를 만드는 코드이다.

PinPoint와 nGrinder는 처음에 t2.micro로 해보았지만 많은 cpu리소스와 메모리를 사용해 t3.medium을 이용하였다.

 

이렇게 다 작성한후 terraform apply 명령어를 통해 AWS 리소스를 만들 수 있다.

 

AWS 콘솔을 보면 EC2 3대가 만들어진 것을 볼 수 있다.

 

Terraform을 통해 통합 테스트 환경을 코드로 관리할 수 있게 되어, 수동으로 AWS 자원을 생성하는 시간을 단축하고 한 번의 명령어로 AWS 환경을 구축하고 구축한 환경을 다시 삭제할 수 있게 되었다. 즉, 성능 테스트만 끝내면 자원을 바로 삭제해 요금이 계속 나가는 것을 방지할 수 있게 되었다.

하지만, 여기서 끝나는 것이 아니다. 각 EC2 인스턴스에 접근해 해당 환경에 맞게 설정을 완료해야 한다. 예를 들어, API 서버는 docker-compose 파일을 작성하여 서버를 시작해야 하며, nGrinder와 PinPoint도 각각 설치 후 시작해야 한다.

설정을 마친 후 성능 테스트를 진행하면, PinPoint에서 성능 테스트 결과를 확인할 수 있다.

 

이제 version 3의 목표는 aws 환경 뿐만 아니라 EC2에 api 서버, ngrinder, pinpoint를 설치해야 하는것 또한 클릭 한 방으로 끝낼 수 있게 할 예정이다. 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
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
글 보관함