如何用100行实现Docker?

Posted K8S中文社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何用100行实现Docker?相关的知识,希望对你有一定的参考价值。

最近逛github无意发现了一个很好地项目bocker, 用上百行的代码就实现了一个简易的docker,然后我看了一下,觉得挺有趣的,简单的玩了一下,也做一些更改(项目很久不更新了,有不支持的地方),简单分析了一下分享出来。

前言

我当时一看100行写docker, 肯定是不可能,以前看像最简化的python加上依赖也得几百行代码如moker,还有go实现的完善一点的也有上千行mydocker,可是这个项目看了一下,还真是只有100多行,不过看使用的是shell, 不过想起来100多行应该也只能用shell完成了吧,不熟悉shell的可以去看一些shell的基本知识就可以了。

目前这个项目主要实现里镜像拉取,镜像查看,容器启动,容器删除,容器查看,容器资源限制,镜像删除,功能都是一些最基本的,也有很多不完善的,我这里大致分析一下他们是的实现原理,分析各个流程,按照操作的顺序正常分析,首先这里讨论的情况是linux环境,推荐使用centos7和ubuntu14以上的系统,流程其实比较简单,底层实现依赖于linux的一些基础组件iptables,cgroup和linux namespace完成网络,资源限制,资源隔离,利用shell去管理这些资源。

开始操作!!

配置环境

官方Vagrantfile的epel数据源有问题,而且网络依赖,整个过程是自动化的,不过不方便调试,这里为了方便个人调试,我将流程写为一步一步的了,操作起来也会比较方便。

加载虚拟环境(vagrant配置文件)

生成Vagrant配置文件

Vagrant配置启动

$script = <<SCRIPT ( echo "echo start---config" ) 2>&1 SCRIPT Vagrant.configure(2) do |config| config.vm.box = 'puppetlabs/centos-7.0-64-nocm' config.ssh.username = 'root' config.ssh.password = 'puppet' config.ssh.insert_key = 'true' config.vm.provision 'shell', inline: $script end 拷贝上边的文件Vim为保存到一个文件中Vagrantfile中 vagrant up (直接启动,这里会去源拉去centos的镜像,时长主要根据个人网络) vagrant ssh (直接进入)

安装依赖

  • 安装rpm源:

wget https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm rpm -ivh epel-release-latest-7.noarch.rpm(官方用的eprl源不存在了)
  • 然后对应依赖:

核心是cgourp, btrfs-progs

yum install -y -q autoconf automake btrfs-progs docker gettext-devel git libcgroup-tools libtool python-pip jq
  • 创建挂载文件系统:(docker镜像支持的一种文件结构) 具体细节可以看链接

https://btrfs.wiki.kernel.org/index.php/Main_Page


fallocate -l 10G ~/btrfs.img mkdir /var/bocker mkfs.btrfs ~/btrfs.img mount -o loop ~/btrfs.img /var/bocker
  • 安装base:

pip install git+https://github.com/larsks/undocker systemctl start docker.service docker pull centos docker save centos | undocker -o base-image
  • 安装linux-utils 一个linux的工具

git clone https://github.com/karelzak/util-linux.git cd util-linux git checkout tags/v2.25.2 ./autogen.sh ./configure --without-ncurses --without-python make mv unshare /usr/bin/unshare
  • 配置网卡和网络转发

echo 1 > /proc/sys/net/ipv4/ip_forward iptables --flush iptables -t nat -A POSTROUTING -o bridge0 -j MASQUERADE iptables -t nat -A POSTROUTING -o enp0s3 -j MASQUERADE ip link add bridge0 type bridge ip addr add 10.0.0.1/24 dev bridge0 ip link set bridge0 up

我简单解释一下上边的流程,由于docker底层网络会利用iptables和linux namespace实现,这里是为了让容器网络正常工作,主要分为2部分。

环境准备完成,可以分析下具体实现了

首先想一下,对docker来讲最重要的就是几部分,一个是镜像,第二个是独立的环境,ip,网络,第三个是资源限制

这里我在代码中增加了一些中文注释方便理解,这个项目叫bocker,我也叫bocker吧

  • 程序入库口

[[ -z "${1-}" ]] && bocker_help "$0"    # @1 执行与help case $1 in    pull|init|rm|images|ps|run|exec|logs|commit|cleanup) bocker_"$1" "${@:2}" ;;    *) bocker_help "$0" ;; esac

help比较简单,程序入口,逻辑相当于是我们程序里面的main函数,根据传入的参数执行不同的函数。

  • 运行环境? 镜像拉去 bocker pull ()

function bocker_pull() { #HELP Pull an image from Docker Hub:
BOCKER pull <name> <tag>    # @1 获取对应镜像进行拉去, 源代码老版本是v1的docker registry是无效的, 我更新为了v2版本    token=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/$1:pull"  | jq '.token'| sed 's/"//g')    registry_base='https://registry-1.docker.io/v2'    tmp_uuid="$(uuidgen)" && mkdir /tmp/"$tmp_uuid"    # @2 获取docker镜像每一层的layter,保存到数组中    manifest=$(curl -sL -H "Authorization: Bearer $token" "$registry_base/library/$1/manifests/$2" | jq -r '.fsLayers' | jq -r '.[].blobSum' )    [[ "${#manifest[@]}" -lt 1 ]] && echo "No image named '$1:$2' exists" && exit 1    # @3 依次获取镜像每一层, 然后init    for id in ${manifest[@]}; do        curl -#L -H "Authorization: Bearer $token" "$registry_base/library/$1/blobs/$id" -o /tmp/"$tmp_uuid"/layer.tar        tar xf /tmp/"$tmp_uuid"/layer.tar -C /tmp/"$tmp_uuid"    done    echo "$1:$2" > /tmp/"$tmp_uuid"/img.source
    bocker_init /tmp/"$tmp_uuid" && rm -rf /tmp/"$tmp_uuid" }

这个项目简易的实现了docker,所以docker镜像仓库肯定是没有实现的,镜像仓库还是使用官方源,这里如果需要使用自己私有源,需要对镜像源和代码都做变更,这里其实逻辑是下载对应镜像每个分层,然后转存到自己的文件镜像存储中,这里我更改了他的逻辑,使用了docker registry api v2版本,(因为作者源v1版本代码已经失效,从官方不能获取正确数据,作者其实已经三年未提交了,docker发展速度太快,也可以理解),流程是首先是auth,获取对应镜像对应权限的进行一个token,然后利用token获取到镜像的每一个layer,这里我用了jq json解析插件,会比较方便的操作Jason,转为shell相关变量,然后下载所有的layer转存到自己的唯一镜像目录中,同时保存一个镜像名为一个文件

  • bocker保存镜像

function bocker_init() { #HELP Create an image from a directory:
BOCKER init # @1 生成随机数镜像,就像生成docker images 唯一id uuid="img_$(shuf -i 42002-42254 -n 1)" if [[ -d "$1" ]]; then    [[ "$(bocker_check "$uuid")" == 0 ]] && bocker_run "$@" # @2 创建对应image文件 btrfs volume    btrfs subvolume create "$btrfs_path/$uuid" > /dev/null    cp -rf --reflink=auto "$1"/* "$btrfs_path/$uuid" > /dev/null    [[ ! -f "$btrfs_path/$uuid"/img.source ]] && echo "$1" > "$btrfs_path/$uuid"/img.source    echo "Created: $uuid" else    echo "No directory named '$1' exists" fi }

这里其实就是保存从镜像仓库拉取下来的layer,然后创建目录,这里需要强调的是docker使用的镜像目录在这里必须是btrfs的文件结构,然后保存对应的镜像名到img.source文件中 ,这里环境准备的时候通过btrfs命令创建了10g的文件系统,docker是支持多种存储系统的,具体详情可以到这里看

Docker storage drivers docs.docker.com如何用100行实现Docker?

  • 有了镜像就可以进行重要的bocker run 了(第一部分)

function bocker_run() { #HELP Create a container:
BOCKER run <image_id> <command>    # @1 环境准备,生成唯一id,检查相关镜像,ip, mac地址    uuid="ps_$(shuf -i 42002-42254 -n 1)"    [[ "$(bocker_check "$1")" == 1 ]] && echo "No image named '$1' exists" && exit 1    [[ "$(bocker_check "$uuid")" == 0 ]] && echo "UUID conflict, retrying..." && bocker_run "$@" && return    cmd="${@:2}" && ip="$(echo "${uuid: -3}" | sed 's/0//g')" && mac="${uuid: -3:1}:${uuid: -2}"    # @2 通过ip link && ip netns 实现隔离的网络namespace与网络通信    ip link add dev veth0_"$uuid" type veth peer name veth1_"$uuid"    ip link set dev veth0_"$uuid" up    ip link set veth0_"$uuid" master bridge0    ip netns add netns_"$uuid"
    ip link set veth1_"$uuid" netns netns_"$uuid"    ip netns exec netns_"$uuid" ip link set dev lo up    ip netns exec netns_"$uuid" ip link set veth1_"$uuid" address 02:42:ac:11:00"$mac"    ip netns exec netns_"$uuid" ip addr add 10.0.0."$ip"/24 dev veth1_"$uuid"    ip netns exec netns_"$uuid" ip link set dev veth1_"$uuid" up    ip netns exec netns_"$uuid" ip route add default via 10.0.0.1    btrfs subvolume snapshot "$btrfs_path/$1" "$btrfs_path/$uuid" > /dev/null

解析:

如何用100行实现Docker?

那么这根网线的两端这里一端是namespace中的设备另外一端则是宿主机,这里结构图解析一下,可以看到docker有个eth0,主机有个veth,他们就是一个veth对。

如何用100行实现Docker?

这样就能让容器里边的bocker正常上网了。

  • bocker run 资源限制(第二部分)

# @3 更改nameserver, 保存cmd    echo 'nameserver 8.8.8.8' > "$btrfs_path/$uuid"/etc/resolv.conf    echo "$cmd" > "$btrfs_path/$uuid/$uuid.cmd"    # @4 通过cgroup-tools工具配置cgroup资源组与调整资源限制    cgcreate -g "$cgroups:/$uuid"    : "${BOCKER_CPU_SHARE:=512}" && cgset -r cpu.shares="$BOCKER_CPU_SHARE" "$uuid"    : "${BOCKER_MEM_LIMIT:=512}" && cgset -r memory.limit_in_bytes="$((BOCKER_MEM_LIMIT * 1000000))" "$uuid"    # @5 执行
    cgexec -g "$cgroups:$uuid"         ip netns exec netns_"$uuid"         unshare -fmuip --mount-proc         chroot "$btrfs_path/$uuid"         /bin/sh -c "/bin/mount -t proc proc /proc && $cmd"         2>&1 | tee "$btrfs_path/$uuid/$uuid.log" || true    ip link del dev veth0_"$uuid"    ip netns del netns_"$uuid"

这里为了简便操作,使用了cgroup工具进行资源限制,cgroup是linux 自带的进程资源限制工具,链接中有对应详情。这里利用了cgroup-tools工具操作cgroup会比较简便,在这里利用cgcreate增加了CPU,set, mem进行限制,通过随机创建的id创建cgroup组,cgset默认增加了CPU, mem的参数限制(如果是程序开发的话会对应的依赖封装库)

下图可以看到其实cgroup对应的数据都是存文件,保存在目录中的。

如何用100行实现Docker?

最后使用 cgroup exec执行启动执行程序,将输出通过tee输出到日志目录。

当程序执行结束,删除对应的网络接口和命名空间,清楚网络接口是为了方便将绑定在主机上的虚拟网卡删除

这里一个bocker run就可以实现了,下边的是一些细节了

  • 清除网络接口

function bocker_cleanup() { #HELP Delete leftovers of improperly shutdown containers:
BOCKER cleanup    # @1 清楚所有的相关网络接口    for ns in $(ip netns show | grep netns_ps_); do [[ ! -d "$btrfs_path/${ns#netns_}" ]] && ip netns del "$ns"; done    for iface in $(ifconfig | grep veth0_ps_ | awk '{ print $1 }'); do [[ ! -d "$btrfs_path/${iface#veth0_}" ]] && ip link del dev "$iface"; done }

ps出相应网卡删除对应的网络接口即可

  • 查看容器日志 bocker logs

function bocker_logs() { #HELP View logs from a container:
BOCKER logs <container_id>    # @1 查看日志    [[ "$(bocker_check "$1")" == 1 ]] && echo "No container named '$1' exists" && exit 1    cat "$btrfs_path/$1/$1.log" }

所有的日志在都是保存在btrfs文件系统对应的子目录中$btrfs_path/$uuid中,这里对应到btrfs_path,所以只需要获取到正确的目录,cat出文件即可

还有几个简单命令我就不分析了,比较简单,可以自己去看开头给的链接,下载源码对应我文中的代码更改。

总结:

整体来说,这个项目利用了shell的优势,实现了一小部分docker的主要功能,框架是有了,还有99%的功能没有实现,比如跨主机通信,端口转发,端口映射,异常处理等等,不过作为学习的项目来说,可以让人眼前一亮,大家也可以根据这个项目的思路去实现一个简单的docker,相信也不会很难。

原文链接:

https://zhuanlan.zhihu.com/p/51010782



推荐阅读


以上是关于如何用100行实现Docker?的主要内容,如果未能解决你的问题,请参考以下文章

如何用ffmpeg截取视频片段&截取时间不准确的坑

如何用30行代码爬取Google Play 100万个App的数据

如何用30行代码爬取Google Play 100万个App的数据

如何用 200 行 JavaScript 代码实现人脸检测?

"如何用70行Java代码实现深度神经网络算法" 的delphi版本

如何用JS代码实现段落换行?