6 镜像后续
问题:之前制作 Nginx 镜像,实现动态化,Nginx 关闭慢
原因:docker stop 以后,docker 向容器发送15号 SIGTERM 信号,
但是容器不识别,docker 会反复发送 SIGTERM 信号,
容器仍然没有关闭,最后会发送 9号 SIGKILL 信号强制杀死,所以这个过程慢
本次用到的压缩包:goteaching-master.zip
一、容器的优雅退出
1、进程的退出 - Linux
1)kill 参数
| 序号 | 信号 | 解释 |
|---|---|---|
| 1 | SIGHUP | 启动被终止的程序,可让该进程重新读取自己的配置文件,类似重新启动。 |
| 2 | SIGINT | 相当于用键盘输入 [ctrl]-c 来中断一个程序的进行。 |
| 9 | SIGKILL | 代表强制中断一个程序的进行,如果该程序进行到一半,那么尚未完成的部分可能会有“半产品”产生 类似 vim会有 .filename.swp 保留下来。 |
| 15 | SIGTERM | 以正常的方式来终止该程序。由于是正常的终止,所以后续的动作会将他完成。 不过,如果该程序已经发生问题、无法使用正常的方法终止时,输入这个 signal 也是没有用的。 |
| 19 | SIGSTOP | 相当于用键盘输入 [ctrl]-z 来暂停一个程序的进行。 |
2)信号
信号是一种进程间通信的形式。一个信号就是内核发送给进程的一个消息,告诉进程发生了某种事件。
当一个信号被发送给一个进程后,进程会立即中断当前的执行流并开始执行信号的处理程序。如果没有为这个信号指定处理程序,就执行默认的处理程序。
进程需要为自己感兴趣的信号注册处理程序,比如为了能让程序优雅的退出(接到退出的请求后能够对资源进行清理)一般程序都会处理 SIGTERM 信号。与 SIGTERM 信号不同,SIGKILL 信号会粗暴的结束一个进程。因此我们的应用应该实现这样的目录:捕获并处理 SIGTERM 信号,从而优雅的退出程序。如果我们失败了,用户就只能通过 SIGKILL 信号这一终极手段了。除了 SIGTERM 和 SIGKILL ,还有像 SIGUSR1 这样的专门支持用户自定义行为的信号
2、Golang 环境部署、使用
1)下载go环境软件包(绿色免安装版)
也可用国内中文社区进行下载:
官网:Downloads - The Go Programming Language
国内中文社区:https://studygolang.com/dl


[root@localhost ~]# wget https://studygolang.com/dl/golang/go1.19.4.linux-amd64.tar.gz
[root@localhost ~]# ls
go1.19.4.linux-amd64.tar.gz
2)golang环境部署
#将压缩包解压到 /usr/local/
[root@localhost ~]# rm -rf /usr/local/go
[root@localhost ~]# tar -xf go1.19.4.linux-amd64.tar.gz -C /usr/local/
[root@localhost ~]# ls /usr/local/go/
api bin codereview.cfg CONTRIBUTING.md doc lib LICENSE misc PATENTS pkg README.md SECURITY.md src test VERSION
#添加go命令调用的环境变量
[root@localhost ~]# vim /etc/profile
[root@localhost ~]# tail -n 1 /etc/profile
export PATH=$PATH:/usr/local/go/bin
[root@localhost ~]# source /etc/profile
#查看go版本
[root@localhost ~]# go version
go version go1.19.4 linux/amd64
3)golang 简单使用
[root@localhost ~]# mkdir -p /go/src/ /go/bin/ /go/pkg/ /go/test/
[root@localhost ~]# vim /go/src/main.go
[root@localhost ~]# cat /go/src/main.go
package main
import "fmt"
func main() {
fmt.Println( "hello,world!" )
}
[root@localhost ~]# go build -o /go/bin/ /go/src/main.go
[root@localhost ~]# tree /go/
/go/
├── bin
│ └── main
├── pkg
├── src
│ └── main.go
└── test
4 directories, 2 files
[root@localhost ~]# PATH=$PATH:/go/bin
[root@localhost ~]# main #能执行的脚本,编译完成以后的可执行程序(没有任何编译环境,也能执行)
hello,world!
3、容器中的信号
1)容器中的进程属于容器的 1 号进程
Docker 的 stop 和 kill 命令都是用来向容器发送信号的。注意,只有容器中的 1 号进程能够收到信号,这一点非常关键!stop 命令会首先发送 SIGTERM 信号,并等待应用优雅的结束。如果发现应用没有结束(用户可以指定等待的时间),就再发送一个 SIGKILL 信号强行结束程序。kill 命令默认发送的是 SIGKILL 信号,当然你可以通过 -s 选项指定任何信号
2)向容器发送信号
$ docker container kill --signal="SIGTERM" 容器名 #向容器发送15号信号,正常退出
$ docker container kill --signal="SIGKILL" 容器名 #向容器发送9号信号,强制退出
4、容器优雅退出
1)容器优雅退出的条件:
管理员给出15号信号( SIGTERM 正常退出信号)
容器内部能识别,并执行15号信号
实验:
1、上传项目压缩包

2、根据 Dockerfile 和 main.go 创建镜像
(可以实现优雅退出)
main.go脚本的逻辑:
启动容器时,启动这个编译完成以后的程序,对信号进行监听(不同的信号给出不同操作),同时写一个死循环保证此程序能一直运行
[root@localhost ~]# unzip goteaching-master.zip
[root@localhost ~]# cd goteaching-master
[root@localhost goteaching-master]# cd 2、优雅退出/
[root@localhost 2、优雅退出]# ls
3、脚本的优雅退出 Dockerfile Dockerfile1 main.go startup.sh
[root@localhost 2、优雅退出]# cat Dockerfile
FROM wangyanglinux/go:run1.0
RUN mkdir /go/src/signal
ADD ./main.go /go/src/signal
RUN cd /go/src/signal && GO111MODULE=off CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
RUN chmod a+x /go/src/signal/main
CMD /go/src/signal/main
[root@localhost 2、优雅退出]#
[root@localhost 2、优雅退出]# cat main.go
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
// 优雅退出go守护进程
func main() {
//创建监听退出chan
c := make(chan os.Signal)
//监听指定信号 ctrl+c kill
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)
go func() {
for s := range c {
switch s {
case syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT:
fmt.Println("退出", s)
ExitFunc()
case syscall.SIGUSR1:
fmt.Println("usr1", s)
case syscall.SIGUSR2:
fmt.Println("usr2", s)
default:
fmt.Println("other", s)
}
}
}()
fmt.Println("进程启动...")
sum := 0
for {
sum++
fmt.Println("sum:", sum)
time.Sleep(time.Second)
}
}
func ExitFunc() {
fmt.Println("开始退出...")
fmt.Println("执行清理...")
fmt.Println("结束退出...")
os.Exit(0)
}
3、制作镜像
[root@localhost 2、优雅退出]# docker build -t signal:v1 .
4、测试优雅退出
[root@localhost ~]# docker run --name signal --rm signal:v1 #运行容器
发送 SIGTERM 信号: 
容器正常退出: 
2)程序封装在脚本中
[root@localhost 2、优雅退出]# docker build -t signal:v2 -f Dockerfile1 .
[root@localhost 2、优雅退出]# docker run --name signal --rm signal:v2
#另开一个终端
$ docker container kill --signal="SIGTERM" signal #此时容器不会退出,因为不识别信号
$ docker stop signal #等待一会后,容器才退出
问题:若将程序封装在脚本中,原有的可优雅退出机制失效
解决:信号传递
脚本 ----> 主程序
docker ----> 1号进程(脚本)----> 主程序
总结:
1、当容器启动后,只有1号进程在收集信号。而此处的容器启动后,1号进程为脚本,并不识别信号处理,所以机制失效
2、docker stop 命令默认发送15号信号,多次尝试若不响应,则会发送9号信号强制关闭
3、镜像动态化需要脚本配合,容器启动后,脚本成为1号进程,接收信号,需要在脚本中添加 trap 关键字,传递信号
实验:
前提环境:
[root@localhost ~]# cd goteaching-master/2、优雅退出/3、脚本的优雅退出/
[root@localhost 3、脚本的优雅退出]# ls
Dockerfile2 main.go startup.sh
[root@localhost 3、脚本的优雅退出]# cat Dockerfile2
FROM wangyanglinux/go:run1.0
RUN mkdir /go/src/signal
ADD ./main.go /go/src/signal
RUN cd /go/src/signal && GO111MODULE=off CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
RUN chmod a+x /go/src/signal/main
ADD ./startup.sh /root
RUN chmod a+x /root/startup.sh
CMD /bin/bash /root/startup.sh
[root@localhost 3、脚本的优雅退出]# cat main.go
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
// 优雅退出go守护进程
func main() {
//创建监听退出chan
c := make(chan os.Signal)
//监听指定信号 ctrl+c kill
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)
go func() {
for s := range c {
switch s {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM:
fmt.Println("退出", s)
ExitFunc()
case syscall.SIGUSR1:
fmt.Println("usr1", s)
case syscall.SIGUSR2:
fmt.Println("usr2", s)
default:
fmt.Println("other", s)
}
}
}()
fmt.Println("进程启动...")
sum := 0
for {
sum++
fmt.Println("sum:", sum)
time.Sleep(time.Second)
}
}
func ExitFunc() {
fmt.Println("开始退出...")
fmt.Println("执行清理...")
fmt.Println("结束退出...")
os.Exit(0)
}
[root@localhost 3、脚本的优雅退出]# cat startup.sh
#!/bin/bash
pid=0
term_handler() {
if [ $pid -ne 0 ]; then
kill -SIGTERM "$pid"
#wait表示阻塞当前进程,若进程未完成,则会等待完成后,再往后执行
wait "$pid"
fi
exit 143;
}
#捕获信号
trap 'kill ${!}; term_handler' SIGTERM
#必须是前台进程,且不可以通过别的方式组建成前台进程(例:/usr/local/nginx/sbin/nginx && tail -f /usr/local/nginx/logs/access.log)
#ps aux | grep | awk
#将nginx变成前台进程,在配置文件 /usr/local/nginx/conf/nginx.conf 中,加daemon off;
/go/src/signal/main &
pid="$!"
while true
do
tail -f /dev/null & wait ${!}
done
1、制作镜像
[root@localhost ~]# cd goteaching-master/2、优雅退出/3、脚本的优雅退出/
[root@localhost 3、脚本的优雅退出]# ls
Dockerfile2 main.go startup.sh
[root@localhost 3、脚本的优雅退出]# docker build -t signal:v3 -f Dockerfile2 .
2、测试优雅退出
[root@localhost ~]# docker run --name signal --rm signal:v3 #启动容器
#另开一个终端
[root@localhost ~]# docker container kill --signal="SIGTERM" signal #发送15号信号
优雅推出:

3)nginx动态化+优雅推出
详细请点击👉 nginx动态化+优雅退出-详细步骤
二、容器镜像分层构建
1、镜像:
需求 ---> 开发 ---> 测试 ---> 运维
测试环境:大而全(有编译环境,有运行)
运维环境:小而稳(仅有运行环境,当出现问题时,回滚到上一个版本,而不会直接修改)
2、镜像分层构建,实现最小镜像
前提:
1)当前生命周期,内部是可分割的(比如使用编译型语言, go 编程)
编译型语言:编译完成后,再运行(go、c)
解释型语言:通过解释器,边解释,边运行(shell、python)
2)docker 版本大于 17.05
在 Dockerfile 中用 COPY --from 关键字
3、实验1:AllStage
完整保存开发环境,运行环境
1)准备
[root@localhost ~]# cd goteaching-master/1、镜像多级构建/1、AllStage/
[root@localhost 1、AllStage]# ls
Dockerfile main.go run.sh
2)制作镜像
[root@localhost 1、AllStage]# docker build -t reptile:v1 .
3)运行容器
[root@localhost 1、AllStage]# docker run --name reptile --rm -v /img:/go/src/Reptile/img reptile:v1
4)另开一个终端,导出图片
[root@localhost ~]# cd /img/
[root@localhost img]# sz 1673080100998610691_67f65fabae9b49e41d50aa592bbafc02.jpg

5)检查镜像大小
[root@localhost 1、AllStage]# docker images |grep reptile
reptile v1 ab96023b1916 13 minutes ago 763 MB
4、实验2:MultiStageScript
利用多个 dockerfile ,结合脚本,实现仅保存运行环境
1)准备
[root@localhost ~]# cd goteaching-master/1、镜像多级构建/2、MultiStageScript/
[root@localhost 2、MultiStageScript]# ls
Dockerfile Dockerfile.Run main.go multistage.sh run.sh
2)运行脚本,自动制作镜像
[root@localhost 2、MultiStageScript]# ./multistage.sh
3)运行容器
$ rm -rf /img/*
[root@localhost 2、MultiStageScript]# docker run --name reptile --rm -v /img:/root/img multistagescript:latest
4)另开一个终端,导出图片
[root@localhost img]# cd /img/
[root@localhost img]# sz 1673083436998389130_eb2323dec01f0815cf654528cb8b1d11.jpg

5)检查镜像大小
[root@localhost img]# docker images |grep multistagescript |grep latest
multistagescript latest 55c5d48deed8 10 minutes ago 12.1 MB
5、实验3:MultiStage
利用 Dockerfile 中的关键字 COPY --from 实现仅保存运行环境(docker 版本大于 17.05)
1)准备
C7-2
#C7-2
yum install -y yum-utils
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
yum install -y docker-ce
## 创建 /etc/docker 目录
mkdir /etc/docker
## 配置 daemon.
cat > /etc/docker/daemon.json <<EOF
{"registry-mirrors": ["https://kfp63jaj.mirror.aliyuncs.com"]}
EOF
# 重启docker服务
systemctl enable docker
reboot
$ mkdir /root/reptile
C7-1
#C7-1
[root@localhost ~]# cd goteaching-master/1、镜像多级构建/3、MultiStage/
[root@localhost 3、MultiStage]# ls
Dockerfile main.go run.sh
[root@localhost 3、MultiStage]# scp * 192.168.20.202:/root/reptile/
#后面步骤都在C7-2中完成
2)制作镜像
[root@localhost 3、MultiStage]# docker build -t reptile:v3 .
3)运行容器
$ rm -rf /img/*
[root@localhost 2、MultiStageScript]# docker run --name reptile --rm -v /img:/root/img reptile:v3
4)另开一个终端,导出图片
[root@localhost img]# cd /img/
[root@localhost img]# sz 1673083601618479433_8f699a344db3426080a24772a1707813.jpg

5)检查镜像大小
[root@localhost img]# docker images |grep reptile |grep v3
reptile v3 14f20a44f1e6 15 minutes ago 12.1MB
详细原理请点击👉 Docker image Build 高级-详细原理
三、镜像构建上下文的例外
git 代码版本控制器
开发时,可实现主线,分支代码开发
解决:上传时,不需要的文件不上传
1、在 Dockerfile 所在目录,创建 .dockerignore 文件
注意:docker 版本大于等于 1.1.0 这个文件才会生效
2、.dockerignore 文件编写方法
.dockerignore 文件的写法和 .gitignore 类似,支持正则和通配符,具体规则如下:
- 每行为一个条目;
- 以
#开头的行为注释; - 空行被忽略;
- 构建上下文路径为所有文件的根路径;
3、文件匹配规则具体语法如下
(也可以从根下开始,以绝对路径方式指定匹配)
| 规则 | 行为 |
|---|---|
| */temp* | 匹配根路径下一级目录下所有以 temp 开头的文件或目录 |
| */*/temp* | 匹配根路径下两级目录下所有以 temp 开头的文件或目录 |
| temp? | 匹配根路径下以 temp 开头,任意一个字符结尾的文件或目录 |
| **/*.go | 匹配所有路径下以 .go 结尾的文件或目录,即递归搜索所有路径 |
| *.md !README.md | 匹配根路径下所有以 .md 结尾的文件或目录,但 README.md 除外 |
注意:越写在最后的,优先级越高(注意规则的优先级)
4、测试练习
使用压缩包中内容作为根目录:dockerignore.tar.gz 按照下列要求,完成文件打包到镜像中
(不要再家目录完成,因为家目录含有一些隐藏文件)要求:
- 排除 .git 文件或者目录
- 排除 .log 文件,或者以 -log-*= 的文件
- 排除 *.tag.gz 文件
- *排除.war 文件,以及 jenkins_home 下的* .txt 文件
- 排除Dockerfile 文件
- 排除.dockerignore 文件,包括所有的 *.md 文件,但是 info.md 除外
- 排除含有temp字符串的文件或目录
- 排除含有go字符串的文件或目录
- 排除含有test字符串的文件或目录
- 排除含有aaaa的字符串或目录
那么最终就剩下 jenkins_home 下的 mark 目录和 info.md 了
解决步骤:
1)上传文件

2)Dockerfile 文件
[root@localhost media]# vim Dockerfile
[root@localhost media]# cat Dockerfile
FROM alpine:3.1
MAINTAINER yq "ignore:v1"
WORKDIR /opt/
COPY . /opt/
CMD ["sleep","66666666"]
3).dockerignore 文件
[root@localhost media]# vim .dockerignore
[root@localhost media]# cat .dockerignore
**/*.git
**/*.log
**/*log*
**/*.tar.gz
**/*.war
jenkins_home/*.txt
Dockerfile
.dockerignore
**/*.md
!**/info.md
**/*temp*
**/*go*
**/*test*
**/*aaaa*
4)制作镜像
[root@localhost media]# docker build -t ignore:v1 .
5)根据镜像运行容器,进入检查
[root@localhost media]# docker run --name test -d ignore:v1
[root@localhost media]# docker exec -it test /bin/sh
/opt # ls
info.md jenkins_home
/opt # find
.
./info.md
./jenkins_home
./jenkins_home/mark