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

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

【nginx】ソケットのキューのサイズを出力するMackerelプラグインを書いた

github.com

LinuxTCPは2段(netdevもあるので厳密には3段以上)のキューの仕組みがあります。クライアントがconnect(2)はサーバ側のOSがackを返した時点でconnect(2)は戻ります。この状態でサーバアプリケーションがaccept(2)を実行することでクライアントとのやりとりを実行することができます。ここのときに最初のsynが来た時点でsyn-queueという最初のキューに格納しackを返すまでの間そこにとどまります。その後クライアントからackが帰ってきたら3-way-handshakeは完了となるので今度はサーバのアプリケーションがaccept(2)するまでaccept-queueになります。それぞれのキューは/proc/net/tcpあたりに情報が書かれています。それを指定されたポートでMackerelに送信するツールとなります。

いつ使えるの?

サーバがaccept(2)を呼び出せないことでレイテンシ遅延しているかどうかを監視することで確認することができます。仮にaccept-queueが高い場合はその分どこかしらがボトルネックになっている場合が多いですNginxとかRedisとかmemcachedでこの辺が高い場合はスケールアウトかスケールアップするしか手段はありません。自分たちで書いてるミドルウェアであれば最近のLinuxであれば色々やりようがあったりします

イベントドリブンアーキテクチャでこの値は高くなるのか

先程挙げたミドルウェアでこの辺の数値が高くなることはCPU使用率が高かったりメモリが全然足らないとかそういうのが無い限りはまあ無いかなと思います。というのもそれぞれ実装方法は違えどイベントドリブンアーキテクチャでの実装になっています。nginxで言えばワーカプロセスごとにepoll_wait(2)でソケットを監視してaccept-queueに溜まった時点でワーカープロセスが起こされ処理を行います。しかし何かしらのイベントがブロッキング処理の場合は話が変わってきます。例えばepoll_wait(2)から戻ったらどんなイベントもブロックせずにノンブロッキングな処理を行うのがベストプラクティスになります。以下のサンプルコードではepoll_waitを実行し戻ってきた値で処理を継続しています。処理自体はノンブロッキングにしておくことで例えばsocketにデータがまだない状態の場合はほかクライアントの処理を継続することができるという仕組みです。

for (;;) {
    nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
        perror("epoll_wait");
        exit(EXIT_FAILURE);
    }

    for (n = 0; n < nfds; ++n) {
        if (events[n].data.fd == listen_sock) {
            conn_sock = accept(listen_sock,
                               (struct sockaddr *) &addr, &addrlen);
            if (conn_sock == -1) {
                perror("accept");
                exit(EXIT_FAILURE);
            }
            setnonblocking(conn_sock);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = conn_sock;
            if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                        &ev) == -1) {
                perror("epoll_ctl: conn_sock");
                exit(EXIT_FAILURE);
            }
        } else {
            do_use_fd(events[n].data.fd);
        }
    } }

仮にブロッキングな処理を行うとどうなるでしょうか?答えはイベントエンジンがそのブロッキングな処理のみを実施するになります。ブロッキングな処理が例えば10秒かかると10秒間は他の処理が全て待たされることになります。もしもnginxが1プロセスのみでこのブロッキングな処理をしている最中は他のクライアントの処理を行うことが出来ません。

HPAとかでスケールしたいのでブロッキング処理をしているときのメトリクスが欲しい

CPU負荷は高くないのになんかNginxで処理時間が長くなっているときに変化するメトリクスがほしかったので今回のツールは力を発揮します。ブロッキング処理はあくまでもIO処理になるのでCPU使用率は上がりません。トラフィック量も直接的には増えません。そしてなんとnginxのアクセスログは処理時間でwriteするのでログの量は減ります。HPAみたいにスケールさせる仕掛けを作るにはこの事象を捉える必要があります。ブロッキング処理が増えるとnginxのacceptする回数が減ります。(ngx_event_acceptの関数コールが減るので)。accept-queueのキューが伸びるのでここを検出可能にすることが出来ました。