1docker版本

docker 17.09

https://docs.docker.com/

appledeAir:~ apple$ docker version

Client: Docker Engine - Community

Version:           18.09.0

API version:       1.39

Go version:        go1.10.4

Git commit:        4d60db4

Built:             Wed Nov  7 00:47:43 2018

OS/Arch:           darwin/amd64

Experimental:      false

Server: Docker Engine - Community

Engine:

Version:          18.09.0

API version:      1.39 (minimum version 1.12)

Go version:       go1.10.4

Git commit:       4d60db4

Built:            Wed Nov  7 00:55:00 2018

OS/Arch:          linux/amd64

Experimental:     false

vagrant

创建linux虚拟机

创建一个目录

mkdir centos7

vagrant init centos/7  #会创建一个vagrant file

vagrant up        #启动

vagrant ssh        #进入虚拟机

vagrant status

vagrant halt      #停机

vagrant destroy    删除机器

docker machine 自动在虚拟机安装docker的工具

docker-machine create demo    virtualbox 里会自动运行一台虚拟机

docker-machine ls          显示有哪些虚拟机在运行

docker-machine ssh demo      进入机器

docker-machine create demo1    创建第二台有docker的虚拟机

docker-machine stop demo1

docker playground https://labs.play-with-docker.com/

运行docker

docker run -dit ubuntu /bin/bash

执行不退出

docker exec -it 33 /bin/bash

把普通用户添加到docker组,不用sudo

sudo gpasswd -a alex docker

/etc/init.d/docker restart

重新登录shell    exit

验证docker version

创建自己的image

from scratch

ADD app.py /

CMD ["/app.py"]

build自己的image

docker build  -t alex/helloworld .

显示当前正在运行的容器

docker container ls

docker container ls -a

显示状态为退出的容器

docker container ls -f "status=exited" -q

删除容器

docker container rm 89123

docker rm 89123

或者一次性删除全部容器

docker rm $(docker container ls -aq)    

删除退出状态的容器

docker rm $(docker container ls -f "status=exited" -q)

删除不用的image

docker rmi 98766

把container,commit成为一个新的image

docker commit 12312312 alexhe/changed_a_lot:v1.0

docker image ls

docker history 901923123     (image的id)

Dockerfile案例:

cat Dockerfile

FROM centos

ENV name Docker

CMD echo "hello $name"

用dockerfile建立image

docker build -t alexhe/firstblood:latest .

从registry拉取

docker pull ubuntu:18.04

Dockerfile案例:

cat Dockerfile

FROM centos

RUN yum install -y vim      

Dockerfile案例:

FROM ubuntu

RUN apt-get update && apt-get install -y python

Dockerfile语法梳理及最佳实践

From ubuntu:18.04

LABEL maintainer="alex@alexhe.net"

LABEL version="1.0"

LABEL description="This is comment"

RUN yum update && yum instlal -y vim \

  python-dev                    #每运行一次run,增加一层layer,需要合并起来

WORKDIR /root                    #进入目录,如果没有目录会自动创建目录

WORKDIR demo                              #进入了/root/demo

ADD hello /

ADD test.tar.gz /                #添加到根目录并解压

WORKDIR /root

ADD hello test/                # /root/test/hello

WORKDIR /root

COPY hello test/                #

大部分情况,copy优于add,add除了copy还有额外功能(解压),添加远程文件/目录请使用curl或者wget

ENV MYSQL_VERSION 5.6            #设置常亮

RUN apt-get install -y mysql-server="${MYSQL_VERSION}" && rm -rf /var/lib/apt/lists/*          #引用常量

RUN vs CMD vs ENTRYPOINT

run:执行命令并创建新的image layer

cmd:设置容器启动后默认执行的命令和参数

entrypoint:设置容器启动时运行的命令

shell格式

RUN apt-get install -y vim

CMD echo "hello docker"

ENTRYPOINT echo "hello docker"

Exec格式

RUN ["apt-get","install","-y","vim"]

CMD ["/bin/echo","hello docker"]

ENTRYPOINT ["/bin/echo","hello docker"]

例子:注意

FROM centos

ENV name Docker

ENTRYPOINT ["/bin/bash","-c","echo","hello $name"]    #这样正解    如果["echo","hello $name"],这样运行了以后还是显示hello $name,没有变量替换。用exec格式,执行的是echo这个命令,而不是shell,所以没办法把变量替换掉。

和上面的区别

FROM centos

ENV name Docker

ENTRYPOINT echo "hello $name"                 #正常 可以显示hello Docker 会用shell执行命令,识别变量

CMD:

容器启动时默认执行的命令,如果docker run指定了其他命令,CMD命令被忽略。如果定义了多个CMD,只有最后一个会执行。

ENTRYPOINT:

让容器以应用程序或者服务的形式运行。不会被忽略,一定会执行。最佳实践:下一个shell脚本作为entrypoint

COPY docker-entrypoint.sh /usr/local/bin/

ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 27017

CMD ["mongod"]

镜像发布

docker login

docker push alexhe/hello-world:latest

docker rmi alexhe/hello-world        #删掉

docker pull alexhe/hello-world:latest    #再拉回来

本地registry private repository

https://docs.docker.com/v17.09/registry/

1.启动私有registry

docker run -d -p 5000:5000 -v /opt/registry:/var/lib/registry --restart always --name registry registry:2

2.其他机器测试 telnet x.x.x.x 5000

3.往私有registry push

3.1先用dockerfile build和打tag

docker build -t x.x.x.x:5000/hello-world .

3.2设置允许不安全的私有库

vim /etc/docker/daemon.json

{
"insecure-registries" : ["x.x.x.x:5000"]
}

vim /lib/systemd/service/docker.service

EnvirmentFile=/etc/docker/daemon.json

/etc/init.d/docker restart

3.3开始push

docker push x.x.x.x:5000/hello-world

3.4验证

registry有api https://docs.docker.com/v17.09/registry/spec/api/#listing-repositories

GET /v2/_catalog

docker pull x.x.x.x:5000/helloworld

Dockerfile  github很多示例https://github.com/docker-library/docs     https://docs.docker.com/engine/reference/builder/#add

Dockerfile案例:安装flask 复制目录中的app.py到/app/ 进入app目录 暴露5000端口 执行app.py

cat Dockerfile

FROM python:2.7

LABEL maintainer="alex he<alex@alexhe.net>"

RUN pip install flask

COPY app.py /app/

WORKDIR /app

EXPOSE 5000

CMD ["python", "app.py"]

cat app.py

from flask import Flask

app = Flask(__name__)

@app.route('/')

def hello():

return "hello docker"

if __name__ == '__main__':

app.run(host="0.0.0.0", port=5000)

docker build -t alexhe/flask-hello-world .        #打包

如果打包时出错

docker run -it 报错时的第几步id  /bin/bash

进入以后看看哪里报错

最后docker run -d alexhe/flask-hello-world   #让容器在后台运行

在运行中的容器,执行命令:

docker exec -it xxxxxx /bin/bash

显示ip地址:

docker exec -it xxxx ip a

docker inspect xxxxxxid

显示容器运行产生的输出:

docker logs xxxxx

dockerfile 案例:

linux的stress工具

cat Dockerfile

FROM ubuntu

RUN apt-get update && apt-get install -y stress

ENTRYPOINT ["/usr/bin/stress"]                  #使用entrypoint加cmd配合使用,cmd为空的,用docker run来接收请求参数

CMD []

使用:

docker build alexhe/ubuntu-stress .

#dokcer run -it alexhe/ubuntu-stress             #无任何参数运行,类似于打印

docker run -it alexhe/ubuntu-stress -vm 1  --verbose   # 类似于运行 stress -vm 1 --verbose

容器资源限制,cpu,ram

docker run --memory=200M alexhe/ubuntu-stress -vm 1       --vm-bytes=500M  --verbose   #直接报错,内存不够。因为给了容器200m,压力测试占用500m

docker run --cpu-shares=10  --name=test1 alexhe/ubuntu-stress -vm1  #和下面一起启动,他的cpu占用率为66%

docker run --cpu-shares=5  --name=test2 alexhe/ubuntu-stress -vm1  #一起启动,他的cpu占用率33%

容器网络

单机:bridge,host,none

多机:Overlay

linux中的网络命名空间

docker run -dit --name test1 busybox /bin/sh -c "while true;do sleep 3600;done"  

docker exec -it xxxxx /bin/sh

ip a 显示网络接口

exit

进入host , 执行ip a ,显示host的接口

container和host的网络namespace是隔离开的

docker run -dit --name test2 busybox /bin/sh -c "while true;do sleep 3600;done"

docker exec xxxxx ip a    #看第二台机器的网络

同一台机器,container之间的网络是相通的。

以下是linux中的网络命名空间端口互通的原理实现(docker的和他类似):

host中执行 ip netns list            查看本机的network namespace

ip netns delete test1

ip netns add test1                    #创建network namespace

ip netns add test2      #创建network namespace

在test1的network namespace中执行ip link

ip netns exec test1 ip link                 #目前状态是down的

ip netns exec test1 ip link set dev lo up             #状态变成了unknown,要两端都连通,他才会变成up

创建一对veth,一个放入test1的namespace,另一个放入test2的namespace。

创建一对veth:

ip link add veth-test1 type veth peer name veth-test2

把veth-test1放入test1的namespace:

ip link set veth-test1 netns test1

看看test1的namespace里的情况:

ip netns exec test1 ip link                 #test1的namespace多了一个veth,状态为down

看看本地的ip link:

ip link                                            #少了一个,说明这一个已经加到了test1的namespace

把veth-test2放入test2的namespace:

ip link set veth-test2 netns test2

看看本地的ip link:

ip link                #又少了一个,说明已经加入到了test2的namespace

看看test2的namespace

ip netns exec test2 ip link        #test2的namespace多了一个veth,状态为down

给两个veth端口添加ip地址:

ip netns exec test1 ip addr add 192.168.1.1/24 dev veth-test1

ip netns exec test2 ip addr add 192.168.1.2/24 dev veth-test2

查看test1和test2的ip link

ip netns exec test1 ip link         #发现没有ip地址,并且端口状态是down

ip netns exec test2 ip link         #发现没有ip地址,并且端口状态是down

把两个端口up起来

ip netns exec test1 ip link set dev veth-test1 up

ip netns exec test2 ip link set dev veth-test2 up

查看test1和test2的ip link

ip netns exec test1 ip link             #发现有ip地址,并且端口状态是up

ip netns exec test2 ip link            #发现有ip地址,并且端口状态是up

从test1的namespace里的veth-test1 执行ping test2的namespace中的veth-test2

ip netns exec test1 ping 192.168.1.2

ip netns exec test2 ping 192.168.1.1

docker的bridge docker0网络:

两个容器test1和test2能互相ping 通,说明两个network namespace是连接在一起的。

目前系统只有一个test1的容器,删除test2容器

显示docker网络:

docker network ls

NETWORK ID          NAME                DRIVER              SCOPE

9d133c1c82ff        bridge              bridge              local

e44acf9eff90        host                host                local

bc660dbbb8b6        none                null                local

显示bridge网络的详情:

docker network inspect xxxxxx(上面显示bridge网络的id)

host上的veth和容器里的eth0是一对儿veth

ip link                     #veth6aa1698@if18

docker exec test1 ip link          #18: eth0@if19

这一对儿veth pair连接到了host上的docker0上面。

yum install bridge-utils

brctl show            #主机上的veth6aa连接在docker0上

新开一个test2的容器:

docker run -dit --name test2 busybox /bin/sh -c "while true;do sleep 3600;done"

看看docker的网络:

docker network inspect bridge

看到container多了一个,地址都有

在host运行ip a,又多了一个veth。

运行brctl show,docker0上有两个接口

docker容器之间的互联link

目前只有一个容器test1

现在创建第二个容器test2

docker run -d --name test2 --link test1 busybox /bin/sh -c "while true;do sleep 3600;done"

docker exec -it test2 /bin/sh

进入test2后,去ping test1的ip地址,通,ping test1的名字 ,也通。

进入test1里,去ping test2的ip地址,通,ping test2的名字,不通。

自己建一个docker network bridge,并让容器连接他。

docker network create -d bridge my-bridge

创建一个容器,test3

docker run -d --name test3 --network my-bridege busybox /bin/sh -c "while true;do sleep 3600;done"

通过brctl show来查看。

把test2的网络换成my-bridge

docker network connect my-bridge test2,当连接进来后,test2就有了2个ip地址。

注意:如果用户自己创建自定义的network,并让一些容器连接进来,这些容器,是能通过名字来互相ping连接的。而默认的bridge不行,就像上面测试一样。

docker的端口映射

创建一个nginx的container

docker run --name web -d -p 80:80 nginx

docker的host和none网络

none network:

docker run -d --name test1 --network none busybox /bin/sh -c "while true;do sleep 3600;done"

docker network inspect none               #可以看到none的network连接了一个container

进入容器:

docker exec -it test1 /bin/sh           #进入看看网络,无任何ip网络

host network:

docker run -d --name test1 --network host busybox /bin/sh -c "while true;do sleep 3600;done"

docker network inspect host

进入容器:

docker exec -it test1 /bin/sh            #查看ip a 网络,容器的ip和主机的ip完全一样,他没有自己独立的namespace

多容器复杂应用的部署:

Flask+redis,   flask的container访问redis的container

cat app.py

from flask import Flask

from redis import Redis

import os

import socket

app = Flask(__name__)

redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379)

@app.route('/')

def hello():

redis.incr('hits')

return 'Hello Container World! I have been seen %s times and my hostname is %s.\n' % (redis.get('hits'),socket.gethostname())

if __name__ == "__main__":

app.run(host="0.0.0.0", port=5000, debug=True)

cat Dockerfile

FROM python:2.7

LABEL maintaner="Peng Xiao xiaoquwl@gmail.com"

COPY . /app

WORKDIR /app

RUN pip install flask redis

EXPOSE 5000

CMD [ "python", "app.py" ]

1.创建redis的container:

docker run -d --name redis redis

2. dokcer build -t alexhe/flask-redis .

3.创建container

docker run -d -p 5000:5000 --link redis --name flask-redis -e REDIS_HOST=redis alexhe/flask-redis                    #和上面源码的相对应

4.进入上面的container,并执行env看一下:

docker exec -it flask-redis /bin/bash

env                         #环境变量

ping redis      #在容器里可以ping redis

5.在主机访问curl 127.0.0.1:5000。可以访问到

多主机间,多container互相通信

docker网络的overlay和underlay:

两台linux 主机 192.168.205.10    192.168.205.11

vxlan数据包(google搜vxlan概念)

cat multi-host-network.md
# Mutil-host networking with etcd ## setup etcd cluster 在docker-node1上 ```
vagrant@docker-node1:~$ wget https://github.com/coreos/etcd/releases/download/v3.0.12/etcd-v3.0.12-linux-amd64.tar.gz
vagrant@docker-node1:~$ tar zxvf etcd-v3.0.12-linux-amd64.tar.gz
vagrant@docker-node1:~$ cd etcd-v3.0.12-linux-amd64
vagrant@docker-node1:~$ nohup ./etcd --name docker-node1 --initial-advertise-peer-urls http://192.168.205.10:2380 \
--listen-peer-urls http://192.168.205.10:2380 \
--listen-client-urls http://192.168.205.10:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.205.10:2379 \
--initial-cluster-token etcd-cluster \
--initial-cluster docker-node1=http://192.168.205.10:2380,docker-node2=http://192.168.205.11:2380 \
--initial-cluster-state new&
``` 在docker-node2上 ```
vagrant@docker-node2:~$ wget https://github.com/coreos/etcd/releases/download/v3.0.12/etcd-v3.0.12-linux-amd64.tar.gz
vagrant@docker-node2:~$ tar zxvf etcd-v3.0.12-linux-amd64.tar.gz
vagrant@docker-node2:~$ cd etcd-v3.0.12-linux-amd64/
vagrant@docker-node2:~$ nohup ./etcd --name docker-node2 --initial-advertise-peer-urls http://192.168.205.11:2380 \
--listen-peer-urls http://192.168.205.11:2380 \
--listen-client-urls http://192.168.205.11:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.205.11:2379 \
--initial-cluster-token etcd-cluster \
--initial-cluster docker-node1=http://192.168.205.10:2380,docker-node2=http://192.168.205.11:2380 \
--initial-cluster-state new&
``` 检查cluster状态 ```
vagrant@docker-node2:~/etcd-v3.0.12-linux-amd64$ ./etcdctl cluster-health
member 21eca106efe4caee is healthy: got healthy result from http://192.168.205.10:2379
member 8614974c83d1cc6d is healthy: got healthy result from http://192.168.205.11:2379
cluster is healthy
``` ## 重启docker服务 在docker-node1上 ```
$ sudo service docker stop
$ sudo /usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-store=etcd://192.168.205.10:2379 --cluster-advertise=192.168.205.10:2375&
``` 在docker-node2上 ```
$ sudo service docker stop
$ sudo /usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-store=etcd://192.168.205.11:2379 --cluster-advertise=192.168.205.11:2375&
``` ## 创建overlay network 在docker-node1上创建一个demo的overlay network ```
vagrant@docker-node1:~$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
0e7bef3f143a bridge bridge local
a5c7daf62325 host host local
3198cae88ab4 none null local
vagrant@docker-node1:~$ sudo docker network create -d overlay demo
3d430f3338a2c3496e9edeccc880f0a7affa06522b4249497ef6c4cd6571eaa9
vagrant@docker-node1:~$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
0e7bef3f143a bridge bridge local
3d430f3338a2 demo overlay global
a5c7daf62325 host host local
3198cae88ab4 none null local
vagrant@docker-node1:~$ sudo docker network inspect demo
[
{
"Name": "demo",
"Id": "3d430f3338a2c3496e9edeccc880f0a7affa06522b4249497ef6c4cd6571eaa9",
"Scope": "global",
"Driver": "overlay",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "10.0.0.0/24",
"Gateway": "10.0.0.1/24"
}
]
},
"Internal": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
``` 我们会看到在node2上,这个demo的overlay network会被同步创建 ```
vagrant@docker-node2:~$ sudo docker network ls
NETWORK ID NAME DRIVER SCOPE
c9947d4c3669 bridge bridge local
3d430f3338a2 demo overlay global
fa5168034de1 host host local
c2ca34abec2a none null local
``` 通过查看etcd的key-value, 我们获取到,这个demo的network是通过etcd从node1同步到node2的 ```
vagrant@docker-node2:~/etcd-v3.0.12-linux-amd64$ ./etcdctl ls /docker
/docker/network
/docker/nodes
vagrant@docker-node2:~/etcd-v3.0.12-linux-amd64$ ./etcdctl ls /docker/nodes
/docker/nodes/192.168.205.11:
/docker/nodes/192.168.205.10:
vagrant@docker-node2:~/etcd-v3.0.12-linux-amd64$ ./etcdctl ls /docker/network/v1./network
/docker/network/v1./network/3d430f3338a2c3496e9edeccc880f0a7affa06522b4249497ef6c4cd6571eaa9
vagrant@docker-node2:~/etcd-v3.0.12-linux-amd64$ ./etcdctl get /docker/network/v1./network/3d430f3338a2c3496e9edeccc880f0a7affa06522b4249497ef6c4cd6571eaa9 | jq .
{
"addrSpace": "GlobalDefault",
"enableIPv6": false,
"generic": {
"com.docker.network.enable_ipv6": false,
"com.docker.network.generic": {}
},
"id": "3d430f3338a2c3496e9edeccc880f0a7affa06522b4249497ef6c4cd6571eaa9",
"inDelete": false,
"ingress": false,
"internal": false,
"ipamOptions": {},
"ipamType": "default",
"ipamV4Config": "[{\"PreferredPool\":\"\",\"SubPool\":\"\",\"Gateway\":\"\",\"AuxAddresses\":null}]",
"ipamV4Info": "[{\"IPAMData\":\"{\\\"AddressSpace\\\":\\\"GlobalDefault\\\",\\\"Gateway\\\":\\\"10.0.0.1/24\\\",\\\"Pool\\\":\\\"10.0.0.0/24\\\"}\",\"PoolID\":\"GlobalDefault/10.0.0.0/24\"}]",
"labels": {},
"name": "demo",
"networkType": "overlay",
"persist": true,
"postIPv6": false,
"scope": "global"
}
``` ## 创建连接demo网络的容器 在docker-node1上 ```
vagrant@docker-node1:~$ sudo docker run -d --name test1 --net demo busybox sh -c "while true; do sleep 3600; done"
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
56bec22e3559: Pull complete
Digest: sha256:29f5d56d12684887bdfa50dcd29fc31eea4aaf4ad3bec43daf19026a7ce69912
Status: Downloaded newer image for busybox:latest
a95a9466331dd9305f9f3c30e7330b5a41aae64afda78f038fc9e04900fcac54
vagrant@docker-node1:~$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a95a9466331d busybox "sh -c 'while true; d" seconds ago Up seconds test1
vagrant@docker-node1:~$ sudo docker exec test1 ifconfig
eth0 Link encap:Ethernet HWaddr ::0A:::
inet addr:10.0.0.2 Bcast:0.0.0.0 Mask:255.255.255.0
inet6 addr: fe80:::aff:fe00:/ Scope:Link
UP BROADCAST RUNNING MULTICAST MTU: Metric:
RX packets: errors: dropped: overruns: frame:
TX packets: errors: dropped: overruns: carrier:
collisions: txqueuelen:
RX bytes: (1.1 KiB) TX bytes: (648.0 B) eth1 Link encap:Ethernet HWaddr ::AC:::
inet addr:172.18.0.2 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80:::acff:fe12:/ Scope:Link
UP BROADCAST RUNNING MULTICAST MTU: Metric:
RX packets: errors: dropped: overruns: frame:
TX packets: errors: dropped: overruns: carrier:
collisions: txqueuelen:
RX bytes: (648.0 B) TX bytes: (648.0 B) lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::/ Scope:Host
UP LOOPBACK RUNNING MTU: Metric:
RX packets: errors: dropped: overruns: frame:
TX packets: errors: dropped: overruns: carrier:
collisions: txqueuelen:
RX bytes: (0.0 B) TX bytes: (0.0 B) vagrant@docker-node1:~$
``` 在docker-node2上 ```
vagrant@docker-node2:~$ sudo docker run -d --name test1 --net demo busybox sh -c "while true; do sleep 3600; done"
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
56bec22e3559: Pull complete
Digest: sha256:29f5d56d12684887bdfa50dcd29fc31eea4aaf4ad3bec43daf19026a7ce69912
Status: Downloaded newer image for busybox:latest
fad6dc6538a85d3dcc958e8ed7b1ec3810feee3e454c1d3f4e53ba25429b290b
docker: Error response from daemon: service endpoint with name test1 already exists. #已经用过不能再用
vagrant@docker-node2:~$ sudo docker run -d --name test2 --net demo busybox sh -c "while true; do sleep 3600; done"
9d494a2f66a69e6b861961d0c6af2446265bec9b1d273d7e70d0e46eb2e98d20
``` 验证连通性。 ```
vagrant@docker-node2:~$ sudo docker exec -it test2 ifconfig
eth0 Link encap:Ethernet HWaddr ::0A:::
inet addr:10.0.0.3 Bcast:0.0.0.0 Mask:255.255.255.0
inet6 addr: fe80:::aff:fe00:/ Scope:Link
UP BROADCAST RUNNING MULTICAST MTU: Metric:
RX packets: errors: dropped: overruns: frame:
TX packets: errors: dropped: overruns: carrier:
collisions: txqueuelen:
RX bytes: (19.5 KiB) TX bytes: (18.9 KiB) eth1 Link encap:Ethernet HWaddr ::AC:::
inet addr:172.18.0.2 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80:::acff:fe12:/ Scope:Link
UP BROADCAST RUNNING MULTICAST MTU: Metric:
RX packets: errors: dropped: overruns: frame:
TX packets: errors: dropped: overruns: carrier:
collisions: txqueuelen:
RX bytes: (648.0 B) TX bytes: (648.0 B) lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::/ Scope:Host
UP LOOPBACK RUNNING MTU: Metric:
RX packets: errors: dropped: overruns: frame:
TX packets: errors: dropped: overruns: carrier:
collisions: txqueuelen:
RX bytes: (0.0 B) TX bytes: (0.0 B) vagrant@docker-node1:~$ sudo docker exec test1 sh -c "ping 10.0.0.3"
PING 10.0.0.3 (10.0.0.3): data bytes
bytes from 10.0.0.3: seq= ttl= time=0.579 ms
bytes from 10.0.0.3: seq= ttl= time=0.411 ms
bytes from 10.0.0.3: seq= ttl= time=0.483 ms
^C
vagrant@docker-node1:~$
```

docker的持久化存储和数据共享:

持久化有两种方式:1,Data Volume  2,Bind Mounting

第一种Data Volume:

容器产生数据,比如日志,数据库,想保留这些数据

例如https://hub.docker.com/_/mysql

docker run -d -e MYSQL_ALLOW_EMPTY_PASSWORD=yes --name mysql1 mysql

查看volume:

docker volume ls

删除volume

docker volume rm xxxxxxxxxx

查看细节:
docker volume inspect xxxxxxxxx

创建第二个mysql container

docker run -d -e MYSQL_ALLOW_EMPTY_PASSWORD=yes --name mysql2 mysql

查看细节:
docker volume inspect xxxxxxxxx

删除container,volume是不会删除的:

docker stop mysql1 mysql2

docker rm mysql1 mysql2

docker volume ls               #数据还在

重新创建mysql1:

docker run -d -v mysq:/var/lib/mysql -e MYSQL_ALLOW_EMPTY_PASSWORD=yes --name mysql1 mysql     #这里的mysq是volume的名字

docker volume ls                     #会显示mysq

进入mysql1的container

创建一个新的数据库

create database docker;

退出容器,把mysql1 container删除

docker rm -f mysql1             #强制停止和删除mysql1这个container

查看volume:
docker volume ls               #还在

创建一个新的mysql2 container,但是volume使用之前的mysq

docker run -d -v mysq:/var/lib/mysql -e MYSQL_ALLOW_EMPTY_PASSWORD=yes --name mysql2 mysql

进入mysql2的容器里,看看数据库在不在

show database    #数据库还在

第二种持久化方式:bind mounting

和第一种方式区别是什么?如果用data volume方式,需要在dockerfile里定义创建的volume,bind mounting不需要,bind mouting只需要在运行时指定本地目录和容器目录一一对应的关系。

然后通过这种方式去做一个同步,就是说本地系统中的文件和容器中的文件是同步的。本地文件做了修改,容器目录中的文件也会做修改。

cat Dockerfile

# this same shows how we can extend/change an existing official image from Docker Hub

FROM nginx:latest

# highly recommend you always pin versions for anything beyond dev/learn

WORKDIR /usr/share/nginx/html

# change working directory to root of nginx webhost

# using WORKDIR is prefered to using 'RUN cd /some/path'

COPY index.html index.html

# I don't have to specify EXPOSE or CMD because they're in my FROM

index.html随便整一个

docker build -t alexhe/my-nginx .

docker run -d -p 80:80 -v $(pwd):/usr/shar/nginx/html --name web alexhe/my-nginx

bind mount其他案例:

cat Dockerfile

FROM python:2.7

LABEL maintainer="alexhe<alex@alexhe.net>"

COPY . /skeleton

WORKDIR /skeleton

RUN pip install -r requirements.txt

EXPOSE 5000

ENTRYPOINT ["scripts/dev.sh"]

开始build image

docker build -t alexhe/flask-skeleton

docker run -d -p 80:5000 -v $(pwd):/skeleton --name flask alexhe/flask-skeleton

其他源码在

/Users/apple/temp/docker-k8s-devops-master/chapter5/labs/flask-skeleton

部署一个WordPress:

docker run -d -v mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=wordpress --name mysql mysql

docker run -d -e WORDPRESS_DB_HOST=mysql:3306 -e WORDPRESS_DB_PASSWORD=root --link mysql -p 8080:80 wordpress

docker compose:

官网介绍https://docs.docker.com/compose/overview/

通过一个yml文件定义多容器的docker应用

通过一条命令就可以根据yml文件的定义去创建或者管理这多个容器

docker compose的三大概念:Services,Networks,Volumes

v2可以运行在单机,v3可以运行在多机

services:一个service代表一个container,这个container可以从dokcerhub的image来创建,或者从本地的dockerfile build出来的image来创建

service的启动,类似docker run,我们可以给其制定network和volume,所以可以给service指定network和volume的引用。

例子:

services:

  db:         (container的名字叫db)

    image:postgres:9.4(docker hub拉取的)

    volumes:

      - "db-data:/var/lib/postgresql/data"

    networks:

      - back-tier

就像这样:

docker run -d --network back-tier -v db-data:/var/lib/postgresql/data postgres:9.4

例子:

services:

  worker:   (container的名字)

    build: ./worker     (不是从dockerhub取,而是从本地build)

    links:

      - db

      - redis

    networks:

      - back-tier

例子:

cat docker-compose.yml

version: '3'

services:

wordpress:

image: wordpress

ports:

- 8080:80

environment:

WORDPRESS_DB_HOST: mysql

WORDPRESS_DB_PASSWORD: root

networks:

- my-bridge

mysql:

image: mysql

environment:

MYSQL_ROOT_PASSWORD: root

MYSQL_DATABASE: wordpress

volumes:

- mysql-data:/var/lib/mysql

networks:

- my-bridge

volumes:

mysql-data:

networks:

my-bridge:

driver: bridge

docker-compose的安装和基本使用:

安装:https://docs.docker.com/compose/install/

curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

docker-compose --version

docker-compose version 1.23.2, build 1110ad01

使用:

用上面的docker-compose.yml

docker-compose up                      #在当前文件夹的docker-compose.yml,启动. 1.创建bridge网络wordpress_my-bridge. 2.创建2个service wordpress_wordpress_1 和wordpress_mysql_1,并且启动container

docker-compose -f xxxx/docker-compose.yml     #在调用指定文件夹中的docker-compose.yml

docker-compose ps           #显示目前的service

docker-compose stop

docker-compose down         #stop and remove,但是不会删除image

docker-compose start

docker-compose up -d       #后台执行,不显示日志

docker-compose images      #显示yml中定义的container和使用的image

docker-compose exec mysql bash         #mysql是yml中定义的service,进入mysql 这台container的bash

案例:docker-compose调用Dockerfile来创建

源码在/Users/apple/temp/docker-k8s-devops-master/chapter6/labs/flask-redis

cat docker-compose.yml

version: "3"

services:

redis:

image: redis

web:

build:

context: .                                #dockerfile的位置

dockerfile: Dockerfile                     #调用目录中的Dockerfile

ports:

- 8080:5000

environment:

REDIS_HOST: redis

cat Dockerfile

FROM python:2.7

LABEL maintaner="alexhe alex@alexhe.net"

COPY . /app

WORKDIR /app

RUN pip install flask redis

EXPOSE 5000

CMD [ "python", "app.py" ]

使用:docker-compose up -d    #如果不使用-d   会一直停在那里  web信息输出在前端

docker-compose中的scale

docker-compose up                   #用上面的yml

docker-compose up --scale web=3 -d         #会报错,报8080已经被占用,需要把上面的ports: - 8000:5000删除

删除后再执行上面的。会启动3个container,并监听了容器本地的5000。可用docker-compose ps查看

但这样不行,容器本地的5000端口我们访问不到。我们需要在yml里新增haproxy

cat docker-compose.yml

version: "3"

services:

redis:

image: redis

web:

build:

context: .

dockerfile: Dockerfile

environment:

REDIS_HOST: redis

lb:

image: dockercloud/haproxy

links:

- web

ports:

- 8080:80

volumes:

- /var/run/docker.sock:/var/run/docker.sock

cat Dockerfile

FROM python:2.7

LABEL maintaner="alex alex@alexhe.net"

COPY . /app

WORKDIR /app

RUN pip install flask redis

EXPOSE 80

CMD [ "python", "app.py" ]

cat app.py

from flask import Flask

from redis import Redis

import os

import socket

app = Flask(__name__)

redis = Redis(host=os.environ.get('REDIS_HOST', '127.0.0.1'), port=6379)

@app.route('/')

def hello():

redis.incr('hits')

return 'Hello Container World! I have been seen %s times and my hostname is %s.\n' % (redis.get('hits'),socket.gethostname())

if __name__ == "__main__":

app.run(host="0.0.0.0", port=80, debug=True)

docker-compose up -d

curl 127.0.0.1:8080

docker-compose up --scale web=3 -d

案例:部署一个复杂的应用,投票系统,

源码:

/Users/apple/temp/docker-k8s-devops-master/chapter6/labs/example-voting-app

python前端+redis+java worker+ pg database + results app

cat docker-compose.yml

version: "3"

services:

voting-app:

build: ./voting-app/.

volumes:

- ./voting-app:/app

ports:

- "5000:80"

links:

- redis

networks:

- front-tier

- back-tier

result-app:

build: ./result-app/.

volumes:

- ./result-app:/app

ports:

- "5001:80"

links:

- db

networks:

- front-tier

- back-tier

worker:

build: ./worker

links:

- db

- redis

networks:

- back-tier

redis:

image: redis

ports: ["6379"]

networks:

- back-tier

db:

image: postgres:9.4

volumes:

- "db-data:/var/lib/postgresql/data"

networks:

- back-tier

volumes:

db-data:

networks:

front-tier:             #没指明driver,默认为bridge

back-tier:

docker-compose up

浏览器通过5000端口投票,5001查看投票结果

docker-compose build        #可以事先build image, 而用up会先build再做start。

docker swarm

创建一个3节点的swarm cluster

manager 192.168.205.10

worker1 192.168.205.11

worker2 192.168.205.12

manager:

docker swarm init --advertise-addr=192.168.205.10

worker1 and 2:

docker swarm join xxxxx

manager:

docker node ls                 #查看所有节点

docker service create --name demo busybox sh -c "while true;do sleep 3600;done"

docker service ls

docker service ps demo         #看service在哪台机器上

docker service scale demo=5        #扩展成5台

如果在work2上,强制删除了一个container, docker rm -f xxxxxxxxx.

这时候如果docker service ls,会显示 REPLICAS 4/5, 过一会儿会显示5/5,在docker service ls里会显示有状态为shutdown的container

docker service rm demo     #删除整个service

docker service ps demo

swarm service 部署WordPress

docker network create -d overlay demo             #创建overlay的网络,docker network ls

docker service create --name mysql --env MYSQL_ROOT_PASSWORD=root --env MYSQL_DATABASE=wordpress --network demo --mount type=volume,source=mysql-data,destination=/var/lib/mysql mysql                   #service中-v是这样的mount格式,名字叫mysql-data,挂载地址在/var/lib/mysql

docker service ls

docker service ps mysql

docker service create --name wordpress -p 80:80 --env WORDPRESS_DB_PASSWORD=root --env WORDPRESS_DB_HOST=mysql --network demo wordpress

docker service ps wordpress

访问manager或者worker的http地址,都能访问到wordpress

集群服务间通信之RoutingMesh

swam有内置服务发现的功能。通过service访问,是连到了overlay的网络。 用到了vip。

首先要有demo的overlay网络。

docker service create --name whoami -p 8000:8000 --network demo -d jwilder/whoami

docker service ls

docker service ps whoami          #运行在manager节点

curl 127.0.0.1:8000

再创建一个busybox service

docker service create --name client -d --network demo busybox sh -c "while true;do sleep 3600;done"

docker service ls

docker service ps client            #运行在work1节点

首先进到swarm 的 worker1节点

docker exec -it xxxx sh        进入这个busybox container

ping whoami           #ping service的name, 10.0.0.7, 这个其实是一个vip,通过lvs创建的

docker service scale whoami=2   #扩展到2台

docker service ps whoami      #有一台运行在work1,一台在work2

进入worker1的节点

docker exec -it xxx sh    #进入busybox container

ping whoami               #还是不变

nslookup  whoami       # 10.0.0.7   虚拟ip

nslookup tasks.whoami   #有2个地址。这才是具体container的真实地址

iptables -t mangle -nL    DOCKER-INGRESS 里做了转发

Routing Mesh的两种体现

Internal:在网络中,container和container是通过overlay网络来进行通信。

Ingress:如果服务有绑定接口,则此服务可以通过任意swarm节点的响应接口访问。服务端口被暴露到每个swarm节点

docker stack 部署wordpress

compose yml的reference:https://docs.docker.com/compose/compose-file/

官方例子:

version: "3.3"

services:
wordpress:
image: wordpress
ports:
- "8080:80"
networks:
- overlay
deploy:
mode: replicated
replicas: 2
endpoint_mode: vip #vip指service互访的时候,往外暴露的是虚拟的ip,底层通过lvs,负载均衡到后端服务器。默认为vip模式。 mysql:
image: mysql
volumes:
- db-data:/var/lib/mysql/data
networks:
- overlay
deploy:
mode: replicated
replicas: 2
endpoint_mode: dnsrr #dnsrr,直接使用service的ip地址,当横向扩展了以后,可能有三个或者四个IP地址,循环调用。 volumes:
db-data: networks:
overlay:

还有labels:打标签

mode:global和replicated,global代表全cluster只有一个,不能做横向扩展。replicated,mode的默认值,可以通过docker service scale做横向扩展。

placement:设定service的限定条件。比如:

version: '3.3'
services:
db:
image: postgres
deploy:
placement:
constraints:
- node.role == manager #db这个service一定会部署到manager这个节点,并且系统环境一定是ubuntu 14.04
- engine.labels.operatingsystem == ubuntu 14.04
preferences:
- spread: node.labels.zone

replicas:如果设置了模式为replicted,可以设置这个值

resources:资源占用和保留。

restart_policy: 重启条件,延迟,重启次数

update_config: 配置更新时的参数,比如可以同时更新2个,要等10秒才更新第二个。


cat docker-compose.yml

version: '3'

services:

web:                                          #这个service叫web

image: wordpress

ports:

- 8080:80

environment:

WORDPRESS_DB_HOST: mysql

WORDPRESS_DB_PASSWORD: root

networks:

- my-network

depends_on:

- mysql

deploy:

mode: replicated

replicas: 3

restart_policy:

condition: on-failure

delay: 5s

max_attempts: 3

update_config:

parallelism: 1

delay: 10s

mysql:                                      #这个service叫mysql

image: mysql

environment:

MYSQL_ROOT_PASSWORD: root

MYSQL_DATABASE: wordpress

volumes:

- mysql-data:/var/lib/mysql

networks:

- my-network

deploy:

mode: global                          #指能创建一台,不允许replicated

placement:

constraints:

- node.role == manager

volumes:

mysql-data:

networks:

my-network:

driver: overlay                 #默认为bridge,但是我们在多机集群里,要改成overlay。

发布:

docker stack deploy  wordpress  --compose-file=docker-compose.yml         #stack的名字为wordpress

查看:

docker stack ls

docker stack ps wordpress

docker stack services wordpress              #显示services replicas的情况。

访问:随便挑一台node的ip 8080端口

注意:docker swarm不能使用上面投票系统中的build,所以要自己build image

投票系统,使用docker swarm部署:

cat docker-compose.yml

version: "3"

services:

redis:

image: redis:alpine

ports:

- "6379"

networks:

- frontend

deploy:

replicas: 2

update_config:

parallelism: 2

delay: 10s

restart_policy:

condition: on-failure

db:

image: postgres:9.4

volumes:

- db-data:/var/lib/postgresql/data

networks:

- backend

deploy:

placement:

constraints: [node.role == manager]

vote:

image: dockersamples/examplevotingapp_vote:before

ports:

- 5000:80

networks:

- frontend

depends_on:

- redis

deploy:

replicas: 2

update_config:

parallelism: 2

restart_policy:

condition: on-failure

result:

image: dockersamples/examplevotingapp_result:before

ports:

- 5001:80

networks:

- backend

depends_on:

- db

deploy:

replicas: 1

update_config:

parallelism: 2

delay: 10s

restart_policy:

condition: on-failure

worker:

image: dockersamples/examplevotingapp_worker

networks:

- frontend

- backend

deploy:

mode: replicated

replicas: 1

labels: [APP=VOTING]

restart_policy:

condition: on-failure

delay: 10s

max_attempts: 3

window: 120s

placement:

constraints: [node.role == manager]

visualizer:

image: dockersamples/visualizer:stable

ports:

- "8080:8080"

stop_grace_period: 1m30s

volumes:

- "/var/run/docker.sock:/var/run/docker.sock"

deploy:

placement:

constraints: [node.role == manager]

networks:

frontend:                            #在swarm模式下默认是overlay的

backend:

volumes:

db-data:

启动:

docker stack deploy  voteapp  --compose-file=docker-compose.yml

docker secret 管理

internal distributed store 是存储在所有swarm manager节点上的,所以manager节点推荐2台以上。存在swarm manger节点的raft database里。

secret可以assign给一个service,这个service就能看到这个secret

在container内部secret看起来像文件,但是实际是在内存中。

secret创建,从文件创建:

vim alexpasswd

admin123

docker secret create my-pw alexpasswd    #给这个secret起个名字叫my-pw

查看:

docker secret ls

从标准输入创建:

echo "adminadmin" | docker secret create my-pw2 -              # 从标准输入创建

删除:

docker secret rm my-pw2

把一个secret暴露给service

docker service create --name client --secret my-pw busybox  sh -c while true;do sleep 3600;done"

查看container在哪个节点上:

docker service ps client

进入这个container:

docker exec -it ccee sh

cd /run/secret/my-pw                   #这里就能看到我们的密码secret

例如mysql的docker:

docker service create --name db --secret my-pw -e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/my-pw mysql

secret 在stack中使用:

有个密码文件:

cat password

adminadmin

docker-compose.yml文件:

version: '3'

services:

web:

image: wordpress

ports:

- 8080:80

secrets:

- my-pw

environment:

WORDPRESS_DB_HOST: mysql

WORDPRESS_DB_PASSWORD_FILE: /run/secrets/my-pw

networks:

- my-network

depends_on:

- mysql

deploy:

mode: replicated

replicas: 3

restart_policy:

condition: on-failure

delay: 5s

max_attempts: 3

update_config:

parallelism: 1

delay: 10s

mysql:

image: mysql

secrets:

- my-pw

environment:

MYSQL_ROOT_PASSWORD_FILE: /run/secrets/my-pw

MYSQL_DATABASE: wordpress

volumes:

- mysql-data:/var/lib/mysql

networks:

- my-network

deploy:

mode: global

placement:

constraints:

- node.role == manager

volumes:

mysql-data:

networks:

my-network:

driver: overlay

# secrets:

#   my-pw:

#    file: ./password

使用:

docker stack deploy wordpress -c=docker-compose.yml

service的更新:

首先建立源service:

docker service create --name web --publish 8080:5000 --network demo alexhe/python-flask-demo:1.0

开始扩展,至少2个:

docker service scale web=2

检查服务 curl 127.0.0.1:8080

while true;do curl 127.0.0.1:8080 && sleep 1;done

更新服务:

可以更新secret,publish port,image等等

docker service update --image alexhe/python-flask-demo:2.0 web

更新端口:

docker service update --publish-rm 8080:5000 --publish-add 8088:5000 web

k8s版本








docker kubernetes Swarm容器编排k8s CICD部署的更多相关文章

  1. [转帖]Kubernetes及容器编排的总体介绍【译】

    Kubernetes及容器编排的总体介绍[译] 翻译自The New Stack<Kubernetes 生态环境>作者:JANAKIRAM MSV和 KRISHNAN SUBRAMANIA ...

  2. 三小时学会Kubernetes:容器编排详细指南

    三小时学会Kubernetes:容器编排详细指南 如果谁都可以在三个小时内学会Kubernetes,银行为何要为这么简单的东西付一大笔钱? 如果你心存疑虑,我建议你不妨跟着我试一试!在完成本文的学习后 ...

  3. 宝塔面板 + Rancher + 阿里云镜像仓库 + Docker + Kubernetes,添加集群、部署 web 应用

    目录 一,安装宝塔面板(V 6.8) 二,使用宝塔安装 Docker,配置阿里云容器服务 三,安装 Rancher (Server) 四,管理 Rancher.添加集群 五,添加 Rancher 应用 ...

  4. [转载]三小时学会Kubernetes:容器编排详细指南

    原翻译by梁晓勇 原英文:Learn Kubernetes in Under 3 Hours: A Detailed Guide to Orchestrating Containers 我很奇怪,为什 ...

  5. docker之swarm容器部署及运维

    1.概念 Docker Swarm 是 Docker 的集群管理工具.它将 Docker 主机池转变为单个虚拟 Docker 主机. Docker Swarm 提供了标准的 Docker API,所有 ...

  6. 28. docker swarm 容器编排简介

    1.采用集群架构 集群架构包含节点和角色 docker 节点中 包含 worker 和 manager 两个角色 manager 相当于 swarm 集群的 大脑  是用来管理配置节点的 (避免单点故 ...

  7. Docker Compose之容器编排开发初探

    1.前言 Docker Compose 是 Docker 官方编排(Orchestration)项目之一,负责快速在集群中部署分布式应用. Compose 是一个用于定义和运行多个 Docker 应用 ...

  8. Docker系列10—容器编排工具Docker Compose详解

    本文收录在容器技术学习系列文章总目录 1.Docker Compose 概述 Compose是一个用于定义和运行多容器Docker应用程序的工具.使用Compose,您可以使用Compose文件来配置 ...

  9. docker kubernetes swarm spring cloud结合学习资源

    http://www.docin.com/p-2062732301.html https://blog.csdn.net/michael_hm/article/details/79213839 htt ...

随机推荐

  1. Debian 命令行方式配置网络

    一.对于有线网络,如果默认没有安装图形界面,进入了 multi-user.target中时,是没有使用NetworkManager管理网络的,此时需要手动配置才能上网 首先得到网卡名称:ip addr ...

  2. SqlServer变量

    MySql变量 => https://www.cnblogs.com/fanqisoft/p/10700669.html 全局变量 局部变量 ⒈全局变量 以@@开头,后面跟相应的字符串,全局变量 ...

  3. selenium采用find_element_by方法识别页面元素

    主要是练习获取页面中的各元素,马克 # coding:utf-8 import time from selenium import webdriver import unittest from pyt ...

  4. Linux Input子系统浅析(二)-- 模拟tp上报键值【转】

    转自:https://blog.csdn.net/xiaopangzi313/article/details/52383226 版权声明:本文为博主原创文章,未经博主允许不得转载. https://b ...

  5. JUC--闭锁 CountDownLatch

    CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,允许一个或者多个线程一直等待. 闭锁可以延迟线程的进度直到其到达终止状态,可以确保某些活动知道其他活动都完成才继续 ...

  6. Linux代理搭建TinyProxy

    操作系统:阿里云CentOS 7.4 64位 安装方法: yum install tinyproxy 配置: vi /etc/tinyproxy/tinyproxy.conf Port 8888 // ...

  7. 高可用Redis(一):通用命令,数据结构和内部编码,单线程架构

    1.通用API 1.1 keys命令和dbsize命令 keys * 遍历所有key keys [pattern] 遍历模式下所有的key dbsize 计算Redis中所有key的总数 例子: 12 ...

  8. java实现一个简单的Web服务器

    注:本段内容来源于<JAVA 实现 简单的 HTTP服务器> 1. HTTP所有状态码 状态码 状态码英文名称 中文描述 100 Continue 继续.客户端应继续其请求 101 Swi ...

  9. js变量污染引起的诡异bug

    js方法是这样的: //保存提货券JSON数据到隐藏字段 saveVoucherListInfoToHiddenFiled: function () { //保存绑定商品信息 var voucherL ...

  10. Angular动画

    Angular动画基于W3C的Web Animations标准.不在Angular Core中了. 组件里面定义一个或多个触发器trigger,每个触发器有一系列的状态和过渡效果来实现. 动画其实就是 ...