咱们目前生产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地址。