Apache2 vs. Nginx 1
특이하게도 레일스 배포 환경에 대한 이야기가 나올 때마다 ‘어떤 웹 서버가 가장 적합한가?’라는 이야기가 빠지지 않는다. 왜일까? 개인적인 생각으로는 아직도 완벽한 레일스 배포 스택이라는 것이 정해지지 않았기 때문인 것 같다. 하지만 전부 제각각인 애플리케이션들을 모두 수용할 수 있는 최적의 솔루션, 마치 은빛 총알 같은 것이 가당키나 한 이야기일까? 그렇다면 방법은 초기에 한가지 선택을 하고 계속 튜닝을 해가는 것 뿐이다. 전에 어떤 블로그에서 이런 내용을 본 적이 있다.
Q: 내 레일스 애플리케이션 앞단에서는 도대체 어떤 웹 서버를 사용해야 합니까?
A: 당신이 가장 편안함을 느끼고, 운영하기 쉽고, 어떤 경우에는 확장도 할 수 있는 그 웹 서버를 사용하세요. 그것이 정답입니다.
백번 동감하는 말이다. 그래서 스프링노트에서는 가장 많이 사용해본 웹서버이기도 하고, 지난 글(레일스 최적의 배포환경)에서 추천한 적도 있는 Apache 2.2.x + mod_proxy_balancer 조합을 사용하고 있고 지금까지도 큰 탈 없이 잘 운영되고 있다. 아파치 설정을 꽤 오래동안 반복하고 있지만, 매번 느끼지만 쉽지 않다. 그런데, 최근 커뮤니티를 보면 Nginx(엔진엑스라고 읽는다)라는 묘한 이름의 웹서버를 많이들 추천하고 있다. 그 이유는 아마도 레일스에서 필요한 기능이란 것이 빠른 정적 파일 서빙과 똑똑한 로드 밸런서가 전부이기 때문이다. 한마디로 아파치는 너무 범용적인(모든 기능이 다 있는) 웹 서버라서 무겁고, nginx는 딱 필요한 기능만 있은 우리가 원하는 그 웹 서버라는 것이다. 기능이 많기 때문에 무겁다 또는 느리다라는 것은 이유없는 미신일까? 그래서 몇 가지 벤치마크를 해보기로 했다.
Nginx는 필자도 처음 설정을 해보는데, 어렵지 않게 레일스 애플리케이션을 올릴 수 있었다. Lighttpd를 테스할 때도 느꼈지만, 아파치 설정 파일은 너무 불친절하다. 특히 요즘 애플리케이션이 많이 필요한 Rewrite 설정의 경우 아파치보다 nginx 설정 문법이 훨씬 간결하고 만들기 쉬웠다. (설정 파일 참조: http://brainspl.at/nginx.conf.txt)
지난 번에도 밝혔지만, 이 결과는 특수한 환경일 수 있으므로 참고만 하기 바란다.
테스트 환경
- CPU: AMD64 2CPU
- MEMORY 8G
- 운영체제: RHEL4
벤치마크에 사용한 소프트웨는 몽그렐 1.0.1, Swiftiply 0.5.1, EventMachine 0.7.2이고, 레일스의 버전은 EdgeRails 리비전 7161이다. 그리고 테스트를 위해 간단하게 Hello World를 출력하는 애플리케이션을 작성했다.
- class TestsController < ApplicationController
def show - @hello = “Hello, world!”
@time = Time.now - end
end
위 애플리케이션을 아파치와 Nginx에서 각각 스레드 모드와 이벤트 모드로 모두 테스트했다. 테스트는 간단한 루비 스크립트를 작성해서 httperf를 이용해 rate(접속 빈도)를 10에서 1000까지 늘리며 반복적으로 테스트했다.
테스트 스크립트는 다음과 같다.
- def test(rate = 10, num = 1000)
- restart_mongrels
- # restart_apache
- restart_nginx
- filename = “bench6/#{rate}_#{num}.txt”
- puts “writing #{filename}”
- File.open(filename, ‘w’) do |f|
- f.write `httperf –num-conns #{num} –rate #{rate} –server test.w3pad.com –uri /tests/1`
- end
- end
- (10..100).step(30) {|n| test(n, 50*n)}
- (200..500).step(100) {|n| test(n, 100*n)}
- (600..1000).step(200) {|n| test(n, 100*n)}
Apache 2.2 with Threaded Mongrels
가장 일반적으로 사용하는 방법이다. 스프링노트도 한달 전까지는 이 설정을 사용했다. 가장 무난한 환경이므로, 큰 망설임없이 추천할 만하다.
접속 빈도가 높아짐에 따라 자연스레 에러가 증가하고 응답도 느려짐을 알 수 있다. 위 테스트가 이번 벤치마크의 베이스라인(baseline)이 될 것이다.
Apache 2.2 with Evented Mongrels
이벤트 기반 몽그렐의 성능이라는 글에서도 적었듯, 몇가지 이유로 이벤트 기반 몽그렐이 더 나은 성능을 보여주고 있다. 현재 스프링노트에서 사용하고 있는 설정이기도 하다. 전에 쓴 글이 맞다면, 아파치 위에서도 이벤트 기반 몽그렐이 더 나은 성능을 보여줘야 한다. 그 결과는 어땠을까?
이벤트 기반 몽그렐을 사용했을 때, 에러 빈도가 낮아지고 특히나, 트래픽이 많을 때 더 나은 성능을 보여줌을 알 수 있다. 역시, 이벤트 기반 몽그렐이 더 나은 선택이다. 다시 비교해보니 rate가 800, 1000일 때 스레드모드의 결과는 처참하기까지 하다. 그에 비해 이벤트기반 몽그렐은 그 상황에서도 어느 정도 버텨주는 모습을 볼 수 있다.
Nginx with Threaded Mongrels
이번에는 Nginx를 사용한 벤치마크를 해보았다. 결론부터 이야기하면, Nginx는 기대 이상(!)의 결과를 보여주었다. 놀라울 정도였다. 결과부터 보자.
아파치에서 이벤트 기반 몽그렐을 사용했을 때보다 약간 나은 성능을 보여줌을 알 수 있다. 같은 스레드 기반 몽그렐을 사용할 때의 아파치와 비교해보면 그 차이가 명확하다. 특히 로드가 높아져도 꾸준한 응답률을 보여주는 부분이 놀랍다.
Nginx with Evented Mongrels
이번에는 마지막으로 Nginx 위에 이벤트 기반 몽그렐을 올렸을 때이다.
예상대로 스레드를 사용할 때 보다 나은 모습을 보인다.
비교
먼저, 평균 응답률을 비교해보자.
위 결과만으로 해석한다면 Nginx와 이벤트 기반 몽그렐을 사용할 때 가장 좋은 성능을 보이고, 그 다음 Nginx와 일반적인 몽그렐, 그리고 아파치는 좀 떨어지는 모습을 보였다. 특히나 스레드기반 몽그렐와 아파치는 저 급히 떨어지는 곡선은 참 위험해 보인다.
다음으로 각각의 응답 시간을 비교해보자.
rate가 낮을 때는 큰 차이가 없다가, 높아질 수록 차이가 보인다. 여기서도 Nginx가 더 나은 모습을 보여주고 있다.
마지막으로 에러 발생 빈도를 살펴보자.
rate가 1000일 때 아파치는 거의 100% 실패율을 보이지만, Nginx는 60%선에 머물고 있다. 이 결과도 마찬가지로 Nginx가 더 나은 모습을 보인다.
결론
그래프들이 보여주듯이 Nginx가 소문처럼 무척 좋은 성능의 웹서버임을 알 수 있다. 적어도 아파치보다는 비슷하거나 더 나은 성능을 보여준다. 그리고 Nginx에 내장된 밸런서의 성능도 좋고, 레일스와도 궁합이 잘 맞는 모습을 볼 수 있었다.
Nginx는 추천할만하다. 특히나 php나 webdav나 이런 여타 다른 기능이 필요없고, 딱 정적 파일과 레일스 밸런서만이 필요하다면 Nginx가 최적일 수도 있겠다는 생각이다. 특히, 간결한 설정 파일과 활발하게 개발되고 있는 모습을 보니 더 믿음이 간다. 그리고 레일스를 위해 Proxy 모듈에 몇 가지 기능을 추가해주겠다는 메일링 리스트의 글도 보이고 말이다. 완소 Nginx!
에필로그
사실 이 벤치마크를 하기 전에는 별 차이없겠지 했다가, 결과를 보고 대단한 발견을 한 것처럼 방방뛰며 좋아했던 기억이 난다. 마치 끝없는 사막을 거닐다가 오아시스라도 찾은 사람처럼 말이다. 그리고 마음 속으로는 Nginx를 도입해서 스프링노트가 빨라질거라는 기대감으로 엄청나게 기뻐하고 있었다.
하지만 하루가 지나고 이틀이 지나고, 또 이 결과를 다시 곱씹어보면서 생각이 조금씩 바뀌고 있다. 정말 이 결과만으로 Nginx가 아파치보다 훨씬 뛰어난 성능을 보입니다라고 할 수 있을까? 스프링노트가 저런 차이를 보이는 정도의 높은 트래픽을 받고 있고, 지금 당장 아파치 웹서버로 버틸 수 없다고 판단되면 위 결과가 의미가 있겠지만, 현 시점(서비스가 잘 운영되고있는)에서 어떤 차이가 있을까 싶어진 것이다.
그리고 다시 첫번째 질문으로 돌아간다.
Q: 내 레일스 애플리케이션 앞단에서는 도대체 어떤 웹 서버를 사용해야 합니까?
A: 당신이 가장 편안함을 느끼고, 운영하기 쉽고, 어떤 경우에는 확장도 할 수 있는 그 웹 서버를 사용하세요. 그것이 정답입니다.
Nginx가 좋은 솔루션이라는 사실을 알게 되어 기쁘다. 그리고 나와 루비 커뮤니티가 택할 수 있는 좋은 선택이 하나 더 늘었다. 잘 모니터링 하고 있다가, 언젠가는 nginx로 갈아타고 말꺼다! 꼭! 꼭!
이 글은 스프링노트에서 작성되었습니다.
쿼리 분석 플러그인 (Query Analyzer / Query Trace)
어떤 애플리케이션이든 배포를 위해서는 필연적으로 ‘최적화’라는 단계를 거쳐야하며, 레일스 애플리케이션도 예외는 아니다. 최적화를 위해서 맨 먼저 해야할 일은 테스트나 벤치마크를 통해 병목 지점을 찾는 것이다. 그런데 필자의 경험이나, 지금까지 읽은 여러 글들을 통해 추측컨데 십중팔구 데이터베이스가 우리가 최초로 정복해야할 병목이다.
레일스는 액티브레코드라는 걸출한 ORM(객체-관계 매핑)을 사용한다. 액티브레코드는 개발 단계에서 데이터베이스의 존재를 몰라도 될 정도로 우아하게 상위 수준에서 지낼 수 있게 도와준다. 그렇게 ‘웹 개발 많이 편해졌어~’라며 룰루랄라 지내다가, 어느 날 애플리케이션 로그 파일, 또는 데이터베이스 쿼리 로그를 열어보면 모두들 깜짝 놀라게 된다. 별 생각없이 짠 코드들이 수많은 (때로는 비 효율적인) 쿼리들을 막 쏟아내고 있는 모습을 보면 정신이 아찔해지기까지 하다.
-
Schedule Load (0.023687) SELECT * FROM schedules WHERE (schedules.id = 3) LIMIT 1
Resource Load (0.001076) SELECT * FROM resources WHERE (resources.id = 328) LIMIT 1
Schedule Load (0.011488) SELECT * FROM schedules WHERE (schedules.id = 3) LIMIT 1
Resource Load (0.022471) SELECT * FROM resources WHERE (resources.id = 328) LIMIT 1
사실 액티브레코드 ORM을 사용하면서 효율적으로 데이터베이스를 사용하려면, 즉 두 마리 토끼를 모두 잡기 위해서는 액티브레코드가 어떻게 동작하는지에 대한 깊은 이해가 필요하다. 공교롭게도 이 깊은 이해라는 것은 ‘경험’이 수반되지 않으면 생기지 않는 것이다. 그렇다면 어디서부터 시작하면 좋을까? 필자가 추천하는 방법은 ‘로그 파일을 뚫어져라 쳐다보는 것’이다. 어떤 코드가 어떤 결과(쿼리)를 만들어내는지 하나 하나 분석하다보면, 어떻게 코드를 짜야하는지, 어떤 코드를 피해야 하는지 자연스레 알 수 있을 것이다. 여기서는 레일스 로그에 정보를 더해 우리의 분석 작업을 도와주는 플러그인을 두 가지 소개하겠다.
이 쿼리 누가 만든거지? - Query Trace
로그 파일을 보면 한 액션을 처리하는 동안 어떤 데이터베이스 쿼리가 만들어지는지를 알 수 있다. 단순한 코드라면 그 쿼리만으로 어느 지점에서 어떤 메서드가 만든 것인지 알아낼 수 있을 것이다. 하지만 조금 복잡한 애플리케이션이라면, 쿼리를 만든 코드를 찾아내는 일도 만만치 않다. 어떻게 해야할까? 여기 답이 있다. 바로 Query Trace 플러그인이다. 이 플러그인을 설치하면 SQL 쿼리와 함께 소스 코드의 어느 지점(호출 스택과 함께)에서 만들어졌는지도 함께 출력해준다. 다음 예를 보자.
-
Schedule Load (0.023687) SELECT * FROM schedules WHERE (schedules.id = 3) LIMIT 1
app/models/available_work.rb:50:in `study_method’
app/helpers/plan_helper.rb:4:in `work_description’
app/views/plan/_resource_schedule.rhtml:27:in `_run_rhtml_plan__resource_schedule’
app/views/plan/_resource_schedule.rhtml:24:in `_run_rhtml_plan__resource_schedule’
app/views/plan/_schedule_listing.rhtml:5:in `_run_rhtml_plan__schedule_listing’
app/views/plan/_schedule_listing.rhtml:3:in `_run_rhtml_plan__schedule_listing’
app/views/plan/_schedule_listing.rhtml:1:in `_run_rhtml_plan__schedule_listing’
app/views/plan/index.rhtml:6:in `_run_rhtml_plan_index’
vendor/plugins/textmate_footnotes/lib/textmate_footnotes.rb:60:in `render’
Resource Load (0.001076) SELECT * FROM resources WHERE (resources.id = 328) LIMIT 1
app/models/available_work.rb:54:in `div_type’
app/helpers/plan_helper.rb:6:in `work_description’
app/views/plan/_resource_schedule.rhtml:27:in `_run_rhtml_plan__resource_schedule’
app/views/plan/_resource_schedule.rhtml:24:in `_run_rhtml_plan__resource_schedule’
app/views/plan/_schedule_listing.rhtml:5:in `_run_rhtml_plan__schedule_listing’
app/views/plan/_schedule_listing.rhtml:3:in `_run_rhtml_plan__schedule_listing’
app/views/plan/_schedule_listing.rhtml:1:in `_run_rhtml_plan__schedule_listing’
app/views/plan/index.rhtml:6:in `_run_rhtml_plan_index’
vendor/plugins/textmate_footnotes/lib/textmate_footnotes.rb:60:in `render’
단순히 호출 스택을 출력해서, 필요없는 정보도 있기는 하지만, 단순히 쿼리만 찍히던 때와 비교하면, 정말 친절한 로그라고 할 수 있다. 쿼리를 줄일 수 있을것 같은 ‘용기’를 만들어주는 플러그인이 아닐까 싶다. 위 로그를 보고 필요없는 쿼리를 찾아서 없앤다던가, 중복되는 쿼리는 한번만 불릴 수 있도록 적절한 캐싱 방법을 적용한다든가 하는 방법을 강구할 수 있다. 스프링노트팀도 오픈 전후로 한동안 Query Trace가 포함된 로그 파일을 뚫어져라 쳐다보고 있었다. 무거운 액션 하나를 처리하는데 1400라인도 넘게 찍혔으니, 이 로그를 보는 일이 쉽지만은 않았으리라 짐작이 될 것이다. 그래도 이 플러그인 덕분에 쿼리 수를 1/4 수준으로 줄일 수 있었다 :)
이 쿼리의 성능은? - Query Analyzer
Query Trace를 이용해 불필요한 쿼리를 제거했다면, 이제 꼭 필요한 쿼리들의 성능을 개선할 차례이다. 그리고 가장 손쉽고 당연히 해야하는 일이 바로 적절한 ‘인덱스’를 만드는 것이다. 모든 개발자가 당연히 인덱스를 잘 잡았을거라고 생각하지만, 실제로 그렇지 않은 경우도 매우 많다. 그래서 간단한 인덱스만 잡아줘도 애플리케이션의 성능이 많게는 10배까지 빨라지기도 한다. 특정 쿼리가 데이터베이스에서 어떻게 해석되는지 알기 위해서는 플랜을 봐야하는데, mysql의 경우 EXPLAIN 명령으로 알 수 있다. 로그를 보고, 특정 쿼리를 사냥해서 데이터베이스 콘솔에서 EXPLAIN을 앞에 붙여 돌려보기를 반복하며 성능이 나쁜 쿼리를 찾을 수 있다. 가만보니, 너무 큰 중복이다! 이를 제거할 방법은? Query Analyzer 플러그인을 사용하면 된다.
Query Analyzer 플러그인을 설치해두는 것만으로 레일스가 만드는 모든 쿼리의 플랜을 함께 보여준다. 이제 우리는 우리의 전략, 즉 ‘뚫어져라 로그 쳐다보기’에만 집중할 수 있게 된 것이다. 로그를 보고 인덱스를 타지 않는 쿼리를 찾아서, 인덱스를 타게 만들어주면 된다.
You Aren’t Gonna Need It
최적화라는 것은 꼭 필요해질 시점에 하는 것이 정답이다. 미리 최적화랍시고 코드만 복잡하게 만든다면, 그것은 명백한 퇴보 행위다. 어떤 애플리케이션은 이런 쿼리 튜닝이 전혀 필요없을 수도 있다. 오래걸리는 작업을 단순히 캐싱해버리는 것만으로 인생을 쉽게 살 수 있다면 그렇게 하는 것이 좋다. 정말 필요하다고 느낄 때(데이터베이스 서버가 힘들어할 때, 또는 DB 처리 시간이 너무 길어 응답 시간이 늦어질 때) 그 때 시작해도 늦지 않다. 필요를 느낀다면, 위에서 소개한 플러그인을 이용해 보다 쉽게 상황을 개선할 수 있기를 바란다.
이 글은 스프링노트에서 작성되었습니다.
여러가지 컨텐츠 형식(Contents Type)을 한 곳에서 처리한다
레일스로 개발한 애플리케이션의 컨트롤러 코드를 보면 아래와 같은 형식의 소스를 쉽게 찾을 수 있다.
- respond_to do |format|
- format.html { render :template => ‘some’ }
- format.xml { render :xml => @obj }
- format.json { render :json => @obj.to_json }
- end
위 코드는 하나의 액션 코드로 여러가지 컨텐츠 형식을 처리하고 있다. 사용자가 XML 형식을 원하면(HTTP 요청에 Accept-Type으로 담아보내거나, format 파라메터에 xml을 넣어서 보내거나) 이를 감지해 응답을 XML 형식으로 보내는 식이다. 좋고 나쁨의 논쟁은 접어두고, 어쨌든 현재의 레일스 스타일로 자리잡은 코드이다.
스프링노트에서는 컨트롤러 메서드 대부분에서 HTML, XML, JSON 출력을 하고 있다. 그리고 일부 메서드에서 Ajax 호출에서 사용할 용도로 JS를 지원한다. 코드 중복을 피하기 위해 RenderResult라는 일종의 Presenter 모듈을 도입하고 있기는 한데, 이는 다음에 다루기로 하자.
여러가지 컨텐츠 형식을 처리해야하는 곳은 꼭 응답 문서를 만들 때만은 아니다. 요청 형식도 여러가지일 수 있다. 예를 들어 페이지 리소스를 POST(레일스 용어로는 create)할 때는 일반적인 HTTP 파라메터 형식(page[name]=name&page[content]=conent)을 사용할 수도 있지만, XML이나 JSON을 직접 보낼 수도 있다. 이를 사용자 선택에 맡기도 서버에서 잘 처리할 수 있다면 보다 유연한 REST 서비스가 될 수 있다.
그래서 스프링노트에서는 다음과 같이 API 요청 파라메터를 처리하는 규칙을 정했다.
- XML 문서를 xml 파라메터에 답아서 보낼 수 있다.
- JSON 문서를 json 파라메터에 담아서 보낼 수 있다.
- HTTP 요청의 Content Type이 ‘application/xml’이라면 RAW_POST_DATA에 XML 문서가 담긴 것이다.
- HTTP 요청의 Content Type이 ‘application/json’이라면 RAW_POST_DATA에 JSON 문서가 담긴 것이다.
- 그 외에는 일반적인 파라메터 형식을 따른다.
이와 같은 규칙으로 응담을 처리하기 위한 코드이다.
-
def api_param
if params[:xml] then Hash.from_xml(params[:xml])
elsif params[:json] then JsonDecoder.parse(params[:json])
elsif request.content_type == “application/xml” then Hash.from_xml(request.raw_post)
elsif request.content_type == “application/json” then JsonDecoder.parse(request.raw_post)
else params
end
rescue
raise BadRequest
end
once :api_param
Hash.from_xml은 XML을 파싱해서 해시 구조를 만들어준다. JsonDecoder는 직접 만든 것으로 내부적으로 ActiveSupport::JSON 모듈을 활용하고 있다. 그리고 혹시 이 과정에서 예외가 발생한다면, 사용자 요청에 문제가 있는 것으로 보고 BadRequest 예외를 발생시킨다(그러면 예외 처리 모듈이 이 예외를 받아서 400 에러 페이지를 렌더링 할 것이다).
이제 사용자가 보낸 객체에 접근하기 위해서 params를 직접 호출하는 대신 api_param을 사용하면 된다.
- def create
- @page = Page.create api_param[‘page’]
- render :xml => @page
- end
이렇게 함으로써 애플리케이션 전반에 일관된 파라메터 규칙을 갖게 할 수 있고, 코드 중복도 피할 수 있다. 만세!
이 글은 스프링노트에서 작성되었습니다.
메서드 호출 결과를 저장한다 4
스프링노트 소스 코드에서 자주 보이는 루비 관용 표현 중 하나는 이런 것이다.
- def hard_stuff
- @cached_result_of_this_method ||= do_some_calculations
- end
같은 계산을 여러 번 하는 낭비를 막기 위한 방법이다. 뷰에서 호출하는 헬퍼 메서드들은 대부분 저런식으로 구현해서 계산 시간을 단축하도록 만들었다. 그러던 어느 날, 인스턴스 변수 이름 짓기가 귀찮아지고, 저런 반복적인 행태가 심한 중복으로 보였다. 역시 DRY를 신봉하는 루비 커뮤니티 답다. :)
그래서 같은 일을 하는데, 좀 더 선언적으로 할 수 있도록 구현했다.
- def hard_stuff
- do_some_calculations
- end
- once :hard_stuff
Object#once 메서드를 구현해 모든 객체에서 선언적으로 ‘한번만 호출되는 메서드’를 만들수 있도록 하였다. 아래는 스프링노트의 lib/method_once.rb 파일의 소스 코드이다.
- # 메서드 결과를 한번만 실행하도록 cache 하는 코드
- class Object
- class <<self
- def once(*methods)
- methods.each{|m| once_method(m)}
- end
- def once_method(method)
- origin = ‘_original_’ + method.to_s
- alias_method origin, method
- define_method method do
- @_once_cache ||= {}
- return @_once_cache[method] if @_once_cache.has_key? method
- @_once_cache[method] = send(origin)
- end
- end
- end
- end
once_method는 원래 구현된 메서드(ex. hard_stuff)를 _orign_hard_stuff로 이름을 바꾸고 hard_stuff를 새로 구현하는 식이다. 그리고 once 메서드는 여러 개의 메서드 이름을 한꺼번에 받아서 처리할 수 있도록 했다.
물론 이 코드를 검증하기 위한 스펙도 있다.
- class TestClass
- def initialize(a); @a=a end
- def normal; @a+=1 end
- def abnormal; @a+=1 end
- def abnormal2; @a+=2 end
- once :abnormal, :abnormal2
- end
- describe “method once” do
- before(:each) do
- @obj = TestClass.new(100)
- end
- it “once로 선언된 메서드는 한번만 실행된다” do
- @obj.normal.should eql(101)
- @obj.normal.should eql(102)
- @obj.abnormal.should eql(103)
- @obj.abnormal.should eql(103)
- @obj.abnormal2.should eql(105)
- @obj.abnormal2.should eql(105)
- end
- end
위 메서드의 문제점은 매개변수에 따라 값이 달라지는 메서드의 캐싱이 불가능하다는 점이다. 이를 알고 있지만 구현하지 않은 이유는 액티브서포트에 훌륭한 대안이 있기 때문이다. 그 내용이 궁금한 사람은 ActiveSupport::CachingTool::HashCaching#hash_cache 메서드를 참조하기 바란다.
이 글은 스프링노트에서 작성되었습니다.
레일스 로그 분석을 활용한 성능 분석
지난 글 몽그렐 클러스터가 필요한 이유에서 설명한 것처럼 레일스 애플리케이션(특히나 몽그렐을 사용할 때는)은 거의 모든 경우에 대해서 빠른 응답 시간을 보장해야 안정적인 서비스를 할 수 있다. 그러나 이는 쉽지만은 않은 목표이기 때문에, 꾸준한 모니터링과 개선이 수반되야한다. 이제 어떤 액션이 애플리케이션 전체의 건강을 해치는 ‘병목 지점’인지를 알아낼 수 있는 방법이 필요하다. 다행히 레일스 로그를 보면 매 액션에 대한 자세한 정보가 담겨 있기 때문에 이 로그를 분석해보면 어렵지 않게 원하는 내용을 찾을 수 있다.
-
Processing PluginsController#index (for 127.0.0.1 at 2007-07-11 13:22:49) [GET]
Session ID: 1
Parameters: {“action”=>”index”, “controller”=>”plugins”} - Completed in 0.01590 (62 reqs/sec) | Rendering: 0.00351 (22%) | Memcache: 0.00725 | DB: 0.00687 (43%) | 200 OK [http://localhost/plugins]
이런 분석을 해주는 툴은 직접 만들어도 되겠지만, Rails Analyzer 프로젝트에서 제공하는 Production Log Analyzer를 사용해도 된다. 이 툴을 실행하면 아래와 같은 정보를 얻을 수 있다.
- 응답 속도 통계
- DB 통신에 사용한 시간 통계
- 화면 렌더링에 사용한 시간 통계
위 값들의 액션별 통계도 제공하지만, 보다 유용한 것은 가장 오랜 시간을 소요한 액션들에 대한 정보이다. 이 액션이 우리가 개선해야할 목록이기도 하다. 위 프로젝트 사이트에 레포트 예제도 있으니, 이를 보고 사용할지를 판단해보기 바란다. 스프링노트팀에서는 매일 새벽 배치 작업을 통해 전날 로그를 분석해서 메일로 받아보고 있다. 그리고 아침마다 아직 할 일이 많다는 사실을 깨우친다. :)
Production Log Analyzer를 사용하기 위해서는 한가지 설정이 필요하다. 레일스 로그 파일을 유심히 보면 알겠지만 한가지 문제점이 있다. 여러 몽그렐 프로세스의 로그들이 모두 섞여서 어느 줄이 어느 요청에서 온 것인지 구분이 되지 않는다는 것이다. 이 문제를 해결하기 위해서는 이 프로그램의 저자는 레일스의 기본 설정 로거 대신에 SyslogLogger를 사용하도록 권장하고 있다. 이는 유닉스 시스템의 syslogd를 이용해 로그를 남기는 것이다. 여러 머신의 로그를 하나로 모으는데도 유용하고, 파일 기반 로거보다 성능 향상도 조금 있을 것으로 기대된다.
하지만 이런 설정의 여의치 않다면 레일스 로그 형식을 살짝 바꿔서 Production Log Analyzer가 처리할 수 있는 형식으로 바꿔줘도 된다. 이 파일을 lib 디렉터리에 만들고 환경 파일(config/environments/production.rb)에 다음과 같이 추가해주면 된다. 자세한 내용은 이 포스트를 참조하기 바란다.
- require ‘hodel_3000_compliant_logger’
config.logger = Hodel3000CompliantLogger.new(config.log_path)
config.logger.level = Logger::INFO
이렇게 해서 성능 병목지점을 찾았다면 이를 해소하기 위해 어떤 작업을 할 수 있을까? 프로파일링을 해볼 수도 있고, 수정과 벤치마크를 반복하는 방법도 있다. 이도 저도 안되면 백그라운드 작업으로 돌리는 방법도 있다. 여기에 관련 내용은 다음 기회에 다루기로 하겠다.
이 글은 스프링노트에서 작성되었습니다.

