【问题标题】:Use Bash ssh to run a Java program on multiple remote machines使用 Bash ssh 在多台远程机器上运行 Java 程序
【发布时间】:2014-04-07 21:51:00
【问题描述】:

我需要在许多远程机器上运行 Java 程序。我在循环中使用 ssh 并调用运行 Java 程序的远程脚本。

你可以想象,这是用于测试集群上的分布式系统。

问题是,在我输入第一个 ssh 会话的密码后,脚本立即挂起。这可能是 bash 错误,因为 Java 程序在本地运行良好。

确切的结构是这样的,一个运行许多远程 bash 脚本的本地 bash 脚本。每个远程脚本编译并运行一个 Java 程序。这个 Java 程序启动一个单独的线程来做一些工作。当收到 SIGINT 信号时,通知 Java 线程以便它可以干净地退出。

我做了一个简化的工作示例。

编辑:下面的代码现在可以工作(为后代修复)

如果你想回答,请不要对代码的结构做太多改动,否则它不会像原来的那样,我也无法理解问题所在。

手动运行的 Bash 脚本

#!/bin/bash

function startBatch()
{
    #the problem was using -n
    ssh -f "$1" "cd $projectDir;./startBatch.sh $2"
}

function stopBatch()
{
    #the problem was using -n
    ssh -f "$1" "pkill -f jnode_.*"
}

projectDir=NetBeansProjects/Runner

#start nodes
nodeNumber=0
while read node; do
    startBatch "$node" "$nodeNumber"
    nodeNumber=$(($nodeNumber + 1))
done < ./nodes.txt

sleep 3

#stop nodes
while read node; do
    stopBatch "$node"
done < ./nodes.txt

由其他脚本运行的 Bash 脚本

#!/bin/bash

#this is a simplified working example
myNumber=$1
$(exec -a jnode_"$myNumber" java -cp build/classes runner.Runner "$myNumber.txt")

这是上述脚本的简化版本。如果您想要正确的日志记录,请检查已接受答案的第二部分。

#!/bin/bash

batchNumber=$1
procNumber=0
batchSize=3
while [ "$procNumber" -lt "$batchSize" ]; do
    procName="$batchNumber"_"$procNumber"
    #this line was no good
    #$(exec -a jnode_"$procName" java -cp build/classes runner.Runner "$procName.txt" &)
    #this line works fine
    exec -a jnode_"$procName" java -cp build/classes runner.Runner "$procName.txt" 1>/dev/null 2>/dev/null &
    procNumber=$(($procNumber + 1))
done

Java Runner(启动线程的东西)

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintStream;

public class Runner {

    public static void main(String[] args) throws FileNotFoundException, InterruptedException {
        //redirect all outputs to a given file
        PrintStream output = new PrintStream(new File(args[0]));
        System.setOut(output);
        System.setErr(output);

        //controlled object
        final MyRunnable myRunnable = new MyRunnable();

        //shutdown the controlled process on command
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                myRunnable.stop = true;
            }
        });

        //run the process
        new Thread(myRunnable).start();
    }
}

Java MyRunnable(正在运行的线程)

public class MyRunnable implements Runnable {

    public boolean stop = false;

    @Override
    public void run() {
        while (!stop) {
            try {
                System.out.println("running");
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                System.out.println("interrupted");
            }
        }
        System.out.println("stopping");
    }
}

不要在你的 Java 程序中使用 System.exit(),否则关闭钩子将不会被正确调用(或完全执行)。从外部发送 SIGINT 消息。


正如在 cmets 中提到的,输入密码可能很无聊。无密码的 RSA 密钥是一种选择,但我们可以做得更好。让我们添加一些安全功能。

创建公钥/私钥对

ssh-keygen -t rsa
Enter file in which to save the key (home/your_user/.ssh/id_rsa): [input ~/.ssh/nameOfKey]
Enter passphrase (empty for no passphrase): [input a passphrase not weaker than your ssh password]

将公钥添加到远程主机的authorized_keys文件中,这样就可以进行认证了。

#first option (use proper command)
ssh-copy-id user@123.45.67.89

#second option (append the key at the end of the file)
cat ~/.ssh/nameOfKey.pub | ssh user@123.45.67.89 "cat >> ~/.ssh/authorized_keys"

现在,如果我们使用 ssh-agent,我们可以让密码只被询问一次(在执行第一个命令时)。请注意,它会询问密码(创建密钥时输入的密码),而不是实际的 ssh 密码。

#activate the agent
eval `ssh-agent`

#add the key, its passphrase will be asked
ssh-add ~/.ssh/keyName1

#add more keys, if needed
ssh-add ~/.ssh/keyName2

您现在拥有一个非常简单但功能强大的分布式系统测试框架。玩得开心。

【问题讨论】:

  • 我知道这不是您问题的直接答案,但一种可能性是在您的机器上创建一个无密码密钥 (ssh-keygen -t rsa),然后将公钥粘贴到每个的 authorized_keys2远程机器,那么您在从您的机器连接时不必处理密码。 SSH 密码提示有时会对脚本交互性造成严重破坏。带有相关的安全陷阱,但它们可能与您的情况无关。

标签: java bash shell ssh remote-access


【解决方案1】:

执行远程命令时,SSH 不会退出,直到远程命令完成。在 Java 程序完成之前,您的远程脚本不会退出,并且 Java 程序在其所有非守护线程退出之前不会退出,并且您的 Java 程序将永远运行。因此,您对 SSH 的服务器端调用将永远运行(嗯,直到您通过其他方式终止它)并且您的脚本挂起。

您需要决定如何让您的 SSH 远程命令立即返回。你有选择。最简单的可能只是在服务器脚本上使用&amp; 调用它,如下所示:

ssh -n "$1" "cd $projectDir;./startBatch.sh $2 &"

更强大的选择是在远程脚本中使用&amp; 调用java,并让服务器端按照您现在的方式运行(不是&amp;),这样您就有机会完全阅读例如远程脚本产生的错误信息。

旁注:至于密码本身(一旦你克服了当前的障碍,你最终将不得不处理它),正如我在对问题的评论中提到的那样:一种可能性是创建无密码密钥 (ssh-keygen -t rsa ) 在您的机器上,然后将公钥粘贴到每台远程机器上的authorized_keys2 中,这样您在从您的机器连接时就不必处理密码了。 SSH 密码提示有时会对脚本交互性造成严重破坏。带有相关的安全陷阱,但它们可能与您的情况无关。


回应下面的cmets。你有几个选择。如果您想通过附加将所有内容捕获到同一个日志文件中,请不要重定向程序输出,而只需将 while 循环所做的所有内容重定向到日志,例如:

while [ "$procNumber" -lt "$batchSize" ]; do
    procName="$batchNumber"_"$procNumber"
    exec -a jnode_"$procName" java -cp build/classes runner.Runner "$procName.txt" &
    procNumber=$(($procNumber + 1))
done >> "$myLog" 2>&1

如果您希望每个进程有一个日志,请附加:

while [ "$procNumber" -lt "$batchSize" ]; do
    procName="$batchNumber"_"$procNumber"
    exec -a jnode_"$procName" java -cp build/classes runner.Runner "$procName.txt" >> "$myLog.$procNumber" 2>&1 &
    procNumber=$(($procNumber + 1))
done

如果您想将应用程序输出与循环中其他命令的输出分开,您也可以将上述两者结合起来。

【讨论】:

  • 这个小例子现在可以工作了,生产版本似乎开始每台远程机器只启动一个进程。我添加了一个不太简化的远程脚本版本来显示问题。所有日志文件都会立即创建,但只有每台机器上的第一个实例会立即开始填充它们,而其他 2 个只有在第一个实例关闭后才开始填充它们。
  • 我想我明白了。我简化了执行行并丢弃了输出(重定向到 dev/null)。现在它工作了,显然。奇怪!
  • @Agostino 很高兴你明白了。在这种情况下,自下而上的测试方法将是最好的方法。首先确保您的 Java 程序按预期运行。然后开发和测试你的远程脚本,确保它启动所有需要的实例并尽快退出,打印/返回相关的状态信息(如果你关心的话)并让你的程序在后台运行。 然后把你的服务器端脚本放在一起,然后看着一切都到位。
  • 顺便说一句,如果这是某种“服务”类型的应用程序,您还可以考虑使用 System V 样式 (/etc/init.d) 或 upstart(如果您的系统有)作业来管理远程机器上的本地实例,而不是自定义脚本。 init 系统具有易于使用的框架,可以顺利启动和停止后台应用程序。
  • 我试着一步一步地在本地完成这项工作。还不够,我看着它崩溃了:D 我记得我在更大的应用程序上看到了一些警告,抱怨确实存在一些丢失的文件。捕捉这样的抱怨可以吗? 1&gt;"$myLog";2&gt;&gt;"$myLog";while 之前,然后在 exec ... 1&gt;&gt;"$myLog" 2&gt;&gt;"$myLog" &amp; 里面。
【解决方案2】:

ssh 的手册页建议如果 ssh 需要输入密码,则使用 -n 将不起作用。你应该使用-f,或者设置无密码ssh,这样你就不需要输入密码了。

从 Mac OS X 手册页中引用 ssh 给出:

 -n      Redirects stdin from /dev/null (actually, prevents reading from stdin).  This must be used when ssh is run in the background.  A common trick is to use this to run X11
         programs on a remote machine.  For example, ssh -n shadows.cs.hut.fi emacs & will start an emacs on shadows.cs.hut.fi, and the X11 connection will be automatically for-
         warded over an encrypted channel.  The ssh program will be put in the background.  (This does not work if ssh needs to ask for a password or passphrase; see also the -f
         option.)

还有:

 -f      Requests ssh to go to background just before command execution.  This is useful if ssh is going to ask for passwords or passphrases, but the user wants it in the back-
         ground.  This implies -n.  The recommended way to start X11 programs at a remote site is with something like ssh -f host xterm.

         If the ExitOnForwardFailure configuration option is set to ``yes'', then a client started with -f will wait for all remote port forwards to be successfully established
         before placing itself in the background.

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-12-06
    • 2020-05-17
    • 2011-09-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多