VIP PaaS在接近兩年時間里,基於kubernetes主要經歷四次網絡方案的變遷:
1. kubernetes + flannel
2. 基於Docker libnetwork的網絡定制
3. kubernetes + contiv + kube-haproxy
4. 應用容器IP固定
先簡單說一下背景,PaaS平台的應用管理包括應用配置管理,應用的運行態管理。一個應用的運行態對應kubernetes的一個Replication Controller(后面使用RC簡稱)和一個Service,應用實例對應kubernetes中的Pod, 我們基於這樣的管理方式,需要提供應用之間的相互調用,同時對部分應用要提供基於http/tcp的直接訪問。
首先說一下kubernetes + flannel。
flannel主要提供了跨主機間的容器通信;
在kubernetes的Pod、Service模型里,kube-proxy又借助iptables實現了Pod和Service間通信。
基於這種網絡訪問功能,我們平台提供了以下功能:
基於gorouter提供的平台域名的訪問 – watch k8s endpoints event管理router信息;
基於skydns並定制化kube2sky組件和kubelet,提供同一命名空間下應用(Pod)之間基於業務域名的訪問 – kube2sky基於k8s Service annotation解析並注冊域名信息、kubelet設置容器啟動時的domain search及外部dns;
實現容器tty訪問控制台 – 每台k8s node部署平台組件 tty agent(根據Pod所屬node信息, 建立對應k8s結點的tty連接);
網絡訪問關系圖如下:
在k8s + flannel的模型下,容器網絡是封閉子網,可以提供平台內部應用之間基於4層和7層的調用,同時對外部提供應用基於域名(工作在七層)的直接訪問,但無法滿足用戶在平台外部需要直接使用IP訪問的需求。
在flannel網絡穩定使用后,開始研究network plugin以使應用服務實例以public IP 方式供用戶直接使用。
當時docker的版本為1.8, 本身還不支持網絡插件.同時 kubernetes本身提供一套基於CNI的網絡插件, 但本身有bug[CNI delete invoked twice with non-infra container id #20379]。
於是我們嘗試從docker network plugin的角度入手,結合libnetwork從docker源碼的角度進行定制。
整個架構分為三層:
- Client Layer – Docker CLI和kubernetes(Docker client);
- Docker Layer – Docker daemon 並在代碼層面集成libnetwork(內置OVS driver);
- Controller Layer – ovsdb-server及network controller(自開發IPAM);
整體訪問結構圖:
整個方案包括以下三個流程:
1. 啟動Docker Daemon:
初始化network controller -> 加載OVS Driver -> OVS Driver調用libovsdb創建docker0-ovs Bridge -> OVS Driver將主機上的一物理網卡attach到docker0-ovs上;
2. 啟動容器:
OVS Driver 創建veth pair 用於連接network namespaces -> OVS Driver調用network controller獲取容器IP和VLAN Tag -> OVS Driver將veth pair的一端添加到docker0-ovs上,並設置VLAN Tag -> OVS Driver設置容器內interface的IP,Mac Address以及路由 -> 設置各network interface為up;
3. 停止容器:
OVS Driver調用network controller釋放容器IP -> 刪除network link -> OVS Driver調用libovsdb刪除port;
libnetwork工作完成了測試階段但沒有經歷上線,隨着Docker版本的推進,Docker1.9開始支持 contiv netplugin,我們開始研究contiv應用,在期間我們也完成了使用haproxy替換kube-proxy的開發[https://github.com/AdoHe/kube2haproxy],並最后采用docker1.10+contiv上線。
這里根據我們實際網絡訪問關系再描述下PaaS在contiv整體部署結構:
Kube-haproxy替代了kube-proxy,主要是提供服務ip的公共調用,同時避免了容器數量增加后帶來的iptables規則的大量增長,方便調試。
contiv帶來的方便是用戶可以根據實例IP直接進行訪問;我們在使用過程中整體比較穩定,中間出現過一次問題: 機房停電導致了部分IP的分配狀態不正確,而且contiv當時還沒有提供查看已分配IP的接口。
Docker 1.10版本支持指定IP啟動容器,並且由於部分應用對實例IP固定有需求,我們開始着手容器IP固定方案的設計與開發。
前面提到應用運行時,對應k8s內一個ReplicationController以及一個Service。 應用的重新部署目前采用的策略主要是重建策略。 重建的流程包括刪除RC及RC下所有Pod,更新並創建新的RC(kubernetes會根據RC配置產生新的POD)。
在默認的k8s+contiv的網絡環境下,容器(Pod)的IP網絡連接是由contiv network plugin來完成的, contiv master只實現了簡單的IP地址分配和回收,每次部署應用時,並不能保證Pod IP不變。所以我們引入了新的Pod層面的IPAM,以保證同一個應用多次發生部署時,Pod IP始終是不變的。
作為Pod層面的IPAM,我們把這一功能直接集成在了kubernetes。Pod作為k8s的最小調度單元,原有的k8s Pod Registry(主要負責處理所有與Pod以及Pod subresource相關的請求:Pod的增刪改查,Pod的綁定及狀態更新,exec/attach/log等操作) 並不支持在創建Pod時為Pod分配IP,Pod IP是通過獲取Pod Infra Container的IP來獲取的,而Pod Infra Container的IP即為contiv動態分配得來的。
Pod Registry 訪問設計圖:
在原有kubernetes代碼基礎上,我們修改了Pod結構(在PodSpec中加入PodIP)並重寫了Pod Registry 同時引入了兩個新的資源對象:
1. Pod IP Allocator: Pod IP Allocator是一個基於etcd的IP地址分配器,主要實現Pod IP的分配與回收。
Pod IP Allocator通過位圖記錄IP地址的分配情況,並且將該位圖持久化到Etcd;
2. Pod IP Recycler: Pod IP Recycler是一個基於etcd的IP地址回收站,也是實現PodConsistent IP的核心。Pod IP Recycler基於RC全名(namespace + RC name)記錄每一個應用曾經使用過的IP地址,並且在下一次部署的時候預先使用處於回收狀態的IP。
Pod IP Recycler只會回收通過RC創建的Pod的IP,通過其他controller或者直接創建的Pod的IP並不會記錄,所以通過這種方式創建的Pod的IP並不會保持不變; 同時Pod IP Recycle檢測每個已回收IP對象的TTL,目前設置的保留時間為一天。
這里對kubelet也進行了改造,主要包括根據Pod Spec中指定IP進行相關的容器創建(docker run加入IP指定)以及Pod刪除時釋放IP操作。
創建和刪除Pod的UML時序圖如下:
Pod的創建在PaaS里主要有兩種情形:
- 應用的第一次部署及擴容,這種情況主要是從IP pool中隨機分配;
- 應用的重新部署:在重新部署時,已經釋放的IP已根據RC全名存放於IP Recycle列表中,這里優先從回收列表中獲取IP,從而達到IP固定的效果。
整體刪除過程為:由PaaSNg或kube-controller-manager調用apiserver Pod Delete並設置DeletionTimestamp, kubelet監聽到刪除時間並獲取GracefulDeletiontime,刪除應用容器, 通知apiserver釋放IP(釋放IP時獲取Pod所屬RC,根據是否有對應RC 名稱決定是否存放在IP Recycle列表),刪除Pause Pod,通知apiserver 刪除Pod對象。
另外為了防止IP固定方案中可能出現的問題,我們在kubernetes中加入了額外的REST api: 包括對已分配IP的查詢,手動分配/釋放IP..。
對目前方案的總結:
容器IP固定方案已上線,運行基本沒問題,但穩定性有待提升。主要表現為偶然性不能在預期時間內停止舊Pod,從而無法釋放IP造成無法復用(初步原因是由於Docker偶爾的卡頓造成無法在規定時間內停止容器)。我們短期的work around是使用額外添加的REST apiss手動修復,后期IP固定方案會繼續加強穩定性並根據需求進行優化。