You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于SSH的Exchange Server远程命令执行性能优化问题

问题:Java JSCH实现SSH中转Exchange PowerShell操作性能极差

场景背景

需在非Exchange域的Unix机器(如AWS EC2实例)上用Java客户端对接本地Exchange Server执行CRUD操作,由于Unix客户端不支持基于WinRM/WSMan的PowerShell远程管理,采用以下方案:

  • 通过SSH连接域内Windows主机Host-A
  • 在Host-A上通过PowerShell远程连接Exchange Server

终端验证性能

MacOS终端手动操作耗时约13秒:

> ssh Administrator@172.18.52.49
# 连接Host-A后打开PowerShell
> pwsh 
> $session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://HOSTB.EX2019.lab/PowerShell -Authentication Kerberos -Credential $Credentials;
> Import-PSSession $session -DisableNameChecking;
> Measure-Command { Get-Mailbox -Resultsize Unlimited | Out-Host }

执行耗时结果:

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 13

JSCH实现的性能问题

用JSCH实现相同逻辑耗时约420秒(7分钟),核心代码如下:

核心Java代码

Session session = new JSch().getSession("Administrator", "172.18.52.49", 22);
session.setConfig("StrictHostKeyChecking", "no");
session.setPassword("Password");

long startTime = System.currentTimeMillis();
session.connect();

ChannelExec channel = (ChannelExec)session.openChannel("exec");
StringBuilder commandBuilder = new StringBuilder();

commandBuilder.append("$username = \\\"Administrator\\\"; ");
commandBuilder.append("$password = \\\"Password\\\"; ");
commandBuilder.append("$encryptedPassword = ConvertTo-SecureString -String $password -AsPlainText -Force; ");
commandBuilder.append("$Credentials = New-Object System.Management.Automation.PSCredential ($username, $encryptedPassword); ");
commandBuilder.append("$session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://HOSTB.EX2019.lab/PowerShell -Authentication Kerberos -Credential $Credentials; ");
commandBuilder.append("Import-PSSession $session -DisableNameChecking -CommandName Get-Mailbox; ");
commandBuilder.append("Measure-Command {Get-Mailbox -Resultsize Unlimited | Out-host}; ");

channel.setCommand(
            "pwsh -command \"" + commandBuilder + "\"");

ByteArrayOutputStream outputBuffer = new ByteArrayOutputStream();
ByteArrayOutputStream errorBuffer = new ByteArrayOutputStream();

InputStream in = channel.getInputStream();
InputStream err = channel.getExtInputStream();

channel.connect();

// 输出读取逻辑
byte[] tmp = new byte[65536];
while (true) {
    while (in.available() > 0) {
        int i = in.read(tmp, 0, 65536);
        if (i < 0) break;
        outputBuffer.write(tmp, 0, i);
    }
    while (err.available() > 0) {
        int i = err.read(tmp, 0, 65536);
        if (i < 0) break;
        errorBuffer.write(tmp, 0, i);
    }
    if (channel.isClosed()) {
        if ((in.available() > 0) || (err.available() > 0)) 
             continue;
        System.out.println("exit-status: " + 
                 channel.getExitStatus());
        break;
    }

    Thread.sleep(1000);
}

System.out.println("output: " + outputBuffer.toString("UTF-8"));
System.out.println("error: " + errorBuffer.toString("UTF-8"));

channel.disconnect();
session.disconnect();

long endTime = System.currentTimeMillis();

System.out.println("Total time between session connection and disconnection with Get-Mailbox command: " + (endTime - startTime) + "ms");

已尝试优化:开启SSH压缩、调整缓冲区大小、改用shell会话,均无明显效果。


原因分析

  1. 输出读取逻辑的阻塞延迟:代码中每次循环后强制Thread.sleep(1000),每一秒才轮询一次输出,当Get-Mailbox返回大量数据时,会累积大量等待时间,这是性能极差的核心原因。
  2. PowerShell非交互模式开销:用pwsh -command一次性执行所有命令时,PowerShell以非交互式模式运行,部分Exchange PowerShell cmdlet在该模式下会有额外的初始化或输出格式化开销。
  3. 命令字符串转义损耗:拼接的命令字符串包含大量转义字符,可能导致PowerShell解析时出现隐性性能损耗,或触发不必要的调试/日志逻辑。

优化方案

1. 重构输出读取逻辑,移除固定延迟

将轮询+长sleep的方式改为异步阻塞读取,避免主动等待:

// 替换原有的输出读取循环
new Thread(() -> {
    try {
        int len;
        while ((len = in.read(tmp)) != -1) {
            outputBuffer.write(tmp, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();

new Thread(() -> {
    try {
        int len;
        while ((len = err.read(tmp)) != -1) {
            errorBuffer.write(tmp, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}).start();

// 用短间隔轮询替代1秒长延迟
while (!channel.isClosed()) {
    Thread.sleep(100);
}

// 确保所有输出都被读取
Thread.sleep(200);

2. 改用PowerShell脚本文件执行

将所有PowerShell逻辑写入.ps1文件,上传到Host-A后执行,避免命令字符串转义和非交互模式的潜在问题:

// 上传脚本文件到Host-A
ChannelSftp sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
sftpChannel.put(new ByteArrayInputStream(
    "$username = \"Administrator\"\n" +
    "$password = \"Password\"\n" +
    "$encryptedPassword = ConvertTo-SecureString -String $password -AsPlainText -Force\n" +
    "$Credentials = New-Object System.Management.Automation.PSCredential ($username, $encryptedPassword)\n" +
    "$session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://HOSTB.EX2019.lab/PowerShell -Authentication Kerberos -Credential $Credentials\n" +
    "Import-PSSession $session -DisableNameChecking -CommandName Get-Mailbox\n" +
    "Measure-Command {Get-Mailbox -Resultsize Unlimited}"
    .getBytes()), "exchange_script.ps1");
sftpChannel.disconnect();

// 执行脚本
channel.setCommand("pwsh -File exchange_script.ps1");

3. 优化PowerShell命令

  • 移除Out-Host:非交互式场景下,Out-Host会强制终端格式化输出,增加处理时间,若无需输出可改用Out-Null,或直接返回原始对象减少格式化开销:
    Measure-Command {Get-Mailbox -Resultsize Unlimited}
    
  • 复用PSSession:若需多次执行命令,不要每次创建新的PSSession,可将会话保存复用,减少重复初始化开销。

4. 调整JSCH通道配置

  • 开启TCP_NODELAY:禁用Nagle算法,减少小数据包传输延迟:
    session.setConfig("tcp_keepalive", "true");
    session.setConfig("nodelay", "true");
    
  • 确保使用足够大的IO缓冲区,减少IO操作次数。

内容的提问来源于stack exchange,提问作者theimpatientcoder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 23:05:54