打开ssh通道是什么意思_tee和ssh结合的坑

最近做一个需求:生成一串数据流,能够同时传到2台远程的机器上对数据流做一些操作。发现了tee和ssh结合的坑需要特别注意。

最早的写法

定义一个函数叫做duplicate_data_stream. 它做的事情是处理1台机器的远程操作,然后把数据流继续往下传。其中使用了tee和ssh的结合

function duplicate_data_stream() {
    local remote_host=$1
    tee >( ssh "${remote_host}" "cat > /tmp/receive_file.txt" ) 
}

这样整个程序框架可以这样写:

generate_data_stream | 
duplicate_data_stream "${machine1}" | 
duplicate_data_stream "${machine2}" > /dev/null

当时考虑这样设计接口,是因为如果以后还要对数据流做其它处理,只要管道后面继续接处理的程序就可以了。

发现问题

上面的程序,实际运行下来,发现了问题:

执行上面的程序之后。它会报错:

tee : standard output: Resource temporarily unavailable

Resource temporarily unavailable 是C的系统错误码:EAGAIN 的文字描述

经过定位,确认数据流从第二段发送到后面第三段的stdout存在问题,发送不了。经过分析后认为,往stdout写数据出现EAGAIN可以说明:1)当时tee的stdout是非阻塞模式;2)生成的数据填入管道buffer太快,导致管道buffer还没被另一端足够快地消费之前就被打满了。

我们观察到的另外一些现象:

  • 出现过EAGAIN报错之后,machine2的数据就停止增长了,使得machine2的数据一直是不完整的。但是,每次向machine1传送的数据都是完整的。说明我们系统使用的tee版本遇到EAGAIN的错误,直接就把对应的文件描述符关掉了,不会做重试处理
  • 就算把duplicate_data_stream "${machine2}"换成gzip之类的本地操作,也会报错EAGAIN
  • 如果在generate_data_stream 之后能够接一个限速的进程,比如:
    generate_data_stream | dd bs=128 | ...
    有时候能够成功,但是非常看运气

解决问题(V1)

为了解决这个问题,采取了几个方案:

  • 一开始怀疑是tee版本太老了,最新的tee可能修复了这个问题。于是下载了最新的coreutils编译,结果出来的tee还是有这个问题,后来我们翻阅了tee的源码,证实了之前的判断,tee确实没有处理EAGAIN。
  • 然后使用Golang写了一个tee的实现,发现Golang的版本不会报EAGAIN,不会报任何错误。但是去machine1和machine2上面确认数据,发现传送的数据都是不完整的。这个可运维性不如原生的tee。所以这个方案也不能采用
  • 最后,做了一些实验,发现只要tee不去使用stdout,而是使用command substitution的虚拟文件,那么多个文件也可以同时发送并且不会报错。利用这个特性,写出了一个替代的方案

替代方案是这样的:

定义了一个函数叫做handle_data_stream。它做的事情就是纯粹处理1台机器的远程操作:

function handle_data_stream() {
    local remote_host=$1
    ssh "${remote_host}" "cat > /tmp/receive_file.txt"
}

整个程序框架变成这样:

generate_data_stream | 
tee >( handle_data_stream "${machine1}" ) >( handle_data_stream "${machine2}" ) > /dev/null

采用这种方案之后,相关的程序就能够顺利运行,并且也不需要对数据流限速,没有性能问题。暂时能够满足需求了

找到根因

经过前面的分析,其实核心原因是tee进程的stdout变成了非阻塞模式,才会出现EAGAIN错误。但是tee默认的stdout是阻塞模式,遇到管道的buffer被打满的情况,它会等待并继续,也就能够完整传输所有内容。那问题就是:为什么tee的stdout会变成非阻塞模式

经过网上一些资料的搜索,找到了核心原因!原因是:tee命令里面的ssh有问题!!!

ssh启动时,会把进程的stdin, stdout, stderr都设置成非阻塞模式。进程在各种fork过程中,stdout被各种共享,比较我们前面duplicate_data_stream那个复杂的tee命令中,所有子命令,只要不是接管道或者重定向,其实是和tee进程共享stdout的设置的。而我们发现,我们在tee里面使用的ssh命令没有接管道或者重定向,也就是说:tee里面的ssh对它的stdout设置了非阻塞模式,实际上把tee进程的stdout也设置成了非阻塞模式

而为什么我们前面的替代方案不会有问题呢?

这是因为替代方案中,tee的stdout的内容不会被任何进程使用,直接丢到/dev/null,所以就不会出现写满buffer的情况,绕过了tee的stdout被变成非阻塞模式的问题困扰。而使用command substitution的虚拟文件(实际是/dev/fd/xxxx)被tee打开进行写操作时,是使用阻塞模式打开的。所以tee向虚拟文件做写操作都是阻塞操作,也就没有EAGAIN的问题。

最终的解决方案(V2)

找到根因之后,解决方法非常简单!只要把tee当中的ssh命令接一个管道或者重定向一下就可以了。最简单的方法就是ssh后面接一个cat就可以了。它的目的是:让ssh的stdout被管道消费,而cat的stdout和tee的stdout共享,两者都保持阻塞模式

具体来说,改成下面这样:

function duplicate_data_stream() {
    local remote_host=$1
    tee >( ssh "${remote_host}" "cat > /tmp/receive_file.txt" | cat )  #最后的cat是关键
}

或者

function duplicate_data_stream() {
    local remote_host=$1
    tee >( ssh "${remote_host}" "cat > /tmp/receive_file.txt" > /dev/null ) 
}

就可以了

参考

https://serverfault.com/questions/369757/why-is-this-tee-losing-stdout​serverfault.com https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=209509#c3​bugs.freebsd.org
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值