문제

  • 2008년에 2개의 샤드가 있었는데
  • 6년동안 수십억 노트를 저장하는 400개의 샤드로 성장.
  • 그래서 SyncChunk 를 만드는데 IO 작업이 엄청 많아짐. (싱크청크는 클라이언트가 업데이트해야할 목록인듯)
  • DB는. MySQL
    • note 는 note 테이블에 쌓이고
    • user_id, USN 복합키로 인덱스가 걸려있다.
    • 근데 실제 행 데이터는 PK순으로 정렬되어 디스크에 clustered 된다.
    • e.g. user_id=123 AND USN > 847 인 100개의 object를 가져와.
      • 해당 user_id 의 데이터가 있는 9개의 다른 테이블을 검사해서 병합하고 USN을 찾아 100개만 리턴 할 것이다.
      • 최악의 경우 1개의 요청을 처리하기 위해 디스크에서 수천개의 비순차적인 페이지를 읽을 가능성이 있다.
    • 이건 엄청나게 비싼 IO작업이고 클라이언트에게 응답을 하는 시간도 느리다.
    • 개인, 회사 간의 노트 공유가 급격히 많아지고 이 문제는 점점 심각해졌다.
    • SSD 로 땜빵한 덕분에 시간을 벌었지만 근본적인 해결책은 아니었다.

해결

  • SyncChunk 를 만드는데 많은 테이블을 찾지 않도록 구조를 바꾸고 싶다.
  • 하나의 테이블에 모든 것을 인덱스 하는 구조로 “Sync Indet” 테이블을 만듬
    • PK 는 [user_id, USN]
      • 이유는: 싱크에 필요한 순서대로 디스크에 클러스터되도록 하기 위해
    • row는 단일 IO에 가능하면 많이 처리할 수 있도록 작게 설계되었다.

sync_index 테이블:

CREATE TABLE IF NOT EXISTS sync_index (
    user_id int UNSIGNED NOT NULL,
    update_sequence_number int UNSIGNED NOT NULL,
        /* USN */
    entry_type tinyint NOT NULL,
        /* tag, note와 같은 이 행이 참조하는 타입. 엔터티는 active,inactive,expunged 세가지 상태가 있는데 이걸 어디다 저장한다는 거지? */
    notebook_id int UNSIGNED,
        /* 권한은 대부분 노트북 수준에서 부여되고. note에 접근하지 않고도 notebook_id 로 필터링 가능 */
    grave_notebook_id int UNSIGNED,
    object_id int UNSIGNED,
    guid binary(16),
    content_class_hash int UNSIGNED,
        /* Evernote Hello, Evernote Food 등은 note에 contentClass가 정의됨. 그리고 prefix가 일치하는 note와만 동기화 가능. 공간절약을 위해 content class 대신 hash 를 저장함. */
    recipient_id int UNSIGNED DEFAULT NULL,
    service_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    PRIMARY KEY (user_id, update_sequence_number),
    KEY objectid_entrytype_idx (object_id, entry_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;

http://blog.evernote.com/tech/2014/01/28/synchronization-speedupification/