用例

用例

用例

我有一个备份软件能够从命名管道读取要备份的数据。我想使用该功能备份仅使用 VM 主机托管在多个不同 VM 中的数据库转储,例如 MySQL 和 PostgreSQL。方法是使用 SSH 连接到 VM,启动相应的转储工具,在 STDOUT 上返回输出,通过 SSH 转发,并将 SSH 的 STDOUT 传输到使用创建的命名管道中mkfifo

需要注意的重要一点是,从管道读取数据会阻塞,直到有内容写入其中,因此 SSH 进程和备份软件需要同时运行。SSH 写入,备份软件依次读取所有可用的命名管道。我已经使用 SSH 和或手动测试过这一点,cat并且vi一切正常:虚拟机内的转储仅在某个读取器连接到管道时开始,并且转储中的所有数据最终都可用。这可以很容易地进行测试,尤其是使用mysqldump,因为它默认输出易于调试的纯文本。

问题

备份软件支持在实际备份处理之前调用钩子脚本。因此,我实现了这样一个脚本,在后台启动 SSH 进程,并假设当钩子脚本本身返回时,备份软件将开始读取管道。钩子机制本身有效,我成功地使用相同的方法在备份之前/之后在虚拟机中创建/销毁文件系统快照。

这里需要注意的重要一点是,备份软件需要等待第一级钩子脚本完成,因为只有这样才能确保所有 SSH 进程都在后台运行。这正是在虚拟机中创建快照的其他钩子的工作方式。

但是,备份软件绝不能等待第一级钩子脚本的子脚本,因为它们需要写入备份软件需要读取的数据。两者都需要并行进行,对于每个创建的命名管道,一个接一个。但目前看来备份软件确实等待所有子进程因为某些原因。

我已经调试过一些东西,并且有点确定主钩子脚本确实完成了。使用 BASH 跟踪函数调用似乎是这样的,PID 也在某个时候消失了。只有所有子进程都保留了下来,就像预期的那样。在使用 杀死所有子进程后pkill,备份软件继续处理,因此它似乎真的在等待所有子进程。由于管道不再有可用的写入器,备份再次永远等待它们,但这是意料之中的。

研究

备份软件是用 Python 实现的,默认情况下等待钩子脚本输出。这是正确的,也是意料之中的,但有些人声称在某些情况下Python 也会等待子进程。不过,使用这种方法来防止这种情况shell=True似乎已被备份软件所采用。

因此只有两个选择:要么备份软件确实在等待所有子进程检索其输出,并且需要更改其实现。或者我在执行 SSH 时做错了什么,这些进程没有正确地与主钩子脚本分离,并且它确实没有完全完成或诸如此类的事情,导致备份软件继续等待。

备份软件代码

以下是钩子脚本被执行

                execute.execute_command(
                    [command],
                    output_log_level=logging.ERROR
                    if description == 'on-error'
                    else logging.WARNING,
                    shell=True,
                )

以下是这个过程是如何开始的

    process = subprocess.Popen(
        command,
        stdin=input_file,
        stdout=None if do_not_capture else (output_file or subprocess.PIPE),
        stderr=None if do_not_capture else (subprocess.PIPE if output_file else subprocess.STDOUT),
        shell=shell,
        env=environment,
        cwd=working_directory,
    )

    if not run_to_completion:
        return process

    log_outputs(
        (process,), (input_file, output_file), output_log_level, borg_local_path=borg_local_path
    )

以下是读取流程输出的摘录

    buffer_last_lines = collections.defaultdict(list)
    process_for_output_buffer = {
        output_buffer_for_process(process, exclude_stdouts): process
        for process in processes
        if process.stdout or process.stderr
    }
    output_buffers = list(process_for_output_buffer.keys())
    # Log output for each process until they all exit.
    while True:
        if output_buffers:
            (ready_buffers, _, _) = select.select(output_buffers, [], [])
            for ready_buffer in ready_buffers:
                ready_process = process_for_output_buffer.get(ready_buffer)
                # The "ready" process has exited, but it might be a pipe destination with other
                # processes (pipe sources) waiting to be read from. So as a measure to prevent
                # hangs, vent all processes when one exits.
                if ready_process and ready_process.poll() is not None:
                    for other_process in processes:
                        if (
                            other_process.poll() is None
                            and other_process.stdout
                            and other_process.stdout not in output_buffers
                        ):
                            # Add the process's output to output_buffers to ensure it'll get read.
                            output_buffers.append(other_process.stdout)
                line = ready_buffer.readline().rstrip().decode()
                if not line or not ready_process:
                    continue
[...]
        still_running = False
        for process in processes:
            exit_code = process.poll() if output_buffers else process.wait()
[...]
        if not still_running:
            break
    # Consume any remaining output that we missed (if any).
    for process in processes:
        output_buffer = output_buffer_for_process(process, exclude_stdouts)
        if not output_buffer:
            continue
[...]

正在运行的进程

以下是备份软件不向前移动时的运行进程。底部的所有 bash 进程很可能包含我启动的 SSH 实例,但我想知道为什么它们仍然与其父 b​​ash 相关联。另一方面,所有这些 bash 实例似乎都已从我的钩子 shell 脚本中正确释放,这应该是提到的一个僵尸进程。我猜僵尸只需要停止就可以解决这个问题……

   1641 ?        Ss     0:02 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
1835356 ?        Ss     0:00  \_ sshd: [USR1] [priv]
1835380 ?        S      0:00  |   \_ sshd: [USR1]@pts/1
1835381 pts/1    Ss     0:00  |       \_ -bash
1835418 pts/1    S      0:00  |           \_ sudo -i
1835612 pts/1    S      0:00  |               \_ -bash
1835621 pts/1    S      0:00  |                   \_ sudo -u [USR2] -i
1835622 pts/1    S      0:00  |                       \_ -bash
1840864 pts/1    S+     0:00  |                           \_ sudo borgmatic create --config /[...]/[HOST]_piped.yaml
1840865 pts/1    S+     0:00  |                               \_ /usr/bin/python3 /usr/local/bin/borgmatic create --config /[...]
1840874 pts/1    Z+     0:00  |                                   \_ [sh] <defunct>
1840918 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840920 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840922 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840924 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840926 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840928 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840930 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840932 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840934 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840936 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840938 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840940 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840942 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840944 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]
1840946 pts/1    S+     0:00 /bin/bash /[...]/other_mysql_dumps.sh [HOST] [TOPIC]

SSH 调用

这是我在某个循环中执行的操作。各个参数不应该太重要,因此重点在于使用nohup、处理输入/输出、将事物置于后台等。

nohup ssh -F ${PATH_LOCAL_SSH_CFG} -f -n ${HOST_OTHER} ${cmd} < '/dev/null' > "${PATH_LOCAL_MNT2}/${db_name}" 2> '/dev/null' &

那么,这是进行本地异步 SSH 调用的正确方法吗?或者我已经做错了什么?

我需要知道我是否应该将调试重点放在我的 shell 调用还是备份软件上。

谢谢!

答案1

nohup ssh -F ${PATH_LOCAL_SSH_CFG} -f -n ${HOST_OTHER} ${cmd} < '/dev/null' > "${PATH_LOCAL_MNT2}/${db_name}" 2> '/dev/null' &

如果想要真正拥有完全独立的后台进程,上述内容是错误的,因为 SSH 的输出重定向到了一个文件。最终是谁执行了重定向?是启动实际命令的 bash 进程,在我的情况下是备份软件的钩子脚本,最终使该钩子脚本成为僵尸。该僵尸试图处理重定向,这就是为什么所有这些bash实例仍然在我的输出中可用的原因ps axf。当重定向到文件更改为时/dev/null,钩子脚本实际上会完全终止,备份软件将继续运行,SSH 实例在中可见ps axf

当然,在我的具体用例中,我需要将输出重定向到文件。这就是全部内容。不过,我“只是”需要它不同:执行我的钩子脚本的 shell 需要启动一个进程来自行处理重定向,而启动的 shell 完全与启动的进程分离。如果 SSH 能够自行写入文件,那将非常容易,但事实似乎并非如此。相反,它似乎完全依赖 shell 来处理重定向。这意味着我需要一个额外的 shell 实例:一个执行 SSH 并负责重定向到文件的实例,同时这个 shell 实例在其父级的后台执行,父级忽略所有输入/输出。

事实上,我一直都离解决方案很近,但就是不完全理解自己在做什么。示例nohup只会让事情变得更加困难,因为我最终需要一个额外的 shell 实例。以下是我之前在其他一些 SO 问题的帮助下已经得到的:

(
  trap '' HUP INT
  ${cmd_exec} <<< "${cmd}"
) < '/dev/null' > "${PATH_LOCAL_MNT2}/${db_name}" 2> '/dev/null' &

https://gist.github.com/bluekezza/e511f3f4429939a0f9ecb6447099b3dc https://stackoverflow.com/a/54688673/2055163

以上将我的 SSH 命令作为复合命令执行,允许忽略 SIGHUP 等。根据当前 shell 的配置,当父级结束时,可能需要在后台运行执行的命令。

需要理解的重要一点是,这实际上已经创建了一个额外的子 shell,这正是我需要的!上面的问题是父 shell 等待子 shell 的输出,因为 STDOUT 的重定向是为父 shell 定义的,与我的nohup示例非常相似。现在这是最简单的部分:因为我们有一个带有单独 STDOUT 的子 shell,它可以简单地重定向到一个单独的文件中!这样,父 shell 就可以完全脱离所有通道,复合命令在后台执行,备份软件继续运行,因为钩子脚本不会变成僵尸,并且最终能够从管道中读取,因为数据库转储器使用 SSH 写入管道。:-)

(
  trap '' HUP INT
  ${cmd_exec} <<< "${cmd}" > "${PATH_LOCAL_MNT2}/${db_name}"
) < '/dev/null' > '/dev/null' 2> '/dev/null' &

我查看了备份软件创建/恢复的文件,它们看起来没问题。我可以看到很多 SQL 命令和数据等。什么没有工作如下:

(
  trap '' HUP INT
  ${cmd_exec} <<< "${cmd}" < '/dev/null' > "${PATH_LOCAL_MNT2}/${db_name}" 2> '/dev/null'
) < '/dev/null' > '/dev/null' 2> '/dev/null' &

我曾想过尝试一下,只是为了安全起见,因为我无论如何都不需要 STDIN 和 STDERR。但是使用上述方法,我根本没有在管道中得到任何内容,实际上只有 EOF,因为备份软件最终会创建空文件。此外,我可以看到数据库转储程序甚至没有在其主机中执行,而对于有效的解决方案,我可以轻松看到系统中的 CPU 和 I/O 负载。可能与我使用某些函数或其他东西执行 SSH 的方式有关,现在不再太在意了。

相关内容