前文中我们首先介绍了Docker网络的相关技术和实现原理,但是我们知道在大规模集群部署的今天,如果我们只通过手动的方式去部署Docker是不现实的,而k8s是目前来说最好用的利器,对容器进行编排治理应用的非常广泛。这里会首先介绍一些k8s的基础知识,然后主要介绍一下k8s的网络情况,主要是与Docker的网络进行结合,希望看完本篇文章你能够对云的网络情况能够有更加清晰的理解。
一、k8s的部署
本小节会介绍一些k8s的部署以及一些原语的关系,目的有两个:一是为了想简单了解k8s的人能够快速的了解,再一个是为后面提到的k8s的网络模型做铺垫,因为既然要聊到网络,那么各个节点之间的关系和全局概念就会比较重要。所以为了避免聊起来没有上下文,所以这第一小节我们就先铺垫一些基础的内容。
k8s提供了很多原语的概念,比如Pod ,Service , Controller 等等,他们都可以被看作为一种资源对象,几乎所有的资源对象都可以通过kubectl工具的相关操作将其保存在etcd中进行持久化存储,然后通过跟踪个对比etcd里面保存的'期望资源状态' 与 当前实际环境中的'实际资源状态'的差异来实现自动控制和自动纠错的高级功能。
我们先把k8s的架构图放在这里,后面聊到每一个点的时候都可以在这里面找到对应关系。
首先介绍一下物理层面的:
- 首先k8s在部署的角度去讲也是分主从关系的,主节点叫做Master , 主要负责整个集群的管理和控制基本上k8s的所有控制命令都发给他,他负责具体的执行过程。所以通常来说Master都运行在一个独立的服务器上,为了HC一般都会部署3台以上。
在Master上面主要运行了3个非常重要的进程 :
- Kubernetes API Server (kube-apiserver) :主要提供了HTTP Rest接口的关键服务进程,是集群操作的入口进程
- Kubernetes Controller Manager (kube-controller-manager) : k8s里面所有资源的自动化控制中心,可以将其理解为资源对象的管家
- Kubernetes Scheduler (kube-scheduler) : 负责资源调度的进程,主要用于协调Pod资源
- Node节点 :在k8s集群中除了Master节点,剩下的都叫做Node节点,在集群中作为主要的工作节点而存在,在Node节点上主要运行着容器资源。每个Node 节点有着自己的核心进程,主要用于与Master节点进行通信以及本节点容器管理等操作。
- kubelet : 主要负责Pod对应的容器的创建,启停等任务
- kube-proxy : 实现k8s Service的通信与负载均衡机制的重要实现着
- Docker Engine (docker) : Docker 引擎,负责本节点容器的创建和管理工作
因为Node 节点可以与Master进行通信,所以可以通过主动注册机制完成Node节点的扩容,同时也需要向Master节点定期汇报自己节点上的容器运行情况,方便Master进行宏观的调控。
再看一下逻辑层面的:
- Pod : pod是k8s中非常重要的一个概念,在一个Pod里面可以运行多个容器,他们之间公用Pod的资源,并且每个Pod在创建之后,里面会有一个叫做Pause的根容器,这个容器主要是用来承担健康检查的重要任务。
k8s会为每个Pod分配一个唯一的IP,一个Pod里面多个容器共享Pod IP , k8s要求底层网络支持集群内的任意两个Pod之间的TCP/IP通信,这通常采用虚拟二层网络技术来实现,比如Flannel 。
所以需要记住的一点就是在k8s集群中,一个Pod里面的容器与另外主机上的Pod容器能够直接通信。
- Replication Controller (RC) : rc也是k8s中非常重要的一个概念,简单的来说,它其实定义了一个期望的场景,既声明了某种Pod的副本数量在任意时刻都符合某个预期值,一个rc 也就是一个Pod的创建模板,k8s会根据模板的需求去创建或者销毁一些pod,最终达到一种预期平衡状态。
- Deployment : 是在1.2版本新引入的一个概念,用于解决Pod的编排问题,为此Deploment在内部使用了Replica Set来实现,所以整体来看可以看作为RC的一次升级,两者的功能相似度很高,从功能的角度来看我们也可以理解为同一类资源。
Deploment相对于RC的一个最大的升级就是我们可以随时知道当前Pod的部署进度,实际上有一个Pod的创建、调度、绑定节点以及在目标Node上启动对应容器这一完整的过程需要一定的时间,所以我们在期待系统启动目标个数的Pod副本状态之前其实是一个变化的过程。
- Service : 也是k8s中的一个比较核心的概念,其实我们可以理解为在k8s中每一个Service(SVC) 就是我们经常提起的微服务架构中的一个微服务。之前提到的Pod , RC 等资源对象其实就是SVC的铺垫。看一张他们之间的关系图
从这里我们可以看到k8s定义了一个SVC的入口地址,这样前端的应用就可以通过这个地址来访问对应的SVC的服务,而SVC的背后是由一组Pod组成的集群实例。RC的作用是保证SVC的服务能力和服务质量始终都符合预期要求。
前面在说到Pod的时候我们提到了每一个Pod都会有一个独立的Pod IP , 然后通过与内部容器的端口号就组成了一个全新的组合 Endpoint 。那多个Pod组成的Pod 集群怎么来处理前端的请求呢?其实这个负载均衡的操作是有Node上面的kube-proxy进程来完成的。在前端Service 接收到请求的时候,会把请求交给Node上面的kube-proxy,再有kube-proxy来将请求转发给具体的某一个Pod来处理。每个Pod的创建和销毁对应的Pod IP就会变化,而SVC一旦创建就会被分配一个Cluster IP ,这个地址在SVC的整个生命周期内是不会变化的。
二、k8s的网络模型与Docker的关联性
上面简单的就介绍这么多,很多点也没有细致的展开,后面会再单独进行分享,但是上面的这些内容已经足够我们理解k8s的网络内容了。
K8s的网络模型的一个非常重要的设计原则是:每个Pod都拥有一个独立的IP,并假定所有的IP都是在一个可以直接连通的,扁平的网络空间中。所以不管他们是否运行在同一个Node里面,都要求他们可以直接通过对方的IP进行访问,这样用户就不需要考虑Pod之间如何通信,也不需要考虑如何将容器的端口映射到主机的端口上的问题了,这点相对于Docker来说比较的方便。而且这样在同一个Pod里面的不同容器因为公用Pod的网络空间,所以他们之间的通信可以用localhost来时间,减少了容器之间通信的麻烦。
我们知道Docker 的网络主要是借助Iptables来完成通信,但是他的一个非常重要的问题就是在处理多主机互联的网络环境下不是很好。所以k8s在设计网络的时候就充分的考虑到了这点。在k8s中关于网络的问题主要在一下四个方面做了特殊的解决处理:
- 容器到容器之间的直接通信
- 抽象的Pod到Pod之间的通信
- Pod到Service之间的通信
- 集群外部与内部组件之间的通信
容器之间的通信:
同一个Pod内的容器是共享同一个网络命名空间的,共享同一个Linux协议栈,所以对于这些容器来说他们就是运行在同一台主机上的应用,他们之间的通信可以直接使用localhost来进行通信,处理起来非常的高效。
Pod之间的通信:
每一个Pod 都有一个全局唯一的IP,所以在同一个Node中不同的Pod之间可以直接通过Pod IP进行通信,根本不需要其他的发现机制。其实这里也是通过docker0虚拟网桥来实现的,不同的Pod 也是通过veth对与宿主机的docker0虚拟网桥进行连接,如果请求的地址不是当前Pod的地址就交给docker0网桥来处理,从而实现了同一个Node不同Pod之间的通信。
那么存在于不同Node上面的Pod的通信呢?这个也是k8s要求的,需要他们之间也可以进行直接通信。那么k8s是怎么实现的呢?我们知道想要位于不同宿主机的两个Pod进行通信,肯定是要通过宿主机的物理网卡进行通信才能实现,但是我们怎么知道位于不同宿主机上面的Pod的IP呢?
k8s会记录所有正在运行的Pod的IP分配情况,并且将这些信息保存在etcd之中(作为SVC的Endpoint), 因为k8s的网络模型是要求任何两个Pod之间是可以通过Pod IP 进行通信的,那么就需要这些Pod IP是不能出现重复的,但是Pod IP 又是属于docker0网段的,所以在k8s中docker0网段的分配是需要协调的。只保证了Pod IP 的唯一还不够,我们还需要知道Pod IP 与 Node IP的映射关系,这样在进行跨Node之间Pod请求的时候,会首先将数据发送到对应的Node上,然后再由Node将数据转发到不同的Pod中,从而实现了跨Node之间Pod的直接访问。
Pod与Service之间的通信:
Service通过Label的选择在逻辑上将多个Pod抽象为一个服务,这样在调用对应的服务的时候,就可以很方便的只通过Service Ip (Cluster IP)进行调用,而不需要去关心这个Service后面具体由哪些Pod来提供服务。在这个基础上也提供了一套负载均衡的策略,可以更加方便的支持服务访问和水平扩展。
我们知道每一个Service被创建的时候会被分配一个Cluster IP , 而在这个Service 的生命周期内这个IP是不会变的,那么正式因为这个不变的特性可以被用来做成DNS的映射,将Service的名称和Cluster IP进行映射,而Service的名称一般情况下我们也不会频繁的去改变它,所以更加方便了我们直接通过Service name进行服务的访问。
这个Cluster IP虽然使用起来很方便,但是我们需要注意一个点,就是这个IP只能在k8s的集群内部来使用,在外部是无法访问这个IP的,而且这个IP也无法被ping通,只能与Pod的端口号一起组成一个Endpoint来提供服务。那么正因为这个Cluster IP 只能在k8s的内部进行使用,那么就可以知道请求的发起方也一定是集群内某个容器中的服务。
每个Service的负载均衡是由在每个Node上面运行的线程kube-proxy来实现的,这个线程通过watch的方式监听着kube-apiserver写入etcd中关于Pod的最近变化,一旦检测到Pod发生了变化,就会立刻将变化体现到对应的iptables 或 ipvs规则中,这样再进行负载均衡的时候才能保证服务的有效性。
总结的来说,就是在一个容器的内部发出请求,通过容器到对应的Pod,最后通过Pod所在节点的kube-proxy进程进行负载均衡转发,直接将请求转发到目标的Pod上面来完成服务的转发。
集群外部与k8s集群的通信:
Service的Cluster IP在集群的内部很好的解决了多副本Pod之间的复杂均衡的调用问题,但是Cluster IP归根结底还是一个只在内部使用的IP,在集群的外部是无法直接访问的。所以怎么从k8s的集群外部来访问集群内部的服务又是另外一个问题。那目前可使用的方案主要是有三种:NodePort Service , LoadBalance Service , Ingress。
- NordPort Service : Node IP是一个公网可见的物理节点,那么他对应的IP是可以在集群之外直接访问的,然后将端口映射到具体的Service上,这样在请求的时候通过Node上面的kube-proxy来实现请求的转发和负载均衡,但是这样做如果集群中节点比较多,维护起来会非常的麻烦,而且我们在直接把集群中的节点IP暴露给公网,也是有一定的安全隐患的。
- LoadBalance Service : 这种方案一般是会在公有云中提供的,通常也是需要支付一定的费用。
- Ingress : 这个是k8s本身提供的一个暴露集群内部服务的解决方案,为访问集群的请求提供路由规则的集合,简单的来说就是提供外部可访问集群的入口,将外部的http/https请求转发到内部的Service服务上。
所以从狭义的角度去看,Ingress就是一个反向代理,和Nginx是一样的角色。这里的Ingress我们是把他理解为一种解决方案,在k8s的内部是分成两部分来实现的:一个是ingress(和解决方式是同样的名字),另外一个是Ingress Controller 。Ingress 是反向代理的规则,在这里定义什么样的请求需要转发到哪个Service上,然后Ingress Controller才是真正的执行反向代理的程序,他负责解析Ingress的反向代理的规则,所有的Ingress Controller都会及时的更新规则,一旦匹配到了任意规则,就会将请求转发到对应的Service中。