这篇文章并不在原计划中。

我原本正在写下一篇:

安装 Loki + Fluent Bit,构建 Kubernetes 日志采集链路

为了给 Nginx 制造一些访问日志,我临时创建了一个 curl Pod:

kubectl run curl \
  -n production \
  --image=curlimages/curl \
  --rm -it \
  -- curl nginx

没想到,命令等待一段时间后失败:

pod "curl" deleted from production namespace
error: timed out waiting for the condition

进一步测试发现,Pod 明明处于 Running,kubectl logs 也完全正常,但就是无法进入容器:

kubectl exec -it curl -n production -- sh

报错:

error sending request:
Post "//[::]:42367/cri/exec/...":
http: server gave HTTP response to HTTPS client

后续的 Loki、Tempo、OpenTelemetry 和故障注入实验都可能用到 kubectl exec。与其在每篇文章中绕过去,不如先把实验室的地基修好。

于是,原计划中的 Loki 篇暂时顺延,这篇意外出现的排障记录成为了系列第四篇。


一、奇怪的现象:Pod 正常,exec 却失败

当前实验环境为:

Ubuntu Server       24.04.4 LTS
Kubernetes          1.36.3
Docker              29.6.2
cri-dockerd         0.4.4
CNI                 Flannel

创建一个长时间运行的测试 Pod:

kubectl create namespace production \
  --dry-run=client -o yaml | kubectl apply -f -

kubectl delete pod curl \
  -n production \
  --ignore-not-found

kubectl run curl \
  -n production \
  --image=curlimages/curl \
  --restart=Never \
  --command -- \
  sleep 3600

Pod 状态正常:

kubectl get pod curl -n production
NAME   READY   STATUS    RESTARTS
curl   1/1     Running   0

日志读取也正常:

kubectl logs deployment/nginx \
  -n production

但执行:

kubectl exec -it curl \
  -n production \
  -- sh

仍然失败:

Post "//[::]:42367/cri/exec/...":
http: server gave HTTP response to HTTPS client

这说明问题并不在 Pod 本身。


二、为什么 logs 正常,exec 却失败

kubectl logs 和 kubectl exec 看起来相似,实际走的链路并不完全相同。

普通容器管理主要使用 CRI Runtime API:

kubelet
   |
   v
cri-dockerd
   |
   v
Docker Engine

而 exec、attach 和 port-forward 还需要使用 Streaming API:

kubectl exec
      |
      v
kube-apiserver
      |
      v
kubelet
      |
      v
CRI Streaming API
      |
      v
cri-dockerd
      |
      v
Container

当前现象可以概括为:

创建 Pod       正常
查看 Pod       正常
读取日志       正常
进入容器       失败

问题范围已经缩小到 Streaming 链路。


三、在社区中找到几乎一模一样的问题

继续搜索完整错误信息后,我找到了 cri-dockerd 官方仓库中的 Issue #569:

Streaming exec/attach fails with kubelet 1.36.x: relative BaseURL (no scheme) misinterpreted as HTTPS

这个 Issue 于 2026 年 7 月 26 日提交,使用的环境是:

cri-dockerd    0.4.4
Docker         29.6.2
Kubernetes     1.36.2

与我的环境几乎完全一致。

Issue 中的错误同样是:

Post "//127.0.0.1:随机端口/cri/attach/...":
http: server gave HTTP response to HTTPS client

最值得注意的是 URL:

//127.0.0.1:35417/cri/...

它只有主机和路径,却没有明确的协议:

http://

Issue 提交者分析认为,新版 kubelet 的代理处理把这个无 Scheme 的地址当成了 HTTPS,但 cri-dockerd 的 Streaming Server 实际使用的是普通 HTTP,最终造成协议不匹配。


四、根因就在一小段代码里

cri-dockerd 0.4.4 的 cmd/server.go 中,Streaming 配置如下:

// Initialize streaming configuration. (Not using TLS now)
streamingConfig := &streaming.Config{
    // Use a relative redirect (no scheme or host).
    BaseURL: &url.URL{Path: "/cri/"},
    Addr:    resolvedAddr,
}

代码已经明确说明:

Not using TLS now

但 BaseURL 只有:

Path: "/cri/"

没有设置:

Scheme
Host

因此 cri-dockerd 返回的地址类似:

//[::]:42367/cri/exec/...

Issue #569 给出的修复方案,是显式补上 http Scheme 和 Host。

修改后:

streamingConfig := &streaming.Config{
    BaseURL: &url.URL{
        Scheme: "http",
        Host:   resolvedAddr,
        Path:   "/cri/",
    },
    Addr: resolvedAddr,
}

代码改动很小,却正好对应错误中的协议错配。


五、准备编译环境

cri-dockerd 0.4.4 的 go.mod 要求 Go 1.26.0,而 Ubuntu 24.04 当前安装的是 Go 1.22.2。

我保留系统原有的 Go,通过 update-alternatives 管理两个版本,而不是直接删除系统版本。

1. 安装编译环境

Ubuntu 24.04:

sudo apt update

sudo apt install -y \
  git \
  make \
  gcc \
  golang

确认 Go:

go version

这里可能有坑。

cri-dockerd 0.4.4 的 go.mod 要求:

go >= 1.26

而 Ubuntu 24.04 自带:

go1.22.x

2. 安装 Go 1.26

下载:

cd /tmp

wget https://go.dev/dl/go1.26.0.linux-amd64.tar.gz

如果 /usr/local/go 已经存在,先改名备份:

if [ -e /usr/local/go ]; then
  sudo mv /usr/local/go \
    "/usr/local/go.backup.$(date +%Y%m%d-%H%M%S)"
fi

解压:

sudo tar -C /usr/local \
  -xzf go1.26.0.linux-amd64.tar.gz

3. 使用 alternatives 管理版本

先记录系统 Go 的真实路径:

SYSTEM_GO=$(readlink -f /usr/bin/go)
SYSTEM_GOFMT=$(readlink -f /usr/bin/gofmt)

echo "$SYSTEM_GO"
echo "$SYSTEM_GOFMT"

注册系统版本:

sudo update-alternatives \
  --install /usr/bin/go go "$SYSTEM_GO" 50

sudo update-alternatives \
  --install /usr/bin/gofmt gofmt "$SYSTEM_GOFMT" 50

注册 Go 1.26:

sudo update-alternatives \
  --install /usr/bin/go go \
  /usr/local/go/bin/go 100

sudo update-alternatives \
  --install /usr/bin/gofmt gofmt \
  /usr/local/go/bin/gofmt 100

切换:

sudo update-alternatives \
  --set go /usr/local/go/bin/go

sudo update-alternatives \
  --set gofmt /usr/local/go/bin/gofmt

验证:

go version
go env GOROOT

应该看到:

go version go1.26.0 linux/amd64
/usr/local/go


六、下载源码并应用补丁

下载 cri-dockerd 0.4.4:

mkdir -p ~/Codes
cd ~/Codes

git clone \
  --branch v0.4.4 \
  --depth 1 \
  https://github.com/Mirantis/cri-dockerd.git

cd cri-dockerd

编辑:

nano cmd/server.go

找到:

streamingConfig := &streaming.Config{
    // Use a relative redirect (no scheme or host).
    BaseURL: &url.URL{Path: "/cri/"},
    Addr:    resolvedAddr,

改为:

streamingConfig := &streaming.Config{
    BaseURL: &url.URL{
        Scheme: "http",
        Host:   resolvedAddr,
        Path:   "/cri/"},
    Addr: resolvedAddr,

确认修改:

git diff -- cmd/server.go

编译:

make cri-dockerd

官方 Makefile 中的 cri-dockerd 目标会直接调用 go build,生成项目根目录下的同名二进制文件。

检查:

ls -lh ./cri-dockerd
./cri-dockerd --buildinfo

七、替换 cri-dockerd 修复版本

为了验证修复效果,这里直接替换当前运行的 cri-dockerd。

不过在替换之前,先保留官方版本备份,方便后续回退验证。

查看当前版本:

which cri-dockerd

cri-dockerd --version

备份原始二进制:

sudo cp /usr/bin/cri-dockerd \
        /usr/bin/cri-dockerd.backup

停止服务:

sudo systemctl stop cri-docker

复制编译后的修复版本:

sudo cp ./cri-dockerd \
        /usr/bin/cri-dockerd

确认权限:

sudo chmod 755 /usr/bin/cri-dockerd

重新启动:

sudo systemctl start cri-docker
sudo systemctl restart kubelet

确认服务正常:

systemctl status cri-docker --no-pager

查看当前运行进程:

ps -ef | grep '[c]ri-dockerd'

确认已经加载新的二进制。


八、验证修复效果

修复完成后,重新创建一个最简单的测试 Pod。

创建测试 Pod:

kubectl delete pod curl \
 --ignore-not-found

kubectl run curl \
  --image=curlimages/curl \
  --restart=Never \
  --command -- \
  sleep 3600

查看状态:

kubectl get pod curl

等待:

NAME   READY   STATUS
curl   1/1     Running

尝试进入容器:

kubectl exec -it curl -- sh

如果修复成功,可以正常进入:

~ $

此时说明:

kubectl exec
       |
       v
kube-apiserver
       |
       v
kubelet Streaming API
       |
       v
cri-dockerd
       |
       v
container

整个链路已经恢复。

退出:

exit


九、回退验证

为了确认修复不是偶然,需要恢复原始版本进行验证。

停止服务:

sudo systemctl stop cri-docker

恢复备份:

sudo cp /usr/bin/cri-dockerd.backup \
        /usr/bin/cri-dockerd

重新启动:

sudo systemctl start cri-docker
sudo systemctl restart kubelet

删除旧测试 Pod:

kubectl delete pod curl

重新创建:

kubectl run curl \
  --image=curlimages/curl \
  --restart=Never \
  --command -- \
  sleep 3600

再次执行:

kubectl exec -it curl -- sh

恢复原始版本后,错误重新出现:

error sending request:
Post "//[::]:xxxxx/cri/exec/...":
http: server gave HTTP response to HTTPS client

重新切换到修复版本后:

kubectl exec -it curl -- sh

恢复正常。

通过正向和反向验证,可以确认:

cri-dockerd Streaming API 的 BaseURL 问题就是导致 kubectl exec 失败的原因。


十、清理测试环境

测试完成后删除临时 Pod:

kubectl delete pod curl

恢复实验环境干净状态。

最终验证结果:

运行版本kubectl exec
原始 cri-dockerd 0.4.4失败
增加 Scheme 和 Host成功
回退原版再次失败
恢复补丁版再次成功

这组正向、反向验证说明:

在当前环境中,Issue #569 给出的根因分析和修复补丁是正确的。


十、这次排障带来的启发

这次问题最迷惑的地方是:

Pod Running
kubectl logs 正常
kubectl exec 失败

原因是这些操作背后并不是完全相同的链路。

创建 Pod 正常,只能说明 CRI Runtime API 可以工作;kubectl exec 还依赖单独的 Streaming API。

而真正帮助我快速找到根因的,并不是继续盲目调整 DNS、Flannel 或 kubelet 参数,而是搜索完整错误信息,并找到与当前版本组合高度一致的社区 Issue。

截至 2026 年 7 月 30 日,Issue #569 仍处于 Open 状态,也没有关联正式修复 PR。因此,本文使用的是经过本地 A/B 验证的临时源码补丁,而不是已经发布的官方修复。未来 cri-dockerd 正式版本包含修复后,应优先恢复官方二进制。

这个意外插曲处理完成后,实验室的交互式调试能力终于恢复正常。

下一篇再回到原计划:

从零搭建一个单节点 K8S 可观测实验室(五):安装 Loki + Fluent Bit,构建 Kubernetes 日志采集链路

更多推荐