docker使用

1.介绍和安装

安装:参考菜鸟教程的手动安装进行安装。菜鸟教程

docker的目的:代码下载下来以后,要配置代码所需的依赖,这很麻烦。所以将代码和代码所需的依赖包装在一个称作为镜像的东西里面,然后我们直接将镜像下载下来,运行这个镜像就相对于运行了代码。

  • 镜像(Image):Docker 镜像(Image),就相当于是一个 root 文件系统,文件系统中存储有代码和代码所需依赖。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。
  • 容器(Container):镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

看一下是不是文件系统:

sudo docker run -i -t ubuntu:20.04 /bin/bash

结果:

参数:

  • -t: 在新容器内指定一个伪终端或终端。
  • -i: 允许你对容器内的标准输入 (STDIN) 进行交互。
    /bin/bash:代表在“root@712f2e8b8439:/#”下输入的脚本,使用/bin/bash来解释执行。

docker 命令每次都需要sudo解决方案

2.基础

2.1 使用docker部署一个简单的c/c++程序

请直接参考:使用docker部署一个简单的c/c++程序

说明:
本博客中使用的镜像codenvy/cpp_gcc应该就是在ubuntu镜像中的相关目录下添加了g++相关的配置环境而已。

查看镜像和容器:

docker images   # 列出镜像列表
docker ps -a    # 查看正在运行和已经停止的容器

删除镜像和容器:

docker rm -f 容器ID或名字   # 删除容器
docker rmi 镜像ID或名字     # 删除镜像

上传镜像到远程:

docker login    # 登录
docker push REPOSITORY名称或IMAGE ID  # 上传
# 拉取远程镜像

2.2 快速入门

请直接参考:
docker快速入门
docker快速入门视频教程

总结如下:

  • 端口映射,让外部可以使用到容器中的服务,如docker run -p 8090:8080 --name test-hello test:v1,其中8090为宿主机端口,8080为容器端口。
  • 目录挂载:1.目录下修改了代码,容器中的代码也相应改变 2.容器里面产生的数据,在容器销毁后,还存在于宿主机中。下面是两种挂载方式。
    • bind mount 方式用绝对路径-v D:/code:/app
    • volume 方式,只需要一个名字,如-v db-data:/app,那么宿主机的/var/lib/docker/volumes/db-data/_data与容器的/app相对应。如果直接使用-v /app,而不取名字,那么宿主机的/var/lib/docker/volumes/xxxx/_data与容器的/app相对应,其中xxxx是一串系统随机分配的字符串。参考:Docker第七篇-Docker挂载Volume数据卷
  • 多容器通信:
    • 创建一个网络,然后将所有容器都放在这个网络下。如docker network create test-net
    • Docker-Compose:同时运行多个服务,所有服务在同一个网络下。不然可能需要配置网络才能让多个服务之间进行通信。(菜鸟教程讲的比较全)。
  • 备份和迁移数据:bind mount直接把宿主机的目录挂进去容器,那迁移数据很方便,直接复制目录就好了。用volume方式挂载的,由于数据是由容器创建和管理的,需要用特殊的方式把数据弄出来。备份volume方式挂载的数据(参考:Docker第七篇-Docker挂载Volume数据卷):
    • 导出:开启一个新容器,将挂载需要备份的 volume 到新容器的备份目录,并挂载宿主机目录到新容器的备份目录。这样就将需要备份的 volume备份到主机目录中。注意:一般会运行 tar 命令把待备份文件压缩为一个文件。
    • 导入:运行容器时,就可以导入压缩文件到容器中。

3.零基础手写一个 Docker(未完)

总结:零基础手写一个 Docker,如下:

3.1 chroot

chroot的作用是:改变进程的根目录,使它不能访问该目录之外的其它文件。进程运行时使用的根目录,叫rootfs (根文件系统)。在linux系统中,进程运行时默认使用的rootfs就是linux的文件系统,即进程运行时,默认从linux的文件系统中查找程序运行的相关依赖。chroot的作用就是改变进程使用的rootfs。
【注】所以我们可以看出chroot只是改变进程使用的根目录,chroot中的进程还是和主机共享cpu等资源。

chroot NEWROOT [COMMAND [ARCG]..]

  • NEWROOT:表示要切换到的新root目录
  • COMMAND:指的是切换root目录后需要执行的命令

复制linux的ls和bash到chroot指定的文件系统中进行使用:

mkdir rootfs_dir_1
cd rootfs_dir_1

# 定位可执行文件ls和bash所在目录
which ls    
/usr/bin/ls
which bash
/usr/bin/bash

# 复制linux的ls和bash到chroot指定的文件系统中
mkdir bin
cp /usr/bin/ls bin
cp /usr/bin/bash bin

# 进入文件系统rootfs_dir_1,并执行/bing/ls
sudo chroot ../rootfs_dir_1 /bin/ls
chroot: failed to run command ‘/bin/ls’: No such file or directory # 找不到执行ls所需要的依赖库,所以报错


file bin/ls  # 查看ls的信息
bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2f15ad836be3339dec0e2e6a3c637e08e48aacbd, for GNU/Linux 3.2.0, stripped
# 可以看到ls是动态链接的执行程序
# bash同理,也是动态链接的执行程序


# 查看ls和bash所依赖的动态库:
ldd /bin/ls
        linux-vdso.so.1 (0x00007fff9ffa5000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007faf2031a000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007faf20128000)
        libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007faf20098000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007faf20092000)
        /lib64/ld-linux-x86-64.so.2 (0x00007faf2037e000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007faf2006f000)

ldd /bin/bash
        linux-vdso.so.1 (0x00007ffd9cde5000)
        libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f001de6b000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f001de65000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f001dc73000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f001dfdb000)

mkdir lib
mkdir lib64

编写cp.sh脚本将依赖库复制到rootfs_dir_1的相应目录下,cp.sh内容如下:

cp /lib/x86_64-linux-gnu/libselinux.so.1 /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libpcre2-8.so.0  /lib/x86_64-linux-gnu/libdl.so.2  /lib/x86_64-linux-gnu/libpthread.so.0 lib/
cp /lib64/ld-linux-x86-64.so.2 lib64/

cp  /lib/x86_64-linux-gnu/libtinfo.so.6 /lib/x86_64-linux-gnu/libdl.so.2  /lib/x86_64-linux-gnu/libc.so.6 lib/

cp /lib64/ld-linux-x86-64.so.2 lib64/

运行cp.sh

sh cp.sh

重新运行/bin/ls和/bin/bash,运行成功,如下:

sudo chroot ../rootfs_dir_1 /bin/ls
bin  cp.sh  lib  lib64

sudo chroot ../rootfs_dir_1 /bin/bash   # 开启一个shell
bash-5.0# 
bash-5.0# ls
bin  cp.sh  lib  lib64
bash-5.0# exit   # 退出

进入bash-5.0#之后运行的ls,指是rootfs_dir_1/bin/ls,即使用bash进入shell以后,然后输入ls,此时shell就会在当前文件系统的bin目录(rootfs_dir_1/bin)中查找可以执行文件ls。

运行sudo chroot ../rootfs_dir_1 ,会自动查找rootfs_dir_1/bin目录下是否有bash可执行文件,用于开启一个shell,如下:

sudo chroot ../rootfs_dir_1  # 开启一个shell
bash-5.0# 

sudo chroot ../rootfs_dir_1 可执行文件A:这样的程序只会在执行完,可执行文件A后,就退出了文件系统rootfs_dir_1。
rootfs_dir_1被当成了根目录,然后 “可执行文件A”代表执行根目录下的可执行文件A。

3.2 busybox

3.2.1 busybox简介与安装

busybox是一个集成了一百多个最常用linux命令和工具的软件,甚至还集成了一个http服务器和一个telnet服务器,而所有这一切功能却只有1M左右的大小适用于

  • 嵌入式设备
  • 移动设备(安卓)
  • 超小的linux发行版(alpine linux)
# 下载
wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2
tar -xvf busybox-1.36.1.tar.bz2
cd busybox-1.36.1/

# 安装依赖
sudo apt-get install build-essential 
sudo apt-get install libncurses5 
sudo apt-get install libncurses5-dev 

# 设置与编译
make menuconfig  
 选择安装路径
 Busybox使用静态库,而不是动态库  Build Busybox as a static binary (no shared libs)


make -j4
make install

# 执行
busybox ls  # 执行busybox中的ls命令
sudo chroot ../my_docker/ /bin/ls # my_docker为busybox的安装目录


# 此时不能使用ps -ef,还需要挂载proc
sudo chroot ../my_docker/ /bin/sh 
mkdir /proc
# 第一个proc代表文件类型,第二个proc也是指proc文件系统,/proc代表挂载点。
mount -t proc proc /proc 
ps -ef  # 得到的是宿主机上的进程信息,并没有做隔离。下面会介绍namespace来做隔离。

【注】虽然 /proc 看起来像一个目录,但它实际上是一个在内存中动态生成的虚拟文件系统,它不占用磁盘空间,而是通过内核将信息暴露给用户空间的。

3.2.2 busybox安装(从busybox的docker镜像中提取)

3.3 namespace创建资源隔离层

Linux namespace的主要作用是做了一层资源隔离,使得进程拥有独立的进程号、端口、网口等。下面是常见的namespace:

Linux用于管理namespace的API

  • clone():创建新的namespace并且将子进程添加为其成员。
  • setns():将进程加入一个已存在的namespace中。
  • unshare():让当前的进程移动至一个新的namespace中

使用unshare() API来进行namespace的隔离,示例如下:
sudo unshare --pid --mount-proc --fork /bin/sh的含义:这个命令首先当然是开启一个unshare进程,--pid --mount-proc --fork会fork出一个子进程,这个子进程拥有独立与主进程(unshare进程)的pid,并且会自动挂载/proc文件系统。/bin/sh代表使用/bin/sh替换掉正在运行的子进程,/bin/sh进程会拥有子进程的pid和/proc文件系统。

clone():创建新的namespace并且创建一个子进程,将此子进程添加到此namespace中。

int clone(int (*fn)(void *),
          void *child_stack, // 生成的子进程的栈空间(不懂)
          flags,        // 指定子进程需要生成的namespace,即指定子进程有哪些资源是独立的
          void *arg,... // 函数fn的参数
          );

如:
process_pid child_pid = \
clone(setup,
      child_stack + STACK_SIZE,
      SIGCHLD | CLONE_NEWUTS | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWNET,
      this);  // CLONE_NEWUTS...在前面的namespace表中可以看到说明

在子进程中通过execv运行一个新的程序:

int execv (const char *path, char *const argv[]);
- 第一个参数path表示接下来要执行的新程序的路径
- argv表示传递给新进程的命令行参数
比如sudo unshare --pid --mount-proc --fork /bin/sh中的/bin/sh应该就是通过execv替换原来的进程。

实例

chroot的代码实现:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>


static void die(const char* error_msg){
    perror(error_msg);
    exit(1);
}

void set_rootdir(char *root_dir){
    chdir(root_dir);
    chroot(".");
}


void start_cmd(char *cmd){
    char *const child_args[] = {cmd,NULL};
    execv(cmd,child_args);
}

int main(){
    char *cmd ="/bin/sh";
    char *root_dir = "/home/ubuntu/projects/docker/my_docker";
    
    set_rootdir(root_dir); // 改变当前进程的根目录
    start_cmd(cmd);        // /bin/sh替换当前进程
    return 0;
}

创建一个namespace隔离宿主机的pid:

// test.cc
#define _GNU_SOURCE   /*注意摆放位置*/         
#include <sched.h>   /*注意摆放位置*/   
#include<sys/types.h>
#include<sys/syscall.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
#include <sys/wait.h>

#define STACK_SIZE (1024*1024)
char child_stack[STACK_SIZE];

static void die(const char* error_msg){
    perror(error_msg);
    exit(1);
}

void set_rootdir(char *root_dir){
    chdir(root_dir);
    chroot(".");
}


void start_cmd(char *cmd){
    char *const child_args[] = {cmd,NULL};
    execv(cmd,child_args);
}

int setup(void *arg){
    char *cmd ="/bin/sh";
    char *root_dir = "/home/ubuntu/projects/docker/my_docker";
    set_rootdir(root_dir); // 改变当前进程的根目录
    start_cmd(cmd);        // /bin/sh替换当前进程
    return 0;
}

int main(){

    pid_t child_pid = clone(setup, \
                child_stack + STACK_SIZE, \
                SIGCHLD  |CLONE_NEWPID , \
                NULL);  // CLONE_NEWPID ...在前面的namespace表中可以看到说明
    if(child_pid<0){
        perror("error");
        return 1;
    }

    waitpid(child_pid,NULL,0);

    return 0;
}
g++ test.cc -o test
sudo ./test
mount -t proc proc /proc 
ps -ef       # 下面输出中,没有宿主机的进程信息,说明隔离成功
PID   USER     TIME  COMMAND
    1 0         0:00 /bin/sh
    4 0         0:00 ps -ef

3.4 CGroup——限制资源的使用

CGroup用于限制资源的使用,比如限制cpu的占用量只能是50%等,其他可以限制的资源,如:CPU、内存、I0、网络。通过操作/sys/fs/cgroup目录下的文件来限制进程资源的使用。可以在/sys/fs/cgroup下创建目录,目录下还可以创建目录...,这样就形成了一颗树。

3.3 容器网络原理

veth设备:veth全称是(Virtual Ethernet)虚拟以太网,相当于一条虚拟的大网线。它模拟了在物理世界里的两块网 卡,以及一条网线。通过它可以 将两个虚拟的设备连接起来,发 送到veth一段虚拟设备的请求会 送另外一端设备中发出。故veth总是成双成对地出现。

bridge网络原理:bridge相当于一个虚拟机交换机。

创建-一个veth对veth1、veth2

sudo ip link add veth1 type veth peer name veth2 # 增加一对veth设备
sudo ip addr add 192.168.1.1/24 dev veth1  # 分配网络IP
sudo ip addr add 192.168.1.2/24 dev veth2
sudo ip link set veth1 up # 
sudo ip link set veth2 up

模拟两台容器通过bridge通信

模拟示意图:

创建一个新的名为net1的netnamespace,并将veth1加入到net1中

# 创建一个名为net1 的net namespace
sudo ip netns add net1
# 新建veth对veth1-veth2
sudo ip link add veth1 type veth peer name veth2
# 把veth1放到名为net1的netnamespace.
sudo ip link set veth1 netns net1
#给veth1配置ip并启用
sudo ip netns exec net1 ip addr add 192.168.0.101/24 dev veth1
sudo ip netns exec net1 ip link set veth1 up
#查看veth1 是否已经正常
sudo ip netns exec net1 ifconfig

创建一个新的名为net2的netnamespace,并将veth3加入到net2中

sudo ip netns add net2
sudo ip link add veth3 type veth peer name veth4
sudo ip link set veth3 netns net2
sudo ip netns exec net2 ip addr add 192.168.0.102/24 dev veth3
sudo ip netns exec net2 ip link set veth3 up
sudo ip netns exec net2 ifconfig

将veth2和veth4加入到bridge中

sudo brctl addbr br0 # 创建网桥
sudo ip link set dev veth2 master br0 # 将vecth2加入网桥
sudo ip link set dev veth4 master br0
sudo ip addr add 192.168.0.100/24 dev br0
sudo ip link set veth2 up
sudo ip link set veth4 up
sudo ip link set br0 up

# net1中的veth1 ping 192.168.0.102 (我的实验ping不通。。)
sudo ip netns exec net1 ping 192.168.0.102 -I veth1

容器访问外网(上面ping不通,这部分没看):。。。。

分层镜像

Docker镜像分层

  • 镜像:Docker镜像由Dockerfile构建,Dockerfile中的每条指令都对应Docker镜像中的一层。使用docker pull拉取镜像时,就可以看出镜像是一层一层被拉去下来的。
    所有层都放在/var/lib/docker/overlay2中,当docker pull其他镜像时,如果某层可以在/var/lib/docker/overlay2找到,就不用重新下载。
  • 容器:使用镜像A创建容器时会创建一个目录diff,对容器的所做的所有更改都将放入到diff中。使用docker commit提交当前容器为镜像时,会将diff目录中所有更改作为一层放到原始镜像A中。
    如果不保存容器为镜像,那么删除容器后,diff也会被删除,而基础镜像则保持不变。

分层镜像的好处:

  • 分层镜像可以使得多个容器可以共享对同一基础镜像的访问,但可以拥有自己的数据状态。
  • 分层镜像使镜像的每一层都是可独立管理的,更容易更新、更改或替换特定层。

分层镜像原理

容器底层-UnionFS 工作原理-AUFS 和 Docker 实现:较新的docker采用overlay2实现镜像分层,所有层都放在/var/lib/docker/overlay2中。通过docker inspect ubuntu可以看到RootFS:Layers,它描述了哪些层来构建镜像ubuntu。

  • 启动容器:每一层中都有一个diff目录,它存放着当前镜像层具体的文件。当使用镜像启动一个容器时,会在/var/lib/docker/overlay2生成两个文件,即xxx...xxx...-init
    • xxx...-init的内容如下所示,diff/etc 子目录包含了 hostname、hosts、resolv.conf 等文件。这些修改往往只对当前的容器有效,在docker commit 之后,这些信息不会跟可读写层一起提交掉。
    • xxx.../merged:镜像的所有层中的diff下的文件都会挂载到xxx.../merged中,而容器中的修改都会保存在xxx.../diff中,当然此修改也会出现xxx.../merged中。

【注】可以发现RootFS:Layers中使用的是sha256不能直接映射到 /var/lib/docker/overlay2目录下的目录名的,这之间有一个比较复杂的映射关系。这里不说明,反正你知道RootFS:Layers的每一层都对应/var/lib/docker/overlay2下的某个目录就行(参考:Docker 镜像分层机制与 AUFS(Advanced Union File System))。

4.八股

什么是 Docker 容器:通过改变了进程的根目录并进行资源隔离和资源限制等操作,相当于封装了整个进程依赖的环境。

Docker 和虚拟机有什么不同?
Docker只是改变了进程的根目录并进行资源隔离和资源限制等操作,容器共享主机操作系统的内核。更少的存储空间、更高的性能。
虚拟机都有自己的操作系统内核和用户空间。

什么是 DockerFile:用于构建镜像,每一条命令都对应镜像中的一层。

使用Docker Compose时如何保证容器A先于容器B运行:Docker Compose用于同时运行多个服务,可以使用depends_on来控制执行顺序。

docker容器之间怎么隔离:chroot改变进程运行的目录、Namespace隔离了主机的PID和网络等、CGroup限制CPU、内存、I0、网络等使用,比如限制cpu使用率只能在50%。

浅析docker容器网桥的实现原理以及docker的四种网络模式和bridge模式的具体原理

posted @ 2022-07-29 22:06  好人~  阅读(137)  评论(0编辑  收藏  举报