동아리 RDS 및 Redis 문서화 + 개선점 분석

김태훈·2024년 6월 3일
0

성균관대 Skkuding

목록 보기
18/19

RDS (PostgreSQL)

저희 동아리에서 사용하는 RDS의 설정정보는 terraform 파일에서 확인이 가능합니다. 하지만 직접 console창에 들어가서 여러 설정 정보들을 직접 확인하는 것을 추천드려요. 😊

(1) 엔진

PostgreSQL을 지원하는 AWS RDS는 크게 두가지를 선택할 수 있었습니다. Aurora에서 지원하는 Engine과 PostgreSQL그 자체에서 지원하는 엔진 두가지입니다.

AWS console에서 Database를 만들면

(2) Credential

RDS에 접근하기 위한 Credential 정보는 필수입니다. Credential을 두가지 방법으로 설정할 수 있습니다.

  1. AWS에게 알아서 맡기기 (동아리에서 사용하는 방법이 아닙니다)
  2. 직접 설정하기

지희는 2번을 사용하고 있습니다. 이는 terraform 설정 정보들을 보면 쉽게 알 수 있습니다. (가독성이 안좋아서 사진도 첨부합니다)

resource "aws_db_instance" "db-test" {
  db_name           = "codedang_db"
  engine            = "postgres"
  engine_version    = "14.10"
  allocated_storage = 5
  instance_class    = "db.t4g.small"

  username = var.postgres_username
  password = random_password.postgres_password.result
  port     = var.postgres_port

  vpc_security_group_ids = [aws_security_group.db.id]
  db_subnet_group_name   = aws_db_subnet_group.db_subnet_group.name

  skip_final_snapshot = true
}

위의 코드가 실제 db instance를 생성하는 terraform 코드입니다. 이때, username과 password부분을 직접 설정하는 것을 볼 수 있습니다.

여기서 variables.tf 에 정의된 username을 RDS Credential username으로 사용합니다. 마찬가지로 port도 variables 정보에서 가져옵니다.

[ variables.tf란?]

테라폼에서는 여러 변수들의 정보들을 variable 변수로 설정해서 여러 모듈에서 이를 가져다 사용할 수 있게합니다. 여러 모듈에서 사용하므로, root 폴더에 variables.tf로 포함시켜서 사용해도 괜찮겠죠? 물론 하나의 모듈에서 단일로 사용하는 정보라면 필요는 없습니다.
하지만, 저희는 모든 Architecture 정보들을 terraform 하나를 관리하므로, 서버에서 DB의 정보를 알기 위해서 variables로 공통으로 관리합니다. 특히, task-definition에서 DB Instance의 EndPoint정보를 알아야겠죠?

그 후에, password를 설정하는데, random_password 가 붙어있습니다. terraform에서는, random_password라는 resource 를 지원합니다. 이를 통해서 무작위의 비밀번호를 terraform에게 생성하게 맡길 수 있습니다. 자세한 내용은 아래 문서를 읽어보시길 바랄게요

Terraform Registry

이를 통해, CD(Continous Deployment) 과정에서 환경변수가 주입되어 비밀번호가 설정됩니다. 여기서 여러번의 deployment를 한다고 비밀번호는 바뀌지 않습니다. 이는 tfstate에서 변경되는지 여부를 확인하기 때문입니다. (자세한내용은 테라폼에서..)

(3) DB Config

AWS RDS를 사용하면서 여러개의 DB Config정보를 설정할 수 있습니다. 크게 두가지의 Config정보가 있을 것 같아요. (참고로, Aurora를 사용하면 스토리지 자동확장 및, Multi-AZ를 기본으로 제공하므로 설정할게 많지 않습니다. 그러니까 비쌉니다.)

  1. AWS에서 Explicit하게 설정할 수 있는 정보
    DB Instance Class, Multi-AZ 사용 유무, Allocated Storage, Provisioned IOPS
  2. DB Instance에서 직접 사용할 parameter 정보들
    AWS에 의존하는 정보들이 아닌, RDS에 의존하는 Parameter 정보들입니다. RDS 자체의 튜닝포인트의 지점을 찾을 때, 이 부분에 집중해서 튜닝하면 좋을 것 같습니다.

현재(2024.06.03 기준)는 RDS Parameter(2번)들은 default parameter 정보들을 사용하고 있습니다.
하지만, AWS에서 Explicit하게 설정하는 정보들은 최소한의 비용을 위해서 Multi-AZ도 사용하고 있지 않고, Instance 클래스도 최소화, Allocated Storage도 최소화 하여 사용하고 있습니다. 이 뜻은, 실제 운영을 하기위해서 개선해나가야하는 지점이라는 점입니다.

여기서 Allocated Storage는 IOPS(초당 처리량)의 영향을 줍니다. 지난 로드테스트 당시에 이러한 Allocated Storage에 주목했던 적이 있어서, 로드테스트 페이지를 참고하시면 될 것 같습니다.
https://velog.io/@goat_hoon/k6%EC%99%80%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0%EA%B8%B0

요약해드리면, Allocated Storage 볼륨에 따라, IOPS의 처리량에 한계가 존재할 수도 있다는 점입니다.

아무튼! Multi-AZ설정이나 여러 로드테스트를 통해서 최소한의 비용으로, RDS의 퍼포먼스를 최대화할 수 있도록 많은 튜닝이 필요한 상태입니다.

(4) API Flow

RDS는 API-server, Admin-server에 사용합니다. 채점서버인 Iris에서는 RDS와 연결되어있지 않습니다.

Iris는 ‘채점’서버일 뿐입니다. Iris서버는 MessageQueue와 Redis와만 Connection을 맺고 있습니다. 그렇다면 채점 결과는 어떻게 저장할까요?

특정 유저가 제출한 코드 정보들을 API서버에서 RabbitMQ에게 전송하고, Iris에서는 RabbitMQ에서 해당 요청을 받아와서, 빠른 코드 채점을 위해 Redis에서 테스트케이스를 가져온 후 이를 이용하여 채점을 하고, 채점 결과를 다시 API-server에게 전달하여, 채점 결과를 API-server에서 RDS에 저장합니다.

Redis (ElastiCache)

Redis는 In-Memory Database입니다. In-Memory라고 하면, 데이터를 미리 캐싱하여 굉장히 빠른속도로 Read할 수 있는 장점을 가져다 줍니다. 다만, 메모리 위에 있기 때문에, 전원이 차단되면 날라갈 수 있는 단점이 있습니다. (다만 Redis에서는 복구할 수 있는 방법이 있다고는 합니다)

(1) Redis는 어디서 사용하나요?

  1. 유저의 JWT 로그인 Token의 저장소로 사용하고 있습니다.
    유저의 Authentication을 확인하기 위해 매번 DB를 조회하는 것은 overhead가 발생하므로 이를 위해 Redis를 사용합니다.
  2. 채점 서버에서 문제별로 test case를 받아오는 데에 사용합니다.

(2) Config

그럼 마찬가지로, terraform code를 보면서 Config정보들을 보겠습니다.

resource "aws_elasticache_cluster" "db_cache" {
  cluster_id               = "elasticache-redis-codedang-1"
  engine                   = "redis"
  node_type                = "cache.t3.micro"
  num_cache_nodes          = 1
  parameter_group_name     = "default.redis7"
  engine_version           = "7.0"
  port                     = var.redis_port
  apply_immediately        = true
  snapshot_retention_limit = 0 # no backup

  security_group_ids = [aws_security_group.redis.id]
  subnet_group_name  = aws_elasticache_subnet_group.redis_subnet_group.name

  log_delivery_configuration {
    destination      = "/elasticache/redis"
    destination_type = "cloudwatch-logs"
    log_format       = "text"
    log_type         = "slow-log"
  }
}

RDS에서 설정했던 정보들과 비슷합니다.

마찬가지로, 최소한의 비용을 위해 node_type을 최소화하였습니다.

사실, Redis에서는 인프라적인 측면에서 많은 정보를 알려드리기가 쉽지 않은 것이, 2024.06.03 기준으로 Redis 관련한 성능 측정을 해보진 않았습니다. 다만, 인프라 외적으로 알려드릴 문제점은 있습니다.

(3) 현재 Redis의 문제점

이전에 go-redis 오픈소스에 bug가 있어서 채점서버에서 채점이 되지 않았던 오류가 발생하였습니다.

https://github.com/skkuding/codedang/issues/1475

go-redis의 issue를 보고 해결할 수는 있었지만, 언제 또 open source에 오류가 발생할지는 모르는 일입니다. 또한, Data Read 성능 개선이라는 측면에서 사용된 Redis가 먹통이 되었다는 이유로 Key-feature인 채점서버 기능의 오류가 있어서는 안됩니다.

저희는 Redis를 마치 SDD, HDD Database를 사용하듯이 코드가 구현되어 있습니다.
현재 Iris 서버에서는 문제별로 테스트케이스를 Redis에 저장하고, 이를 가져다가 채점에 사용합니다. 하지만 Redis에 문제가 생길 경우, 이를 해결하기 위한 fall-back 로직이 없습니다. 이를 위한 코드 구현이 필요합니다.

즉, Redis에만 의존할 것이 아니라, Redis와 통신 과정에서 오류가 생길경우, 직접 S3에 접근하여 가져다 사용할 수 있는 로직이 필요합니다.
현재는 해당 Redis에 문제가 있을 경우 빈 Testcase를 반환함 → 이를 수정해야 한다.

func (t *testcaseManager) GetTestcase(problemId string) (Testcase, error) {
	isExist, err := t.cache.IsExist(problemId)
	if err != nil {
		return Testcase{}, fmt.Errorf("GetTestcase: %w", err)
	}

	if !isExist {
		bytes, err := t.source.Get(problemId)
		if err != nil {
			return Testcase{}, fmt.Errorf("get testcase: %w", err)
		}

		elements := []Element{}
		err = json.Unmarshal(bytes, &elements) // validate
		if err != nil {
			return Testcase{}, fmt.Errorf("invalid testcase data: %w", err)
		}

		testcase := Testcase{Elements: elements}
		err = t.cache.Set(problemId, testcase)
		if err != nil {
			return Testcase{}, fmt.Errorf("cache set: %w", err)
		}

		return testcase, nil
	}

	data, err := t.cache.Get(problemId)
	if err != nil {
		return Testcase{}, fmt.Errorf("testcase: %s: %w", problemId, err)
	}

	testcase := Testcase{}
	err = testcase.UnmarshalBinary(data)
	if err != nil {
		return Testcase{}, fmt.Errorf("testcase: %w", err)
	}
	return testcase, nil
}

마찬가지로, 유저의 Authentication에 필요한 fall back로직도 마찬가지입니다.

이를 개선하길 바라며… 이만 마칩니다!

profile
기록하고, 공유합시다

0개의 댓글