github.com/rish1988/moby@v25.0.2+incompatible/libcontainerd/supervisor/remote_daemon.go (about) 1 package supervisor // import "github.com/docker/docker/libcontainerd/supervisor" 2 3 import ( 4 "context" 5 "os" 6 "os/exec" 7 "path/filepath" 8 "runtime" 9 "strings" 10 "time" 11 12 "github.com/containerd/containerd" 13 "github.com/containerd/containerd/defaults" 14 "github.com/containerd/containerd/services/server/config" 15 "github.com/containerd/containerd/sys" 16 "github.com/containerd/log" 17 "github.com/docker/docker/pkg/pidfile" 18 "github.com/docker/docker/pkg/process" 19 "github.com/docker/docker/pkg/system" 20 "github.com/pelletier/go-toml" 21 "github.com/pkg/errors" 22 "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" 23 "google.golang.org/grpc" 24 "google.golang.org/grpc/credentials/insecure" 25 ) 26 27 const ( 28 maxConnectionRetryCount = 3 29 healthCheckTimeout = 3 * time.Second 30 shutdownTimeout = 15 * time.Second 31 startupTimeout = 15 * time.Second 32 configFile = "containerd.toml" 33 binaryName = "containerd" 34 pidFile = "containerd.pid" 35 ) 36 37 type remote struct { 38 config.Config 39 40 // configFile is the location where the generated containerd configuration 41 // file is saved. 42 configFile string 43 44 daemonPid int 45 pidFile string 46 logger *log.Entry 47 48 daemonWaitCh chan struct{} 49 daemonStartCh chan error 50 daemonStopCh chan struct{} 51 52 stateDir string 53 54 // oomScore adjusts the OOM score for the containerd process. 55 oomScore int 56 57 // logLevel overrides the containerd logging-level through the --log-level 58 // command-line option. 59 logLevel string 60 } 61 62 // Daemon represents a running containerd daemon 63 type Daemon interface { 64 WaitTimeout(time.Duration) error 65 Address() string 66 } 67 68 // DaemonOpt allows to configure parameters of container daemons 69 type DaemonOpt func(c *remote) error 70 71 // Start starts a containerd daemon and monitors it 72 func Start(ctx context.Context, rootDir, stateDir string, opts ...DaemonOpt) (Daemon, error) { 73 r := &remote{ 74 stateDir: stateDir, 75 Config: config.Config{ 76 Version: 2, 77 Root: filepath.Join(rootDir, "daemon"), 78 State: filepath.Join(stateDir, "daemon"), 79 }, 80 configFile: filepath.Join(stateDir, configFile), 81 daemonPid: -1, 82 pidFile: filepath.Join(stateDir, pidFile), 83 logger: log.G(ctx).WithField("module", "libcontainerd"), 84 daemonStartCh: make(chan error, 1), 85 daemonStopCh: make(chan struct{}), 86 } 87 88 for _, opt := range opts { 89 if err := opt(r); err != nil { 90 return nil, err 91 } 92 } 93 r.setDefaults() 94 95 if err := system.MkdirAll(stateDir, 0o700); err != nil { 96 return nil, err 97 } 98 99 go r.monitorDaemon(ctx) 100 101 timeout := time.NewTimer(startupTimeout) 102 defer timeout.Stop() 103 104 select { 105 case <-timeout.C: 106 return nil, errors.New("timeout waiting for containerd to start") 107 case err := <-r.daemonStartCh: 108 if err != nil { 109 return nil, err 110 } 111 } 112 113 return r, nil 114 } 115 116 func (r *remote) WaitTimeout(d time.Duration) error { 117 timeout := time.NewTimer(d) 118 defer timeout.Stop() 119 120 select { 121 case <-timeout.C: 122 return errors.New("timeout waiting for containerd to stop") 123 case <-r.daemonStopCh: 124 } 125 126 return nil 127 } 128 129 func (r *remote) Address() string { 130 return r.GRPC.Address 131 } 132 133 func (r *remote) getContainerdConfig() (string, error) { 134 f, err := os.OpenFile(r.configFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600) 135 if err != nil { 136 return "", errors.Wrapf(err, "failed to open containerd config file (%s)", r.configFile) 137 } 138 defer f.Close() 139 140 if err := toml.NewEncoder(f).Encode(r); err != nil { 141 return "", errors.Wrapf(err, "failed to write containerd config file (%s)", r.configFile) 142 } 143 return r.configFile, nil 144 } 145 146 func (r *remote) startContainerd() error { 147 pid, err := pidfile.Read(r.pidFile) 148 if err != nil && !errors.Is(err, os.ErrNotExist) { 149 return err 150 } 151 152 if pid > 0 { 153 r.daemonPid = pid 154 r.logger.WithField("pid", pid).Infof("%s is still running", binaryName) 155 return nil 156 } 157 158 cfgFile, err := r.getContainerdConfig() 159 if err != nil { 160 return err 161 } 162 args := []string{"--config", cfgFile} 163 164 if r.logLevel != "" { 165 args = append(args, "--log-level", r.logLevel) 166 } 167 168 cmd := exec.Command(binaryName, args...) 169 // redirect containerd logs to docker logs 170 cmd.Stdout = os.Stdout 171 cmd.Stderr = os.Stderr 172 cmd.SysProcAttr = containerdSysProcAttr() 173 // clear the NOTIFY_SOCKET from the env when starting containerd 174 cmd.Env = nil 175 for _, e := range os.Environ() { 176 if !strings.HasPrefix(e, "NOTIFY_SOCKET") { 177 cmd.Env = append(cmd.Env, e) 178 } 179 } 180 181 startedCh := make(chan error) 182 go func() { 183 // On Linux, when cmd.SysProcAttr.Pdeathsig is set, 184 // the signal is sent to the subprocess when the creating thread 185 // terminates. The runtime terminates a thread if a goroutine 186 // exits while locked to it. Prevent the containerd process 187 // from getting killed prematurely by ensuring that the thread 188 // used to start it remains alive until it or the daemon process 189 // exits. See https://go.dev/issue/27505 for more details. 190 runtime.LockOSThread() 191 defer runtime.UnlockOSThread() 192 err := cmd.Start() 193 startedCh <- err 194 if err != nil { 195 return 196 } 197 198 r.daemonWaitCh = make(chan struct{}) 199 // Reap our child when needed 200 if err := cmd.Wait(); err != nil { 201 r.logger.WithError(err).Errorf("containerd did not exit successfully") 202 } 203 close(r.daemonWaitCh) 204 }() 205 if err := <-startedCh; err != nil { 206 return err 207 } 208 209 r.daemonPid = cmd.Process.Pid 210 211 if err := r.adjustOOMScore(); err != nil { 212 r.logger.WithError(err).Warn("failed to adjust OOM score") 213 } 214 215 if err := pidfile.Write(r.pidFile, r.daemonPid); err != nil { 216 _ = process.Kill(r.daemonPid) 217 return errors.Wrap(err, "libcontainerd: failed to save daemon pid to disk") 218 } 219 220 r.logger.WithField("pid", r.daemonPid).WithField("address", r.Address()).Infof("started new %s process", binaryName) 221 222 return nil 223 } 224 225 func (r *remote) adjustOOMScore() error { 226 if r.oomScore == 0 || r.daemonPid <= 1 { 227 // no score configured, or daemonPid contains an invalid PID (we don't 228 // expect containerd to be running as PID 1 :)). 229 return nil 230 } 231 if err := sys.SetOOMScore(r.daemonPid, r.oomScore); err != nil { 232 return errors.Wrap(err, "failed to adjust OOM score for containerd process") 233 } 234 return nil 235 } 236 237 func (r *remote) monitorDaemon(ctx context.Context) { 238 var ( 239 transientFailureCount = 0 240 client *containerd.Client 241 err error 242 delay time.Duration 243 timer = time.NewTimer(0) 244 started bool 245 ) 246 247 defer func() { 248 if r.daemonPid != -1 { 249 r.stopDaemon() 250 } 251 252 // cleanup some files 253 _ = os.Remove(r.pidFile) 254 255 r.platformCleanup() 256 257 close(r.daemonStopCh) 258 timer.Stop() 259 }() 260 261 // ensure no races on sending to timer.C even though there is a 0 duration. 262 if !timer.Stop() { 263 <-timer.C 264 } 265 266 for { 267 timer.Reset(delay) 268 269 select { 270 case <-ctx.Done(): 271 r.logger.Info("stopping healthcheck following graceful shutdown") 272 if client != nil { 273 client.Close() 274 } 275 return 276 case <-timer.C: 277 } 278 279 if r.daemonPid == -1 { 280 if r.daemonWaitCh != nil { 281 select { 282 case <-ctx.Done(): 283 r.logger.Info("stopping containerd startup following graceful shutdown") 284 return 285 case <-r.daemonWaitCh: 286 } 287 } 288 289 os.RemoveAll(r.GRPC.Address) 290 if err := r.startContainerd(); err != nil { 291 if !started { 292 r.daemonStartCh <- err 293 return 294 } 295 r.logger.WithError(err).Error("failed restarting containerd") 296 delay = 50 * time.Millisecond 297 continue 298 } 299 300 client, err = containerd.New( 301 r.GRPC.Address, 302 containerd.WithTimeout(60*time.Second), 303 containerd.WithDialOpts([]grpc.DialOption{ 304 grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()), 305 grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()), 306 grpc.WithTransportCredentials(insecure.NewCredentials()), 307 grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(defaults.DefaultMaxRecvMsgSize)), 308 grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(defaults.DefaultMaxSendMsgSize)), 309 }), 310 ) 311 if err != nil { 312 r.logger.WithError(err).Error("failed connecting to containerd") 313 delay = 100 * time.Millisecond 314 continue 315 } 316 r.logger.WithField("address", r.GRPC.Address).Debug("created containerd monitoring client") 317 } 318 319 if client != nil { 320 tctx, cancel := context.WithTimeout(ctx, healthCheckTimeout) 321 _, err := client.IsServing(tctx) 322 cancel() 323 if err == nil { 324 if !started { 325 close(r.daemonStartCh) 326 started = true 327 } 328 329 transientFailureCount = 0 330 331 select { 332 case <-r.daemonWaitCh: 333 case <-ctx.Done(): 334 } 335 336 // Set a small delay in case there is a recurring failure (or bug in this code) 337 // to ensure we don't end up in a super tight loop. 338 delay = 500 * time.Millisecond 339 continue 340 } 341 342 r.logger.WithError(err).WithField("binary", binaryName).Debug("daemon is not responding") 343 344 transientFailureCount++ 345 if transientFailureCount < maxConnectionRetryCount || process.Alive(r.daemonPid) { 346 delay = time.Duration(transientFailureCount) * 200 * time.Millisecond 347 continue 348 } 349 client.Close() 350 client = nil 351 } 352 353 if process.Alive(r.daemonPid) { 354 r.logger.WithField("pid", r.daemonPid).Info("killing and restarting containerd") 355 r.killDaemon() 356 } 357 358 r.daemonPid = -1 359 delay = 0 360 transientFailureCount = 0 361 } 362 }