forked from kubernetes/kubernetes
-
Notifications
You must be signed in to change notification settings - Fork 0
/
image_manager.go
300 lines (251 loc) · 8.39 KB
/
image_manager.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 kubelet
import (
"fmt"
"sort"
"sync"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/record"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/cadvisor"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/dockertools"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
docker "github.com/fsouza/go-dockerclient"
"github.com/golang/glog"
)
// Manages lifecycle of all images.
//
// Implementation is thread-safe.
type imageManager interface {
// Applies the garbage collection policy. Errors include being unable to free
// enough space as per the garbage collection policy.
GarbageCollect() error
// Start async garbage collection of images.
Start() error
// TODO(vmarmol): Have this subsume pulls as well.
}
// A policy for garbage collecting images. Policy defines an allowed band in
// which garbage collection will be run.
type ImageGCPolicy struct {
// Any usage above this threshold will always trigger garbage collection.
// This is the highest usage we will allow.
HighThresholdPercent int
// Any usage below this threshold will never trigger garbage collection.
// This is the lowest threshold we will try to garbage collect to.
LowThresholdPercent int
}
type realImageManager struct {
// Connection to the Docker daemon.
dockerClient dockertools.DockerInterface
// Records of images and their use.
imageRecords map[string]*imageRecord
imageRecordsLock sync.Mutex
// The image garbage collection policy in use.
policy ImageGCPolicy
// cAdvisor instance.
cadvisor cadvisor.Interface
// Recorder for Kubernetes events.
recorder record.EventRecorder
// Reference to this node.
nodeRef *api.ObjectReference
}
// Information about the images we track.
type imageRecord struct {
// Time when this image was first detected.
detected time.Time
// Time when we last saw this image being used.
lastUsed time.Time
// Size of the image in bytes.
size int64
}
func newImageManager(dockerClient dockertools.DockerInterface, cadvisorInterface cadvisor.Interface, recorder record.EventRecorder, nodeRef *api.ObjectReference, policy ImageGCPolicy) (imageManager, error) {
// Validate policy.
if policy.HighThresholdPercent < 0 || policy.HighThresholdPercent > 100 {
return nil, fmt.Errorf("invalid HighThresholdPercent %d, must be in range [0-100]", policy.HighThresholdPercent)
}
if policy.LowThresholdPercent < 0 || policy.LowThresholdPercent > 100 {
return nil, fmt.Errorf("invalid LowThresholdPercent %d, must be in range [0-100]", policy.LowThresholdPercent)
}
im := &realImageManager{
dockerClient: dockerClient,
policy: policy,
imageRecords: make(map[string]*imageRecord),
cadvisor: cadvisorInterface,
recorder: recorder,
nodeRef: nodeRef,
}
return im, nil
}
func (im *realImageManager) Start() error {
// Initial detection make detected time "unknown" in the past.
var zero time.Time
err := im.detectImages(zero)
if err != nil {
return err
}
go util.Forever(func() {
err := im.detectImages(time.Now())
if err != nil {
glog.Warningf("[ImageManager] Failed to monitor images: %v", err)
}
}, 5*time.Minute)
return nil
}
func (im *realImageManager) detectImages(detected time.Time) error {
images, err := im.dockerClient.ListImages(docker.ListImagesOptions{})
if err != nil {
return err
}
containers, err := im.dockerClient.ListContainers(docker.ListContainersOptions{
All: true,
})
if err != nil {
return err
}
// Make a set of images in use by containers.
imagesInUse := util.NewStringSet()
for _, container := range containers {
imagesInUse.Insert(container.Image)
}
// Add new images and record those being used.
now := time.Now()
currentImages := util.NewStringSet()
im.imageRecordsLock.Lock()
defer im.imageRecordsLock.Unlock()
for _, image := range images {
currentImages.Insert(image.ID)
// New image, set it as detected now.
if _, ok := im.imageRecords[image.ID]; !ok {
im.imageRecords[image.ID] = &imageRecord{
detected: detected,
}
}
// Set last used time to now if the image is being used.
if isImageUsed(&image, imagesInUse) {
im.imageRecords[image.ID].lastUsed = now
}
im.imageRecords[image.ID].size = image.VirtualSize
}
// Remove old images from our records.
for image := range im.imageRecords {
if !currentImages.Has(image) {
delete(im.imageRecords, image)
}
}
return nil
}
func (im *realImageManager) GarbageCollect() error {
// Get disk usage on disk holding images.
fsInfo, err := im.cadvisor.DockerImagesFsInfo()
if err != nil {
return err
}
usage := int64(fsInfo.Usage)
capacity := int64(fsInfo.Capacity)
// Check valid capacity.
if capacity == 0 {
err := fmt.Errorf("invalid capacity %d on device %q at mount point %q", capacity, fsInfo.Device, fsInfo.Mountpoint)
im.recorder.Eventf(im.nodeRef, "invalidDiskCapacity", err.Error())
return err
}
// If over the max threshold, free enough to place us at the lower threshold.
usagePercent := int(usage * 100 / capacity)
if usagePercent >= im.policy.HighThresholdPercent {
amountToFree := usage - (int64(im.policy.LowThresholdPercent) * capacity / 100)
glog.Infof("[ImageManager]: Disk usage on %q (%s) is at %d%% which is over the high threshold (%d%%). Trying to free %d bytes", fsInfo.Device, fsInfo.Mountpoint, usagePercent, im.policy.HighThresholdPercent, amountToFree)
freed, err := im.freeSpace(amountToFree)
if err != nil {
return err
}
if freed < amountToFree {
err := fmt.Errorf("failed to garbage collect required amount of images. Wanted to free %d, but freed %d", amountToFree, freed)
im.recorder.Eventf(im.nodeRef, "freeDiskSpaceFailed", err.Error())
return err
}
}
return nil
}
// Tries to free bytesToFree worth of images on the disk.
//
// Returns the number of bytes free and an error if any occured. The number of
// bytes freed is always returned.
// Note that error may be nil and the number of bytes free may be less
// than bytesToFree.
func (im *realImageManager) freeSpace(bytesToFree int64) (int64, error) {
startTime := time.Now()
err := im.detectImages(startTime)
if err != nil {
return 0, err
}
im.imageRecordsLock.Lock()
defer im.imageRecordsLock.Unlock()
// Get all images in eviction order.
images := make([]evictionInfo, 0, len(im.imageRecords))
for image, record := range im.imageRecords {
images = append(images, evictionInfo{
id: image,
imageRecord: *record,
})
}
sort.Sort(byLastUsedAndDetected(images))
// Delete unused images until we've freed up enough space.
var lastErr error
spaceFreed := int64(0)
for _, image := range images {
// Images that are currently in used were given a newer lastUsed.
if image.lastUsed.After(startTime) {
break
}
// Remove image. Continue despite errors.
glog.Infof("[ImageManager]: Removing image %q to free %d bytes", image.id, image.size)
err := im.dockerClient.RemoveImage(image.id)
if err != nil {
lastErr = err
continue
}
delete(im.imageRecords, image.id)
spaceFreed += image.size
if spaceFreed >= bytesToFree {
break
}
}
return spaceFreed, lastErr
}
type evictionInfo struct {
id string
imageRecord
}
type byLastUsedAndDetected []evictionInfo
func (ev byLastUsedAndDetected) Len() int { return len(ev) }
func (ev byLastUsedAndDetected) Swap(i, j int) { ev[i], ev[j] = ev[j], ev[i] }
func (ev byLastUsedAndDetected) Less(i, j int) bool {
// Sort by last used, break ties by detected.
if ev[i].lastUsed.Equal(ev[j].lastUsed) {
return ev[i].detected.Before(ev[j].detected)
} else {
return ev[i].lastUsed.Before(ev[j].lastUsed)
}
}
func isImageUsed(image *docker.APIImages, imagesInUse util.StringSet) bool {
// Check the image ID and all the RepoTags.
if _, ok := imagesInUse[image.ID]; ok {
return true
}
for _, tag := range image.RepoTags {
if _, ok := imagesInUse[tag]; ok {
return true
}
}
return false
}