Published on

팀업 백업 서비스 회고

Authors
  • avatar
    Name
    이건창
    Twitter

Introduction

수만명의 사용자가 사용하던 협업툴 운영 종료하면서 필요에 따라 백업 데이터를 생성해 전달하는 서비스를 구축하고 서버를 운영하게 됐어. 그 과정에서 백업 신청한 사용자의 데이터를 조회해 압축하는 과정과 서버를 운영하는 역할을 맡게 됐지.

적절한 기준점을 찾자

처음에는 요구되는 실행 시간을 기준으로 얼만큼의 성능이 필요한지를 고려했어. 계획한 내용은 다음과 같아.

  • 매일 166명(10,000명 / 60일)을 처리해야 한다.
    • 유저 10만 명으로 가정한 후 10%가 백업한다고 가정한다면 두 달 동안 10,000 명의 백업을 수행해야 한다.
  • 한 명당 최소 13분(166명 / 18시간) 정도 실행 시간이 나와야 한다.
    • 백업 되는 시간을 제외하면 대략 18시간 이내에 166명을 처리해야 한다.

데이터가 많은 사용자를 기준으로 처음 실행 했을 때는 30분 그다음 15분 마지막으로 7분으로 단축하면서 필요한 만큼의 성능 개선 작업만 수행했어.

성능 개선은 끝이 없고 기준치를 넘으면 오버엔지니어링이라 생각해. 사용자가 불편하지 않는데 속도를 개선하면 의미가 있는걸까? 나는 목적에 의해서 행동하고 가설을 세워 적절한 타협점을 잡는걸 중요하게 생각해.

스프링 배치를 활용한 유연한 기능 변경 경험

사용자들의 백업을 스프링 배치를 활용해 실행 흐름을 자유롭게 풀어갈 수 있는 장점을 활용했어. 스프링 배치를 학습하는 과정에서 나만의 언어로 이해하기 위해 노력했지.

그림 1

위 그림은 팀원들에게 어떻게 작업할지 공유하기 위해 그린 샘플이야.

그 과정에서 스프링 배치를 사용하지 않고도 충분히 수행 할 수 있지 않냐는 질문을 받았어. 생각해보면 단순한 자바 코드로도 작업들을 수행하는 데 지장이 없었지. 그래서 장점과 단점을 정리해봤어.

  • 배치 장점
    • 독립적인 단위로 작업을 관리해 기능 변경에 유리
    • 독립적인 단위로 작업을 관리해 실행 흐름 변경에 유리
    • 배치 실행을 효율적으로 도와주는 기능 존재
  • 배치 단점
    • 학습 난이도 존재
    • 작업이 적다면 오버엔지니어링

위와 같은 장점은 운영에서 두드러졌어. 실패하는 작업은 다른 실행 방법으로 관리가 필요했는데, retry를 활용해 쉽게 대응 할 수 있었지.

논리적으로만 생각했던 장점들을 운영에서 경험 할 수 있어서 좋았어.

성능 이슈

슬로우 쿼리 경험

개발 과정에서 기능 변경 성능 튜닝을 위해 쿼리를 빈번하게 수정했어. 쿼리를 수정 할 때마다 실행 계획을 확인 했음에도 불고하고 슬로우 쿼리가 발생했어. 원인은 최악의 데이터 셋인 경우의 실행 계획을 파악하지 못했던게 컸어.

UNION 으로 인해 쿼리 실행 시간이 30초나 걸리기도 했어. ㅋㅋㅋ 슬로우 쿼리의 가장 큰 문제점은 데이터베이스 접근 과정을 병목 지점으로 만드는 일이야. 지속적으로 요청을 보내 커넥션을 재활용 해야 하는데 반환받는게 느리면 어떻겠어. 당연히 문제가 생길 수 밖에 없지.

잘못된 쿼리는 Connection Timeout 문제가 따라올 수 밖에 없었어. 이러한 경험 덕분에 실행 시간이 올래 걸리는 경우 쿼리 실행을 종료해 Read Timeout을 발생시키고 이후 사용자가 불쾌하지 않도록 실패를 어떻게 처리할지 고민해야 한다는 것을 깨달았지.

그리고 슬로우 쿼리를 확인 할 수 있는 시스템 지표가 있었으면 좋겠다는 생각이 들었어. 슬로우 쿼리가 발생 한다는 건 서비스 구현이 70% 되고 나서 개발 환경에서 테스트 할 때 발생했지. 이런 문제를 개선하기 위해 진행율과 실행 시간을 파악할 수 있도록 로그를 찍었어.

... 생략
2023-10-11T16:31:33.957+09:00  INFO 1 --- [nPool-worker-30] [percent : 23%]
2023-10-11T16:31:33.709+09:00  INFO 1 --- [nPool-worker-32] [percent : 21%]
2023-10-11T16:31:33.655+09:00  INFO 1 --- [nPool-worker-34] [percent : 19%]
2023-10-11T16:31:33.469+09:00  INFO 1 --- [nPool-worker-25] [percent : 17%]
2023-10-11T16:31:33.452+09:00  INFO 1 --- [or-http-epoll-9] [percent : 14%]
2023-10-11T16:31:33.326+09:00  INFO 1 --- [nPool-worker-29] [percent : 12%]
2023-10-11T16:31:33.133+09:00  INFO 1 --- [nPool-worker-33] [percent : 10%]
2023-10-11T16:31:33.133+09:00  INFO 1 --- [nPool-worker-35] [percent : 8%]
2023-10-11T16:31:33.037+09:00  INFO 1 --- [nPool-worker-22] [percent : 6%]
2023-10-11T16:31:32.296+09:00  INFO 1 --- [nPool-worker-24] [percent : 4%]
2023-10-11T16:31:31.502+09:00  INFO 1 --- [nPool-worker-27] [percent : 2%]

쿼리 실행 시간을 측정 할 수 있는 시스템 메트릭 수집 툴을 사용했더라면 더 원만하게 해결 할 수 있어보였어. 덕분에 메트릭에 대한 중요성을 체감하고 다음 프로젝트에서는 맞는 메트릭 툴을 적용하기 위해 이것저것 연습해보고 있어.

지금은 두 개 정도의 메트릭 툴을 학습하고 사용해봤어. 정리한 위치 공유할게.

미비했던 성능 튜닝 경험

목표했던 실행 시간 내에 모든 작업을 처리하기 위해서 성능 튜닝이 필요했어. 그래서 빈번하게 발생하는 파일 I/O를 해결하기 위해 메모리에 데이터를 적재할 수 있도록 작업 방식을 변경했지. 결과적으로 파일 I/O 횟수를 줄일 수 있었지만 실행 시간은 비약적으로 줄어들지 않았어.

파일 I/O1/3으로 줄였지만 실행 시간이 많이 줄어들지 않았던 문제를 파악하는 시간을 가졌고, 실행 시간을 많이 소유하는 쿼리 실행 시간을 줄이는 시간도 가졌어.

이번 경험을 계기로 성능 개선은 리소스를 효율적으로 사용하는 일임을 깨달았어. 다음부터는 가용할 수 있는 리소스를 파악하고 제품에서 활용 할 수 있을지 판단하는 단계로 접근해야겠어.

QA를 통한 이해 관계자 간 소통 경험

요구사항 정제는 중요해

QA 단계에서 정책과 다른 부분들에 대해서 수정 요청이 많았어. 특히나 정책을 제대로 파악하지 못한점이 가장 컸어. qa 과정에서 소프트웨어 테스팅 책을 읽었는데, 명세 기반 테스트의 중요성을 체감하게 됐지. 난 정책을 검증하지 못하고 단순하게 코드만 검증하는 테스터였어.

앞으로는 개발 일정에서 요구사항 정제하는 시간을 더 투자할 생각이야.

커뮤니케이션을 잘한다는 기준

개발 단계에서 불편해 보이는 부분은 개선될 수 있도록 공유했지만 모든 이해관계자에게 전파되지 못해서 QA 팀과 소통이 어려웠어. 소통의 어려움을 겪으면서 의사소통을 잘한다는 건 나의 의견이 이해관계자들에게 넓게 전파된다는 의미란 걸 이해했지.

앞으로 어떻게하면 이해관계자들과 원활한 의사소통을 수행할지 고민해봐야겠어.

어떤 포지션인지, 얼만큼 노력해야 하는지

배포가 얼마 남지 않았을 때 기능 개선을 요청이 들어올 때 였어. 편의성은 높아지겠지만 기존 실행 흐름과 달리 새로운 테이블도 집계해야 해 성능에도 영향을 미칠 수 있는 문제가 발생했어. 사용자 편의성 개선 작업에서 개발자는 사용자 편의성을 중시하는 것보다 시스템 안정성을 고려해야 하는 포지션이 아닌가를 고민이 됐지.

좋은 선택을 하기 위해 선택의 폭을 넓힐려면

운영 이슈

Out of heap memory 이슈

일부 사용자가 실패하는 케이스가 존재했는데, 모니터링으로 확인했을 때 한 번의 요청에서 정말 많은 메모리 사용량이 필요했어. 사용한 메모리 양이 2.3GB 밖에 되지 않았지만 OOM 이슈가 발생했지. 시스템에서는 4GB까지 사용 할 수 있도록 허락은 받았지만 실제 물리 메모리 사용을 위해 allocate 할 때 거부당한 것 같아.

4GB에 다다르기 전에 실패했으니 어떤게 문제인지 쉽게 예상 할 수 없었어. 예상되는 문제는 다음과 같아.

  • 보안을 고려해 힙 오프셋의 난수화에 따른 메모리 사이즈 결정
  • 메모리 파편화로 인해 사이즈 부족
  • LinkedList를 ArrayList로 생성하기 위해 배열만큼의 메모리를 할당받다보니

제일 마지막께 유력한 듯 해. ArrayList까지 만들게 되면 순간적으로 4.82GB가 필요했던거지.

그림 3

위와 같은 가설로 해결책을 생각했어.

  • 대안 1. 메모리에 데이터를 적재하지 않고 디스크에 적재한 다음 압축을 진행한다.
  • 대안 2. JVM 힙 사이즈를 임시적으로 늘린다.

가장 빠르게 해결하는 방법은 코드 작성을 안하는 거더라구! 그래서 JVM 힙 사이즈를 임시적으로 늘려 시도한 후 실패한다면 코드 수정 작업을 진행하자!는 결론에 다다랐어. 다행히 요구되는 힙 사이즈 사용량이 4.82GB여서 코드 수정없이 문제 해결 할 수 있었어.

마지막으로 가용하는 16GB 공간 중 free 영역이 4GB 이기 때문에 지속적인 메모리 모티터링 필요해보기도 했어. 메모리를 정말 알뜰하게 쓰려다보니 이런저런 문제들이 많이 발생하는 듯 해.

해당 이슈에서 코드 말고 정책으로 해결하자는 의미를 이해하게 됐어. 가치를 빠르고 높게 전달하기 위해서는 어떤 방법이 좋은지 고민해야 해.

Batch provider에서 발생하는 BadSqlException

데이터 크기는 많지 않지만 실패하는 사용자가 있었어.

Encountered an error executing step noteStep in job backupJob

org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [
	SELECT   ni.note_seq          AS note,
             ...
	     JOIN note.note_info ni
	     ON nu.note_seq = ni.note_seq WHERE (nu.user_seq = ?
	     AND nu.deletetime IS NULL) AND ((note > ?)) ORDER BY note ASC LIMIT 1000
]

Caused by: java.sql.SQLSyntaxErrorException: (conn=828) Unknown column 'note' in 'where clause'

Spring batch에서 제공하는 paging provider는 자동으로 pk 컬럼을 찾아 데이터를 페이징하고 있어. 그런데 pk 컬럼을 별칭으로 설정하게 되면 where 절에서는 pk 컬럼이 아닌 별칭으로 찾으려는 버그가 있었고, 쿼리문에서 확인 할 수 있듯이 ((note > ?)) 라는 쿼리문에서 문제가 발생했지.

라이브러리에서 동일한 이슈가 있었는지 확인했는데 고칠 생각이 없어보였어.

버그를 잡지 못한 원인 파악을 고민했는데, 테스트 과정에서 페이징 건 수 이상인 경우를 검증 못해서 발생하게 됐지 스프링 배치를 활용한다면 페이징 단위를 줄여서 페이징이 원활하게 수행되는지 검증하는 작업이 필요해 보였어.

앞으로는 테스트 케이스를 넓게 바라볼 수 있는 시야를 가지도록 노력해야겠어.