Docker容器实现原理及容器隔离性踩坑介绍

本文讲述了 关于容器隔离性的一个“坑”

正如Docker官方的口号:“Build once,Run anywhere,Configure once,Run anything”,Docker 被贴上了如下标签:轻巧、秒级启动、版本管理、可移植性等等,这些优点让它出现之初就收到极大的关注。现在,Docker已经不仅仅是开发测试阶段使用的工具,大家已经在生产环境中大量使用。今天我们给大家介绍关于容器隔离性的一个“坑”。在此之前,我们先来回顾一下 Docker 容器的底层实现原理。

容器底层实现

我们都知道,虚拟机与容器的底层实现原理是不同的,正如下图对比:

虚拟机实现资源隔离的方法是利用一个独立的 Guest OS,并利用 Hypervisor 虚拟化CPU、内存、IO 设备等实现的。例如,为了虚拟化内存,Hypervisor 会创建一个shadow page table,正常情况下,一个 page table 可以用来实现从虚拟内存到物理内存的翻译。相比虚拟机实现资源和环境隔离的方案,Docker 就显得简练很多,它不像虚拟机一样重新加载一个操作系统内核,引导、加载操作系统内核是一个比较耗时而又消耗资源的过程,Docker 是利用 Linux 内核特性实现的隔离,运行容器的速度几乎等同于直接启动进程。

关于 Docker 实现原理,简单总结如下:

  • 使用 Namespaces 实现了系统环境的隔离,Namespaces 允许一个进程以及它的子进程从共享的宿主机内核资源(网络栈、进程列表、挂载点等)里获得一个仅自己可见的隔离区域,让同一个 Namespace 下的所有进程感知彼此变化,对外界进程一无所知,仿佛运行在一个独占的操作系统中;
  • 使用 CGroups 限制这个环境的资源使用情况,比如一台16核32GB的机器上只让容器使用2核4GB。使用 CGroups 还可以为资源设置权重,计算使用量,操控任务(进程或线程)启停等;
  • 使用镜像管理功能,利用 Docker 的镜像分层、写时复制、内容寻址、联合挂载技术实现了一套完整的容器文件系统及运行环境,再结合镜像仓库,镜像可以快速下载和共享,方便在多环境部署。

正因为 Docker 不像虚机虚拟化一个 Guest OS,而是利用宿主机的资源,和宿主机共用一个内核,所以会存在下面问题:

注意:存在问题并不一定说就是安全隐患,Docker 作为最重视安全的容器技术之一,在很多方面都提供了强安全性的默认配置,其中包括:容器 root 用户的 Capability 能力限制,Seccomp 系统调用过滤,Apparmor 的 MAC 访问控制,ulimit 限制,镜像签名机制等。

1、Docker 是利用 CGroups 实现资源限制的,只能限制资源消耗的最大值,而不能隔绝其他程序占用自己的资源;

2、Namespace 的6项隔离看似完整,实际上依旧没有完全隔离 Linux 资源,比如/proc 、/sys 、/dev/sd*等目录未完全隔离,SELinux、time、syslog 等所有现有 Namespace 之外的信息都未隔离。

容器隔离性踩过的坑

在使用容器的时候,大家很可能遇到过这几个问题:

1、在 Docker 容器中执行 top、free 等命令,会发现看到的资源使用情况都是宿主机的资源情况,而我们需要的是这个容器被限制了多少 CPU,内存,当前容器内的进程使用了多少;

2、在容器里修改/etc/sysctl.conf,会收到提示”sysctl: error setting key ‘net.ipv4….’: Read-only file system”;

3、程序运行在容器里面,调用API获取系统内存、CPU,取到的是宿主机的资源大小;

4、对于多进程程序,一般都可以将 worker 数量设置成 auto,自适应系统CPU核数,但在容器里面这么设置,取到的CPU核数是不正确的,例如 Nginx,其他应用取到的可能也不正确,需要进行测试。

这些问题的本质都一样,在 Linux 环境,很多命令都是通过读取/proc 或者 /sys 目录下文件来计算资源使用情况,以free命令为例:

lynzabo@ubuntu:~$ strace free
execve("/usr/bin/free", ["free"], [/* 66 vars */]) = 0
...
statfs("/sys/fs/selinux", 0x7ffec90733a0) = -1 ENOENT (No such file or directory)
statfs("/selinux", 0x7ffec90733a0) = -1 ENOENT (No such file or directory)
open("/proc/filesystems", O_RDONLY) = 3
...
open("/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 3
...
open("/proc/meminfo", O_RDONLY) = 3
+++ exited with 0 +++
lynzabo@ubuntu:~$

包括各个语言,比如 Java,NodeJS,这里以 NodeJS 为例:

const os = require('os');
const total = os.totalmem();
const free = os.freemem();
const usage = (free - total) / total * 100;

NodeJS 的实现,也是通过读取/proc/meminfo文件获取内存信息。Java 也是类似。

我们都知道,JVM 默认的最大 Heap 大小是系统内存的1/4,假若物理机内存为10G,如果你不手动指定Heap大小,则JVM默认Heap大小就为2.5G。JavaSE8(<8u131) 版本前还没有针对在容器内执行高度受限的 Linux 进程进行优化,JDK1.9以后开始正式支持容器环境中的CGroups内存限制,JDK1.10这个功能已经默认开启,可以查看相关Issue (Issue地址:https://bugs.openjdk.java.net/browse/JDK-8146115 )。熟悉 JVM 内存结构的人都清楚,JVM Heap 是一个只增不减的内存模型,Heap 的内存只会往上涨,不会下降。在容器里面使用Java,如果为 JVM 未设置 Heap 大小,Heap 取得的是宿主机的内存大小,当 Heap 的大小达到容器内存大小时候,就会触发系统对容器OOM,Java 进程会异常退出。常见的系统日志打印如下:

memory: usage 2047696kB, limit 2047696kB, failcnt 23543
memory+swap: usage 2047696kB, limit 9007199254740991kB, failcnt 0
......
Free swap = 0kB
Total swap = 0kB
......
Memory cgroup out of memory: Kill process 18286 (java) score 933 or sacrifice child

对于 Java 应用,下面提供两个办法来设置 Heap

1、对于 JavaSE8(<8u131)版本,手动指定最大堆大小。

docker run 的时候通过环境变量传参确切限制最大 heap 大小:

docker run -d -m 800M -e JAVA_OPTIONS='-Xmx300m' openjdk:8-jdk-alpine

2、对于 JavaSE8(>8u131)版本,可以使用上面手动指定最大堆大小,也可以使用下面办法,设置自适应容器内存限制。

docker run 的时候通过环境变量传参确切限制最大 heap 大小

docker run -d -m 800M -e JAVA_OPTIONS='-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1' openjdk:8-jdk-alpine

对比这两种方式,第一种方式缺乏灵活性,在确切知道内存限制大小的情况下可以使用,第二种方法必须在 JavaSE8(>8u131) 版本才能使用。

当你启动一个容器时候,Docker 会调用 libcontainer 实现对容器的具体管理,包括创建 UTS、IPS、Mount 等 Namespace 实现容器之间的隔离和利用 CGroups 实现对容器的资源限制,在其中,Docker 会将宿主机一些目录以只读方式挂载到容器中,其中包括/proc、/dev、/dev/shm、/sys目录,同时还会建立以下几个链接:

  • /proc/self/fd->/dev/fd
  • /proc/self/fd/0->/dev/stdin
  • /proc/self/fd/1->/dev/stdout
  • /proc/self/fd/2->/dev/stderr 

保证系统 IO 不会出现问题,这也是为什么在容器里面取到的是宿主机资源原因。

了解了这些,那么我们在容器里该如何获取实例资源使用情况呢,下面介绍两个方法。

从CGroups中读取

Docker 在 1.8 版本以后会将分配给容器的 CGroups 信息挂载进容器内部,容器里面的程序可以通过解析 CGroups 信息获取到容器资源信息。

在容器里面可以运行 mount 命令查看这些挂载记录

...
cgroup on /sys/fs/cgroup/cpuset type cgroup (ro,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/cpu type cgroup (ro,nosuid,nodev,noexec,relatime,cpu)
cgroup on /sys/fs/cgroup/cpuacct type cgroup (ro,nosuid,nodev,noexec,relatime,cpuacct)
cgroup on /sys/fs/cgroup/memory type cgroup (ro,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (ro,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (ro,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/blkio type cgroup (ro,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/perf_event type cgroup (ro,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (ro,nosuid,nodev,noexec,relatime,hugetlb)
...

在这里我们不讲解 CGroups 对 CPU 和内存的限制都有哪些,只介绍基于Kubernetes 编排引擎下的计算资源管理,对容器 CGroups 都做了哪些支持:

  • 当为 Pod 指定了 requests,其中 requests.cpu 会作为 –cpu-shares 参数值传递给 docker run 命令,当一个宿主机上有多个容器发生 CPU 资源竞争时这个参数就会生效,参数值越大,越容易被分配到 CPU,requests.memory 不会作为参数传递给 Docker,这个参数在 Kubernetes 的资源 QoS 管理时使用;
  • 当为Pod指定了 limits,其中limits.cpu会作为 –cpu-quota 参数的值传递给docker run 命令,docker run 命令中另外一个参数–cpu-period 默认设置为100000,通过这两个参数限制容器最多能够使用的CPU核数,limits.memory 会作为–memory 参数传递给docker run 命令,用来限制容器内存,目前Kubernetes 不支持限制 Swap 大小,建议在部署 Kubernetes 时候禁用 Swap.

Kubernetes 1.10以后支持为 Pod 指定固定 CPU 编号,我们在这里不详细介绍,就以常规的计算资源管理为主,简单讲一下以 Kubernetes 作为编排引擎,容器的CGroups 资源限制情况:

1、读取容器 CPU 核数

# 这个值除以100000得到的就是容器核数
~ # cat  /sys/fs/cgroup/cpu/cpu.cfs_quota_us
400000

2、获取容器内存使用情况(USAGE / LIMIT)

~ # cat /sys/fs/cgroup/memory/memory.usage_in_bytes
4289953792
~ # cat /sys/fs/cgroup/memory/memory.limit_in_bytes
4294967296

将这两个值相除得到的就是内存使用百分比。

3、获取容器是否被设置了 OOM,是否发生过 OOM

~ # cat /sys/fs/cgroup/memory/memory.oom_control
oom_kill_disable 0
under_oom 0
~ #
~ #

这里需要解释一下:

  • oom_kill_disable 默认为0,表示打开了 oom killer,就是当内存超时会触发 kill进程。可以在使用 docker run 时候指定 disable oom,将此值设置为1,关闭oom killer;
  • under_oom 这个值仅仅是用来看的,表示当前的 CGroups 的状态是不是已经oom 了,如果是,这个值将显示为1。

4、获取容器磁盘I/O

~ # cat /sys/fs/cgroup/blkio/blkio.throttle.io_service_bytes
253:16 Read 20015124480
253:16 Write 24235769856
253:16 Sync 0
253:16 Async 44250894336
253:16 Total 44250894336
Total 44250894336

5、获取容器虚拟网卡入/出流量

~ # cat /sys/class/net/eth0/statistics/rx_bytes
10167967741
~ # cat /sys/class/net/eth0/statistics/tx_bytes
15139291335
~ #

使用LXCFS

由于习惯性等原因,在容器中使用 top、free 等命令仍然是一个较为普遍存在的需求,但是容器中的/proc、/sys目录等还是挂载的宿主机目录,有一个开源项目:LXCFS.LXCFS 是基于 FUSE 实现的一套用户态文件系统,使用 LXCFS,让你在容器里面继续使用 top、free 等命令变成了可能。但需要注意,LXCFS 可能会存在很多问题,建议在线上环境先不要使用。

总结

容器给大家带来了很多便利,很多公司已经或正在把业务往容器上迁移。在迁移过程中,需要清楚上面介绍的这个问题是不是会影响应用的正常运行,并采取相应的办法绕过这个坑。

这篇文章的分享就到这里,希望对大家有所帮助。

Image placeholder
CangLan
未设置
  34人点赞

没有讨论,发表一下自己的看法吧

推荐文章
打造高逼格、可视化的Docker容器监控系统平台

关于Docker技术的文章之前也断断续续写了几篇:Docker容器系列文章|Docker技术入门(一)Docker容器系列文章|Docker技术入门(二)Docker容器系列文章|这20个Docker

万字长文:聊聊几种主流Docker网络的实现原理

一、容器网络简介容器网络主要解决两大核心问题:一是容器的IP地址分配,二是容器之间的相互通信。本文重在研究第二个问题并且主要研究容器的跨主机通信问题。实现容器跨主机通信的最简单方式就是直接使用host

JVM CPU Profiler技术原理及源码深度解析

本文介绍了JVM平台上CPUProfiler的实现原理,希望能帮助读者在使用类似工具的同时也能清楚其内部的技术实现。引言研发人员在遇到线上报警或需要优化系统性能时,常常需要分析程序运行行为和性能瓶颈。

浅析RunLoop原理及其应用

引言:一个APP的启动与结束都是伴随着RunLoop循环往复的,不断的循环、不断的往复。当线程被杀掉、APP退出后被系统以占用内存为由杀掉,RunLoop就消失了。但平时开发中很少见到RunLoop,

聊一聊 MySQL 中的事务及其实现原理

说到数据库,那就一定会聊到事务,事务也是面试中常问的问题,我们先来一个面试场景:面试官:"事务的四大特性是什么?" 我:"ACID,即原子性(Atomicity)、隔离性(Isolation)、持久性

自己撸一个 LaraDock(使用 Docker LNMP 部署 PHP 开发环境)

项目简介 DockerLNMP是基于docker-compose开发的运行在Docker上的LNMP开发环境,包含PHP、MySQL、Redis等镜像并支持多版本切换,满足您的学习、开发和测试需求。

Nacos 服务注册与发现原理分析

Nacos另一个非常重要的特性就是服务注册与发现,说到服务的注册与发现相信大家应该都不陌生,在微服务盛行的今天,服务是非常重要的,而在Nacos中服务更被称为他的一等公民。Nacos支持几乎所有主流类

OpenStack容器服务Zun初探与原理分析

01Zun服务简介Zun是OpenStack的容器服务(ContainersasService),类似于AWS的ECS服务,但实现原理不太一样,ECS是把容器启动在EC2虚拟机实例上,而Zun会把容器

使用 Docker 创建 Hyperf 项目

@[TOC]安装Hyperf开发容器dockerrun-d--nameuser_center\ --restart=always\ #映射到宿主机目录,这样我们就直接在/home/wwwroot/us

用docker拓展压测工具artillery的能力

指定域名的IP地址最近在用artillery做压测时遇到一个问题。我需要压测某一个通过域名访问的服务,而这个域名背后的机器地址需要随着压测用例变化的。通常这就是DNS要做的事情-把不变的域名和变化的地

使用kubei一步部署k8s高可用集群(包含docker安装、k8s组件安装、master初始化和加入nodes节点)

kubei(kubernetesinstaller)是一个go开发的用来部署kubernetes高可用集群的命令行工具,该工具可在Windows、Linux、Mac中运行kubei原理:通过ssh连接

基于 Hyperf 实现 RabbitMQ + WebSocket 消息推送

#介绍 基于Hyperf+WebSocket+RabbitMQ实现的一个简单大屏幕的消息推送。 #思路 利用WebSocket协议让客户端和服务器端保持有状态的长链接,保存链接上来的客户端id。订阅发

Docker 麻烦大了

Docker是容器技术的典范,但其近况似乎不佳。 在早前泄露的一份备忘录中,DockerCEO罗博·比尔登(RobBearden)赞扬了公司的员工,但话术却非常地耐人寻味: 尽管“不确定性带来了巨大

15 个 Docker 初学者必须掌握的命令

这篇文章我基本上不会做put操作。如果你认为这些命令缺少了什么其他方面重要的东西,那么你需要自行检查Docker文档(https://docs.docker.com/) pull pull命令和gi

docker 安装 Laravel 环境 (nginx mariadb PHP7.3)

1,安装mariadb创建网路dockernetworkcreate--subnet=172.18.0.0/16mynetworkdockerrun-d-p3306:3306-eMYSQL_ROOT_

docker-compose 搭建 dnmp 总结

说明从零开始写一份dnmp环境的docker-compose.yml(由于redis比较常用,也包括进来)。总体设计示意图: 网络分配上,让要直接通讯的容器同属于一个网络,不直接通讯的容器属于不同的网

使用 Docker 创建 Lumen 项目

lumen6需要PHP>=7.2,我们仍然使用docker进行开发,我制作了一个基于PHP7.3的镜像,直接拉来用就可以了dockerrun-d--namephp-apache-dev\ --rest

使用 Docker 部署 Spring Boot 项目

Docker技术发展为微服务落地提供了更加便利的环境,使用Docker部署SpringBoot其实非常简单,这篇文章我们就来简单学习下。首先构建一个简单的SpringBoot项目,然后给项目添加Doc

Docker最佳实践:5个方法精简镜像

本文记录了精简Docker镜像尺寸的必要性及好处精简Docker镜像的好处很多,不仅可以节省存储空间和带宽,还能减少安全隐患。优化镜像大小的手段多种多样,因服务所使用的基础开发语言不同而有差异。本文将

三丰云CentOS7.3服务器上安装Docker

下面分享一下如何在自己购买的三丰云服务器(CentOS7.3版本)上安装Docker大神可直接看如下命令进行安装,菜鸟可按后面给出的傻瓜式安装步骤进行操作:更新Yum包:yumupdate安装所需的包

Docker Compose 安装 Jenkins

前言上回我们已经安装好了Docker,这篇我们利用Compose,通过配置docker-compose.yml安装jenkins。之所以使用Compose,是因为后续如果有更多容器,能够更为方便直观的

CentOS7 Docker CE 安装

前言最近想要搭建一个Jenkins用于持续集成,查阅了官网文档后,发现Docker镜像安装十分便利。一直久仰Docker大名,直到2020年才接触实在是落后了太多。那么,就先拆解一下接下来的内容:一、

老司机带你用 PHP 实现 Websocket 协议

我为什么会写这篇文章? 当初作为编程小白的我,刚刚从事后台工作,觉得http是个很牛逼的东西,然而后面随着自己深入学习并实践之后,觉得原来和我所想的天壤之别,没大家想象的那么复杂,仅仅是个协议嘛!。后

深入探究 RocketMQ 事务机制的实现流程,为什么它能做到发送消息零丢失?

1、解决消息丢失的第一个问题:订单系统推送消息领丢失既然我们已经明确了消息在基于MQ传输的过程中可能丢失的几个地方,那么我们接着就得一步一步考虑如何去解决各个环节丢失消息的问题,首先要解决的第一个问题

HBase实战:记一次Safepoint导致长时间STW的踩坑之旅

本文记录了HBase中Safepoint导致长时间STW此问题的解决思路及办法。过程记录现象:小米有一个比较大的公共离线HBase集群,用户很多,每天有大量的MapReduce或Spark离线分析任务