社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  docker

关于如何用100行如何实现docker

爱布偶的zzy • 5 年前 • 303 次点击  
阅读 72

关于如何用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去管理这些资源。

开始操作!!

配置环境

最好是vagrant (如果是mac和windows建议使用该环境,如果linux,系统内核较高则可直接操作), vagrant可以帮我们实现轻量级的开发环境,个人非常喜欢,它操作和管理vm,处理更重环境会比较方便,这里需要提前配置好环境,我在链接中附上了官方地址,按照教程配置即可。

官方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镜像支持的一种文件结构) 具体细节可以看链接btrfs wiki


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部分。

1 首先需要创建一块虚拟网卡bridge0,然后配置bridge0网卡的nat地址转换,这里bridge相当于docker中的docker0,bridge0相当于在网络中的交换机二层设备,他可以连接不同的网络设备,当请求到达Bridge设备时,可以通过报文的mac地址进行广播和转发,所以所有的容器虚拟网卡需要在bridge下,这也是连接namespace中的网络设备和宿主机网络的方式,这里下变会有讲解。(如果需要实现overlay等,需要换用更高级的转换工具,如用ovs来做类vxlan,gre 协议转换)

2 开启开启内核转发和配置iptables MASQUERADE,这是为了用MASQUERADE规则将容器的ip转换为宿主机出口网卡的ip,在linux namespace中,请求宿主机外部地址时,将namespace中的原地址换成宿主机作为原地址,这样就可以在namespace中进行地址正常转换了。

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

首先想一下,对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:\nBOCKER 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:\nBOCKER 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图标

  • 有了镜像就可以进行重要的bocker run 了(第一部分)
function bocker_run() { #HELP Create a container:\nBOCKER 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
复制代码

解析:

在运行bocker run时会进行一些列配置,我在也加了也进行了注释,第一部先生成相关配置,首先会通过shuf函数生成每个bocker唯一的Id,进行相关合法性检验,然后根据生成的截取生成的随机数id,截取部分字段组成ip地址和mac地址(注意这里可能会有概率ip冲突,后期应该需要优化)

第二部分,生成Linux veth对(Veth是成对的出现在虚拟网络设备,发送动Veth虚拟设备的请求会从另一端的虚拟设备发出,在容器的虚拟化场景中,经常会使用Veth连接不同的namespace) , 利用ip命令创建veth对 veth0_xx, veth1_xx,创建唯一uuid namespace, 绑定veth1到namespace中, 对其绑定ip,mac地址,然后绑定路由,启动网卡,网络接口,这里用到的veth对,你可以再简单的理解为一跟网线连接,图解一下。



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



这样就能让容器里边的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对应的数据都是存文件,保存在目录中的。



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

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

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

  • 清除网络接口
function bocker_cleanup() { #HELP Delete leftovers of improperly shutdown containers:\nBOCKER 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:\nBOCKER 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,相信也不会很难。



今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/bnXOJdNdM9
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/26916
 
303 次点击