咱們目前生產k8s和calico使用ansible二進制部署在私有機房,沒有使用官方的calico/node容器部署,而且由於沒有使用network policy只部署了confd/bird進程服務,沒有部署felix。
採用BGP(Border Gateway Protocol)方式來部署網絡,而且採用 Peered with TOR (Top of Rack) routers
方式部署,每個worker node和其置頂交換機創建bgp peer配對,置頂交換機會繼續和上層核心交換機創建bgp peer配對,這樣能夠保證pod ip在公司內網能夠直接被訪問。node
BGP: 主要是網絡之間分發動態路由的一個協議,使用TCP協議傳輸數據。好比,交換機A下連着12臺worker node,能夠在每一臺worker node上安裝一個BGP Client,如Bird或GoBGP程序,
這樣每一臺worker node會把本身的路由分發給交換機A,交換機A會作路由聚合,以及繼續向上一層核心交換機轉發。交換機A上的路由是Node級別,而不是Pod級別的。
平時在維護k8s雲平臺時,有時發現一臺worker節點上的全部pod ip在集羣外無法訪問,通過排查發現是該worker節點有兩張內網網卡eth0和eth1,eth0 IP地址和交換機創建BGP
鏈接,並獲取其as number號,可是bird啓動配置文件bird.cfg裏使用的eth1網卡IP地址。而且發現calico裏的 Node
數據的IP地址ipv4Address和 BGPPeer 數據的交換機地址peerIP也對不上。能夠經過以下命令獲取calico數據:git
calicoctl get node ${nodeName} -o yaml calicoctl get bgppeer ${peerName} -o yaml
一番抓頭撓腮後,找到根本緣由是咱們的ansible部署時,在調用網絡API獲取交換機的bgp peer的as number和peer ip數據時,使用的是eth0地址,
而且經過ansible任務calicoctl apply -f bgp_peer.yaml
寫入 Node-specific BGP Peer數據,
寫入calico BGP Peer數據裏使用的是eth0交換機地址。可是ansible任務跑到配置bird.cfg配置文件時,環境變量IP使用的是eth1 interface,
寫入calico Node數據使用的是eth1網卡地址,而後被confd進程讀取Node數據生成bird.cfg文件時,使用的就會是eth1網卡地址。這裏應該是使用eth0纔對。github
找到問題緣由後,就愉快的解決了。shell
可是,又忽然想知道,calico是怎麼寫入Node數據的?代碼原來在calico啓動代碼 startup.go 這裏。
官方提供的calico/node容器裏,會啓動bird/confd/felix等多個進程,而且使用runsvdir(相似supervisor)來管理多個進程。容器啓動時,也會進行運行初始化腳本,
配置在這裏 L11-L13 :api
# Run the startup initialisation script. # These ensure the node is correctly configured to run. calico-node -startup || exit 1
因此,能夠看下初始化腳本作了什麼工做。網絡
當運行calico-node -startup
命令時,實際上會執行 L111-L113 ,
也就是starup模塊下的startup.go腳本:app
func main() { // ... if *runStartup { logrus.SetFormatter(&logutils.Formatter{Component: "startup"}) startup.Run() } // ... }
startup.go腳本主要作了三件事情 L91-L96 :less
因此,初始化時只作一件事情:往calico裏寫入一個Node數據,供後續confd配置bird.cfg配置使用。看一下啓動腳本具體執行邏輯 L97-L223 :ide
func Run() { // ... // 從NODENAME、HOSTNAME等環境變量或者CALICO_NODENAME_FILE文件內,讀取當前宿主機名字 nodeName := determineNodeName() // 建立CalicoClient: // 若是DATASTORE_TYPE使用kubernetes,只須要傳KUBECONFIG變量值就行,若是k8s pod部署,都不須要傳,這樣就和建立 // KubernetesClient同樣道理,能夠參考calicoctl的配置文檔:https://docs.projectcalico.org/getting-started/clis/calicoctl/configure/kdd // 若是DATASTORE_TYPE使用etcdv3,還得配置etcd相關的環境變量值,能夠參考: https://docs.projectcalico.org/getting-started/clis/calicoctl/configure/etcd // 平時本地編寫calico測試代碼時,能夠在~/.zshrc里加上環境變量,能夠參考 https://docs.projectcalico.org/getting-started/clis/calicoctl/configure/kdd#example-using-environment-variables : // export CALICO_DATASTORE_TYPE=kubernetes // export CALICO_KUBECONFIG=~/.kube/config cfg, cli := calicoclient.CreateClient() // ... if os.Getenv("WAIT_FOR_DATASTORE") == "true" { // 經過c.Nodes.Get("foo")來測試下是否能正常調用 waitForConnection(ctx, cli) } // ... // 從calico中查詢nodeName的Node數據,若是沒有則構造個新Node對象 // 後面會用該宿主機的IP地址來更新該Node對象 node := getNode(ctx, cli, nodeName) var clientset *kubernetes.Clientset var kubeadmConfig, rancherState *v1.ConfigMap // If running under kubernetes with secrets to call k8s API if config, err := rest.InClusterConfig(); err == nil { // 若是是kubeadm或rancher部署的k8s集羣,讀取kubeadm-config或full-cluster-state ConfigMap值 // 爲後面配置ClusterType變量以及建立IPPool使用 // 咱們生產k8s目前沒使用這兩種方式 // ... } // 這裏邏輯是關鍵,這裏會配置Node對象的spec.bgp.ipv4Address地址,並且獲取ipv4地址策略多種方式 // 能夠直接給IP環境變量本身指定一個具體地址如10.203.10.20,也能夠給IP環境變量指定"autodetect"自動檢測 // 而自動檢測策略是根據"IP_AUTODETECTION_METHOD"環境變量配置的,有can-reach或interface=eth.*等等, // 具體自動檢測策略能夠參考:https://docs.projectcalico.org/archive/v3.17/networking/ip-autodetection // 咱們的生產k8s是在ansible里根據變量獲取eth{$interface}的ipv4地址給IP環境變量,而若是機器是雙內網網卡,不論是選擇eth0仍是eth1地址 // 要和建立bgp peer時使用的網卡要保持一致,另外還得看這臺機器默認網關地址是eth0仍是eth1的默認網關 // 有關具體如何獲取IP地址,下文詳解 configureAndCheckIPAddressSubnets(ctx, cli, node) // 咱們使用bird,這裏CALICO_NETWORKING_BACKEND配置bird if os.Getenv("CALICO_NETWORKING_BACKEND") != "none" { // 這裏從環境變量AS中查詢,能夠給個默認值65188,不影響 configureASNumber(node) if clientset != nil { // 若是是選擇官方那種calico/node集羣內部署,這裏會patch下k8s的當前Node的 NetworkUnavailable Condition,意思是網絡當前不可用 // 能夠參考https://kubernetes.io/docs/concepts/architecture/nodes/#condition // 目前咱們生產k8s沒有calico/node集羣內部署,因此不會走這一步邏輯,而且咱們生產k8s版本太低,Node Conditions裏也沒有NetworkUnavailable Condition err := setNodeNetworkUnavailableFalse(*clientset, nodeName) // ... } } // 配置下node.Spec.OrchRefs爲k8s,值從CALICO_K8S_NODE_REF環境變量裏讀取 configureNodeRef(node) // 建立/var/run/calico、/var/lib/calico和/var/log/calico等目錄 ensureFilesystemAsExpected() // calico Node對象已經準備好了,能夠建立或更新Node對象 // 這裏是啓動腳本的最核心邏輯,以上都是爲了查詢Node對象相關的配置數據,主要做用就是爲了初始化時建立或更新Node對象 if _, err := CreateOrUpdate(ctx, cli, node); err != nil { // ... } // 配置集羣的IP Pool,即整個集羣的pod cidr網段,若是使用/18網段,每個k8s worker Node使用/27子網段,那就是集羣最多能夠部署2^(27-18)=512 // 臺機器,每臺機器能夠分配2^(32-27)=32-首位兩個地址=30個pod。 configureIPPools(ctx, cli, kubeadmConfig) // 這裏主要寫一個名字爲default的全局FelixConfiguration對象,以及DatastoreType不是kubernetes,就會對於每個Node寫一個該Node的 // 默認配置的FelixConfiguration對象。 // 咱們生產k8s使用etcdv3,因此初始化時會看到calico數據裏會有每個Node的FelixConfiguration對象。另外,咱們沒使用felix,不須要太關注felix數據。 if err := ensureDefaultConfig(ctx, cfg, cli, node, getOSType(), kubeadmConfig, rancherState); err != nil { log.WithError(err).Errorf("Unable to set global default configuration") terminate() } // 把nodeName寫到CALICO_NODENAME_FILE環境變量指定的文件內 writeNodeConfig(nodeName) // ... } // 從calico中查詢nodeName的Node數據,若是沒有則構造個新Node對象 func getNode(ctx context.Context, client client.Interface, nodeName string) *api.Node { node, err := client.Nodes().Get(ctx, nodeName, options.GetOptions{}) // ... if err != nil { // ... node = api.NewNode() node.Name = nodeName } return node } // 建立或更新Node對象 func CreateOrUpdate(ctx context.Context, client client.Interface, node *api.Node) (*api.Node, error) { if node.ResourceVersion != "" { return client.Nodes().Update(ctx, node, options.SetOptions{}) } return client.Nodes().Create(ctx, node, options.SetOptions{}) }
經過上面代碼分析,有兩個關鍵邏輯須要仔細看下:一個是獲取當前機器的IP地址;一個是配置集羣的pod cidr。學習
這裏先看下配置集羣pod cidr邏輯 L858-L1050 :
// configureIPPools ensures that default IP pools are created (unless explicitly requested otherwise). func configureIPPools(ctx context.Context, client client.Interface, kubeadmConfig *v1.ConfigMap) { // Read in environment variables for use here and later. ipv4Pool := os.Getenv("CALICO_IPV4POOL_CIDR") ipv6Pool := os.Getenv("CALICO_IPV6POOL_CIDR") if strings.ToLower(os.Getenv("NO_DEFAULT_POOLS")) == "true" { // ... return } // ... // 從CALICO_IPV4POOL_BLOCK_SIZE環境變量中讀取block size,即你的網段要分配的子網段掩碼是多少,好比這裏默認值是/26 // 若是選擇默認的192.168.0.0/16 ip pool,而分配給每一個Node子網是/26網段,那集羣能夠部署2^(26-16)=1024臺機器了 ipv4BlockSizeEnvVar := os.Getenv("CALICO_IPV4POOL_BLOCK_SIZE") if ipv4BlockSizeEnvVar != "" { ipv4BlockSize = parseBlockSizeEnvironment(ipv4BlockSizeEnvVar) } else { // DEFAULT_IPV4_POOL_BLOCK_SIZE爲默認26子網段 ipv4BlockSize = DEFAULT_IPV4_POOL_BLOCK_SIZE } // ... // Get a list of all IP Pools poolList, err := client.IPPools().List(ctx, options.ListOptions{}) // ... // Check for IPv4 and IPv6 pools. ipv4Present := false ipv6Present := false for _, p := range poolList.Items { ip, _, err := cnet.ParseCIDR(p.Spec.CIDR) if err != nil { log.Warnf("Error parsing CIDR '%s'. Skipping the IPPool.", p.Spec.CIDR) } version := ip.Version() ipv4Present = ipv4Present || (version == 4) ipv6Present = ipv6Present || (version == 6) // 這裏官方作了適配,若是集羣內有ip pool,後面邏輯就不會調用createIPPool()建立ip pool if ipv4Present && ipv6Present { break } } if ipv4Pool == "" { // 若是沒配置pod網段,給個默認網段"192.168.0.0/16" ipv4Pool = DEFAULT_IPV4_POOL_CIDR // ... } // ... // 集羣內已經有ip pool,這裏就不會重複建立 if !ipv4Present { log.Debug("Create default IPv4 IP pool") outgoingNATEnabled := evaluateENVBool("CALICO_IPV4POOL_NAT_OUTGOING", true) createIPPool(ctx, client, ipv4Cidr, DEFAULT_IPV4_POOL_NAME, ipv4IpipModeEnvVar, ipv4VXLANModeEnvVar, outgoingNATEnabled, ipv4BlockSize, ipv4NodeSelector) } // ... 省略ipv6邏輯 } // 建立ip pool func createIPPool(ctx context.Context, client client.Interface, cidr *cnet.IPNet, poolName, ipipModeName, vxlanModeName string, isNATOutgoingEnabled bool, blockSize int, nodeSelector string) { //... pool := &api.IPPool{ ObjectMeta: metav1.ObjectMeta{ Name: poolName, }, Spec: api.IPPoolSpec{ CIDR: cidr.String(), NATOutgoing: isNATOutgoingEnabled, IPIPMode: ipipMode, // 由於咱們生產使用bgp,這裏ipipMode值是never VXLANMode: vxlanMode, BlockSize: blockSize, NodeSelector: nodeSelector, }, } // 建立ip pool if _, err := client.IPPools().Create(ctx, pool, options.SetOptions{}); err != nil { // ... } }
而後看下自動獲取IP地址的邏輯 L498-L585 :
// 給Node對象配置IPv4Address地址 func configureIPsAndSubnets(node *api.Node) (bool, error) { // ... oldIpv4 := node.Spec.BGP.IPv4Address // 從IP環境變量獲取IP地址,咱們生產k8s ansible直接讀取的網卡地址,可是對於雙內網網卡,有時這裏讀取IP地址時, // 會和bgp_peer.yaml裏採用的IP地址會不同,咱們目前生產的bgp_peer.yaml裏默認採用eth0的地址,寫死的(由於咱們機器網關地址默認都是eth0的網關), // 因此這裏的IP必定得是eth0的地址。 ipv4Env := os.Getenv("IP") if ipv4Env == "autodetect" || (ipv4Env == "" && node.Spec.BGP.IPv4Address == "") { adm := os.Getenv("IP_AUTODETECTION_METHOD") // 這裏根據自動檢測策略來判斷選擇哪一個網卡地址,比較簡單不贅述,能夠看代碼 **[L701-L746](https://github.com/projectcalico/node/blob/release-v3.17/pkg/startup/startup.go#L701-L746)** // 和配置文檔 **[ip-autodetection](https://docs.projectcalico.org/archive/v3.17/networking/ip-autodetection)** , // 若是使用calico/node在k8s內部署,根據一些討論言論,貌似使用can-reach=xxx能夠少踩不少坑 cidr := autoDetectCIDR(adm, 4) if cidr != nil { // We autodetected an IPv4 address so update the value in the node. node.Spec.BGP.IPv4Address = cidr.String() } else if node.Spec.BGP.IPv4Address == "" { return false, fmt.Errorf("Failed to autodetect an IPv4 address") } else { // ... } } else if ipv4Env == "none" && node.Spec.BGP.IPv4Address != "" { log.Infof("Autodetection for IPv4 disabled, keeping existing value: %s", node.Spec.BGP.IPv4Address) validateIP(node.Spec.BGP.IPv4Address) } else if ipv4Env != "none" { // 咱們生產k8s ansible走的是這個邏輯,並且直接取的是eth0的IP地址,subnet會默認被設置爲/32 // 能夠參考官網文檔:https://docs.projectcalico.org/archive/v3.17/networking/ip-autodetection#manually-configure-ip-address-and-subnet-for-a-node if ipv4Env != "" { node.Spec.BGP.IPv4Address = parseIPEnvironment("IP", ipv4Env, 4) } validateIP(node.Spec.BGP.IPv4Address) } // ... // Detect if we've seen the IP address change, and flag that we need to check for conflicting Nodes if node.Spec.BGP.IPv4Address != oldIpv4 { log.Info("Node IPv4 changed, will check for conflicts") return true, nil } return false, nil }
以上就是calico啓動腳本執行邏輯,比較簡單,可是學習了其代碼邏輯以後,對問題排查會更加駕輕就熟,不然只能傻瓜式的亂猜,
儘管碰巧解決了問題可是不知道爲何,後面再次遇到相似問題仍是不知道怎麼解決,浪費時間。
本文主要學習了下calico啓動腳本執行邏輯,主要是往calico裏寫部署宿主機的Node數據,容易出錯的地方是機器雙網卡時可能會出現Node和BGPPeer數據不一致,
bird無法分發路由,致使該機器的pod地址無法集羣外和集羣內被路由到。
目前咱們生產calico用的ansible二進制部署,經過日誌排查也不方便,仍是推薦calico/node容器化部署在k8s內,調用網絡API與交換機bgp peer配對時,獲取相關數據邏輯,
能夠放在initContainers裏,而後calicoctl apply -f bgp_peer.yaml
寫到calico裏。固然,不排除中間會踩很多坑,以及時間精力問題。
總之,calico是一個優秀的k8s cni實現,使用成熟方案BGP協議來分發路由,數據包走三層路由且中間沒有SNAT/DNAT操做,也很是容易理解其原理過程。後續,會寫一寫kubelet在建立sandbox容器的network namespace時,如何調用calico命令來建立相關網絡對象和網卡,以及使用calico-ipam來分配當前Node節點的子網段和給pod分配ip地址。