Learn DevOps 第二章:Start DevOps with Docker(一)

发布时间 2023-11-28 10:35:41作者: Offer多多

一、Introduction and installation

这一张让我们来看一些让开发运维变得非常简单的东西:Containerization。

我们身处微服务的世界,有数百个微服务,一些用Java构建,一些用python构建,还有一些可能是用Javascript构建的。这三种语言的应用程序所需的部署环境各不相同,部署过程也可能因应用程序的不同而不同。如果我们自动供应服务器并为微服务配置它们,则需要不同的配置和脚本,这样维护这些环境成为一个大难题。有没有办法让所有这些微服务都拥有相同的部署环境和部署过程呢?这就是Docker提供的解决方案。

一旦有了任何应用程序或微服务,就可以构建包含微服务的Docker映像。一旦构建了一个docker映像,无论docker是用于python应用程序还是其他程序,我们都可以用相同的方式运行。运行docker映像所需的软件是相同的,与docker映像包含的内容无关。一旦使用Container,代码的基础结构就会变的十分简单,我们可以配置服务器集群,当这些服务器上安装了container runtime时,就可以直接将容器部署到这些服务器上,无需担心是什么语言编写的程序。容器化使得DevOps变得十分简单。

Docker是一个开源的平台,用于自动化应用程序的部署、扩展和管理。它通过容器化技术,将应用程序及其依赖项打包成一个独立的可移植容器,从而实现在不同的环境中一致地运行。包括如下几方面:

  1. 环境一致性:Docker确保应用在不同的环境中运行一致,减少问题。
  2. 快速部署:Docker可以快速在不同的环境中部署应用。
  3. 持续集成和持续部署(CI/CD):Docker可以与CI工具集成,自动构建、测试和部署应用,简化交付流程。
  4. 资源利用率:Docker允许在同一台主机上运行多个容器,提高硬件资源利用率。
  5. 可伸缩性:Docker简化了应用的水平扩展,提高系统的可伸缩性。
  6. 版本控制:Docker镜像支持版本控制,便于管理不同的应用版本。
  7. 隔离性:Docker提供了良好的容器隔离,防止应用冲突,提高系统稳定性。

现在我们要安装docker,并运行一些基本的命令,将使你可以很好的了解docker。由于我本机已经装好了docker,所以在这里不再讲述,大家可以自行搜索教程。

接下来我们将看一个十分有用的用例,假如你是DevOps团队的一员,团队正在开发一个令人惊叹的新项目,将使用java、python、javascript这三种不同的语言构建api,我们已经准备好了所有这些应用程序的基本版本,并且希望能尽快部署到环境中。你有一个配对的团队成员,他想了解你如何把应用程序部署到环境中。你们两个坐在桌子前,启动终端,输入以下命令:

我们可以看到docker的版本,非常好,现在你们准备先部署python应用程序,所以你开始执行一个简单的命令:

docker run -p 5000:5000 in28min/hello-world-python:0.0.1.RELEASE

运行命令时我们会发现它首先说“unable to fine image”,接着试图从一个docker注册表中提取一些东西,进行下载。下载完成后几秒钟内,我们就能看到应用程序已经启动并运行。接着我们在浏览器中输入:

localhost:5000

可以看到程序已经运行,我们只花了几秒钟就能快速部署程序并且运行,这令你的队友惊呆了,他说“我曾经从开发团队那里得到一份文档,描述硬件、操作系统的特定版本、软件的特定版本以及要部署的应用程序的特定版本。我们需要手动按照说明操作,创建一个服务器,在服务器上安装一个操作系统,然后安装正确的软件,安装tomecat(tomcat是一个开源的、轻量级的java web应用程序服务器,用于支持java网络应用的运行环境)或者运行程序所需要的工具,并安装正确的语言Java或者python,之后,我将下载应用程序并且进行部署,在手动设置环境时候,我经常犯很多错误。”

这时你告诉他:“有了docker,我们不需要关心运行应用程序的细节,我们实际上不需要担心应用程序是什么语言构成的。也不需要担心Java的版本或者用于构建应用程序的框架是什么,运行程序需要什么软件。”开发人员只需要创建一个docker image,无论docker runtime安装在什么地方,都可以用相同的方式运行image。无论image包含Java、python还是nodejs,我们都可以用相同的方式运行。

现在让我们试着运行Java程序,首先关闭正在运行的python容器,使用“Ctrl+C”来终止容器。

可以看到种植之后刷新网站,已经无法访问。输入新的命令:

docker run -p 5000:5000 in28min/hello-world-java:0.0.1.RELEASE

将python改成Java,无需更改其他内容,无需安装Java,也无需安装特定的需求、框架 或者运行程序的工具,就可以快速启动Java程序并运行:

Java应用程序下载时间稍长,从图中最后一行我们可以看到大约花了6.928秒来启动这个应用程序。接着我们去刚才的网站刷新,可以看到java程序已经成功运行:

接着我们来测试nodejs程序,同样使用“Ctrl+C” 来停止当前的Java应用。接着输入下边的命令:

docker run -p 5000:5000 in28min/hello-world-nodejs:0.0.1.RELEASE

现在我们去浏览器可以看到js程序已经成功运行:

可以看到,只要我们有了docker image,无论是python、Java还是js,都可以使用相同的docker命令运行。不需要按照手册进行环境配置,我们只需要运行一个简单的命令就可以运行程序。

 

二、Docker相关概念

接着,让我们了解一下在运行前边的命令时后台发生了什么,让我们重新回顾一下命令:

docker run -p 5000:5000 in28min/hello-world-nodejs:0.0.1.RELEASE

“in28min/hello-world-nodejs:0.0.1.RELEASE”是一个docker image的路径,存储在名为Docker hub的registry上,Docker hub是一个公共的Docker registry,网址为“hub.docker.com”。

一个docker registry包含了许多版本的不同应用程序的存储库。Docker hub是一个公共的注册表,所有人都可以访问。在企业中工作时,我们通常使用私有的存储库,以便只有有权访问应用程序映像的人员才能访问。现在我们可以想知道这个特定的image存储在什么地方,那么我们可以复制路径,到网站中:

接着我们就可以来到image的页面:

当我们要指定图像的路径时,我们需要指定两件事:

  • in28min/hello-world-nodejs: Docker repository
  • 0.0.1.RELEASE:image版本

点击tag我们可以看到0.0.1版本,此特定映像包含运行特定应用程序所需的所有内容,例如运行需要的软件、库、依赖项等。我们可以为同一个存储库使用多个tag,如图中的“0.0.1.RELEASE”和“0.0.2.RELEASE”用于多个不同版本的应用。

当执行这个命令时,它首先显示无法在本地找到image,是因为Docker首先检查image在本地是否存在,如果无法找到,就连接到Docker hub,从repository中提取image,下拉到本地计算机。一旦下载了image,Docker就会运行该特定的image。运行的image被称为Container,image是静态的,在存储库中,他只是一组字节,大约33MB,当image被下载到本地机器上时,也只是一组字节被下载下来。当image正在运行时,被称为容器。在后边的课程中我们会看到,对于一个image,我们可以有许多正在运行的Container。

一个镜像(image)是一个轻量级、独立的可执行软件包,包含运行应用程序所需要的一切,包括代码、运行时、系统工具、库以及设置。镜像是一个静态的定义,类似于一个软件的安装包。一个容器(Container)是根据镜像运行的实体,容器是镜像的一个运行时实例,包含了镜像以及正在运行的应用程序。容器提供了一个隔离的运行环境,确保应用程序及其依赖项在不同的环境中具有一致性和可移植性。

我们可以使用同一个镜像启动多个容器实例,这意味着我们可以在同一台机器上运行多个相同应用的拷贝,每个拷贝都是一个独立的容器。每个容器都是相互隔离的,它们拥有自己的文件系统、网络空间和进程空间。它们可以在同一主机上运行,互不影响。多个容器可以根据需要扩展或收缩,我们可以根据负载需求增加或减少容器的数量实现水平扩展。

最后一件事是,我们为什么要使用“-p 5000”呢,因为每当我们运行一个容器时,它是一个内部码头网络的一部分,称为桥网络。因此,默认情况下,所有的容器都在桥接网络内部运行,容器之间可以相互通信,但是对外部是不可访问的,除非端口暴露在外面,否则我们无法进入container。因此我们在这里做的就是将容器端口5000映射到主机端口5000,实现这一点的操作是“-p”。通过将容器内部的端口5000映射到主机的端口5000,这样我们可以通过主机的端口5000访问容器内运行的应用程序。

在这个例子中,nodejs应用程序在容器中运行在端口5000上,而我们希望从主机的端口5000访问它,所以使用“-p 5000:5000”,前边是本地端口,后边是容器端口。

 

三、容器运行

可以看到不同版本的release,hub.docker.com是一个registry,“in28min/hello-world-rest-api”是一个repository,“0.0.1.RELEASE”是tag。

现在,如果我们希望能够同时运行三个程序,python、Java和javascript,如何做到这一点呢?

方法就是打开另一个终端窗口,可以看到运行失败,因为nodejs已经把5000端口占据了。

 我们需要改变端口,使用5001来运行,而不是5000,下边命令将容器内的5000映射到主机的5001端口。

docker run -p 5001:5000 in28min/hello-world-python:0.0.1.RELEASE

 

localhost: 通常指向本地计算机的回送地址,即 127.0.0.1。这个地址是一个特殊的地址,用于本地机器上的网络测试和通信。当应用程序在 localhost 上绑定端口时,它仅监听来自本地机器的连接。

 

 

0.0.0.0: 这是一个特殊的地址,表示监听所有网络接口上的连接,而不仅仅是本地机器。当应用程序在 0.0.0.0 上绑定端口时,它将接受来自任何网络接口的连接,包括本地网络和外部网络。

 

在 Docker 中,当容器内的应用程序绑定到 0.0.0.0 时,它可以接受来自容器外部的连接。因此,当你在 Docker 中使用 -p 5000:5000 将容器的端口映射到主机的端口时,实际上是在容器内部的应用程序上使用了 0.0.0.0 地址,使得它可以接受来自任何网络接口的连接,包括主机的网络接口。

可以看到python程序已经在5001端口上运行了。同样的操作我们可以运行Java程序,重新打开一个终端窗口,但是要使用5002端口。

docker run -p 5002:5000 in28min/hello-world-java:0.0.1.RELEASE

去浏览器可以看到java程序已经运行了。

现在我们在三个不同的端口上运行了三个不同的应用程序,我们能够快速、轻松地启动它们,我们要做的就是执行三个简单的命令。更有趣的是,我们可以运行同一个应用程序的多个实例,如果需要的话,我们可以在端口5003上启动另一个python程序实例。

docker run -p 5003:5000 in28min/hello-world-python:0.0.1.RELEASE

这里我们可以看到python程序已经成功运行了,container使用十分灵活。现在我们使用单独的终端窗口来启动每一个容器,但是这存在一个问题,过一段时间后,在单独的窗口中启动所有的内容会变得非常麻烦,我们不知道从哪个窗口启动了哪个容器。那么如何从同一个窗口中启动所有的容器呢,下一步我们将着手这个问题。

 

Command used in this section

https://github.com/in28minutes/devops-master-class/tree/master/docker#commands

 

四、分离模式运行容器

接下来让我们关闭一些窗口,使用“Ctrl+C”终止程序。现在我们只保留nodejs的窗口,但是同样执行“Ctrl+C”操作,现在我们没有任何正在运行的容器。现在我们希望从同一个窗口中启动所有这些容器,我们可以注意到,容器已经绑定到了终端,因此,我们可以直接在终端中查看程序的所有日志。我们要做的是在分离模式下启动容器,不把terminal和container绑定在一起,如何做到这一点呢?我们可以通过添加一个名为“-d”的选项来实现(detached)。

docker run -d -p 5000:5000 in28min/hello-world-nodejs:0.0.1.RELEASE

此时我们就无法查看这个容器的日志了,打开浏览器可以看到程序已经成功运行。

此时我们已经成功地在分离模式下启动容器,如果我们想启动python应用程序,我们不需要再打开新的窗口,而是直接在现有窗口输入以下命令:

docker run -d -p 5001:5000 in28min/hello-world-python:0.0.1.RELEASE

此时去浏览器刷新可以看到程序已经成功运行。

在detached模式下运行的伟大之处在于,一切都在后台发生,所有的容器都在后台运行,但是我们发现,此时终端内没有类似于之前的运行日志了,如果我们想查看日志,也非常简单。命令之下的一串字符是容器的ID,每个容器在启动时都会分配一个ID,我们可以使用ID查看容器的日志。

此时我们就可以看到刚才两个容器的日志了,此外,docker有一个很有趣的地方,就是我们不需要输入完整的ID,只需要输入一些子字符串即可。

如图所示,我们同样可以查到容器的日志。如果我们想持续查看特定应用程序的日志怎么办,可以通过“-f”实现:

此时如果我们刷新网页,日志就会持续更新。此时日志是和terminal绑定的,此时我们执行“Ctrl+C”,就可以断开与日志的连接,程序依旧正常运行。

 

五、Container运行命令

接下来,我们将使用几个命令,查看有哪些容器在运行,以及本地有哪些image。小tips,可以使用“clear”来清理终端界面。

在终端运行:

docker images

可以看到本地的images,当我们运行之前的命令时,image会被下载到本地:

运行下边的命令查看正在运行的容器,ls是list的缩写:

docker container ls

我们可以看到container的id,用于启动容器的image,启动所用的命令,创建的时间,运行的状态,up表示已经启动并运行,我们还可以看到发布的端口。我们还可以看到容器的name,当我们运行命令时,我们没有真正为容器分配一个默认的名称,所以docker会为每一个容器分配一个默认的名称。

ls只显示当前在运行的容器,那么如果我想查看所有的容器呢,包括之前停止运行的容器,我们可以使用下边的命令:

docker container ls -a

现在,让我们运行一下docker container ls来查看一下正在运行的容器。

如果我们想要停止一个容器,应该如何操作呢?在这里我们只能看到ID的一个子串,之前我们看到的ID是一个很长的字符串。现在我们可以使用图上给的ID子串,或者仍然使用前几个字符,运行如下命令停止第一个容器:

docker container stop 238f

然后我们查看现在正在运行的容器:

可以看到被停止的容器已经不在活动列表中了,接着我们去对应的端口进行查看:

已经是无法访问的状态了,说明我们已经成功停止了该容器。

 

六、Docker体系结构

接下来我们了解一下Docker的体系结构,当我们安装docker desktop时,我们实际上安装了两个东西,一个是Docker Client(客户端),另一个是Docker Daemon(守护程序)。即使是Docker的本地安装也使用类似于client-server体系结构的东西,Docker客户端是客户端,Docker守护程序类似于服务器。我们在Docker Client中运行命令,Docker client将它们发送到Docker守护进程进行执行。

比如当我们执行一个命令Docker run image,Docker client所做的就是把这个命令发送给Docker Daemon,Docker daemon是真正负责执行该特定命令的守护进程。Docker Daemon负责很多事情,负责管理容器、管理本地image,主要负责从image repository中提取image。如果我们在本地计算机创建一些Docker image,Docker daemon还负责将这些映像push到image repository。

当我们运行“docker images”时,会发生什么事?

Docker client将命令发送到Docker Daemon,Docker daemon查看存在的本地image,并将结果发送回来,也就是我们显示在这里的结果:

当我们运行“Docker container ls”时也是如此,我们在Docker客户端上执行命令,Docker client将其发送到Docker Daemon,然后返回当前正在运行的容器列表。如果我们加上“-a”,Docker Daemon就会返回所有状态的容器列表。

当我们运行容器时,也会在Docker client和Docker Daemon之间发生大量通信。我们看几个例子,之前我们运行程序的命令如下:

docker run -p 5000:5000 in28min/hello-world-nodejs:0.0.1.RELEASE    

运行这个程序之后,我们可以看到容器启动,这实际上是Docker client将命令行发送到Docker Daemon,然后Docker Daemon在本地查看image是否可用,做一些类似“Docker images”的动作来查看所需的image是否可用,可以看到对应的image在本地是可用的,所以它不会进入Docker registry,而是直接使用现有的本地image。但是,如果实际上要运行一个不在本地的应用程序,Docker Daemon在收到命令后,会发现image在本地不存在,然后与Docker registry进行通信,尝试下载image,然后尝试运行。

现在让我们通过日志观察一下应用程序是否已经启动:

我们可以看到程序已经成功启动,接下来去网页查看程序是否成功运行,结果我们发现程序没有正常运行,这是为什么呢?

 

原因可以从日志中看出,程序没有在5001端口运行,而是在8080端口,让我们查看一下container的运行状态:

可以看到rest api正在运行,让我们停止这个container,尝试重新启动它,此时我们把容器的端口改成8080:

程序成功运行,所以,无论何时运行容器,都需要知道容器在哪个端口上运行。并且我们可以发现,当我们再次进行docker run时,image没有被再次下载,这是因为image在本地已经下载过了,Docker Daemon知道image在本地可用,不会去Docker hub中下载,而是直接运行。

 

七、Docker流行的原因

让我们一起看看为什么Docker这么受欢迎,第一个原因是标准化的应用程序打包,第二个原因是多平台支持,第三个非常重要的原因是Docker容器是轻量级的并且彼此相互隔离。

现在我们将详细讨论其中的每一项,从第一个原因开始,即标准化应用程序打包。Docker的有趣之处在于应用程序打包是标准化的.无论是构建Java程序还是其他语言的程序,都将构建Docker映像,而Docker映像将包含运行应用程序所需的所有内容,这确保我们可以在任何地方运行此映像。我们不需要担心这个image中有什么,一旦有了这个image,我们就可以在任何地方运行,这就是多平台支持的用武之地。一旦安装了Docker engine,我们就可以在本地计算机或者数据中心,或者云中运行Docker image。每个云提供商都对Docker提供了惊人的支持,例如AWS,Azure,Google cloud等,这些都可以运行容器。它们还提供了Kubernetes实现来进行容器编排和运行。

因此,需要了解的一点是,构建Docker image后,无论是针对什么类型的程序,我们都可以在云、数据中心或者本地计算机中运行该image。

最后一个原因是Docker container是轻量级的,并且相互隔离。这里的轻量级是什么意思?在Docker之前,虚拟化的方式是使用虚拟机。

上图是一个典型的虚拟机架构,有了虚拟机,我们就有了hardware、HostOS,接着我们有了一个称为Hypervisor的东西来引入virtualization。再上层是虚拟机安装的地方,每个虚拟机都有一个GuestOS。所以在虚拟机体系结构中,有两个操作系统,一个是GuestOS,一个是HostOS,因此虚拟机是Heavyweight的。在进行虚拟化的时候,我们无法真正利用硬件的全部功能,这就是Docker的用武之地。

在使用Docker时,运行container只需要一个Docker engine,一旦安装了这个engine,就可以在其上运行任何类型的容器,image包含运行container所需的所有内容,包含应用程序和他们需要的所有软件,因此实际上我们不需要其他任何东西。因此,Docker container是轻量级的,Docker的整个体系结构非常高效。Docker容器不仅是轻量级的,而且彼此之间也是相互隔离的。我们可以为每个容器分配特定的内存和特定数量的CPU,比如我们给container分配20%的CPU,则它在任何时间点都不能使用超过20%的CPU,类似的我们可以对内存设置类似的限制。并且,Docker engine可以确保一个容器的故障不会真正影响任何其他容器,这确保一个容器的问题不会蔓延到其他容器,环境的其余部分保持稳定。