图解kubernetes命令执行核心实现

本文深入探讨了Kubernetes(k8s)中命令执行的核心实现,涉及apiserver、kubelet、CRI及Docker组件。重点讲解了协议切换、流拷贝、连接重定向和Stream的使用。通过http的Upgrade和101状态码实现协议转换,利用SPDY协议进行数据传输。kubectl命令执行分为Pod合法性检测和命令执行两步,apiserver作为代理转发请求至kubelet,kubelet借助CRI.RuntimeService执行命令。整个过程涉及TCP流对拷、协议提升、Hijacker接口和Stream建立,体现了k8s的复杂网络通信机制。
摘要由CSDN通过智能技术生成

K8s中的命令执行由apiserver、kubelet、cri、docker等组件共同完成, 其中最复杂的就是协议切换以及各种流拷贝相关,让我们一起来看下关键实现,虽然代码比较多,但是不会开发应该也能看懂,祝你好运

1. 基础概念

K8s中的命令执行中有很多协议相关的处理, 我们先一起看下这些协议处理相关的基础概念

1.1 Http协议中的Connection与Upgrade

image.png

HTTP/1.1中允许在同一个链接上通过Header头中的Connection配合Upgrade来实现协议的转换,简单来说就是允许在通过HTTP建立的链接之上使用其他的协议来进行通信,这也是k8s中命令中实现协议升级的关键

1.2 Http协议中的101状态码

image.png

在HTTP协议中除了我们常见的HTTP1.1,还支持websocket/spdy等协议,那服务端和客户端如何在http之上完成不同协议的切换呢,首先第一个要素就是这里的101(Switching Protocal)状态码, 即服务端告知客户端我们切换到Uprage定义的协议上来进行通信(复用当前链接)

1.3 SPDY协议中的stream

image.png

SPDY协议是google开发的TCP会话层协议, SPDY协议中将Http的Request/Response称为Stream,并支持TCP的链接复用,同时多个stream之间通过Stream-id来进行标记,简单来说就是支持在单个链接同时进行多个请求响应的处理,并且互不影响,k8s中的命令执行主要也就是通过stream来进行消息传递的

1.4 文件描述符重定向

image.png

在Linux中进程执行通常都会包含三个FD:标准输入、标准输出、标准错误, k8s中的命令执行会将对应的FD进行重定向,从而获取容器的命令的输出,重定向到哪呢?当然是我们上面提到过的stream了(因为对docker并不熟悉,所以这个地方并不保证Docker部分的准确性)

1.5 http中的Hijacker

image.png

在client与server之间通过101状态码、connection、upragde等完成基于当前链接的转换之后, 当前链接上传输的数据就不在是之前的http1.1协议了,此时就要将对应的http链接转成对应的协议进行转换,在k8s命令执行的过程中,会获取将对应的request和response,都通过http的Hijacker接口获取底层的tcp链接,从而继续完成请求的转发

1.6 基于tcp的流对拷的转发

在通过Hijacker获取到两个底层的tcp的readerwriter之后,就可以直接通过io.copy在两个流上完成对应数据的拷贝,这样就不需要在apiserver这个地方进行协议的转换,而是直接通过tcp的流对拷就可以实现请求和结果的转发

基础大概就介绍这些,接下来我们一起去看看其底层的具体实现,我们从kubectl部分开始来逐层分析

2.kubectl

Kubectl执行命令主要分为两部分Pod合法性检测和命令执行, Pod合法性检测主要是获取对应Pod的状态,检测是否在运行, 这里我们重点关注下命令执行部分

2.1 命令执行核心流程

image.png

命令执行的核心分为两个步骤:1.通过SPDY协议建立链接 2)构建Stream建立链接

func (*DefaultRemoteExecutor) Execute(method string, url *url.URL, config *restclient.Config, stdin io.Reader, stdout, stderr io.Writer, tty bool, terminalSizeQueue remotecommand.TerminalSizeQueue) error {
    exec, err := remotecommand.NewSPDYExecutor(config, method, url)
    if err != nil {
        return err
    }
    return exec.Stream(remotecommand.StreamOptions{
        Stdin:             stdin,
        Stdout:            stdout,
        Stderr:            stderr,
        Tty:               tty,
        TerminalSizeQueue: terminalSizeQueue,
    })
}

2.2 exec请求构建

我们可以看到这个地方拼接的Url /pods/{namespace}/{podName}/exec其实就是对应apiserver上面pod的subresource接口,然后我们就可以去看apiserver端的请求处理了

    // 创建一个exec
        req := restClient.Post().
            Resource("pods").
            Name(pod.Name).
            Namespace(pod.Namespace).
            SubResource("exec")
        req.VersionedParams(&corev1.PodExecOptions{
            Container: containerName,
            Command:   p.Command,
            Stdin:     p.Stdin,
            Stdout:    p.Out != nil,
            Stderr:    p.ErrOut != nil,
            TTY:       t.Raw,
        }, scheme.ParameterCodec)
return p.Executor.Execute("POST", req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue)

2.3 建立Stream

image.png

在exec.Stream主要是通过Headers传递要建立的Stream的类型,与server端进行协商

    // set up stdin stream
    if p.Stdin != nil {
        headers.Set(v1.StreamType, v1.StreamTypeStdin)
        p.remoteStdin, err = conn.CreateStream(headers)
        if err != nil {
            return err
        }
    }

    // set up stdout stream
    if p.Stdout != nil {
        headers.Set(v1.StreamType, v1.StreamTypeStdout)
        p.remoteStdout, err = conn.CreateStream(headers)
        if err != nil {
            return err
        }
    }

    // set up stderr stream
    if p.Stderr != nil && !p.Tty {
        headers.Set(v1.StreamType, v1.StreamTypeStderr)
        p.remoteStderr, err = conn.CreateStream(headers)
        if err != nil {
            return err
        }
    }

3.APIServer

APIServer在命令执行的过程中扮演了代理的角色,其负责将Kubectl和kubelet之间的请求来进行转发,注意这个转发主要是基于tcp的流对拷完成的,因为kubectl和kubelet之间的通信,实际上是spdy协议,让我们一起看下关键实现吧

3.1 Connection

image.png

Exec的SPDY请求会首先发送到Connect接口, Connection接口负责跟后端的kubelet进行链接的建立,并且进行响应结果的返回,在Connection接口中,首先会通过Pod获取到对应的Node信息,并且构建Location即后端的Kubelet的链接地址和transport

func (r *ExecREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
    execOpts, ok := opts.(*api.PodExecOptions)
    if !ok {
        return nil, fmt.Errorf("invalid options object: %#v", opts)
    }
    // 返回对应的地址,以及建立链接
    location, transport, err := pod.ExecLocation(r.Store, r.KubeletConn, ctx, name, execOpts)
    if err != nil {
        return nil, err
    }
    return newThrottledUpgradeAwareProxyHandler(location, transport, false, true, true, responder), nil
}

3.2 获取后端服务地址

在获取地址主要是构建后端的location信息,这里会通过kubelet上报来的信息获取到对应的node的host和Port信息,并且拼装出pod的最终指向路径即这里的Path字段/exec/{namespace}/{podName}/{containerName}

    
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值