网卡驱动升级导致容器网络设备丢失问题
背景
线上网络采用macvlan方案。线上宿主创建的vlan id是3 ,对应的接口是eth0.3 或 bond4.3
现象描述
i40e网卡升级驱动,理论上不需要重启宿主,几秒后即可自动恢复。但是线上宿主升级驱动后,容器内的网络设备却丢失了,当前场景下kubelet 会自动重建容器,但是重建容器成本较高,尤其对于静态容器而言。理论上网络设备可以自行创建并添加到对应的namespace。
简单描述就是网卡驱动升级后,容器关联的网络接口消失,容器被重建。
1.复现与分析
测试环境找到一台同类型的网卡宿主,升级网卡驱动后,ifconfig命令检查发现宿主上创建的接口eth0.3 或 bond4.3 会消失,容器内也只有一个lo接口,eth0接口消失。
之后容器会被重建,重建是因为kubelet会做容器sandbox是否改变的检查,发现sandbox有变化,就会重建sandbox和容器。校验代码如下
func (m *kubeGenericRuntimeManager) podSandboxChanged(pod *v1.Pod, podStatus *kubecontainer.PodStatus) (bool, uint32, string) {
// 检查容器是否存在对应的sandbox
if len(podStatus.SandboxStatuses) == 0 {
glog.V(2).Infof("No sandbox for pod %q can be found. Need to start a new one", format.Pod(pod))
return true, 0, ""
}
// 检查sandbox status / state
// 检查net ns
...
// 因为升级驱动,网卡接口都丢失了,所以这里IP不存在的,这就是升级网卡驱动后,kubelet会重建宿主上容器的原因
// Needs to create a new sandbox when the sandbox does not have an IP address.
if !kubecontainer.IsHostNetworkPod(pod) && sandboxStatus.Network.Ip == "" {
glog.V(2).Infof("Sandbox for pod %q has no IP address. Need to start a new one", format.Pod(pod))
return true, sandboxStatus.Metadata.Attempt + 1, sandboxStatus.Id
}
...
}
2.尝试只重建sandbox
如果修改kubelet代码,当发现sandbox变化后只重建sandbox是否可以避免这个问题? 这样修改会使业务容器和sandbox处于不同networks ns,造成mac地址冲突,而且会使得net ns泄漏。
3.重新梳理
那这样剩下的方案就是,主动创建丢失的网络接口,并将容器加入到对应的net ns 中。 当然这个过程中需要停止kubelet,防止我们修复完成前,kubelet将容器重建。重新梳理修复的步骤大致如下
-
查找当前running 容器的net ns,
记录每个net ns 的ip/mac/gateway/route信息等
停止kubelet
升级宿主网卡驱动
给每个丢失网络接口的net ns添加接口,
为接口设置对应的ip mac ,添加路由信息
检查网络连接
启动kubelet
4.测试与验证
前置说明
业务容器ID 9220d55cfcfb
pause容器ID 2f843b3a7858 也是netns的ID
-
停止kubelet
- 查看容器内的网络设备,并执行驱动升级。
- 执行网卡驱动升级程序后查看容器内网络接口。
- 尝试添加网络设备后,查看容器内接口。此时ip与驱动升级前相同,但是mac地址已经发生了变化。
- 检查网络连接。
- 添加默认路由再次检查网络连接。
- 启动kubelet并检查容器是否发生重建。
其中执行的修复命令如下
ip link add link eth0 name eth0.3 type vlan id 3
ip link set dev eth0.3 up
ip link add macvlan1 link eth0.3 type macvlan mode bridge
ip link set macvlan1 netns 2f843b3a7858
ip netns exec 2f843b3a7858 ip link set macvlan1 name eth0
ip net exec 2f843b3a7858 ip addr add 10.85.223.57/24 dev eth0
ip net exec 2f843b3a7858 ifconfig eth0 up
ip net exec 2f843b3a7858 route add -net 0.0.0.0 netmask 0.0.0.0 gw 10.85.223.1 dev eth0
ip net exec 2f843b3a7858 route add -net 10.85.223.0 netmask 255.255.255.0 dev eth0
5.主要实现
线上使用的cni-plugin 会保存每个netns的ip/mac/gateway/route等信息。创建sandbox时,cni-plugin会创建checkpoint信息,存放在cni-plugin的checkpoints 目录下。
func cmdAdd(args *skel.CmdArgs) error {
...
createCheckpoint(n.CheckDir, &checkpoint{
Netns: args.Netns,
Podname: string(ipamargs.K8S_POD_NAME),
Sandbox: string(ipamargs.K8S_POD_INFRA_CONTAINER_ID),
Ifname: args.IfName,
Result: result,
})
...
}
cni-plugin的目录结构
├── acllogs
│ ├── ..
├── bin
│ ├── ...
├── checkpoints
│ ├── 06c9b8d0a800
│ ├── 09920c78d880
│ ├── 0add59081941
│ ├── 0f5eba98c4fb
│ ├── 1355497be5ea
│ ├── 1766785621e3
│ ├── 1bc7b3f72acf
│ ├── 1d8e4c069d08
│ ├── 2f843b3a7858
│ ├── 34abfa833f9d
│ ├── 3a4392ccdf53
│ ├── 3d6f878ccbc1
│ ├── 435832b14df3
│ ├── 49ba75959910
│ ├── 54190050dfe7
│ └── e426a9e06f95
├── conf
│ ├── ...
└── logs
├── ...
└── ...
cat 06c9b8d0a800 | jq
{
"netns": "/proc/111576/ns/net",
"podname": "xxxx",
"sandbox": "06c9b8d0a800735982222588c28e70140820acfd04a120264b87a20306322f55",
"ifname": "eth0",
"result": {
"interfaces": [
{
"name": "eth0",
"mac": "6e:92:1a:b4:fc:34",
"sandbox": "/proc/111576/ns/net"
}
],
"ips": [
{
"version": "4",
"interface": 0,
"address": "10.161.75.155/22",
"gateway": "10.161.72.1"
}
],
"routes": [
{
"dst": "0.0.0.0/0",
"gw": "10.161.72.1"
}
],
"dns": {}
}
}
这个checkpoint是线上cni-plugin定制的,与kubelet 创建sandbox的checkpoint不同。 kubelet给sanbox创建的checkpoint主要是version、podname、host port/container port的校验数据和校验和。这个checkpoint file在社区版的cni-plugin也是没有的。
有了这个checkpoint信息,我们就很容易进行恢复了,而且恢复后的mac地址都没有发生变化。 主要是从cni-plugin中摘出的代码,完整实现见 https://github.com/kongfei605/i40e_net_fix
// ip link add link eth0 name eth0.3 type vlan id 3
// 宿主机上存在两类网卡 bond4 或者 eth0
// master => "bond4" master2 => "eth0"
// 查找eth0 或者bond4
m, err := netlink.LinkByName(master) // first try bond4, if not found, use eth0
if err != nil {
if !strings.Contains(err.Error(), "not found") {
return nil, err
}
master = master2
}
// 拼接 成eth0.3 或者bond4.3
vlanLinkName := fmt.Sprintf("%s.%d", master, vlanID)
vlanLink, err := netlink.LinkByName(vlanLinkName)
if err != nil {
if !strings.Contains(err.Error(), "not found") {
log.Printf("failed to lookup vlan %s: %v", vlanLinkName, err)
return nil, err
}
}
...
// 执行添加 eth0.3 或者 bond.4
if err := netlink.LinkAdd(vlan); err != nil {
log.Printf("failed to add vlan %s: %v", vlanLinkName, err)
return nil, err
}
...
// ip link set dev eth0.3 up
if err := netlink.LinkSetUp(result); err != nil {
log.Printf("failed to set up vlan link %s: %v", vlanLinkName, err)
return nil, err
}
// ip link add xxx(随机命名) link eth0.3 type macvlan mode bridge
tmpName, err := ip.RandomVethName()
if err != nil {
return nil, err
}
mv := &netlink.Macvlan{
LinkAttrs: netlink.LinkAttrs{
MTU: conf.MTU,
Name: tmpName,
ParentIndex: m.Attrs().Index,
Namespace: netlink.NsFd(int(netns.Fd())),
},
Mode: netlink.MACVLAN_MODE_BRIDGE,
}
if err := netlink.LinkAdd(mv); err != nil {
return nil, fmt.Errorf("failed to create macvlan: %v", err)
}
// ip link set xxxx netns $ns
netns.Do(func(_ ns.NetNS) error {
...
// ip netns exec $ns ip link set xxx name eth0
err := ip.RenameLink(tmpName, ifName)
if err != nil {
_ = netlink.LinkDel(mv)
return fmt.Errorf("failed to rename macvlan to %q: %v", ifName, err)
}
macvlan.Name = ifName
...
})
func ConfigureIface(ifName string, res *current.Result) error {
...
link, err := netlink.LinkByName(ifName)
if err != nil {
return fmt.Errorf("failed to lookup %q: %v", ifName, err)
}
// ip netns exec $ns ifconfig eth0 up
if err := netlink.LinkSetUp(link); err != nil {
return fmt.Errorf("failed to set %q UP: %v", ifName, err)
}
var v4gw, v6gw net.IP
for _, ipc := range res.IPs {
if ipc.Interface == nil {
continue
}
intIdx := *ipc.Interface
if intIdx < 0 || intIdx >= len(res.Interfaces) || res.Interfaces[intIdx].Name != ifName {
// IP address is for a different interface
return fmt.Errorf("failed to add IP addr %v to %q: invalid interface index", ipc, ifName)
}
// ip net exec $ns ip addr add $ip/$mask dev eth0
addr := &netlink.Addr{IPNet: &ipc.Address, Label: ""}
if err = netlink.AddrAdd(link, addr); err != nil {
return fmt.Errorf("failed to add IP addr %v to %q: %v", ipc, ifName, err)
}
gwIsV4 := ipc.Gateway.To4() != nil
if gwIsV4 && v4gw == nil {
v4gw = ipc.Gateway
} else if !gwIsV4 && v6gw == nil {
v6gw = ipc.Gateway
}
}
if v6gw != nil {
ip.SettleAddresses(ifName, 10)
}
for _, r := range res.Routes {
routeIsV4 := r.Dst.IP.To4() != nil
gw := r.GW
if gw == nil {
if routeIsV4 && v4gw != nil {
gw = v4gw
} else if !routeIsV4 && v6gw != nil {
gw = v6gw
}
}
// ip net exec $ns route add -net $net netmask $mask dev eth0
if err = ip.AddRoute(&r.Dst, gw, link); err != nil {
// we skip over duplicate routes as we assume the first one wins
if !os.IsExist(err) {
return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, ifName, err)
}
}
}
return nil
}