基于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会话,均无明显效果。
原因分析
- 输出读取逻辑的阻塞延迟:代码中每次循环后强制
Thread.sleep(1000),每一秒才轮询一次输出,当Get-Mailbox返回大量数据时,会累积大量等待时间,这是性能极差的核心原因。 - PowerShell非交互模式开销:用
pwsh -command一次性执行所有命令时,PowerShell以非交互式模式运行,部分Exchange PowerShell cmdlet在该模式下会有额外的初始化或输出格式化开销。 - 命令字符串转义损耗:拼接的命令字符串包含大量转义字符,可能导致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
相关产品推荐
相关产品推荐

