包管理器将软件安装到我们的服务器上。在RedHat/CentOS上,其使用的包管理器为yum和rpm;在Debian/Ubuntu上,我们将讨论apt和dpkg软件包管理器。dpkg是Debian的软件包管理器,类似于RedHat/CentOS系列的rpm。
作为软件包的管理器,它们必须有三个重要功能:
在Ubuntu 16.04之前,我们使用apt-get和apt-cache命令。在我们曾经使用apt-get install的地方,现在我们使用apt安装。我们曾经使用apt-cache show的地方,现在我们使用apt show。如果你掌握了传统的apt-get、apt-cache命令,很容易切换到更新的apt命令。这里暂且还以传统的apt-get、apt-cache进行讨论。
APT包管理器在服务器上保存软件安装源列表。每个源都包含存储库列表。存储库作为可用软件包的索引。当您搜索软件包时,Apt将检查此列表来进行安装。
安装源及其存储库清单保存在两个位置:
可以通过更新上述文件或文件夹来更新可用的存储库。
另外,我们可以手动添加第三方存储库,也可以使用add-apt-repository
命令。add-apt-repository
命令将添加一个源到/etc/apt/sources.list文件或/etc/apt/sources.list.d目录。添加的存储库将适用于我们的服务器版本。该命令还可以添加一个密钥,用于验证其中的存储库和包的真实性。
我们可以通过运行sudo apt-get update
来更新Apt对可用包和版本的了解。这将读取存储库列表,并更新可用于安装的软件包和版本。在安装任何软件之前或添加新的存储库后都要运行此操作。这将确保它安装最新的可用版本的软件包。源列表更新后,我们可以根据软件包的名称来安装任何我们想要的软件。
(1)普通安装:apt-get install softname1 softname2 …;
(2)修复安装:apt-get -f install softname1 softname2… ;(-f Atemp to correct broken dependencies)
(3)重新安装:apt-get –reinstall install softname1 softname2…;
(1)普通安装:dpkg -i package_name.deb
(1)移除式卸载:apt-get remove softname1 softname2 …;(移除软件包,当包尾部有+时,意为安装)
(2)清除式卸载:apt-get remove –purge remove softname1 softname2…;(同时清除配置)
清除式卸载:apt-get purge sofname1 softname2…;(同上,也清除配置文件)
(1)移除式卸载:dpkg -r pkg1 pkg2 …;
(2)清除式卸载:dpkg -P pkg1 pkg2…;
apt-cache search # ------(package 搜索包)
apt-cache show #------(package 获取包的相关信息,如说明、大小、版本等)
apt-cache depends #-------(package 了解使用依赖)
apt-cache rdepends # ------(package 了解该包被哪些包依赖)
apt-file search filename -----查找filename属于哪个软件包
apt-file list packagename -----列出软件包的内容
apt-file update --更新apt-file的数据库
dpkg --info "软件包名" --列出软件包解包后的包名称.
dpkg -l --列出当前系统中所有的包.可以和参数less一起使用在分屏查看. (类似于rpm -qa)
dpkg -l |grep -i "软件包名" --查看系统中与"软件包名"相关联的包.
dpkg -s 查询已安装的包的详细信息.
dpkg -L 查询系统中已安装的软件包所安装的位置. (类似于rpm -ql)
dpkg -S 查询系统中某个文件属于哪个软件包. (类似于rpm -qf)
dpkg -I 查询deb包的详细信息,在一个软件包下载到本地之后看看用不用安装(看一下呗).
dpkg --listfiles 查询系统中属于该软件包的文件
]]>Kubernetes是一个Google主导的生产就绪、企业级、成熟的容器编排平台。如果不使用云服务商提供的部署工具,在你自己的集群中搭建Kubernetes环境是比较繁琐的。而Rancher这个开源的容器管理平台为我们完美地解决了快速部署Kubernetes这个问题。
玩过Kubernetes(以下简称k8s)的小伙伴都知道,在本地手动部署一个完整的k8s环境比较复杂,因为它有多个组件需要安装配置。首先需要一个etcd数据库用作存储;其次如果要实现它的overlay网路的话,要部署flannel;然后需要部署master和node节点上的相关软件包;最后当然node节点上不能缺少docker容器引擎。而在这个过程中每个组件的配置有错误,最后都会导致整个k8s环境的不可用。
对于想尝试k8s,或者并不希望花费太多的时间用于构建一个基于容器集群的开发环境的人,虽然k8s官方提供了minikube这个工具,但是对于国内的用户来说,如果不经过配置,其需要的许多依赖都由于网络环境的原因访问不了,所以在国内使用minikube的体验并不很好。
幸好有Rancher这个开源的全栈化的容器管理工具存在,他支持了常见的容器编排和管理引擎,包括k8s,mesos,swarm及其自己开发的cattle引擎,使得部署容器集群的工作非常简单。本文就来看看Rancher是怎样部署k8s的。
Rancher提供了一个应用商店(Catalog),它保存着各种应用程序栈的部署模板,通过使用这套模板可以实现应用程序的快速部署。而Rancher对于各种容器管理引擎的部署也是通过应用程序模板来实现的,只不过这套模板在应用商店中的类型是基础设施服务,本质上与其它应用程序模板没有太大的不同。关于Rancher的使用入门,可以参考这篇文章或者官方文档的快速开始向导。
另外,如果通过Rancher在部署时没有进行其它配置,在添加环境时选择的环境模板为Kubernetes的话,添加主机后整个Kubernetes环境虽然能运行起来,但在后面实际使用时,会发现Kubernetes的Dashboard无法使用,其它的一些Kubernetes的Add-Ons也无法使用。部署pod时,会发现程序会卡在拉取镜像的步骤。这是因为Rancher部署Kubernetes后,Kubernetes默认通过Google的docker镜像库来拉取镜像,而这个库在国内的网络是无法访问的。Rancher Labs的国内的同事通过修改环境模板,使Rancher部署的k8s通过国内的阿里云来拉取一些Pod基础设施容器镜像,优化了中国区的使用体验,这篇文章对于部署过程进行了说明。下面我们就根据这篇文章的说明来实际操作一遍。
这里的运行环境使用了四台Virtual Box虚拟机,每台主机的内存设置为2G,操作系统为CentOS7,已经安装好了docker引擎(版本1.12.3),docker可以从网络上正常拉取镜像。四台机器的IP地址及主机名如下:
rancher-k8s-master:192.168.56.10
rancher-k8s-node1:192.168.56.100
rancher-k8s-node2:192.168.56.101
rancher-k8s-node3:192.168.56.102
直接使用文章中的命令在master上来启动Rancher Server:
sudo docker run -d --restart=unless-stopped \
-e DEFAULT_CATTLE_CATALOG_URL='{"catalogs":{"community":{"url":"https://github.com/rancher/community-catalog.git","branch":"master"},"library":{"url":"https://github.com/niusmallnan/rancher-catalog.git","branch":"k8s-cn"}}}' \
--name rancher-server \
-p 8082:8080 rancher/server:stable
上面的命令比普通的运行Rancher Server的命令其实就是多了一个启动时的环境变量,告诉Rancher Server可以去哪里下载Rancher的Catalog。上面指定的地址其Catalog中的部分配置已经经过修改,可以让Rancher部署的k8s从阿里云来拉取镜像。
等待rancher server启动起来之后,通过浏览器访问http://192.168.56.10:8082/,就进入了Rancher的web页面。我们需要使用k8s的编排引擎,Rancher默认的编排引擎是Cattle,所以我们需要先创建一个环境。将鼠标放到主页左上角的Default菜单上,会自动显示下拉菜单,点选“环境管理”,然后点选“添加环境”。在添加环境页面上,填入名称(这里我们填入k8s)和描述(这里我们没有填写),在下面的环境模板中选择Kubernetes,然后点击创建,便添加了一个名为k8s的新环境。如下图:
点击“切换至此环境”图标,便进入了k8s的环境。
此时该环境的页面提示需要添加至少一台主机。点击页面上部的“添加主机”链接,首先让你确认Rancher API的Base URL,点击保存后,页面会出现可以添加的主机类型的选项,默认为Custom。由于我们这里要建立本地的集群环境,所以选择Custom。如果要在公有云环境下创建主机,请选择相应的公有云类型。在选择了Custom环境的页面中,系统给出了添加主机的6个步骤。按照页面的步骤在每台主机上操作。对于我们的环境来说,就是在node1、node2、node3这三个节点上操作。在每台主机上都配置好docker环境(注意需要使用支持k8s的docker版本)并且运行了第5步的docker脚本后,点击关闭,页面回到k8s的dashboard页面,如下图:
这时候请耐心等待,如果是第一次进行添加主机操作的话,系统会到互联网上拉取相关的镜像,这需要一段时间。最后整个环境都部署完成之后,页面会出现Kubernetes UI的按钮:
这时候整个k8s的环境便通过Rancher部署好了。可以点击一下Kubernetes UI的按钮,来看看其仪表板:
可见通过Rancher来部署k8s环境,整个过程是多么简单。
在整个k8s环境都部署好了之后,就可以通过kubectl命令行工具来与k8s交互了。在Rancher的k8s环境下,KUBERNETES下拉菜单中有一项为CLI,这个菜单的作用是调出k8s的命令行kubectl。本文使用的是Rancher v1.6.2版,在Rancher页面为中文环境时,KUBERNETES菜单下的CLI页面似乎不太正常,点击之后没有显示,也许是我环境配置的问题。如果你也遇到相同的问题,只要切换成英文后刷新页面CLI页面便可以显示出来。然后就可以使用页面上的kubectl与k8s交互。或者点击“Generate Config”生成kubectl的配置文件,将配置拷贝到你的kubectl所在的环境下,就可以使用k8s了。
另外,Rancher已经为用户配置好了Helm和Tiller,可以在其CLI页面直接使用。简单来说,Helm是k8s下的包管理器,你可以将其类比为yum、apt或homebrew,它提供了在k8s下部署集群服务组件的方式。Rancher的应用商店Catalog也与其类似。关于Helm的介绍,可以参考这篇文章 。
declare 与 typeset 命令是bash的内建命令,两者是完全一样的,用来声明shell变量,设置变量的属性。
-r 设置变量为只读;后面不加变量名则显示所有只读变量。
-i 设置变量为整数;后面不加变量名则显示所有整形变量。
-a 设置变量为数组array;后面不加变量名则显示所有数组变量。
-f 如果后面没有参数的话会列出之前脚本定义的所有函数,如果有参数的话列出以参数命名的函数。
-x 设置变量在脚本外也可以访问到;后面不加变量名则显示所有可以在脚本外访问的变量。
无选项则显示所有变量。
代码示例请参考这里。
]]>Vagrant是一个开源的基于ruby的开源虚拟机管理工具,它能够以脚本化的方式启动、停止、和和删除虚拟机,而且它能够完成自己定义网络分配、初始化执行脚本、添加硬盘等各种复杂的动作,最重要的是Vagrant提供了类似于docker image的box。本文就来简单探索一下Windows下Vagrant的使用。
在Linux或MacOS环境下,由于Vagrant依赖的一些程序(rsync、ssh等)比较全,在使用过程中遇到的问题可能相对较少。然而在Windows环境下,由于Windows的cmd命令行与bash环境区别较大,可能会遇到相对较多的问题。本文主要描述了在Windows环境中如何正确使用Vagrant。
首先,Windows下Vagrant的安装和配置可以参考这篇文章。然而在使用vagrant ssh
登录虚拟机时,却遇到如下错误:
D:\VM\sfh>vagrant ssh
`ssh` executable not found in any directories in the %PATH% variable. Is an
SSH client installed? Try installing Cygwin, MinGW or Git, all of which
contain an SSH client. Or use your favorite SSH client with the following
authentication information shown below:
Host: 127.0.0.1
Port: 2222
Username: vagrant
Private key: D:/VM/sfh/.vagrant/machines/default/virtualbox/private_key
这是因为Windows的cmd命令行环境,并没有ssh命令可用,于是便请出Cygwin这个Windows下的工具。简单来说,Cygwin其实就是一个命令行工具,它利用Windows的一些函数来实现Linux独有的API。Cygwin的安装也很简单,只要在其官网下载安装程序包安装即可,首次安装时,其默认只安装最小化的基础包,其它的一些程序需要选择安装。另外,可以将国内的镜像添加到安装源列表中,这里推荐使用网易的镜像(使用说明参考这里)。
在安装了Cygwin后,确认默认的包都已经安装可以正常运行之后(点击Cygwin64 Terminal快捷方式,可以正常运行cygwin命令行程序),再次运行安装程序。这次选择安装rsync和ssh。如下图:
首先在Search框中输入rsync,稍等片刻待安装程序过滤出rsync包之后,点击Skip前面那个箭头,Bin一栏下变成选择状态;然后在Search框中输入openssh,同样点选Skip前面的箭头,Bin一栏下变成选择状态后,点击下一步便完成了rsync和ssh程序的安装。测试一下,打开Cygwin命令行界面,分别输入rsync和ssh命令后,都有命令行提示输出,这两个程序便成功安装了。
下面所有的Vagrant命令,都通过Cygwin命令行窗口来执行,这样,在Vagrant执行过程中再调用rsync程序和ssh程序等第三方程序,便不会报错了。通过如下的Vagrantfile来安装两台CentOS7的virtualbox的虚拟机:
进入该文件所在文件夹,打开Cygwin,执行vagrant up
:
$ vagrant up
Bringing machine 'node1' up with 'virtualbox' provider...
Bringing machine 'node2' up with 'virtualbox' provider...
==> node1: Importing base box 'centos/7'...
...
==> node1: Machine booted and ready!
...
==> node1: Rsyncing folder: /cygdrive/d/VM/vagrant_centos7/ => /vagrant
==> node2: Importing base box 'centos/7'...
...
==> node2: Machine booted and ready!
...
==> node2: Rsyncing folder: /cygdrive/d/VM/vagrant_centos7/ => /vagrant
尝试ssh登陆到主机上:
$ vagrant ssh node1
[vagrant@localhost ~]$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 52:54:00:ba:51:2b brd ff:ff:ff:ff:ff:ff
inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic eth0
valid_lft 85998sec preferred_lft 85998sec
inet6 fe80::5054:ff:feba:512b/64 scope link
valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 08:00:27:ae:06:e5 brd ff:ff:ff:ff:ff:ff
inet 192.168.56.10/24 brd 192.168.56.255 scope global eth1
valid_lft forever preferred_lft forever
inet6 fe80::a00:27ff:feae:6e5/64 scope link
valid_lft forever preferred_lft forever
可见,在windows环境中,通过Cygwin执行vagrant命令后,命令都能正常执行。
结束语:通过Vagrant可以方便地实现对虚拟机的管理,包括建立和删除虚拟机、配置虚拟机运行参数、管理虚拟机运行状态、自动化配置和安装开发环境必须的各类软件、打包和分发虚拟机运行环境等。这给开发环境的搭建,尤其是集群环境的搭建提供了自动化的解决方案。同时也给需要在不同平台环境下工作的人员提供了便利。
]]>Swarm是Docker公司在2014年12月初发布的一套较为简单的工具,用来管理Docker集群,它将一群Docker宿主机变成一个单一的,虚拟的主机。随着Docker的不断发展,Swarm也不断更新。目前的Swarm有三种类型:Swarm, Swarmkit 和 Swarm模式。
简单来说,旧的Swarm可以作为容器运行,与Docker Engine是分离的;SwarmKit是可独立运行的二进制文件,需要从源码构建;而Swarm模式已经将SwarmKit整合到Docker引擎当中,可以通过Docker命令进行Swarm集群的操作。三者的区别也可以参考这里。本文主要通过SwarmKit进行Swarm集群的搭建。多个集群节点的创建通过docker-machine来完成。SwarmKit的概念和架构这里不再赘述。
对于集群来说,必然涉及到多个节点。当集群节点的数量较多时,如何创建和管理这些节点便成为急需解决的问题。所幸的是,目前有许多的配置管理工具可以完成这些任务,可以是puppet,chef,ansible或salt;另外,目前可选的大量公有云和私有云解决方案,简化了集群节点的创建,可以通过其提供的API进行操作,省略了繁杂的物理服务器搭建配置过程。下面就使用docker-machine来创建集群节点,并完成swarm集群的搭建过程。
由于SwarmKit需要通过编译生成二进制可执行文件,为了简化这个过程,我们直接使用已经编译好的SwarmKit的容器镜像。
通过如下命令创建swarmkit-master节点,然后运行swarmkit的manager:
|
|
上面第1行命令创建了virtualbox的虚拟机节点,并命名为swarmkit-master;
第2行命令直接运行了一个fsoppelsa/swarmkit的docker镜像,此镜像包含了编译后的swarmkit可执行文件swarmd及管理工具swarmctl,将docker主机的4242端口映射到容器的4242端口,并且将docker主机的socket挂载到了容器内部,最后运行了swarmd程序,通过参数将其置于监听模式,监听来自于所有ip地址对4242端口的请求。
通过如下命令创建3个worker节点:
|
|
上述命令运行了一个bash循环,注意循环体内部命名结尾处的“&”,它使命令放入背景执行,其效果是并行创建3个节点。命令正常结束后,可以查看一下节点的状态:
|
|
可见,docker-machine创建了4个节点,一个master,3个worker,并且都在其上安装了docker引擎。
将3个worker节点依次加入第一步创建的master节点中:
|
|
在加入master时,在bash循环体内部并没有使用“&”,效果是命令串行执行。这是因为每次循环,都需要先通过第2行命令连接到相应的worker节点,然后通过第3行命令执行加入manager的指令,如果并行执行会造成连接节点和执行加入manager命令的混乱。这在worker节点较多时,会降低程序执行的效率。解决思路可以通过每次循环内部先创建一个新的bash运行环境,然后在这个新的bash环境下连接相应节点然后加入manager,这样就可以并行运行命令了。
最后,连接到master节点上,通过swarmctl检查一下集群的状态:
|
|
可见,我们的swarm集群已经成功创建。
通过SwarmKit创建service:
下面我们通过swarmctl程序,创建一个由nginx容器组成的service。首先检查一下集群中是否已经存在service:
|
|
可见集群中还没有任何service。下面来创建一个名为web的服务:
|
|
上述命令创建了一个名为web的由nginx镜像组成的有3个replicas的service。这需要一段时间来生效,因为每个节点都需要下载nginx的镜像。可以再查询一下集群中的服务:
|
|
上述命令3/3说明需要3个replicas,当前有3个replicas正在运行。可以进一步查看任务的详细信息:
|
|
通过上述命令可以发现在master的节点上也运行了一个web任务,在SwarmKit中默认这是允许的。如果在生产环境中不想让任务分配到manager节点上,可以通过使用labels和constrains来限制任务可以运行的节点。
结束语:本文通过docker-machine创建了4个集群节点,通过使用其virtualbox的driver,在单台主机上创建了4台基于virtualbox的虚拟机。这对于搭建开发或测试环境是非常方便的。
如果使用docker-machine提供的其它的driver,可以在各种公有云如AWS上创建节点。在公有云或私有云上,可以不受单台主机内存的约束,调整创建主机时bash循环运行的次数,从而创建大量节点。
另外,本文通过SwarmKit搭建的服务,只是一个很初级的集群,没有使用集群的一些高级特性,例如负载均衡和VxLAN的网络。这些高级特性都集成在Docker的Swarm模式中,所以在以后创建Swarm集群时,可以通过使用docker版本1.12+来创建Swarm模式的集群。
Docker Machine是一个简化Docker安装的命令行工具,通过一个简单的命令行即可在相应的平台上创建主机,比如VirtualBox、 Digital Ocean、Microsoft Azure,然后在创建的主机上安装Docker。
Docker Machine通过不同的driver,可以在不同的平台上创建主机,包括各种公有云、私有云及虚拟机等。本文主要是通过Docker Machine提供的Generic driver,来将已有的主机通过ssh纳入其管理,这在使用某个Docker Machine并不支持的云供应商时非常有用。
另外,也可以通过Docker Machine Generic driver提供的功能,来将Docker的远程API进行TLS加密。在文章使用TLS加密Docker的API中,我们通过OpenSSL软件包,来生成TLS通讯所需的证书、秘钥等文件,然后在启动docker daemon时通过指定tls相关的启动参数,来将docker daemon安全地开放到网络上。Docker Machine将此过程全部自动化,简化了操作过程。
这个过程是怎样实现的呢?
下面实践一下这个过程,使用的环境如下:
centos7 192.168.71.131 Docker-machine所在主机
centos7-A 192.168.71.167
centos7-B 192.168.71.168
我们想要在centos7主机上通过docker-machine命令连接到另外两台主机的docker引擎上。
/etc/sudoers
文件中加入一行yangdong ALL=(ALL) NOPASSWD: ALL
即可(用你自己的用户名替换最前面的yangdong)。
|
|
在执行上述命令时,最后docker-machine会通过netstat检查docker所打开的端口,如果缺少这个命令会报错,不过并不影响使用。通过如下命令检查安装是否成功:
[yangdong@centos7 ~]$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
centos7-A - generic Running tcp://192.168.71.167:2376 v1.12.6
centos7-B - generic Running tcp://192.168.71.168:2376 v1.12.6
这样,两台主机便纳入了docker-machine的管理了,可以通过docker-machine提供的各种命令操作远程的docker引擎了。
关于Generic driver的更详细信息可以参考官方文档。
Raft是一个比Paxos更易理解和实现的分布式一致性算法。它允许一组机器像一个整体一样工作,即使其中一些机器出现故障也能够继续工作下去。
分布式系统中的一致性算法可以使系统中的各个agents保持一致的值,并且可以从这些agents当中选举出一个leader。Paxos和Raft是两个比较知名的一致性算法。它们有类似的性能,不过Raft更简单一些,更容易理解,所以在分布式存储实现中更加流行。Consul和Etcd就是在Raft算法的基础上实现的。
一个Raft集群包含若干个服务器节点。在任何时刻,每一个服务器节点都处于这三个状态之一:跟随者Follower、候选人Candidate或者领导人Leader。
三者之间的转换关系如下图所示:
后面会结合选举过程来详细看看角色转换的过程。
如下图所示,每个Term由选举开始,在这个时间内若干处于Candidate状态的服务器竞争产生新的Leader(图中蓝色部分)。如果某个服务器成为了Leader,那在接下来的时间便成为一般的操作时间(图中绿色部分);如果没有选举出Leader(图中t3),则Term递增,开始新一任期选举。每次Term的递增都将发生新一轮的选举,Raft保证一个Term内最多只有一个Leader。
Log复制主要作用是用于保证节点的一致性,这阶段所做的操作也是为了保证一致性与高可用性;当Leader选举出来后便开始负责客户端的请求,所有请求都必须先经过Leader处理,这些请求或说成命令也就是这里说的日志。Leader接受到客户端命令之后,将其追加到Log的尾部,然后向集群内其他服务器发出AppendEntries RPC(也作为心跳信息),这引发其他服务器复制新的命令操作,当大多数服务器复制完之后,Leader将这个操作命令应用到内部状态机,并将执行结果返回给客户端。
如下图所示的Log结构图:
每个Log中的项目包含2个内容:Term编号(方框上部的数字)和操作命令本身;还有一个全局的Log Index来指示Log项目在Log中的顺序编号。当大多数服务器在Log中存储了该项目,则可认为该项目是可以提交的,比如上图中的Log Index为7之前的项目都可以提交。
在最后面参考资料的动画演示中,也展示了当网络出现问题,导致整个集群分区成两个部分,然后又恢复后,日志的复制过程。
安全性是用来保证每个节点都执行相同序列的安全机制,如当某个Follower在当前Leader提交命令时不可用了,稍后可能该Follower又会被选举为Leader,这时新Leader可能会用新的Log覆盖先前已提交的Log,这就是导致节点执行不同序列;安全性就是用于保证选举出来的Leader一定包含先前已经提交Log的机制。
为了达到安全性,Raft增加了两个约束条件:
RPyC(Remote Python Call)是用于远程过程调用,集群和分布式计算的python库。RPyC克服了进程和计算机之间的物理界限,使得操作远程对象就像操作本地对象一样。
使用rpyc编写c/s结构程序,完全不用考虑老式的socket编程,现在只用编写简单的3、5行代码即可完成以前多行代码完成的功能。在RPyC 3.0版之后,其提供了一种基于服务的新的RPyC编程模型。基本上,一个服务有如下的模板代码:
上面的代码有几点需要注意:
on_connect
和on_disconnect
方法。exposed_
开始的方法可以被远程访问到,其它的方法不能被远程访问。例如上面例子的exposed_get_answer
方法,客户端可以通过名称get_answer
远程调用,但是客户端调用不了 get_question
方法。在客户端,服务被暴露为连接的root对象,下面看一下客户端的调用实例:
通过上面的实例可见通过RPyC编写远程过程调用是多么得简单。
RPyC不仅仅对Socket进行了封装,它还支持同步与异步操作、回调和远程服务以及透明的对象代理,可以在Server与Client之间传递Python的任意对象,性能也比较高效。可以参考官方文档的教程进行更多了解。
]]>Python的Methods(方法)是定义在类的内部的函数。Python中通常有3中类型的方法:实例方法,类方法,静态方法。本文就来简单了解一下。
下面来看一个实例代码:
|
|
还有另外一种调用实例方法的方式,这种调用方法就可以看出参数self的作用:
一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。
知乎上的用户回答了“什么是回调函数?”,这篇文章对回调函数进行了一个清晰得讲解。
该文中有一个形象的比喻:有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数(to register a callback function)。
回调机制提供了非常大的灵活性。有时候,回调并不仅仅发生在应用程序和库函数之间,也可能发生在不同的应用程序之间,这时候就可以把调用回调函数的库函数称为中间函数。程序可以在运行时,通过登记不同的回调函数,来决定、改变中间函数的行为。这就比简单的函数调用要灵活太多了。下面看一个简单的示例:
|
|
运行callback_demp.py
,输出如下:
3
5
9
上面第19行代码传入的回调函数是一个匿名函数。在“Python lambda简介”文章中介绍过Python的lambda函数,它可以用来指定小的回调函数。
通过上面的示例可以看出,在调用中间函数时,通过传入不同的回调函数作为参数,可以产生不同的结果。
Git-Flow是构建在Git之上的一个组织软件开发活动的模型,是在Git之上构建的一项软件开发最佳实践。Git Flow是一套使用Git进行源代码管理时的一套行为规范和简化部分Git操作的工具。它是在一篇名为“一种成功的Git分支模型”的博文中第一次被提出。
Production 分支
也就是我们经常使用的Master分支,这个分支是最近发布到生产环境的代码,最近发布的Release,这个分支只能从其他分支合并,不能在这个分支直接修改。
Develop 分支
这个分支是我们是我们的主开发分支,包含所有要发布到下一个Release的代码,这个主要合并自其他分支,比如Feature分支。
Feature 分支
这个分支主要是用来开发一个新的功能,一旦开发完成,我们合并回Develop分支进入下一个Release。
Release分支
当你需要一个发布一个新Release的时候,我们基于Develop分支创建一个Release分支,完成Release后,我们合并到Master和Develop分支。
Hotfix分支
当我们在Production发现新的Bug时候,我们需要创建一个Hotfix, 完成Hotfix后,我们合并回Master和Develop分支,所以Hotfix的改动会进入下一个Release。
下面的内容出自博文“git-flow备忘清单”,该篇文章对于git-flow的相关命令描述十分清晰,这里将该文的主要内容复制过来。
OSX:
$ brew install git-flow-avh 或 $ port install git-flow-avh
Ubuntu:
$ apt-get install git-flow
其它Linux:
$ wget --no-check-certificate -q https://raw.githubusercontent.com/petervanderdoes/gitflow-avh/develop/contrib/gitflow-installer.sh && sudo bash gitflow-installer.sh install stable; rm gitflow-installer.sh
为了自定义你的项目,Git flow 需要初始化过程。
使用 git-flow,从初始化一个现有的 git 库内开始:
git flow init
你必须回答几个关于分支的命名约定的问题。建议使用默认值。
本文仅包含git flow最常用的功能,并非所有可用的命令都涵盖在这里。可以通过git命令来完成git flow所描述的模型,这里安装的git flow命令是在git命令的基础上进行了包装,以简化完成git flow模型所使用的命令。
]]>NAT全名是Network Address Translation,字面上的意思是网络地址转换,它还可以分为SNAT(Source Network Address Translation,源地址转换)和DNAT(Destination Network Address Translation,目的地址转换)。SNAT主要是用来做默认网关的,而DNAT主要是用来做端口映射的。
本篇就来简单看看在Linux系统上的SNAT和DNAT的配置。
Linux的NAT可以通过iptables配置实现。下图是一个简化的网络数据包在Linux主机中iptables各个表中处理的过程。为了简单,在该图中只标出了iptables中表filter和nat的链:
在任何一个IP数据包中,都会有Source IP Address与Destination IP Address这两个字段,数据包所经过的Linux路由器主机也是根据这两个字段是判定数据包是由什么地方发过来的,它要将数据包发到什么地方去。而iptables的DNAT与SNAT就是根据这个原理,对Source IP Address与Destination IP Address进行修改。
SNAT主要是用来给内网的主机提供连接到Internet的默认网关,借用鸟哥的图:
如上图,SNAT操作的是iptables中NAT表的Postrouting链,将ip包的源地址替换为网关的公网地址。
DNAT可以将内网机器的端口映射到外网上面,仍然借用鸟哥的图:
如上图,DNAT操作的是iptables中NAT表的Prerouting链,将ip包的目的地址替换为内网要映射端口的主机地址。
所以,NAT服务器一定是路由器,因为NAT服务器一定进行了数据包的转发。
从上面的背景知识可知,要配置NAT,必须开启Linux内核的转发功能:
echo 1 > /proc/sys/net/ipv4/ip_forward #临时生效,重启失效
要永久生效,需要:
vi /etc/sysctl.conf
修改其中的net.ipv4.ip_forward = 1
。
然后执行:
syscty -p
立刻生效。
由于NAT是通过内核的nat模块提供的,而内核的iptable_nat模块默认是没有加载的,通过如下命令手动加载该模块:
modprobe iptable_nat
不过,后面在进行iptables操作时,-j SNAT或-j MASQUERSDE及-j DNAT会自动加载iptable_nat内核模块。这里如果没有手动加载,后面的操作也会将该模块自动加载上。
从第一张图可以知道,如果数据包需要进行转发,还要通过iptables中filter表的FORWARD链。所以需要确保iptables规则中FORWARD链可以转发相应的数据包。可以通过如下命令确认:
# iptables -L
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
从上面命令的输出可以看出,FORWARD链默认规则是ACCEPT,而且它没有设置其它规则,所以数据包是可以通过FORWARD链的。如果FORWARD链的默认规则为DROP或REJECT,需要在FORWARD链中再添加相关规则以允许相应的数据包通过。
假如在NAT机器上,想要将192.168.1.0/24网段的数据包的源地址修改为公网的ip58.20.51.66,通过端口eth送出,可以:
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth1 -j SNAT --to-source 58.20.51.66
另外一种方法:
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth1 -j MASQUERADE
假如需要将对外网ip202.103.96.112网口eth1的访问,映射到对内网ip192.168.0.112的访问;
iptables -t nat -A PREROUTING -i eth1 -p tcp -d 202.103.96.112 --dport 80 -j DNAT --to-destination 192.168.0.112:80
]]>Cobbler是一个Linux安装服务器,它可以快速安装配置一个网络安装环境。Cobbler通过将设置和管理一个安装服务器所涉及的各种任务集中在一起,简化了整个安装服务器的系统配置。
在之前的文章,介绍过通过salt cloud在云端批量部署计算节点。但是如果我们需要的是快速批量部署物理服务器的话,salt-cloud并不能实现,这样就需要通过其它工具的辅助。
对于Linux系统自动化部署可以采用pxe+dhcp+tftp配合kickstart实现,但是面对多版本、多部署需求的情况下,这种普通的部署方式可能达不到我们的要求;这时候就需要借助cobbler开源自动化部署工具来实现。Cobbler是一款快速的网络系统部署工具,其最大的特点是集合了所有系统部署所需服务,如DHCP、DNS、TFTP,这样你在部署一台操作系统的时候不需要在各个服务之间协调切换。
下面就来看一下Cobbler的简单使用:
本文的实验环境为CentOS7(网段为192.168.71.0/24):
1)首先安装epel的源:yum -y install epel-release
2)安装cobbler相关服务:yum -y install cobbler cobbler-web dhcp pykickstart xinetd
3)关闭SELinux:setenforce 0
4)关闭防火墙:systemctl stop firewalld
Cobbler配置非常灵活,可以实现的功能也很多。下面配置一个可以通过网络安装CentOS7系统的Cobbler安装服务器。为了简单起见,我们配置一个可以使该服务器运行的最小配置:
/etc/cobbler/settings
文件这里只列出修改了默认值的项:
将manage_dhcp设置为1的意思是让Cobbler管理dhcp服务;选项next_server用在DHCP配置文件中,向机器告知提供引导文件的服务器地址;选项server在机器安装期间用于提供Cobbler 服务器地址。
/etc/cobbler/dhcp.template
文件
|
|
该文件主要配置DHCP服务可以分配IP地址的网段、默认网关地址、DNS服务器地址、子网掩码及可以分配的IP地址池。
/etc/xinetd.d/tftp
文件将该文件中disable = yes
改为disable = no
通过命令systemctl start httpd rsyncd xinetd cobblerd
启动相关服务。
可以通过systemctl status httpd rsyncd xinetd cobblerd
查看相关服务的信息。
下面看看cobbler的一些重要文件目录:
/etc/cobbler # 配置文件目录
/etc/cobbler/settings # cobbler主配置文件
/etc/cobbler/dhcp.template # DHCP服务的配置模板
/etc/cobbler/tftpd.template # tftp服务的配置模板
/etc/cobbler/rsync.template # rsync服务的配置模板
/etc/cobbler/iso # iso模板配置文件目录
/etc/cobbler/pxe # pxe模板文件目录
/etc/cobbler/power # 电源的配置文件目录
/etc/cobbler/users.conf # Web服务授权配置文件
/etc/cobbler/users.digest # web访问的用户名密码配置文件
/etc/cobbler/dnsmasq.template # DNS服务的配置模板
/etc/cobbler/modules.conf # Cobbler模块配置文件
/var/lib/cobbler # Cobbler数据目录
/var/lib/cobbler/config # 配置文件
/var/lib/cobbler/kickstarts # 默认存放kickstart文件
/var/lib/cobbler/loaders # 存放的各种引导程序
/var/www/cobbler # 系统安装镜像目录
/var/www/cobbler/ks_mirror # 导入的系统镜像列表
/var/www/cobbler/images # 导入的系统镜像启动文件
/var/www/cobbler/repo_mirror # yum源存储目录
/var/log/cobbler # 日志目录
/var/log/cobbler/install.log # 客户端系统安装日志
/var/log/cobbler/cobbler.log # cobbler日志
通过命令cobbler check
来检查前面的配置是否正确。这里的输出只是给出建议的配置,有这些错误并不代表着Cobbler安装服务器不能正常运行。下面给出了本机运行上述命令的输出:
The following are potential configuration items that you may want to fix:
1 : some network boot-loaders are missing from /var/lib/cobbler/loaders, you may run 'cobbler get-loaders' to download them, or, if you only want to handle x86/x86_64 netbooting, you may ensure that you have installed a *recent* version of the syslinux package installed and can ignore this message entirely. Files in this directory, should you want to support all architectures, should include pxelinux.0, menu.c32, elilo.efi, and yaboot. The 'cobbler get-loaders' command is the easiest way to resolve these requirements.
2 : enable and start rsyncd.service with systemctl
3 : debmirror package is not installed, it will be required to manage debian deployments and repositories
4 : The default password used by the sample templates for newly installed machines (default_password_crypted in /etc/cobbler/settings) is still set to 'cobbler' and should be changed, try: "openssl passwd -1 -salt 'random-phrase-here' 'your-password-here'" to generate new one
5 : fencing tools were not found, and are required to use the (optional) power management features. install cman or fence-agents to use them
Restart cobblerd and then run 'cobbler sync' to apply changes.
修复第1项配置的方式为运行命令cobbler get-loaders
;
修复第2项配置的方式为启动rsyncd服务并且使其开机自动运行:systemctl enable rsyncd
,由于我们之前已经启动该服务,这里只是使其开机自动运行;
由于我们要安装RHEL系列的Linux发行版,所以忽略第3项配置;
第4项配置提醒我们新安装的Linux默认的root密码仍然为cobbler,这里我们保留默认密码,不做修改;
第5项配置是与电源相关的模块,我们暂时不需要,暂且忽略。
最后,通过命令cobbler sync
来应用前面的配置,如果最后输出“*** TASK COMPLETE ***”表示命令成功完成。
下面需要在cobbler中加入一个完成的系统安装镜像,首先将光盘ISO镜像挂载到某处:
# mount -t iso9660 -o loop,ro /path/to/isos/CentOS-7.0-1406-x86_64-DVD.iso /mnt
将光盘镜像挂载后,便可以运行导入命令了:
# cobbler import --name=CentOS7 --arch=x86_64 --path=/mnt
由于CentOS7光盘镜像较大,上述命令可能需要一段时间才能完成。如果上述命令成功完成,便可以使用cobbler distro list
和cobbler profile list
命令来列出系统导入的镜像:
# cobbler distro list
CentOS7-x86_64
# cobbler profile list
CentOS7-x86_64
使用cobbler distro report --name=CentOS7-x86_64
和cobbler profile report --name=CentOS7-x86_64
命令来列出更详细的信息。
在有了distro和profile之后,就可以创建一个system了。创建一个system对象的最主要的原因是每台机器不同的网络配置。
如果不创建system对象,使用profiles也可以完成系统的安装,只是被安装的系统只能使用DHCP的网络配置,但使用system对象就可以指定更详细的网络配置选项。提供给system对象的信息越详细,cobbler就会更多得自动配置被安装的系统。
下面配置一个system对象:
# cobbler system add --name=test --profile=CentOS7-x86_64
system对象可以根据被安装机器的MAC地址(这里假设为00:0C:29:A5:33:A0)来识别出不同的主机,从而给该主机的某个网络接口指定的特定的ip地址,子网掩码等信息:
# cobbler system edit --name=test --interface=eth0 --mac=00:50:56:38:B2:53 --ip-address=192.168.71.20 --netmask=255.255.255.0 --static=1 --dns-name=test.mydomain.com
被安装系统的默认网关以及机器名并不是按照每个网络接口指定的,所以这里单独添加:
# cobbler system edit --name=test --gateway=192.168.71.2 --hostname=test.mydomain.com
上述命令便完成了一个system对象的创建及编辑。可以使用cobbler system report --name=test
命令查看详细信息:
# cobbler system report --name=test
Name : test
TFTP Boot Files : {}
Comment :
Enable gPXE? : <<inherit>>
Fetchable Files : {}
Gateway : 192.168.71.2
Hostname : test.mydomain.com
Image :
IPv6 Autoconfiguration : False
IPv6 Default Device :
Kernel Options : {}
Kernel Options (Post Install) : {}
Kickstart : <<inherit>>
Kickstart Metadata : {}
LDAP Enabled : False
LDAP Management Type : authconfig
Management Classes : <<inherit>>
Management Parameters : <<inherit>>
Monit Enabled : False
Name Servers : []
Name Servers Search Path : []
Netboot Enabled : True
Owners : <<inherit>>
Power Management Address :
Power Management ID :
Power Management Password :
Power Management Type : ipmitool
Power Management Username :
Profile : CentOS7-x86_64
Internal proxy : <<inherit>>
Red Hat Management Key : <<inherit>>
Red Hat Management Server : <<inherit>>
Repos Enabled : False
Server Override : <<inherit>>
Status : production
Template Files : {}
Virt Auto Boot : <<inherit>>
Virt CPUs : <<inherit>>
Virt Disk Driver Type : <<inherit>>
Virt File Size(GB) : <<inherit>>
Virt Path : <<inherit>>
Virt PXE Boot : 0
Virt RAM (MB) : <<inherit>>
Virt Type : <<inherit>>
Interface ===== : eth0
Bonding Opts :
Bridge Opts :
CNAMES : []
InfiniBand Connected Mode : False
DHCP Tag :
DNS Name : test.mydomain.com
Per-Interface Gateway :
Master Interface :
Interface Type :
IP Address : 192.168.71.20
IPv6 Address :
IPv6 Default Gateway :
IPv6 MTU :
IPv6 Prefix :
IPv6 Secondaries : []
IPv6 Static Routes : []
MAC Address : 00:50:56:38:B2:53
Management Interface : False
MTU :
Subnet Mask : 255.255.255.0
Static : True
Static Routes : []
Virt Bridge :
虽然在每次编辑了system对象后,cobbler都会进行一次“lite sync”,但通过命令cobbler sync
进行一次完整的sync也是不错的,这可以使cobbler自动重新生成dhcpd.conf文件及重启DHCP服务。
上述配置全部完成后,就可以启动被安装系统了。如果是使用虚拟机进行安装,确保分配给它的内存不少于2GB,如果分配的内存过小的话,安装过程会报错。启动主机后,由于没有系统,主机会查找可以启动的设备,最后找到网卡,便通过PXE进行网络安装,如下图:
整个安装过程全部都是自动化进行,不需要人为干预。安装结束后,可以通过用户root密码cobbler登录系统。系统的ip地址,主机名等配置已经在安装过程中自动完成。
本文对Cobbler进行了一个简单的探索,配置了一个最简单的Cobbler安装服务器。Cobbler的功能还有很多,可以满足许多部署需求,更详细的信息可以参考其官方文档。
]]>了解Linux系统如何启动对于理解Linux如何运转是非常有帮助的,本文就来了解一下Linux的启动过程。
先借用一个启动的流程图:
上图的大致过程如下:
随着计算机软硬件的发展,传统的PC BIOS的局限性显现出来,于是其替代产品Unified Extensible Firmware Interface(UEFI)便开发出来。GPT的分区格式也是UEFI标准的一部分。通过UEFI搭配GPT进行启动的过程与传统BIOS搭配MBR是有区别的。详细信息可以参考鸟哥BIOS与UEFI开机检测程序。
在内核级别启动过程的最后(上图的最后一步),kernel会初始化一个pid为1的init进程,它根据配置文件启动其它进程。在不同的Linux发行版当中,主要有三个不同的init的实现,分别为:
下面分别来看一下各个不同实现的启动过程。
本篇对于Linux的启动过程进行了一个概略。首先从硬件部分来看,分为两种:一种是传统的BIOS搭配MBR方式,另一种是较新的UEFI搭配GPT方式。其次,在进入用户空间后,根据不同的init实现方式,分为System V init,Upstart以及Systemd等。各个方式的详细信息请参考相关文档。
]]>Bash提供了一些操作变量的功能,可以使用很短的表达式来操作变量。由于bash变量的内容大部分为字符串,所以这些功能就可以非常方便地操作变量中保存的字符串,如删除部分字符,替换部分字符。另外bash还提供了用于测试变量是否设置的功能。
变量设置方式 | 说明 |
---|---|
${变量#关键字} ${变量##关键字} |
若变量内容从头开始的数据符合“关键字”,则将符合的最短数据删除。 若变量内容从头开始的数据符合“关键字”,则将符合的最长数据删除。 |
${变量%关键字} ${变量%%关键字} |
若变量内容从尾向前的数据符合“关键字”,则将符合的最短数据删除。 若变量内容从尾向前的数据符合“关键字”,则将符合的最长数据删除。 |
${变量/旧字符串/新字符串} ${变量//旧字符串/新字符串} |
若变量内容符合“旧字符串”,则第一个旧字符串会被新字符串替换。 若变量内容符合“旧字符串”,则全部的旧字符串会被新字符串替换。 |
除了上述变量设置方式,还有如下用法:
${var:num}在var中提取第num个字符到末尾的所有字符。
若num为正数,从左边0处开始;若num为负数,从右边开始提取字串,但必须使用在冒号后面加空格或一个数字或整个num加上括号,如${var: -2}、${var:1-3}或${var:(-2)}。
${var:num1:num2}表示从$var字符串的第$num1个位置开始提取长度为$num2的子串。
num1是位置,num2是长度,不能为负数。
${#var}获取var变量的长度。
代码实例参考鸟哥第十章变量内容的删除与取代。
var=${str:=expr}
如果str为null或空字符串,则var的值为expr,并且expr赋值给str;如果str非空则var的值为$strvar=${str:?expr}
如果str为null或空字符串,则var的值为null,并且expr输出至stderr;如果str非空则var的值为$strvar=${str:-expr}
与第一个“=”的类似,只是expr不赋值给strvar=${str:+expr}
与上面“-”的相反
代码实例也参考鸟哥第十章变量内容的测试与替换或者这里。
与Python中所有其它东西一样,异常是一个对象。它由Python的raise语句产生,产生之后它会影响程序的正常执行过程,程序不会继续执行raise之后的语句,而是搜索当前的调用链来寻找可以处理该异常的异常处理程序。
下面列出Python中的部分异常:
BaseException
KeyboardInterrupt
Exception
ArithmeticError
ZeroDivisionError
AttributeError
EnvironmentError
IOError
OSError
WindowsError (Windows)
VMSError (VMS)
LookupError
IndexError
NameError
UnboundLocalError
ReferenceError
RuntimeError
NotImplementedError
SyntaxError
IndentationError
TabError
TypeError
ValueError
Warning
DeprecationWarning
SyntaxWarning
UserWarning
FutureWarning
ImportWarning
建议任何用户自定义的异常应该以Exception
为基类,而不是BaseException
,是因为当有如下的代码段时:
用户可以通过Ctrl-C终止try部分的代码执行,而不会触发异常处理程序,因为KeyboardInterrupt
异常并不是Exception
的子类。
很多异常由Python的built-in函数自动抛出,如除数为0等异常。
也可以通过raise语句抛出:
raise exception(args)
语句中exception是异常的类型(例如,NameError);args是一个异常参数值。该参数是可选的,如果不提供,异常的参数是”None”。例如:
>>> raise IndexError("Just kidding")
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
raise IndexError("Just kidding")
IndexError: Just kidding
Python中异常的捕获和处理一般形式如下:
try:
body
except exception_type1 as var1:
exception_code1
except exception_type2 as var2:
exception_code2
.
.
.
except:
default_exception_code
else:
else_body
finally:
finally_body
except 块的选择:
当一个类继承自Exception及其派生类,那么就是自定义的异常。例如:
class MyError(Exception):
pass
你可以像其它异常一样抛出、捕获并处理自定义的异常:
>>> raise MyError("Some information about what went wrong")
Traceback (most recent call last):
File "<pyshell#30>", line 1, in <module>
raise MyError("Some information about what went wrong")
MyError: Some information about what went wrong
如果在抛出异常是提供了多个参数,它将以元组的形式传递到异常处理程序,可以通过args变量访问它们:
try:
raise MyError("Some information", "my_filename", 3)
except MyError as error:
print("Situation: problem {0} with file {1}: {2}".format(
(error.args[2], error.args[1], error.args[0]))
]]>Python中的命名空间(namespace)是标识符到对象的映射,通常以字典的形式表现出来。变量的作用域(scope)是Python程序的文本区域,在该区域某个命名空间中的名字可以被直接引用。赋值(assignment)操作不会拷贝,只是把标识符和对象做一个绑定。
当一段代码在Python中执行时,它有三个命名空间:local
,global
和built-in
。在执行过程中遇到了某个标识符时,Python首先尝试在local
命名空间中查找它,如果没有找到,再在global
命名空间中查找,如果还是没有找到,接着在built-in
命名空间中查找。如果都不存在,则被认为是一个错误,会抛出一个“NameError”异常。如图所示:
{% asset_img 1.png “标识符在命名空间中的查找顺序”%}
命名空间都是有创建时间和生存期的。对于Python built-in names组成的命名空间,它在Python解释器启动的时候被创建,在解释器退出的时候才被删除;对于一个Python模块的global namespace,它在这个module被import的时候创建,在解释器退出的时候退出;对于一个函数的local namespace,它在函数每次被调用的时候创建,函数返回的时候被删除。下面看一个例子:
>>> def f(x):
print ("global: ",globals())
print ("Entry local: ",locals())
y = x
print ("Exit local: ",locals())
>>> z = 2
>>> globals()
{'__package__': None, 'f': <function f at 0x0000000000B76EA0>, '__doc__': None, '__name__': '__main__', 'z': 2, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__builtins__': <module 'builtins' (built-in)>}
>>> f(z)
global: {'__package__': None, 'f': <function f at 0x0000000000B76EA0>, '__doc__': None, '__name__': '__main__', 'z': 2, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__builtins__': <module 'builtins' (built-in)>}
Entry local: {'x': 2}
Exit local: {'y': 2, 'x': 2}
>>>
可以看到,刚进入函数f(x)时,只有参数x在f的local命名空间,之后y加入了f的local命名空间。f的global命名空间与我们交互会话的global命名空间相同,因为f是在交互会话时定义的。
再来看一个引入模块的例子,假如有如下的文件:
为了使结果更清晰,下面使用了keys()方法只显示出globals方法返回字典的keys(标识符):
>>> list(globals().keys())
['__name__', '__loader__', '__doc__', '__spec__', '__package__', '__builtins__']
>>> import scopetest
>>> z = 2
>>> list(globals().keys())
['__name__', '__loader__', '__doc__', 'z', '__spec__', '__package__', 'scopetest', '__builtins__']
>>> scopetest.f(z)
global: ['__name__', '__loader__', '__doc__', '__file__', '__spec__', '__package__', 'f', '__builtins__', '__cached__', 'v']
entry local: {'x': 2}
exit local: {'y': 2, 'w': 6, 'x': 2}
>>>
可见,在交互式会话中,随着模块的import和变量z的定义,交互会话的global命名空间增加了scopetest和z标识符。在scopetest模块的f函数的global命名空间中,包含了方法f和v(但是并没有交互会话中的z)。
[总结]一个模块的引入,函数的调用,类的定义都会引入命名空间;函数中的再定义函数,类中的成员函数定义会在局部namespace中再次引入局部namespace。另外,赋值语句是起一个绑定或重绑定的作用(bind or rebind)。函数调用的参数传递是赋值,不是拷贝。
global语句用来声明一系列变量,这些变量会引用到当前模块的全局命名空间的变量(module-level namespace),如果该变量没有定义,也会在全局空间中添加这个变量。
global var1, var2
nonlocal语句(nonlocal是Python3.2引入的)
Python2.7中还没有nonlocal语句。nonlocal语句用来声明一系列的变量,这个声明会从声明处从里到外的namespace去搜寻这个变量(the nearest enclosing scope),直到模块的全局域(不包括全局域),找到了则引用这个命名空间的这个名字和对象,若作赋值操作,则直接改变外层域中的这个名字的绑定。nonlocal语句声明的变量不会 在当前scope的namespace字典中加入一个key-value对,如果在外层域中没有找到,则如下报错。
>>> SyntaxError: no binding for nonlocal 'spam' found
一个nonlocal和global的测试例子:
第5行的语句:nonlocal spam 没有在函数do_nonlocal()的域中创建一个变量,而是去引用到了外层的,10行定义的spam。
第8行的global spam,在全局域中创建了一个name,9行将其绑定在字符串常量对象”global spam”上。
本文参考了http://www.cnblogs.com/livingintruth/p/3296010.html 这篇文章及“The Quick Python Book Second Edition”。
]]>如果要编写较复杂的带有较多命令行参数选项的bash脚本,通过bash内置的getopts命令进行处理较为方便。通过其与while循环配合,可以处理以“-”指定的参数。
在编写bash脚本时,经常需要获取到脚本后面的参数。bash提供了几个变量来表示位置参数,如下:
如果脚本后面参数选项较复杂时,单单使用上述变量并不十分方便,于是通过while循环与getopts及其提供的OPTARG与OPTIND变量相配合,可以十分方便得处理命令行参数。其一般的形式类似于如下:
getopts options variable
options
为命令行参数列表,每个字母代表一个选项,字母后面如果带“:”意味着选项除了定义本身之外,还会带上一个参数作为选项的值。如果命令行中包含了没有在getopts列表中的选项,会有警告信息,如果在整个options
字符串最前面也加上个“:”,就能消除警告信息了。variable
是当命令行参数是参数列表中的字母时,getopts将匹配的命令行参数字母保存在variable
变量中,并且返回命令执行成功的状态值(就是0值)。如果命令行参数与参数列表中的字母不匹配,getopts将保存一个“?”到variable
变量中,也返回一个命令执行成功的状态值。OPTARG
和OPTIND
。OPTARG
用来取当前选项的值,OPTIND
代表当前选项在参数列表中的位移。下面看一个简单的例子:
下面通过不同的命令行选项调用该脚本:
[yangdong@centos7-A ~]$ ./getopts.sh
OPTIND starts at 1
[yangdong@centos7-A ~]$ ./getopts.sh -a
OPTIND starts at 1
Option a is specified
OPTIND is now 2
[yangdong@centos7-A ~]$ ./getopts.sh -b
OPTIND starts at 1
./getopts.sh: 选项需要一个参数 -- b
Unknown option
OPTIND is now 2
[yangdong@centos7-A ~]$ ./getopts.sh -b optb
OPTIND starts at 1
Option b has value optb
OPTIND is now 3
[yangdong@centos7-A ~]$ ./getopts.sh -b optb -a
OPTIND starts at 1
Option b has value optb
OPTIND is now 3
Option a is specified
OPTIND is now 4
[yangdong@centos7-A ~]$ ./getopts.sh -c
OPTIND starts at 1
./getopts.sh: 非法选项 -- c
Unknown option
OPTIND is now 2
[yangdong@centos7-A ~]$ ./getopts.sh -ab optab
OPTIND starts at 1
Option a is specified
OPTIND is now 1
Option b has value optab
OPTIND is now 3
使用getopts处理参数虽然是方便,但仍然有几个小小的局限:
Salt-Formulas是一些预先写好的Salt States,可以通过check out官方的formula,来进行软件的安装,配置,服务的管理等任务。本篇我们通过使用官方的zabbix-formula,来进行zabbix的安装和配置,同时简单学习一下Salt Formulas的使用。
zabbix是一个开源的系统及网络监视软件,可以对各种服务器、操作系统、网络设备等进行监控。Zabbix目前提供了自己的安装源,可以通过操作系统的包管理器进行安装。salt是一个配置管理工具,可以批量对各种操作系统及设备进行管理。之前的文章介绍过salt的安装配置,可以参考文章“salt的安装和配置”或“通过salt cloud在云端批量部署计算节点”。
这里使用的环境如下:
机器名:centos7-A, IP:192.168.71.167, 将安装zabbix-server和zabbix-agent
机器名:centos7-B, IP:192.138.71.168, 将安装zabbix-agent
其中centos7-A也是salt的master所在的机器。
在centos7-A,也就是salt master机器上,进行如下操作:
克隆或下载zabbix-formula,mysql-formula,apache-formula存储库到某个目录中:
|
|
将新添加的目录加入到master配置文件中的file_roots项中:
|
|
重启salt master。
根据SALT FORMULAS的官方文档,在完成手动添加formula目录后,formula应该提供一些默认的配置而立即可用。如果需要进一步的配置,大部分的formulas可以通过Pillar数据进行配置,可以参考每个Formula仓库的pillar.example
文件查询可用的配置。然而对于此zabbix-formula来说,它依赖于配置好的mysql和apache的formulas,所以在上一步操作时,也下载了上述的formulas。
这里我们先不配置任何Pillar数据,看看此formula提供的一些默认state是怎样的。使用state.show_sls可以查看某个state而不用实际执行,例如:
上面省略了大部分的输出,主要看其baseurl的输出,为2.2版。如果我们想安装2.4版的zabbix,可以通过配置Pillar数据来实现。根据pillar.example文件,配置如下:
刷新pillar数据后,再次使用state.show_sls查看zabbix.repo的state,如下:
根据baseurl的输出,可以看出repo换为2.4版了。所以,几乎所有的配置都可以通过pillar来配置。
这里给出在centos7下的最终的pillar数据,大部分选项保持默认:
上面的配置在centos7环境下,如果不加pidfile的路径,启动zabbix-agent会报错。
可以通过top文件来指定哪个minion可以应用哪个state。我们需要在centos7-A机器上安装zabbix-server,zabbix-frontend和zabbix-agent,在centos7-B机器上只安装zabbix-agent,所以我们在写top配置文件时,在A机器上安装所有组件,在B机器上只安装agent。top文件配置如下:
由于此formula依赖于mysql-formula,而mysql-formula默认将本地root用户设定了密码,所以要通过salt对mysql进行配置的话,需要将一些连接信息加入到数据库所在机器的/etc/salt/minion文件中:
其中root的密码是通过命令salt 'centos7-A' state.show_sls mysql.server
查询到的。
另外,在执行zabbix.mysql.schema
的state过程中,在centos7环境下,没有名为“mysql-client”的软件包,导致此state执行失败(Issues),临时的解决方法参考这里。
在上述配置都完成后,可以通过命令salt '*' state.highstate test=True
先测试一下,然后去掉test=True
参数将整个top文件都应用到对应的机器上。
在执行highstate命令时,由于环境配置的不同,很可能不能一次成功,根据失败的输出,进行相应配置文件的调整,然后再次执行,直到命令成功为止。
上面highstate执行成功后,便可以登录zabbix的web页面了。只是登录后,页面最上方会出现需要设置timezone的警告。这个配置没有在zabbix-formula中找到,可以手动修改zabbix.conf
文件(文件位置在/etc/httpd/conf.d
文件夹下),也可以通过salt的file.managed模块进行配置。只需要将php_value date.timezone Asia/Shanghai
前面的注释去掉,然后写入希望配置的时区即可。
本篇通过zabbix-formula学习了salt-formulas的使用。由于formulas还在不断得开发,里面的有些formula还不太完善。例如nginx-formula就还没有加入对centos7的支持,导致在centos7环境下,使用它就出现报错。不过,对于学习如何使用salt进行系统管理,formulas还是提供了一些指导的作用。
]]>在Python中,通过multiprocessing模块内的Process类来创建新的进程。例如:
通过Process类中的start方法来启动新的进程。有三种不同的启动方式:spawn,fork和forkserver。这三种方式的详细区别请参考官方文档。
Process类中的join方法的作用是等待子进程结束后再继续往下执行。
multiprocessing模块内包括两种进程间通信的方式:Queues和Pipes;同时也提供了其它方式在进程间共享状态,不过官方文档也提到在并发程序设计中尽量避免这种方式。
先来看一下Queues的例子:
multiprocessing模块中的Queue几乎是queue.Queue的克隆,并且Queues是线程和进程安全的,这意味着多个线程或进程访问同一个Queue实例时,不会出现数据混乱的状况。下面来看一个使用Pipe的例子:
Pipe方法返回一对连接对象,代表管道的两端,每个连接对象都有send()和recv()方法。如果不同的进程(或线程)在同一时刻试图在管道的同一端进行读写,数据可能会出现混乱,在不同端是没有问题的。Python中的对象大部分都不是进程(或线程)安全的,所以在不同的进程(或线程)操作同一对象时,需要使用同步机制。
进程的同步是通过Lock实现的。实例如下:
Pool类提供了一个批量创建多个子进程的方法,可以给定子进程数的上限,避免无限地消耗系统的资源。看一下官方文档的实例:
Python中使用线程的方式有很多都与使用进程的方式类似,这里就不再列出实例代码。
在Python中,通过threading模块内的Thread类来创建新的进程。通过Thread类中的start方法启动线程,join方法等待线程结束。
线程间最简单的通信方式是通过threading模块内的Event类。当然也可以通过共享对象来在线程间共享状态信息。由于queue.Queue是线程和进程安全的,所以它也是线程通信使用理想对象。其它对象大部分都不是线程(或进程)安全的,所以在不同的线程(或进程)操作同一对象时,需要使用同步机制。
threading模块中提供了Lock,RLock,Condition,Semaphore等类进行线程间的同步。
关于threading模块中更详细的使用信息,请参考官方文档。
]]>Python允许我们使用它的APIs编写多线程或多进程应用。本篇就来了解一下Python多线程与多进程的基本概念,下篇了解一下实际的代码实例。
Python的多线程或多进程的调度是通过操作系统的调度程序实现的。当一个线程或进程阻塞,例如等待IO时,该线程或进程被操作系统降低执行的优先级,所以CPU内核可以被分配用来执行其它有实际计算任务的线程或进程。
下图描述了Python的线程或进程架构:
线程存在于进程之内。一个进程可以包含多个线程,但通常包含至少一个线程,这个线程被称为主线程。在一个进程内的线程共享进程的内存,所以进程内的不同线程的通信可以通过引用共享的对象来实现。不同的进程并不共享同一块内存,所以进程间的通信是通过其它接口如文件、sockets或特别分配的共享内存区域来实现的。
当线程需要执行操作时,它请求操作系统的线程调度程序给其分配一些CPU时间。调度程序根据各种参数来将CPU的核心分配给等待的线程,调度程序的实现根据操作系统的不同而不同。同一个进程中运行的不同线程可能同时运行在不同的CPU核心上(但CPython例外)。
Python的CPython解释器(从www.python.org下载的标准版解释器)包含一个Global Interpreter Lock(GIL),它的存在确保了Python进程中同时只有一个线程可以执行,即使有多个CPU核心可用。所以CPython程序中的多线程并不能通过多个cpu核心并行执行。不过,即使是这样,在等待I/O时被阻塞的线程仍然被操作系统降低执行优先级并放入背景等待,以便让真正有计算任务的线程可以执行,下图简单地描述了这个过程:
上图中的Waiting for GIL状态是某个线程已经完成了I/O,在退出阻塞状态想要开始执行时,另外一个线程持有GIL,所以已经就绪的线程被强制等待。在很多的网络应用程序中,花在等待I/O上的时间比实际处理数据的时间要多得多。只要不是有非常大的并发连接数,由GIL导致的连接的线程的阻塞是相对较低的,所以对于这些网络服务程序,使用线程的方式实现并发连接,仍然是一个合适的架构。
对于操作系统来说,一个应用就是一个进程。比如打开一个浏览器,它是一个进程;打开一个记事本,它是一个进程。每个进程有它特定的进程号。他们共享操作系统的内存资源。进程是操作系统分配资源的最小单位。
而对于每一个进程而言,比如一个视频播放器,它必须同时播放视频和音频,就至少需要同时运行两个“子任务”,进程内的这些子任务就是通过线程来完成。线程是最小的执行单元。一个进程它可以包含多个线程,这些线程相互独立,同时又共享进程所拥有的资源。
以使用OS资源的角度来说,进程比线程更加“重量级”,创建一个新的进程需要的时间比创建一个新线程来说要多,而且进程使用更多的内存资源。
有一点需要注意的是,如果你需要执行一个计算任务密集的Python程序,最好是通过多进程来实现。因为如果程序中的每个线程都有繁重的计算任务,它们都要使用CPU,而由于GIL的存在,这些线程并不能真正的在不同的CPU核心上并行执行,所以这会严重降低程序整体的性能。
本篇了解了Python中多线程和多进程的基本概念,下篇看看实际的代码实例。
]]>任何编程语言中的网络编程都可以从sockets开始。Sockets是什么?简单来说,一个socket是一个可以使应用程序进行网络或进程间通信的虚拟终端。例如某台计算机上的进程可以通过socket与同一台或另外一台主机上的进程进行通信。通常这个发起通信的进程称为客户端,而接受连接请求的进程为服务端。
Python有非常简单的方式来使用socket接口。通过下面的图片,展示了一个客户端/服务端通信的流程,可以从下图了解到如何使用socket API:
有了这张图片,就大概概括了客户端/服务端通过socket进行通信所使用的函数。下表给出了具体的函数参数:
函数 | 描述 |
---|---|
服务器端套接字 | |
s.bind(address) | 绑定地址(host,port)到套接字, 在AF_INET 下,以元组(host,port)的形式表示地址 |
s.listen(backlog) | 开始 TCP 监听,backlog 指定在拒绝连接之前,操作系统可以挂起的最大连接数量,该值至少为1,大部分应用程序设为5就可以了 |
s.accept() | 被动接受 TCP 客户端连接,(阻塞式)等待连接的到来 |
客户端套接字 | |
s.connect(address) | 主动初始化 TCP 服务器连接,一般 address 的格式为元组(hostname,port),如果连接出错,返回 socket.error 错误 |
s.connect_ex(address) | connect() 函数的扩展版本,出错时返回出错码,而不是抛出异常 |
公共用途的套接字函数 | |
s.recv(bufsize[,flag]) | 接收 TCP 数据,数据以字符串形式返回,bufsize 指定要接收的最大数据量,flag 提供有关消息的其他信息,通常可以忽略 |
s.send(string[,flag]) | 发送TCP数据,将 string 中的数据发送到连接的套接字 ,返回值是要发送的字节数量,该数量可能小于 string 的字节大小 |
s.sendall(string[,flag]) | 完整发送 TCP 数据,完整发送 TCP 数据,将 string 中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据,成功返回None,失败则抛出异常, |
s.recvform(bufsize[,flag]) | 接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址 |
s.sendto() | 发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数 |
s.close() | 关闭套接字 |
s.getpeername() | 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port) |
s.getsockname() | 返回套接字自己的地址。通常是一个元组(ipaddr,port) |
s.setsockopt(level,optname,value) | 设置给定套接字选项的值 |
s.getsockopt(level,optname[.buflen]) | 返回套接字选项的值 |
s.settimeout(timeout) | 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect()) |
s.gettimeout() | 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。 |
s.fileno() | 返回套接字的文件描述符。 |
s.setblocking(flag) | 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常 |
s.makefile() | 创建一个与该套接字相关连的文件 |
在开发Python程序时,往往需要在同一主机上安装不同版本的Python来测试程序。pyenv可以满足这种需求。它可以在单主机上安装多个版本的Python,并且可以在这些版本中自由切换。
有多种方式可以安装pyenv,这里介绍两种较简便的方式:
可以通过Homebrew安装:
可以通过automatic installer进行安装:
首先,通过您使用的Linux发行版的包管理器安装git,然后通过如下命令安装pyenv:
$ curl -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash
安装完成后,可以通过pyenv version
命令测试安装是否正常,如果输出如下,便说明安装正常:
system (set by /home/your_username/.pyenv/version)
完整的用法请参考Command Reference。
这里给出几个pyenv使用的例子:
首先,安装完pyenv后,想通过其安装不同版本的Python解释器,可以先查看pyenv支持管理的Python版本,包括Anaconda, Jython, pypy, and stackless等,通过如下命令:
$ pyenv install --list
然后安装某个特定的Python版本,可以通过如下命令:
$ pyenv install 2.7.6
$ pyenv install 2.6.8
上面的命令最后一个参数可以是pyenv install --list
命令输出的任何版本。上面的命令pyenv首先会连接Python的官网来下载相应版本的源代码,然后其在本地进行编译安装。所以在使用pyenv install 2.7.6
命令之前,需要确保本机安装了编译工具包(redhat系列发行版可以通过sudo yum groupinstall development
命令安装)。
然而,从官网下载源码包实在是太慢了!可以通过国内的镜像将Python源码包下载到本地,然后将其保存到$PYENV_ROOT/cache/
目录(如果该目录不存在需要先创建),下面是一个这种方式的例子:
wget http://mirrors.sohu.com/python/3.5.2/Python-3.5.2.tar.xz -P ~/.pyenv/cache/ && pyenv install -v 3.5.2
上面的命令先通过wget在sohu的镜像下载源码,保存到$PYENV_ROOT/cache/中,然后执行安装,-v参数显示安装过程的详细信息。也可以通过设置PYTHON_BUILD_MIRROR_URL环境变量的方式来设置镜像。
如果以上的方法都不好用,可以自己通过源码编译,确保使用了–prefix=$(pyenv root)/versions/3.1.5的编译选项,将编译后的可执行文件安装到version目录下的版本号命名的目录(这里是3.1.5),这样便可以通过pyenv便可以将该版本也纳入管理。
在安装完成新版本的Python之后,需要运行:
$ pyenv rehash
进入某个目录后,执行pyenv local 2.7.6
命令,便可以指定进入当前目录后使用的某个Python版本。如果不加最后的参数,是列出该目录中使用的Python版本。
在任何位置执行pyenv shell 2.7.6
命令,便指定了当前shell所使用的Python版本,它会覆盖通过pyenv local x.x.x
命令指定的某目录使用的Python版本。
如果不加最后的参数,也是列出当前shell使用的Python版本。
命令pyenv versions
列出本机可以使用的Python版本,前面的*表示当前使用的版本:
Python的lambda函数也叫匿名函数,即,函数没有具体的名称。它可以使代码看起来更简洁,更易理解。
lambda函数也就是匿名函数可以通过如下的表达式定义:
其中args是以逗号分隔的参数列表,expression是包含那些参数的表达式。例如:
lambda函数是一个表达式。多个语句和其它非表达式的语句如for或while等,不能出现在lambda表达式中。lambda主要用来指定小的回调函数。例如,如果想要对一个名称列表进行非大小写敏感的排序,可以这样写:
在浏览开源社区的代码时,有时候会看到在调用super函数时有的有参数,有的没有参数。其实这只是Python2和Python3不同的语法而已。Python3允许使用不带参数的super函数,而Python2的super函数中需要带有参数。
在Python3的官方文档中也说明了这一点。拿文档中的例子来看:
如果是Python2的话,必须使用super(C, self).method(arg)
这种语法。
Python中的tuples(元组)是经常用来表示简单的数据结构,但它只能通过下标来访问其中的数据,这导致代码难于阅读和维护。Python的collections模块包含一个namedtuple()函数,用来创建一个tuple的子类,其可以通过属性名称访问tuple中的元素。
下面的代码片段使用了namedtuple()函数:
虽然namedtuple看起来像一个普通的类实例(如type的输出所示),但它是普通的tuple的子类,支持所有普通tuple的操作,而且增加了通过使用属性名访问其中数据的功能。
|
|
可以定义为:
以上两个版本可以通过类似的方式访问其中的数据。然而使用namedtuple更加节省内存,同时也支持所有的元组操作,如unpacking等。但使用namedtuple访问属性值时,不如通过类那样高效。例如如果s是namedtuple实例而不是普通的类实例时,访问s.shares需要几乎两倍的时间。所以,如果你的目标是定义一个需要更新很多实例属性的高效数据结构,那么命名元组并不是你的最佳选择。
|
|
如果你真的需要改变属性的值,那么可以使用命名元组实例的 _replace() 方法, 它会创建一个全新的命名元组并将对应的字段用新的值取代。比如:
各种语言都有它自己特定的函数参数定义方法。Python对于函数参数的定义非常灵活,它提供了三种定义函数参数的方式。
这是Python中最简单的给函数传递参数的方式。当调用函数时,调用代码中的函数参数与函数定义时的参数的顺序一致。例如:
上述代码中,调用函数时3->x,4->y。也可以在定义函数时,给函数参数指定默认值,类似于下面这样:
包含有默认值的函数参数,必须在函数参数列表的最后面的位置。如果上面的函数定义中arg1也包含默认值的话,所有后面的参数都应该包含默认值。
在调用时,如果没有指定带默认值的函数参数,则使用参数中的默认值。例如调用上面的函数时,通过fun(1)这种方式调用时,1->arg1,arg2=default2,arg3=default3;通过fun(1,2,3)这种方式调用时,1->arg1,2->arg1,3->arg3。
可以在调用函数时,使用相应函数参数的名称来传递参数。例如还是上面power函数的定义,在调用时,通过如下方式:
上面的函数调用,因为使用了函数参数的名称来传递参数,所以参数的位置便无关紧要了。
Python的函数也可以处理可变数量的函数参数。可以有两种方式:
在函数的参数列表中,最后一个参数前添加一个*
号,便可以将传入该函数的多余的参数存在以该参数命名的元组中。例如,有如下函数定义:
通过如下方式调用:
在函数的参数列表中,最后一个参数前添加一个**
号,便可以将传入该函数的多余的关键字参数存在以该参数命名的字典中。例如,有如下函数定义:
通过如下方式调用:
可以混合上面的3种方式定义函数,只要可变长度的函数参数定义在函数参数列表的最后。例如:
如果想要通过Python写出一个基于命令行的程序,如果程序的命令行参数较多,可以使用Python内置的argparse模块来处理命令行参数。
可以通过一个实例程序来探索argparse的用法:
可以通过如下的方式使用上面定义的脚本文件:
为了解析命令行选项,你首先要创建一个ArgumentParser
实例, 并使用add_argument()
方法声明你想要支持的选项。 在每个add_argument()
调用中,dest
参数指定将解析结果存入以该参数命名的属性中。metavar
参数被用来生成帮助信息。action
参数指定跟属性对应的处理逻辑,通常的值为store
,被用来存储某个值或将多个参数值收集到一个列表中。 更详细的add_argument()的参数解释请参考官网(或中文翻译)。
在设置完参数后,便可以执行parser.parse()
方法了。它会处理sys.argv
的值并返回一个结果实例。每个参数值都会被设置成该实例中add_argument()
方法的dest
参数指定的属性值。
还很多种其他方法解析命令行选项。 例如,你可能会手动的处理 sys.argv 或者使用 getopt 模块。 但是,如果你采用本节的方式,将会减少很多冗余代码,底层细节 argparse 模块已经帮你处理了。 你可能还会碰到使用 optparse 库解析选项的代码。 尽管 optparse 和 argparse 很像,但是后者更先进,因此在新的程序中你应该使用它。
]]>unittest测试框架是Python的内置模块。它与其它语言的单元测试框架如JUnit类似。本文简单看一下此模块的使用。
unittest模块一般的使用流程如下:
下面看一个基本的实例:
使用python test.py运行该文件后,结果如下:
如果在测试脚本后面加入-v参数,如python test.py -v,则会显示更详细的测试信息:
如果在一个文件夹中有多个测试源文件,要运行所有的这些源文件,对于每个文件都运行一个命令是非常烦人的。这时候使用test discovery就非常方便了。假设所有的测试源代码文件都保存在tests文件夹中,则只要运行python -m unittest discover tests
就可以将该文件夹中所有的测试文件都运行一遍。
Linux From Scratch旨在帮助用户更好得理解Linux如何正常运转,里面的程序如何协同工作以及程序之间如何相互依赖的。了解 Linux 系统如何工作的关键就是知道每个软件包的作用以及为什么你(或系统)需要它。本文总结了构建LFS 7.7-systemd的注意事项,而不是对构建过程中使用的命令进行记录。
要进行LFS的构建,用户需要对Linux系统有一定的了解,不然会在构建过程中遇到各种各样的问题。
构建LFS 7.7-systemd的宿主系统需要满足一定需求,我构建的过程使用的宿主机是CentOS7(内核版本3.10.0-327.36.3)的VMware Workstation 12.0虚拟机,使用version-check.sh脚本检查后,建立了指向/usr/bin/bison的名为/usr/bin/yacc的软链接,其它工具基本满足构建所需。
在分区时,根据书中第二章的要求在虚拟机上添加了一个的硬盘,VMware虚拟机添加硬盘时默认为SCSI硬盘,根据第八章编译内核使用的默认选项和GRUB的配置,不能正常启动系统,主要的原因可能是内核编译参数的配置没有对或缺少某些主要驱动程序所导致的。后面再来看解决方法。
分区完成后,在新的分区创建了ext4类型的文件系统。然后挂载新分区,设置$LFS变量。然后根据第三章下载软件包和补丁。
第四章在宿主机添加了lfs普通用户以降低编译过程中可能导致的风险,对该用户的环境进行了设置,主要目的是设置该用户独有的环境变量,不使用宿主机原有的环境变量。
第五章构建了用于构建LFS系统使用的工具链。其中的Binutils和GCC都构建了两遍,这主要是为了解决循环依赖的问题:因为需要一个编译器来编译一个编译器。
构建过程的第一步是构建一个宿主系统无关的新工具链,它是通过先安装交叉编译的Binutils和GCC,使用它们编译glibc等库,然后第二遍编译Binutils和GCC,这次它们被链接到刚才编译好的glibc等库上。这样与宿主系统无关的新工具链就构建成功了。其中5.2节工具链技术备注解释了一些基本原理和技术细节,需要重点理解。
第二步使用这个第二遍编译好的工具链,构建其它剩下的基础工具。
第五章编译的工具都安装在$LFS/tools目录中,这些工具可以用来构建第六章的正式的LFS系统。
第六章的前几节目的是准备一个虚拟环境,这个环境通过执行chroot命令进入,从而可以方便地在宿主机和LFS虚拟chroot环境中进行切换。同时本章也介绍了各种不同的包管理器的实现。后面很大的篇幅是具体编译每个软件使用的命令和每个软件包包含的大概内容。最后本章介绍了如何剥离可执行程序中的调试符号,从而使可执行文件的大小减小很多。
第七章对系统进行了简单的配置,先是网络包括主机名,ip地址,主机名,DNS解析,hosts表等。之后介绍了系统设备的管理(由于这部分比较接近于硬件,我对于这部分的理解还很浅),配置了时间区域等内容,然后创建了/etc/inputrc和/etc/shells文件。
第八章首先创建了/etc/fstab文件,系统启动后会根据此文件的内容挂载根文件系统。然后就是编译内核和配置GRUB了。根据书中make defconfig
命令创建了默认的.config文件,选择了书中建议的内核选项后,系统仍然无法启动,无法挂载根文件系统。
由于宿主机的内核可以正常启动虚拟机,所以该内核中包含的驱动可以识别虚拟机的SCSI硬盘,于是便想到直接使用宿主机的内核来启动系统。宿主机的/boot分区是独立的。此分区只包含内核及GRUB相关的文件,可以修改GRUB的配置,待内核启动完成后挂载LFS的分区即可,下面是操作过程:
上面的第2、3行命令分别复制当前宿主机的内核和map文件,重命名用于我们的LFS系统;
第4行命令使用CentOS系统自带的dracut命令制作initramfs文件,在此文件中包含可以识别ext4文件系统的模块,以便能正确读出LFS硬盘分区上的文件。dracut命令的详细用法可以参考鸟哥的私房菜;
第6行命令将与宿主机内核相关的模块也全部复制到LFS分区上。
最后是修改grub.cfg文件,来添加一个启动菜单选项,来使用刚才我们复制的内核文件及initramfs文件。详细的GRUB2的设定也参考鸟哥grub2设定,可以先使用grub2-mkconfig
命令生成新的grub.cfg文件,然后修改包含刚才复制的内核文件名称的菜单项,最后的菜单项为:
另外,也可以按照LFS8.3.1的建议,可以拷贝主机系统的内核配置文件 .config(如果有的话)到解压后的 linux-3.19 目录下来跳过内核配置(使用make oldconfig
命令),再编译内核。不过最后仍然需要制作initramfs文件来辅助启动。如何编译内核可以参考鸟哥核心编译一章。
最后,别忘记根据第九章的说明,配置/etc/os-release
文件,缺少了该文件,系统仍然无法正常启动!经过以上操作后,LFS系统便可以通过与宿主机完全相同的内核来进行启动,不会出现无法识别硬盘,无法识别ext4分区的现象。
在Docker版本1.12之后,Swarm集成进了Docker引擎,而跨主机的容器网络也有所发展。在Docker较老的版本中,要实现跨主机的容器网络可以使用第三方的基于SDN的解决方案,而最新版的Docker引擎在swarm模式下原生支持overlay networks,本篇就来实践一下。
在Docker1.12版之后,Docker引擎原生支持跨主机的容器网络,但创建跨主机的容器网络时,需要满足一些前提条件:
以上两个条件满足任意一个即可以创建跨主机的容器网络。本篇以第一个条件为前提创建跨主机的容器网络。如果想了解基于key-value存储系统的跨主机网络,可以参考文章“理解Docker跨多主机容器网络”或官方文档。
通过在Swarm模式的Docker引擎创建的跨主机容器网络,并不需要外部的key-value存储系统,这种swarm模式的overlay networks包含以下特性:
下面就在swarm集群中实际创建一个overlay network。这里使用文章“新版Docker的Swarm模式”中创建的Swarm集群。
可以通过如下命令在manager节点上创建overlay网络:
[yangdong@centos7 ~]$ docker network create \
> --driver overlay \
> --subnet 10.0.9.0/24 \
> my-network
5fke0hya379rbmfhdemevngjx
--subnet
命令行参数指定overlay网络使用的子网网段。创建完成后使用docker network ls
查看创建的网络:
[yangdong@centos7 ~]$ docker network ls
NETWORK ID NAME DRIVER SCOPE
2a820cde1d0c bridge bridge local
...
5fke0hya379r my-network overlay swarm
在将服务连接到这个创建的网络之前,网络覆盖到manager节点。上面输出的SCOPE为swarm
表示将服务部署到Swarm时可以使用此网络。在将服务连接到这个网络后,Swarm只将该网络扩展到特定的worker节点,这个worker节点被swarm调度器分配了运行服务的任务。在那些没有运行该服务任务的worker节点上,网络并不扩展到该节点。
在创建服务时可以通过--network
参数将服务连接到某个网络:
[yangdong@centos7 ~]$ docker service create \
> --replicas 3 \
> --name my-web \
> --network my-network \
> nginx
51fcns6k7bqknqnmsjbyo0unf
上面名为“my-web”的服务启动了3个task,用于运行每个任务的容器都可以彼此通过overlay网络进行通信。Swarm集群将网络扩展到所有任务处于Running状态的节点上。
在manager节点上,通过下面的命令查看哪些节点有处于running状态的任务:
[yangdong@centos7 ~]$ docker service ps my-web
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
78s6t4si84br75njmc79zux54 my-web.1 nginx centos7-A Running Running about a minute ago
99ogxe6mcc3v30e3ugtx53qg7 my-web.2 nginx centos7-B Running Running about a minute ago
ag94ocnqubfp4naf6v1utqbog my-web.3 nginx centos7 Running Running about a minute ago
可见三个节点都有处于running状态的任务,所以my-network网络扩展到三个节点上。
可以查询某个节点上关于my-network的详细信息:
[yangdong@centos7 ~]$ docker network inspect my-network
[
{
"Name": "my-network",
"Id": "5fke0hya379rbmfhdemevngjx",
"Scope": "swarm",
"Driver": "overlay",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "10.0.9.0/24",
"Gateway": "10.0.9.1"
}
]
},
"Internal": false,
"Containers": {
"f6cb2bc95344e74d2790403ae1072c14cd696247ae62482b7cbf198621b39456": {
"Name": "my-web.3.ag94ocnqubfp4naf6v1utqbog",
"EndpointID": "020adbf9337f08fdf31b1e6282a7e28599e60f93d582ebe101a17f7c4eace2e2",
"MacAddress": "02:42:0a:00:09:05",
"IPv4Address": "10.0.9.5/24",
"IPv6Address": ""
}
},
"Options": {
"com.docker.network.driver.overlay.vxlanid_list": "257"
},
"Labels": {}
}
]
从上面的信息可以看出在节点centos7上,名为my-web的服务有一个名为my-web.3.ag94ocnqubfp4naf6v1utqbog的task连接到名为my-network的网络上。
可以通过查询服务来获得服务的虚拟IP地址,如下:
[yangdong@centos7 ~]$ docker service inspect \
> --format='{{json .Endpoint.VirtualIPs}}' \
> my-web
[{"NetworkID":"5fke0hya379rbmfhdemevngjx","Addr":"10.0.9.2/24"}]
整个网络结构如下图所示:
加入my-network网络的容器彼此之间可以通过IP地址通信,也可以通过名称通信。
默认情况下,当创建了一个服务并连接到某个网络后,swarm会为该服务分配一个VIP。此VIP根据服务名映射到DNS。在网络上的容器共享该服务的DNS映射,所以网络上的任意容器可以通过服务名访问服务。
在同一overlay网络中,不用通过端口映射来使某个服务可以被其它服务访问。Swarm内部的负载均衡器自动将请求发送到服务的VIP上,然后分发到所有的active的task上。
下面的实例展示了在同一个网络中添加了一个busybox服务,此服务可以通过名称my-web访问前面创建的nginx服务:
在manager节点上,部署一个busybox服务到与my-web服务相同的网络中,也就是my-network中:
|
|
查询busybox运行在哪个节点上:
|
|
登录busybox运行的节点,打开busybox的交互shell:
|
|
从busybox容器内部,查询DNS来查看my-web的VIP:
|
|
从busybox容器内部,使用特殊查询
|
|
从busybox容器内部,通过wget来访问my-web服务中运行的nginx网页服务器
|
|
Swarm的负载均衡器自动将HTTP请求路由到VIP上,然后到一个active的task容器上。它根据round-robin选择算法将后续的请求分发到另一个active的task上。
在创建服务时,可以配置服务直接使用DNS round-robin而无需使用VIP。这是通过在创建服务时指定--endpoint-mode dnsrr
命令行参数实现的。当你想要使用自己的负载均衡器时可以使用这种方式。
下面的例子创建了一个dnsrr终端模式的服务:
[yangdong@centos7 ~]$ docker service create \
> --replicas 3 \
> --name my-dnsrr-service \
> --network my-network \
> --endpoint-mode dnsrr \
> nginx
3uk1nzvd2iwg87evkcu8z1y90
当通过服务名称查询DNS时,DNS服务返回所有任务容器的IP地址:
/ # nslookup my-dnsrr-service
Server: 127.0.0.11
Address 1: 127.0.0.11
Name: my-dnsrr-service
Address 1: 10.0.9.10 my-dnsrr-service.3.0sm1n9o8hygzarv5t5eq46okn.my-network
Address 2: 10.0.9.9 my-dnsrr-service.2.b3o1uoa8m003b2kk0ytl9lawh.my-network
Address 3: 10.0.9.8 my-dnsrr-service.1.55za4c83jq9846rle6eigiq15.my-network
通常Docker官方推荐使用dig,nslookup或其它DNS查询工具来查询通过DNS对服务名的访问。因为VIP是逻辑IP,ping并不是确认VIP连通性的正确的工具。
本篇文章的大部分内容翻译自官方文档,如有不当的地方欢迎批评指正。
]]>Docker Swarm是原生的Docker集群工具。在Docker版本1.12之后,Docker引擎加入了Swarm模式,将Swarm集成进来。大多数用户应该使用集成进Docker引擎的Swarm模式。单独的Docker Swarm仍然可用,只是它没有集成进Docker引擎的API和CLI命令当中。
在文章“使用Swarm进行Docker集群的部署”中,我们探索了如何通过Docker Swarm镜像部署Docker集群。随着Docker的不断开发,在版本1.12之后,Docker引擎加入了Swarm模式,用户可以通过原生的Docker CLI命令操作Swarm集群。本篇就对这种Swarm模式进行简单的概览。更详细的信息可以参考官方文档。
本篇使用的环境包括3个节点,一个作为Swarm的manager节点,两个为worker节点,机器名和IP地址如下:
centos7: 192.168.71.131 (Swarm manager)
centos7-A: 192.168.71.167 (Swarm worker)
centos7-B: 192.168.71.168 (Swarm worker)
在三台主机上,都安装了Docker引擎1.12版,并且主机相互之间可以ping通,主机上的2377,7946,4789端口都打开。
下面开始创建swarm。登录到centos7主机上,执行如下命令:
[yangdong@centos7 ~]$ docker swarm init --advertise-addr 192.168.71.131
Swarm initialized: current node (41atspd62he1vshs4jmhpyufj) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join \
--token SWMTKN-1-49ueborzkg0v6l3xu2g1d5zfgsjn1xobqvctwozq14m07n1ak0-2llwi551ii09zeyus5r3zi3un \
192.168.71.131:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
--advertise-addr
参数表示其它swarm中的worker节点使用此ip地址与manager联系。命令的输出包含了其它节点如何加入集群的命令。
使用docker info
和docker node ls
查看集群中的相关信息:
[yangdong@centos7 ~]$ docker info
...
Swarm: active
NodeID: 41atspd62he1vshs4jmhpyufj
Is Manager: true
ClusterID: 5nl0kyz1dfmkgg2sx04vr8zoi
Managers: 1
Nodes: 1
Orchestration:
Task History Retention Limit: 5
...
[yangdong@centos7 ~]$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
41atspd62he1vshs4jmhpyufj * centos7 Ready Active Leader
node ID旁边那个*号表示现在连接到这个节点上。
登录到centos7-A主机上,执行前面创建swarm时输出的命令:
[yangdong@centos7-A ~]$ docker swarm join \
> --token SWMTKN-1-49ueborzkg0v6l3xu2g1d5zfgsjn1xobqvctwozq14m07n1ak0-2llwi551ii09zeyus5r3zi3un \
> 192.168.71.131:2377
This node joined a swarm as a worker.
如果当时创建swarm时没有记下命令的输出,可以通过在manager节点上运行docker swarm join-token worker
命令来获取如何加入swarm的命令。
在另一台主机centos7-B上也执行相同的命令,最后在manager节点上看一下集群节点的状态:
[yangdong@centos7 ~]$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
0b57ews522yiz8xyhy3jz34ci centos7-B Ready Active
41atspd62he1vshs4jmhpyufj * centos7 Ready Active Leader
d1lurs40mlah70spbetcsl9rw centos7-A Ready Active
在centos7也就是manager节点上运行如下命令来部署服务:
[yangdong@centos7 ~]$ docker service create --replicas 1 --name helloworld alpine ping docker.com
50r6d8w4cwzi45s8865p9pdn4
--replicas
参数指定服务由几个实例组成。最后的命令行参数alpine ping docker.com
指定了使用alpine镜像创建服务,实例启动时运行ping docker.com命令。这与docker run命令是一样的。
使用docker service ls
查看正在运行服务的列表:
[yangdong@centos7 ~]$ docker service ls
ID NAME REPLICAS IMAGE COMMAND
50r6d8w4cwzi helloworld 1/1 alpine ping docker.com
在部署了服务之后,登录到manager节点,运行下面的命令来显示服务的信息。参数--pretty
使命令输出格式化为可读的格式,不加--pretty
可以输出更详细的信息:
[yangdong@centos7 ~]$ docker service inspect --pretty helloworld
ID: 50r6d8w4cwzi45s8865p9pdn4
Name: helloworld
Mode: Replicated
Replicas: 1
Placement:
UpdateConfig:
Parallelism: 1
On failure: pause
ContainerSpec:
Image: alpine
Args: ping docker.com
Resources:
使用命令docker service ps <SERVICE-ID>
可以查询到哪个节点正在运行该服务:
[yangdong@centos7 ~]$ docker service ps helloworld
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
541qk5jdrb71ypna9y5zw2l33 helloworld.1 alpine centos7 Running Running 12 minutes ago
登录到manager节点,使用命令docker service scale <SERVICE-ID>=<NUMBER-OF-TASKS>
来将服务扩展到指定的实例数:
[yangdong@centos7 ~]$ docker service scale helloworld=5
helloworld scaled to 5
再次查询服务的状态列表:
[yangdong@centos7 ~]$ docker service ps helloworld
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR
541qk5jdrb71ypna9y5zw2l33 helloworld.1 alpine centos7 Running Running 16 minutes ago
96s46qpl3qd94ntw3n2bt81m8 helloworld.2 alpine centos7-B Running Running 17 seconds ago
6p1u8hj4y31i4pjmwh8zvvf2h helloworld.3 alpine centos7-A Running Running 8 seconds ago
02jn1fxkx8juwizk6fjgv9r9n helloworld.4 alpine centos7-A Running Running 9 seconds ago
btbrvtnjjmgyb8emwmznziho9 helloworld.5 alpine centos7 Running Running 27 seconds ago
可见Swarm创建了4个新的task来将整个服务的实例数扩展到5个。这些服务分布在不同的Swarm节点上。
在manager节点上运行docker service rm helloworld
便可以将服务删除。删除服务时,会将服务在各个节点上创建的容器一同删除,而并不是将容器停止。
此外Swarm模式还提供了服务的滚动升级,将某个worker置为维护模式,及路由网等功能。在Docker将Swarm集成进Docker引擎后,可以使用原生的Docker CLI对容器集群进行各种操作,使集群的部署更加方便、快捷。
]]>本篇文章主要探索Docker的单机容器网络,了解一下单个Docker主机上网络的各种模式,从而为后续理解跨主机容器网络打下基础。
Docker默认容器网络的建立和控制是一种结合了network namespace,iptables,Linux网桥及route table等多种技术的综合解决方案,本篇主要针对于如何使用单主机网络的各种模式,对于实现细节不做过多的探索(这篇文章http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/对于docker单主机网络的实现机制做了详细的探索)。
Docker支持的网络模式主要有如下几种:
启动容器时,使用了参数--network="none"
。在此种模式下,容器和外部网络没有连接。在容器中只有loopback的网络接口,但它没有对外的任何路由。
启动容器时,使用了参数--network="bridge"
或者未指定此参数。这是docker默认的网络模式。它允许此主机上的容器彼此进行通信,也允许容器访问主机的外部网络。下图显示了Docker bridge网络的示意图:
在主机上,docker创建了一个通常名为docker0的网桥,这里它的ip设置为172.17.0.1.在创建每个容器时,同时创建了一对veth网络接口。接口的一端连接到docker0的网桥上,另一端连接到容器的内部。从容器发起的到外部网络的连接是通过IP forwarding和设置了NAT规则的iptables rules实现的(图中的绿色箭头)。从外部网络到容器内部的连接使用了一条完全不同的路径。如果容器将自身的端口映射到主机上,则docker会启动一个docker-proxy进程来进行监听,通过此proxy将数据转发到容器中(图中的红色箭头)。
默认情况下,同一台docker主机上的容器彼此之间可以通过他们的IP地址进行通信。如果需要通过容器的主机名进行通信,容器之间必须设置了link。
启动容器时,使用了参数--network="host"
。在这种模式下,容器共享主机的networking namespace,所以主机上的网络接口对容器都可用,同时在bridge模式下docker所做的各种网络设置都被略过,这意味着容器的网络性能和普通的主机网络性能一样快。在运行一些对网络性能要求较高的应用时,如负载均衡器或高性能web服务器时,应使用此模式。
但此模式给予容器对本地系统服务的完全访问权限,所以比其它模式的安全性要差。
启动容器时,使用了参数--network="container:<name|id>"
。这种模式下容器使用了另外一个容器的networking namespace,也就是说它与另外一个容器共享网络栈。
在这种模式下,用户可以使用Docker网络驱动或外部网络驱动plugin来创建自定义的网络。用户可以将多个容器连接到同一个网络上。一旦容器连接到用户自定义的网络后,容器可以使用另外一个容器的ip地址或名称来彼此通信。此功能需要Docker 1.10之后的版本。在较新版本的docker daemon中内置了一个DNS服务器,对于任意在创建时指定了name或net-alias或通过link提供了别名的容器,它可以提供内置的服务发现功能,从而无需再使用第三方软件提供的DNS服务(无需再使用文章“使用resolvable通过DNS查找容器”中提供的方法)。
对于overlay网络或使用了支持多主机连接的插件的容器,连接到同一多主机网络但从不同主机上启动的容器也可以彼此之间以这种方式通信。
Docker的network命令既可以用于单主机网络的相关操作,也可以用于多主机overlay网络的操作,本篇主要使用其与单主机网络相关的命令。
当在主机上安装docker后,docker引擎自动创建三个网络,可用如下命令列出默认的三个网络:
[yangdong@centos7 ~]$ docker network ls
NETWORK ID NAME DRIVER SCOPE
2a820cde1d0c bridge bridge local
54be0bc791bf host host local
8488a8a4ca59 none null local
除此之外,用户还可以创建自己的bridge或overlay的网络。如果运行docker network create
命令并且指定一个网络名称,则此命令为用户创建一个bridge网络:
[yangdong@centos7 ~]$ docker network create simple-network
a88875cc258fb24bbf55db67efefd05976dc8d1a8e25a2166a1acbd1dc9e125a
[yangdong@centos7 ~]$ docker network inspect simple-network
[
{
"Name": "simple-network",
"Id": "a88875cc258fb24bbf55db67efefd05976dc8d1a8e25a2166a1acbd1dc9e125a",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1/16"
}
]
},
"Internal": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
可以将容器动态得连接到一个或多个网络上。一旦连接后,容器可以通过其它容器的ip地址或名称进行通信。下面看一下例子:
首先创建两个容器:
[yangdong@centos7 ~]$ docker run -itd --name=container1 busybox
bdaadbef1c5b3a53c1cf54ddda70e170e386fc578c815f07b09d21ca2fcd3b20
[yangdong@centos7 ~]$ docker run -itd --name=container2 busybox
126cf3af1ddd033a0925ca879e8d744293cb95949d560877ca630a29b4630630
然后创建一个隔离的bridge的网络用于测试:
[yangdong@centos7 ~]$ docker network create -d bridge --subnet 172.25.0.0/16 isolated_nw
38159357c0979fdbc6ca0be29475867115e002ef27d8f79fee014b03ffd86b8d
这里通过命令行参数--subnet
指定了容器使用的子网网段。下面将container2连接到刚才创建的网络上:
[yangdong@centos7 ~]$ docker network connect isolated_nw container2
然后启动第三个容器,在启动的同时将其连接到isolated_nw网络上,同时手动指定该容器的ip:
[yangdong@centos7 ~]$ docker run --network=isolated_nw --ip=172.25.3.3 -itd --name=container3 busybox
d649491fd218c65fb0dc26aa79cb0d6f43dabac2bdc2f404c515f97042e60206
只要容器连接到一个由用户指定子网网段(通过--subnet
)的网络上时,就可以为容器指定ip地址。
上述命令运行完毕后,整个主机上的网络状态如下图所示:
{% asset_img 3.png %}
使用docker attach命令连接到运行的container2容器内部并查看其网络栈:
[yangdong@centos7 ~]$ docker attach container2
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:05
inet addr:172.17.0.5 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:acff:fe11:5/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:9 errors:0 dropped:0 overruns:0 frame:0
TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:718 (718.0 B) TX bytes:648 (648.0 B)
eth1 Link encap:Ethernet HWaddr 02:42:AC:19:00:02
inet addr:172.25.0.2 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:acff:fe19:2/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:32 errors:0 dropped:0 overruns:0 frame:0
TX packets:8 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:3282 (3.2 KiB) TX bytes:648 (648.0 B)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
可见eth0连接到了默认的bridge网络,eth1连接到了用户创建的isolated_nw网络,此网络可以通过docker内置的DNS服务器进行其它容器的名称解析,所以在container2中可以通过名称ping通container3:
/ # ping -w 4 container3
PING container3 (172.25.3.3): 56 data bytes
64 bytes from 172.25.3.3: seq=0 ttl=64 time=0.146 ms
64 bytes from 172.25.3.3: seq=1 ttl=64 time=0.113 ms
64 bytes from 172.25.3.3: seq=2 ttl=64 time=0.100 ms
64 bytes from 172.25.3.3: seq=3 ttl=64 time=0.112 ms
--- container3 ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 0.100/0.117/0.146 ms
然而在默认的bridge网络中,并不是这样。在默认的bridge网络中,Docker并不支持自动的服务发现:
/ # ping -w 4 container1
ping: bad address 'container1'
在默认的bridge网络中,可以使用传统的docker run --link
命令来启用通过名称的解析。当然,在没有使用--link
时,可以通过彼此的ip地址进行通信。
退出container2的终端,使用快捷键CTRL-p然后CTRL-q。
在这个示例当中,container2连接到了两个网络上,所以它可以与container1和container3通信。但container1和container3并不在一个网络当中所以它们之间并不能通信。下面连接到container3的控制台然后测试一下(container1的ip为172.17.0.4):
[yangdong@centos7 ~]$ docker attach container3
/ # ping 172.17.0.4
PING 172.17.0.2 (172.17.0.2): 56 data bytes
^C
--- 172.17.0.2 ping statistics ---
4 packets transmitted, 0 packets received, 100% packet loss
用户可以通过docker network disconnect
命令断开容器到某个网络的连接,当断开此连接后,容器就不能通过此网络与其它容器通信了:
[yangdong@centos7 ~]$ docker network disconnect isolated_nw container2
[yangdong@centos7 ~]$ docker attach container2
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:05
inet addr:172.17.0.5 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:acff:fe11:5/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:17 errors:0 dropped:0 overruns:0 frame:0
TX packets:16 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1222 (1.1 KiB) TX bytes:1152 (1.1 KiB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:16 errors:0 dropped:0 overruns:0 frame:0
TX packets:16 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:922 (922.0 B) TX bytes:922 (922.0 B)
/ # ping container3
ping: bad address 'container3'
当一个网络中所有的容器都停止或断开连接后,可以移除该网络:
[yangdong@centos7 ~]$ docker network disconnect isolated_nw container3
[yangdong@centos7 ~]$ docker network rm isolated_nw
isolated_nw
[yangdong@centos7 ~]$ docker network ls
NETWORK ID NAME DRIVER SCOPE
2a820cde1d0c bridge bridge local
54be0bc791bf host host local
8488a8a4ca59 none null local
a88875cc258f simple-network bridge local
更详细信息可以参考官方文档。
]]>上篇文章通过Docker快速搭建了ELK日志分析系统,体会了Docker对于应用程序的部署提供的便利性。本篇尝试一下通过Docker快速搭建Mesos高可用集群,再次使用docker进行应用程序的快速部署。
在Mesos高可用设计中,引入了ZooKeeper集群来辅助Leader的选举,这在当前的分布式集群中比较流行,比如Docker Swarm高可用集群同时支持利用consul、etcd、ZooKeeper进行Leader的选举,Kubernetes也采用了etcd等实现了自身的高可用。这种设计可以理解为大集群+小集群,小集群也就是ZooKeeper/etcd/consul集群,它们为大集群服务,比如提供Leader的选举,为大集群提供配置数据的存储和服务发现等功能。在一个复杂的系统中,这个小集群可以为系统的多个服务组件同时提供服务。因此在部署高可用Mesos集群时,必须首先部署好一个ZooKeeper集群,这里我们直接使用文章“使用Docker快速搭建ZooKeeper高可用集群”中搭建的ZooKeeper集群,集群有3个节点,包括:
centos7-A:192.168.71.167
centos7-B:192.168.71.168
centos7-C:192.168.71.169
这三个节点上已经部署了ZooKeeper,下面部署3个节点的Mesos master,后面的Mesos Agent(Mesos slave)仍然部署在这三台机器上,在三个节点上分别运行如下命令:
上面的命令中MESOS_HOSTNAME
和MESOS_IP
要与每个节点的hostname和ip相对应。下面在每个节点上启动agent:
与master类似,命令中的MESOS_HOSTNAME
和MESOS_IP
要与每个节点的hostname和ip相对应。如果要加入更多的agent节点,只需要在新的节点上运行上述命令,便可以将新的agent节点加入集群。
上述命令在每个节点上执行完毕后,整个高可用集群便搭建完毕了,可以通过日志和mesos提供的web页面来进行验证。通过浏览器访问三个master中的第一IP地址,http://192.168.71.167:5050/结果如下图:
下面还是通过docker将Marathon Framework加入到此集群中:
Marathon也支持高可用的配置,与mesos master类似,上述命令在每个节点都执行一次,并且在每个节点都指定该节点自己的ip和hostname,这样Marathon便也成为高可用的部署了。
本篇通过docker快速部署了高可用的mesos集群。当然也可以通过配置管理工具如salt等,将上述命令写成state文件的形式,使部署更加容易管理,从而使整个IT基础设施通过配置文件的形式管理起来。
]]>ELK是一个非常常用的日志收集系统,它是由elasticsearch,logstash,kibana组成。本文通过docker-compose来快速搭建起一个ELK日志栈,使用了最简化的配置,可以立即用于开发环境或测试环境。如果用于生产环境的日志收集和分析,还需要进一步的完善。
首先简单了解一下ELK框架的概览,可通过下图来说明:
下面就来进行ELK栈的搭建。所用的环境机器名为centos7,ip地址为192.168.71.131。
我们首先进行logstash配置文件的编写,如下:
在上面的配置文件中,input部分配置logstash的input plugin,这里设置为在tcp和udp端口5000进行监听。logstash支持多种input插件,可以通过这些插件收集不同数据源的日志。详细信息参考Input plugins。
类似的,output部分配置其output plugin,这里设置将数据发送到elasticsearch进行进一步处理。详细信息参考output plugin.
还可以在配置文件中配置Filter plugins,以便对于原始的日志数据进行过滤。这里没有配置filter部分。关于logstash配置文件的完整说明可以参考官方文档。
完成了logstash的配置文件,下一步就可以编写ELK的docker-compose的配置文件了:
在启动此compose file之前,需要将主机的vm.max_map_count值调高(链接),命令如下:
[yangdong@centos7 ~]$ sudo sysctl -w vm.max_map_count=262144
[sudo] password for yangdong:
vm.max_map_count = 262144
然后在docker-compose.yml和logstash.conf所在的文件夹下启动docker-compose:
[yangdong@centos7 logging]$ docker-compose up
...
观察程序的日志输出,确认启动正常,这样我们的ELK栈就快速搭建成功了。
目前只是搭建了ELK栈,还没有日志传输到logstash上面,下面就使用上篇文章“通过Logspout路由docker容器日志”中构建的logspout来将另外一台主机上的docker日志发送到logstash上。这个主机的机器名为centos7-A,ip地址为192.168.71.167。整个架构如下图所示:
通过如下命令启动logspout容器:
[yangdong@centos7-A ~]$ docker run -d --name="logspout" \
--volume=/var/run/docker.sock:/var/run/docker.sock \
-e ROUTE_URIS=logstash+tcp://192.168.71.131:5000 \
registry.cn-beijing.aliyuncs.com/andyyoung01/logspout-logstash
这样,整个centos7-A机器上的docker日志都能收集到ELK所在的机器上了。类似的,如果有多台docker主机,都可以在其主机上运行此命令来将日志集中收集到ELK机器上。
本篇通过docker-compose快速搭建了ELK日志栈,没有考虑生产环境中可能遇到的各种情况。如果要搭建ELK日志分析系统的生产环境,或者需要保存一定时间长度的日志,需要考虑使用docker中的volume来将数据保存到特定位置,也可以通过搭建elasticsearch集群的方式进行扩展。
]]>Logspout是一个运行在Docker容器中的程序,它可以将其所在主机上的其它Docker容器的日志路由到所配置的任何地方。它是一个无状态的容器化程序,并不是用来管理日志文件或查看日志的,它主要是用于将主机上容器的日志发送到其它地方。目前它只捕获其它容器中的程序发送到stdout和stderr的日志。
Logspout是一个很小的Docker容器,因为它是基于Alpine Linux构建的。如果没有特殊需求的话(如将日志路由到kafka,logstash等),需要先拉取官方镜像:
[yangdong@centos7 ~]$ docker pull gliderlabs/logspout:v3.1
然后通过将主机上的docker socket挂载到Logspout容器内部,来使其可以通过使用Docker API来将容器的日志发送到不同的终端:
[yangdong@centos7 ~]$ docker run --name="logspout" \
--volume=/var/run/docker.sock:/var/run/docker.sock \
gliderlabs/logspout:v3.1 \
syslog+tls://logs.papertrailapp.com:55555
这里的日志目标终端是通过在启动logspout容器时传给容器内部的命令给出的(此处为syslog+tls://logs.papertrailapp.com:55555)。此URI告诉logspout将日志发送给位于logs.papertrailapp.com:55555的syslog程序,并且使用了tls加密传输选项。如果要指定多个日志目标终端,只需使用逗号将多个URI分隔开。
logspout容器将收集所有其它没有使用-t
参数启动的容器,并且这些容器的日志驱动配置为系统默认的兼容journald和json-file的日志类型(即这些容器的日志可以通过docker logs命令查看)。
默认情况下,logspout会将它所在主机上所有满足前面条件容器的日志都进行路由,如果需要它忽略某个容器,可以在启动容器时设置环境变量-e 'LOGSPOUT=ignore'
,这样此容器的日志便会被logspout忽略。
通过使用logspout的httpstream module,可以通过curl时时查看logspout路由的日志,甚至可以不提供日志路由的URI:
[yangdong@centos7 ~]$ docker run -d --name="logspout" \
--volume=/var/run/docker.sock:/var/run/docker.sock \
--publish=127.0.0.1:8000:80 \
gliderlabs/logspout:v3.1
[yangdong@centos7 ~]$ curl http://127.0.0.1:8000/logs
默认的logspout包含很多內建模块,可以通过编辑modules.go文件,重新构建logspout来增加或减少新的模块。如果需要将日志路由到kafka,logstash等程序,需要添加第三方模块来实现此功能。
下面通过实例将logspout-logstash这个第三方模块加入到logspout中。
在https://github.com/andyyoung01/logspout-logstash/tree/test_branch/custom已经修改好了需要添加的模块,主要修改的文件如下:
上述文件的最后一行为添加的logspout-logstash模块,其它行为默认的模块。重新构建logspout后,可以通过指定环境变量ROUTE_URIS=logstash://host:port
来给定logstash服务器的URI。例如:
[yangdong@centos7 ~]$ docker run --name="logspout" \
--volume=/var/run/docker.sock:/var/run/docker.sock \
-e ROUTE_URIS=logstash+tcp://logstash.home.local:5000 \
registry.cn-beijing.aliyuncs.com/andyyoung01/logspout-logstash
logstash默认使用UDP协议,上面指定URI时,通过在logstash协议后添加+tcp
来切换成使用TCP协议。这样就可以将日志路由到logstash服务器上进行进一步的处理了。
上篇文章我们构建了一个Jenkins镜像,本篇我们通过一个实例使用一下上篇构建的镜像,来组成一个持续集成环境。典型的持续集成环境包含的功能包括:从源代码仓库拉取代码,源代码的编译,对编译后的代码进行单元测试、集成测试及系统测试等,测试完成后对代码的打包。这些功能可以通过Jenkins的插件来实现。
本篇的代码使用了“Using Docker”一书当中的identidock代码。它是由python语言编写的简单的web应用程序。程序的基本功能是根据用户输入的用户名,生成一个专属于该用户的图标,如下图所示:
完整的程序由三部分组成:一个由Flask框架编写的web程序,一个基于redis的缓存,以及一个生成图标的web服务。这三个部分都最终都通过容器提供服务,组成微服务的架构。程序的代码主要在第一个部分即web程序部分,另外两个部分都直接使用现成的容器镜像,即一个redis镜像和一个amouat/dnmonster镜像。整个程序可以通过docker-compose组成测试环境。程序的源代码在https://github.com/andyyoung01/ci-testing/tree/master/identidock。
下面我们主要关注通过上篇构建的Jenkins镜像,来拉取程序源代码,运行此程序的单元测试,在测试完成后可以通过bash脚本将程序tagging,然后推送到镜像库中等待部署。
首先,下载上篇构建的Jenkins容器镜像:
[yangdong@centos7 ~]$ docker pull registry.cn-beijing.aliyuncs.com/andyyoung01/jenkins-with-docker
为了方便持久化Jenkins容器的配置等,我们创建一个数据容器:
[yangdong@centos7 ~]$ docker run --name jenkins-data \
> registry.cn-beijing.aliyuncs.com/andyyoung01/jenkins-with-docker \
> echo "Jenkins Data Container"
Jenkins Data Container
下面创建Jenkins服务器容器,并且通过--volumes-from
命令行参数使用前面创建的数据容器,来持久化JENKINS_HOME中的数据:
[yangdong@centos7 ~]$ docker run -d -v /var/run/docker.sock:/var/run/docker.sock \
> --volumes-from jenkins-data -p 8080:8080 --name jenkins \
> registry.cn-beijing.aliyuncs.com/andyyoung01/jenkins-with-docker
4f94030e525ea573c98a8d24df2540075f5a43f091890256fb7295d137192754
容器需要一段时间初始化,此时可以通过如下命令查看容器的初始化过程:
[yangdong@centos7 ~]$ docker logs -f jenkins
...
从上面的日志输出可以查询到Jenkins服务器的初始化密码,使用此密码登录Jenkins的web界面。经过一系列的初始化后,最终进入了Jenkins的web界面。下面开始通过此web页面创建创建持续集成环境:
|
|
上面的部分配置了持续集成环境的构建和测试部分,以及测试成功后对代码的打包。
最后点击“保存”按钮保存所有设置,回到该项目的主页后,点击左边的“立即构建”链接开始构建项目。这里是手动触发的构建,当然还有许多其它触发构建的方式可以配置。
稍微看一下上面的bash脚本。它是通过docker-compose来设置测试环境的。在运行了单元测试后,如果没有问题,继续运行系统测试。系统测试也成功后,对镜像打标签,然后推送到镜像存储库中(这里没有实际推送上去)。
在web页面的“Console Output”链接下,可以看到构建过程中的日志输出,如下图:
可见,这里我们通过bash脚本完成了大多数的持续集成步骤。实际上,Jenkins提供了许多插件,可以用来配置整个持续集成的Pipeline。
Jenkins是一个很受欢迎的开源的CI服务器。本篇我们将构建一个Jenkins容器,并在容器中安装docker及docker-compose,以便于通过此容器构建docker镜像。在容器内使用docker,可以通过两种方法实现:一种是将docker socket挂载到容器内部;另一种是使用Docker-in-Docker(DinD)。
我们将以官方的Jenkins镜像为基础,将Docker socket挂载到容器内部,并且在容器内部安装docker及docker-compose,从而可以在Jenkins容器内部构建镜像。本篇不使用DinD的方式。这两种方式的对比可以通过下图说明:
Docker-in-Docker(DinD)是在一个Docker容器内部再运行一个Docker。它需要一些配置。请参考https://github.com/jpetazzo/dind。
下面就来通过Dockerfile构建Jenkins容器。基本的思路就是选用官方的Jenkins基础镜像,然后安装Docker可执行程序以及docker-compose可执行程序。由于想在容器内部挂载宿主机上的docker socket,就需要容器内部的Jenkins用户有足够的访问权限,这是通过将jenkins用户加入无密码的sudo权限实现的。这里没有将其加入到Docker用户组,因为这可能会带来潜在的可移植性问题。这样,所有在容器内部执行的关于Docker的命令就需要通过sudo运行。Dockerfile如下:
|
|
上述文件中的第4行切换到了root用户,以便于后面安装docker和docker-compose。第5-19行主要就是安装docker和docker-compose的命令。第15行的作用是将jenkins用户加入sudo免密码权限,从而可以访问到宿主机的docker socket。最后几行命令的作用是切换回jenkins用户并且安装一些jenkins的插件。
下面通过阿里云的镜像仓库控制台来构建此镜像。由于镜像构建过程中需要从互联网下载各种软件包,国内的网络下载一些软件时速度过慢,而阿里云提供的“海外机器构建”选项可以大大提高软件的下载速度,从而节省大量时间。
构建完成后,可以测试一下是否可以在jenkins容器内部使用docker命令。
[yangdong@centos7 ~]$ docker pull registry.cn-beijing.aliyuncs.com/andyyoung01/jenkins-with-docker
[yangdong@centos7 ~]$ docker run -v /var/run/docker.sock:/var/run/docker.sock \
> registry.cn-beijing.aliyuncs.com/andyyoung01/jenkins-with-docker \
> sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9c6a9d18e6d6 registry.cn-beijing.aliyuncs.com/andyyoung01/jenkins-with-docker "/bin/tini -- /usr/lo" 29 seconds ago Up 11 seconds 8080/tcp, 50000/tcp pensive_nobel
可见,在容器内部可以运行docker命令,此命令查询到了宿主机或jenkins container内部正在运行的docker容器。上面是通过阿里云进行镜像构建。还可以通过docker官方的docker hub进行镜像构建,过程与阿里云类似,这里不再赘述。
本篇文章我们构建了一个jenkins容器,并在容器内部安装了docker和docker-compose,从而可以在运行完各种测试后,在容器内部通过docker命令自动构建测试完成后的代码的镜像,从而组成持续集成或持续交付pipeline的一部分。下篇通过一个实际的例子来看看怎样配置这个pipeline。
]]>除了提供以结构化的方法进行配置管理的功能外,salt也提供了许多其它的功能,例如Event系统和Reactor系统。在salt中,Event系统为基础的通信方式。Reactor系统允许我们根据接收到的特定的Event,来进行特定的操作。
在salt中,event系统通过event bus在minions和masters上传递需要的数据。Events是非常简单的数据结构。每个Events中,都包含一个ID和其相应的数据。这个Event ID称为tag,它的格式与文件系统非常类似,是一个层次结构,以“/”分隔不同的层次。可以通过在master上,使用state.event的runner来查看event:
[root@centos7-A ~]# salt-run state.event pretty=True
这时此命令会阻塞,等待事件的到来。在打开另一个终端后,通过sudo salt ‘centos7-A’ test.ping发送一个命令,在此终端上可以看到下面的输出:
20161031111459171418 {
"_stamp": "2016-10-31T03:14:59.216257",
"minions": [
"centos7-A"
]
}
salt/job/20161031111459171418/new {
"_stamp": "2016-10-31T03:14:59.243522",
"arg": [],
"fun": "test.ping",
"jid": "20161031111459171418",
"minions": [
"centos7-A"
],
"tgt": "centos7-A",
"tgt_type": "glob",
"user": "sudo_yangdong"
}
salt/job/20161031111459171418/ret/centos7-A {
"_stamp": "2016-10-31T03:15:00.037574",
"cmd": "_return",
"fun": "test.ping",
"fun_args": [],
"id": "centos7-A",
"jid": "20161031111459171418",
"retcode": 0,
"return": true,
"success": true
}
上面的输出中,大括号前面的部分为tag,大括号中的部分为传输的data。常见的Events的种类有’salt/auth’、’salt/minion’、’salt/job’等。上边的输出为’salt/job’类型的event。当请求从master发送到minion上时,生成了一个salt/job/<job-id>/new
的event;当对应的minion返回其响应时,生成了salt/job/<job-id>/ret/<minion-id>
的event。
还有另外的方式可以用来生成events,例如通过event.fire_master,event.send或event.fire:
[root@centos7-A ~]# salt-call event.fire_master '{"data":"salt events test"}' 'salt/test'
local:
True
在前面阻塞的终端上可以得到下面的输出:
salt/auth {
"_stamp": "2016-10-31T03:36:02.692162",
"act": "accept",
"id": "centos7-A",
"pub": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwZwF/rh+jJTMoz04dmq4\nQrSXWrByDdIo8yqfcPsL0XZlaGHdWZzzDn0BC9J2dvg8b+81dKhd2DivnD6di6Bu\nnoFC62XCj3b7yrs1uyJyfoOk6CMwQaCyBlmgHUk2OedeO+yxM9p/l9NmLWnrpi47\nauz0HIoWxu6L0LGljLcazgUD9qHxe5E4emLwTsQp2PYtS0KqIl0yWDVUoYIbraZp\nHHybJNAePqM/p/rZkG+2GeardOYPiWuE7r6Xv5gPvoHCz4KScfjCm2ooYgMueg3X\nlgmOVlhHhEG0G32mvsinRNucfsUvEOZLo2PeWPpnaSgX3bVjSLAFoCsEhKsOYGlp\nQwIDAQAB\n-----END PUBLIC KEY-----",
"result": true
}
salt/test {
"_stamp": "2016-10-31T03:36:03.461534",
"cmd": "_minion_event",
"data": {
"data": "salt events test"
},
"id": "centos7-A",
"tag": "salt/test"
}
salt/job/20161031113603486635/ret/centos7-A {
"_stamp": "2016-10-31T03:36:03.505026",
"arg": [
"{\"data\":\"salt events test\"}",
"salt/test"
],
"cmd": "_return",
"fun": "event.fire_master",
"fun_args": [
"{\"data\":\"salt events test\"}",
"salt/test"
],
"id": "centos7-A",
"jid": "20161031113603486635",
"retcode": 0,
"return": true,
"tgt": "centos7-A",
"tgt_type": "glob"
}
salt中reactor系统的基本功能是根据从minions上收到的events,以运行states或运行命令的方式来做出反应。下面就来配置一个最简单的reactor:
上面的配置中,在master的主配置文件夹中添加了一个reactor.conf的文件,此文件可以是任意名称,只要扩展名为conf便可。第1行表明这是关于reactor的配置。第2行表明当收到的events的tag为’salt/test’时,将要执行第3行定义的文件中的内容。
上面的配置第1行为配置的名称。第2行local.cmd.run模块相当于当模块从salt命令行运行时的cmd.run这个执行模块。第3行通过使用tgt的属性,指定了centos7-A这个特定的minion。第4行的arg属性指定了要执行的命令。
完成上述的配置后,重启salt master,重新读入配置文件。然后使用上一节的相关命令来监听event:
[root@centos7-A ~]# salt-run state.event pretty=True
在另一个终端发送event:
[root@centos7-A tmp]# salt-call event.fire_master '{"data":"salt events test"}' 'salt/test'
最后,在/tmp文件夹中发现生成了test_output的文件。
通过event和reactor系统,可以将上述过程应用到持续集成或持续部署环境中。例如当一个构建任务成功执行后,一个event可以发送到master上,通知master运行部署任务。
]]>如果需要进行大数据处理,就需要有多个计算节点的支持。如何快速部署成百上千的计算节点便成为一个急需解决的问题。而公有云省去了我们架设物理服务器的工作,剩下的就是在公有云上如何快速部署大量计算节点的问题。通过salt cloud可以完美地解决此问题。
Salt cloud支持管理很多公有云,包括阿里云,AWS,GCE等,本文主要介绍如何借助Salt Cloud配置亚马逊弹性计算云(EC2)实例。
可以通过salt的bootstrap脚本进行安装。
上面的第1、2行命令通过salt的bootstrap脚本安装salt的v2016.3.3版本。此例子是在EC2的实例上运行的安装脚本,它使用的Linux发行版为Amazon Linux,其包管理器同Redhat系列发行版一样,也是yum。由于已经运行过bootstrap脚本,saltstack的安装源便加入了系统。第3行命令通过yum在此机器上安装了salt-master。最后将salt-master的服务启动。
/etc/salt/cloud
: 此文件是salt-cloud的基础配置文件。也可以通过将配置文件放置在/etc/salt/cloud.conf.d
文件夹内,文件名以conf作为扩展来实现对salt-cloud的配置。在此文件中包含salt-cloud的一些核心配置参数,例如pool_size,决定了当salt-cloud使用-P参数时,salt cloud操作的VMs的数量。此文件也可以包含一些关于minion的配置信息。或者是关于云供应商的一些配置参数。然而如果将所有配置信息放置在一个文件中,会导致配置文件过于庞大,难于维护,所以一些相关的配置分散到其它的配置文件中,使可读性和维护性更强。/etc/salt/cloud.providers
: 此文件包含云供应商相关的配置参数,如一些认证信息,密钥的名称和位置,可用区等。类似地,可以将配置文件放置在/etc/salt/cloud.providers.d
文件夹中,以conf作为文件名扩展。/etc/salt/cloud.profiles
: 此文件包含云供应商的更多的一些参数,主要包括用来配置云主机实例的一些参数。同样,/etc/salt/cloud.profiles.d
文件夹可以包含不同的配置文件。/etc/salt/cloud.map
: 此文件包含使用哪个profile的配置信息启动哪个实例。当使用多个云供应商时,可以对每个云供应商创建一个文件,这样就可以集中管理各个不同云供应商上的实例。官方文档的位置为https://docs.saltstack.com/en/latest/topics/cloud/index.html#configuration。
在了解了salt-cloud的安装和配置文件后,我们来实际应用一下。这个示例在aws的EC2实例上安装了salt-cloud,也安装了salt-master,通过salt-cloud批量部署4个计算节点,可用区a有2个节点,可用区b有2个节点。节点部署后,将自动在这些节点上安装salt-minion并且连接到刚才部署这些节点使用的salt-cloud节点的salt-master上。下面看一下几个配置文件中的内容:
上述文件第1行为provider的名称,其它配置文件可以根据此名称引用到这个provider。
第3、4行提供了用于访问AWS EC2的id和key。这个id和key可以在AWS的IAM控制台获取或设置。
第6、7行提供了用于登录新创建的minion的key的名称和文件位置。这个key是在AWS EC2的控制面板的密钥对页面生成的,生成后将密钥从AWS下载到本地的相应位置,并修改文件的权限为0400。
第9-14行指定了新建实例相关的参数,例如地点、可用区、实例大小等内容。
第16行指定了此配置文件所用的driver。旧版本的salt-cloud此字段为provider,新版本此字段修改为driver。这里是针对于AWS EC2进行的配置,所以指定的driver为EC2,配置文件中的很多配置选项都是EC2专有的。如果指定的是其它的driver,则配置文件中的内容针对于其它相应的公有云。
上述配置文件配置了三个profile。一个名为ec2_prod_common_app,一个针对于可用区a,另一个针对于可用区b。针对于可用区a或b的profile使用了extends
关键字,表明这个profile是扩展自ec2_prod_common_app的profile的,所有在common中配置的字段在a或b的profile中都可用,并且可以重写相同字段名称的配置。
下面逐行看一下该配置文件。第1、16、26行为三个profile的名称。在第一个profile中,第2行字段provider表明这个profile继承了上面配置的provider中的各项配置。
第3-8行配置了新建的minion中minion配置文件的内容和grains配置文件的内容。
第9行tag的内容指定了AWS中EC2节点包含的标签信息,使用这些信息更容易在AWS众多的资源中进行定位。
第10行指定了AWS EC2中的image id,根据此id,可以确定创建minion时所用的操作系统镜像。
第11行指定了创建minion后需要运行的脚本,此脚本的一般位置在/usr/lib/python2.6/site-packages/salt/cloud/deploy/,它用于在minion上安装部署salt相关的可执行程序。
第12行确保在安装后salt的各种模块如grains、pillars等从master同步到minions上。
第14行指明是使用公有ip地址还是使用私有ip地址来传输部署脚本。如果salt-cloud命令是运行在EC2节点内则使用private_ips,如果是运行在EC2节点之外则使用public_ips。
再来看两个针对于不同可用区配置的profile。
第17、27行的可用区覆盖了之前在provider中配置的可用区,针对于这两个profile使用不同的可用区。
剩下的配置主要用来配置minion上的网络接口。AssociatePublicIpAddress
字段指定了当minions启动后,是否为其分配公有ip地址。如果指定为True,就为其指定了公有ip,可以在EC2之外访问到该实例。SubnetId
指定了该网络接口所在的VPC的id,其值可以通过AWS VPC控制台获取或设置。SecurityGroupId
指定了该网络接口所在的安全组id,其值可以通过EC2控制台的安全组里获取或设置。
上面的map文件指定了哪个minions使用哪个profile下的配置。
上面的配置都完成后,可以通过下面的命令来创建minions:
[root@ip-172-31-17-221 salt]# salt-cloud -m /etc/salt/cloud.maps.d/aws.map -P
The following virtual machines are set to be created:
app01
app02
Proceed? [N/y] y
... proceeding
[ERROR ] Public IP not detected. If private IP is meant for bootstrap you must specify "ssh_interface: private_ips" in your profile.
[ERROR ] Public IP not detected. If private IP is meant for bootstrap you must specify "ssh_interface: private_ips" in your profile.
app01:
----------
amiLaunchIndex:
0
architecture:
x86_64
...
app02:
----------
amiLaunchIndex:
0
architecture:
x86_64
...
创建完成后,测试一下是否可以连接到这些新创建的minions上:
[root@ip-172-31-17-221 salt]# salt \* test.ping
app01:
True
app02:
True
app03:
True
app04:
True
到此,完成了AWS EC2的批量创建。如果需要更多的minions,只需要编辑map文件加入更多得主机。
可以通过下面的命令删除新创建的minions:
可见,通过salt-cloud可以在公有云上快速批量部署计算节点。如果通过传统的物理服务器来部署包含几十个节点的集群,通常需要不少的时间,而通过salt cloud工具,部署包含几十台上百台节点的集群基本上不到半天就可以完成。
]]>