필수 요구사항 - HJ-Rich/2022-MyRSS GitHub Wiki

필수 요구사항



✅ 1차 데모 요구사항

팀의 문화/원칙 소개

1. Make it work, make it right, make it fast. by Kent Beck

  • Minimum Visible Product를 빠르게 생산하는 것을 무엇보다 최우선으로 삼는다.
  • 더 나은 구현을 위한 고민 때문에 MVP 생산이 지연되어서는 안된다.
  • 동작하는 애플리케이션이 생산된 뒤, 달리는 기차의 바퀴를 교체하자.

2. 기억보단 기록을 by 이동욱

  • 모든 것을 영원히 기억할 순 없다. 고로 기록한다.
  • 내가 다른 이들의 기록을 통해 성장했듯, 나의 기록도 누군가에겐 도움이 될 수 있다.

3. 비즈니스 관점을 겸비한다

  • 개발자로서 더 나은 아키텍처, 더 나은 성능을 추구하는 것은 당연한 일이다.
  • 추가로 내 노동의 결과물이 어떤 비즈니스 가치를 창출하는지에도 관심을 가져야 한다.
  • 고객에게 어떤 가치를 전달할 것인가. 나는 어떤 가치를 창출할 것인가. 나는 어떤 문제를 해결하고 싶은가.
  • 애플리케이션은 이 질문에 답하는 수단이어야 한다.

맨 위로



✅ 2차 데모 요구사항

브랜치 전략 공유

release 브랜치를 생략한 Git Flow를 선택

  • Git Flow, Github Flow 등의 선택지 중에 Git Flow를 선택
  • 다만 release 브랜치와 이를 이용한 QA 과정은 develop 브랜치에서 수행함으로써 생략
    • Github Flow 등의 단일 브랜치만 운영하기에는 실제 운영 환경과 최대한 유사한 환경에서의 테스트 과정을 누락하기 어렵다고 판단.
    • 별도의 QA팀이 존재하지 않고, 애플리케이션이 이제 개발 시작 단계라면, Develop 브랜치에서 테스트 후 운영 배포해도 충분하다고 판단.
  • 이슈 생성 -> develop 브랜치에서 feature/개발내용 브랜치 파생 -> develop으로 PR & 머지 -> main에 develop PR & 머지 프로세스로 진행.
    • MVP 1차 배포 전까지는 develop 브랜치에 직접 작업
  • 🥄 Git Flow 한 스푼
  • Git Flow 가 CI/CD 와 어울리지 않는 이유 by David Farley

API 설계 공유

/api 를 prefix로 갖는 Restful API 설계를 추구

  • 모든 api는 /api/** 형식으로 /api를 prefix로 가지도록 설계
    • Web Server에서 프론트엔드, 백엔드를 분기하는데 location이 /api 일 경우 백엔드로 분기하도록 처리하는데 사용
    • 프론트와 백엔드가 Same-Origin에 존재할 수 있게 해주는 데 핵심적인 역할
  • Restful API 를 가급적 준수하여 직관적이도록 설계
    • 조회 GET, 생성 POST, 수정 PATCH, 삭제 DELETE 메서드를 사용
      • POST 는 GET, PUT, DELETE 에 확실히 속하지 않는 요청에도 사용함
      • PUT 은 해당 자원이 없으면 생성 및 201응답, 있으면 대체 및 200응답하는 것이 규약. 따라서 수정은 PATCH 선택
  • 예시 엔드포인트들
    • GET /api/feeds?page={page}&size={size} : 전체 피드 조회 및 페이징
    • POST /api/rss : JSON 형태의 body 값을 이용해 RSS 정보 등록
    • DELETE /api/rss/{rssId} : PathVariable로 전달된 RSS 식별자를 통해 RSS 정보 삭제

개발 서버에 배포 후 데모

개발 서버와 운영 서버의 분리

  • 개발, 운영 환경을 분리함
  • 개발 서버는 develop 브랜치, 운영 서버는 main 브랜치 기준으로 배포됨
  • 따라서 개발 서버의 변경사항은 운영 환경에 영향을 미치지 않음
  • 개발 서버는 적극적으로 배포 및 롤백을 수행하기에 부담이 없어서 선 적용 및 테스트 후 이슈가 없다면 main으로 머지하는 방식으로 사용

쉘 스크립트 또는 CI 도구를 활용한 배포 자동화

Github Webhook -> Jenkins Multibranch-pipeline -> 서버 배포

  • Github -> Jenkins -> 서버 로 자동 배포되도록 구성
  • Github에서 Jenkins로 연결은 Github의 Webhook 기능을 이용
  • Jenkins에서는 리포지토리 내 변경사항을 감지해서 frontend/** backend/** 각 경로 이하 변경이 있을 경우에만 프론트, 백엔드를 각각 재배포
    • 또한 Jenkins에서는 develop, main 브랜치를 식별하여 develop일 경우 개발환경으로, main일 경우 운영환경으로 배포
  • Jenkins는 빌드 결과를 서버로 전달한 뒤, 서버에 있는 쉘 스크립트를 실행하고 Slack으로 알림을 보냄으로써 본인의 Job을 종료
  • 서버의 쉘 스크립트는 기존에 배포되어 있던 프로세스를 종료한 뒤, 새로운 빌드 파일로 프로세스를 백그라운드 실행함으로써 최종 배포 처리
  • Jenkins, Multibranch-pipeline을 이용한 CD

맨 위로



✅ 3차 데모 요구사항

API 문서화

  • RestDocs와 테스트를 이용해 API 문서를 자동으로 생성하도록 구성
    • build.gradle 에 플러그인 추가 및 태스크 설정 추가
    • Documentation Test 작성
    • build 결과물인 snippet을 조합하는 adoc 작성
    • adoc들을 통합하는 adoc 작성
    • index.html로 문서 제공
image



테스트 전략을 수립하고 자동화 테스트 코드를 추가

  • Github Actions를 이용해 PR Open, Synchronize, Merge, Push 이벤트 발생 시 CI 수행
    • gradle build 기반으로 CI 수행
    • 테스트 수행 결과로 생성되는 xml파일을 통해 테스트 커버리지, 테스트 실패 사항을 PR Checks 코멘트로 자동 등록하도록 구성
    • Slack Notification 라이브러리를 통해 CI 중 빌드 실패 발생 시 Slack 알림 전송
    • 빌드 결과를 SonarQube 서버에 전송하여 PR Decoration 코멘트로 작성해줌
  • Jenkins를 이용한 CD에서도 배포 전 gradle build를 수행하기에 테스트 과정이 포함됨
    • 빌드 실패시 배포되지 않으며, Slack으로 알림 전송하도록 구성
  • Github Actions 를 이용한 CI 테스트 자동화
  • Jacoco with Github Action

디버깅할 수 있는 로그 파일 출력

  • logback 을 이용한 로컬, 개발, 운영 환경별 별도 설정 진행
  • 로컬 설정
    • springframework : INFO 이상
    • com.myrssmanager : DEBUG 이상
    • 출력 위치 : 콘솔
  • 개발 및 운영 설정
    • springframework : INFO 이상
    • com.myrssmanager : INFO 이상 (개발에선 DEBUG 이상)
    • 출력 위치 : 파일 출력
    • 파일 출력 상세 설정 :
      • RollingFileAppender를 이용해 로그 레벨 별 별도 파일로 출력하도록 구성
      • RollingFileAppender를 이용해 날짜별로 구분, 10MB 이상이 될 경우 파일별로도 구분하도록 구성
      • RollingFileAppender를 AsyncAppender 로 감싸서 비동기로 출력하게 하여 성능 향상 노림



실 서버 도메인 연결, HTTPS 적용 + WS + WAS 연결을 통해 443포트로 데모

맨 위로



✅ 4차 데모 요구사항

정적 분석 리포트를 공유한다.

  • Github Actions 를 이용, PR 생성 시 정적 분석 결과를 SonarQube 서버로 전송
  • PR 생성을 통해 SonarQube 서버로 분석 결과가 전송될 시, PR에 Comment 형식으로 리포트가 작성되도록 구성
  • SonarQube 서버에서는 Code Smell 등을 감지하여 클린 코드를 위한 리팩터링을 진행
  • SonarQube with Github Actions (PR Decoration)
SonarQube Code Smell



CloudWatch logs 대시보드를 구성한다.

  • 백엔드 서버에서 발생하는 로그 스트림과 메트릭을 AWS CloudWatch Server로 전송하도록 구성
    • 로그백을 통해 생성되는 특정 로그 출력 폴더 이하 모든 .log 파일을 전송하도록 설정
    • CPU 이용률, 네트워크 I/O 메트릭도 추가적으로 전송
  • AWS CloudWatch Dashboard 를 구성하여 웹을 통해 쉬운 모니터링이 가능하도록 구성
  • AWS CloudWatch

image



맨 위로



✅ 5차 데모 요구사항

레벨4 기간 중 개발할 작업 목록 공유

  • 비로그인 상태에서 볼 수 있는 추천 기술블로그 피드 채널 제공 -> 완료
  • Github 로그인, 로그아웃 -> 완료
  • 로그인 상태에서 구독, 개인화하여 구성되는 피드 채널 제공 -> 완료
  • 특정 피드를 공유하며 자신의 생각을 덧붙여 공유할 수 있는 SNS 채널 제공 -> 개발 예정
    • SNS 채널 피드에 대한 좋아요, 댓글 기능 제공 -> 보류
  • (Optional) 회원이 메일 설정을 활성화할 경우, 구독한 RSS 중 지난 한 주간 새롭게 등록된 피드들을 요약하여 메일 발송 서비스 -> 보류
  • (Optional) 회원이 Slack Webhook URL제공 시, 구독한 RSS 중 새로운 피드 등록 시 Slack 알림 발송 서비스 -> 보류

톰캣 설정 중 아래 값을 적절하게 설정하고, 해당 값으로 설정한 이유를 공유

threads max, max connections, accept count

  • 설정에 대한 스프링 공식문서 설명
  • 설정에 대한 Tomcat 공식문서 설명
  • 결론
    • server.tomcat.accept-count=50
    • server.tomcat.max-connections=400
    • server.tomcat.threads.min-spare=10
    • server.tomcat.threads.max=10
  • 도출 과정
    • 개발 서버 피드 조회 API에 대해 300명이 10번 호출하는 상황으로 부하테스트
    • acceptCount, maxConnections, threads.max 기준으로 테스트 수행
    • 기본값 : 100, 8192, 200 -> 197 TPS, 1293ms
    • threads.max 테스트
      • 100, 50, 10, 5, 1로 줄여가며 테스트
      • 10일 때 213 TPS, 1160ms 로 가장 좋은 결과
      • threads.max 는 10으로 결정
    • max-connections 테스트
      • threads.max를 20분의 1 수준으로 줄였으니 같은 비율로 줄여 400부터 시작
      • 400, 100, 10, 10(accept-count도 10) 로 줄여가며 테스트
      • max-connections와 accept-count 모두 10으로 줄였을 땐 153 TPS, 1008ms 로 성능 저하 발생
      • 그외엔 214 ~ 216 TPS, 991ms ~ 1143ms 로 유의미한 차이가 없음
      • CPU 연산 보다는 I/O 블럭이 많은 웹 애플리케이션 특성 상, 스레드 보다 많은 커넥션 수를 설정하는 것이 적절하다고 판단
      • 추가로 쓰레드 기본 설정값의 비율과 맞춰 400을 설정하기로 결정
    • accept-count
      • max-connections가 가득 찼을 때 OS에서 요청을 담아두는 큐 크기
      • OS 설정에 따라 이 설정을 무시할 수도 있음
      • accept-count가 가득 찬 뒤 요청이 추가로 오면 OS가 연결 거부하거나 타임아웃됨
      • 처리할 수 있는 양에 따라 accept-count도 조절해야할 것으로 판단
      • 기본값 100에서 50으로 줄여서 설정하기로 결정

image
  • 테스트 설정

image
  • 최종 설정 부하 테스트 결과 : 221 TPS, 1110ms
  • threads.max=10, min-spare=10, max-connection=400, accept-count=50
  • 기본 설정인 200, 10, 8192, 100일 때의 성능은 197 TPS, 1293ms
  • 커스터마이징 후 TPS 10.86%, 평균응답시간 14.15% 개선



서비스에서 사용하는 쿼리를 정리하고, 각 쿼리에서 사용하는 인덱스 설정

  • 서비스에서 사용하는 모든 조회 쿼리와 테이블에 설정한 인덱스 공유
  • 인덱스를 설정할 수 없는 쿼리가 있는 경우, 인덱스를 설정할 수 없는 이유 공유

  • 공통 테스트 환경 관련
    • EC2 인스턴스 신규 생성, MySQL 설치, 운영 환경 DDL을 이용해 테이블 생성
    • JdbcTemplate batchUpdate로 100만건을 10만개씩 나눠서 10회에 걸쳐 총 1000만건 더미 데이터 삽입
    • 1000만 건 데이터 삽입에 짧게는 3분에서 길게는 9분 소요

회원 관련 쿼리

  1. 테스트 전 예상
  • 로그인 시, Github OAuth를 통해 얻은 Provider Id를 기준으로 DB에 일치하는 회원이 존재하는지 검증
  • 존재하지 않을 경우 Save, 존재할 경우 조회된 결과를 기반으로 로그인 처리
  • Provider Id를 조회 조건으로 사용하므로 인덱스 설정해야할 것으로 추측

  1. 테스트 결과
  • provider_id 컬럼으로 회원을 조회했을 때, 4.2초
  • id(PK) 컬럼으로 회원을 조회했을 때, 0.08초
  • provider_id 컬럼에 인덱스 설정 후 0.08초로 50배 성능 향상

  1. 실행 계획 비교
image image
  • 위부터 차례대로 id로 조회, 인덱스 없는 provider_id 로 조회, 인덱스 적용 후 provider_id 로 조회.
  • 인덱스를 사용하도록 실행계획이 변경되었음

  1. 삽입 성능 비교
image image
  • 인덱스 적용 전과 후의 1000만 건 삽입 속도 비교
  • 쓰기 성능 하락은 무시해도 될 수준으로 판단



피드 관련 쿼리

  1. 테스트 전 예상
  • id로 조회, link 컬럼으로 조회 두 가지 쿼리가 사용됨
  • id로 조회는 PK 사용이므로 별도 설정 불필요할 것으로 예상
  • link 컬럼에 인덱스 설정을 통한 성능 향상이 예상됨

  1. 테스트 결과
image
  • link 컬럼이 varchar(1000) 이어서 utf8mb4 형식에서 4000byte가 됨.
  • index 최대 길이가 약 3000byte 여서 길이 제한 초과로 인해 인덱스 설정이 불가함.
  • 대신 fulltext index 설정을 진행해봄
  • 유사성을 기준으로 정렬해주므로 limit 1 을 추가해서 PK 조회와 거의 유사한 수준으로 성능 구현 되었음
  • 다만, fulltext 인덱스로 인한 쓰기 성능의 극단적인 저하가 유발됨.
  • 삽입 양이 늘어날수록 쓰기가 지연되어 빈 테이블에 1000만건을 삽입하는 로직 자체가 예외 발생되어버림
  • URL Shortener와 JPA Converter를 이용해 DB 인서트 시 길이를 줄여볼까 고민했으나 오버엔지니어링이라고 판단
  • URL이 750글자를 초과한다면 그것까진 대응하지 않아도 된다고 판단하여 link 컬럼을 varchar(750)으로 수정하고 인덱스 설정으로 마무리
  • link 컬럼으로 조회 시에도 PK 조회와 동일한 수준의 성능 확인

  1. 실행 계획 비교
image
  • 위에서부터 id로 조회, 인덱스 없이 link로 조회, 인덱스 설정 후 link로 조회
  • 인덱스 설정 전, 1000만 건 데이터에서 link 로 조회 시 14초 소요
  • 인덱스 설정 후 id 조회와 유사하게 0.14초 소요로 성능 개선

  1. 삽입 성능 비교
image
  • 회원 사례와 동일하게 인덱스 설정으로 인한 쓰기 성능 하락은 미비했음
  • 단, link 컬럼의 길이를 1000에서 750으로 줄여 일반 인덱스를 걸었을 때의 이야기.
  • link 길이를 1000으로 유지하려 fulltext 인덱스를 걸었을 때엔 인덱스 생성에만 8분이 소요됨.
  • fulltext 인덱스를 설정 한 후, 1000만 건 데이터를 삽입 시도 해봤으나, 데이터가 늘어날수록 삽입 속도가 크게 늘어나다가, 700만개째 삽입 시도 중 예외 발생하며 처리 실패.
  • fulltext 설정 시, where 절에 match against 문법을 이용해 유사성 내림차순 조회라는 강력한 기능을 사용할 수 있지만, 삽입 성능을 크게 포기해야함을 알 수 있었음
  • 사용자와 직접 인터렉션하지 않는 피드 업데이트 로직에서는 fulltext 인덱스를 통한 쓰기 성능이 다소 희생되더라도 추후 개발될 수 있는 피드 검색에서의 강력한 조회 기능을 위해 ElasticSearch 등의 검색 엔진을 도입하기 전까지는 피드의 제목과 description에 fulltext 인덱스를 거는 것도 방법이 될 수 있겠다.



북마크 관련 쿼리

  1. 테스트 전 예상
  • bookmark 테이블은 id(pk), member_id(fk), feed_id(fk) 를 갖는 테이블
  • where절에 member_id, feed_id 를 모두 사용하는 이미 북마크한 피드인지 조회하는 쿼리 존재
  • where절에 member_id 만 사용해서 한 회원의 구독한 피드들을 조회하는 쿼리 존재
  • 모조리 인덱스이니 성능이 아주 나쁘진 않을 듯 한데, fk 둘을 묶어서 조회하는 경우, 묶어서 새롭게 키를 생성하면 더 성능이 향상될지 궁금하다

  1. 테스트 결과
  • member_id(fk)로만 조회하는 경우 (회원 개인의 북마크한 피드 조회)

    • member_id(fk)를 where절에서 사용하여 조회할 경우, 1000만건 기준 1초 소요
    • 인덱스로 member_id 를 활용하지만, Using Where, Temporary, filesort가 사용됨
    • member_id가 카디널리티가 더 높아서 member_id, feed_id로 다중컬럼 인덱스 생성
    • 1000만건 기준 1초에서 0.2초로 개선
    • 다중컬럼 인덱스가 사용되고, Using Where 이후 Using Index가 추가됨
  • member_id(fk) + feed_id(fk)로 조회하는 경우 (북마크 추가 시 이미 북마크 되어있는지 여부 확인)

    • member_id = ? and feed_id = ? 로 where절에서 조회하는 경우 1000만건 기준 54ms 소요
    • Using intersect로 두 인덱스로 조회한 결과를 머지하여 필터링
    • 다중 컬럼 인덱스 처리 후
    • 1000만 건 기준 54ms 에서 46ms로 개선
    • Using intersect에서 Using Index로 개선

  1. 실행 계획 비교
image image
  • member_id(fk)로만 조회 시 feed_id와 다중 컬럼 인덱스를 잡지 않았을 때(위)와 잡았을 때(아래)
  • 설정 이후 다중 컬럼 인덱스가 인덱스 키로 사용되고, Using Where 이후 Using Index가 추가되었다

image image
  • member_id(fk) + feed_id(fk) 로 조회할 시 다중컬림 인덱스 잡지 않았을 때(위)와 잡았을 때(아래)
  • 설정 이후 다중 컬럼 인덱스가 인덱스 키로 사용되고, Using Intersect가 Using Index로 개선되었다

  1. 삽입 성능 비교
image image
  • 다중 컬럼 인덱스를 잡기 전과 후의 1000만 건 삽입 성능 비교
  • 241초에서 544초로 다소 하락
  • bookmark에 대한 삽입은 회원이 북마크를 추가할 때 발생
  • 여러 건이 동시에 발생하지 않고 무조건 단건으로만 발생
  • 북마크 추가 시 조회 후 쓰기인데, 조회 성능이 훨씬 개선되므로 다중컬럼 인덱스를 거는 것이 사용성 개선에 적절하다고 판단



맨 위로



✅ 6차 데모 요구사항

hikariCP 커넥션풀 설정

  • 수치를 설정하고 정한 이유를 발표한다
  • hikariCP configuration 보고 필요한 값 설정한다
  1. 풀 사이즈에 대해 : 기본값 10 그대로 유지
image



  • 공식 문서 와 문서 내 영상을 참고
  • 사용중인 인스턴스 코어가 1개이기 때문에 기본설정인 1에서 풀 사이즈를 수정할 필요를 느끼지 못했음.
  • 실제 부하 테스트 결과, 최대 풀 사이즈를 100으로 늘리더라도, 성능 차이가 없었고, 커넥션을 최대 40개까지만 생성함

  1. 그외 설정에 대해 : 권장값 그대로 사용
  • Statement의 캐싱에 대한 설정이 대부분.
  • Hikari가 기본값에서 설정 변경을 권장하는 이유를 추측해보면 다음과 같다.
    • 초기에 설정됐던 기본값 보다는 더 넉넉하게 설정을 줄 수 있을 만큼 하드웨어가 발전함
    • MySQL 버전이 업데이트되며 서버 사이드 캐싱이 가능해짐
  • Statement 캐싱 사이즈를 늘리고, 캐싱을 활성화해주는 옵션들. 권장 값 그대로 사용함.
  • 부하테스트 시 성능상 유의미한 차이는 발견되지 않음
dataSource.cachePrepStmts=true
dataSource.prepStmtCacheSize=250
dataSource.prepStmtCacheSqlLimit=2048
dataSource.useServerPrepStmts=true
dataSource.useLocalSessionState=true
dataSource.rewriteBatchedStatements=true
dataSource.cacheResultSetMetadata=true
dataSource.cacheServerConfiguration=true
dataSource.elideSetAutoCommits=true
dataSource.maintainTimeStats=false





무중단 배포를 구현하고 배포 과정을 발표한다

  • 서비스를 배포하는 중간에도 사용자는 서비스를 계속해서 사용할 수 있어야 한다

구현 과정

  • 아이디어 정리

    • 이중화를 하지 않았기에 블루 그린 배포로 간단히 구현하기로 선택
    • A, B 포트 두개를 오가며 사용하기로 결정
    • 현재 배포된 포트가 A인지 B인지 식별 후 현재 배포되어 있지 않은 포트를 신규 배포 대상 포트로 설정
    • 신규 배포 대상 포트에 실행중인 프로세스가 있을 경우 kill 수행
    • Jenkins를 통해 전달받은 jar 파일을 신규 배포 대상 포트에 백그라운드 프로세스로 실행
    • 20초 대기 후 actuator health check 엔드포인트로 요청을 보낸 후 status:UP 상태 확인 및 실패 시 프로세스 중단
    • Nginx 설정 수정 및 reload
    • 기존 배포 대상 포트 프로세스 종료
  • 실제 구현

    • 쉘 스크립트에서 조건문, 변수 생성 및 값 할당, 변수 재사용 방법 학습
      • A, B 포트 식별 및 변수 생성 및 할당, 배포 대상 포트에 프로세스 실행중일 경우 종료 처리
    • 기존 배포에서 사용하던 nohup 문법으로 실행. sleep 문법을 통해 배포 대기
    • curl -s 명령을 통해 헬스체크한 결과를 변수에 할당 및 status:UP과 응답 문자열이 일치하는지 확인
      • 일치하지 않을 경우 배포 프로세스를 중단해야 했는데, exit 문장을 사용할 경우 클라이언트가 종료되는 이슈 발생
      • early return 을 위해 함수로 선언하여 사용하도록 개선
    • Nginx의 reload 기능을 이용해 restart 없이 proxy_pass 설정을 변경할 수 있음을 이용하기로 결정
      • 동적 변경을 위해 proxy_pass 를 변수처리 후, 변수와 값을 담은 파일을 외부에서 include 하도록 nginx 설정 파일 수정
      • WAS 서버에서 해당 설정을 담은 파일을 생성한 후, scp를 통해 Nginx 서버로 전송하여 include 될 파일 덮어쓰기 처리
      • ssh 명령을 통해 reload 호출
    • 여기까지 성공되었을 경우, 기존 배포 포트 종료
  • 무중단 검증 및 가용성 측정 image

image
  • 개발 서버 배포 프로세스를 실행시킨 후, Jmeter를 이용해 초당 10번의 요청을 전송 테스트
  • 약 1400건의 요청 중, 2건이 정상 응답에 실패함
  • 초당 10번에서 2건이니, 크게 잡아도 0.5초 정도 다운타임으로 가정
  • 1주일에 한 번 배포한다고 가정했을 때, 1년에 다운타임이 30초 미만이므로 six nine, 99.9999% 가용성으로 판정





1,000만명이 사용해도 서비스할 수 있도록 팀의 인프라 아키텍처 개선 방향을 고민한다 (구현 X)



전제


1백 명 이하 사용자 : WAS & DB 분리, 서브넷 분리


image

  • 초기 개발을 마치고 서비스를 시작하는 시점
  • 목표
    • 높은 성능 보다는 비용 효율적일 것
    • WAS, DB 서버의 분리
    • 기본적인 보안
    • 기본적인 모니터링
  • EC2 인스턴스 선택
    • 과도하게 높은 성능을 선택하면 초기 비용 부담이 커짐
    • 지나치게 낮은 성능을 선택하면 비용은 아끼지만 비즈니스 어려울 수 있음
    • 적절한 성능의 인스턴스를 선택하되, 추후 변경 가능한 점 참고.
  • Route 53
    • AWS의 DNS 서비스
    • Public IP를 AWS VPC 내 인스턴스와 연결해준다
  • 서브넷 Public/Private 분리
    • 사용자가 접근해야하는 서브넷과 아닌 서브넷을 분리함
    • WAS는 퍼블릭 서브넷, DB는 프라이빗 서브넷
  • AWS Shield 를 통해 DDos 방어
  • CloudWatch 로 모니터링 및 AWS SNS를 이용한 알람 구성



1천 명 이상의 사용자 : ELB + WAS 이중화 + Managed DB 이중화


image

  • 아직 소규모 서비스이지만, 이제부터는 트래픽 증가를 대비해야 한다
  • 목표
    • AWS 로드밸런서 + WAS 이중화
    • DB 이중화
    • Managed DB 사용 (인스턴스에 직접 설치한 MySQL이 아닌, AWS Managed DB)
  • 다중 가용 영역 활용
    • AWS 하나의 리전에는 최소 2개 이상의 가용 영역이 존재한다.
    • 가령 서울 리전에는 4개의 가용 영역이 존재한다.
    • AWS 로드밸런서를 통해 이중화를 할 때, 아예 다른 가용 영역에 WAS를 추가하는 것이다
  • ELB를 이용한 수평적 확장
    • 안정성을 위해 WAS를 이중화하려면, 이를 앞에서 분배해주는 역할을 누군가 해줘야함
    • AWS의 ELB가 이를 해줌. Elastic Load Balancer
    • 서울 리전 내 A 가용영역에 WAS하나, B 가용영역에 WAS하나를 구성하고, ELB가 서로 다른 가용영역에 있는 WAS 둘에 부하를 분산해줌
  • Managed DB로 전환
    • EC2에 설치된 데이터베이스가 아닌 AWS가 관리해주는 DB서비스 이용
    • 관리 포인트 ⤵️
    • 확장성, 가용성, 내구성, 성능 ⤴️
    • 읽기/쓰기 분리
    • RDS 유형 중 Amazon Aurora 소개
      • MySQL, PostgreSQL과 호환
      • 내부 튜닝을 통해 더 높은 성능을 보인다고 함
      • 3개 가용영역, 6벌 복제, 최대 15개 복제본, 최대 64TB 자동확장



ALB와 NLB


image

  • 큰 범주에서 둘 다 AWS에서 제공하는 ELB 서비스의 일종이다.
  • ALB는 Application Load Balancer
    • L7 기반, HTTP2, HTTPS 지원
    • 소스 IP가 유지되지 않음. 필요시 X-Forwarded-For 같은 헤더 이용해야함
  • NLB는 Network Load Balancer
    • L4 기반, TCP, UDP, TLS 지원
    • ALB와 달리 고정IP 사용 가능



1만 명 이상의 사용자


image

  • 더 이상 소규모가 아님. 중규모 정도는 됨
  • 목표
    • 성능에 본격적으로 신경써야할 시기
    • 인스턴스 증가로 인한 관리 비용이 증가할 시기. 자동화 시작해야.
    • 인스턴스 증가로 인한 비용을 줄일 방법도 필요
    • 보안도 강화해야함
  • Auto Scaling Group을 이용해 성능, 가용성, 비용 효율을 모두 챙김
  • CloudFront, S3를 이용해 정적, 동적 리소스에 대해 캐싱 적용
  • AWS Systems Manager를 이용해 여러 인스턴스 관리 편의성 증대
  • AWS WAF, Shield, GuardDuty 를 이용해 보안 고도화



AWS Auto Scaling Group


image

  • 온 프레미스 환경이라면 피크 트래픽에 맞춰 리소스를 구성해야 한다.
  • 클라우드 환경에선 오토 스케일링을 통해 최적의 리소스를 자동으로 활용 가능.
  • AWS 오토 스케일링
    • 오토 스케일링 그룹에 쓰레드풀, 커넥션풀 만들듯이 최소 최대값을 설정
    • 서버 장애시 최소 수량만큼 자동 복구
    • CloudWatch 지표 기반 스케일링
    • 다른 RDS와 AuroraDB는 DB 인스턴스 자체를 오토 스케일링 가능
      • 다른 RDS들은 스토리지 용량만 오토 스케일링 가능
    • AuroraDB 인스턴스 CPU 메트릭을 기준으로 오토 스케일링 설정 가능



10만 명 이상의 사용자


image

  • 컨테이너 기반 서비스로 전환을 시작해야할 시기

    • 러닝커브가 있어서 처음부터 도입하진 않음
    • 컨테이너는 표준화, 경량화, 이식성, 쉬운 배포로 인해 MSA 필수
    • MySQL : AuroraDB = Kubernetes : EKS(Elastic Kubernetes Service)
  • DB 읽기 성능 개선을 위해 캐싱 적용 필요

  • EKS를 이용해 EC2인스턴스를 대체

  • DB 읽기 성능 개선을 위해 ElasticCache 사용.

    • Cache Miss 일 때에만 DB로 요청 전송



100만 명 이상의 사용자


image image

  • 목표
    • 용도에 맞는 DB 적용
    • 샤딩을 통한 DB 분산
    • 재해복구 (DR) 및 멀티리전 서비스
  • NoSQL 사용하기 - Amazon DynamoDB
    • 장바구니, 위시리스트 처럼 쓰기가 많은 경우 적절
    • 대규모 요청에도 한 자릿수 ms 응답시간
  • 읽기 작업은 ElasticCache → Cash miss 시 DB 또는 DynamoDB
  • 쓰기 작업은 일부 선택한 작업은 DynamoDB에 더 빠르게
  • CDK - Infra as a code
    • 재해 복구를 위해 사용
    • 파이썬, 노드JS, 타입스크립트, 자바 등 언어 사용 가능
    • 인프라, 서비스 구성 자동화 가능
    • 재활용 가능한 템플릿 생성 가능
    • DR 상황에서 복구하도록 구성 가능
  • 데이터 백업
    • S3와 DB에 저장되는 데이터를 다른 리전에 복제시켜둘 수 있음
    • 데이터는 미리 복제 또는 스냅샷을 사용
    • DR 상황에서 구성해두었던 CDK를 이용해 빠르게 복구 구성 가능



1000만 명 이상의 사용자


image image

  • 목표
    • 다중 리전별 서비스를 활성화
  • Aurora 글로벌 DB
    • 여러 리전에 읽기 전용을 구성 (ex. 서울, 런던, 시드니)
    • 단, 쓰기는 한 곳에서만 가능
  • DynamoDB 글로벌 테이블
    • 여러 리전에서 읽기 쓰기 가능
    • 변경이 자동으로 글로벌 리전에 전파됨
  • ElasticCache 글로벌 데이터스토어
    • 읽기 성능 개선을 위해 사용하던 ElasticCache도 글로벌화



결론

  • SPOF 제거를 위한 이중화, Failover, 다중 가용영역, 다중 리전
  • 오토 스케일링
  • 성능, 부하분산을 위해 캐시 사용
  • 모니터링과 보안

맨 위로



⚠️ **GitHub.com Fallback** ⚠️