iptables/ipset 备忘 - vqiu.cn

转自:https://vqiu.cn/ipset-note/,文章有点机翻的痕迹,不过不影响理解。

简介

ipset是iptables的扩展,允许创建一次匹配整个“地址”集的防火墙规则。 与通过线性存储和遍历的普通iptables链不同,IP集存储在索引数据结构中,即使在处理大型集时,查找也非常高效。

ipset只是iptables的扩展,所以本篇文章不仅是ipset的使用笔记,同时也是iptables的使用笔记。

安装

// Debian
apt-get install ipset

//RHEL
yum -y install ipset
12345

快速入门

需求,禁1.1.1.12.2.2.2IP访问服务器,使用以下iptables 命令可实现:

iptables -A INPUT -s 1.1.1.1 -j DROP
iptables -A INPUT -s 2.2.2.2 -j DROP

同时,我们如果使用ipset也可以实现

ip -N myset iphash
ipset -A myset 1.1.1.1
ipset -A myset 2.2.2.2
iptables -A INPUT -m set --set myset src -j DROP

上面的ipset命令创建了一个带有两个地址(1.1.1.1和2.2.2.2)的新集合( ipset类型为iphash )。

然后iptables命令引用带有匹配规范的-m set --set myset src ,这意味着“匹配源头与之匹配(即包含在内)名为myset的集合的数据包”

标志src表示匹配“源”。 标志dst将匹配“destination”,并且标志src,dst将在源和目标上匹配。

在上面的第二个版本中,只需要一个iptables命令,无论该组中包含多少额外的IP地址。 尽管此示例仅使用两个地址,但您可以轻松定义1,000个地址,而基于ipset的配置仍然只需要一个iptables规则,而前一种方法,如果没有ipset的优势,则需要1,000个iptables规则。

应用场景

ipset优点

除了性能提升之外,ipset还允许在许多场景中进行更直接的配置。

如果要定义一个防火墙条件,该条件与1.1.1.1或2.2.2.2中的所有数据包相匹配,并在mychain中继续处理,请注意以下内容不起作用:

iptables -A INPUT -s ! 1.1.1.1 -g mychain
iptables -A INPUT -s ! 2.2.2.2 -g mychain

如果一个数据包来自1.1.1.1,它将与第一个规则不匹配(因为源地址 1.1.1.1),但它将匹配第二个规则(因为源地址不是 2.2.2.2)。 如果数据包来自2.2.2.2,它将匹配第一个规则(因为源地址不是 1.1.1.1)。 规则相互抵消,所有数据包都匹配,包括1.1.1.1和2.2.2.2。

虽然还有其他方法可以正确构建规则并在没有ipset的情况下实现所需的结果,但没有一种方法可以直观或直接:

ipset -N myset iphash
ipset -A myset 1.1.1.1
ipset -A myset 2.2.2.2
iptables -A INPUT -m set ! --set myset src -g mychain

在上面,如果一个数据包来自1.1.1.1,它将与规则不匹配(因为源地址1.1.1.1与设置的myset匹配)。 如果数据包来自2.2.2.2,则它与规则不匹配(因为源地址2.2.2.2与设置的myset匹配)。

虽然这是一个简单的例子,但它说明了在单个规则中拟合完整条件的基本好处。 在许多方面,单独的iptables规则彼此是相互独立的,并且将单独的规则合并为单个逻辑条件并不总是直截了当,直观或最优,特别是当涉及混合正常和反向测试时。 ipset只是让这些情况下的生活更轻松。

ipset的另一个好处是可以独立于活动的iptables规则来操作集合。 添加/更改/删除条目是一件小事,因为信息简单且顺序无关紧要。 编辑平面列表不需要经过深思熟虑。 另一方面,在iptables中,除了每个规则是一个明显更复杂的对象这一事实之外,规则的顺序至关重要,因此就地规则修改更加繁重且可能容易出错。

配合NAT使用

出站NAT(SNAT或IP伪装)允许专用LAN内的主机访问Internet。 适当的iptables NAT规则匹配源自专用LAN的Internet绑定数据包,并将源地址替换为网关本身的地址(使网关看起来是源主机并隐藏其后面的私有“真实”主机)。

NAT会自动跟踪活动连接,以便将返回的数据包转发回正确的内部主机(通过将目标地址从网关地址更改回原始内部主机的地址)。

以下是执行此操作的简单出站NAT规则的示例,其中10.0.0.0/24是内部LAN:

iptables -t nat -A POSTROUTING \ -o eth0 -j MASQUERADE 

事实证明,这是ipset的另一个伟大的应用程序。 假设除了充当本地专用LAN(10.0.0.0/24)的Internet网关之外,您的盒子还可以直接路由到其他四个专用网络(10.30.30.0/24,10.40.40.0/24,192.168.4.0/23和172.22.0.0/22)。 运行以下命令:

ipset -N routed_nets nethash
ipset -A routed_nets 10.30.30.0/24
ipset -A routed_nets 10.40.40.0/24
ipset -A routed_nets 192.168.4.0/23
ipset -A routed_nets 172.22.0.0/22
iptables -t nat -A POSTROUTING \
         -s 10.0.0.0/24 \
         -m set ! --set routed_nets dst \
         -j MASQUERADE

正如您所看到的,ipset可以很容易地确定您想要匹配的内容和不需要的内容。 此规则将伪装从内部LAN(10.0.0.0/24)通过该框的所有流量,除了绑定到routed_nets集中任何网络的数据包,保留对这些网络的正常直接IP路由。 由于此配置完全基于网络地址,因此您不必担心适当的连接类型(VPN的类型,跳数等),也不必担心物理接口和拓扑。

这应该是这样的。 因为这是纯粹的第3层(网络层)实现,所以实现它所需的基础分类也应该是纯层3。

限制某些PC只能访问某些公共主机

让我们说老板担心某些员工在互联网上玩而不是工作,并要求你限制他们的PC访问他们需要能够工作的特定网站,但他不希望这样。影响所有PC(例如他)。

要限制三台PC(10.0.0.5,10.0.0.6和10.0.0.7)只能访问worksite1.com,worksite2.com和worksite3.com,请运行以下命令:

ipset -N limited_hosts iphash
ipset -A limited_hosts 10.0.0.5
ipset -A limited_hosts 10.0.0.6
ipset -A limited_hosts 10.0.0.7
ipset -N allowed_sites iphash
ipset -A allowed_sites worksite1.com
ipset -A allowed_sites worksite2.com
ipset -A allowed_sites worksite3.com
iptables -I FORWARD \
         -m set --set limited_hosts src \
         -m set ! --set allowed_sites dst \
         -j DROP

此示例与单个规则中的两个集匹配。 如果源匹配limited_hosts且目标与allowed_sites不匹配,则丢弃该数据包(因为limited_hosts仅允许与allowed_sites通信)。

请注意,因为此规则位于FORWARD链中,所以它不会影响与防火墙本身之间的通信,也不会影响内部流量(因为该流量甚至不会涉及防火墙)。

阻止除某些PC之外的所有主机的访问(反向场景)

假设老板想阻止访问局域网内所有主机上的一组站点,除了他的PC和他的助手的PC。 对于多样性,在这个例子中,让我们通过MAC地址而不是IP匹配boss和助手PC。 让我们说MAC是11:11:11:11:11:11和22:22:22:22:22:22,其他人阻止的网站是badsite1.com,badsite2.com和badsite3.com 。

代替使用第二个ipset来匹配MAC,让我们利用带有MARK目标的多个iptables命令来标记数据包,以便在同一链中的后续规则中进行处理:

ipset -N blocked_sites iphash
ipset -A blocked_sites badsite1.com
ipset -A blocked_sites badsite2.com
ipset -A blocked_sites badsite3.com
iptables -I FORWARD -m mark --mark 0x187 -j DROP
iptables -I FORWARD \
         -m mark --mark 0x187 \
         -m mac --mac-source 11:11:11:11:11:11 \
         -j MARK --set-mark 0x0
iptables -I FORWARD \
         -m mark --mark 0x187 \
         -m mac --mac-source 22:22:22:22:22:22 \
         -j MARK --set-mark 0x0
iptables -I FORWARD \
         -m set --set blocked_sites dst \
         -j MARK --set-mark 0x187

正如您所看到的,因为您没有使用ipset来执行上一个示例中的所有匹配工作,所以这些命令更复杂。 因为有多个iptables命令,所以必须认识到它们的顺序非常重要。

请注意,这些规则是使用-I选项(插入)而不是-A(追加)添加的。 插入规则后,它将添加到链的顶部,将所有现有规则向下推。 因为正在插入这些规则中的每一个,所以有效顺序是相反的,因为随着每个规则的添加,它将被插入到前一个规则之上。

上面的最后一个iptables命令实际上成为FORWARD链中的第一个规则。 此规则匹配目标与blocked_sites ipset匹配的所有数据包,然后使用0x187(任意选择的十六进制数)标记这些数据包。 接下来的两条规则仅匹配要排除的主机的数据包以及已标记为0x187的数据包。 然后这两个规则将这些数据包上的标记设置为0x0,“清除”0x187标记。

最后,最后一个iptables规则(由上面的第一个iptables命令表示)丢弃所有具有0x187标记的数据包。 这应该匹配在blocked_sites集中具有目的地的所有数据包,除了来自任一排除的MAC的数据包,因为这些数据包上的标记在达到DROP规则之前被清除。

这只是解决问题的一种方法。 除了使用第二个ipset之外,另一种方法是利用用户定义的链。

如果你想使用第二个ipset而不是mark技术,你将无法达到上述的确切结果,因为ipset没有machash集类型。 但是,有一个macipmap集类型,但这需要在IP和MAC上匹配,而不是像上面那样单独使用MAC。

注意事项:在大多数实际情况中,此解决方案实际上不适用于网站,因为许多可能成为blocked_sites集合的主机(如Facebook,MySpace等)可能有多个IP地址,这些IP可能经常变化。 iptables / ipset的一般限制是只有在解析为单个IP时才应指定主机名。

此外,主机名查找仅在命令运行时发生,因此如果IP地址更改,防火墙规则将不会知道更改,仍将引用旧IP。 因此,实现这些类型的Web访问策略的更好方法是使用HTTP代理解决方案,例如Squid。 该主题显然超出了本文的范围。

自动禁止尝试访问无效服务的主机

ipset还为iptables提供了“目标扩展”,它提供了一种基于任何iptables规则动态添加和删除集合条目的机制。 您无需使用ipset命令手动添加条目,而是可以让iptables随时为您添加条目。

例如,如果远程主机尝试连接到端口25,但您没有运行SMTP服务器,则可能没有任何好处。 要拒绝主持人有机会主动尝试其他任何操作,请使用以下规则:

ipset -N banned_hosts iphash
iptables -A INPUT \
         -p tcp --dport 25 \
         -j SET --add-set banned_hosts src
iptables -A INPUT \
         -m set --set banned_hosts src \
         -j DROP

如果数据包到达端口25,比如源地址为1.1.1.1,则会立即将其添加到banned_hosts,就像运行此命令一样:

ipset -A banned_hosts 1.1.1.1

由于DROP规则,从1.1.1.1开始的所有流量都被阻止。

请注意,这也将禁止尝试运行端口扫描的主机,除非他们知道要避免端口25。

清除运行配置

如果要清除ipset和iptables配置(设置,规则,条目)并重置为新的打开防火墙状态(在防火墙脚本的顶部有用),请运行以下命令:

iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -t filter -F
iptables -t raw -F
iptables -t nat -F
iptables -t mangle -F
ipset -F
ipset -X

无法销毁“正在使用”的集合,这意味着由一个或多个iptables规则引用(使用ipset -X )。 因此,为了确保从任何状态完全“重置”,必须首先刷新iptables链(如上所示)。

生产实例

结合生产的SSH,配置如下:

ipset create whitelist-ssh hash:net hashsize 4096
ipset add whitelist-ssh 192.168.10.0/23

iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -m set --match-set whitelist-ssh src -p tcp --dport 22 -m comment --comment "Allow Access SSH" -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -m comment --comment "Deny Access SSH" -j DROP

ipset list
iptables -t filter -vxnL

ipset save > /etc/ipset.up.rules
iptables-save > /etc/iptables/rules.v4

//往/etc/rc.local加入以下:
ipset restore < /etc/ipset.up.rules
iptables-restore < /etc/iptables/rules.v4

模拟fail2ban 功能

60秒内访问3次SSH,将源IP禁止60秒

ipset create badguys hash:net timeout 900
ipset add badguys 192.168.0.0/16 nomatch
ipset add badguys 172.16.0.0/16 nomatch

iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -m set --match-set badguys -j DROP

iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 3 --rttl --name SSH -j set --add-set badguys src

或者

ipset create badguys hash:net timeout 900

iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -m set --match-set badguys src -j DROP
iptables -A INPUT -p tcp --dport 22 -m hashlimit --hashlimit-above 5/min --hashlimit-burst 5 --hashlimit-mode srcip --hashlimit-name SSH-bruteforce -j SET --add-set badguys src

模拟Port Knocking 功能

ipset create knock hash:ip timeout 10
ipset create knock2 hash:ip timeout 10
ipset create whosthere hash:ip timeout 300

iptables -A INPUT -m set --match-set whosthere src -j ACCEPT
iptables -A INPUT -p tcp --dport 1989 -j SET --add-set knock src
iptables -A INPUT -p tcp -m set --match-set knock src --dport 2016 -j SET --add-set knock2 src
iptables -A INPUT -p udp -m set --match-set knock2 src --dport 1864 -j SET --add-set whosthere src

依次按以下敲门

  • 1989/TCP
  • 2016/TCP
  • 1864/UDP

批量禁端口

ipset -N BAD-PORTS bitmap:port range 135-1512
ipset -A BAD-PORTS 135
ipset -A BAD-PORTS 137-139
ipset -A BAD-PORTS 144
ipset -A BAD-PORTS 445
ipset -A BAD-PORTS 1512

iptables -A FORWARD -p tcp -m set --match-set BAD-PORTS dst -j DROP
iptables -A FORWARD -p udp -m set --match-set BAD-PORTS dst -j DROP

或者 对TCP与UDP分别对应

ipset create bl-tcp-ports bitmap:port range 0-65535
ipset create bl-udp-ports bitmap:port range 0-65535
ipset add bl-tcp-ports 23
ipset add bl-tcp-ports 1433
ipset add bl-tcp-ports 2323
ipset add bl-tcp-ports 3306
ipset add bl-tcp-ports 3389
ipset add bl-tcp-ports 5060

定义开放服务端口

  • 简单版本
ipset create services bitmap:port range 0-65535
ipset add services 443

iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -t filter -A INPUT -p tcp -i ens192 -d 172.16.4.109 -m conntrack --ctstate NEW -m set --match-set services dst -j ACCEPT
  • IP + 端口结合 iptables 实现如下:
-A INPUT -s 192.0.2.42 -d 0/0 -p tcp --dport 22 -j ACCEPT 
-A INPUT -s 192.0.2.42 -d 0/0 -p tcp --dport 25 -j ACCEPT
-A INPUT -s 198.51.100.54 -d 0/0 -p tcp --dport 22 -j ACCEPT
-A INPUT -s 198.51.100.54 -d 0/0 -p tcp --dport 25 -j ACCEPT

使用ipset 简化

-A INPUT -p tcp -m set --match-set ADMIN_ADDR src -m set --match-set RESTRICTED_PORTS dst -j ACCEPT

ipset create ADMIN_ADDR hash:ip
ipset create RESTRICTED_PORTS bitmap:port
ipset add ADMIN_ADDR 192.0.2.42,198.51.100.54
ipset add RESTRICTED_PORTS 22,25

ipset add ADMIN_ADDR $friend_ip
ipset del ADMIN_ADDR $friend_ip 

实现管理员IP白名单功能

ipset create trustedAdminsIP hash:ip
ipset add trustedAdminsIP 192.0.2.1
ipset add trustedAdminsIP 192.0.2.2
ipset list trustedAdminsIP

iptables -A INPUT -i lo -j ACCEPT

iptables -A INPUT -m state ---state RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -m set --match-set trustedAdminsIP src -p tcp -m tcp --dport 22 -j ACCEPT

ipset del trustedAdminsIP 192.0.2.2
ipset flush trustedAdminsIP
ipset destroy trustedAdminsIP 

或优雅的写法

ipset create whitelist -exist hash:net family inet
ipset add whitelist 119.147.144.176/29
ipset add whitelist 39.108.115.157

iptables -I  INPUT 2 -p tcp --dport 22 -m set --match-set whitelist src -j LOG --log-prefix "IP Whitelisted INPUT "
iptables -t filter -I INPUT 3 -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW -m set --match-set whitelist src -m comment --comment "Allow Access SSH" -j ACCEPT

IP黑名单功能

ipset create blacklist hash:net
ipset add blacklist 185.93.185.237 -exist
ipset add blacklist 204.2.134.0/24 -exist
ipset add blacklist 208.100.26.228 -exist
iptables -I INPUT 1 -m set --match-set blacklist src -j LOG --log-prefix "IP Blacklisted INPUT"
iptables -I INPUT 2 -m set --match-set blacklist src -j DROP

使用systemd 管理

Unit]
Description=ipset persistent rule service
Before=firewalld.service
ConditionFileNotEmpty=/etc/sysconfig/ipset
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/ipset -exist -file /etc/sysconfig/ipset restore
ExecStop=/usr/sbin/ipset -file /etc/sysconfig/ipset save
[Install]
WantedBy=multi-user.target

引用

  • [1] http://ipset.netfilter.org/iptables-extensions.man.html
  • [2] https://blog.manasg.com/fun-with-ipset-and-iptables/
  • [3] http://ipset.netfilter.org/ipset.man.html

iptables 限速

从知乎这个问题开始,限速命令如下:

iptables -A OUTPUT -d 192.168.4.204 -m limit --limit 35/s --limit-burst 40 -j ACCEPT
iptables -A OUTPUT -d 192.168.4.204 -j DROP

命令行中主要用到了 iptables 的扩展模块 limit,也就是 -m limit

Limit match

从 iptables 指南上看出 limit 和 limit-burst 最初不是拿来限速的。是拿来限制日志记录的次数的。

这个匹配操作必须由-m limit明确指定才能使用。有了它的帮助,就可以对指定的规则的日志数量加以限制,以免你被信息的洪流淹没哦。比如,你可以事先设定一个限定值,当符合条件 的包的数量不超过它时,就记录;超过了,就不记录了。我们可以控制某条规则在一段时间内的匹配次数 (也就是可以匹配的包的数量),这样就能够减少DDoS syn flood攻击的影响。这是它的主要作用,当然,还有很多其他作用(译者注:比如,对于某些不常用的服务可以限制连接数量,以 免影响其他服务)。limit match也可以用英文感叹号取反,如:-m limit ! --limit 5/s 表示在数量超过限定值后,所有的包都会被匹配。

在开头的命令行中,--limit 35/s 意思是每秒钟允许35个包出栈。

limit-burst

limit-burst 是个初始值,匹配次数过了这个初始值之后,就由 --limit xxx/s 来控制。

在开头的命令行中,--limit-burst 40 意思是允许40个包作为缓冲区,用满了再匹配 --limit xxx/s

参考资料


word 压缩文件 - 某乎

https://zhuanlan.zhihu.com/p/61727378

先打开这个需要压缩的Word文档,然后在Word表格里上方功能区中找到“文件”选项,左侧的菜单栏这里找到并点击 另存为

img

Word 会自动弹出“另存为”对话框,在这里我们先不要点击保存。在保存的旁边有个“工具”选项,我们点击后选择最下面的“压缩图片”;

img

在弹出的“图片压缩”对话框中,我们可以看到下面有很多分类,它可以把Word文档的图片压缩成各种品质,选择以后点击“保存”就可以了。

img

在压缩Word文件时候,可以选择将需要操作的文件保存在一个文件夹中,便于方便后面的操作;

img


K8S 问题排查: cgroup 内存泄露问题 - Vermouth

转自:http://www.xuyasong.com/?p=2049

前言

这篇文章的全称应该叫:[在某些内核版本上,cgroup 的 kmem account 特性有内存泄露问题],如果你遇到过 pod 的 "cannot allocated memory"报错,node 内核日志的“SLUB: Unable to allocate memory on node -1”报错,那么恭喜你中招了。

这个问题在 pingcap 的文章和腾讯云的官方修复都发过,原因也讲的很清楚,不过因为版本差异,文章里的方法有所变动,这里做下总结

现象

我们的环境:

  • K8S 版本: 1.11、1.13、1.16
  • docker 版本:18.09
  • 机器操作系统:centos7、centos6
  • 机器内核版本:3.10

可能会出现以下几种现象:

1.pod 状态异常,describe pod 显示原因为: no allocated memory

img

2.节点上执行 dmesg 有日志显示:slub无法分配内存:SLUB: Unable to allocate memory on node -1

img

3.节点 oom开始按优先级杀进程,有可能会导致有些正常 pod 被杀掉

4.机器free 查看可用内存还有很多,却无法分配,怀疑是内存泄露。

原因

一句话总结:

cgroup 的 kmem account 特性在 3.x 内核上有内存泄露问题,如果开启了 kmem account 特性 会导致可分配内存越来越少,直到无法创建新 pod 或节点异常。

几点解释:

  1. kmem account 是cgroup 的一个扩展,全称CONFIG_MEMCG_KMEM,属于机器默认配置,本身没啥问题,只是该特性在 3.10 的内核上存在漏洞有内存泄露问题,4.x的内核修复了这个问题。
  2. 因为 kmem account 是 cgroup 的扩展能力,因此runc、docker、k8s 层面也进行了该功能的支持,即默认都打开了kmem 属性
  3. 因为3.10 的内核已经明确提示 kmem 是实验性质,我们仍然使用该特性,所以这其实不算内核的问题,是 k8s 兼容问题。

其他细节原因下面会解释

解决方案

推荐方案三

方案一

既然是 3.x 的问题,直接升级内核到 4.x 及以上即可,内核问题解释:

  • https://github.com/torvalds/linux/commit/d6e0b7fa11862433773d986b5f995ffdf47ce672
  • https://support.mesosphere.com/s/article/Critical-Issue-KMEM-MSPH-2018-0006

这种方式的缺点是:

  1. 需要升级所有节点,节点重启的话已有 pod 肯定要漂移,如果节点规模很大,这个升级操作会很繁琐,业务部门也会有意见,要事先沟通。
  2. 这个问题归根结底是软件兼容问题,3.x 自己都说了不成熟,不建议你使用该特性,k8s、docker却 还要开启这个属性,那就不是内核的责任,因为我们是云上机器,想替换4.x 内核需要虚机团队做足够的测试和评审,因此这是个长期方案,不能立刻解决问题。
  3. 已有业务在 3.x 运行正常,不代表可以在 4.x 也运行正常,即全量升级内核之前需要做足够的测试,尤其是有些业务需求对os做过定制。

因为 2 和 3 的原因,我们没有选择升级内核,决定使用其他方案

方案二

修改虚机启动的引导项 grub 中的cgroup.memory=nokmem,让机器启动时直接禁用 cgroup的 kmem 属性

修改/etc/default/grub 为:
GRUB_CMDLINE_LINUX="crashkernel=auto net.ifnames=0 biosdevname=0 intel_pstate=disable cgroup.memory=nokmem"

生成配置:
/usr/sbin/grub2-mkconfig -o /boot/grub2/grub.cfg

重启机器:
reboot 

验证:
cat /sys/fs/cgroup/memory/kubepods/burstable/pod*/*/memory.kmem.slabinfo 无输出即可。

Bash

Copy

这个方式对一些机器生效,但有些机器替换后没生效,且这个操作也需要机器重启,暂时不采纳

方案三

在 k8s 维度禁用该属性。issue 中一般建议修改 kubelet代码并重新编译

对于v1.13及其之前版本的kubelet,需要手动替换以下两个函数。

vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs/memory.go

func EnableKernelMemoryAccounting(path string) error {
    return nil
}

func setKernelMemory(path string, kernelMemoryLimit int64) error {
    return nil
}

重新编译并替换 kubelet

make WHAT=cmd/kubelet GOFLAGS=-v GOGCFLAGS="-N -l"

对于v1.14及其之后版本的kubelet 通过添加BUILDTAGS来禁止 kmem accounting.

make BUILDTAGS="nokmem" WHAT=cmd/kubelet GOFLAGS=-v GOGCFLAGS="-N -l"

我们遇到1.16 版本的BUILDTAGS=”nokmem“编译出来的 let 还是有问题,还是通过修改代码的方式使其生效:

修改文件
vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs/kmem.go



// +build linux,!nokmem

package fs

import (
    "errors"
)

func EnableKernelMemoryAccounting(path string) error {
    return nil
}

func setKernelMemory(path string, kernelMemoryLimit int64) error {
    return errors.New("kernel memory accounting disabled in this runc build")
}

编译前,可以编辑下文件 hack/lib/version.sh,将 KUBE_GIT_TREE_STATE="dirty" 改为 KUBE_GIT_TREE_STATE="clean",确保版本号干净。

这里面提下两篇文章:

  • pingcap:https://pingcap.com/blog/try-to-fix-two-linux-kernel-bugs-while-testing-tidb-operator-in-k8s/
  • 腾讯云:https://tencentcloudcontainerteam.github.io/2018/12/29/cgroup-leaking/

都修改了 kubelet,pingcap 的文章有提到,docker18.09 默认关闭了 kmem,我们用的就是 18.09,但其实 docker 是打开了的,包括现在最新版的 docker-ce,直接 docker run 出来的容器也有 kmem

因此只修改 kubelet 在某些情况下是有问题的,判断依据是:

  • /sys/fs/cgroup/memory/memory.kmem.slabinfo
  • /sys/fs/cgroup/memory/kubepods/memory.kmem.slabinfo
  • /sys/fs/cgroup/memory/kubepods/burstabel/pod123456/xxx/memory.kmem.slabinfo

上边的三个文件,前两个是由 let 生成,对应 pod 维度的,修复 kubelet 后cat 该文件发现没有开启 kmem符合预期,但第三个是开启了的,猜测是 docker 层runc 生成容器时又打开了

因此,最简单的方式是和腾讯一样,直接修改下层的runc,在 runc层面将kmem直接写死为 nokmem

runc 文档:https://github.com/opencontainers/runc/blob/a15d2c3ca006968d795f7c9636bdfab7a3ac7cbb/README.md

方式:用最新版的 runc, make BUILDTAGS="seccomp nokmem" 然后 替换 /usr/bin/runc

验证:替换了 runc 后,不重启 docker,直接 kubectl run 或者 docker run, 新容器都会禁用 kmem,当然如果 kill 老 pod,新产生的 pod也禁用了kmem,证明没有问题

验证方式

找到一个设置了 request、limit的 pod,然后获取其 cgroup 中的 memory.kmem.slabinfo文件,如果报错或为 0,就证明没开 kmem,就没问题。

cat /sys/fs/cgroup/memory/kubepods/burstable/pod*/*/memory.kmem.slabinfo 

你也可以直接新建一个:

kubectl run nginx-1 --image=hub.baidubce.com/cce/nginx-alpine-go:latest --port=80 --restart=Never --requests='cpu=100m,memory=100Mi' --limits="cpu=200m,memory=200Mi"

然后 docker ps | grep nginx-1 得到容器 id

find /sys/fs/cgroup/memory -name "memory.kmem.slabinfo" | grep 容器 id,得到slabinfo的路径,直接 cat看结果

这个验证方式也是上边的复现方式。

影响范围

k8s在 1.9版本开启了对 kmem 的支持,因此 1.9 以后的所有版本都有该问题,但必须搭配 3.x内核的机器才会出问题。

一旦出现会导致新 pod 无法创建,已有 pod不受影响,但pod 漂移到有问题的节点就会失败,直接影响业务稳定性。因为是内存泄露,直接重启机器可以暂时解决,但还会再次出现

原理解释

kmem 是什么

kmem 是cgroup 的一个扩展,全称CONFIG_MEMCG_KMEM,属于机器默认配置。

内核内存与用户内存:

内核内存:专用于Linux内核系统服务使用,是不可swap的,因而这部分内存非常宝贵的。但现实中存在很多针对内核内存资源的攻击,如不断地fork新进程从而耗尽系统资源,即所谓的“fork bomb”。

为了防止这种攻击,社区中提议通过linux内核限制 cgroup中的kmem 容量,从而限制恶意进程的行为,即kernel memory accounting机制。

使用如下命令查看KMEM是否打开:

# cat /boot/config-`uname -r`|grep CONFIG_MEMCG
CONFIG_MEMCG=y
CONFIG_MEMCG_SWAP=y
CONFIG_MEMCG_SWAP_ENABLED=y
CONFIG_MEMCG_KMEM=y

cgroup 与 kmem机制

使用 cgroup 限制内存时,我们不但需要限制对用户内存的使用,也需要限制对内核内存的使用。kernel memory accounting 机制为 cgroup 的内存限制增加了 stack pages(例如新进程创建)、slab pages(SLAB/SLUB分配器使用的内存)、sockets memory pressure、tcp memory pressure等,以保证 kernel memory 不被滥用。

当你开启了kmem 机制,具体体现在 memory.kmem.limit_in_bytes 这个文件上:

/sys/fs/cgroup/memory/kubepods/pod632f736f-5ef2-11ea-ad9e-fa163e35f5d4/memory.kmem.limit_in_bytes

实际使用中,我们一般将 memory.kmem.limit_in_bytes 设置成大于 memory.limit_in_bytes,从而只限制应用的总内存使用。

kmem 的 limit 与普通 mem 的搭配,参考这篇文章:https://lwn.net/Articles/516529/

cgroup 文档: https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt

kmem属性的漏洞

在4.0以下版本的 Linux 内核对 kernel memory accounting 的支持并不完善,在3.x 的内核版本上,会出现 kernel memory 无法回收,bug 解释:

docker 与 k8s 使用 kmem

以上描述都是cgroup层面即机器层面,但是 runc 和 docker 发现有这个属性之后,在后来的版本中也支持了 kmem ,k8s 发现 docker支持,也在 1.9 版本开始支持。

1.9版本及之后,kubelet 才开启 kmem 属性

kubelet 的这部分代码位于:

https://github.com/kubernetes/kubernetes/blob/release-1.12/vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs/memory.go#L70-L106

对于k8s、docker 而言,kmem 属性属于正常迭代和优化,至于 3.x 的内核上存在 bug 不能兼容,不是k8s 关心的问题

但 issue 中不断有人反馈,因此在 k8s 1.14 版本的 kubelet 中,增加了一个编译选项 make BUILDTAGS="nokmem",就可以编译 kubelet 时就禁用 kmem,避免掉这个问题。而1.8 到1.14 中间的版本,只能选择更改 kubelet 的代码。

slub 分配机制

因为节点 dmesg 的报错是:SLUB: Unable to allocate memory on node -1

cgroup 限制下,当用户空间使用 malloc 等系统调用申请内存时,内核会检查线性地址对应的物理地址,如果没有找到会触发一个缺页异常,进而调用 brk 或 do_map 申请物理内存(brk申请的内存通常小于128k)。而对于内核空间来说,它有2种申请内存的方式,slub和vmalloc:

  • slab用于管理内存块比较小的数据,可以在/proc/slabinfo下查看当前slab的使用情况,
  • vmalloc操作的内存空间为 VMALLOC_START~4GB,适用于申请内存比较大且效率要求不高的场景。可以在/proc/vmallocinfo中查看vmalloc的内存分布情况。
  • 可以在/proc/buddyinfo中查看当前空闲的内存分布情况,

其他的表现

  • 除了最上面提到的无法分配内存问题,kmem 还会导致其他现象,如pod资源占用过高问题
  • 复现该问题还有一种方式,就是疯狂创建 cgroup 文件,直到 65535 耗尽,参考:https://github.com/kubernetes/kubernetes/issues/61937

linux dmesg 命令

Linux dmesg命令用于显示开机信息。

kernel会将开机信息存储在ring buffer中(环形缓冲区)。若是开机时来不及查看信息,可利用dmesg来查看。开机信息亦保存在/var/log目录中,名称为dmesg的文件里。

我们可以从dmesg中获得诸如系统架构、cpu、挂载的硬件,RAM等多个运行级别的大量的系统信息。当计算机启动时,系统内核(操作系统的核心部分)将会被加载到内存中。在加载的过程中会显示很多的信息,在这些信息中我们可以看到内核检测硬件设备。

常用命令行:

dmesg -T                # 显示时间戳
dmesg | head -20        # 开始20行日志
dmesg | tail -20        # 最后20行日志
dmesg | more            # 单页显示

tail -f /var/log/dmesg  # 实时监控dmesg的日志

搜索特定硬件:

grep 命令的‘-i’选项表示忽略大小写。

dmesg | grep -i usb
dmesg | grep -i dma
dmesg | grep -i tty
dmesg | grep -i memory
语  法: dmesg [-cn][-s]
参  数:

-c  显示信息后,清除ring buffer中的内容。
-s  预设置为8196,刚好等于ring buffer的大小。
-n  设置记录信息的层级。