2007년 03월 26일
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 객체로 다룬다. 웹 프로그램에서는 이 객체를 통하여 값을 얻거나 쓰면 된다. 보통 다음과 같은 식으로 프로그래밍하는데
여기서 each singleton 메서드는 내부적으로는 accept 메서드를 호출해서 객체를 얻고 yield하는 동작을 한다. 실제로 이렇게 구현되어 있지는 않고 c로 구현되어 있지만 만약 ruby로 구현한다면 다음과 같은 식이다.
accept 메서드는 fcgiapp api의 FCGX_Accept 함수를 이용하여 구현되어 있다. 코드는 대충 다음과 같다. 변수 선언과 에러 처리, 각종 설정 적용들은 빼고 기본 동작만 추렸다.
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의 스레드를 사용하지 않는 예제들은 모두 이런 방식이다).
대충 수정을 해서 테스트해 보니 동작은 잘 되었다. 제작자에게 패치를 보냈는데 받아들여질지 어떨지는 모르겠다(안 되는 영어로 보냈으니 제대로 내용이 전달되었는지 모르겠다).
문제는 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 | 2007/03/26 11:33 | 트랙백
2007년 03월 16일
Rake를 사용해 보고
이제까지 자바 프로젝트에서 빌드 툴로 ant를 사용해왔다. 그러나 ant만으로는 빌드를 유연하게 구성하는 게 복잡했다. xml을 사용하기 때문에 코드도 복잡하고 기본적인 제어 구문도 사용하기 복잡했다. ant 안에서 스크립트를 실행시킬 수도 있으나 배보다 배꼽이 커지는 격이다.
그래서 이제까지 빌드 스크립트는 ant를 이용해서 compile, jar pack, unit testing을 하고 그 외의 부분(파일 조작, 원격 전송 등)은 셸 스크립트를 이용해왔다. 셸 스크립트로 충분하긴 했지만 좀더 간결하고 강력한 기능이 아쉬울 때도 있었다. 그러다 만난 것이 rake였다.
rake는 ruby 언어를 이용한 빌드 툴이다. ant처럼 xml을 쓴다던가 또는 특별한 종류의 문법을 이용해서 빌드 스크립트를 작성하지 않고 ruby 언어를 그대로 이용해서 빌드 스크립트를 작성한다. 다양하게 변신할 수 있는 ruby 언어의 또 다른 변신 모습을 볼 수 있다.
rake를 쓰기 위해서는 Rakefile을 작성해야 한다(마치 make를 위해 Makefile을 작성하는 것처럼). 이 Rakefile을 ruby 언어로 작성하는데 전용 클래스와 메서드를 이용하고 코딩 스타일을 조금 다르게 해서 작성하므로 전체적인 모양새는 Makefile과 비슷하다.
rake에서 make나 ant의 target에 해당하는 것이 task이다. task에는 task, file, directory, rule이 있는데 이들은 모두 메서드로 첫번째 인자가 target이고 두번째 인자가 동작을 지정한 block이다. 그래서 이 task도 loop나 block에 넣어서 지정할 수도 있다. 그 외에 파일 시스템 조작을 위한 fileutils와 shell 명령 실행을 위한 FileUtils가 기본 포함되어 있다는 것을 제외하면 일반 ruby 스크립트를 작성하듯이 작성하면 된다.
rake는 ruby 문법을 모르면 잘 사용하기 힘들다. 일단 ruby 문법만 알면 tutorial과 reference를 보면서 금세 익힐 수 있다. 나도 rake tutorial과 user guide를 읽고 난 후 reference를 뒤져 가며 대략 하루만에 쓸만한 빌드 스크립트를 만들 수 있었다.
Rakefile을 작성할 때 참조한 문서는 다음과 같다. ruby 문서에는 빠져 있는 내용도 꽤 있어서 인터프리터로 객체를 조사해 보면서 참조해야 하는 경우도 많다. 그게 좀 아쉽기는 한데 ruby 언어 자체가 좀 그렇게 제멋대로인 면이 있는 거 같다.
그래서 이제까지 빌드 스크립트는 ant를 이용해서 compile, jar pack, unit testing을 하고 그 외의 부분(파일 조작, 원격 전송 등)은 셸 스크립트를 이용해왔다. 셸 스크립트로 충분하긴 했지만 좀더 간결하고 강력한 기능이 아쉬울 때도 있었다. 그러다 만난 것이 rake였다.
rake는 ruby 언어를 이용한 빌드 툴이다. ant처럼 xml을 쓴다던가 또는 특별한 종류의 문법을 이용해서 빌드 스크립트를 작성하지 않고 ruby 언어를 그대로 이용해서 빌드 스크립트를 작성한다. 다양하게 변신할 수 있는 ruby 언어의 또 다른 변신 모습을 볼 수 있다.
rake를 쓰기 위해서는 Rakefile을 작성해야 한다(마치 make를 위해 Makefile을 작성하는 것처럼). 이 Rakefile을 ruby 언어로 작성하는데 전용 클래스와 메서드를 이용하고 코딩 스타일을 조금 다르게 해서 작성하므로 전체적인 모양새는 Makefile과 비슷하다.
rake에서 make나 ant의 target에 해당하는 것이 task이다. task에는 task, file, directory, rule이 있는데 이들은 모두 메서드로 첫번째 인자가 target이고 두번째 인자가 동작을 지정한 block이다. 그래서 이 task도 loop나 block에 넣어서 지정할 수도 있다. 그 외에 파일 시스템 조작을 위한 fileutils와 shell 명령 실행을 위한 FileUtils가 기본 포함되어 있다는 것을 제외하면 일반 ruby 스크립트를 작성하듯이 작성하면 된다.
task :name => [:prereq1, :prereq2] do
print "blabla"
end
file "sample.txt" do
# file 작업
end
rule '.o' => ['.c'] do |t|
sh "cc #{t.source} -c -o #{t.name}"
end
rake는 ruby 문법을 모르면 잘 사용하기 힘들다. 일단 ruby 문법만 알면 tutorial과 reference를 보면서 금세 익힐 수 있다. 나도 rake tutorial과 user guide를 읽고 난 후 reference를 뒤져 가며 대략 하루만에 쓸만한 빌드 스크립트를 만들 수 있었다.
Rakefile을 작성할 때 참조한 문서는 다음과 같다. ruby 문서에는 빠져 있는 내용도 꽤 있어서 인터프리터로 객체를 조사해 보면서 참조해야 하는 경우도 많다. 그게 좀 아쉽기는 한데 ruby 언어 자체가 좀 그렇게 제멋대로인 면이 있는 거 같다.
- http://docs.rakeruby.org : rake 튜토리얼과 유저 가이드를 볼 수 있다. 맨 처음 읽어 봐야 할 문서.
- http://rake.rubyforge.org : rake 레퍼런스
- http://ruby-doc.org/stdlib : fileutils 항목을 참조한다.
# by | 2007/03/16 08:10 | 트랙백
◀ 이전 페이지다음 페이지 ▶



















