搭建docker镜像私有仓库碰到的问题

之前我有过一篇博客是专门讲如何搭建docker镜像私有仓库的。《搭建docker镜像私有仓库》
最近将docker升级到1.12版本后又搭建了私有仓库。当镜像仓库的容器运行起来之后,我在其它节点上想要拉取该仓库中的镜像,遇到了如下报错:
这里写图片描述
该报错的原因在于:
docker从1.3版本以后对Registry的访问都是用的https。这是基于安全的考虑的。而我的私有仓库容器使用http协议。所以造成了这样的报错。
修复办法很简单,将该节点的docker daemon对此私有仓库的访问协议由https改为http即可。
使用命令vim /etc/docker/daemon.json
这个daemon.json中存储了docker daemon的一些配置参数,使用json写成的。
我们只需要在这里加上一句
"insecure-registries":["registry.com:5000"]
这句话的意思是此docker daemon以后在访问registry.com:5000时使用http协议而不是https协议。
如果没有daemon.json文件,那么就新建该文件且在其中写入上述的那句话。
当然,registry.com是我的镜像仓库容器所在的节点IP。你要改为你自己的IP才行。
接下来输入命令systemctl daemon-reload,该命令重载daemon配置。
输入命令systemctl restart docker重启docker。
如果这时候重启失败并报错
这里写图片描述
那么就运行命令rm -rf /var/run/docker.sock/
将docker.sock目录删除再运行systemctl restart docker
完成之后就发现我们能在该节点上拉取此镜像仓库的镜像了。

Docker底层的内核知识——cgroups

概述

我的上一篇博客Docker底层的内核知识——namespace讲解了内核中支持Docker作资源隔离的机制namespace。本篇文章主要讲述Docker背后内核的另一机制——cgourps。cgroups不仅可以用来限制被namespace隔离的资源,还可以为资源设置权重、计算使用量、操控任务启停等。

从Linux系统中看cgroups

我们依然先从操作系统中直观的看一下cgroups是个什么东西。
这里写图片描述
进入到sys/fs/cgroup/目录下,我们可以看到许多目录,如上图所见。
这些如blkio、cpu、cpuacct等目录都是cgroups机制的子系统,子系统的概念一会再解释。简单来说,就是这些东西就是对cgroup做资源限制的。blkio负责为块设备设定输入/输出限制,比如物理驱动设备——磁盘。cpu使用调度程序控制任务对cpu的使用,cpuacct自动生成cgroup中任务对CPU资源使用情况的报告。
而正如我们所见,为了让cgroups便于用户理解和使用,也为了用精简的内核代码为cgroup提供熟悉的权限和命名空间管理,内核开发者们按照Linux虚拟文件系统转换器接口实现了一套名为cgroup的文件系统,非常巧妙地用来表示cgroups的层级概念,把各个子系统的实现都封装到文件系统的各项操作中。那么,我们就可以像操作文件一样对cgroups的层级进行浏览和操作管理。除了cgroup文件系统以外,内核没有为cgroups的访问和操作添加任何系统调用,这点与namespace不同,namespace都是通过系统调用来进行操作和管理的。

cgroups的由来

在操作系统中感受了cgroups,下面我们来看看cgroups的由来。
cgroups是由Google的工程师于2006年提出的,最初名为process container。为什么叫这个名字呢?因为刚开始有cgroups的概念时,namespace其实是cgroups的一个子系统,也就是说,cgroups当时还具有资源隔离的功能。不过后来的发展将namespace独立出去了。另外,由于container具有多重含义容易引起误解,就在2007年更名为control groups,并整合进Linux内核,顾名思义就是把任务放到一个组里面加以控制,这个组就是cgroup。

cgroups的官方定义

在这里给出最严谨的官方定义:
cgroups是Linux内核提供的一种机制,这种机制可以根据需求把一系列系统任务及其子任务整合(或分隔)到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架。

关于cgroups的术语

相信大家经常会听人说cgroups或者cgroup。就比如我文章前面,一会提到cgroups,一会提到cgroup,这两个术语之间有什么区别呢?接下来列举关于cgroups的几个术语加以理解:
cgroups:cgroups是Linux内核中的一个机制,我们用它来作容器资源的限制等功能。
cgroup:cgroup中文叫做控制组。它是cgroups实现资源控制的一个基本单位。cgroup表示按某种资源控制标准划分而成的一个任务组。它其中包含有一个或多个任务。
task:前面介绍cgroup时提到的任务就是task。任务表示系统的一个进程或线程。之所以把进程和线程统称为任务,是因为内核本身的调度和管理并没有对进程和线程做区分,只根据clone创建时传入参数的不同从概念上区别进程和线程,所以cgroups中简化称之。
subsystem:cgroups中的子系统。一个子系统就是一个资源调度控制器。比如前面一开始让大家看的cpu、memory、blkio、cpuacct等都是子系统。
hierarchy:中文叫做层级。层级由一系列cgroup以一个树状结构排列而成。每个层级通过绑定对应的子系统进行资源控制。前面一开始看到的目录就是子系统,而这些子系统就挂载着层级。一个层级内创建一个目录就类似于fork一个cgroup,这个cgroup继承了父cgroup的配置属性。当然,后面可以进行配置属性的修改。
语言叙述太苍白,请看实际操作:
这里写图片描述
我们进入cpu子系统的层级下,创建一个目录cgroup1,这个cgroup1就是一个新创建的cgroup控制组,我们可以看下cgroup1中的内容。
这里写图片描述
可以看到cgroup1目录下直接有这么多文件,且和cpu目录下本来存在的文件相同。实际上就是cgroup1继承了根cgroup的配置属性。这个根cgroup是新建层级之时就有的。
我们分别看一下两个目录下cpu.shares的值,会发现它们都是1024。
这里写图片描述

cgroups的作用

我们上面说完了cgroups的一些术语。以后对cgroups的术语就不用傻傻分不清楚了。我们常说cgroups是做资源限制的,而cgroups的功能其实不单单是做资源限制的。接下来我们看一下cgroups具体能干些什么。
cgroups为我们提供了四项功能:
资源限制:cgroups可以对任务使用的资源总额进行限制。如cpu的使用,memory的使用。
优先级分配:通过分配的CPU时间片数量及磁盘IO带宽大小,实际上就相当于控制了任务运行的优先级。
资源统计:cgroups可以统计系统的资源使用量,如CPU使用时长、内存用量等信息,该功能可以用于资源使用的计费。
任务控制:cgroups可以对任务执行挂起、恢复等操作。

子系统简介

前面已经解释过了什么是子系统。这里对子系统再进行详细的讲述,因为一个子系统就是一个资源控制器,每个子系统都独立地控制一种资源。
这里写图片描述
这是我们一开始就看到的图片,接下来我们挑几个重要的子系统再进行解释:
blkio:可以为块设备设定输入/输出限制,比如物理驱动设备。
cpu:使用调度程序控制任务对CPU的使用。
cpuacct:自动生成cgroup中任务对CPU资源使用情况的报告。实际就是记录CPU的使用情况。
cpuset:可以为cgroup中的任务分配独立的CPU和内存。当然,分配独立的CPU的前提是节点是多处理器的。
devices:可以开启或关闭cgroup中任务对设备的访问。
freezer:可以挂起或恢复cgroup中的任务。这就是cgroups的任务控制功能。
memory:可以设定cgroup中任务对内存使用量的限定,并且自动生成这些任务对内存资源使用情况的报告。
perf_event:使用后使cgroup中的任务可以进行统一的性能测试。
net_cls:Docker没有直接使用它,它通过使用等级识别符标记网络数据包,从而允许Linux流量控制程序识别从具体cgroup中生成的数据包。
pids:用来限制cgroup中任务的数量。
hugetlb:支持cgroups限制巨页的使用。

cgroup超出限额的后果

我们都知道cgroup作为一个资源控制的基本单位。那么,当cgroup使用的资源超出了分配的限额会有什么后果呢?
以内存为例,当进程所需的内存超过了它所属的cgroup最大限额时,如果Linux设置了OOM,那么进程会收到OOM信号并结束;否则进程就会被挂起,进入睡眠状态,直到cgroup中其他进程释放了足够的内存资源为止。Docker中是默认开启OOM的。其他子系统的资源限制与此类似。

总结

cgroups相比于namespace解释起来比较复杂,因为要做实际的演示比较难。最直观的了解cgroups就是通过看cgroup文件系统来感受它。(cd /sys/fs/cgroup/)除了cgroup文件系统以外,内核没有为cgroups的访问和操作添加任何系统调用。虽然操作方式不同,cgroups和namespace一个做资源控制,一个做资源隔离。共同成为Docker背后的内核支持机制。

Docker底层的内核知识——namespace

概述

用过Docker的开发者都知道,Docker容器在本质上是宿主机上的一个进程。也就是常说的容器是操作系统级的虚拟化。容器与容器之间做了资源的隔离,所以在一个容器内部的各种操作会给人一种仿佛在独立的系统环境中的感觉。外部应用对容器进行访问时,也会有这种感觉。而做这种容器资源隔离的Linux内核机制就是namespace。

感受一下namespace的存在

在具体了解namespace之前,我们先感受一下namespace的存在。
我们可以使用命令sudo ls -l /proc/[pid]/ns查看pid为[pid]的进程所属的namespace。比如我查看pid为1的进程。
这里写图片描述
可以看到namespace共分为7种类型。分别为ipc、mnt、pid、uts、net、cgroups、user。
如果某个软链接如ipc指向了同一个ipc namespace,那么这两个进程则是在同一个ipc namespace下的。如
这里写图片描述
我们可以看到pid为2的进程与pid为1的进程同属一个ipc namespace。因为它们的指向相同。
以此类推,这两个进程的mnt、net、pid、user、cgroups、uts namespace也都相同。
如若两个进程某个软链接指向不同,即说明这两个进程该资源已经被隔离了。

操作namespace的API

既然我们知道了实现容器资源隔离的Linux内核机制是namespace,那么,我们就想了解一下Linux提供的namespace操作API。
包括有clone(),setns(),unshare(),接下来分别做简单介绍:

clone()

clone()系统调用大家应该都比较熟悉,它的功能是创建一个新的进程。有别于系统调用fork(),clone()创建新进程时有许多的选项,通过选择不同的选项可以创建出合适的进程。我们也可以使用clone()来创建一个属于新的namespace的进程。这是Docker使用namespace的最基本的方法。
这里写图片描述
我们可以用man命令查看clone()的调用方式。
fn:传入子进程运行的程序主函数
child_stack:传入子进程使用的栈空间
flags:使用哪些标志位,与namespace相关的标志位主要包括CLONE_NEWIPC、CLONE_NEWPID、CLONE_NEWNS、CLONE_NEWNET、CLONE_USER、CLONE_UTS。具体含义后面会详述。
arg:传入的用户参数

setns()

这个系统调用顾名思义就是设置namespace。详细说来,就是将进程加入到一个已经存在的namespace中。对应于Docker的操作就是在一个Docker容器中用exec运行一个新命令。因为一个Docker容器其实就是一个已经存在的namespace,而用Docker exec执行一个命令,就是将该命令在该容器的namespace中运行,也就是将该命令的进程加入到一个已经存在的namespace中。
依然用man命令看一下这个系统调用的使用。
这里写图片描述
fd:表示要加入的namespace的文件描述符。它是一个指向/proc/[pid]/ns目录的文件描述符,可以通过直接打开该目录下的链接得到。
nstype:让调用者可以检查fd指向的namespace类型是否符合实际的要求。参数为0表示不检查。

unshare()

这个系统调用与clone()很像,都是做一个新的隔离。而且都通过选择flags来选择隔离的资源。不同之处在于clone()创建了一个新的进程,而unshare()是在原进程上作隔离。
这里写图片描述
参数flags是标志位,选择需要隔离的资源。与clone()的falgs参数基本相同,这里就不赘述了。

namespace分类详述

mount namespace

mount namespace通过隔离文件系统挂载点对文件系统进行隔离。隔离之后,不同的mount namespace下的文件结构发生变化也不会互相影响。或许有人注意到,在clone()的flags中,表示新mount namespace的标志位是CLONE_NEWNS。这是因为mount namespace是历史上第一个Linux namespace。

cgroup namespace

cgroup Namspace虚拟化了进程的cgroups视图。cgroups是Linux内核的一个工具,用来做资源的限制的。这里对此就不详述了,下次会写一篇专门讲述cgroups机制的文章。

PID namespace

我们都知道,在Linux操作系统中,每一个进程的PID都是在系统中是唯一的。而在容器中,进程的PID可以和另一个容器中某进程的PID相同。这就是对PID的虚拟化。因为两个容器处于不同的PID namespace下,所以这两个容器的PID可以有重复出现。
另外,每一个PID namespace下都会有一个PID为1的进程,它会像传统Linux中的init进程一样拥有特权,起特殊作用。
我们可以写一段代码来感受下PID namespace的隔离。

#define _GNU_SOURCE
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<sched.h>
#include<signal.h>
#include<unistd.h>

#define STACK_SIZE (1024*1024)

static char child_stack[STACK_SIZE];
char * const child_args[] = {
        "/bin/bash",
        NULL
};

int child_main(void *args) {
        printf("在子进程中!\n");
        execv(child_args[0],child_args);
        return 1;
}

int main() {
        printf("程序开始:\n");
        int child_pid = clone(child_main, child_stack + STACK_SIZE,  CLONE_NEWPID|SIGCHLD, NULL);
        waitpid(child_pid, NULL, 0);
        printf("已退出\n");
        return 0;
}

编译运行代码,结果如下
这里写图片描述
我们可以看到,使用clone()新创建了一个进程并进行隔离之后,此当前进程的pid为1。当退出进程后当前进程号又恢复为4639。这个pid为1的进程就是PID namespace中的第一进程,也就是我刚才说的像是Linux下拥有特权的init进程。
我们也可以在新的PID namespace下看看ps命令的结果。
这里写图片描述
奇怪的是,为什么在新PID Namespce下使用ps命令还是能看到所有的进程呢?难道不是已经将PID隔离了吗?理论上应该是不能看到的。
这是因为ps命令或者top命令都是从Linux系统中的/proc目录下取值的。因为这个时候我们和还没有用mount namespace进行挂载点的隔离,所以我们总是可以看到这些PID。

IPC namespace

同样的道理,IPC namespace也是一种namespace,它隔离了IPC(进程间通信)如信号量、消息队列和共享内存。在同一个IPC namespace下的进程互相可见,不同IPC namespace下的进程互相不可见。
我们看下如下示例:

#define _GNU_SOURCE
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<sched.h>
#include<signal.h>
#include<unistd.h>

#define STACK_SIZE (1024*1024)

static char child_stack[STACK_SIZE];
char * const child_args[] = {
        "/bin/bash",
        NULL
};

int child_main(void *args) {
        printf("在子进程中!\n");
        execv(child_args[0],child_args);
        return 1;
}

int main() {
        printf("程序开始:\n");
        int child_pid = clone(child_main, child_stack + STACK_SIZE,  CLONE_NEWIPC|SIGCHLD, NULL);
        waitpid(child_pid, NULL, 0);
        printf("已退出\n");
        return 0;
}

这里写图片描述
如图,首先我们使用ipcmk -Q创建了一个消息队列。可以知道,这个消息队列是在该IPC namespace下的。然后我们依然通过clone()创建了一个新进程,该进程位于新的IPC namespace中。于是使用ipcs -q命令查看该namespace下的消息队列,发现在刚才namespace下创建的消息队列在该namespace下并没有出现。这就说明了IPC namespace将进程间通信消息队列隔离了。

user namespace

user namespace主要隔离安全相关的标识符和属性,包括用户ID、用户组ID、root目录、key以及特殊权限。简单来说,我们可以在Linux中用非root的用户来创建一个容器,它创建的容器进程却属于拥有超级权限的用户。

UTS namespace

UTS(Unix Time-sharing System) namespace提供了主机名与域名的隔离。这样,我们每一个容器都可以拥有自己独立的主机名和域名了,在外部进行访问时好似访问了一个独立的节点。
同样,我们用clone()创建一个位于新的UTS namespace下的新进程。

#define _GNU_SOURCE
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
#include<sched.h>
#include<signal.h>
#include<unistd.h>

#define STACK_SIZE (1024*1024)

static char child_stack[STACK_SIZE];
char * const child_args[] = {
        "/bin/bash",
        NULL
};

int child_main(void *args) {
        printf("在子进程中!\n");
        sethostname("Newnamespace",12);
        execv(child_args[0],child_args);
        return 1;
}

int main() {
        printf("程序开始:\n");
        int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUTS|SIGCHLD, NULL);
        waitpid(child_pid, NULL, 0);
        printf("已退出\n");
        return 0;
}

编译并运行代码:
这里写图片描述
我们发现,在运行这个程序后主机名改为了Newnamespace了,这就说明,在新的UTS namespace下,主机名被隔离了,我们允许每个容器拥有自己独立的主机名和域名。

network namespace

network namespace主要提供了关于网络资源的隔离,包括网络设备、IPv4和IPv6协议栈、IP路由表、防火墙、套接字等。简单说,我们在每个容器中都可以启动一个Apache进程并占用“80端口”而不会出现端口冲突。我们知道,假设计算机只有一个物理网络设备时,该设备只能位于一个network namespace下提供网络服务。解决的方法是通过创建veth pair在不同的network namespace间进行通信。

总结

本文从功能角度分类讨论了namespace。并举了一些例子进行实际感受。实际上Docker底层的内核知识不仅包括用来资源隔离的namespace,还包括用来作资源限制与资源监控的cgroups。下一篇文章会简述cgroups的功能及原理。

搭建docker镜像私有仓库

1

docker官方提供了docker hub来存储我们的镜像文件。但由于docker hub管理的都是公开的镜像,并且访问的时候有时会特别的慢。所以我们打算来搭建一个私有的仓库,需要使用官方库中的registry镜像。

2

下载registry镜像

docker pull registry

运行registry镜像
docker run -d -p 5000:5000 -v /root/pf_registry:/var/lib/registry registry
以上的-d与-p参数在前一篇博客中已经讲过,所以,我们将容器的5000端口映射到宿主机的5000端口。我们一会就可以访问这个端口来访问容器
-v参数是用来将容器的数据绑定到宿主机上的,如此一来,容器中/var/lib/registry的数据就与/root/pf_registry中的数据同步了,做这件事的目的是将容器中存储的镜像文件在宿主机上也能够存在。这样如果容器停止时我们也能够看到镜像文件。

3

这些做好了,我们的仓库也就搭建好了,接下来就可以push我们的镜像文件到私有仓库中了
在push之前,我们需要将镜像的名称改为registry.pf.com:5000/tomcat的格式。其中registry.pf.com是宿主机的IP地址,之所以用registry.pf.com而不用IP地址,是因为我在/etc/hosts中进行了设置,如下:
这里写图片描述
5000是之前映射到的宿主机的端口
tomcat是镜像的名称

命令如下:
docker tag tomcat registry.pf.com:5000/tomcat

4

此时,我们就可以将tomcat镜像文件push到私有仓库中了
docker push registry.pf.com:5000/tomcat
push之后,我们就可以进入我们指定的绑定容器的目录下来查看push成功的镜像文件
如下:
这里写图片描述

1

从docker hub中将tomcat镜像拉下来
这里写图片描述

2

可以看一下详细信息,使用docker pull tomcat拉取tomcat时若不指定标签,则会使用默认的latest标签
这里写图片描述

3

接下来我们要让这个镜像作为容器在后台运行起来,并且将该容器的端口号暴露出来,也就是说,将容器的端口映射到我们自己计算机的物理端口上
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CHgE1WCy-1611660147218)(http://img.blog.csdn.net/20170802180545791?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaHR5NDY1NjU=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]
-d参数是让tomcat容器在后台运行
-p参数是将容器的端口映射给宿主机的端口
这里写图片描述
我们可以用docker run --help来看该命令各个参数的意义
注意:这里的16000是宿主机的端口,8080是容器的端口,不可以写反

4

成功执行后,我们就可以通过访问16000端口来看tomcat是否成功运行
这里写图片描述
可以看到,我这里已经成功运行了
或者在命令行中使用curl命令来看是否成功

5

同样也可以使用并测试nginx,但要注意的是nginx容器的端口是80
这里写图片描述