tyamada112’s blog

技術的なこととかの備忘

sendfile()の挙動を観察する。

sendfileについての備忘など。

qiita.com

1. sendfile()

NAME
sendfile - transfer data between file descriptors

SYNOPSIS
#include <sys/sendfile.h>

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

DESCRIPTION
sendfile() copies data between one file descriptor and another. Because this
copying is done within the kernel, sendfile() is more efficient than the combi-
nation of read(2) and write(2), which would require transferring data to and
from user space.

fd間のストリームでデータ共有を行うシステムコール
read(), write()等のバイト情報をユーザランドのバッファに吐き出す処理が発生しない。
そのため、メモリキャッシュ及びソケットバッファのカーネル空間に閉じた処理での完結が可能。

2. 何が起こるのか。

このシステムコールを呼ぶとどんな動きをするのか。
nginxのsendfileディレクティブを使って、httpリクエスト発生時に、fd周り、メモリ、バッファ読み込み、吐き出し周りの挙動を観察してみる。

設定自体はsendfileディレクティブを有効化するのみ。Module ngx_http_core_module

通常、おまじない的にtcp_nopushも同時に有効化するが、 転送されるファイルの内容HTTPヘッダ情報 のパケットの扱いを観察するため、ここではコメントアウトしておく。

http {
  server {
     sendfile on;
     #tcp_nopush on;
~~
~~
~~
  }
}

2-1. sendfile無効の場合

{{EPOLLIN, {u32=2655952912, u64=140396546924560}}}, 512, -1) = 1
accept4(6, {sa_family=AF_INET, sin_port=htons(50376), sin_addr=inet_addr("{{ source ip }}")}, [16], SOCK_NONBLOCK) = 3
epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLET, {u32=2655953296, u64=140396546924944}}) = 0
epoll_wait(8, {{EPOLLIN, {u32=2655953296, u64=140396546924944}}}, 512, 60000) = 1
recvfrom(3, "GET / HTTP/1.1\r\nHost: xxx.xxx.xxx"..., 1024, 0, NULL, NULL) = 396
stat("/usr/shere/nginx/html/index.html", {st_mode=S_IFREG|0664, st_size=55, ...}) = 0
open("/usr/shere/nginx/html/index.html", O_RDONLY|O_NONBLOCK) = 9
fstat(9, {st_mode=S_IFREG|0664, st_size=55, ...}) = 0
pread(9, "sendfile test"..., 55, 0) = 55
writev(3, [{"HTTP/1.1 200 OK\r\nServer: nginx/1"..., 244}, {"sendfile test"..., 55}], 2) = 299
write(4, "{{ source ip }} - - [03/Nov/2018:06"..., 197) = 197
close(9) = 0
setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0
recvfrom(3, 0x1468830, 1024, 0, 0, 0) = -1 EAGAIN (Resource temporarily unavailable)
epoll_wait(8,

2-2. sendfile有効の場合

{{EPOLLIN, {u32=1826033680, u64=139940450471952}}}, 512, -1) = 1
accept4(6, {sa_family=AF_INET, sin_port=htons(49775), sin_addr=inet_addr("{{ source ip }}")}, [16], SOCK_NONBLOCK) = 3
epoll_ctl(8, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLET, {u32=1826034064, u64=139940450472336}}) = 0
epoll_wait(8, {{EPOLLIN, {u32=1826034064, u64=139940450472336}}}, 512, 60000) = 1
recvfrom(3, "GET / HTTP/1.1\r\nHost: xxx.xxx.xxx"..., 1024, 0, NULL, NULL) = 396
stat("/usr/shere/nginx/html/index.html", {st_mode=S_IFREG|0664, st_size=55, ...}) = 0
open("/usr/shere/nginx/html/index.html", O_RDONLY|O_NONBLOCK) = 9
fstat(9, {st_mode=S_IFREG|0664, st_size=55, ...}) = 0
setsockopt(3, SOL_TCP, TCP_CORK, [1], 4) = 0
writev(3, [{"HTTP/1.1 200 OK\r\nServer: nginx/1"..., 244}], 1) = 244
sendfile(3, 9, [0], 55) = 55
write(4, "{{ source ip }} - - [03/Nov/2018:05"..., 197) = 197
close(9) = 0
setsockopt(3, SOL_TCP, TCP_CORK, [0], 4) = 0
recvfrom(3, 0x1071830, 1024, 0, 0, 0) = -1 EAGAIN (Resource temporarily unavailable)
epoll_wait(8,

pread()、つまりfdからの読み込み -> バッファ吐き出しが実行されていない。 fd間での通信のみで、ユーザ空間でのデータの受け渡しは発生していないように見える。

3. パケットを観察してみる。

パケットのやり取りに差異はあるか、観察してみる。

3-1. sendfile無効の場合

1 0.000000000 {{ source ip }} -> {{ dest ip }} TCP 66 52140 -> 80 [FIN, ACK] Seq=1 Ack=1 Win=4108 Len=0 TSval=1758067629 TSecr=2399948
2 0.000181005 {{ dest ip }} -> {{ source ip }} TCP 66 80 -> 52140 [FIN, ACK] Seq=1 Ack=2 Win=235 Len=0 TSval=2401820 TSecr=1758067629
3 0.000684120 {{ source ip }} -> {{ dest ip }} TCP 66 52140 -> 80 [ACK] Seq=2 Ack=2 Win=4108 Len=0 TSval=1758067629 TSecr=2401820
4 4.198461822 {{ source ip }} -> {{ dest ip }} TCP 78 52143 -> 80 [SYN, ECN, CWR] Seq=0 Win=65535 Len=0 MSS=1460 WS=32 TSval=1758071810 TSecr=0 SACK_PERM=1
5 4.198607450 {{ dest ip }} -> {{ source ip }} TCP 74 80 -> 52143 [SYN, ACK, ECN] Seq=0 Ack=1 Win=28960 Len=0 MSS=1460 SACK_PERM=1 TSval=2402869 TSecr=1758071810 WS=128
6 4.198970205 {{ source ip }} -> {{ dest ip }} TCP 66 52143 -> 80 [ACK] Seq=1 Ack=1 Win=131744 Len=0 TSval=1758071810 TSecr=2402869
7 4.199186363 {{ source ip }} -> {{ dest ip }} HTTP 462 GET / HTTP/1.1
8 4.199219557 {{ dest ip }} -> {{ source ip }} TCP 66 80 -> 52143 [ACK] Seq=1 Ack=397 Win=30080 Len=0 TSval=2402870 TSecr=1758071810
9 4.199341119 {{ dest ip }} -> {{ source ip }} HTTP 365 HTTP/1.1 200 OK (text/html)
10 4.199707812 {{ source ip }} -> {{ dest ip }} TCP 66 52143 -> 80 [ACK] Seq=397 Ack=300 Win=131456 Len=0 TSval=1758071811 TSecr=2402870

3-2. sendfile有効の場合

1 0.000000000 {{ source ip }} -> {{ dest ip }} TCP 78 50661 -> 80 [SYN, ECN, CWR] Seq=0 Win=65535 Len=0 MSS=1460 WS=32 TSval=1756061495 TSecr=0 SACK_PERM=1
2 0.000035493 {{ dest ip }} -> {{ source ip }} TCP 74 80 -> 50661 [SYN, ACK, ECN] Seq=0 Ack=1 Win=28960 Len=0 MSS=1460 SACK_PERM=1 TSval=1901979 TSecr=1756061495 WS=128
3 0.000814846 {{ source ip }} -> {{ dest ip }} TCP 66 50661 -> 80 [ACK] Seq=1 Ack=1 Win=131744 Len=0 TSval=1756061496 TSecr=1901979
4 0.001317144 {{ source ip }} -> {{ dest ip }} HTTP 462 GET / HTTP/1.1
5 0.001343425 {{ dest ip }} -> {{ source ip }} TCP 66 80 -> 50661 [ACK] Seq=1 Ack=397 Win=30080 Len=0 TSval=1901979 TSecr=1756061496
6 0.001587658 {{ dest ip }} -> {{ source ip }} TCP 310 HTTP/1.1 200 OK [TCP segment of a reassembled PDU]
7 0.001735116 {{ dest ip }} -> {{ source ip }} HTTP 121 HTTP/1.1 200 OK (text/html)
8 0.002948842 {{ source ip }} -> {{ dest ip }} TCP 66 50661 -> 80 [ACK] Seq=397 Ack=245 Win=131520 Len=0 TSval=1756061497 TSecr=1901979
9 0.003177666 {{ source ip }} -> {{ dest ip }} TCP 66 50661 -> 80 [ACK] Seq=397 Ack=300 Win=131456 Len=0 TSval=1756061497 TSecr=1901979

リクエストの受信から返送まで問題なくできている。 (sendfile有効化時は、ヘッダ情報とファイルの内容が1パケットで送りきれていない。
今回の実験では、前述したとおりtcp_nopushを有効化していないため、ヘッダとファイルの内容自体が別パケットで送られている。)

4. 図

図にしてみる。

4-1. sendfile無効化

f:id:tyamada112:20181203121731p:plain

  1. 接続待ち(epoll_xxx)
  2. ソケットの受信データを取得(recvfrom)
  3. ファイル確認(stat)
  4. ファイルサイズ確認(fstat)
  5. ヘッダ情報とデータを返送(writev)
  6. アクセスログに追記(write)
  7. ファイルクローズ(close)

4-2. sendfile有効化

f:id:tyamada112:20181203121759p:plain

  1. 接続待ち(epoll_xxx)
  2. ソケットの受信データを取得(recvfrom)
  3. ファイル確認(stat)
  4. ファイルサイズ確認(fstat)
  5. ヘッダ情報を返送(writev)
  6. データの読み込みと送信(sendfile)
  7. アクセスログに追記(write)
  8. ファイルクローズ(close)

5. 速度比較

  • 設定無し
  • sendfile有効、tcp_nopush無効
  • sendfile有効、tcp_nopush有効

の3パターンでレスポンス完了までの時間を測定してみる。

f:id:tyamada112:20181203133633p:plain

基本tcp_nopushとのセットというのはこういうことなのかな。
転送するファイルの内容次第というのもあると思うが、ファイルディスクリプタ間での通信で完了できるとはいえ、 パケット分割の負荷については、改めて考慮しておく。