서버 개발을 하다보면 batch를 수행하게 될 일들이 종종있다. 종종이 어떨때는 자주가 되기도 한다. batch application을 개발하는 것과는 별개로 한번만 수행되는 batch 작업을 하게 되는 경우도 많다.

이런 일회성 배치는 생각보다 자주 수행하게 되는데 많을 땐 한 해에 10번을 넘게 돌리기도 했다. 최근 데이터 정리를 위해 오랜만에 일회성 배치를 돌리면서 내 생각들을 정리한다.

one time batch

일반적으로 사용하는 배치작업과 일회성 배치는 결이 좀 다른 것 같다.
한번만 수행하면 된다는 특징이 있다.

일회성 배치를 뭐라고 하는지 모르겠지만 one time batch라고 하면 좋을 것 같다.

일회성 배치를 돌리는 상황들을 생각해보면 아래와 같은 케이스들이 있다. 다른 케이스가 더 있을지도 모르지만 내가 batch를 수행했던 이유들은 이렇다.

1. migration

마이그레이션을 하는 이유 repository의 구조 변경을 하는 경우가 많다.
우리는 마이그레이션을 수 차례 진행했는데 이런 케이스다.

  • database를 변경하는 케이스
  • database 효율화를 위해 table을 통합하거나 스키마를 변경하는 케이스
  • storage key를 변경하는 케이스

legacy 모듈에는 database 스키마나 storage key가 굉장히 이상하게 작성되어 있는 경우가 종종 있다.
신규 피처를 추가하거나 리팩토링하면서 이런 로직들을 안고 분기처리할 수도 있지만 마이그레이션을 하면 코드 관리가 더 좋았다.
마이그레이션도 처음이 어렵지 여러번 하다보니 괜찮기도 했고.

2. delete

서비스를 eof 시킬때도 일회성 배치를 활용했다. 서비스가 통채로 사라진다면 일정시간 보관 후 데이터를 전부 날리면 되겠지만 우리는 서비스의 일부 content만 eof되는 케이스가 있었다.

이때도 일회성 배치를 계속 사용했다.

3. check

서비스에 버그가 있어서 장애를 겪는 사용자가 있는지 파악하기 위해 일회성 배치를 활용하기도 했다.
데이터 저장에 버그가 있는 이슈를 발견했을 때 이런 케이스에 해당하는 유저 데이터가 어느정도 되는지 파악하기 위해 사용하기도 한다.

4. revise

버그를 찾았다면 수정을 해야할 경우에도 일회성 배치를 사용한다.

배치에서 생각할 것

일회성 배치를 수십 차례 수행하면서 느낀 것. 배치에서 필요한 고려사항들.
이 느낀점은 한참 배치를 많이 수행하던 21년도 회고에서 회고한 내용도 포함한다.

확장성 보장하기

대부분의 일회성 배치들은 대부분 정해진 기간이 있다.
마이그레이션이라면 기간이, eof를 위한 삭제에도 기간이 있고 이슈 해결을 위한 작업들은 언제나 ASAP.

기간을 맞추기 위해, 혹은 낭비되는 cpu를 채우거나 인스턴스 숫자를 조절하기 위해서 보통 batch의 worker(instance 혹은 thread)가 확장 가능한 구조가 굉장히 중요하다.
우리는 배치를 한번 돌리면 대상이 수천만에서 수억이기 때문에 확장이 많이 필요했다.

instance를 더 붙이고 싶은데 불가능하거나, 성능을 높이려면 재시작이 항상 필요하다면 곤란하다. 배치를 돌리다보면 성능을 늘리고 보니 더 늘리게 되는 경우가 태반이니까.=

멱등성 보장하기

재시작이 필요하지 않은 확장성을 보장하더라도, 배치를 돌리다보면 여러 이유로 배치 프로세스를 중단하고 재시작을 하는 경우가 많다. 배치 트리거 인스턴스에서 memory/disk가 부족해서 scale up, ebs 추가를 하기도 했고.
db thorttling 이슈로 input data를 정리해서 재수행하기도 했다.
뭐 그렇지 않더라도 인스턴스가 내려간다거나 하는 케이스도 고려되야할 것 같다.

재수행 할 때마다 수행된 타겟이 batch에 다시 들어가더라도 문제가 없도록 멱등성이 고려되어야 한다.
그렇지 않으면 어디까지 돌렸는지를 정확하게 정리하는데 더 많은 시간을 쓰게 된다.
배치 수행이 한번 되든 열번 되든 타겟에는 한번 된 것과 동일한 문제 없는 상태여야한다는 말.

재수행 고려하기

멱등성만 보장된다고 문제가 없는 건 아니다.
초기에 돌렸던 일회성 배치중 하나는 멱등성이 완벽하게 보장되었지만 재수행에 대한 고려가 되어있지 않은 케이스가 있었다.
배치가 돌아간 타겟이 다시 시간을 잡아먹으면서 배치가 길어지고, 타겟을 좀 더 추려내기 위해 개발자가 붙어 작업을 하기도 했다.

재수행이 고려되어 있지 않다면 일주일을 배치를 돌리고 다시 돌렸을 때, 동작은 동일하지만 일주일을 꼬박 다시 기다려야한다거나 하는 이슈가 생길 수 있다. 물론 실제로 수행하다보면 일주일이나 돌렸다면 어느정도 타겟을 정리 하겠지만 앞서 말했듯 타겟을 명확하게 정리하는 일은 생각보다 귀찮다.
이미 수행된 타겟이 다시 재수행되는 경우 재수행 시간이 오래걸리지 않도록 고려가 필요하다.

migration이라면 rollback 구조를 파악하기

많은 배치와 migration을 수행하면서 간단한 migration에는 자신감이 충만해질 때가 있다.
그래서 rollback에 대한 생각을 접어둘 때가 있는데, 배포 이후에 rollback이 불가능한 경우를 생각하면 아찔하다.
migration은 사용자의 data를 손대는 배치다보니 작업 수행 전후로 확인해야할 내용들을 명확히 정리하고 수행해야 한다.
migration 이후에도 이슈가 리포트되면 이전 상태로 rollback할 수 있는 구조를 가져가는 편이 안전하다.

지표 확보하기

배치를 수행하는데 이게 언제 끝날지 감이 잡히지 않는다면 곤란하다.
기간을 맞추기 위해선 tps나 batch의 예상 시간을 조회할 수 있는 지표를 남기면 유용하다.
이대로 배치를 돌리면 1년이 넘게 걸리겠다는 인사이트를 지표를 통해 얻은적이 많다.
지표를 확보하고 필요하다면 배치를 확장한다.

남은 타겟의 수나 예상 시간을 지표화하면 인스턴스에 들어가서 진행 상황을 보지 않더라도 잘 수행되고 있는지, 배치가 완료되었는지를 확인할 수 있어 좋다.

data flow 확인하기

우리는 지금 global 5개 region에서 서비스를 하고 있다.
file이 오가는 batch의 경우 binary data가 region을 넘어가는 경우가 발생하기도 하는데, 최대한 효율적으로 binary flow를 잡아갈 수 있도록 설계가 필요하다.
한 번은 설계를 잘 해놓고, binary flow를 놓쳐서 성능이 현저하게 떨어져서 다시 개발한 적이 있었다.

배치 개발하기

배치에서 필요한 것들을 생각해보면 배치를 어떻게 개발해야할지가 그려진다.
저 요구사항들을 맞추기 위해 우리가 개발했던 방향들.

batch api 개발

배치는 api를 사용하는 편이 좋다.
예전에는 db를 직접 조작하는 스크립트를 수행하는 적도 있었지만 스크립트 배치는 단점이 너무 크다.

  1. 소스코드가 잘 관리되지 않는다.
  2. 마찬가지로 소스 히스토리가 관리되지 않는다.
  3. 테스트가 넉넉하지 않고 허점이 있을 수 있다.
  4. 위험하다.

반면 서비스 코드에 api로 작성하게 되면 히스토리 관리나 테스트, 안전성을 확보할 수 있다.
그리고 서비스 코드의 domain과 usecase들을 활용할 수 있어서 개발이 훨씬 간단하다.

위에서 말했듯 일회성 배치에서는 확장성이 가장 중요하다.
일회성 배치를 위해 배치 인스턴스를 확장 가능하게 가져가고 타겟을 분배하는 아키텍처를 잡는 것은 시간 낭비다.
was의 pipeline을 사용하거나 확장해서 사용하면 이걸 고려하지 않아도 된다.

batch runner 개발

batch api를 만들었다면 target 리스트를 보고 api를 호출하는 runner가 있어야 한다.
runner는 언어가 무관하지만 경험상으론 golang이 가장 적절하다.

초창기 우리팀의 일회성 배치는 python 스크립트로 타겟들을 읽어서 api를 호출하는 방식으로 구현됐다.
내가 이걸 처음 돌리게 됐을 때 대상 타겟이 수 억개 였고 python process를 늘리더라도 1년 이상의 시간이 걸리는 상황.
runner 자체가 오래걸리니 부하 조절을 할수가 없는 상황이었다.

이를 해결하기 위해 golang으로 runner를 개발했다.
golang의 경우 kb 단위의 메모리를 사용하는 경량 쓰레드인 go-routine 덕분에 쓰레드 조절에 문제가 없었다.
backend가 충분히 받쳐준다면 runner의 문제로 부하를 조절하지 못하는 상황은 없다.

  • 배치 수행하면서 c5.xlarge로도 3000개의 thread까지 호출했다.
  • 이건 api의 workload가 중요한데 당시 batch api는 평균 1000ms.
  • 이 때도 우리 일정상 더 높은 부하가 필요하지 않았을 뿐 c5.xlarge에서 3000개의 go-routine이 부담스럽진 않았다.

이렇게 작성한 runner는 배치마다 조금씩 업데이트해서 현재는 모든 배치에서 거의 동일한 runner를 사용할 수 있게 됐다.

batch runner 개발시 생각할 것

runner에서는 배치 로그를 확인할 수 있으면 좋다.
어떤 이유로 실패했는지를 로그를 보고 알 수 있다면 api를 수정하기 쉽다. 이건 api도 어느정도 설계가 잘 되어야 한다.

배치 결과 실패한 타겟들을 별도로 뽑을 수 있어야 한다.
실패한 타겟들을 따로 추출할 필요 없이 배치 수행시 실패한 타겟을 별도로 뽑아서 재수행시 해당 타겟들만 재수행할 수 있는 구조가 되어야 한다.
배치는 한 번에 성공하기는 불가능하고 수정과 배포를 하며 수차례 재수행하는데 수행이 간단해야 한다.

  • 이런 재수행이 한번의 스크립트로 수행될 수 있어야 재수행에 대한 시간낭비가 적고 귀찮음도 줄 수 있다.

동적으로 thread 추가가 가능해야 한다.
우리는 worker라고 불렀는데 property를 통해 thread를 동적으로 늘릴 수 있어야 한다.
지표를 확인하고 부하를 참고한 뒤 재수행 없이 확장 가능하도록 thread를 property 변경으로 늘릴 수 있는 구조를 만들어야 한다.

문서화

배치를 수행하는 정보를 기록하고 관리해야한다.
한 해에 열 번이 넘는 일회성 배치를 작업하면서 우리는 배치작업을 폴더 이름으로 관리했다.
우리도 이렇게 배치를 많이 돌리게 될줄은 몰랐지. 나중 가서야 우리가 문서화를 제대로 하지 않았다는 것을 확인했다.

  • migration1, migration2, migration 21, …

어떤 배치에서 어떤 작업들을 했는지 보이지가 않았다.
이런 배치들은 이름만 봐도 알 수 있게 분리되고 관리되어야 한다.

시간이 지나고 이 배치들을 돌아봐야 할 때가 있다.
이슈가 생겼다거나, 특정 시점의 데이터가 수상하다거나.
배치에 대한 정보가 명확하게 기록되어 있지 않으면 나중엔 이걸 찾는 것도 일이다.

배치가 열 번이 넘어갈 때쯤 나는 팀 docs 아래 문서 하나를 만들었다.
히스토리를 관리하는 history.md에 배치 시작 시간, 끝나는 시간, 사용한 api, 타겟과 배치 특이사항을 기록하는 표를 작성했다.
배치 수행 기록을 한눈에 볼 수 있게 문서화하는 것이 중요하다.

문서화의 다른 목적은 휴가를 잘 가기 위한 방향이다.
배치는 생각하지 못한 이슈로 길어지기도 한다.
내가 휴가를 가야될 때 다른 팀원이 배치 진행사항을 확인할 수 있도록 문서화 되어야 한다.