Tomcatの内部実装原理の解析

一、Tomcatのディレクトリ構造

bin:Tomcatに関連する実行スクリプトを格納するディレクトリ。例えば、Tomcatを起動するstart.shや停止するshutdown.shが含まれる。

conf:Tomcatの設定ファイルを格納するディレクトリ。

  • server.xml:Tomcatのサーバー設定ファイル。Tomcatの起動時にサーバーが作成され、Service、Connector、Engine、Hostsなどのコンポーネントを設定する。
  • web.xml:TomcatのServlet仕様の標準設定ファイル。Tomcatは基本的にServletとして機能し、主要な設定はDefaultServletとJspServletの設定である。
  • tomcat-users.xml:Tomcatのユーザーとロールの設定ファイル。通常は必要ない。
  • context.xml:すべてのHostのデフォルト設定情報を格納する。

logs:Tomcatが稼働中に生成するログファイルを格納するディレクトリ。

work:jspがコンパイルされて生成される.classファイルを格納するディレクトリ。

webapps:Tomcatのウェブアプリケーションを公開するディレクトリ。

lib:Tomcatが依存するjarファイルを格納するディレクトリ。

二、Tomcatのコンポーネントと全体アーキテクチャー

2.1、Tomcatのコンポーネント

Server:サーバーを表すコンポーネント。Tomcatのインスタンスを起動すると、JVMが起動し、これに対応するServerが作成される。

Service:サーバーが提供するサービスを表すコンポーネント。デフォルトのサービス名はCatalina。各Serviceは関連するEngineと一組のConnectorを含む。

Connector:接続器を表すコンポーネント。指定されたポートを監視し、指定された通信プロトコルのリクエストを受信し、関連するEngineに転送する。Engineの処理結果をクライアントに返す。

Engine:Servletエンジンを表すコンポーネント。EngineはConnectorから転送されたリクエストをホスト名に基づいて適切なHostに転送する。

Host:ホストを表すコンポーネント。Engineから転送されたリクエストを処理する。

Context:Webアプリケーションを表すコンポーネント。Hostが処理したリクエストのパスに基づいて適切なコンテキストを選択する。

2.2、Tomcatのコンポーネント設定

Serverの設定

タグ<Server>を使用し、ポート番号やシャットダウンコマンドを設定する。例:

<Server port="8005" shutdown="SHUTDOWN">

この設定は、Tomcatサービスが8005番ポートを監視し、SHUTDOWNコマンドを使用してTomcatを停止することを示す。複数のTomcatインスタンスを同じ物理マシンで起動する場合は、異なるポート番号を指定する必要がある。

Serviceの設定

タグ<Service>を使用し、サービス名を設定する。例:

<Service name="Catalina">

Connectorの設定

タグ<Connector>を使用し、接続器の設定を行う。例:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000"
                  maxPostSize="-1" URIEncoding="UTF-8"
                  maxThreads="200"  redirectPort="8443" />

Connectorの主な設定項目:

設定項目 説明 デフォルト値
port 接続器が監視するポート番号 8080
protocol 使用するプロトコル 選択肢:BIO、NIO、NIO2、APR
connectionTimeout 接続タイムアウト時間(ミリ秒) 60000
maxThreads 最大同時処理スレッド数 200
minSpareThreads 最小のスペアスレッド数 10
acceptCount 待ち行列の最大数 100
maxConnections 最大接続数 NIOとNIO2では10000、APRでは8192
maxHttpHeaderSize HTTPリクエストヘッダの最大サイズ(バイト) 8192(8KB)
tcpNoDelay _TCP_NO_DELAY_属性の設定 true
compression gzip圧縮の有効化 off(無効)
compressionMinSize 圧縮の最小サイズ(バイト) 2048(2KB)
URIEncoding URLエンコーディングの文字エンコーディングを設定 UTF-8
redirectPort HTTPSリダイレクトポート 443

Engineの設定

タグ<Engine>を使用し、サーブレットエンジンを設定する。例:

<Engine name="Catalina" defaultHost="localhost">

defaultHost:Tomcatがサポートする完全修飾ドメイン名(FQDN)の仮想ホスト。仮想ホストをEngine内で定義するため、接続器が非定義の仮想ホスト宛てのリクエストを受けた場合、このdefaultHostに転送される。

Hostの設定

タグ<Host>を使用し、ホストまたは仮想ホストを定義する。例:

<Host name="local" appBase="webapps" unpackWARS="true" autoDeploy="true">

appBase:非バンドルされたWebアプリケーションやWARファイルを格納するディレクトリ。

unpackWARS:WARファイルを展開して使用するかどうか。

autoDeploy:Tomcatが稼働中にappBaseディレクトリに配置されたアプリケーションを自動的にデプロイするかどうか。

Contextの設定

タグ<Context>を使用し、Webアプリケーションを定義する。例:

<Context path="/order" docBase="/web/order">

path:Webアプリケーションのルートパス。docBase:Webアプリケーションの物理パス。

2.3、Tomcatの全体アーキテクチャー

Tomcatの全体構成は以下のようになる。Serverインスタンスを起動すると、Serviceが作成され、各ServiceにはEngineとConnectorが関連付けられる。Connectorはポートを監視し、リクエストをEngineに転送する。Engineはリクエストのホスト名に基づいてHostを選択し、Hostはリクエストパスに基づいてContextを選択する。

2.4、Tomcatのリクエスト処理フロー

  1. クライアントがHTTPリクエストを送信する。例:http://localhost:8080/order/createOrder
  2. 8080番ポートを監視しているConnectorがリクエストを受信し、Engineに転送する。
  3. Engineはリクエストのホスト名を取得し、定義されたHostと一致するものを選択する。
  4. 選択されたHostはリクエストパスを解析し、適切なContextを選択する。
  5. Contextは具体的なServletを検索し、HttpServletRequestとHttpServletResponseオブジェクトを作成し、Servletの処理メソッドを呼び出す。
  6. Servletがリクエストを処理し、結果をHttpServletResponseに返す。
  7. Hostがリクエストを処理し、結果をConnectorに返す。
  8. ConnectorがEngineから結果を受け取り、クライアントに返す。

三、Tomcatの実装原理

3.1、Tomcatの起動原理

Tomcatの起動に関連するスクリプトはbinディレクトリ内に格納されている。起動に関連する主なスクリプトはstartup.sh、catalina.sh、setclasspath.shである。

startup.sh(Windowsではstartup.bat)はcatalina.shを呼び出して起動を開始する。catalina.shは環境変数を設定し、setclasspath.shを呼び出す。また、JAVA_OPTSを使用してアプリケーションの起動パラメータを設定することもできる。

Tomcatの起動クラスはorg.apache.catalina.startup.Bootstrap이며、mainメソッドが起動を担当する。起動の核心ロジックは以下のような流れになる。

public static void main(String args[]) {
    synchronized (daemonLock) {
        if (daemon == null) {
            Bootstrap bootstrap = new Bootstrap();
            try {
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                return;
            }
            daemon = bootstrap;
        } else {
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }
    }

    try {
        String command = "start";
        if (args.length > 0) {
            command = args[args.length - 1];
        }

        if (command.equals("startd")) {
            args[args.length - 1] = "start";
            daemon.load(args);
            daemon.start();
        } else if (command.equals("stopd")) {
            args[args.length - 1] = "stop";
            daemon.stop();
        } else if (command.equals("start")) {
            daemon.setAwait(true);
            daemon.load(args);
            daemon.start();
            if (daemon.getServer() == null) {
                System.exit(1);
            }
        } else if (command.equals("stop")) {
            daemon.stopServer(args);
        } else if (command.equals("configtest")) {
            daemon.load(args);
            if (daemon.getServer() == null) {
                System.exit(1);
            }
            System.exit(0);
        }
    } catch (Throwable t) {
        System.exit(1);
    }
}

起動の核心はBootstrapのinitメソッドとstartメソッドである。initメソッドはクラスローダーを初期化し、Tomcatの主要コンポーネントを設定する。startメソッドは起動パラメータに基づいて起動、停止、設定テストを行う。

3.1.1、Catalinaの初期化

Bootstrapのinitメソッドは以下のようになる。

public void init() throws Exception {
    initClassLoaders();
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    SecurityClassLoad.securityClassLoad(catalinaLoader);
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();
    Method method = startupInstance.getClass().getMethod("setParentClassLoader", new Class[]{ClassLoader.class});
    method.invoke(startupInstance, new Object[]{sharedLoader});
    catalinaDaemon = startupInstance;
}

このメソッドはTomcatの主要コンポーネントであるCatalinaを初期化する。loadメソッドは以下のようになり、server.xmlを解析し、Tomcatのコンポーネントを初期化する。

public void load() {
    initDirs();
    initNaming();
    File file = configFile();
    Digester digester = createStartDigester();
    try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getServerXml()) {
        InputStream inputStream = resource.getInputStream();
        InputSource inputSource = new InputSource(resource.getURI().toURL().toString());
        inputSource.setByteStream(inputStream);
        digester.push(this);
        digester.parse(inputSource);
    } catch (Exception e) {
    }
    getServer().setCatalina(this);
    getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
    initStreams();
    try {
        getServer().init();
    } catch (LifecycleException e) {
    }
}

Serverのinitメソッドは、Service、Connector、Engine、Host、Contextなどのコンポーネントを初期化する。

3.1.2、Catalinaの起動

Catalinaのstartメソッドは以下のようになる。

public void start() {
    try {
        String command = "start";
        if (args.length > 0) {
            command = args[args.length - 1];
        }
        if (command.equals("start")) {
            daemon.load(args);
            daemon.start();
            if (daemon.getServer() == null) {
                System.exit(1);
            }
        }
    } catch (Throwable t) {
        System.exit(1);
    }
}

Serverのstartメソッドは、Service、Connector、Engineなどのコンポーネントを起動する。

3.1.3、Engineの起動

EngineのstartInternalメソッドは以下のような流れになる。

protected void startInternal() throws LifecycleException {
    super.initInternal();
    if (engine != null) {
        engine.init();
    }
    for (Executor executor : findExecutors()) {
        if (executor instanceof JmxEnabled) {
            ((JmxEnabled) executor).setDomain(getDomain());
        }
        executor.init();
    }
    mapperListener.init();
    synchronized (connectorsLock) {
        for (Connector connector : connectors) {
            connector.init();
        }
    }
}

Connectorのinitメソッドは、ProtocolHandlerを初期化し、ポートを監視する。

3.1.4、Connectorの起動

ConnectorのstartInternalメソッドは、ProtocolHandlerのstartメソッドを呼び出す。ProtocolHandlerのstartメソッドは、具体的な通信プロトコルを処理するEndpointを起動する。

public void startInternal() {
    int errorDelay = 0;
    while (true) {
        try {
            if (endpoint.isRunning()) {
                break;
            }
            errorDelay = handleExceptionWithDelay(errorDelay);
        } catch (Throwable t) {
            log.error(sm.getString("endpoint.start.fail"), t);
        }
    }
}

Endpointの起動は、ポートの監視とリクエストの受信を担当する。

3.2、Tomcatのリクエスト処理原理

  1. クライアントがHTTPリクエストを送信する。
  2. Connectorがリクエストを受信し、Engineに転送する。
  3. Engineがホスト名を解析し、適切なHostを選択する。
  4. Hostがパスを解析し、適切なContextを選択する。
  5. ContextがServletを検索し、HttpServletRequestとHttpServletResponseオブジェクトを作成し、Servletの処理メソッドを呼び出す。
  6. Servletがリクエストを処理し、結果をHttpServletResponseに返す。
  7. Hostがリクエストを処理し、結果をConnectorに返す。
  8. ConnectorがEngineから結果を受け取り、クライアントに返す。

四、Tomcatの高度な設計

4.1、Tomcatの設定最適化

1. JVM設定最適化

TomcatはJVMプロセスとして稼働するため、JVMの設定を最適化することでTomcatのパフォーマンスを向上させることができる。

2. Connector設定最適化

Connectorの設定項目:

  • maxThreads:最大同時処理スレッド数。
  • acceptCount:待ち行列の最大数。
  • maxConnections:最大接続数。
  • executor:スレッドプール設定。

3. IO設定最適化

TomcatはBIO、NIO、APRの通信モードをサポートしている。NIOとAPRはより高い通信効率を提供する。

4.2、Tomcatの非同期処理

Tomcatは非同期Servletをサポートしている。非同期Servletは、リクエスト受信とビジネスロジックの処理を分離することで、Tomcatのスレッドプールを効率的に利用することができる。

例:

@WebServlet(name = "asyncServlet", urlPatterns = "/async", asyncSupported=true)
public class AsyncServlet extends HttpServlet {

    ExecutorService executorService = Executors.newFixedThreadPool(20);

    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse res) {
        try {
            final AsyncContext context = req.startAsync();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000L);
                        context.getResponse().getWriter().println("非同期処理完了");
                        context.complete();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

非同期Servletの設定は、@WebServletのasyncSupported属性をtrueに設定する必要がある。

4.3、Tomcatの設計

Tomcatの設計は以下のコンポーネントを含む。

  1. 接続器:ポート監視、リクエスト受信。
  2. コンパイラ:リクエストをプロトコルに基づいて解析する。
  3. ビジネスプロセッサー:リクエストを処理する。
  4. パスマッパー:パスとビジネスプロセッサーをマッピングする。
  5. リクエストディスパッチャー:リクエストを適切なビジネスプロセッサーに転送する。

Nettyを用いてTomcatを設計する例:

public class TomcatServer {

    static Map<String, UrlMapping> controllerMap = new HashMap<>();
    static ExecutorService executorService = Executors.newFixedThreadPool(50);

    public static void main(String[] args) throws InterruptedException {
        ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        parseMapping(context);
        startServer(8000);
    }

    private static void parseMapping(ApplicationContext context) {
        for (String beanName : context.getBeanNamesForAnnotation(Controller.class)) {
            Object bean = context.getBean(beanName);
            RestController controller = bean.getClass().getAnnotation(RestController.class);
            String basePath = controller.value();
            Method[] methods = bean.getClass().getMethods();
            for (Method method : methods) {
                RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                if (requestMapping != null) {
                    String methodPath = requestMapping.value()[0];
                    UrlMapping urlMapping = new UrlMapping();
                    urlMapping.setController(bean);
                    urlMapping.setMethod(method);
                    urlMapping.setUrl(basePath + "/" + methodPath);
                    controllerMap.put(urlMapping.getUrl(), urlMapping);
                }
            }
        }
    }

    private static void startServer(int port) throws InterruptedException {
        ServerBootstrap bootstrap = new ServerBootstrap();
        int processorsCount = Runtime.getRuntime().availableProcessors();
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(2);
        NioEventLoopGroup workGroup = new NioEventLoopGroup(processorsCount * 2);
        bootstrap.group(bossGroup, workGroup);
        bootstrap.channel(NioServerSocketChannel.class);
        bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
        bootstrap.handler(new LoggingHandler());
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                ChannelPipeline pipeline = socketChannel.pipeline();
                pipeline.addLast(new HttpServerCodec());
                pipeline.addLast(new ChunkedWriteHandler());
                pipeline.addLast(new HttpObjectAggregator(64 * 1024));
                pipeline.addLast(new TomcatHttpHandler());
            }
        });
        bootstrap.bind(port).sync();
    }

    static class TomcatHttpHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            if (msg instanceof FullHttpRequest) {
                FullHttpRequest request = (FullHttpRequest) msg;
                QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
                Map<String, Object> params = new HashMap<>();
                decoder.parameters().entrySet().forEach(v -> {
                    params.put(v.getKey(), v.getValue().get(0));
                });
                String url = request.uri().replaceFirst("/", "");
                String path = url.substring(0, url.indexOf("?"));
                UrlMapping mapping = controllerMap.get(path);
                if (mapping != null) {
                    executorService.execute(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                Object result = mapping.getMethod().invoke(mapping.getController(), params.values().toArray());
                                FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
                                response.headers().set("Content-Type", "application/json");
                                response.content().writeBytes(JSON.toJSONString(result).getBytes());
                                ctx.writeAndFlush(response);
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            } catch (InvocationTargetException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                }
            } else {
                FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND);
                response.headers().set("Content-Type", "text/html");
                ctx.writeAndFlush(response);
            }
        }
    }
}

この例では、Nettyを使用して簡易的なTomcatサーバーを設計している。HTTPリクエストを受信し、パスに基づいて適切なControllerを選択し、ビジネスロジックを実行する。

タグ: Tomcat Servlet NIO APR HTTP/1.1

7月23日 23:04 投稿