美文网首页Docker容器
Docker入门简介

Docker入门简介

作者: 西北偏北 | 来源:发表于2019-04-08 22:52 被阅读2次

    Docker是什么

    Docker是一种虚拟化技术,类似虚拟机,这使得安装在其中的程序能够只依赖虚拟机的环境,而不受外部操作系统环境的影响。同虚拟机不同的是,Docker的虚拟容器占用空间更小,使得它比虚拟机更容易分发和多实例安装。

    Docker容器化技术的整个开发使用方式非常类似java应用开发,这里同java应用开发做一个类比,帮助有过java开发经验的同学快速掌握其中的核心概念

    1554642418878.png

    Dockerfile

    相当于Java应用开发中的Maven配置文件pom.xml或则gradle的build.gradle文件。java开发中的pom.xml和build.gradle是用来声明java应用依赖的jar包,和应用的构建方式。而Dockerfile是用来声明一个程序依赖的环境和构建运行方式。比如redis的Dockerfile如下:

    # 第一部分,声明redis程序依赖系统环境,是使用的debian
    FROM debian:stretch-slim
    
    # 第二部分,配置系统权限,添加新的组和用户,专供redis使用
    RUN groupadd -r redis && useradd -r -g redis redis
    
    # 第三部分,是安装系统更新,环境变量配置,以及下载redis并安装
    ENV GOSU_VERSION 1.10
    RUN set -ex; \
        \
        fetchDeps=" \
            ca-certificates \
            dirmngr \
            gnupg \
            wget \
        "; \
        apt-get update; \
        apt-get install -y --no-install-recommends $fetchDeps; \
        rm -rf /var/lib/apt/lists/*; \
        \
        dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
        wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
        wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
        export GNUPGHOME="$(mktemp -d)"; \
        gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
        gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
        gpgconf --kill all; \
        rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \
        chmod +x /usr/local/bin/gosu; \
        gosu nobody true; \
        \
        apt-get purge -y --auto-remove $fetchDeps
    
    ENV REDIS_VERSION 5.0.4
    ENV REDIS_DOWNLOAD_URL http://download.redis.io/releases/redis-5.0.4.tar.gz
    ENV REDIS_DOWNLOAD_SHA 3ce9ceff5a23f60913e1573f6dfcd4aa53b42d4a2789e28fa53ec2bd28c987dd
    
    # for redis-sentinel see: http://redis.io/topics/sentinel
    RUN set -ex; \
        \
        buildDeps=' \
            ca-certificates \
            wget \
            \
            gcc \
            libc6-dev \
            make \
        '; \
        apt-get update; \
        apt-get install -y $buildDeps --no-install-recommends; \
        rm -rf /var/lib/apt/lists/*; \
        \
        wget -O redis.tar.gz "$REDIS_DOWNLOAD_URL"; \
        echo "$REDIS_DOWNLOAD_SHA *redis.tar.gz" | sha256sum -c -; \
        mkdir -p /usr/src/redis; \
        tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1; \
        rm redis.tar.gz; \
        \
    
        grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 1$' /usr/src/redis/src/server.h; \
        sed -ri 's!^(#define CONFIG_DEFAULT_PROTECTED_MODE) 1$!\1 0!' /usr/src/redis/src/server.h; \
        grep -q '^#define CONFIG_DEFAULT_PROTECTED_MODE 0$' /usr/src/redis/src/server.h; \
        \
        make -C /usr/src/redis -j "$(nproc)"; \
        make -C /usr/src/redis install; \
        \
        rm -r /usr/src/redis; \
        \
        apt-get purge -y --auto-remove $buildDeps
        
    # 第四部分,设置redis后续命令的工作目录
    RUN mkdir /data && chown redis:redis /data
    VOLUME /data
    WORKDIR /data
    
    #第五部分,启动redis服务,并配置向外暴露的端口
    COPY docker-entrypoint.sh /usr/local/bin/
    ENTRYPOINT ["docker-entrypoint.sh"]
    
    EXPOSE 6379
    CMD ["redis-server"]
    

    可能每个不同的Docker程序,其Dockerfile略有不同,但大致都可以总结为这么几步

    • 声明运行系统环境
    • 安装系统更新,安装程序
    • 配置环境变量
    • 设置向外暴露的端口,并启动程序

    image

    相当于java应用开发中的jar包。java中基于pom.xml或build.gradle build而成jar。而docker中,基于Dockerfile build出的是image。它可以像jar包一样,提交到Docker的中央仓库,并被下发指其它机器使用。一个使用Dockerfile构建image的demo如下:

    1. 先用python开发一个简单的web服务,名为app.py

       from flask import Flask
       from redis import Redis, RedisError
       import os
       import socket
       
       # Connect to Redis
       redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2)
       
       app = Flask(__name__)
       
       @app.route("/")
       def hello():
           try:
               visits = redis.incr("counter")
           except RedisError:
               visits = "<i>cannot connect to Redis, counter disabled</i>"
       
           html = "<h3>Hello {name}!</h3>" \
                  "<b>Hostname:</b> {hostname}<br/>" \
                  "<b>Visits:</b> {visits}"
           return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname(), visits=visits)
       
       if __name__ == "__main__":
           app.run(host='0.0.0.0', port=80)
      
    2. 再编写Dockerfile

       # 从程序代码中,我们知道使用的python,需要依赖python的环境。python环境的image在docker公共仓库中,可以直接使用,在这个image基础上,添加我们的应用,构建另一个image
       FROM python:2.7-slim
       
       # 把容器看做一个小型操作系统的话,这一步设置后续命令在这个容器操作系统内的路径。名字可以任意。相当于普通linux中的cd命令。路径不存在应该可以直接创建。Dockerfile后续的所有命令,都是在这个文件夹下执行的
       WORKDIR /app1
       
       # 将宿主机的当前路径内容拷贝到app1下
       COPY . /app1
       
       # 从app.py程序代码中,可以看到其依赖FLask库环境和Redis,这里通过pip安装,这一步是在python image的内部执行的,不是外部环境。相当于再给python的image系统镜像安装东西
       RUN pip install --trusted-host pypi.python.org Flask
       RUN pip install --trusted-host pypi.python.org Redis
       
       # 将容器的80端口暴露出来。
       EXPOSE 80
       
       # 在容器内设置一个环境遍历,key为NAME, value为world。就像linux中设置环境变量一样。只不过这里是在容器这个操作系统内设置环境变量,相应的容器中的程序可以读取这个环境变量
       ENV NAME World
       
       # 这一步放在最后,前面的所有命令基本上把程序要求的环境都初始化好了,这里直接执行命令,CMD的第一个参数是程序命令,后面的是参数。这里就是通过python来run app.py。 由于当前路径是/app1(前面WORKDIR设置的),并且其中包含app.py,所以在该路径下执行python app.apy当然找得到程序文件
       CMD ["python", "app.py"]
      
    3. 构建image
      在宿主机上创建一个文件夹,名字任意,将Dockerfile和app.py 都放置其中(因为Dockerfile中有一个命令COPY . /app1,所以要确保程序跟Dockerfile在同一的路径下,才可以拷贝进去。当然你可以不在一个路径下,那就需要修改Dockerfile命令,将具体app.py的路径写全),然后在该路径下执行构建命令构建image,并将其取名为hellworld

      docker build --tag=helloworld .

    1. 发布image
      你可以像发布jar一样,将image发布到docker中央仓库,或公司的私有仓库,具体方式这里就不展开讨论了。

    container

    类似于java应用中的jar运行。我们基于image运行后,会创建一个运行的实例,即为container,容器。比如我们可以使用以下命令,通过前面build的image,创建一个container

    docker run -p 4000:80 helloworld
    

    network

    container需要对外进行通信,可能需要网络服务。有5种网络驱动可供docker配置,用来配置docker的联网行为。

    • bridge 桥接模式,通过链路层设备链接host网络,它同host使用不同的ip,一般在单节点的host使用这种方式,默认是这种方式

    • host 模式,container直接跟host公用一个ip,这也意味这container暴露什么端口,通过host的ip可直接访问,不推荐这种方式

    • overlay docker集群的网络连接驱动方式

    • Macvlan 对docker配置mac地址,通过物理地址进行网络通信

    • none 使docker没有任何网络连接

    data volumes

    container中的程序运行时,可能会产生一些数据,或者需要使用一些数据,甚至希望同其它container共享数据。那么实现这些的方式就是data volumes,它对应docker的存储概念,后续会详细讲解。

    docker daemon

    类似于Java虚拟机。它负责image构建,分发,获取,执行,以及container、volumes、network等上述核心组件的管理,屏蔽底层操作系统的细节,使得基于docker构建的服务能够跨平台。我们一般通过docker CLI也即docker命令行来向docker deamon发送命令执行上述管理。


    1554642870860.png

    Docker的基本使用方式

    作为普通用户大多数时候,我们只是从中央仓库中获取别人制作好的image,在本地创建container来提供服务,比如获取mysql的image,在本地创建一个mysql的servers。所以下面主要介绍对container的一些核心操作命令。

    获取image

    使用如下命令去远程仓库中拉取,image文件

     docker pull IMAGE[:TAG]
    

    比如我们想要获取redis的image,在中央仓库中我们可以看到有很多redis的image,他们用不同的tag区分


    1554644617846.png

    我们可以通过指定tag来拉取特定的image,比如我们拉取tag为5.0.4-alpine的image。docker pull redis:5.0.4-alpine

    如何创建一个container

    docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
    

    其命令主干是docker run IMAGE。每一次run,都会创建一个新的container

    我们基于前面拉到的redis image启动一个containerdocker run redis:5.0.4-alpine

    可以在创建的时候指定许多参数,比如创建container时,指定名字docker run --name test-redis redis:5.0.4-alpine

    将容器中的程序以后台形式运行docker run --name some-redis -d redis

    查看docker相关组件

    我们可以使用ls命令,来查看docker中container,image,network,volume等组件的id和名字,就像linux中的ls命令一样。

    docker container ls #查看正在运行的container
    docker container ls --all #查看包括已停止和运行中的所有container
    docker image ls #查看本地拥有的image
    docker network ls#查看当前系统具有的网络驱动
    docker volume ls#查看当前系统具有的volume存储
    

    如何停止一个container

    docker container stop CONTAINER_ID|CONTAINER_NAME
    

    可以使用container的id或name来将处在run中的container停止

    如何启动一个start

    通过上述的ls命令,获取到container的名字或id,然后通过命令docker container start CONTAINER_ID|CONTAINER_NAME来启动容器,举例docker container start 48b24d849908

    如何查看container中的程序执行日志

    我们可以将container当做一个小的linux系统。那启动后如何登入?有两种方式,第一种是attach命令到指定的容器,比如

    sudo docker container attach 48b24d849908
    

    但这个命令是只将当前的host终端attach到指定的container中正在运行的进程。并显示其输出。但并不能任意的浏览container的其他系统目录。如果仅仅是为了看当前container中的运行程序日志,大可不必用上述方法,直接用logs命令输出即可(当然这种方式的能看到日志的前提是,container中的程序将日志输出到了STDOUT或STDERR中才行)比如:

    sudo docker container logs 48b24d849908
    

    如果嫌输出的日志太多,也可以管道加less慢慢看

    sudo docker container logs 48b24d849908 | less
    

    想要正真的直接登录container去浏览其系统文件,需要使用一下命令

    sudo docker container exec -it 48b24d849908 /bin/bash
    

    当然这个要容器里确实有bash程序才行。exec还可以run程序中的其他命令

    如何启动一个一次性的container

    基于image创建一个container后,如果不主动删除,那么该container会一直存在,若以希望container被停止后,自动删除。那么可以在创建命令run中加参数--rm。例如:

    docker run --rm --name some-redis -d redis
    

    如何让容器自动重启

    有时我们希望宿主机在重启后,或docker deamon重启后,相应的container能自动重启。那么在创建container时,使用参数--restart来控制重启行为。重启策略主要有以下几种

    • no 默认选项,不会自动重启container

    • on-failure 当container非正常退出时,自动重启

    • always 无论什么情况都自动重启。但手动停止容器后,需要docker daemon进程重启时,才会重启container,也即宿主机重启时,会重启container

    • unless-stopped 同always类似,但是手动停止的container不会在自动重启。

    举例docker run -dit --restart unless-stopped redis

    如何做端口映射

    程序运行在container中。container又被docker deamon管理。所以需要将container中的程序暴露的端口,映射到宿主机自己的指定端口,否则外部程序无法直接同container通信。可以在创建时指定参数-p来指定。例如:docker run -p 6379:6379/udp -p6379:6379 redis:5.0.4-alpine

    其中冒号左边为宿主机的端口,右边为container中程序暴露的端口。斜杠后面指定暴露的端口类型是UDP还是TCP,如果是TCP可以不写。

    如何映射文件系统

    container中程序可能需要读或写一些数据,要使得这些数据能够被宿主机可见,需要像端口映射一样,将container中的文件路径映射到外部文件系统中。这些外部的文件系统可以是宿主机的文件系统,也可是docker管理的volume。这里以宿主机的文件系统为例

    docker run -v /home/v2ray_proxy:/etc/v2ray -p 1081:1081  v2ray/official  v2ray -config=/etc/v2ray/config.json
    

    将宿主机路径/home/v2ray_proxy映射到container的/etc/v2ray路径,这样宿主机在/home/v2ray_proxy中修改的内容,container可以通过其/etc/v2ray路径获取到。反之亦然。

    如何清理所有不使用的container、image、volume、network

    可以使用rm命令,删除指定id或name的相关组件。比如:

    docker container rm CONTAINER_ID
    docker image rm IMAGE_ID
    docker volume rm VOLUME_ID
    docker network rm NETWOKR_ID
    

    可能上述手动挨个删太麻烦,你可以使用prune命令,直接将符合需求的组件全部删除。比如:

    docker image prune#删除未被任何容器使用的image
    docker container prune#删除所有未启动的container
    docker volume prune#删除所有未被使用的volume
    docker network prune#删除所有未被使用的网络
    docker system prune#删除所有未被使用的container,image ,volume, network。docker 1.7以上需要显示执行`--volumes`参数,才能一并将volume也删除,之所以这么做是害怕一不小心把数据给删了。多加参数增加了误删数据的门槛
    

    以上所有的删除prune命令,都可以基于过滤条件来删除。加参数--filter即可,比如删除过去24小时未启动的容器

    docker container prune --filter "until=24h"
    

    如何查看container的资源使用情况

    使用命令docker stats

    Layer

    一个Dockerfile最终会被构建成image,一个image被run后会生成一个container。为了最大化共享存储文件,减少存储空间的浪费,docker引入了层的概念layer. Docerkfile中RUN, COPY, ADD三个命令会产生layer

    一个dockerfile中从上下到下的命令,反应到image上是由下到上的层,每一层都是基于上一层进行构建的。layer又分为image layer和container layer,前者是image构建时,每句dockerfile命令对应生成的layer,后者是通过image 生成一个新的container 时,container所独有的read writer 层。

    container的read writer layer是container的程序读写文件时,文件的存储的层,它会随着container的销毁而销毁。通常来说,container运行生成或修改文件内容最好不要放到其read write layer,因为不方便cotnainer间共享,又容易影响container本身的读写性能,所以一般通过volume或bind mount的方式,将container读写的文件内容映射挂载到外部。

    比如,Dockerfile

    FROM ubuntu:15.04
    COPY . /app
    RUN make /app
    CMD python /app/app.py
    
    • 第一句是基础层,表示基于ubuntu15的image构建
    • 第二句在ubuntu15的基础上,将宿主机当前路径的内容拷贝到image的/app路径做为新的layer
    • 第三句,使用make命令,将/app中的文件进行编译,生成的内容为新的layer
    • 第四句,使用python命令运行上一步build的可执行文件app.py,其对应container中的R/W layer

    其对应的image层的示例为:

    多个container公用image layer的示例:

    1554727796358.png

    文件系统

    Docker中的任何数据的产生,默认都是存储在了container的write layer,这带来了以下一些问题:

    • 不方便备份和访问,因为数据在容器里面
    • 数据易丢失,当容器被删除后,数据也跟着被删除
    • 不方便程序更新,容器跟数据绑定了,这个时候你想通过更新的image,创建新的容器来达到升级程序的目的变得很难,因为你要丢数据

    为了解决这些问题,Docker提出数据更容器分离的理念。以挂载的路径来区分,有以下三种挂载方式

    • volume mount 受docker deamon管理的文件系统
    • bind mount 当前宿主机的文件系统
    • tmpfs mount 内存
    1554728019701.png

    Volume mount

    创建volume的几种方式

    1. 直接用volume命令创建例如docker volume create my-vol

    2. 在创建一个container时或service时,通过参数-v或者--mount挂载volume时,volume不存在,也会自动创建。举例如下:

       //volume名为myvol2,挂载到container的指定目录为/app
       $ docker run -d \
         --name devtest \
         -v myvol2:/app \
         nginx:latest
      
      
       //创建四个nginx container组成的service
       $ docker service create -d \
         --replicas=4 \
         --name devtest-service \
         --mount source=myvol2,target=/app \
         nginx:latest
      

    -v--mount
    这两个都能指定挂载的volume(如果不存在,都会创建),创建service时,只能使用mount命令。-v参数后面直接指定所有的配置value不直观,--mount的配置,则是以key=value的形式体现,能够清楚的知道指定配置项意义。能通过他们配置的信息有:

    • source container外的宿主文件系统(bind mount时,source就是宿主的文件路径)或volume
    • destination path: container 内的指定路径
    • 读写模式:对挂载的宿主文件路径或volume是否有读写的权利
    • driver: 如果挂载到container的是volume时,配置该volume的驱动类型。volume的驱动类型默认是local,也即宿主机所在文件系统。但有些volume对应的存储可能是aws,所以其驱动就不是local.

    使用-v参数的大概形式为:

    -v <source>:<destination>
    //其中source可以忽略,忽略时,默认创建一个匿名的volume
    

    使用--mount参数的大概形式为:

         --mount 'type=volume,src=<VOLUME-NAME>,dst=<CONTAINER-PATH>,volume-driver=local,volume-opt=type=nfs,volume-opt=device=<nfs-server>:<nfs-path>'
         //其中src可以写为source
         //dst 可以写为destination或target
    

    像volume中填充内容
    如果一个空的volume挂载到指定的container目录,并且该目录下已经有内容,那么这些内容会自动被复制到volume下。举例如下;

    $ docker run -d \
      --name=nginxtest \
      --mount source=nginx-vol,destination=/usr/share/nginx/html \
      nginx:latest
    //名为nginx-vol的volume,里面会被拷贝进/usr/share/nginx/html文件
    

    bind mount

    volume是挂载一个由docker 守护进程管理的文件系统到container。而bind mount是直接挂载宿主机的任意文件路径到container。这样宿主机其他进程该挂载路径下的文件内容,container也会感受到,反之亦然。其挂载命令跟volume差不读,不再赘述,只是其mount的type为bind。

    简单总结来看,希望容器间相互共享内容,使用volume挂载到container
    希望容器和宿主机之间相互共享内容,使用bind mount

    tmpfs mount

    tmpfs是将容器指定路径映射到内存,这样当容器对指定路径写数据时,不会写到容器自己的write layer。并且tmpfs不能被容器共享,即A容器mount 的tmpfs,不能被B容器读到,这就使得tmpfs非常适合存储一些易失的,且容器独有的私密信息。

    tmpfs只能在linux的docker中使用

    tmpfs的挂载也有两种参数方式,一是--tmpfs,二是--mount,前者不能指定任何参数,后者则可以,后者的功能和工作范围都比较广。

    volume和bind在挂载时,需要指定一个source,而tmpfs的挂载不需要,只用指定挂载到对应contaienr的路径即可。

    后话

    容器化使得部署应用变得简单方便。docker还提供了swarm,使得服务以集群化形式编排和部署同样变得简单。这里不再详述。

    使用容器化提供服务时,需要遵循微服务化的原则,保持服务的原子性,即一个container只提供一种服务。这样更加方便后期管理和程序扩展。

    参考资料

    https://docs.docker.com/

    相关文章

      网友评论

        本文标题:Docker入门简介

        本文链接:https://www.haomeiwen.com/subject/udimiqtx.html