菜单
本页目录

6 镜像后续

问题:之前制作 Nginx 镜像,实现动态化,Nginx 关闭慢

原因:docker stop 以后,docker 向容器发送15号 SIGTERM 信号,
			但是容器不识别,docker 会反复发送 SIGTERM 信号,
			容器仍然没有关闭,最后会发送 9号 SIGKILL 信号强制杀死,所以这个过程慢

本次用到的压缩包:goteaching-master.zip

一、容器的优雅退出

1、进程的退出 - Linux

1)kill 参数

序号信号解释
1SIGHUP启动被终止的程序,可让该进程重新读取自己的配置文件,类似重新启动。
2SIGINT相当于用键盘输入 [ctrl]-c 来中断一个程序的进行。
9SIGKILL代表强制中断一个程序的进行,如果该程序进行到一半,那么尚未完成的部分可能会有“半产品”产生
类似 vim会有 .filename.swp 保留下来。
15SIGTERM以正常的方式来终止该程序。由于是正常的终止,所以后续的动作会将他完成。
不过,如果该程序已经发生问题、无法使用正常的方法终止时,输入这个 signal 也是没有用的。
19SIGSTOP相当于用键盘输入 [ctrl]-z 来暂停一个程序的进行。

2)信号

信号是一种进程间通信的形式。一个信号就是内核发送给进程的一个消息,告诉进程发生了某种事件。

	当一个信号被发送给一个进程后,进程会立即中断当前的执行流并开始执行信号的处理程序。如果没有为这个信号指定处理程序,就执行默认的处理程序。
	进程需要为自己感兴趣的信号注册处理程序,比如为了能让程序优雅的退出(接到退出的请求后能够对资源进行清理)一般程序都会处理 SIGTERM 信号。与 SIGTERM 信号不同,SIGKILL 信号会粗暴的结束一个进程。因此我们的应用应该实现这样的目录:捕获并处理 SIGTERM 信号,从而优雅的退出程序。如果我们失败了,用户就只能通过 SIGKILL 信号这一终极手段了。除了 SIGTERM 和 SIGKILL ,还有像 SIGUSR1 这样的专门支持用户自定义行为的信号

2、Golang 环境部署、使用

1)下载go环境软件包(绿色免安装版)

也可用国内中文社区进行下载:

官网:Downloads - The Go Programming Language

国内中文社区:https://studygolang.com/dl

image-20230107095904188

image-20230107100145309

[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、上传项目压缩包

image-20230107103541729

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 信号: image-20230107105830331

容器正常退出: image-20230107105817405

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号信号

优雅推出:

image-20230107111826533

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

image-20230107163704623

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

image-20230107172457404

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)准备

👉 新版 docker 安装步骤

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

image-20230107172754480

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)上传文件

image-20230109002547946

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