关于container:DIUN开源的镜像更新通知工具

咱们通常能够将一台或多台服务器作为Docker主机,应用容器跑一些开源的工具服务。而往往咱们不晓得该什么时候这个这些利用有了更新的版本,最近发现了一个开源的工具,能够查看主机上运行的容器的镜像是否有更新,并能够通过集成多种渠道发送更新告诉,这款工具就是 DIUN(Docker Image Update Notifier) 。 DUIN介绍DUIN是一款应用GO语言编写的命令行工具,能够本地运行,也能够通过容器运行(开发者提供了构建好的镜像 ),当监控的容器镜像在相应的注册表(Registry)中更新时,能够接管到相应的告诉。 DUIN反对多种监控配置(Providers): Docker - 剖析Docker主机上运行容器的镜像,并查看其更新Podman - 相似Docker,须要Podman以服务形式启动Kubernetes - 剖析Kubernetes集群中的Pods,查看pod应用的镜像Swarm - 剖析Swarm集群中服务应用的镜像Nomad - 相似Docker,剖析Nomad引擎运行的镜像Dockerfile - 剖析Dockerfile中援用的镜像File - yaml格局的配置文件,间接配置须要查看的镜像信息DUIN反对集成多种告诉渠道,例如 Discord, Slack,Matrix,Telegram 以及 Webhook 等。 DUIN应用示例这里将演示在Docker主机上应用Docker Compose来运行duin服务,并集成Slack,将告诉发送到相应的频道。 docker-compose.yml : services: diun: image: crazymax/diun:latest container_name: diun hostname: home200-diun command: serve volumes: - diundata:/data - "/var/run/docker.sock:/var/run/docker.sock" environment: - "TZ=Asia/Shanghai" - "LOG_LEVEL=info" - "LOG_JSON=false" - "DIUN_WATCH_WORKERS=20" - "DIUN_WATCH_SCHEDULE=0 */6 * * *" - "DIUN_WATCH_JITTER=30s" - "DIUN_PROVIDERS_DOCKER=true" - "DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=true" - "DIUN_NOTIF_SLACK_WEBHOOKURL=https://hooks.slack.com/services/xxxxxxxxxxxxx" restart: on-failurevolumes: diundata:下面的环境变量中 ...

March 18, 2023 · 1 min · jiezi

关于container:把大象装入货柜里Java容器内存拆解

[图片源:https://bell-sw.com/announcem...] 介绍测试环境配置容量 POD 容量配置JVM 容量配置 神秘的 MaxDirectMemorySize 默认值maxThreadCount 最大线程数起源使用量 Java 的视角看使用量 如何采集理论使用量原生利用的视角看使用量 *lib.so 动静库占用*.jar mapping 占用glibc malloc 耗费GC 内存耗费tmpfs 内存耗费操作系统 RSS CGroup 限度潜在问题和举荐解决办法 Native Buffer 限度glibc malloc arena 的节约Jetty 线程池Java code cache 慢涨容器的内存限度瞻望免责申明领会介绍置信很多人都晓得,云环境中,所有服务都必须作资源限度。内存作为一个重要资源当然不会例外。限度说得容易,但如何在限度的同时,保障服务的性能指标(SLA)就是个技术和艺术活。 为利用内存设置下限,从来不是个容易的事。因为设置下限的理据是: 应用程序对内存的应用和回收逻辑,而这个逻辑个别异样地简单古代操作系统简单的虚拟内存治理、物理内存调配、回收机制如果是 Java ,还要加上: JVM 中各类型组件的内存管理机制以上 3 个方面还能够进一步细分。每一个细分都有它的内存机制。而只有咱们漏算了其中一个,就有可能让利用总内存应用超限。 而让人揪心的是,当利用总内存应用超限时,操作系统会无情地杀死利用过程(OOM, Out Of Memory)。而很多人对这一无所觉,只晓得容器重启了。而这可能是连锁反应的开始: 如果容器 OOM 的起因只是个偶尔,那还好说。如果是个 BUG 引起的,那么这种 OOM 可能会在服务的所有容器中一一暴发,最初服务瘫痪原来服务容器群的资源就缓和,一个容器 OOM 敞开了,负载平衡把流量分到其它容器,于是其它容器也呈现同样的 OOM。最初服务瘫痪JVM 是个 Nice 的经理,在发现内存缓和时,就不厌其烦地进行利用线程和执行 GC,而这种内存缓和的信号,在设计界称为“背压(Backpressure)”。但操作系统相同,是个雷厉风行的司令,一发现有过程超限,间接一枪 OOM Killed。 或者你深入研究过 cgroup memory,它其实也有一个 Backpressure 的告诉机制,不过当初的容器和 JVM 均疏忽之。终上所述,容器过程 OOM Kllled 是件应该防止,但须要深入研究能力防止的事件。 ...

October 7, 2021 · 6 min · jiezi

关于container:创建最小化的容器镜像四静态二进制文件

引言这是如何制作最小化Docker镜像系列文章的第四篇:动态二进制文件。 在第一篇文章中,我谈到了如何通过编写更好的Dockerfiles创立较小的镜像;在第二篇文章中,我探讨了如何应用docker-squash压缩镜像层以制作较小的镜像;在第三篇文章中,我介绍了如何将Alpine Linux用作较小的根底镜像。 在这篇文章中,我将探讨制作最小化镜像的最终形式:动态二进制文件。 如果应用程序没有任何依赖关系,并且除了应用程序自身之外什么都不须要,这种状况下该怎么做? 这就是动态二进制文件所实现的,它们包含运行在二进制文件自身中的动态编译程序的所有依赖项。为了了解其含意,让咱们退后一步。 动静链接大多数应用程序是应用称为动静链接的过程构建的,每个应用程序在编译时都是以这样一种形式来实现的,即它定义了须要运行的库,但实际上在其外部并不蕴含这些库。 这对于操作系统发行版来说十分重要,因为能够独立于应用程序更新库,然而在容器内运行应用程序时,它并不是那么重要。 每个容器镜像都蕴含它将要应用的所有文件,因而无论如何都不会重用这些库。 来看一个例子,创立一个简略的C++程序并按如下所示进行编译,则将取得一个动静链接的可执行文件。 ianlewis@test:~$ cat hello.cpp #include <iostream>int main() { std::cout << "Hello World!\n"; return 0;}ianlewis@test:~$ g++ -o hello hello.cpp$ ls -lh hello-rwxrwxr-x 1 ianlewis ianlewis 8.9K Jul 6 07:31 hellog++实际上正在执行两个步骤,它正在编译我的程序并将其链接。 编译这一步只会创立一个一般的C++指标文件,链接这一步是增加运行应用程序所需的依赖项。 辛运的是,大多数编译工具都做到了这一点,编译和链接能够按如下形式进行。 ianlewis@test:~$ g++ -c hello.cpp -o hello.oianlewis@test:~$ g++ -o hello hello.oianlewis@test:~$ ls -lhtotal 20K-rwxrwxr-x 1 ianlewis ianlewis 8.9K Jul 6 07:41 hello-rw-rw-r-- 1 ianlewis ianlewis 85 Jul 6 07:31 hello.cpp-rw-rw-r-- 1 ianlewis ianlewis 2.5K Jul 6 07:41 hello.o通过在Linux零碎上对其运行ldd命令会输入命令行指定的每个程序或共享对象所需的共享对象(共享库) 。 如果你应用的是Mac OS,则能够通过运行otool -L取得雷同的信息。 ...

May 8, 2021 · 3 min · jiezi

关于container:Container-Runtimes-三高级容器运行时

引言这是系列文章的第三篇,在第一篇文章中,我概述了容器运行时,同时介绍了低级容器运行时与高级容器运行时之间的区别;在第二篇文章中我具体介绍了低级容器运行时,并构建了一个简略的低级容器运行时。高级容器运行时的技术栈要高于低级容器运行时,低级容器运行时负责容器的理论运行,而高级容器运行时负责容器镜像额传输和治理。 通常,高级运行时提供了守护程序应用程序和API,近程应用程序能够应用它们来逻辑运行容器并对其进行监督,但 它们位于底层运行时或委托给底层运行时或其余高层运行时进行理论工作。 高级运行时还能够提供听起来有些低级的性能,但这些性能能够在计算机上的各个容器中应用。 例如,其中一个性能可能是网络名称空间的治理,并容许容器退出另一个容器的网络名称空间。 这里有一个概念图,用于理解各个组件如何组合在一起: 高级运行时示例为了更好地了解高级运行时,请看一些示例。 像低级运行时一样,每个运行时都实现了不同的性能。 DockerDocker是最早的开源容器运行时之一。 它是由提供PaSS服务的公司dotCloud开发的,用于在容器中运行其用户的Web应用程序。 Docker是一个容器运行时,其中蕴含构建,打包,共享和运行容器。 Docker具备C/S架构,最后是作为整体守护程序,dockerd和docker client程序构建的。 该守护程序提供了构建容器,治理镜像和运行容器的大多数逻辑,以及一个API,能够从客户端命令行运行命令从守护程序获取信息。 它是第一个风行的运行时,它整合了在构建和运行容器的生命周期中所需的全副性能。 Docker最后同时实现了高级和低级运行时性能,但起初这些局部被合成为runc和容器化的独自我的项目。 Docker当初包含dockerd守护程序,docker-containerd守护程序以及docker-runc。 docker-containerd和docker-runc只是Docker打包的“香草”容器和runc的版本。dockerd提供诸如构建镜像之类的性能,而dockerd应用docker-containerd提供诸如镜像治理和运行中的容器之类的性能。 例如,Docker的构建步骤实际上只是一些逻辑,该逻辑解释Dockerfile,应用containerd在容器中运行必要的命令,并将生成的容器文件系统保留为镜像。 containerdcontainerd是从Docker分离出来的高级运行时。 就像runc一样被合成为低级运行时组件,containered也被合成为Docker的高级运行时组件。 containerd实现下载镜像,治理镜像以及从镜像运行容器。 当须要运行容器时,它将镜像解压缩到OCI runtime bundle中,而后将其打包到runc来运行它。 容器化还提供了可用于与其交互的API和客户端应用程序,容器命令行客户端是ctr。 ctr相干命令提取容器镜像: $ sudo ctr images pull docker.io/library/redis:latest列出以后所有镜像: $ sudo ctr images list从镜像运行一个容器: $ sudo ctr container create docker.io/library/redis:latest redis列出运行的容器: $ sudo ctr container list进行容器: $ sudo ctr container delete redis这些命令相似于用户与Docker交互的形式,然而,与Docker相比,containerd只专一于运行中的容器,因而它不提供构建容器的机制。 Docker专一于最终用户和开发人员用例,而containerd则专一于操作具体的容器实例,例如在服务器上运行容器,而诸如构建容器镜像之类的工作留给其余工具解决。 rkt在上一篇文章中,我提到rkt同时具备低级和高级性能的运行时,与Docker一样,rkt容许您构建容器镜像,在本地存储库中获取和治理容器镜像,并通过单个命令运行它们。 rkt不足Docker的性能,因为它不提供长期运行的守护程序和近程API。 你能够应用如下命令获取近程镜像: $ sudo rkt fetch coreos.com/etcd:v3.3.10列出本地镜像: $ sudo rkt image listID NAME SIZE IMPORT TIME LAST USEDsha512-07738c36c639 coreos.com/rkt/stage1-fly:1.30.0 44MiB 2 minutes ago 2 minutes agosha512-51ea8f513d06 coreos.com/oem-gce:1855.5.0 591MiB 2 minutes ago 2 minutes agosha512-2ba519594e47 coreos.com/etcd:v3.3.10 69MiB 25 seconds ago 24 seconds ago删除镜像: ...

May 7, 2021 · 1 min · jiezi

关于container:Container-Runtime-一-介绍

前言 在波及到容器时你常常听到一个术语:容器运行时。每个人对这个术语都有不同的了解,即便在容器社区也是这种状况。这篇文章是这个系列文章的第一篇,它们别离是: 容器运行时介绍:为什么它们令人如此困惑?深刻Low-Level 运行时深刻High-Level 运行时Kubernetes运行时和CRI 这篇文章将会介绍容器运行时是什么和为什么会让人如此困惑,而后深刻介容器运行时不同的类型,它们是如何工作的以及它们之间的不同点。 通常来说,一个计算机程序员可能将一个程序的运行周期了解为"运行时",或者是反对其运行的语言的特定实现,其中的一个例子是`Java HotSpot运行时,后者更靠近"容器运行时"的概念。容器运行时负责一个容器运行的所有局部,而容器实际上并未在运行程序自身。正如咱们将从本系列文章中看到的那样,运行时实现了各个级别的性能个性,但实际上运行一个容器就是调用容器运行时所需的全副。 为什么容器让人如此困惑?Docker于2013年公布,它端到端地解决了开发者运行容器的诸多问题: 容器镜像格局构建镜像的办法(Dcokerfile/docker build)治理容器镜像的办法(Docker images, docker rm, etc)治理容器实例的办法(docker ps, docker rm, etc)分享容器镜像的办法(docker push/pull)运行容器的办法(docker run)在那时,Docker是一个一体化的零碎,然而事实上这些性能个性彼此之间并没有相互依赖,它们中的每一个都能够在一个更小更专一的工具实现,每个工具都能够在一个通用的规范(容器规范)下一起应用。因为这个缘故,Docker,Google,CoreOS以及其余的供应商一起创建了凋谢容器标准(OCI),同时他们拆分了容器运行这部分的代码将其作为一个工具或lib库称之为run c,而后捐献给OCI作为OCI运行规范的参考范例。 最后,这使Docker对OCI的捐献感到困惑,他们捐献的是一个规范而不是运行容器的办法,其中并没有蕴含镜像格局或者镜像仓库拉取推送规定,当你运行一个Docker容器的时候,它们实际上通过了以下的步骤: 下载镜像将镜像文件解开为bundle文件,将一个文件系统拆分成多层从bundle文件运行容器Docker标准化的仅仅是第三步。在此之前,每个人都认为容器运行时反对Docker反对的所有性能。最终,Docker方面廓清:原始OCI标准指出,只有“运行容器”的局部组成了runtime。这种“概念失联”始终继续到明天,并使“容器运行时”成为一个令人困惑的话题。心愿我能证实单方都不是齐全谬误的,并且在本博文中将宽泛应用该术语。 Low-Level和High-Level容器运行时当人们想到容器运行时时,可能会想到许多示例。runc,lxc,lmctfy,Docker(containerd),rkt,cri-o。这些中的每一个都是针对不同的状况而构建的,并实现了不同的性能。有些容器(例如containerd和cri-o容器)实际上应用runc来运行容器,在runc之上实现了镜像治理和API接口。与runc的Low-Level个性相比,您能够将这些性能(包含图像传输,图像治理,图像解压缩和API)视为High-Level个性。思考到这点,能够看到容器运行时空间相当简单,每个运行时涵盖了从Low-Level到High-Level的不同局部,这里有一张十分直观的图: 因而,从理论登程,通常只专一于正在运行的容器的runtime通常称为“Low-Level容器运行时”,反对更多高级性能(如镜像治理和gRPC / Web API)的运行时通常称为“High-Level容器运行时”,“High-Level容器运行时”或通常仅称为“容器运行时”,我将它们称为“高级容器运行时”。值得注意的是,Low-Level容器运行时和High-Level容器运行时是解决不同问题的、从根本上不同的事物。容器是通过Linux nanespace和Cgroups实现的,Namespace能让你为每个容器提供虚拟化系统资源,像是文件系统和网络,Cgroups提供了限度每个容器所能应用的资源的如内存和CPU使用量的办法。在最低级别的运行时中,容器运行时负责为容器建设namespaces和cgroups,而后在其中运行命令,Low-Level容器运行时反对在容器中应用这些操作系统个性。 通常状况下,开发人员想要运行一个容器不仅仅须要Low-Level容器运行时提供的这些个性,同时也须要与镜像格局、镜像治理和共享镜像相干的API接口和个性,而这些个性个别由High-Level容器运行时提供。就日常应用来说,Low-Level容器运行时提供的这些个性可能满足不了日常所需,因为这个缘故,惟一会应用Low-Level容器运行时的人是那些实现High-Level容器运行时以及容器工具的开发人员。那些实现Low-Level容器运行时的开发者会说High-Level容器运行时比方containerd和cri-o不像真正的容器运行时,因为从他们的角度来看,他们将容器运行的实现外包给了runc。然而从用户的角度来看,它们只是提供容器性能的单个组件,能够被另一个的实现替换,因而从这个角度将其称为runtime依然是有意义的。即便containerd和cri-o都应用runc,然而它们是截然不同的我的项目,反对的个性也是十分不同的。

April 26, 2021 · 1 min · jiezi

Docker-容器内无法通过-HTTP-访问外网

现象内/外网 IP 和 域名 可以 ping 通容器内无法访问宿主机所在内网及外网的 Web 服务(404)通过 curl 查看返回头信息感觉是所有 Web 请求被中转到一个固定的 Nginx 服务器所有容器(包括新创建的)均出现以上问题分析通过 docker run --net host 创建的容器不存在上述的问题docker run 默认使用 bridge 桥接网络,初步判断是 bridge 设置问题通过 自定义网桥 也未解决问题最终怀疑是宿主机上的桥接网卡 docker0 的问题解决通过重建 docker0 网络解决问题 $ sudo service docker stop$ sudo pkill docker$ sudo iptables -t nat -F$ sudo ifconfig docker0 down$ sudo brctl delbr docker0$ sudo service docker start其实最终也没有确定问题所在,只是暂时解决了问题。

July 8, 2019 · 1 min · jiezi

Docker学习之Container容器4

容器是 Docker 又一核心概念。简单的说,容器是独立运行的一个或一组应用,以及它们的运行态环境。对应的,虚拟机可以理解为模拟运行的一整套操作系统(提供了运行态环境和其他系统环境)和跑在上面的应用。关于容器的操作主要有: 创建启动停止导入导出删除等等启动容器启动容器有两种方式,一种是基于镜像新建一个容器并启动,另外一个是将在终止状态( stopped )的容器重新启动。因为 Docker 的容器实在太轻量级了,很多时候用户都是随时删除和新创建容器。 新建并启动所需要的命令主要为 docker run 这跟在本地直接执行 /bin/echo 'hello world' 几乎感觉不出任何区别。 下面的命令则启动一个 bash 终端,允许用户进行交互,如: docker run -t -i ubuntu:18.04 /bin/bash其中, -t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上, -i 则让容器的标准输入保持打开。 那么当我们用docker run命令来创建并启动容器的时候,会发生哪些事儿呢? 检查本地是否存在指定的镜像,不存在就从公有仓库下载启动利用镜像创建并启动一个容器分配一个文件系统,并在只读的镜像层外面挂载一层可读写层从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去从地址池配置一个 ip 地址给容器执行用户指定的应用程序执行完毕后容器被终止所以当我们通过bash进入终端的时候,其实就是进入另一个系统。 启动已终止容器可以利用 docker container start 命令,直接将一个已经终止的容器启动运行。 容器的核心为所执行的应用程序,所需要的资源都是应用程序运行所必需的。除此之外,并没有其它的资源。可以在伪终端中利用 ps 或 top 来查看进程信息。 可见,容器中仅运行了指定的 bash 应用。这种特点使得 Docker 对资源的利用率极高,是货真价实的轻量级虚拟化。 后台运行更多的时候,我们会在后台运行容器,这时可以加上-d参数来实现。下面是每个1秒打印一次hello world。 docker run ubuntu:18.04 /bin/sh -c "while true; do echo hello world;sleep 1;done"如果使用了 -d 参数运行容器,则就是在后台进行运行: ...

June 19, 2019 · 1 min · jiezi

实现PHP的自动依赖注入容器-EasyDI容器

[TOC] Last-Modified: 2019年5月10日16:15:36 1. 前言在看了一些容器实现代码后, 就手痒想要自己实现一个, 因此也就有了本文接下来的内容. 首先, 实现的容器需要具有以下几点特性: 符合PSR-11标准实现基本的容器存储功能具有自动依赖解决能力本项目代码由GitHub托管 可使用Composer进行安装 composer require yjx/easy-di 2. 项目代码结构|-src |-Exception |-InstantiateException.php (实现Psr\Container\ContainerExceptionInterface) |-InvalidArgumentException.php (实现Psr\Container\ContainerExceptionInterface) |-UnknownIdentifierException.php (实现Psr\Container\NotFoundExceptionInterface) |-Container.php # 容器|-tests |-UnitTest |-ContainerTest.php3. 容器完整代码代码版本 v1.0.1<?phpnamespace EasyDI;use EasyDI\Exception\UnknownIdentifierException;use EasyDI\Exception\InvalidArgumentException;use EasyDI\Exception\InstantiateException;use Psr\Container\ContainerExceptionInterface;use Psr\Container\ContainerInterface;use Psr\Container\NotFoundExceptionInterface;class Container implements ContainerInterface{ /** * 保存 参数, 已实例化的对象 * @var array */ private $instance = []; private $shared = []; private $raw = []; private $params = []; /** * 保存 定义的 工厂等 * @var array */ private $binding = []; public function __construct() { $this->raw(ContainerInterface::class, $this); $this->raw(self::class, $this); } /** * Finds an entry of the container by its identifier and returns it. * * @param string $id Identifier of the entry to look for. * * @throws NotFoundExceptionInterface No entry was found for **this** identifier. * @throws ContainerExceptionInterface Error while retrieving the entry. * * @return mixed Entry. */ public function get($id, $parameters = [], $shared=false) { if (!$this->has($id)) { throw new UnknownIdentifierException($id); } if (array_key_exists($id, $this->raw)) { return $this->raw[$id]; } if (array_key_exists($id, $this->instance)) { return $this->instance[$id]; } $define = array_key_exists($id, $this->binding) ? $this->binding[$id] : $id; if ($define instanceof \Closure) { $instance = $this->call($define, $parameters); } else { // string $class = $define; $params = (empty($this->params[$id]) ? [] : $this->params[$id]) + $parameters; // Case: "\\xxx\\xxx"=>"abc" if ($id !== $class && $this->has($class)) { $instance = $this->get($class, $params); } else { $dependencies = $this->getClassDependencies($class, $params); if (is_null($dependencies) || empty($dependencies)) { $instance = $this->getReflectionClass($class)->newInstanceWithoutConstructor(); } else { $instance = $this->getReflectionClass($class)->newInstanceArgs($dependencies); } } } if ($shared || (isset($this->shared[$id]) && $this->shared[$id])) { $this->instance[$id] = $instance; } return $instance; } /** * @param callback $function * @param array $parameters * @return mixed * @throws InvalidArgumentException 传入错误的参数 * @throws InstantiateException */ public function call($function, $parameters=[], $shared=false) { //参考 http://php.net/manual/zh/function.call-user-func-array.php#121292 实现解析$function $class = null; $method = null; $object = null; // Case1: function() {} if ($function instanceof \Closure) { $method = $function; } elseif (is_array($function) && count($function)==2) { // Case2: [$object, $methodName] if (is_object($function[0])) { $object = $function[0]; $class = get_class($object); } elseif (is_string($function[0])) { // Case3: [$className, $staticMethodName] $class = $function[0]; } if (is_string($function[1])) { $method = $function[1]; } } elseif (is_string($function) && strpos($function, '::') !== false) { // Case4: "class::staticMethod" list($class, $method) = explode('::', $function); } elseif (is_scalar($function)) { // Case5: "functionName" $method = $function; } else { throw new InvalidArgumentException("Case not allowed! Invalid Data supplied!"); } try { if (!is_null($class) && !is_null($method)) { $reflectionFunc = $this->getReflectionMethod($class, $method); } elseif (!is_null($method)) { $reflectionFunc = $this->getReflectionFunction($method); } else { throw new InvalidArgumentException("class:$class method:$method"); } } catch (\ReflectionException $e) {// var_dump($e->getTraceAsString()); throw new InvalidArgumentException("class:$class method:$method", 0, $e); } $parameters = $this->getFuncDependencies($reflectionFunc, $parameters); if ($reflectionFunc instanceof \ReflectionFunction) { return $reflectionFunc->invokeArgs($parameters); } elseif ($reflectionFunc->isStatic()) { return $reflectionFunc->invokeArgs(null, $parameters); } elseif (!empty($object)) { return $reflectionFunc->invokeArgs($object, $parameters); } elseif (!is_null($class) && $this->has($class)) { $object = $this->get($class, [], $shared); return $reflectionFunc->invokeArgs($object, $parameters); } throw new InvalidArgumentException("class:$class method:$method, unable to invoke."); } /** * @param $class * @param array $parameters * @throws \ReflectionException */ protected function getClassDependencies($class, $parameters=[]) { // 获取类的反射类 $reflectionClass = $this->getReflectionClass($class); if (!$reflectionClass->isInstantiable()) { throw new InstantiateException($class); } // 获取构造函数反射类 $reflectionMethod = $reflectionClass->getConstructor(); if (is_null($reflectionMethod)) { return null; } return $this->getFuncDependencies($reflectionMethod, $parameters, $class); } protected function getFuncDependencies(\ReflectionFunctionAbstract $reflectionFunc, $parameters=[], $class="") { $params = []; // 获取构造函数参数的反射类 $reflectionParameterArr = $reflectionFunc->getParameters(); foreach ($reflectionParameterArr as $reflectionParameter) { $paramName = $reflectionParameter->getName(); $paramPos = $reflectionParameter->getPosition(); $paramClass = $reflectionParameter->getClass(); $context = ['pos'=>$paramPos, 'name'=>$paramName, 'class'=>$paramClass, 'from_class'=>$class]; // 优先考虑 $parameters if (isset($parameters[$paramName]) || isset($parameters[$paramPos])) { $tmpParam = isset($parameters[$paramName]) ? $parameters[$paramName] : $parameters[$paramPos]; if (gettype($tmpParam) == 'object' && !is_a($tmpParam, $paramClass->getName())) { throw new InstantiateException($class."::".$reflectionFunc->getName(), $parameters + ['__context'=>$context, 'tmpParam'=>get_class($tmpParam)]); } $params[] = $tmpParam;// $params[] = isset($parameters[$paramName]) ? $parameters[$paramName] : $parameters[$pos]; } elseif (empty($paramClass)) { // 若参数不是class类型 // 优先使用默认值, 只能用于判断用户定义的函数/方法, 对系统定义的函数/方法无效, 也同样无法获取默认值 if ($reflectionParameter->isDefaultValueAvailable()) { $params[] = $reflectionParameter->getDefaultValue(); } elseif ($reflectionFunc->isUserDefined()) { throw new InstantiateException("UserDefined. ".$class."::".$reflectionFunc->getName()); } elseif ($reflectionParameter->isOptional()) { break; } else { throw new InstantiateException("SystemDefined. ".$class."::".$reflectionFunc->getName()); } } else { // 参数是类类型, 优先考虑解析 if ($this->has($paramClass->getName())) { $params[] = $this->get($paramClass->getName()); } elseif ($reflectionParameter->allowsNull()) { $params[] = null; } else { throw new InstantiateException($class."::".$reflectionFunc->getName()." {$paramClass->getName()} "); } } } return $params; } protected function getReflectionClass($class, $ignoreException=false) { static $cache = []; if (array_key_exists($class, $cache)) { return $cache[$class]; } try { $reflectionClass = new \ReflectionClass($class); } catch (\Exception $e) { if (!$ignoreException) { throw new InstantiateException($class, 0, $e); } $reflectionClass = null; } return $cache[$class] = $reflectionClass; } protected function getReflectionMethod($class, $name) { static $cache = []; if (is_object($class)) { $class = get_class($class); } if (array_key_exists($class, $cache) && array_key_exists($name, $cache[$class])) { return $cache[$class][$name]; } $reflectionFunc = new \ReflectionMethod($class, $name); return $cache[$class][$name] = $reflectionFunc; } protected function getReflectionFunction($name) { static $closureCache; static $cache = []; $isClosure = is_object($name) && $name instanceof \Closure; $isString = is_string($name); if (!$isString && !$isClosure) { throw new InvalidArgumentException("$name can't get reflection func."); } if ($isString && array_key_exists($name, $cache)) { return $cache[$name]; } if ($isClosure) { if (is_null($closureCache)) { $closureCache = new \SplObjectStorage(); } if ($closureCache->contains($name)) { return $closureCache[$name]; } } $reflectionFunc = new \ReflectionFunction($name); if ($isString) { $cache[$name] = $reflectionFunc; } if ($isClosure) { $closureCache->attach($name, $reflectionFunc); } return $reflectionFunc; } /** * Returns true if the container can return an entry for the given identifier. * Returns false otherwise. * * `has($id)` returning true does not mean that `get($id)` will not throw an exception. * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. * * @param string $id Identifier of the entry to look for. * * @return bool */ public function has($id) { $has = array_key_exists($id, $this->binding) || array_key_exists($id, $this->raw) || array_key_exists($id, $this->instance); if (!$has) { $reflectionClass = $this->getReflectionClass($id, true); if (!empty($reflectionClass)) { $has = true; } } return $has; } public function needResolve($id) { return !(array_key_exists($id, $this->raw) && (array_key_exists($id, $this->instance) && $this->shared[$id])); } public function keys() { return array_unique(array_merge(array_keys($this->raw), array_keys($this->binding), array_keys($this->instance))); } public function instanceKeys() { return array_unique(array_keys($this->instance)); } public function unset($id) { unset($this->shared[$id], $this->binding[$id], $this->raw[$id], $this->instance[$id], $this->params[$id]); } public function singleton($id, $value, $params=[]) { $this->set($id, $value, $params, true); } /** * 想好定义数组, 和定义普通项 * @param $id * @param $value * @param bool $shared */ public function set($id, $value, $params=[], $shared=false) { if (is_object($value) && !($value instanceof \Closure)) { $this->raw($id, $value); return; } elseif ($value instanceof \Closure) { // no content } elseif (is_array($value)) { $value = [ 'class' => $id, 'params' => [], 'shared' => $shared ] + $value; if (!isset($value['class'])) { $value['class'] = $id; } $params = $value['params'] + $params; $shared = $value['shared']; $value = $value['class']; } elseif (is_string($value)) { // no content } $this->binding[$id] = $value; $this->shared[$id] = $shared; $this->params[$id] = $params; } public function raw($id, $value) { $this->unset($id); $this->raw[$id] = $value; } public function batchRaw(array $data) { foreach ($data as $key=>$value) { $this->raw($key, $value); } } public function batchSet(array $data, $shared=false) { foreach ($data as $key=>$value) { $this->set($key, $value, $shared); } }}3.1 容器主要提供方法容器提供方法: ...

May 10, 2019 · 7 min · jiezi

Docker 使用简介

Docker 是使用 GoLang 开发的开源容器引擎,可以方便的打包开发好的应用,然后分发到任意 linux 主机上。 与传统的虚拟机相比拥有以下优势: 高效的系统资源利用率由于不需要进行硬件虚拟和运行完整的操作系统等额外开销,无论是应用执行速度、内存损耗或者文件存储速度, Docker 都更加高效 更快的启动速度Docker 容器应用直接运行与宿主内核,无需启动完整的操作系统,可以做到秒级启动 一致的运行环境Docker 镜像提供了除内核外的完整运行环境,确保了应用运行环境的一致性 持续交付和部署可以通过 Docker 镜像来实现服务的持续交付、部署。使用 Dockerfile 来构建镜像,使用持续集成系统进行集成测试;使用镜像结合持续部署系统进行自动部署 迁移轻松只需要迁移镜像及镜像运行的数据就可在其他主机或平台运行 易于维护和扩展由于使用镜像进行部署,使维护更为容易。由于支持在镜像的基础上进行定制,使得扩展变得更简单。而官方也维护了一大批高质量的镜像,大大降低了镜像的制作成本 基本概念仓库Docker 提供了仓库(Repository)用于存放制作好的镜像,方便使用者获取,在本地可通知配置多个 Repository 。 拉取可以使用命令来拉取镜像: docker pull [repo url>/]image name> 默认的 repo url 是 hub.docker.com ,拉取默认仓库中的镜像时是不需要 url 的。如拉取 debian : docker pull debian 。 推送我们也可将自己制作好的镜像推送到仓库,以便分发,使用命令: docker push [<repo url>/]<image name>[:<image tag>> 搜索使用 docker search 命令则可搜索默认 repo url 内的镜像。 镜像加速 由于默认 repo url 在国外,为了加快拉取速度,需要指定其为国内的,向 /etc/docker/daemon.json 中添加: { "registry-mirrors": ["https://registry.docker-cn.com"]}便可使用 Docker 在中国的镜像加速站。 ...

April 21, 2019 · 2 min · jiezi

如何实现Laravel的服务容器

如何实现服务容器(Ioc Container)1. 容器的本质服务容器本身就是一个数组,键名就是服务名,值就是服务。服务可以是一个原始值,也可以是一个对象,可以说是任意数据。服务名可以是自定义名,也可以是对象的类名,也可以是接口名。// 服务容器$container = [ // 原始值 ’text’ => ‘这是一个字符串’, // 自定义服务名 ‘customName’ => new StdClass(), // 使用类名作为服务名 ‘StdClass’ => new StdClass(), // 使用接口名作为服务名 ‘Namespace\StdClassInterface’ => new StdClass(),];// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //// 绑定服务到容器$container[‘standard’] = new StdClass();// 获取服务$standard = $container[‘standard’];var_dump($standard);2. 封装成类为了方便维护,我们把上面的数组封装到类里面。$instances还是上面的容器数组。我们增加两个方法,instance用来绑定服务,get用来从容器中获取服务。class BaseContainer{ // 已绑定的服务 protected $instances = []; // 绑定服务 public function instance($name, $instance) { $this->instances[$name] = $instance; } // 获取服务 public function get($name) { return isset($this->instances[$name]) ? $this->instances[$name] : null; }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //$container = new BaseContainer();// 绑定服务$container->instance(‘StdClass’, new StdClass());// 获取服务$stdClass = $container->get(‘StdClass’);var_dump($stdClass);3. 按需实例化现在我们在绑定一个对象服务的时候,就必须要先把类实例化,如果绑定的服务没有被用到,那么类就会白白实例化,造成性能浪费。为了解决这个问题,我们增加一个bind函数,它支持绑定一个回调函数,在回调函数中实例化类。这样一来,我们只有在使用服务时,才回调这个函数,这样就实现了按需实例化。这时候,我们获取服务时,就不只是从数组中拿到服务并返回了,还需要判断如果是回调函数,就要执行回调函数。所以我们把get方法的名字改成make。意思就是生产一个服务,这个服务可以是已绑定的服务,也可以是已绑定的回调函数,也可以是一个类名,如果是类名,我们就直接实例化该类并返回。然后,我们增加一个新数组$bindings,用来存储绑定的回调函数。然后我们把bind方法改一下,判断下$instance如果是一个回调函数,就放到$bindings数组,否则就用make方法实例化类。class DeferContainer extend BaseContainer{ // 已绑定的回调函数 protected $bindings = []; // 绑定服务 public function bind($name, $instance) { if ($instance instanceof Closure) { // 如果$instance是一个回调函数,就绑定到bindings。 $this->bindings[$name] = $instance; } else { // 调用make方法,创建实例 $this->instances[$name] = $this->make($name); } } // 获取服务 public function make($name) { if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 执行回调函数并返回 $instance = call_user_func($this->bindings[$name]); } else { // 还没有绑定到容器中,直接new. $instance = new $name(); } return $instance; }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //$container = new DeferContainer();// 绑定服务$container->bind(‘StdClass’, function () { echo “我被执行了\n”; return new StdClass();});// 获取服务$stdClass = $container->make(‘StdClass’);var_dump($stdClass);StdClass这个服务绑定的是一个回调函数,在回调函数中才会真正的实例化类。如果没有用到这个服务,那回调函数就不会被执行,类也不会被实例化。4. 单例从上面的代码中可以看出,每次调用make方法时,都会执行一次回调函数,并返回一个新的类实例。但是在某些情况下,我们希望这个实例是一个单例,无论make多少次,只实例化一次。这时候,我们给bind方法增加第三个参数$shared,用来标记是否是单例,默认不是单例。然后把回调函数和这个标记都存到$bindings数组里。为了方便绑定单例服务,再增加一个新的方法singleton,它直接调用bind,并且$shared参数强制为true。对于make方法,我们也要做修改。在执行$bindings里的回调函数以后,做一个判断,如果之前绑定时标记的shared是true,就把回调函数返回的结果存储到$instances里。由于我们是先从$instances里找服务,所以这样下次再make的时候就会直接返回,而不会再次执行回调函数。这样就实现了单例的绑定。class SingletonContainer extends DeferContainer{ // 绑定服务 public function bind($name, $instance, $shared = false) { if ($instance instanceof Closure) { // 如果$instance是一个回调函数,就绑定到bindings。 $this->bindings[$name] = [ ‘callback’ => $instance, // 标记是否单例 ‘shared’ => $shared ]; } else { // 调用make方法,创建实例 $this->instances[$name] = $this->make($name); } } // 绑定一个单例 public function singleton($name, $instance) { $this->bind($name, $instance, true); } // 获取服务 public function make($name) { if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 执行回调函数并返回 $instance = call_user_func($this->bindings[$name][‘callback’]); if ($this->bindings[$name][‘shared’]) { // 标记为单例时,存储到服务中 $this->instances[$name] = $instance; } } else { // 还没有绑定到容器中,直接new. $instance = new $name(); } return $instance; }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //$container = new SingletonContainer();// 绑定服务$container->singleton(‘anonymous’, function () { return new class { public function __construct() { echo “我被实例化了\n”; } };});// 无论make多少次,只会实例化一次$container->make(‘anonymous’);$container->make(‘anonymous’);// 获取服务$anonymous = $container->make(‘anonymous’);var_dump($anonymous)上面的代码用singleton绑定了一个名为anonymous的服务,回调函数里返回了一个匿名类的实例。这个匿名类在被实例化时会输出一段文字。无论我们make多少次anonymous,这个回调函数只会被执行一次,匿名类也只会被实例化一次。5. 自动注入自动注入是Ioc容器的核心,没有自动注入就无法做到控制反转。自动注入就是指,在实例化一个类时,用反射类来获取__construct所需要的参数,然后根据参数的类型,从容器中找到已绑定的服务。我们只要有了__construct方法所需的所有参数,就能自动实例化该类,实现自动注入。现在,我们增加一个build方法,它只接收一个参数,就是类名。build方法会用反射类来获取__construct方法所需要的参数,然后返回实例化结果。另外一点就是,我们之前在调用make方法时,如果传的是一个未绑定的类,我们直接new了这个类。现在我们把未绑定的类交给build方法来构建,因为它支持自动注入。class InjectionContainer extends SingletonContainer{ // 获取服务 public function make($name) { if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 执行回调函数并返回 $instance = call_user_func($this->bindings[$name][‘callback’]); if ($this->bindings[$name][‘shared’]) { // 标记为单例时,存储到服务中 $this->instances[$name] = $instance; } } else { // 使用build方法构建此类 $instance = $this->build($name); } return $instance; } // 构建一个类,并自动注入服务 public function build($class) { $reflector = new ReflectionClass($class); $constructor = $reflector->getConstructor(); if (is_null($constructor)) { // 没有构造函数,直接new return new $class(); } $dependencies = []; // 获取构造函数所需的参数 foreach ($constructor->getParameters() as $dependency) { if (is_null($dependency->getClass())) { // 参数类型不是类时,无法从容器中获取依赖 if ($dependency->isDefaultValueAvailable()) { // 查找参数的默认值,如果有就使用默认值 $dependencies[] = $dependency->getDefaultValue(); } else { // 无法提供类所依赖的参数 throw new Exception(‘找不到依赖参数:’ . $dependency->getName()); } } else { // 参数类型是类时,就用make方法构建该类 $dependencies[] = $this->make($dependency->getClass()->name); } } return $reflector->newInstanceArgs($dependencies); }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //class Redis{}class Cache{ protected $redis; // 构造函数中依赖Redis服务 public function __construct(Redis $redis) { $this->redis = $redis; }}$container = new InjectionContainer();// 绑定Redis服务$container->singleton(Redis::class, function () { return new Redis();});// 构建Cache类$cache = $container->make(Cache::class);var_dump($cache);6. 自定义依赖参数现在有个问题,如果类依赖的参数不是类或接口,只是一个普通变量,这时候就无法从容器中获取依赖参数了,也就无法实例化类了。那么接下来我们就支持一个新功能,在调用make方法时,支持传第二个参数$parameters,这是一个数组,无法从容器中获取的依赖,就从这个数组中找。当然,make方法是用不到这个参数的,因为它不负责实例化类,它直接传给build方法。在build方法寻找依赖的参数时,就先从$parameters中找。这样就实现了自定义依赖参数。需要注意的一点是,build方法是按照参数的名字来找依赖的,所以parameters中的键名也必须跟__construct中参数名一致。class ParametersContainer extends InjectionContainer{ // 获取服务 public function make($name, array $parameters = []) { if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 执行回调函数并返回 $instance = call_user_func($this->bindings[$name][‘callback’]); if ($this->bindings[$name][‘shared’]) { // 标记为单例时,存储到服务中 $this->instances[$name] = $instance; } } else { // 使用build方法构建此类 $instance = $this->build($name, $parameters); } return $instance; } // 构建一个类,并自动注入服务 public function build($class, array $parameters = []) { $reflector = new ReflectionClass($class); $constructor = $reflector->getConstructor(); if (is_null($constructor)) { // 没有构造函数,直接new return new $class(); } $dependencies = []; // 获取构造函数所需的参数 foreach ($constructor->getParameters() as $dependency) { if (isset($parameters[$dependency->getName()])) { // 先从自定义参数中查找 $dependencies[] = $parameters[$dependency->getName()]; continue; } if (is_null($dependency->getClass())) { // 参数类型不是类或接口时,无法从容器中获取依赖 if ($dependency->isDefaultValueAvailable()) { // 查找默认值,如果有就使用默认值 $dependencies[] = $dependency->getDefaultValue(); } else { // 无法提供类所依赖的参数 throw new Exception(‘找不到依赖参数:’ . $dependency->getName()); } } else { // 参数类型是类时,就用make方法构建该类 $dependencies[] = $this->make($dependency->getClass()->name); } } return $reflector->newInstanceArgs($dependencies); }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //class Redis{}class Cache{ protected $redis; protected $name; protected $default; // 构造函数中依赖Redis服务和name参数,name的类型不是类,无法从容器中查找 public function __construct(Redis $redis, $name, $default = ‘默认值’) { $this->redis = $redis; $this->name = $name; $this->default = $default; }}$container = new ParametersContainer();// 绑定Redis服务$container->singleton(Redis::class, function () { return new Redis();});// 构建Cache类$cache = $container->make(Cache::class, [’name’ => ’test’]);var_dump($cache);提示:实际上,Laravel容器的build方法并没有第二个参数$parameters,它是用类属性来维护自定义参数。原理都是一样的,只是实现方式不一样。这里为了方便理解,不引入过多概念。7. 服务别名别名可以理解成小名、外号。服务别名就是给已绑定的服务设置一些外号,使我们通过外号也能找到该服务。这个就比较简单了,我们增加一个新的数组$aliases,用来存储别名。再增加一个方法alias,用来让外部注册别名。唯一需要我们修改的地方,就是在make时,要先从$aliases中找到真实的服务名。class AliasContainer extends ParametersContainer{ // 服务别名 protected $aliases = []; // 给服务绑定一个别名 public function alias($alias, $name) { $this->aliases[$alias] = $name; } // 获取服务 public function make($name, array $parameters = []) { // 先用别名查找真实服务名 $name = isset($this->aliases[$name]) ? $this->aliases[$name] : $name; return parent::make($name, $parameters); }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //$container = new AliasContainer();// 绑定服务$container->instance(’text’, ‘这是一个字符串’);// 给服务注册别名$container->alias(‘string’, ’text’);$container->alias(‘content’, ’text’);var_dump($container->make(‘string’));var_dump($container->make(‘content’));8. 扩展绑定有时候我们需要给已绑定的服务做一个包装,这时候就用到扩展绑定了。我们先看一个实际的用法,理解它的作用后,才看它是如何实现的。// 绑定日志服务$container->singleton(’log’, new Log());// 对已绑定的服务再次包装$container->extend(’log’, function(Log $log){ // 返回了一个新服务 return new RedisLog($log);});现在我们看它是如何实现的。增加一个$extenders数组,用来存放扩展器。再增加一个extend方法,用来注册扩展器。然后在make方法返回$instance之前,按顺序依次调用之前注册的扩展器。class ExtendContainer extends AliasContainer{ // 存放扩展器的数组 protected $extenders = []; // 给服务绑定扩展器 public function extend($name, $extender) { if (isset($this->instances[$name])) { // 已经实例化的服务,直接调用扩展器 $this->instances[$name] = $extender($this->instances[$name]); } else { $this->extenders[$name][] = $extender; } } // 获取服务 public function make($name, array $parameters = []) { $instance = parent::make($name, $parameters); if (isset($this->extenders[$name])) { // 调用扩展器 foreach ($this->extenders[$name] as $extender) { $instance = $extender($instance); } } return $instance; }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //class Redis{ public $name; public function __construct($name = ‘default’) { $this->name = $name; } public function setName($name) { $this->name = $name; }}$container = new ExtendContainer();// 绑定Redis服务$container->singleton(Redis::class, function () { return new Redis();});// 给Redis服务绑定一个扩展器$container->extend(Redis::class, function (Redis $redis) { $redis->setName(‘扩展器’); return $redis;});$redis = $container->make(Redis::class);var_dump($redis->name);9. 上下文绑定有时侯我们可能有两个类使用同一个接口,但希望在每个类中注入不同的实现,例如两个控制器,分别为它们注入不同的Log服务。class ApiController{ public function __construct(Log $log) { }}class WebController{ public function __construct(Log $log) { }}最终我们要用以下方式实现:// 当ApiController依赖Log时,给它一个RedisLog$container->addContextualBinding(‘ApiController’,‘Log’,new RedisLog());// 当WebController依赖Log时,给它一个FileLog$container->addContextualBinding(‘WebController’,‘Log’,new FileLog());为了更直观更方便更语义化的使用,我们把这个过程改成链式操作:$container->when(‘ApiController’) ->needs(‘Log’) ->give(new RedisLog());我们增加一个$context数组,用来存储上下文。同时增加一个addContextualBinding方法,用来注册上下文绑定。以ApiController为例,$context的真实模样是:$context[‘ApiController’][‘Log’] = new RedisLog();然后build方法实例化类时,先从上下文中查找依赖参数,就实现了上下文绑定。接下来,看看链式操作是如何实现的。首先定义一个类Context,这个类有两个方法,needs和give。然后在容器中,增加一个when方法,它返回一个Context对象。在Context对象的give方法中,我们已经具备了注册上下文所需要的所有参数,所以就可以在give方法中调用addContextualBinding来注册上下文了。class ContextContainer extends ExtendContainer{ // 依赖上下文 protected $context = []; // 构建一个类,并自动注入服务 public function build($class, array $parameters = []) { $reflector = new ReflectionClass($class); $constructor = $reflector->getConstructor(); if (is_null($constructor)) { // 没有构造函数,直接new return new $class(); } $dependencies = []; // 获取构造函数所需的参数 foreach ($constructor->getParameters() as $dependency) { if (isset($this->context[$class]) && isset($this->context[$class][$dependency->getName()])) { // 先从上下文中查找 $dependencies[] = $this->context[$class][$dependency->getName()]; continue; } if (isset($parameters[$dependency->getName()])) { // 从自定义参数中查找 $dependencies[] = $parameters[$dependency->getName()]; continue; } if (is_null($dependency->getClass())) { // 参数类型不是类或接口时,无法从容器中获取依赖 if ($dependency->isDefaultValueAvailable()) { // 查找默认值,如果有就使用默认值 $dependencies[] = $dependency->getDefaultValue(); } else { // 无法提供类所依赖的参数 throw new Exception(‘找不到依赖参数:’ . $dependency->getName()); } } else { // 参数类型是一个类时,就用make方法构建该类 $dependencies[] = $this->make($dependency->getClass()->name); } } return $reflector->newInstanceArgs($dependencies); } // 绑定上下文 public function addContextualBinding($when, $needs, $give) { $this->context[$when][$needs] = $give; } // 支持链式方式绑定上下文 public function when($when) { return new Context($when, $this); }}class Context{ protected $when; protected $needs; protected $container; public function __construct($when, ContextContainer $container) { $this->when = $when; $this->container = $container; } public function needs($needs) { $this->needs = $needs; return $this; } public function give($give) { // 调用容器绑定依赖上下文 $this->container->addContextualBinding($this->when, $this->needs, $give); }}// ———– ↓↓↓↓示例代码↓↓↓↓ ———– //class Dog{ public $name; public function __construct($name) { $this->name = $name; }}class Cat{ public $name; public function __construct($name) { $this->name = $name; }}$container = new ContextContainer();// 给Dog类设置上下文绑定$container->when(Dog::class) ->needs(’name’) ->give(‘小狗’);// 给Cat类设置上下文绑定$container->when(Cat::class) ->needs(’name’) ->give(‘小猫’);$dog = $container->make(Dog::class);$cat = $container->make(Cat::class);var_dump(‘Dog:’ . $dog->name);var_dump(‘Cat:’ . $cat->name);10. 完整代码class Container{ // 已绑定的服务 protected $instances = []; // 已绑定的回调函数 protected $bindings = []; // 服务别名 protected $aliases = []; // 存放扩展器的数组 protected $extenders = []; // 依赖上下文 protected $context = []; // 绑定服务实例 public function instance($name, $instance) { $this->instances[$name] = $instance; } // 绑定服务 public function bind($name, $instance, $shared = false) { if ($instance instanceof Closure) { // 如果$instance是一个回调函数,就绑定到bindings。 $this->bindings[$name] = [ ‘callback’ => $instance, // 标记是否单例 ‘shared’ => $shared ]; } else { // 调用make方法,创建实例 $this->instances[$name] = $this->make($name); } } // 绑定一个单例 public function singleton($name, $instance) { $this->bind($name, $instance, true); } // 给服务绑定一个别名 public function alias($alias, $name) { $this->aliases[$alias] = $name; } // 给服务绑定扩展器 public function extend($name, $extender) { if (isset($this->instances[$name])) { // 已经实例化的服务,直接调用扩展器 $this->instances[$name] = $extender($this->instances[$name]); } else { $this->extenders[$name][] = $extender; } } // 获取服务 public function make($name, array $parameters = []) { // 先用别名查找真实服务名 $name = isset($this->aliases[$name]) ? $this->aliases[$name] : $name; if (isset($this->instances[$name])) { return $this->instances[$name]; } if (isset($this->bindings[$name])) { // 执行回调函数并返回 $instance = call_user_func($this->bindings[$name][‘callback’]); if ($this->bindings[$name][‘shared’]) { // 标记为单例时,存储到服务中 $this->instances[$name] = $instance; } } else { // 使用build方法构建此类 $instance = $this->build($name, $parameters); } if (isset($this->extenders[$name])) { // 调用扩展器 foreach ($this->extenders[$name] as $extender) { $instance = $extender($instance); } } return $instance; } // 构建一个类,并自动注入服务 public function build($class, array $parameters = []) { $reflector = new ReflectionClass($class); $constructor = $reflector->getConstructor(); if (is_null($constructor)) { // 没有构造函数,直接new return new $class(); } $dependencies = []; // 获取构造函数所需的参数 foreach ($constructor->getParameters() as $dependency) { if (isset($this->context[$class]) && isset($this->context[$class][$dependency->getName()])) { // 先从上下文中查找 $dependencies[] = $this->context[$class][$dependency->getName()]; continue; } if (isset($parameters[$dependency->getName()])) { // 从自定义参数中查找 $dependencies[] = $parameters[$dependency->getName()]; continue; } if (is_null($dependency->getClass())) { // 参数类型不是类或接口时,无法从容器中获取依赖 if ($dependency->isDefaultValueAvailable()) { // 查找默认值,如果有就使用默认值 $dependencies[] = $dependency->getDefaultValue(); } else { // 无法提供类所依赖的参数 throw new Exception(‘找不到依赖参数:’ . $dependency->getName()); } } else { // 参数类型是一个类时,就用make方法构建该类 $dependencies[] = $this->make($dependency->getClass()->name); } } return $reflector->newInstanceArgs($dependencies); } // 绑定上下文 public function addContextualBinding($when, $needs, $give) { $this->context[$when][$needs] = $give; } // 支持链式方式绑定上下文 public function when($when) { return new Context($when, $this); }}class Context{ protected $when; protected $needs; protected $container; public function __construct($when, Container $container) { $this->when = $when; $this->container = $container; } public function needs($needs) { $this->needs = $needs; return $this; } public function give($give) { // 调用容器绑定依赖上下文 $this->container->addContextualBinding($this->when, $this->needs, $give); }} ...

April 14, 2019 · 7 min · jiezi

K8S 生态周报| 2019.04.01~2019.04.07

「K8S 生态周报」内容主要包含我所接触到的 K8S 生态相关的每周值得推荐的一些信息。欢迎订阅知乎专栏「k8s生态」。Kubernetes client-go v11.0.0 正式发布这是最后一个使用 dep 作为依赖管理的版本,后续版本将转向使用 go modules.Kubernetes 生态中的相关项目大多都已转向或正在转向使用 go modules 了,这也是一个技术风向,理性选择。Releasecontainerd 1.2.6 正式发布这是 containerd 1.2 的第 6 个 patch 版本,主要更新:在默认的 seccomp profile 白名单增加了 io_pgetevents 和 statx 这两个系统调用;修复了在 1.2.5 中自定义 cgroup path 无法工作的 bug;更新 CNI 插件到 v0.7.5 以修复 CVE-2019-9946;更新 runc 版本,修复在无 SELinux 系统下的失败情况;当然还有一些其他的改进和修复,比如修复了 pod 的 UTS namespace 等,建议阅读 ReleaseNote。Docker CE 19.03.0-beta1 版本发布这是 Docker CE 修改发布周期后,第二个发布的版本,上个版本是 Docker CE 18.09 。从发布周期来看,由原先的季度发布改成半年发布,也意味着 Docker 的日渐成熟。正式版估计会在 5 月发布,从 beta 版中能看到一些主要更新:API 更新至 v1.40;允许以非 root 用户运行 dockerd (Rootless mode),这将更有利于容器安全(这也是我最期待的一个特性);移除 v1 manifest 支持;移除 AuFS 存储驱动支持,会有提示信息(当前是废弃,还未完全移除,在上个版本中 devicemapper 也已被标记为废弃);实验性的对 compose 和 Kubernetes 提供一些额外支持,比如 x-pull-secret 和 x-pull-policy;实验性的对 Windows 和 LCOW 提供一些支持:比如提供对 cpu 和 内存的限制;除此之外,在 builder 和 API 方面也都有一些修复和改进,建议阅读 ReleaseNote。推荐阅读: Linkerd v2 从产品中吸取的教训Linkerd v2 使用 Go 和 Rust 进行了重写,这篇文章是在 Linkerd v2 发布 6 个月之后写的,该团队认为使用 Go 和 Rust 重写是非常值得的,并且也已经在生产中得到了验证。文章内容不错,推荐阅读。文章地址 Linkerd v2: How Lessons from Production Adoption Resulted in a Rewrite of the Service Mesh。可以通过下面二维码订阅我的文章公众号【MoeLove】 ...

April 8, 2019 · 1 min · jiezi

为了学习理解依赖注入和路由,自己撸了一个PHP框架

如何提高自己编写代码的能力呢?作为web开发者,我们通常都是基于面向对象OOP来开发的,所以面向对象的设计能力或者说设计模式的运用能力尤为重要,当然还有开发语言本身特性和基础的灵活运用。我们可以去阅读一些优秀的开源项目,理解里面的代码设计,去学习和造轮子来提高自己。我比较关注web framework中的路由、HTTP、依赖注入容器这几部分,路由和http处理是web框架必不可少的,整个框架的服务对象依赖解析也是很重要的,有了依赖注入容器可以实现类很好的解耦。Dependency Injection Container先来说下什么是依赖注入,依赖注入是一种允许我们从硬编码的依赖中解耦出来,从而在运行时或者编译时能够修改的软件设计模式(来自维基百科 Wikipedia)。依赖注入通过构造注入,函数调用或者属性的设置来提供组件的依赖关系。下面的代码中有一个 Database 的类,它需要一个适配器来与数据库交互。我们在构造函数里实例化了适配器,从而产生了耦合。这会使测试变得很困难,而且 Database 类和适配器耦合的很紧密。<?phpnamespace Database;class Database{ protected $adapter; public function __construct() { $this->adapter = new MySqlAdapter; }}class MysqlAdapter {}这段代码可以用依赖注入重构,从而解耦<?phpnamespace Database;class Database{ protected $adapter; public function __construct(MySqlAdapter $adapter) { $this->adapter = $adapter; }}class MysqlAdapter {}现在我们通过外界给予 Database 类的依赖,而不是让它自己产生依赖的对象。我们甚至能用可以接受依赖对象参数的成员函数来设置,或者如果 $adapter 属性本身是 public的,我们可以直接给它赋值。根据依赖注入的概念,我们的框架实现了这些特性。Dependency injection Container基于PSR-11规范实现,包括3种注入实现方式:构造方法注入(Constructor Injection)、setter方法或属性注入(Setter Injection)、匿名回调函数注入,代码如下:1.构造方法注入(Constructor Injection)<?php declare(strict_types=1);namespace Examples;use Eagle\DI\Container;class Foo{ /** * @var \Examples\Bar / public $bar; /* * Foo constructor. * @param \Examples\Bar $bar / public function __construct(Bar $bar) { $this->bar = $bar; }}/class Bar {}/class Bar { public $baz; public function __construct(Baz $baz) { $this->baz = $baz; }}class Baz {}$container = new Container;$container->set(Foo::class)->addArguments(Bar::class);$container->set(Bar::class)->addArguments(Baz::class);$foo = $container->get(Foo::class);var_dump($foo, $foo->bar);var_dump($foo instanceof Foo); // truevar_dump($foo->bar instanceof Bar); // truevar_dump($foo->bar->baz instanceof Baz); // true2.方法注入<?phpdeclare(strict_types=1);namespace Examples;require ‘vendor/autoload.php’;use Eagle\DI\Container;class Controller{ public $model; public function __construct(Model $model) { $this->model = $model; }}class Model{ public $pdo; public function setPdo(\PDO $pdo) { $this->pdo = $pdo; }}$container = new Container;$container->set(Controller::class)->addArguments(Model::class);$container->set(Model::class)->addInvokeMethod(‘setPdo’, [\PDO::class]);$container->set(\PDO::class) ->addArguments([‘mysql:dbname=test;host=localhost’, ‘root’, ‘111111’]);$controller = $container->get(Controller::class);var_dump($controller instanceof Controller); // truevar_dump($controller->model instanceof Model); // truevar_dump($controller->model->pdo instanceof \PDO); // true3.匿名回调函数注入<?phpdeclare(strict_types=1);namespace Examples;require ‘vendor/autoload.php’;use Eagle\DI\Container;class Controller{ public $model; public function __construct(Model $model) { $this->model = $model; }}class Model{ public $pdo; public function setPdo(\PDO $pdo) { $this->pdo = $pdo; }}$container = new Container;$container->set(Controller::class, function () { $pdo = new \PDO(‘mysql:dbname=test;host=localhost’, ‘root’, ‘111111’); $model = new Model; $model->setPdo($pdo); return new Controller($model);});$controller = $container->get(Controller::class);var_dump($controller instanceof Controller); // truevar_dump($controller->model instanceof Model); // truevar_dump($controller->model->pdo instanceof \PDO); // true自动布线 (auto wiring)<?phpdeclare(strict_types=1);namespace AutoWiring;require ‘vendor/autoload.php’;use Eagle\DI\ContainerBuilder;class Foo{ /* * @var \AutoWiring\Bar / public $bar; /* * @var \AutoWiring\Baz / public $baz; /* * Construct. * * @param \AutoWiring\Bar $bar * @param \AutoWiring\Baz $baz / public function __construct(Bar $bar, Baz $baz) { $this->bar = $bar; $this->baz = $baz; }}class Bar{ /* * @var \AutoWiring\Bam / public $bam; /* * Construct. * * @param \AutoWiring\Bam $bam */ public function __construct(Bam $bam) { $this->bam = $bam; }}class Baz{ // ..}class Bam{ // ..}$container = new ContainerBuilder;$container = $container->build();$foo = $container->get(Foo::class);var_dump($foo instanceof Foo); // truevar_dump($foo->bar instanceof Bar); // truevar_dump($foo->baz instanceof Baz); // truevar_dump($foo->bar->bam instanceof Bam); // trueRoute再介绍下路由使用的例子,route可以使用symfony的http foundation组件来处理HTTP请求(http messages)。<?phprequire ‘vendor/autoload.php’;use Eagle\Route\Router;use Symfony\Component\HttpFoundation\Request;$router = new Router();$router->get(’/articles’, function () { return ‘This is articles list’;});$router->get(’/articles/{id:\d+}’, function ($id) { return ‘Article id: ’ . $id;});$router->get(’/articles/{id:\d+}[/{title}]’, function ($id, $title) { return ‘Article id: ’ . $id . ‘, title: ’ . $title;});/匹配处理路由组/$router->group(’/articles’, function () use ($router) { $router->get(’/list’, function() { return ‘This is articles list’; }); $router->get(’/detail’, function ($id, $title) { return ‘Article detail id: ’ . $id . ‘, title: ’ . $title; });});$request = new Request();$routeHandler = $router->getRouteHandler();$response = $routeHandler->handle($request);echo $response;其它的ORM、cache、filesystem、session、validation等组件可以使用composer来由用户自由扩展。项目地址 https://github.com/parvinShi/… ...

March 11, 2019 · 2 min · jiezi

Kubernetes v1.13对原始块卷支持转移到beta

作者:Ben Swartzlander(NetApp),Saad Ali(谷歌)Kubernetes v1.13将原始块卷(raw block volume)支持转移到beta。此功能允许持久卷(persistent volume)作为块设备(block device),而不是作为已安装的文件系统在容器内部公开。什么是块设备?块设备允许随机访问固定大小的块中的数据。硬盘驱动器、SSD和CD-ROM驱动器都是块设备的示例。持久存储通常以分层方式实现,在块设备(如旋转磁盘或SSD)之上使用文件系统(如ext4)。然后,应用程序读取和写入文件,而不是在块上操作。操作系统负责使用指定的文件系统,将文件作为块读取和写入底层设备。值得注意的是,整个磁盘都是块设备,磁盘分区也是,存储区域网络(SAN)设备的LUN也是。为什么要将原始块卷添加到kubernetes?有些专门的应用程序需要直接访问块设备,例如,文件系统层会引入不必要的开销。最常见的情况是数据库,它们更喜欢直接在底层存储上组织数据。原始块设备也常用于任何本身实现某种存储服务的软件(软件定义的存储系统)。从程序员的角度来看,块设备是一个非常大的字节数组,通常具有一些最小的读写粒度,通常为512字节,但更常见为4K或更大。随着在Kubernetes内部运行数据库软件和存储基础架构软件变得越来越普遍,Kubernetes中对原始块设备支持的需求变得更加重要。哪个卷插件支持原始块?在发布此博客时,以下树内(in-tree)卷类型支持原始块:AWS EBSAzure DiskCinderFibre ChannelGCE PDiSCSILocal volumesRBD (Ceph)Vsphere树外(Out-of-tree)CSI卷驱动程序也可以支持原始块卷。 Kubernetes CSI对原始块卷的支持目前是alpha。请参阅此处的文档。Kubernetes原始块卷API原始块与普通卷有很多共同点。两者都是通过创建绑定到PersistentVolume对象的PersistentVolumeClaim对象来请求的,并通过将它们包含在PodSpec的volumes数组中而附加到Kubernetes中的Pod。但是有两个重要的区别。首先,要请求原始块PersistentVolumeClaim,必须在PersistentVolumeClaimSpec中设置volumeMode =“Block”。将volumeMode留空与指定volumeMode =“Filesystem”相同,这会导致传统行为。PersistentVolumes在其PersistentVolumeSpec中也有一个volumeMode字段,而“Block”类型的PVC只能绑定到“Block”类型的PV,而“Filesystem”PVC只能绑定到“Filesystem”PV。其次,在Pods中使用原始块卷时,必须在PodSpec的Container部分而不是VolumeMount中指定VolumeDevice。VolumeDevices具有devicePaths而不是mountPaths,并且在容器内部,应用程序将在该路径中看到设备而不是已安装的文件系统。应用程序打开、读取和写入容器内的设备节点,就像它们将与非容器化或虚拟化环境中的系统上的任何块设备进行交互一样。创建新的原始块PVC首先,确保你选择的存储类关联的配置程序是支持原始块的配置程序。然后创建PVC。apiVersion: v1kind: PersistentVolumeClaimmetadata: name: my-pvcspec: accessModes: - ReadWriteMany volumeMode: Block storageClassName: my-sc resources: requests: storage: 1Gi使用原始块PVC在pod定义中使用PVC时,可以选择块设备的设备路径,而不是文件系统的安装路径。apiVersion: v1kind: Podmetadata: name: my-podspec: containers: - name: my-container image: busybox command: - sleep - “3600” volumeDevices: - devicePath: /dev/block name: my-volume imagePullPolicy: IfNotPresent volumes: - name: my-volume persistentVolumeClaim: claimName: my-pvc作为存储供应商,如何在我的CSI插件中添加对原始块设备的支持?CSI插件对原始块支持仍然是alpha,但今天可以添加支持。CSI规范详细说明了如何处理具有BlockVolume功能而不是MountVolume功能的卷请求。CSI插件可以支持这两种卷。有关更多详细信息,请参阅此处。问题/陷阱因为块设备实际上是设备,所以可以从容器内部对它们执行低级操作,这是文件系统卷无法实现的。例如,实际上是SCSI磁盘的块设备支持使用Linux ioctls向设备发送SCSI命令。默认情况下,Linux不允许容器将SCSI命令从容器内部发送到磁盘。为此,你必须将SYS_RAWIO功能授予容器安全上下文(context)以允许此操作。请参阅此处的文档。此外,虽然Kubernetes保证向容器提供块设备,但不能保证它实际上是SCSI磁盘或任何其他类型的磁盘。用户必须确保所需的磁盘类型与其pod一起使用,或者仅部署可处理各种块设备类型的应用程序。怎样能了解更多?在此处查看有关快照功能的其他文档。我如何参与?加入Kubernetes存储SIG和CSI社区,帮助我们添加更多优秀功能,并改进现有功能如原始块存储!鸣谢特别感谢帮助Kubernetes增加块卷支持的所有贡献者,包括:Ben Swartzlander(https://github.com/bswartz)Brad Childs(https://github.com/childsb)Erin Boyd(https://github.com/erinboyd)Masaki Kimura(https://github.com/mkimuram)Matthew Wong(https://github.com/wongma7)Michelle Au(https://github.com/msau42)Mitsuhiro Tanino(https://github.com/mtanino)Saad Ali(https://github.com/saad-ali)KubeCon + CloudNativeCon和Open Source Summit大会日期:会议日程通告日期:2019 年 4 月 10 日会议活动举办日期:2019 年 6 月 24 至 26 日KubeCon + CloudNativeCon和Open Source Summit赞助方案KubeCon + CloudNativeCon和Open Source Summit多元化奖学金现正接受申请KubeCon + CloudNativeCon和Open Source Summit即将首次合体落地中国KubeCon + CloudNativeCon和Open Source Summit购票窗口,立即购票! ...

March 8, 2019 · 1 min · jiezi

CNCF参与Google Summer of Code 2019!感兴趣的学生现在是加入的最佳时机

Google Summer of Code(GSoC)是面向开源开发领域新贡献者的最知名和最受欢迎的计划之一,我们很高兴地宣布CNCF(Cloud Native Computing Foundation,云原生计算基金会)参与GSoC 2019!CNCF是过去几年(包括2017年和2018年)GSoC中最活跃的参与者之一,我们将在2019年继续这项工作,有近50个项目构想涉及13个CNCF项目。如果你是学生,计划参加GSoC 2019,现在是审查项目构想,并与导师开始讨论的最佳时机。这里分享一篇去年浙江大学研究生,在谐云实习的Jian Liu,有关“GSoC 18:Kata Containers对containerd的支持”的经验。Kubernetes项目构想Integrate kube-batch with pytorch-operator/mxnet-operatorImplement volume snapshotting support into the external Manila provisionerEnable full e2e tests for external Azure cloud providerCSI driver for AzureDiskCSI driver for AzureFileCSI driver for BlobfuseStreamline and simplify SASS for the Kubernetes websiteFully automate API and reference doc generationAdd support for Custom Resource Definitions to the DashboardAdd plugin mechanism to the DashboardPrometheus项目构想Benchmarks for TSDBContinue the work on PrombenchPersist Retroactive Rule ReevaluationsOptimize queries using regex matchers for set lookupsPackage for bulk importsOpen Policy Agent (OPA)项目构想IntelliJ plugin to experiment with and visualize policy evaluationInteractive website detailing OPA integrationsIntegration with IPTablesCoreDNS项目构想Support source-IP based query block/allowSupport Google Cloud DNS backendSupport Azure DNS backendTiKV项目构想Migrate to tower-grpcIntroduce other storage enginesBuild TiKV clients in different languagesAuto-tune RocksDBRook项目构想Upgrade Rook to a more advanced operator/controller frameworkStorage provider features and enhancementsEnable multiple network interfaces for Rook storage providersEnhance and extend the Rook framework for storage solutionsExpand coverage and scope of Rook’s continuous integration (CI) systemDynamic provisioning for portable workloadsLinkerd and Envoy项目构想Benchmarks for Linkerd and EnvoyVirtual Kubelet项目构想Conformance testing for Virtual KubeletLinkerd项目构想Cross-cloud integration testingAuto-UpdateConformance ValidationAlertmanager IntegrationKafka Introspectionrkt项目构想Add support for the OCI runtime spec by implementing a runc stage2Add native OCI image supportcontainerd项目构想Snapshotter implementation for block devicesp2p or remote blob store implementationFalco项目构想Improved Falco OutputsAdditional Event SourcesLayer 7 Inspection and DetectionFalco integration with AI/ML platformsPrometheus Metrics ExporterPerformance Analysis and OptimizationFalco rules profiles for applications and security benchmarksCortex项目构想Improve Ingester HandoverCentralized Rate LimitingUse etcd in Cortex请在CNCF的GitHub页面上查看2019年项目构想的完整列表,包括描述、推荐技巧和导师资料等。KubeCon + CloudNativeCon和Open Source Summit大会日期:会议日程通告日期:2019 年 4 月 10 日会议活动举办日期:2019 年 6 月 24 至 26 日KubeCon + CloudNativeCon和Open Source Summit赞助方案KubeCon + CloudNativeCon和Open Source Summit多元化奖学金现正接受申请KubeCon + CloudNativeCon和Open Source Summit即将首次合体落地中国KubeCon + CloudNativeCon和Open Source Summit购票窗口,立即购票! ...

March 6, 2019 · 2 min · jiezi

恭喜 containerd 毕业

今年的第一篇文章更新,带来一个重大的消息。CNCF(云原生计算基金会)在美国时间 2019 年 2 月 28 日宣布 containerd 今天正式毕业了。这是 CNCF 中毕业的第 5 个项目,之前已经毕业的项目为 Kubernetes、Prometheus、Envoy 和 CoreDNS 。containerd 2014 年从 Docker 孵化出来,最初是作为 Docker 引擎的底层管理器;在 2017 年 3 月被 CNCF 接受后,containerd 几乎成为了行业容器运行引擎的标准,它专注于简单,健壮和可移植性,任何人都可以使用它来构建自己的容器引擎/平台。“When Docker contributed containerd to the community, our goal was to share a robust and extensible runtime that millions of users and tens of thousands of organizations have already standardized on as part of Docker Engine,” said Michael Crosby, containerd maintainer and Docker engineer.截至目前,containerd 的 GitHub 项目有 3533 个 Star ;221 个 Watch 和 726 个 Fork,贡献者超过了 166 位。相信在之后也会发展的更好。下面附上 containerd 的架构图,以后会更新关于 containerd 相关原理的文章。再次恭喜 containerd 毕业。可以通过下面二维码订阅我的文章公众号【MoeLove】 ...

March 1, 2019 · 1 min · jiezi

CNCF宣布containerd毕业

阿里云、AWS、Cloud Foundry、Docker、Google、IBM、Rancher Labs以及更多支持促进生态系统最广泛采用的容器运行引擎加利福尼亚州旧金山,2018年2月28日 - 支持Kubernetes®和Prometheus™等开源技术的CNCF®(云原生计算基金会Cloud Native Computing Foundation®)今天宣布,在Kubernetes、Prometheus、Envoy和CoreDNS之后,containerd是第五个毕业项目。要从孵化的成熟水平到毕业,项目必须表现出蓬勃的采用、多样性、正式的治理过程,以及对社区永续性和包容性的坚定承诺。“在差不多两年前被CNCF接纳后,containerd继续看到显着的发展势头,展示了对基础容器技术的需求。”CNCF首席技术官Chris Aniszczyk说。“项目社区投放了大量的工作和协作,以稳定核心容器运行引擎的开发和测试,社区也努力扩大其维护者和采用基础,同时通过外部安全审计,所以我很激动看到项目毕业。”在2014年出生于Docker,containerd最初是Docker引擎的低层运行管理器。继2017年3月被CNCF接受之后,containerd已经成为行业标准的容器运行引擎,专注于简单、健壮和可移植,其最广泛的用途和采用是作为Docker引擎和OCI runc执行器的中间层。 “当Docker向社区贡献containerd的时候,我们的目标是分享一个强大且可扩展的运行引擎,这个引擎作为Docker Engine的一部分已经是数百万用户和成千上万的组织的标准。”containerd维护者和Docker工程师Michael Crosby说。“随着我们扩大范围以满足Docker平台和Kubernetes生态系统等现代化容器平台的需求,看到containerd在过去一年的采用和进一步的创新得到了回报。随着containerd的采用不断增长,我们期待整个生态系统继续合作推动我们的行业发展。”“IBM Cloud Kubernetes Service(IKS)致力于为我们的客户提供卓越的托管Kubernetes体验。为实现这一目标,我们一直在努力简化IKS的架构和运营状况。”IBM Cloud Kubernetes Service杰出工程师Dan Berg说。“迁移到containerd有助于简化我们替客户配置和管理的Kubernetes架构。通过采用containerd作为我们的容器引擎,我们减少了架构中的附加层,从而为我们的客户改善了运营,并提高了服务性能。”containerd自成立以来就拥有各种维护者和审阅者,目前有来自阿里巴巴、Cruise Automation、Docker、Facebook、Google、华为、IBM、微软、NTT、特斯拉等公司的14位提交者,4,406份提交和166位贡献者。可以在DevStats上找到containerd项目统计信息、贡献者统计信息等。“自成立以来,阿里巴巴一直使用containerd,我们很高兴看到该项目达到了这一里程碑。containerd作为容器运营引擎的开放、可靠和通用基础发挥着关键作用。在阿里云,我们的阿里云Kubernetes服务和无服务器Kubernetes利用了containerd的简单、稳健和可扩展性。”阿里云高级工程师Li Yi说。“阿里巴巴团队将继续致力于社区以推动创新。”为了正式从孵化状态毕业,该项目采用了CNCF行为准则,执行了独立的安全审计,并确定了自己的治理结构以发展社区。此外,containerd还必须获得(并维护)核心基础设施倡议(Core Infrastructure Initiative,CII)最佳实践徽章。CII徽章于2018年9月1日完成,显示了对代码质量和安全最佳实践的持续承诺。containerd背景containerd是行业标准的容器运行引擎,强调简单、健壮和可移植性。containerd可用作Linux和Windows的守护程序。containerd管理其主机系统的完整容器生命周期,从镜像传输和存储,到容器执行和监督,到低级存储,到网络附件等。有关下载、文档以及如何参与,请到https://github.com/containerd…。KubeCon + CloudNativeCon和Open Source Summit大会日期:会议日程通告日期:2019 年 4 月 10 日会议活动举办日期:2019 年 6 月 24 至 26 日KubeCon + CloudNativeCon和Open Source Summit赞助方案KubeCon + CloudNativeCon和Open Source Summit多元化奖学金现正接受申请KubeCon + CloudNativeCon和Open Source Summit即将首次合体落地中国KubeCon + CloudNativeCon和Open Source Summit购票窗口,立即购票!

March 1, 2019 · 1 min · jiezi

PHP实现一个轻量级容器

什么是容器在开发过程中,经常会用到的一个概念就是依赖注入。我们借助依懒注入来解耦代码,选择性的按需加载服务,而这些通常都是借助容器来实现。容器实现对对象的统一管理,并且确保对象实例的唯一性容器可以很轻易的找到有很多实现示例,如 PHP-DI 、 YII-DI 等各种实现,通常他们要么大而全,要么高度适配特定业务,与实际需要存在冲突。出于需要,我们自己造一个轻量级的轮子,为了保持规范,我们基于 PSR-11 来实现。 PSR-11 PSR 是 php-fig 提供的标准化建议,虽然不是官方组织,但是得到广泛认可。PSR-11 提供了容器接口。它包含 ContainerInterface 和 两个异常接口,并提供使用建议。/** * Describes the interface of a container that exposes methods to read its entries. /interface ContainerInterface{ /* * Finds an entry of the container by its identifier and returns it. * * @param string $id Identifier of the entry to look for. * * @throws NotFoundExceptionInterface No entry was found for this identifier. * @throws ContainerExceptionInterface Error while retrieving the entry. * * @return mixed Entry. / public function get($id); /* * Returns true if the container can return an entry for the given identifier. * Returns false otherwise. * * has($id) returning true does not mean that get($id) will not throw an exception. * It does however mean that get($id) will not throw a NotFoundExceptionInterface. * * @param string $id Identifier of the entry to look for. * * @return bool / public function has($id);}实现示例我们先来实现接口中要求的两个方法abstract class AbstractContainer implements ContainerInterface{ protected $resolvedEntries = []; /* * @var array / protected $definitions = []; public function __construct($definitions = []) { foreach ($definitions as $id => $definition) { $this->injection($id, $definition); } } public function get($id) { if (!$this->has($id)) { throw new NotFoundException(“No entry or class found for {$id}”); } $instance = $this->make($id); return $instance; } public function has($id) { return isset($this->definitions[$id]); }实际我们容器中注入的对象是多种多样的,所以我们单独抽出实例化方法。 protected function make($name) { if (isset($this->resolvedEntries[$name])) { return $this->resolvedEntries[$name]; } $definition = $this->definitions[$name]; $params = []; if (is_array($definition) && isset($definition[‘class’])) { $params = $definition; $definition = $definition[‘class’]; unset($params[‘class’]); } $object = $this->reflector($definition, $params); return $this->resolvedEntries[$name] = $object; } public function reflector($concrete, array $params = []) { if ($concrete instanceof \Closure) { return $concrete($params); } elseif (is_string($concrete)) { $reflection = new \ReflectionClass($concrete); $dependencies = $this->getDependencies($reflection); foreach ($params as $index => $value) { $dependencies[$index] = $value; } return $reflection->newInstanceArgs($dependencies); } elseif (is_object($concrete)) { return $concrete; } } /* * @param \ReflectionClass $reflection * @return array / private function getDependencies($reflection) { $dependencies = []; $constructor = $reflection->getConstructor(); if ($constructor !== null) { $parameters = $constructor->getParameters(); $dependencies = $this->getParametersByDependencies($parameters); } return $dependencies; } /* * * 获取构造类相关参数的依赖 * @param array $dependencies * @return array $parameters * / private function getParametersByDependencies(array $dependencies) { $parameters = []; foreach ($dependencies as $param) { if ($param->getClass()) { $paramName = $param->getClass()->name; $paramObject = $this->reflector($paramName); $parameters[] = $paramObject; } elseif ($param->isArray()) { if ($param->isDefaultValueAvailable()) { $parameters[] = $param->getDefaultValue(); } else { $parameters[] = []; } } elseif ($param->isCallable()) { if ($param->isDefaultValueAvailable()) { $parameters[] = $param->getDefaultValue(); } else { $parameters[] = function ($arg) { }; } } else { if ($param->isDefaultValueAvailable()) { $parameters[] = $param->getDefaultValue(); } else { if ($param->allowsNull()) { $parameters[] = null; } else { $parameters[] = false; } } } } return $parameters; }如你所见,到目前为止我们只实现了从容器中取出实例,从哪里去提供实例定义呢,所以我们还需要提供一个方法. /* * @param string $id * @param string | array | callable $concrete * @throws ContainerException */ public function injection($id, $concrete) { if (!is_string($id)) { throw new \InvalidArgumentException(sprintf( ‘The id parameter must be of type string, %s given’, is_object($id) ? get_class($id) : gettype($id) )); } if (is_array($concrete) && !isset($concrete[‘class’])) { throw new ContainerException(‘数组必须包含类定义’); } $this->definitions[$id] = $concrete; }只有这样吗?对的,有了这些操作我们已经有一个完整的容器了,插箱即用。不过为了使用方便,我们可以再提供一些便捷的方法,比如数组式访问。class Container extends AbstractContainer implements \ArrayAccess{ public function offsetExists($offset) { return $this->has($offset); } public function offsetGet($offset) { return $this->get($offset); } public function offsetSet($offset, $value) { return $this->injection($offset, $value); } public function offsetUnset($offset) { unset($this->resolvedEntries[$offset]); unset($this->definitions[$offset]); }}这样我们就拥有了一个功能丰富,使用方便的轻量级容器了,赶快整合到你的项目中去吧。点击这里查看完整代码 ...

January 27, 2019 · 3 min · jiezi

Kubernetes的容器存储接口(CSI)GA了

作者:Saad Ali,Google高级软件工程师Kubernetes实施的容器存储接口(CSI)已在Kubernetes v1.13版本中升级为GA。CSI的支持在Kubernetes v1.9版本中作为alpha引入,并在Kubernetes v1.10版本中升级为beta。GA里程碑表明Kubernetes用户可能依赖于该功能及其API,而不必担心将来回归(regression)导致的向后不兼容的更改。GA功能受Kubernetes弃用(deprecation)政策保护。为何选择CSI?虽然在CSI之前,Kubernetes提供了一个功能强大的卷插件系统,但是在Kubernetes添加对新卷插件的支持是一项挑战:卷插件是“树内”(“in-tree”),这意味着他们的代码是核心Kubernetes代码的一部分,并随核心Kubernetes一起提供二进制文件。希望向Kubernetes添加对其存储系统的支持(或修复现有卷插件中的错误)的供应商被迫与Kubernetes发布流程保持一致。此外,第三方存储代码导致核心Kubernetes二进制文件中的可靠性和安全性问题,代码通常很难(在某些情况下不可能)让Kubernetes维护者进行测试和维护。CSI是作为将任意块和文件存储存储系统暴露于容器编排系统(CO)上,如Kubernetes,的容器化工作负载的标准而开发的。随着容器存储接口的采用,Kubernetes卷层变得真正可扩展。使用CSI,第三方存储供应商可以编写和部署插件,在Kubernetes中暴露新的存储系统,而无需触及核心Kubernetes代码。这为Kubernetes用户提供了更多存储选项,使系统更加安全可靠。新的改变?随着升级到GA,Kubernetes对CSI的实施引入了以下变化:Kubernetes现在与CSI spec v1.0和v0.3兼容(而不是CSI spec v0.2)。CSI spec v0.3和v1.0之间存在重大变化,但Kubernetes v1.13支持这两个版本,因此任何一个版本都适用于Kubernetes v1.13。请注意,随着CSI 1.0 API的发布,使用0.3或更老版本CSI API的CSI驱动程序被弃用(deprecated),并计划在Kubernetes v1.15中删除。CSI spec v0.2和v0.3之间没有重大变化,因此v0.2驱动程序也应该与Kubernetes v1.10.0+一起使用。CSI规范v0.1和v0.2之间存在重大变化,因此在使用Kubernetes v1.10.0+之前,必须将实现非常旧的CSI 0.1驱动程序更新为至少0.2兼容。Kubernetes VolumeAttachment对象(在v1.9 storage v1alpha1 group引入,并在v1.10中添加到v1beta1 group)在v1.13已添加到的storage v1 group。Kubernetes CSIPersistentVolumeSource卷类型已升级为GA。Kubelet设备插件注册机制,即kubelet发现新CSI驱动程序的方式,已在Kubernetes v1.13中提升为GA。如何部署CSI驱动程序?对如何在Kubernetes上部署,或管理现有CSI驱动程序感兴趣的Kubernetes用户,应该查看CSI驱动程序作者提供的文档。如何使用CSI卷?假设CSI存储插件已部署在Kubernetes集群上,用户可以通过熟悉的Kubernetes存储API对象使用CSI卷:PersistentVolumeClaims,PersistentVolumes和StorageClasses。文档在这里。虽然Kubernetes实施CSI是Kubernetes v1.13中的GA功能,但它可能需要以下标志:API服务器二进制文件和kubelet二进制文件:–allow-privileged=true大多数CSI插件都需要双向安装传播(bidirectional mount propagation),只能在特权(privileged)pod启用。只有在此标志设置为true的群集上才允许使用特权pod,这是某些环境(如GCE,GKE和kubeadm)的默认设置。动态配置你可以通过创建指向CSI插件的StorageClass,为支持动态配置(dynamic provisioning)的CSI Storage插件启用卷的自动创建/删除(creation/deletion)。例如,以下StorageClass通过名为“csi-driver.example.com”的CSI卷插件,动态创建“fast-storage”卷。kind: StorageClassapiVersion: storage.k8s.io/v1metadata: name: fast-storageprovisioner: csi-driver.example.comparameters: type: pd-ssd csi.storage.k8s.io/provisioner-secret-name: mysecret csi.storage.k8s.io/provisioner-secret-namespace: mynamespaceGA的新功能,CSI的external-provisioner外部配置商(v1.0.1+)保留以csi.storage.k8s.io/为前缀的参数键。如果密钥(key)不对应于一组已知密钥,则简单地忽略这些值(并且不将其传递给CSI驱动程序)。CSI外部配置商v1.0.1也支持旧的秘密参数密钥(csiProvisionerSecretName,csiProvisionerSecretNamespace等),但被弃用(deprecated),可能会在CSI外部配置商的未来版本中删除。动态配置由PersistentVolumeClaim对象的创建触发。例如,以下PersistentVolumeClaim使用上面的StorageClass触发动态配置。apiVersion: v1kind: PersistentVolumeClaimmetadata: name: my-request-for-storagespec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi storageClassName: fast-storage调用卷配置时,参数类型:pd-ssd和秘密通过CreateVolume调用,递给CSI插件csi-driver.example.com。作为响应,外部卷插件提供新卷,然后自动创建PersistentVolume对象以表示新卷。然后,Kubernetes将新的PersistentVolume对象绑定到PersistentVolumeClaim,使其可以使用。如果快速存储(fast-storage)StorageClass标记为“default”,则不需要在PersistentVolumeClaim中包含storageClassName,默认情况下将使用它。预先配置的卷你可以通过手动创建PersistentVolume对象来表示现有卷,从而在Kubernetes中暴露预先存在的卷。例如,以下PersistentVolume暴露名为“existingVolumeName”的卷,该卷属于名为“csi-driver.example.com”的CSI存储插件。apiVersion: v1kind: PersistentVolumemetadata: name: my-manually-created-pvspec: capacity: storage: 5Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain csi: driver: csi-driver.example.com volumeHandle: existingVolumeName readOnly: false fsType: ext4 volumeAttributes: foo: bar controllerPublishSecretRef: name: mysecret1 namespace: mynamespace nodeStageSecretRef: name: mysecret2 namespace: mynamespace nodePublishSecretRef name: mysecret3 namespace: mynamespace连接(Attaching)和安装(Mounting)你可以在任何pod或pod模板中引用绑定到CSI卷的PersistentVolumeClaim。kind: PodapiVersion: v1metadata: name: my-podspec: containers: - name: my-frontend image: nginx volumeMounts: - mountPath: “/var/www/html” name: my-csi-volume volumes: - name: my-csi-volume persistentVolumeClaim: claimName: my-request-for-storage当引用CSI卷的pod被调度时,Kubernetes将针对外部CSI插件(ControllerPublishVolume、NodeStageVolume、NodePublishVolume等)触发相应的操作,以确保指定的卷被连接(attached)和安装(mounted),并准备好给pod里的容器使用。有关详细信息,请参阅CSI实施设计文档和文档。如何编写CSI驱动程序?kubernetes-csi网站详细介绍了如何在Kubernetes上开发、部署和测试CSI驱动程序。一般而言,CSI驱动程序应与Kubernetes一起部署以下侧车/辅助(sidercar/helper)容器:external-attacher观察Kubernetes VolumeAttachment对象,并触发针对CSI端点的ControllerPublish和ControllerUnpublish操作。external-provisioner观察Kubernetes PersistentVolumeClaim对象,并触发针对CSI端点的CreateVolume和DeleteVolume操作。node-driver-registrar通过Kubelet设备插件机制,使用kubelet注册CSI驱动程序。cluster-driver-registrar (Alpha)通过创建CSIDriver对象,向Kubernetes集群注册CSI驱动程序,该对象使驱动程序能够自定义Kubernetes与其交互的方式。external-snapshotter (Alpha)观察Kubernetes VolumeSnapshot CRD对象,并触发针对CSI端点的CreateSnapshot和DeleteSnapshot操作。livenessprobe可以包含在CSI插件pod中,以启用Kubernetes Liveness Probe机制。存储供应商可以使用这些组件为其插件构建Kubernetes部署,而他们的CSI驱动程序完全不需知道Kubernetes。CSI驱动程序列表CSI驱动程序由第三方开发和维护。你可以在此处找到CSI驱动程序的列表。树内(in-tree)卷插件怎么样?有计划将大多数持久的远程树内卷插件迁移到CSI。有关详细信息,请参阅设计文档。GA的限制CSI的GA实施具有以下限制:短暂(Ephemeral)的本地卷必须创建PVC(不支持pod内联引用CSI卷)。下一步?致力于移动Kubernetes CSI的alpha功能到beta:Raw block volumes拓扑感知。Kubernetes理解和影响CSI卷的配置位置(zone可用区,region地域等)的能力。取决于CSI CRD的功能(例如“跳过附加”和“挂载时的Pod信息”)。卷快照努力完成对本地短暂卷的支持。将远程持久性树内卷插件迁移到CSI。怎样参与?Slack频道wg-csi和谷歌讨论区kubernetes-sig-storage-wg-csi,以及任何标准的SIG存储通信渠道都是接触SIG存储团队的绝佳媒介。像Kubernetes一样,这个项目是许多来自不同背景的贡献者共同努力的结果。我们非常感谢本季度主动帮助项目达成GA的新贡献者:Saad Ali (saad-ali)Michelle Au (msau42)Serguei Bezverkhi (sbezverk)Masaki Kimura (mkimuram)Patrick Ohly (pohly)Luis Pabón (lpabon)Jan Šafránek (jsafrane)Vladimir Vivien (vladimirvivien)Cheng Xing (verult)Xing Yang (xing-yang)David Zhu (davidz627)如果你有兴趣参与CSI或Kubernetes存储系统的任何部分的设计和开发,请加入Kubernetes存储特别兴趣小组(SIG)。我们正在快速成长,一直欢迎新的贡献者。2019年KubeCon + CloudNativeCon中国论坛提案征集(CFP)现已开放KubeCon + CloudNativeCon 论坛让用户、开发人员、从业人员汇聚一堂,面对面进行交流合作。与会人员有 Kubernetes、Prometheus 及其他云原生计算基金会 (CNCF) 主办项目的领导,和我们一同探讨云原生生态系统发展方向。2019年中国开源峰会提案征集(CFP)现已开放在中国开源峰会上,与会者将共同合作及共享信息,了解最新和最有趣的开源技术,包括 Linux、容器、云技术、网络、微服务等;并获得如何在开源社区中导向和引领的信息。大会日期:提案征集截止日期:太平洋标准时间 2 月 15 日,星期五,晚上 11:59提案征集通知日期:2019 年 4 月 1 日会议日程通告日期:2019 年 4 月 3 日幻灯片提交截止日期:6 月 17 日,星期一会议活动举办日期:2019 年 6 月 24 至 26 日2019年KubeCon + CloudNativeCon + Open Source Summit China赞助方案出炉啦 ...

January 22, 2019 · 1 min · jiezi

解决 docker 容器无法通过 IP 访问宿主机问题

问题起源在使用 docker 的过程中我不幸需要在 docker 容器中访问宿主机的 80 端口, 而这个 80 端口是另外一个容器 8080 端口映射出去的. 当我在容器里通过 docker 的网桥 172.17.0.1 访问宿主机时, 居然发现:curl: (7) Failed to connect to 172.17.0.1 port 80: No route to host查找问题原因可以确定的是容器与宿主机是有网络连接的, 因为可以在容器内部通过 172.17.0.1 Ping 通宿主机:root@930d07576eef:/# ping 172.17.0.1PING 172.17.0.1 (172.17.0.1) 56(84) bytes of data.64 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.130 ms也可以在容器内部访问其它内网和外网.iptables 显示也允许 docker 容器访问:# iptables –list | grep DOCKERDOCKER-ISOLATION all – anywhere anywhere DOCKER all – anywhere anywhere Chain DOCKER (1 references)Chain DOCKER-ISOLATION (1 references)之后在查找一些资料后发现这个问题: NO ROUTE TO HOST network request from container to host-ip:port published from other container.解释正如 Docker Community Forms 所言, 这是一个已知的 Bug, 宿主机的 80 端口允许其它计算机访问, 但是不允许来自本机的 Docker 容器访问. 必须通过设置 firewalld 规则允许本机的 Docker 容器访问.gypark 指出可以通过在 /etc/firewalld/zones/public.xml 中添加防火墙规则避免这个问题:<rule family=“ipv4”> <source address=“172.17.0.0/16” /> <accept /></rule>注意这里的 172.17.0.0/16 可以匹配 172.17.xx.xx IP 段的所有 IP.之后重启下防火墙:systemctl restart firewalld之后就可以在 docker 容器内部访问宿主机 80 端口.其它问题实际上当我又用 vmware 新开了一台虚拟机希望能重现这个问题的时候, 发现在新的虚拟机上居然没有类似的问题. 也就是说容器可以直接通过172.17.0.1访问宿主机 80 端口, 查看防火墙配置也没看到有172.17.xx.xx的白名单.猜测是由于在新的虚拟机安装的 docker 是 Docker version 1.12.5, build 047e51b/1.12.5, 也就是 Red Hat 从 docker 开源版本迁出开发的版本, 而之前的是 Docker version 17.06.2-ce, build cec0b72 属于 Docker-CE, 可能是 docker 版本有差异, Red Hat 顺便把那个 Known Bug 修复了. ...

January 9, 2019 · 1 min · jiezi

探索runC(下)

回顾本文接 探索runC(上) 前文讲到,newParentProcess() 根据源自 config.json 的配置,最终生成变量 initProcess ,这个 initProcess 包含的信息主要有cmd 记录了要执行的可执行文件名,即 “/proc/self/exe init”,注意不要和容器要执行的 sleep 5 混淆了cmd.Env 记录了名为 _LIBCONTAINER_FIFOFD=%d 记录的命名管道exec.fifo 的描述符,名为_LIBCONTAINER_INITPIPE=%d记录了创建的 SocketPair 的 childPipe 一端的描述符,名为_LIBCONTAINER_INITTYPE=“standard"记录要创建的容器中的进程是初始进程initProcess 的 bootstrapData 记录了新的容器要创建哪些类型的 Namespace。/* libcontainer/container_linux.go */func (c linuxContainer) start(process Process) error { parent, err := c.newParentProcess(process) / 1. 创建parentProcess (已完成) / err := parent.start(); / 2. 启动这个parentProcess / ……准备工作完成之后,就要调用 start() 方法启动。注意: 此时 sleep 5 线索存储在变量 parent 中runC create的实现原理 (下)start() 函数实在太长了,因此逐段来看/ libcontainer/process_linux.go /func (p initProcess) start() error { p.cmd.Start() p.process.ops = p io.Copy(p.parentPipe, p.bootstrapData) …..}p.cmd.Start() 启动 cmd 中设置的要执行的可执行文件 /proc/self/exe,参数是 init,这个函数会启动一个新的进程去执行该命令,并且不会阻塞。io.Copy 将 p.bootstrapData 中的数据通过 p.parentPipe 发送给子进程/proc/self/exe 正是runc程序自己,所以这里相当于是执行runc init,也就是说,我们输入的是runc create命令,隐含着又去创建了一个新的子进程去执行runc init。为什么要额外重新创建一个进程呢?原因是我们创建的容器很可能需要运行在一些独立的 namespace 中,比如 user namespace,这是通过 setns() 系统调用完成的,而在setns man page中写了下面一段话A multi‐threaded process may not change user namespace with setns(). It is not permitted to use setns() to reenter the caller’s current user names‐pace即多线程的进程是不能通过 setns()改变user namespace的。而不幸的是 Go runtime 是多线程的。那怎么办呢 ?所以setns()必须要在Go runtime 启动之前就设置好,这就要用到cgo了,在Go runtime 启动前首先执行嵌入在前面的 C 代码。具体的做法在nsenter README描述 在runc init命令的响应在文件 init.go 开头,导入 nsenter 包/ init.go /import ( “os” “runtime” “github.com/opencontainers/runc/libcontainer” _ “github.com/opencontainers/runc/libcontainer/nsenter” “github.com/urfave/cli”)而nsenter包中开头通过 cgo 嵌入了一段 C 代码, 调用 nsexec()package nsenter// nsenter.go /#cgo CFLAGS: -Wallextern void nsexec();void attribute((constructor)) init(void) { nsexec();}/import “C"接下来,轮到 nsexec() 完成为容器创建新的 namespace 的工作了, nsexec() 同样很长,逐段来看/ libcontainer/nsenter/nsexec.c /void nsexec(void){ int pipenum; jmp_buf env; int sync_child_pipe[2], sync_grandchild_pipe[2]; struct nlconfig_t config = { 0 }; / * If we don’t have an init pipe, just return to the go routine. * We’ll only get an init pipe for start or exec. / pipenum = initpipe(); if (pipenum == -1) return; / Parse all of the netlink configuration. / nl_parse(pipenum, &config); …… 上面这段 C 代码中,initpipe() 从环境中读取父进程之前设置的pipe(_LIBCONTAINER_INITPIPE记录的的文件描述符),然后调用 nl_parse 从这个管道中读取配置到变量 config ,那么谁会往这个管道写配置呢 ? 当然就是runc create父进程了。父进程通过这个pipe,将新建容器的配置发给子进程,这个过程如下图所示:发送的具体数据在 linuxContainer 的 bootstrapData() 函数中封装成netlink msg格式的消息。忽略大部分配置,本文重点关注namespace的配置,即要创建哪些类型的namespace,这些都是源自最初的config.json文件。至此,子进程就从父进程处得到了namespace的配置,继续往下, nsexec() 又创建了两个socketpair,从注释中了解到,这是为了和它自己的子进程和孙进程进行通信。void nsexec(void){ ….. / Pipe so we can tell the child when we’ve finished setting up. / if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_child_pipe) < 0) // sync_child_pipe is an out parameter bail(“failed to setup sync pipe between parent and child”); / * We need a new socketpair to sync with grandchild so we don’t have * race condition with child. */ if (socketpair(AF_LOCAL, SOCK_STREAM, 0, sync_grandchild_pipe) < 0) bail(“failed to setup sync pipe between parent and grandchild”); }然后就该创建namespace了,看注释可知这里其实有考虑过三个方案first clone then clonefirst unshare then clonefirst clone then unshare最终采用的是方案 3,其中缘由由于考虑因素太多,所以准备之后另写一篇文章分析接下来就是一个大的 switch case 编写的状态机,大体结构如下,当前进程通过clone()系统调用创建子进程,子进程又通过clone()系统调用创建孙进程,而实际的创建/加入namespace是在子进程完成的switch (setjmp(env)) { case JUMP_PARENT:{ ….. clone_parent(&env, JUMP_CHILD); ….. } case JUMP_CHILD:{ …… if (config.namespaces) join_namespaces(config.namespaces); clone_parent(&env, JUMP_INIT); …… } case JUMP_INIT:{ }本文不准备展开分析这个状态机了,而将这个状态机的流程画在了下面的时序图中,需要注意的是以下几点namespaces在runc init 2完成创建runc init 1和runc init 2最终都会执行exit(0),但runc init 3不会,它会继续执行runc init命令的后半部分。因此最终只会剩下runc create进程和runc init 3进程再回到runc create进程func (p initProcess) start() error { p.cmd.Start() p.process.ops = p io.Copy(p.parentPipe, p.bootstrapData); p.execSetns() ……再向 runc init发送了 bootstrapData 数据后,便调用 execSetns() 等待runc init 1进程终止,从管道中得到runc init 3的进程 pid,将该进程保存在 p.process.ops/ libcontainer/process_linux.go */func (p *initProcess) execSetns() error { status, err := p.cmd.Process.Wait() var pid *pid json.NewDecoder(p.parentPipe).Decode(&pid) process, err := os.FindProcess(pid.Pid) p.cmd.Process = process p.process.ops = p return nil}继续 start()func (p *initProcess) start() error { …… p.execSetns() fds, err := getPipeFds(p.pid()) p.setExternalDescriptors(fds) p.createNetworkInterfaces() p.sendConfig() parseSync(p.parentPipe, func(sync syncT) error { switch sync.Type { case procReady: ….. writeSync(p.parentPipe, procRun); sentRun = true case procHooks: ….. // Sync with child. err := writeSync(p.parentPipe, procResume); sentResume = true } return nil }) ……可以看到,runc create又开始通过pipe进行双向通信了,通信的对端自然就是runc init 3进程了,runc init 3进程在执行完嵌入的 C 代码后(实际是runc init 1执行的,但runc init 3也是由runc init 1间接clone()出来的),因此将开始运行 Go runtime,开始响应init命令sleep 5 通过 p.sendConfig() 发送给了runc init进程init命令首先通过 libcontainer.New(””) 创建了一个 LinuxFactory,这个方法在上篇文章中分析过,这里不再解释。然后调用 LinuxFactory 的 StartInitialization() 方法。/ libcontainer/factory_linux.go */// StartInitialization loads a container by opening the pipe fd from the parent to read the configuration and state// This is a low level implementation detail of the reexec and should not be consumed externallyfunc (l LinuxFactory) StartInitialization() (err error) { var ( pipefd, fifofd int envInitPipe = os.Getenv("_LIBCONTAINER_INITPIPE") envFifoFd = os.Getenv("_LIBCONTAINER_FIFOFD") ) // Get the INITPIPE. pipefd, err = strconv.Atoi(envInitPipe) var ( pipe = os.NewFile(uintptr(pipefd), “pipe”) it = initType(os.Getenv("_LIBCONTAINER_INITTYPE")) // // “standard” or “setns” ) // Only init processes have FIFOFD. fifofd = -1 if it == initStandard { if fifofd, err = strconv.Atoi(envFifoFd); err != nil { return fmt.Errorf(“unable to convert _LIBCONTAINER_FIFOFD=%s to int: %s”, envFifoFd, err) } } i, err := newContainerInit(it, pipe, consoleSocket, fifofd) // If Init succeeds, syscall.Exec will not return, hence none of the defers will be called. return i.Init() //}StartInitialization() 方法尝试从环境中读取一系列_LIBCONTAINER_XXX变量的值,还有印象吗?这些值全是在runc create命令中打开和设置的,也就是说,runc create通过环境变量,将这些参数传给了子进程runc init 3拿到这些环境变量后,runc init 3调用 newContainerInit 函数/ libcontainer/init_linux.go */func newContainerInit(t initType, pipe *os.File, consoleSocket *os.File, fifoFd int) (initer, error) { var config initConfig / read config from pipe (from runc process) / son.NewDecoder(pipe).Decode(&config); populateProcessEnvironment(config.Env); switch t { …… case initStandard: return &linuxStandardInit{ pipe: pipe, consoleSocket: consoleSocket, parentPid: unix.Getppid(), config: config, // <=== config fifoFd: fifoFd, }, nil } return nil, fmt.Errorf(“unknown init type %q”, t)}newContainerInit() 函数首先尝试从 pipe 读取配置存放到变量 config 中,再存储到变量 linuxStandardInit 中返回 runc create runc init 3 | | p.sendConfig() — config –> NewContainerInit()sleep 5 线索在 initStandard.config 中回到 StartInitialization(),在得到 linuxStandardInit 后,便调用其 Init()方法了/ init.go */func (l *LinuxFactory) StartInitialization() (err error) { …… i, err := newContainerInit(it, pipe, consoleSocket, fifofd) return i.Init() }本文忽略掉 Init() 方法前面的一大堆其他配置,只看其最后func (l *linuxStandardInit) Init() error { …… name, err := exec.LookPath(l.config.Args[0]) syscall.Exec(name, l.config.Args[0:], os.Environ())}可以看到,这里终于开始执行 用户最初设置的 sleep 5 了 ...

December 31, 2018 · 4 min · jiezi

探索 runC (上)

前言容器运行时(Container Runtime)是指管理容器和容器镜像的软件。当前业内比较有名的有docker,rkt等。如果不同的运行时只能支持各自的容器,那么显然不利于整个容器技术的发展。于是在2015年6月,由Docker以及其他容器领域的领导者共同建立了围绕容器格式和运行时的开放的工业化标准,即Open Container Initiative(OCI),OCI具体包含两个标准:运行时标准(runtime-spec)和容器镜像标准(image-spec)。简单来说,容器镜像标准定义了容器镜像的打包形式(pack format),而运行时标准定义了如何去运行一个容器。本文包含以下内容:runC的概念和使用runC运行容器的原理剖析本文不包含以下内容:docker engine使用runCrunC概念runC是一个遵循OCI标准的用来运行容器的命令行工具(CLI Tool),它也是一个Runtime的实现。尽管你可能对这个概念很陌生,但实际上,你的电脑上的docker底层可能正在使用它。至少在笔者的主机上是这样。root@node-1:~# docker info…..Runtimes: runcDefault Runtime: runc …..安装runCrunC不仅可以被docker engine使用,它也可以单独使用(它本身就是命令行工具),以下使用步骤完全来自runC’s README,如果依赖项Go version 1.6或更高版本libseccomp库 yum install libseccomp-devel for CentOS apt-get install libseccomp-dev for Ubuntu下载编译# 在GOPATH/src目录创建’github.com/opencontainers’目录> cd github.com/opencontainers> git clone https://github.com/opencontainers/runc> cd runc> make> sudo make install或者使用go get安装# 在GOPATH/src目录创建github.com目录> go get github.com/opencontainers/runc> cd $GOPATH/src/github.com/opencontainers/runc> make> sudo make install以上步骤完成后,runC将安装在/usr/local/sbin/runc目录使用runC创建一个OCI BundleOCI Bundle是指满足OCI标准的一系列文件,这些文件包含了运行容器所需要的所有数据,它们存放在一个共同的目录,该目录包含以下两项:config.json:包含容器运行的配置数据container 的 root filesystem如果主机上安装了docker,那么可以使用docker export命令将已有镜像导出为OCI Bundle的格式# create the top most bundle directory> mkdir /mycontainer> cd /mycontainer# create the rootfs directory> mkdir rootfs# export busybox via Docker into the rootfs directory> docker export $(docker create busybox) | tar -C rootfs -xvf -> ls rootfs bin dev etc home proc root sys tmp usr var有了root filesystem,还需要config.json,runc spec可以生成一个基础模板,之后我们可以在模板基础上进行修改。> runc spec> lsconfig.json rootfs生成的config.json模板比较长,这里我将它process中的arg 和 terminal进行修改{ “process”: { “terminal”:false, <– 这里改为 true “user”: { “uid”: 0, “gid”: 0 }, “args”: [ “sh” <– 这里改为 “sleep”,“5” ], “env”: [ “PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin”, “TERM=xterm” ], “cwd”: “/”, }, “root”: { “path”: “rootfs”, “readonly”: true }, “linux”: { “namespaces”: [ { “type”: “pid” }, { “type”: “network” }, { “type”: “ipc” }, { “type”: “uts” }, { “type”: “mount” } ], }} config.json 文件的内容都是 OCI Container Runtime 的订制,其中每一项值都可以在Runtime Spec找到具体含义,OCI Container Runtime 支持多种平台,因此其 Spec 也分为通用部分(在config.md中描述)以及平台相关的部分(如linux平台上就是config-linux)process:指定容器启动后运行的进程运行环境,其中最重要的的子项就是args,它指定要运行的可执行程序, 在上面的修改后的模板中,我们将其改成了"sleep 5"root:指定容器的根文件系统,其中path子项是指向前面导出的中root filesystem的路径linux: 这一项是平台相关的。其中namespaces表示新创建的容器会额外创建或使用的namespace的类型运行容器现在我们使用create命令创建容器# run as root> cd /mycontainer> runc create mycontainerid使用list命令查看容器状态为created# view the container is created and in the “created” state> runc listID PID STATUS BUNDLE CREATED OWNERmycontainerid 12068 created /mycontainer 2018-12-25T19:45:37.346925609Z root 使用start命令查看容器状态# start the process inside the container> runc start mycontainerid在5s内 使用list命令查看容器状态为running# within 5 seconds view that the container is runningrunc listID PID STATUS BUNDLE CREATED OWNERmycontainerid 12068 running /mycontainer 2018-12-25T19:45:37.346925609Z root 在5s后 使用list命令查看容器状态为stopped# after 5 seconds view that the container has exited and is now in the stopped staterunc listID PID STATUS BUNDLE CREATED OWNERmycontainerid 0 stopped /mycontainer 2018-12-25T19:45:37.346925609Z root 使用delete命令可以删除容器# now delete the containerrunc delete mycontaineridrunC 的实现原理runC可以启动并管理符合OCI标准的容器。简单地说,runC需要利用OCI bundle创建一个独立的运行环境,并执行指定的程序。在Linux平台上,这个环境就是指各种类型的Namespace以及Capability等等配置代码结构runC由Go语言实现,当前(2018.12)最新版本是v1.0.0-rc6,代码的结构可分为两大块,一是根目录下的go文件,对应各个runC命令,二是负责创建/启动/管理容器的libcontainer,可以说runC的本质都在libcontainerrunc create的过程以上面的例子为例,以’runc create’这条命令来看runC是如何完成从无到有创建容器create命令的响应入口在 create.go, 我们直接关注其注册的Action的实现,当输入runc create mycontainerid时会执行注册的Action,并且参数存放在Context中/* run.go */Action: func(context *cli.Context) error { …… spec, err := setupSpec(context) status, err := startContainer(context, spec, CT_ACT_CREATE, nil) …..}setupSpec:从命令行输入中找到-b 指定的 OCI bundle 目录,若没有此参数,则默认是当前目录。读取config.json文件,将其中的内容转换为Go的数据结构specs.Spec,该结构定义在文件 github.com/opencontainers/runtime-spec/specs-go/config.go,里面的内容都是OCI标准描述的startContainer:尝试创建启动容器,注意这里的第三个参数是 CT_ACT_CREATE, 表示仅创建容器。本文使用linux平台,因此实际调用的是 utils_linux.go 中的startContainer()。startContainer()根据用户将用户输入的 id 和刚才的得到的 spec 作为输入,调用 createContainer() 方法创建容器,再通过一个runner.run()方法启动它/× utils_linux.go ×/func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts libcontainer.CriuOpts) (int, error) { id := context.Args().First() container, err := createContainer(context, id, spec) r := &runner{ container: container, action: action, init: true, …… } return r.run(spec.Process)}这里需要先了解下runC中的几个重要数据结构的关系Container 接口在runC中,Container用来表示一个容器对象,它是一个抽象接口,它内部包含了BaseContainer接口。从其内部的方法的名字就可以看出,都是管理容器的基本操作/ libcontainer/container.go */type BaseContainer interface { ID() string Status() (Status, error) State() (*State, error) Config() configs.Config Processes() ([]int, error) Stats() (*Stats, error) Set(config configs.Config) error Start(process *Process) (err error) Run(process Process) (err error) Destroy() error Signal(s os.Signal, all bool) error Exec() error}/ libcontainer/container_linux.go */type Container interface { BaseContainer Checkpoint(criuOpts *CriuOpts) error Restore(process *Process, criuOpts *CriuOpts) error Pause() error Resume() error NotifyOOM() (<-chan struct{}, error) NotifyMemoryPressure(level PressureLevel) (<-chan struct{}, error)}有了抽象接口,那么一定有具体的实现,linuxContainer 就是一个实现,或者说,它是当前版本runC在linux平台上的唯一一种实现。下面是其定义,其中的 initPath 非常关键type linuxContainer struct { id string config *configs.Config initPath string initArgs []string initProcess parentProcess …..}Factory 接口在runC中,所有的容器都是由容器工厂(Factory)创建的, Factory 也是一个抽象接口,定义如下,它只包含了4个方法type Factory interface { Create(id string, config *configs.Config) (Container, error) Load(id string) (Container, error) StartInitialization() error Type() string}linux平台上的对 Factory 接口也有一个标准实现—LinuxFactory,其中的 InitPath 也非常关键,稍后我们会看到// LinuxFactory implements the default factory interface for linux based systems.type LinuxFactory struct { // InitPath is the path for calling the init responsibilities for spawning // a container. InitPath string …… // InitArgs are arguments for calling the init responsibilities for spawning // a container. InitArgs []string}所以,对于linux平台,Factory 创建 Container 实际上就是 LinuxFactory 创建 linuxContainer回到createContainer(),下面是其实现func createContainer(context *cli.Context, id string, spec specs.Spec) (libcontainer.Container, error) { / 1. 将配置存放到config / rootlessCg, err := shouldUseRootlessCgroupManager(context) config, err := specconv.CreateLibcontainerConfig(&specconv.CreateOpts{ CgroupName: id, UseSystemdCgroup: context.GlobalBool(“systemd-cgroup”), NoPivotRoot: context.Bool(“no-pivot”), NoNewKeyring: context.Bool(“no-new-keyring”), Spec: spec, RootlessEUID: os.Geteuid() != 0, RootlessCgroups: rootlessCg, }) / 2. 加载Factory / factory, err := loadFactory(context) if err != nil { return nil, err } / 3. 调用Factory的Create()方法 / return factory.Create(id, config)}可以看到,上面的代码大体上分为将配置存放到config加载Factory调用Factory的Create()方法第1步存放配置没什么好说的,无非是将已有的 spec 和其他一些用户命令行选项配置换成一个数据结构存下来。而第2部加载Factory,在linux上,就是返回一个 LinuxFactory 结构。而这是通过在其内部调用 libcontainer.New()方法实现的/ utils/utils_linux.go */func loadFactory(context cli.Context) (libcontainer.Factory, error) { ….. return libcontainer.New(abs, cgroupManager, intelRdtManager, libcontainer.CriuPath(context.GlobalString(“criu”)), libcontainer.NewuidmapPath(newuidmap), libcontainer.NewgidmapPath(newgidmap))}libcontainer.New() 方法在linux平台的实现如下,可以看到,它的确会返回一个LinuxFactory,并且InitPath设置为"/proc/self/exe",InitArgs设置为"init"/ libcontainer/factory_linux.go */func New(root string, options …func(*LinuxFactory) error) (Factory, error) { ….. l := &LinuxFactory{ ….. InitPath: “/proc/self/exe”, InitArgs: []string{os.Args[0], “init”}, } …… return l, nil}得到了具体的 Factory 实现,下一步就是调用其Create()方法,对 linux 平台而言,就是下面这个方法,可以看到,它会将 LinuxFactory 上记录的 InitPath 和 InitArgs 赋给 linuxContainer 并作为结果返回func (l *LinuxFactory) Create(id string, config configs.Config) (Container, error) { …. c := &linuxContainer{ id: id, config: config, initPath: l.InitPath, initArgs: l.InitArgs, } ….. return c, nil}回到 startContainer() 方法,再得到 linuxContainer 后,将创建一个 runner 结构,并调用其run()方法/ utils_linux.go */func startContainer(context *cli.Context, spec *specs.Spec, action CtAct, criuOpts *libcontainer.CriuOpts) (int, error) { id := context.Args().First() container, err := createContainer(context, id, spec) r := &runner{ container: container, action: action, init: true, …… } return r.run(spec.Process)}runner 的 run() 的入参是 spec.Process 结构,我们并不需要关注它的定义,因为它的内容都来源于 config.json 文件,spec.Process 不过是其中 Process 部分的 Go 语言数据的表示。run() 方法的实现如下:func (r *runner) run(config *specs.Process) (int, error) { …… process, err := newProcess(config, r.init) …… switch r.action { case CT_ACT_CREATE: err = r.container.Start(process) / runc start / case CT_ACT_RESTORE: err = r.container.Restore(process, r.criuOpts) / runc restore / case CT_ACT_RUN: err = r.container.Run(process) / runc run / default: panic(“Unknown action”) } …… return status, err}上面的 run() 可分为两部分调用 newProcess() 方法, 用 spec.Process 创建 libcontainer.Process,注意第二个参数是 true ,表示新创建的 process 会作为新创建容器的第一个 process根据 r.action 的值决定如何操作得到的 libcontainer.Processlibcontainer.Process 结构定义在 /libcontainer/process.go, 其中大部分内容都来自 spec.Process/ parent process */// Process specifies the configuration and IO for a process inside// a container.type Process struct { Args []string Env []string User string AdditionalGroups []string Cwd string Stdin io.Reader Stdout io.Writer Stderr io.Writer ExtraFiles []*os.File ConsoleWidth uint16 ConsoleHeight uint16 Capabilities *configs.Capabilities AppArmorProfile string Label string NoNewPrivileges *bool Rlimits []configs.Rlimit ConsoleSocket *os.File Init bool ops processOperations}接下来就是要使用 Start() 方法了func (c *linuxContainer) Start(process Process) error { if process.Init { if err := c.createExecFifo(); err != nil { / 1.创建fifo / return err } } if err := c.start(process); err != nil { / 2. 调用start() */ if process.Init { c.deleteExecFifo() } return err } return nil}Start() 方法主要完成两件事创建 fifo: 创建一个名为exec.fifo的管道,这个管道后面会用到调用 start() 方法,如下func (c *linuxContainer) start(process Process) error { parent, err := c.newParentProcess(process) / 1. 创建parentProcess / err := parent.start(); / 2. 启动这个parentProcess */ …… start() 也完成两件事:创建一个 ParentProcess调用这个 ParentProcess 的 start() 方法那么什么是 parentProcess ? 正如其名,parentProcess 类似于 linux 中可以派生出子进程的父进程,在runC中,parentProcess 是一个抽象接口,如下:type parentProcess interface { // pid returns the pid for the running process. pid() int // start starts the process execution. start() error // send a SIGKILL to the process and wait for the exit. terminate() error // wait waits on the process returning the process state. wait() (*os.ProcessState, error) // startTime returns the process start time. startTime() (uint64, error) signal(os.Signal) error externalDescriptors() []string setExternalDescriptors(fds []string)}它有两个实现,分别为 initProcess 和 setnsProcess ,前者用于创建容器内的第一个进程,后者用于在已有容器内创建新的进程。在我们的创建容器例子中,p.Init = true ,所以会创建 initProcessfunc (c *linuxContainer) newParentProcess(p Process) (parentProcess, error) { parentPipe, childPipe, err := utils.NewSockPair(“init”) / 1.创建 Socket Pair / cmd, err := c.commandTemplate(p, childPipe) / 2. 创建 *exec.Cmd / if !p.Init { return c.newSetnsProcess(p, cmd, parentPipe, childPipe) } if err := c.includeExecFifo(cmd); err != nil { / 3.打开之前创建的fifo / return nil, newSystemErrorWithCause(err, “including execfifo in cmd.Exec setup”) } return c.newInitProcess(p, cmd, parentPipe, childPipe) / 4.创建 initProcess */}newParentProcess() 方法动作有 4 步,前 3 步都是在为第 4 步做准备,即生成 initProcess创建一对 SocketPair 没什么好说的,生成的结果会放到 initProcess创建 *exec.Cmd,代码如下,这里设置了 cmd 要执行的可执行程序和参数来自 c.initPath,即源自 LInuxFactory 的 “/proc/self/exe”,和 “init” ,这表示新执行的程序就是runC本身,只是参数变成了 init,之后又将外面创建的 SocketPair 的一端 childPipe放到了cmd.ExtraFiles ,同时将_LIBCONTAINER_INITPIPE=%d加入cmd.Env,其中 %d为文件描述符的数字func (c *linuxContainer) commandTemplate(p *Process, childPipe *os.File) (*exec.Cmd, error) { cmd := exec.Command(c.initPath, c.initArgs[1:]…) cmd.Args[0] = c.initArgs[0] cmd.ExtraFiles = append(cmd.ExtraFiles, p.ExtraFiles…) cmd.ExtraFiles = append(cmd.ExtraFiles, childPipe) cmd.Env = append(cmd.Env, fmt.Sprintf("_LIBCONTAINER_INITPIPE=%d", stdioFdCount+len(cmd.ExtraFiles)-1), ) …… return cmd, nil}includeExecFifo() 方法打开之前创建的 fifo,也将其 fd 放到 cmd.ExtraFiles 中。最后就是创建 InitProcess 了,这里首先将_LIBCONTAINER_INITTYPE=“standard"加入cmd.Env,然后从 configs 读取需要新的容器创建的 Namespace 的类型,并将其打包到变量 data 中备用,最后再创建 InitProcess 自己,可以看到,这里将之前的一些资源和变量都联系了起来func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, childPipe *os.File) (*initProcess, error) { cmd.Env = append(cmd.Env, “_LIBCONTAINER_INITTYPE="+string(initStandard)) nsMaps := make(map[configs.NamespaceType]string) for _, ns := range c.config.Namespaces { if ns.Path != "” { nsMaps[ns.Type] = ns.Path } } _, sharePidns := nsMaps[configs.NEWPID] data, err := c.bootstrapData(c.config.Namespaces.CloneFlags(), nsMaps) if err != nil { return nil, err } return &initProcess{ cmd: cmd, childPipe: childPipe, parentPipe: parentPipe, manager: c.cgroupManager, intelRdtManager: c.intelRdtManager, config: c.newInitConfig(p), container: c, process: p, bootstrapData: data, sharePidns: sharePidns, }, nil}回到 linuxContainer 的 start() 方法,创建好了 parent ,下一步就是调用它的 start() 方法了func (c *linuxContainer) start(process Process) error { parent, err := c.newParentProcess(process) / 1. 创建parentProcess (已完成) / err := parent.start(); / 2. 启动这个parentProcess */ ……—– 待续 ...

December 27, 2018 · 6 min · jiezi

使用Rook+Ceph在Kubernetes上作持久存储

作者:Earl C. Ruby III我想在新的Kubernetes集群上安装Prometheus和Grafana,但为了使这些软件包能够工作,他们需要一些地方来存储持久数据。当我在Seagate担任云架构师时,我已经对Ceph进行了性能和规模测试,并且在过去的一年里玩过Rook,所以我决定安装Rook+Ceph,并将其用于Kubernetes集群的数据存储。Ceph是一个分布式存储系统,提供对象、文件和块存储。在每个存储节点上,您将找到Ceph存储对象的文件系统和Ceph OSD(对象存储守护程序)进程。在Ceph集群上,您还可以找到Ceph MON(监控)守护程序,它们确保Ceph集群保持高可用性。Rook充当Ceph在Kubernetes的业务流程层,将OSD和MON流程部署为POD副本集。来自Rook的README文件:Rook将存储软件转变为自我管理、自我扩展和自我修复的存储服务。它通过自动化部署,引导,准备,配置,扩展,升级,迁移,灾难恢复,监控和资源管理来实现此目的。 Rook使用底层云原生容器管理,调度和编排平台提供的工具来执行其职责。https://github.com/rook/rook/…当我创建集群时,我构建了具有40GB硬盘的VM,所以使用5个Kubernetes节点,在我的集群上提供了大约200GB的存储空间,其中大部分都将用于Ceph。安装Rook+Ceph安装Rook+Ceph非常简单。在我的个人群集上,我按照以下步骤安装了Rook+Ceph v0.9.0:git clone git@github.com:rook/rook.gitcd rookgit checkout v0.9.0cd cluster/examples/kubernetes/cephkubectl create -f operator.yamlkubectl create -f cluster.yamlRook将POD部署在两个命名空间中,即rook-ceph-system和rook-ceph。 在我的群集上,POD花了大约2分钟来部署,初始化并进入运行状态。当我等待一切都完成时,我检查了POD状态:$ kubectl -n rook-ceph-system get podNAME READY STATUS RESTARTS AGErook-ceph-agent-8tsq7 1/1 Running 0 2d20hrook-ceph-agent-b6mgs 1/1 Running 0 2d20hrook-ceph-agent-nff8n 1/1 Running 0 2d20hrook-ceph-agent-vl4zf 1/1 Running 0 2d20hrook-ceph-agent-vtpbj 1/1 Running 0 2d20hrook-ceph-agent-xq5dv 1/1 Running 0 2d20hrook-ceph-operator-85d64cfb99-hrnbs 1/1 Running 0 2d20hrook-discover-9nqrp 1/1 Running 0 2d20hrook-discover-b62ds 1/1 Running 0 2d20hrook-discover-k77gw 1/1 Running 0 2d20hrook-discover-kqknr 1/1 Running 0 2d20hrook-discover-v2hhb 1/1 Running 0 2d20hrook-discover-wbkkq 1/1 Running 0 2d20h$ kubectl -n rook-ceph get podNAME READY STATUS RESTARTS AGErook-ceph-mgr-a-7d884ddc8b-kfxt9 1/1 Running 0 2d20hrook-ceph-mon-a-77cbd865b8-ncg67 1/1 Running 0 2d20hrook-ceph-mon-b-7cd4b9774f-js8n9 1/1 Running 0 2d20hrook-ceph-mon-c-86778859c7-x2qg9 1/1 Running 0 2d20hrook-ceph-osd-0-67fff79666-fcrss 1/1 Running 0 35hrook-ceph-osd-1-58bd4ccbbf-lsxj9 1/1 Running 1 2d20hrook-ceph-osd-2-bf99864b5-n4q7v 1/1 Running 0 2d20hrook-ceph-osd-3-577466c968-j8gjr 1/1 Running 0 2d20hrook-ceph-osd-4-6856c5c6c9-92tb6 1/1 Running 0 2d20hrook-ceph-osd-5-8669577f6b-zqrq9 1/1 Running 0 2d20hrook-ceph-osd-prepare-node1-xfbs7 0/2 Completed 0 2d20hrook-ceph-osd-prepare-node2-c9f55 0/2 Completed 0 2d20hrook-ceph-osd-prepare-node3-5g4nc 0/2 Completed 0 2d20hrook-ceph-osd-prepare-node4-wj475 0/2 Completed 0 2d20hrook-ceph-osd-prepare-node5-tf5bt 0/2 Completed 0 2d20h最后工作现在我需要再做两件事,才能安装Prometheus和Grafana:我需要让Rook成为我的集群的默认存储提供程序。由于Prometheus Helm chart请求使用XFS文件系统格式化的卷,因此我需要在所有Ubuntu Kubernetes节点上安装XFS工具。(默认情况下,Kubespray尚未安装XFS,尽管目前有一个PR解决这个问题。)要使Rook成为默认存储提供程序,我只需运行kubectl命令:kubectl patch storageclass rook-ceph-block -p ‘{“metadata”: {“annotations”:{“storageclass.kubernetes.io/is-default-class”:“true”}}}‘这会更新rook-ceph-block存储类,并使其成为群集上存储的默认值。如果没有指定特定的存储类,我安装的任何应用程序都将使用Rook+Ceph进行数据存储。由于我使用Kubespray构建集群,而Kubespray使用Ansible,因此在所有主机上安装XFS工具的最简单方法之一,是使用Ansible“在所有主机上运行单个命令”功能:cd kubesprayexport ANSIBLE_REMOTE_USER=ansibleansible kube-node -i inventory/mycluster/hosts.ini \ –become –become-user root \ -a ‘apt-get install -y xfsprogs’现在已经安装了XFS,我可以使用Helm成功部署Prometheus和Grafana:helm install –name prometheus stable/prometheushelm install –name grafana stable/grafanaHelm chart安装Prometheus和Grafana,并在Rook+Ceph上为Prometheus Server和Prometheus Alert Manager(使用XFS格式化)创建持久存储卷。Prometheus仪表板Grafana仪表板Rook给Prometheus服务器的持久存储希望您觉得这个有帮助。 ...

December 19, 2018 · 1 min · jiezi

Open Policy Agent登陆Helm Hub

早前介绍过Helm Hub。Helm Hub方便您在许多人和组织托管的许多分布式存储库中查找chart。Open Policy Agent(OPA)项目刚刚提了PR,现在大家也可以轻松找到和使用OPA chart。OPA是一个开源的通用政策引擎,可在整个堆栈中实现统一的上下文感知策略实施。政策是一组管理服务行为的规则。启用政策使用户能够阅读、编写和管理这些规则,而无需专业的开发或操作专业知识。当您的用户无需重新编译源代码即可实施政策时,您的服务就是启用了政策。这个chart将OPA安装为Kubernetes准入控制器。对Helm和OPA刚兴趣的可以到链接多了解。

December 19, 2018 · 1 min · jiezi

研发中:联邦SPIFFE信任域

作者:Daniel Feldman介绍联邦信任域是SPIFFE和SPIRE最高需求和活跃开发的功能之一。在这篇博文中,我将概述我们当前的计划以及实施它的挑战。什么是联邦?SPIFFE信任域中的证书共享一个信任根。 这是一个根信任捆绑包,由使用非标准化格式和协议在控制平面之间和内部共享的多个证书组成。然而,这还不够好。许多组织都有多个信任根源:可能是因为他们与不同的管理员有不同的组织划分,或者因为他们有偶尔需要沟通的独立的临时和生产环境。类似的用例是组织之间的SPIFFE互操作性,例如云供应商与其客户之间的互操作性。这两种用例都需要一个定义明确、可互操作的方法,以便一个信任域中的工作负载对不同信任域中的工作负载进行身份验证。这是联邦。联邦设计要实现联邦,我们必须在不同的SPIFFE服务器之间共享公钥。这不是一次性操作;由于密钥轮换,每个信任域的公钥会定期更改。每个联邦域必须定期下载其他域的公钥,其频率至少与密钥轮换一样快。定期下载证书的数据格式尚未最终确定。我们目前的想法是让SPIFFE的实现去使用JWKS格式,在一个众所周知的URL上公开发布证书。然后,要启动联邦关系,实现可以下载JWKS数据,并从中导入证书。我们喜欢JWKS,因为它是一种通用的、可扩展的格式,用于共享可以容纳JWT和X.509证书的密钥信息。(出于安全原因,SPIFFE需要不同的JWT和X.509标识的密钥材料 - 它们不能只是以不同格式编码的相同公钥。)JWKS的灵活性允许单个联邦API支持JWT和X.509 。工作负载APISPIFFE工作负载API提供用于读取联邦公钥的端点。此API与用于读取当前信任域的证书的API不同,所以应用程序可以区分本地和联邦域的客户端。SPIRE的实验支持虽然它尚未正式标准化为SPIFFE的一部分,但是SPIRE已经可以提供JWKS的实验性实施。挑战外部SPIFFE服务器的初始身份验证联邦API存在引导问题:如果双方都没有共享信任根,则无法建立初始安全连接。其一种解决方案,是使用两个SPIFFE服务器信任的证书颁发机构的Web PKI。另一种解决方案,是使用手动身份验证机制来消除对公共证书颁发机构(CA)的需求。SPIRE使用与节点和工作负载注册类似的方式实现联邦。随着我们扩展注册API,可以通过该API操作联邦,就像节点和工作负载注册一样。网络中断容错每次SPIFFE实现,从同等的SPIFFE实现,导入新证书时,它都会使用上一个已知捆绑包对连接进行身份验证。如果网络中断很长,并且两个SPIFFE实现无法通信,超过完整的密钥轮换周期,那么它们将无法继续进行通信,从而破坏了联邦关系。其一种解决方案,是将密钥轮换间隔,设置为长于可能的最长网络中断长度(或者如果发生长中断,则重新初始化联邦)。这是设计权衡:如果密钥轮换间隔较长,则受损密钥也将在较长时间内保持有效。或者,如果Web PKI可用于SPIFFE服务器,则可用于保护联邦连接。我们相信联邦SPIFFE服务器之间的Web PKI,将是一种常见的设计模式,因为它避免了长网络中断导致密钥轮换的问题。传递与双向联邦Kerberos和Active Directory具有与联邦相同的,称为“跨领域信任”。在大多数情况下,跨领域信任是双向的(双方互相信任)和传递(如果A信任B,B信任C,然后A信托C)。SPIFFE中的双向联邦通常(但并非总是如此)是可取的。对于公共API,API提供程序可能希望使用Web PKI来保护连接的服务器端,并使用SPIFFE来保护客户端。因此,我们不会自动配置双向联邦。对于具有许多信任域的大型组织,传递联邦可以简化实现复杂性。但是,传递联邦可能难以推断SPIFFE实现的安全属性。出于这个原因,我们现在没有在SPIFFE中实现传递联邦。目前,用户必须通过添加更多联邦关系,来手动配置传递和双向联邦。联邦信任域SVID的范围在Web PKI中,每个人都信任相同的根证书颁发机构。在SPIFFE中,彼此不完全信任的组织可能仍希望联邦其信任域。应用程序必须验证每个SVID是否由拥有该信任域的SPIFFE服务器颁发。想象一个奇怪的世界,可口可乐和百事可乐必须交换数据。为此,他们联邦各自的信任域。可口可乐的SPIFFE证书根,添加到百事可乐的信托商店,反之亦然。在证书验证的简单实现中,可口可乐服务器可以欺骗性地冒充百事可乐网络上的百事可乐服务器,因为百事可乐信任可口可乐的根证书!这是问题所在:根证书没有“范围”。任何CA都可以为任何名称签署证书。如果所有CA都受信任,例如在单个公司内,则可以。在具有多个CA的环境中,每个CA都应该只允许签署具有特定名称的证书,不然这会导致安全漏洞。防止这种情况的一种方法是使用X.509名称约束扩展。名称约束扩展允许将CA证书限制为为特定域名颁发证书。但是,在TLS库中对名称约束扩展的支持是有限的,并且它不能解决未来SPIFFE身份格式(如JWT)的问题。由于这些原因,SPIFFE不包括名称约束扩展。这意味着所有使用SVID的应用程序都必须检查SVID中的SPIFFE ID,是否与签署证书的实际CA的信任域匹配。这意味着检查百事可乐SVID不是被可口可乐的CA签名。当前广泛使用的应用程序(例如Web服务器和代理)不执行此检查。结论联邦对于SPIFFE的成功实施至关重要。但是,以安全和可用的方式实施它仍然存在挑战。我们正在努力与社区一起努力应对这些挑战,如果您有远见,我们很乐意听取您的意见!要了解有关SPIFFE联邦的更多信息:查看新的Java SPIFFE Federation Demo,它演示了在Tomcat服务器环境中使用SPIRE在两个域之间进行联邦。加入SPIFFE Slack与专家讨论SPIFFE。加入SPIFFE SIG-SPEC双月会议,设计SPIFFE联邦会的未来。 (不久将有一个单独的联邦工作组。)

December 18, 2018 · 1 min · jiezi

KubeCon西雅图:2018年以顶尖的云原生社区活动来结束!

作者:Natasha Woods随着KubeCon西雅图的闭幕,这是我们迄今为止最丰富的节目中所有云原生优点的快照。 门票售罄的KubeCon + CloudNativeCon北美2018是在过去的CNCF活动中拥有最多的出席和等待名单,来自世界各地的8000多名贡献者、最终用户、供应商和开发者,在华盛顿州西雅图聚会超过三天,以进一步推动教育和采用云原生计算,并围绕这个快速发展的生态系统分享见解。现场有8,000人参加,另有2,000人在等待名单上体验重大的FOMO,他们观看直播主题演讲并阅读Twitter推文,KubeCon西雅图出席人数比去年在奥斯汀举办的KubeCon活动增加了83%。在与会者人数增加的同时,伟大的“开发者会议”体验保持不变!看到这个社区的发展速度真的令人兴奋。 如果你错过了展会上的巨大职位公告板 - 市场正在招聘!KubeCon西雅图的女性在为期三天的主题演讲中,女性是前沿和中心,我们听到了从KubeCon联合主席Liz Rice关于CNCF社区更新,及我们的一些项目维护人员 - 包括Microsoft的Michelle Noorali关于Helm更新;Lyft的Matt Klein关于Envoy更新;以及Google的Aparna Sinha有关Kubernetes增长概述。所有主题演讲中有40%来自女性,云原生女士们正在登台演出!KubeCon联合主席Janet Kuo解释Kubernetes的“无聊”是一件好事,Liz Rice以她在Aqua Security的角色在另一个主题演讲中强调安全的重要性,她说,“CNCF不会在这里抛出眩光事件,但是为了帮助我们作为一个社区进行协调,并确保我们有适当的治理,并使得更难将权限交给一些随机的人,这一点很重要,因为越来越多的公司依赖开源技术。良好的治理是如何作为一个社区,使我们可以从安全攻击中拯救自己。”在压轴的主题演讲,Kelsey Hightower在他的无服务器主题演讲向惊人真实隐藏低调的女性,他的母亲和Motown Diana Ross女王,发出了呐喊。最终用户主题演讲和会议担当技术领导角色的杰出女性也出现在重要的“如何在企业中获得成效”的主题演讲和会议中。Airbnb软件工程师Melanie Cebula发现了使开箱即用的Kubernetes对开发者不那么友好的一些关键问题。她还根据Airbnb的经验,制定了解决这些问题的10项策略,使1000名工程师能够大规模开发数百个Kubernetes服务。Uber的Celina Ward和Matt Schallert分享了他们为独特的状态工作量创建operator的经验。 他们讨论了思想的重大转变,并为观众提供了一个框架,用于使用Kubernetes原语来表达他们的有状态的工作量,以及在不过度设计解决方案的情况下,编写创新抽象概念的建议。Capital One宣布他们对社区的更多承诺,通过升级并成为CNCF金牌终端用户会员!其他最终用户分组会议和展位展品包括:Home Depot、Nordstrom、Lyft、Buffer、密歇根大学、诺基亚、T-Mobile、三星SDS、雅虎日本公司、BlackRock、AT&T、今日美国网络、Two Sigma等等!建立一个强大而多样化的社区!在一方面,KubeCon西雅图有这么多女性演讲者是一个巨大的进步;另一方面,也有许多活动汇集了云原生社区的多样性,包括速度网络联系和指导、多样化午餐、通过Meetup和KubeCon参与者奖学金建立社区的会议。CNCF的多元化计划为来自传统上代表性不足,和/或边缘化群体的147名受助人,提供奖学金,以参加KubeCon西雅图!西雅图的30万美元投资 - 在会议中投资在多元化最多 - 来自由CNCF的大部分捐款,以及奖学金赞助商Aspen Mesh、MongoDB、Twistlock、Two Sigma和VMware。包括西雅图在内,CNCF在过去两年中提供了超过485个多样性奖学金参加KubeCon。CNCF也与Kubernetes辅导计划合作,为学员在KubeCon提供交流机会。KubeCon西雅图有66名导师和180多名学员参加了这个计划。奖励辛勤工作和奉献精神:连续第三年的CNCF社区奖,由VMware赞助,突出了所有CNCF项目中最活跃的大使和最优秀的贡献者。Top Cloud Native Committer - 在一个或多个CNCF项目中拥有令人难以置信的技术技能和显着技术成就的个人。2018年的得奖者是Jordan Liggitt。Top Cloud Native Ambassador - 具有令人难以置信的社区导向技能,专注于传播信息并与整个Cloud Native社区或特定项目共享知识的个人。2018年的得奖者是Michael Hausenblas。对于任何开源项目,都不可能忽视那些花费无数小时的时间来完成平常任务的人。这就是Chris Aniszczyk带回Chop Wood/Carry Water(伐木/挑水)奖项的原因。今年的奖项表彰了Davanum Srinivas、Dianne Mueller、Christoph Blecker、Nikhita Raghunath、Paris Pittman、Richard Hartmann、Tim Pepper、April Kyle Nassi、Jorge Castro、Babak “Bobby” Salamat、Reinhard Nagele、Zach Arnold、Kris Nova和Stephen Augustus的努力不懈。同场活动在会议的第0天(12月10日)举行了27场同场活动。有许多伟大的技术和社区建设会议,包括Linkerd在生产环境的101,Kubernetes贡献者峰会和第一个EnvoyCon!CNCF项目现况和由来许多CNCF项目都有公告、精彩的分组会议、快闪演讲、社区建设与维护者会面的机会,以及技术深入了解。Phippy加入CNCF家族!2016年,Deis(现在是微软的一部分)平台架构师Matt Butcher正在寻找一种方法,向技术和非技术人员解释Kubernetes。 受到女儿的多产毛绒动物系列的启发,他提出了“Kubernetes儿童图鉴指南”的想法。因此,诞生了Phippy,黄色长颈鹿和PHP应用程序,以及她的朋友。在会议第一天的主题演讲中,Matt和合著者Karen Chu宣布微软向CNCF捐赠了Phippy,并在现场阅读Phippy Goes to the Zoo:Kubernetes故事,作为Kubernetes儿童插画指南提供了官方续集。作为微软捐赠书籍和角色的一部分,CNCF已根据知识共享署名许可(CC-BY)许可所有这些材料,这意味着您可以出于任何目的重新混合、转换和构建材料,甚至商业用途。CNCF第3个快乐生日可能很难相信,我们所有的扩张性增长 - 今年CNCF成员数增加了110%,新增了169家成员 - 但CNCF仍然年轻。我们在本周与云原生社区庆祝了我们的第三个生日!MoPOP、Chihuly Gardens和Space Needle的所有参加者聚会在所有参与者聚会上,会议参观者体验了 Museum of Pop Culture(MoPOP)的魅力,Chihuly Garden and Glass的美丽以及Space Needle的景色。主题演讲和会记录现已推出所有演示文稿和视频均可供观看:会议的主题演讲和会议演示的幻灯片可从时间表中获得:会议详细信息包括演示文稿的链接和YouTube上的视频Youtube播放列表中的主题演讲和所有其他会议在Flickr上查找照片!谢谢你们!记下日期!立即注册参加KubeCon + CloudNativeCon 2019欧洲,计划于5月20日至23日在西班牙巴塞罗那的Fira Barcelona举行。 CFP现已开放,于2019年1月18日关闭。KubeCon + CloudNativeCon 2019中国,定于6月27日至28日在上海世博展览馆举行。 CFP将于本月晚些时候开放,于2019年2月1日关闭。KubeCon + CloudNativeCon 2019北美,定于11月19日至21日在加利福尼亚州圣地亚哥的圣地亚哥会议中心举行。CFP将于2019年5月6日开放,于2019年7月12日关闭。 ...

December 17, 2018 · 1 min · jiezi

介绍Helm Hub

介绍Helm Hub作者:Matt FarinaHelm的设计,考虑了会有许多分布式存储库。与Homebrew Taps和Debian APT存储库一样,Helm可以添加,和使用许多存储库。虽然,Helm的stable和incubator存储库,从一开始就是前沿和中心,但我们并不打算将这些作为唯一的公共存储库。考虑到这一点,我们很高兴地宣布Helm Hub。Helm Hub方便您在许多人和组织托管的许多分布式存储库中查找chart。Helm存储库可以通过多种方式托管,包括GitHub或Gitlab页面,对象存储,使用Chartmuseum和通过服务提供商。如果您有想要列出的chart存储库,请转到GitHub上的Hub存储库,并按照说明进行操作。该过程就像拉取请求一样简单。Helm Hub基于Monocular构建,Monocular一直是Helm的一部分。这最初是由Bitnami和Deis制作的,Deis现在是微软的一部分。随着Helm Hub的复杂性增加,Monocular需要增强其处理许多存储库和chart的能力。想要成为Helm和CNCF社区一员的UI和UX的设计师,可以在这个领域贡献并发挥作用。我们期待Helm Hub开创chart开发和分享的新阶段。

December 14, 2018 · 1 min · jiezi

docker指令学习记录

前言本文为学习整理和参考文章,不具有教程的功能。其次,后面将会陆续更新各种应用的容器化部署的实践,如MySQL容器化,Jenkins容器化,以供读者参考。镜像获取docker pull [options] [Docker Registry地址]<仓库名>:<标签>-a, –all-tags: 下载该镜像的所有版本Docker Registry地址默认为Docker Hub,一般格式为IP:端口号仓库名为两段式 <用户名>:<软件名> 默认用户名为library标签不填则默认为latest列出镜像docker images [options] [Repository[:tag]]默认情况会展示所有最终镜像,如果加上了镜像名,则会展示该镜像的所有信息-a, –all: 展示所有镜像,包括中间层-f, –filter filter: 根据某种条件对镜像进行筛选–format string: 使用go的模板语法-q, –quiet: 只返回镜像的IDdocker images -f since=mongo:3.2 #查看mongo3.2版本之后建立的镜像,如果是要在之前,则使用beforedocker images –format “{{.ID}}:{{.Repository}}” #输出结构为ID:Repository虚悬镜像虚悬镜像是指既没有仓库名,也没有标签的镜像。这种镜像的产生常常由于当前的仓库名和标签被更新版本占用,导致当前境像失效。docker images -f danling=true #列出所有虚悬镜像docker rmi $(docker images -q -f dangling=true) #利用复合指令删除虚悬镜像commit镜像commit会将容器的存储层保存下来成为新的镜像docker commit [options] <容器ID或容器名> [<仓库名>[:<标签>]]-a, –author string: 容器所有者-c, –change list: 在容器上执行Dockerfile指令-m, –message string: 提交信息-p, –pause: 提交过程中停止容器的运行,默认为truedocker history IMAGE #显示镜像的历史记录docker diff CONTAINER #查看容器的改动尽量不要使用commit指令构建镜像Dockerfile构建镜像利用Dockerfile构建镜像。docker build [options] PATH | URL | –f, –file string: Dockerfile的路径–rm: 成功构建后删除中间镜像-t, –tag: 以name:tag的形式为镜像命名docker build -t nginx:v3 . #执行当前目录下的Dockerfile并构建镜像,新的镜像名为nginx:v3docker build https://…… #直接从github构建,会自动clone这个项目,切换到指定分支(默认为master),并进入指定目录进行构建最后的路径是指镜像构建的上下文,docker在build的时候会把该上下文中的而所有内容全部打包上传给docker引擎。当在Dockerfile中需要引用相对路径时,就是以该上下文作为当前指令执行的目录。可以编写.dockerignore文件来剔除无需打包的文件。在默认情况下,如果不指定Dockerfile的位置,就会从构建的上下文寻找Dockerfile来执行FROM指定基础镜像,Dockerfile的第一行必须制定基础镜像RUN执行命令。RUN指令会新建一层并在其上执行指令,指令完成之后再commit该镜像。所以RUN指令中的内容应当尽可能合并,并且记得清除冗余的内容如缓存等。RUN <指令>RUN [“可执行文件”, “参数1”, “参数2”]RUN mkdir newDir \ && touch newFileCOPY将构建上下文中源路径中的内容复制到目标路径之下。可以使用通配符。如果目标目录不存在,容器会帮助创建。复制过程不改变文件属性。COPY 源路径 目标路径COPY [“源路径”,…,“目标路径”]COPY hom* /mydir/CMD默认的容器的主进程的启动命令,在运行时可以指定新的命令来替代镜像设置中的默认命令。比如ubuntu的默认指令是/bin/bash。如果使用第一种形式,则会以sh -c的形式执行,这样就能够得到环境变量。容器中的应用都应该前台执行。CMD <命令>CMD [“可执行文件”, “参数一”, “参数二”, …]CMD [“参数一”, “参数二”…]CMD [“nginx”, “-g”, “daemon off;"]docker run -it ubuntu #直接进入bash,因为默认指令为/bin/bashdocker run -it ubuntu /etc/os-release #默认指令变成/etc/os-releaseENTRYPOINT指定容器启动程序及参数,当指定了ENTRYPOINT之后,CMD的含义就变成了ENTRYPOINT的参数。从而实现我们在build镜像时可以根据配置修改启动指令的参数。在docker run运行时可以用–entrypoint覆盖ENTRYPOINT “CMD"ENTRYPOINT [“可执行文件”, “参数一”, “参数二”…]ENV设置环境变量ENV KEY VALUEENV KEY1=VALUE2 KEY2=VALUE2ARG同ENV,设置环境变量并为其提供默认值,不同的是在容器运行时,这些值将不存在。在运行时可以用–build-arg <参数名>:<值>覆盖ARG <参数名>[=默认值]VOLUMN指定匿名卷,防止用户忘记挂载,运行时用-v HOST_DIR/CONTAINER_DIR进行覆盖VOLUMN PATHEXPOSE声明运行时容器提供的服务端口,运行时应用并不会因为这个声明而打开这个端口。docker run -P时会对声明的端口随机映射EXPOSE 端口一 端口二WORKDIR指定容器之后各层的工作目录。因为本层的cd并不会顺带到下一层。WORKDIR PATHUSER改变之后层执行RUN,ENTRYPOINT等指令的身份RUN groupadd -r redis && useradd -r -g redis redisUSER redisRUN [“redis-server”]ONBUILDONBUILD 其它指令用于构建基础镜像,被引用是才会真正执行。可以提取出重复的部分,方便维护删除docker rmi [options] <image1> [<image2>….] #删除镜像docker rm [options] <container1> [<container2>…] #删除容器进入容器docker attach CONTAINER_NAME查看数据卷信息docker inspect CONTAINER_NAME匿名的数据卷默认位于/var/lib/docker/volumes之下查看容器docker logs [-f] container查看端口映射配置docker port container container_port容器链接–link container_name:alias ...

October 19, 2018 · 1 min · jiezi

猫头鹰的深夜翻译:持久化容器存储

前言临时性存储是容器的一个很大的买点。“根据一个镜像启动容器,随意变更,然后停止变更重启一个容器。你看,一个全新的文件系统又诞生了。”在docker的语境下:# docker run -it centos[root@d42876f95c6a /]# echo “Hello world” > /hello-file[root@d42876f95c6a /]# exitexit# docker run -it centos[root@a0a93816fcfe /]# cat /hello-filecat: /hello-file: No such file or directory当我们围绕容器构建应用程序时,这个临时性存储非常有用。它便于水平扩展:我们只是从同一个镜像创建多个容器实例,每个实例都有自己独立的文件系统。它也易于升级:我们只是创建了一个新版本的映像,我们不必担心从现有容器实例中保留任何内容。它可以轻松地从单个系统移动到群集,或从内部部署移动到云:我们只需要确保集群或云可以访问registry中的镜像。而且它易于恢复:无论我们的程序崩溃对文件系统造成了什么损坏,我们只需要从镜像重新启动一个容器实例,之后就像从未发生过故障一样。因此,我们希望容器引擎依然提供临时存储。但是从教程示例转换到实际应用程序时,我们确实会遇到问题。真实的应用必修在某个地方存储数据。通常,我们将状态保存到某个数据存储中(SQL或是NOSQL)。这也引来了同样的问题。数据存储也是位于容器中吗?理想情况下,答案是肯定的,这样我们可以利用和应用层相同的滚动升级,冗余和故障转移机制。但是,要在容器中运行我们的数据存储,我们再也不能满足于临时存储。容器实例需要能够访问持久存储。如果使用docker管理持久性存储,有两种主流方案:我们可以在宿主机文件系统上指定一个目录,或者是由Docker管理存储:# docker volume create datadata# docker run -it -v data:/data centos[root@5238393087ae /]# echo “Hello world” > /data/hello-file[root@5238393087ae /]# exitexit# docker run -it -v data:/data centos[root@e62608823cd0 /]# cat /data/hello-fileHello worldDocker并不会保留第一个容器的根目录,但是它会保留“data”卷。而该卷会被再次挂载到第二个容器上。所以该卷是持久存储。在单节点系统上这样的方法是ok的。但是在一个容器集群环境下如Kubernetes或是Docker Swarm,情况会变得复杂。如果我们的数据存储容器可能在上百个节点中的任意一个上启动,而且可能从一个节点随时迁移到另一个节点,我们无法依赖于单一的文件系统来存储数据,我们需要一个能够感知到容器的分部署存储的方案,从而无缝集成。容器持久化的需求在深入容器持久化的方案之前,我们应该先了解一下这个方案应该满足什么特性,从而更好的理解各种容器持久化方案的设计思路。冗余将应用移动到容器中并且将容器部署到一个编排环境的原因在于我们可以有更多的物理节点,从而可以支持部分节点当掉。同理,我们也希望持久化存储能够容忍磁盘和节点的崩溃并且继续支持应用运行。在持久化的场景下,冗余的需求更加重要了,因为我们无法忍受任何数据的丢失。分布式冗余的持久化驱动我们使用某种分布式策略,至少在磁盘层面上。但是我们还希望通过分布式存储来提高性能。当我们将容器水平扩展到成百上千个节点上是,我们不希望这些节点竞争位于同一个磁盘上的数据。所以当我们将服务部署到各个区域的环境上来减少用户延时时,我们还希望将存储也同时分布式部署。动态的容器架构持续变更。新版本不断的被构建,更新,应用被添加或是移除。测试用例被创建并启动,然后被删除。在这个架构下,需要能够动态的配置和释放存储。事实上,配置存储应当和我们声明容器实例,服务和网络连通性一样通过声明来实现。灵活性容器技术在飞速发展,我们需要能够引入新的存储策略,并且将应用移植到新的存储架构上。我们的存储策略需要能够支持任何底层架构,从开发人员用于测试的单节点到一个开放的云环境。透明性我们需要为各种类型的应用提供村塾,而且我们需要持续更新存储方案。这意味着我们不应该将应用强关联与一个存储方案。因此,存储需要看上去像是原生的,也就是对上层用户来说仿佛是一个文件系统,或者是某种现有的,易于理解的API。云原生存储另一种说法是我们希望容器存储解决方案是“Cloud Native”(云原生的)。云原生计算组织(CNCF)定义了云原生系统的三个属性。这些属性也适用于存储:容器打包: 我们的物理或虚拟存储位于容器之外,但是我们希望它仅对特定容器课件(这样的话,容器就不会共享存储,除非特殊需求)。除此以外,我们可能希望容器化存储管理软件本身,从而利用容器化来管理和升级存储管理软件。动态管理:对于有状态容器的持久部署,我们需要在无需管理员认为干预的情况下,分配存储给新的容器,并清理失效的存储。面向微服务:当我们定义一个容器的时候,他应当明确的制定对存储的依赖。除此以外,存储管理软件本身应当基于微服务部署,从而更好的实现扩容和异地部署。提供容器存储为了满足容器持久化存储的需求,Kubernetes和Docker Swarm提供了一组声明式资源来声明并绑定持久化存储至容器。这些持久化存储的功能构建与一些存储架构之上。我们首先来看一下这两种环境下是如何支持容器来声明对持久化存储的以来的。Kubernetes在Kubernetes中,容器存活于Pods中。每个pod包含一个或多个容器,它们共享网络栈和持久存储。持久化存储的定义位于pod定义的volumn字段下。该卷可以被挂在到pod的任意一个容器下。比如,一下有一个Kubernetes的Pod定义,它使用了一个emptyDir卷在容器间共享信息。emptyDir卷初始为空,即使pod被迁移到另一个节点上仍将保存下来(这意味着容器的崩溃不会使其消失,但是node崩溃会将其删除)apiVersion: v1kind: Podmetadata: name: hello-storagespec: restartPolicy: Never volumes: - name: shared-data emptyDir: {} containers: - name: nginx-container image: nginx volumeMounts: - name: shared-data mountPath: /usr/share/nginx/html - name: debian-container image: debian volumeMounts: - name: shared-data mountPath: /pod-data command: ["/bin/sh"] args: ["-c", “echo Hello from the debian container > /pod-data/index.html”]如果你将以上内容保存到名为two-containers.yaml并使用kubectl create -f two-containers.yaml将其部署到kubernetes上,我们可以使用pod的IP地址来访问NGINX服务器,并获取新建的index.html文件。这个例子说明了Kubernetes是如何支持在pod中使用volumn字段声明一个存储依赖的。但是,这不是真正的持久化存储。如果我们的Kubernetes容器使用AWS EC2,yaml文件如下:apiVersion: v1kind: Podmetadata: name: webserverspec: containers: - image: nginx name: nginx volumeMounts: - mountPath: /usr/share/nginx/html name: web-files volumes: - name: web-files awsElasticBlockStore: volumeID: <volume-id> fsType: ext4在这个例子中,我们可以重复创建和销毁pod,同一个持久存储会被提供给新的pod,无论容器位于哪个节点上。但是,这个例子还是无法提供动态存储,因为我们在创建pod之前必须先创建好EBS卷。为了从Kubernetes获得动态存储的支持,我们需要另外两个重要的概念。第一个是storageClass,Kubernetes允许我们创建一个storageClass资源来收集一个持久化存储供应者的信息。然后将其和persistentVolumeClaim,一个允许我们从storageClass动态请求持久化存储的资源结合起来。Kubernetes会帮我们向选择的storageClass发起请求。这里还是以AWS EBS为例:kind: StorageClassapiVersion: storage.k8s.io/v1metadata: name: file-storeprovisioner: kubernetes.io/aws-ebsparameters: type: io1 zones: us-east-1d, us-east-1c iopsPerGB: “10”—kind: PersistentVolumeClaimapiVersion: v1metadata: name: web-static-filesspec: resources: requests: storage: 8Gi storageClassName: file-store—apiVersion: v1kind: Podmetadata: name: webserverspec: containers: - image: nginx name: nginx volumeMounts: - mountPath: /usr/share/nginx/html name: web-files volumes: - name: web-files persistentVolumeClaim: claimName: web-static-files如你所见,我们还在使用volume关键字来制定pod所需要的持久化存储,但是我们使用了额外的PersistentVolumeClaim声明来请求Kubernenetes替我们发起请求。总体上,集群管理员会为每一个集群部署一个StorageClass代表可用的底层存储。然后应用开发者会在第一次需要持久存储时指定PersistentVolumeClaim。之后根据应用程序升级的需要部署和更换pod,不会丢失持久存储中的数据。Docker SwarmDocker Swarm利用我们在单节点Docker卷上看到的核心卷管理功能, 从而支持能够为任何节点上的容器提供存储:version: “3"services: webserver: image: nginx volumes: - web-files:/usr/share/nginx/htmlvolumes: web-files: driver: storageos driver-opts: size: 20 storageos.feature.replicas: 2当我们使用docker栈部署时,Docker Swarm会创建web-files卷,仿佛它并不存在。这个卷会被保留,及时我们删除了docker栈。总的来说,我们可以看到Kubernetes和Docker都满足了云原生存储的要求。他们允许容器声明依赖的存储,并且动态的管理存储从而使其在应用需要时可见。无论容器在集群的哪个机器上运行,他们都能够提供持久存储。 ...

October 4, 2018 · 1 min · jiezi