15 | 容器网络:我修改了/proc/sys/net下的参数,为什么在容器中不起效?

本文仅作为学习记录,非商业用途,侵删,如需转载需作者同意。

今天了解下和 Network Namespace 相关的一个问题,容器中的网络参数。

一、问题再现

容器中运行的应用程序,如果需要用到 tcp/ip 协议栈的话,常常需要修改一些内核中网络协议栈的参数。

很大一部分网络参数都在 /proc 文件系统下的 /proc/sys/net/ 目录里。
内核网络相关的参数

1、直接到/proc 文件系统下的/proc/sys/net/ 目录里对参数做修改
2、或者使用sysctl 工具来修改

先在宿主机上修改几个参数:


# # The default value:
# cat /proc/sys/net/ipv4/tcp_congestion_control
cubic
# cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
# cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75
# cat /proc/sys/net/ipv4/tcp_keepalive_probes
9
 
# # To update the value:
# echo bbr > /proc/sys/net/ipv4/tcp_congestion_control
# echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time
# echo 10 > /proc/sys/net/ipv4/tcp_keepalive_intvl
# echo 6 > /proc/sys/net/ipv4/tcp_keepalive_probes
#
 
# # Double check the value after update:
# cat /proc/sys/net/ipv4/tcp_congestion_control
bbr
# cat /proc/sys/net/ipv4/tcp_keepalive_time
600
# cat /proc/sys/net/ipv4/tcp_keepalive_intvl
10
# cat /proc/sys/net/ipv4/tcp_keepalive_probes
6

然后在修改后的宿主机上启动容器,看下容器中对应的参数的数值是多少。


# docker run -d --name net_para centos:8.1.1911 sleep 3600
deec6082bac7b336fa28d0f87d20e1af21a784e4ef11addfc2b9146a9fa77e95
# docker exec -it net_para bash
[root@deec6082bac7 /]# cat /proc/sys/net/ipv4/tcp_congestion_control
bbr
[root@deec6082bac7 /]# cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
[root@deec6082bac7 /]# cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75
[root@deec6082bac7 /]# cat /proc/sys/net/ipv4/tcp_keepalive_probes
9

结果看到,只有一个值跟宿主机上一样,其他的数值还是缺省值没变。

这是为什么呢?

二、知识详解

2.1、如何理解Network Namespace

操作手册中对 Network Namespace 做了描述 network_namespaces(7) — Linux manual page

  • 1、网络设备:指lo、eth0等网络设备,可以通过ip link 看到他们
  • 2、ipv4和ipv6协议栈,IP层以及上面的TCP和UDP 协议栈也是每个namespace 独立工作的
    相关的参数也是Namespace独立的,参数大多数是在/proc/sys/net/ 目录下面,也包括了 TCP和UDP 的port 资源
  • 3、IP路由表。容器中 ip route 可以看到不同的路由
  • 4、防火墙规则,也就是iptables 规则
  • 5、网络的状态信息,可以从 /proc/net 和 /sys/class/net 里得到,这里的状态基本包括 前面4种资源的状态信息。
2.2、Namespace 的操作

我们可以通过系统调用clone()或者 unshare()这两个函数来建立新的Network Namespace

第一种方法:
伴随着新进程建立,同时也建立出新的Network Namespace 。 通过 clone()系统调用带上 CLONE_NEWNET flag 来实现的。
Clone 建立出来一个新的进程,新的进程所在的Network Namespace 也是新的, ip link 命令查看可以确认新的 Network Namespace 已经好了。

作者操作代码


int new_netns(void *para)
{
            printf("New Namespace Devices:\n");
            system("ip link");
            printf("\n\n");
 
            sleep(100);
            return 0;
}
 
int main(void)
{
            pid_t pid;
 
            printf("Host Namespace Devices:\n");
            system("ip link");
            printf("\n\n");
 
            pid =
                clone(new_netns, stack + STACK_SIZE, CLONE_NEWNET | SIGCHLD, NULL);
            if (pid == -1)
                        errExit("clone");
 
            if (waitpid(pid, NULL, 0) == -1)
                        errExit("waitpid");
 
            return 0;
}

第二种方法:
unshare() 来改变当前进程的Network Namespace

int main(void)
{
            pid_t pid;
 
            printf("Host Namespace Devices:\n");
            system("ip link");
            printf("\n\n");
 
            if (unshare(CLONE_NEWNET) == -1)
                        errExit("unshare");
 
            printf("New Namespace Devices:\n");
            system("ip link");
            printf("\n\n");
 
            return 0;
}

其他的Namespace 也是通过clone() 或者 unshare()系统调用来建立的。

创建容器的程序runC 也是用 unshare()给新建的容器建立Namespace 的

我们使用Docker 或者containerd 去启动容器,最后都会调用 runC 在Linux 中把容器启动起来。


命令行也可以 创建、查看、进入 等操作Namespace。

ip netns 可以用来设置 Network Namespace

lsns 查看已有的Namespace

nsenter 进入到某个Namespace 中

如下代码编译出来的执行程序,创建一个Network Namespace,查看,并进入Namespace查看网络情况


# ./clone-ns &
[1] 7732
# Host Namespace Devices:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 74:db:d1:80:54:14 brd ff:ff:ff:ff:ff:ff
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
    link/ether 02:42:0c:ff:2b:77 brd ff:ff:ff:ff:ff:ff
 
 
New Namespace Devices:
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
 
# lsns -t net
        NS TYPE NPROCS   PID USER    NETNSID NSFS COMMAND
4026531992 net     283     1 root unassigned      /usr/lib/systemd/systemd --switched-root --system --deserialize 16
4026532241 net       1  7734 root unassigned      ./clone-ns
# nsenter -t 7734 -n ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

三、解决问题

容器里Network Namespace参数,不是完全继承宿主机的,也不是建立新的Namespace时全部重新初始化的。

tcp_keepalive 三个参数都是重新初始化的
tcp_congestion_control 都是从Host Namespace 里复制过来的

static int __net_init tcp_sk_init(struct net *net)
{
…
        net->ipv4.sysctl_tcp_keepalive_time = TCP_KEEPALIVE_TIME;
        net->ipv4.sysctl_tcp_keepalive_probes = TCP_KEEPALIVE_PROBES;
        net->ipv4.sysctl_tcp_keepalive_intvl = TCP_KEEPALIVE_INTVL;/* Reno is always built in */
        if (!net_eq(net, &init_net) &&
            try_module_get(init_net.ipv4.tcp_congestion_control->owner))
                net->ipv4.tcp_congestion_control = init_net.ipv4.tcp_congestion_control;
        else
                net->ipv4.tcp_congestion_control = &tcp_reno;}

启动的一个普通的容器,不是privileged 的容器,里面有一些东西是不能修改的。

# docker run -d --name net_para centos:8.1.1911 sleep 3600
977bf3f07da90422e9c1e89e56edf7a59fab5edff26317eeb253700c2fa657f7
# docker exec -it net_para bash
[root@977bf3f07da9 /]# echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time
bash: /proc/sys/net/ipv4/tcp_keepalive_time: Read-only file system
[root@977bf3f07da9 /]# cat /proc/mounts | grep "proc/sys"
proc /proc/sys proc ro,relatime 0 0

runC 出于安全考虑,把容器中所有 /proc/ 和 /sys 相关的目录都做了 read-only mount 处理。

如何修改容器中的网络参数呢

1、nsenter 工具,在宿主机上修改容器里的网络参数,需要宿主机上的root权限。生产中此方法不太可行。

tcp 链接建立好了,即使改了新参数,也不会生效,需要重启应用,生产环境不太合适。

因此最合适的时机是容器启动后,应用程序启动之前,修改需要改动的参数。

runC 对 /proc/sys 目录做 read-only mount 之前,预留出修改的接口,用来修改容器里的参数,同样也是sysctl 参数。

Docker中的 -sysctl 和 kubernetes allowed-unsafe-sysctls 特性也都利用了runC的sysctl 参数修改接口,允许容器在启动的时候修改容器里的参数。

举例如下:

# docker run -d --name net_para --sysctl net.ipv4.tcp_keepalive_time=600 centos:8.1.1911 sleep 3600
7efed88a44d64400ff5a6d38fdcc73f2a74a7bdc3dbc7161060f2f7d0be170d1
# docker exec net_para cat /proc/sys/net/ipv4/tcp_keepalive_time
600

四、重点总结

Network Namespace:

  • 隔离网络设备,ip协议栈,ip路由表,防火墙规则,以及可以显示独立的网络状态信息

可以通过clone()或者 unshare()系统调用来建立新的Namespace。

在这里插入图片描述

由于安全原因,普通容器的/proc/sys 是read-only mount 的,所以在容器启动以后,我们无法在容器内部修改 /proc/sys/net 下网络相关的参数。

通过 runC sysctl 相关的接口,在容器启动的时候对容器内的网络参数做配置。

  • Docker的话使用 -sysctl 参数
  • kubernetes,用到allowed unsaft sysctl 特性

五、评论

1、

# nsenter -t <pid> -n bash -c 'echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time' (root 用户)
# sudo nsenter -t <pid> -n sudo bash -c 'echo 600 > /proc/sys/net/ipv4/tcp_keepalive_time' (非 root 用户)

其中, 表示容器 init 进程在宿主机上看到的 PID。

2、
docker exec、kubectl exec、ip netns exec、nsenter 等命令原理相同,都是基于 setns 系统调用,切换至指定的一个或多个 namespace(s)。

3、
宿主机的进入容器网络地址空间通过nsenter --target $(docker inspect -f {.State.Pid}) --net

4、
问题:
1.在/proc/sys/net 的诸多参数里,如何确认哪些是host level 哪些是容器level的呢?
2.对于host level的这些参数,在启动容器的时候通过sysctl能修改么?如果能修改,是不是相当于同时影响了同host里其他容器的运行时参数呢?

回答:
很好的问题,对于/proc/sys/net下的参数,你在容器中看到的,基本都是network namespace下的。
对于容器启动的时候,runc只允许修改namesapce下的参数值,而不会修改host相关的值。

可以参看一下runc的sysctl validation的代码:

https://github.com/opencontainers/runc/blob/ff819c7e9184c13b7c2607fe6c30ae19403a7aff/libcontainer/configs/validate/validator.go#L135

5、
问题:
请教个问题,对于proc文件系统的其他目录容器怎么隔离的呢,比如在容器里面free命令看到的是宿主机的内存。

回答:
/proc下的内容大部分是没有隔离的,就想你说的/proc/meminfo, /proc/stat 在容器中看到的都和宿主机上的是一样的。
其他有一些因为namespace不同而不同的/proc的内容,
比如 /proc/<pid>/, 容器下只能看到自己pid namespace里的进程pid。
/proc/mounts的内容和mount namespace相关。
还有IPC namesapce相关的一些/proc下的参数,等等。

6、
问题:
既然nsenter与docker exec 原理一样,为啥nsenter修改proc/sys/net不会报错无权限呢

回答:
docker exec 同时进入了容器的pid/mnt/net namespace, 而 nsenter 修改参数的时候,只是进入了容器的net namespace.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值