网络
- 在一台宿主机上创建多个container,之间可以ping通,为啥呢?通过
docker network ls
查看各网络
- docker0作为网关,互联着宿主机的多个容器,所以能互相ping
- 有个查看网桥的命令:
brctl show
,这有两个接口,(不太理解网桥技术,interface?)
- 访问外网需要NAT,都要经过宿主机eth0,外部无法区分是哪个docker container在请求
- 可以通过
ip route
、iptables --list -t nat
等命令查看转发规则 - 我觉得可以学学curl,命令行版的postman
- 自己创建bridge:
docker network create
- 当然,可以让container使用自己的bridge转发:
docker run -idt --name self-bri --network self-bridge busybox /bin/sh
- 怎么让这容器连上另一个网桥(bridge)呢:
docker network connect bridge self-bri
- 此时进入容器查看,就多了个网卡,用来接入到docker0的网络,属于此网络的容器也都能ping通
- 因为用的是B类网络,16位子网掩码,17/18是两个网段,connect就是添加路由
- 类似的,可以使用
disconnect
断开连接,就ping不通那边的容器了~
- 怎么让这容器连上另一个网桥(bridge)呢:
- 这里连接新网络时新增了虚拟网卡eth1,因为不是一个子网吧!不能增加路由吗?
- 可以指定网桥的网段(子网):
docker network create -d bridge --gateway 172.200.0.1 --subnet 172.200.0.0/16 self-subnet
- 怎么让容器使用指定的子网呢?connect或者在创建时使用
--network
- 怎么让容器使用指定的子网呢?connect或者在创建时使用
- 类似的,可以使用
docker network prune
清除不用的网络
端口映射
- 之前一直在使用
-p
参数创建容器,将容器内服务的端口映射到宿主机,方便外部访问 - 环境:两个网络之间(例如两台虚拟机):roy和roy01
- 在roy上使用NGINX镜像建立服务,
docker run -idt --name nginx-doc nginx sh
,可以访问roy01:ping 192.168.109.129
- 但roy01不能访问nginx服务,所以需要:
docker run -idt --name nginx-doc -p 8080:80 nginx sh
- 此时本地可以
127.0.0.1:8080
访问,也可以在roy01通过:192.168.109.3:8080
访问
- 在roy上使用NGINX镜像建立服务,
- 使用
sudo iptables -t nat -nvxL
可以看到转发规则 - 之前还在Dockerfile中使用过
EXPOSE 5000
,他的主要作用是提示,方便我们inspect到这个镜像使用的端口,再用-p
映射出来
host&null
- docker默认有三种网络类型,上面说了bridge网桥,会给容器自动设置docker0网段的IP
- host的特点是完全复制主机的网络:
docker run -idt --name host-demo --network host busybox sh
- null的特点是不给容器设置任何网络,完全用户自定义,用的较少
namespace
- Linux的命名空间是一种隔离技术,常用的Namespace有 user namespace, process namespace, network namespace等
- docker的网络正是使用了
network namespace
,不同的容器有各自的IP和路由表 - 这里使用
brctl
命令手动实现一个隔离的系统
- ns1和ns2使用namespace技术互相隔离,通过建立bridge通信
- 实验脚本:创建namespace并定义interface的IP,然后UP
#!/bin/bash bridge=$1 namespace=$2 addr=$3 vethA=veth-$namespace vethB=eth00 sudo ip netns add $namespace sudo ip link add $vethA type veth peer name $vethB sudo ip link set $vethB netns $namespace sudo ip netns exec $namespace ip addr add $addr dev $vethB sudo ip netns exec $namespace ip link set $vethB up sudo ip link set $vethA up sudo brctl addif $bridge $vethA
# 建立网桥 sudo brctl addbr mydocker0 # 搭起来 sh add-ns-to-br.sh mydocker0 ns1 172.16.1.1/16 sh add-ns-to-br.sh mydocker0 ns2 172.16.1.2/16 sudo ip link set dev mydocker0 up
- 测试
# 进到ns1的环境 sudo ip netns exec ns1 bash ip a ping 172.16.1.2
- 小练习
compose
- docker-compose,将我们准备容器、设置网络的步骤定义在
.yaml
文件中,方便我们容器化 - 安装
# 源码链接:https://github.com/docker/compose/releases,到了2.3.3 sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose docker-compose --version # 也可以通过python安装,推荐这种,可能要升级一下pip # https://www.cnblogs.com/yangjisen/p/13171063.html pip install docker-compose sudo ln -s /usr/local/python37/bin/docker-compose /usr/bin/docker-compose
语法
- yaml文件的基本结构(语法),就相当于shell脚本,把平时的Linux命令组合起来自动化
version: "3.8" services: # 容器 servicename: # 服务名,这个名字也是内部 bridge 网络可解析使用的 name(DNS) container_name: # 容器名字,默认是servicename image: # 镜像的名字 command: # 可选,如果设置,则会覆盖默认镜像里的 CMD命令 environment: # 可选,相当于 docker run里的 --env volumes: # 可选,相当于docker run里的 -v networks: # 可选,相当于 docker run里的 --network ports: # 可选,相当于 docker run里的 -p servicename2: volumes: # 可选,相当于 docker volume create networks: # 可选,相当于 docker network create
demo
- 将下面的步骤改造为使用yaml
# 准备app.py文件 from flask import Flask from redis import Redis import os import socket app = Flask(__name__) # 连接redis服务器 redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379) @app.route('/') def hello(): redis.incr('hits') return f"Hello Container World! I have been seen {redis.get('hits').decode('utf-8')} times and my hostname is {socket.gethostname()}.\n" # hostname=container ID
# Dockerfile,准备镜像 FROM python:3.9.5-slim RUN pip install flask redis && \ groupadd -r flask && useradd -r -g flask flask && \ mkdir /src && \ chown -R flask:flask /src USER flask COPY app.py /src/app.py WORKDIR /src ENV FLASK_APP=app.py REDIS_HOST=redis EXPOSE 5000 # 提示使用5000端口 CMD ["flask", "run", "-h", "0.0.0.0"]
docker image build -t flask-demo . # 建个网络 docker network create -d bridge demo-network docker image pull redis # 搞个redis容器 docker container run -d --name redis-server --network demo-network redis # flask容器,这里通过--env动态地将redis server的host设置为环境变量,container也可获取 docker container run -d --network demo-network --name flask-demo --env REDIS_HOST=redis-server -p 5000:5000 flask-demo # 这里用到了docker0可以解析所连容器域名(host)的功能 # 访问 http://127.0.0.1:5000
- 这里实现的功能是:每次访问网页,都会使用Redis服务器的incr功能计数并显示
- 改写为使用docker-compose完成,当然,镜像需要自己准备好
# docker-compose.yml version: "3.8" services: flask-demo: image: flask-demo:latest environment: - REDIS_HOST=redis-server # 传给容器的动态ENV networks: - demo-network ports: - 8080:5000 # 映射到8080,flask默认5000 redis-server: image: redis:latest networks: - demo-network networks: # 先不自定义网段,都默认,所以应该是172.18.0.1 demo-network:
- 怎么用这个yml文件呢?类似Dockerfile文件的命名和使用,配置需要放在当前执行命令的文件夹下(也可
-f
),且名称为docker-compose.yml
- 这个文件夹的名字最好设置为项目名,因为后续创建的网络和容器都会用文件夹名为前缀
- 命令:
docker-compose up -d
,使用-d后台运行
- 有的容器没起来不用怕,问题解决了再起;这个前缀可以在
up
时可使用-p
参数修改,但后面的命令就都要带上-p,所以不建议使用
- 还带了个后缀,这个和scale有关!反正它用名称管理,最好默认
- OK,目前它已经帮我们建立了容器和网络,浏览器能正常访问并计数!
docker-compose down
(Stop and remove resources),docker-compose ps
,都要在yml文件夹下
镜像
- 上面的镜像都是拉过来的或者build好的,compose不能biu?当然可以
version: "3.8" services: flask-demo: build: ./flask # 这里放build镜像所需要的Dockerfile和代码,或者: context: ./flask dockerfile: Dockerfile-dev # 通过传参指定不同的Dockerfile image: flask-demo:latest # 默认build出来的镜像还是以文件夹名为前缀,设置image就可以重命名为flask-demo enviroment: ...
- 命令行执行:
docker-compose build
,这样就可以只build镜像,并不会创建容器- 还可以直接
docker-compose up -d
,连build带创建容器 - 也可以指定service:
docker-compose build flask-demo
- 还可以直接
- 可以直接拉取镜像:
docker-compose pull
,当然,需要yml文件中定义service- 也可以指定service:
docker-compose pull service-name
- 这些命令各司其职,但比较智能
- 也可以指定service:
- docker-compose可build/pull/run,但要通过service-name;在维护的过程中,最常用的是三个命令
docker-compose up -d --build
docker-compose restart
docker-compose up -d --remove-orphans
,yml文件修改,删掉某些service之后会有孤儿容器
网络
- 详细介绍一下yml中
network
的定义- 之前说过docker网络会给容器建立一个内部的DNS服务器,所以容器之间可以互相ping名称
- docker-compose不仅可以ping容器名字,还可以 ping 服务名;如果我们不定义network还会创建默认的bridge网络
- 测试
# Dockerfile FROM alpine:latest RUN apk update && apk upgrade && \ apk add --no-cache net-snmp-tools && \ mkdir /var/lib/net-snmp && \ mkdir /var/lib/net-snmp/mib_indexes && \ apk add --no-cache snmptt && \ apk add --no-cache openssh-client && \ apk add --no-cache busybox-extras && \ apk add --no-cache curl && \ apk add --no-cache iperf3 && \ apk add --no-cache bind-tools && \ apk add --no-cache nmap && \ apk add --no-cache scanssh && \ apk add --no-cache mtr WORKDIR /omd VOLUME ["/omd"] CMD []
# docker-compose.yml version: "3.8" services: box1: build: ./ image: roy/net:latest command: /bin/sh -c "while true;do sleep 3600;done" box2: build: ./ image: roy/net:latest command: /bin/sh -c "while true;do sleep 3600;done"
# 如果build出问题,直接拉吧 version: "3.8" services: box1: image: xiaopeng163/net-box:latest command: /bin/sh -c "while true;do sleep 3600;done" box2: image: xiaopeng163/net-box:latest command: /bin/sh -c "while true;do sleep 3600;done"
- 网络、容器都是以文件夹为前缀
- 配置网络属性
version: "3.8" services: box1: image: xiaopeng163/net-box:latest command: /bin/sh -c "while true;do sleep 3600;done" networks: - test-network1 - test-network2 box2: image: xiaopeng163/net-box:latest command: /bin/sh -c "while true;do sleep 3600;done" networks: - test-network1 networks: test-network1: ipam: driver: default config: - subnet: "172.18.126.0/24" test-network2: ipam: driver: default
水平扩展
- 切换到之前的flask文件夹下,修改yml文件
version: "3.8" services: flask-demo: build: ./ image: flask-demo:latest environment: - REDIS_HOST=redis-server networks: - demo-network redis-server: image: redis:latest networks: - demo-network # 这里需要用到client,到容器内网请求,因为没有暴露端口 ports 到宿主机 # 可以在client里,把某个进程暴露出来,这个进程负责请求flask client: image: xiaopeng163/net-box:latest command: sh -c "while true; do sleep 3500; done;" # 要么别指定,要么都指定 networks: - demo-network networks: demo-network:
- 扩展出多个flask-demo service,这就是
scale
,注意看后缀数字 - 此时还有个问题,我们进client后
ping flask-demo
啥结果?- 每次返回的IP不确定,可能是flask-demo1/2/3,做了负载均衡
- 上面用client在子网内请求,因为flask要扩展,无法定义ports暴露给宿主机,这里用NGINX代理
# nginx.conf server { # 这个server可以理解为NGINX代理服务器 listen 80 default_server; # default表示127.0.0.1,代理本机,暴露谁就监听谁=代理谁(拦截) location / { proxy_pass http://flask:5000; # 转发请求给这个地址; 使用flask域名解析;在一个网段即可,不需要关心服务器和代理服务器是否为一台机器 } } # 这个配置表示所有80的请求都代理到访问 http://flask:5000 # NGINX只是推荐参考:https://blog.youkuaiyun.com/Roy_Allen/article/details/114672519?spm=1001.2014.3001.5502
# docker-compose.yml version: "3.8" services: flask: build: context: ./flask dockerfile: Dockerfile image: flask-demo:latest environment: - REDIS_HOST=redis-server networks: - backend - frontend redis-server: image: redis:latest networks: - backend nginx: image: nginx:stable-alpine ports: - 8000:80 depends_on: - flask volumes: - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - ./var/log/nginx:/var/log/nginx networks: - frontend networks: backend: frontend:
- 这里有两个网络,因为flask和redis互相配合(后端),NGINX代理只和flask有关
- 这里还将NGINX的配置和日志做了mount,到本地保存
- 执行:
docker-compose up -d
- 访问:http://127.0.0.1:8000/
ENV
- 可以在yml文件中设置环境变量,就是
environment
,这个例子里主要是给app.py用version: "3.8" services: flask: build: context: ./flask dockerfile: Dockerfile image: flask-demo:latest environment: - REDIS_HOST=redis-server - REDIS_PASS=${REDIS_PASSWORD} networks: - backend - frontend redis-server: image: redis:latest # redis-server redis-cli是redis定义的命令 command: redis-server --requirepass ${REDIS_PASSWORD} networks: - backend nginx: image: nginx:stable-alpine ports: - 8000:80 depends_on: - flask volumes: - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - ./var/log/nginx:/var/log/nginx networks: - frontend networks: backend: frontend:
# app.py from flask import Flask from redis import StrictRedis import os import socket app = Flask(__name__) redis = StrictRedis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379, password=os.environ.get('REDIS_PASS')) @app.route('/') def hello(): redis.incr('hits') return f"Hello Container World! I have been seen {redis.get('hits').decode('utf-8')} times and my hostname is {socket.gethostname()}.\n"
- 执行:
docker-compose up -d --build
,不太智能了,你改了py没给你build直接用的缓存image
- 执行:
- 新建配置文件,这里用默认名字,也可以在up的时候用
--env-file
指定# .env REDIS_PASSWORD=roy123
- 这种将环境变量单独配置的方法可以提高安全性,将env文件加到
-gitignore
- 用
docker-compose config
可以看到网络和服务(容器)的配置
- 这种将环境变量单独配置的方法可以提高安全性,将env文件加到
服务依赖
- 服务依赖和健康检查经常一起使用,先看什么是健康检查
# Dockerfile FROM python:3.9.5-slim RUN pip install flask redis && \ apt-get update && \ apt-get install -y curl && \ groupadd -r flask && useradd -r -g flask flask && \ mkdir /src && \ chown -R flask:flask /src USER flask COPY app.py /src/app.py WORKDIR /src ENV FLASK_APP=app.py REDIS_HOST=redis EXPOSE 5000 # HEALTHCHECK 的更多用法可以看官网 # 这里表示30s请求一次,如果失败三次则unhealthy HEALTHCHECK --interval=30s --timeout=3s \ CMD curl -f http://localhost:5000/ || exit 1 CMD ["flask", "run", "-h", "0.0.0.0"] # app.py还是需要密码的那个
- 执行:
docker image build -t flask-healthy .
,docker container run -d --network mybridge --name flask-h --env REDIS_PASS=abc123 flask-healthy
,这里要通过run指定ENV,REDIS_HOST在镜像指定了 - 一分半后docker ps看应该会显示
unhealthy
,docker container inspect ID
- 启动redis,指定到相同网段:
docker run -d --name redis --network mybridge redis:latest redis-server --requirepass abc123
;(还是没看到healthy)
- 执行:
- 在docker-compose中怎么检查呢?其实就是配置文件的不同,在官网都能找到例子
version: "3.8" services: flask: build: context: ./flask dockerfile: Dockerfile image: flask-demo:latest environment: - REDIS_HOST=redis-server - REDIS_PASS=${REDIS_PASSWORD} healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5000"] interval: 30s timeout: 3s retries: 3 start_period: 40s depends_on: redis-server: condition: service_healthy networks: - backend - frontend redis-server: image: redis:latest command: redis-server --requirepass ${REDIS_PASSWORD} healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 1s timeout: 3s retries: 30 networks: - backend nginx: image: nginx:stable-alpine ports: - 8000:80 depends_on: flask: condition: service_healthy volumes: - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - ./var/log/nginx:/var/log/nginx networks: - frontend networks: backend: frontend:
healthcheck
各属性定义了之前Dockerfile中HEALTHCHECK
的内容,这里可以更改redis密码让flask失败(unhealthy)depends_on
即服务(容器)依赖,所依赖的服务必须先启动
- 注意:因为服务依赖,所以如果flask不健康NGINX会启动失败,一直等到healthy状态,
docker-compose up -d
才能成功!
练习
小结
- docker-compose解放了我们管理容器的双手,从build到run,还有配置网络、水平扩展、服务依赖等都能定义
- 但不建议在生产环境中使用
- 多机器如何管理?
- 如果跨机器做scale横向扩展?
- 容器失败退出时如何新建容器确保服务正常运行?
- 如何确保零宕机时间?
- 如何管理密码,Key等敏感数据?
- docker-swarm分布式管理工具来了