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 在公司内网能够间接被拜访。
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 数据:
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 才对。
找到问题起因后,就欢快的解决了。
然而,又忽然想晓得,calico 是怎么写入 Node 数据的?代码原来在 calico 启动代码 startup.go 这里。
官网提供的 calico/node 容器里,会启动 bird/confd/felix 等多个过程,并且应用 runsvdir(相似 supervisor)来治理多个过程。容器启动时,也会进行运行初始化脚本,
配置在这里 L11-L13 :
# 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 脚本:
func main() {
// ...
if *runStartup {logrus.SetFormatter(&logutils.Formatter{Component: "startup"})
startup.Run()}
// ...
}
startup.go 脚本次要做了三件事件 L91-L96:
- 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-L223:
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 地址。