5주차 3 - CodingInterviewStudy/CrackingTheCodingInterview GitHub Wiki

시스템 설계 및 규모 확장성

좋은 해법과 나쁜 해법은 있지만 완벽한 해법은 없다.

시스템 설계 류의 문제는 실제 세계에서 어떻게 행동할지 보기 위해 고안된 문제로,

실제 일을 하듯이 면접관에게 질문하고 장단점을 토론하면 된다.

면접자가 할 일은 기업이 수 억 들여 만든 복잡한 시스템을 재설계 하는 것이 아니다.

문제를 분석하고 풀 수 있는 능력을 입증하려는 목적이 크다.

접근방법

  • 면접자의 의사소통 능력을 평가하기 위한 문제이므로, 면접관과 끊임없이 소통해야 한다.
  • 처음에는 포괄적으로 접근한다.
  • 화이트보드를 사용해 면접관이 잘 이해할 수 있도록 그림을 그리며 설명한다.
  • 면접관이 우려하는 부분을 인정하고 적절하게 수정한다.
  • 가정을 할 때는 명확히 언급한다. 잘못된 가정은 문제를 완전히 다르게 바꿔버리는데, 명확히 밝혀두면 실수를 했을 때 면접관이 바로잡아 줄 수 있다.
  • 정확히 가정할 수 없다면 이미 알고 있는 수치를 기준으로 어림잡아 본다.

시스템 설계를 위한 단계별 접근법

1단계: 문제의 범위를 한정하기

내가 만들려는 시스템이 면접관이 원하는 것인지 알아보는 단계이다.

예를들어 TinyURL을 만든다면 아래와 같은 것을 알아야 하겠다.

  • 개개인이 원하는 대로 축약된 URL을 만들 수 있는가 / 축약된 URL이 항상 자동으로 생성되는가
  • 클릭에 대한 통계를 기록할 필요가 있는가
  • 한 번 설정된 URL은 영원히 없어지지 않는가 / 일정 시간이 지나면 삭제되는가

또한, 아래와 같은 기능을 만들어야 하겠다.

  • URL을 축약
  • URL을 분석
  • 축약된 URL과 연결된 URL을 검색
  • 사용자 계정 및 링크 관리

2단계: 합리적인 가정으로 문제를 구체화하기

적절한 제약을 가정해 문제를 풀어야 한다.

  • 하루에 100개의 URL만 처리하면 된다 / 하루에 최대 백만 개의 URL을 생성한다
  • 최근 데이터에 비해 10분 오차가 있어도 된다 / 방금 추가한 URL은 곧바로 사용할 수 있어야 한다

3단계: 시스템의 주요한 부분을 다이어그램으로 표현하기

아래와 같이 시스템 뼈대를 그려보는 단계다.

  • 여러 개의 프론트엔드 서버가 백엔드에서 데이터를 받아오는 시스템
  • 한 서버군은 인터넷에서 데이터를 긁어오고 다른 서버군은 이 데이터를 분석하는 작업하는 시스템

4단계: 발생할 수 있는 핵심 문제를 찾는다

병목 지점 등 시스템이 풀어야 할 주된 문제를 찾는다.

  • 어떤 URL은 드물게 사용되는 반면 특정 URL의 사용량이 갑자기 치솟는 경우

5단계: 핵심 문제를 해결할 수 있도록 다시 설계한다

규모확장을 위한 단계별 접근법

1-2단계: 질문으로 구체화, 현실적 제약을 무시

면접관이 언급하지 않은 세부사항이 있는지 문제를 정확히 이해한 것이 맞는지

면접관과 질답하며 문제를 구체화한다.

그 다음 메모리 제약도, 처리능력 제약도 없는 상태에서 시스템 뼈대를 설계한다.

3단계: 현실적 제약을 고려해 해결할 문제를 찾는다

  • 컴퓨터 한 대에 저장할 수 있는 데이터의 크기

  • 데이터를 여러 조각으로 쪼갰을 때 어떤 문제가 발생할 것인지

    • 어떤 논리로 데이터를 나눌 것인가
    • 특정 컴퓨터가 어느 데이터 조각을 사용했는지 어떻게 알 수 있을 것인가

4단계: 위 단계에서 찾은 문제를 해결한다

사전지식

시스템 설계 문제는 면접자가 무엇을 알고 있는지 확인하는 문제는 아니다.

다만, 특정 개념을 알고 있다면 더 쉽게 풀 수 있다.

이하 모든 개념들은 깊이 있고 복잡한 문제이므로 추가 학습이 필요하다.

시스템 설계의 핵심 개념

수직적 / 수평적 규모확장

  • 수직적 규모확장(vertical scaling) : 특정 노드의 자원의 양을 늘리는 방법이다. 서버에 메모리를 추가해 처리능력을 키울 수 있다.

  • 수평적 규모확장(horizontal scaling) : 노드의 개수를 방법이다. 서버를 추가함으로써 서버 한 대가 다루는 부하를 줄일 수 있다.

서버 부하 분산 장치(load balancer)

서버에 걸리는 부하를 여러 대의 서버에 균일하게 분산시켜, 서버 한 대 때문에 전체 시스템이 죽거나 다운되는 상황을 방지한다.

데이터베이스 역정규화(denormalization)과 NoSQL

SQL과 같은 관계형 데이터베이스의 조인 연산은 시스템이 커질수록 굉장히 느려지므로, 필요에 따라 데이터베이스를 합쳐 역정규화할 수 있다.

  • 역정규화: 데이터베이스 테이블에 여분의 정보를 추가해서 읽기 연산 속도를 향상
  • 정규화: 데이터베이스 테이블간 중복된 데이터를 허용하지 않으므로써 무결성을 유지하고 저장용량을 줄임

NoSQL 같은 비정형 데이터베이스는(redis, mongoDB 등) 조인 연산 자체를 지원하지 않는다. 유연한 규모 확장에 좋도록 설계되어 있다.

NoSQL 데이타 모델링 #1-데이타모델과, 모델링 절차 (tistory.com)

NoSQL 데이타 모델링 #2- 데이타 모델링 패턴 (tistory.com)

데이터베이스 분할(샤딩)

샤딩은 데이터를 어떻게 나누어 저장할지, 어떤 데이터가 어디에 저장되어 있는지 알 수 있는 방식이다.

  • 수직적 분할: 자료의 특성별로 분할
    • 소셜 네트워크를 만들 때 개인정보/메시지 관련부분 등 특성에 따라 자료를 분할
    • 특정 테이블의 크기가 일정 수준 이상으로 커지면 데이터베이스를 재분할해야 할 수도 있음
  • 키 혹은 캐시 기반 분할
    • mod(key, n)의 값을 이용해 N개의 서버에 분할
    • 서버의 개수가 고정되어 있어 서버를 추가할 때마다 모든 데이터를 재분배해야 함
  • 디렉터리 기반 분할: 데이터를 찾을 때 사용되는 조회 테이블을 유지
    • 상대적으로 서버를 추가하기 쉬움
    • 조회 테이블이 병목지점이 될 수 있다
    • 지속적으로 테이블을 읽는 행위가 전체 성능에 영향을 미칠 수 있음

캐싱

인메모리 캐시를 사용하면 결과를 빠르게 가져올 수 있다.

비동기식 처리 & 큐

  • 이상적이라면, 속도가 느린 작업은 비동기식으로 처리해야 한다.

    • 작업 소요시간을 미리 보여주고 끝나면 사용자에게 알려줄 수도 있다.
  • 경우에 따라 동기 작업으로 속도가 느려지는 대신 덜 정확하게 처리해도 될 수 있다.

    • 어떤 포럼에 글과 댓글이 새로 달렸다고해서, 웹페이지를 매번 새로 불러오는 것은 비효율적이다.

네트워크 성능 척도

애플리케이션의 특성에 따라, 이를테면 게임은 지연속도가 대용량 데이터의 전송은 처리량이 중요하겠다.

  • 대역폭(bandwidth): 단위 시간에 전송할 수 있는 데이터의 최대치.
  • 처리량(throughput): 단위 시간에 실제로 전송된 데이터의 양.
  • 지연속도(latency): 데이터를 전송하는 데 걸리는 시간.

MapReduce

매우 큰 데이터를 처리하는 데 사용. 많은 과정을 병렬로 처리할 수 있게 도와줌.

  • Map은 데이터를 입력받아 <key, value> 쌍을 반환
  • Reduce는 키, 값을 입력으로 받은 뒤 내부 처리과정을 거쳐 새로운 키, 값을 반환. 다른 Reduce로 넘겨 이 과정을 반복할 수 있음.

더 고려할 점

  • 실패: 시스템의 어떤 부분이 실패했을 때의 대비책
  • 가용성과 신뢰성
    • 가용성: 사용 가능한 시스템의 시간을 백분율로 나타낸 것
    • 신뢰성: 특정 단위 시간에 시스템이 사용 가능한 확률을 나타낸 것
  • 읽기중심, 쓰기중심
    • 읽기중심: 캐시 사용을 고려
    • 쓰기중심: 큐 사용을 고려
  • 보안

문제풀이

▶ 연습: 문서 키워드 검색

  • 수백만 개의 문서에서 특정 단어 리스트가 포함된 문서를 찾으려고 한다 → 데이터를 분할 처리
  • 단어가 등장하는 순서는 중요치 않지만, 단어가 완전히 일치해야 한다 → 각 단어별 검색결과의 교집합 구하기
  • 검색 연산은 여러 번 호출한다 → 시간이 오래 걸리더라도 전처리 작업 가능

1단계

문서가 수십 개라면

전처리 과정으로 모든 문서에 대한 해시테이블을 만들 수 있다.

"books" -> {doc1, 2, 3}
"many" -> {doc2, 4}

"many books"를 탐색하려면 다 집합의 교집합을 구하면 된다.

2단계

문서가 수백만 개라면

  • 해시테이블을 어떻게 분할할까?
    • 키워드 단위로: a-c키워드는 서버1, d-f 키워드는 서버2 등
    • 문서 단위로: 문서1-10에 대한 키워드는 서버1, 문서5-13에 대한 키워드는 서버2 등
  • 데이터를 처리해 분할하는 작업은 어떻게 처리할까?
  • 데이터가 어떤 컴퓨터에 보관되어 있는지 어떻게 알 수 있을까?

3단계

  • 키워드 단위로 해시테이블을 분할했을 경우
    • 조회 테이블을 작고 단순하게 만들 수 있으며, 각 컴퓨터에 조회 테이블 복사본을 저장할 수도 있음
    • 새로운 문서나 단어가 추가되면 굉장히 많이 이동시켜야 할 수도 있음
    • 여러 키워드를 입력받으면 컴퓨터별 검색결과의 교집합을 구하면 됨

▶ 주식 데이터

  • 폐장 시점 주가정보(시작가, 종가, 최고가)를 최대 1000개 클라이언트에게 제공
  • 데이터는 어떤 형태로든 저장할 수 있다
  • 개발과 배포, 시스템 모니터링, 사용자에게 전송되는 정보 관리를 수행해야 함

미리 고려할점

  • 클라이언트/서버측 구현/유지보수가 용이해야 한다
  • 상황에 맞춰 유연하게 변경할 수 있어야 한다
  • 서비스에 과도한 부담이 가지 않도록 규모확장이 용이해야 한다

방안1 : 텍스트파일

텍스트 파일을 FTP서버로 배포하는 경우

  • 사용, 백업, 유지보수가 용이하다

  • 쿼리를 날리려면 텍스트 파일을 파싱해야 한다.

    • 새로운 칼럼이 추가되면 기존 파싱 방법이 동작하지 않을 수 있다

방안2 : SQL DB

데이터를 일반적인 SQL데이터베이스로 관리하는 경우

  • 장점
    • 클라이언트에서 쿼리를 날려 여러 부가기능을 제공할 수 있다
    • 쿼리 취소, 데이터 백업, 보안 유지 등 기능이 데이터베이스 기능에 포함되어 구현이 쉽다
    • 기존 애플리케이션에 통합하기 쉽다
  • 단점
    • 개발결과물이 필요 이상으로 무거워진다
    • 데이터를 조회하려면 쿼리를 작성하고 사용자가 보기쉽게 추가적으로 구현해야 한다
    • 클라이언트에게 적절한 권한을 부여하지 않으면 보안상 문제가 발생할 수 있다

방안3 : XML (JSON도 마찬가지)

  • 장점
    • 사람이 보기에도, 기계가 처리하기도 쉽다
    • 대부분 프로그래밍 언어에 XML/JSON을 처리하는(파싱/백업) 라이브러리가 포함되어 있다
    • 데이터 포맷이 정해져 있으므로 새 칼럼을 추가해도 파서를 새로 구현할 필요는 없다
  • 단점
    • 클라이언트가 필요한 게 자료 중 일부라고 하더라도 모든 자료를 전송한다
    • 쿼리를 수행하려면 전체 파일을 파싱해야 한다

▶ 소셜 네트워크

  • 페이스북이나 링크드인과 같은 대규모 소셜 네트워크를 위한 자료구조는 어떻게 설계하겠는가?

  • 두 사람 사이의 최단 경로를 보여주는 알고리즘은 어떻게 설계할 것인가

단계1: 문제단순화

  • 각 사용자를 노드 친구 관계를 간선으로 하는 그래프를 가정

  • 두 사용자간 경로는 한 사용자부터 시작해 너비 우선 탐색

    • 깊이 우선 탐색을 하면 최단경로를 찾지 못할 수 있고, 또한 불필요하게 수백 만 개 노드를 탐색할 수 있다
  • 두 사용자간 경로를 각 사용자부터 시작해 양방향 너비 우선 탐색

    • S에서 D까지 경로를 탐색할 때, 각각 K명의 친구를 가지고 있다면
    • S→D 너비우선탐색은 K+K*K 친구들을 거쳐야하나, 양방향 탐색은 K+K로 끝이난다
    • 단, 양방향 탐색은 시작지점과 도착지점에 모두 접근 가능할 때 사용할 수 있는 방법

소셜 네트워크 연습문제 구현코드(careercup/CtCI-6th-Edition)

다음은 구현코드에 대한 간략한 설명이다.

Person
데이터노드
- ArrayList<Integer> friends : 친구ID 목록
- Integer personID : 본인ID
- String info : 부가정보

PathNode
- Person person : 현재 노드
- PathNode previousNode : 이전에 방문한 노드
- LinkedList<Person> collapse(boolean startWithRoot) : 매개인자로 받은 플래그가 true면 앞에서 뒤로 탐색하는 경우이고, false면 뒤에서 앞으로 탐색하는 경우이다.

BFSData
BFS 탐색을 위한 래퍼클래스. 일반적으로 플래그 배열을 사용해서 방문여부를 표시하지만, 동시에 여러 개의 탐색이 진행될 수 있기 때문에 방문여부를 별도 객체에서 처리한다.
- Queue<PathNode> toVisit : BFS로 방문할 노드
- HashMap<Integer, PathNode> visited : BFS로 방문한 노드

테스트 코드에 대한 설명이다.

QuestionA
너비우선탐색 테스트

QuestionB
양방향 너비우선탐색 테스트
- ::mergePath : 충돌이 발생하면 양방향에서 탐색한 노드를 합친다.
- ::searchLevel : 깊이기준 한 단계씩만 들어가서 찾는 노드가 있는지 확인한다. 현재 탐색방향의 노드가 역방향 탐색에서 방문한 노드라면 해당 데이터노드를 반환한다. 충돌이 없으면 null반환.
- ::findPathBiBFS : searchLevel로 단계별로 탐색하고, 충돌이 발생하면 mergePath로 탐색한 노드를 합친다.

Tester
- ::printPeople : 데이터노드 목록을 출력
- ::isEqual : 데이터노드 목록이 동일한지 비교
- ::isEquivalent : isEqual의 오버로딩

단계2: 수백만 사용자처리

데이터노드가 아주 많아 한 서버기계에 저장할 수 없다면

적절한 논리에 따라 서버기계에 분산저장한 후, 추상화된 계층을 통해 접근할 수 있다.

이를테면 사용자ID와 서버ID가 매칭된 해시테이블을 둘 수 있겠다.

Machine 
데이터를 분산저장할 추상화된 서버기계
- HashMap<Integer, Person> persons : ID/데이터노드

Server 
데이터를 분산/처리하는 추상화된 서버
- HashMap<Integer, Machine> machines : 서버기계 목록
- HashMap<Integer, Integer> personToMachineMap : 어떤 데이터노드가 어떤 서버기계에 들어있는지

단계3: 최적화

  • 다른 서버에 대한 탐색을 줄이기: 탐색해야할 친구 중 같은 서버에 있는 친구는 한 번에 조회한다.
  • 연관된 데이터를 같은 서버기계에 저장하는 등 보다 나은 방법으로 사용자 정보를 분산보관한다.

더 고려할 점

  • 서버가 죽는다면 알고리즘에 어떤 영향을 미칠까?
  • 캐싱을 이용하려면?
  • 그래프의 끝을 만날 때까지 무한히 탐색해야 할까? 탐색을 그만둘 시점은 어떻게 정할까?
  • 탐색을 시작할 위치를 정할 때, A와 B를 이어줄 가능성이 높은 C노드를 어떻게 알 수 있을까?

▶ 웹 크롤러

  • 크롤러가 무한루프에 빠지지 않으려면 어떻게 해야 하는가

해법

웹을 단순히 그래프라고 가정하면, 사이클이 존재하는지 확인하면 된다.

  • 방문한 페이지를 해시테이블에 저장하고 방문여부를 확인하면 된다.
    • 방문여부 기준이 URL이라면 : HTTP GET으로 URL에 포함된 인자가 달라지면 완전히 다른 페이지로 갈 수 있다. 그런데 웹 애플리케이션이 인식하지 않는 인자는 URL 뒤에 붙여도 같은 페이지로 간다. URL을 페이지를 구별하는 유일한 값으로 볼 수는 없다.
    • 방문여부 기준이 페이지 내용이라면 : 해당 페이지가 무작위로 데이터를 추천해주는 페이지라면, 해당 페이지를 언제나 다른 페이지라고 볼 수는 없다.
    • 페이지간 유사성을 가늠해볼 수 있다 : 페이지 내용과 URL을 토대로, 방문한 페이지들과 충분히 유사하다면 우선순위를 낮춘다. 내용 일부와 URL을 토대로 모종의 시그니처를 생성할 수 있겠다.
      • 페이지의 특정 섹션과 URL을 토대로 시그니처를 생성
      • 데이터베이스 쿼리를 통해 해당 시그니처가 최근 탐색된 적 있는지 확인
      • 해당 시그니처를 갖는 페이지가 최근 탐색되었으면 우선순위를 낮춰 데이터베이스에 추가
      • 그렇지 않다면 해당 페이지를 탐색하고 그 페이지에 연결된 링크를 데이터베이스에 추가

▶ 중복 URL

  • 100억 개의 URL에서 중복된 문서를 찾으려면? '중복'이란 '같은 URL'을 말한다.

단계1

  • URL이 평균 100개의 문자로 구성되고 각 문자는 4바이트라고 할 때
  • 100억 개의 URL은 4테라의 메모리가 필요하다

일단 모든 URL을 메모리에 저장할 수 있다고 가정해보면, 살펴본 URL에 대해 true를 반환하는 해시테이블로 충분하다.

단계2

  • 디스크에 분할저장 : (해시값 % 4000) 식을 통해 1GB 4000개 파일로 분할한다. 이후 각 파일을 메모리에 올려 URL의 해시테이블을 생성하고 중복이 존재하는지 찾는다.
  • 여러서버에 분할저장
    • 연산을 병렬로 처리할 수 있다
    • 서버에 장애가 발생할 경우, 서버 개수가 증가함에 따라 복잡해지는 시스템 등을 고려해야한다.

▶ 캐시

간단한 검색엔진 역할을 수행하는 웹서버가 있다.

  • 100개의 컴퓨터가 검색 요청을 처리한다. 컴퓨터집단에 processSearch 메서드를 요청하면, 그중 어떤 컴퓨터가 작업을 처리한다.
  • processSearch 메서드는 아주 고비용이다.
  • 최근 검색 요청을 캐시에 저장하는 메커니즘을 설계하라. 또한 데이터가 바뀌었을 때 어떻게 캐시를 갱신할 것인지 반드시 설명하라.

가정

  • 모든 쿼리는 최초로 호출된 서버에서 처리한다
  • 캐시하고자 하는 쿼리의 수는 굉장히 크다(수백만 개)
  • 서버간 호출은 상대적으로 빨리 처리한다
  • 쿼리결과는 정렬된 URL리스트이며, 각 원소에는 최대 50글자 제목과 200글자 요약문이 포함된다
  • 가장 인기 있는 쿼리의 경우 항상 캐시에 보관되어 있다

시스템요구사항

주요기능은 다음과 같다

  • 주어진 키를 사용한 빠른 탐색
  • 오래된 데이터를 버리고 새로운 데이터로 교체
  • 쿼리 결과가 변경될 경우 캐시를 변경하거나 삭제

단계1: 단일시스템 캐시설계

오래된 데이터를 쉽게 제거하는 동시에 주어진 키에 대응되는 값을 효율적으로 찾는 자료구조는 무엇인가?

  • 연결리스트: 오래된 데이터를 쉽게 제거할 수 있다. 리스트가 지정된 크기를 넘어가면 연결리스트의 마지막 항목을 제거하도록 구현할 수도 있다.

  • 해시테이블: 데이터를 효율적으로 탐색할 수 있지만, 오래된 데이터를 간단히 제거할 수 있는 것은 아니다.

  • 순서가 유지되는 해시테이블(자바의 TreeMap) : 오래된 데이터를 쉽게 제거할 수 있고 데이터도 효율적으로 탐색할 수 있다.

  • 연결리스트 + 해시테이블: 해시테이블에 삽입할 노드를 별도 연결리스트로 저장해둔다.

Node
- Node prev
- Node next
- public String[] results
- public String query

Cache
- public HashMap<String, Node> map : 빠른 탐색을 위한 참조용 해시테이블 <쿼리, 노드>
- public Node head : 연결리스트의 머리
- public Node tail : 연결리스트의 꼬리
- ::moveToFront : removeFromLinkedList로 연결리스트에서 제거한 노드를 연결리스트의 맨앞으로 옮긴다.
- ::removeFromLinkedList : 주어진 노드를 연결리스트에서 제거한다.
- ::getResult : 주어진 쿼리로 해시테이블에서 검색해 결과를 반환한다. 
- ::insertResult : 주어진 쿼리와 결과값을 해시테이블에 저장한다.

단계2: 여러 서버로 확장

서버간 캐시를 어떻게 공유할 것인지가 핵심

방법1

  • 각 서버별로 별도의 캐시
    • 같은 쿼리가 반복되도 서버별로 새로운 쿼리로 인식하므로 효과적이지 않음

방법2

  • 각 서버에 캐시 복사본을 둔다
    • 새로운 데이터가 캐시에 추가되면 모든 서버로 보내진다
    • 캐시를 갱신할 때마다 서버간 작업을 N번 수행하며, 데이터 저장공간도 N배 더 커야한다

방법3

  • 각 서버에 캐시 일부를 저장한다
    • 캐시가 서버별로 분할저장되어, 어떤 서버에 어떤 캐시가 있는지 찾아야 한다
    • hash(query) % N을 사용해서 쿼리를 배정할 수 있다

단계3: 내용이 변경되면 결과를 갱신

언제 쿼리결과가 바뀌는지 살펴보자.

  • URL이 가리키는 페이지 내용이 바뀌거나 삭제되었을 때
  • 페이지의 랭킹이 바뀌어 결과의 순서가 변경될 때
    • 내용/랭킹 데이터를 추가로 저장/처리해야 한다
    • 주기적으로 탐색해서 갱신하는 방식이 있다
  • 특정한 쿼리에 관련있는 새로운 페이지가 등장할 때
    • 특정 쿼리에 대하여 탐색해서 갱신하는 방식이 있다
    • 그러나 최선은 쿼리에 대한 캐시를 일정 기간 동안만 유지하는 것이다

캐시 연습문제 구현코드(careercup/CtCI-6th-Edition)

▶ 판매 순위

  • 가장 잘 팔리는 제품의 리스트(전체/카테고리별)를 출력해야 한다

단계1: 문제를 구체화

  • 전자상거래 시스템 전체를 설계하는 것이 아니므로 관련있는 몇가지에만 집중
  • '잘 팔린다' = 전체기간에 걸쳐 팔린 총량인지, 지난 한주/한달 동안인지 등, 여기서는 지난 한 주 동안의 판매량
  • 각 제품은 여러 카테고리에 포함될 수 있고 서브카테고리는 없다고 가정

단계2: 합리적인 가정

  • 통계 결과가 언제나 100% 최신 데이터는 아닐 수 있다.
  • 인기 있는 제품의 경우 정확도가 중요하지만, 인기가 별로 없는 제품은 약간의 오차도 허용한다
  • 가장 있기 있는 제품은 한 시간마다 갱신하지만, 데이터 범위가 정확히 7일이 아닐 수 있다
  • 카테고리 분류는 판매자 이름을 기준으로 하고, 가격/날짜를 기준으로 하지 않는다

단계3: 주요 구성요소 그리기

[구매시스템] -(1)→ [데이터베이스] -(2)→ [판매순위데이터] → [프론트엔드]
(1) 주문정보를 저장한다
(2) 판매순위에 따라 정렬한다

단계4: 핵심문제 파악

  • 데이터베이스에 너무 자주 접근한다. 필요한 데이터만 가져와서 처리한다.

    • 과거 구매기록은 다른 시스템이 맡고 있다고 가정하고, 판매 데이터만 따로 저장해서 처리한다
    • 지난 일주일간 데이터를 한 칼럼에 저장하면 매일 갱신해야 하므로
    • 월-일로 환형큐와 같이 여러 칼럼에 나누어 저장하면 연산을 줄일 수 있다
  • 데이터베이스에 너무 자주 기록한다. 한 번에 모았다가 처리한다.

    • 초단위로 구매기록이 들어오므로 너무 자주 기록한다. 메모리 캐시와 같은 저장소에 저장했다가 주기적으로 모아서 기록한다
    • 만약 제품ID가 4바이트 구매량이 4바이트라면, 천만 개의 제품을 표현하려면 40메가 바이트면 충분하다
    • 단, 제품별로 메모리 캐시에서 구매량을 읽어들이는 주기가 달라 결과가 편향될 수 있다
      • 메모리 캐시를 시간대별로 나눠서 제품별로 차이가 없도록 하거나
      • 모든 데이터를 읽어들이기 전까지는 이전 데이터로 처리하도록 할 수 있다
  • 데이터베이스 join은 비용이 비싸다.

    • 각 카테고리마다 제품의 정보를 읽어오면 값비싼 join연산을 여러번 해야한다
    • 제품과 카테고리에 대해 join을 수행해 정보를 읽어들이고 새로 정렬할 수도 있다
    • 카테고리 내부에서 데이터를 정렬하고 정보를 읽어들이고 전체 결과는 다시 전체를 정렬할 수도 있다
    • 혹은 처음부터 제품 정보를 함께 저장해둘 수도 있다
  • 데이터베이스 쿼리는 비용이 비싸므로, 맵리듀스 같은 다른 시스템을 사용할 수 있다

더 고려할 점

  • 병목현상이 일어날 수 있는 지점은?
  • 서브 카테고리가 있다면?
  • 데이터가 더 정확해야 한다면? 이를테면 적어도 30분 이내에 갱신된 것이어야 한다면?

▶ 개인 재정관리자

▶ Pastebin

추가자료

  • 2의 제곱수(아스키 문자 하나는 2바이트)
2의 n제곱 근사치 이름 축약형
10 1천 1킬로바이트 1KB
20 1백만 1메가바이트 1MB
30 10억 1기가바이트 1GB
40 1조 1테라바이트 1TB
50 1000조 1페타바이트 1PB
  • 응답지연 값(2020년 기준)
    • 메모리는 빠르지만 디스크는 느리다
    • 디스크 탐색은 가능한 피해야한다
    • 단순한 압축 알고리즘은 빠르다
    • 데이터를 인터넷으로 전송하기전 가능한 압축하라
    • 데이터센터는 여러지역에 분산되어 있는데, 센터들간 데이터 주고받는 데 시간이 걸린다
연산 시간
L1 캐시 참조 1ns
분기 예측 오류 3ns
L2 캐시 참조 4ns
뮤텍스 락/언락 17ns
주 메모리 참조 100ns
Zippy로 1KB 압축 2000ns = 2μs
상용 네트워크에서 2KB 전송 44ns
SSD상에서 임의 위치의 데이터 읽기 16,000ns = 16μs
메모리에서 1MB 순차적으로 읽기 3,000ns = 3μs
같은 데이터 센터 내에서 메시지 왕복 지연시간 500,000ns = 500μs
SSD에서 1MB 순차적 읽기 49,000ns = 49μs
디스크 탐색 2,000,000ns = 2ms
한 패킷이 캘리포니아에서 네덜란드까지 왕복 지연시간 150,000,000ns = 150ms

*나노초(ns), 마이크로초(μs), 밀리초(ms)

*1나노초=10^-9초, 1마이크로초=10^-6초=1000나노초, 1밀리초=10^3초=1000마이크로초

  • 가용성 관련수치
가용률 하루당 장애시간 주당 장애시간 개월당 장애시간 연간 장애시간
99% 14.40분 1.68시간 7.31시간 3.65일
99.9% 1.44분 10.08분 43.83분 8.77시간
99.99% 8.64초 1.01분 4.38분 52.60분
99.999% 864.00밀리초 6.05초 26.30초 5.26분
99.9999% 86.40밀리초 604.80밀리초 2.63초 31.56초

예제: 트위터 초당 쿼리수와 저장소 요구량 추정해보기

  • 가정

    • 월간 능동사용자는 3억명
    • 50% 사용자는 트위터를 매일 사용
    • 평균적으로 각 사용자는 매일 2건의 트윗을 올림
    • 미디어를 포함하는 트윗은 10% 정도
    • 데이터는 5년간 보관한다
  • 추정

    • QPS추정
      • 일간 능동사용자 = 3억 X 50% = 1.5억 명
      • QPS = 1.5억 명 X 2트윗/24시간/3600초 = 약 3500
      • 최대 QPS = 2 X QPS = 약 7000
    • 미디어 저장을 위한 저장소 요구량 추정
      • 평균 트윗크기: id에 64바이트, 텍스트에 140바이트, 미디어에 1MB
      • 미디어 저장소 요구량: 1.5억 X 2 X 10% X 1MB = 30TB/일
      • 5년간 미디어를 보관하기 위한 요구량: 30TB X 365 X 5 = 약55PB

알렉스 쉬, 이병준 옮김, 가상 면접 사례로 배우는 대규모 시스템 설계 기초, 2020, 인사이트, pp.33-38

Google Pro Tip: Use Back-Of-The-Envelope-Calculations To Choose The Best Design, 2011/01

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