Tomcat NIO Connector における通信処理とスレッド構成の解析

Tomcat Connector の役割とパッケージ構成

Tomcat アーキテクチャにおいて、Connector コンポーネントはソケットレベルでの通信処理を担います。具体的には、クライアントからの socket 接続を受け入れ、プロトコルに基づいてリクエストを解析する役割を担います。ソースコード上では、主に org.apache.catalina.connector および org.apache.coyote パッケージ下に実装されています。

Connector は Service コンテナの子要素として定義され、Service はさらに Server コンテナに所属します。これらのインスタンス化は、server.xml 設定ファイルの記述に基づき、Catalina クラス内で Digester を利用して行われます。デフォルト設定では、HTTP リクエスト処理用と AJP プロトコル用の 2 種類の Connector が用意されています。

サポートされる Connector の種類

Tomcat が提供する主要な Connector 実装は以下の通りです。

  1. HTTP Connector: HTTP プロトコルを解析します。実装方式により、ブロッキング IO を使用する BIO と、ノンブロッキング IO を使用する NIO に分類されます。本稿では NIO 実装に焦点を当てます。
  2. AJP Connector: Apache JServ Protocol を使用し、Apache HTTP Server などのウェブサーバーと Tomcat を連携させる際に利用されます。通信効率に優れています。
  3. APR HTTP Connector: C 言語で実装されたネイティブライブラりを JNI 経由で呼び出します。静的コンテンツの配信性能向上を目的としています。

利用するプロトコルは、server.xml 内の protocol 属性で指定可能です。

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443" />

Connector の初期化処理

Connector クラスのコンストラクタでは、指定されたプロトコル名に基づいて適切な ProtocolHandler をインスタンス化します。

public Connector(final String protocolSpec) {
    configureProtocol(protocolSpec);
    ProtocolHandler handler = null;
    try {
        final Class<?> handlerClass = Class.forName(this.handlerClassName);
        handler = (ProtocolHandler) handlerClass.getDeclaredConstructor().newInstance();
    } catch (Exception ex) {
        log.error("ProtocolHandler initialization failed", ex);
    } finally {
        this.protocolHandler = handler;
    }

    if (Globals.STRICT_SERVLET_COMPLIANCE) {
        uriCharset = StandardCharsets.ISO_8859_1;
    } else {
        uriCharset = StandardCharsets.UTF_8;
    }
}

private void configureProtocol(String protocol) {
    boolean useApr = AprLifecycleListener.isAprAvailable() &&
            AprLifecycleListener.getUseAprConnector();

    if ("HTTP/1.1".equals(protocol) || protocol == null) {
        if (useApr) {
            setHandlerClass("org.apache.coyote.http11.Http11AprProtocol");
        } else {
            setHandlerClass("org.apache.coyote.http11.Http11NioProtocol");
        }
    } else if ("AJP/1.3".equals(protocol)) {
        if (useApr) {
            setHandlerClass("org.apache.coyote.ajp.AjpAprProtocol");
        } else {
            setHandlerClass("org.apache.coyote.ajp.AjpNioProtocol");
        }
    } else {
        setHandlerClass(protocol);
    }
}

この処理により、各 Connector は固有の ProtocolHandler を保持し、特定のポートでネットワークリクエストを監視する準備が整います。ただし、実際のビジネスロジック処理は Container コンポーネントが担当します。

ライフサイクルの開始段階である startInternal メソッドにおいて、ProtocolHandler の起動が行われます。

protected void startInternal() throws LifecycleException {
    if (getPort() < 0) {
        throw new LifecycleException("Invalid port configuration");
    }

    setState(LifecycleState.STARTING);

    try {
        protocolHandler.start();
    } catch (Exception e) {
        throw new LifecycleException("ProtocolHandler start failed", e);
    }
}

HTTP/1.1 および NIO を使用する場合、Http11NioProtocolorg.apache.tomcat.util.net.NioEndpoint インスタンスを生成し、ポート監視およびリクエスト解析の実際の作業を委譲します。

NioEndpoint のスレッドモデル

HTTP リクエストの解析プロセスにおいて、Tomcat は以下の 3 種類のスレッドを活用します。

  • Acceptor: ソケット接続の受け入れ
  • Poller: 準備完了したソケットの監視(セレクタ.poll)
  • Worker: 実際のリクエスト処理

1. Acceptor スレッド

Acceptor は Runnable インターフェースを実装しており、その名の通り接続受け入れを専門とします。serverSocket.accept() を呼び出して SocketChannel を取得し、それを Tomcat 独自の NioChannel へラップします。NIO を採用していても、接続受付自体はブロッキング方式で行われます。Acceptor はスレッドプールによって管理され、NioEndpoint の起動時に生成されます。

public void startInternal() throws Exception {
    if (!running) {
        running = true;
        paused = false;

        // キャッシュの初期化
        processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                socketProperties.getProcessorCache());
        eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getEventCache());
        nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                socketProperties.getBufferPool());

        if (getExecutor() == null) {
            createExecutor();
        }

        initializeConnectionLatch();

        // Poller スレッドの起動
        pollers = new Poller[getPollerThreadCount()];
        for (int i = 0; i < pollers.length; i++) {
            pollers[i] = new Poller();
            Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-" + i);
            pollerThread.setPriority(threadPriority);
            pollerThread.setDaemon(true);
            pollerThread.start();
        }

        startAcceptorThreads();
    }
}

protected final void startAcceptorThreads() {
    int count = getAcceptorThreadCount();
    acceptors = new Acceptor[count];

    for (int i = 0; i < count; i++) {
        acceptors[i] = createAcceptor();
        String threadName = getName() + "-Acceptor-" + i;
        acceptors[i].setThreadName(threadName);
        Thread t = new Thread(acceptors[i], threadName);
        t.setPriority(getAcceptorThreadPriority());
        t.setDaemon(getDaemon());
        t.start();
    }
}

Acceptor の主要なロジックは run メソッド内にあります。ここでは接続数の制限チェックを行い、ソケットを受け入れた後に設定処理へ渡します。

protected class Acceptor extends AbstractEndpoint.Acceptor {
    @Override
    public void run() {
        int retryWait = 0;

        while (this.isActive) {
            while (paused && this.isActive) {
                state = AcceptorState.PAUSED;
                try { Thread.sleep(50); } catch (InterruptedException e) {}
            }

            if (!this.isActive) break;
            state = AcceptorState.RUNNING;

            try {
                countUpOrAwaitConnection();
                SocketChannel channel = null;
                try {
                    channel = serverSocket.accept();
                } catch (IOException ioe) {
                    countDownConnection();
                    if (this.isActive) {
                        retryWait = handleExceptionWithDelay(retryWait);
                        throw ioe;
                    } else {
                        break;
                    }
                }
                retryWait = 0;

                if (this.isActive && !paused) {
                    if (!configureSocket(channel)) {
                        closeSocket(channel);
                    }
                } else {
                    closeSocket(channel);
                }
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                log.error("Accept failed", t);
            }
        }
        state = AcceptorState.ENDED;
    }

    private void closeSocket(SocketChannel channel) {
        countDownConnection();
        try { channel.socket().close(); } catch (IOException ioe) {}
        try { channel.close(); } catch (IOException ioe) {}
    }
}

ソケット受け入れ後、configureSocket (元々 setSocketOptions) が呼び出され、ここで非ブロッキング設定および Poller への登録が行われます。

protected boolean configureSocket(SocketChannel channel) {
    try {
        channel.configureBlocking(false);
        Socket sock = channel.socket();
        socketProperties.setProperties(sock);

        NioChannel nioChan = nioChannels.pop();
        if (nioChan == null) {
            SocketBufferHandler bufhandler = new SocketBufferHandler(
                    socketProperties.getAppReadBufSize(),
                    socketProperties.getAppWriteBufSize(),
                    socketProperties.getDirectBuffer());
            if (isSSLEnabled()) {
                nioChan = new SecureNioChannel(sock, bufhandler, selectorPool, this);
            } else {
                nioChan = new NioChannel(sock, bufhandler);
            }
        } else {
            nioChan.setIOChannel(channel);
            nioChan.reset();
        }
        
        getPoller0().register(nioChan);
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        return false;
    }
    return true;
}

2. Poller スレッド

Poller も Runnable を実装する NioEndpoint の内部クラスです。主な責務は、セレクタを輪詢し、読み書き準備が整ったソケットを検出することです。これにより IO のマルチプレクスが実現されます。

public Poller() throws IOException {
    this.selector = Selector.open();
}

Acceptor がソケットをacceptした後、register メソッドを介して PollerEvent が生成され、イベントキューに追加されます。

public void register(final NioChannel socket) {
    socket.setPoller(this);
    NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this);
    socket.setSocketWrapper(ka);
    ka.setPoller(this);
    ka.setReadTimeout(getSocketProperties().getSoTimeout());
    ka.setWriteTimeout(getSocketProperties().getSoTimeout());
    ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
    ka.setSecure(isSSLEnabled());
    
    PollerEvent r = eventCache.pop();
    ka.interestOps(SelectionKey.OP_READ);
    
    if (r == null) r = new PollerEvent(socket, ka, OP_REGISTER);
    else r.reset(socket, ka, OP_REGISTER);
    addEvent(r);
}

Poller の run メソッドでは、イベントキューの処理とセレクタの選択処理が交互に行われます。

public void run() {
    while (true) {
        boolean hasEvents = false;
        try {
            if (!close) {
                hasEvents = events();
                if (wakeupCounter.getAndSet(-1) > 0) {
                    keyCount = selector.selectNow();
                } else {
                    keyCount = selector.select(selectorTimeout);
                }
                wakeupCounter.set(0);
            }
            if (close) {
                events();
                timeout(0, false);
                try { selector.close(); } catch (IOException ioe) {}
                break;
            }
        } catch (Throwable x) {
            log.error("", x);
            continue;
        }

        if (keyCount == 0) hasEvents = (hasEvents | events());

        Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;
        while (iterator != null && iterator.hasNext()) {
            SelectionKey sk = iterator.next();
            NioSocketWrapper attachment = (NioSocketWrapper) sk.attachment();
            if (attachment == null) {
                iterator.remove();
            } else {
                iterator.remove();
                processKey(sk, attachment);
            }
        }
        timeout(keyCount, hasEvents);
    }
    getStopLatch().countDown();
}

イベントキュー処理 (events) では、登録イベントがセレクタへ実際に登録されます。

public boolean events() {
    boolean result = false;
    PollerEvent pe = null;
    for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++) {
        result = true;
        try {
            pe.run();
            pe.reset();
            if (running && !paused) {
                eventCache.push(pe);
            }
        } catch (Throwable x) {
            log.error("", x);
        }
    }
    return result;
}

PollerEvent の実行時、ソケットチャネルがセレクタへ登録され、読込イベントが監視対象となります。

3. Worker スレッド (SocketProcessor)

Worker スレッドは実際のリクエスト処理を担当します。Poller が準備完了したソケットを検出すると、processKey を経由して処理を委譲します。

protected void processKey(SelectionKey sk, NioSocketWrapper attachment) {
    try {
        if (close) {
            cancelledKey(sk);
        } else if (sk.isValid() && attachment != null) {
            if (sk.isReadable() || sk.isWritable()) {
                if (attachment.getSendfileData() != null) {
                    processSendfile(sk, attachment, false);
                } else {
                    unreg(sk, attachment, sk.readyOps());
                    boolean closeSocket = false;
                    if (sk.isReadable()) {
                        if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
                            closeSocket = true;
                        }
                    }
                    if (!closeSocket && sk.isWritable()) {
                        if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) {
                            closeSocket = true;
                        }
                    }
                    if (closeSocket) cancelledKey(sk);
                }
            }
        } else {
            cancelledKey(sk);
        }
    } catch (CancelledKeyException ckx) {
        cancelledKey(sk);
    } catch (Throwable t) {
        log.error("", t);
    }
}

processSocket メソッドでは、スレッドプールから SocketProcessor を取得し、タスクとして実行します。

public boolean processSocket(SocketWrapperBase<S> socketWrapper,
        SocketEvent event, boolean dispatch) {
    try {
        if (socketWrapper == null) return false;
        
        SocketProcessorBase<S> sc = processorCache.pop();
        if (sc == null) {
            sc = createSocketProcessor(socketWrapper, event);
        } else {
            sc.reset(socketWrapper, event);
        }
        Executor executor = getExecutor();
        if (dispatch && executor != null) {
            executor.execute(sc);
        } else {
            sc.run();
        }
    } catch (RejectedExecutionException ree) {
        getLog().warn("Executor failed", ree);
        return false;
    } catch (Throwable t) {
        getLog().error("Processing failed", t);
        return false;
    }
    return true;
}

スレッド間の連携フロー

Http11NioProtocol は Java NIO を基盤とし、Acceptor、Poller、Worker の 3 種類のスレッドによって構成されています。Acceptor はソケット接続を受け入れる生産者として機能し、生成されたイベントを EventQueue へ投入します。Poller は消費者としてイベントキューを監視し、セレクタを通じて IO 準備状態を確認します。準備が整った接続は、最終的に Worker スレッドプールへ渡され、ビジネスロジックの実行へと至ります。このモデルにより、高い同時接続処理能力と効率的なリソース利用が実現されています。

タグ: Tomcat JavaNIO ServletContainer NetworkArchitecture Coyote

8月7日 18:13 投稿