0. Table Of Contents

 

 

1. 많은 인덱스를 생성하지 않는다.

다음 사진과 같이 인덱스를 하나의 사전이라고 생각해보자.

위 같은 구조에서 collection에 name이라는 property가 “AB“라는 document를 추가한다고 가정하자. 현재 collection에 등록된 document의 name property에 의한 순서는 “A-B-C-D”로 되어있지만, 추가가 되게 되면 “A-AB-B-C-D“로 변경이 되어야 한다.

먄약에 위처럼 데이터가 엄청 많이 추가되는 상황이라면 위 사전의 목차는 계속해서 업데이트 되어야 할 것이다. 즉, collection의 업데이트가 많은 구조의 index 또한 업데이트가 많이 일어날 것이며 index가 많을 수록 이러한 현상이 많아질 가능성이 높아진다.

또한 인덱스는 시스템 메모리에 상주하고 있는데, 인덱스가 많아져서 메모 디스크 공간을 메모리 처럼 사용하기 위해 가상메모리를 형성하게 된다. 이러한 상태가 지속되게되면 mongodb의 전체적인 퍼포먼스에 부정적인 영향을 줄 수 있다.

이러한 이유로 index를 많이 만드는 것을 지양하는 것이 좋다.

 

 

2. Index Prefix를 적극적으로 이용하자.

MongoDB를 이용해본 개발자라면 compound index의 개념을 알 것이다. 모르는 분은 여기(MongoDB Official Document)에서 개념을 참고한다. 아래와 같은 compound index를 예를 들어보자.

 

db.students.createIndex({name:1, grade:1, createdAt:1})

 

 

위같은 compound index를 해석하면 다음과 같다.

“name으로 asc 정렬한 다음 grade를 asc정렬하고, 마지막으로 createdAt으로 asc 정렬한 index“

 

위 처럼 해석된 compound index가 존재하려면 다음과 같은 조건이 필요하다.

“name으로 asc 정렬한 다음 grade를 asc 정렬한 index”

 

또한, 위처럼 해석된 compound index가 존재하려면 다음과 같은 조건이 필요하다.

“name으로 asc 정렬한 index“

즉, compound index의 경우에 순서를 지킨 (compound)index가 자동으로 적용된다는 것을 알 수 있으며, 공식문서에도 이에 대한 내용을 볼 수 있다. 정리하면, 위 index를 생성함으로써 부가적으로 사용가능해진 index는 다음과 같다.

 

db.students.createIndex({name:1, grade:1, createdAt:1})
db.students.createIndex({name:1, grade:1})
db.students.createIndex({name:1})

 

 

 

 

3. Multi Sorting의 경우 sort 방향 신경써서 index를 설계하자

single index의 경우에는 sort 방향이 필요 없다. 위에서 언급한 사전을 예로 들면, 단순히 사전의 목차를 처음부터 보느냐 마지막꺼부터 역방향으로 보느냐 차이기 때문이다. 그러나 compound index의 경우, 정렬된 것을 다른 것을 기준으로 정렬하기 때문에 sorting 방향을 엄격하게 지킬 필요가 있다. 이를 지키지 않을 시, index를 찾는 것이 아닌 collection 상에서 full scan이 일어나게 된다.

 

 

아래 처럼 index를 만들었다고 가정하자.

 

db.students.createIndex({name:1, grade:1})
db.students.createIndex({name:1, grade:-1})
db.students.createIndex({grade:1, name:1})
db.students.createIndex({grade:1, name:-1})

 


첫 번째 라인의 index를 이용하면 아래와 같은 index들을 사용할 수 있다.

 

db.students.createIndex({name:1, grade:1})    -> original index
db.students.createIndex({name:1})             -> can be used by index prefix
db.students.createIndex({name:-1})            -> can be used inverse of index prefix
db.students.createIndex({name:-1, grade:-1}). -> can be used by inverse of original index

 

 

 

나머지 index들도 위 케이스와 같이 동일하게 적용하면 아래와 같은 find method에 대해 full scan을 할 필요 없이 index만으로 sorting 처리할 수 있게 된다.

 

db.find().sort({name:1, grade:1})
db.find().sort({name:1, grade:-1})
db.find().sort({name:-1, grade:1})
db.find().sort({name:-1, grade:-1})
db.find().sort({grade:1, name:1})
db.find().sort({grade:1, name:-1})
db.find().sort({grade:-1, name:1})
db.find().sort({grade:-1, name:-1})
db.find().sort({grade:1})
db.find().sort({grade:-1})
db.find().sort({name:1})
db.find().sort({name:-1})

 

 

이 외의 sort를 이용하기 위해서는 추가적인 index를 설계해야할 것이며, 현재 가지고 있는 index가 얼마만큼의 sorting을 cover할 수 있는지 잘 판단하여야 한다.

 

 

4. 하나의 collection을 여러개의 collection으로 분리하자

하나의 collection 내부에 많은 document를 가지게 되면 아래와 같은 현상을 자연스럽게 수반하게 된다.

  • index의 size가 증가한다.
  • index의 카디널리티가 증가한다

위 상황은 아래와 같은 상황에서 인덱스를 이용하여 query result를 return 할 때, query processor가 불필요한 index 를 참조하기 때문에 퍼포먼스가 낮아질 수 있다. 따라서, 여러 collection으로 분리하는 전략이 하나의 선택이 될 수 있다.

 

 

5. MongoDB를 4.0이상 버전으로 유지하자.

 

5.1. Non blocking Secondary Read

4.0 이전 버전에는 write가 primary에 완전히 commit 된 데이터들을 secondary에 전달완료 하기 전까지는 read operation을 block한다.

이를 mongodb는 WiredTiger timestamp를 이용하여 update에 대한 order를 보장하고, consistency snapshot를 이용하여 secondary 복제가 일어나는 순간에는 secondary가 아닌 snapshot를 읽게 함으로써 non blocking secondary read를 구현하였다.

 

 

5.2. Multi Transaction

RDB의 경우, 서로 관계가 있는 데이터의 경우는 정규화를 해서 데이터를 insert하지만, mongodb의 경우 이를 정규화 하지 않고 하나의 document에 저장하는 성질을 가지고 있다. 이 이론에 기반하여 MongoDB에서 제시하는 이상적인 collection 설계에 따르면 document 하나로 데이터의 무결성이 보장이 된다. 그러나 모든것이 이상적일 수가 없기 때문에 이를 개발자들은 관계를 가진 여러개의 document에 대해 무결성을 보장하기 위해 pending, rollback을 모두 직접 구현한 2-Phase-Commit 패턴으로 해결하였다. 그러나 4.0 이상의 버전에는 multi trransaction을 지원하므로 이를 사용하는 것을 권장한다.

 

 

 

6. Reference

0. Table Of Contents

 

1. 설계 배경

개인 프로젝트를 진행하던 도중 업로드한 개인 프로필 사진을 저장해야하는 기능을 개발하게 되었다. 프로필 사진을 저장하고 불러오는 단계에서 최대한의 성능을 내게 하고자 고민하게 되었다.

 

 

2. 고려해야할 항목

  • 사진파일이 올라갈 때 너무 큰 크기의 사진이 올라가게 되면 돈을 많이 내게 된다.
  • 사진 파일이 올라간 aws s3경로를 보고 이 사진이 어떤 사진인지 확실하게 알면 좋다.
  • AWS S3도 내부적으로 파일을 찾을 때 성능적인 문제가 생긴다고 하던데, 이를 고려한 설계가 들어가면 좋다.

 

 

3. AWS S3 Reference 분석

AWS 내부적으로 S3 Bucket에서 파일을 어떻게 분포시키느냐에 따라 search 성능이 떨어질 수 있다고 들은게 있기 때문에 이에 대해서 성능적인 이슈를 하나씩 찾아본 결과, aws official blog를 찾을 수 있었고, 이를 아래에 요약을 해보았다.

S3에는 splitting이 필요한 keyspace를 지속적으로 모니터링하는 자동화기능이 있습니다. 내부적으로 high rate request, 너무 많은 key(aws s3 object) 등의 요소에 따라 파티션을 새로 생성하여 key를 이동시킵니다. 이러한 작업이 성능적으로 큰 영향을 미치진 않지만, 단일 파티션에 많은 key에 대해 request rate가 증가하면 이러한 작업이 많이 발생하기 때문에 사전에 내부적으로 파티셔닝이 많이 일어나지 않는 key path 설계를 하는 것이 중요합니다.

 

3.1. 문제상황 예시

위 요약 내용을 설명하기 위해 아래에 예시를 통해 알아보도록 하자.

당신이 쓰기 및 읽기가 많이 이루어지는 파일들을 다음과 같은 형식으로 Service라는 bucket안에 s3 key로 만들었다고 가정하자.

bucket/user/schedule/<userId>
bucket/user/info/<userId>
bucket/user/secret/<userId>
bucket/user/company/<userId>
bucket/user/log/<userId>

 

위 같은 경우처럼 설계되었을 시, 유저가 요청하면 aws는 아래 그림과 같이 aws key를 탐색한다.

 

위 그림과 같은 아키텍처에서 엄청나게 많은 요청이 들어왔을 때, user 디렉토리에 부하가 많이 걸릴 것이다. 이 결과로 aws s3 내부적으로는 부하를 해결하기 위해 파티셔닝이 일어날 것이다.

 

3.2. 문제상황 해결

위 3.1 그림에서 한 디렉토리로 request가 몰리지 않도록해보자. 가장 분포가 넓은 것을 위주로 key path를 설계를 한다면 기존 user라는 디렉토리로 과부하가 read/write 요청이 몰리지 않을 것이다.

위 사진에서 가장 분포가 넓고 공통점을 많이 가진 key path를 재설계해보았을 때 아래와 같은 구조를 생각해볼 수 있다.

bucket/<userId>/schedule
bucket/<userId>/info
bucket/<userId>/secret
bucket/<userId>/etc
bucket/<userId>/log
.
.
.

위 구조를 모식화 하면 아래 사진과 같다.

 

 

 

초안보다 개선된 점은 다음과 같다.

1. 기존 user 디렉토리를 여러명의 유저 개개인 관점으로 쪼개어 하나의 디렉토리로 몰리지 않게 함.
2. 유저의 속성으로 쪼갤 수 있었으나, 유저의 속성(schedule, info, secret…)보다는 userId 값이 분포가 훨씬 넓기 때문에 userId로 쪼갠 것이 부하분산에 유리하다.

 

 

4. 실전 적용

표현해야하는 정보 및 고려사항은 다음과 같다.

  • 유저 프로필 사진을 업로드하고, 경로를 보고 이 사진이 어떤 사진인지 알 수 있어야 한다.
  • S3 key 이름에 특정인이 암시되지 않아야 한다.
  • key를 가지고 버전관리가 되어야하며, 지난 사진은 삭제시켜야한다.
  • 프로필 사진의 크기가 여러개 존재할 수 있다.

이 사항들과 위 내용을 고려한 결과, 다음과 같은 s3 key path를 설계할 수 있었다.

 

bucket/<USER_ID>/profile/<IMG_SIZE>/<TIMESTAMP>

 

  • <USER_ID> : 유저의 고유 id
  • profile : 유저의 프로필 사진이라는 뜻의 path, 유저의 다른 부분들이 저장될 수 있기 때문에 이 부분을 유저의 어떤 자원인지 명시해주기로 함.
  • <IMG_SIZE> : 200x200, 400x400같이 해당 사진의 크기가 몇인지 표시해주는 path
  • <TIMESTAMP> : 원본 사진 파일 이름을 알 필요가 없으며, 주기적으로 aws lambda를 이용해 timestamp가 오래된것들은 삭제시킬 수 있다.

 

 

5. 결론 및 느낀점

  • Cloud 자원 역시 근본은 데이터센터 자원을 사용하는 것이기 때문에, 최대한의 효율을 낼 수 있는 방법을 충분히 리서치를 하여 적용시키는 방법이 더 바람직하다.
  • 위 aws s3 key설계가 현재 학습 중인 mongoDB의 get 부하분산이랑 되게 비슷하다는 느낌을 받았다.

 

 

6. Reference

0. Table Of Contents

1. Coldstart

FaaS Service는 쓰지 않는 상태일 때는 function instance 대기 상태가 아닌 생성되지 않은 상태로 유지되다가 request가 들어올 경우, function instance가 생성되며 이에 대한 요청을 핸들링하기 시작한다. 이 때, request를 handling할 instance가 없으면 delay가 생길 수 있다.

 

cloud function은 다음과 같은 경우 새로운 함수 인스턴스가 생성된다.

  • cloud function을 새로 배포할 경우
  • auto scaling으로 인해 확장되는 경우
  • 긴 시간동안 function이 호출이 되지 않았을 경우
  • 내부적인 오류로 인스턴스를 대체할 경우

 

위의 경우에 대해 대책을 세우지 않으면 무거운 FaaS를 설계하였을 때, delay가 길어질 수 있으니 주의해야한다.

이를 극복할 수 있는 방법으로 해당 function instance가 cold start가 되지 않도록 지속적으로 health check를 하는 방법이 있다.

 

 

2. Stateless Environment

function이 실행되더라도 각 요청마다 다른 함수 인스턴스에서 요청처리를 할 수 있기 때문에, 전역변수를 이용한 출력은 매번 달라질 수 있다. 그렇기 때문에 이러한 데이터는 db, cloud storage 등의 서비스를 이용하여 제어를 해야하는 것이 바람직하다. 그에 대한 이유는 아래 코드 및 실행결과를 보도록 하자.

 

cloud function sample code

 

7월 15일 오후 2시 37분 실행했을 때의 상태

 

 

 

7월 15일 오후 6시 8분 실행했을 때의 상태

 

 

2시 37분 cloud function 을 invoke하고 나서, 약 3시간 30분 정도를 실행하고 있지 않다가 실행한 결과, 전역변수가 초기화 된 것을 볼 수 있다. 즉, function instance를 cold start를 하였기 때문에 전역변수가 초기화 된 것을 볼 수 있다.

이를 방지하기 위해 상태값을 가진 변수 및 객체는 DB를 이용하여 다른 곳에서 캐싱 또는 저장이 되어있어야 한다.

 

 

 

3. 한도

GCP Cloud Function의 한도의 경우, 아래 사진을 참고하도록 한다. 자유자재로 customizing 한 다른 resource와 달리, FaaS Service는 경량화된 서비스이기 때문에 이러한 부분에서 약점을 지닌다. 서비스 설계시 정말로 FaaS를 사용해서 설계를 해도 문제가 없는 아키텍처인지 분석이 필요하다.

 

 

  1. 누구게여 2021.07.17 01:56

    멋진사람 !!

+ Recent posts