apache / mina-sshd

Apache MINA sshd is a comprehensive Java library for client- and server-side SSH.
https://mina.apache.org/sshd-project/
Apache License 2.0
847 stars 353 forks source link

how to set apache mina sshd io thread count? #518

Open coderZoe opened 2 weeks ago

coderZoe commented 2 weeks ago

Version

sshd-core 2.12.0

Bug description

Hello,

I am using sshd-core to interact with some hardware devices. My code is structured as follows:

public abstract class BaseSshSessionSender extends AbstractSessionSender {
    protected SshClient client;

    protected ClientSession session;
    protected ClientChannel channel;
    protected ScheduledExecutorService scheduledExecutorService;

    public BaseSshSessionSender(Device device, SenderMetaData metaData, ScheduledExecutorService scheduledExecutorService) {
        super(device, metaData);
        this.scheduledExecutorService = scheduledExecutorService;
    }

    protected void doConnect() throws Exception {
        this.client = SshClient.setUpDefaultClient();
        this.client.start();
        this.client.setGlobalRequestHandlers(Arrays.asList(KeepAliveHandler.INSTANCE, NoMoreSessionsHandler.INSTANCE));

        ConnectFuture connectFuture = client.connect(metaData.getUserName(), metaData.getIp(), metaData.getPort())
                .verify(metaData.getConnectTimeout());
        if (connectFuture.isConnected()) {
            this.session = connectFuture.getSession();
        } else {
            this.status = SenderStatus.DISCONNECT;
            throw new ConnectException(connectFuture.getException().getMessage());
        }

        this.session.addPasswordIdentity(metaData.getPassword());
        AuthFuture authFuture = this.session.auth().verify(metaData.getAuthTimeout());
        if (!authFuture.isSuccess()) {
            throw new AuthException("auth fail:" + authFuture.getException().getMessage());
        }
        this.channel = createChannel();
        this.status = SenderStatus.CONNECTING;
    }

    public ClientChannel createChannel() throws Exception {
        ClientChannel channel = this.session.createChannel(ClientChannel.CHANNEL_SHELL);
        channel.setStreaming(ClientChannel.Streaming.Async);
        channel.open().verify();
        channel.getAsyncOut().read(new ByteArrayBuffer(BUFFER_PIPE_SIZE)).addListener(new ChannelResponseListener(this));
        return channel;
    }
}

For each device, I create a BaseSshSessionSender object to communicate, maintaining a long connection with the device. However, I've noticed that creating such a device connection spawns cpu*2 threads. For instance, with 100 devices on an 8-core CPU, maintaining these connections requires 1600 threads, which is a substantial overhead. Given that my communication with these devices is infrequent, I do not need so many threads.

I am looking for a way to specify the number of I/O threads during connection setup, similar to how one might use bootstrap.group(new NioEventLoopGroup(1)) in Netty. Does sshd-core support this configuration? If so, how can I set it up?

Thank you for your assistance.

Actual behavior

each device connection spawns cpu*2 threads

Expected behavior

specify the number of I/O threads during connection setup

Relevant log output

No response

Other information

No response

coderZoe commented 2 weeks ago

well,I have discovered another issue: there is a memory leak when reconnecting the client. Here is an example:

I register a listener for each session to monitor if the session disconnects. Once it disconnects, I attempt to reconnect proactively.

this.session.addSessionListener(new SessionListener() {
    @Override
    public void sessionClosed(Session session) {
        synchronized (BaseSshSessionSender.this){
            if(BaseSshSessionSender.this.status != SenderStatus.DISCONNECT && BaseSshSessionSender.this.status != SenderStatus.DEATH){
                BaseSshSessionSender.this.status = SenderStatus.DISCONNECT;
                BaseSshSessionSender.this.reConnect(true);
            }
        }
    }
});
protected void reConnect(boolean immediately){
    this.scheduledExecutorService.schedule(this::connect, immediately?0L:metaData.getReconnectInterval().getSeconds(), TimeUnit.SECONDS);
}

public void connect() {
    try {
        if(this.status != SenderStatus.DEATH){
            this.doConnect();
        }
    } catch (Exception e) {
        this.status = SenderStatus.DISCONNECT;
        this.reConnect(false);
    }
}

Assuming each client currently uses cpu*2 threads, a reconnection will use a new client, which creates another cpu*2 threads. However, the previously created cpu*2 threads for the client are not reclaimed. This means each reconnection increases the number of threads by cpu*2, causing the total number of threads to keep growing.

I tested this with Jprofiler, and found that at service startup, there were around 1200 sshd threads, but after about 5 minutes, there were nearly 2000 threads (because I intentionally connected some clients to unreachable networks, triggering reconnections). This situation clearly leads to a memory leak. How can this be resolved?

Thank you for your assistance.

JinHeap commented 2 days ago

SshClient.setUpDefaultClient() will bulid new client, then every time call doConnect it is new client. But as example should stop old client. maybe you can move client init (setUpDefaultClient) to BaseSshSessionSender instance init, but you should also call stop.

https://github.com/apache/mina-sshd/blob/04ae57b7c55961c271d9fa38990ae1d5982661cc/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java#L219