Инженеры, регулярно использующие 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.
