Kubernetes學習筆記之Calico Startup源碼解析

Overview

咱們目前生產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-L96less

  • Detecting IP address and Network to use for BGP.
  • Configuring the node resource with IP/AS information provided in the environment, or autodetected.
  • Creating default IP Pools for quick-start use.(能夠經過NO_DEFAULT_POOLS關閉,一個集羣就只須要一個IP Pool,
    不須要每一次初始化都去建立一次。不過官方代碼裏已經適配了若是集羣內有IP Pool,能夠跳過建立,因此也能夠不關閉。咱們生產k8s ansible部署這裏是選擇關閉,不關閉也不影響)

因此,初始化時只作一件事情:往calico裏寫入一個Node數據,供後續confd配置bird.cfg配置使用。看一下啓動腳本具體執行邏輯 L97-L223ide

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地址。

相關文章
相關標籤/搜索