基本概念

基本概念

历史简介

Docker项目最初由dotCloud公司发起,2013年开源,该项目的成功导致在2013年底dotCloud公司更名为Docker。 Docker使用Go语言开发,基于Linux的namespace+cgroup实现进程隔离控制,并通过容器镜像进行分发。 OCI(开放容器接口)协议包括运行时和镜像两部分内容,目前容器技术在实现上已不止docker一种,但基本都是遵守OCI规范的。

运行时在实现上最初基于LXC,之后为libcontainer,再演变为runC和containerd。其中runC就是OCI协议运行时部分的参考实现,containerd只是守护进程,向上提供统一gRPC接口。 containerd和runC已加入到CNCF(云原生)项目。

容器镜像可以看作是一层一层的数据,镜像本身都是只读的,在容器运行时,最上层(运行时层)是可写的,下面的(镜像层)仍然都是只读层。命令docker create执行完成就会生成一个可写层,供容器运行时使用。

docker-fs.png

容器最核心的价值是打包应用环境,解决分布式系统运维问题。容器和镜像仓库可以很好解决单节点部署问题。但构建分布式运维平台,还需要编排、调度、伸缩、监控、容错、负载均衡、服务发现、滚动升级等诸多容器本身没有的功能。本文单讲容器相关内容。

docker

一个较为完整的容器功能包括客户端、服务端、镜像仓库、容器、镜像、网络。客户端供用户操作,服务端即daeon进程,镜像仓库用于分发镜像,剩下的容器、镜像和网络实际是三个驱动,docker在设计是采用接口加驱动的方法,可以在统一接口的前提下,在不同场景下使用不同的驱动。

在早期所有功能都集中在daemon中,后来由于标准化拆成多个组件,从docker-1.11开始已经有了containerd和runC组件。在不同版本,启动方法也有所不同。

docker-1.8- docker -d
docker-1.8 docker daemon
docker-1.11 dockerd

细分后的模块:

dockerd
容器守护进程,调用containerd管理容器生命周期,其他模块管理镜像存储等
containerd
包括运行时和镜像管理,向daemon提供gRPC接口
RunC
RunC是按照OCF标准的具体实现,供containerd调用,负责容器起停和资源隔离等

namespace

Linux名字空间有六个,如下所示。

CLONE_NEWNS Linux 2.4.19
CLONE_NEWUTS Linux 2.6.19
CLONE_NEWIPC Linux 2.6.19
CLONE_NEWPID Linux 2.6.24
CLONE_NEWNET Linux 2.6.24 - Linux 2.6.29
CLONE_NEWUSER Linux 2.6.23 - Linux 3.8

创建进程调用clone()时指定相关标志位可以实现隔离,另外还有两个和名字空间相关的接口, setns用于加入到某个名字空间,unshare用于脱离某个名字空间。

USER名字空间用于实现在容器中看到的是root用户权限,但实际在宿主上并非root权限这样的功能,以提升安全性。

cgroup

控制组可用于限制容器所能使用的资源,如CPU、内存和磁盘等。控制组在功能上包括限制、审计、优先级和运行控制等。控制组在工作上按子系统划分,包括如下子系统,可以通过lssubsys来查看。

cpu cpuacct cpuset CPU控制、审计、独立CPU
memory 内存限制
blkio IO限制
net_cls net_prio 网络等级、流量优先级
devices 设备访问
freezer 挂起或恢复任务

只需在cgroup/memory下创建一个目录就创建出一个memory的cgroup,该目录中会自动创建出所需文件。

fs

容器镜像基于分层的思想构建,实现这些分层理念,需要用到UnionFS,而UnionFS在具体实现起来又有很多方法,因而出现了不同的存储驱动方案。

容器存储会用到COW(写时复制)技术,多个进程共享同一份文件,当有进程要写入时,把要写入的部分复制给进程用于写入,这样可以让所有程序共享镜像层。

容器在实现上都是采用抽象接口加底层驱动的方式来做的,驱动分三类:运行时、存储、网络。

主要存储驱动如下所示:

docker-fs-driver.png

另外需要注意docker-1.10对镜像存储驱动做了大改,原来的层ID是随机生成的,修改后基于内容SHA256计算,这样做的目的是借助CAS(Content Addressable Storage)提升安全性,可以在拉取或推送镜像时进行完整性校验。

  • DeviceMapper

    DeviceMapper基于Linux内核的Device Mapper框架实现。该框架实现了物理设备与虚拟存储的映射,或虚拟设备与虚拟设备的映射。基于该框架可以实现磁盘自由划分,如LVM2、software RAIDs、dm-cryptdisk encryption等。 Device Mapper框架提供两个功能:thin-provisioning和snapshot。

    thin-provisioning
    类似虚拟内存,提供给使用者的空间只有在使用者进行写操作时才真正进行分配。假设使用者有一块100g的thin块设备,使用了20g,实际系统提供的大小就是20g,只有当使用者进行存储操作时才分配更多的空间,直到100g。
    snapshot
    快照是一种COW策略的实现,假设从A设备做快照得到B设备时,并未对A进行完整拷贝,而是当对B设备进行写操作时,才将需要改变的那部分做属于B设备的拷贝。

    DeviceMapper借助thin-provisioning和snapshot实现了镜像的分层结构和存储优化。

    在用devicemapper作为驱动的docker中,每个镜像和容器都对应一个设备,通过对镜像做snapshot操作得到容器,所以容器中拥有镜像的内容且操作这些内容不影响镜像本身,因为容器和镜像对应不同的设备。

    DeviceMapper的大致思路是,先通过虚拟化技术得到一个thin-pool设备(可理解成一个资源池),接着在thin-pool上建立一个基础设备,此后docker上所有镜像和容器都是基于此设备的snapshot。

    DeviceMapper有两种模式可选,loop-lvm和direct-lvm。两个模式的区别就在建立thin-pool的方法不同。

    loop-lvm:不推荐用于生产环境,容器数量增大时性能急剧下降

    • 创建两个稀疏文件data和metadata
    • 将这两个文件映射成两个块设备(loopback块设备)
    • 将两个设备通过内核中Device Mapper映射成thin-pool

    direct-lvm:利用了基于Device Mapper的LVM

    • 将空余块设备(可以是分区)创建成physical volume(pv)
    • 在由这些PV组成volume group(vg)
    • 从vg中建立两个logical volume(lv),data和matedata
    • 将data和matedata映射成thin-pool

    可通过lsblk查看设备信息,loop-lvm可以看到是loop设备,而direct-lvm为块设备。

  • OverlayFS

    OverlayFS和Aufs相比主要是设计上更简单并在linux-3.18加入到了内核中。由于OverlayFS只工作在两层,比Aufs的多层查询性能也会好点。也支持页缓存,多个容器共享页,高效利用内存。

    目前docker搞了两个基于overlayfs的存储驱动,overlay2驱动对inode性能做了优化,原生支持多层镜像(最大128),但是只支持linux-4.0以上的版本。

    OverlayFS在设计上只有两层,镜像在底层,容器运行时在上层,统一出来就是合并层。创建一个容器时,overlay驱动将合并层提供给容器使用。

    这就是说多层镜像不能对应多层OverlayFS,而是使用一个目录来表示一个层,并利用硬链接技术来引用底层数据。镜像数据存储在/var/lib/docker/overlay2目录下。

    • 读取时,从上层开始找,找到就读取,否则就找下层。
    • 写入时,如果上层有文件就直接写,没有就复制下层到上层再写入。
    • 删除时,通过创建特殊文件或目录表示文件或目录已删除。
    • 重命名,只允许原路经和目标路径都在顶层才能操作,否则返回EXDEV

    注意事项:

    • overlay驱动会消耗大量inode,overlay2不存在此问题
    • 首次写入引发的向上复制,大文件会增加复制负担
    • open:对同一个路径的文件打开,指定模式不同(RO/RW),实际指向的文件可能会不一样
    • rename:和aufs一样,只能顶层之间的路径相互重命名

    目前overlayfs是存储驱动首选。

镜像仓库

构建镜像

FROM ubuntu:14.04.2                     # 基准镜像
MAINTAINER mickyching@gmail.com         # 作者
ARG Name=micky                          # ARG仅在构建镜像时有效
ENV GOPATH=/home/go GOROOT=/home/go     # ENV在容器运行时也有效
LABEL Owner=$Name                       # 标签
RUN useradd -m micky && apt-get update  # 执行命令
USER micky                              # 切换用户
WORKDIR /home/micky                     # 切换工作目录
COPY calc.py ./                         # 复制文件
VOLUME /data                            # 挂载目录,位于:/var/lib/docker/volumes/
EXPOSE 22                               # 暴露端口
CMD [1+1]                               # 启动命令
ENTRYPOINT ["./calc.py"]                # Init程序

要点:

  • 尽量减少ENV和RUN的调用次数,这样可以减少镜像层数
  • 在VOLUME指令后对其目录的写入操作都不会记录到镜像
  • 在一个干净的目录中执行build命令,因为构建是会将整个目录发送给daemon
docker build -t calc .                  # 编译
docker run --rm calc '1+1'              # 运行
root@f38c87f2a42d:/# mount
...
/dev/disk/by-uuid/1fec...ebdf on /etc/hostname type ext4 ...
/dev/disk/by-uuid/1fec...ebdf on /etc/hosts type ext4 ...
/dev/disk/by-uuid/1fec...ebdf on /etc/resolv.conf type ext4 ...
...

不要在容器镜像中修改这些配置,而应该启动容器时通过命令行指定相关参数。

harbor

有了镜像之后就面临一个存储和分发的问题,除了使用共有镜像仓库,也可以搭建私有镜像仓库,私有仓库体验上更快更安全。

这里讨论如下几个问题:

  1. 数据存储如何管理
  2. 安全认证如何实现
  3. 如何提升可用性
  4. 如何跨地域
  • 数据存储

    在存储方面,docker开源了distribution,用于实现镜像仓库的存储。

  • 认证授权

    在帐号方面,可以使用vmware开源的harbor。为了使harbor和自身帐号系统相通,需要改动认证相关的代码,来实现认证和鉴权。如何觉得harbor自带的UI不满足要求,还可以独立设计UI,这是也是目前很多公共镜像仓库的做法。

    harbor-arch.png

    Harbor的主要模块如上图所示,功能如下:

    • proxy 即nginx反向代理
    • registry 即docker开源的distribution
    • UI 是harbor的核心服务,提供认证和界面
    • DB 数据库可用mysql服务
    • log 用rsyslog收集日志

    为了跟进一步说明harbor工作原理,这里以docker login为例说明:

    1. docker login url 发出请求到url:443(或url:80)
    2. proxy收到请求,代理给registry
    3. registry服务那里配置了token服务地址,返回401错误和token服务地址
    4. docker客户端向token服务地址发送用户名和密码请求token
    5. proxy将请求代理给token服务
    6. token服务校验成功后生成token并返回
    7. 登录成功后,docker会把用户名和密码保存在本地

    其他操作和docker login一样,如docker push操作过程如下:

    1. 重复docker login过程,发送请求到proxy,代理到registry,进而得到token服务地址
    2. docker向token服务请求token,这里会提供额外信息,指明操作类型(push/pull)和对象(image)
    3. 请求通过proxy代理给token服务,服务进行认证和鉴权,鉴权成功就返回token
    4. docker得到token后,向registry发出推送请求,registrya验证token后开始传输

    Harbor的每个项目有三种角色:管理员、开发、访客。管理员可以管理帐号,开发可以读写仓库,访客只能读仓库。

  • 高可用性

    可用性提升一般采用LB负载均衡实现,即LB后面可以挂多个Harbor,这些Harbor本身无状态,至于数据部分采用共享存储,如多个Harbor共享一块网盘,这里还需注意共享存储一般只用于一个地域。负载均衡方法还需要考虑用户session如何在多实例上共享,如使用redis进行共享。

  • 跨域同步

    最后一个问题是在地域规模扩大时,如何提升镜像仓库的性能和可用性。

    主从模式可以有效解决分发问题,写入还是直接写到中央数据中心。对于分发地域较广的情况,可以在此基础上增加分层,通过多级分层以减少中央分发压力。

    harbor-master-slave.png

    如果镜像被删除,缓存节点是如何知道被删除或者有更新,确保缓存可以得到最新的镜像。

    采用这种分级的方式仍然不能解决写入问题,对于跨境仓库,需要多数据中心,单个数据中心会导致境外上传缓慢。跨中心复制可以用Job Service组件,Job Service会将执行进度更新到DB中,用户可以查看进度。实现上接收API请求并调度任务,但是要考虑如何限流以及如何干预复制过程(如取消、重试)。

    复制是针对项目来手动触发的,一般不做跨数据中心自动同步。

容器云

微服务

微服务

  • 什么是微服务

    微服务是一种高内聚、松耦合的架构风格。

  • 微服务的好处
    • 敏捷开发:技术栈灵活,可以应对需求频繁变更
    • 敏捷发布:独立部署,没有依赖,不用完整编译一个完整单体发布
    • 伸缩性好:水平扩展、按需扩展
    • 鲁棒性好:可用性高
    • 减少冗余:减少代码、模块、功能冗余
  • 微服务的问题
    • 技术栈灵活导致环境复杂:该问题由docker天生解决
    • 服务规模上升导致发布复杂:需要编排支持,可由kubernetes解决
    • 全局监控
    • 分布式问题追踪
    • 资源竞争时如何处理:如优先级控制
    • 完整体系支撑:如网络、存储等
  • 微服务的需求
    • 安全 登录 鉴权 隔离
    • 文档 仓库 部署 编排 集成 测试 终端
    • 注册 发现 均衡 伸缩 升级 灰度 回滚
    • 过滤 流控 熔断 降级 容错 灾备
    • 日志 聚合 监控 汇聚 检查 恢复 告警
    • 网络 存储

    docker-micro-service.png