k8s pod terminal 断线重连
前言很久没有写博客了,貌似最近没有学到什么新知识,都是运用已知的知识,解决各种问题和功能。没办法了,那就把最近做的一个简简单单的功能,拿出来说说吧,功能很简单,需要支持 k8s pod terminal 断开重连。分析我们知道,使用命令kubectl exec -it pods/PODNAME -- bash [--kubeconfig /PATH/TO/KUBECONFIG] [--namesp
前言
很久没有写博客了,貌似最近没有学到什么新知识,都是运用已知的知识,解决各种问题和功能。没办法了,那就把最近做的一个简简单单的功能,拿出来说说吧,功能很简单,需要支持 k8s pod terminal 断开重连。
分析
我们知道,使用命令
kubectl exec -it pods/PODNAME -- bash [--kubeconfig /PATH/TO/KUBECONFIG] [--namespace NAMESPACE]
也就是exec可以执行命令的方式获取到terminal,这样就可以在本地使用kubeconfig,访问到拿到 pod 的 terminal,进而做下一步操作。
需求
如果用户需要执行的命令是个需要很久才能结束的命令,抑或者是需要长久的保持terminal 不断开,比如电脑睡眠、突然断网后,还需要保持terminal不断开。除非用户主动退出terminal。比如输入exit命令,或者kill 掉进程。
简言之:
电脑断网恢复后,能够重连 terminal
电脑休眠后,能够重连 terminal
现状
断网重连
我们知道kubectl exec获取到的terminal 会由于网络断开而断开,而如果不断网的话,k8s 是有心跳机制的,默认5s发送一次心跳,见代码
vendor/k8s.io/client-go/transport/spdy/spdy.go:37
upgradeRoundTripper := spdy.NewRoundTripperWithConfig(spdy.RoundTripperConfig{
TLS: tlsConfig,
FollowRedirects: true,
RequireSameHostRedirects: false,
Proxier: proxy,
PingPeriod: time.Second * 5,
})
因此,如果不断网的话,讲道理 terminal是不会断掉的,而如果是断网重连,我们可以通过terminal断开后,网络情况决定是否重新链接,也就是,如果terminal断开了,首先检查一下网络,如果网络不通,则重连,很容易实现。
伪代码
spdyExecutor, err := remotecommand.NewSPDYExecutor(config, method, url)
if err != nil {
return err
}
return spdyExecutor.Stream(
remotecommand.StreamOptions{
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
Tty: tty,
TerminalSizeQueue: terminalSizeQueue,
},
)
这里返回的时候,判断err 是否为空,如果不为空,则判断和apiserver时候能建立正常连接来决定是否重连。
睡眠后醒来重连
这个就稍微有点儿难做了,睡眠后会断网,断网terminal 会断开。而睡眠结束后,网络也立即恢复了,因此无法通过网络连接情况的决定是否重连,同时如果系统睡眠,则程序会挂起,而程序如果挂起,那么判断程序是否挂起的逻辑是无法生效的。也就是说,程序不知道自己是不是处于睡眠中。
奇思妙想一
时间。如果挂起,则程序就不在运行态了,那么程序内部的时间和物理时间是对不上的。会产生时间偏移。可以通过这个偏移来判断是否产生了睡眠。具体做法可以是:进入termianl之前,记录开始时间,同时在程序内部开启一个goroute,每过一秒加1,当terminal 返回的时候,获取到当前时间。使用当前时间 - 开始时间 == 计时器
实验失败
很奇怪,这个偏移对不上,明明电脑可能睡了10分钟,但是偏移量可能只有一两秒。
奇思妙想二
获取系统睡眠事件。也就是在程序中监听系统事件,讲道理,如果程序挂起,应该会收到挂起事件和恢复事件。伪代码
signals := make(chan os.Signal, 5)
signal.Notify(signals, os.Interrupt, syscall.SIGHUP, syscall.SIGSTOP, syscall.SIGTSTP, syscall.SIGCONT)
<-signals
实验失败
很遗憾,没收到任何一个信号,不知道为啥。也有可能是当时没弄好,也许是可以拿到的。。
奇思妙想三
看远码,改源码。看一下断开是如果断开的,通过翻看代码,找到了一点儿蛛丝马迹
/Users/naison/go/pkg/mod/k8s.io/client-go@v0.21.2/tools/remotecommand/errorstream.go:36
func watchErrorStream(errorStream io.Reader, d errorStreamDecoder) chan error {
errorChan := make(chan error)
go func() {
defer runtime.HandleCrash()
message, err := ioutil.ReadAll(errorStream)
switch {
case err != nil && err != io.EOF:
errorChan <- fmt.Errorf("error reading from error stream: %s", err)
case len(message) > 0:
errorChan <- d.decode(message)
default:
errorChan <- nil // <--在这里
}
close(errorChan)
}()
return errorChan
}
这里会开启一个errorstream,同时从stream中获取事件,如果有事件,则decode出消息
/Users/naison/go/pkg/mod/k8s.io/client-go@v0.21.2/tools/remotecommand/v4.go:82
这里可以看到如果消息是成功断开的,则返回nil,异常情况则返回err,但是
default:
errorChan <- nil // <--在这里
这里如果是睡眠导致的网络断开,那么读一个已经关闭的连接,就会返回io.EOF,而,EOF会走到默认分支,返回一个nil,因此就导致了terminal return的时候,err是空的。
实验成功
将
default:
errorChan <- nil // <--在这里
改为
default:
errorChan <- fmt.Errorf("disconnect should notify by remote server") // <--在这里
这样就好了,然后return返回的时候,就可以拿到err,从而进行重新连接了。
解决方案
就是上面提到的修改remotecommand的源码,该一行代码即可。
伪代码
func Terminal(podName string, containerName string, shell string) error {
t := term.TTY{Raw: true}
t.MonitorSize(t.GetSize())
// remove log eof
k8sruntime.ErrorHandlers = k8sruntime.ErrorHandlers[1:]
first := true
go func() {
for {
cmd := shell
if first {
cmd = fmt.Sprintf("clear; %s", shell)
first = false
}
// 进入terminal的操作,spdy写流即可,就不贴代码了
err := Exec(podName, containerName, []string{"sh", "-c", cmd})
if err == nil {
os.Exit(0)
}
time.Sleep(time.Second * 1)
}
}()
return t.Safe(func() error { select {} })
}
后记
这个简单的功能,让我了解到
- 断开terminal链接的操作,应该是由远端通知过来的。因为本地只是不停的监听标准输入,然后写到和api-server建立的链接中,所有的命令都是在远端运行的,即便是exit命令,也不例外。
更多推荐
所有评论(0)