1. ホーム

Undertow、Tomcat、Jettyサーバーの違いについて

2022-03-15 07:20:02
<パス

Undertow、Tomcat、Jettyのサーバー構成の詳細とパフォーマンステスト

	This article is a study of the more popular Java framework spring boot triggered by the thinking, I hope you can learn together.
	undertow, jetty and tomcat can be said to be the javaweb project today's three hottest servers, tomcat is a heavyweight server under apache, needless to say has a long history, withstand the test of time. This article will analyze and test jetty and undertow servers to compare their performance. This article will compare the performance of the two servers by analyzing and testing them.
	It is worth mentioning that both jetty and undertow are highly concurrent and lightweight servers based on NIO, supporting servlet 3.1 and websockets, so it is important to understand what NIO is first.


NIO(ノンブロッキング入出力)

  • チャンネル
  • セレクタ
  • バッファー
  • アクセプター



      クライアントとセレクタは、バッファの読み取りとデータ劉翔、チャネルチャネルを介してデータの流れについて懸念していないデータを書き込むようにします。アクセプタは、すべての接続チャネルを受け入れ、チャネルにそれらを登録するための責任があります。全体のプロセスは、クライアントとサーバー間のノンブロッキング、すなわち非同期です。



    JettyとUndertowの主な構成



      サーバ側で気になるのは、接続タイムアウトやソケットタイムアウト、タスクタイムアウトの設定ではなく、ワーカスレッドやI/Oスレッドの割り当てなどのスレッドプールの設定です。jettyはこの点で少し恣意的で、グローバルスレッドプールqueuedThreaPoolを使っていますが、スレッドの最小数は8、最大数は200になっているようです。Acceptor スレッドのデフォルトは 1、Selector スレッドのデフォルトは 2 です。また、undertowはより合理的で、Acceptorは再帰ループで登録し、I/Oに使うスレッドはCPUのデフォルトのスレッド数、ワークスレッドはCPUのスレッド数×8とします。



      サーバーの場合、スレッドをどのように割り当てるかで、同時実行性能を向上させることができます。そこで、以下では、2つのサーバーの詳細な構成について分析します。



      サーバーがチャンネル登録を実装する方法




    Jettyでは、アクセプタのスレッド数をデフォルトで1に設定することができます。詳細な実装は以下の通りです。
protected void doStart() throws Exception {
        if(this._defaultProtocol == null) {
            throw new IllegalStateException("No default protocol for " + this);
        } else {
            this._defaultConnectionFactory = this.getConnectionFactory(this._defaultProtocol);
            if(this._defaultConnectionFactory == null) {
                throw new IllegalStateException("No protocol factory for default protocol \'" + this._defaultProtocol + "\' in " + this);
            } else {
                SslConnectionFactory ssl = (SslConnectionFactory)this.getConnectionFactory(SslConnectionFactory.class);
                if(ssl ! = null) {
                    String i = ssl.getNextProtocol();
                    ConnectionFactory a = this.getConnectionFactory(i);
                    if(a == null) {
                        throw new IllegalStateException("No protocol factory for SSL next protocol: \'" + i + "\' in " + this);
                    }
                }

                super.doStart();
                this._stopping = new CountDownLatch(this._acceptors.length);

               

 

~~for(int var4 = 0; var4 < this._acceptors.length; ++var4) {
                    AbstractConnector.Acceptor var5 = new AbstractConnector.Acceptor(var4, null);
                    this.addBean(var5);
                    this.getExecutor().execute(var5);
                }~~ 

 

         this.LOG.info("Started {}", new Object[]{this});
            }
        }
    }


   "~~"がスレッドラッピングを解除したところが、すべてのアクセプターのスレッドが開始される場所であり、ここではそのスレッドがどのように実行されるかを詳しく説明します。

public void run() {
            Thread thread = Thread.currentThread();
            String name = thread.getName();
            this._name = String.format("%s-acceptor-%d@%x-%s", new Object[]{name, Integer.valueOf(this._id), Integer.valueOf(this.hashCode ()), AbstractConnector.this.toString()});
            thread.setName(this._name);
            int priority = thread.getPriority();
            if(AbstractConnector.this._acceptorPriorityDelta ! = 0) {
                thread.setPriority(Math.max(1, Math.min(10, priority + AbstractConnector.this._acceptorPriorityDelta)));
            }

            AbstractConnector stopping = AbstractConnector.this;
            synchronized(AbstractConnector.this) {
                AbstractConnector.this._acceptors[this._id] = thread;
            }

            while(true) {
                boolean var24 = false;

                try {
                    var24 = true;
                    if(!AbstractConnector.this.isRunning()) {
                        var24 = false;
                        break;
                    }

                    try {
                        Lock stopping2 = AbstractConnector.this._locker.lock();
                        Throwable var5 = null;

                        try {
                            if(!AbstractConnector.this._accepting && AbstractConnector.this.isRunning()) {
                                AbstractConnector.this._setAccepting.await();
                                continue;
                            }
                        } catch (Throwable var41) {
                            var5 = var41;
                            throw var41;
                        } finally {
                            if(stopping2 ! = null) { if(var5 !
                                if(var5 ! = null) {
                                    try {
                                        stopping2.close();
                                    } catch (Throwable var38) {
                                        var5.addSuppressed(var38);
                                    }
                                } else {
                                    stopping2.close();
                                }
                            }

                        }
                    } catch (InterruptedException var43) {
                        continue;
                    }

                    try {
                        AbstractConnector.this.accept(this._id);
                    } catch (Throwable var40) {
                        if(!AbstractConnector.this.handleAcceptFailure(var40)) {
                            var24 = false;
                            break;
                        }
                    }
                } finally {
                    if(var24) {
                        thread.setName(name);
                        if(AbstractConnector.this._acceptorPriorityDelta ! = 0) {
                            thread.setPriority(priority);
                        }

                        AbstractConnector stopping1 = AbstractConnector.this;
                        synchronized(AbstractConnector.this) {
                            AbstractConnector.this._acceptors[this._id] = null;
                        }

                        CountDownLatch stopping4 = AbstractConnector.this._stopping;
                        if(stopping4 ! = null) {
                            stopping4.countDown();
                        }

                    }
                }
            }

           

  while ループが確立されたすべての接続チャネルをリッスンし、チャネルを SelectorManager にサブミットしているのがわかります。



Undertowではこれを扱わず、ChannelListener APIを使用して、Selectorをチャンネルに登録することでイベント通知を行います。Channelが作成されると、I/Oスレッドが与えられ、ChannelListenerのすべてのコールバックメソッドが実行されます。

SelectionKey registerChannel(AbstractSelectableChannel channel) throws ClosedChannelException {
    if(currentThread() == this) {
        return channel.register(this.selector, 0);
    } else if(THREAD_SAFE_SELECTION_KEYS) {
        SelectionKey task1;
        try {
        

    

~~task1 = channel.register(this.selector, 0);~~ 

        } finally {
            if(this.polling) {
                this.selector.wakeup();
            }

        }

        return task1;
    } else {
        WorkerThread.SynchTask task = new WorkerThread;
        this.queueTask(task);

        SelectionKey var3;
        try {
            this.selector.wakeup();
            var3 = channel.register(this.selector, 0);
        } finally {
            task.done();
        }

        return var3;
}


つまり、設計アーキテクチャに関係なく、どちらのサーバーもNIO実装に基づいており、IPのハッシュによってすべてのI/Oオペレーションを実行するセレクタを持ち、チャンネルを異なるワークスレッドまたはセレクタマネージャに入れ、特定の処理はワークスレッドプールで行われることが分かります。なので、jettyのセレクタの数とundertowのIOThreadsの数は、どちらもセレクタというかI/O操作をするために使われるスレッドの数だと思います。違うのはjettyのグローバルスレッドプールです。そして、2つのサーバーの性能の良し悪しを決めるのは、読み取りと書き込みの効率だけでなく、ライフサイクルプロセス管理など、容量を運ぶ2つのサーバーの場合です。結局のところ、仕事のために使用される郡のすべてのオーバーヘッドは、ビジネス、すべての個人的な感情にある。I/O操作、管理、船は、2つのサーバーのメリットを決定します。

JettyとUndertowの圧力テスト解析

準備のためのツール

  • 圧力試験用包囲網
  • モニタリング用VisualVm

プロジェクトの準備。



Jetty:acceptors=1、selectors=2、最小・最大スレッド数=200

Undertow: work_threads=200,io_threads=2です。

ピエゾメトリックグラデーション。



siege -c 50 -r 2000 -t 2 --log=/Users/maybo/joinit_200.log http://127.0.0.1:8080/test

siege -c 80 -r 2000 -t 2 --log=/Users/maybo/joinit_200.log http://127.0.0.1:8080/test

siege -c 100 -r 2000 -t 2 --log=/Users/maybo/joinit_200.log http://127.0.0.1:8080/test

テスト結果です。

<テーブル サーバー ヒット 成功率 スループット 平均経過時間 Jetty 11488 100% 96.25 trans/sec 0.00秒 ジェット機 18393 100% 153.92 トランス/秒 0.00秒 ジェット機 21484 99.99% 179.51 trans/sec 0.01秒 アンダートウ 11280 100% 94.02 trans/sec 0.00秒 アンダートウ 19442 100% 163.35 trans/sec 0.01秒 アンダートウ 23277 100% 195.54 trans/sec 0.01秒 トムキャット 10845 100% 90.95 trans/sec 0.02秒 トムキャット 21673 99.98% 181トランス/秒 0.01秒 トムキャット 25084 99.98% 209.10 trans/sec 0.01秒

Undertowのスループットは高負荷時にJettyより高く、JettyとUndertowの成功率の差は圧力が高くなるにつれて広がる。Undertowのスループットは高負荷時にはJettyよりも高く、JettyとUndertowの成功率の差は圧力が高くなるにつれて広がります。一方、サーバーの処理能力はそれほど高くない負荷ではほぼ同じで、JettyがUndertowよりもわずかに高くなります。



今回のテストでは、ネットワークで転送するデータ量が少なすぎたため、データ転送量を連続的に増やして負荷を観測することはしませんでした。私自身は、1台のサーバーのI/Oをテストするだけでなく、データ転送量を変化させることで、大きなデータテキストの高負荷時の3台のサーバーのパフォーマンスを見ることにしたのです。

大容量データテスト

1892byteのレスポンスデータを用いて3台のサーバーのパフォーマンスをテストし、オープンスレッドの実行状況をグラフにしたものです。



アンダートウ







桟橋です。







トムキャット







  実験の結果、UndertowとTomcatの負荷能力は非常に近いが、Undertowの方が優れており、Jettyは十分とは言い難いことがわかった。UndertowとTomcatの違いは、UndertowがI/Oに使用するスレッド数を調整できるのに対し、Tomcatはそうではなく、少なくともスプリングブートではそうなっていないため、負荷容量が制限されることです。また、Jettyはグローバルな共有スレッドプールのため、SelectorやAcceptorがブロックする状況が発生し、I/O操作が制限されることになります。しかし、あまりにも重い負荷の場合には、作業スレッドは、スループットを向上させるプログラムを処理するために、より多くの占有リソースを持っていることができるという利点があります。しかし、全体としてはその差は小さい。

結論



  どんなサーバーにも、それぞれの良さがあります。開発者にとっては、どのような技術点が問題解決に最も役立つかを選択することが目標です。