1

I am writing a TCP server application, and I was bench testing how many concurrent TCP sockets I can manage. After acceptance the socket is passed to a client class which spawns a separate listener thread to do work.

I was able to get 9462 active connections before the JVM failed to spawn more threads and therefore dropping any new connections.

[1805.442s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 1024k, guardsize: 0k, detached. [1805.442s][warning][os,thread] Failed to start the native thread for java.lang.Thread "Thread-15915" Exception in thread "Thread-0" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached at java.base/java.lang.Thread.start0(Native Method) at java.base/java.lang.Thread.start(Thread.java:802) at socketservertest.TCPServer.run(TCPServer.java:139) at java.base/java.lang.Thread.run(Thread.java:833)

The system still have 40% available RAM, I suspect this is a limit on the JVM settings or perhaps a restriction on the debian OS itself? If so, how can I configure the system to allow more memory?

Regards Dominick

What I have tried: Look in the java documentation and other forums for solutions, nothing helpful found.

Desired outcome: Be able to handle as many threads as possible (clients), starting goal maybe 50K connections?

Edit 07/07/2023 Sorry for lack of details, for the threadpool solution, from my understanding that works well for short live threads, not my case, as the clients are basically sensors and other embedded devices with small memory and few resources, the reason why I am not using TOMCAT and websocket and directly using TCP sockets and some prorpietary bin messaging system carries a byte for command, and the some other bytes of arg data (raw sensor reading etc.) that will be processed by the server. This connection will remain intact as long as the sensor/device is online, reason for that is to have a Real-time ability to control suck devices.

this._threadPool    = Executors.newCachedThreadPool();

This is the server main loop

/**
 * The main server thread, this listens and accepts incoming client's connections.
 */
@Override
public void run()
{
    System.out.printf("Server started on port %d%n", this.getPortN());
    
    while (!this._socket.isClosed())
    {
        System.out.println("Server awaiting new client!");
        try
        {
            Socket socket = this._socket.accept();
            socket.setKeepAlive(true);
            socket.setTcpNoDelay(true);
            socket.setSoTimeout(this._timeout);
            String uuid = UUID.randomUUID().toString();
            TCPClient client = this._clientFactory.CreateClientObject(socket, uuid, this._clients);// new TCPClient(this._clients, uuid, socket);
            this._clients.add(client);
            this._threadPool.execute(client);
            //var t = new Thread(client);
            //t.start();
            System.out.printf
            (
                "Server accepted client %s, with uuid %s\n\r", 
                socket.getRemoteSocketAddress(), 
                uuid
            );
        }
        catch (IOException ioe)
        {
            try
            {
                this.close();
            }
            catch (IOException ioe2)
            {
                ioe2.printStackTrace();
            } 
        }
    }
    System.out.println("Server stopped");
}
Dominick
  • 31
  • 7
  • I think you'd be hard pressed to find a machine that can support 50K concurrent users. I'd think about using a load balancer... Also, you could look into using non-block I/O. Requests share threads with non-blocking I/O – Isaiah van der Elst Jul 05 '23 at 21:10
  • 1
    Smaller thread stack. More memory available to JVM. Increased process/resource limits. But I think the design that needs one thread per socket is wrong. – Arfur Narf Jul 05 '23 at 21:23
  • 1
    You may try to check `cat /proc/sys/kernel/threads-max`, perhaps there is a limit you're facing. The suggestions about reducing stack size above also make sense, each thread in a Java program takes a significant amount of native memory in the JVM process for its stack, so it should help too. – Yahor Barkouski Jul 05 '23 at 22:17
  • But please, be aware that having a large number of threads might not be the most efficient way to handle many concurrent tasks due to context-switching overhead, so maybe you'd like to consider alternatives like using a thread pool – Yahor Barkouski Jul 05 '23 at 22:19
  • See https://stackoverflow.com/questions/17126998/why-java-nio-can-be-superior-to-standard-java-sockets and https://stackoverflow.com/questions/65100995/java-nio-based-scalable-non-blocking-tcp-client-server-design-recommend-way-to – tgdavies Jul 05 '23 at 23:46
  • Buy more memory. – user207421 Jul 06 '23 at 01:35
  • 1
    You don't need 50,000 threads. You're model of thread per client is broken, you should try to think of clients as tasks that are performed by threads. That way you have a thread pool to do your work. Although with [Virtual Threads](https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/util/concurrent/Executors.html#newVirtualThreadPerTaskExecutor()) you might be able to use a thread per client. – matt Jul 06 '23 at 07:47
  • 1
    You may want to consider [Project Loom](https://developer.okta.com/blog/2022/08/26/state-of-java-project-loom) in recent versions of OpenJDK. – David Conrad Jul 06 '23 at 14:20
  • [See also](https://wiki.openjdk.org/display/loom/Main) – David Conrad Jul 06 '23 at 14:29
  • I ran the server on a window 10 laptop with an 3rd gen i5 and 8GB RAM. I was able to accept and maintain 41K connection (running 6 clients on 2 other different machines). It did chew up all the RAM, but CPU utilization was between 2 and 5%, with picks at 20% while clients where joining in (bombing the poor thing), this is using TLSv1.3 which comes with a price I am sure. So I don't think the actually use of threads for doing each client work is such a problem. There is definitely a hard limitation of 9246 maximum threads Debian 11 is imposing on the JVM, and I can't see to change that. – Dominick Jul 06 '23 at 22:48
  • @Dominick I'm not sure who you're making this application for. In an enterprise environment, you'd distribute the load over a number of machines. Using NIO and virtual threads is a more efficient mechanism, but scaling out is more important. #1 I'd focus on making the application stateless so that it can scale out. Then #2 make the process efficient and make it so it can scale up. – Isaiah van der Elst Jul 06 '23 at 23:04

3 Answers3

2

The maximum number of threads you can create will be system dependent. Please check your system's capacity:

Windows: no limit

Linux: cat /proc/sys/kernel/threads-max

The stack-trace also suggests the issue may be related to memory. Every thread needs its own stack, and each thread will have data on the heap while executing. The JVM has default memory limits. Look into setting the stack and heap sizes.

Set stack size usage (-Xss)

Usage: java -Xss1024k -jar app.jar

In the error, you see pthread_create failed. Your jvm is using pthread to create native OS threads. The total stack size will be defined by your host system. It may be configurable through the OS. This property (-Xss) sets the stack size per thread. To avoid running out, try to make the size as small as possible.

Set heap size (-Xmx)

Usage: java -Xmx1024m -jar app.jar

Assuming you're holding variables in these threads, you'll also want to keep an eye on the heap. Each thread will be running some procedure, allocating memory along the way. That memory will be allocated on the heap. 50,000 threads is a lot concurrent threads. You may be using a lot of memory. Use this property (-Xmx) to increase the heap size.

Isaiah van der Elst
  • 1,435
  • 9
  • 14
  • Please read https://stackoverflow.com/questions/4967885/jvm-option-xss-what-does-it-do-exactly – tgdavies Jul 05 '23 at 23:38
  • 1
    There is no suggestion that the JVM is running out of heap, and suggesting a 1GB stack size *for each thread* is completely inappropriate. – tgdavies Jul 05 '23 at 23:40
  • The first of your suggestions will make the OPs problems much worse. The second will make them no better. I can think of no better reasons for downvoting an answer. – tgdavies Jul 06 '23 at 05:02
  • Of course you can improve your answer. Downvotes can be withdrawn. – tgdavies Jul 06 '23 at 05:04
  • the thread-max returns 63264, no idea how many of this threads are used by the system, top reports 227 total task, and plenty memory, especially if including the swap file. starting the JVM with the -Xss1024k did not change the outcome, seems to throw exceptions right around 9400 ish active sockets (each spawning a thread to listen and do stuff). I am going to run the server on window to see if this is a debian JVM implementation issue. – Dominick Jul 06 '23 at 19:20
  • @Dominick Try to set it lower, -Xss512k. Unless it's a very light process, I don't think you'll make it to 50K. But lowing the stack size should allow for more. – Isaiah van der Elst Jul 06 '23 at 21:01
1

On Linux (wsl2) I can get 30,000 threads with my default settings and reduced stack size. (-Xss150k) with this program:

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.CountDownLatch;

public class App 
{
    public static void main( String[] args ) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        int numberOfThreads = 33_000; // OK at 30_000
        Thread[] threads = new Thread[numberOfThreads];
        for (int i = 0; i < numberOfThreads; ++i) {
            threads[i] = new Thread(() -> {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            threads[i].start();
        }
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();
        System.out.println(bean.getThreadCount());
        latch.countDown();
        for (int i = 0; i < numberOfThreads; ++i) {
            threads[i].join();
        }
    }
}

The error that I see at 33,000 threads is:

[7.274s][warning][os,thread] Attempt to protect stack guard pages failed (0x00007f6bb46a2000-0x00007f6bb46a6000).
#
# A fatal error has been detected by the Java Runtime Environment:
# Native memory allocation (mprotect) failed to protect 16384 bytes for memory to guard stack pages
# An error report file with more information is saved as:
# /home/tgdavies/dev/so76628100/hs_err_pid395685.log
...
OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x00007f6bb467c000, 16384, 0) failed; error='Not enough space' (errno=12)

Which I interpreted as running out of swap. I had 4GB of swap configured. I configured 32GB of swap, but that didn't help.

The error report file has some useful tips, including:

#   JVM is running with Zero Based Compressed Oops mode in which the Java heap is
#     placed in the first 32GB address space. The Java Heap base address is the
#     maximum limit for the native heap growth. Please use -XX:HeapBaseMinAddress
#     to set the Java Heap base and to place the Java Heap above 32GB virtual address.

This blog post looks relevant: https://poonamparhar.github.io/out_of_memory_on_64_bit_platform/ but using uncompressed oops didn't help.

tgdavies
  • 10,307
  • 4
  • 35
  • 40
  • I ran your test program. tried to start the JVM with -Xss150k, I tried smaller but 136k seems to be the minimum. The program get stock and need to be forcefully shutdown after reaching the 9466 threads spawn, same results from my server. I am lost here, I don't understand what is going on. I am going to append the terminal output. – Dominick Jul 07 '23 at 15:52
  • `nick@debian:~/NetBeansProjects/TestThreadsMax/dist$ java -Xss150k -jar TestThreadsMax.jar [2.884s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 152k, guardsize: 0k, detached. [2.884s][warning][os,thread] Failed to start the native thread for java.lang.Thread "Thread-9466" Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached at java.base/java.lang.Thread.start0(Native Method)` – Dominick Jul 07 '23 at 15:58
0

I was suggested to look into project loom and the virtual threads to be able to make use of effective use of the one thread per client design (haven't explored that route yet). By digging documentation over the weekend, I was able to implement a better way to accept new connections and properly route data to the proper client without needing an individual thread per client. I was able to drastically increase the client count while lowering overhead and system memory usage by implementing the server with the NIO implementation of the socket server. With 3 test PC running a test client I was able to accept 36,036 connections and only spawning a peak of 210 workers threads managed by a thread pool (running the test client on windows you can only spawn about 16400 sockets before the SocketExeption with message maximum connection reached is thrown per PC, lot less under debian 11 from my testing).

private final ExecutorService _workers; initialized as follow this._workers = Executors.newCachedThreadPool();

Below is the new sever main loop making use of the NIO implementation with the multiplexer selector object, and a thread pool to do work when the selector returns a slectablekey. New challenge is now in implementing SSL server, since NIO from my current understanding do not offer an alternative from javax.net.ssl.SSLServerSocket.

    /**
 * The main server thread, this listens and accepts incoming client's connections.
 */
@Override
public void run()
{
    System.out.println("Awaiting new client connection!");
    while (!this._channel.socket().isClosed())
    {
        try
        {
            this._selector.select();
            
            Iterator<SelectionKey> keys = this._selector.selectedKeys().iterator();
            
            while (keys.hasNext())
            {
                SelectionKey key = keys.next();
                keys.remove();
                if (key.isAcceptable())
                {
                    ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
                    SocketChannel sc = ssc.accept();
                    sc.configureBlocking(false);
                    sc.register(this._selector, SelectionKey.OP_READ);
                    sc.socket().setTcpNoDelay(true);
                    sc.socket().setSoTimeout(this._cliTimeout);
                    var acceptor = this._factory.create(this._clients, sc);
                    var client   = acceptor.createClient(this._workers);
                    if (client != null)
                    {
                        this._clients.put(sc, client);
                        System.out.printf
                        (
                            "Client %s accepter from %s\n\r", 
                            client.getSessionID(), 
                            client.getRemoteAddress()
                        );
                    }
                    else
                    {
                        System.err.printf("Client from %s rejected!\n\r", sc.getRemoteAddress());
                        try
                        {
                            sc.socket().setSoLinger(true, 5);
                            sc.close();
                        }
                        catch (IOException ioex)
                        {
                            System.err.printf
                            (
                                "Client from %s closig error %s!\n\r", 
                                sc.getRemoteAddress(),
                                ioex.getMessage()
                            );
                        }
                    }
                    System.out.println("Awaiting new client connection!");
                }
                else if (key.isReadable())
                {
                    SocketChannel sc = (SocketChannel)key.channel();
                    var client = this._clients.get(sc);
                    if (client != null)
                        this._workers.execute(client);
                    else
                        sc.close();
                }
            }
        }
        catch (IOException ioex)
        {
            try
            {
                this.close();
            }
            catch (IOException ioex2)
            {
                ioex2.printStackTrace();
            }
        }
        catch (ClosedSelectorException csex)
        {
            // The multiplexer (selector) was closed.
            // therefore this thread can finnaly be terminated.
            break;
        }
        catch (CancelledKeyException ckex)
        {
            // Do nothing for this exception, the key operation was canceled.
            // this do not mean the server have problems or was shutdown.
        }
    }
}

Implementing SSL socket and the remote peer handlers for each connections. To get the SSL engine to work took me some real documentation digging, but got this working with the following code.

/**
 * Main listener thread for this client.
 */
@Override
public final void run()
{
    this.decrypt();
}

/**
 * Handles the hand shake.
 * @throws java.lang.InterruptedException
 * @throws java.io.IOException
 */
public void handShake() throws InterruptedException, IOException
{
    System.out.printf("SSL handShake %s... Start\n\r", this._id);
    while (true)
    {
        if (this._isHandshakeRxedHello)
        {
            HandshakeStatus status = this._sslEngine.getHandshakeStatus();
            if (null != status)
                switch (status)
                {
                    case FINISHED:
                        System.out.printf("SSL handShake %s... Finish HANDSHAKE INTERRUPTED\n\r", this._id);
                        return;
                    case NOT_HANDSHAKING:
                        if (this._isHandshakeRxedHello)
                        {
                            System.out.printf("SSL handShake %s... Finish OK\n\r", this._id);
                            this.onConnected();
                            return;
                        }
                        break;
                    case NEED_WRAP:
                        this.encrypt(ByteBuffer.allocate(0));
                        break;
                    case NEED_TASK:
                        this._sslEngine.getDelegatedTask().run();
                        break;
                    default:
                        break;
                }
        }
        else
        {
            if (this._channel.socket().isClosed())
            {
                System.out.printf("SSL handShake %s... Finish ERROR SOCKET CLOSE\n\r", this._id);
                return;
            }
        }
        Thread.sleep(100);
    }
}

/**
 * Decrypt RX cypher data, and process the plain text data.
 */
private synchronized void decrypt()
{
    try
    {
        this._rxCyper.clear();
        int len = this._channel.read(this._rxCyper);
        if (len == -1)
        {
            this.cleanUpAndClose();
        }
        else if (len > 0)
        {
            this._rxCyper.flip();
            this._rxBuffer.clear();
            this._tsLastRx = Clock.systemUTC().instant().getEpochSecond();
            SSLEngineResult results;
            while (this._rxCyper.hasRemaining())
            {
                results = this._sslEngine.unwrap(this._rxCyper, this._rxBuffer);
                if (results.bytesProduced() > 0)
                {
                    this._rxBuffer.flip();
                    this.onReveived(this._rxBuffer);
                }
                else
                    this._isHandshakeRxedHello = true;
            }
        }
    }
    catch (IOException ex)
    {
        this.cleanUpAndClose();
    }
}

/**
 * Encrypt data and TX cypher to remote peer.
 * @param data the plain text data to be sent.
 * @throws SSLException
 * @throws IOException 
 */
private synchronized void encrypt(final ByteBuffer data) throws SSLException, IOException
{
    this._txCyper.clear();
    var status = this._sslEngine.wrap(data, this._txCyper);
    if (status.getStatus() == SSLEngineResult.Status.OK)
    {
        this._txCyper.flip();
        this._channel.write(this._txCyper);
        if (status.getHandshakeStatus() == HandshakeStatus.NEED_WRAP)
            this.encrypt(data);
    }
    else if (status.getStatus() == SSLEngineResult.Status.BUFFER_OVERFLOW)
    {
        var b = ByteBuffer.allocate(this._sslEngine.getSession().getPacketBufferSize());
        status = this._sslEngine.wrap(data, b);
        if (status.getStatus() == SSLEngineResult.Status.OK)
        {
            b.flip();
            this._channel.write(b);
        }
        else
        {
            throw new SSLException("Failed to encrypt!");
        }
    }
}

This is the main loop for the NIO implementation of a remote peer connection handler without SSL. This will pull new data and do onReceive logics on a thread from the pool, this are non blocking methods.

/**
 * Main listener thread for this client.
 */
@Override
public final void run()
{
    this.rxhandler();
}

/**
 * Low level RX handler.
 */
private synchronized void rxhandler()
{
    try
    {
        this._rxBuff.clear();
        int len = this._channel.read(this._rxBuff);
        if (len == -1)
        {
            this.cleanUpAndClose();
            return;
        }
        else if (len > 0)
        {
            this._tsLastRx = Clock.systemUTC().instant().getEpochSecond();
            this._rxBuff.flip();
            this.onReceived(this._rxBuff);
        }
    }
    catch (IOException ex)
    {
        this.cleanUpAndClose();
    }
}

Finally to start one of the 4 implementation of the server. Obviously there is alot of code I left out, but this should help someone else trying to implement a non blocking server using NIO and a thread pool.

ITCPServer server = null;
    boolean run = true;
    try
    {
        if (nio)
        {
            if (ssl)
            {
                System.out.println("Starting NIO:SSL server!");
                server = NioTCPServer.create
                (
                    listeningPort, 
                    100000, 
                    new NioTCPServerTLSv13AcceptorFactory
                    (
                        new NioDebugTCPRemClientFactory(),
                        "cert.p12",
                        "password"
                    )
                );
                server.setClientTimeout(0);
            }
            else
            {
                System.out.println("Starting NIO server!");
                server = NioTCPServer.create
                (
                    listeningPort, 
                    100000, 
                    new NioTCPServerAcceptorFactory
                    (
                        new NioDebugTCPRemClientFactory()
                    )
                );
                server.setClientTimeout(0);
            }
        }
        else
        {
            if (ssl)
            {
                System.out.println("Starting SSL server!");
                server = TCPServer.createSSL
                (
                    listeningPort, 
                    100000, 
                    new DebugTCPRemClientFactory(),
                    "cert.p12",
                    "password"
                );
            }
            else
            {
                System.out.println("Starting server!");
                server = TCPServer.create
                (
                    listeningPort, 
                    100000,
                    new DebugTCPRemClientFactory()
                );
            }
        }
        Thread t = new Thread(server);
        t.start();
        System.out.printf("Server started listening at %s:%d\n\r", InetAddress.getLocalHost(), listeningPort);
Dominick
  • 31
  • 7