Message Queue에 대한 단상 & Beanstalkd on Ubuntu 16.04 삽질기

Development / SQS / devops / Message Queue / Beanstalkd

우리는 기존에 인프라 간 느슨한 결합 구조를 위한 Message QueueAWS SQS(Simple Queue Service)를 오랫동안 사용해왔다. SQS의 장점은 무수하다. 몇 개를 꼽아보자면 일단 AWS 완전 관리형으로 Ops 포인트가 들어가지 않고, 매우 안정적이고, AWS Console에서 UI로 관리 및 모니터링 되고, Cloudwatch와 연동하여 Auto Scaling 등에 바로 활용할 수 있고, 인프라 부담 고려 없이 미친듯이 Push를 해도 거뜬하며, 여러 Queue를 동시 생성 및 가용 할 수 있고, Dead letter queue를 별도 지정할 수 있으며, Visibility 개념을 활용한 깔끔한 생명주기를 가지고 있다. (헥헥..) 하지만 단점으로는 한 개의 Queue에서 동시에 120,000개의 메시지만 inflight 상태로 있을 수 있으며(우리는 병렬로 Queue를 운용해 이 문제를 타개했었다.), 엔터프라이즈 규모로 넘어가게 되면 기하급수적으로 비용이 증가한다는 것이다.

우리 시스템의 아키텍쳐상, Message Queue는 사람으로 치자면 척수에 해당하는 매우 중요한 역할을 하기 때문에 될 수 있으면 (제발)건드리지 말자라는 입장을 고수하고 있었으나, 트래픽의 급작스런 증가에 따라 상기 단점에서 지적된 '비용' 이슈가 매우매우 커져서 도저히 감당할 수 없는 지경에 이르르고 말았다. (월간 약 100억 건 이상의 메시지를 처리해야 했기에..) 그래서 우리는 해야만 했다. 마이그레이션을.

마이그레이션 검토 대상은 AWS Kinesis Stream, RabbitMQ, Beanstalkd 이렇게 세 개 였다. Kinesis는 사실은 메시지큐는 아니고 스트림 서비스지만 예전부터 우리의 다양하고도 거대한 데이터 스트림을 한 곳 으로 모아보자는 대통합에 대한 지대한 관심이 있었고, 더불어 요새 넘나 힙하게 떠오르고 있기 때문에(?) 선정되었고, RabbitMQ는 워낙 유명한 AMQP 기반의 메시지 브로커임과 동시에 예전에 직접 사용해보았던 경험이 있었기 때문에 선정되었고(홈페이지에 들어가면 RabbitMQ is the most widely deployed open source message broker라고 떡하니 써있다. 자신감 오졌다.), Beanstalkd는 사실 이번에 리디북스 블로그를 통해 처음 들어보았는데, 솔루션 자체가 내가 쩰루 좋아하는 Minimalism 컨셉이길래 선정되었다. C로 작성되었으며, 엄청 가볍고, 캐치프레이즈가 Beanstalk is a simple, fast work queue.이다. (수 년 전 Django와 Flask 중에 Flask를 골랐던 이유와 같았다.)

각각의 장단을 정리하고 보니, Kinesis Stream은 마이그레이션에 시간이 너무 오래걸려(Job Producer와 Consumer 소스를 대대적으로 모두 교체해야 했기에) 단기적으로 비용 평가(기회비용을 포함한 내 작업 cost > 비용 절감액)에서 탈락함과 동시에 AWS에 또 다시 디펜던시가 생긴다는 단점이 있었고, RabbitMQ는 현재 인프라 구조에서 사용하기에는 필요 없이 기능이 많았다. 반면에 Beanstalkd는 최근 릴리즈가 2014년이었기에 조금 수상했지만, 지금도 계속 많은 사람들이 사용하고 있기에 그만큼 Stable한 상태일거라는 안도감이 들었고, 메시지의 생명주기도 너무 마음에 들었다. (Bury와 Kick이라니. 얼마나 유용하고 귀여운가.) 그래! 그래서 나는 Beanstalkd로 결정했다. (사실 조금 답정너였던 것은 인정한다. 미니멀리즘 최고. ㅎㅎㅎ)

Beanstalkd는 다른 솔루션들과 마찬가지로 다양한 언어의 라이브러리를 지원하는데, 우리의 Back-end application은 거의 Python으로 작성되어 있기 때문에, 가장 GitHub Star가 많고 문서화도 잘 되어 있는 Beanstalkc(Apache License 2.0)라는 라이브러리를 사용하기로 했고, 일부 Lua(w/ NGINX)로 작성된 구간에서는 lua-resty-beanstalkd(BSD License)를 활용하기로 했다. 모니터링 도구는 Docker Image가 만들어져 있어 쉽게 설치 가능한 Beanstalkd View(MIT License)를 사용하기로 했다. 이를 이용해 시스템 현황 및 큐 통계 모니터링, 메시지 Peek, Kick, 특정 Tube clearing 등이 모두 가능하다. 통계 모니터링 부분이 조금 부족한데, 아직까지는 만족한다. Ruby 기반인 것 같고 나중에 시간 나면 수정해서 기여도 해보고 싶다.

Beanstalkd를 설치하거나 실제 사용하는 방법, 컨셉 등은 워낙 좋은 문서들이 많으니 여기서는 패스하고, 설치 후 운용하면서 겪었던 삽질들에 대해 조금 논해보고자 한다.

첫 번 째로는 바로 도저히 실행 옵션이 바뀌지 않아요가 되겠다. 일단 Listen interface를 127.0.0.1에서 0.0.0.0으로 바꿔야 했고, 안정성을 위해 메모리가 아닌 파일 기반의 Buffer를 사용하기 위해 -b 옵션을 줘서 실행하고 싶었는데, /etc/init.d/beanstalkd를 아무리 수정해도 적용이 되지를 않았다. 너무나 답답했다. 근데 이건 내가 너무나도 바보였던 것이, 주석에 "/etc/default/beanstalkd의 설정이 overriding 됩니다. 거기서 수정할 수 있어요~" 라고 친절하게 되어 있었는데 그걸 못 봤던 것이다. 하하.

두 번 째는 실제 대규모의 Put과 Reserve를 하니 클라이언트에서 계속 Connection 에러가 발생했던 것이다. 이래저래 찾아보다가 /var/log/syslog에서 결국 Too many open files에러가 발생하였음을 알게되었고, ulimit을 수정하였다. 하지만 널리 알려진 ulimit 수정 방법을 모두 수행했음에도 불구하고 $ cat /proc/{프로세스_ID}/limits 해보면 Max open files에 적용이 되어있지를 않았다. 열라 구글링 해보니 일반 설정과 마찬가지로 /etc/init.d/beanstalkd에서 수정할 수 있다라는 글도 찾아냈지만, 이 것 도 나에게는 정상 작동하지 않았다. 결국 찾아낸 해답은 /lib/systemd/system/beanstalkd.service[Service] 항목에 직접 LimitNOFILE={원하는_limit_수}를 추가해주는 것이었다. (이건 아직도 내가 Linux 운용 능력이 부족함을 보여준 것이라 생각한다. ㅠ.ㅠ)

세 번 째는 고가용성 처리를 어떻게 할 것인가에 대한 것이었는데, AWS ELB에서 Active-StandBy 구조를 통해 해결했다.

네 번 째는 소스 레벨에서 Connection Keep-Alive 혹은 Pooling, 그리고 Thread-safe에 관한 것이었는데, 위에서 기술한 Beanstalkc 라이브러리가 커넥션 풀링이 안되고, Thread-safe하지 않다는 것이 문제였다. NewRelic을 통해 모니터링 해보니, 생각보다 서버에 접속하는 비용이 비싸지 않아 일단 이 부분은 Singleton을 모두 제거하고 Put task가 있을 때 마다 클라이언트 객체를 생성하여 connection을 새로 붙었다가 끊어주는 방식으로 변경하여 해결하였다. 아마 장기적으로는 커넥션 풀링을 할 수 있도록 랩핑하는 라이브러리를 직접 만들어야할지도 모르겠다.

이러한 삽질을 거치고, 현재 모든 트래픽을 SQS에서 Beanstalkd를 통해 처리되도록 변경하여 수 일 째 운용중인데 매우 안정적이다. 조금 더 지켜봐야 알겠지만 지금으로써는 안정성과 비용 모두를 다 잡은듯하다. (c4.2xlarge에서 운용하는데 CPU 왕 쪼금 먹음. Scale-Down 해도 될 것 같다.) 차후 진행해야할 (필수)태스크는 Queue가 밀렸을 때 Consumer(Worker)를 기존에 SQS를 사용했을 때와 같이 AWS Auto Scaling에서 자동으로 Scale-Out/In 할 수 있도록 만들어야 하는 것이다. 끝~

Share on : Twitter, Facebook or Google+