2008년을 보내며

December 22nd, 2008

작년 이맘때, '성공한 느낌'을 가지며 2008년을 마무리할 수 있기를 바란다는 작은(?) 소망을 이야기한 적 있다.

그리고 1년이 정말 번개처럼 지나가버렸다. 1년동안 나는 무엇을 얻고 무엇을 잃은 것일까?

 

스프링노트와_함께하는_크리스마스~.jpg

 

자기 반성(+비판) 차원에서 글을 쓰기 시작했지만, 크리스마스 카드를 계속 보고 있으니 마음이 따뜻해진다.

2009년에는 항상 긍정적인 아우라로 주변을 변화시킬 수 있는 내가 되기를...

 

Happy Christmas!

 

GTD(Getting Things Done) 플로우를 보면 이런 부분이 있다.

 

그림_13.png

그림 출처: 3 more GTD wallpapers!

 

일을 할 때는 다음 작업을 찾아서, 2분안에 할 수 있으면 당장 해버리고 그렇지 않으면 미루거나 위임하라는 것이다. 이런 원칙은 빠른 응답이 생명인 요즘 웹 개발에도 똑같이 적용된다. 오래 걸릴만한 일은 미루고 일단 사용자에게 응답을 보내라는 것이다.

 

약간 억지스러웠지만, 어쨌든 웹 애플리케이션을 개발할 때 비동기 작업이 필요한 경우는 꽤 많다. 예를 들어 스프링노트처럼 내 데이터 전체를 내려받게 해주는 기능이 있고 해보자. 이 기능은 오래 걸리기도 하지만, DB 쿼리도 많고, 압축이라도 한다면 CPU, Disk를 많이 소모하는 무거운 기능이다. 이런 기능은 비동기로 처리하는게 좋다.

 

비동기로 작업이 떠오르는 경우는 이럴때다.

 

  • '짧은 시간'안에 끝날 것이라는 보장할 수 없는 작업
  • 너무 많은 시스템 리소스를 소비하는 무거운 작업

 

특히나 레일스 애플리케이션은 비동기 작업이 더 절실한데.. 그 이유는 몽그렐 클러스터가 필요한 이유에서 찾아보기 바란다.

 

당신의 선택은?

비동기 처리를 위해 사용할 수 있는 솔루션은 너무나도 많고 다양하다. 많이 언급되는 것들 중 몇가지를 선택해서 테스트한 후 고르는 것이 좋겠다. 아니면 일반화해서 나중에 플러그인만 바꿔 끼울 수 있도록 하는 것도 좋은 접근이다!

 

그림_14.png

<그림> 선택이 너무 많다. 출처는 Handling Long-Running Tasks in Rails

 

내가 솔루션을 선택하는 방법은 일단 내가 가진 요구사항을 충족하는 '가장 가벼운' 것을 고르는 것이다. 혹시 나중에 필요할지 모르는 기능은 머릿속에서 지운다. 그리고 나중에 진짜 그 기능이 필요해지면, 그 때 고민해도 좋다. 그런 근거로 내가 선택한 것은 Shopify의 일부로 개발되었다는 Delayed Job(DJ)이다. github팀의 이 글에도 큰 영향을 받았다. 이 팀에서 DJ를 선택한 이유를 아래처럼 설명하고 있다.

 

It is simple, required no research beyond the short README, works wonderfully with Rails, is fast, is hackable, solves both the queue and the worker problems, and has no external dependecies. Also, it’s hosted on GitHub!

 

Delayed Job 설정 및 사용법

DJ는 DB를 Job Queue로 사용하므로, 아래처럼 마이그레이션을 수행해줘야 한다.

 

  1. create_table :delayed_jobs, :force => true do |table|
      table.integer :priority, :default => 0
      table.integer :attempts, :default => 0
      table.text :handler
      table.string :last_error
      table.datetime :run_at
      table.datetime :locked_at
      table.datetime :failed_at
      table.string :locked_by
      table.timestamps
    end

 

그리고 Job 객체를 만들어 위 테이블에 직렬화해서 넣어준다.

 

  1. class NewsletterJob < Struct.new(:text, :emails)
      def perform
       emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) }
      end
    end

 

큐에 작업을 넣는 코드는 아래와 같다.

 

  1. Delayed::Job.enqueue NewsletterJob.new('lorem ipsum...', Customers.find(:all).collect(&:email))

 

이게 DJ 사용법의 거의 전부다. 별도의 S/W를 설치할 필요가 없고, 사용법이 간결한 것이 가장 큰 장점이다.

 

Job Runner

이제, Job Queue에 담긴 내용을 꺼내서 실행하는 job_runner를 만들어준다. 아래와 같은 내용으로 script/job_runner를 작성한다.

 

  1. #!/usr/bin/env ruby

    unless ARGV.size == 2
      $stderr.puts "USAGE: #{$0} environment pid_file_path"
      exit 1
    end

    RAILS_ENV = ARGV[0]
    File.open(ARGV[1], 'w+') {|f| f.write("#{$$}") }

    require File.dirname(__FILE__) + '/../config/environment'
    Delayed::Worker.new.start

 

monit을 이용한 모니터링과 배포

44570084_5be779d27d_m.jpg이 작업을 하기전에 가장 궁금했던 것이, 어떻게 job_runner를 관리하는가 였다. 여기서 관리라 함은,

 

  • 배포 후 최신 버전을 띄운다
  • job_runner가 잘 돌아가고 있는지 모니터링 한다.

 

먼저 job_runner를 구동하는 컨트롤 프로그램을 작성한다. /bin/job_runner_ctl이다.

 

  1. #!/bin/bash
    cd /path/to/app

    if [ "$1" == "start" ]; then
      nohup /usr/bin/ruby script/job_runner production /path/to/log/job_runner.pid &
    else
      ps ux | awk '/script\/job_runner/ && !/awk/ {print $2}' |xargs -i kill {}
      rm /path/to/log/job_runner.pid
    fi

 

그리고 job_runner_ctl을 monit에 등록한다.

 

  1. check process job_runner with pidfile /path/to/log/job_runner.pid
      start program = "/bin/job_runner_ctl start"
      stop program = "/bin/job_runner_ctl stop"
     
      if totalmem is greater than 1024.0 MB for 5 cycles then restart # eating up memory?
      if cpu is greater than 50% for 2 cycles then alert # s an email to admin
      if cpu is greater than 80% for 3 cycles then restart # hung process?
      if loadavg(5min) greater than 10 for 8 cycles then restart # bad, bad, bad
      if 3 restarts within 5 cycles then timeout # something is wrong, call the sys-admin

 

마지막으로 할 일은 Capistrano의 restart 작업에 job_runner를 재시작하는 명령을 추가하는 일이다.

 

  1. desc "restart web application servers"
    task :restart, :roles => :app do
      # (..omitted..)
      run "monit restart job_runner"
    end

 

마치며

꽤 걱정을 했는데, 그에 비해 별 문제없이 Delayed Job을 서비스에 적용할 수 있었다. 조금 더 대용량을 다루는 사이트가 되면 그때는 Beanstalkd, starling, ActiveMQ 등을 고려해야할까? 일단은 defer it!

 

 

지난 글 변하지 않는 것은 없다. 이제 대세는 Passenger(aka. mod_rails)에서 말한대로 Passenger가 현재 시점에서 가장 추천할만한 환경임에 변함이 없다. 게다가 최근 Global Queuing 기능이 들어갔다는 소식은 기존 애플리케이션도 Passenger로 옮겨타는 것을 고려하도록 유혹한다.

 

하지만, 이런 저런 이유로 아직도(?) 몽그렐을 사용하고 있는 곳을 위해 몇가지 팁을 공유한다.

 

이벤트 머신을 활용한 Swiftiply

루비 1.8(MRI)의 경우, 설계상의 이유(그린 쓰레드) 때문에 이벤트 기반 몽그렐의 성능이 더 낫다. 단순히 swiftiply 젬을 설치하고 환경변수만 설정하면 되는 문제이므로, 이벤트 기반 몽그렐을 사용하는게 더 낫다. 안정성 문제는 전혀 없는 것으로 알려져있다.

 

HAProxy를 이용한 글로벌 큐잉(Global Queuing)

보통 사용하는 HTTP 서버에서 제공하는 로더 밸런서(예를들어, 아파치라면 mod_proxy_balancer)를 사용하지만, HAProxy를 이용한 글로벌 큐잉을 도입히는게 낫다.

 

  1.     server myapp_3000 127.0.0.1:3000 check inter 60000 minconn 1 maxconn 1

 

위처럼 HAProxy의 설정에서 maxconn을 1로 제한하면, HAProxy가 요청을 큐잉하면, 몽그렐에는 한번에 하나의 요청만을 보낸다. 약간 더 자세한 설명은 스프링노트 배포 환경 Before & After: Capistrano, God, HAProxy, Seesaw!을 참고하면 좋겠다.

 

현 상태를 신고받는 mongrel_proctitle

mongrel_proctitle 젬을 설치하면 현재 처리하고 있는 요청에 대한 정보를 프로세스 타이틀로 표시해준다. 이런 식이다.

 

  1. mongrel_rails [10010/2/358]: handling 127.0.0.1: HEAD /feed/cal/global/91/6de4

 

여기 담긴 정보는 왼쪽부터 보자면 다음과 같다.

 

  • The port that Mongrel is serving
  • Requests currently queued/being processed concurrently
  • Requests processed during server lifetime
  • What it's doing
  • The client IP
  • The current req (method / path)

 

간단한 아이디어에서 시작했지만, 문제를 찾는데 걸리는 시간을 꽤 단축해주는 유용한 녀석이기도 하다.

 

참고로, Evented Mongrel의 경우는 mongrel_proctitle이 동작하지 않는다. mongrel_proctitle.rb 파일을 열어 아래 코드를 추가해주자.

 

  1.     def dispatch_to_handlers_with_proctitle(handlers,request,response)
          unless @handler
            @handler = ProctitleHandler.new(@titler)
            register("/", @handler, true)
          end
          @titler.request do
            return dispatch_to_handlers_without_proctitle(handlers,request,response)
          end
          end 
          alias_method :dispatch_to_handlers_without_proctitle, :dispatch_to_handlers
        alias_method :dispatch_to_handlers, :dispatch_to_handlers_with_proctitle
      end 

 

참고

 

Leah Culver씨가 공개한 파이썬 라이브러리를 이용하면 어렵지 않게 OAuth 컨슈머를 구현할 수 있다.

(하지만 라이브러리라기보다는 예제 코드에 가까워 사용하기 조금 불편한 API라는 느낌이다.)

 

이 코드에서 제공하는 예제를 살짝 수정해 스프링노트에 접속해서 페이지 하나를 가져오는 OAuth 컨슈머 예제 코드를 만들었으니 필요하다면 참고하시길.

 

  1. # setup
    client = SpringnoteClient(CONSUMER_TOKEN, CONSUMER_SECRET)

    # get request token
    token = client.fetch_request_token()
    print 'please visit %s in your browser and press any key.' % client.authorize_url(token)

    # get access token
    token = client.fetch_access_token(token)

    # access some protected resources
    print client.get_page(144, 'deepblue')

 

전체 소스 코드는 여기 있다.

조만간 파이썬용 스프링노트 클라이언트 라이브러리를 만들어 제공할 예정이다(이 글의 주인공에게 제작 의뢰했음 ^^).

 

RubyQuiz를 풀며 Code Beautifier를 간단하게 구현해보려다가, 약간 삼천포로 빠졌다. 그치만 그 과정에서 사용하면 즐거운 도구들을 만났다. 그 친구들을 소개하고, 어떻게 활용할 수 있을지 고민해보자.

 

RubyInline

RubyInline은 루비 코드 안에서 다른 언어로 만든 코드를 적을 수 있는 라이브러리다. 아래 예제는 루비 코드가 C 코드를 포함하고 있다.

 

  1. class Hello
      inline do |builder|
        builder.include "<stdio.h>"
        builder.c 'void hello() { puts("hello world"); }'
      end
    end

 

위 파일을 실행하면 ~/.ruby_inline 디렉토리에서 C 코드를 컴파일하여 수행한다. 느리기로 유명한 루비에서 속도가 중요한 일을 할 때, 또는 다른 언어로된 코드와의 연동을 위해 사용할 수 있다.

 

ParseTree

ParseTree는 RubyInline을 이용해 루비 코드의 Parse tree를 추출하여 s-expression으로 반환한다. 예를 들면 이런 식이다.

 

  1.   def conditional1(arg1)
        if arg1 == 0 then
          return 1
        end
        return 0
      end

 

이 코드를 변환하면 아래와 같다.

 

  1.   [:defn,
        :conditional1,
        [:scope,
         [:block,
          [:args, :arg1],
          [:if,
           [:call, [:lvar, :arg1], :==, [:array, [:lit, 0]]],
           [:return, [:lit, 1]],
           nil],
          [:return, [:lit, 0]]]]]

 

<그림> 1+1을 나타내는 ParseTree

 

Ruby2Ruby

Ruby2Ruby는 ParseTree가 반환하는 s-expression을 이용해 다시 루비 코드를 생성해낸다. ruby2ruby를 이용하면 동적으로 언어를 다루는 일을 쉽게 할 수 있다. 

 

좋은 예 중 하나는 내가 루비세미나에서 소개한 적이 있는 Heckle이다. Heckle은 코드의 특정 부분을 Ruby2Ruby를 이용해 변경하고, 그 때 테스트가 깨지는지 확인하는 도구다. 물론 깨져야 테스트를 촘촘하게 잘 만든 것이다.

 

또, Ruby2Ruby를 이용하면 Code Beautifier를 정말 간단하게 구현할 수 있다. 

 

  1. sexp = ParseTree.new.parse_tree_for_string($stdin.read).first
    puts Ruby2Ruby.new.process(sexp)

 

위 코드가 전부다.

 

  1. #!/usr/bin/ruby -rcgi
    H,B=%w'HomePage w7.cgi?n=%s';c=CGI.new'html4';n,d=c['n']!=''?c['n']:H,c['d'];t=`
    cat #{n}`;d!=''&&`echo #{t=CGI.escapeHTML(d)} >#{n}`;c.instance_eval{out{h1{n}+
    a(B%H){H}+pre{t.gsub(/([A-Z]\w+){2}/){a(B%$&){$&}}}+form("get"){textarea('d'){t
    }+hidden('n',n)+submit}}}

 

이런 요상한 코드가 있으면 아래처럼 읽을 수 있는 코드로 바꿔준다.

 

  1. H = , B =  = ["HomePage", "w7.cgi?n=%s"]
    c = CGI.new("html4")
    n, d = (not (c["n"] == "")) ? (c["n"]) : (H), c["d"]
    t = `\ncat #{n}`
    ((not (d == "")) and `echo #{t = CGI.escapeHTML(d)} >#{n}`)
    c.instance_eval do
      out do
        (((h1 { n } + a((B % H)) { H }) + pre { t.gsub(/([A-Z]\w+){2}/) { a((B % $&)) { $& } } }) + form("get") { ((textarea("d") { t } + hidden("n", n)) + submit) })
      end
    end

 

ParseTree와 Ruby2Ruby를 이용해 또 어떤 재미있는 일을 할 수 있을까? 

 

Forbidden Fruit: A Taste of Ruby's ParseTree

Goruko2008에 이와 관련된 재미있는 발표가 있으니 한번 확인해보자 . 아래는 루비 코드를 SQL로 바꿔주는 참신한 ORM인 Ambition을 공개한 Chris Wanstrath의 발표자료를 보자. 동영상도 공개되어 있다.

 

Chris는 ParseTree와 Ruby2Ruby를 이용해서 이런 일을 한다.

 

  • mapreducerb: RingyDingy를 이용한 간단한 분산 처리
  • sake: rake 코드를 만들어내기 위해 ruby2ruby를 사용한다.
  • Ambition: 루비 Enumerable을 이용한 코드를 SQL, ActiveRecord 코드, LDAP 쿼리 등으로 변환한다.

 

다시 한번 생각해보자. 또 어떤 재미있는 일을 할 수 있을까? 

 

프로덕션에서도 사용가능하다: merb의 예

성능을 떨어뜨리는 코드는 버그라는 철학을 가진 Merb에서도 ruby2ruby를 사용한다. merb-action-args가 그 예인데 요청의 쿼리 파라메터를 컨트롤러의 액션 메서드의 파라매터로 매핑해주는 역할을 한다. def bar(baz) 라는 액션이 있고, "/foo/bar?baz=bat"라는 요청이 들어오면 foo("bat")을 호출해주는 식이다. 조금 더 자세한 설명은 이 링크에서 확인할 수 있다.

 

이 사용예에서의 핵심은 특정 루비 메서드의 파라메터 목록을 런타임에 찾아내는 아래 코드다.

 

예를 들어, 아래같은 Example 클래스가 있다고 하자.

 

  1. class Example
      def hello(one,two="two",three)
      end

      def goodbye
      end
    end

 

get_args 메서드를 사용하면 메서드의 파라메터와 기본값을 알 수 있다.

 

  1. Example.instance_method(:hello).get_args
    #=> [[:one], [:two, "two"], [:three, "three"]]

    Example.instance_method(:goodbye).get_args 
    #=> nil

 

구현은 아래와 같다.

 

  1.   def get_args
        klass, meth = self.to_s.split(/ /).to_a[1][0..-2].split("#")
        # Remove stupidity for #<Method: Class(Object)#foo>
        klass = $` if klass =~ /\(/
        ParseTreeArray.translate(Object.const_get(klass), meth).get_args
      end

 

merb-action-args는 구동시에 Merb::AbstractController를 상속받는 모든 클래스의 메서드에 대해 파라매터 정보를 구축해놓고 있다가, 디스패치 작업에 이용한다.

 

메타프로그래밍의 재미

메타프로그래밍은 조금 더 일을 잘하기 위한 노력이다. 약간이라도 읽기 쉬운 코드, 반복을 최대한 줄이고 효율적인 작업이 가능하려면 어떻게 해야할까?를 고민하다보면 어느새 메타프로그래밍에 푹 빠지게 된다. 루비는 메타프로그래밍을 자연스럽게 할 수 있는 언어다. 그리고 다양한 예제와 프랙티스, 그리고 도구들이 있다. 남은 것은 코드를 조금 더 잘 만들려는 개발자들의 작은 열정과 틀을 깨는 상상력이 아닐까 싶다.

 

참조

 

 

<< 이전 글