열다섯번째 날: 자랑스러운 우리 회사의 숨은 일꾼 만들기

저자

@y0ngbin - aka 용사장 / Minivelo++ / 맞춤법 전문가

시작하며

첫눈이 내리고, 어김없이 캐럴이 울려퍼지면, 거리에는 삼삼오오 소중한 사람들과 함께 즐거운 시간을 보내는 사람들로 북적거리는 연말이 찾아온 것을 느낄 수 있습니다. 하지만 모두가 즐거운 시간을 보내는 이 순간에도 원활한 서비스를 위해 누군가는 어딘가에서 묵묵히 일을 해야만 합니다. 크리스마스이브에 야근을 해야 하는 그 누군가가 여러분이 되지 않기 위해서는 그 일을 맡길 다른 사람을 얼른 찾아야 합니다.

Gearman은 이런 우리의 고민을 해결해 줄 수 있는 근사한 해결책 중에 하나입니다. Gearman은 여러분이 작성한 일꾼을 밤낮으로 감시하며 언제든 원하는 일을 처리할 수 있는 환경을 제공해 줍니다. 일꾼은 항상 시킨 일을 묵묵히 할 뿐만 아니라 필요하면 언제든지 일꾼의 수를 늘리거나 줄일 수 있습니다. '일꾼1'과 '일꾼2'가 잡 큐 작업장에서 만나 비밀 사내 연애 끝에 결혼을 해 '일꾼3'을 출산하게 되어 육아휴직을 주어야 할 일도 없고, 임금인상이나 퇴직금을 요구하지도 않으며, 노조를 결성해서 파업할 걱정도 없으므로 회사를 경영하는 처지에서는 더할 나위 없이 좋은 대안입니다.

그리고 Gearman과 같은 잡 큐 방식의 구조를 빌릴 경우, 시간이 오래 걸리는 작업에 대해 병목이 되는 지점에 추가적인 작업을 더 할당해서 그 일을 병렬로 처리해 작업의 처리 효율을 높힐 수도 있고, 때로는 '서비스1'과 '서비스2'에서 반복적으로 사용하는 공통 기능 X를 각각의 서비스마다 중복해 구현하지 않고 외부에 두어 구조를 개선할 수도 있습니다.

Gearman 소개

Gearman은 memcached, MogileFS으로 유명한 Danga Interactive에서 개발한 제품 중에 하나로 유명한 펄 해커인 Brad Fitzpatrick 씨가 제작했습니다. 초창기 Gearman은 순수하게 펄로 작성되었고 클라이언트 라이브러리도 주로 펄만 고려했습니다. 하지만 이후 Eric Day 씨가 Gearman을 C로 재작성하고 타 언어에 대한 클라이언트 라이브러리 지원 등을 강화하면서 지금의 Gearman은 언어 중립적이고 포괄적인 분산 프로세스 플랫폼으로 발전했습니다. 현재 Gearman의 워커로 등록시킬수 있는 언어는 C와 Perl, Python, PHP, Java, Go, MySQL의 UDF(User Defined Function)가 있습니다. 따라서 Gearman을 사용하면 Java로 작성한 워커를 Perl로 작성한 응용프로그램에서 사용하거나 그 반대의 상황을 만드는 것이 아주 쉬워집니다.

Gearman 구성요소

Gearman을 이해하기 위해서는 먼저 Gearman을 구성하고 있는 요소들의 특징과 역할을 이해할 필요가 있습니다.

Gearman 서버는 클라이언트에서 전달받은 작업을 적절한 워커로 분배하는 역할을 합니다. 초기 Gearman은 작업을 따로 저장하지 않고 메모리에서 처리했지만 현재는 다양한 영속 저장방법을 지원하고 있습니다. (libmemcached, libdrizzle, SQLite, Mysql, Postgres, tokyocabinet, Redis, Mongodb) Gearman 서버는 멀티서버를 지원하기 때문에 단일 장애지점(Single Point of Failure)을 피해서 구성할수 있습니다. 현재 사용할 수 있는 Gearman 서버 구현체는 C로 작성된 gearmand과 펄로 작성된 Gearman::Server가 있습니다.

Gearman 워커는 실제 우리가 원하는 작업을 수행하는 프로세스입니다. 위에 언급한 것처럼 다양한 언어로 작성할 수 있습니다. Gearman 워커의 구현체는 펄로 구현된 Gearman::Worker과 C로 구현된 libgearmand, 그리고 libgearmand를 사용해 구현된 Gearman::XS::Worker가 있습니다.

Gearman 클라이언트는 워커에게 작업을 시키는 프로세스입니다. 마찬가지로 다양한 언어로 작성될 수 있습니다. C로 작성된 libgearmand와 펄로 구현된 Gearman::Client가 있으며, libgearmand를 사용해 구현된 Gearman::XS::Client가 있습니다.

Gearman 서버 및 환경 구성하기

Gearman의 동작방식을 이해하기 위한 간단한 예제를 살펴보겠습니다.

먼저 CPAN을 통해 간단하게 Gearman::Server를 설치하고 기동해 봅시다. 현재 활발하게 개발되고 다양한 기능을 갖추고 있는 것은 C로 구현된 gearmand입니다. 지금은 간단한 테스트를 하기 위해 순수하게 펄로만 구현된 Gearman::Server를 사용해 보겠습니다.

$ cpanm -n Gearman::Server Gearman::Worker Gearman::Client
$ gearmand

Gearman::Server를 설치하고 나면 gearmand라는 CLI 도구가 설치됩니다. gearmand를 실행하면 127.0.0.1에 7003번 포트로 Gearman 서버가 구동됩니다.

Gearman 클라이언트 작성

다음으로 워커를 작성하는 대신, 워커를 이용하는 클라이언트 코드를 먼저 작성해 보겠습니다. 이렇게 순서를 바꿔서 진행하는 이유는 조금더 Gearman의 동작 방식을 잘 이해하기 위해서 입니다.

#!/usr/bin/env perl
use Gearman::Client;
use Storable qw( freeze );
my $client = Gearman::Client->new;
$client->job_servers('127.0.0.1:7003');

my $tasks  = $client->new_task_set;
my $handle = $tasks->add_task(
    sum => freeze( [ 3, 5 ] ),
    {
        on_complete => sub { print ${ $_[0] }, "\n" }
    }
);
$tasks->wait;

위 코드는 sum이라는 이름의 워커에게 35를 인자로 전달하고 작업이 끝나면 처리 결과를 화면에 출력하는 간단한 클라이언트입니다. Gearman은 언어 중립적이고 펄과 무관하므로 클라이언트와 워커 사이에 인자를 전달하기 위해서는 Stroable이나 JSON과 같은 마샬링 처리가 필요합니다. 이 코드를 실행하면 프로세스가 종료되지 않고 계속 대기하는것을 볼 수 있습니다. 현재 클라이언트가 처리 요청한 sum 워커가 존재하지 않기 때문에 적절한 워커가 생성될 때까지 작업 요청이 대기하는 것입니다.

Gearman Worker의 작성

자 이제 sum이라는 일을 처리해줄 워커를 만들어봅시다.

#!/usr/bin/env perl
use Gearman::Worker;
use Storable qw( thaw );
use List::Util qw(sum);

my $worker = Gearman::Worker->new;
$worker->job_servers('127.0.0.1:7003');
$worker->register_function( sum => sub { sum @{ thaw( $_[0]->arg ) } } );
$worker->work while 1;

워커를 만들기 위해서는 Gearman::Worker 객체를 사용합니다. 먼저 워커를 등록할 적절한 Gearman 서버를 지정합니다. 그리고 register_function() 함수로 워커를 등록합니다. sum 워커는 List::Utilsum() 함수를 제공합니다. 위에서 설명한 것처럼 클라이언트에서 인자를 Storable로 직렬화(Serialize)했기 때문에 워커도 마찬가지로 Storablethaw() 함수를 통해 언마샬링하고 있습니다. 이 코드를 실행시키면 거의 동시에 앞서서 실행했던 클라이언트 쪽 프로세스가 8이라는 결과를 내고 종료합니다. 이 코드가 실행되는 순간 잡 서버에는 sum 워커가 등록되고 기존에 대기중이었던 작업이 할당되고 처리되면서 클라이언트 요청이 성공적으로 끝나기 때문입니다.

이 워커 코드를 한대의 장비에서 여러 개 띄울 수도 있고, 심지어 분리된 장비에 나눠서 띄울 수도 있습니다. 어떤 방식으로든 워커를 잡 서버에 등록하기만 하면 그다음부터 클라이언트에서 워커가 어디에 어떻게 떠있는지 알 필요 없이 단순하게 요청하고 결과를 받으면 잡 서버가 알아서 작업 분배를 합니다.

Gearman 실전

마지막으로 개인적으로 Gearman을 활용해 문제를 해결했던 상황을 간단하게 소개하고 오늘의 기사를 정리하겠습니다.

당시 1만 개 정도의 웹페이지를 긁어온 뒤 필요한 자료를 추출하고 저장하는 작업이 있었습니다. 먼저 그 작업을 평범한 스크립트로 작성해서 실행시켜 본 결과 주로 로컬의 CPU와 메모리를 소모하는 자료의 추출 및 저장 프로세스보다 외부의 네트워크 자원을 소모하고 대역폭의 영향을 받는 웹페이지 추출 작업이 대부분 시간을 소모하는 병목지점임을 알게 되었습니다.

이 프로세스의 속도를 개선하기 위해서 Gearman을 사용하기로 하고 다음과 같이 진행했습니다. 먼저 기존에 작성했던 스크립트에서 혼재되어있는 코드를 각각의 작업영역별로 함수단위로 정리하고 예전과 같게 동작하도록 수정했습니다. 그래서 4개의 함수로 list_fetch(), detail_fetch(), store_page(), parse_page()가 만들어졌습니다.

각각의 함수를 별도의 파일로 분리하고 Gearman::XS::Worker를 사용해 워커로 등록했습니다. 앞서 분석한 결과에 따라 주요 병목지점인 list_fetch() 워커를 2개, detail_fetch() 워커를 6개 실행하고, 나머지 store_page(), parse_page() 워커는 하나씩만 실행했습니다.

작업이 순차적으로 진행될 필요가 있는 경우, 즉 list_fetch()한 페이지를 통해 복수 페이지에 대한 detail_fetch() 요청이 생성되고 그 결과에 대해 parse_page()store_page() 요청이 생기는 경우에는 Gearman 워커 코드 내에 Gearman 클라이언트 코드를 삽입해 작업이 연쇄적으로 일어날 수 있도록 조정했습니다.

C로 구현된 gearmand를 컴파일해 설치하고 작업의 저장은 로컬 SQLite를 사용했으며 Perl 워커와 클라이언트는 각각 Gearman::XS::Client, Gearman::XS::Worker를 사용해 작성했습니다. 결과적으로 병목지점이 되던 네트워크 사용 부분이 해소되면서 전체 실행시간이 70% 정도 개선되었습니다. 당시 사용했던 코드는 github에 올려두었으니 참고하시기 바랍니다.

정리하며

처음 Gearman을 접한다면 서버와 워커, 클라이언트가 분리되어있는 구조가 조금은 복잡하고 불필요하게 느껴질 수도 있지만 단순하게 매번 반복적으로 실행되는 어떤 프로세스를 재사용 될 수 있도록 구조화하는데 있어서 Gearman의 처리방식은 합리적이고 이점이 많습니다. 만약 아직도 단순하고 반복적인 업무를 매번 일일이 실행시키며 귀중한 시간을 낭비하고 있다면 올겨울 Gearman과 그의 충실하고 믿음직스러운 일꾼들에게 그 일을 맡겨보는 것은 어떨까요?

참고자료

blog comments powered by Disqus