ruby fcgi 모듈의 keepalive 문제

ruby on rail을 현재 운영하고 있는 서비스 개발에 적용하는 것을 검토하고 있다.  현재 서비스는 자바로 개발되어 있고 이 기존 서비스에 rail로 개발한 서비스를 융합하기 위해서 자바 서버의 fast cgi 연동 기능으로 rail을 운영하는 것을 시도해 보고 있다.

문제는 ruby의 fast cgi 모듈(ruby-fcgi)이 fast cgi의 keep alive 연결을 제대로 처리하지 못한다는 점이다.  웹 페이지 요청이 첫번째는 제대로 처리되나 다음번 요청은 처리가 되지 않는다.  단 하나의 클라이언트로만 테스트해 보면 홀수번째 요청은 제대로 처리되고 짝수번째 요청은 처리가 되지 않는다. 여러 클라이언트로 동시에 요청을 날려보면 반반씩 처리가 되었다가 안되었다 하게 된다.  웹 서버가 keep alive 모드로 fast cgi 연결을 하면 첫 번째 요청만 제대로 처리하고 연결이 유지되어 있는 connection을 통해 들어온 요청은 처리하지 않기 때문이다.  웹 서버의 fast cgi 구현에 따라 이 문제는 발생할 수도 있고 발생 안 할 수도 있다. lighttpd나 apache와 같은 경우는 이런 문제가 발생하지 않는다.

ruby-fcgi 모듈은 fastcgi c 라이브러리를 랩핑한 것이다.  이 모듈은 하나의 fcgi 요청을 ruby 객체로 다룬다.  웹 프로그램에서는 이 객체를 통하여 값을 얻거나 쓰면 된다.  보통 다음과 같은 식으로 프로그래밍하는데

FCGI.each do |request|
    out = request.out
    out.print "Content-type: text/html\r\n\r\n"
    out.print "hello"
    request.finish
end

여기서 each singleton 메서드는 내부적으로는 accept 메서드를 호출해서 객체를 얻고 yield하는 동작을 한다.  실제로 이렇게 구현되어 있지는 않고 c로 구현되어 있지만 만약 ruby로 구현한다면 다음과 같은 식이다.

def each
    while request = accept
       yield request
    end
end

accept 메서드는 fcgiapp api의 FCGX_Accept 함수를 이용하여 구현되어 있다.  코드는 대충 다음과 같다.  변수 선언과 에러 처리, 각종 설정 적용들은 빼고 기본 동작만 추렸다.

static VALUE fcgi_s_accept(VALUE self)
{
  FCGX_Request * req = ALLOC(FCGX_Request);
 
  FCGX_InitRequest(req, 0, 0);

  FD_ZERO(&readfds);
  FD_SET(req->listen_sock, &readfds);
  rb_thread_select(req->listen_sock+1, &readfds, NULL, NULL, NULL);

  FCGX_Accept_r(req);
  obj = Data_Make_Struct(self, fcgi_data, fcgi_mark, fcgi_free_req, data);
  data->req = req;
  data->in  = Data_Wrap_Struct(cFCGIStream, 0, 0, req->in);
  data->out = Data_Wrap_Struct(cFCGIStream, 0, 0, req->out);
  data->err = Data_Wrap_Struct(cFCGIStream, 0, 0, req->err);
  data->env = rb_hash_new();

  env = req->envp;
  for (; *env; env++) {
    int size = 0;
    pkey = *env;
    pvalue = pkey;
    while( *(pvalue++) != '=') size++;
    key   = rb_str_new(pkey, size);
    value = rb_str_new2(pvalue);
    OBJ_TAINT(key);
    OBJ_TAINT(value);
    rb_hash_aset(data->env, key, value);
  }
  return obj;
}

FCGX_Accept 함수는 accept call과 모양과 동작이 비슷하긴 하지만 내부적인 동작은 다르다.  중요한 것은 FCGX_Accept를 호출한다고 해서 매번 그 안에서 accept가 일어나지는 않는다는 거다.  keep alive를 처리하기 위해서 그런 것인데 접속이 없다면 accept를 호출해서 접속을 받고, 접속이 유지되고 있다면 그 접속을 이용해서 메타 데이터(cgi의 경우 환경 변수로 넘어오는 값)를 읽은 후 리턴하게 된다.  값을 읽기 전에 이전에 처리 중에 있던 request는 finish를 하는 작업도 한다(FCGX_Finish 함수 호출)

그런데 ruby-fcgi는 FCGX_Accept가 accept 함수를 매번 호출한다고 생각해서인지 listening하고 있는 서버 소켓을 select 호출을 통해 read ready가 된 후(서버 소켓이므로 accept가 들어온 후)에나 FCGX_Accept를 호출하고 있다.  이렇게 되면 keep alive 모드로 연결되어 접속이 계속 유지되어 있는 연결로 보낸 요청은 처리가 되지 않는다.

또한 FCGX_Request 구조체의 데이터를 매번 accept 메서드 안에서 메모리를 할당하고 초기화를 하고 있다. 이 구조체 안에 연결이 유지되고 있는 소켓의 descriptor를 저장해 두고 keep alive를 처리하고 있기 때문에 이 구조체를 request 단위로 생성, 초기화, 소멸해서는 안 되며 한 스레드 단위로 생성, 초기화, 소멸해야 한다.  fcgi의 스레드 예제에도 스레드 단위로 FCGX_Request 객체를 생성시켜 계속 사용하고 있다.

따라서 ruby-fcgi 모듈에서 keep alive 연결을 제대로 동작할 수 있게 하려면 select 호출을 빼야 하고, FCGX_Accept 구조체 변수를 스레드 단위로 관리할 수 있게 바꿔야 한다.  보통 ruby-fcgi 모듈을 멀티 스레드 환경으로 운영하지는 않으니까 모듈 시작시에 초기화해서 계속 공유하는 방식으로 구현하면 될 것이다(fcgi의 스레드를 사용하지 않는 예제들은 모두 이런 방식이다).

대충 수정을 해서 테스트해 보니 동작은 잘 되었다.  제작자에게 패치를 보냈는데 받아들여질지 어떨지는 모르겠다(안 되는 영어로 보냈으니 제대로 내용이 전달되었는지 모르겠다).

by Corund | 2007/03/26 11:33 | 트랙백

◀ 이전 페이지다음 페이지 ▶