jongpak / dev-tip

My Dev tips
1 stars 0 forks source link

[nginx] sendfile과 gzip #12

Open jongpak opened 4 years ago

jongpak commented 4 years ago

nginx 설정중에 sendfile이라는 디렉티브가 있다. (http://nginx.org/en/docs/http/ngx_http_core_module.html#sendfile)

이 디렉티브는 파일을 서빙할때 sendfile 시스템콜을 사용하여 서빙을 한다. read/write를 이용한 IO작업시에는 하드웨어로부터 읽은 데이터를 커널공간에서 유저공간으로 복사하는 작업이 수반된다. sendfile 시스템콜을 사용하면 유저공간으로 복사하지 않고 바로 커널공간에서 IO 작업을 수행한다. 그렇기 때문에 유저공간으로 복사하는 불필요한 오버헤드를 줄일 수 있다. (https://linux.die.net/man/2/sendfile)

그런데.. sendfile과 gzip을 함께 사용하면?

gzip 압축은 유저모드에서 수행되기 때문에 데이터가 유저공간까지 올라와야한다.. 그런데 sendfile은 커널공간에서 바로 IO를 수행하는 것이기 때문에 gzip과 sendfile은 함께 사용될 수 없을것 같다. 또는 둘중에 하나는 무시되어야 할것 같다.

nginx worker 프로세스에 strace 걸어서 시스템콜이 어떻게 수행되는지 살펴보자 🎉


실험1) sendfile on + gzip off

기대한대로 sendfile 시스템콜을 사용하여 데이터가 서빙된다. gzip off 이므로 gzip 압축은 수행되지 않았다.

[jongpak@test-jongpak nginx]$ sudo strace -p 56254
strace: Process 56254 attached
// (1) 커넥션 받아들이는 부분
epoll_wait(10, [{EPOLLIN, {u32=1268473872, u64=140107396636688}}], 512, -1) = 1
accept4(6, {sa_family=AF_INET, sin_port=htons(61376), sin_addr=inet_addr("10.25.144.209")}, [16], SOCK_NONBLOCK) = 3
epoll_ctl(10, EPOLL_CTL_ADD, 4, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1268474568, u64=140107396637384}}) = 0
epoll_wait(10, [{EPOLLIN, {u32=1268474336, u64=140107396637152}}], 512, 59999) = 1
recvfrom(3, "GET / HTTP/1.1\r\nHost: test-jongp"..., 1024, 0, NULL, NULL) = 517

// (2) 파일 여는 부분
stat("/home/jongpak/apps/nginx-1.18.0/html/index.html", {st_mode=S_IFREG|0644, st_size=612, ...}) = 0
open("/home/jongpak/apps/nginx-1.18.0/html/index.html", O_RDONLY|O_NONBLOCK) = 7
fstat(7, {st_mode=S_IFREG|0644, st_size=612, ...}) = 0

// (3) HTTP 헤더 전송
writev(3, [{"HTTP/1.1 200 OK\r\nServer: nginx/1"..., 238}], 1) = 238

// (4) sendfile 시스템콜 호출
sendfile(3, 7, [0] => [612], 612)       = 612

// (5) 로그파일 쓰는 부분
write(8, "10.25.144.209 - - [29/Aug/2020:2"..., 199) = 199

실험2) sendfile on + gzip on

둘다 on 하면 어떻게 될까? sendfile 과 gzip 둘중 하나는 무시되어야 할거 같다. 결과는? gzip이 수행되고 sendfile은 수행되지 않는다.

strace와 함께 ltrace 로 확인을 해보면, pread64 (파일읽기) 다음에 deflate 함수호출이 수행되는 것도 확인이 가능하다. (deflate는 gzip 데이터 압축을 수행하는 함수)

[jongpak@test-jongpak nginx]$ sudo strace -p 57377
strace: Process 57377 attached

// (1) 커넥션 받아들이는 부분
accept4(6, {sa_family=AF_INET, sin_port=htons(62502), sin_addr=inet_addr("10.25.144.209")}, [16], SOCK_NONBLOCK) = 4
epoll_ctl(10, EPOLL_CTL_ADD, 4, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=1268474336, u64=140107396637152}}) = 0
epoll_wait(10, [{EPOLLIN, {u32=1268473872, u64=140107396636688}}, {EPOLLIN, {u32=1268474336, u64=140107396637152}}], 512, 60000) = 2
recvfrom(4, "GET / HTTP/1.1\r\nHost: test-jongp"..., 1024, 0, NULL, NULL) = 517

// (2) 파일 여는 부분
stat("/home/jongpak/apps/nginx-1.18.0/html/index.html", {st_mode=S_IFREG|0644, st_size=612, ...}) = 0
open("/home/jongpak/apps/nginx-1.18.0/html/index.html", O_RDONLY|O_NONBLOCK) = 8
fstat(8, {st_mode=S_IFREG|0644, st_size=612, ...}) = 0

// (3) read / write 순으로 시스템콜 된다 (writev에서 헤더와 gzip압축된 데이터를 쓰고 있다)
pread64(8, "<!DOCTYPE html>\n<html>\n<head>\n<t"..., 612, 0) = 612
writev(4, [{"HTTP/1.1 200 OK\r\nServer: nginx/1"..., 249}, {"180\r\n", 5}, {"\37\213\10\0\0\0\0\0\4\3uR=o\3340\f\335\365+\230\314\227S\203\242\313U5P\344\3"..., 384}, {"\r\n0\r\n\r\n", 7}], 4) = 645

// (4) 로그파일 쓰는 부분
write(3, "10.25.144.209 - - [29/Aug/2020:2"..., 199) = 199

그러면 sendfile과 gzip을 같이 사용할 방법은 없나?

sendfile 사용시에도 gzip 압축을 사용하고 싶다면? 😮 gzip_static 디렉티브를 사용하면 된다! (http://nginx.org/en/docs/http/ngx_http_gzip_static_module.html)

gzip_static on 으로 설정해놓으면, 만약 index.html 파일을 요청할 경우 index.html.gz 파일이 있다면 해당 파일로 서빙을 하게 된다. 정말 그런지 strace로 확인해보았다.

[jongpak@test-jongpak html]$ strace -p 65805
strace: Process 65805 attached
// 생략...
recvfrom(3, "GET / HTTP/1.1\r\nHost: test-jongp"..., 1024, 0, NULL, NULL) = 517
stat("/home/jongpak/apps/nginx-1.18.0/html/index.html", {st_mode=S_IFREG|0644, st_size=612, ...}) = 0

// gz 파일을 연다(!)
open("/home/jongpak/apps/nginx-1.18.0/html/index.html.gz", O_RDONLY|O_NONBLOCK) = 7
fstat(7, {st_mode=S_IFREG|0644, st_size=392, ...}) = 0
writev(3, [{"HTTP/1.1 200 OK\r\nServer: nginx/1"..., 240}], 1) = 240

// 그리고 sendfile을 한다 (!)
sendfile(3, 7, [0] => [392], 392)       = 392

그런데 만약 .gz 파일이 없다면?

[jongpak@test-jongpakhtml]$ strace -p 65805
strace: Process 65805 attached
// 생략...
recvfrom(3, "GET /test.html HTTP/1.1\r\nHost: t"..., 1024, 0, NULL, NULL) = 526

// test.html.gz 파일을 열어보려고 했으나 파일이 없다 (ENOENT)
open("/home/jongpak/apps/nginx-1.18.0/html/test.html.gz", O_RDONLY|O_NONBLOCK) = -1 ENOENT (No such file or directory)
// test.html 파일로 서비스한다
open("/home/jongpak/apps/nginx-1.18.0/html/test.html", O_RDONLY|O_NONBLOCK) = 4
fstat(4, {st_mode=S_IFREG|0644, st_size=612, ...}) = 0
writev(3, [{"HTTP/1.1 200 OK\r\nServer: nginx/1"..., 238}], 1) = 238

// sendfile on + gzip off 로 되어있으므로 sendfile을 사용
sendfile(3, 4, [0] => [612], 612)       = 612

결론1

sendfile은 파일을 그대로 서빙해야할때 유용하다. 파일 데이터를 조작하려면 결국 유저공간으로 데이터를 올려야하기 때문에 자동으로 무시된다 (무시될수 밖에 없다..) 만약 sendfile + gzip을 사용해야한다면 gzip_static 옵션으로 미리 압축된 파일을 준비해놓으면 된다.

또한 sendfile 디렉티브 말고 sendfile_max_chunk 라는 디렉티브도 존재한다. (http://nginx.org/en/docs/http/ngx_http_core_module.html#sendfile_max_chunk) sendfile 시스템콜 호출시 한번에 보낼 청크의 크기를 설정하게 되는데, 만약 max_chuck제한이 없으면 빠른 연결하나가 워커 프로세스를 완전히 점유(소비)할수 있다고 한다

결론2

위 글에서는 gzip을 예시로 들었지만, 유저공간에서 데이터를 다루어야하는 작업일 경우 sendfile 시스템콜을 사용할 수 없다 (당연한 것이지만..)

결론3

nginx 코드를 통해 위 동작은 확인하고 싶었지만, 아직 nginx 코드레벨까지 알지 못해서 확인을 못했다..ㅠ 뭔가 output_filter_chain이 있을 경우 sendfile을 무시하는 코드가 있던거 같은데.. 맞는지는 모르겠다.


부연설명) strace와 ltrace

위 두가지 도구는 프로세스 내부의 동작 흐름, 커널간 연관부분을 엿보는데 많은 도움이 된다. 예전에 nodejs 진단시에도 많은 도움을 주었었다. (타이머로 인한 CPU 상승 원인 파악 시)

서버에 기본 패키지로 설치 되어있지 않을 경우 yum 으로 설치가 가능하다.