Commit 4796c7b4 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #40727 from Random-Liu/handle-cri-in-place-upgrade

Automatic merge from submit-queue CRI: Handle cri in-place upgrade Fixes https://github.com/kubernetes/kubernetes/issues/40051. ## How does this PR restart/remove legacy containers/sandboxes? With this PR, dockershim will convert and return legacy containers and infra containers as regular containers/sandboxes. Then we can rely on the SyncPod logic to stop the legacy containers/sandboxes, and the garbage collector to remove the legacy containers/sandboxes. To forcibly trigger restart: * For infra containers, we manually set `hostNetwork` to opposite value to trigger a restart (See [here](https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kuberuntime/kuberuntime_manager.go#L389)) * For application containers, they will be restarted with the infra container. ## How does this PR avoid extra overhead when there is no legacy container/sandbox? For the lack of some labels, listing legacy containers needs extra `docker ps`. We should not introduce constant performance regression for legacy container cleanup. So we added the `legacyCleanupFlag`: * In `ListContainers` and `ListPodSandbox`, only do extra `ListLegacyContainers` and `ListLegacyPodSandbox` when `legacyCleanupFlag` is `NotDone`. * When dockershim starts, it will check whether there are legacy containers/sandboxes. * If there are none, it will mark `legacyCleanupFlag` as `Done`. * If there are any, it will leave `legacyCleanupFlag` as `NotDone`, and start a goroutine periodically check whether legacy cleanup is done. This makes sure that there is overhead only when there are legacy containers/sandboxes not cleaned up yet. ## Caveats * In-place upgrade will cause kubelet to restart all running containers. * RestartNever container will not be restarted. * Garbage collector sometimes keep the legacy containers for a long time if there aren't too many containers on the node. In that case, dockershim will keep performing extra `docker ps` which introduces overhead. * Manually remove all legacy containers will fix this. * Should we garbage collect legacy containers/sandboxes in dockershim by ourselves? /cc @yujuhong * Host port will not be reclaimed for the lack of checkpoint for legacy sandboxes. https://github.com/kubernetes/kubernetes/pull/39903 /cc @freehan /cc @yujuhong @feiskyer @dchen1107 @kubernetes/sig-node-api-reviews **Release note**: ```release-note We should mention the caveats of in-place upgrade in release note. ```
parents 67859efa b9cf8ebe
...@@ -17,6 +17,7 @@ go_library( ...@@ -17,6 +17,7 @@ go_library(
"docker_checkpoint.go", "docker_checkpoint.go",
"docker_container.go", "docker_container.go",
"docker_image.go", "docker_image.go",
"docker_legacy.go",
"docker_sandbox.go", "docker_sandbox.go",
"docker_service.go", "docker_service.go",
"docker_streaming.go", "docker_streaming.go",
...@@ -53,6 +54,8 @@ go_library( ...@@ -53,6 +54,8 @@ go_library(
"//vendor:github.com/docker/go-connections/nat", "//vendor:github.com/docker/go-connections/nat",
"//vendor:github.com/golang/glog", "//vendor:github.com/golang/glog",
"//vendor:k8s.io/apimachinery/pkg/util/errors", "//vendor:k8s.io/apimachinery/pkg/util/errors",
"//vendor:k8s.io/apimachinery/pkg/util/sets",
"//vendor:k8s.io/apimachinery/pkg/util/wait",
], ],
) )
...@@ -64,6 +67,7 @@ go_test( ...@@ -64,6 +67,7 @@ go_test(
"docker_checkpoint_test.go", "docker_checkpoint_test.go",
"docker_container_test.go", "docker_container_test.go",
"docker_image_test.go", "docker_image_test.go",
"docker_legacy_test.go",
"docker_sandbox_test.go", "docker_sandbox_test.go",
"docker_service_test.go", "docker_service_test.go",
"helpers_test.go", "helpers_test.go",
......
...@@ -75,6 +75,15 @@ func (ds *dockerService) ListContainers(filter *runtimeapi.ContainerFilter) ([]* ...@@ -75,6 +75,15 @@ func (ds *dockerService) ListContainers(filter *runtimeapi.ContainerFilter) ([]*
result = append(result, converted) result = append(result, converted)
} }
// Include legacy containers if there are still legacy containers not cleaned up yet.
if !ds.legacyCleanup.Done() {
legacyContainers, err := ds.ListLegacyContainers(filter)
if err != nil {
return nil, err
}
// Legacy containers are always older, so we can safely append them to the end.
result = append(result, legacyContainers...)
}
return result, nil return result, nil
} }
...@@ -360,6 +369,15 @@ func (ds *dockerService) ContainerStatus(containerID string) (*runtimeapi.Contai ...@@ -360,6 +369,15 @@ func (ds *dockerService) ContainerStatus(containerID string) (*runtimeapi.Contai
ct, st, ft := createdAt.UnixNano(), startedAt.UnixNano(), finishedAt.UnixNano() ct, st, ft := createdAt.UnixNano(), startedAt.UnixNano(), finishedAt.UnixNano()
exitCode := int32(r.State.ExitCode) exitCode := int32(r.State.ExitCode)
// If the container has no containerTypeLabelKey label, treat it as a legacy container.
if _, ok := r.Config.Labels[containerTypeLabelKey]; !ok {
names, labels, err := convertLegacyNameAndLabels([]string{r.Name}, r.Config.Labels)
if err != nil {
return nil, err
}
r.Name, r.Config.Labels = names[0], labels
}
metadata, err := parseContainerName(r.Name) metadata, err := parseContainerName(r.Name)
if err != nil { if err != nil {
return nil, err return nil, err
......
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dockershim
import (
"testing"
dockercontainer "github.com/docker/engine-api/types/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/kubernetes/pkg/kubelet/dockertools"
"k8s.io/kubernetes/pkg/kubelet/types"
)
func TestConvertLegacyNameAndLabels(t *testing.T) {
for desc, test := range map[string]struct {
names []string
labels map[string]string
expectNames []string
expectLabels map[string]string
expectError bool
}{
"legacy infra container": {
names: []string{"k8s_POD.hash1_podname_podnamespace_poduid_randomid"},
labels: map[string]string{
types.KubernetesPodNameLabel: "podname",
types.KubernetesPodNamespaceLabel: "podnamespace",
types.KubernetesPodUIDLabel: "poduid",
types.KubernetesContainerNameLabel: "POD",
containerHashLabel: "hash",
containerRestartCountLabel: "0",
},
expectNames: []string{"k8s_POD_podname_podnamespace_poduid_0"},
expectLabels: map[string]string{
types.KubernetesPodNameLabel: "podname",
types.KubernetesPodNamespaceLabel: "podnamespace",
types.KubernetesPodUIDLabel: "poduid",
types.KubernetesContainerNameLabel: "POD",
annotationPrefix + containerHashLabel: "hash",
annotationPrefix + containerRestartCountLabel: "0",
containerTypeLabelKey: containerTypeLabelSandbox,
},
},
"legacy application container": {
names: []string{"k8s_containername.hash_podname_podnamespace_poduid_randomid"},
labels: map[string]string{
types.KubernetesPodNameLabel: "podname",
types.KubernetesPodNamespaceLabel: "podnamespace",
types.KubernetesPodUIDLabel: "poduid",
types.KubernetesContainerNameLabel: "containername",
containerHashLabel: "hash",
containerRestartCountLabel: "5",
containerTerminationMessagePathLabel: "terminationmessagepath",
containerTerminationMessagePolicyLabel: "terminationmessagepolicy",
containerPreStopHandlerLabel: "prestophandler",
containerPortsLabel: "ports",
},
expectNames: []string{"k8s_containername_podname_podnamespace_poduid_5"},
expectLabels: map[string]string{
types.KubernetesPodNameLabel: "podname",
types.KubernetesPodNamespaceLabel: "podnamespace",
types.KubernetesPodUIDLabel: "poduid",
types.KubernetesContainerNameLabel: "containername",
annotationPrefix + containerHashLabel: "hash",
annotationPrefix + containerRestartCountLabel: "5",
annotationPrefix + containerTerminationMessagePathLabel: "terminationmessagepath",
annotationPrefix + containerTerminationMessagePolicyLabel: "terminationmessagepolicy",
annotationPrefix + containerPreStopHandlerLabel: "prestophandler",
annotationPrefix + containerPortsLabel: "ports",
containerTypeLabelKey: containerTypeLabelContainer,
},
expectError: false,
},
"invalid sandbox name": {
names: []string{"POD_podname_podnamespace_poduid_0"},
expectError: true,
},
"invalid dockershim container": {
names: []string{"containername_podname_podnamespace_poduid_5"},
expectError: true,
},
} {
t.Logf("TestCase %q", desc)
names, labels, err := convertLegacyNameAndLabels(test.names, test.labels)
require.Equal(t, test.expectError, err != nil)
assert.Equal(t, test.expectNames, names)
assert.Equal(t, test.expectLabels, labels)
}
}
// getFakeLegacyContainers returns a list of fake legacy containers.
func getFakeLegacyContainers() []*dockertools.FakeContainer {
return []*dockertools.FakeContainer{
{
ID: "12",
Name: "k8s_POD.hash1_podname_podnamespace_poduid_randomid",
Config: &dockercontainer.Config{
Labels: map[string]string{
types.KubernetesPodNameLabel: "podname",
types.KubernetesPodNamespaceLabel: "podnamespace",
types.KubernetesPodUIDLabel: "poduid",
types.KubernetesContainerNameLabel: "POD",
containerHashLabel: "hash1",
containerRestartCountLabel: "0",
},
},
},
{
ID: "34",
Name: "k8s_legacycontainer.hash2_podname_podnamespace_poduid_randomid",
Config: &dockercontainer.Config{
Labels: map[string]string{
types.KubernetesPodNameLabel: "podname",
types.KubernetesPodNamespaceLabel: "podnamespace",
types.KubernetesPodUIDLabel: "poduid",
types.KubernetesContainerNameLabel: "legacyContainer",
containerHashLabel: "hash2",
containerRestartCountLabel: "5",
},
},
},
}
}
// getFakeNewContainers returns a list of fake new containers.
func getFakeNewContainers() []*dockertools.FakeContainer {
return []*dockertools.FakeContainer{
{
ID: "56",
Name: "k8s_POD_podname_podnamespace_poduid_0",
Config: &dockercontainer.Config{
Labels: map[string]string{
types.KubernetesPodNameLabel: "podname",
types.KubernetesPodNamespaceLabel: "podnamespace",
types.KubernetesPodUIDLabel: "poduid",
types.KubernetesContainerNameLabel: "POD",
containerTypeLabelKey: containerTypeLabelSandbox,
},
},
},
{
ID: "78",
Name: "k8s_newcontainer_podname_podnamespace_poduid_3",
Config: &dockercontainer.Config{
Labels: map[string]string{
types.KubernetesPodNameLabel: "podname",
types.KubernetesPodNamespaceLabel: "podnamespace",
types.KubernetesPodUIDLabel: "poduid",
types.KubernetesContainerNameLabel: "newcontainer",
annotationPrefix + containerHashLabel: "hash4",
annotationPrefix + containerRestartCountLabel: "3",
containerTypeLabelKey: containerTypeLabelContainer,
},
},
},
}
}
func TestListLegacyContainers(t *testing.T) {
ds, fDocker, _ := newTestDockerService()
newContainers := getFakeLegacyContainers()
legacyContainers := getFakeNewContainers()
fDocker.SetFakeContainers(append(newContainers, legacyContainers...))
// ListContainers should list only new containers when legacyCleanup is done.
containers, err := ds.ListContainers(nil)
assert.NoError(t, err)
require.Len(t, containers, 1)
assert.Equal(t, "78", containers[0].Id)
// ListLegacyContainers should list only legacy containers.
containers, err = ds.ListLegacyContainers(nil)
assert.NoError(t, err)
require.Len(t, containers, 1)
assert.Equal(t, "34", containers[0].Id)
// Mark legacyCleanup as not done.
ds.legacyCleanup.done = 0
// ListContainers should list all containers when legacyCleanup is not done.
containers, err = ds.ListContainers(nil)
assert.NoError(t, err)
require.Len(t, containers, 2)
assert.Contains(t, []string{containers[0].Id, containers[1].Id}, "34")
assert.Contains(t, []string{containers[0].Id, containers[1].Id}, "78")
}
func TestListLegacyPodSandbox(t *testing.T) {
ds, fDocker, _ := newTestDockerService()
newContainers := getFakeLegacyContainers()
legacyContainers := getFakeNewContainers()
fDocker.SetFakeContainers(append(newContainers, legacyContainers...))
// ListPodSandbox should list only new sandboxes when legacyCleanup is done.
sandboxes, err := ds.ListPodSandbox(nil)
assert.NoError(t, err)
require.Len(t, sandboxes, 1)
assert.Equal(t, "56", sandboxes[0].Id)
// ListLegacyPodSandbox should list only legacy sandboxes.
sandboxes, err = ds.ListLegacyPodSandbox(nil)
assert.NoError(t, err)
require.Len(t, sandboxes, 1)
assert.Equal(t, "12", sandboxes[0].Id)
// Mark legacyCleanup as not done.
ds.legacyCleanup.done = 0
// ListPodSandbox should list all sandboxes when legacyCleanup is not done.
sandboxes, err = ds.ListPodSandbox(nil)
assert.NoError(t, err)
require.Len(t, sandboxes, 2)
assert.Contains(t, []string{sandboxes[0].Id, sandboxes[1].Id}, "12")
assert.Contains(t, []string{sandboxes[0].Id, sandboxes[1].Id}, "56")
}
func TestCheckLegacyCleanup(t *testing.T) {
for desc, test := range map[string]struct {
containers []*dockertools.FakeContainer
done bool
}{
"no containers": {
containers: []*dockertools.FakeContainer{},
done: true,
},
"only new containers": {
containers: getFakeNewContainers(),
done: true,
},
"only legacy containers": {
containers: getFakeLegacyContainers(),
done: false,
},
"both legacy and new containers": {
containers: append(getFakeNewContainers(), getFakeLegacyContainers()...),
done: false,
},
} {
t.Logf("TestCase %q", desc)
ds, fDocker, _ := newTestDockerService()
fDocker.SetFakeContainers(test.containers)
ds.legacyCleanup.done = 0
clean, err := ds.checkLegacyCleanup()
assert.NoError(t, err)
assert.Equal(t, test.done, clean)
assert.Equal(t, test.done, ds.legacyCleanup.Done())
}
}
...@@ -244,12 +244,23 @@ func (ds *dockerService) PodSandboxStatus(podSandboxID string) (*runtimeapi.PodS ...@@ -244,12 +244,23 @@ func (ds *dockerService) PodSandboxStatus(podSandboxID string) (*runtimeapi.PodS
} }
network := &runtimeapi.PodSandboxNetworkStatus{Ip: IP} network := &runtimeapi.PodSandboxNetworkStatus{Ip: IP}
netNS := getNetworkNamespace(r) netNS := getNetworkNamespace(r)
hostNetwork := sharesHostNetwork(r)
// If the sandbox has no containerTypeLabelKey label, treat it as a legacy sandbox.
if _, ok := r.Config.Labels[containerTypeLabelKey]; !ok {
names, labels, err := convertLegacyNameAndLabels([]string{r.Name}, r.Config.Labels)
if err != nil {
return nil, err
}
r.Name, r.Config.Labels = names[0], labels
// Forcibly trigger infra container restart.
hostNetwork = !hostNetwork
}
metadata, err := parseSandboxName(r.Name) metadata, err := parseSandboxName(r.Name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hostNetwork := sharesHostNetwork(r)
labels, annotations := extractLabels(r.Config.Labels) labels, annotations := extractLabels(r.Config.Labels)
return &runtimeapi.PodSandboxStatus{ return &runtimeapi.PodSandboxStatus{
Id: r.ID, Id: r.ID,
...@@ -318,7 +329,7 @@ func (ds *dockerService) ListPodSandbox(filter *runtimeapi.PodSandboxFilter) ([] ...@@ -318,7 +329,7 @@ func (ds *dockerService) ListPodSandbox(filter *runtimeapi.PodSandboxFilter) ([]
c := containers[i] c := containers[i]
converted, err := containerToRuntimeAPISandbox(&c) converted, err := containerToRuntimeAPISandbox(&c)
if err != nil { if err != nil {
glog.V(4).Infof("Unable to convert docker to runtime API sandbox: %v", err) glog.V(4).Infof("Unable to convert docker to runtime API sandbox %+v: %v", c, err)
continue continue
} }
if filterOutReadySandboxes && converted.State == runtimeapi.PodSandboxState_SANDBOX_READY { if filterOutReadySandboxes && converted.State == runtimeapi.PodSandboxState_SANDBOX_READY {
...@@ -353,6 +364,16 @@ func (ds *dockerService) ListPodSandbox(filter *runtimeapi.PodSandboxFilter) ([] ...@@ -353,6 +364,16 @@ func (ds *dockerService) ListPodSandbox(filter *runtimeapi.PodSandboxFilter) ([]
result = append(result, checkpointToRuntimeAPISandbox(id, checkpoint)) result = append(result, checkpointToRuntimeAPISandbox(id, checkpoint))
} }
} }
// Include legacy sandboxes if there are still legacy sandboxes not cleaned up yet.
if !ds.legacyCleanup.Done() {
legacySandboxes, err := ds.ListLegacyPodSandbox(filter)
if err != nil {
return nil, err
}
// Legacy sandboxes are always older, so we can safely append them to the end.
result = append(result, legacySandboxes...)
}
return result, nil return result, nil
} }
......
...@@ -153,7 +153,6 @@ func NewDockerService(client dockertools.DockerInterface, seccompProfileRoot str ...@@ -153,7 +153,6 @@ func NewDockerService(client dockertools.DockerInterface, seccompProfileRoot str
glog.Infof("Setting cgroupDriver to %s", cgroupDriver) glog.Infof("Setting cgroupDriver to %s", cgroupDriver)
} }
ds.cgroupDriver = cgroupDriver ds.cgroupDriver = cgroupDriver
return ds, nil return ds, nil
} }
...@@ -179,6 +178,8 @@ type dockerService struct { ...@@ -179,6 +178,8 @@ type dockerService struct {
// cgroup driver used by Docker runtime. // cgroup driver used by Docker runtime.
cgroupDriver string cgroupDriver string
checkpointHandler CheckpointHandler checkpointHandler CheckpointHandler
// legacyCleanup indicates whether legacy cleanup has finished or not.
legacyCleanup legacyCleanupFlag
} }
// Version returns the runtime name, runtime version and runtime API version // Version returns the runtime name, runtime version and runtime API version
...@@ -241,6 +242,8 @@ type dockerNetworkHost struct { ...@@ -241,6 +242,8 @@ type dockerNetworkHost struct {
// Start initializes and starts components in dockerService. // Start initializes and starts components in dockerService.
func (ds *dockerService) Start() error { func (ds *dockerService) Start() error {
// Initialize the legacy cleanup flag.
ds.LegacyCleanupInit()
return ds.containerManager.Start() return ds.containerManager.Start()
} }
......
...@@ -41,7 +41,8 @@ func newTestNetworkPlugin(t *testing.T) *mock_network.MockNetworkPlugin { ...@@ -41,7 +41,8 @@ func newTestNetworkPlugin(t *testing.T) *mock_network.MockNetworkPlugin {
func newTestDockerService() (*dockerService, *dockertools.FakeDockerClient, *clock.FakeClock) { func newTestDockerService() (*dockerService, *dockertools.FakeDockerClient, *clock.FakeClock) {
fakeClock := clock.NewFakeClock(time.Time{}) fakeClock := clock.NewFakeClock(time.Time{})
c := dockertools.NewFakeDockerClient().WithClock(fakeClock) c := dockertools.NewFakeDockerClient().WithClock(fakeClock)
return &dockerService{client: c, os: &containertest.FakeOS{}, networkPlugin: &network.NoopNetworkPlugin{}, checkpointHandler: NewTestPersistentCheckpointHandler()}, c, fakeClock return &dockerService{client: c, os: &containertest.FakeOS{}, networkPlugin: &network.NoopNetworkPlugin{},
legacyCleanup: legacyCleanupFlag{done: 1}, checkpointHandler: NewTestPersistentCheckpointHandler()}, c, fakeClock
} }
// TestStatus tests the runtime status logic. // TestStatus tests the runtime status logic.
......
...@@ -76,7 +76,6 @@ func makeContainerName(s *runtimeapi.PodSandboxConfig, c *runtimeapi.ContainerCo ...@@ -76,7 +76,6 @@ func makeContainerName(s *runtimeapi.PodSandboxConfig, c *runtimeapi.ContainerCo
s.Metadata.Uid, // 4 sandbox uid s.Metadata.Uid, // 4 sandbox uid
fmt.Sprintf("%d", c.Metadata.Attempt), // 5 fmt.Sprintf("%d", c.Metadata.Attempt), // 5
}, nameDelimiter) }, nameDelimiter)
} }
// randomizeName randomizes the container name. This should only be used when we hit the // randomizeName randomizes the container name. This should only be used when we hit the
......
...@@ -23,6 +23,7 @@ import ( ...@@ -23,6 +23,7 @@ import (
"os" "os"
"reflect" "reflect"
"sort" "sort"
"strings"
"sync" "sync"
"time" "time"
...@@ -232,6 +233,9 @@ func (f *FakeDockerClient) SetFakeContainers(containers []*FakeContainer) { ...@@ -232,6 +233,9 @@ func (f *FakeDockerClient) SetFakeContainers(containers []*FakeContainer) {
Names: []string{c.Name}, Names: []string{c.Name},
ID: c.ID, ID: c.ID,
} }
if c.Config != nil {
container.Labels = c.Config.Labels
}
if c.Running { if c.Running {
f.RunningContainerList = append(f.RunningContainerList, container) f.RunningContainerList = append(f.RunningContainerList, container)
} else { } else {
...@@ -349,7 +353,30 @@ func (f *FakeDockerClient) ListContainers(options dockertypes.ContainerListOptio ...@@ -349,7 +353,30 @@ func (f *FakeDockerClient) ListContainers(options dockertypes.ContainerListOptio
// TODO(random-liu): Is a fully sorted array needed? // TODO(random-liu): Is a fully sorted array needed?
containerList = append(containerList, f.ExitedContainerList...) containerList = append(containerList, f.ExitedContainerList...)
} }
return containerList, err // TODO: Support other filters.
// Filter containers with label filter.
labelFilters := options.Filter.Get("label")
if len(labelFilters) == 0 {
return containerList, err
}
var filtered []dockertypes.Container
for _, container := range containerList {
match := true
for _, labelFilter := range labelFilters {
kv := strings.Split(labelFilter, "=")
if len(kv) != 2 {
return nil, fmt.Errorf("invalid label filter %q", labelFilter)
}
if container.Labels[kv[0]] != kv[1] {
match = false
break
}
}
if match {
filtered = append(filtered, container)
}
}
return filtered, err
} }
// InspectContainer is a test-spy implementation of DockerInterface.InspectContainer. // InspectContainer is a test-spy implementation of DockerInterface.InspectContainer.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment