LINE Manga 데이터베이스 샤딩 – 데이터베이스 엔지니어 편

들어가며

안녕하세요. 데이터베이스 팀 Oda입니다. LINE Manga의 데이터베이스 샤딩(sharding) 작업에 대해서 서버 엔지니어 편을 먼저 소개드렸는데요. 이번 글은 데이터 엔지니어 편입니다. 샤딩을 하게 된 배경과 대응 방침을 데이터베이스 엔지니어 입장에서 검토한 사항을 중심으로 공유하겠습니다.

 

샤딩을 하게 된 배경

먼저 LINE Manga에서 데이터베이스 샤딩 작업을 하게 된 배경에 대해 설명하겠습니다.

서비스 인수인계

과거로 좀 거슬러 올라가 보겠습니다. 제가 LINE Manga 담당 데이터베이스 엔지니어가 된 것은 2018년 2월 중순입니다. LINE Manga는 2013년 4월 9일에 서비스를 시작했으니 5주년을 눈앞에 두고 있었습니다. 전임자는 마침 마이그레이션 작업 시점이라며 함께 작업을 진행하면서 서비스를 인수인계했습니다. 마이그레이션 작업의 목적은 다음과 같았습니다.

  • 주요 목적
    • 지원 종료된 서버 배제
    • 데이터 증가에 일시적으로 대응
    • 성능 개선
  • 부차적인 목적
    • 서비스 인수인계 작업의 일환 

제가 인수인계를 받은 2018년 2월 중순, 마이그레이션 작업 후 서버는 아래와 같은 상황이었습니다.

  • 변경 전: NVMe-SSD 3.0TB 중 2.6TB 사용, 총 6대(마스터 1대, 참조 전용 4대, 백업 전용 1대)
  • 변경 후: NVMe-SSD 6.0TB 중 2.6TB 사용, 총 6대(마스터 1대, 참조 전용 4대, 백업 전용 1대)

참고로 이 시기에도 서비스가 계속 성장할 것이라고 예측해 샤딩을 검토하고 있었습니다. 하지만 LINE Manga의 대규모 업데이트를 앞두고 개발 자원이 부족했기 때문에 스케일 업으로만 대응했습니다.

서비스 성장 속도를 체감

때는 2018년 10월, 제가 LINE Manga 업무에 많이 익숙해졌을 즈음입니다. 네이버 웹툰의 무료 디지털 만화 서비스였던 ‘XOY’와 통합하면서 인기 작품을 옮기고 전편 무료 보기 캠페인을 실시했는데요. 덕분에 활동 사용자수(active users)가 급격히 증가하면서 복제 지연이 발생했습니다. 아래는 참조 전용 서버의 복제 지연을 나타낸 그래프인데요. x축이 시간 경과, y축이 지연 시간을 나타냅니다. 시간이 지날수록 지연 시간이 증가하는 것을 알 수 있습니다.

복제 지연은 개발 쪽에서 잘못되어 발생하는 경우가 많기 때문에 당시 그런 부분이 없는지도 알아봤는데요. 원인은 결국 활동 사용자수가 증가한 것이었습니다. 조사 중 바이너리 로그 로테이션 속도의 변화를 보고 업데이트 양이 비정상적으로 증가하고 있다는 사실을 알게 되었습니다. LINE Manga에서는 max_binlog_size를 1GB로 설정했었는데요. 10분에 1회 로테이션되는 빈도였습니다. 조사하기 전날엔 최고치가 40분에 1회였기 때문에 당시 활동 사용자수가 상당히 많았다는 점을 알 수 있습니다. 이 문제를 해결하기 위해 설정을 아래와 같이 변경했습니다.

  • 변경 전: innodb_flush_log_at_trx_commit = 1
  • 변경 후: innodb_flush_log_at_trx_commit = 2 

나중에 추가로 진행한 조사에서 데이터 증가 속도가 예상했던 수준을 넘어섰던 점과 잔여 디스크 용량이 부족했던 점이 문제였던 것으로 밝혀졌습니다. 인수인계 당시 2.6TB였던 디스크 사용량이 8개월 후인 2018년 10월에는 4.6TB에 달해 있었습니다. 증가 속도로 추산해 보면 6개월 뒤엔 디스크가 가득 차버리는 거죠.

 

서비스 성장에 발맞추기 위한 샤딩 작업 진행

지속적으로 성장하는 LINE Manga 서비스에 발맞추기 위해 표면화된 문제를 정리하고 이를 해결하기 위한 대책을 마련했습니다.

표면화된 문제 정리

LINE Manga의 MySQL 서버 문제 정리

전편 무료 보기 캠페인을 실시하면서 일시적으로 활동 사용자수가 급증하여 복제 지연이 발생했습니다.

  • 향후 사용자가 증가했을 때 같은 문제가 발생할 것으로 예상됩니다.
  • 향후 캠페인이나 프로모션이 진행되면 같은 문제가 다시 발생할 수 있습니다.

현재와 같은 증가량이 유지된다면 잔여 디스크 용량이 반년을 버틸 수 없습니다.

  • 첫 번째 문제를 해결하기 위한 항구적인 대책을 시행할 시간이 부족할 수 있습니다.
  • 현재 사용하고 있는 서버(NVMe-SSD 6TB)가 사실상 최고 사양입니다.

LINE Manga 팀 체제 문제 정리

서비스 통합과 캠페인 실시와 같이 사용자의 서비스 이용에 영향을 미치는 사안이 데이터베이스 엔지니어에게 미리 공유되지 않아 사전 검증이 불가능했고, 이로 인해 문제가 발생 후 원인 규명 및 대응에 시간이 소요되었습니다.

표면화된 각 문제에 대한 대책

복제 지연과 디스크 용량 부족 문제는 둘 다 쓰기 양의 증가가 근본적인 원인입니다. 따라서 전부터 논의되었던 샤딩을 구체적으로 검토할 필요가 생겼습니다. 하지만 모든 데이터를 수평적으로 샤딩하는 것은 어려운 일입니다. 그래서 복제 지연 시에 업데이트 양이 증가했던 특정 테이블을 중심으로 기능적으로 추출할 수 있는 방법(수직 샤딩)이 없을지 서버 엔지니어와 함께 검토했습니다. 검토하기 위해 performance_schema.events_statements_summary_by_digest에서 해당 테이블에 대한 쿼리를 골라 서버 엔지니어에게 전달했고, 서버 엔지니어는 실현 가능성이 있는지 검토했습니다.

LINE Manga의 MySQL 서버 문제에 대한 대책

검토 결과 샤딩 대상으로 선정된 5개의 테이블을 MySQL에서 추출하기로 했습니다. 전부 합쳐 3TB 정도였는데요. 그중 가장 큰 테이블은 무려 2TB나 되었습니다. 당연히 레코드 삽입 성능도 저하되어 있어서 대상 테이블을 좀 더 수평적으로 샤딩하는 방법도 검토했습니다. 대상 테이블은 모두 사용자의 로그 데이터였고, 사용자 ID(member_id)를 기반으로 8개로 샤딩하기로 했습니다.

  • $DB_SHARD_HASHSLOT = 65536
  • crc32(member_id) % $DB_SHARD_HASHSLOT
    • shard1 : 00000 – 08191
    • shard2 : 08192 – 16383
    • shard3 : 16384 – 24575
    • shard4 : 24576 – 32767
    • shard5 : 32768 – 40959
    • shard6 : 40960 – 49151
    • shard7 : 49152 – 57343
    • shard8 : 57344 – 65535

그리고 샤딩 작업을 진행하기 위해선 디스크 용량을 확보해야 하기 때문에 아래와 같은 절차를 거쳐 현재 사용하지 않는 테이블을 삭제했습니다. 

  1. 사용하지 않는 테이블 조사
    • 사용하지 않는 테이블을 산출하기 위해 아래 2개 테이블을 사용해서 대기 시간(latency) 0인 테이블을 추려냄1
      • performance_schema.table_io_waits_summary_by_table
      • performance_schema.file_summary_by_instance
  2. 영속적인 백업을 받아 예측하지 못한 사태에 대비
  3. 사용하지 않는 테이블 삭제
    • 일시적인 대책으로 테이블 명 변경 후 innodb_buffer_pool_dump_now설정을 이용해 다른 영향이 없는 상태라는 걸 확인한 후 테이블 삭제

위 작업으로 디스크 용량을 500GB 정도 확보할 수 있었고(아래 그래프 참조), 5개의 테이블을 마이그레이션하기에 충분한 시간을 벌었습니다.

LINE Manga 팀 체제 문제에 대한 대책

이 문제는 신규 기능을 구현하거나 캠페인 혹은 프로모션을 실시할 땐, 꼭 서버 쪽 인력에게 미리 정보를 공유하는 간단한 방법으로 해결하기로 결정했습니다. 또한 배포 시점에는 실시간으로 모니터링하여 배포 전과 후에 어떤 부분이 달라졌는지도 살펴보고 있습니다. 최근에 ‘화양연화 Pt.0<SAVE ME>’라는 작품이 게재되기 시작했는데요. 사전에 정보를 공유 받았기 때문에 미리 참조 전용 서버를 추가하는 대응을 할 수 있었습니다.

 

샤딩 방법 검토

지금까지 샤딩을 하게 된 배경을 설명했는데요. 이제 실제로 검토했던 내용을 소개하겠습니다. 아래는 각 방안을 검토한 내용을 간략하게 정리한 표입니다.

마이그레이션 방법장점단점선택비고
이중 쓰기 구현을 통해 샤딩 환경으로 단계적으로 마이그레이션
  • 데이터베이스 엔지니어 작업 분량 적음
  • 서버 엔지니어 작업 분량 많음
  • 이중 쓰기 기간에 데이터 정합성 여부 별도 확인 필요
  • 이중 쓰기 기간에 서비스 속도 저하 발생 가능, 사용자 반응이 악화될 가능성 있음
  • 단계적으로 마이그레이션하기 때문에 그때마다 서비스에 영향 발생 가능성 있음
  • 샤딩 후 불필요한 7개 샤드의 데이터를 삭제하는 데에 시간이 오래 걸림
X
Generated Column과 범위 파티셔닝(range partitioning)을 이용해 샤딩 환경으로 마이그레이션
  • member_id에서 Generated Column 생성, 파티셔닝 키로 사용
  • 파티션은 각 샤드의 계산 로직으로 실시
  • 서버 엔지니어 작업 분량 적음
  • 일반적인 복제와 파티셔닝을 통한 샤딩 방법이라 데이터 부정합 발생 가능성 적음
  • 샤딩 후 불필요한 7개 샤드의 데이터를 쉽게 삭제할 수 있음
  • 데이터베이스 엔지니어 작업 분량 많음
  • 기본 키(primary key)를 버리지 않으면 사용불가
  • 데이터 양에 따라 준비하는 데에 시간이 걸림
X
  • Generated Column을 STORED 유형으로 생성하면 RBR(row-based replication)의 경우 slave에서 반응하지 않으니 주의해야 함
  • Generated Column을 VIRTUAL 유형으로 생성하면 기본 키에 추가할 수 없기 때문에 기본 키를 삭제하고 고유 키(unique key)로 만들어야 함
컬럼 추가 후 추가한 컬럼과 범위 파티셔닝을 이용해 샤딩 환경으로 마이그레이션
  • 추가한 컬럼을 파티셔닝 키로 사용
  • 추가한 컬럼은 member_id를 사용한 계산식으로 그때그때 업데이트
  • 파티션은 각 샤드의 계산 로직으로 실시
  • 서버 엔지니어 작업 분량 적음
  • 일반적인 복제와 파티셔닝을 통한 샤딩 방법이라 데이터 부정합 발생 가능성 적음
  • 샤딩 후 불필요한 7개 샤드 데이터 쉽게 삭제 가능
  • 데이터베이스 엔지니어 작업 분량 많음
  • 기본 키 변경 필요
  • 데이터 양에 따라 준비하는 데에 시간이 걸림
O
  • 기본 키를 변경하기 때문에 RBR의 경우 레코드를 찾을 수 없어서 복제에 실패함. 이 문제를 해결하려면 slave_rows_search_algorithms을 table_scan으로 변경하거나, 바이너리 로그를 보고 수동으로 업데이트해야 함

다음으로 각 방안에 대해 자세하게 설명하겠습니다.

이중 쓰기를 통해 샤딩 환경으로 단계적으로 마이그레이션하는 방법

이중 쓰기를 통해 샤딩 환경으로 단계적으로 마이그레이션하는 방법은 다음과 같습니다.

샤딩 대상이 되는 5개 테이블을 MySQL에서 샤딩 환경으로 마이그레이션한 뒤 사용자 ID(member_id) 기반으로 8분할 샤딩합니다. 이때 샤딩 규칙은 아래 계산식의 결과가 어느 범위에 속하는지에 따라 결정합니다. 

  • 계산식: crc32(member_id) % 65536

작업할 때 기존 환경을 배려하는 동시에 아래와 같은 이점을 확보하기 위해 중간 MySQL을 설치합니다.

  • MySQL을 5.7 버전에서 5.6 버전으로 롤백할 때 발생하는 문제에 대한 대책
    • 현재 MySQL 5.6 버전에서 바이너리 로그(binlog)의 Anonymous_gtid_log_event를 버전 차이 때문에 파싱(parsing)하지 못해서 롤백할 수 없기 때문
  • 모든 작업이 끝났을 때 백업으로서의 역할 기대
  • 복제 시작할 때 8대의 샤드로 바이너리 로그가 한꺼번에 전송되는 문제에 대한 대책

중간 MySQL을 설치할 땐 대상 테이블만 동기화하기 위해 아래와 같은 복제 필터를 사용합니다.

  • replicate-do-table=d1.t1
  • replicate-do-table=d1.t2
  • replicate-do-table=d1.t3
  • replicate-do-table=d1.t4
  • replicate-do-table=d1.t5

아래와 같은 순서에 따라 단계적으로 전환합니다. 

  • 1차 메인터넌스(maintenance) 작업
    • MySQL 복제(replication) 동기화 중지, 대신 이중 쓰기 구현
  • 2차 메인터넌스 작업(배포만 진행)
    • 참조 쿼리를 샤딩 환경에 할당
  • 3차 메인터넌스 작업(배포만 진행)
    • 이중 쓰기 구현 제거
  • 사후 작업: 불필요한 데이터 삭제
    • 주 MySQL의 5개 테이블에 DROP TABLE 명령 실행
    • 각 샤드에서 샤딩 계산식의 결과가 본인 샤드가 아닌 데이터를 DELETE 명령으로 삭제

채택하지 않은 이유

단계적인 마이그레이션에 많은 비용이 발생합니다. 단계별로 소스 코드가 존재하여 관리가 번거롭고, 이중 쓰기 중에는 쓰기 작업을 2번 진행하기 때문에 사용자 반응에 악영향을 줄 가능성이 있습니다. 각 단계별로 문제가 발생할 리스크가 있었고 마이그레이션 기간도 길어집니다.

또한 마이그레이션 후 불필요한 데이터를 삭제하는 데에도 많은 비용이 발생합니다. 각 샤드에서 샤딩 계산식의 결과가 본인 샤드가 아닌 데이터를 삭제하는 데에 시간이 오래 걸리는데요. 수십억 개의 레코드 중 약 8분의 7에 해당하는 레코드를 서비스에 영향 없이 삭제해야 했습니다. 또한 삭제만으로는 IBD 파일의 크기가 바뀌지 않기 때문에 ALTER TABLE 명령도 실행해야 합니다.

Generated Column과 범위 파티셔닝을 이용해 샤딩 환경으로 마이그레이션하는 방법

Generated Column과 범위 파티셔닝을 이용해 샤딩 환경으로 마이그레이션하는 방법은 다음과 같습니다.

샤딩 대상이 되는 5개 테이블을 MySQL에서 샤딩 환경으로 마이그레이션한 뒤 사용자 ID(member_id) 기반으로 8분할 샤딩합니다. 이때 샤딩 규칙은 아래 계산식의 결과가 어느 범위에 속하는지에 따라 결정합니다. 

  • 계산식: crc32(member_id) % 65536

또한 작업할 때 기존 환경을 배려하는 동시에 아래와 같은 이점을 확보하기 위해 중간 MySQL을 설치합니다.

  • MySQL 5.7 버전에서 5.6 버전으로 롤백할 때 발생하는 문제에 대한 대책
    • 현재 MySQL 5.6 버전에서 바이너리 로그의 Anonymous_gtid_log_event를 버전 차이 때문에 파싱하지 못해서 롤백할 수 없기 때문
  • 모든 작업이 끝났을 때 백업으로서의 역할 기대
  • 복제 시작할 때 8대의 샤드로 바이너리 로그가 한꺼번에 전송되는 문제에 대한 대책

중간 MySQL을 설치할 땐 대상이 되는 테이블만 동기화하기 위해 아래와 같은 복제 필터를 사용합니다.

  • replicate-do-table=d1.t1
  • replicate-do-table=d1.t2
  • replicate-do-table=d1.t3
  • replicate-do-table=d1.t4
  • replicate-do-table=d1.t5

중간 MySQL의 각 테이블에 아래 ALTER TABLE 명령을 실행합니다.

  • ALTER TABLE t1 
    ADD `shard_hash` int(11) GENERATED ALWAYS AS ((crc32(`member_id`) % 65536)) STORED NOT NULL,
    DROP PRIMARY KEY, 
    ADD PRIMARY KEY(c1, shard_hash);

샤딩 환경 각 테이블에 아래 ALTER TABLE 명령을 실행합니다.2

  • ALTER TABLE t1
    PARTITION BY RANGE (shard_hash)
    (
    PARTITION shard1 VALUES LESS THAN (8192) ENGINE = InnoDB,
    PARTITION shard2 VALUES LESS THAN (16384) ENGINE = InnoDB,
    PARTITION shard3 VALUES LESS THAN (24576) ENGINE = InnoDB,
    PARTITION shard4 VALUES LESS THAN (32768) ENGINE = InnoDB,
    PARTITION shard5 VALUES LESS THAN (40960) ENGINE = InnoDB,
    PARTITION shard6 VALUES LESS THAN (49152) ENGINE = InnoDB,
    PARTITION shard7 VALUES LESS THAN (57344) ENGINE = InnoDB,
    PARTITION shard8 VALUES LESS THAN (65536) ENGINE = InnoDB,
    PARTITION shardx VALUES LESS THAN (99999) ENGINE = InnoDB
    );

이번 방안에선 아래와 같이 한번에 전환합니다.

  • 메인터넌스 작업
    • MySQL 복제 동기화 중지
    • EXCHANGE PARTITION 명령으로 필요한 파티션(샤드 데이터)만 추출
    • 불필요한 데이터 삭제
      • 주 MySQL의 5개 테이블에 DROP TABLE 명령 실행
      • 파티션 테이블에 DROP TABLE 명령 실행
  • 사후 작업
    • 기본 키에서 shard_hash 제거
    • 각 샤드 MySQL에 추가한 shard_hash 삭제

채택하지 않은 이유

LINE Manga 환경에서는 바이너리 로그 포맷으로 MIXED 유형을 사용합니다. 이 때문에 Row-based 방식으로 복제되어 Stored Generated Column이 반응하지 않아서 기대했던 대로 파티셔닝되지 않았습니다.

위 문제를 해결하기 위해선 기본 키를 유니크 키로 변경해서 Virtual Generated Column을 사용하면 된다는 사실을 이미 다음 마이그레이션 안을 채택한 후에 알게 되었습니다.

  • Virtual Generated Column은 기본 키에는 추가할 수 없다는 제약이 있음
  • 파티셔닝에는 기본 키를 사용하거나, 기본 키가 없는 테이블의 유니크 키를 사용해야 하는 제약이 있음
  • 일반적인 환경에서는 모든 기본 키를 유니크 키로 삼아 Virtual Generated Column과 함께 구현하면 좋음

컬럼 추가 후 추가한 컬럼과 범위 파티셔닝을 이용해 샤딩 환경으로 마이그레이션하는 방법

컬럼 추가 후 추가한 컬럼과 범위 파티셔닝을 이용해 샤딩 환경으로 마이그레이션하는 방법은 다음과 같습니다.

샤딩 대상이 되는 5개 테이블을 MySQL에서 샤딩 환경으로 마이그레이션한 뒤 사용자 ID(member_id) 기반으로 8분할 샤딩합니다. 이때 샤딩 규칙은 아래 계산식의 결과가 어느 범위에 속하는지에 따라 결정합니다. 

  • 계산식: crc32(member_id) % 65536

작업할 때 기존 환경을 배려하는 동시에 아래와 같은 이점을 확보하기 위해 중간 MySQL을 설치합니다.

  • MySQL 5.7 버전에서 5.6 버전으로 롤백할 때 발생하는 문제 해결
    • 현재 MySQL 5.6 버전에서 바이너리 로그의 Anonymous_gtid_log_event를 버전 차이 때문에 파싱하지 못해서 롤백할 수 없기 때문
  • 모든 작업이 끝났을 때 백업으로서의 역할 기대
  • 복제 시작할 때 8대의 샤드로 바이너리 로그가 한꺼번에 전송되는 문제 해결

중간 MySQL엔 대상 테이블만 동기화하기 위해 아래와 같은 복제 필터를 사용합니다.

  • replicate-do-table=d1.t1
  • replicate-do-table=d1.t2
  • replicate-do-table=d1.t3
  • replicate-do-table=d1.t4
  • replicate-do-table=d1.t5

중간 MySQL의 각 테이블에 아래 ALTER TABLE 명령을 실행합니다.

  • ALTER TABLE t1 ADD shard_hash INT DEFAULT 65536, 
    DROP PRIMARY KEY, 
    ADD PRIMARY KEY(c1, shard_hash);

중간 MySQL shard_hash는 아래 명령으로 정기적으로 업데이트합니다.

  • UPDATE t1 
    SET shard_hash = crc32(member_id) mod 65536 
    WHERE created_on > @지난 번 업데이트 시간;

샤딩 환경 각 테이블에 아래 ALTER TABLE 명령을 실행합니다.

  • ALTER TABLE t1
    PARTITION BY RANGE (shard_hash)
    (
    PARTITION shard1 VALUES LESS THAN (8192) ENGINE = InnoDB,
    PARTITION shard2 VALUES LESS THAN (16384) ENGINE = InnoDB,
    PARTITION shard3 VALUES LESS THAN (24576) ENGINE = InnoDB,
    PARTITION shard4 VALUES LESS THAN (32768) ENGINE = InnoDB,
    PARTITION shard5 VALUES LESS THAN (40960) ENGINE = InnoDB,
    PARTITION shard6 VALUES LESS THAN (49152) ENGINE = InnoDB,
    PARTITION shard7 VALUES LESS THAN (57344) ENGINE = InnoDB,
    PARTITION shard8 VALUES LESS THAN (65536) ENGINE = InnoDB,
    PARTITION shardx VALUES LESS THAN (99999) ENGINE = InnoDB
    );

이번 방안에선 아래와 같이 한번에 전환합니다.

  • 메인터넌스 작업
    • MySQL 복제 동기화 중지
    • EXCHANGE PARTITION 명령으로 필요한 파티션(샤드 데이터)만 추출
    • 불필요한 데이터 삭제
      • 주 MySQL의 5개 테이블에 DROP TABLE 명령 실행
      • 파티션 테이블에 DROP TABLE 명령 실행
  • 사후 작업
    • 기본 키에서 shard_hash를 제거
    • 각 샤드 MySQL에 추가한 shard_hash 컬럼 삭제

채택한 이유

예상했던 대로 파티셔닝되었습니다. 또한 불필요한 데이터 삭제 작업을 메인터넌스 시간에 할 수 있다는 점도 좋았습니다. 메인터넌스 작업할 때 해야하는 작업은 아래와 같이 데이터 양에 좌우되지 않는 작업 뿐이었습니다.

  • 복제 중지
  • EXCHANGE PARTITION 명령 실행
  • DROP TABLE 명령 실행

사후 작업인 PK 변경과 컬럼 삭제는 데이터 양에 좌우되는 작업이긴 하지만, 온라인 DDL(data definition language)로 실시 가능했습니다.

 

채택한 방안으로 샤딩 실시

작업은 준비 작업과 메인터넌스 작업으로 나누어 진행했습니다. 각 작업에 대한 설명과 작업 결과에 대해 말씀드리겠습니다.

준비 작업

준비 작업은 데이터를 마이그레이션하고 파티셔닝 키를 구현, 업데이트한 뒤 파티셔닝을 실시하는 순서로 진행했습니다.

데이터 마이그레이션

데이터 마이그레이션은 먼저 중간 MySQL에서 기존 환경의 전체 백업 1TB(5TB의 MySQL 데이터를 압축)을 받는 데서 시작합니다. Xtrabackup을 이용했고 복원에는 4시간 정도 걸렸습니다. 복원을 완료한 후 대상 테이블을 제외한 다른 테이블을 삭제합니다. 삭제 대상이 대략 2TB 정도였기 때문에 중간 MySQL은 3TB가 되었습니다. 그때부터 복제 지연을 회복시키는 작업을 진행합니다. 복제 필터를 사용해서 대상인 5개 테이블만 동기화합니다. 백업 전송 시간까지 포함하니 기초가 되는 중간 MySQL 구축 작업에는 거의 하루가 걸렸습니다.

파티셔닝 키 구현과 업데이트

각 테이블에 파티셔닝 키가 될 shard_hash 컬럼을 추가해서 기본 키로 구성했습니다. ALTER TABLE 명령을 실행한 뒤 방치하는 작업을 몇 날 며칠 계속했고, 예상 완료 일시를 서버 엔지니어에게 계속 공유했습니다. 일주일에 걸친 shard_hash 컬럼 추가 작업을 끝낸 후에는 shard_hash값 업데이트 작업으로 넘어갔습니다. 처음에는 100억 개 정도되는 레코드를 모두 업데이트했습니다. 이 작업에도 1시간 정도가 걸렸는데요. 이후에 정기 업데이트를 구현했습니다. member_id가 변경되지 않도록 서비스가 설계되어서3 항상 새로운 레코드를 계속 업데이트하면 되니 아주 편했습니다. 여기까지 진행한 시점에 백업(3TB의 MySQL 데이터)을 받고 샤딩 환경 구축에 착수했습니다.

파티셔닝

전 단계에서 받은 백업을 각 샤드 MySQL에 복원한 후 파티셔닝을 진행했습니다. 파티셔닝은 이번 작업에서 가장 시간이 많이 걸린 부분이었습니다. 가장 큰 테이블이 2TB 정도였는데요. ALTER TABLE문을 완료하는데 5일이나 걸렸습니다. 또한 작업이 진행되는동안 복제를 중지시켰기 때문에, 이후 지연 복구 작업에도 많은 시간이 걸렸습니다. 이 단계에서 처음에 예상했던 메인터넌스 작업 날짜를 맞출 수 없다고 판명되어 사업부에서 일정을 조정해 주었습니다.

메인터넌스 작업

메인터넌스 작업은 데이터 크기에 좌우되지 않는 작업으로만 구성하는 것이 중요합니다. 또한 당일에는 여러 환경에서 동시에 작업을 실시하기 때문에 모두 복사 후 붙여넣기로 끝낼 수 있도록 노력했습니다. 이렇게 노력한 덕분에 당일 작업은 스케줄대로 진행되었습니다. 메인터넌스 작업 시작부터 데이터베이스 작업, 애플리케이션 배포 및 QA 작업까지 문제없이 이루어졌습니다.

메인터넌스 작업 종료 직후 과부하 발생

메인터넌스 작업 종료 직후에 앱이 느려져서 많은 사용자가 불편을 겪었습니다. 캐시 구현 실수가 원인으로 MySQL 서버에 과부하가 걸린 상태였습니다. 특정 쿼리가 대량으로 실행되고 있어서 서버 엔지니어에게 공유하여 조치 받았습니다.

메인터넌스 작업 결과

메인터넌스 작업으로 일 데이터 증가량이 8분할되어 앞으로 6년 정도는 늘어나는 데이터를 견딜 수 있는 환경이 마련되었습니다. 또한 사용자가 계속 증가하더라도 샤딩 기능이 이미 구현되어 있기 때문에 비교적 쉽게 리샤딩을 할 수 있게 되었습니다. 예를 들어 8 샤드에서 16 샤드로 변경할 경우, 애플리케이션에선 샤딩 계산식을 8분할에서 16분할로 변경하고 데이터베이스에선 각 샤드의 데이터를 shard_hash의 범위로 2분할해서 데이터 마이그레이션하면 됩니다. 또한 서버 부하 문제도 크게 개선되었습니다. 이번 마이그레이션 작업에서 JOIN 쿼리가 분할된 덕분에 느린 쿼리가 많이 사라졌습니다.

아래는 메인터넌스 작업 후 디스크 크기와 CPU 사용률, 디스크 I/O 성능을 보여주는 그래프입니다.  

디스크 크기
CPU 사용률
디스크 I/O

 

마치며

LINE Manga 서비스는 이번 대응을 통해 속도가 더욱 빨라질 것입니다. 때때로 데이터베이스 엔지니어는 서비스에 얼마나 개입할 수 있는 지가 중요한데요. 이번 작업에서 그런 부분을 잘 알 수 있었습니다. 진화를 거듭하는 서비스에 발맞춰 갈 수 있도록 더욱 적극적으로 참여할 생각이니 앞으로도 LINE Manga 서비스를 많이 이용해주시기 바랍니다!

LINE에서는 이 밖에도 다양한 서비스를 제공하고 있는데요. 그 서비스의 뒷편에서 저희 데이터베이스 엔지니어들이 활약하고 있습니다. 함께 일하실 분을 모집하고 있으니 관심 있으신 분은 부담 없이 지원해 주세요.

 


 

  1. 외부 조인(outer join)으로 레코드가 항상 0건인 경우에도 0이 되니 주의해야 합니다. 
  2. 범위 파티셔닝(range partitioning)을 사용한 이유는 아래와 같습니다.
    • EXCHANGE PARTITION 명령으로 데이터 크기에 상관 없이 필요한 데이터 추출 가능
    • EXCHANGE PARTITION 명령 실행 후 테이블 DROP 명령 실행으로 불필요한 데이터 한꺼번에 삭제 가능
    • EXCHANGE PARTITION 명령으로 추출한 데이터는 원본 테이블 데이터의 1/8 크기인 IBD 파일이기 때문에, IBD 파일 축소가 필요하지 않음
  3. 실제로는 사용자 대응을 위해 member_id 간 데이터 마이그레이션을 했던 사실이 메인터넌스 작업 직후 발견되어 조치했습니다.

Related Post