|
| 1 | +package dockergc |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "io" |
| 7 | + "os" |
| 8 | + "os/exec" |
| 9 | + "sort" |
| 10 | + "strconv" |
| 11 | + "strings" |
| 12 | + "time" |
| 13 | + |
| 14 | + "github.com/spf13/cobra" |
| 15 | + |
| 16 | + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" |
| 17 | + kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" |
| 18 | + |
| 19 | + dockerapi "github.com/docker/engine-api/client" |
| 20 | + dockertypes "github.com/docker/engine-api/types" |
| 21 | + dockerfilters "github.com/docker/engine-api/types/filters" |
| 22 | + cmdutil "github.com/openshift/origin/pkg/cmd/util" |
| 23 | + "github.com/openshift/origin/pkg/cmd/util/clientcmd" |
| 24 | + configcmd "github.com/openshift/origin/pkg/config/cmd" |
| 25 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 26 | +) |
| 27 | + |
| 28 | +const ( |
| 29 | + DefaultImageGCHighThresholdPercent = int32(80) |
| 30 | + DefaultImageGCLowThresholdPercent = int32(60) |
| 31 | +) |
| 32 | + |
| 33 | +var ( |
| 34 | + DefaultMinimumGCAge = metav1.Duration{Duration: time.Hour} |
| 35 | +) |
| 36 | + |
| 37 | +// DockerGCConfigCmdOptions are options supported by the dockergc admin command. |
| 38 | +type dockerGCConfigCmdOptions struct { |
| 39 | + Action configcmd.BulkAction |
| 40 | + |
| 41 | + // MinimumGCAge is the minimum age for a container or unused image before |
| 42 | + // it is garbage collected. |
| 43 | + MinimumGCAge metav1.Duration |
| 44 | + // ImageGCHighThresholdPercent is the percent of disk usage after which |
| 45 | + // image garbage collection is always run. |
| 46 | + ImageGCHighThresholdPercent int32 |
| 47 | + // ImageGCLowThresholdPercent is the percent of disk usage before which |
| 48 | + // image garbage collection is never run. Lowest disk usage to garbage |
| 49 | + // collect to. |
| 50 | + ImageGCLowThresholdPercent int32 |
| 51 | +} |
| 52 | + |
| 53 | +var ( |
| 54 | + dockerGC_long = templates.LongDesc(` |
| 55 | + Perform garbage collection to free space in docker storage |
| 56 | +
|
| 57 | + If the OpenShift node is configured to use a container runtime other than docker, |
| 58 | + docker will still be used to do builds. However OpenShift itself will not |
| 59 | + manage the docker storage since it is not the container runtime for pods. |
| 60 | + |
| 61 | + This utility allows garbage collection to do be done on the docker storage.`) |
| 62 | + |
| 63 | + dockerGC_example = templates.Examples(` |
| 64 | + # Perform garbage collection with the default settings |
| 65 | + %[1]s %[2]s`) |
| 66 | +) |
| 67 | + |
| 68 | +func NewCmdDockerGCConfig(f *clientcmd.Factory, parentName, name string, out, errout io.Writer) *cobra.Command { |
| 69 | + options := &dockerGCConfigCmdOptions{ |
| 70 | + Action: configcmd.BulkAction{ |
| 71 | + Out: out, |
| 72 | + ErrOut: errout, |
| 73 | + }, |
| 74 | + MinimumGCAge: DefaultMinimumGCAge, |
| 75 | + ImageGCHighThresholdPercent: DefaultImageGCHighThresholdPercent, |
| 76 | + ImageGCLowThresholdPercent: DefaultImageGCLowThresholdPercent, |
| 77 | + } |
| 78 | + |
| 79 | + cmd := &cobra.Command{ |
| 80 | + Use: fmt.Sprintf("%s [NAME]", name), |
| 81 | + Short: "Perform garbage collection to free space in docker storage", |
| 82 | + Long: dockerGC_long, |
| 83 | + Example: fmt.Sprintf(dockerGC_example, parentName, name), |
| 84 | + Run: func(cmd *cobra.Command, args []string) { |
| 85 | + err := Run(f, options, cmd, args) |
| 86 | + if err == cmdutil.ErrExit { |
| 87 | + os.Exit(1) |
| 88 | + } |
| 89 | + kcmdutil.CheckErr(err) |
| 90 | + }, |
| 91 | + } |
| 92 | + |
| 93 | + cmd.Flags().DurationVar(&options.MinimumGCAge.Duration, "minimum-ttl-duration", options.MinimumGCAge.Duration, "Minimum age for a container or unused image before it is garbage collected. Examples: '300ms', '10s' or '2h45m'.") |
| 94 | + cmd.Flags().Int32Var(&options.ImageGCHighThresholdPercent, "image-gc-high-threshold", options.ImageGCHighThresholdPercent, "The percent of disk usage after which image garbage collection is always run.") |
| 95 | + cmd.Flags().Int32Var(&options.ImageGCLowThresholdPercent, "image-gc-low-threshold", options.ImageGCLowThresholdPercent, "The percent of disk usage before which image garbage collection is never run. Lowest disk usage to garbage collect to.") |
| 96 | + |
| 97 | + options.Action.BindForOutput(cmd.Flags()) |
| 98 | + |
| 99 | + return cmd |
| 100 | +} |
| 101 | + |
| 102 | +// parseInfo parses df output to return capacity and used in bytes |
| 103 | +func parseInfo(str string) (int64, int64, error) { |
| 104 | + fields := strings.Fields(str) |
| 105 | + if len(fields) != 4 { |
| 106 | + return 0, 0, fmt.Errorf("unable to parse df output") |
| 107 | + } |
| 108 | + value, err := strconv.ParseInt(fields[2], 10, 64) |
| 109 | + if err != nil { |
| 110 | + return 0, 0, err |
| 111 | + } |
| 112 | + capacityKBytes := int64(value) |
| 113 | + value, err = strconv.ParseInt(fields[3], 10, 64) |
| 114 | + if err != nil { |
| 115 | + return 0, 0, err |
| 116 | + } |
| 117 | + usageKBytes := int64(value) |
| 118 | + return capacityKBytes * 1024, usageKBytes * 1024, nil |
| 119 | +} |
| 120 | + |
| 121 | +// getRootDirInfo returns the capacity and usage in bytes for the docker root directory |
| 122 | +func getRootDirInfo(rootDir string) (int64, int64, error) { |
| 123 | + cmd := exec.Command("df", "-k", "--output=size,used", rootDir) |
| 124 | + output, err := cmd.Output() |
| 125 | + if err != nil { |
| 126 | + return 0, 0, err |
| 127 | + } |
| 128 | + return parseInfo(string(output)) |
| 129 | +} |
| 130 | + |
| 131 | +func bytesToMB(bytes int64) int64 { |
| 132 | + return bytes / 1024 / 1024 |
| 133 | +} |
| 134 | + |
| 135 | +type oldestContainersFirst []dockertypes.Container |
| 136 | + |
| 137 | +func (s oldestContainersFirst) Len() int { return len(s) } |
| 138 | +func (s oldestContainersFirst) Swap(i, j int) { s[i], s[j] = s[j], s[i] } |
| 139 | +func (s oldestContainersFirst) Less(i, j int) bool { return s[i].Created < s[j].Created } |
| 140 | + |
| 141 | +type oldestImagesFirst []dockertypes.Image |
| 142 | + |
| 143 | +func (s oldestImagesFirst) Len() int { return len(s) } |
| 144 | +func (s oldestImagesFirst) Swap(i, j int) { s[i], s[j] = s[j], s[i] } |
| 145 | +func (s oldestImagesFirst) Less(i, j int) bool { return s[i].Created < s[j].Created } |
| 146 | + |
| 147 | +// parseDockerTimestamp parses the timestamp returned by Interface from string to time.Time |
| 148 | +func parseDockerTimestamp(s string) (time.Time, error) { |
| 149 | + // Timestamp returned by Docker is in time.RFC3339Nano format. |
| 150 | + return time.Parse(time.RFC3339Nano, s) |
| 151 | +} |
| 152 | + |
| 153 | +func doGarbageCollection(ctx context.Context, client *dockerapi.Client, options *dockerGCConfigCmdOptions, rootDir string) error { |
| 154 | + capacityBytes, usageBytes, err := getRootDirInfo(rootDir) |
| 155 | + if err != nil { |
| 156 | + return err |
| 157 | + } |
| 158 | + |
| 159 | + highThresholdBytes := capacityBytes * int64(options.ImageGCHighThresholdPercent) / 100 |
| 160 | + lowThresholdBytes := capacityBytes * int64(options.ImageGCLowThresholdPercent) / 100 |
| 161 | + if usageBytes < highThresholdBytes { |
| 162 | + fmt.Printf("usage is under high threshold (%vMB < %vMB)\n", bytesToMB(usageBytes), bytesToMB(highThresholdBytes)) |
| 163 | + return nil |
| 164 | + } |
| 165 | + |
| 166 | + attemptToFreeBytes := usageBytes - lowThresholdBytes |
| 167 | + freedBytes := int64(0) |
| 168 | + fmt.Printf("usage exceeds high threshold (%vMB > %vMB), attempting to free %vMB\n", bytesToMB(usageBytes), bytesToMB(highThresholdBytes), bytesToMB(attemptToFreeBytes)) |
| 169 | + |
| 170 | + // conatiners |
| 171 | + exitedFilter := dockerfilters.NewArgs() |
| 172 | + exitedFilter.Add("status", "exited") |
| 173 | + containers, err := client.ContainerList(ctx, dockertypes.ContainerListOptions{All: true, Filter: exitedFilter}) |
| 174 | + if ctx.Err() == context.DeadlineExceeded { |
| 175 | + return ctx.Err() |
| 176 | + } |
| 177 | + if err != nil { |
| 178 | + return err |
| 179 | + } |
| 180 | + fmt.Println(len(containers), "exited containers found") |
| 181 | + sort.Sort(oldestContainersFirst(containers)) |
| 182 | + for _, c := range containers { |
| 183 | + if freedBytes > attemptToFreeBytes { |
| 184 | + fmt.Printf("usage is below low threshold, freed %vMB\n", bytesToMB(freedBytes)) |
| 185 | + return nil |
| 186 | + } |
| 187 | + age := time.Now().Sub(time.Unix(c.Created, 0)) |
| 188 | + if age < options.MinimumGCAge.Duration { |
| 189 | + fmt.Println("remaining containers are too young") |
| 190 | + break |
| 191 | + } |
| 192 | + fmt.Printf("removing container %v (size: %v, age: %v)\n", c.ID, c.SizeRw, age) |
| 193 | + err := client.ContainerRemove(ctx, c.ID, dockertypes.ContainerRemoveOptions{RemoveVolumes: true}) |
| 194 | + if err != nil { |
| 195 | + fmt.Printf("unable to remove container: %v", err) |
| 196 | + } else { |
| 197 | + freedBytes += c.SizeRw |
| 198 | + } |
| 199 | + } |
| 200 | + |
| 201 | + // images |
| 202 | + images, err := client.ImageList(ctx, dockertypes.ImageListOptions{}) |
| 203 | + if ctx.Err() == context.DeadlineExceeded { |
| 204 | + return ctx.Err() |
| 205 | + } |
| 206 | + if err != nil { |
| 207 | + return err |
| 208 | + } |
| 209 | + sort.Sort(oldestImagesFirst(images)) |
| 210 | + for _, i := range images { |
| 211 | + if freedBytes > attemptToFreeBytes { |
| 212 | + fmt.Printf("usage is below low threshold, freed %vMB\n", bytesToMB(freedBytes)) |
| 213 | + return nil |
| 214 | + } |
| 215 | + // filter openshift infra images |
| 216 | + if strings.HasPrefix(i.RepoTags[0], "registry.ops.openshift.com/openshift3") || |
| 217 | + strings.HasPrefix(i.RepoTags[0], "docker.io/openshift") { |
| 218 | + fmt.Println("skipping infra image", i.RepoTags[0]) |
| 219 | + } |
| 220 | + // filter young images |
| 221 | + age := time.Now().Sub(time.Unix(i.Created, 0)) |
| 222 | + if age < options.MinimumGCAge.Duration { |
| 223 | + fmt.Println("remaining images are too young") |
| 224 | + break |
| 225 | + } |
| 226 | + fmt.Printf("removing image %v (size: %v, age: %v)\n", i.ID, i.Size, age) |
| 227 | + _, err := client.ImageRemove(ctx, i.ID, dockertypes.ImageRemoveOptions{PruneChildren: true}) |
| 228 | + if err != nil { |
| 229 | + fmt.Printf("unable to remove container: %v", err) |
| 230 | + } else { |
| 231 | + freedBytes += i.Size |
| 232 | + } |
| 233 | + } |
| 234 | + |
| 235 | + return nil |
| 236 | +} |
| 237 | + |
| 238 | +// Run runs the dockergc command. |
| 239 | +func Run(f *clientcmd.Factory, options *dockerGCConfigCmdOptions, cmd *cobra.Command, args []string) error { |
| 240 | + fmt.Println("docker build garbage collection daemon") |
| 241 | + fmt.Printf("MinimumGCAge: %v, ImageGCHighThresholdPercent: %v, ImageGCLowThresholdPercent: %v\n", options.MinimumGCAge, options.ImageGCHighThresholdPercent, options.ImageGCLowThresholdPercent) |
| 242 | + client, err := dockerapi.NewEnvClient() |
| 243 | + if err != nil { |
| 244 | + return err |
| 245 | + } |
| 246 | + timeout := time.Duration(2 * time.Minute) |
| 247 | + ctx, cancel := context.WithTimeout(context.Background(), timeout) |
| 248 | + defer cancel() |
| 249 | + info, err := client.Info(ctx) |
| 250 | + if err != nil { |
| 251 | + return err |
| 252 | + } |
| 253 | + rootDir := info.DockerRootDir |
| 254 | + if rootDir == "" { |
| 255 | + return fmt.Errorf("unable to determine docker root directory") |
| 256 | + } |
| 257 | + |
| 258 | + for { |
| 259 | + err := doGarbageCollection(ctx, client, options, rootDir) |
| 260 | + if err != nil { |
| 261 | + return err |
| 262 | + } |
| 263 | + <-time.After(time.Minute) |
| 264 | + return nil |
| 265 | + } |
| 266 | +} |
0 commit comments