세상에 나쁜 코드는 없다

[Toy Project] 대규모 데이터를 처리하는 게시판(1) - 웹애플리케이션 개발 및 더미데이터 생성 본문

웹개발/백엔드

[Toy Project] 대규모 데이터를 처리하는 게시판(1) - 웹애플리케이션 개발 및 더미데이터 생성

Beomseok Seo 2023. 10. 31. 12:34

🔗
대규모 데이터를 처리하는 게시판 시리즈 (1) - 웹애플리케이션 개발 및 더미데이터 생성 (2) - JMeter와 테스트플랜 생성, 최초 성능 테스트 (3) - Index(1) (4) - Index(2) (4) - DB Connection (5) - InnoDB Buffer Pool (6) - Replication, Partitioning

개요

지금까지 여러 토이프로젝트를 해오면서 단일 테이블의 레코드 개수가 1억건을 넘기는 경우가 없었어서 대규모 데이터가 애플리케이션에 어떤 영향을 미치는지, 이를 해결하기 위해서는 어떤것을 도입해야하는지, 이것을 테스트하고 평가하는 항목은 어떻게 구성해야하는지 등에 대해 잘 모르고 있다는 생각이 들었습니다.

마침 구름톤 트레이닝에서 간단한 게시판을 만드는 과제가 생겨서, 이번 과제를 수행하면서 함께 이러한 내용들에 대해 고민해보고자 합니다.

처음해보는 것들이라 미숙한 부분이 많습니다. 어떠한 피드백이든 환영입니다.

게시판 개발

구성요소

게시판은 정말 간단하게 만들었습니다.

각각의 글들은 제목, 작성자, 내용, 작성일, 비밀번호를 가지고 있으며, 여러개의 댓글이 달릴 수 있습니다.

댓글은 작성자, 내용, 비밀번호, 작성일, 비밀번호를 갖고 있습니다.

게시판의 글은 10개 단위로 offset 기반 페이지네이션을 통해 제공됩니다. 또한 제목과 작성자라는 조건을 통해 특정 글을 검색할 수 있으며, 이 글 또한 페이지네이션이 적용되어 있습니다.

개별 글 조회 페이지에서는 댓글을 함께 볼 수 있습니다. 각각의 글 혹은 댓글은 작성시 입력했던 비밀번호를 통해 삭제할 수 있습니다.

Kotlin Springboot 프레임워크로 개발하였고, 템플릿 엔진은 mustache를 사용했습니다.

더미데이터 생성

추후에 찾아본 결과 더미데이터를 생성해주는 도구가 생각보다 많다는 것을 알게 되었습니다. 하지만 저는 무식한 방법을 선택했습니다.

create table article
(
    id         bigint      not null auto_increment,
    content    varchar(100),
    created_at datetime(6) not null,
    password   varchar(100),
    title      varchar(100),
    updated_at datetime(6) not null,
    writer     varchar(100),
    primary key (id)
);

create table comment
(
    id         bigint not null auto_increment,
    content    varchar(100),
    created_at datetime(6),
    password   varchar(100),
    updated_at datetime(6),
    writer     varchar(100),
    article_id bigint,
    primary key (id),
    foreign key (article_id) references article(id)
);
create
    definer = boarduser@`%` procedure InsertArticlesAndComments()
BEGIN
    DECLARE i INT DEFAULT 1;
    DECLARE articleId INT;

    WHILE i <= 500000 DO
            INSERT INTO Article (title, content, password, writer ,created_at, updated_at)
            VALUES
                (CONCAT('title', i), CONCAT('content', i), CONCAT('password', i),CONCAT('writer',i), NOW(), NOW());

            SET articleId = LAST_INSERT_ID(); 

            CALL InsertCommentsForArticle(articleId);

            SET i = i + 1;
        END WHILE;
END;

create
    definer = boarduser@`%` procedure InsertCommentsForArticle(IN article_id int)
BEGIN
    DECLARE j INT DEFAULT 1;

    WHILE j <= 300 DO
            INSERT INTO Comment (article_id, content, password,writer ,created_at, updated_at)
            VALUES
                (article_id, CONCAT('commentContent', j), CONCAT('password', j), CONCAT("writer",j) ,NOW(), NOW());

            SET j = j + 1;
        END WHILE;
END;

CALL InsertArticlesAndComment();

SQL 반복문을 통해 50만개의 Article을 생성하고, 각각의 Article에 300개의 댓글을 추가하여 총 1.5억개의 Comment를 생성했습니다.

처음에는 이 프로시져를 RDS에서 바로 실행시켰습니다. 이 방법은 RDS의 연산 속도에 의해서 레코드가 삽입되는 속도가 매우 느렸고, 또한 간헐적으로 RDS 세션이 끊켜서 도중에 멈추는 경우가 발생했습니다.

이를 해결하기 위해서 로컬에서 작업하고, mysqldump를 통해 데이터를 RDS로 옮기는 방식을 택했습니다. 로컬에서의 작업은 RDS에서 수행하는 것보다 훨씬 빨랐고, mysqldump로 데이터를 전송하는 시간을 감안한다 하더라도 전체 작업시간은 훨씬 단축되었습니다. 이 방법으로 하루 컴퓨터를 켜놓고 자니 로컬에 모든 데이터가 삽입되었고, mysqldump로 옮기는 시간은 약 5시간이 걸렸던 것 같습니다.

이 방식은 로컬에서 데이터를 만들 때 bulk inserting을 하지 않으며, 별도로 설정해주지 않는다면 삽입과정이 각각의 트랜잭션에서 수행되므로 시간이 더욱 오래걸렸던 것 같습니다. 다음번에 더미데이터를 생성할 때에는 이런점을 더 고려해서 빠르게 데이터를 만들 예정입니다.

이렇게 만들어진 데이터는 약 18GB 정도가 되었습니다. 이러한 형태의 데이터 셋에서 Comment 부분의 데이터가 1.5억개로 가장 많기 때문에, 이를 더 효과적으로 사용할 수 있게 ‘댓글 게시판’ 기능을 추가로 만들었습니다.

댓글 게시판은 마치 글 게시판 처럼 댓글의 목록을 페이지네이션으로 볼 수 있는 기능입니다. 이는 일반적인 서비스에서는 제공되지 않지만, 이번 토이프로젝트에서는 성능 개선의 효과를 크게 느낄 수 있을 것 같아 만들어 보았습니다.

이렇게만 작성하면 되게 간단하게 만든 것 같아 보이지만, 이 과정에서 많은 문제를 맞닥뜨렸습니다. 이 문제들은 간단하게 아래에서 소개하고자 합니다.

Trouble Shooting

안일한 데이터 사이즈 설계

처음에는 별 생각 없이 더미데이터를 생성했습니다. ‘이왕 하는거 규모는 클수록 좋지’라고 생각해서 Article을 1,000만개를 만들고 Comment는 3억개를 만들었습니다. 이렇게 만든 데이터 셋은 약 40GB 정도가 됐는데, 이를 20GiB의 용량을 갖는 RDS로 옮기고 나니 조금 문제가 발생했습니다.

Mysqldump를 통해 데이터를 옮길 때 Article을 먼저 다 삽입한 뒤 Comment 를 삽입하는데, 이 과정에서 용량이 부족하므로 Comment 중 일부만 삽입되었습니다. 이는 예상한 결과이고 테스트하는데 큰 문제가 없을거라 생각했는데, 다음과 같은 문제가 발생했습니다.

  1. 테스트 생성의 문제

다음 글에서 더 자세히 다루겠지만, 저는 테스트를 최신글과 전체글을 분리하여 수행하고자 했습니다. 최신글의 페이지를 조회하는 요청과 개별 최신글을 조회하는 요청을 만드려는데, 최신 글에는 Comment가 달려있지 않아 정상적인 테스트를 진행할 수 없었습니다.

  1. RDS 용량 부족 상태

RDS 생성에서 작성한 용량과 근접해진다면 RDS는 ‘스토리지가 꽉 참’이라는 상태를 가지며 정상적으로 동작하지 않습니다. 그냥 데이터 용량이 꽉 차면 더 이상 생성만 안되겠지라고 안일하게 생각했었는데 이 경우 DB를 아예 사용할 수 없었습니다.

Lock table size

위와같은 문제를 해결하기 위해서 RDS에 있는 데이터를 삭제하고자 했습니다. Comment를 1.5억개만 남겨두고 삭제하고, comment가 달려있지 않은 article 또한 삭제해서 잘 정리된 더미데이터를 만들고자 했습니다.

물론 이 삭제 과정을 RDS에서 수행하는 것은 매우 느릴 것이므로, 로컬에서 삭제한 뒤 mysqldump로 RDS로 옮길 계획이었습니다.

이를 위해 작성한 쿼리는 다음과 같은데, 이는 오류를 뱉어냈습니다.

delete from comment where id > 150000000;
.. 3시간 후 ..
[HY000][1206] The total number of locks exceeds the lock table size

검색을 통해 알아보니 이는 많은 행을 수정하거나 삭제할 때 발생하는 이슈로, 데이터베이스가 한번에 처리할 수 있는 lock의 개수를 초과하기 때문에 발생한다고 합니다.

MySQL에서는 종류 불문 쿼리를 처리할 때 대상 데이터들을 메모리에 올립니다. 이 메모리 공간의 크기는 innodb_buffer_pool 이 결정합니다. 만약 대상 데이터의 크기가 버퍼의 사이즈보다 크다면, 버퍼 풀에서는 처리된 데이터를 내보내고 새 데이터를 Disk로부터 가져오는 과정을 반복합니다. 이 동작을 하는 과정에서 해당 테이블에 데이터 조작 명령어가 들어온다면 일관성이 깨질 수 있기 때문에 테이블 전체에다가 lock을 걸게 되는데, 테이블이 너무 크다면 위와 같은 오류가 발생하게 됩니다.

검색을 통해 알아보니 이러한 쿼리를 대규모 데이터에 사용하는 것은 당연히 금기시되는 일이라고 합니다. 저는 몰랐습니다.

따라서 이를 해결하기 위해서는 배치를 도입해 삭제할 범위를 결정하고 그것들을 여러개의 작업단위로 쪼개어서 수행해야 한다고 합니다.

CREATE PROCEDURE batch_delete()
BEGIN
	SET @start_id = 150000000;
	SET @end_id = 300000000;

	SET @batch_size = 10000;

	-- 삭제 루프 시작
	batch_delete_loop: LOOP
	SET @delete_query = CONCAT('DELETE FROM comment WHERE id BETWEEN ', @start_id, ' AND ', @start_id + @batch_size); PREPARE stmt FROM @delete_query; EXECUTE stmt; DEALLOCATE PREPARE stmt;

	SET @start_id = @start_id + @batch_size + 1;

	IF @start_id > @end_id
		THEN LEAVE batch_delete_loop;
		END IF;
	END LOOP batch_delete_loop;
END;


CALL batch_delete();

이러한 삽질을 통해 데이터를 옮기기 까지 사흘 정도 걸렸던 것 같습니다.. 답답함의 연속이었지만 평소에 보기 어려운 에러들도 마주하기도 하며 나름 도움이 되었던 것 같습니다.


Uploaded by N2T