Skip to content

Commit 299f412

Browse files
committed
cmd: ex: standalone docker garbage collector
1 parent 067e5ec commit 299f412

File tree

2 files changed

+268
-0
lines changed

2 files changed

+268
-0
lines changed

pkg/cmd/openshift/openshift.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/openshift/origin/pkg/oc/cli/cmd"
3232
"github.com/openshift/origin/pkg/oc/experimental/buildchain"
3333
configcmd "github.com/openshift/origin/pkg/oc/experimental/config"
34+
"github.com/openshift/origin/pkg/oc/experimental/dockergc"
3435
exipfailover "github.com/openshift/origin/pkg/oc/experimental/ipfailover"
3536
)
3637

@@ -175,6 +176,7 @@ func newExperimentalCommand(name, fullName string) *cobra.Command {
175176

176177
experimental.AddCommand(validate.NewCommandValidate(validate.ValidateRecommendedName, fullName+" "+validate.ValidateRecommendedName, out, errout))
177178
experimental.AddCommand(exipfailover.NewCmdIPFailoverConfig(f, fullName, "ipfailover", out, errout))
179+
experimental.AddCommand(dockergc.NewCmdDockerGCConfig(f, fullName, "dockergc", out, errout))
178180
experimental.AddCommand(buildchain.NewCmdBuildChain(name, fullName+" "+buildchain.BuildChainRecommendedCommandName, f, out))
179181
experimental.AddCommand(configcmd.NewCmdConfig(configcmd.ConfigRecommendedName, fullName+" "+configcmd.ConfigRecommendedName, f, out, errout))
180182
deprecatedDiag := diagnostics.NewCmdDiagnostics(diagnostics.DiagnosticsRecommendedName, fullName+" "+diagnostics.DiagnosticsRecommendedName, out)
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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

Comments
 (0)