地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【systemd】Type=notifyについて調べたメモ

systemdのカスタムサービスファイルを書くときにどのTypeで起動を判定するかのTypeでType=notifyについて調べたメモです。

github.com

Type=notifyとは

以下のような記載があります。フォアグラウウンドで実行を継続するデーモンで使えるType=simpleとほぼ同じようですが起動/停止などをsd_notify(3)というsystemd用の関数用いて通知を行います。ちなみに使用するにはsystemd-develをインストールする必要があります。

Type=notify: Type=simple と同じですが、利用可能になったときにデーモンが systemd に信号を送るように条件がつけられます。この通知のリファレンス実装は libsystemd-daemon.so によって提供されています。

参考: systemd - ArchWiki

simpleとの違い

simpleはコマンドを実行したタイミングで起動完了と判断します。極端な例ですが以下のようなソースでinit_service()でサービスの初期化処理をしてそこで失敗してデーモンが終了してもsystemctlのstartコマンドは正常終了となります。

#include <unistd.h>
#include <stdio.h>

int init_service()
{
        // config読んだり、他サービスから情報を取ってきたりしてデーモン起動準備をする。失敗する可能性もある
}

int main(int arg, char **argv)
{
        if(init_service())
            return 0;
        start_daemon();
}

これ自体は何が問題になるかというとinit_service()の結果を待たずにコマンドが戻ってきてしまうのでsystemctl start test && 前段のコマンドが成功している前提のコマンドみたいに&&を繋いだだけの処理は失敗する可能性が出てきてしまいます。(例えば何らかのデータストアを起動させつつクライアントアプリを起動するみたいなケース)

この話自体はそこまで問題にはならなそうでExecStartPre/Postを使うことでサービス起動前後にsleepなりコネクションの状態をloopで監視するスクリプトを入れることで解決はできる。解決策はいろいろありそうだがその中の一つである"Type=notify"を例に挙げて調べてみる。

Type=notifyを使うにはsd_notify(3)という関数を用いて起動が完了したことをsystemdに通知することができる。以下のようにsleep(30)の部分でsystemctlは戻らずに通知が来るまでコマンドが停止してくれる。

#include <unistd.h>
#include <stdio.h>
#include <systemd/sd-daemon.h>

int main(int arg, char **argv)
{
        sleep(30)
    if (sd_notify(0, "READY=1"))
        fprintf(stderr, "failed sd_notify");
    return 0;
}

これを入れることでsystemctl start test && systemctl start test2とか内部のユニットファイルでtest.serviceとtest2.serviceで依存関係を持たせてもtestのサービス起動通知はまではtest2は起動を待ってくれるようになる。パッと用途は思いつきませんでしたが起動以外にもステータスをsystemdに送信することができてSTOPPINGなどでプロセス停止中などを通知することも可能となります。

www.freedesktop.org

apache/httpdなんかでも利用されていて有効にしておくことでsystemctl statusの結果に統計情報を出力することが可能となるようです。こんな機能があるんですね〜という。

static int systemd_monitor(apr_pool_t *p, server_rec *s)
{    
    sd_notifyf(0, "READY=1\n"
               "STATUS=Total requests: %lu; Idle/Busy workers %d/%d;" // STATUSの部分がsystemctl statusに出力されるようになる
               "Requests/sec: %.3g; Bytes served/sec: %sB/sec\n",
               sload.access_count, sload.idle, sload.busy,
               ((float) sload.access_count) / (float) up_time, bps);

    return DECLINED;
}

どうやって通知してるのか

qiita.com

上記の記事にある通りUnixドメインソケットを用いた通信を行なっているようです。netstatgrepすると以下のようになっている。

$ netstat --protocol=unix |grep notify
unix  3      [ ]         DGRAM                    10138    /run/systemd/notify

httpdをtraceすると以下のような通信を行なっていることがわかった。sd_notify(3)なしで通信も容易にできそうですね。

sendmsg(8, {msg_name={sa_family=AF_UNIX, sun_path="/run/systemd/notify"}, msg_namelen=21, msg_iov=[{iov_base="READY=1\nSTATUS=Total requests: 2; Current requests/sec: 0; Current traffic:   0 B/sec\n", iov_len=86}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, MSG_NOSIGNAL) = 86

【Ruby】メモリの共有率を計算するスクリプトを書いた

github.com

マルチプロセスモデルなどで動くアプリケーションがどれくらい他プロセスとメモリを共有しているのかを計算スクリプトを書いてみました。

Linuxに関してのCoWの細かい話は他記事で書いてるので省略。引数のpidからtgidを中で取得したりして子プロセスをのPIDを指定したら関連のプロセスの分のメモリ共有率を計算したり/proc/${PID}/cmdlineにあるコマンドでgrepして計算したりする処理も入れたりもしてみました。以下は出力。定期的にkillすること想定してプロセスの起動時間もついでに出してます。

[root@slave ~]# ruby shmget.rb nginx
28880: 84.59[%] (900/1064) 2021-08-28 11:58:00 -0400 nginx: master process /usr/sbin/nginx
28881: 73.32[%] (1616/2204) 2021-08-28 11:58:00 -0400 nginx: worker process
28882: 73.32[%] (1616/2204) 2021-08-28 11:58:00 -0400 nginx: worker process
28883: 73.32[%] (1616/2204) 2021-08-28 11:58:00 -0400 nginx: worker process
28884: 73.32[%] (1616/2204) 2021-08-28 11:58:00 -0400 nginx: worker process

PID, 共有率, (shared-memory/rss), プロセス起動時間, cmdlineの内容みたいな順番で表示してます。Unicornとかhttpdのpreforkで動いているアプリケーションの測定などなど用途はいろいろありそうです。procfsから取れる情報をパースしてるだけになってるので他にも何かしらアイディア浮かべば追加していきたい。

【Linux】SO_REUSEPORTに入門してGracefulなrestartを目指す

ジムのマッサージ機に乗りながらsocket(7)のmanを読んでいたらSO_REUSEPORTというオプションがあって気になって調べたのでメモ

SO_REUSEPORT (Linux 3.9 以降)

Permits multiple AF_INET or AF_INET6 sockets to be bound to an identical socket address. This option must be set on each socket (including the first socket) prior to calling bind(2) on the socket. To prevent port hijacking, all of the processes binding to the same address must have the same effective UID. This option can be employed with both TCP and UDP sockets.

Man page of SOCKET

ざっくりいうとSO_REUSEPORTは同一ポートに複数のlisten ソケットが bind できるようになるというものらしい。accept(2)の負荷分散の改善だったりの話題が記載されていた。この問題自体は単一のソケットに対して複数プロセスがepoll(2)してるケースで通信可能になったときに全てのプロセスが起こされてaccept(2)しにいってしまうという問題。SO_REUSEPORTはこれを解決するよ的な話が書かれていた。

WebサーバでのThundering Herdは過去の話? | monolithic kernel

このレイヤの性能改善の話は正直難しくてまだよくわかっていないが機能の説明だけ見ると自作デーモンとかをサクッとGraceful-restartに対応させるのに便利なんじゃないかと思ったのでそっちの方向でみていく。

Graceful-restartとは

ググるとITに関わらず色々な世界で使われる単語のようなのでこの記事では「TCPサーバとして動いているプロセスをダウンタイム無しでプログラムを入れ替える」と定義して進める。

ダウンタイムとは

以下のような簡易tcpサーバを立てる。

#!/usr/bin/env python
import socket

def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(("0.0.0.0", 80))
    s.listen(5)

    while True:
        conn, addr = s.accept()
        print("accepted!")
        ret = conn.recv(16)
        conn.close()


if __name__ == '__main__':
    main()

このときプロセスの内容を更新したいとなった場合に取る手順は以下。

  • ① プロセスの終了をするためにシグナルを送る
  • ② プロセスの終了を待つ
  • ③ プロセスを起動させる

②と③の間はコネクションを受けれない。この時間がダウンタイムとなる。最小手順にしてるのでコードの更新やプロセスの初期化処理(cofigパースとかコネクションプール生成とか)といった時間が加算される。

どうするのが良いのか

  • 複数サーバいて前段にロードバランサがいるならdrain機能とかあるならそこでリクエストが来ないよう止めて安全に再起動させちゃう
  • そういった機能が搭載されたミドルウェアを使う(Server::Starterはとてもとてもすごく便利だった。。)

みたいな選択肢がありそう。ただ上で書いたサンプルが既に稼働していてLBも入れれない&ミドルウェア導入も無理!みたいな場合にサンプルコードに実装するなら以下のようになるイメージ(実装イメージであって動くは不明。。)

① Graceful-restartを行う用のシグナルハンドラを設定する
② シグナルを受けたらfork(2)を実行し新しいコードを読み込んだ子プロセスを生成する(親プロセスのlisten socketも子プロセスが引き継ぐ)
③ 親プロセスはaccept(2)を停止し子プロセスのみでaccept(2)を実行する
④ 親プロセスはシグナルなり正常終了のパスを作るなりして終了する

なんか大変そう

なんで? <- 同一ポートに複数のlisten ソケットが bindできないから

で、やっと登場するのが「SO_REUSEPORT」!!

もう一回貼っておく。SO_REUSEPORTがあれば複数プロセスがbindできるので複数プロセスが立ち上げることができるという仕組み。指定しないとOSError: [Errno 98] Address already in useという何回見たかわからないエラーとなってしまう。

SO_REUSEPORT (Linux 3.9 以降)

Permits multiple AF_INET or AF_INET6 sockets to be bound to an identical socket address. This option must be set on each socket (including the first socket) prior to calling bind(2) on the socket. To prevent port hijacking, all of the processes binding to the same address must have the same effective UID. This option can be employed with both TCP and UDP sockets.

Man page of SOCKET

ソースは一行修正で対応することが可能。複数プロセスであることが分かりやすいようにpidも出すように修正してみる。

--- s.py 2021-08-25 15:18:32.046623000 +0000
+++ s2.py 2021-08-25 15:18:45.729623000 +0000
@@ -1,16 +1,15 @@
 #!/usr/bin/env python
-import socket,os
+import socket

     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
     s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

-        print(f"accepted!: {os.getpid()}")
+        print("accepted!")
         ret = conn.recv(16)
         conn.close()

実践

2コンソールからプロセスを起動する。どちらも起動できてる時点でオプションが有効化されていることがわかる。ローカルからncを複数回打つことでそれぞれでacceptが成功していることがわかった。便利!プロセス1をアップデートするために停止してもプロセス2がacceptし続けるのでダウンタイム0でコードを更新させることが可能ということがわかった。(ただしshutdownをGracefulにする必要はある)

# コンソール1
[root@c2cdb3a47b61 ]# python3 s.py
accepted!: 241

# コンソール2
[root@c2cdb3a47b61 ]# python3 s.py
accepted!: 242

straceも貼っておく。setsockopt(2)でSO_REUSEPORTを指定してあることが確認できた。

socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_IP) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEPORT, [1], 4) = 0
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 5)                            = 0

実験していて気づいたこと(未解決。。😭)

今回の実験をやってて気になったのが1点あった。3way handshake済かつaccept待ちの状態のコネクションがある場合にプロセスを終了するとクライアントにFINが飛ぶこと。

ざっくりやったこと

① ソースを修正してコネクションがaccept待ちになるようにする
② プロセスを2つ起動する。ncを使ってconnect(2)を発行。プロセス1、プロセス2と呼ぶ
③ ssコマンドを使ってプロセス1がaccept(2)待ちになってることを確認
④ プロセス1を終了させる。tcpdumpでクライアントにfinが来てることを確認

Linuxをわかってないポイントなんだろうけど個人的にはプロセス2が代わりにaccept(2)してくれたりしないのかなと思っていた。acceptキューはどうやらソケットごとに持つらしい。なんかの記事で1本化もするよみたいな話題を見た気がしたが違ったらしい。やるならGraceful-shutdownを実装してNONBLOCKINGで呼び出し、加えてタイムアウト機能とかも必要になりそう。

以下は作業時のメモログ。

# 2プロセスあげた時のss
State       Recv-Q Send-Q     Local Address:Port                    Peer Address:Port
LISTEN      0      5                      *:80                                 *:*
LISTEN      0      5                      *:80                                 *:*

# accept待ちにした時のss。Recv-Qのカウントが上がっている
State       Recv-Q Send-Q     Local Address:Port                    Peer Address:Port
LISTEN      0      5                      *:80                                 *:*
LISTEN      1      5                      *:80                                 *:*
ESTAB       0      0             172.17.0.2:80                        172.17.0.1:64716

# プロセス1を終了させた時のtcpdump。クライアントにfinが飛んでいる。
00:45:17.338669 IP6 ::1.80 > ::1.62532: Flags [F.], seq 1, ack 1, win 6371, options [nop,nop,TS val 3122313103 ecr 1463932082], length 0
00:45:17.338707 IP6 ::1.62532 > ::1.80: Flags [.], ack 2, win 6371, options [nop,nop,TS val 1463943117 ecr 3122313103], length 0
00:45:17.338749 IP6 ::1.62532 > ::1.80: Flags [F.], seq 1, ack 2, win 6371, options [nop,nop,TS val 1463943117 ecr 3122313103], length 0
00:45:17.338791 IP6 ::1.80 > ::1.62532: Flags [.], ack 2, win 6371, options [nop,nop,TS val 3122313103 ecr 1463943117], length 0

感想

metacpan.org

Server::Starterがすごすぎると改めて感じた。

【Nginx】proxy_cache_lockが思ってたのと違った件

背景

pkg.go.dev

ISUCONの過去問を眺めていたらキャッシュのThundering Herd問題を解決するのにsingleflightというライブラリを使った解決策があることを知ったので調べていたらNginxでも似たようなことをできそうと言うことを知ったので調べてみたメモ。

キャッシュのThundering Herd問題

キャッシュのThundering Herd問題とは

キャッシュアサイドパターンとか呼ばれるパターンのシステム構成があるとしてキャッシュに格納されるデータがそれぞれ単一の生存時間(TTL)を持つ場合にキャッシュデータがエクスパイアした際に発生に発生し得る問題。

例えばエクスパイアされるデータが大量アクセスを並列に受けている場合にオリジンへデータを取得する処理が大量に発生してしまう。この処理が軽いものなら問題は特に起きないが重い計算やクエリをDBへ投げるなどどいった場合にシステムが高負荷になってレスポンス遅延や最悪の場合サービスダウンにまでなってしまう恐れがある。これがThundering HerdやCache Stampedeと呼ばれる問題。

blog.nomadscafe.jp

解決方法

有名な解決方法はwikiにあるように3つある。singleflightは①の方法を使って対策をする実装となっている。(あくまでも1プロセスに限った絞り方なので厳密に1リクエストにするにはプロセス間やサーバ間で分散ロックの仕組みは別途必要となるREDIS による分散ロック)

  • ① ロックを取ってオリジンへのリクエストを絞る
  • ② 他のプロセスで事前にキャッシュを生成する
  • ③ 期限切れ前に一定の確率で期限を更新する

Wikipedia: Cache Stampede

ここで思ったのがNginxにはProxyCacheという機能があって「Nginx -> upstreamの段階でリクエスト絞れれば早いのでは?」と思いそれっぽいオプションないかなと調べ始めたのが記事を書き始めたきっかけ。

proxy_cache_lockというのがあった

proxy_cache_lockというのがあった

Syntax:  proxy_cache_lock on | off;
Default:    
proxy_cache_lock off;
Context:    http, server, location
This directive appeared in version 1.1.12.

When enabled, only one request at a time will be allowed to populate a new cache element identified according to the proxy_cache_key directive by passing a request to a proxied server. Other requests of the same cache element will either wait for a response to appear in the cache or the cache lock for this element to be released, up to the time set by the proxy_cache_lock_timeout directive.

Module ngx_http_proxy_module

プロキシサーバーにリクエストを渡すことで、proxy_cache_keyディレクティブに従って識別された新しいキャッシュ要素に一度に1つのリクエストのみを送れるようになるとのこと。これで要件を満たせそう!となったがそんなに甘くはなかった。

検証用環境を作る

環境は以下。

  • Webアプリ: Flask
  • Webサーバ: nginx-1.21.1

アプリ

threadedをtrueにすることで並行にリクエストを受けれるようになる。(gunicornとか入れれば良いが簡易的な検証なのでこれで)。sleepというエンドポイントを用意して重いリクエストを再現する。

#!/bin/env python

from flask import Flask
import time

app = Flask(__name__)

@app.route("/sleep")
def sleep():
    time.sleep(2)
    return "sleeping"


if __name__ == "__main__":
    app.run(host="0.0.0.0", threaded=True)

nginx.conf

proxy_cacheとproxy_cache_lockを設定してあればよい。

upstream app {
    server flask:5000;
}

proxy_cache_path /var/cache/nginx keys_zone=zone1:1m max_size=1g inactive=24h;

server {
    listen       80;
    server_name  localhost;

    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_cache zone1;
        proxy_cache_valid 200 302 10s;
        proxy_cache_lock on;
        add_header X-Nginx-Cache $upstream_cache_status;
    }
}

いざ実験!

4プロセスで合計4リクエストを投げる。

seq 1 16 | xargs -I{} -P 4 curl http://localhost/sleep -o /dev/null -s

1回目

flask_1が1回であとはnginx_1だけが出力されている。成功している模様。完璧!

flask_1  | 192.168.80.4 - - [23/Aug/2021 15:33:30] "GET /sleep HTTP/1.1" 200 -
nginx_1  | 192.168.80.1 - - [23/Aug/2021:15:33:30 +0000] "GET /sleep HTTP/1.1" 200 8 "-" "curl/7.64.1" "-"
nginx_1  | 192.168.80.1 - - [23/Aug/2021:15:33:30 +0000] "GET /sleep HTTP/1.1" 200 8 "-" "curl/7.64.1" "-"
nginx_1  | 192.168.80.1 - - [23/Aug/2021:15:33:30 +0000] "GET /sleep HTTP/1.1" 200 8 "-" "curl/7.64.1" "-"
nginx_1  | 192.168.80.1 - - [23/Aug/2021:15:33:30 +0000] "GET /sleep HTTP/1.1" 200 8 "-" "curl/7.64.1" "-"

キャッシュのttl切れを待って2回目を実施

flask_1  | 192.168.80.4 - - [23/Aug/2021 15:33:48] "GET /sleep HTTP/1.1" 200 -
flask_1  | 192.168.80.4 - - [23/Aug/2021 15:33:48] "GET /sleep HTTP/1.1" 200 -
flask_1  | 192.168.80.4 - - [23/Aug/2021 15:33:49] "GET /sleep HTTP/1.1" 200 -
nginx_1  | 192.168.80.1 - - [23/Aug/2021:15:33:49 +0000] "GET /sleep HTTP/1.1" 200 8 "-" "curl/7.64.1" "-"
flask_1  | 192.168.80.4 - - [23/Aug/2021 15:33:49] "GET /sleep HTTP/1.1" 200 -
nginx_1  | 192.168.80.1 - - [23/Aug/2021:15:33:49 +0000] "GET /sleep HTTP/1.1" 200 8 "-" "curl/7.64.1" "-"
nginx_1  | 192.168.80.1 - - [23/Aug/2021:15:33:49 +0000] "GET /sleep HTTP/1.1" 200 8 "-" "curl/7.64.1" "-"
nginx_1  | 192.168.80.1 - - [23/Aug/2021:15:33:49 +0000] "GET /sleep HTTP/1.1" 200 8 "-" "curl/7.64.1" "-"

!!??

1回目の結果と違って全てのリクエストがupstreamまで全てのリクエストが到達してしまっている。原因をググるもよくわからない。

ソースを眺める

gdbを使って関連しそうな処理を特定。

ngx_http_file_cache_lockという関数が具体的にキャッシュのロックを取って取得できたプロセスがupstreamへリクエストを流すと言った処理を行なっている。この関数を呼び前段の関数(ngx_http_file_cache_open)を見る。

ngx_int_t
ngx_http_file_cache_open(ngx_http_request_t *r)
{
    // キャッシュファイルを読み取る。NGX_OKならコンテンツをreadして返す
    if (ngx_open_cached_file(clcf->open_file_cache, &c->file.name, &of, r->pool)
        != NGX_OK)
    {
        switch (of.err) {

        case 0:
            return NGX_ERROR;

        // ファイル/ディレクトリが存在しないケース
        case NGX_ENOENT:
        case NGX_ENOTDIR:
            goto done;  // ここのgotoに入る必要がある
        }
    }

done:

    if (rv == NGX_DECLINED) {
        // ここでロックを取る処理を呼び出す。
        return ngx_http_file_cache_lock(r, c);
    }

    return rv;
}

// ロックを取るなどの処理はこっち。"ngx_shmtx_lock"とあるように共有メモリを使ったロック
static ngx_int_t
ngx_http_file_cache_lock(ngx_http_request_t *r, ngx_http_cache_t *c)
{
    ngx_shmtx_lock(&cache->shpool->mutex);

    timer = c->node->lock_time - now;

    // 最初のリクエストのみここに入る
    if (!c->node->updating || (ngx_msec_int_t) timer <= 0) {
        c->node->updating = 1;
        c->node->lock_time = now + c->lock_age;
        // ステータスを更新する
        c->updating = 1;
        c->lock_time = c->node->lock_time;
    }

    ngx_shmtx_unlock(&cache->shpool->mutex);

キャッシュファイル/ディレクトリが存在しないケースでなければ処理そのもの(ngx_http_file_cache_lock)が動かない仕様らしい。キャッシュがEXPIRED(ttl切れ)の場合はどうやら動かないらしい。やるならgoto ラベルするケースにcacheのttlを見るような処理が必要となる。(またはpurgeする仕組みを独自に実装する)

この件に関しては言及しているコメントはちらほら見つけることができた。概ね調べた内容と同じようなことが書かれていた。

stackoverflow.com

やりたいことをやるなら以下の機能を組み合わせることで実現できそうなので別記事で対応してみることにする。

* proxy_cache_background_update
* proxy_cache_use_stale

おまけ: ApacheBenchは最初に1リクエスト飛ばしている?

github.com

abコマンドでテストしようとしてたらうまくいかなかった。ab -c 3 -n 3 http://localhost/sleepみたいに実行したら1リクエストだけ先に飛ばしてそれを待って残りの2リクエストを処理するような流れになっているみたいでした(ソースは未確認)。この動きだと最初のリクエストがproxy_cacheに載ってしまって都合が良くないと言った現象が起きてました。

以下はtcpdumpの結果。理由はよくわからないですがテスト結果に必要な情報を初回リクエストで取得してるとかですかね〜。

#### 1リクエスト目
01:04:39.878390 IP6 ::1.50763 > ::1.80: Flags [S], seq 1933344289, win 65535, options [mss 16324,nop,wscale 6,nop,nop,TS val 3092785126 ecr 0,sackOK,eol], length 0
01:04:39.878502 IP6 ::1.50763 > ::1.80: Flags [.], ack 2113740241, win 6371, options [nop,nop,TS val 3092785126 ecr 4235194127], length 0
01:04:39.878525 IP6 ::1.50763 > ::1.80: Flags [P.], seq 0:82, ack 1, win 6371, options [nop,nop,TS val 3092785126 ecr 4235194127], length 82: HTTP: GET /sleep HTTP/1.0
01:04:42.884627 IP6 ::1.50763 > ::1.80: Flags [.], ack 165, win 6369, options [nop,nop,TS val 3092788128 ecr 4235197129], length 0
01:04:42.884711 IP6 ::1.50763 > ::1.80: Flags [.], ack 166, win 6369, options [nop,nop,TS val 3092788128 ecr 4235197129], length 0

#### 2以降。sleepで指定した時間を待ってリクエストしている。
01:04:42.885305 IP6 ::1.50764 > ::1.80: Flags [P.], seq 0:82, ack 1, win 6371, options [nop,nop,TS val 1632426051 ecr 2438025469], length 82: HTTP: GET /sleep HTTP/1.0
01:04:42.885321 IP6 ::1.50765 > ::1.80: Flags [P.], seq 0:82, ack 1, win 6371, options [nop,nop,TS val 3660829198 ecr 1860469325], length 82: HTTP: GET /sleep HTTP/1.0