专题:Docker 云图

专题:Docker 云图

Docker在PHP项目开发环境中的应用

{%}

作者/ 徐谦

华尔街见闻CTO,开源爱好者,关注PHP及Web开发,曾向多个知名PHP开源项目提交代码。译有《自制编程语言》、《游戏开发的数学和物理》等。

环境部署是所有团队都必须面对的问题,随着系统越来越大,依赖的服务也越来越多,比如我们目前的一个项目就会用到:

  • Web服务器:Nginx

  • Web程序:PHP + Node

  • 数据库:MySQL

  • 搜索引擎:ElasticSearch

  • 队列服务:Gearman

  • 缓存服务:Redis + Memcache

  • 前端构建工具:npm + bower + gulp

  • PHP CLI工具:Composer + PHPUnit

因此团队的开发环境部署随之暴露出若干问题:

1. 依赖服务很多,本地搭建一套环境成本越来越高,初级人员很难解决环境部署中的一些问题

2. 服务的版本差异及OS的差异都可能导致线上环境BUG

3. 项目引入新的服务时所有人的环境需要重新配置

对于问题1,可以用Vagrant这样的基于虚拟机的项目来解决,团队成员共享一套开发环境镜像。对于问题2,可以引入类似PHPBrew这样的多版本PHP管理工具来解决。但两者都不能很好地解决问题3,因为虚拟机镜像没有版本管理的概念,当多人维护一个镜像时,很容易出现配置遗漏或者冲突,一个很大的镜像传输起来也不方便。

Docker的出现让上面的问题有了更好的解决方案,虽然个人对于Docker大规模应用到生产环境还持谨慎态度,但如果仅仅考虑测试及开发,私以为Docker的容器化理念已经是能真正解决环境部署问题的银弹了。

下面介绍Docker构建PHP项目开发环境过程中的演进,本文中假设你的操作系统为Linux,已经安装了Docker,并且已经了解Docker是什么,以及Docker命令行的基础使用,如果没有这些背景知识建议先自行了解。

Hello World

首先还是从一个PHP在Docker容器下的Hello World实例开始。我们准备这样一个PHP文件index.php

<?php
echo "PHP in Docker";

然后在同目录下创建文本文件并命名为Dockerfile,内容为:

# 从官方PHP镜像构建
FROM       php

# 将index.php复制到容器内的/var/www目录下
ADD        index.php /var/www

# 对外暴露8080端口
EXPOSE     8080

# 设置容器默认工作目录为/var/www
WORKDIR    /var/www

# 容器运行后默认执行的指令
ENTRYPOINT ["php", "-S", "0.0.0.0:8080"]

构建这个容器:

docker build -t allovince/php-helloworld .

运行这个容器

docker run -d -p 8080:8080 allovince/php-helloworld

查看结果:

curl localhost:8080
PHP in Docker

这样我们就创建了一个用于演示PHP程序的Docker容器,任何安装过Docker的机器都可以运行这个容器获得同样的结果。而任何有上面的php文件和Dockerfile的人都可以构建出相同的容器,从而完全消除了不同环境,不同版本可能引起的各种问题。

想象一下程序进一步复杂,我们应该如何扩展呢,很直接的想法是继续在容器内安装其他用到的服务,并将所有服务运行起来,那么我们的Dockerfile很可能发展成这个样子:

FROM       php
ADD        index.php /var/www

# 安装更多服务
RUN        apt-get install -y \
           mysql-server \
           nginx \
           php5-fpm \
           php5-mysql

# 编写一个启动脚本启动所有服务
ENTRYPOINT ["/opt/bin/php-nginx-mysql-start.sh"]

虽然我们通过Docker构建了一个开发环境,但觉不觉得有些似曾相识呢。没错,其实这种做法和制作一个虚拟机镜像是差不多的,这种方式存在几个问题:

  • 如果需要验证某个服务的不同版本,比如测试PHP5.3/5.4/5.5/5.6,就必须准备4个镜像,但其实每个镜像只有很小的差异。

  • 如果开始新的项目,那么容器内安装的服务会不断膨胀,最终无法弄清楚哪个服务是属于哪个项目的。

使用单一进程容器

上面这种将所有服务放在一个容器内的模式有个形象的非官方称呼:Fat Container。与之相对的是将服务分拆到容器的模式。从Docker的设计可以看到,构建镜像的过程中可以指定唯一一个容器启动的指令,因此Docker天然适合一个容器只运行一种服务,而这也是官方更推崇的。

分拆服务遇到的第一个问题就是,我们每一个服务的基础镜像从哪里来?这里有两个选项:

选项一、统一从标准的OS镜像扩展,比如下面分别是Nginx和MySQL镜像

FROM ubuntu:14.04
RUN  apt-get update -y && apt-get install -y nginx



FROM ubuntu:14.04
RUN  apt-get update -y && apt-get install -y mysql

这种方式的优点在于所有服务可以有一个统一的基础镜像,对镜像进行扩展和修改时可以使用同样的方式,比如选择了ubuntu,就可以使用apt-get指令安装服务。

问题在于大量的服务需要自己维护,特别是有时候需要某个服务的不同版本时,往往需要直接编译源码,调试维护成本都很高。

选项二、直接从Docker Hub继承官方镜像,下面同样是Nginx和MySQL镜像

FROM nginx:1.9.0



FROM mysql:5.6

Docker Hub可以看做是Docker的Github,Docker官方已经准备好了大量常用服务的镜像,同时也有非常多第三方提交的镜像。甚至可以基于Docker-Registry项目在短时间内自己搭建一个私有的Docker Hub。

基于某个服务的官方镜像去构建镜像,有非常丰富的选择,并且可以以很小的代价切换服务的版本。这种方式的问题在于官方镜像的构建方式多种多样,进行扩展时需要先了解原镜像的Dockerfile

出于让服务搭建更灵活的考虑,我们选择后者构建镜像。

为了分拆服务,现在我们的目录变为如下所示结构:

~/Dockerfiles
├── mysql
│   └── Dockerfile
├── nginx
│   ├── Dockerfile
│   ├── nginx.conf
│   └── sites-enabled
│       ├── default.conf
│       └── evaengine.conf
├── php
│   ├── Dockerfile
│   ├── composer.phar
│   ├── php-fpm.conf
│   ├── php.ini
│   ├── redis.tgz
└── redis
    └── Dockerfile

即为每个服务创建单独文件夹,并在每个服务文件夹下放一个Dockerfile。

MySQL容器

MySQL继承自官方的MySQL5.6镜像,Dockerfile仅有一行,无需做任何额外处理,因为普通需求官方都已经在镜像中实现了,因此Dockerfile的内容为:

FROM mysql:5.6

在项目根目录下运行

docker build -t eva/mysql ./mysql

会自动下载并构建镜像,这里我们将其命名为eva/mysql

由于容器运行结束时会丢弃所有数据库数据,为了不用每次都要导入数据,我们将采用挂载的方式持久化MySQL数据库,官方镜像默认将数据库存放在/var/lib/mysql,同时要求容器运行时必须通过环境变量设置一个管理员密码,因此可以使用以下指令运行容器:

docker run -p 3306:3306 -v ~/opt/data/mysql:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -it eva/mysql

通过上面的指令,我们将本地的3306端口绑定到容器的3306端口,将容器内的数据库持久化到本地的~/opt/data/mysql,并且为MySQL设置了一个root密码123456

Nginx容器

Nginx目录下提前准备了Nginx配置文件nginx.conf以及项目的配置文件default.conf等。Dockerfile内容为:

FROM nginx:1.9

ADD  nginx.conf      /etc/nginx/nginx.conf
ADD  sites-enabled/*    /etc/nginx/conf.d/
RUN  mkdir /opt/htdocs && mkdir /opt/log && mkdir /opt/log/nginx
RUN  chown -R www-data.www-data /opt/htdocs /opt/log

VOLUME ["/opt"]

由于官方的Nginx1.9是基于Debian Jessie的,因此首先将准备好的配置文件复制到指定位置,替换镜像内的配置,这里按照个人习惯,约定/opt/htdocs目录为Web服务器根目录,/opt/log/nginx目录为Nginx的Log目录。

同样构建一下镜像

docker build -t eva/nginx ./nginx

并运行容器

docker run -p 80:80 -v ~/opt:/opt -it eva/nginx

注意我们将本地的80端口绑定到容器的80端口,并将本地的~/opt目录挂载到容器的/opt目录,这样就可以将项目源代码放在~/opt目录下并通过容器访问了。

PHP容器

PHP容器是最复杂的一个,因为在实际项目中,我们很可能需要单独安装一些PHP扩展,并用到一些命令行工具,这里我们以Redis扩展以及Composer来举例。首先将项目需要的扩展等文件提前下载到php目录下,这样构建时就可以从本地复制而无需每次通过网络下载,大大加快镜像构建的速度:

wget https://getcomposer.org/composer.phar -O php/composer.phar
wget https://pecl.php.net/get/redis-2.2.7.tgz -O php/redis.tgz

php目录下还准备好了php配置文件php.ini以及php-fpm.conf,基础镜像我们选择的是PHP 5.6-FPM,这同样是一个Debian Jessie镜像。官方比较亲切的在镜像内部准备了一个docker-php-ext-install指令,可以快速安装如GD、PDO等常用扩展。所有支持的扩展名称可以通过在容器内运行docker-php-ext-install获得。

来看一下Dockerfile

FROM php:5.6-fpm

ADD php.ini    /usr/local/etc/php/php.ini
ADD php-fpm.conf    /usr/local/etc/php-fpm.conf

COPY redis.tgz /home/redis.tgz
RUN docker-php-ext-install gd \
    && docker-php-ext-install pdo_mysql \
    && pecl install /home/redis.tgz && echo "extension=redis.so" > /usr/local/etc/php/conf.d/redis.ini
ADD composer.phar /usr/local/bin/composer
RUN chmod 755 /usr/local/bin/composer

WORKDIR /opt
RUN usermod -u 1000 www-data

VOLUME ["/opt"]

在构建过程中做了这样一些事情:

1. 复制php和php-fpm配置文件到相应目录

2. 复制redis扩展源代码到/home

3. 通过docker-php-ext-install安装GD和PDO扩展

4. 通过pecl安装Redis扩展

5. 复制composer到镜像作为全局指令

按照个人习惯,仍然设置/opt目录作为工作目录。

这里有一个细节,在复制tar包文件时,使用的Docker指令是COPY而不是ADD,这是由于ADD指令会自动解压tar文件

现在终于可以构建+运行了:

docker build -t eva/php ./php
docker run -p 9000:9000 -v ~/opt:/opt -it eva/php

在大多数情况下,Nginx和PHP所读取的项目源代码都是同一份,因此这里同样挂载本地的~/opt目录,并且绑定9000端口。

PHP-CLI的实现

php容器除了运行php-fpm外,还应该作为项目的php cli使用,这样才能保证php版本、扩展以及配置文件保持一致。

例如在容器内运行Composer,可以通过下面的指令实现:

docker run -v $(pwd -P):/opt -it eva/php composer install --dev -vvv

这样在任意目录下运行这行指令,等于动态将当前目录挂载到容器的默认工作目录并运行,这也是PHP容器指定工作目录为/opt的原因。

同理还可以实现phpunit、npm、gulp等命令行工具在容器内运行。

Redis容器

为了方便演示,Redis仅仅作为缓存使用,没有持久化需求,因此Dockerfile仅有一行

FROM redis:3.0

容器的连接

上面已经将原本在一个容器中运行的服务分拆到多个容器,每个容器只运行单一服务。这样一来容器之间需要能互相通信。Docker容器间通讯的方法有两种,一种是像上文这样将容器端口绑定到一个本地端口,通过端口通讯。另一种则是通过Docker提供的Linking功能,在开发环境下,通过Linking通信更加灵活,也能避免端口占用引起的一些问题,比如可以通过下面的方式将Nginx和PHP链接起来:

docker run -p 9000:9000 -v ~/opt:/opt --name php -it eva/php
docker run -p 80:80 -v ~/opt:/opt -it --link php:php eva/nginx

在一般的PHP项目中,Nginx需要链接PHP,而PHP又需要链接MySQL,Redis等。为了让容器间互相链接更加容易管理,Docker官方推荐使用Docker-Compose完成这些操作。

用一行指令完成安装

pip install -U docker-compose

然后在Docker项目的根目录下准备一个docker-compose.yml文件,内容为:

nginx:
    build: ./nginx
    ports:
      - "80:80"
    links:
      - "php"
    volumes:
      - ~/opt:/opt

php:
    build: ./php
    ports:
      - "9000:9000"
    links:
      - "mysql"
      - "redis"
    volumes:
      - ~/opt:/opt

mysql:
    build: ./mysql
    ports:
      - "3306:3306"
    volumes:
      - ~/opt/data/mysql:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: 123456

redis:
    build: ./redis
    ports:
      - "6379:6379"

然后运行docker-compose up,就完成了所有的端口绑定、挂载、链接操作。

更复杂的实例

上面是一个标准PHP项目在Docker环境下的演进过程,实际项目中一般会集成更多更复杂的服务,但上述基本步骤仍然可以通用。比如EvaEngine/Dockerfiles是为了运行我的开源项目EvaEngine准备的基于Docker的开发环境,EvaEngine依赖了队列服务Gearman,缓存服务Memcache、Redis,前端构建工具Gulp、Bower,后端Cli工具Composer、PHPUnit等。具体实现方式可以自行阅读代码。

经过团队实践,原本大概需要1天时间的环境安装,切换到Docker后只需要运行10余条指令,时间也大幅缩短到3小时以内(大部分时间是在等待下载),最重要的是Docker所构建的环境都是100%一致的,不会有人为失误引起的问题。未来我们会进一步将Docker应用到CI以及生产环境中。

阅读卧龙阁专栏

阅读图灵社区原文

《Docker开发实践》作者曾金龙:迅雷云的Docker开发实践

{%}

曾金龙就职于迅雷网络,是国内覆盖面最广的“迅雷P2P引擎”核心研发成员。他毕业于中山大学,具有计算机科学硕士学位,他的研究方向是P2P网络、音视频传输和CEP系统。曾金龙对Docker技术有着深入的理解,是国内较早将Docker引入到实际软件开发、测试和部署中的人。他在2015年出版了《Docker开发实践》一书。

问:作为迅雷P2P引擎的核心开发成员,能否谈一下当时开发的经历以及克服了哪些困难?

我一开始工作的时候就进入到了一家在P2P领域NO.1的公司,而且还是核心团队,所以压力非常大。我发现,现实的P2P依然处在第一代P2P的水平,和研究上的P2P有很大差异。我们在高校里面研究的是各种结构化的P2P,叫做第二代P2P。这就意味着即便我是以P2P为研究方向来到了迅雷,但一切都是归零的。这是理想与现实之间的差异,对我个人而言是个困难。

开发上的困难有两个方面,一是新做的项目往往配置基础环境会耗费大量的时间,而且开发不会碰到的问题在测试和运营那里会“莫名其妙”地出现,所以容器是解决程序员痛点的好技术。另外就是迅雷云是全国最大的P2P数据平台,所以里面有非常多分工不同的服务器,有打洞的Punch服务、有索引的Hub服务、跟踪的Tracker服务以及CDN管理服务等,而且处理的数据量非常大。让公司各个事业群的项目组合理管理和使用这些集群确实不容易,而大多数问题都来自不正确的使用,所以我们必须为各个事业群提供更为易用的服务和更加统一的接口。所以我们后面在迅雷云引入了Docker。

问:能否谈一下迅雷云使用Docker的过程?

其实最初的时候,迅雷团队对Docker是怀有谨慎的态度的。不能因为别人说好我们就用,需要先试,真的好我们才敢用,尝到甜头我们才愿意用。一开始我们用Docker来加快团队之间的协作效率,因为通过它部署的环境可以一次构建到处使用,所以开发、测试和部署都能够在一个非常一致的环境中进行,这是很初级的用法。随着对这个模式的认可,迅雷内部越来越多的团队接纳了我们的建议,于是迅雷、迅雷看看、迅雷游戏都采用了Docker,引入到了开发、测试和部署中。

后面我们的服务器项目基本就迁移到了Docker之上。当然,一开始大家对Docker的安全性都会有所顾虑,但其实大多数顾虑都是因为对Docker不够理解而产生的。有了内部产品线的支持,我们后面把迅雷云部分服务器资源用Docker重新部署。迅雷云本身就是一个非常高效的集群系统,现在引入Docker是为了让资源使用更加便捷。因为研发人员的流动性比较高,不是所有的开发者对迅雷云内部资源的使用都熟悉,所以我们现在就是借助Docker去为各个业务的开发提供一套更普适的接口。

问:迅雷云在容器方面做了哪些优化和调整?

一是整合现有的集群,将部分集群更改为Docker集群,特别是新增部分。二、由于迅雷云是一个重数据、轻业务的云,所以在数据管理方面,像Docker的Data volumes和Data volume containers这种简单的方式还是不能够满足,所以我们主要还是以迅雷云自己的管理系统为主。为了能够让Docker之上的业务更好地使用我们的集群,我们添加了适配层。三、调度算法是迅雷云定制优化的。最后,我们有自建的私有仓库,提供常用镜像,让我们的开发更加快速。

问:Adrian Cockcroft是云技术社区中的思想家之一,他将微服务与Docker的结合视为一种颠覆,认为这种开发方式会极大提高开发团队敏捷性和适应性。请问迅雷云团队是否有这方面的实践?对于这方面的技术融合你有什么样的建议?

微服务是把一个原本很复杂的服务解耦拆解为一些相对功能单一的服务,这在迅雷云内部是一直都非常提倡的。我们认为一个服务专心做好一件事是最好的,这对解耦、易用、升级都非常有好处。迅雷内部把Hub服务和Tracker服务都进行了分解和抽象,提供给各个项目组。Hub本身是很复杂的,但我们的服务接口却是个位数,包括对CDN资源服务的使用方面我们也是重构了一套更加简单易用的服务。

有一点很遗憾的是我觉得迅雷云里面做得最好的微服务并没有使用到Docker,也就是我们的“水晶计划”。这主要是因为我们的节点是各种客户端设备,但我们的设计宗旨依然是遵循微服务的。水晶计划的节点是各种各样的设备,差异性非常大。但没有关系,在用户那里,它就是可靠的CDN。我个人觉得微服务加上Docker是非常合适的,因为微服务很重要的一点就是解耦,而采用容器技术来解耦隔离是非常恰当的。再进一步考虑到服务的扩容和迁移的话,微服务设计加上Docker作为载体也是非常合适的。我的建议就是,一个服务一个容器,或者说一种服务,一类容器,这样一一对应地去部署。

问:Docker的image和安全问题一直都受到关注,特别在去年的出现的ShellShock和Heartbleed问题之后,请问在Docker的安全方面你是否有哪些经验可以分享?

其实,越多人使用的技术越安全,但一旦不安全造成的损失也非常大。迅雷在使用Docker这方面一直没有把数据层面交给迅雷云之外的系统。即便是Docker使用迅雷云的数据,也是通过我们的适配层,而通过适配层接进来的操作是要经过授权的,对使用能力也要进行限制,同时操作都是可跟踪和统计的。另外就是对用户的授权管理,严格规范的管理在安全方面很有必要。还有就是做好隔离工作,不仅仅是通过内核的Namespace/cgroup,同时也可以在不同层面隔离,比如VM和容器的隔离等。

问:在ClusterHQ最近的一项调查中显示,只有38%的IT专业人士在生产环境中使用容器,人们对于容器技术使用的顾虑包括安全、数据管理、IT人员技能、永久存储与可靠性等问题。你认为以Docker为代表的容器技术面临的最大阻碍是什么?是否在不远的未来有望跨越技术应用的鸿沟?

我觉得当前容器技术面临的最大阻力是安全问题和管理工具。安全性一直是企业使用容器所担心的问题,而另一个问题就是管理工具,特别是对集群的管理。现有的集群管理工具,比如Kubernetes,并不是很好上手,对于很多运维人员而言,特别是对一些小企业来说,运维人员可能并没有深入地去学习这些工具怎么使用。所以如果这些工具的部署配置和使用能够非常简单,那么容器在实际生产中可能会越来越广泛。

这种情况有些类似于ISO/OSI的参考模型和TCP/IP协议模型,前者从设计上而言是很优秀的,但后者却成了事实标准,为什么?因为简单。所以一门好技术,不但要技术本身好,而且要做到让别人更好地接纳也很重要。Docker相对于以前的诸如lxc的系统是一个进步,我希望Docker能继续做好整个工具链,让Docker集群更加平易近人。

问:Docker, CoreOS, 以及其他业界成员一起创立了“开放容器计划(OCP)”,你认为OCP的成立对于软件开发行业会造成什么影响?

Docker和CoreOS这对竞争对手握手言和,一起成立OCP,对于整个容器生态圈而言是一个非常重大的利好。因为OCP可以让容器技术更加统一化,如果所有的容器都遵循一套标准,那么使用容器的开发者就省了很多麻烦。而且有了统一的标准,就意味着在不同平台上的容器也可以相互迁移,Docker的镜像可以迁移到CoreOS上,反之亦然。破除各自为阵,容器技术会变得更加易用,同时也会加快其在产业上的应用。

问:Docker和CoreOS各自都具有哪些优势?它们的结合会带来哪些好处?

Docker更想去做一个以容器为中心的服务平台,所以它给用户提供了很多便利的操作,还包括一些集群管理工具链。CoreOS则更加侧重容器本身的标准化,CoreOS的开放标准更能够促进容器的开放,这也是OCP的宗旨,此外,CoreOS在安全性和可组合性上也做得更好。它们的结合可以取二者优势,弥补各自不足。

问:Docker最新发布的“Docker试验性发布”是一次有趣的尝试,请问你是否使用过上面的实验性功能?你觉得这个系统的发布对于Docker来说有哪些好处?

如果你指的是out-of-process volume plugins,我有了解但没有使用过。在对数据方面采用插件的形式,让用户自己去适配不同的存储系统是非常有必要的,所以我觉得这个功能非常有用。我在上面也提到了,我们迅雷云就写了一套适配层去操作我们自己的数据系统。所以后面我们团队会去深入地理解这个功能,然后跟进。

问:学习Docker的进阶过程有哪几个阶段?你希望程序员们以怎样的方式使用《Docker开发实践》这本书?

我觉得学习Docker有三个阶段。第一个阶段是对Docker本身功能的使用,也就是入门层,你要理解什么是镜像、容器、Registry、Dockerfile等,了解这些对象的基本操作,能够构建出自己的应用环境来。第二个阶段是能够驾驭Docker集群。这个阶段你需要学习和使用Docker相关的工具,比如Kubernetes、Shipyard、Machine+Swarm+Compose等,根据不同的需求,需要使用不同的工具。第三个阶段就是发现和解决这些工具里面的问题,为自己的场景深度定制。

我觉得程序员可以从两个方面来使用好《Docker开发实践》这本书,一方面可以把它当做开发手册,因为这本书的内容几乎涵盖了Docker所有的知识点,我们对其内容进行了很系统地梳理。所以当你对Docker有什么疑问的时候,把《Docker开发实践》放在电脑前面,随手翻一翻基本就可以解决。另一方面,可以作为案例引导。这本书后面两篇甄选了当前经典的应用场景,一步步带领读者把服务搭建起来,并且把可能遇到的问题都标出来,教读者如何解决。所以,读者也可以参照本书的案例去更好更快地在实际生产中使用Docker,提升效率、少走弯路。

问:《Docker开发实践》的高级篇对Docker生态圈做了非常翔实的介绍和实践,为什么要对Docker生态圈着墨如此之多?对于这部分内容的遴选你有什么样的标准和原则?

Docker本身的操作其实很简单,如果复杂的话,Docker就不会像今天这样成功。然而我所知道的市面上之前所有关于Docker的书籍,都是把基础操作写了一大篇,然后堆砌一些案例,就成了一本书,我认为这是在浪费读者的钱和时间。基础的东西写成几章是合理的,写成一本书就是打肿脸充胖子了。我们在《Docker开发实践》中把这些内容进行了压缩,不想去滥竽充数,也不会去浪费笔墨。

上面我提到的Docker面临的阻碍很大一点就是它的工具链不完善和不易用。你会发现在网络上程序员求助最多的也是Docker生态圈的实践,因为Docker做得还不够好,稍微不小心就犯错。所以,既然基础篇不该讲那么多,而且生态圈又是使用Docker的开发者心中的痛点,那么我就着重笔墨去写这部分,让大家少走弯路。这部分内容看上去有些散,每一章都是讲一个工具或者是一组工具,但是书中选的Kubernetes、Shipyard等都是我们最需要的和最热点的工具,而且它们的构建又相对复杂。所以,我遴选的原则就是,重要又是难点的,我就选。所以这就成了《Docker开发实践》中的高级篇。

{%}

Kubernetes的核心概念和核心组件

作者/ 曾金龙

曾金龙就职于迅雷网络,是国内覆盖面最广的“迅雷P2P引擎”核心研发成员。他毕业于中山大学,具有计算机科学硕士学位,他的研究方向是P2P网络、音视频传输和CEP系统。曾金龙对Docker技术有着深入的理解,是国内较早将Docker引入到实际软件开发、测试和部署中的人。他在2015年出版了《Docker开发实践》一书。

Kubernetes 简介

Kubernetes是Google公司开源的大规模容器集群管理系统。利用Kubernetes,能方便地管理跨机器运行的容器化应用。它为容器化应用提供资源调度、部署、服务发现、扩展机制等功能,具体如下:

  • 使用Docker对应用进行打包、实例化和运行;

  • 以集群的方式运行和管理跨主机的容器;

  • 解决跨主机容器的通信问题;

  • 提供自我修复功能,保证系统运行的健壮性。

Kubernetes目前处于快速迭代开发之中,几乎每周都会推出新的版本。作为一个容器管理框架,Kubernetes可以被部署在物理集群和各类云环境中,例如GCE、vSphere、CoreOS、OpenShift、Azure等。

接下来,我们将会从Kubernetes的核心概念、设计框架等入手,让读者更好地理解它。等有了相关基础之后,我们将会在CentOS 7上实验Kubernetes。

核心概念

Kubernetes的核心概念包含节点(Node)、Pod、服务(Service)、备份控制器(Replication Controller)、卷(Volume)和标签(Label)。鉴于备份控制器和卷比较简要,这里不再详细介绍,后面介绍架构时一起说明。

节点

节点是Kubernetes系统中的一台工作机器,常被称为Minion,即从属主机。它可以是物理机,也可以是虚拟机。每一个节点都包含了Pod运行所需的必要服务,例如Docker、kubelet和网络代理(proxy)。节点受Kubernetes系统中的主节点控制。和Pod、服务不一样,节点本身并不属于Kubernetes的概念,它是云平台中的虚拟机或者实体机。所以,当一个节点加入到Kubernetes系统中时,它将会创建一个数据结构来记录该节点的信息。另外,不是所有节点都能够加入到Kubernetes系统中的,只有那些通过验证的节点才能够成为Kubernetes节点。

目前,节点的管理有两种方式:节点管理器(Node Controller)和通过命令手动管理。

  • 节点管理器。它是Kubernetes主控节点上管理集群节点的组件,主要包含两个功能:集群节点的同步和单个节点生命周期的管理。当有节点加入到Kubernetes中时,节点管理器将会创建节点信息;当有节点需要从Kubernetes中删除时,节点管理器则会删除该节点的节点信息。需要注意的是,节点管理器并不会真正创建节点本身,而仅仅创建节点的元数据,用于跟踪节点的状态。所以,节点上的服务需要用户自己安装。单个节点的生命周期的管理目前尚在开发之中。

  • 手动管理节点。Kubernetes的管理员可以通过kubectl命令来管理节点。和节点管理器一样,使用kubectl命令创建和删除节点时,也只是删除节点的配置信息。

Pod

在Kubernetes中,Pod是最小的可创建、调度和管理的部署单元。它是容器化环境中的“逻辑主机”,可以包含一个或多个有关联的容器,并且容器之间可以共享数据卷。例如,一个Web站点应用由前端、后端和数据库组成,这三个组件运行在各自的容器中,我们可以创建包含这三个容器的Pod。

可以看出,容器存在于Pod之中,而Pod又存在于节点之中。那么,为什么需要抽象出Pod这个概念呢?下面从资源共享和通信、管理这两个方面介绍一下。

  • 资源共享和通信。同一Pod中的容器拥有相同的网络命名空间、IP地址和端口区间,它们之间可以直接用localhost来发现和通信。在无层次的共享网络中,每个Pod都有一个IP地址,用于跟其他物理主机和容器进行通信,Pod的名字也被用作主机名。此外,同一Pod的容器可以共享数据卷。在将来,Pod内的容器还可以共享IPC命名空间、CPU和内存等。

  • 管理。从管理的角度来看,Pod比容器站在更高的层面,它简化了应用的部署和管理。Pod可以自动处理主机托管、资源共享、协调复制和依赖管理等问题。

虽然可以将Pod用在垂直依赖的应用栈中,但它更适合用在多个应用的横向协作部署中,为多个容器提供集中的辅助功能。具体的使用用例有:

  • 内容管理系统、文件和数据的装载和本地缓存管理等;

  • 日志和检出点备份、压缩、轮换和快照;

  • 数据变更监控、日志末端数据读取、日志和监控适配器和事件打印;

  • 代理、桥接和适配器;

  • 控制器、管理器、配置编辑和更新。

为什么不在一个容器中直接运行多个应用而采用在一个Pod中运行多个容器呢?主要原因如下所示。

  • 透明性。底层系统可以获取到Pod内的容器,这样底层系统就可以为容器提供诸如进程管理和资源监控等服务,这会给用户带来不少便利。

  • 解耦软件依赖。分为多个容器后,每个容器都可以单独存在,并且当其中某个应用需要升级时,只影响到一个容器。后续Pod将会支持单个容器的在线升级。

  • 易用性。用户不需要使用自己的进程管理程序,直接用Docker管理容器即可。此外,用户也不用再担心信号量和退出码的传递等问题。

  • 高效性。因为底层系统提供了更多的管理,这使得容器更加轻便。

服务

Kubernetes的服务是一系列Pod以及这些Pod的访问策略的抽象。

Kubernetes中的Pod是具有时效性的,它会随着时间而变化。虽然每个Pod都有一个单独的IP地址,但是该IP地址却不是静态不变的。例如,当系统触发RepliController对Pod进行备份时,Pod的地址很可能会发生改变。这将会导致一个问题。我们试想,在Kubernetes系统中,有一群Pod作为后端给前端提供服务,如果后端的IP地址是变动的,那么前端又如何去发现和使用后端的服务呢?

Kubernetes的服务也叫作微服务(micro-service),它用于定义一系列Pod的逻辑关系以及它们的访问规则。服务的目标是为了隔绝前端和后端的耦合性,让前端透明地使用该项服务,而不需要知道该项服务具体由哪些后台机器提供。Kubernetes会为一个服务分配一对,该IP和端口并不是真实的地址和端口,而是一个虚拟IP,当前端通过该对访问服务时,服务代理将会将请求重定向到合适的后端机器。

1. 定义服务

Kubernetes中的服务是一个REST(Representational State Transfer,表述性状态转移)对象。下面演示了一个服务的定义:

{
    "id": "myapp",
    "selector": {
    "app": "MyApp"
    },
    "containerPort": 9376,
    "protocol": "TCP",
    "port": 8765
}

上述示例定义了一个id为myapp的服务,它通过选择器选择那些带有app=MyApp标签的Pod作为服务的提供者。所有被选择的Pod都将9376端口暴露,用于监听该项服务的请求。前端客户可以通过$MYAPP_SERVICE_PORT和$MYAPP_SERVICE_HOST来访问该项服务。

2. 工作原理

在Kubernetes中,每个节点都运行着一个服务代理(service proxy),它监控来自Kubernetes主控节点的消息,主控节点会向其传递诸如添加和删除服务以及服务的端点列表等信息。服务代理维护着一个映射表,表中每一项是服务和该服务的提供者列表的映射关系,服务代理还会为每一个服务在本地开放一个端口,当节点需要使用某项服务时,将请求发送至该端口,然后由服务代理通过某种策略(例如轮换策略)安排服务的具体提供节点。

当一个Pod加入到Kubernetes集群中时,主控节点会在它上面为每一个已经存在的服务分配一系列环境变量。变量的命名形如SVCNAME_SERVICE_HOST,其中SVCNAME是服务名称的大写。例如,服务redis-master在端口6379上提供TCP服务,其虚拟IP地址为10.0.0.11,那么该项服务对应的环境变量就有:

REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11

这些环境变量主要说明服务的地址、端口和协议。需要注意的是,既然一个Pod加入到Kubernetes集群时,会被设置这些跟服务相关的环境变量,这意味着一个Pod只能发现在它加入之前就已经存在的服务。当然,如果服务支持了DNS,该条限制将会失效。

如图1所示,在前端Pod加入到该Kubernetes集群时,主控节点的apiserver会将MyApp服务的服务信息推送给该Pod,这些信息包含了服务及其对应的端点(Endpoint)列表。前端Pod需要使用后端Pod提供的MyApp服务,它并不直接联系后端Pod,而是将请求发给本地的服务代理,服务代理会将该服务请求分配给这三个后端Pod中的一个。服务可以动态地增加和删除提供服务的Pod,而前端Pod感知不到这些细节的变化,除非是正在为它提供服务的Pod状态发生了改变。在Kubernetes服务的工作原理背后,还有两个细节需要注意:冲突避免和Portal。

{%}

图1 前端通过服务代理来访问后端提供的服务

3. 冲突避免

Kubernetes的一个设计理念就是不能随便让用户去承受服务的失败,特别是用户根本就没有犯错的情况下。这里我们考虑一下服务端口冲突的问题:假如第一项服务选取了80端口作为服务端口,那么其他服务就不能再选该端口作为服务端口了。为了避免这种冲突问题,Kubernetes不仅选择了为每一个服务分配端口,同时也配置了一个IP。这样每一个服务都拥有自己的IP和端口对应,从而避免了服务端口冲突。

4. Portal

Portal就是前面提到的服务的二元组。这里的IP是虚拟IP,它并不是一台特定机器的真实IP。用户访问某个服务,就是访问某个服务的Portal。当请求投递到该Portal之后,该请求会根据规则重定向到某个特定的服务提供者Pod。下面我们举例说明该问题。如图2所示,每一个节点都会有一个服务代理,当节点加入到Kubernetes集群中时,主控节点的apiserver程序会将服务的配置信息投递给服务代理,服务代理保存服务配置信息,并根据Portal信息设置iptables的网络规则,这里我们假设Portal为10.0.0.1:1234。前端通过链接Portal来访问服务,iptables监听到该请求,将该请求重定向到配置好的代理端口上,服务代理从该端口接过服务请求,然后根据策略选取一个后端Pod,最后由选中的后端Pod来给前端Pod提供服务。

{%}

图2 服务代理响应服务的过程

标签

标签(Label)是一组附加在对象上的键值对。标签常用来从一组对象中选取符合条件的对象,这也是Kubernetes中目前为止最重要的节点分组方法。标签的本质是附属在对象上的非系统属性类的元数据,即它不是名字、Id以及对象的硬件属性,而是一些附加的键值对,这些键值对对对象本身没什么影响,但对我们操作对象却极为有用。例如,下面是一个Pod节点的配置文件:

{
    "id": "redis-master",
    "kind": "Pod",
    "apiVersion": "v1beta1",
    "desiredState": {
        "manifest": {
            "version": "v1beta1",
            "id": "redis-master",
            "containers": [{
                "name": "master",
                "image": "dockerfile/redis",
                "cpu": 100,
                "ports": [{
                    "containerPort": 6379,
                    "hostPort": 6379
                }]
            }]
        }
    },
    "labels": {
        "name": "redis-master"
    }
}

可以看到,除了id、kind等这些系统属性外,还有labels属性,它是这个Pod的标签,它的键值对为"name":"redis-master"。有了这个标签,我们就可以在部署服务时,通过标签来指定只有包含该标签的Pod才可以部署该服务。例如,我们定义如下服务:

{
    "id": "redis-master",
    "kind": "Service",
    "apiVersion": "v1beta1",
    "port": 6379,
    "containerPort": 6379,
    "selector": {
        "name": "redis-master"
    },
    "labels": {
        "name": "redis-master"
    }
}

这个redis-master服务通过selector(选择子)来选择标签为"name":"redis-master"的Pod来部署服务。当然,该服务自己又定义了一个标签,其中的键值对为"name":"redis-master",这样需要使用该服务的应用又可以根据该标签来过滤服务。

架构和组件

Kubernetes的架构如图3所示。

{%}

图3 Kubernetes的架构

在Kubernetes集群中,主要包含主控节点(Master)和从属节点(Minion),前者负责整个集群的管理工作,后者是集群的工作节点群。

主控节点

主控节点包含apiserver、调度器(Scheduler)和控制器管理器(Controller Manager)。

1. apiserver

在apiserver中,我们定义了诸多Kubernetes的核心对象以及它们的操作。这些核心对象包含Pod注册表(Pod Registry)、控制器注册表(Controller Registry)、服务注册表(Service Registry)、端点注册表(Endpoint Registry)、从属注册表(Minion Registry)、绑定注册表(Binding Registry)。

在分别说明这些注册表之前,我们先来看看主控节点是如何处理客户端请求的,具体如图4所示。

{%}

图4 主控节点处理客户端请求的流程

(1) kubectl将用户命令发送给Kubernetes客户端。

(2) Kubernetes客户端将请求发送给apiserver。

(3) apiserver根据请求类型,选择具体的REST存储API对请求进行处理。例如创建Pod,那么则是Pod Registry存储,并将该值存储于Etcd中。

(4) 在apiserver响应请求之后,调度器会去Kubernetes客户端收集从属节点的节点信息以及Pod信息。

(5) 根据所收集的信息,调度器将新建的Pod分发到合适的从属节点上。

在apiserver的存储库中,存储着Kubernetes的各种核心对象信息,它们是一个个注册表。下面我们简要说明最重要的几个注册表。

  • 从属注册表。负责跟踪集群中的从属节点信息。Kubernetes将节点注册表的信息封装成RESTful形式并提供API,通过这些API可以创建和删除从属节点,目前不支持从属节点的修改。Scheduler根据从属的节点信息决定是否将新的Pod分配到该节点。

  • Pod注册表。记录集群中的Pod信息以及Pod和从属节点的映射关系。通过REST接口,可以对Pod进行创建(Create)、获取(Get)、列出(List)、更新(Update)和删除(Delete)等操作。此外,可以通过watch接口监听Pod的事件,例如创建、删除等。

  • 服务注册表。负责跟踪集群中所有服务的信息。通过REST接口,可以对服务进行创建、获取、列出、更新和删除等操作。此外,也可以通过watch接口设置事件监听,监听服务的变更事件。

  • 控制器注册表。负责跟踪集群中所有的控制器,例如备份控制器(Replication Controller)的信息。通过REST接口,可以对控制器进行创建、获取、列出、更新和删除等操作。此外,还可以通过watch接口监听控制器的事件。

  • 端点注册表。负责收集服务的端点信息。一个服务可以和多个端点关联,每一个端点就是集群中提供该服务的从属节点。通过REST接口,可以对该表项进行创建、获取、列出、更新、删除和监视操作。

  • 绑定注册表。它是记录Pod和节点之间绑定关系的表,只有创建操作。

2. 调度器

它负责收集和分析当前Kubernetes集群中所有从属节点的资源使用情况,并为新建的Pod分配资源。调度器会实时监控Kubernetes集群中未分发的Pod,同时实时追踪集群中所有运行的Pod,根据这些Pod的资源使用状况,为尚未分发的Pod选择一个最为合适的从属节点。一旦将某个Pod分配到某个从属节点,那就意味着这个节点的资源被占用,这部分资源就不可能再分配给其他Pod,除非该Pod被回收。在将Pod分发到指定从属节点后,调度器会把Pod的相关信息写回到apiserver。

3. 控制器管理器

目前,Kubernetes的控制器有端点控制器(Endpoint Controller)和备份控制器(Replication Controller)。端点控制器主要保证服务和Pod之间的映射关系是最新的。例如当Pod失效时,则应该及时更新服务与它的映射,将它从服务的映射列表中去除。当有新的符合条件的Pod加入到一项服务时,需要更新服务的端点信息。备份控制器用于控制Pod的备份,解决Pod的扩容缩容问题。分布式应用出于提升服务性能和容错性等考虑,需要复制多份资源并根据负载而动态增加或者减少。当某项服务的某个Pod因为异常而宕机时,备份控制器在检测到该事件发生后,会立即新建一个该服务的Pod,以保证服务质量。

备份控制器的主要用法如下所示。

  • 调度。备份控制器会保证指定Pod的指定副本数量在运行,当节点异常退出时,立即新建Pod副本进行替换。

  • 扩容缩容。根据需要动态增加或减少Pod数量。

  • 逐步更新。某项服务需要更新时,可以对一个个的Pod进行升级更新。

  • 应用的多分支跟踪。一个应用可以有多个分支,并且在集群中可以同时运行多个分支,而分支之间通过标签来识别。

从属节点

从属节点是Kubernetes集群中真正工作的节点。除了包含Pod外,还有用于管理和通信的基础设施,主要是kubelet组件和服务代理。

1. kubelet

kubelet主要负责管理Pod及容器,以及与apiserver通信。它接收来自主控节点的apiserver组件发来的命令和任务,并与Etcd、http等服务交互。kubelet包含Docker客户端、根目录、Pod Worker、Etcd客户端、Cadvisor客户端以及Health Checker组件。它的工作具体包括:

  • 通过Pod Worker给Pod分配任务;

  • 同步Pod的状态;

  • 从Cadvisor获取容器、Pod、宿主主机信息;

  • 管理Pod容器,包括运行一个指定的容器,创建网络容器,给容器绑定数据卷和端口,杀死和删除容器以及在容器中运行命令。

2. 服务代理

我们这里说到的代理即为服务代理。每生成一种服务,代理都会从Etcd中获取该服务的端点列表信息,然后根据配置设置iptables规则,对服务请求进行重定向。

组件交互流程

在介绍完核心概念、架构和组件之后,接下来说明这些组件是如何串联起来为我们服务的。这里我们介绍一下创建Pod、备份控制器和服务的流程。

1. 创建Pod的流程

图5展示的是创建Pod的时序图。首先用户发起一个创建Pod的请求,kubectl会将该请求发送给主控节点的apiserver组件;apiserver收到请求后,会向Etcd服务器请求添加一个Pod对象。调度器定时获取整个集群中从属节点和Pod的状态,收集其中的资源利用状况,然后决定将本次新建的Pod分配到哪个从属节点。当从属节点和Pod绑定后,调度器会将该绑定信息返回给apiserver,然后apiserver会将该绑定持久化到Etcd中。此后,从属节点上的kubelet会定时地向Etcd汇报该绑定的状态。kubelet会根据Pod配置信息创建并启动容器。

{%}

图5 创建Pod的时序图

2. 创建备份控制器的流程

图6是创建备份控制器的时序图。用户通过kubectl创建控制器,kubectl会将该请求投递到主控节点的apiserver组件上,apiserver会在Etcd上新建controller对象。控制器管理器定时从apiserver查询控制器的状态,获取控制器相关Pod的状态。当apiserver收到查询Pod状态的请求后,它需要向相关的从属节点发出Pod状态查询,从属节点上的kubelet组件收到该请求后,会进一步向Pod中的容器发起查询,然后将状态返回给apiserver,进一步返回给控制器管理器。控制器管理器同步好Pod状态信息后,如有需要,例如发现有些Pod已经异常退出了,那么它就需要创建Pod的副本,以保证集群中始终保持设定数量的Pod在运行。

{%}

图6 创建备份控制器的流程

3. 创建服务的流程

图7是创建服务的时序图。用户先创建一个新的应用服务,apiserver会通知Etcd建立相应的service对象。控制器管理器获取该Pod列表后,将这些Pod都设定为该服务的端点,这些端点信息会保存在Etcd中。从属节点上的服务代理会定期从apiserver那里获取所有服务的信息,发现有新的服务并确认自己属于服务的提供者之后,接着在本地创建套接字用于监听该服务的请求,设置iptables的网络规则,用于服务的重定向。此外,代理会及时更新服务的端口列表,以供服务响应。

{%}

图7 创建服务的时序图

 

{%}

《Docker开发实践》由浅入深地介绍了Docker的实践之道,首先讲解Docker的概念、容器和镜像的相关操作、容器的数据管理等内容,接着通过不同类型的应用说明Docker的实际应用,然后介绍了网络、安全、API、管理工具Fig、Kubernetes、shipyard以及Docker三件套(Machine+Swarm+Compose)等,最后列举了常见镜像、Docker API等内容。 本文街选择《Docker开发实践》

Mesos+Docker+Go,用300行代码创建一个分布式系统

作者/ John Walter

John毕业于北卡罗来纳大学,他在那里学习的是电影。在DZone,他专注于物联网和移动开发,他是物联网Zone和移动Zone的引导者,他对社区充满热情。他的爱好包括制作电影和打篮球。

 

    虽然Docker和Mesos已成为不折不扣的Buzzwords ,但是对于大部分人来说它们仍然是陌生的,下面我们就一起领略Mesos 、Docker和Go 配合带来的强大破坏力,如何通过300行代码打造一个比特币开采系统。

时下,对于大部分IT玩家来说, Docker和Mesos都是熟悉和陌生的:熟悉在于这两个词无疑已成为大家讨论的焦点,而陌生在于这两个技术并未在生产环境得到广泛使用,因此很多人仍然不知道它们究竟有什么优势,或者能干什么。近日, John Walter在Dzone上撰文 Creating a Distributed System in 300 Lines With Mesos, Docker, and Go,讲述了Mesos、Docker和Go配合带来的强大破坏力,本文由OneAPM工程师编译整理。

诚然,构建一个分布式系统是很困难的,它需要可扩展性、容错性、高可用性、一致性、可伸缩以及高效。为了达到这些目的,分布式系统需要很多复杂的组件以一种复杂的方式协同工作。例如,Apache Hadoop 在大型集群上并行处理 TB 级别的数据集时,需要依赖有着高容错的文件系统(HDFS)来达到高吞吐量。

在之前,每一个新的分布式系统,例如Hadoop和Cassandra ,都需要构建自己的底层架构,包括消息处理、存储、网络、容错性和可伸缩性。庆幸的是,像Apache Mesos这样的系统,通过给分布式系统的关键构建模块提供类似操作系统的管理服务,简化了构建和管理分布式系统的任务。Mesos抽离了CPU 、存储和其它计算资源,因此开发者开发分布式应用程序时能够将整个数据中心集群当做一台巨型机对待。

构建在Mesos上的应用程序被称为框架,它们能解决很多问题:Apache Spark,一种流行的集群式数据分析工具;Chronos ,一个类似cron的具有容错性的分布式scheduler ,这是两个构建在Mesos上的框架的例子。构建框架可以使用多种语言,包括C++,Go,Python,Java,Haskell和Scala。

在分布式系统用例上,比特币开采就是一个很好的例子。比特币将为生成acceptable hash的挑战转为验证一块事务的可靠性。可能需要几十年,单台笔记本电脑挖一块可能需要花费超过 150 年。结果是,有许多的“采矿池”允许采矿者将他们的计算资源联合起来以加快挖矿速度。Mesosphere 的一个实习生, Derek ,写了一个比特币开采框架(https://github.com/derekchiang/Mesos-Bitcoin-Miner),利用集群资源的优势来做同样的事情。在接下来的内容中,会以他的代码为例。

1个Mesos框架有1个scheduler和1个executor组成。schedule和Mesos master通信并决定运行什么任务,而executor运行在slaves上面,执行实际任务。大多数的框架实现了自己的scheduler,并使用1个由Mesos提供的标准executors。当然,框架也可以自己定制executor。在这个例子中即会编写定制的scheduler,并使用标准命令执行器(executor)运行包含我们比特币服务的Docker镜像。

对这里的scheduler来说,需要运行的有两种任务——one miner server task and multiple miner worker tasks。server会和一个比特币采矿池通信,并给每个worker分配blocks 。Worker会努力工作,即开采比特币。

任务实际上被封装在executor框架中,因此任务运行意味着告诉Mesos master在其中一个slave上面启动一个executor。由于这里使用的是标准命令执行器(executor),因此可以指定任务是二进制可执行文件、bash脚本或者其他命令。由于Mesos支持Docker,因此在本例中将使用可执行的Docker镜像。Docker是这样一种技术,它允许你将应用程序和它运行时需要的依赖一起打包。

为了在Mesos中使用Docker镜像,这里需要在Docker registry中注册它们的名称:

const (
    MinerServerDockerImage = "derekchiang/p2pool"
    MinerDaemonDockerImage = "derekchiang/cpuminer"
)

然后定义一个常量,指定每个任务所需资源:

const (
    MemPerDaemonTask = 128  // mining shouldn't be    memory-intensive
    MemPerServerTask = 256
    CPUPerServerTask = 1    // a miner server does not use much     CPU
)

现在定义一个真正的scheduler,对其跟踪,并确保其正确运行需要的状态:

type MinerScheduler struct {
    // bitcoind RPC credentials
    bitcoindAddr string
    rpcUser      string
    rpcPass      string
    // mutable state
    minerServerRunning  bool
    minerServerHostname string
    minerServerPort     int    // the port that miner daemons
                               // connect to
    // unique task ids
    tasksLaunched        int
    currentDaemonTaskIDs []*mesos.TaskID
}

这个scheduler必须实现下面的接口:

type Scheduler interface {
    Registered(SchedulerDriver, *mesos.FrameworkID,     *mesos.MasterInfo)
    Reregistered(SchedulerDriver, *mesos.MasterInfo)
    Disconnected(SchedulerDriver)
    ResourceOffers(SchedulerDriver, []*mesos.Offer)
    OfferRescinded(SchedulerDriver, *mesos.OfferID)
    StatusUpdate(SchedulerDriver, *mesos.TaskStatus)
    FrameworkMessage(SchedulerDriver, *mesos.ExecutorID,
                     *mesos.SlaveID, string)
    SlaveLost(SchedulerDriver, *mesos.SlaveID)
    ExecutorLost(SchedulerDriver, *mesos.ExecutorID,   *mesos.SlaveID,
                 int)
    Error(SchedulerDriver, string)
}

现在一起看一个回调函数:

func (s *MinerScheduler) Registered(_ sched.SchedulerDriver,
      frameworkId *mesos.FrameworkID, masterInfo *mesos.MasterInfo) {
    log.Infoln("Framework registered with Master ", masterInfo)
}
func (s *MinerScheduler) Reregistered(_ sched.SchedulerDriver,
      masterInfo *mesos.MasterInfo) {
    log.Infoln("Framework Re-Registered with Master ",  masterInfo)
}
func (s *MinerScheduler) Disconnected(sched.SchedulerDriver) {
    log.Infoln("Framework disconnected with Master")
}

Registered 在scheduler成功向Mesos master注册之后被调用。

Reregistered 在scheduler与Mesos master断开连接并且再次注册时被调用,例如,在 master 重启的时候。

Disconnected 在scheduler与Mesos master断开连接时被调用。这个在master挂了的时候会发生。

目前为止,这里仅仅在回调函数中打印了日志信息,因为对于一个像这样的简单框架,大多数回调函数可以空在那里。然而,下一个回调函数就是每一个框架的核心,必须要认真的编写。

ResourceOffers 在scheduler从master那里得到一个offer的时候被调用。每一个offer包含一个集群上可以给框架使用的资源列表。资源通常包括 CPU 、内存、端口和磁盘。一个框架可以使用它提供的一些资源、所有资源或者一点资源都不给用。

针对每一个offer,现在期望聚集所有的提供的资源并决定是否需要发布一个新的server任务或者一个新的worker任务。这里可以向每个offer发送尽可能多的任务以测试最大容量,但是由于开采比特币是依赖CPU的,所以这里每个offer运行一个开采者任务并使用所有可用的CPU资源。

for i, offer := range offers {
    // … Gather resource being offered and do setup
    if !s.minerServerRunning && mems >= MemPerServerTask &&
            cpus >= CPUPerServerTask && ports >= 2 {
        // … Launch a server task since no server is running and     we
        // have resources to launch it.
    } else if s.minerServerRunning && mems >= MemPerDaemonTask {
        // … Launch a miner since a server is running and we have     mem
        // to launch one.
    }
}

针对每个任务都需要创建一个对应的TaskInfo message,它包含了运行这个任务需要的信息。

s.tasksLaunched++
taskID = &mesos.TaskID {
    Value: proto.String("miner-server-" +
                        strconv.Itoa(s.tasksLaunched)),
}

Task IDs由框架决定,并且每个框架必须是唯一的。

containerType := mesos.ContainerInfo_DOCKER
task = &mesos.TaskInfo {
    Name: proto.String("task-" + taskID.GetValue()),
    TaskId: taskID,
    SlaveId: offer.SlaveId,
    Container: &mesos.ContainerInfo {
        Type: &containerType,
        Docker: &mesos.ContainerInfo_DockerInfo {
            Image: proto.String(MinerServerDockerImage),
        },
    },
    Command: &mesos.CommandInfo {
        Shell: proto.Bool(false),
        Arguments: []string {
            // these arguments will be passed to run_p2pool.py
            "--bitcoind-address", s.bitcoindAddr,
            "--p2pool-port", strconv.Itoa(int(p2poolPort)),
            "-w", strconv.Itoa(int(workerPort)),
            s.rpcUser, s.rpcPass,
        },
    },
    Resources: []*mesos.Resource {
        util.NewScalarResource("cpus", CPUPerServerTask),
        util.NewScalarResource("mem", MemPerServerTask),
    },
}

TaskInfo message指定了一些关于任务的重要元数据信息,它允许Mesos节点运行Docker容器,特别会指定name、task ID、container information以及一些需要给容器传递的参数。这里也会指定任务需要的资源。

现在TaskInfo已经被构建好,因此任务可以这样运行:

driver.LaunchTasks([]*mesos.OfferID{offer.Id}, tasks,     &mesos.Filters{RefuseSeconds: proto.Float64(1)})

在框架中,需要处理的最后一件事情是当开采者server关闭时会发生什么。这里可以利用 StatusUpdate 函数来处理。

在一个任务的生命周期中,针对不同的阶段有不同类型的状态更新。对这个框架来说,想要确保的是如果开采者server由于某种原因失败,系统会Kill所有开采者worker以避免浪费资源。这里是相关的代码:

if strings.Contains(status.GetTaskId().GetValue(), "server") &&
    (status.GetState() == mesos.TaskState_TASK_LOST ||
        status.GetState() == mesos.TaskState_TASK_KILLED ||
        status.GetState() == mesos.TaskState_TASK_FINISHED ||
        status.GetState() == mesos.TaskState_TASK_ERROR ||
        status.GetState() == mesos.TaskState_TASK_FAILED) {
    s.minerServerRunning = false
    // kill all tasks
    for _, taskID := range s.currentDaemonTaskIDs {
        _, err := driver.KillTask(taskID)
        if err != nil {
            log.Errorf("Failed to kill task %s", taskID)
        }
    }
    s.currentDaemonTaskIDs = make([]*mesos.TaskID, 0)
}

万事大吉!通过努力,这里在Apache Mesos上建立一个正常工作的分布式比特币开采框架,它只用了大约300行GO代码。这证明了使用Mesos框架的API编写分布式系统是多么快速和简单。

阅读英文原文

阅读图灵社区原文

VLIS实验室云计算组张磊:在这里,学术界工业界不分家

{%}

张磊,浙江大学计算机学院博士生,科研人员,VLIS实验室云计算组技术负责人、策划人。Kubernetes项目贡献者和维护者,Docker项目贡献者。前Cloud Foundry中国团队和百度私有云项目组成员。InfoQ、CSDN、《程序员》杂志等多篇浙大系技术文章的贡献者和策划人,他还是《Docker——容器与容器云》一书的主要作者之一。

问:你现在的目标是成为一位计算机科学家吗?

是。

问:你是从什么时候开始想成为一位计算机科学家的?

大概是由于中学时候就开始玩电脑,然后自然而然就进入了计算机专业。由于我的兴趣主要在云计算以及Linux操作系统上,所以我的导师跟课题都是跟这些相关的。

2011年我们开始专注于开源云计算技术,当时开源的力量正在逐渐浮现。后来事实证明我们对趋势的判断是对的,因为从那个时候开始,软件的开发和发布以及整个生命周期就发生了改变。从此以后,可以说开源技术掌握了云计算行业,甚至是整个计算机行业的主流发展态势。于是,我们开始更深入地钻研Cloud Foundry, Docker, Kubernetes这样的技术,并且作为这些项目的贡献者成为了社区中的重要成员。

问:你认为在云计算领域,学术跟产业之间有没有明确的界限?

首先,云计算这个领域本身就比较特殊,它其实没有很多基础性研究,所以这个领域的学术跟工业是分不开的,中间没有一个明显的界限。比如,伯克利的AMP实验室做的一套高性能数据分析系统,最终开源出去就变成现在的明星项目Spark,成为工业界大数据的事实标准。在这些领域中,没有任何一个界限能够划定哪些技术是学术的,而工业界不能用。因为云计算本来就是“站在巨人肩膀上”的一种技术,它基于已有的分布式系统来做进一步的创新和整合。

问:你现在在SEL实验室的工作是什么?

我主要负责实验室云计算团队的技术工作 ,以及与技术相关的其他事宜,包括开源以及一些商业上的技术合作。

问:SEL实验室的前身是VLIS实验室,当初建立VLIS实验室的目的是什么?现在SEL实验室的关注点有没有变化?

我们实验室最开始的研究方向就是软件工程以及计算机软件。实验室一开始就注重从学校的角度跟知名企业建立强强联合的关系,致力于为工业界提供最好的大规模信息系统的开发技术和能力。在那个时候,我们的主要关注点是金融信息系统的开发,并且同全球最大的资金托管机构美国道富银行建立了紧密的合作研发关系。从这个时候开始,我们向工业界输出了大量的技术能力,整个北美市场的股票交易系统的后台都是我们实验室师生参与重新开发的。而软件技术发展到现在,新一代的大规模分布式系统开始更多地以知名开源项目作为表现形式,这些技术也就自然成为我们新的关注点。其中最重要的还是云计算技术,但是我们略有侧重,更关注轻量化的云计算技术,我们认为这将是一个新的变革。

问:你们实验室跟Cloud Foundry还有百度、思科等知名企业都是以什么形式合作的?

首先肯定有人才上的合作,因为学校本来是人才,我们会选择一些从事这个方向的优秀的学生,联合工业界的公司,比如去道富,VMware,思科总部或者百度等等,完成一个以技术研究为核心,以实际开发为途径的长期合作学习的过程。学生不是实习几个月然后回来,而是从开始到毕业,从研究的方向到最后的毕业论文都跟这些公司的真实技术场景紧密相关,并且专注于这些IT巨头的核心技术以及云计算平台的开发和研究工作。

问:你们实验室为什么在Docker还不太完善的时候就敢去尝试这种技术?

容器技术不是一种新技术,它其实很早就存在了。在此之前,我们搞Linux内核的时候已经用过类似的技术,而且做PaaS用到的技术也是基于Linux容器的,所以我们团队很久以前就对这种技术有过很多接触和研究。2013年的时候,我们同学主要在从事的是VMware的Warden容器的研发,紧接着Docker就出现了,并且比前一代容器技术要完善很多,所以我们就自然而然地转到了Docker上。这就是为什么Docker一出现就会引起我们的关注。轻量化的云计算技术一定会以这样的方式实现,只不过实现不同,而我们肯定会选择更好的实现。

问:你本人是怎么成为Kubernetes和Docker项目的贡献者的?

我是从Cloud Foundry团队出来的,而Cloud Foundry是一个纯粹的开源项目,它没有市场人员和项目经理这样的角色来干扰工程师的工作,所有贡献者都是通过远程协作和结对编程来贡献代码的。我,以及我们实验室的大多数同学从一开始就是这样一种工作模式,所以对于我们来说,参与开源项目是很自然的,而且我们也不像其他公司那样寻求互等的商业利益。

我们认为开源项目是一定要参与的,不仅要参与,还要学会主导项目的方向,成为维护者和更重要的核心代码贡献者。我刚才讲过,我们的愿景就是要基于开源软件做事情,做研究也好,做进一步的商业活动也好,一定要进入到社区里面,而不仅仅是一个使用者。

问:Docker一直存在安全方面的问题,在这方面你有哪些经验可以和大家分享?

我也在《Docker——容器和容器云》里提到过,Docker本身确实有安全问题,但是一定要分场景讨论。比如,什么样的场景下我会在一台裸机上部署Docker;什么样的场景下我会让Docker容器跑在虚拟机里面。在目前这种情况下,如果你是一个公有云提供商,我认为你还是要把容器拷在你的虚拟机里面,防止出现逃逸状况。

我们在书里讲过,你的Docker容器和整个Docker Daemon环境最好做安全加固,在操作系统层面做很多加固,设置权限,并且在整个系统的设计上把权限设计和授权设计摆在第一位,逐层来把不正确的行为过滤掉。另外,一定要区分场景。对于私有云的话,安全要求满足第一点就可以了。但是对于公有云来说,一定要做最高等级的安全预案。

以上是从业务方面讲,但是从技术方面讲,容器本身的安全问题是很难解决的,但是有一些努力的方向非常值得我们关注。比如最新的Rocket集成了英特尔在CPU上的一些虚拟化技术来做到硬件加固,这就是等级很高的安全技术。另外还有像国内赵鹏他们做的Hyper,是一个基于虚拟化技术的容器,它跟虚拟机的安全系数几乎是一样的。所以我觉得从另一方面说,这些技术应该得到大家的重视,并且集成到我们现有的解决方案里面。

问:现在在生产环境中使用Docker的人还是不太多,其背后的原因多种多样,你认为现在Docker面临的最大的阻碍是什么?

第一个问题在于Docker所属公司本身的强势,以及他们自己想做一揽子事情的态度。这个问题使得Docker现在变得非常臃肿,而且导致本来应该专注解决的问题没有解决掉。如果他们投入主要力量来解决容器的安全问题,我觉得反而要比现在的态势好。

第二,我们使用Docker也好,对它做二次开发也好,其实我们不要把自己的眼光局限在Docker上面。让Docker只做容器的事情其实是最好的选择,其余的事情交给更专业的人。我们不要过分相信来自某些商业化的宣传,比如一个人或者一个团队就能完成从开发到部署到运维的所有流程。哪怕你一开始可以,但是随着业务的正常增长,技术发展到一定程度之后你一定是做不到的。所以一定是专业的人做专业的事。

问:Docker和CoreOS一起创立了一个开放容器计划(OCP)。你认为OCP的成立对于软件开发行业会造成什么影响?

容器镜像不单指Docker镜像,它很久之前就已经存在了。容器镜像作为一种软件的发布方式,现在已经得到了大家的认可,成为了行业的事实标准。并且由于容器镜像本身已经存在了很久,所以它本身标准的普及是比较容易的。所以OCP的成立其实是为这种镜像发布的方式提供了一个技术上的标准。

以前,这一套东西虽然可以做标准,但是没有技术来支撑它,现在有了。我们通过标准容器来支撑标准镜像。所以,这两个标准如果能够在OCP里面得到统一,我相信它对整个软件工程将来的发展都是有很大影响的。我们现在学校的课程里就已经引入了完全基于容器的软件工程设计模式,谷歌也提出了基于容器的编程模型,将来的软件工程一定会向这方向发展。

问:你有一篇文章叫做《从Borg到Kubernetes》。你觉得Kubernetes今后的发展会怎么样?它和Mesos分别会向什么方向发展?

我们最近也跟谷歌的人一起交流了很久,首先,Kubernetes确实背负了很多Borg之前的优秀的设计理念,其中包括Borg在谷歌内部大规模集群业务的应用。虽然现在还有一些应用我们看不到,但是Kubernetes将来的发展目标一定是用来解决这些问题。

Mesos和Kubernetes在一开始发展时其实是非常直接的竞争对手,因为这两者关注的事情有很多是一致的。但是随着这两个项目的继续发展,它们已经形成了合作关系。比如,Mesos本来就是一个优秀的调度器,那么接下来Mesos会更关注这个业务。并且Mesos可以被更方便地集成到Kubernetes里,作为Kubernetes的一个核心调度器来工作。

这两个项目现在的关注点其实是不一样的,使用的场景也不一样。在今后的发展中,它们会逐渐融合对方的优点。互相之间的集成会越来越多,互相之间的重合会越来越少。

问:学术跟产业之间的脱轨问题一直以来经常被人们所诟病,浙大在这方面做得很不错,有没有什么经验可以分享?

首先,作为一所学校,要学会如何在我们国家的体制下,在不影响正常的教学、科研的前提下,获得企业的支持。学校需要鼓励学生在看似枯燥的学业中找到真正的个人兴趣点。比如我们实验室就涌现出了非常多的代码贡献者、作者、领域内的小专家。因为我们实验室从一开始就鼓励个人发展自己的兴趣,并且鼓励同学向大家分享你的工作成果。这样,工业界会自然而然地关注过来,合作也接踵而至。

另一方面,如果学校自身的硬件条件很强,技术水平很高的话,学校就可以为工业界做出很多贡献,无论是开源贡献,还是参与到工业的开发。并且还有一点很重要,就是学校要想办法把实验室的技术和研究经验转化成工程上可以应用到增值需求里的东西,而不只是埋头写论文。所以,我们实验室在硕士生阶段,不会提出苛刻的论文要求。我们更希望你的论文是对一个开源项目的贡献,或者是我们和产业界合作的课题的相关实际工程经验。

同时,我们也在浙大试点了一个学院,整个软件学院在以更加工程的方式推动科研的走向。我们课题组从十年前开始做这样的事情,所以学术跟产业之间的脱轨问题我们这边几乎是没有的。

问:浙大有这么好的环境,但是很多其他大学没有能力提供这样的环境。你建议在一般大学学习的学生怎样来丰富自己的专业知识?

首先对于一个学生,尤其是CS专业的学生,我认为实习是最重要的。你一定要想办法在跟导师融洽相处的前提下,寻求到与自己专业相关的实习机会,并且珍惜这些机会,因为实习能够使你的专业技能得到锻炼。并且你应该想办法把实习转化成毕业论文,或者是学校要求的课程设计。这样的经历会对你在工业界的影响力也好,工作也好,起到很大的积极作用。

问:你认为学习Docker需要几个阶段?

我们在书的后记里面讲过,不止是Docker,对于任何一个开源项目来说,都有这样的三个或四个阶段。 首先,你要去用,而且不只要用,还要变成一个优秀的玩家。对于开源项目的所有指令、所有设计,你应该有一个感性认识。

在这个基础之上就是源码,要读源码。读源码是一件非常有意思的事,但是在这个过程中,你要学会提问,带着问题去读源码,才会有收获。

然后就是转化,转化包括几种情况。比如,你可以将容器技术转化成你们实验室的某个项目的基础或者工作中的整个项目。另外,你要学会对项目做贡献。从最开始的找bug、解决bug、修改文档,到最后提出自己的特性、融入到社区里,只有这样你才能够获得最多的知识,以最快的速度提高自己在这个领域中的能力。

这三步之后,如果你在这个方面做得更多,可以考虑一些商业化的事情。比如你可以做一些相关的买卖,或者在你的公司里推广这些开源技术。

问:你觉得读者应该怎样使用《Docker——容器和容器云》这本书,读者在哪个阶段需要用到这本书?

这本书应该更适合在第二或第三阶段阅读。

这本书的一个特点就是它倾向于把原理帮你从源码中抽象出来,而不仅仅带你走读代码。因为代码很快会过时,所以我们特别注重抽象原理。在第三阶段的时候,你要去做开发,所以你对技术的熟悉程度和原理必然要有一个深刻的认识。而这本书的很多实践章节,包括我们对代码的整个框架结构的分析,会对你有很大的帮助。

问:《Docker——容器和容器云》的关注点和其他类似的书有什么不同?

首先,我们不认为容器就是Docker,我们认为它只是容器技术一种优秀的实现。所以我们的书叫《Docker——容器与容器云》,我们更关注所有基于容器的云平台的实现方式。你可以把容器理解为我们现在的虚拟机,把容器云理解为OpenStack,所以我们这本书肯定要先讲虚拟机原理,以此为基础我们才能讲清楚容器云,也就是容器的大规模管理方式。

很多目前市面上的书只关注于Docker本身,而我们更关注Docker背后的libcontainer也就是runc的工作原理,于此同时,我们还非常关注所谓的容器技术与大规模容器集群管理的结合方式。我们非常想为大家解决的一个问题就是,真正的大规模容器集群管理应该是什么样的。我们认为Kubernetes现在的方向是非常好的,所以在书中我们对它做了一个非常详细深入的解读,这在国内外应该是首次,而且从谷歌和CoreOS工程师的反馈来看,甚至在国外可能也是第一次。

{%}

阅读Docker源代码的神兵利器

作者/ 张磊

浙江大学计算机学院博士生,科研人员,VLIS实验室云计算组技术负责人、策划人。Kubernetes项目贡献者和maintainer,Docker项目贡献者。前Cloud Foundry中国团队和百度私有云项目组成员。InfoQ、CSDN、《程序员》杂志等多篇浙大系技术文章的贡献者和策划人。

正所谓“磨刀不误砍柴工”,本文我们将介绍几个阅读源码所要用到的神兵利器。其中,LiteIDE是国人开发的专为Go语言而生的集成开发环境(IDE),它可以用以简洁和美观著称的Sublime Text 2编辑器进行源码阅读。当然,作为一代Geek,我们还可以选择使用长盛不衰的Vim和Emacs。相信通过本节的阅读,一定能让读者在阅读源码时更加得心应手。

Golang开发环境的安装

阅读Go源码之前,安装Go语言的开发环境是必不可少的。下面我们介绍下载和安装的步骤。

1. 下载官方的Go语言安装包

请根据操作系统的版本(FreeBSD、Linux、Mac OS X或者Windows)以及处理器的架构(386、amd64或者arm)进行选择。下载地址为:https://golang.org/dl/ ,是Google提供的服务,可能需要使用VPN才能访问。

2. 安装Go语言安装包

选择合适的版本下载完成后,就可以开始进行Go语言安装包的安装了,过程如下。

FreeBSD、Linux以及Mac OS X之tar安装

对于FreeBSD、Linux以及Mac OS X用户来说,下载好的tar压缩文件需要再执行以下步骤才算是安装完成。

把压缩包解压至/usr/local目录下,命令如下:

tar -C /usr/local -xzf go$VERSION.$OS-$ARCH.tar.gz

选择适合的压缩包进行安装,例如,如果在64位Linux系统上安装Go 1.2.1版本,那么对应的压缩包就是go1.2.1.linux-amd64.tar.gz。

把/usr/local/go/bin添加到系统的环境变量中,可以通过把下面这行命令加入到/etc/profile(系统所有用户都受影响)或者$HOME/.profile(当前用户受影响)文件中来完成。

export PATH=$PATH:/usr/local/go/bin

提示

Go的安装环境默认安装在/usr/local(Windows系统是C:\)路径下。如果指定某个本地目录为安装路径,就必须设置$GOROOT环境变量。如果要把安装包解压至$HOME目录下,就需要把下面两行代码加入到$HOME/.profile文件中。

export GOROOT=$HOME/go export PATH=$PATH:$GOROOT/bin

Mac OS X之pkg快速安装

下载后缀为.pkg的相关安装包,打开后按照图形界面的指引操作即可顺利安装。使用这种方法时,安装包默认会安装到/usr/local目录下,/usr/local/go/bin也会被加入到环境变量。安装完成后,在终端使用时需要重新开启一个会话,以便生效。

Windows安装

除了源码安装以外,Go官方给用户提供了两种安装开发环境的方法:一种是手动解压缩Zip包安装,这需要设置环境变量;另一种是全自动安装。

MSI安装: 打开MSI文件,按照指引界面一步步操作即可,默认安装在C:\Go路径下。安装器会自动把C:\Go\bin目录加入到环境变量中。同样,需要重启命令行使之生效。

Zip安装: 把Zip文件下载并解压缩到自己选择的目录,推荐C:\Go。如果放到C:\Go以外的目录,需要设置GOROOT变量。然后把解压缩后Go目录下的bin目录(如C:\Go\bin)加入到PATH环境变量中。

Windows下设置环境变量: 在Windows系统下,可以通过“计算机”→“系统属性”→“高级”→“PATH”来设置环境变量。

3. 测试Go语言环境

完成以上步骤后,Go语言环境便安装完成了,最后我们来测试一下。

首先,创建一个hellow.go的空白文件,输入以下代码:

package main
import "fmt"
func main() {
    fmt.Printf("hello, world\n")
}

然后通过Go语言工具编译运行,示例如下:

$ go run hello.go
hello, world

如果看到了hello world,那么一切便大功告成了。

工具的配置与技巧

遗憾的是,Google官方并没有开发出一款专门为Golang打造的IDE,但开源社区为此作出了巨大的贡献。本节将介绍几种常见的IDE与编辑器的配置和使用技巧。

1. LiteIDE

LiteIDE是国人开发的一款专门为Go语言量身定做的IDE,它简单实用、开源并且可跨平台。

下载与安装

LiteIDE的安装文件托管在sourceforge平台上,因为项目是开源的,我们也可以在GitHub上下载项目源代码进行安装。下载地址为:http://sourceforge.net/projects/liteide/files

打开下载目录,如图1所示,会看到各个版本的下载目录,推荐下载最新的版本。有了Go开发环境的安装经验,安装LiteIDE就会简单很多。

{%}

图1 LiteIDE下载列表

下载相应版本的LiteIDE后进行解压缩。不同操作系统的用户,LiteIDE对应的安装方法如下。

  • Mac OS X:把解压后的LiteIDE.app直接拖动到Application文件夹下,以后即可在Launchpad中方便地找到并打开它。

  • Windows: 解压缩后,在liteide/bin目录下,双击liteide.exe即可打开运行。

  • Linux: 解压缩后,在liteide/bin/目录下,双击liteide即可打开运行。

  • LiteIDE支持Go语言阅读的功能简介

LiteIDE支持以下Go语言阅读功能。

  • Go语言包浏览器(Package browser)

  • 类视图和大纲(Class view and outline)

  • 文档浏览(Document browser)

  • Go语言Api函数检索(GOPATH API index)

  • 代码跳转(Jump to Declaration)

  • 代码表达式信息显示(Find Usages)

LiteIDE使用的图标是太极两仪的样式,有着浓浓的中国风,打开它就会看到介绍。可以使用打开文件夹功能,直接打开Docker源代码文件夹。如图2所示,在左边可以看到文件浏览目录、类试图、文件夹列表、大纲以及包浏览器这几个功能;右边是打开的Docker官方的README.md文件,可以看到LiteIDE支持预览Markdown格式,这样可以在这个IDE上面方便地读文档。

{%}

图2 LiteIDE功能界面展示

打开IDE以后,大家可以在设置中查看和修改快捷键设置,如图3所示。

{%}

图3 LiteIDE快捷键设置

需要注意的是,LiteIDE的代码跳转依旧不够完善,只有当跳转到的代码所在的文件处于打开(已经浏览过)状态时,才能正确地跳转。

Linux或者Mac OS X用户可以配合grep命令进行查看。如果我们要查找的函数是get_instance,则命令如下:

grep -rl get_instance docker/api
grep -rl [grep_pattern] [file_diretory]

Windows用户可以在文件中使用查找功能。

相信现在你已经能使用LiteIDE比较方便地阅读源码了。

2. Sublime Text 2

Sublime Text 2(简称ST2)自2012年发布以来,便以其简洁华丽的外观、多平台的支持、多语言支持以及超强的可扩展性而风靡起来。同样,作为一款文本编辑器,与VIM相比,它自带有目录树等功能,省去了一些配置上的麻烦,易于上手。

下载

打开Sublime Text的官方网站,页面下方映入眼帘的就是一个大大的Download图标,它会根据操作系统的平台自动选择适合的版本,只需直接点击Download即可下载。当然我们也可以打开下载列表页面(http://www.sublimetext.com/2),根据平台选择合适的版本进行下载。

安装

不同系统的用户可以参照不同的方式下载安装Sublime Text 2,方法如下。

  • Windows:下载后打开exe文件,按照引导界面进行安装即可。

  • Mac OS X:下载后打开dmg文件,按照引导界面进行安装即可。

  • Linux: 下载后进行解压缩,解压缩后进入目录点击sublime_text即可使用。但这样不便于使用,可以按照以下步骤进行配置,以便在终端也可方便地使用Sublime(假设已经解压缩到HOME目录)。

    mv $HOME/Sublime\ Text\ 2/ /opt/ sudo ln -s /opt/Sublime\ Text\ 2/sublime_text /usr/local/bin/subl
    
    

以上配置完成后,我们便可以方便地在终端通过subl命令打开Sublime Text 2了。

插件安装

Package Control

  • 点击菜单栏偏好设置(Preferences)->包浏览(Browse Packages)。

  • 打开它的上一级目录Sublime-Text-2/,可以看到“Installed Packages”、“Packages”等文件夹。

  • 下载Control.sublime-package包,并且复制到Installed Packages/目录中。

  • 重启Sublime Text。

GoSublime

  • 安装完Package Control后,默认情况下,通过快捷键Ctrl+Shift+P①即可调出命令执行模块, 输入Package Control:Install Package即可激活包安装。
  • 在跳出的可用包列表中,键入GoSublime后按回车键,即可下载并自动安装。
  • 安装完GoSublime后,即可使用诸如代码补全、格式调整等功能。

CTags

  • 与GoSublime相同,通过快捷键Ctrl+Shift+P调出命令执行模块,输入Package Control:Install Package,在跳出的可用包列表中键入CTags,再按回车键即可自动安装。

  • 安装完成后,在Docker项目的根目录下点击鼠标右键,执行CTags:Rebuild Tags,对项目进行CTags检索。

  • 通过快捷键Ctrl+Shift+ . 即可进行代码跳转(跳转到定义),通过快捷键Ctrl+Shift+,即可跳回。

至此,Sublime的介绍就完成了,熟练运用以上3个插件,不仅便于阅读Docker源码,对于Go语言项目的编写也会有非常大的帮助。

3. Vim

相信习惯使用文本编辑器的读者,一定对开源软件Vim相当熟悉和亲切。Vim被誉为“编辑器之神”,学习曲线极为陡峭,但是,一旦熟练掌握了Vim自成体系的一套快捷键,代码编辑速度将快速提升,你也会从此对Vim爱不释手。本节只作为对Vim老用户的抛砖引玉,不适于Vim的初学者使用。

插件的安装

go-vim是一款让Vim可以高度支持Go语言的Vim插件,所以也是要使用Vim作为Go IDE的必装插件。

如果使用pathogen对插件进行管理,那么只要执行如下步骤即可。

cd ~/.vim/bundle
git clone https://github.com/fatih/vim-go.git

对于Vundle用户,需要在.vimrc文件中加入下面这行:

Plugin 'fatih/vim-go'

并且打开Vim,在命令模式下执行:PluginInstall①)。

安装完插件后,为确保所有依赖的二进制文件(如gocode、godef、 goimports等),可以在命令模式下执行如下命令进行自动安装:

:GoInstallBinaries

除go-vim外,还有如下可选插件。

  • 实时代码自动补全插件:YCM 或 neocomplete;

  • 侧边栏类试图插件:tagbar;

  • 代码段自动生成插件: ultisnips 或 neosnippet;

  • 目录浏览器插件:nerdtree。

常用命令
  • :GoDef [identifier]:用于代码跳转,默认会跳转的项为鼠标定位的函数或变量,后面可以跟标识符进行跳转。

  • :GoDoc:用于打开相关Go文档。

  • :GoInfo:用于显示鼠标定位的标识符的变量类型。

  • :GoFmt:用于对选中的代码进行Go格式化。

  • :GoDeps:用于显示当前包依赖的其他包。

  • :GoFiles:用于显示依赖当前包的其他包。

  • :GoInstallBinaries:用于安装Go语言的Vim依赖项。

  • :GoUpdateBinaries:用于更新Go语言的Vim依赖项。

关于Vim的介绍到此结束,但相信对于Vim老用户来说,探索才刚刚开始。Vim作为一个经久不衰、广受赞誉的好工具,一直都是每一个Geek心中最好的神兵利器!

4. Emacs

Emacs作为与Vim齐名的文本编辑器,号称“神之编辑器”,用来浏览和编写Go代码也是非常方便的。本节也只作为Emacs老用户的抛砖引玉,在此之前,用户需要做好适合自己的配置。下面我们就以Emacs24为例,简单介绍几个实用的插件,用户需要先使用go get命令安装好gofmt、godef、godoc、gocode等工具。

go-mode

go-mode在提供了自动缩进和语法高亮功能的基础上,还整合了Go语言自带的工具,如gofmt、godoc、godef等。在Emacs24以后的版本中,可以使用自带的Package工具进行安装,命令如下:M-x package-install go-mode下面我们主要介绍格式整理以及定义跳转两项功能的配置。

格式整理

格式整理功能直接调用了gofmt工具,该工具能使用户的代码风格与其他开发人员保持一致。在Emacs中,用户可以直接调用gofmt命令,对当前窗口的代码进行格式整理。另一种方式是为before-save-hook添加函数,示例如下:

(add-hook 'before-save-hook 'gofmt-before-save)

这样在用户每次存档时就会自动进行格式整理。

定义跳转

定义跳转使用了godef工具,该工具能分析用户的代码、其他包内的代码以及Go标准库,实现在这三者间的定义跳转。Emacs提供了godef-jump命令实现跳转,默认绑定键为C-c C-j,用户也可以自己定义按键绑定,如绑定到F3键:

(add-hook 'go-mode-hook
    '(lambda () (local-set-key (kbd "<f3>") 'godef-jump)))

为了在跳转之后能跳转回来,用户可以添加如下配置,这样可以使用F2键回到原先的位置。

(add-hook 'go-mode-hook
    '(lambda () (local-set-key (kbd "<f2>") 'pop-tag-mark)))

此外,go-mode还提供了管理imports、使用godoc等工具,这里不再一一赘述。

company-go

company-go调用gocode工具提供自动完成功能,用户可以直接使用Package工具安装company-mode和company-go,并进行如下配置:

(add-hook 'go-mode-hook 'company-mode)
(add-hook 'go-mode-hook
    (lambda ()
    (set (make-local-variable 'company-backends) '(company-go))
    (company-mode)))

Emacs还为用户提供了极大的自由度,建议用户使用最新版本的Emacs和插件,善用Package功能和网上贡献的工具,这样能获得最新的功能和更好的体验。

 

{%}

《Docker——容器与容器云》从实践者的角度出发,基于Docker和Kubernetes最新源码,系统梳理了Docker容器技术和Kubernetes项目的实现原理和设计思路,有助于读者在实际场景中利用Docker容器和容器云解决问题并启发新的思考。本书节选自《Docker——容器与容器云》