支撑大规模公有云的Kubernetes改进与优化 (2)

2020-06-01 00:00:00 的是 配置 容器 网络 租户

接着上一篇支撑大规模公有云的Kubernetes改进与优化 (1)


接下来我们按照kubernetes创建容器的详细过程,以及可能存在的问题。


一、API Server的认证,鉴权,Quota


当客户需要创建一个pod的时候,需要先请求API Server。


Kubernetes的API Server要负责进行认证和鉴权。



Kubernetes的认证方式也是如OpenStack或者AWS一样,是通过Token和PKI进行认证的。


所谓Token的方式就是在服务端配置一个很长的字符串,在客户端请求的时候带上这个字符串,两面匹配了,认证就通过了。


而PKI的方式复杂一些,PKI是Public Key Infrastracture,也即通过证书的方式进行认证。



为什么叫做PKI呢?在出现Public Key和Private Key对这种方式之前,人们一直用的是对称加密的方式,客户端和服务端共享一个Private Key,两面对上了,就算认证通过了,但是如何传输,交换,约定这个Private Key是一个大问题,一旦在公网上传输,一定有可能被窃取,一旦泄露了,就被黑了。


后来就发明了Public Key和Private Key也即公钥和私钥对体系,因为以前是没有公钥的,所以这个体系叫做Public Key Infrastracture。


私钥是放在用户自己手里的,不能够在公网上进行传播,私钥千万不能丢,丢了就要赶紧换一个。而公钥是可以随便放在公网上的任何地方的。所以客户端和服务器各自抱着自己的私钥,然后拿到对方的公钥,私钥加密的只有对应的公钥可以解密,实现了相互的安全通信。


然而公钥可以随便放在网上,如何能够保证公钥能够被信任,不被篡改呢?就需要一个权威机构对这个公钥进行盖章,盖过章的就叫做Certificate,就像你的毕业证书被你的学校盖章了一样。权威机构成为CA,CA会有很多个级别,低的级别叫Self-signed Certificate,也就是没啥权威机构盖章,我自己给自己盖章,只要我自己足够牛,例如订票网站12306,你添加他的证书的时候,浏览器会说不信任,但是没关系,你可以强行信任,不信任他你信任谁呢?还有更别的CA,一级一级盖章上去。


这也是为什么Kubernetes的X509 Client Certs这种认证方式配置的是--client-ca-file=SOMEFILE。


说完了认证,接下来我们再说鉴权。


鉴权一般采取的方式都是RBAC,也即Role Based Access Control。



这是基本的RBAC的一个模型,有了这个模型,使得云对于权限的控制非常的灵活。

例如可以灵活定义Role之间的相互关系,例如互斥的关系,继承的关系。

另外可以事先定义Role和Permission之间的关系,他们之间的关系相对稳定,不会频繁变化,而一个User属于哪个Role,这个反而是经常改变的。

我们可以灵活的控制某个层级的管理员的权限,让这个管理员可以将用户关联或者解关联一个Role,但是不给这个管理员权限配置某个Role的Permission,因为后者显然更专业,需要更高层级的管理员来操作。


有了RBAC的权限设置,云平台可以灵活的控制资源的权限,隔离,账单等。



例如AWS有一个层级的账号体系,其中账号对应一个租户,租户之间完全隔离,租户之下有子账号,租户之内隔离性差一些,因而子账号多用于一个租户下面的多个子部门或者子团队的权限控制。而如果需要多个公司之间需要完全隔离,但是又有一个母公司的存在,或者一个公司统一结账,然后将资源转售给其他的公司,则不同的公司则不建议使用子账号,而应该使用账号,而母公司使用组织。


然而Kubernetes使用了相对简单的Attribute-based access control (ABAC) ,仅仅通过设置用户,访问哪个资源,访问哪个namespace,是否只读等。还不能满足公有云复杂的账号体系。


后是设置ResourceQuota,也是针对namespace的。


Kubernetes的租户是用namespace隔离的,然而namespace是逻辑隔离,而非物理隔离,处于不同namespace的容器很可能会在同一台主机,甚至同一二层网桥上面,无论是计算,网络,存储没有实现真正的隔离。这对于私有云的可信租户是没有问题的,但是对于公有云的非可信租户,则有很大的问题。


二、Kubernetes的Scheduler


kube-scheduler启动的时候会创建一个NewSchedulerServer,并且运行它,app.Run(s)。


当前的scheduler回去etcd里面竞选自己是leader,如果是的话,则运行调度功能sched.Run()。


func (s *Scheduler) Run() {

go wait.Until(s.scheduleOne, 0, s.config.StopEverything)

}


其中scheduleOne的算法有以下的步骤:

获取下一个需要调度的Pod。

pod := s.config.NextPod()


使用调度算法找到一个具体的Node节点。

dest, err := s.config.Algorithm.Schedule(pod, s.config.NodeLister)


将Pod和节点绑定在一起。

b := &v1.Binding{

    ObjectMeta: metav1.ObjectMeta{Namespace: pod.Namespace, Name: pod.Name},

    Target: v1.ObjectReference{

        Kind: "Node",

        Name: dest,

    },

}

err := s.config.Binder.Bind(b)


好了,重点来了,是调度算法。


对于默认的调度算法generic_scheduler.go来讲:

步预选,过滤掉不符合条件的节点,在kubernetes里面叫predicate。

例如PodFitsResources:检查Node的资源是否充足,包括允许的Pod数量、CPU、内存、GPU个数等。

另外还有一些规则可以通过标签选择部分节点,可以满足亲和性和反亲和性要求。

第二步优选,通过优先级排序,选择优先级高的节点,在kubernetes里面叫做Prioritizing。

例如LeastRequestedPriority:优先调度到请求资源少的节点上。


是不是看到了你熟悉的预选和优选,对的OpenStack也是这样做的,分别叫做Filtering和Weighting。


但是Kubernetes的调度是在Pod层级的,Node对于租户来讲没有隔离,没有分别,全部可见。不同的租户的Pod是会调度到一台机器上的,这样安全性就有问题。


三、Kubernetes的kubelet启动容器


Kubelet启动的时候会运行一个KubeletServer,会调用RunKubelet,终调用startKubelet。


在这里面不断的循环,查看有没有需要更新的Pod。


kl.syncLoop(updates, kl)


如果是新添加一个Pod,kube*.ADD,则HandlePodAdditions,这事情会dispatchWork给一批podWorkers来做。


podWorkers会调用syncPodFn,也即kubelet.syncPod,会真正创建容器。


创建容器的时候,当然先要下载容器的镜像,但是容器的镜像如何管理呢?


Kubernetes本身没有方案,需要你自己去解决。


当然简单的是搭建一个Docker Registry,但是这个是单机版,只能测试使用,或者有一个Harbor,可以进行镜像的同步,权限的控制,是比较热的镜像管理软件,当然也可以你自己去搭建一个高可用的集群,当然需要有一个高可用的后端存储保存镜像,一般会用对象存储,那使用Swift呢,还是用Ceph呢?


好了下载了镜像,可以运行容器了,刚才说了,容器是隔离性不好的,是否需要采用隔离性好的容器实现呢?


当前业内有两种主流的实现方式,其实都是基于加一层微内核虚拟机来做这件事情的。


其中一个是的Hyper,加了一层虚拟化层如下图。



他容器的创建过程是先启动一个虚拟机,然后将Docker的rootfs映射进去,然后再启动容器。



另外就是Intel出的Clear Container。


在V1的时候,使用的是kvmtool。



在V2的时候,用的是qemu-lite


都是非常轻量级的虚拟化。


四、Kubernetes的Kubelet配置网络


容器创建好了,是应该为容器配置网络了。


默认容器的网络模型如下:



这个网络模型非常简单,详细用过容器的人一看就能看明白,但是他是如何实现的呢?


用到了以下的技术:

  • Network Namespace

  • Veth pair

  • Iptables NAT

  • 网桥brctl

  • 路由iproute


如果我们不使用Docker,能不能自己用这些技术把这个经典的网络搭建起来呢?


获取容器的PID,是namespace的名字,--format里面是go的语法

docker inspect '--format={{ .State.Pid }}' test 

DOCKERPID=12065

NSPID=12065


默认Docker创建的网络namespace不在默认路径下 ,ip netns看不到,所以需要ln一下

rm -f /var/run/netns/12065

ln -s /proc/12065/ns/net /var/run/netns/12065


在宿主机上的网卡

LOCAL_IFNAME=veth3pl12065 

在容器内的网卡

GUEST_IFNAME=veth3pg12065 


创建网卡对,veth pair可以做到发到任何一个网卡的包另一个都能够收到

ip link add name veth3pl12065 mtu 1500 type veth peer name veth3pg12065 mtu 1500


将主机上的网卡连接到网桥上,并启动网桥

ip link set veth3pl12065 master testbr

ip link set veth3pl12065 up


将容器的网卡塞到容器的namespace里面

ip link set veth3pg12065 netns 12065


将容器内网卡重命名

ip netns exec 12065 ip link set veth3pg12065 name eth3


给容器内网卡设置ip地址

ip netns exec 12065 ip addr add 192.168.0.2/24 dev eth3

ip netns exec 12065 ip link set eth3 up


后从容器内访问容器外要配置SNAT

-A POSTROUTING -s 192.168.0.0/16 ! -o docker0 -j MASQUERADE


需要配置默认的路由

ip netns exec 12065 ip route add default via 192.168.0.1 dev eth3



那Kubernetes是怎么配置网络的呢?


其实Kubernetes本身是不管网络的,但是他对网络有下面的需求,无论你自己如何实现网络,都要满足下面三个条件。

  • 所有的容器都可以在不使用NAT的情况下同别的容器通信

  • 所有的节点都可以在不使用NAT的情况下同所有的容器通信

  • 容器的地址和别人看到的地址一样


Kubernetes配置容器网络的标准叫做CNI。


在Kubelet的配置中,有一个配置叫做network-plugin-dir,kubelet会从这个文件夹里面查找插件,另一个配置network-plugin,是真正的插件的名字。


Kubernetes常用下面两类插件,一类是kubelet,也即配置--network-plugin=kubenet,他非常简单,仅仅使用CNI中的bridge和host-local插件做本地的配置。


更加通用的是cni,也即配置--network-plugin=cni,会调用--cni-bin-dir下面的cni插件以及--cni-conf-dir下面的CNI配置来配置网络。


详细的CNI的文档在这里https://github.com/containernetworking/cni


例如对于一个Bridge网络可以如下配置



{

"cniVersion": "0.2.0",

"name": "mynet",

"type": "bridge",

"bridge": "cni0",

"isGateway": true,

"ipMasq": true,

"ipam": {

"type": "host-local",

"subnet": "10.22.0.0/16",

"routes": [

{ "dst": "0.0.0.0/0" }

]

}

}


那么CNI如何处理这个CNI网络呢,这里要用到用Go语言写的这个Plugin。


https://github.com/containernetworking/plugins/tree/master/plugins/main/bridge


如果看这个Bridge.go的代码,发现他做了下面的事情。

  • 如果没有网桥则创建网桥ensureBridge

  • 获取容器所在的namespace

  • 配置veth pair,一个在namespace里面,一个在namespace外面,外面的网卡添加到网桥上

  • 调用IPAM来配置容器的IP地址,由于配置文件中类型为host-local,调用host-local命令

  • 配置网关和路由


看到这个过程,是不是非常熟悉,对的就是上面我们手动做的那一部分。是不是技术还是很通用的,只要知道了原理,就很容易理解很多相关的机制。


当然仅仅简单的Bridge网络是不能满足需求的,因为无法满足Kubernetes对于网络的三个条件。


好在Kubernetes很火,有很多网络的方案。


常用的一个是flannel。



Flannel在每台机器上,还是传统的二层网络,然后通过一个flanneld进程,将包封装为UDP的包,再加上服务器的IP和MAC,到达另一端的flanneld,就会解封装,然后转发给容器。


Flannel的一个缺点是每台机器的网段要求不同,这样缺少了一些灵活性。


现在火的一种方案是Calico。


Calico和其他通过UDP隧道或者VXLAN隧道不同,不需要通过封装,直接用服务器的MAC地址就可以了。



但是在容器里面访问另一个IP地址的时候,是需要知道那个IP地址对应的MAC地址的,一般通过ARP协议进行,对于Calico这种方式,需要进行arp欺骗,而不是真的将ARP包封装后广播出去。


Felix会在内核里面写入iptables,路由信息,arp相应等,来控制容器发出的包的流向。



每台服务器都知道哪些容器在自己的上面,他们是如何互相知道其他的容器在哪里呢?这就需要通过BGP协议进行通知,说这个IP地址在我这里,把包给我,我能帮忙送达。


Calico虽然没有Overlay网络的损耗,变成了纯三层网络的转发,对于普通的应用是没有问题的,但是有的应用需要用到二层的机制,例如keepalived,就有问题了。


另外Calico会有大量的iptables规则和路由规则需要关联,运维难度也很大。


并且Calico不是标准的协议,无法实现容器,虚拟机,物理机,物理交换机的统一的管理,对于构建一个复杂的网络还是能力不足的。


相对而言Vxlan要标准的多。


好了,配置完毕网络了,容器可以上网了。


但是这个过程中主要描述了业界的普遍做法,以及可能会遇到的一些问题。下一篇文章会详细描述网易的佳实践。








相关文章