문제는 이렇습니다. 2008년에는 샤드가 두 개였는데, 6년 동안 수십억 개의 노트를 저장하는 400개의 샤드로 성장했습니다. 그러다 보니 SyncChunk를 만드는 데에 IO 작업이 엄청나게 많아졌습니다. 싱크청크는 클라이언트가 업데이트해야 할 목록인 듯합니다.

DB는 MySQL입니다. note는 note 테이블에 쌓이고, user_id, USN 복합키로 인덱스가 걸려 있습니다. 그런데 실제 행 데이터는 PK 순으로 정렬되어 디스크에 clustered됩니다. 예를 들어 user_id=123 AND USN > 847인 100개의 object를 가져온다고 하면, 해당 user_id의 데이터가 들어 있는 아홉 개의 다른 테이블을 검사해서 병합하고 USN을 찾아 100개만 리턴해 줍니다. 최악의 경우 한 번의 요청을 처리하기 위해 디스크에서 수천 개의 비순차적인 페이지를 읽어야 할 가능성이 있습니다. 이는 매우 비싼 IO 작업이고, 클라이언트에게 응답하는 시간도 느려집니다. 개인과 회사 간의 노트 공유가 급격히 늘면서 이 문제가 점점 심각해졌습니다. SSD로 임시방편을 마련해 시간을 벌었지만, 근본적인 해결책은 아니었습니다.

해결책은 다음과 같습니다. SyncChunk를 만들 때 여러 테이블을 뒤지지 않도록 구조를 바꾸고 싶었습니다. 그래서 하나의 테이블에 모든 것을 인덱스하는 “Sync Index” 테이블을 만들었습니다. 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/