저희 전산 동아리 PAIM에서 만든 프로젝트 중 현재 가장 많은 학우들이 애용하고 있는 프로젝트는 단연 강의록 사이트입니다. 교수님들께서 의대 수업의 특성상 PPT를 수업 프리젠테이션으로 많이 사용하시다 보니 다들 강의록 PPT에 필기를 하고 뽑아보고 나눠보고 돌려보죠. PPT파일의 크기는 적게는 1MB미만에서 크게는 100MB에 이르기까지 다양합니다만, 거의 100여명의 학생들이 동시에 50~100MB의 파일을 다운로드 받는 때에는 서버가 버티질 못하더라구요. 하긴 39만원짜리 PC서버에서 무얼 더 바라겠냐마는, 뭔가 근본적인 문제가 있는게 아닌가 의심되었습니다. 아니나 다를까 강의록 사이트 총 책임자인 호종이가 레일스의 send_file의 문제점을 파악했습니다. 메모리에 파일을 다 올리지 않고 조각내서 전송하겠다고 다짐한 send_file이건만 파일이 전송될 때 마다 메모리를 뭉텅 뭉텅 소모하는 것이지요.
Mongrel 제작자인 Zed의 이야기에 따르면 send_file의 구현이 OS 마다 다르다 보니 Mongrel 1.0 부터는 안정성의 문제로 send_file를 지원하지 않기로 했답니다. 어쩐지 Mongrel을 업그레이드 하기 전에는 적어도 서버가 죽는 일은 없었는데 말이죠.
사실 지금에 와서 생각해보면 레일스의 send_file의 문제라기보다는 Apache + Mongrel 조합에서 생겨날 수 밖에 없는 필연적인 문제였습니다. 아무리 Mongrel이 send_file 해준다고 하더라도 결국 client는 Apache를 통해서 파일을 전송 받기 때문에 Mongrel이 찔끔 찔끔 파일을 보내줘봤자 그걸 잠시 보관해야할 Apache를 위해서라도 메모리는 소모되는 것이지요. 게다가 proxy_balancer로 여러 Mongrel을 띄웠다면 매번 서로 다른 Mongrel이 파일을 찔끔찔끔 직접 읽고 있으니 서버가 감당하지 못한 거지요.
Lighttpd에는 X-Sendfile이라는 개념이 도입되었습니다. 웹 프로그램이 파일을 읽어서 웹 서버에 보내는 것이 아니라 웹 프로그램은 파일 위치만 헤더(Header)로 알려주고 웹 서버가 send_file처럼 전송해야할 파일을 직접 전송해주는 것이지요. 이걸 Apache에서도 가능하게 하는 모듈이 mod_xsendfile입니다.
우선 모듈을 컴파일 해야죠. mod_xsendfile 링크에서 소스를 받아서 아파치의 동적 모듈로 컴파일 합니다.
apxs2 -cia의 옵션을 잠깐 설명드리면 -c는 동적 모듈로 컴파일을 하겠다는, -i는 설치를 하겠다는, -a는 설정파일까지 수정하겠다는 의미입니다. 설정 파일 수정이 자동으로 안되시면 /etc/apache2/httpd.conf파일에 다음을 추가하세요
이제 send_file로 파일을 직접 전송하는 것 대신에 헤더만 써주면 됩니다. 다음처럼요.
덤으로 팁입니다. 헤더가 잘 전송되고 있는지 보려면 curl --head http://..... 를 이용해보세요. 헤더 정보만 볼 수 있어서 잘 되고 있는지 테스트 하실 수 있습니다. 만약 헤더에 X-Sendfile...이라는 내용이 있다면 헤더는 잘 만들었는데, 아파치가 처리를 안하고 있다는 것입니다.
또 덤으로...
저의 경우 한글 인코딩 문제로 고생을 많이해서 서버를 UTF-8으로 통일했습니다. 그래도 파일 이름이 겹치는 것을 처리하기 귀찮아서 파일이 업로드되면 원래의 파일 이름을 DB에 저장하고 서버에는 그 DB의 id로 이름을 바꿔서 저장합니다. "한글.jpg"라는 파일이 101번째 업로드된 파일이라면 서버에는 "101"이라는 파일로 저장되지요. 그리고는 다음과 같이 전송합니다.
주석처리된 send_file 문과 비교해서 보세요. 헤더에 파일 이름을 따로 지정할 수 있기 때문에 DB상의 파일 이름을 넣어주고 아파치의 파일 전송을 위해 절대 경로를 알려줍니다. 혹여 DB상의 파일 이름이 "한글.jpg"에서 "../../../../etc/passwd" 등으로 조작되더라도 중요한 시스템 파일이 유출되는 일은 없겠지요. 어짜피 전송할 파일은 DB의 id로 지정되어있으니까요. 그리고 윈도우+IE일때는 UTF-8으로 된 파일을 처리하지 못하기 때문에 filename로 손봐줬습니다.
휴우~ 글이 길어졌습니다. Mongrel이 정적인 파일 전송에 쥐약이기로 유명하다는 소문은 들었지만 막상 서버가 죽어나가니 뭔가 대안을 찾아야했습니다. 강의록 사이트의 과부하 때문에 Lightttpd로 옮겨볼까도 생각했습니다만, Apache의 X-sendfile 덕분에 한시름 놨습니다. 혹시 대용량 파일 전송으로 고민하고 계신분들은 X-sendfile을 시도해보세요~ :)
Mongrel 제작자인 Zed의 이야기에 따르면 send_file의 구현이 OS 마다 다르다 보니 Mongrel 1.0 부터는 안정성의 문제로 send_file를 지원하지 않기로 했답니다. 어쩐지 Mongrel을 업그레이드 하기 전에는 적어도 서버가 죽는 일은 없었는데 말이죠.
사실 지금에 와서 생각해보면 레일스의 send_file의 문제라기보다는 Apache + Mongrel 조합에서 생겨날 수 밖에 없는 필연적인 문제였습니다. 아무리 Mongrel이 send_file 해준다고 하더라도 결국 client는 Apache를 통해서 파일을 전송 받기 때문에 Mongrel이 찔끔 찔끔 파일을 보내줘봤자 그걸 잠시 보관해야할 Apache를 위해서라도 메모리는 소모되는 것이지요. 게다가 proxy_balancer로 여러 Mongrel을 띄웠다면 매번 서로 다른 Mongrel이 파일을 찔끔찔끔 직접 읽고 있으니 서버가 감당하지 못한 거지요.
Lighttpd에는 X-Sendfile이라는 개념이 도입되었습니다. 웹 프로그램이 파일을 읽어서 웹 서버에 보내는 것이 아니라 웹 프로그램은 파일 위치만 헤더(Header)로 알려주고 웹 서버가 send_file처럼 전송해야할 파일을 직접 전송해주는 것이지요. 이걸 Apache에서도 가능하게 하는 모듈이 mod_xsendfile입니다.
우선 모듈을 컴파일 해야죠. mod_xsendfile 링크에서 소스를 받아서 아파치의 동적 모듈로 컴파일 합니다.
curl -O http://celebnamer.celebworld.ws/stuff/mod_xsendfile/mod_xsendfile.capxs2에서 애러가 난다구요? 없는 명령이라구요? 데비안을 쓰신다면 아파치 모듈 컴파일이 되도록
apxs2 -cia mod_xsendfile.c
apt-get install apache2-prefork-dev로 development 패키지를 설치합니다.
apxs2 -cia의 옵션을 잠깐 설명드리면 -c는 동적 모듈로 컴파일을 하겠다는, -i는 설치를 하겠다는, -a는 설정파일까지 수정하겠다는 의미입니다. 설정 파일 수정이 자동으로 안되시면 /etc/apache2/httpd.conf파일에 다음을 추가하세요
LoadModule xsendfile_module /usr/lib/apache2/modules/mod_xsendfile.so그리고 다음처럼 모듈을 사용하겠다고 추가하세요
XSendFile on만약 데이안 다운 모듈 관리를 원하신다면 위의 모듈 로드, 모듈 설정 내용을 /etc/apache2/mods-available/xsendfile.load와 xsendfile.conf에 담으시고 a2enmod xsendfile 이라고 터미널이 입력해보세요. 알아서 두 파일을 /etc/apache2/mods-enabled 디렉토리에 링크를 걸어줘서 웹서버를 재시작하면 모듈이 올라갑니다.
XSendFileAllowAbove on
이제 send_file로 파일을 직접 전송하는 것 대신에 헤더만 써주면 됩니다. 다음처럼요.
response.headers['Content-Type'] = "application/force-download":) 좀 부담이 줄었나요??
response.headers['Content-Disposition'] = "attachment; filename=\"#{filename}\""
response.headers["X-Sendfile"] = local_file
response.headers['Content-length'] = File.size(local_file)
response.headers['Cache-Control'] = 'must-revalidate'
render :nothing => true
덤으로 팁입니다. 헤더가 잘 전송되고 있는지 보려면 curl --head http://..... 를 이용해보세요. 헤더 정보만 볼 수 있어서 잘 되고 있는지 테스트 하실 수 있습니다. 만약 헤더에 X-Sendfile...이라는 내용이 있다면 헤더는 잘 만들었는데, 아파치가 처리를 안하고 있다는 것입니다.
또 덤으로...
저의 경우 한글 인코딩 문제로 고생을 많이해서 서버를 UTF-8으로 통일했습니다. 그래도 파일 이름이 겹치는 것을 처리하기 귀찮아서 파일이 업로드되면 원래의 파일 이름을 DB에 저장하고 서버에는 그 DB의 id로 이름을 바꿔서 저장합니다. "한글.jpg"라는 파일이 101번째 업로드된 파일이라면 서버에는 "101"이라는 파일로 저장되지요. 그리고는 다음과 같이 전송합니다.
a = Attachment.find(params[:id])
a.hit!
if @request.env["HTTP_USER_AGENT"] =~ /IE/
filename = Iconv.iconv('euckr', 'utf8', a.original_filename)
else
filename = a.original_filename
end
# send_file(File.join(UPLOAD_DIR, a.id.to_s), :filename => filename, :type => a.content_type)
local_file = File.join(RAILS_ROOT, UPLOAD_DIR, a.id.to_s)
response.headers['Content-Type'] = "application/force-download"
response.headers['Content-Disposition'] = "attachment; filename=\"#{filename}\""
response.headers["X-Sendfile"] = local_file
response.headers['Content-length'] = File.size(local_file)
response.headers['Cache-Control'] = 'must-revalidate'
render :nothing => true
주석처리된 send_file 문과 비교해서 보세요. 헤더에 파일 이름을 따로 지정할 수 있기 때문에 DB상의 파일 이름을 넣어주고 아파치의 파일 전송을 위해 절대 경로를 알려줍니다. 혹여 DB상의 파일 이름이 "한글.jpg"에서 "../../../../etc/passwd" 등으로 조작되더라도 중요한 시스템 파일이 유출되는 일은 없겠지요. 어짜피 전송할 파일은 DB의 id로 지정되어있으니까요. 그리고 윈도우+IE일때는 UTF-8으로 된 파일을 처리하지 못하기 때문에 filename로 손봐줬습니다.
휴우~ 글이 길어졌습니다. Mongrel이 정적인 파일 전송에 쥐약이기로 유명하다는 소문은 들었지만 막상 서버가 죽어나가니 뭔가 대안을 찾아야했습니다. 강의록 사이트의 과부하 때문에 Lightttpd로 옮겨볼까도 생각했습니다만, Apache의 X-sendfile 덕분에 한시름 놨습니다. 혹시 대용량 파일 전송으로 고민하고 계신분들은 X-sendfile을 시도해보세요~ :)
이 글과 관련있는 글을 자동검색한 결과입니다 [?]
- 웹서버의 시작 Apache(DSO) by 제갈장비
- 아파치 버전 2.2 성능 향상 정리 by 부자아빠
- [Ruby] #000 루비 & rails 설치 by 원사마
- PHP 다운로드 구현 by 견우
- PHP에서 excel 파일로 데이터 다운 받기 by Hwan





