to inspire confidence in somebody.

0%

如果K8s使用Calico作为网络方案的话,应该都会知道Calico是个纯3层的方案,也是就说,所有的数据包,都是通过路由的形式找到对应机器和容器的,然后通过BGP协议来将所有的路由同步到所有的机器或者数据中心,来完成整个网络的互联。
简单的来说,Calico针对一个容器,在主机上创建了一堆veth pair,其中一端在主机,一端在容器的网络空间里,然后在主机和容器中分别设置几条路由,来完成网络的互联,我们可以看一个例子:

主机上:

1
$ ip addr
2
...
3
771: cali45b9132fec1@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1440 qdisc noqueue state UP group default
4
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 14
5
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link
6
       valid_lft forever preferred_lft forever
7
...
8
9
$ ip route 
10
...
11
10.218.240.252 dev cali45b9132fec1 scope link
12
...

容器里:

1
$ ip a
2
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
3
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4
    inet 127.0.0.1/8 scope host lo
5
       valid_lft forever preferred_lft forever
6
3: eth0@if771: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1440 qdisc noqueue state UP
7
    link/ether 66:fb:34:db:c9:b4 brd ff:ff:ff:ff:ff:ff
8
    inet 10.218.240.252/32 scope global eth0
9
       valid_lft forever preferred_lft forever
10
11
$ ip route
12
default via 169.254.1.1 dev eth0
13
169.254.1.1 dev eth0

按照上面的逻辑,可以理一下:

  • 当目的地址是10.218.240.252的数据包,也就是目的是容器的数据包,到达主机,主机根据10.218.240.252 dev cali45b9132fec1 scope link这条路由,将数据包丢给cali45b9132fec1这个veth
    ,然后容器中对应的eth0就可以收到数据包了。
  • 当容器中的数据包需要发出,就是走默认路由,也就是default via 169.254.1.1 dev eth0,将数据包丢给eth0,这时主机对应的cali45b9132fec1可以收到包,然后继续进行路由选择,转发到对应端口。

这么一看好像没什么问题,但是总觉得不对,为什么容器里的默认网关是169.254.1.1呢?二层是怎么处理的?

我们重新思考一下数据包的传输:
当一个数据包的目的地址不是本机,所以需要查询路由表,当查到路由表中的网关之后,需要获取网关的MAC地址,并将数据包的MAC地址修改成网关地址,然后发送到对应的网卡。

问题来了。在容器里的网关是169.254.1.1,那网关的MAC地址是什么?
正常情况下,内核会对外发送ARP请求,去询问整个二层网络中谁拥有169.254.1.1这个IP地址,拥有这个IP地址的设备会将自己的MAC返回。
但是现在的情况是,对于容器和主机,都没有169.254.1.1这个IP,甚至,在主机上的端口cali45b9132fec1,MAC地址也是一个无用的ee:ee:ee:ee:ee:ee。所以,如果仅仅是目前的状况,容器和主机网络根本就无法通信!
所以Calico是怎么做到的呢?在Calico的FAQ里,官方给了答案:

Why can’t I see the 169.254.1.1 address mentioned above on my host?

Calico tries hard to avoid interfering with any other configuration on the host. Rather than adding the gateway address to the host side of each workload interface, Calico sets the proxy_arp flag on the interface. This makes the host behave like a gateway, responding to ARPs for 169.254.1.1 without having to actually allocate the IP address to the interface.

Calico利用了网卡的proxy_arp功能,具体的,是将/proc/sys/net/ipv4/conf/DEV/proxy_arp置为1,当设置这个标志之后,主机就会看起来像一个网关,会响应所有的ARP请求,并将自己的MAC地址告诉客户端。
也就是说,当容器发送ARP请求时,主机会告诉容器,我拥有169.254.1.1这个IP,我的MAC地址是XXX,这样,容器就可以顺利的将数据包发出来了,于是网络就通了。

其实Calico不仅仅设置了这个标志,但是这个标志是最重要的,毕竟关系到网络是否能通的问题。看了看Cailco的代码,发现Calico还设置了其他几个标志位:

  • /proc/sys/net/ipv4/conf/DEV/rp_filter => 1:开启反向路径过滤,确认数据包来源,对于普通容器,IP基本无法伪装,但是如果是VM(Calico也支持VM),很容易伪装IP地址,所以为了安全打开这个选项。
  • /proc/sys/net/ipv4/conf/DEV/route_localnet => 1:允许路由到本地。
  • /proc/sys/net/ipv4/neigh/DEV/proxy_delay => 0:默认情况下,主机为了减少ARP风暴的可能,会延迟一段时间回复ARP包,这个选项关闭这个延迟。
  • /proc/sys/net/ipv4/conf/DEV/forwarding => 1:允许转发数据包(如果不允许转发的话,那数据包就出不去主机了)。

上面是IPv4的情况,如果是IPv6的网络,则会设置:

  • /proc/sys/net/ipv6/conf/DEV/proxy_ndp => 1:这个和proxy_arp是一样的。
  • /proc/sys/net/ipv4/conf/DEV/forwarding => 1:同IPv4。

偶然看到一篇cloudflare的博客How to drop 10 million packets per second,如何实现单核情况下一秒钟丢弃1000万个数据包,原文循序渐进,从最简单的用户态丢弃到使用非常新的技术XDP,逐步将单核丢包性能提升到10mpps,很有意思,网上也没有看到原文的中文版本,所以这里顺便翻译一下,看看cloudflare是如何处理类似的情况的。

阅读全文 »

想要删除K8s里的一个Namespace,结果删除了所有该Namespace资源之后使用kubectl delete namespace test发现删除不掉,一直卡在Terminating状态,使用--force参数依然无法删除,报错:
Error from server (Conflict): Operation cannot be fulfilled on namespaces "test": The system is ensuring all content is removed from this namespace. Upon completion, this namespace will automatically be purged by the system.
找了一圈,发现这个Issue,里面有条评论

kubectl get namespace annoying-namespace-to-delete -o json > tmp.json
then edit tmp.json and remove”kubernetes”

curl -k -H “Content-Type: application/json” -X PUT –data-binary @tmp.json https://kubernetes-cluster-ip/api/v1/namespaces/annoying-namespace-to-delete/finalize

and it should delete your namespace,

跟着试了一下,很管用,直接就删除了:
先运行kubectl get namespace test -o json > tmp.json,拿到当前namespace描述,然后打开tmp.json,删除其中的spec字段。因为这边的K8s集群是带认证的,所以又新开了窗口运行kubectl proxy跑一个API代理在本地的8081端口。最后运行curl -k -H "Content-Type: application/json" -X PUT --data-binary @tmp.json http://127.0.0.1:8001/api/v1/namespaces/test/finalize

搞定!

阅读全文 »

如果K8s中某个Node节点重启,在Event信息中会有一条消息,大致内容如Node xxxxx has been rebooted, boot id: xxx,而如果是重启kubelet,则不会有这条消息,所以kubelet是怎么判断是自己重启了还是机器重启了呢?

搜索了一下代码,在https://github.com/kubernetes/kubernetes/blob/v1.10.12/pkg/kubelet/kubelet_node_status.go#L621这里,会判断上次记录的Node的BootID和当前从cAdvisor获取的BootID是否相同,如果不同则说明机器重启了。

那么cAdvisor是怎么获得这个BootID的呢?看了一下cAdvisor的文档,发现默认是从/proc/sys/kernel/random/boot_id这个文件读取的。针对这个文件,找到一段解析:

/proc/sys/kernel/random/boot_id: A random ID that is regenerated on each boot. As such it can be used to identify the local machine’s current boot. It’s universally available on any recent Linux kernel. It’s a good and safe choice if you need to identify a specific boot on a specific booted kernel.

是内核暴露的一个接口,每次启动都会随机生成一个ID,是一个比较通用和安全的判断启动的办法。

参考:

  1. http://0pointer.de/blog/projects/ids.html

由于业务的需要,需要在我们的一台虚拟化机器上,实现如下的配置:

首先,需要将两块网卡设置Bonding并配置交换机对应端口trunk模式;在此基础上,添加宿主机的IP地址,并添加相应的VLAN,最后,还需要添加一个Bridge,用于桥接创建的虚拟机。

由于本身这台机器就是Openstack的宿主机,所以当前的状况是除了所需要的一个Bridge,其他都已经配置完成了,并且由于Openstack的原因,已经有个Bridge virbr0被绑定到bond0上了。
但是呢,这个Bridge是给ovs用的,也就是说,桥接在virbr0上的网络需要自己带上VLAN的tag才能正常工作,而我们希望的是再有一个Bridge br0,桥接在br0上不需要管理VLAN,保持和宿主机相同就可以。

阅读全文 »

在Kubernetes中,可以针对每个Pod设置DNS的策略,通过PodSpec下的dnsPolicy字段可以指定相应的策略,目前支持的策略如下:

  • Default“: Pod继承所在宿主机的设置,也就是直接将宿主机的/etc/resolv.conf内容挂载到容器中。
  • ClusterFirst“: 默认的配置,所有请求会优先在集群所在域查询,如果没有才会转发到上游DNS。
  • ClusterFirstWithHostNet“: 和ClusterFirst一样,不过是Pod运行在hostNetwork:true的情况下强制指定的。
  • None“: 1.9版本引入的一个新值,这个配置忽略所有配置,以Pod的dnsConfig字段为准。
阅读全文 »

一行Python代码生成随机字符串:

1
import random, string; print(''.join(random.choice(string.ascii_letters + string.digits) for _ in range(15)))
1
# python -c "import random, string; print(''.join(random.choice(string.ascii_letters + string.digits) for _ in range(15)))"
2
hTvLXAGUzTISKmZ

如果是Python3,还可以使用:

1
import random, string; print(''.join(random.choices(string.ascii_letters + string.digits, k=15)))
1
# python3 -c "import random, string; print(''.join(random.choices(string.ascii_letters + string.digits, k=15)))"            
2
BRYr0FncnkXUL9F

先说说背景,为什么会要了解一下/etc/resolv.conf配置,起因是一个跑在k8s集群的一个业务出现问题,仔细排查后,发现其中一个Pod的域名解析有问题,域名login.example.com被解析到了一个IP,而这个IP地址是另一个范域名*.ichenfu.com的解析,经过一番调查,最终发现是同事在配置一台机器上的kubelet时填错了clusterDomain的配置,将原本需要配置为c2.ichenfu.com的配置写成了c1.ichenfu.com,那么问题来了,为什么这么配置会导致DNS解析到一个错误的,而且是完全不相干的地址的呢?下面就慢慢分析一下。

首先还原一下场景,默认情况下,kubelet启动Pod的时候,会将DNS配置注入到Pod中,出问题的Pod里/etc/resove.conf内容如下:

1
nameserver 10.254.0.2
2
search default.svc.c1.ichenfu.com svc.c1.ichenfu.com c1.ichenfu.com localdomain
3
options ndots:5
阅读全文 »

这两天因为内部kubernetes的网络配置问题和同事交流了一下,由于内部使用了calico网络,在内部pod出网时有两种选择,使用nat或者不使用nat,为此还经历了一番讨论,突然发现自己对netfilter包括其相关的很多概念还是比较模糊,所以查了查资料,尝试深入了解一下。

netfilter

在网上找到了一张图,发现还是能比较清楚的描述整个netfilter架构的,来源来自http://xkr47.outerspace.dyndns.org,先把图贴出来:

netfilter packet flow

这张图更像是从iptables chain的角度去描述netfilter数据流,总的来说其实不太影响最终的理解,实际netfilter提供了NF_IP_PRE_ROUTINGNF_IP_LOCAL_IN
NF_IP_FORWARDNF_IP_LOCAL_OUTNF_IP_POST_ROUTING几个HOOK点,具体到图上:

- `PREROUTING`: 对应`NF_IP_PRE_ROUTING`,看名字就可以知道,该HOOK在收到数据包,进行路由判断之前触发;
- `INPUT`: 对应`NF_IP_LOCAL_IN`,当经过`PREROUTING`阶段,如果目的地址是本机,那么将触发`INPUT`,之后就可能被传给应用程序处理;
- `FORWARD`: 对应`NF_IP_FORWARD`,对应如果数据包在路由表中是需要转发到另一个网络接口的,那么将触发`FORWARD`;
- `POSTROUTING`: 对应`NF_IP_POST_ROUTING`,所有数据包在进行路由选择之后,在实际发送给网络接口之前,会触发`POSTROUTING`;
- `OUTPUT`: 对应`NF_IP_LOCAL_OUT`,对于所有本地生成的数据包,在路由选择之前会触发`OUTPUT`。

PS:根据文档描述,包括上图中的备注也说明了,在实际上,对于本地生成的数据包,是先进行过一次路由选择,拿到一些需要的信息(比如源IP和一些IP选项)后,再触发`OUTPUT`的。

实际上netfilter最重要的就是提供这些HOOK点,针对图上的这些HOOK点,可以方便的注册各种处理逻辑来实现对包的处理,像常用的LVS,也是利用了这一系列的HOOK,来实现负载均衡功能。

iptables

说完netfilter的基本信息,需要在具体说一下iptables的主要数据流,实际上iptables也是在netfilter上注册了一系列的HOOK,并将这些HOOK通过几个table来管理,同样是针对上面的图,从iptables table这个角度来看,
也可以很直观的看到iptables的所有表,到底都在netfilter的哪些阶段被注册了,在很的教程中,都喜欢以table维度来介绍数据流,个人觉得是没有从hook这个维度看起来清晰的。

需要说明的是,因为NAT包含SNAT(修改源地址)和DNAT(修改目的地址),而这两种NAT发生作用的时间也是不一样的,在图上可以看到,DNAT发生在NF_IP_PRE_ROUTINGNF_IP_LOCAL_OUT阶段,
而SNAT发生在NF_IP_POST_ROUTING阶段,其实也很好理解,仔细想想就可以知道为什么是这样了。

不过对于上图里,和实际不对应的地方,iptables的SNAT其实也是可以在INPUT里实现的,而图上并没有画出来。

Connection Tracking

最后再说一下Connection Tracking(连接跟踪),连接跟踪也是在netfilter上实现的,可以给iptables提供在连接的各个阶段对数据包进行操作的能力,也就是可以提供一个跟状态挂钩的服务(毕竟TCP链接是有状态的)。
数据包进入网络栈后,只经过一些基本的检查,以及raw表操作之后,很快就会被连接追踪给追踪了,根据收到的包,可以根据实际情况针对性的修改追踪中的各种链接状态,当然连接追踪也是可以跳过的,只需要在raw表中操作数据包将数据包添加NOTRACK标记,那么连接跟踪将会不处理数据包和其连接。

整体内容不是很多,也没有非常深入去了解所有的机制,特别是代码方面,但是一张图还是能提供非常多的信息,特别是对整体的架构了解帮助很大,具体到更多的应用,就额外再进行记录吧~

参考:

  1. https://www.digitalocean.com/community/tutorials/a-deep-dive-into-iptables-and-netfilter-architecture
  2. https://www.netfilter.org/documentation/HOWTO//netfilter-hacking-HOWTO-3.html

Harbor是vmware中国开发的一款企业级的DockerRegistry服务器,我们内部也是有搭建了一个Harbor,但是版本是0.5,对于当前最新的release版本1.5.2而言已经太老了,确实也有一些问题,比如不支持多级的镜像名称,某些情况下会触发bug导致panic。

所以需要升级一下,既然考虑升级了,就干脆升级到最新的版本1.5.2了。首先说一下目前的Harbor,官方提供的离线安装包里,默认是本地启动一个MySQL,将Harbor需要的一些数据存储在本地的MySQL中的,这个是不能接受的,所以在之前的部署中,是使用了外部的一个MySQL,同样,registry的存储在线上也是使用了共享存储,保证可用性。

不过Harbor的1.5.2版本对于0.5版本变化还是比较大的,首先是增加了adminserver这个角色,将所有的配置都拿到adminserver中存储,ui组件通过http请求定期向adminserver请求当前最新的配置信息,其次是数据库结构,新版本和旧版本相比数据库结构发生了很大的变化。

对于升级操作,官方也提供了解决方案,可以参考migration_guide进行升级,升级工具的镜像官方也是提供了,但是这其中存在一个问题,就是升级工具依赖本地的MySQL,也就是说,这个工具只能工作在MySQL是Harbor离线安装包启动的情况下,如果使用了外部的MySQL,这个升级工具就无法直接使用了。

所以呢,最终还是需要去看一下官方的升级工具是如何实现的,看能否通过其他办法手动升级,于是就花了点时间看了一下代码,找到了最后的实现方式,具体的代码在alembic/mysql这个目录下,原理也很简单,官方使用了一个Python的工具alembic实现了数据库结构的版本管理。

手动运行数据库升级,首先需要安装alembic工具,可以通过pip安装,或者针对不同的发行版找对应的软件包。

下面开始操作:

阅读全文 »