Kube-#01.1 先从Docker 容器开始

Kubernetes上的各个应用都在容器中运行,而大部分容器都使用Docker构建。(其他容器环境还包括Linux Container (LXC) 之类,但是在Kubernetes中运用较少。Docker部分上是基于LXC开发的)

容器和虚拟机的设计目的是差不多:他们都能给其中的应用程序提供一个虚拟的操作系统和运行环境。但在实现方法上两者较为不同。

  • 虚拟机对硬件的模拟程度更深,会给虚拟机分配一定数量的CPU、内存,硬盘资源。有些虚拟机Hypervisor还会虚拟化CPU的型号和指令集以达成更好的可移植性。
    • 因为如此,虚拟机的资源消耗较大,性能损失也较大。与之相对的,虚拟机有着更高的灵活性,可以实现一些底层的操作,包括写CPU寄存器,更改系统内核设置等。
    • 因为有着虚拟显卡,虚拟机可以提供图形界面。
    • 虚拟机可以运行和宿主机不一样的操作系统。例如,在Linux系统中运行的虚拟机可以运行Windows虚拟机。
    • 虚拟机可以运行和宿主机不一样的CPU架构。例如,x86的处理器可以运行ARM的虚拟机。
    • 一般应用场景下,虚拟机内部会运行多个进程,多个软件,甚至可以当作完整的另一台计算机来使用。
  • 容器不虚拟出硬件资源,而是和宿主机共享操作系统内核,包括CPU、内存等。容器在内核之上定义了一个应用层。
    • 容器的资源消耗较小,其消耗的资源基本上等于内部进程消耗的资源。一个容器甚至可以只占几百KB内存。
    • 如果在容器内部查看CPU和内存等系统资源的话,会发现CPU、内存数量等和宿主机是一样的。这是因为CPU数量,内存信息等是通过内核获取的,而容器和宿主机使用同一个内核。
      • 在创建和运行Docker容器时,我们可以对容器可使用的资源数量进行限制,包括CPU时间限制和内存限制。还可以通过宿主机上容器线程的CPU关联性(CPU Affinity) 限制容器只能使用某几个CPU核心。但是因为是同一个内核,/proc/cpuinfo/proc/meminfo 显示的总会是宿主机的CPU和内存数量。
      • CPU限制不一定需要是整数。比如可以限制容器只能使用1.5个CPU。这是利用“CPU时间”计算的。比如一个8核的CPU在1000毫秒内有8000毫秒·CPU单位,而1.5个CPU则代表容器可以使用其中的1500个CPU单位。毫秒·CPU单位可被称为mCPU。例如,一个八核的处理器可以提供8000mCPU。一个容器可以分配到其中的1500mCPU
    • 理论上,容器可以使用图形界面:在Linux环境下,把宿主机的X Server直通给容器,就可以让容器产生图形界面。但是事实上没人这么做。
    • 容器可以使用GPU进行计算:Nvidia提供的Docker CUDA SDK,可以将宿主机上的CUDA驱动传递给Docker容器,容器便可以使用CUDA函数进行GPU计算。
    • 一般一个容器只会运行一个进程(Process),但是这个进程可以有很多的线程(Thread)。虚拟机从外部关闭时,会给内部系统发一个ACPI的关机信号,而Docker从外部关闭时,会给主线程发送SIGTERM
    • 容器不能直接运行在不同的操作系统上。比如,Windows系统无法原生运行Linux的容器。
      • Windows上的Docker环境,Docker Desktop,是先使用WSL2或者Hyper-V创建了一个Linux的虚拟机,再在这个虚拟机里跑容器的。
      • Linux上暂时无法运行Windows容器。主要是因为这需要在Linux上运行一个Windows虚拟机,这会牵涉到授权和证书的问题,而Windows不是免费的,所以没有这方面的工作。
    • 容器不能跑和宿主机不同的CPU架构。

实践 1 总之先整个Docker玩一玩

在进一步了解Docker底层原理之前,不如先整一个来玩一玩

Windows系统

Step 1. 安装WSL 2

Windows系统首先需要安装一个WSL 2 (Windows Subsystem for Linux 2)。如上所述 Linux容器无法直接在Windows下运行。需要先安装WSL,再升级到WSL2。

首先确认你的操作系统版本和系统架构,至少需要 Windows 10 1903 Build 18362,且ARM版本的Windows暂时不支持Docker Desktop。操作系统版本过低的话可以使用升级助手来更新。

如果你不清楚自己的系统是什么架构,可以在命令行中使用 systeminfo | find "System Type" 来确认。大部分的电脑会显示x86-based(32位系统)或x64-based(64位系统)。显示ARM字样的话就是ARM架构。

systeminfo | find “System Type”

然后启动一个管理员权限的命令行,执行这个命令,启用WSL功能。

dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

然后使用这个命令,启用Windows的虚拟机功能。注意:启用这个功能可能会导致VirtualBox等虚拟机无法使用。新版本的VMWare和Hyper-V不受影响。

dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

这时再安装WSL 2 的内核更新包

然后再将WSL的默认版本设为2:

wsl --set-default-version 2

WSL 2 就安装完毕了。

Step 2. 安装Docker Desktop

在这里下载Docker Desktop for Windows: Docker Desktop for Windows by Docker | Docker Hub。点击右侧的 “Get Docker” 即可下载安装包。

(可选)在安装完成后,可以进入Docker Desktop的设置,将Docker Hub源改为中国区的镜像。点击右上角的齿轮,选择Docker Engine,在registry-mirrors中可以加入以下镜像:

  • 有稳定外网连接时,不建议设置额外的镜像源。
  • https://docker.mirrors.ustc.edu.cn :中科大镜像
  • http://hub-mirror.c.163.com:网易镜像。注意这个没有TLS。
  • 也可以去这个地方 获得一个阿里云的镜像源:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors 这个不是镜像地址 不要直接复制!

更改完成后,选择Apply&Restart,重启Docker Engine。

设置镜像源之后(不要抄这张图里的设置 – docker-cn.com 镜像源已经没了)

Linux系统

以下仅提供在常见发行版上安装最新稳定版的操作。具体不同发行版的安装方法可见 Install Docker Engine | Docker Documentation

Debian / Ubuntu

首先添加Docker的GPG公钥和APT源。Docker 的APT源是使用HTTPS的,所以还得安装一些依赖库。

sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

然后更新软件源列表,安装

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

CentOS / RHEL

使用yum-utils添加Docker的yum源,然后直接安装

sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install docker-ce docker-ce-cli containerd.io

Arch Linux

简单暴力的优雅美学

pacman -S docker

Docker镜像的命名规则

Docker镜像的名字包含了两个部分,REPOSITORYTAG。REPOSITORY一般指明了这个镜像的内容,而TAG用来区分不同的版本。TAG省略时默认为latest,代表最新版。REPOSITORY中可能还包含了一个镜像的registry,如果不指定的话则默认为Docker Hub(docker.io)。看下面的几个例子:

  • python:REPOSITORY为python,tag被省略了,默认为latest。通过查阅Docker Hubs的文档,我们可以看到latest在Linux环境下,目前等同于3.9.6-buster(撰写本文时,Python最新版本为3.9.6),即这是一个基于Debian Buster的Python 3.9.6镜像。
  • python:3.9.5-alpine:顾名思义,这是一个基于Alpine Linux的Python 3.9.5的镜像。REPOSITORY为python,TAG部分为3.9.5-alpine
  • mcr.microsoft.com/mssql/server:2019-latest:这个镜像的REPOSITORY部分为mcr.microsoft.com/mssql/server。注意它的REPOSITORY有一个mcr.microsoft.com/的前缀,意味着这个镜像并不处在Docker Hub(docker.io)上,而是处在Microsoft Container Registry (MCR) (mcr.microsoft.com)上。在使用这个镜像的时候,Docker不会访问docker.io,而是会从mcr.microsoft.com下载这个镜像。TAG部分为2019-latest。这个镜像是Microsoft SQL Server 2019。Microsoft SQL Server by Microsoft | Docker Hub。同样,如果你构建了一个镜像,并把它命名为 mcr.microsoft.com/xxxx,那么在使用docker push将镜像发布到registry上的时候,docker命令行软件也不会联系docker.io,而是直接联系mcr.microsoft.com并尝试发布镜像。当然了是push不上去的,需要权限以及安全审查,开源审查之类,这个onboarding烦得要死。

常用的Docker镜像介绍

在构建我们自己的Docker镜像时,需要有一个“基础镜像”作为起点。他们一般提供了一个操作系统或者某个编程语言的运行环境。当然基础镜像也是可以拿来运行的。常见的一些基础镜像如下。

首先介绍一下:C语言有几种基础运行库:glibc,musl和uclibc。glibc是最常用的,功能也最全,大部分Linux大型版都基于它,当然体积也比较大。musl的体积较少,占用资源少,常见于路由器之类的嵌入式Linux设备。uclibc也是一个体积较小的C语言基本库,u代表micro,我没用过。

  • alpine:Alpine Linux,以小而著称。镜像只有5M左右的大小,而且有着包管理器apk (Alpine Package Keeper),可以方便地安装一些库和软件。使用的C语言基本库是musl
  • busybox:Busybox是一套Linux工具箱,是为嵌入式Linux和极少量资源而设计的。它在一个软件里提供了Linux基本所有的基础功能,包括cat, grep等,甚至包括wget。他有使用musl,glibc和uclibc的版本,最小的镜像只有700多KB,使用glibc的也不过2M左右
  • ubuntu:Ubuntu,没什么好说的。
  • debian:Debian,没什么好说的。
  • python:包含了Python,pip之类,有基于Debian, Alpine Linux和Windows Server Core的版本。基本上覆盖了Python 2 – Python 3的所有版本。
    • 注意,如果需要使用numpy,pandas之类的话,不建议选择Alpine Linux的版本。这些库包含C语言的部分在Ubuntu之类平台上有预编译好的版本,而在Alpine上构建的时候,需要下载gcc并重新编译。不仅构建时间长,而且构建出来的镜像可能比Debian版本差不多大,甚至还要大(因为下载了很多native的依赖库比如libgomp之类,而这些依赖库在musl上比在glibc上要大一些)。
  • openjdk:JDK,从Java 6 开始全都有。有基于Debian和Windows Server Core的版本
    • 没有基于Alpine的版本,因为JVM用的是glibc,而且没有musl版。想在Alpine上用的话可以用zulu-openjdk-alpine
    • 一般使用的时候会用jlink构建出一个JRE来再用。openjdk本身镜像接近200M。

跑一个镜像试一试

下面我们可以运行一个镜像,顺便认识一下“容器和宿主机共享内核”的本质。

首先来确认一下系统的内核和CPU数量。在Windows下的话,别忘了Docker Desktop是在WSL 2的虚拟机里跑容器的。先使用WSL进入Docker Desktop跑容器用的WSL 2 虚拟机。docker-desktop是Docker Desktop自动部署的一个WSL 2发行版,用来跑容器,一般不需要自己去访问,但我们可以切一个命令行进去。用Linux的直接跳到下一步

wsl -d docker-desktop

然后我们看一看/proc,确认一下系统的CPU和内存。运行这两个指令(一次复制粘贴一行,分开执行)

cat /proc/cpuinfo | grep processor
cat /proc/meminfo | grep MemTotal
在Windows WSL下的运行结果

可以看到,这个宿主机拥有4核CPU(包括超线程),8148284KB(大概8G)的内存。然后我们运行一个busybox的Docker容器,看一看容器里面看到的系统资源是多少。

如果你是Windows用户的话,现在可以退出这个WSL的命令行了。使用exit退出docker-desktop的命令行。

在主机上执行以下命令。

docker run -it busybox /bin/sh

这个命令指的是,从名为busybox(也就是busybox:latest)的镜像创建一个Docker容器,并给他分配一个TTY(-t选项),将当前终端的输入/输出连接到这个容器(-i,代表交互式,interactive),并在这个容器里运行/bin/sh(也就是打开一个shell)。

镜像是包含自己的启动指令的,比如你构建的镜像,启动指令可能是打开你自己的应用程序。这里加了/bin/sh进行了一个覆盖,也就是指忽略镜像定义的启动指令,而是运行/bin/sh。(虽然busybox镜像本身定义的启动指令就是/bin/sh,但自己指定也有好处 – 知道自己在干什么。)

运行后的结果

然后我们就来到了这个新的shell里。这个shell就是这个容器的终端了。执行uname -a,再执行以下上面两行命令看一看

可以看到,容器内部看到的CPU和内存 与主机没有区别,uname -a显示的也是宿主机的内核名称。现在我们在主机上执行docker ps,查看一下这个容器的信息

在主机上执行docker ps的结果

可以看到,这个容器正在运行,它的ID是bc591211e876,使用的镜像是busybox,执行的指令是/bin/sh,已经启动了三分钟。NAMES下的laughing_wozniak是Docker为这个容器自动生成的一个名字(因为我们在使用docker run的时候没有给容器起名)。

接下来,在容器内部执行hostname,看看容器的主机名

可以看到,容器内部看到的自己的主机名是和容器的ID一样的。Docker自动设置了/etc/hostname文件。

接下来我们试一试限制容器的资源。首先,在容器内执行exit,退出这个容器的命令行。再使用docker rm bc591211e876或者docker rm laughing_wozniak来删除这个容器。

启动一个新的容器,这次加上CPU和内存限制。

docker run --cpuset-cpus "0-1" -m 512M -it busybox /bin/sh

这指的是,容器只能使用CPU的0和1核心,而且内存限制为512M。再在容器里看一看/proc

可以看到,即使已经限制了容器的CPU核心和内存使用,通过/proc看到的CPU和内存信息仍然是和宿主机一样的。这还是因为共享内核。

再试试别的…

下面就可以试试看别的容器镜像了,比如可以使用docker run -it python /bin/sh来启动一个预装了Python运行时的容器,然后试试看pip和python REPL。

关于启动/终止容器,一些常用的Docker命令

注:这里只介绍极少量的命令。今后会介绍更多。

  • docker run:运行容器。可以配置终端、网络端口映射、CPU内存限制等,具体 –help
    • docker run python:启动一个python容器,让它在后台运行,不给它分配TTY。很明显这指令没有什么用。
  • docker ps:查看运行中的容器,列出他们的CONTAINER ID和NAME
  • docker ps -a:查看所有容器,包括运行中的和停止的。
  • docker stop <CONTAINER ID/NAME> :给这个容器中的主进程(启动进程)发送一个SIGTERM信号,让他停止。
  • docker stop <CONTAINER ID/NAME> -f:给这个容器中的主进程发送一个SIGKILL,强制终止。
  • docker rm <CONTAINER ID/NAME>:移除一个容器。这个容器必须已经停止了。
  • docker images:查看本地保存的docker镜像,包括你自己构建的和下载的。
  • docker rmi <IMAGE ID/REPOSITRY:TAG>:删除一个本地的docker镜像。这个镜像必须没有在被任何容器使用。

Leave A Comment