可能大家会比较奇怪明明讲的是Django为啥要介绍Docker,实不相瞒如果大家不知道Docker对Django的学习并无影响,但是个人之前很早就听说Docker了,借着这个机会也学习下,趁着有这个应用场景也事件下,其实个人对Docker也不是很熟悉,只是现学现卖。将整个过程贯通起来,个人接触新东西一般喜欢围绕着问题展开,遇到不明白的在网上找资料或者找书去了解。这篇博客也采用这种方式,下面就围绕着几个问题展开,这篇博客不会对Docker进行太深入了解,目标是够用就好。深入的知识在后续大家使用的时候遇到问题再在实践中解决,毕竟精力有限。

1. 容器化 vs 虚拟化

虚拟化是通过中间件将一台或者多台独立机器虚拟运行与物理硬件之上,用户并不能感知为他们服务的到底是哪台机器,事实上呈现在用户面前的就和使用一部机器是一样的感觉,只不过这部机器在物理范畴上可能不是单纯一台主机,可能有多台机器组成的一个集群。虚拟机是抽象硬件资源,每一个虚拟机实例占用指定数量的CPU、内存、硬盘等资源,这些资源每个虚拟机实例之间不会共享。

而什么是容器化呢?容器化从应用出发,将应用分割成多个容器,而这些容器直接运行在操作系统内核上的用户空间,容器技术可以让多个独立用户空间运行在同一台宿主机上。也就是说容器技术是抽象软件资源,它和Linux上运行的一个应用程序没有太大区别。

早期,大家都认为虚拟化方式可以最大程度上提供虚拟化管理的灵活性。但是随着时间推移,大家发现,虚拟化技术有个问题就是:每个虚拟机都需要运行一个完整的操作系统以及其中安装好的大量应用程序。但实际生产开发环境里,我们更关注的是自己部署的应用程序,如果每次部署发布我都得搞一个完整操作系统和附带的依赖环境,那么这让任务和性能变得很重和很低下。这时候,人们就在想,有没有其他什么方式能让人更加的关注应用程序本身,底层多余的操作系统和环境可以共享和复用?换句话来说,那就是我部署一个服务运行好后,我再想移植到另外一个地方,我可以不用再安装一套操作系统和依赖环境,这就是容器化提出的场景。

2. 容器化的特点

容器是自包含的,它打包了应用程序及其所有依赖,可以直接运行。
容器是可移植的,这就可以确保应用在开发环境、测试环境、生产环境等都有完全一样的运行环境。
容器是互相隔离的,同一主机上运行的多个容器,不会互相影响。
容器是轻量级的,体现在容器的秒级启动,并且占用资源很少。

3. 什么是Docker,Docker的组成及镜像结构

Docker是一个能够把开发应用程序自动部署到容器的开源引擎,使用Docker开发人员只需要关心容器中运行的应用程序,运维人员只需要关心如何管理容器。
它能保证写代码的开发环境与应用程序要部署的生产环境一致性。这对经常出现开发环境是好的,等到部署上去各种问题,又得各种联调的程序员来说无疑是巨大的福音。

Docker 目前大量用于:
持续集成和持续部署 (CI/CD), 加速应用管道自动化和应用部署,
以及结合微服务技术构建可伸缩扩展的服务框架,
服务器资源共享
创建隔离的运行环境
这些场景。

那Docker又是由那些组件组成的呢?
Docker 主要由

  1. Docker引擎:Docker引擎是由客户端服务器架构的程序,客户端通过docker命令行工具以及一套restful API向Docker服务器发出请求,Docker服务器或者称为守护进程完成所有工作并返回。Docker服务器和客户端可以在同一台宿主机器上运行,也可以从本地的Docker客户端连接到另一台宿主机上远程Docker服务器。
  2. Docker镜像:用户基于镜像运行自己的容器,可以把镜像当作容器的源代码,或者相当于我们安装系统的光盘,写Dockerfile就相当于刻录系统光盘。
  3. Docker容器:如果说Docker镜像相当于系统光盘,那么Docker容器就是由这个系统光盘制作出来的可以跑的系统。
  4. Registry: 和我们的github类型,github存储的是代码,而Registry存储的是Docker的镜像,换句话说它就是Docker镜像仓库。

下面是整个Docker组件的组成图:

从 Docker 的使用角度来说最为关键的是镜像的制作,Docker 镜像的制作是通过Dockerfile来完成的,Dockerfile的编写我们会在下面进行介绍,这里我们先来看下Docker 镜像的组成:

容器基于镜像启动和运行。可以说Docker镜像是容器的基石,Docker的镜像是一个层叠的只读文件系统,它的最底端是一个引导文件系统及bootfs。 Docker用户几乎永远都不会和引导文件系统有交互,实际上当一个容器启动后,bootfs会被移到内存中,引导文件将被卸载。Docker镜像的第二层是rootfs(root文件系统),位于引导文件系统之上,可以有多种操作系统。 在传统的linux系统中root文件系统最先会以只读的方式加载,当引导和启动完成后他才会被切换成读写模式。 但是在Docker里,root文件系统永远只能是只读,并且Docker会用联合加载系统在rootfs之上加载更多的只读文件系统。 联合加载只得是一次加载多个文件系统。但是在外面看来只有一个文件系统。联合加载会将各层文件系统加载到一起, 这样最终的文件系统会包含所有的文件及目录。Docker将这样的文件系统称为镜像。 一个镜像可以放到另一个镜像顶部,位于下面的镜像称为父镜像。一个容器中可以运行用户的一个或多个进程。当一个容器启动时,Docker会在镜像的最顶层增加一个读写文件系统,我们在Docker中运行的程序就是在这个层运行并执行的。第一次启动Docker时,读写层是空的,当文件发生变化后都会应用到这一层。比如修改一个文件,先将该文件从只读层复制到读写层,然后隐藏只读层,这就是Docker的写时复制。

4. Docker的常用操作命令

镜像操作:

将镜像拉到本地             [docker pull ubuntu]
查看当前已经有的镜像 [docker images]
查找镜像 [docker search xxx]
删除镜像 [docker rmi d5a6e75613ea]

登录注销docker hub [docker login/logout]
上传镜像:在docker hub 上创建一个docker地址。 标准格式为 用户名/docker镜像名 比如我这边创建的docker镜像名 为testdocker 构建命令如下:
docker build -t "tbfungeek/testdocker:0.0.1" .
使用下面命令就可以进行pushdockerhub
docker push tbfungeek/testdocker:0.0.1

容器操作:

查看docker info      [docker info]
查看当前正在运行的容器 [docker ps -a]
创建容器 [docker run -dit -p 8888:80 --name test ubuntu /bin/bash]
删除容器 [docker rm 容器id]
启动容器 [docker start xxxx]
重启容器 [docker restart xxxx]
附加到容器中 [docker attach xxxx]
退出容器 [exit]
停止容器 [docker stop]
查看日志 [docker logs -f xxxx]
查看端口 [docker port 4d17d19e34e2]

5. DockerFile的常用指令

构建会在Docker后台守护进程(daemon)中执行,而不是CLI中。构建前,构建进程会将全部内容(递归)发送到守护进程。
在创建一个Docker 镜像的时候推荐重新新建一个空的目录作为构建Docker的上下文,并且将Dockerfile放在上下文目录下的顶层目录(虽然可以通过-f参数来指定构建Docker的目录但是推荐还是放在上下文目录的顶层),在这个上下文文件夹中只存放用于构建当前 Docker镜像所必须的文件,对于不需要的文件通过dockerignore文件进行忽略。

Docker 守护进程会一条一条的执行Dockerfile中的指令,而且会在每一步提交并生成一个新镜像,最后会输出最终镜像的ID。生成完成后,Docker 守护进程会自动清理你发送的上下文。
Dockerfile文件中的每条指令会被独立执行,并会创建一个新镜像,RUN cd /tmp等命令不会对下条指令产生影响。
Docker 会重用已生成的中间镜像,以加速docker build的构建速度。

1. 创建目录
2. 创建Dockerfile
3. 编写Dockerfile
# Version: 0.0.1
FROM ubuntu:latest
MAINTAINER linxiaohai "tbfungeek@163.com"
RUN apt-get update && apt-get install vim
EXPOSE 80
4. 编译 Dockerfile生成镜像
docker build -f web_container/Dockerfile .
docker build --no-cache -t "标签linxiaohai/web:v1" .
docker build --no-cache -t "标签linxiaohai/web:v1" git@github.com:xxx/web_container
FROM
FROM <image>
FROM <image>:<tag>
FROM <image>@<digest>

在Dockerfile中第一条非注释指令一定是FROM,它指定了以哪一个镜像作为基准镜像,首先会先判断本地是否存在,如果不存在则会从仓库下载,这里推荐使用官方镜像

LABEL

给构建的镜像打标签。
如果base image中也有标签,则继承,如果是同名标签,则覆盖。为了减少layer数量,尽量将标签写在一个LABEL指令中去,如:

LABEL author="lin xiaohai" \
version="0.0.1"
指定后可以通过docker inspect查看:
"Labels": {
"author": "lin xiaohai",
"version": "0.0.1"
}
VOLUME

VOLUME用于创建挂载点,即向基于所构建镜像创始的容器添加卷

VOLUME ["/var/log"]
VOLUME /var/log /var/db

如,通过VOLUME创建一个挂载点:

ENV volum "/home/mydata"
VOLUME ${volum}

构建的镜像,并指定镜像名为docker_file。构建镜像后,使用新构建的运行一个容器。运行容器时,需-v参将能本地目录绑定到容器的卷(挂载点)上,以使容器可以访问宿主机的数据。

docker run -dit -v ~/test:/home/mydata/ --name "volumetests" docker_file
USER

USER用于指定运行镜像所使用的用户:

USER daemon

使用USER指定用户时,可以使用用户名、UID或GID,或是两者的组合。以下都是合法的指定试:

USER user
USER user:group
USER uid
USER uid:gid
USER user:gid
USER uid:group

使用USER指定用户后,Dockerfile中其后的命令RUN、CMD、ENTRYPOINT都将使用该用户。镜像构建完成后,通过docker run运行容器时,可以通过-u参数来覆盖所指定的用户。

WORKDIR
WORKDIR /path/to/workdir

WORKDIR指令用于设置Dockerfile中的RUN、CMD和ENTRYPOINT指令执行命令的工作目录(默认为/目录),该指令在Dockerfile文件中可以出现多次,如果使用相对路径则为相对于WORKDIR上一次的值

ARG

ARG用于指定传递给构建运行时的变量:

ARG <name>[=<default value>]

在使用docker build构建镜像时,可以通过–build-arg =参数来指定或重设置这些变量的值。
docker内置了一批构建参数,可以不用在Dockerfile中声明:HTTP_PROXY、http_proxy、HTTPS_PROXY、https_proxy、FTP_PROXY、ftp_proxy、NO_PROXY、no_proxy

RUN

RUN指令会在当前镜像的顶层执行任何命令,并commit成新的(中间)镜像,提交的镜像会在后面继续用到。
上面看到RUN后的格式有两种写法。

shell格式,相当于执行/bin/sh -c ““:

RUN apt-get install vim -y

exec格式,不会触发shell,所以$HOME这样的环境变量无法使用,但它可以在没有bash的镜像中执行,而且可以避免错误的解析命令字符串:

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

RUN ["/bin/bash", "-c", "apt-get install vim -y"] 与shell风格相同

RUN可以执行任何命令,然后在当前镜像上创建一个新层并提交。提交后的结果镜像将会用在Dockerfile文件的下一步。
通过RUN执行多条命令时,可以通过\换行执行,也可以在同一行中,通过分号分隔命令:

CMD

一个Dockerfile里只能有一个CMD,如果有多个,只有最后一个生效。CMD指令的主要功能是在build完成后,为了给docker run启动到容器时提供默认命令或参数,这些默认值可以包含可执行的命令,也可以只是参数(此时可执行命令就必须提前在ENTRYPOINT中指定)。
它与ENTRYPOINT的功能极为相似,区别在于如果docker run后面出现与CMD指定的相同命令,那么CMD会被覆盖;而ENTRYPOINT会把容器名后面的所有内容都当成参数传递给其指定的命令(不会对命令覆盖)。另外CMD还可以单独作为ENTRYPOINT的所接命令的可选参数。
CMD与RUN的区别在于,RUN是在build成镜像时就运行的,先于CMD和ENTRYPOINT的,CMD会在每次启动容器的时候运行,而RUN只在创建镜像时执行一次,固化在image中。

ENTRYPOINT

ENTRYPOINT命令设置在容器启动时执行命令,如果有多个ENTRYPOINT指令,那只有最后一个生效。
使用exec格式,在docker run 的所有参数,都会追加到ENTRYPOINT之后,并且会覆盖CMD所指定的参数(如果有的话)。当然可以在run时使用–entrypoint来覆盖ENTRYPOINT指令。
以推荐使用的exec格式为例:
我们可以使用ENTRYPOINT来设置基本不会变化的命令,用CMD来设置其它的可能改变的默认启动命令或选项(docker run会覆盖的)。

ENV

用于设置环境变量:

ENV <key> <value>
设置了后,后续的RUN命令都可以使用,当运行生成的镜像时这些环境变量依然有效,如果需要在运行时更改这些环境变量可以在运行docker run时添加-env <key>=<value>参数来修改
ADD

在构建镜像时,复制上下文中的文件到镜像内,格式:

ADD <src>... <dest>
ADD ["<src>",... "<dest>"]

可以是文件、目录,也可以是文件URL。可以使用模糊匹配(wildcards,类似shell的匹配),可以指定多个,必须是在上下文目录和子目录中,无法添加../a.txt这样的文件。如果是个目录,则复制的是目录下的所有内容,但不包括该目录。如果是个可被docker识别的压缩包,docker会以tar -x的方式解压后将内容复制到

可以是绝对路径,也可以是相对WORKDIR目录的相对路径。如果路径不存在则会自动级联创建,根据你的需要是里是否需要反斜杠/,习惯使用/结尾从而避免被当成文件。

COPY

COPY的语法与功能与ADD相同,只是不支持上面讲到的是远程URL、自动解压这两个特性,但是Best Practices for Writing Dockerfiles建议尽量使用COPY,并使用RUN与COPY的组合来代替ADD,这是因为虽然COPY只支持本地文件拷贝到container,但它的处理比ADD更加透明,建议只在复制tar文件时使用ADD,如ADD trusty-core-amd64.tar.gz /。

EXPOSE

EXPOSE指令告诉容器在运行时要监听的端口,但是这个端口是用于多个容器之间通信用的(links),外面的host是访问不到的。要把端口暴露给外面的主机,在启动容器时使用-p选项。

ONBUILD

向镜像中添加一个触发器,当以该镜像为base image再次构建新的镜像时,会触发执行其中的指令。格式:

ONBUILD [INSTRUCTION]

比如我们生成的镜像是用来部署Python代码的,但是因为有多个项目可能会复用该镜像。所以一个合适的方式是:

[...]
# 在下一次以此镜像为base image的构建中,执行ADD . /app/src,将项目代目添加到新镜像中去
ONBUILD ADD . /app/src
# 并且build Python代码

ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

注意
ONBUILD只会继承给子节点的镜像,不会再继承给孙子节点。
ONBUILD ONBUILD或者ONBUILD FROM或者ONBUILD MAINTAINER是不允许的。

STOPSIGNAL

STOPSIGNAL用于设置停止容器所要发送的系统调用信号:

STOPSIGNAL signal

所使用的信号必须是内核系统调用表中的合法的值,如:9、SIGKILL

可以通过如下材料进行进一步学习:

http://www.cnblogs.com/qcloud1001/p/9273549.html
https://legacy.gitbook.com/book/yeasy/docker_practice/details
http://product.dangdang.com/23941643.html
https://github.com/qianlei90/Blog/issues/35
https://github.com/qianlei90/Blog/issues/36
http://seanlook.com/2014/11/17/dockerfile-introduction/
https://docs.docker.com/get-started/#docker-concepts

1.为什么选用Django

为什么选用Django,从个人角度来看就是因为它简单,上手容易,等到用熟后再切换到其他的框架比如Nodejs,Spring Boot,无非就是换个语言,思想是可以共同的,这就是我选用Django的原因,那它的简单体现在哪里?

  1. 开发语言为Python简单,在IT界有句名言“人生苦短,我用python”,python简单吗?只能说上手容易,能够做的覆盖面广,一门语言到深入的时候都是困难的。
  2. 切换框架组件十分容易,换个数据库就只要修改个setting配置
  3. 集成的组件大而全,对我而言最为诱人的是它集成的Admin后台。

2.Django架构总览

说到Django的架构不得不提到MTV,大家可能听说过MVC MVVM MVP这些,其实MTV也可以看成MVC,那MTV分别代表什么呢?
M - Model 也就是 Django中的数据模型
T - Template 等同MVC中的V 也就是Django中的视图层
V - View 等同于MVC中的C 也就是Django中的控制层

我们就依靠下面的图来讲下整个请求处理流程:

  1. 请求源发起网络请求,这里以常见的GET/POST为例子,这里的请求源有很多,比如常见的浏览器,手机app
  2. 请求到达框架后首先会经过请求中间件的处理
  3. 经过中间件处理后的请求会发送到路由上进行路由分配,路由分配是将这些请求分配到各个View中。这些请求作为View的参数传递进去。
  4. 到达view后,在view中会从数据库中将数据取出来,封装成一个一个Model对象,使用这些对象来完成我们的任务,在这个过程中可能会返回文件系统,比如图片,音视频资源,然后将这些资源整合在一起,传递给Template
  5. 在Template层中会将从view层中传递过来的数据整合到界面上,渲染出来,形成一个response。
  6. 生成response后在送达到用户浏览器之前可能还会经过response 中间件处理后送达到用户浏览器。

3.WSGI uWSGI ngix

这里我们不涉及到部署方面的知识,这些会在后面专门章节进行介绍,这里只不过介绍下这些概念,让大家对整个服务端结构有个大体的认识,我们前面讲的是从一个web app角度来看的一个流程,下面我们介绍的是从一台服务器的角度来看这个问题,区别在哪里呢?一个服务器可以包含一个或者多个应用。那就多出了服务器与web app的一个交互过程。

首先我们简要介绍下用户通过浏览器访问网页,具体经过了哪些环节。

  1. 用户输入需要访问网站的url,浏览器将这些封装成符合http格式的Request请求,这个请求中可以包含请求首行、请求头和请求体这些内容。
  2. 上面的Request请求是应用层数据,要通过网络请求发送出去需要再由操作系统完成TCP、IP、MAC层封装,最终送到网卡以比特流形式传递出去。
  3. 经过网络传输,比特流到达服务器端,被服务器接收,服务器操作系统依次剥离MAC、IP、TCP层封装,取出应用层数据,也就是浏览器发送出的Request请求,并交给应用层的Web应用。
  4. Web 应用解析Request请求内容,并生成Respond响应,交给服务器
  5. Respond响应也是应用层数据,由服务器OS完成TCP、IP、MAC层封装,送到网卡处以比特流形式送出。
  6. 经过网络传输,包含resonse的比特流到达服务器端,被用户机器接收。
  7. 用户机器逐一去掉 MAC、IP、TCP层封装,取出应用层数据,也就是Respond响应,并交给应用层的浏览器。浏览器根据Response响应内容,显示在用户面前。

我们再看下下面这张图:

我们上面讲的内容是django 那个框,这个框的输入是一个request输出是一个response,我们必须注意到一点服务器操作系统和web app也是有交互的:服务器操作系统将Request请求传给Web APP,Web APP处理后,将Respond响应传给服务器操作系统,那么,服务器操作系统怎么把Request请求传给Web APP?这就涉及到了WSGI接口。WSGI接口其实是一个协议,一个规范是抽象的东西。这个接口连接了服务器操作系统和和Web APP。

一般我们讲到一个东西的必要性就会做出如下假设?如果没有这个东西情况会怎样?如果没有wsgi,那么服务器OS来了个请求后首先需要判断这个请求是发送给哪个web app,必须根据不同的web app类型调用不同的接口,通过不同的接口将request传递过去。这显然不合常规,那正常的做法是怎样呢?正常应该有个公共的协议,我们不关心具体的web framework的类型是啥,你是Django 也好,是Flask也要,只要你是符合wsgi接口的web框架,我都能保证调用同一个接口将request从服务器操作系统传递给web app。也就是保证了不同web框架对外的一致性。

那么ngix的作用是什么呢?nginx的功能十分强大,我们只关心它的请求分发的功能。谈到它需要将它与web app的路由进行对比,web app的路由是将到达app的请求,细分到各个响应函数进行处理,而nginx的作用是决定某个请求分发到哪个web app,也就是说,首先某个请求从一个端口进来,通过ngix将其分配到该台服务器上的某个web app上面,web app路由再将这些请求,细分到函数进行处理。大致是这样一个区别。不知道大家理解了没有。记住一个服务器有不止一个web app,这里的web app用的web框架不止有一种,假设来了个请求你要怎么在这些不同的web框架之间进行分配。理顺了这些问题就ok了。