On stop container

Time:2020-10-23

On stop container

docker stop

For dockers, generally speaking, throughdocker stopCommand to implement the stop container instead ofdocker kill

The specific orders are as follows:

docker stop [OPTIONS] CONTAINER [CONTAINER...]

The main process in the container (process with PID 1) will receive SIGTERM and sigkill after the grace period. The application program in the container can choose to ignore or not process SIGTERM signal. However, once the timeout period is reached, the program will be forcibly killed by the system, because the sigkill signal is sent directly to the system kernel, and the application program has no chance to process it.

As for the grace period, the default value is 10s. Of course, the specific time can be set by parameters.

docker stop --help

Usage:  docker stop [OPTIONS] CONTAINER [CONTAINER...]

Stop one or more running containers

Options:
      --help       Print usage
  -t, --time int   Seconds to wait for stop before killing it (default 10)

For k8s, the default grace period of pod is 30s. adoptterminationGracePeriodSecondsParameter setting.

Why need elegant stop docker?

Your program needs some exit work, such as saving checkpoint, recycling some resource objects, etc. If your service is an HTTP server, you need to complete the requests that have been processed. If it is a long link, you also need to take the initiative to turn off keepalive.

If you run the container in k8s, the whole mechanism of k8s is a watch based parallel mechanism, and we can’t guarantee the serial execution of operations. For example, when you delete a pod, you need to change the iptables rule and remove the upstream of lb.

Why can’t your application receive SIGTERM shutdown signals?

  • Your business process is not process 1

Dockerfile supports two formats to define entry point: shell format and exec format.

Exec formatAs follows:

ENTRYPOINT ["/app/bin/your-app", "arg1", "arg2"]

This format ensures that your main process receives a shutdown signal.

Examples

The program code is as follows:

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    c := make(chan os.Signal)
    //Monitoring signal
    signal.Notify(c, syscall.SIGTERM)
    go func() {
        for s := range c {
            switch s {
            case syscall.SIGTERM:
                fmt.Println ("exit:, s)"
                ExitFunc()
            default:
                fmt.Println ("other signals?", s)
            }
        }
    }()
    fmt.Println ("program started")
    sum := 0
    for {
        sum++
        fmt.Println ("dormant":, sum, "seconds")
        time.Sleep(1 * time.Second)
    }
}

func ExitFunc() {
    fmt.Println (start exit...)
    fmt.Println ("perform cleanup...")
    fmt.Println (end exit...)
    os.Exit(0)
}

The dockerfiler is as follows. We use multi-stage Construction:

FROM golang:latest as builder

WORKDIR /go/src
COPY main.go .

RUN CGO_ENABLED=0 go build -o stop ./main.go

From alpine:latest

WORKDIR /root/
COPY --from=builder /go/src/stop .
RUN chmod +x /root/stop

ENTRYPOINT ["/root/stop"]

Build image:

docker build -t stop .
Sending build context to Docker daemon  3.584kB
Step 1/9 : FROM golang:latest as builder
latest: Pulling from library/golang
376057ac6fa1: Pull complete 
5a63a0a859d8: Pull complete 
496548a8c952: Pull complete 
2adae3950d4d: Pull complete 
039b991354af: Pull complete 
0cca3cbecb14: Pull complete 
59c34b3f33f3: Pull complete 
Digest: sha256:1e36f8e9ac49d5ee6d72e969382a698614551a59f4533d5d61590e3deeb543a7
Status: Downloaded newer image for golang:latest
 ---> 7e5e8028e8ec
Step 2/9 : WORKDIR /go/src
 ---> Running in efb1e4b1c200
Removing intermediate container efb1e4b1c200
 ---> 312e98c07647
Step 3/9 : COPY main.go .
 ---> 2dc4088e6548
Step 4/9 : RUN CGO_ENABLED=0 go build -o stop ./main.go
 ---> Running in 6d18a1ef07ff
Removing intermediate container 6d18a1ef07ff
 ---> a207b2ecdd67
Step 5/9 : From alpine:latest
latest: Pulling from library/alpine
Digest: sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed79109ea4fb9a54
Status: Downloaded newer image for alpine:latest
 ---> f70734b6a266
Step 6/9 : WORKDIR /root/
 ---> Running in a308fc079da2
Removing intermediate container a308fc079da2
 ---> a14716065730
Step 7/9 : COPY --from=builder /go/src/stop .
 ---> 3573b92b9ab3
Step 8/9 : RUN chmod +x /root/stop
 ---> Running in f620b3287636
Removing intermediate container f620b3287636
 ---> 3cbc57300792
Step 9/9 : ENTRYPOINT ["/root/stop"]
 ---> Running in 86f23ea9306f
Removing intermediate container 86f23ea9306f
 ---> 283788e6ad37
Successfully built 283788e6ad37
Successfully tagged stop:latest

Run the image in a terminal:

docker run stop

Stop the container at the other terminal:

docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
91eeef705489        stop                "/root/stop"        12 seconds ago      Up 11 seconds                           clever_leavitt

docker stop 91eeef705489
91eeef705489

The final output is as follows:

Started the program
Sleep: 1 second
Sleep: 2 seconds
Sleep: 3 seconds
Dormant: 4 seconds
Sleep: 5 seconds
Dormant: 6 seconds
Sleep: 7 seconds
Sleep: 8 seconds
Sleep: 9 seconds
Sleep: 10 seconds
Dormant: 11 seconds
Dormant: 12 seconds
Sleep: 13 seconds
Sleep: 14 seconds
Dormant: 15 seconds
Dormant: 16 seconds
Sleep: 17 seconds
Sleep: 18 seconds
Sleep: 19 seconds
Dormant: 20 seconds
Sleep: 21 seconds
Sleep: 22 seconds
Exit: terminated
Start exiting
Perform cleanup
End exit

Through the standard output, our program receives the SIGTERM signal and performs some exit work.

Shell formatAs follows:

ENTRYPOINT "/app/bin/your-app arg1 arg2"

Shell format takes your entry point as/bin/sh -cTo run.

Example:

The code remains unchanged, and the dockerfile is changed to:

FROM golang:latest as builder

WORKDIR /go/src
COPY main.go .

RUN CGO_ENABLED=0 go build -o stop ./main.go

From alpine:latest

WORKDIR /root/
COPY --from=builder /go/src/stop .
RUN chmod +x /root/stop

ENTRYPOINT "/root/stop"

Build a new image:

$ docker build -t stop-shell -f Dockerfile-shell .

Sending build context to Docker daemon  4.608kB
Step 1/9 : FROM golang:latest as builder
 ---> 7e5e8028e8ec
Step 2/9 : WORKDIR /go/src
 ---> Using cache
 ---> 312e98c07647
Step 3/9 : COPY main.go .
 ---> Using cache
 ---> 2dc4088e6548
Step 4/9 : RUN CGO_ENABLED=0 go build -o stop ./main.go
 ---> Using cache
 ---> a207b2ecdd67
Step 5/9 : From alpine:latest
 ---> f70734b6a266
Step 6/9 : WORKDIR /root/
 ---> Using cache
 ---> a14716065730
Step 7/9 : COPY --from=builder /go/src/stop .
 ---> Using cache
 ---> 3573b92b9ab3
Step 8/9 : RUN chmod +x /root/stop
 ---> Using cache
 ---> 3cbc57300792
Step 9/9 : ENTRYPOINT "/root/stop"
 ---> Running in 199ca0277b08
Removing intermediate container 199ca0277b08
 ---> e0fe6a86ee1e
Successfully built e0fe6a86ee1e
Successfully tagged stop-shell:latest

Repeat the above steps, and the final observed results are as follows:

It's programmed
Sleep: 1 second
Sleep: 2 seconds
Sleep: 3 seconds
Dormant: 4 seconds
Sleep: 5 seconds
Dormant: 6 seconds
Sleep: 7 seconds
Sleep: 8 seconds
Sleep: 9 seconds
Sleep: 10 seconds
Dormant: 11 seconds
Dormant: 12 seconds
Sleep: 13 seconds
Sleep: 14 seconds
Dormant: 15 seconds
Dormant: 16 seconds
Sleep: 17 seconds
Sleep: 18 seconds
Sleep: 19 seconds
Dormant: 20 seconds
Sleep: 21 seconds
Sleep: 22 seconds
Sleep: 23 seconds
Dormant: 24 seconds
Exit: terminated
Start exiting
Perform cleanup
End exit

Shell format, our main program also received the shutdown signal, and did the exit work.

To verify, wedocker execIn the docker shell container running, execute PS:

docker exec -it 0299308034e7 sh
~ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /root/stop
   12 root      0:00 sh
   17 root      0:00 ps 

Our application process is process 1, so we can still receive SIGTERM signals.

When our application is directly a starting entry, there is no difference between the two formats in terms of receiving a shutdown signal.

If our startup script is similar to run.sh What happens to shell scripts?

When we start our application with a shell script, our application is no longer process 1. At this time, the shell process will not notify our application process to exit. We need to do some special processing in the shell script to achieve the same effect.

All you need to do is tell your shell to replace itself with your application. To do this, the shell has the exec command (similar to the exec format mentioned earlier). See exec syscall for details.

stay run.sh Replace in

/app/bin/your-app

Is:

exec /app/bin/your-app

Example:

ours run.sh The script is as follows:

#!/bin/sh

exec /root/stop

Then our dockerfile is changed to:

FROM golang:latest as builder

WORKDIR /go/src
COPY main.go .

RUN CGO_ENABLED=0 go build -o stop ./main.go

From alpine:latest

WORKDIR /root/
COPY --from=builder /go/src/stop .
COPY run.sh .
RUN chmod +x /root/stop

ENTRYPOINT ["/root/run.sh"]

After building a new image, run the image:

docker run stop-shell-runsh

Started the program
Sleep: 1 second
Sleep: 2 seconds
Sleep: 3 seconds

Then it enters the container to executeps

docker exec -it 97adce7dd7e4 sh
~ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /root/stop
   14 root      0:00 sh
   19 root      0:00 ps 

You can see that although our startup script is run.sh But afterexecAfter that, the application becomes process 1.

Stop running the container to see the shutdown status:

docker stop  97adce7dd7e4

You can then see that the container has the following output:

Sleep: 104 seconds
Sleep: 105 seconds
Sleep: 106 seconds
Sleep: 107 seconds
Sleep: 108 seconds
Sleep: 109 seconds
Sleep: 110 seconds
Sleep: 111 seconds
Dormant: 112 seconds
Sleep: 113 seconds
Dormant: 114 seconds
Dormant: 115 seconds
Dormant: 116 seconds
Sleep: 117 seconds
Exit: terminated
Start exiting
Perform cleanup
End exit
  • Listening for the wrong signal

Not all code frameworks support SIGTERM, such as SIGINT in Python’s ecology.

For example:

try:
    do_work()
except KeyboardInterrupt:
    cleanup()

So the default is to send SIGTERM signal, we can still set other signals.

The easiest solution is to add a line to the dockerfile:

STOPSIGNAL SIGINT

Although we regard the application as process 1, it can receive signals, but it also brings other problems, such as zombie process. This problem is very common in the use of docker. You can refer to my other article – avoid running nodejs as PID 1 under the docker image.

Best practices

useinitSystem. We recommend TiNi here.

TiNi is the simplest you can think ofinit。 All TiNi does is span out the child process, wait for it to exit, harvest the zombie process and perform signal forwarding.

Using TiNi has the following advantages:

  • It protects you from software that accidentally creates a zombie process, which can (over time!) Make the whole system lack of PID (and make it unusable).
  • It ensures that the default signal handler is suitable for the software you are running in the docker image. For example, for TiNi, SIGTERM correctly terminates your process even if you don’t explicitly install a signal handler.
  • It’s completely transparent! Docker images without TiNi will be used with TiNi without any changes.

Example:

The new dockerfile is as follows:

FROM golang:latest as builder

WORKDIR /go/src
COPY main.go .

RUN CGO_ENABLED=0 go build -o stop ./main.go

From alpine:latest

RUN apk add --no-cache tini
WORKDIR /root/
COPY --from=builder /go/src/stop .
RUN chmod +x /root/stop

ENTRYPOINT ["/sbin/tini", "--", "/root/stop"]

Build image:

docker build -t stop-tini -f Dockerfile-tini .

Run TiNi image:

$ docker run stop-tini

Started the program
Sleep: 1 second
Sleep: 2 seconds
Sleep: 3 seconds
Dormant: 4 seconds
Sleep: 5 seconds
Dormant: 6 seconds
Sleep: 7 seconds

...

At this point, it is executed at another terminaldocker execEnter the container and executeps

docker exec -it a727bd6617f4 sh
~ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /sbin/tini -- /root/stop
    7 root      0:00 /root/stop
   14 root      0:00 sh
   20 root      0:00 ps

As you can see, TiNi is process 1 and our application is a child of process 1 (number 7).

Stop the container:

docker stop  a727bd6617f4

Finally, our run container has the following output:

Sleep: 82 seconds
Sleep: 83 seconds
Sleep: 84 seconds
Sleep: 85 seconds
Dormancy: 86 seconds
Exit: terminated
Start exiting
Perform cleanup
End exit

We can see that although our business process is not process 1, it also receives a stop signal.

Of course, it’s all thanks to TiNi, which forwards the signal to our application.

Recommended Today

Explain module, import and export in JavaScript

Author: Tania rascia Crazy technology house Original text:https://www.taniarascia.com/j… In the era of the Internet, websites are mainly developed with HTML and CSS. If you load JavaScript into a page, it usually provides effects and interactions in the form of small fragments. Generally, all JavaScript code is written in a file and loaded into a filescriptTag. […]