内容简介:最近逛github无意发现了一个很好地项目我当时一看
最近逛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是支持多种存储系统的,具体详情可以到这里看
。
- 有了镜像就可以进行重要的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中, 对其 绑定i p,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,相信也不会很难。
以上所述就是小编给大家介绍的《关于如何用100行如何实现docker》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- php如何实现session,自己实现session,laravel如何实现session
- AOP如何实现及实现原理
- webpack 实现 HMR 及其实现原理
- Docker实现原理之 - OverlayFS实现原理
- 为什么实现 .NET 的 ICollection 集合时需要实现 SyncRoot 属性?如何正确实现这个属性?
- 自己实现集合框架(十):顺序栈的实现
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。