Дайджесты новостей
Сетевая миграция стриминга Kubernetes со сбойного устаревшего протокола SPDY на стабильный дуплексный WebSocket через L7-прокси.

Миграция стриминга Kubernetes со SPDY на WebSockets

Инженеры, регулярно использующие kubectl exec или kubectl port-forward, знакомы с внезапными сбоями: сессия отладки рвется ошибкой 502 Bad Gateway, передача дампа через kubectl cp зависает, а корпоративный прокси сбрасывает соединение. Причина этой нестабильности крылась в архитектурном компромиссе десятилетней давности.

Интерактивные потоки ввода-вывода между консолью администратора, kube-apiserver и демоном kubelet на рабочих узлах работали поверх протокола SPDY/3.1. Созданный Google в эпоху зарождения HTTP/2, SPDY был заброшен индустрией еще в 2015 году. Однако в ядре Kubernetes он оставался ключевым транспортом. Инициатива KEP-4006 наконец переводит стриминг оркестратора на стандарт WebSockets (RFC 6455).

Забытый протокол в сердце облачной платформы

Командам exec, attach, cp и port-forward требуется долговременное дуплексное соединение с передачей независимых потоков: stdin, stdout, stderr, сигналов изменения размера окна и кодов завершения. В 2014 году SPDY/3.1 подходил для этого благодаря поддержке мультиплексирования поверх одного TCP-сокета.

Но за годы сетевой ландшафт изменился:

  • Балансировщики облачных провайдеров, Envoy и корпоративные файрволы давно исключили поддержку SPDY.
  • При попытке пробросить порт через современный L7-прокси сессия сбрасывалась.
  • Инженерам приходилось строить обходные SSH-туннели и бастионы.

Архитектура KEP-4006: рукопожатие и сабпротоколы

Главная задача миграции состояла в сохранении обратной совместимости. Клиент отправляет стандартный запрос HTTP/1.1 с заголовком Upgrade: websocket и согласует сабпротоколы через Sec-WebSocket-Protocol.

Спецификация вводит зарегистрированные идентификаторы: v5.channel.k8s.io для терминалов и SPDY/3.1+portforward.k8s.io для проброса портов. Название последнего сохраняет преемственность: внутренняя логика каналов внутри WebSocket-фрейма осталась прежней, избавив ядро от переписывания мультиплексора.

GET /api/v1/namespaces/default/pods/my-pod/portforward HTTP/1.1
Host: k8s-apiserver.internal:6443
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: SPDY/3.1+portforward.k8s.io

Согласование каналов и конфигурация client-go

В библиотеке client-go новый транспорт включается автоматически. При возникновении проблем клиент выполняет деградацию до SPDY. В коде на Go настройка выглядит прозрачно:

package main

import (
    "net/http"
    "net/url"
    "os"

    "k8s.io/client-go/tools/portforward"
    "k8s.io/client-go/transport"
    "k8s.io/client-go/transport/spdy"
)

func startTunnel(apiURL *url.URL, cfg *transport.Config) (*portforward.PortForwarder, error) {
    os.Setenv("KUBECTL_PORT_FORWARD_WEBSOCKETS", "true")

    roundTripper, upgrader, err := spdy.RoundTripperFor(cfg)
    if err != nil {
        return nil, err
    }

    ports := []string{"9090:80"}
    stopChan := make(chan struct{}, 1)
    readyChan := make(chan struct{})

    return portforward.New(
        spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, apiURL),
        ports,
        stopChan,
        readyChan,
        os.Stdout,
        os.Stderr,
    )
}

Что меняется для инфраструктуры и разработчиков

Начиная с Kubernetes 1.31 переход на WebSockets включен по умолчанию. Это дает три осязаемых преимущества:

  • Совместимость с L7-прокси: Envoy и облачные балансировщики маршрутизируют стриминг по типовым политикам WebSockets.
  • Браузерные веб-терминалы: Дашборды платформенной инженерии могут открывать консоль пода напрямую через new WebSocket().
  • Стабильность автоматизации: Пропадают таймауты при долгих операциях деплоя в CI/CD.

При необходимости прежний транспорт можно вернуть переменными KUBECTL_REMOTE_COMMAND_WEBSOCKETS=false и KUBECTL_PORT_FORWARD_WEBSOCKETS=false.