我们的业务Docker
镜像是在centos/systemd镜像基础上构建的,业务进程由systemd
来启动。最近需要对业务逻辑进行改造,需要识别传入的环境变量。看上去是相当简单的改动,但在我们的进程中加入读取环境变量的逻辑却发现读取不到传入的变量内容,最终定位原因是在systemd
的环境变量的处理。
NFQUEUE机制导致DNS请求5秒超时分析
在一台CentOS 7.0
服务器(内核版本号:3.10.0-123.el7.x86_64
)上安装我们的安全防护程序后,会出现curl
访问网址超时5秒的情况。现象如下:
1 | [root@localhost ~]# time curl -s www.baidu.com -o /dev/null |
通过strace
分析程序调用的过程:
1 | strace -f -tt -o curl.strace curl -s www.baidu.com -o /dev/null |
从strace
输出可以看到, 第一次curl
调用sendmmsg
同时发送了两个DNS
数据包,分别是A
记录和AAAA
记录请求,但是只收到了A
记录响应包:

然后等待5秒超时后,依次调用sendto
和recvfrom
串行处理两个DNS
请求, 这次两个DNS
响应全部收到后,继续向下执行:

而从抓包结果分析,tcpdump
只能看到第一次同时发送的两个DNS
请求中的A
记录请求,AAAA
记录请求数据包被内核协议栈丢弃了:
1 | 17:04:24.772049 IP 10.10.10.89.57416 > 114.114.114.114.53: 37081+ A? www.baidu.com. (31) |
本地环境Kubernetes LoadBalancer实现
Kubernetes
没有给本地环境(Bare-metal
, On-Premise
)提供负载均衡实现,LoadBalancer
类型的服务主要在各大公有云厂商上能够得到原生支持。在本地环境创建LoadBalancer
类型的服务后,服务的EXTERNAL-IP
会一直处于<pending>
状态。这是因为在本地环境没有相应的controller
来处理这些LoadBalancer
服务。比如:
1 | [root@master1 vagrant]# kubectl get svc |
之前的文章<<基于LVS DR模式的Kubernetes Service External-IP实现>>介绍了手动设置EXTERNAL-IP
的方式实现外部负载均衡。本文通过在本地环境实现一个简单的controller
来处理LoadBalancer
类型服务自动实现负载均衡器。架构示意如图:

LoadBalancer
是位于Kubernetes
集群外的独立集群。可以通过ECMP
将请求分散到不同的LoaderBalancer
节点上,LoadBalancer
再将请求分发到Kubernetes
的node
上。
基于LVS DR模式的Kubernetes Service External-IP实现
之前的文章<<Kubernetes Service网络通信路径>>介绍了kubernetes
的几种Service
。如果要暴露服务给kubernetes
集群外使用,可以选择NodePort
和LoadBalancer
。但LoadBalancer
现在主要在各大公有云厂商上能够原生支持。而使用NodePort
暴露服务,将使用一个非常大的端口,无法使用原始的端口号来暴露服务,比如mysql
的3306
端口。
Service
的官方文档中介绍了一种辅助方式, 叫External-IP
, 可以在worker
节点上会通过该IP
来暴露服务,而且可以使用在任意类型的service
上。集群外的用户就可以通过该IP
来访问服务。但如果这个IP
只存在于一个worker
节点上,那么就不具备高可用的能力了,我们需要在多个worker
节点上配置这个VIP:Virtual IP
。我们可以使用LVS
(也叫做IPVS
)的DR(Director Routing)
模式作为外部负载均衡器将流量分发到多个worker
节点上,同时保持数据包的目的地址为该VIP
。
DR
模式只会修改数据包的目的MAC
地址为后端RealServer
的MAC
地址,因而要求负载均衡器Director
和RealServer
在同一个二层网络,而且响应包不会经过Director
。
Kubernetes Service网络通信分析
之前的文章<<Kubernetes网络和CNI>>和<<Kubernetes flannel网络分析>>介绍了Kubernetes集群的pod
网络的通信过程。
pod
本质上非固定的,经常发生变化,而pod IP
在pod
销毁和创建的时候会发生变更,因而不能直接对外提供服务。Kubernetes
通过service
资源来对外提供服务,service
的IP
是固定的,它自动绑定一组pod
并根据不同实现将流量转发到这些pod
中, 并在流量转发的过程中实现负载均衡(load balance)。
在Kubernetes
的node
节点上的主要组件有kube-proxy
和kubelet
, kubelet
会调用相关的CNI
实现完成POD
网络的通信。而kube-proxy
则负责上述的service
与POD
之间的流量转发。实际上,在之前文章的实验环境里,即使把node
节点上的kube-proxy
组件都停止,也不会影响pod
网络通信。
service
本质就是将一组pod
通过固定IP
暴露给使用者,可以由ip:port:protocol
来标识。
service
主要以下几种类型:
ClusterIP
: 用Kubernetes
集群内部IP
暴露服务,也就是说只有在Kubernetes
集群内才可以访问这个service
。这是默认的service
类型。ClusterIP
的范围是在kube-apiserver
启动时通过-service-cluster-ip-range
参数指定的。这些IP
只能在kubernetes
集群内进行访问。service
的相关信息是在yaml
文件中定义的,最终暴露的信息可表示为:1
spec.clusterIp:spec.ports[*].port:spec.ports[*].protocol
NodePort
: 在Kubernetes
集群的所有node
节点上使用相同的固定port
来暴露服务。这种类型会自动创建ClusterIP
类型的服务,NodePort
的service
会将流量转发到ClusterIP
类型的服务。服务的使用者可以使用NodeIP:NodePort
来访问该服务。这种类型服务暴露的信息可以表示为:1
2<NodeIP>:spec.ports[*].nodePort:spec.ports[*].protocol
spec.clusterIp:spec.ports[*].port:spec.ports[*].protocolLoadBalancer
: 是通过kubernetes
集群外部设施所提供的IP
来暴露服务。NodePort
和ClusterIP
类型的服务会被自动创建。不同的LoadBalancer
负责实现外部IP:port
与NodePort
服务的映射。这种类型暴露的信息可以表示为:1
2
3spec.loadBalancerIp:spec.ports[*].port:spec.ports[*].protocol
<NodeIP>:spec.ports[*].nodePort:spec.ports[*].protocol
spec.clusterIp:spec.ports[*].port:spec.ports[*].protocol
Kubernetes flannel网络分析
flannel
是coreos
开源的Kubernetes CNI
实现。它使用etcd
或者Kubernetes API
存储整个集群的网络配置。每个kubernetes
节点上运行flanneld
组件,它从etcd
或者Kubernetes API
获取集群的网络地址空间,并在空间内获取一个subnet
,该节点上的容器IP
都从这个subnet
中分配,从而保证不同节点上的IP
不会冲突。flannel
通过不同的backend
来实现跨主机的容器网络通信,目前支持udp
,vxlan
,host-gw
等一系列backend
实现。本文介绍vxlan backend
下的容器通信过程。
flannel
在v0.9.0
版本上对vxlan
的实现作了改动。源码中有一段非常详细的注释介绍了不同版本的设计与实现:
1 | // Some design notes and history: |
v0.9.0
之前版本的实现主要依赖vxlan
内核模块的L2MISS
和L3MISS
消息机制。L2MISS
是指vxlan
设备在ARP
表中找不到内层IP
对应的MAC
地址时会给用户态程序发送netlink
消息。L3MISS
是指vxlan
设备在FDB
表中找不到VXLAN
协议内层MAC
地址所属的VTEP
的IP
地址时会给用户态程序发送netlink
消息。之前的文章<<动态维护FDB表项实现VXLAN通信>>介绍过相关概念和操作。本文主要分析v0.9.0
版本上的实现方式。
Linux core dump过程分析
Linux进程在运行中异常中止或者崩溃,Linux会将进程当时的内存信息、寄存器信息、函数调用堆栈等信息存储到文件系统的一个文件中,这个过程叫做core dump
, 生成的core
文件可以使用gdb
来分析诊断程序存在的问题。最近对core dump
的生成过程及相应进程退出的过程有些疑惑。究竟是崩溃进程的内核task
退出前生成core
文件还是崩溃的task
先退出之后kernel
检测到进程崩溃后额外来生成的呢?带着这个疑问去做些了实验和代码分析。
默认情况下,core dump
文件被命名为core.pid
, 如core.12345
。内核参数/proc/sys/kernel/core_pattern
可以定义core dump
文件名模板。具体模板参数可以参考文档。但内核有个小巧特性,如果core_pattern
的第一个字符为管道符:|
, 内核会执行|
之后的配置的可执行程序,并通过pipe
将core dump
内容传给这个用户态程序。
ABRT:Automated Bug Reporting Tool
服务收集应用崩溃的core
文件就是使用这个机制,abrt-ccpp
服务会将core_pattern
设置为:
1 | |/usr/libexec/abrt-hook-ccpp %s %c %p %u %g %t e %P %I %h |
Security Boot开启下CentOS8内核模块签名及加载
Security Boot
机制是UEFI
的一个特性,用于确保固件所加载的代码是可信的。它通过将可信根
存放在固件中,在加载阶段校验所加载的二进制可信。当前实现主要是基于X.509
证书的公私钥体系。固件使用可信的公钥来校验所加载的bootloader
, bootloader
再来校验内核或者第二阶段的bootloader
, 内核校验所加载的内核模块,这样一级级完成校验。
在开启了Security Boot
机制的CentOS8上,加载我们未签名的内核模块会失败:
1 | [root@localhost virtualdev]# insmod ./virtualdev.ko |
接下来我们来通过签名我们的内核模块完成加载。要签名和校验我们的内核模块,我们需要一对公钥私钥。私钥用于签名,公钥需要加载到系统固件的MOK: Machine Owner Key
列表中用于校验被签名的模块。
Kubernetes POD环境的NFQUEUE机制
之前的文章<<Kubernetes环境中NFQUEUE与MARK机制冲突>>介绍了我们使用NFQUEUE
机制将数据包送往用户态进行安全检测。之前程序逻辑是将来自虚拟网络设备的数据包直接放行。而当把逻辑修改为对POD
虚拟网卡的流量也进行检测时,POD
网络就无法连通了。排查发现数据包送上用户态之后,并没有收到用户态程序的裁决信息。
在对NFQUEUE
的源码实现了进行草略分析后,发现NFQUEUE
机制是支持network namespace
的。POD
虚拟网络设备的数据包送往用户态的队列是在POD
独有的network namespace
中创建的,和默认的network namespace
:init_net
中的队列是完全独立的。我们的用户态程序运行是在init_net
中运行,而POD
的network namespace
中并没有用户态程序在读取队列获取数据包,因而数据包会被丢弃。
和之前文章同样,通过简化程序来进行实验。实验的Kubernetes
环境有3个node
, 容器组网使用flannel
。
我们创建了两个busybox
的pod
:
1 | kubectl run busybox1 --image=busybox --command -- sleep 3600 |
他们分别位于node1
和node2
上:
1 | [root@master1 scripts]# kubectl get pods -o wide |
我们从busybox2
中访问busybox1
,网络连通正常:
1 | [root@master1 scripts]# kubectl exec -ti busybox2-7d76b658b6-h5r2k -- ping -c2 10.230.96.4 |
ivshmem PCI设备中断机制驱动示例
之前的文章<<QEMU虚拟机内识别ivshmem设备>>介绍了在虚拟机内通过用户态程序访问ivshmem
设备的共享内存。在虚拟机之间或者宿主机与虚拟机之间通过共享内存进行通信的情形下,共享内存的两端必须依赖轮询方式来实现通知机制。这种方式是ivshmem
提供的ivshmem-plain
的使用方式。除此之外,ivshmem
还提供了ivshmem-doorbell
的使用方式,它提供了基于中断的通知机制。
ivshmem-doorbell
提供了两种中断方式,一种是传统的基于INTx
的中断, 它主要使用BAR0
的Interrupt Mask
和Interrupt Status
两个寄存器;另一种是基于MSI-X
的中断,它主要使用BAR0
的IVPosition
和Doorbell
两个寄存器。参考共享的设备端叫做peer
。IVPosition
寄存器存储该peer
的数字标识符(0-65535), 称做peer_id
。该寄存器为只读寄存器。而Doorbell
寄存器为只写寄存器。ivshmem-doorbell
支持多个中断向量,写入Doorbell
寄存器则触发共享该内存的某个peer
的某个中断。Doorbell
为32
位,低16
位为peer_id
,而高16
位为中断向量号(这里是从0
开始的顺序号,而非PCI
驱动在Guest虚拟机内部所申请的向量号)。
使用ivshmem-doorbell
机制需要运行ivshmem-server
。ivshmem-server
根据参数创建共享内存,并通过监听本地UNIX DOMAIN SOCKET
等待共享内存的peer
来连接。添加了ivshmem-doorbell
设备的QEMU
进程会连接该socket
, 从而获取ivshmem-server
所分配的一个peer_id
。ivshmem-doorbell
支持多个中断向量,ivshmem-server
会为ivshmem
虚拟PCI
设备支持的每个中断向量创建一个eventfd
,并将共享内存以及为所有客户端中断向量所创建的eventfd
都通过SCM_RIGHTS
机制传递给所有客户端进程。这样所有的peer
便都具备了独立的两两之间的通知通道。之后在虚拟机内通过触发ivshmem
虚拟PCI
设备的DOORBELL
寄存器的写入,虚拟机的QEMU
进程便会通过DOORBELL
寄存器中的peer_id
和中断向量号来找到相应的eventfd
,从而通知到对端的QEMU
进程来产生相应的PCI
中断。
要使用中断机制,用户态程序是无能为力的,需要编写相应的PCI
驱动来实现。本文通过一个简单的PCI
驱动示例来说明ivshmem-doorbell
的MSI-X
中断机制的使用。