标签 Kernel 下的文章

Hello,Termux

程序员或多或少都有一颗Geek(极客)的心^0^。- Tony Bai

折腾开始。

这一切都源于前不久将手机换成了Xiaomi的MIX2。因为青睐开放的系统(相对于水果公司系统的封闭,当然Mac笔记本除外^0^),我长期使用Android平台的手机。但之前被三星Note3手机的“大屏”搞的不是很舒服,这两年一直用5寸及以下的手机,因为单手操作体验良好。MIX2的所谓“全面屏”概念又让我回归到了大屏时代。

除了大屏,现在手机“豪华”的硬件配置也让人惊叹:高通骁龙835,8核,最高主频 2.45GHz;6GB以上的LPDDR4x的双通道大内存,怪不得微软和高通都开始合作生产基于高通ARM处理器的Win10笔记本了,这配置支撑在笔记本上办公+浏览网页绰绰有余。不过对于不怎么玩游戏的我而言,这种配置仅仅用作手机日常功能有些浪费。于是有了“mobile coding”的想法和需求,至少现在是这样想的,冲动也好,伪需求也好,先实现了再说。

一、神器Termux,不仅仅是一个terminal emulator

所谓”mobile coding”不仅仅是要通过手机ssh到服务器端进行coding,还要支持在手机上搭建一个dev环境。dev环境这个需求是以往我安装的ConnectBot等ssh client端工具所无法提供的,而其他一些terminal工具,诸如Terminal Emulator for Android仅仅提供一些shell命令的支持,适合于那些喜爱使用命令行对Android机器进行管理的”administrator”们,但对dev环境的搭建支持有限的。于是神器Termux登场了。

Termux是什么?Termux首先是一个Android terminal emulator,可以像那些terminal工具一样,提供基本的shell操作命令;除此之外更为重要的是它不仅仅是一个terminal emulator。Termux提供了一套模拟的Linux环境,你可以在无需root、无需root、无需root的情况下,像在PC linux环境下一样进行各种Linux操作,包括使用apt工具进行安装包管理、定制shell、访问网络、编写源码、编译和运行程序,甚至将手机作为反向代理、负载均衡服务器或是Web服务器,又或是做一些羞羞的hack行为等。

1、安装

Termux仅支持Android 5.0及以上版本(估计现在绝大多数android机都满足这一条件)。在国内建议使用F-Droid安装Termux(先下载安装F-Droid,再在F-Droid内部搜索Termux,然后点击安装),国内的各种安装助手很少有对这个工具的支持。或是到apk4fun下载Termux的apk包(size非常小)到手机中安装(安装时需要连接着网络)。当前Termux的最新版本为0.54

在桌面点击安装后的Termux图标,我们就启动了一个Termux应用,见下图:

img{512x368}

2、Termux初始环境探索

Mix2手机的Android系统使用的是Android 7.1.1版本,桌面Launcher用的是MIUI 9.1稳定版,默认的shell是bash。通过Termux,我们可以查看Android 7.1.1.使用的Linux内核版本如下:

$uname -a
Linux localhost 4.4.21-perf-g6a9ee37d-06186-g2b2a77b #1 SMP PREEMPT Thu Oct 26 14:55:45 CST 2017 aarch64 Android

可以看出Linux内核是4.4.21,采用的CPU arch family是ARM aarch64

我再来看一下Termux提供的常见目录结构:

Home路径:

$cd ~/
$pwd
/data/data/com.termux/files/home

//或者通过环境变量HOME获取:

$echo $HOME
/data/data/com.termux/files/home

长期使用Linux的朋友可能会发现,这个HOME路径好是奇怪,一般的标准Linux发行版,比如Ubuntu都是在”/home”下放置用户目录,但termux环境中HOME路径却是一个奇怪的位置。在Termux官方Wiki中,我们得到的答案是:Termux是一个prefixed system。

这个prefix的含义我理解颇有些类似于我们在使用configure脚本时指定的–prefix参数的含义。我们在执行configure脚本时,如果不显式地给–prefix传入值,那么make install后,包将被install在标准位置;否则将被install在–prefix值所指定的位置。

prefixed system意味着Termux中所有binaries、libraries、configs都不是放在标准的位置,比如:/usr/bin、/bin、/usr/lib、/etc等下面。Termux expose了一个特殊的环境变量:PREFIX(类似于configure –prefix参数选项):

$echo $PREFIX
/data/data/com.termux/files/usr

$cd $PREFIX
$ls -F
bin/  etc/  include/  lib/  libexec/  share/  tmp/  var/

是不是有些似曾相识?但Termux的$PREFIX路径与标准linux的根路径下的目录结构毕竟还存在差别,但有着对应关系,这种对应关系大致是:

Termux的$PREFIX/bin  <=>  标准Linux环境的 /bin和/usr/bin
Termux的$PREFIX/lib  <=>  标准Linux环境的 /lib和/usr/lib
Termux的$PREFIX/var  <=>  标准Linux环境的 /var
Termux的$PREFIX/etc  <=>  标准Linux环境的 /etc

因此,基本可以认为Termux的$PREFIX/就对应于标准Linux的/路径。

3、更新源和包管理

Termux的牛逼之处在于它基于debian的APT包管理工具进行软件包的安装、管理和卸载,就像我们在Ubuntu下所做的那样,非常方便。

Termux自己维护了一个源,提供各种专门为termux定制的包:

# The main termux repository:
#deb [arch=all,aarch64] http://termux.net stable main

同时,termux-packages项目为开发者和爱好者提供了构建工具和脚本,通过这些工具和脚本,我们可以将自己需要的软件包编译为可以在termux运行的版本,并补充到Termux的源之中。我大致测试了一下官方这个源还是可用的,虽然初始连接的响应很缓慢。

国内清华大学维护了一个Termux的镜像源,你可以通过编辑 /data/data/com.termux/files/usr/etc/apt/sources.list文件或执行apt edit-sources命令编辑源(在Shell配置中添加export EDITOR=vi后,apt edit-sources才能启动编辑器进行编辑):

# The main termux repository:
#deb [arch=all,aarch64] http://termux.net stable main
deb [arch=all,aarch64] http://mirrors.tuna.tsinghua.edu.cn/termux stable main

剩下的操作与Ubuntu上的一模一样,无非apt update后,利用apt install安装你想要的包。目前Termux源中都有哪些包呢?可以通过apt list命令查看:

$apt list
Listing... Done
aapt/stable 7.1.2.33-1 aarch64
abduco/stable 0.6 aarch64
abook/stable 0.6.0pre2-1 aarch64
ack-grep/stable 2.18 all
alpine/stable 2.21 aarch64
angband/stable 4.1.0 aarch64
apache2/stable 2.4.29 aarch64
apache2-dev/stable 2.4.29 aarch64
apksigner/stable 0.4 all
apr/stable 1.6.3 aarch64
apr-dev/stable 1.6.3 aarch64
apr-util/stable 1.6.1 aarch64
apr-util-dev/stable 1.6.1 aarch64
apt/stable,now 1.2.12-3 aarch64 [installed]
apt-transport-https/stable 1.2.12-3 aarch64
... ...
zile/stable 2.4.14 aarch64
zip/stable 3.0-1 aarch64
zsh/stable,now 5.4.2-1 aarch64 [installed]

查看是否有需要更新的包列表:

$apt list --upgradable

以安装golang为例:

$apt install golang
....
$go version
go version go1.9.2 android/arm64

img{512x368}

Termux源中的包似乎更新的很勤奋,Go 1.9.2才发布没多久,这里已经是最新版本了,这点值得赞一个!

二、开发环境搭建

我的目标是mobile coding,需要在Termux上搭建一个dev环境,以Go环境为例。

1、sshd

在搭建和配置阶段,如果直接通过Android上的软键盘操作,即便屏再大,那个体验也是较差的。我们最好通过PC连到termux上去安装和配置,这就需要我们在Termux上搭建一个sshd server。下面是步骤:

$apt install openssh
$sshd

就这么简单,一个sshd的server就在termux的后台启动起来了。由于Termux没有root权限,无法listen数值小于1024的端口,因此termux上sshd默认的listen端口是8022。另外termux上的sshd server不支持用户名+密码的方式进行登录,只能用免密登录的方式,即将PC上的~/.ssh/id_rsa.pub写入termux上的~/.ssh/authorized_keys文件中。关于免密登录的证书生成方法和导入方式,网上资料已经汗牛充栋,这里就不赘述了。导入PC端的id_rsa.pub后,PC就可以通过下面命令登录termux了:

$ssh 10.88.46.79  -p 8022
Welcome to Termux!

Wiki:            https://wiki.termux.com
Community forum: https://termux.com/community
IRC channel:     #termux on freenode
Gitter chat:     https://gitter.im/termux/termux
Mailing list:    termux+subscribe@groups.io

Search packages:   pkg search <query>
Install a package: pkg install <package>
Upgrade packages:  pkg upgrade
Learn more:        pkg help

其中10.88.46.79是手机的wlan0网卡的IP地址,可以在termux中使用ip addr命令获得:

$ip addr show wlan0
34: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 3000
    ... ...
    inet 10.88.46.79/20 brd 10.88.47.255 scope global wlan0
       valid_lft forever preferred_lft forever
    ... ...

2、定制shell

Termux支持多种主流Shell,默认的Shell是Bash。很多开发者喜欢zsh + oh-my-zsh的组合,Termux也是支持的,安装起来也是非常简单的:

$ apt install git
$ apt install zsh
$ git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh
$ cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc
$ chsh zsh

与在PC上安装和配置zsh和oh-my-zsh没什么两样,你完全可以按照你在PC上的风格定制zsh的Theme等,我用的就是默认theme,所以也无需做太多变化,顶多定制一下PROMPT(~/.oh-my-zsh/themes/robbyrussell.zsh-theme中的PROMPT变量)的格式^0^。

3、安装vim-go

在terminal内进行Go开发,vim-go是必备之神器。vim-go以及相关自动补齐、snippet插件安装在不同平台上都是大同小异的,之前写过两篇《Golang开发环境搭建-Vim篇》和《vim-go更新小记》,大家可以参考。

不过这里有一个较为关键的问题,那就是Termux官方源中的vim 8.0缺少了对python和lua的支持:

 $vim --version|grep py
+cryptv          +linebreak       -python          +viminfo
+cscope          +lispindent      -python3         +vreplace
$vim --version|grep lua
+dialog_con      -lua             +rightleft       +windows

而一些插件又恰需要这些内置的支持,比如ultisnips需要vim自带py支持;neocomplete又依赖vim的lua支持。这样如果你还想要补齐和snippet特性,你就需要在Termux下面自己编译Vim的源码了(configure时加上对python和lua的支持)。

4、中文支持

无论是PC还是Termux使用的都是UTF8的内码格式,但是在安装完vim-go后,我试着用vim编辑一些简单的源码,发现在vim中输入的中文都是乱码。这里通过一个配置解决了该问题:

//~/.vimrc

添加一行:

set enc=utf8

至于其中的原理,可以参见我N年前写的《也谈VIM字符集编码设置》一文。

三、键盘适配

现阶段,写代码还是需要键盘输入的(憧憬未来^0^)。

1、软键盘

使用原生自带的默认软键盘在terminal中用vim进行coding,那得多执着啊,尤其是在vim大量使用ESC键的情况下(我都没找到原生键盘中ESC键在哪里:()。不过Termux倒是很具包容心,为原生软键盘提供了扩展支持:用两个上下音量键协助你输入一些原生键盘上没有或者难于输入的符号,比如(全部的模拟按键列表参见这里):

清理屏幕:用volume down + L 来模拟 ctrl + L
结束前台程序:用volume down + C 来模拟 ctrl + C
ESC:用volume up + E 来模拟
F1-F9: 用volume up + 1 ~ 9 来模拟

据网友提示:volume up + Q键可以打开扩展键盘键,包括ESC、CTRL、ALT等,感谢。

这样仅能满足临时的需要,要想更有效率的输入,我们需要Hacker’s Keyboard。顾名思义,Hacker’s Keyboard可以理解为专为Coding(无论出于何种目的)的人准备的。和Termux一样,你可以从F-droid安装该工具。启动该app后,app界面上有明确的使用说明,如果依旧不明确,还可以查看这篇图文并茂的文章:《How to Use Hacker’s Keyboard》。默认情况下,横屏时Hacker’s keyboard会使用”Full 5-row layout”,即全键盘,竖屏时,则是4-row layout。你可以通过“系统设置”中的“语言和输入法”配置中对其进行设置,让Hacker’s keyboard无论在横屏还是竖屏都采用全键盘(我们屏幕够大^0^):

img{512x368}
横屏

img{512x368}
竖屏

Hacker’s Keyboard无法支持中文输入,这点是目前的缺憾,不过我个人写代码时绝少使用中文,该问题忽略不计。

2、外接蓝牙键盘

Hacker’s Keyboard虽然一定程度提升了Coding时的输入效率,但也仅是权宜之计,长时间大规模通过软键盘输入依旧不甚可取,外接键盘是必须的。对于手机而言,目前最好的外接连接方式就是蓝牙。蓝牙键盘市面上现在有很多种,我选择了老牌大厂logitechK480。这款键盘缺点是便携性差点、按键有些硬,但按键大小适中;而那些超便携的蓝牙键盘普遍键帽太小,长时间Coding的体验是个问题。

img{512x368}

Termux对外接键盘的支持也是很好的,除了常规输入,通过键盘组合键Ctrl+Alt与其他字母的组合实现各种控制功能,比如:

ctrl + alt + c => 实现创建一个新的session;
ctrl + alt + 上箭头/下箭头 => 实现切换到上一个/下一个session的窗口;
ctrl + alt + f => 全屏
ctrl + alt +v => 粘贴
ctrl + alt + +/- => 实现窗口字体的放大/缩小

不过,外接键盘和Hacker’s keyboard有一个相同的问题,那就是针对Termux无法输入中文。我尝试了百度、搜狗等输入法,无论如何切换(正常在其他应用中,通过【shift + 空格】实现中英文切换)均只是输入英文。

四、存储

到目前为止,我们提到的路径都在termux的私有的内部存储(private internal storage)路径下,这类存储的特点是termux应用内部的、私有的,一旦termux被卸载,这些数据也将不复存在。Android下还有另外两种存储类型:shared internal storage和external storage。所谓shared internal storage是手机上所有App可以共享的存储空间,放在这个空间内的数据不会因为App被卸载掉而被删除掉;而外部存储(external storage)主要是指外部插入的SD Card的存储空间。

默认情况下,Termux只支持private internal storage,意味着你要做好数据备份,否则一旦误卸载termux,数据可就都丢失了;数据可以用git进行管理,并sync到云端。

Termux提供了一个名为termux-setup-storage的工具,可以让你在Termux下访问和使用shared internal storage和external storage;该工具是termux-tools的一部分,你可以通过apt install termux-tools来安装这些工具。

执行termux-setup-storage(注意:这个命令只能在手机上执行才能弹出授权对话框,通过远程ssh登录后执行没有任何效果)时,手机会弹出一个对话框,让你确认授权:

img{512x368}

一旦授权,termux-setup-storage就会在HOME目录下建立一个storage目录,该目录下的结构如下:

➜  /data/data/com.termux/files/home $tree storage
storage
├── dcim -> /storage/emulated/0/DCIM
├── downloads -> /storage/emulated/0/Download
├── movies -> /storage/emulated/0/Movies
├── music -> /storage/emulated/0/Music
├── pictures -> /storage/emulated/0/Pictures
└── shared -> /storage/emulated/0

6 directories, 0 files

我们看到在我的termux下,termux-setup-storage在storage下建立了6个符号链接,其中shared指向shared internal storage的根目录,即/storage/emulated/0;其余几个分别指向shared下的若干功能目录,比如:相册、音乐、电影、下载等。我的手机没有插SD卡,可能也不支持(市面上大多数手机都已经不支持了),如果插了一张SD卡,那么termux-setup-storage还会在storage目录下j建立一个符号链接指向在external storage上的一个termux private folder。

现在你就可以把数据放在shared internal storage和external storage上了,当然你也可以在Termux下自由访问shared internal storage上的数据了。

五、小结

Termux还设计了支持扩展的Addon机制,支持通过各种Addon来丰富Termux功能,提升其能力,这些算是高级功能,在这篇入门文章里就先不提及了。好了,接下来我就可以开始我的mobile coding了,充分利用碎片时间。后续在使用Termux+k480的过程中如果遇到什么具体的问题,我再来做针对性的解析。


微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

源创会2017沈阳站讲稿:基于Harbor的高可用企业级私有容器镜像仓库部署实践

上周六开源中国源创会在沈阳举办了一次技术活动,很荣幸以本地讲师的身份和大家交流了一个topic: “基于Harbor的高可用企业级私有容器镜像仓库部署实践”。之所以选择这个topic,是因为这是我们团队的项目实践心得。很多企业和组织在深入使用Docker之后,都会有类似的高可用私有容器仓库搭建的需求,于是我就把我们摸索的实践和填坑过程拿出来,用30分钟与大家分享一下。另外这算是一个入门级的分享,并未深入过多原理。以下就是本次分享的内容讲稿整理。如有不妥或不正确的地方,欢迎交流指正。

img{512x368}

大家下午好,欢迎各位来到源创会沈阳站。在这里我也代表沈阳的IT人欢迎源创会来到沈阳,希望能有更多的像源创会这样的组织到沈阳举办技术活动。非常高兴能有这个机会在源创会这个平台上做分享, 今天和大家一起探讨的题目是:“基于Harbor的高可用企业级私有容器镜像仓库部署实践”。题目有些长,简单来说就是如何搭建一个好用的镜像仓库。

img{512x368}

首先做个简单的自我介绍。我叫白明,东软(注:源创会这次活动的会场在东软沈阳园区)是我的主场,在这里工作很多年,目前就职东软云科技;Gopher一枚,近两年主要使用Go语言开发;技术译者,曾参与翻译过《七周七语言》一书;并且参与过智慧城市架构系列丛书的编著工作;GopherChina大会讲师,这里顺便说一下GopherChina大会,它是目前中国地区规模最大、水平最高的Go语言技术大会,一般每年4月份在北京或上海举行。希望有志于Go语言开发的开发者积极参与;Blogger,写博10多年,依旧笔耕不倦;目前主要从事Docker&kubernetes的研究和实践。

当今,IT技术发展飞快。五年前, IT从业者口中谈论最多的技术是Virtual Machine,即虚拟化技术,人们经常争论的是到底是vmware的技术好,还是原生kvm技术稳定,又或是xen的技术完美。转眼间五年过去了,大家口中经常讨论的技术词汇发生了变化,越来越多的技术人在谈论Docker,谈论容器。

Docker是什么? Docker这门技术非常热,但我们要透过现象看其本质:

Docker技术并不是新技术,而是将已有技术进行了更好的整合和包装

内核容器技术以一种完整形态最早出现在Sun公司Solaris操作系统上,Solaris是当时最先进的服务器操作系统。2005年Solaris发布Solaris Container技术,从此开启了内核容器之门。

IT技术发展的趋势就是这样:商业有的,开源也要有。三年后,即2008年,以Google公司开发人员为主导的Linux Container,LXC功能在被merge到Linux内核。LXC是一种内核级虚拟化技术,主要基于namespacescgroup技术,实现共享一个os kernel前提下的进程资源隔离,为进程提供独立的虚拟执行环境,这样的一个虚拟的执行环境就是一个容器。本质上说,LXC容器与现在的Docker所提供容器是一样的。但是,当时LXC处于早期阶段,开发人员可能更为关注LXC的技术实现,而对开发体验方面有所忽略,导致LXC技术使用门槛较高,普通应用开发者学习、理解和使用它的心智负担较高,因此应用并不广泛。

这一情况一直持续到2013年,当时美国一家名不见经传的公司dotCloud发布了一款平台工具Docker,对外宣称可以实现:“build,ship and run any app and anywhere”。Docker实质上也是基于namespaces和cgroup技术的,Docker的创新之处在于其基于union fs技术定义了一套应用打包规范,真正将应用及其运行的所有依赖都封装到一个特定格式的文件中,这种文件就被称为image,即镜像文件。同时,Docker还提供了一套抽象层次更高的工具集,这套工具对dev十分友好,具有良好的开发体验(Developer eXperience),开发者无需关心namespace, cgroups之类底层技术,即可很easy的启动一个承载着其应用的容器:

Docker run ubuntu echo hello

因此, 从2013发布以来,Docker项目就像坐上了火箭,发展迅猛,目前已经是github上最火爆的开源项目之一。这里还要提一点就是:Docker项目是使用go语言开发的,Docker项目的成功,也或多或少得益于Go优异的开发效率和执行效率。

Docker技术的出现究竟给我们带来了哪些好处呢,个人觉得至少有以下三点:

  • 交付标准化:Docker使得应用程序和依赖的运行环境真正绑定结合为一体,得之即用。这让开发人员、测试和运维实现了围绕同一交付物,保持开发交付上下文同步的能力,即“test what you write, ship what you test”;
  • 执行高效化:应用的启动速度从原先虚拟机的分钟级缩短到容器的秒级甚至ms级,使得应用可以支持快速scaling伸缩;
  • 资源集约化:与vm不同的是,Container共享一个内核,这使得一个container的资源消耗仅为进程级别或进程组级别。同时,容器的镜像也因为如此,其size可以实现的很小,最小可能不足1k,平均几十M。与vm动辄几百兆的庞大身段相比,具有较大优势。

有了image文件后,自然而言我们就有了对image进行存取和管理的需求,即我们需要一个镜像仓库,于是Docker推出了Docker registry这个项目。Docker Registry就是Docker image的仓库,用来存储、管理和分发image的;Docker registry由Docker公司实现,项目名为distribution,其实现了Docker Registr 2.0协议,与早前的Registry 1.x协议版本相比,Distribution采用Go语言替换了Python,在安全性和性能方面都有了大幅提升;Docker官方运行着一个世界最大的公共镜像仓库:hub.docker.com,最常用的image都在hub上,比如反向代理nginx、redis、ubuntu等。鉴于国内访问hub网速不佳,多使用国内容器服务厂商提供的加速器。Docker官方还将Registry本身打入到了一个image中,方便开发人员快速以容器形式启动一个Registry:

docker run -d -p 5000:5000 --restart=always --name registry registry:2

不过,这样启动的Registry更多仅仅是一个Demo级别或满足个体开发者自身需要的,离满足企业内部开发流程或生产需求还差了许多。

既然Docker官方运行着免费的镜像仓库,那我们还需要自己搭建吗?实际情况是,对Docker的使用越深入,对私有仓库的需求可能就越迫切。我们先来看一组Docker 2016官方的调查数据,看看Docker都应用在哪些场合。 从Docker 2016官方调查来看,Docker 更多用于dev、ciDevOps等环节,这三个场合下的应用占据了半壁江山。而相比于公共仓库,私有镜像仓库能更好的满足开发人员在这些场合对镜像仓库的需求。理由至少有四点:

  • 便于集成到内部CI/Cd
    以我司内部为例,由于公司内部办公需要使用正向代理访问外部网络,要想将Public Registry集成到你的内部CI中,技术上就会有很多坎儿,整个搭建过程可能是非常痛苦的;

  • 对镜像可以更全面掌控
    一般来说,外部Public Registry提供的管理功能相对单一,往往无法满足企业内部的开发和交付需求;

  • 内部网络,网络传输性能更好
    内部开发运维流水线很多环节是有一定的时间敏感性的,比如:一次CI如果因为network问题导致image pull总是timeout,会让dev非常闹心,甚至影响整体的开发和交付效率。

  • 出于安全考虑
    总是有企业不想将自己开发的软件或数据放到公网上,因此在企业内部选择搭建一个private registry更会让这些企业得到满足;另外企业对仓库的身份验证可能还有LDAP支持的需求,这是外部registry无法满足的。

一旦企业决定搭建自己的private仓库,那么就得做一个private仓库的技术选型。商业版不在我们讨论范围内,我们从开源软件中挑选。不过开源的可选的不多,Docker 官方的Registry更聚焦通用功能,没有针对企业客户需求定制,开源领域我们大致有两个主要候选者:SUSEPortus和Vmware的Harbor。针对开源项目的技术选型,我个人的挑选原则最简单的就是看社区生态,落实到具体的指标上包括:

  • 项目关注度(即star数量)
  • 社区对issue的反馈数量和积极性
  • 项目维护者对issue fix的积极程度以及是否有远大的roadmap

对比后,我发现在这三个指标上,目前Harbor都暂时领先portus一段距离,于是我们选择Harbor。

Harbor是VMware中国团队开源的企业级镜像仓库项目,聚焦镜像仓库的企业级需求,这里从其官网摘录一些特性,大家一起来看一下:

– 支持基于角色的访问控制RBAC;
– 支持镜像复制策略(PUSH);
– 支持无用镜像数据的自动回收和删除; – 支持LDAP/AD认证;
– Web UI;
– 提供审计日志功能;
– 提供RESTful API,便于扩展;
– 支持中文&部署Easy。

不过,Harbor默认安装的是单实例仓库,并非是高可用的。对于接纳和使用Docker的企业来说,镜像仓库已经企业内部开发、交付和运维流水线的核心,一旦仓库停掉,流水线将被迫暂停,对开发交付的效率会产生重要影响;对于一些中大型企业组织,单实例的仓库性能也无法满足需求,为此高可用的Harbor势在必行。在设计Harbor HA方案之前,我们简单了解一下Harbor组成架构。

一个Harbor实例就是一组由docker-compose工具启动的容器服务,主要包括四个主要组件:

  • proxy
    实质就是一个反向代理nginx,负责流量路由分担到ui和registry上;

  • registry
    这里的registry就是原生的docker官方的registry镜像仓库,Harbor在内部内置了一个仓库,所有仓库的核心功能均是由registry完成的;

  • core service
    包含了ui、token和webhook服务;

  • job service
    主要用于镜像复制供。

同时,每个Harbor实例还启动了一个MySQL数据库容器,用于保存自身的配置和镜像管理相关的关系数据。

高可用系统一般考虑三方面:计算高可用、存储高可用和网络高可用。在这里我们不考虑网络高可用。基于Harbor的高可用仓库方案,这里列出两个。

img{512x368}

两个方案的共同点是计算高可用,都是通过lb实现的多主热运行,保证无单点;存储高可用则各有各的方案。一个使用了分布式共享存储,数据可靠性由共享存储provider提供;另外一个则需要harbor自身逻辑参与,通过镜像相互复制的方式保持数据的多副本。

两种方案各有优缺点,就看哪种更适合你的组织以及你手里的资源是否能满足方案的搭建要求。

方案1是Harbor开发团队推荐的标准方案,由于基于分布式共享存储,因此其scaling非常好;同样,由于多Harbor实例共享存储,因此可以保持数据是实时一致的。方案1的不足也是很明显的,第一:门槛高,需要具备共享存储provider;第二搭建难度要高于第二个基于镜像复制的方案。

方案2的优点就是首次搭建简单。不足也很多:scaling差,甚至是不能,一旦有三个或三个以上节点,可能就会出现“环形复制”;镜像复制需要时间,因此存在多节点上数据周期性不一致的情况;Harbor的镜像复制规则以Project为单位配置,因此一旦新增Project,需要在每个节点上手工维护复制规则,非常繁琐。因此,我们选择方案1。

我们来看一下方案1的细节: 这是一幅示意图。

  • 每个安放harbor实例的node都mount cephfs。ceph是目前最流行的分布式共享存储方案之一;
  • 每个node上的harbor实例(包含组件:ui、registry等)都volume mount node上的cephfs mount路径;
  • 通过Load Balance将request流量负载到各个harbor实例上;
  • 使用外部MySQL cluster替代每个Harbor实例内部自维护的那个MySQL容器;对于MySQL cluster,可以使用mysql galera cluster或MySQL5.7以上版本自带的Group Replication (MGR) 集群。
  • 通过外部Redis实现访问Harbor ui的session共享,这个功能是Harbor UI底层MVC框架-beego提供的。

接下来,我们就来看具体的部署步骤和细节。

环境和先决条件:

  • 三台VM(Ubuntu 16.04及以上版本);
  • CephFS、MySQL、Redis已就绪;
  • Harbor v1.1.0及以上版本;
  • 一个域名:hub.tonybai.com:8070。我们通过该域名和服务端口访问Harbor,我们可以通过dns解析多ip轮询实现最简单的Load balance,虽然不完美。

第一步:挂载cephfs

每个安装Harbor instance的节点都要mount cephfs的相关路径,步骤包括:

#安装cephfs内核驱动
apt install ceph-fs-common

# 修改/etc/fstab,添加挂载指令,保证节点重启依旧可以自动挂载cephfs
xx.xx.xx.xx:6789:/apps/harbor /mnt/cephfs/harbor ceph name=harbor,secretfile=/etc/ceph/a dmin.secret,noatime,_netdev 0 2

这里涉及一个密钥文件admin.secret,这个secret文件可以在ceph集群机器上使用ceph auth tool生成。

img{512x368}

前面提到过每个Harbor实例都是一组容器服务,这组容器启动所需的配置文件是在Harbor正式启动前由prepare脚本生成的,Prepare脚本生成过程的输入包括:harbor.cfg、docker-compose.yml和common/templates下的配置模板文件。这也是部署高可用Harbor的核心步骤,我们逐一来看。

第二步:修改harbor.cfg

我们使用域名访问Harbor,因此我们需要修改hostname配置项。注意如果要用域名访问,这里一定填写域名,否则如果这里使用的是Harbor node的IP,那么在后续会存在client端和server端仓库地址不一致的情况;

custom_crt=false 关闭 crt生成功能。注意:三个node关闭其中两个,留一个生成一套数字证书和私钥。

第三步:修改docker-compose.yml

docker-compose.yml是docker-compose工具标准配置文件,用于配置docker-compose即将启动的容器服务。针对该配置文件,我们主要做三点修改:

  • 修改volumes路径
    由/data/xxx 改为:/mnt/cephfs/harbor/data/xxx
  • 由于使用外部Mysql,因此需要删除mysql service以及其他 service对mysql service的依赖 (depends_on)
  • 修改对proxy外服务端口 ports: 8070:80

第四步:配置访问external mysql和redis

external mysql的配置在common/templates/adminserver/env中,我们用external Mysql的访问方式覆盖下面四项配置:

MYSQL_HOST=harbor_host
MYSQL_PORT=3306
MYSQL_USR=harbor
MYSQL_PWD=harbor_password

还有一个关键配置,那就是将RESET由false改为true。只有改为true,adminserver启动时,才能读取更新后的配置

RESET=true

Redis连接的配置在common/templates/ui/env中,我们需要新增一行:

_REDIS_URL=redis_ip:6379,100,password,0

第五步:prepare并启动harbor

执行prepare脚本生成harbor各容器服务的配置;在每个Harbor node上通过下面命令启动harbor实例:

docker-compose up -d

启动后,可以通过docker-compose ps命令查看harbor实例中各容器的启动状态。如果启动顺利,都是”Up”状态,那么我们可以在浏览器里输入:http://hub.tonybai.com:8070,不出意外的话,我们就可以看到Harbor ui的登录页面了。

至此,我们的高可用Harbor cluster搭建过程就告一段落了。

Troubleshooting

不过,对Harbor的认知还未结束,我们在后续使用Harbor的过程中遇到了一些问题,这里举两个例子。

问题1: docker login hub.tonybai.com:8070 failed

现象日志:

Error response from daemon: Get https://hub.tonybai.com:8070/v1/users/: http: server gave HTTP response to HTTPS client

通过错误日志分析应该是docker daemon与镜像仓库所用协议不一致导致。docker engine默认采用https协议访问仓库,但之前我们搭建的Harbor采用的是http协议提供服务,两者不一致。

解决方法有两种,这里列出第一种:让docker引擎通过http方式访问harbor仓库:

在/etc/docker/daemon.json中添加insecure-registry:

{
    "insecure-registries": ["hub.tonybai.com:8070"]
}

重启docker service生效

第二种方法就是让Harbor支持https,需要为harbor的proxy配置私钥和证书,位置:harbor.cfg中

#The path of cert and key files for nginx, they are applied only the protocol is set to https
ssl_cert = /data/cert/server.crt
ssl_cert_key = /data/cert/server.key

这里就不细说了。

问题2:docker login hub.tonybai.com:8070 有时成功,有时failed

现象日志:

第一次登录成功:
# docker login -u user -p passwd http://hub.tonybai.com:8070 Login Succeeded

第二次登录失败:
# docker login -u user -p passwd http://hub.tonybai.com:8070
Error response from daemon: login attempt to http://hub.tonybai.com:8070/v2/ failed with status: 401 Unauthorized

这个问题的原因在于对docker registry v2协议登录过程理解不够透彻。docker registry v2是一个两阶段登录的过程:

  • 首先:docker client会到registry去尝试登录,registry发现request中没有携带token,则返回失败应答401,并告诉客户端到哪里去获取token;
  • 客户端收到应答后,获取应答中携带的token service地址,然后到harbor的core services中的token service那里获取token(使用user, password进行校验)。一旦token service校验ok,则会使用private_key.pem生成一个token;
  • 客户端拿到token后,再次到registry那里去登录,这次registry用root.crt去校验客户端携带的token,校验通过,则login成功。

由于我们是一个harbor cluster,如果docker client访问的token service和registry是在一个harbor实例中的,那么login就会ok;否则docker client就会用harbor node1上token service生成的token到harbor node2上的registry去登录,由于harbor node2上root.crt与harbor node1上private_key.pem并非一对,因此登录失败

解决方法:将所有节点上使用同一套root.crt和private_key.pem。即将一个harbor node(harbor.cfg中custom_crt=true的那个)上的 common/config/ui/private_key.pem和 common/config/registry/root.crt复制到其他harbor node;然后重建各harbor实例中的容器。

至此,我们的高可用Harbor仓库部署完了。针对上面的配置过程,我还做了几个录屏文件,由于时间关系,这里不能播放了,大家可以在下面这个连接下载并自行播放收看。

Harbor install 录屏: https://pan.baidu.com/s/1o8JYKEe

谢谢大家!

讲稿slide可以在这里获取到。

微博:@tonybai_cn
微信公众号:iamtonybai
github.com: https://github.com/bigwhite

理解Kubernetes网络之Flannel网络

第一次采用kube-up.sh脚本方式安装Kubernetes cluster目前运行良好,master node上的组件状态也始终是“没毛病”:

# kubectl get cs
NAME                 STATUS    MESSAGE              ERROR
controller-manager   Healthy   ok
scheduler            Healthy   ok
etcd-0               Healthy   {"health": "true"}

不过在第二次尝试用kubeadm安装和初始化Kubernetes cluster时遇到的各种网络问题还是让我“心有余悸”。于是趁上个周末,对Kubernetes的网络原理进行了一些针对性的学习。这里把对Kubernetes网络的理解记录一下和大家一起分享。

Kubernetes支持FlannelCalicoWeave network等多种cni网络Drivers,但由于学习过程使用的是第一个cluster的Flannel网络,这里的网络原理只针对k8s+Flannel网络。

一、环境+提示

凡涉及到Docker、Kubernetes这类正在active dev的开源项目的文章,我都不得不提一嘴,那就是随着K8s以及flannel的演化,本文中的一些说法可能不再正确。提醒大家:阅读此类技术文章务必结合“环境”。

这里我们使用的环境就是我第一次建立k8s cluster的环境:

# kube-apiserver --version
Kubernetes v1.3.7

# /opt/bin/flanneld -version
0.5.5

# /opt/bin/etcd -version
etcd Version: 3.0.12
Git SHA: 2d1e2e8
Go Version: go1.6.3
Go OS/Arch: linux/amd64

另外整个集群搭建在阿里云上,每个ECS上的OS及kernel版本:Ubuntu 14.04.4 LTS,3.19.0-70-generic。

在我的测试环境,有两个node:master node和一个minion node。master node参与workload的调度。所以你基本可以认为有两个minion node即可。

二、Kubernetes Cluster中的几个“网络”

之前的k8s cluster采用的是默认安装,即直接使用了配置脚本中(kubernetes/cluster/ubuntu/config-default.sh)自带的一些参数,比如:

//摘自kubernetes/cluster/ubuntu/config-default.sh

export nodes=${nodes:-"root@master_node_ip root@minion_node_ip"}
export SERVICE_CLUSTER_IP_RANGE=${SERVICE_CLUSTER_IP_RANGE:-192.168.3.0/24}
export FLANNEL_NET=${FLANNEL_NET:-172.16.0.0/16}

从这里我们能够识别出三个“网络”:

  • node network:承载kubernetes集群中各个“物理”Node(master和minion)通信的网络;
  • service network:由kubernetes集群中的Services所组成的“网络”;
  • flannel network: 即Pod网络,集群中承载各个Pod相互通信的网络。

node network自不必多说,node间通过你的本地局域网(无论是物理的还是虚拟的)通信。

service network比较特殊,每个新创建的service会被分配一个service IP,在当前集群中,这个IP的分配范围是192.168.3.0/24。不过这个IP并不“真实”,更像一个“占位符”并且只有入口流量,所谓的“network”也是“名不符实”的,后续我们会详尽说明。

flannel network是我们要理解的重点,cluster中各个Pod要实现相互通信,必须走这个网络,无论是在同一node上的Pod还是跨node的Pod。我们的cluster中,flannel net的分配范围是:172.16.0.0/16。

在进一步挖掘“原理”之前,我们先来直观认知一下service network和flannel network:

Service network(看cluster-ip一列):

# kubectl get services
NAME           CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE
index-api      192.168.3.168   <none>        30080/TCP   18d
kubernetes     192.168.3.1     <none>        443/TCP     94d
my-nginx       192.168.3.179   <nodes>       80/TCP      90d
nginx-kit      192.168.3.196   <nodes>       80/TCP      12d
rbd-rest-api   192.168.3.22    <none>        8080/TCP    60d

Flannel network(看IP那列):

# kubectl get pod -o wide
NAME                           READY     STATUS    RESTARTS   AGE       IP            NODE
my-nginx-2395715568-gpljv      1/1       Running   6          91d       172.16.99.3   {master node ip}
nginx-kit-3872865736-rc8hr     2/2       Running   0          12d       172.16.57.7   {minion node ip}
... ...

三、平坦的Flannel网络

1、Kubenetes安装后的网络状态

首先让我们来看看:kube-up.sh在安装k8s集群时对各个K8s Node都动了什么手脚!

a) 修改docker default配置

在ubuntu 14.04下,docker的配置都在/etc/default/docker文件中。如果你曾经修改过该文件,那么kube-up.sh脚本方式安装完kubernetes后,你会发现/etc/default/docker已经变样了,只剩下了一行:

master node:
DOCKER_OPTS=" -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --bip=172.16.99.1/24 --mtu=1450"

minion node:
DOCKER_OPTS=" -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --bip=172.16.57.1/24 --mtu=1450"

可以看出kube-up.sh修改了Docker daemon的–bip选项,使得该node上docker daemon在该node的fannel subnet范围以内为启动的Docker container分配IP地址。

b) 在etcd中初始化flannel网络数据

多个node上的Flanneld依赖一个etcd cluster来做集中配置服务,etcd保证了所有node上flanned所看到的配置是一致的。同时每个node上的flanned监听etcd上的数据变化,实时感知集群中node的变化。

我们可以通过etcdctl查询到这些配置数据:

master node:

//flannel network配置
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} get  /coreos.com/network/config
{"Network":"172.16.0.0/16", "Backend": {"Type": "vxlan"}}

# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls  /coreos.com/network/subnets
/coreos.com/network/subnets/172.16.99.0-24
/coreos.com/network/subnets/172.16.57.0-24

//某一node上的flanne subnet和vtep配置
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} get  /coreos.com/network/subnets/172.16.99.0-24
{"PublicIP":"{master node ip}","BackendType":"vxlan","BackendData":{"VtepMAC":"b6:bf:4c:81:cf:3b"}}

minion node:
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} get  /coreos.com/network/subnets/172.16.57.0-24
{"PublicIP":"{minion node ip}","BackendType":"vxlan","BackendData":{"VtepMAC":"d6:51:2e:80:5c:69"}}

或用etcd 提供的rest api:

# curl -L http://127.0.0.1:{etcd listen port}/v2/keys/coreos.com/network/config
{"action":"get","node":{"key":"/coreos.com/network/config","value":"{\"Network\":\"172.16.0.0/16\", \"Backend\": {\"Type\": \"vxlan\"}}","modifiedIndex":5,"createdIndex":5}}
c) 启动flanneld

kube-up.sh在每个Kubernetes node上启动了一个flanneld的程序:

# ps -ef|grep flanneld

master node:
root      1151     1  0  2016 ?        00:02:34 /opt/bin/flanneld --etcd-endpoints=http://127.0.0.1:{etcd listen port} --ip-masq --iface={master node ip}

minion node:
root     11940     1  0  2016 ?        00:07:05 /opt/bin/flanneld --etcd-endpoints=http://{master node ip}:{etcd listen port} --ip-masq --iface={minion node ip}

一旦flanneld启动,它将从etcd中读取配置,并请求获取一个subnet lease(租约),有效期目前是24hrs,并且监视etcd的数据更新。flanneld一旦获取subnet租约、配置完backend,它会将一些信息写入/run/flannel/subnet.env文件。

master node:
# cat /run/flannel/subnet.env
FLANNEL_NETWORK=172.16.0.0/16
FLANNEL_SUBNET=172.16.99.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

minion node:
# cat /run/flannel/subnet.env
FLANNEL_NETWORK=172.16.0.0/16
FLANNEL_SUBNET=172.16.57.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

当然flanneld的最大意义在于根据etcd中存储的全cluster的subnet信息,跨node传输flannel network中的数据包,这个后面会详细说明。

d) 创建flannel.1 网络设备、更新路由信息

各个node上的网络设备列表新增一个名为flannel.1的类型为vxlan的网络设备:

master node:

# ip -d link show
4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/ether b6:bf:4c:81:cf:3b brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local {master node local ip} dev eth0 port 0 0 nolearning ageing 300

minion node:

349: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/ether d6:51:2e:80:5c:69 brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local  {minion node local ip} dev eth0 port 0 0 nolearning ageing 300

从flannel.1的设备信息来看,它似乎与eth0存在着某种bind关系。这是在其他bridge、veth设备描述信息中所没有的。

flannel.1设备的ip:

master node:

flannel.1 Link encap:Ethernet  HWaddr b6:bf:4c:81:cf:3b
          inet addr:172.16.99.0  Bcast:0.0.0.0  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1450  Metric:1
          RX packets:5993274 errors:0 dropped:0 overruns:0 frame:0
          TX packets:5829044 errors:0 dropped:292 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1689890445 (1.6 GB)  TX bytes:1144725704 (1.1 GB)

minion node:

flannel.1 Link encap:Ethernet  HWaddr d6:51:2e:80:5c:69
          inet addr:172.16.57.0  Bcast:0.0.0.0  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1450  Metric:1
          RX packets:6294640 errors:0 dropped:0 overruns:0 frame:0
          TX packets:5755599 errors:0 dropped:25 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:989362527 (989.3 MB)  TX bytes:1861492847 (1.8 GB)

可以看到两个node上的flannel.1的ip与k8s cluster为两个node上分配subnet的ip范围是对应的。

下面是两个node上的当前路由表:

master node:

# ip route
... ...
172.16.0.0/16 dev flannel.1  proto kernel  scope link  src 172.16.99.0
172.16.99.0/24 dev docker0  proto kernel  scope link  src 172.16.99.1
... ...

minion node:

# ip route
... ...
172.16.0.0/16 dev flannel.1
172.16.57.0/24 dev docker0  proto kernel  scope link  src 172.16.57.1
... ...

以上信息将为后续数据包传输分析打下基础。

e) 平坦的flannel network

从以上kubernetes和flannel network安装之后获得的网络信息,我们能看出flannel network是一个flat network。在flannel:172.16.0.0/16这个大网下,每个kubernetes node从中分配一个子网片段(/24):

master node:
  --bip=172.16.99.1/24

minion node:
  --bip=172.16.57.1/24

root@node1:~# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls  /coreos.com/network/subnets
/coreos.com/network/subnets/172.16.99.0-24
/coreos.com/network/subnets/172.16.57.0-24

用一张图来诠释可能更为直观:

img{512x368}

这个是不是有些像x86-64的虚拟内存寻址空间啊(同样是平坦内存地址访问模型)!

在平坦的flannel network中,每个pod都会被分配唯一的ip地址,且每个k8s node的subnet各不重叠,没有交集。不过这样的subnet分配模型也有一定弊端,那就是可能存在ip浪费:一个node上有200多个flannel ip地址(xxx.xxx.xxx.xxx/24),如果仅仅启动了几个Pod,那么其余ip就处于闲置状态。

2、Flannel网络通信原理

这里我们模仿flannel官方的那幅原理图,画了一幅与我们的实验环境匹配的图,作为后续讨论flannel网络通信流程的基础:

img{512x368}

如上图所示,我们来看看从pod1:172.16.99.8发出的数据包是如何到达pod3:172.16.57.15的(比如:在pod1的某个container中ping -c 3 172.16.57.15)。

a) 从Pod出发

由于k8s更改了docker的DOCKER_OPTS,显式指定了–bip,这个值与分配给该node上的subnet的范围是一致的。这样一来,docker引擎每次创建一个Docker container,该container被分配到的ip都在flannel subnet范围内。

当我们在Pod1下的某个容器内执行ping -c 3 172.16.57.15,数据包便开始了它在flannel network中的旅程。

Pod是Kubernetes调度的基本unit。Pod内的多个container共享一个network namespace。kubernetes在创建Pod时,首先先创建pause容器,然后再以pause的network namespace为基础,创建pod内的其他容器(–net=container:xxx),这样Pod内的所有容器便共享一个network namespace,这些容器间的访问直接通过localhost即可。比如Pod下A容器启动了一个服务,监听8080端口,那么同一个Pod下面的另外一个B容器通过访问localhost:8080即可访问到A容器下面的那个服务。

在之前的《理解Docker容器网络之Linux Network Namespace》一文中,我相信我已经讲清楚了单机下Docker容器数据传输的路径。在这个环节中,数据包的传输路径也并无不同。

我们看一下Pod1中某Container内的路由信息:

# docker exec ba75f81455c7 ip route
default via 172.16.99.1 dev eth0
172.16.99.0/24 dev eth0  proto kernel  scope link  src 172.16.99.8

目的地址172.16.57.15并不在直连网络中,因此数据包通过default路由出去。default路由的路由器地址是172.16.99.1,也就是上面的docker0 bridge的IP地址。相当于docker0 bridge以“三层的工作模式”直接接收到来自容器的数据包(而并非从bridge的二层端口接收)。

b) docker0与flannel.1之间的包转发

数据包到达docker0后,docker0的内核栈处理程序发现这个数据包的目的地址是172.16.57.15,并不是真的要送给自己,于是开始为该数据包找下一hop。根据master node上的路由表:

master node:

# ip route
... ...
172.16.0.0/16 dev flannel.1  proto kernel  scope link  src 172.16.99.0
172.16.99.0/24 dev docker0  proto kernel  scope link  src 172.16.99.1
... ...

我们匹配到“172.16.0.0/16”这条路由!这是一条直连路由,数据包被直接送到flannel.1设备上。

c) flannel.1设备以及flanneld的功用

flannel.1是否会重复docker0的套路呢:包不是发给自己,转发数据包?会,也不会。

“会”是指flannel.1肯定要将包转发出去,因为毕竟包不是给自己的(包目的ip是172.16.57.15, vxlan设备ip是172.16.99.0)。
“不会”是指flannel.1不会走寻常套路去转发包,因为它是一个vxlan类型的设备,也称为vtep,virtual tunnel end point。

那么它到底是怎么处理数据包的呢?这里涉及一些Linux内核对vxlan处理的内容,详细内容可参见本文末尾的参考资料。

flannel.1收到数据包后,由于自己不是目的地,也要尝试将数据包重新发送出去。数据包沿着网络协议栈向下流动,在二层时需要封二层以太包,填写目的mac地址,这时一般应该发出arp:”who is 172.16.57.15″。但vxlan设备的特殊性就在于它并没有真正在二层发出这个arp包,因为下面的这个内核参数设置:

master node:

# cat /proc/sys/net/ipv4/neigh/flannel.1/app_solicit
3

而是由linux kernel引发一个”L3 MISS”事件并将arp请求发到用户空间的flanned程序。

flanned程序收到”L3 MISS”内核事件以及arp请求(who is 172.16.57.15)后,并不会向外网发送arp request,而是尝试从etcd查找该地址匹配的子网的vtep信息。在前面章节我们曾经展示过etcd中Flannel network的配置信息:

master node:

# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls  /coreos.com/network/subnets
/coreos.com/network/subnets/172.16.99.0-24
/coreos.com/network/subnets/172.16.57.0-24

# curl -L http://127.0.0.1:{etcd listen port}/v2/keys/coreos.com/network/subnets/172.16.57.0-24
{"action":"get","node":{"key":"/coreos.com/network/subnets/172.16.57.0-24","value":"{\"PublicIP\":\"{minion node local ip}\",\"BackendType\":\"vxlan\",\"BackendData\":{\"VtepMAC\":\"d6:51:2e:80:5c:69\"}}","expiration":"2017-01-17T09:46:20.607339725Z","ttl":21496,"modifiedIndex":2275460,"createdIndex":2275460}}

flanneld从etcd中找到了答案:

subnet: 172.16.57.0/24
public ip: {minion node local ip}
VtepMAC: d6:51:2e:80:5c:69

我们查看minion node上的信息,发现minion node上的flannel.1 设备mac就是d6:51:2e:80:5c:69:

minion node:

#ip -d link show

349: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/ether d6:51:2e:80:5c:69 brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local 10.46.181.146 dev eth0 port 0 0 nolearning ageing 300

接下来,flanned将查询到的信息放入master node host的arp cache表中:

master node:

#ip n |grep 172.16.57.15
172.16.57.15 dev flannel.1 lladdr d6:51:2e:80:5c:69 REACHABLE

flanneld完成这项工作后,linux kernel就可以在arp table中找到 172.16.57.15对应的mac地址并封装二层以太包了。

到目前为止,已经呈现在大家眼前的封包如下图:

img{512x368}

不过这个封包还不能在物理网络上传输,因为它实际上只是vxlan tunnel上的packet。

d) kernel的vxlan封包

我们需要将上述的packet从master node传输到minion node,需要将上述packet再次封包。这个任务在backend为vxlan的flannel network中由linux kernel来完成。

flannel.1为vxlan设备,linux kernel可以自动识别,并将上面的packet进行vxlan封包处理。在这个封包过程中,kernel需要知道该数据包究竟发到哪个node上去。kernel需要查看node上的fdb(forwarding database)以获得上面对端vtep设备(已经从arp table中查到其mac地址:d6:51:2e:80:5c:69)所在的node地址。如果fdb中没有这个信息,那么kernel会向用户空间的flanned程序发起”L2 MISS”事件。flanneld收到该事件后,会查询etcd,获取该vtep设备对应的node的”Public IP“,并将信息注册到fdb中。

这样Kernel就可以顺利查询到该信息并封包了:

master node:

# bridge fdb show dev flannel.1|grep d6:51:2e:80:5c:69
d6:51:2e:80:5c:69 dst {minion node local ip} self permanent

由于目标ip是minion node,查找路由表,包应该从master node的eth0发出,这样src ip和src mac地址也就确定了。封好的包示意图如下:

img{512x368}

e) kernel的vxlan拆包

minion node上的eth0接收到上述vxlan包,kernel将识别出这是一个vxlan包,于是拆包后将flannel.1 packet转给minion node上的vtep(flannel.1)。minion node上的flannel.1再将这个数据包转到minion node上的docker0,继而由docker0传输到Pod3的某个容器里。

3、Pod内到外部网络

我们在Pod中除了可以与pod network中的其他pod通信外,还可以访问外部网络,比如:

master node:
# docker exec ba75f81455c7 ping -c 3 baidu.com
PING baidu.com (180.149.132.47): 56 data bytes
64 bytes from 180.149.132.47: icmp_seq=0 ttl=54 time=3.586 ms
64 bytes from 180.149.132.47: icmp_seq=1 ttl=54 time=3.752 ms
64 bytes from 180.149.132.47: icmp_seq=2 ttl=54 time=3.722 ms
--- baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 3.586/3.687/3.752/0.072 ms

这个通信与vxlan就没有什么关系了,主要是通过docker引擎在iptables的POSTROUTING chain中设置的MASQUERADE规则:

mastre node:

#iptables -t nat -nL
... ...
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.16.99.0/24       0.0.0.0/0
... ...

docker将容器的pod network地址伪装为node ip出去,包回来时再snat回容器的pod network地址,这样网络就通了。

四、”不真实”的Service网络

每当我们在k8s cluster中创建一个service,k8s cluster就会在–service-cluster-ip-range的范围内为service分配一个cluster-ip,比如本文开始时提到的:

# kubectl get services
NAME           CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE
index-api      192.168.3.168   <none>        30080/TCP   18d
kubernetes     192.168.3.1     <none>        443/TCP     94d
my-nginx       192.168.3.179   <nodes>       80/TCP      90d
nginx-kit      192.168.3.196   <nodes>       80/TCP      12d
rbd-rest-api   192.168.3.22    <none>        8080/TCP    60d

这个cluster-ip只是一个虚拟的ip,并不真实绑定某个物理网络设备或虚拟网络设备,仅仅存在于iptables的规则中:

Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */

# iptables -t nat -nL|grep 192.168.3
Chain KUBE-SERVICES (2 references)
target     prot opt source               destination
KUBE-SVC-XGLOHA7QRQ3V22RZ  tcp  --  0.0.0.0/0            192.168.3.182        /* kube-system/kubernetes-dashboard: cluster IP */ tcp dpt:80
KUBE-SVC-NPX46M4PTMTKRN6Y  tcp  --  0.0.0.0/0            192.168.3.1          /* default/kubernetes:https cluster IP */ tcp dpt:443
KUBE-SVC-AU252PRZZQGOERSG  tcp  --  0.0.0.0/0            192.168.3.22         /* default/rbd-rest-api: cluster IP */ tcp dpt:8080
KUBE-SVC-TCOU7JCQXEZGVUNU  udp  --  0.0.0.0/0            192.168.3.10         /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
KUBE-SVC-BEPXDJBUHFCSYIC3  tcp  --  0.0.0.0/0            192.168.3.179        /* default/my-nginx: cluster IP */ tcp dpt:80
KUBE-SVC-UQG6736T32JE3S7H  tcp  --  0.0.0.0/0            192.168.3.196        /* default/nginx-kit: cluster IP */ tcp dpt:80
KUBE-SVC-ERIFXISQEP7F7OF4  tcp  --  0.0.0.0/0            192.168.3.10         /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
... ...

可以看到在PREROUTING环节,k8s设置了一个target: KUBE-SERVICES。而KUBE-SERVICES下面又设置了许多target,一旦destination和dstport匹配,就会沿着chain进行处理。

比如:当我们在pod网络curl 192.168.3.22 8080时,匹配到下面的KUBE-SVC-AU252PRZZQGOERSG target:

KUBE-SVC-AU252PRZZQGOERSG  tcp  --  0.0.0.0/0            192.168.3.22         /* default/rbd-rest-api: cluster IP */ tcp dpt:8080

沿着target,我们看到”KUBE-SVC-AU252PRZZQGOERSG”对应的内容如下:

Chain KUBE-SVC-AU252PRZZQGOERSG (1 references)
target     prot opt source               destination
KUBE-SEP-I6L4LR53UYF7FORX  all  --  0.0.0.0/0            0.0.0.0/0            /* default/rbd-rest-api: */ statistic mode random probability 0.50000000000
KUBE-SEP-LBWOKUH4CUTN7XKH  all  --  0.0.0.0/0            0.0.0.0/0            /* default/rbd-rest-api: */

Chain KUBE-SEP-I6L4LR53UYF7FORX (1 references)
target     prot opt source               destination
KUBE-MARK-MASQ  all  --  172.16.99.6          0.0.0.0/0            /* default/rbd-rest-api: */
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            /* default/rbd-rest-api: */ tcp to:172.16.99.6:8080

Chain KUBE-SEP-LBWOKUH4CUTN7XKH (1 references)
target     prot opt source               destination
KUBE-MARK-MASQ  all  --  172.16.99.7          0.0.0.0/0            /* default/rbd-rest-api: */
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            /* default/rbd-rest-api: */ tcp to:172.16.99.7:8080

Chain KUBE-MARK-MASQ (17 references)
target     prot opt source               destination
MARK       all  --  0.0.0.0/0            0.0.0.0/0            MARK or 0x4000

请求被按5:5开的比例分发(起到负载均衡的作用)到KUBE-SEP-I6L4LR53UYF7FORX 和KUBE-SEP-LBWOKUH4CUTN7XKH,而这两个chain的处理方式都是一样的,那就是先做mark,然后做dnat,将service ip改为pod network中的Pod IP,进而请求被实际传输到某个service下面的pod中处理了。

五、参考资料




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:


如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:


以太币:


如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多