一個 Linux 容器能看見的“網(wǎng)絡椧挪ぃ”,實際上是被隔離在它自己的 Network Namespace 當中的华蜒。而所謂“網(wǎng)絡椪尬常”,就包括了:網(wǎng)卡(Network Interface)叭喜、回環(huán)設備(Loopback Device)贺拣、路由表(Routing Table)和iptables規(guī)則。對于一個進程來說捂蕴,這些要素譬涡,其實就構(gòu)成了它發(fā)起和響應網(wǎng)絡請求的基本環(huán)境。
作為一種最簡單的使容器與目標網(wǎng)絡聯(lián)通起來的方法啥辨,就是將他以某種形式借用宿主機的網(wǎng)絡棧涡匀。
借用宿主機網(wǎng)絡棧
容器可以聲明直接使用宿主機的網(wǎng)絡棧(–net=host),即:不開啟 Network Namespace溉知,比如:
$ docker run –d –net=host --name nginx-host nginx
在這種情況下陨瘩,這個容器啟動后腕够,直接監(jiān)聽的就是宿主機的80端口。像這樣直接使用宿主機網(wǎng)絡棧的方式舌劳,雖然可以為容器提供良好的網(wǎng)絡性能帚湘,但也會不可避免地引入共享網(wǎng)絡資源的問題,比如端口沖突甚淡。所以大诸,在大多數(shù)情況下,我們都希望容器進程能使用自己Network Namespace里的網(wǎng)絡棧贯卦,即:擁有屬于自己的IP地址和端口资柔。
Veth設備
這個被隔離的容器進程,要跟其他Network Namespace里的容器進程脸侥、甚至宿主機進行交互建邓,需要一個聯(lián)通到宿主機的連線盈厘。通過創(chuàng)建Veth設備可以解決這個問題:
- veth和其它的網(wǎng)絡設備都一樣睁枕,一端連接的是內(nèi)核協(xié)議棧
- veth設備是成對出現(xiàn)的,另一端兩個設備彼此相連
- 一個設備收到協(xié)議棧的數(shù)據(jù)發(fā)送請求后沸手,會將數(shù)據(jù)發(fā)送到另一個設備上去
基于以上幾點外遇,veth設備非常適合于作為連接不同Network Namespace的連線。
事實上契吉,我們進入容器看到的網(wǎng)卡跳仿,其實就是veth設備的一端,它的另一端在宿主機的主network namespace上捐晶。要讓容器或者pod具備獨立的網(wǎng)絡棧菲语,基本上都是從這個veth設備入手進行考慮,在宿主機上添加各種路由策略惑灵、網(wǎng)橋等山上,使容器流量去往正確的方向。
CNI是什么
CNI便是k8s用于實現(xiàn)以上任務的標準接口英支。通過編寫支持這套協(xié)議方法的插件佩憾,可以實現(xiàn)自己對k8s網(wǎng)絡的管理。包括:
- CNI Plugin負責給容器配置網(wǎng)絡干花,它包括兩個基本的接口:
配置網(wǎng)絡: AddNetwork(net NetworkConfig, rt RuntimeConf) (types.Result, error)
清理網(wǎng)絡: DelNetwork(net NetworkConfig, rt RuntimeConf) error - IPAM Plugin負責給容器分配IP地址妄帘,主要實現(xiàn)包括host-local和dhcp。而虎牙Athena容器云所實現(xiàn)的ip不變的方案池凄,便是通過在ipam plugin中控制ip的分配策略所實現(xiàn)抡驼。
以上兩種插件的支持,使得k8s的網(wǎng)絡可以支持各式各樣的管理模式肿仑,當前在業(yè)界也出現(xiàn)了大量的支持方案致盟,其中比較流行的比如flannel桑阶、calico等。
kubernetes配置了cni網(wǎng)絡插件后勾邦,其容器網(wǎng)絡創(chuàng)建流程為:
- kubernetes先創(chuàng)建pause容器生成對應的network namespace
- 調(diào)用網(wǎng)絡driver蚣录,因為配置的是CNI,所以會調(diào)用CNI相關代碼
- CNI driver根據(jù)配置調(diào)用具體的CNI插件
- CNI插件給pause容器配置正確的網(wǎng)絡眷篇,pod中其他的容器都是用pause的網(wǎng)絡
其中萎河,CNI Plugin是獨立的可執(zhí)行文件,被上層的容器管理平臺(kubelet)調(diào)用蕉饼。網(wǎng)絡插件只有兩件事情要做:把容器加入到網(wǎng)絡(AddNetwork)以及把容器從網(wǎng)絡中刪除(DelNetwork)虐杯。
對應于具體的CNI框架內(nèi)的實現(xiàn),即兩個基本操作ADD和DEL昧港,前者用于加入容器網(wǎng)絡擎椰,后者用于從容器網(wǎng)絡中退出。
當CNI插件被調(diào)用時创肥,首先進入main函數(shù)达舒,main函數(shù)會對環(huán)境變量和標準輸入中的配置信息進行解析,接著根據(jù)解析得到的操作方式(ADD或DEL)叹侄,轉(zhuǎn)入具體的執(zhí)行函數(shù)完成網(wǎng)絡的配置工作巩搏。如果是ADD操作,則調(diào)用cmdAdd()函數(shù)趾代,反之贯底,如果是DEL操作,則調(diào)用cmdDel()函數(shù)撒强。從宏觀角度來看禽捆,以上為CNI插件的實現(xiàn)框架主體。
編寫自己的cni插件
按照上述說法飘哨,由于cni插件是kubelet以二進制的形式調(diào)用的胚想,具體實現(xiàn)上主體為cmdAdd,cmdDel兩大函數(shù)。
package main
import (
...
)
const (
...
)
func cmdAdd(args *skel.CmdArgs) error {
conf := types.NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
log.Errorf("Error loading config from args: %v", err)
return errors.Wrap(err, "add cmd: error loading config from args")
}
versionDecoder := &cniversion.ConfigDecoder{}
confVersion, err := versionDecoder.Decode(args.StdinData)
if err != nil {
return err
}
// 在此實現(xiàn):
// 1. 調(diào)用ipam plugin接口進行ip申請
// 2. 容器及宿主機各自網(wǎng)絡棧內(nèi)的操作杖玲,如創(chuàng)建veth顿仇,配置ip地址,配置路由等
ips := []*current.IPConfig{{Version: "4", Address: *ipnet}}
result := ¤t.Result{
IPs: ips,
}
return cnitypes.PrintResult(result, confVersion)
}
func cmdDel(args *skel.CmdArgs) error {
conf := types.NetConf{}
if err := json.Unmarshal(args.StdinData, &conf); err != nil {
log.Errorf("Error loading config from args: %v", err)
return errors.Wrap(err, "add cmd: error loading config from args")
}
versionDecoder := &cniversion.ConfigDecoder{}
confVersion, err := versionDecoder.Decode(args.StdinData)
if err != nil {
return err
}
// 在此實現(xiàn):
// 1. 調(diào)用ipam plugin接口進行ip釋放
// 2. 容器及宿主機各自網(wǎng)絡棧內(nèi)的操作摆马,如刪除veth臼闻,刪除路由等
return nil
}
func main() {
log.SetLevel(log.DebugLevel)
ConfigLocalFilesystemLogger(logPath, 24*60*time.Hour, 24*time.Hour)
exitCode := 0
if e := skel.PluginMainWithError(cmdAdd, nil, cmdDel, cniversion.All, "<版本說明等信息>"); e != nil {
exitCode = 1
log.Error("Failed CNI request: ", e)
if err := e.Print(); err != nil {
log.Error("Error writing error JSON to stdout: ", err)
}
}
os.Exit(exitCode)
}
至于其中ipam接口如何調(diào)用,則不一定按照官方的ipam plugin規(guī)范編寫囤采,甚至可將ipam相關邏輯結(jié)合到cni plugin中述呐。也可以獨立服務,并通過API蕉毯、RPC等方式調(diào)用乓搬。
坑在哪
本文介紹了容器網(wǎng)絡CNI插件的原理思犁,簡化后的代碼接口清晰簡潔,主體流程圍繞在:解析參數(shù)进肯、執(zhí)行add/del激蹲、返回結(jié)果上,按照CNI規(guī)范的結(jié)構(gòu)編寫配置江掩、入?yún)⒀琛⒎祷刂担蓪崿F(xiàn)簡單CNI邏輯环形。
但CNI特點在于具體的網(wǎng)絡配置策泣、網(wǎng)絡連通方案、ip分配方案可由插件自定義抬吟,k8s僅負責調(diào)用萨咕。因此CNI的出現(xiàn),使k8s網(wǎng)絡架構(gòu)更為靈活多變火本,也引入了更加復雜的因素:
- 因為k8s與容器網(wǎng)絡的相對分離危队,容易形成容器的聲明周期與其網(wǎng)絡資源的生命周期相脫節(jié)
kubelet通過pleg進行的pod聲明周期管理,使pod可控发侵,但容器網(wǎng)絡不可控交掏。CNI的編寫上妆偏,需要支持上層(kubelet)調(diào)用的多次重入及冪等刃鳄,且pod與cni無強依賴,容易導致容器網(wǎng)絡遺漏钱骂、殘留等問題發(fā)生叔锐。
因此,在cni插件編寫上见秽,需進行更多的一致性校驗及補漏愉烙、清理工作。