Python中的if __name__==’__main__’

背景

初学者在学习Python的过程中,一定都遇到过if __name__ == '__main__'这样的语句。那么,这样的一个语句有什么作用呢?在什么时候需要使用这个判断语句呢?

举个例子

执行:

def main():
    print "hello world!"
    print __name__

if __name__ == '__main__':
 main()

print __name__

文件名为:tesy.py
执行结果为:
在这里插入图片描述
可以看到,直接执行该模块,__name__变量为__main__
执行:

import test

文件名为:test1.py
执行结果为:
在这里插入图片描述
可以看到,将test作为模块导入另一个文件中,得到的结果为test。
这是因为在导入test模块后,也会执行该模块。当__name____main__时才会执行main()。
然而test作为模块导入另一个文件时,__name__为模块名:test。因此最后只打印了一个test。

语句的作用

我们应该都知道类似__xxx__这种形式的变量是python的内置变量,在声明用户变量时不可以与内置变量重复。而这个__name__内置变量代表的是模块名(模块也就是python文件)。该模块名在直接运行模块时为__main__,在作为模块导入时为模块本身名称。
所以,这个语句的作用就是:当该模块直接运行时执行if__name__=='__main__'中的语句。当该模块作为模块导入到别的文件中时,因为__name__不为__main__,于是这个判断中的语句不能被执行。

计算广告基础

背景

我们都知道,互联网最重要的就是流量与数据。有了流量与数据就有了变现的基础。我们都知道互联网变现大部分要依靠广告,于是近些年在线广告发展突飞猛进。所谓在线广告,指的就是在互联网平台上投放的广告。那么,计算广告又是什么呢?本文大致对计算广告的基础进行讲述。

广告

要说到计算广告是什么,那么肯定得先聊聊最普通意义上的广告。在有互联网之前,广告都是实体广告,也就是线下广告。就是我们会经常看到的杂志中的广告页,宣传栏中的广告,高速路旁的大广告牌等等。后来互联网逐渐发展成熟,才有了所谓的线上广告。也就是在互联网这个媒介上的广告。从宏观的角度来看广告,我们首先看看广告的参与主体有哪些。

广告的参与主体

给出一个定义,取自《当代广告学》这本书:
广告是由已确定的出资人通过各种媒介进行的有关产品(商品、服务和观点)的,通常是有偿的、有组织的、综合的、劝服性的非人员的信息传播活动。
定义是晦涩难懂,但是我们一定能从中发现两个主动的参与方:出资人媒体。我们也可以称他们为需求方供给方需求方出资人,他需要广告位来为自己做信息传播。媒体供给方,因为他能够提供广告位。那么,有了两个主动的参与方,当然必须有被动的参与方,也就是我们了,广告学中称作受众
为什么要强调一下广告的参与主体呢?因为需求方供给方受众这三个参与主体的利益博弈关系就是广告活动永远的主线。广告业发展到现今,就是这三者的利益博弈过程。

广告的本质目的

广告这一商业行为的本质目的是什么呢?很多人立马会想说当然是给需求方做宣传然后更好的买东西啊。这么说没错,但是不够准确。
实际上广告的目的可以分为两种:一个是品牌广告,这种广告的目的是希望借助供给方的力量来快速接触大量用户,以达到宣传品牌形象、提升中长期购买率与利润空间的目的。比如说Nike:Just do it,英特尔:登~登登登登。他们的广告目的就是为了宣传品牌的形象,主要目的不是指望受众看了广告立马就去买产品回来。第二个是效果广告,这种广告则希望利用广告手段立马带来大量的购买行为。就比如我写CSDN博客,上面就会出现京东的我最近才浏览的物品,价格都在上面标着,这种广告就希望我立马点击进行去购买。

在线广告创意类型

上面说到了一般意义上的广告。接下来说下在线广告有哪些创意类型:

1.横幅广告


横幅广告最常见了

2.文字链广告


一看到这个我就想到百度搜索的前几个

3.富媒体广告


这种广告创意冲击力比较强

4.视频广告


经常看视频的人当然熟悉

5.社交广告

微信朋友圈里时不时会冒出来的广告推送

6.移动广告

移动端里的广告

7.邮件定向营销广告


当然,广告的创意类型不只这几种,还有很多,留心的话就会发现。

在线广告和线下广告

在谈到计算广告之前,我们再聊一下在线广告对比与传统的线下广告到底有什么不同之处?

1.技术和计算导向

在线广告可以进行精细的受众定向,也就是说,它通过标签化的方法可以使特别的广告推送给特别的人。技术使得广告决策和交易向着计算驱动的方向发展。

2.效果的可衡量性

在线广告可以通过展示和点击日志的形式直接记录广告效果,点击率从一个侧面可以反馈广告的效果。

3.创意和投放方式的标准化

标准化使大家可以充分利用整个市场的流动性,更快的创造更多价值。

4.媒体概念的多样化

简单说,就是互联网提供了交互功能,一个链接使得受众更接近购买行为。

5.数据驱动的投放策略

近年各行各业说的最多的就是数据的重要性。在线广告利用数据驱动使得投放策略更得当,受众定向更精细。

计算广告

了解了在线广告与线下广告的区别。基本也就明白了什么是计算广告了。技术使得广告决策和交易朝着计算驱动的方向发展。计算广告的核心问题是为一系列用户和环境的组合找到一个最后合适的广告投放策略,以此优化整体广告活动的利润。

对于广告投放来说有价值的数据

前面说到了在线广告由数据驱动。那么,我们作为受众来看看,自己的哪些数据被用于了对我们的广告投放。

1.用户标识

首先理所当然是用户标识,就是说,重要的是要知道哪些行为来自同一用户。我们就算收集了很多用户行为数据,但是如果不能把投放的对象和这些数据精确关联,那么这些数据都做了无用功。业界一般采用的是cookie作为用户标识,可能有人会觉得cookie长期一致性并不好,因为用户可能会换浏览器或是cookie被清除,不过对于广告来说,最重要的是用户的近期行为,所以cookie基本也够用了。
手机端IOS的用户标识符是IDFA,类似与cookie,这是苹果公司专为广告设计的。Android端一般采用Android IS或IMEI作为标识信息。

2.用户行为

用户行为分为四种:决策行为、主动行为、半主动行为、被动行为。
决策行为:决策行为是在广告主页面中发生的行为,对应着明确的用户兴趣。比如在某宝下单,或是下单前的加入购物车,比价,搜索等行为。
主动行为:包括广告的点击、搜索和搜索点击。这些是用户对广告的主动行为。
半主动行为:分享和网页浏览。这两种行为并不能说受众有明确的兴趣,但是这种行为的数据量是最大的,一定被加以利用。
被动行为:广告的浏览行为。

3.人口属性

比如性别、年龄等信息。这些数据一般来自用户的实名绑定服务。这些数据可能会在某些合作方中分享。

4.地理位置

电脑端一般通过IP定位,精确度不高,不过能定位到哪个城市一般来说也够用了。手机端会通过GPS和蜂窝流量定位,这个定位就十分精确了,可以准确到几百米。这些数据可用于推荐附近的吃的,电影院之类受地理位置限制极大的线下业务。

5.社交关系

社交网络反映了人与人之间的关系,也隐含了“兴趣相似”的合理推测。某些受众的兴趣会通过好友的兴趣来预测。

总结

本文主要从受众的角度宏观上的聊了聊在线广告的基础知识。实际上在线广告学发展迅速,有许多产品形态,如ADN(广告网络)、ADX(广告交易平台)、DSP(需求方平台)、DMP(数据处理平台)等等。每一个展开都有许多可以讲。核心就是围绕前面介绍的三个主体的利益博弈。感兴趣的话可以参考刘鹏的《计算广告》。

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

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

Docker底层的内核知识——cgroups

概述

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

从Linux系统中看cgroups

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

cgroups的由来

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

cgroups的官方定义

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

关于cgroups的术语

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

cgroups的作用

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

子系统简介

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

cgroup超出限额的后果

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

总结

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

Docker底层的内核知识——namespace

概述

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

感受一下namespace的存在

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

操作namespace的API

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

clone()

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

setns()

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

unshare()

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

namespace分类详述

mount namespace

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

cgroup namespace

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

PID namespace

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

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

#define STACK_SIZE (1024*1024)

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

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

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

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

IPC namespace

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

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

#define STACK_SIZE (1024*1024)

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

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

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

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

user namespace

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

UTS namespace

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

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

#define STACK_SIZE (1024*1024)

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

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

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

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

network namespace

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

总结

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

在Kubernetes集群中部署Heapster

背景

公司的容器云平台需要新增应用的自动扩缩容功能,以便能够更加智能化的对应用进行管理。

Kubernetes官方提供了HPA(Horizontal Pod Autoscaling)资源对象。要让我们部署的应用做到自动水平的(水平指的是增减Pod副本数量)进行扩缩容,我们只需要在Kubernetes集群中创建HPA资源对象,然后让该资源对象关联某一需要进行自动扩缩容的应用即可。

HPA默认的是以Pod平均CPU利用率作为度量指标,也就是说,当应用Pod的平均CPU利用率高于设定的阈值时,应用就会增加Pod的数量。CPU利用率的计算公式是:Pod当前CPU的使用量除以它的Pod Request(这个值是在部署deployment时自己设定的)值。而平均CPU利用率指的就是所有Pod的CPU利用率的算术平均值。

Pod平均CPU利用率的计算需要知道每个Pod的CPU使用量,目前是通过查询Heapster扩展组件来得到这个值,所以需要安装部署Heapster。

接下来我就将我们在Kubernetes集群中部署Heapster的过程记录下来,也会描述我们在部署过程中遇到的问题以及解决的方法,希望能够帮助到也准备在Kubernetes集群中部署Heapster的朋友。

Heapster成功部署之后,我们使用了性能测试工具对http应用做了压力测试,以观察HPA进行自动扩缩容时的实际效果。

部署过程

1

首先,我们在github中搜索Heapster,会找到Kubernetes中的Heapster库:
这里写图片描述
将这个库clone到集群的master节点中。

在Heapster目录下运行命令kubectl create -f deploy/kube-config/standalone/Heapster-controller.yaml

理论上创建完成后会启动三个资源对象,deployment、service、serviceaccount,此时Heapster应该就能够为HPA提供CPU的使用量了。

此时为验证Heapster是否可用,在集群中部署一个HPA资源对象,关联某个应用,并设定阈值为90:
这里写图片描述
查看这个HPA时我们可以看到,CURRENT的CPU利用率为,也就是说,HPA没能从Heapster中取得CPU的使用量。

于是,我们用kubectl get pod --namespace=kube-system命令查看Heapster的pod的运行情况,发现是拉取镜像失败的缘故。

打开部署Heapster的yaml文件如下:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: Heapster
  namespace: kube-system
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: Heapster
  namespace: kube-system
spec:
  replicas: 1
  template:
    metadata:
      labels:
        task: monitoring
        k8s-app: Heapster
    spec:
      serviceAccountName: Heapster
      containers:
      - name: Heapster
        image: gcr.io/google_containers/Heapster-amd64:v1.4.0
        imagePullPolicy: IfNotPresent
        command:
        - /Heapster
        - --source=kubernetes:https://kubernetes.default
---
apiVersion: v1
kind: Service
metadata:
  labels:
    task: monitoring
    # For use as a Cluster add-on (https://github.com/kubernetes/kubernetes/tree/master/cluster/addons)
    # If you are NOT using this as an addon, you should comment out this line.
    kubernetes.io/cluster-service: 'true'
    kubernetes.io/name: Heapster
  name: Heapster
  namespace: kube-system
spec:
  ports:
  - port: 80
    targetPort: 8082
  selector:
    k8s-app: Heapster

可以看到23行,镜像是要从谷歌的gcr.io镜像仓库中拉取的,由于我们的集群服务器不能连通外网,所以镜像当然就拉取失败了。

2

此时,我们使用之前使用过的自己仓库中的Heapster镜像来替代此yaml文件中使用的镜像。

清除刚才部署失败的资源对象后再次在Heapster目录下使用命令:kubectl create -f deploy/kube-config/standalone/Heapster-controller.yaml

这个时候发现对应的资源对象都创建成功了,而且pod也成功运行了。

此时看HPA的情况如下:
这里写图片描述
此时问题没有得到解决,虽然Heapster的pod运行起来了,但是HPA还是没能取到CPU使用量的值。

现在的情况来看,此时还不能确定问题是在于Heapster安装没成功还是HPA没能从Heapster中取得值。

所以,接下来就是要确定一下Heapster是否安装成功了。

这个时候,我们想到去Heapster中的docs目录下看看有没有什么debug的方法,也就是进入Heapster目录下的docs中的debugging。

用curl命令对URL/api/v1/model/debug/allkeys做取值,得到的结果如下:
这里写图片描述
然后再用kubectl describe hpa yce --namespace=yce命令来查看HPA资源对象的详情
这里写图片描述
Message中显示,从Heapster获取指标失败。

此时结论就得出了,Heapster已经成功安装,不过集群无法从Heapster中获取到监控数据。

好了,接下来我们决定再给Heapster换一个镜像试试,有可能是我们的镜像版本问题导致与Kubernetes集群不能协同工作。

在docker hub中搜索到了版本为v1.2.0的Heapster镜像,替换为此镜像后再次创建资源对象。

3

创建成功后,惊喜出现了
这里写图片描述
可以看到,HPA已经可以从Heapster中取到值了。然后用kubectl top node命令查看节点的指标,发现也可以取到值了。
这里写图片描述

对HPA关联的应用做压力测试

理论上Heapster已经安装成功。为了验证HPA是否可用,我们写了一个简单的http程序,名为helloworldhey。该程序就是做了一个自增整型变量1000次的循环。

将该程序打包成镜像,部署对应的deployment和service资源对象,然后在集群外部通过暴露的节点端口用性能测试工具ab对其进行压力测试。可以看到结果如下:
这里写图片描述
可以看到,我这里设置的阈值为50%,也就是说,在平均CPU使用率超过50%时,HPA会对pod进行自动的扩容,也即是增加pod的数量,使增加后的pod总数量可以让平均CPU使用率低于50%。

可以看到此时的CPU使用率已经为110%,所以,理论上HPA应该自动的拉起2个pod来分担CPU的使用量。看看pod的目前情况,结果如下:
这里写图片描述
可以看到此时共有三个pod,与预期的结果相同。

不过,可能有人会有疑问,为什么最初创建的pod会重启多次?原因就在于Kubernetes在部署deployment时,每个pod都设置有requests与limits值,当该pod的CPU(或内存)使用量超过该limits值时,kubelet就会重启这个pod,由于在压力测试的开始时刻只有此一个pod,所以CPU使用量肯定是超过了这个limit值于是pod就被重启了。

至此,heapter就被部署成功且可以正常使用了。

Kubernetes网络原理

概述

Kubernetes的搭建与使用少不了网络基础设施的搭建工作, 本文简述了Kubernetes所需要的网络基础环境,Docker的网络实现以及Kubernetes的网络实现。最后,简单提了一下可以实现这些网络基础的网络开源组件Flannel。希望可以通过这些简单的语言描述,让初学者能够初步了解Kubernetes的网络原理。

Kubernetes网络模型

Kubernetes网络模型设计的基础原则:每个Pod都有一个独立的IP地址,这样Pod之间可以直接进行通信,无论它们是否在同一个Node上。而且用户就不需要考虑将容器的端口映射到节点的物理端口上了。

一个Pod分配一个独立的IP,一个Pod内部的所有容器共享一个Linux网络堆栈,包括它们的IP地址、网络设备、配置等都是共享的。所以容器之间是可以通过localhost来连接对方端口的。从这个层面来看,Pod内部容器之间的隔离性相比于不同Pod的容器有所降低,但也只是它们不用相同的端口而已。Pod中的容器有点类似于VM中的进程。

Kubernetes网络模型的必要基础:
1.集群中所有的容器和容器之间可以不通过NAT进行通信
2.节点可以不通过NAT与任一容器进行通信
3.容器的地址和别人看到的地址是一样的

就是说,并不是节点安装了Kubernetes与Docker就可以工作了。还必须满足以上的网络模型才行。而原生的Docker并不能很好的支持这些要求。所以需要一些网络组件来满足这些基础网络要求。

Docker的网络基础

Docker本身的技术依赖于近年来Linux内核虚拟化技术的发展,所以Docker对Linux内核有很强的依赖性。这接下来讲述一下Docker使用到的与Linux网络有关的主要技术。

网络的命名空间

网络命名空间代表的是一个独立的协议栈,所以它们之间是相互隔离的。Docker利用了网络的命名空间特性,实现了不同容器之间网络的隔离。

Veth设备对

引入Veth设备对的目的是让两个网络命名空间进行通信。在Docker内部,Veth设备对也是联系容器到外面的重要设备,离开它是不行的。

网桥

网桥是二层交换机(数据链路层),转发的依据是MAC地址。
在Linux的内部网络栈里也实现了网桥设备,与实际的网桥设备作用相似。是Linux内部各种网络设备之间相互转发数据的二层设备。但Linux网桥与实际的交换机又有不同之处,因为它不仅可能会转发或丢弃,还可能被送到网络协议栈的上层(网络层),从而被自己(网络协议栈)所消化。

Iptables/Netfilter

iptables-save命令可以查看Iptables中的内容
这里写图片描述
我们可以看到service的IP转发到Pod的IP

路由

Linux系统包含一个完整的路由功能。当IP层进行数据的发送或转发时,会使用路由表来决定发往哪里。

Docker的网络实现

标准的Docker支持四种网络模式:host、container、none、bridge。
在Kubernetes管理模式下,通常只会使用bridge模式。如图:
这里写图片描述
这里写图片描述
这里写图片描述
使用ip route show table local type local可以查看本地设备的地址,也显示了docker0网桥的IP地址
这样,在节点内部容器之间可以相互通信,在节点外部则不行。必须将容器的端口映射到主机的端口上才行,docker在跨主机通信时会面临很多的问题。

Kubernetes的网络实现

Kubernetes网络设计主要解决以下场景:
1.容器之间的通信
2.Pod之间的通信
3.Pod到Service之间的通信
4.集群外部与内部组件之间的通信

容器之间的通信

同一个Pod下的容器共享同一个网络命名空间,共享同一个Linux协议栈。所以它们就像在同一台机器上一样。可以相互之间直接通信。可以用localhost访问彼此的端口。

如果容器2运行的是mysql容器,那么容器1通过localhost:3306就直接能够访问运行在容器2上的Mysql了。当然,既然像是在同一台机器上的不同进程,那么它们之间也就是可以通过IPC(进程间通信)进行通信(如消息队列或管道)。
这里写图片描述

Pod之间的通信

同一Node内的Pod之间的通信

这里写图片描述
同一个Node内的不同Pod都是通过Veth连接在同一个Docker0网桥上的。所以它们之间是可以直接进行通信的。而且,非本地数据的网络数据都是默认发送到Docker0网桥然后由它进行中转的。
Docker0网桥会路由到节点上的所有Pod的,这个可以通过看Kubernetes任一节点上的路由表了解
这里写图片描述
这个就表示docker0的IP地址为10.0.82.1,而该节点上的Pod都是10.0.82网段的。

不同Node上的Pod之间的通信

不同Node上的Pod与Pod之间进行通信,肯定是要通过节点的物理网卡的。而且不同的Pod肯定是不能有相同的私有IP的。另外,我们还必须要有Node IP找到该Pod的IP才成。
对于这些要求,Flannel就可以做得到。
这里写图片描述

开源的网络组件

Kubernetes是谷歌的开源项目,它假定了所有的Pod都是在一个可以直接连通的扁平的网络空间中,因为它建立在GCE上,而GCE已经实现了这些要求。

但是对于一个私有云来说,要想用Kubernetes就必须自己来实现这个网络假设。有些开源软件可以做到这些。

Flannel

Flannel实现了两点来保证Kubernetes的底层网络:
1.保证集群中每个Pod的IP不冲突
2.建立了一个覆盖网络,保证数据可以在不同节点之间的Pod传递(通过Pod节点路由表可以找到Pod与节点的对应关系)
这里写图片描述

Kubernetes的主要组件概述

概述

本文试图用简单的语言描述Kubernetes主要组件的作用及其关系。这里我讲解的Kubernetes主要组件有API Server、Controller Manager、Scheduler、kubelet、kube-proxy,其中前三者运行于集群的Master节点,后两者运行于集群的Slave节点。接着描述了一下用于存储Kubernetes集群信息的Etcd,它是一个高可用、强一致性的服务发现存储仓库。最后,我抛出了一个我所遇到的一个问题。大家可以一同思考一下问题出在了哪里。

API Server

Kubernetes API Server通过kube-apiserver进程提供服务,该进程运行于Master节点上。

API Server是Kubernetes的核心组件,是各个组件通信的渠道,有如下特性:

1.集群管理的API入口

我们如果要创建一个资源对象如Deployment、Service、RC、ConfigMap等,都是要通过API Server的。

当然,我们可能是通过命令行的kubectl命令将一个yaml/json格式的文件create进行创建,还可能是通过写代码的方式使用如client-go这样的操作Kubernetes的第三方包来操作集群。总之,最终,都是通过API Server对集群进行操作的。通过API Server,我们就可以往Etcd中写入数据。Etcd中存储着集群的各种数据。
这里写图片描述

2.资源配额控制的入口

Kubernetes可以从各个层级对资源进行配额控制。如容器的CPU使用量、Pod的CPU使用量、namespace的资源数量等。这也是通过API Server进行配置的。将这些资源配额情况写入到Etcd中。

3.提供了完备的集群安全机制

Controller Manager

Replication Controller

副本控制器。用来保证Deployment或者RC中副本的数量的。

Node Controller

通过API Server监控Etcd中存储的关于节点的各类信息,这些信息是kubelet定时推给API Server的,由API Server写入到Etcd中。这些节点信息包括:节点健康状况、节点资源、节点名称、节点地址信息、操作系统版本、Docker版本、kubelet版本等。监控到节点信息若有异常情况,则会对节点进行某种操作,如节点状态变为故障状态,则删除节点与节点相关的Pod等资源的信息。
这里写图片描述

ResourceQuota Controller

将期望的资源配额信息通过API Server写入到Etcd中。然后ResourceQuota Controller会定时的统计这些信息,在系统请求资源的时候就会读取这些统计信息,如果不合法就不给分配该资源,则创建行为会报错。
这里写图片描述

Namespace Controller

用户是可以通过API Server创建新的namespace并保存在Etcd中的。Namespace Controller会定时通过API Server读取这些Namespace信息并做对应的对于Namespace的一些操作。

Endpoints Controller

负责生成和维护所有Endpoints对象的控制器。Endpoints表示了一个Service对应的所有Pod副本的访问地址。

一个Service可能对应了多个Endpoints,那么,在创建一个新的Service时Endpoints Controller就会生成对应的Endpoints。在Service被删除时,Endpoints Controller就会删除对应的Endpoints。等等。

Scheduler

Kubernetes的调度器。Scheduler监听API Server,当需要创建新的Pod时。Scheduler负责选择该Pod与哪个Node进行绑定。将此绑定信息通过API Server写入到Etcd中。

若此时与Node A进行了绑定,那么A上的Kubelet就会从API Server上监听到此事件,那么该Kubelet就会做相应的创建工作。

此调度涉及到三个对象,待调度的Pod,可用的Node,调度算法。简单的说,就是使用某种调度算法为待调度的Pod找到合适的运行此Pod的Node。
这里写图片描述
关于Kubernetes Scheduler的调度算法,我会再另写一篇文章。

Kubelet

每个Node节点上都会有一个Kubelet负责Master下发到该节点的具体任务,管理该节点上的Pod和容器。而且会在创建之初向API Server注册自身的信息,定时汇报节点的信息。它还通过cAdvisor监控容器和节点资源。

节点管理

Kubelet在创建之初就会向API Server做自注册,然后会定时报告节点的信息给API Server写入到Etcd中。默认为10秒。

Pod管理

Kubelet会监听API Server,如果发现对Pod有什么操作,它就会作出相应的动作。例如发现有Pod与本Node进行了绑定。那么Kubelet就会创建相应的Pod且调用Docker Client下载image并运行container。

容器健康检查

有三种方式对容器做健康检查:
1.在容器内部运行一个命令,如果该命令的退出状态码为0,则表明容器健康。
2.TCP检查。
3.HTTP检查。

cAdvisor资源监控

Kubelet通过cAdvisor对该节点的各类资源进行监控。如果集群需要这些监控到的资源信息,可以安装一个组件Heapster。

Heapster会进行集群级别的监控,它会通过Kubelet获取到所有节点的各种资源信息,然后通过带着关联标签的Pod分组这些信息。

如果再配合InfluxDB与Grafana,那么就成为一个完整的集群监控系统了。

Kube-proxy

负责接收并转发请求。Kube-proxy的核心功能是将到Service的访问请求转发到后台的某个具体的Pod。

无论是通过ClusterIP+Port的方式还是NodeIP+NodePort的方式访问Service,最终都会被节点的Iptables规则重定向到Kube-proxy监听服务代理端口,该代理端口实际上就是SocketServer在本地随机打开的一个端口,SocketServer是Kube-proxy为每一个服务都会创建的“服务代理对象”的一部分。

当Kube-proxy监听到Service的访问请求后,它会找到最适合的Endpoints,然后将请求转发过去。具体的路由选择依据Round Robin算法及Service的Session会话保持这两个特性。

Etcd

Etcd一种k-v存储仓库,可用于服务发现程序。在Kubernetes中就是用Etcd来存储各种k-v对象的。

所以我也认为Etcd是Kubernetes的一个重要组件。当我们无论是创建Deployment也好,还是创建Service也好,各种资源对象信息都是写在Etcd中了。

各个组件是通过API Server进行交流的,然而数据的来源是Etcd。所以维持Etcd的高可用是至关重要的。如果Etcd坏了,任何程序也无法正常运行了。

以下是我的环境中的Etcd集群
这里写图片描述
通过以下命令我们就可以看到该集群中yce这个namespace下的pod有哪些,此为目录,列出的都是键值
这里写图片描述
接下来,我们就可以查看某一个具体键值的value值
这里写图片描述
可以看到这个value值是一个Json格式的文件。

一个问题

我做了一个实验,如果从Etcd中直接改yce这个deployment的Json值,从replicas的值从1改到3,发现deployment的desired值发生了变化,但是current值并没有变化,也就是说,Pod的数量并没有因为Etc中的值的改变而改变。难道是因为Replicas Controller没有监控到Etcd中的此变化?
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
如上所示,更改了Etcd中deployment的replicas的值之后,可以用kubectl edit看到该deployment的replicas值确实发生了变化。但是deployment的current值却并无变化。

若是用kubectl edit更改deployment的replicas值就会发现deployment的current值是会发生变化的。请大家思考一下这是为什么。

总结

Kubernetes的这些组件各自分别有着重要的功能。它们之间协同工作,共同保证了Kubernetes对于容器化应用的自动管理。

其中API Server起着桥梁的作用,各个组件都要通过它进行交互。Controller Manager像是集群的大管家,管理着许多事务。Scheduler就像是一个调度亭,负责Pod的调度工作。

Kubelet则在每个节点上都有,像是一个执行者,真正创建、修改、销毁Pod的工作都是由它来具体执行。Kube-proxy像是负载均衡器,在外界需要对Pod进行访问时它作为代理进行路由工作,将具体的访问分给某一具体的Pod实例。

Etcd则是Kubernetes的数据中心,用来存储Kubernetes创建的各类资源对象信息。

这些组件缺一不可,无论少了哪一个Kubernetes都不能进行正常的工作。这里大概讲了下各组件的功能,感兴趣的可以分析Kubernetes的源码,github中就有。

用client-go调用Kubernetes API

背景

client-go是go语言访问Kubernetes API的一种新的框架。本文记录client-go访问Kubernetes的过程。最终实现了给服务器端发送container ID,服务器端会返回该container所在pod的pod Name与Spacename。我们Kubernetes集群中一个pod都只运行了一个container。
程序用httprouter包来作为http的访问接口。返回的值编码为Json文件,客户拿到Json之后再进行解析,最终拿到需要的信息。如图:
这里写图片描述
程序实现基本功能之后,用Dockerfile将程序打包成docker镜像。

源代码

package main

import (
    "flag"
    "fmt"
    "github.com/julienschmidt/httprouter"
    "net/http"
    "io"
    "io/ioutil"
    apiv1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
    "encoding/json"
)

type Container_msg struct {
    Name string
    Namespace string
}

func main() {
    router := httprouter.New()

    kubeconfig := flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
    flag.Parse()

    if *kubeconfig == "" {
        panic("-kubeconfig not specified")
    }

    config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
    if err != nil {
        panic(err)
    }

    clientset, err := kubernetes.NewForConfig(config)

    if err != nil {
        panic(err)
    }

    router.POST("/api/v1/containerID",func(w http.ResponseWriter, r *http.Request, _ httprouter.Params){

        podsClient := clientset.Pods(apiv1.NamespaceAll)

        // List Pods
        list, err := podsClient.List(metav1.ListOptions{
        })
        if err != nil {
            panic(err)
        }

        body,_ := ioutil.ReadAll(io.LimitReader(r.Body,1122334))


        for _, d := range list.Items {
            containerID := d.Status.ContainerStatuses[0].ContainerID[9:]
            if string(body)==containerID{
                container_msg := Container_msg{
                    d.Name,
                    d.Namespace,
                }

                c,err := json.Marshal(container_msg)

                if err != nil{
                    panic(err)
                }

                fmt.Fprintf(w,"%v", c)
            }
        }
    })
        http.ListenAndServe(":8082", router)
}

具体实现

上面我已经先把程序的源代码贴出来了,接下来就进行逐步分析

1.获取第三方包client-go与httprouter

https://github.com/kubernetes/client-go
https://github.com/julienschmidt/httprouter
进入它们的github中看使用说明按步骤安装即可。
简单来说,就是分别使用go get k8s.io/client-go/…与go get github.com/julienschmidt/httprouter命令即可。
不过要记得在安装之前配置GOPATH环境变量。比如我的项目是放在naruto目录中的,所以我的GOPATH设置为naruto目录。
配置完GOPATH环境变量,安装完第三方包,我们就可以直接使用它们了。

2.使用httprouter包中的方法获取客户发来的container ID

router := httprouter.New()

建立http连接

http.ListenAndServe(":8082", router)
在8082端口进行监听

router.POST
获取客户端POST的数据

3.用client-go获取Kubernetes集群中pod信息

clientset, err := kubernetes.NewForConfig(config)
使用config建立client-go与Kubernetes集群的关系
config文件是Kubernetes集群的配置文件,存放在集群Master节点的~/.kube/config中
建立好连接之后, 我们就可以使用clientset的对应方法来获取pod或deployment等对象资源的信息。

4.比较获取到的container ID与集群中各pod的container ID

因为我们集群中一个pod仅运行着一个container,所以,我们只需要将pod中container的ID得到之后与获取到的客户发来了container ID做比较即可。假如相等,就可以把此pod的信息返回给客户端。

body,_ := ioutil.ReadAll(io.LimitReader(r.Body,1122334))

for _, d := range list.Items {
    containerID := d.Status.ContainerStatuses[0].ContainerID[9:]
    if string(body)==containerID{
     container_msg := Container_msg{
               d.Name,
               d.Namespace,
           }

     c,err := json.Marshal(container_msg)

      if err != nil{
             panic(err)
           }

     fmt.Fprintf(w,"%v", c)

打包成docker镜像

通过编写Dockerfile文件来建立一个新的镜像。

FROM img.reg.3g:15000/go:1.8     //以这个镜像为基础镜像
COPY naruto /pf      //将naruto目录下的文件复制到容器中文件系统/目录下的pf目录中
ENV GOPATH /pf            //配置环境变量
RUN go build /pf/main_v1.go   //编译写好的程序
CMD /main_v1 -kubeconfig=/pf/config   //以运行该程序的进程为容器的主进程

我们需要将main.go、config、第三方包、Dockerfile都放在同一目录下。通过docker build命令建立镜像。

在kubernetes中创建tomcat与mysql集群

背景

相信大家应该都在自己的电脑中部署过tomcat与mysql集群,我们可以通过访问tomcat来访问后台数据库。kubernetes是一个可自动部署、扩缩、管理容器化应用的工具。本篇博客主要讲解如何在kubernetes中部署tomcat与mysql集群。当然,这涉及到kubernetes的自动化部署功能。至于扩缩、管理功能,我这里就不详述了,以后有机会再讲解。

基础

在kubernetes中部署tomcat与mysql集群之前必须要有以下这些基础:

  1. 已安装、配置kubernetes
  2. 集群中有tomcat与mysql容器镜像
  3. 有docker基础

具体步骤

部署tomcat

创建tomcat RC对象

我们想要在kubernetes集群中配置tomcat服务器,首先要保证集群中的节点上有tomcat镜像,镜像可以从docker Hub中拉取,也可以放在自己的私有仓库中。这在我之前的博客中讲过,这里就不详述了。

要部署tomcat服务,我们需要做两件事,一是创建RC(Replication Controller),二是创建Service。RC是kubernetes中的副本控制器,也就是说,RC负责自动部署容器化应用。Service是我们访问tomcat服务的入口地址,我们是通过Service来对该服务就行访问的。

创建RC与Service对象,我这里用的是创建yaml文件的方式。yaml文件中的内容是声明式的。这些声明让kubernetes做你想要它做的事情。声明与命令有所不同,声明是告诉它你的需要是什么,而不涉及具体的实现的步骤,而命令,如linux中的ls,你是告诉它去做什么。声明与命令是不同的,请自己体会。话不多说了,上代码:

  1 apiVersion: v1
  2 kind: ReplicationController
  3 metadata:
  4   name: myweb
  5 spec:
  6   replicas: 5
  7   selector:
  8     app: myweb
  9   template:
 10     metadata:
 11       labels:
 12         app: myweb
 13     spec:
 14       containers:
 15       - image: kubeguide/tomcat-app:v1
 16         name: myweb
 17         resources:
 18           limits:
 19             cpu: "2"
 20             memory: 4Gi
 21         ports:
 22         - containerPort: 8080
 23         env:
 24         - name: MYSQL_SERVICE_HOST
 25           value: 'mysql'
 26         - name: MYSQL_SERVICE_PORT
 27           value: '3306'

如上所示就是我写的yaml文件,此文件名为myweb-rc1.yaml
这里我简单的说明一下此yaml文件声明了什么:

  1 apiVersion: v1      //描述RC对象的版本是v1
  2 kind: ReplicationController    //我现在在声明RC对象
  3 metadata:     //metadata中的是对此RC对象描述信息
  4   name: myweb      //此RC对象在default命名空间中名为myweb,同一个命名空间中的命名一定是不同的
  5 spec:     //spec中是对RC对象的具体描述
  6   replicas: 5    //我要创建5个副本,单位当然是pod
  7   selector:      //选择器,用来选择对象的
  8     app: myweb    //我选择了标签为app: myweb的pod
  9   template:     //模版,以下用来描述创建的pod的模版
 10     metadata:   //对pod模版描述的元数据
 11       labels:      //给以下的东西打上标签,以让selector来选择
 12         app: myweb   //给pod模版打上app: myweb这样的标签
 13     spec:             //对pod模版的具体描述
 14       containers:         //以下就是要放入pod模版中的容器了
 15       - image: kubeguide/tomcat-app:v1    //选择镜像
 16         name: myweb         //容器名
 17         resources:           //给该容器分配的资源大小
 18           limits:
 19             cpu: "2"
 20             memory: 4Gi
 21         ports:         //容器端口号
 22         - containerPort: 8080         
 23         env:          //给该容器设置环境变量,这里就可以将mysql与我们的tomcat连接
 24         - name: MYSQL_SERVICE_HOST
 25           value: 'mysql'
 26         - name: MYSQL_SERVICE_PORT
 27           value: '3306'

大家可能看完比较蒙,仔细研究就发现其实yaml文件的规范还是比较严谨的。它是通过缩进与对齐的方式来表达了具体的信息的。比如一个metadata,在我这个yaml文件中就有两个。实际上通过缩进与对齐,我们就可以了解到,第一个metadata是对这个RC对象进行描述的元数据,而第二个metadata因为缩进了,实际上它是对pod模版进行描述的元数据。当然是不一样的。spec也有两个,同样的道理,大家可以自行分析。

创建tomcat RC对象的结果

如下,我截了个屏:
这里写图片描述
可以看到,我在创建完了tomcat的RC对象之后,它就立马自动部署了5个pod,这5个pod已经健康的跑起来了。为什么是创建5个pod?因为我在yaml文件中声明了我需要5个副本。是不是很方便?

创建tomcat Service对象

单单创建了RC对象还不行,虽然RC对象为我们自动部署了5个pod,但是我们还需要一个Service对象来作为入口地址来对创建好的tomcat进行访问,所以,接下来我们的任务就是创建Service对象。
此文件名为myweb-svc1.yaml。还是直接上代码:

  1 apiVersion: v1
  2 kind: Service
  3 metadata:
  4   name: myweb
  5 spec:
  6   ports:
  7   - name: myweb-svc
  8     port: 8080
  9     targetPort: 8080
 10     nodePort: 31111
 11   selector:
 12     app: myweb
 13   type: NodePort

关于yaml文件声明的含义我已经解释过了。这里不再重复解释。只挑不同的地方强调一下:

  1 apiVersion: v1
  2 kind: Service   //对象是Service了哦
  3 metadata:
  4   name: myweb
  5 spec:  
  6   ports:
  7   - name: myweb-svc         //端口名称,Service是必须指定端口名称的
  8     port: 8080          //Service的端口号
  9     targetPort: 8080        //容器暴露的端口号
 10     nodePort: 31111       //node的真实端口号
 11   selector:
 12     app: myweb     //Service选择了标签为app: myweb的pod
 13   type: NodePort

重点在于三个端口的区别,容器有端口,Service有端口,node也有真实的端口号,这里我们将这三者关联起来,在一会访问的时候会看出门道。所以,我们就拿结果说话吧。

创建tomcat Service对象的结果

这里写图片描述
大家可以看到,我已经创建好了名为myweb的Service。而且这个Service也已经通过selector选择了刚才创建好的5个pod。所以我可以通过Service来访问tomcat的服务了。我这里使用命令行来演示。

访问服务的方式

我们可以通过两种方式来访问已经创建好的服务。

1. 集群内部访问服务

我们可以通过Service IP + Service端口号的方式来从集群内部访问已经创建好的服务,所以,我们来看看tomcat Service的IP与端口号。
这里写图片描述
好了,现在可以进行访问了。
这里写图片描述
可以看到该服务已经通了,已经可以访问了,具体访问的是哪个pod中的tomcat,这是要根据具体情况进行负载均衡的。

2. 集群外部访问服务

从集群的外部我们可以通过node IP + node端口号的方式来对服务进行访问。pod实际上已经分在了不同的node中了,我们只用找到其中一个pod所在的node就行了。
这里写图片描述
可以看到我这个pod所在的IP地址。刚才已经看到了,在yaml文件中我将物理端口设为31111。好的,接下来就可以从外部对服务进行访问了。
这里写图片描述
可以看到,此时也可以访问服务。

部署mysql

如果你能将tomcat部署成功了,那么部署mysql就没有什么好讲的了。照前面做就行了。

创建mysql RC对象

我将此RC对象命名为mysql-rc1.yaml
直接上代码:

  1 apiVersion: v1
  2 kind: ReplicationController
  3 metadata:
  4   name: mysql
  5 spec:
  6   replicas: 1
  7   selector:
  8     app: mysql
  9   template:
 10     metadata:
 11       labels:
 12         app: mysql
 13     spec:
 14         containers:
 15         - image: img.reg.3g:15000/mysql:5.7.13
 16           name: mysql
 17           resources:
 18             limits:
 19               cpu: "2"
 20               memory: 4Gi
 21           ports:
 22           - containerPort: 3306
 23           env:
 24           - name: MYSQL_ROOT_PASSWORD
 25             value: "123456"

不同之处在于创建mysql的RC对象yaml文件时,注入了一个name为MYSQL_ROOT_PASSWORD的环境变量,这个是给mysql数据库设置密码,这个环境变量的注入是必须的,如果没有此环境变量,虽然RC可以被创建成功,但是系统是无法启动mysql容器的。不信可以试试。算了,还是给你演示一下吧。

创建mysql RC对象的结果

我把环境变量删了之后运行的结果如下:
这里写图片描述
可以看到,删掉环境变量的mysql RC虽然可以创建成功,但是容器启动失败了。
增加环境变量之后的结果
这里写图片描述
增加环境变量之后就可以启动起来容器了

创建mysql Service对象

同样道理,mysql也需要一个访问入口地址。创建的Service yaml文件名为mysql-svc1.yaml
还是直接上代码

  1 apiVersion: v1
  2 kind: Service
  3 metadata:
  4   name: mysql
  5 spec:
  6   ports:
  7   - name: mysql-svc
  8     port: 3306
  9     targetPort: 3306
 10     nodePort: 31101
 11   selector:
 12     app: mysql
 13   type: NodePort

没什么好解释的。

创建mysql Service对象的结果

这里写图片描述
可以看到,mysql Service对象也已经启动成功了。

由于前面已经详细介绍过如何访问服务了,这里我就不再赘述了。大家可以自己访问mysql的服务看看。

需要注意的细节

我认为,在kubernetes中部署应用集群的时候有两点特别需要注意的,当然,是我会犯错的地方,与大家分享:

  1. yaml文件的格式问题
    我初次编写yaml文件可是花了不少时间,就是因为格式的问题,总是创建不了RC或者Servcice。尤其是需要注意,缩进要用空格缩进而不能用TAB键缩进。否则是成功不了的。
  2. 理解pod、Service、node IP以及端口的关系
    这里的概念可能会比较难以理解,但一定要搞清楚,这是访问时的重点

总结

在kubernetes中部署容器,不仅可以用yaml,也可以用Json。还有,现在有许多容器化应用都是通过deployment部署的。当然,如果用RC部署也是可以的。我是第一次在kubernetes中部署容器化应用集群,所以就从简单的入手。希望本篇博客能够帮助到有些也刚刚接触kubernetes的同学。