Интерпретация принципа работы kubectl exec

Kubernetes

для обычных иKubernetesДля инженеров YAML, которые имеют дело с этим, наиболее часто используемые команды:kubectl exec, который позволяет отлаживать приложение, выполняя команды непосредственно внутри контейнера. Если вы не удовлетворены его использованием, хотите знатьkubectl execработает, то эта статья заслуживает вашего внимательного прочтения. Эта статья будет относиться кkubectl,API Server,Kubeletи соответствующий код в API-интерфейсе Container Runtime Interface (CRI) Docker, чтобы понять, как работает эта команда.

Принцип работы kubectl exec можно представить на схеме:

kubectl exec

Сначала рассмотрим пример:

🐳 → kubectl version --short 
Client Version: v1.15.0 
Server Version: v1.15.3

🐳 → kubectl run nginx --image=nginx --port=80 --generator=run-pod/v1
pod/nginx created

🐳 → kubectl get po     
NAME    READY   STATUS    RESTARTS   AGE 
nginx   1/1     Running   0          6s  

🐳 → kubectl exec nginx -- date
Sat Jan 25 18:47:52 UTC 2020

🐳 → kubectl exec -it nginx -- /bin/bash 
root@nginx:/#

Первый kubectl Exec выполняется внутри контейнераdateкоманда, второй kubectl exec использует-iи-tАргумент переходит в интерактивную оболочку контейнера.

Повторите вторую команду kubectl exec, чтобы распечатать более подробный журнал:

🐳 → kubectl -v=7 exec -it nginx -- /bin/bash                                                         
I0125 10:51:55.434043   28053 loader.go:359] Config loaded from file:  /home/isim/.kube/kind-config-linkerd
I0125 10:51:55.438595   28053 round_trippers.go:416] GET https://127.0.0.1:38545/api/v1/namespaces/default/pods/nginx
I0125 10:51:55.438607   28053 round_trippers.go:423] Request Headers:
I0125 10:51:55.438611   28053 round_trippers.go:426]     Accept: application/json, */*
I0125 10:51:55.438615   28053 round_trippers.go:426]     User-Agent: kubectl/v1.15.0 (linux/amd64) kubernetes/e8462b5
I0125 10:51:55.445942   28053 round_trippers.go:441] Response Status: 200 OK in 7 milliseconds
I0125 10:51:55.451050   28053 round_trippers.go:416] POST https://127.0.0.1:38545/api/v1/namespaces/default/pods/nginx/exec?command=%2Fbin%2Fbash&container=nginx&stdin=true&stdout=true&tty=true
I0125 10:51:55.451063   28053 round_trippers.go:423] Request Headers:
I0125 10:51:55.451067   28053 round_trippers.go:426]     X-Stream-Protocol-Version: v4.channel.k8s.io
I0125 10:51:55.451090   28053 round_trippers.go:426]     X-Stream-Protocol-Version: v3.channel.k8s.io
I0125 10:51:55.451096   28053 round_trippers.go:426]     X-Stream-Protocol-Version: v2.channel.k8s.io
I0125 10:51:55.451100   28053 round_trippers.go:426]     X-Stream-Protocol-Version: channel.k8s.ioI0125 10:51:55.451121   28053 round_trippers.go:426]     User-Agent: kubectl/v1.15.0 (linux/amd64) kubernetes/e8462b5
I0125 10:51:55.465690   28053 round_trippers.go:441] Response Status: 101 Switching Protocols in 14 milliseconds
root@nginx:/#

Здесь есть два важных HTTP-запроса:

Подресурс принадлежит ресурсу K8S и представлен в виде подпути под родительским ресурсом, например/logs,/status,/scale,/execЖдать. Операции, поддерживаемые каждым подресурсом, варьируются от объекта к объекту.

Наконец сервер API вернулся101 UgradeОтвет, указывающий клиенту, что он переключился наSPDYпротокол.

SPDY позволяет мультиплексировать независимые потоки stdin/stdout/stderr/spdy-error в одном TCP-соединении.

1. Анализ исходного кода API-сервера

Запрос сначала пойдет на сервер API, давайте посмотрим, как регистрируется сервер API.rest.ExecRestОбработчик для обработки запросов подресурсов/execиз. Этот процессор используется для определенияexecУзел для входа.

API Server во время запускаПервое, что нужно сделать, это направить встроенныйGenericAPIServerЗагрузите ранний устаревший API (устаревший API):

if c.ExtraConfig.APIResourceConfigSource.VersionEnabled(apiv1.SchemeGroupVersion) {
    // ...
    if err := m.InstallLegacyAPI(&c, c.GenericConfig.RESTOptionsGetter, legacyRESTStorageProvider); err != nil {
        return nil, err
    }
}

Во время загрузки API типLegacyRESTStorage создавать экземпляр,Создаватьstorage.PodStorageПример:

podStorage, err := podstore.NewStorage(
    restOptionsGetter,
    nodeStorage.KubeletConnectionInfo,
    c.ProxyTransport,
    podDisruptionClient,
)
if err != nil {
    return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
}

впоследствииstoreage.PodStorageЭкземпляр будет добавлен на картуrestStorageMapсередина. Обратите внимание, что карта будетpods/execсопоставляется сpodStorageизrest.ExecRestпроцессор.

restStorageMap := map[string]rest.Storage{
    "pods":             podStorage.Pod,
    "pods/attach":      podStorage.Attach,
    "pods/status":      podStorage.Status,
    "pods/log":         podStorage.Log,
    "pods/exec":        podStorage.Exec,
    "pods/portforward": podStorage.PortForward,
    "pods/proxy":       podStorage.Proxy,
    "pods/binding":     podStorage.Binding,
    "bindings":         podStorage.LegacyBinding,

podstorageПредусмотрено для модулей и подресурсовCURDАбстракция логики и стратегии. Подробнее см. во встроенномgenericregistry.Store

map restStorageMapбудет экземплярapiGroupInfoчастьGenericAPIServerсередина:

if err := s.installAPIResources(apiPrefix, apiGroupInfo, openAPIModels); err != nil {
    return err
}

// Install the version handler.
// Add a handler at /<apiPrefix> to enumerate the supported api versions.
s.Handler.GoRestfulContainer.Add(discovery.NewLegacyRootAPIHandler(s.discoveryAddresses, s.Serializer, apiPrefix).WebService())

вGoRestfulContainer.ServeMuxСопоставит URL-адреса входящего запроса с разными обработчиками.

Далее ориентируемся на процессорtherest.ExecRestработает, этоConnect()метод вызывает функциюpod.ExecLocation()определить размер контейнера в стручкеexecподресурсURL:

// Connect returns a handler for the pod exec proxy
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
}

функцияpod.ExecLocation()возвращениеURLИспользуется сервером API, чтобы решить, к какому узлу подключиться.

Далее проанализируйте узлы наKubeletисходный код.

2. Анализ исходного кода Kubelet

прибытьKubeletЗдесь нам нужно обратить внимание на две вещи:

  • Как регистрируется Kubeletexecпроцессор?
  • Кубелет сDocker APIКак взаимодействовать?

Процесс инициализации KubeletОчень сложный, в основном включающий две функции:

  • PreInitRuntimeService(): использоватьdockershimпакет для инициализацииCRI.
  • RunKubelet(): Зарегистрируйте обработчик и запустите службу Kubelet.

обработчик регистрации

Когда Kubelet запускается, егоRunKubelet()функция вызовет приватную функциюstartKubelet()Приходитьзапускатьkubelet.KubeletпримеризListenAndServe()метод, то метод будетфункция вызоваListenAndServeKubeletServer() , используя конструкторNewServer()Чтобы установить «отладочный» процессор:

// NewServer initializes and configures a kubelet.Server object to handle HTTP requests.
func NewServer(
    // ...
    criHandler http.Handler) Server {
    // ...
    if enableDebuggingHandlers {
        server.InstallDebuggingHandlers(criHandler)
        if enableContentionProfiling {
            goruntime.SetBlockProfileRate(1)
        }
    } else {
        server.InstallDebuggingDisabledHandlers()
    }
    return server
}

InstallDebuggingHandlers()использование функцииgetExec()Обработчик для регистрации схемы HTTP-запроса:

// InstallDebuggingHandlers registers the HTTP request patterns that serve logs or run commands/containers
func (s *Server) InstallDebuggingHandlers(criHandler http.Handler) {
  // ...
  ws = new(restful.WebService)
    ws.
        Path("/exec")
    ws.Route(ws.GET("/{podNamespace}/{podID}/{containerName}").
        To(s.getExec).
        Operation("getExec"))
    ws.Route(ws.POST("/{podNamespace}/{podID}/{containerName}").
        To(s.getExec).
        Operation("getExec"))
    ws.Route(ws.GET("/{podNamespace}/{podID}/{uid}/{containerName}").
        To(s.getExec).
        Operation("getExec"))
    ws.Route(ws.POST("/{podNamespace}/{podID}/{uid}/{containerName}").
        To(s.getExec).
        Operation("getExec"))
    s.restfulCont.Add(ws)

вgetExec()Процессор снова вызоветs.hostв случаеGetExec()метод:

// getExec handles requests to run a command inside a container.
func (s *Server) getExec(request *restful.Request, response *restful.Response) {
      // ...
    podFullName := kubecontainer.GetPodFullName(pod)
    url, err := s.host.GetExec(podFullName, params.podUID, params.containerName, params.cmd, *streamOpts)
    if err != nil {
        streaming.WriteError(err, response.ResponseWriter)
        return
    }
    // ...
}

s.hostсоздается какkubelet.Kubeletэкземпляр типа, который содержит ссылки наStreamingRuntimeинтерфейс, интерфейс в свою очередьсоздавать экземплярзаkubeGenericRuntimeManagerэкземпляр, то естьменеджер среды выполнения. Менеджер среды выполнения — это Kubelet сDocker APIключевые компоненты взаимодействия,GetExec()Метод реализуется им:

// GetExec gets the endpoint the runtime will serve the exec request from.
func (m *kubeGenericRuntimeManager) GetExec(id kubecontainer.ContainerID, cmd []string, stdin, stdout, stderr, tty bool) (*url.URL, error) {
    // ...
    resp, err := m.runtimeService.Exec(req)
    if err != nil {
        return nil, err
    }

    return url.Parse(resp.Url)
}

GetExec()позвоню сноваruntimeService.Exec()способ, копай дальше и найдешьruntimeServiceопределяется в пакете CRIинтерфейс.kuberuntime.kubeGenericRuntimeManagerизruntimeServiceсоздается какkuberuntime.instrumentedRuntimeServiceтип, реализуемый имruntimeService.Exec()метод:

func (in instrumentedRuntimeService) Exec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) {
    const operation = "exec"
    defer recordOperation(operation, time.Now())

    resp, err := in.service.Exec(req)
    recordError(operation, err)
    return resp, err
}

Вложенные сервисные объекты экземпляров toolsedRuntimeServiceсоздавать экземплярзаtheremote.RemoteRuntimeServiceэкземпляр типа. Этот тип реализуетExec()метод:

// Exec prepares a streaming endpoint to execute a command in the container, and returns the address.
func (r *RemoteRuntimeService) Exec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) {
    ctx, cancel := getContextWithTimeout(r.timeout)
    defer cancel()

    resp, err := r.runtimeClient.Exec(ctx, req)
    if err != nil {
        klog.Errorf("Exec %s '%s' from runtime service failed: %v", req.ContainerId, strings.Join(req.Cmd, " "), err)
        return nil, err
    }

    if resp.Url == "" {
        errorMessage := "URL is not set"
        klog.Errorf("Exec failed: %s", errorMessage)
        return nil, errors.New(errorMessage)
    }

    return resp, nil
}

Exec()метод будет/runtime.v1alpha2.RuntimeService/ExecинициироватьgRPCперечислитьчтобы сторона выполнения подготовила конечную точку потоковой передачи для выполнения команд в контейнере (см.Docker shimСм. следующий раздел для получения дополнительной информации о настройке его в качестве сервера gRPC).

сервер gRPC, позвонивRuntimeServiceServer.Exec()путь кобработать запрос, метод состоит изdockershim.dockerServiceРеализация структуры:

// Exec prepares a streaming endpoint to execute a command in the container, and returns the address.
func (ds *dockerService) Exec(_ context.Context, req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) {
    if ds.streamingServer == nil {
        return nil, streaming.NewErrorStreamingDisabled("exec")
    }
    _, err := checkContainerStatus(ds.client, req.ContainerId)
    if err != nil {
        return nil, err
    }
    return ds.streamingServer.GetExec(req)
}

строка 10ThestreamingServerЯвляетсяstreaming.Serverинтерфейс, который находится в конструктореdockershim.NewDockerService()создается в:

// create streaming server if configured.
if streamingConfig != nil {
    var err error
    ds.streamingServer, err = streaming.NewServer(*streamingConfig, ds.streamingRuntime)
    if err != nil {
        return nil, err
    }
}

посмотриGetExec()Как реализован метод:

func (s *server) GetExec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) {
    if err := validateExecRequest(req); err != nil {
        return nil, err
    }
    token, err := s.cache.Insert(req)
    if err != nil {
        return nil, err
    }
    return &runtimeapi.ExecResponse{
        Url: s.buildURL("exec", token),
    }, nil
}

Видно, что это всего лишь URL-адрес простой комбинации токенов, возвращаемый клиенту Причина, по которой генерируется токен, заключается в том, что команда пользователя может содержать различные символы и символы различной длины, которые необходимо отформатировать как простой токен. Токен будет кэшироваться локально, и последующий реальный запрос exec будет нести этот токен, и предыдущий конкретный запрос будет найден через этот токен. вrestful.WebServiceэкземпляр будет подexecЗапросы направляются на эту конечную точку:

// InstallDebuggingHandlers registers the HTTP request patterns that serve logs or run commands/containers
func (s *Server) InstallDebuggingHandlers(criHandler http.Handler) {
  // ...
  ws = new(restful.WebService)
    ws.
        Path("/exec")
    ws.Route(ws.GET("/{podNamespace}/{podID}/{containerName}").
        To(s.getExec).
        Operation("getExec"))
    ws.Route(ws.POST("/{podNamespace}/{podID}/{containerName}").
        To(s.getExec).
        Operation("getExec"))
    ws.Route(ws.GET("/{podNamespace}/{podID}/{uid}/{containerName}").
        To(s.getExec).
        Operation("getExec"))
    ws.Route(ws.POST("/{podNamespace}/{podID}/{uid}/{containerName}").
        To(s.getExec).
        Operation("getExec"))
    s.restfulCont.Add(ws)

Создайте прокладку Docker

PreInitRuntimeService()функцияв качестве сервера gRPC,ответственныйСоздайте и начнитеДокер шим. в волеdockershim.dockerServiceКогда создается экземпляр типа, пусть он вложенstreamingRuntimeссылка на экземплярdockershim.NativeExecHandlerэкземпляр (который реализуетdockershim.ExecHandlerинтерфейс).

ds := &dockerService{
    // ...
    streamingRuntime: &streamingRuntime{
        client:      client,
        execHandler: &NativeExecHandler{},
    },
    // ...
}

с помощью ДокераexecОсновная реализация API для выполнения команд в контейнереNativeExecHandler.ExecInContainer()метод:

func (*NativeExecHandler) ExecInContainer(client libdocker.Interface, container *dockertypes.ContainerJSON, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize, timeout time.Duration) error {
    // ...
    startOpts := dockertypes.ExecStartCheck{Detach: false, Tty: tty}
    streamOpts := libdocker.StreamOptions{
        InputStream:  stdin,
        OutputStream: stdout,
        ErrorStream:  stderr,
        RawTerminal:  tty,
        ExecStarted:  execStarted,
    }
    err = client.StartExec(execObj.ID, startOpts, streamOpts)
    if err != nil {
        return err
    }
    // ...

вот и конецKubeletВызвать ДокерexecМесто API.

Последнее, что нужно знать, это то, чтоstreamingServerкак работает процессорexecпросить. Сначала нужно его найтиexecобработчик, мы прямо из конструктораstreaming.NewServer()Начните смотреть вниз, потому что это/exec/{token}путь привязан кserveExecГде находится процессор:

ws := &restful.WebService{}
endpoints := []struct {
    path    string
    handler restful.RouteFunction
}{
    {"/exec/{token}", s.serveExec},
    {"/attach/{token}", s.serveAttach},
    {"/portforward/{token}", s.servePortForward},
}

все отправлено вdockershim.dockerServiceЗапросы экземпляров заканчиваются вstreamingServerсделано на процессоре, потому чтоdockerService.ServeHTTP()метод вызоветstreamingServerпримерServeHTTP()метод.

serveExecпроцессор будетВызов функции remoteCommand.ServeExec(), что делает эта функция? он вызовет ранее упомянутыйExecutor.ExecInContainer()метод, в то время какExecInContainer()Путь в том, чтобы знать, как взаимодействовать с DockerexecДля связи через API:

// ServeExec handles requests to execute a command in a container. After
// creating/receiving the required streams, it delegates the actual execution
// to the executor.
func ServeExec(w http.ResponseWriter, req *http.Request, executor Executor, podName string, uid types.UID, container string, cmd []string, streamOpts *Options, idleTimeout, streamCreationTimeout time.Duration, supportedProtocols []string) {
    // ...
    err := executor.ExecInContainer(podName, uid, container, cmd, ctx.stdinStream, ctx.stdoutStream, ctx.stderrStream, ctx.tty, ctx.resizeChan, 0)
    if err != nil {
    // ...
    } else {
    // ...    
    }
}

3. Резюме

Эта статья через толкованиеkubectl,API ServerиCRIисходный код, чтобы помочь вам понятьkubectl execКак работает команда, разумеется, Docker здесь не при чемexecДетали API, также не охваченныеdocker execПринцип работы.

Сначала kubectl отправляет сообщение на сервер API.GETиPOSTзапрос, сервер API возвратил101 UgradeОтвет, указывающий клиенту, что он переключился наSPDYпротокол.

Затем API-сервер используетstorage.PodStorageиrest.ExecRestдля обеспечения отображения процессора и логики выполнения, гдеrest.ExecRestпроцессор решаетexecУзел для входа.

Наконец, КубелетDocker shimзапрашивает URL-адрес конечной точки потоковой передачи и помещаетexecПеренаправить запрос в DockerexecAPI. Затем kubelet добавляет этот URL вRedirectМетод возвращается на сервер API, и запрос будет перенаправлен на соответствующий сервер потоковой передачи.execзапрос и поддерживать длинную цепочку.

Хотя в этой статье рассматривается только команда kubectl exec, другие подкоманды, такие какattach,port-forward,logи т. д.) следует аналогичному шаблону реализации:

kubectl


Kubernetes 1.18.2 1.17.5 1.16.9 1.15.12 Адрес выпуска пакета автономной установкиstore.lameleg.com, добро пожаловать на опыт. Использовалась последняя версия sealos v3.3.6. Конфигурация разрешения имени хоста была оптимизирована, lvscare монтирует /lib/module для решения проблемы загрузки ipvs при загрузке, исправляет несовместимость между netlink сообщества lvscare и ядром 3.10, а sealos генерирует сертификат вековой давности и другие функции. Больше возможностейGitHub.com/Под гневом/Цвет ах.... Добро пожаловать, чтобы отсканировать QR-код ниже, чтобы присоединиться к группе DingTalk.Группа DingTalk объединяет роботов тюленей и может видеть динамику тюленей в режиме реального времени.