虚拟网络之容器网络(Docker,kubernetes)

虚拟网络之容器网络(Docker,kubernetes)

前言

随着现在容器化的技术的推进,容器网络的运维的场景也在增多,主流的docker 和 kubernetes 的网络通信模型需要了解,明确支持的网络通信模型以及实现机制和技术特点;本篇主要目的在于简单介绍容器的基本介绍和详述docker 和 kubernetes 中涉及的网络通信部分;

容器

原本并没有虚拟机,所有的应用都直接运行在物理机上,计算资源和存储资源都难以增减,要么不够用,要么是把过剩的资源浪费掉,所以虚拟机被广泛应用,物理机的使用场景被极大地压缩到了像数据库系统这样的特殊应用上面。

原本也没有容器,我们把大部分的应用运行在虚拟机上面,只有少部分特殊应用仍然运行在物理机上。但现在所有的虚拟机技术方案都无法回避两个主要的问题,一个是Hypervisor本身的资源消耗与磁盘I/O性能的降低,另一个是虚拟机仍然是一个独立的操作系统,对很多类型的应用来说都显得太重了。所以,容器技术出现并逐渐火热,所有应用可以直接运行在物理机的操作系统之上,可以直接读写磁盘,应用之间通过计算、存储和网络资源的命名空间进行隔离,为每个应用形成一个逻辑上独立的“容器操作系统”。

通过上面的描述,很容易会对当前主流的两种虚拟化实例类型有了认知;至于其中容器的发展历程这里就不再赘述了;

容器技术框架

服务器层包含容器运行时的两种场景,泛指容器运行的环境。

资源管理层的核心目标是对服务器和操作系统资源进行管理,以支持上层的容器运行引擎。

运行引擎层主要指常见的容器系统,包括Docker、Rkt、Hyper、CRIO等,负责启动容器镜像、运行容器应用和管理容器实例。运行引擎又可以分为管理程序(DockerEngine、OCID、hyper、Rkt、CRIO等)和运行时环境(runC/Docker、runV/Hyper、runZ/Solaris等)。需要注意的是,运行引擎是单机程序(类似虚拟化中的KVM和Xen),引擎运行在服务器和操作系统之上,接受上层集群管理系统的管理。

容器的集群管理系统类似于针对虚拟机的集群管理系统,它们都是通过对一组服务器运行分布式应用,细微区别只是在于后者需要运行在物理服务器上,而前者既可以运行在物理服务器上,也可以运行在虚拟服务器上。常见的容器集群管理系统有Kubernetes、DockerSwarm、Mesos,其中Kubernetes的地位可以与OpenStack相比。

应用层泛指所有运行在容器上的应用程序,以及所需的辅助系统,包括监控、日志、安全、编排、镜像仓库等。

云原生以容器为核心技术,分为运行时(Runtime)和Orchestration两层,Runtime负责容器的计算、存储、网络;Orchestration负责容器集群的调度、服务发现和资源管理。

往下是基础设施和配置管理。容器可以运行在各种系统上,包括公有云、私有云、物理机等,同时还依赖于自动化部署工具、容器镜像工具、安全工具等运维系统才能工作。

Docker

Docker使用客户—服务器模型,客户端接收用户的请求,然后发送给服务器端,服务器端接收消息后,执行相应的动作,包括编译、运行、发布容器等。其中,客户端和服务器端可以运行在相同的主机上,也可运行在不同的主机上,它们之间可以通过RESTAPI、Socket等进行交互,如下图所示为Docker架构。

(1)Docker守护进程

Docker守护进程(dockerd)始终监听是否有新的请求到达。当用户调用某个命令或API时,这些调用将被转化为某种类型的请求发送给dockerd。Docker守护进程管理各种Docker对象,包括镜像(Image)、容器(Container)、网络、卷、插件等。

(2)Docker客户端Docker客户端是Docker用户与Docker守护进程进行交互的主要方式。当我们在命令行界面输入Docker命令时,Docker客户端将封装消息并传递给dockerd,dockerd根据消息的类型采取不同的行动。

(3)Docker仓库Docker仓库存储着Docker的镜像。Docker Hub和Docker Cloud是公共的Docker仓库,任何人都可以将自己的本地镜像上传到公共仓库,或者将公共仓库的镜像下载到本地。我们可以使用配置文件来指定Docker仓库的位置,默认的仓库是Docker hub。

例如,当我们想从仓库拉取Nginx镜像时,首先在命令行输入如下命令,开始从远端拷贝Nginx镜像到本地。

$ docker pull nginx

一旦上述命令执行完毕,Nginx镜像将会被拷贝到本地,并由Docker引擎管理。通过list命令检查已经拉取的成功的镜像。

由于镜像已经被拉取到本地,当启动Nginx镜像的容器时,Docker引擎将直接从本地读取内容,运行速度将非常快,可以使用如下命令运行一个容器。

$ docker run-name web1 –d –p 8080:80 nginx
1.Docker网络

通过加载不同的驱动程序,Docker网络可以实现不同类型的网络。目前存在以下几种驱动程序。

  • bridge:默认的网络驱动程序,创建的网络为桥接网络。桥接网络适用于在同一个主机上运行的容器之间的通信,对于不同主机上容器之间的通信,可以在操作系统级别管理路由或使用overlay类型的网络。
  • host:使用host作为驱动程序,容器将直接使用宿主机的网络,并去除容器与宿主机之间的网络隔离。目前host驱动仅适用于Docker 17.06及更高版本的Swarm服务。
  • overlay:overlay类型的网络可以连接多个宿主机,从而使得Swarm服务之间能够相互通信。
  • macvlan:macvlan网络允许为容器分配MAC地址,使其显示为网络上的物理设备。
  • none:使用none驱动的容器,将被禁用所有网络。none驱动不适用于Swarm服务。
  • network plugin:可以从Docker Store或第三方供应商处获得网络插件。

默认设置下使用bridge驱动时,Docker会自动生成一个虚拟网桥(名称默认为docker0),并且从RFC1918定义的私有地址块中为其分配一个子网。Docker为每一个生成的容器分配一个已连接至网桥的虚拟以太网设备veth,而veth则通过Linux Namespace 在容器中映射显示为eth0,容器内部的eth0会在网桥的地址段内被分配一个IP地址。

结果仅当多个Docker容器处于同一物理主机内(即连接于同一虚拟网桥)时,它们才能互相通信。处于不同机器内的容器间无法相互通信,而且事实上它们可能拥有完全相同的网域及IP地址。如下图所示,容器A、B、C分别通过veth pair连接至网桥docker0,使得以上三个容器能够相互通信。

对于运行在其他物理主机的容器,由于无法连接至物理主机192.168.100.100上的网桥docker0,从而无法与容器A、B、C进行通信。

为了使得跨主机的Docker容器间能够实现互相通信,必须要在主机自身的IP地址上为它们分配端口并将这些端口转发或代理至容器处。显然,容器间必须相互协调小心使用分配的端口,或是动态分配这些端口(即是docker -p 8080:80 类似这样的分配端口,并避免port占用)。

2.Libnetwork

Docker在1.9版本中引入了一整套的Docker Network子命令和跨主机网络支持。其实,早在Docker 1.7版本中,网络部分代码就已经被剥离并单独成为了Docker的网络库,即Libnetwork。此后,容器的网络模式也被抽象变成了统一接口的驱动。

为了标准化网络驱动的开发步骤和支持多种网络驱动,Docker公司在Libnetwork中使用了CNM(Container Networ kModel)。CNM定义了构建容器虚拟化网络的模型,同时还提供了可以用于开发多种网络驱动的标准化接口和组件。Libnetwork和Docker daemon及各个网络驱动的关系如下图所示。

Docker daemon通过调用Libnetwork对外提供的API完成网络的创建和管理。Libnetwork内部则使用了CNM来实现网络功能。CNM中主要有沙盒(sandbox)、端点(endpoint)和网络(network)3种组件。

  • 沙盒:一个沙盒包含了一个容器网络栈的信息。沙盒可以对容器的接口(interface)、路由和DNS设置等进行管理。沙盒的实现可以是Linux network namespace、FreeBSD Jail或类似的机制。一个沙盒可以有多个端点和网络。
  • 端点:一个端点可以加入一个沙盒和一个网络。端点的实现可以是veth pair、Open vSwitch内部端口或类似的设备。一个端点可以属于一个网络并且只属于一个沙盒。
  • 网络:一个网络是一组可以直接互相联通的端点。网络的实现可以是Linux Bridge、VLAN等。一个网络可以包含多个端点。

Docker最初只有三种网络类型(bridge、none、host),在引入Libnetwork之后,又增加了overlay、remote driver、macvlan等。现在用户能以驱动/插件的形式,使用其他特定类型的网络插件实体,例如overlay。

Libnetwork将以接口的形式为Docker提供网络服务。为了支持第三方的驱动,引入了remote driver类型,通过统一的Json-RPC接口,让更专业的网络供应商加入Docker的生态圈来,使用户不再局限于原生驱动,大大提高了Docker网络的升级扩展能力。

kubernetes

容器是很轻量化的技术,相对于物理机和虚拟机而言,在等量资源的基础上能创建出更多的容器实例。一旦面对着分布在多台主机上且拥有数百个容器的大规模应用时,传统的或单机的容器管理解决方案就会变得力不从心。

另一方面,由于为微服务提供了越来越完善的原生支持,在一个容器集群中的容器粒度越来越小、数量越来越多,这种情况下,容器或微服务都需要接受管理并有序接入外部环境,从而实现调度、负载均衡及分配等任务。简单而高效地管理快速增长的容器实例,自然成了一个容器编排系统的主要任务。

而Kubernetes就是容器编排和管理系统中的当红选手。Kubernetes的核心是如何解决自动部署,扩展和管理容器化应用程序。

如下图所示,Kubernetes属于主从的分布式集群架构,包含Master和Node:Master作为控制节点,调度管理整个系统;Node是运行节点,运行业务容器。每个Node上运行有多个Pod,Pod中可以运行多个容器(通常一个Pod中只部署一个容器,也可以将一些高度耦合的容器部署在一起),然而Pod无法直接对来自Kubernetes集群外部的访问提供服务。

kubernetes基本概念说明

1.Master

Master节点上面主要由四个组件组成:API Server、Scheduler、Controller Manager、etcd。

etcd是Kubernetes用于存储各个资源状态的分布式数据库,采用Raft协议作为一致性算法。

API Server(kube-apiserver)主要提供认证与授权、管理API版本等功能,通过RESTfulAPI向外提供服务,任何对资源(Pod、Deployment、Service等)进行增删改查等操作都要交给API Server处理后再提交给etcd。

Scheduler(kube-scheduler)负责调度Pod到合适的Node上,根据集群的资源和状态选择合适的节点创建Pod。如果把Scheduler看作一个黑匣子,那么它的输入是Pod和由多个Node组成的列表,输出是Pod和一个Node的绑定,即将Pod部署到Node上。Kubernetes提供了调度算法的实现接口,用户可以根据自己的需求定义自己的调度算法。

如果说API Server做的是“前台”的工作的话,则Controller Manager就是负责的“后台”。每个资源一般都对应有一个控制器,而Controller Manager就是负责管理这些控制器的。比如我们通过API Server创建一个Pod,当Pod创建成功后,API Server的任务就算完成了,而后面保证Pod的状态始终和预期一样的重任就由Controller Manager去完成了。

2.Pod

Kubernetes将容器归类到一起,形成“容器集”(Pod)。

Pod是Kubernetes的基本操作单元,也是应用运行的载体。整个Kubernetes系统都是围绕着Pod展开的,比如如何部署运行Pod、如何保证Pod的数量、如何访问Pod等。

Pod为容器分组增加了一个抽象层,有助于调用工作负载,并为这些容器提供所需的联网和存储等服务。

一个Pod表示一组容器所形成的集合,以及这些容器所共享的一些资源。这些资源包括:

  • 所共享的存储,如卷(Volume)。
  • 网络,如Pod的IP地址。
  • 运行这些容器所需的相关信息,如容器镜像版本、使用的特定端口等。

同一Pod下的多个容器共用一个IP,则不能出现重复的端口号,比如在一个Pod下运行两个Nginx就会有一个容器出现异常。一个Pod下的多个容器可以使用localhost加端口号访问对方的端口。

如下图所示,每个圆圈代表一个Pod,圆圈中的正方体代表一个应用程序容器,圆柱体代表一个卷。Pod 4中包含了三个应用程序容器、两个卷,该Pod的IP地址为10.10.10.4。而Pod1中只包含了一个应用程序容器。

3.Node

Node指Kubernetes中的工作机器(worker machine),可以是虚拟机,也可以是物理机,由Master来进行管理。每个Node上可以运行多个Pod,Master会根据集群中每个Node上的可用资源情况自动地调度Pod的部署。每个Node上都会运行以下组件。

  • kubelet:是Master在每个Node节点上面的agent,负责Master和Node之间的通信,并管理Pod和容器。
$ ps -ef | grep kubelet | grep -v grep
root     114577      1 14 Aug11 ?        4-04:40:03 /usr/bin/kubelet --kubeconfig=/etc/kubernetes/kubeconfig --logtostderr=false --v=3 --address=XX.XX.XX.XXX  --allow-privileged=false --v=3 --system-reserved=cpu=1,memory=1Gi,egress=100M --symlink-root-dir=/gaia/log_service/logs/ --local-disks=/data/kube_local_disk --log-dir=/gaia/k8s/log/kubelet --pod-infra-container-image=docker.oa.com:8080/public/kubernetes_pause:latest --network-plugin=cni --cluster-dns=192.168.192.2 --cluster-domain=cluster.local --reserved-local-disk-capacity=10 --bandwidth-limit-iface=bond1 --eviction-hard=memory.available<3952Mi,allocatableMemory.available<1Gi,nodeMemory.available<1Gi --eviction-soft=memory.available<6586Mi,allocatableMemory.available<2Gi,nodeMemory.available<2Gi --eviction-soft-grace-period=memory.available=30s,allocatableMemory.available=30s,nodeMemory.available=30s --eviction-max-pod-grace-period=60 --eviction-minimum-reclaim=memory.available=100Mi --healthz-bind-address=0.0.0.0 --eviction-pressure-transition-period=1m0s --cpu-reserved-enabled=true --maximum-dead-containers=30 --minimum-container-ttl-duration=1m --free-disk-thread-hold-gb=10 --serialize-image-pulls=false --feature-gates=ExpandPersistentVolumes=true,PVCProtection=true,CPUManager=true,PodPriority=true,Accelerators=true,DevicePlugins=true --cgroups-per-qos=true --cpu-manager-policy=static --allow-privileged=true
  • kube-proxy:实现了Kubernetes中的服务发现和反向代理功能。在反向代理方面,kube-proxy支持TCP和UDP连接转发,默认基于Round

    Robin算法将客户端流量转发到与Service对应的一组后端pod。在服务发现方面,kube-proxy使用etcd的watch机制,监控集群中Service

    和Endpoint对象数据的动态变化,并且维护一个Service到Endpoint的映射关系,从而保证了后端Pod的IP变化不会对访问者造成影响。另外,kubeproxy还支持sessionaffinity。

  • 一个容器:负责从Registry拉取容器镜像、解压缩容器及运行应用程序。

下图展示了一个Node,其中部署了四个Pod,此外Node上还运行有kubelet和一个Docker容器。

Pod是有生命周期的,Pod被分配到一个Node上之后,就不会离开这个Node,直到被删除。

当某个Pod失败时,首先会被Kubernetes清理掉,之后Replication Controller会在其他机器(或本机)上重建Pod,重建之后Pod的ID发生了变化,与原有的Pod将拥有不同的IP地址,因而将会是一个新的Pod。所以,Kubernetes中Pod的迁移,实际指的是在新Node上重建Pod。而将Pod部署在Service中,使得Kubernetes可以自动协调Pod之间的更改,从而支持应用程序的持续运行。

4.Replication Controller -------(当前很少用,已被deployment替代)

Replication Controller(RC)是Kubernetes中的另一个核心概念,应用托管在Kubernetes之后,Kubernetes需要保证应用能够持续运行,这就是RC的工作内容,它会确保任何时间Kubernetes中都有指定数量的Pod在运行。在此基础上,RC还提供了一些更高级的特性,比如弹性伸缩、滚动升级等。

RC与Pod的关联是通过Label来实现的,Label是一系列的Key/Value对。Label机制是Kubernetes中的一个重要设计,通过Label进行对象的关联,可以灵活地进行分类和选择。对于Pod,需要设置其自身的Label来进行标识。

Label的定义是任意的,但是必须具有可标识性,比如设置Pod的应用名称和版本号等。另外,Lable不具有唯一性,为了更准确地标识一个Pod,应该为Pod设置多个维度的label。例如:

kubectl get rc --all-namespaces
kubectl get pods --show-labels --all-namespaces

由于目前RC 在实际运维过程中,目前已经被Deployment 取代;关于RC控制的弹性伸缩和滚动升级这里就不在说明;

5.Service

为了适应快速的业务需求,微服务架构已经逐渐成为主流。微服务架构的应用需要有非常好的服务编排支持,Kubernetes中的核心要素Service便提供了一套简化的服务代理和发现机制,天然适应微服务架构。

在Kubernetes中,受到RC调控时,Pod副本是变化的,对应虚拟IP也是变化的,比如发生迁移或伸缩的时候,这对于Pod的访问者来说是不可接受的。

Service是服务的抽象,定义了一个Pod的逻辑分组,和访问这些Pod的策略,执行相同任务的Pod可以组成一个Service,并以Service的IP提供服务。Service的目标是提供一种桥梁,它会为访问者提供一个固定的访问地址,用于在访问时重定向到相应的后端,这使得非Kubernetes原生的应用程序在无须为Kubemces编写特定代码的前提下,能轻松访问后端。

Service同RC一样,都是通过Label来关联Pod的。一组Pod能够被Service访问到,通常是通过Label Selector实现的。Service负责将外部的请求发送到Kubernetes内部的Pod,同时也将内部Pod的请求发送到外部,从而实现服务请求的转发。

当Pod发生变化时(增加、减少、重建等),Service会及时更新。这样一来,Service就可以作为Pod的访问入口,起到代理服务器的作用,而对于访问者来说,通过Service进行访问,无须直接感知Pod。

需要注意的是,Kubernetes分配给Service的固定IP地址是一个虚拟IP地址,并不是一个真实的IP地址,在外部是无法寻址的。在真实的系统实现上,Kubernetes通过kube-proxy实现虚拟IP路由及转发的。所以正如前面所说的,每个Node上都需要部署Proxy组件,从而实现Kubernetes层级的虚拟转发网络。

  • Service内部负载均衡

    当Service的Endpoints包含多个IP地址的时候,服务代理存在多个后端,将进行请求的负载均衡。默认的负载均衡策略是轮询或随机(由kube-proxy的模式决定)。

  • 多个Service如何避免地址和端口冲突

    Kubernetes为每个Service分配一个唯一的ClusterIP,所以当使用ClusterIP:Port的组合访问一个Service的时候,不管Port是什么,这个组合是一定不会发生重复的。

    另一方面,kube-proxy为每个Service真正打开的是一个绝对不会重复的随机端口,用户在Service描述文件中指定的访问端口会被映射到这个随机端口上。这就是为什么用户在创建Service时可以随意指定访问端口。

  • 新一代副本控制器Replica Set

    这里所说的Replica Set(RS),可以被认为是“升级版”的Replication Controller。也就是说,RS也是用于保证与Label Selector匹配的Pod数量维持在期望状态。区别在于RS引入了对基于子集的selector查询条件,而RC仅支持基于值相等的selector查询条件。

    这是目前从用户角度看,两者唯一的显著差异。社区引入这一API的初衷是用于取代vl版本中的RC,也就是说,当v1版本被废弃时,RC就完成了它的历史使命,而由RS来接管其工作。虽然RS可以被单独使用,但是目前它多被Deployment用于进行Pod的创建、更新与删除。Deployment在滚动升级等方面提供了很多非常有用的功能。

kubectl get rs --all-namespaces
kubectl get deployment --all-namespaces
6.Deployment

Kubernetes提供了一种更加简单的更新RC和Pod的机制,叫作Deployment。通过在Deployment中描述所期望的集群状态,Deployment Controller会将现在的集群状态在一个可控的速度下逐步更新成期望的集群状态。

Deployment的主要职责同样是为了保证Pod的数量和健康,继承了上面所述的Replication Controller全部功能(90%的功能与Replication Controller完全一样),可以看作新一代的Replication Controller。

但是,它又具备了ReplicationController之外的新特性:

  • 事件和状态查看:可以查看Deployment的升级详细进度和状态。

  • 回滚:当升级Pod镜像或相关参数的时候发现问题,可以使用回滚操作回滚到上一个稳定的版本或指定的版本。

  • 版本记录:每一次对Deployment的操作都能保存下来,给予后续可能的回滚使用。

  • 暂停和启动:对于每一次升级,都能够随时暂停和启动。

    多种升级方案:Recreate,删除所有已存在的Pod,重新创建新的;

    Rolling Update,滚动升级,逐步替换的策略,支持更多的附加参数,例如设置最大不可用Pod数量,最小升级间隔时间等。

与RC比较,Deployment具有明显的优势,Deployment使用了RS,是更高一层的概念。RC只支持基于等式的selector(env=dev或environment!=qa),但RS还支持新的,基于集合的selector(version in (v1.0,v2.0)或env notin(dev,qa)),这对复杂的运维管理很方便。使用Deployment升级Pod,只需要定义Pod的最终状态,Kubernetes会执行必要的操作。此外,Deployment还拥有更加灵活强大的升级、回滚功能。

7.Namespace

对于同一物理集群,Kubernetes可以虚拟出多个虚拟集群,这些虚拟集群即称为Namespace。

Kubernetes中的Namespace并不是Linux中的Namespace。Kubernetes中的Namespace旨在解决的场景为:多个用户分布在多个团队或项目中,但这些用户使用同一个Kubernetes集群。Kubernetes通过Namespace将一个集群的资源分配给了多个用户。

Namespace中包含的资源通常有Pod、Service和Replication Controller等,但是一些较底层的资源并不属于任何一个Namespace,如Node、PersistentVolume不属于任何一个Namespace。同一个Namespace下的资源名称必须唯一,但是不同Namespace下的资源名称可以重复。

Kubernetes网络

如下图所示,Kubernetes的网络通信可以分为以下几个部分:Pod内部的容器间通信、Pod间通信、Pod与Service之间的网络通信、Kubernetes外部与Service之间的网络通信。

1.Pod内部的容器间通信

Kubernetes为每一个Pod分配了一个IP地址,且同一个Pod内的容器共享Pod的网络命名空间(包括IP地址和网络端口),这也意味着它们之间的访问可以用localhost加上容器端口的方式。这种网络模型被称为“IP-per-Pod”。

该模型的实现需要利用一个Docker容器作为“pod容器”并确保其命名空间已开启,也就是说,Kubernetes在创建Pod时,会首先在Node节点上创建一个运行在Docker Bridge网络上的“pod容器”,并为这个Pod容器创建虚拟网卡eth0及分配IP地址。而Pod里的容器(称为App容器),只需要在创建时使用-net=container:<id>加入该网络命名空间,这样所有的Docker容器就运行在同一个网络命名空间中。

Kubernetes这种“IP-per-pod”网络模型,能让Pod里的容器之间通过localhost网络访问,同时也意味着Pod里的容器必须有效协调使用端口,在端口分配上不能发生冲突,而Pod里的容器根本不用担心和其他Pod里的容器发生端口冲突。

kubectl get pods $pod_name --all-namespaces -o wide
[root@master /]# kubectl get pods $pod_name --all-namespaces -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP           NODE         NOMINATED NODE   READINESS GATES
$pod_name                    1/1     Running   0          47h   $node_ip   $node_ip   <none>           <none>
# READY 表示该pod当前只有1个容器;
# 登录node节点,执行docker ps -a ; 根据pod_name过滤
docker ps -a | grep $pod_name
[root@k8s /]# docker ps -a | grep $pod_name           
636eeada2fc6        docker.oa.com:8080/$namespace_name/$pod_name                            "/bin/bash -c '/usr/…"   2 days ago          Up 2 days                                       k8s_$pod_name_204d2ae1-08d0-4ac0-a1e4-dea17fdb9e7b_0
03b830bd3254        registry.tce.com/library/pause:3.1                                         "/pause"                 2 days ago          Up 2 days                                       k8s_POD_$pod_name_xxx_204d2ae1-08d0-4ac0-a1e4-dea17fdb9e7b_0

其中容器 pause 就是上文所讲的会首先在Node节点上创建一个运行在Docker Bridge网络上的“pod容器”,并为这个Pod容器创建虚拟网卡eth0及分配IP地址。

Kubernetes 中所谓的 pause 容器有时候也称为 infra 容器,它与用户容器”捆绑“运行在同一个 Pod 中,最大的作用是维护 Pod 网络协议栈;

pause容器镜像非常小,目前在700KB左右;永远处于Pause状态;

其他所有容器都会通过 Join Namespace 的方式加入到 Infra container 的 Network Namespace 中。所以说一个 Pod 里面的所有容器,它们看到的网络视图是完全一样的。即:它们看到的网络设备、IP地址、Mac地址等等,跟网络相关的信息,其实全是一份,这一份都来自于 Pod 第一次创建的这个 Infra container。而其他所有网络资源,都是一个 Pod 一份,并且被 Pod 中的所有容器共享。这就是 Pod 的网络实现方式。

由于需要有一个相当于说中间的容器存在,所以整个 Pod 里面,必然是 Infra container 第一个启动。并且整个 Pod 的生命周期是等同于 Infra container 的生命周期的,与容器 A 和 B 是无关的。这也是为什么在 Kubernetes 里面,它是允许去单独更新 Pod 里的某一个镜像的,即:做这个操作,整个 Pod 不会重建,也不会重启,这是非常重要的一个设计。

kubernetes中的pause容器主要为每个业务容器提供以下功能:

  • 在pod中担任Linux命名空间共享的基础;
  • 启用pid命名空间,开启init进程。

$ docker run -d --name pause -p 8880:80 test/pause-amd64:3.0
# --net=container:pause
$ cat <<EOF >> nginx.conff
error_log stderr;
events { worker_connections  1024; }
http {
    access_log /dev/stdout combined;
    server {
        listen 80 default_server;
        server_name example.com www.example.com;
        location / {
            proxy_pass http://127.0.0.1:2368;
        }
    }
}
EOF
$ docker run -d --name nginx -v `pwd`/nginx.conf:/etc/nginx/nginx.conf --net=container:pause --ipc=container:pause --pid=container:pause nginx
$ docker run -d --name ghost --net=container:pause --ipc=container:pause --pid=container:pause ghost

# --net=container:pause --ipc=contianer:pause --pid=container:pause
# 三个容器处于同一个namespace中,init进程为pause
# 在ghost容器中同时可以看到pause和nginx容器的进程,并且pause容器的PID是1。而在kubernetes中容器的PID=1的进程即为容器本身的业务进程。
# 一旦 init 进程被销毁,同一 PID namespace 下的进程也随之被销毁。
$ ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   1024     4 ?        Ss   13:49   0:00 /pause
root         5  0.0  0.1  32432  5736 ?        Ss   13:51   0:00 nginx: master p
systemd+     9  0.0  0.0  32980  3304 ?        S    13:51   0:00 nginx: worker p
node        10  0.3  2.0 1254200 83788 ?       Ssl  13:53   0:03 node current/in
root        79  0.1  0.0   4336   812 pts/0    Ss   14:09   0:00 sh
root        87  0.0  0.0  17500  2080 pts/0    R+   14:10   0:00 ps aux

在容器中,必须要有一个进程充当每个 PID namespace 的 init 进程,使用 Docker 的话,ENTRYPOINT 进程是 init 进程。如果多个容器之间共享 PID namespace,那么拥有 PID namespace 的那个进程须承担 init 进程的角色,其他容器则作为 init 进程的子进程添加到 PID namespace 中。

2.Pod间通信

Pod有可能在同一个Node上运行,也有可能在不同的Node上运行,所以Pod间的通信也分为两类:同一个Node内的Pod之间和不同Node上的Pod之间的通信。

1.同一个Node内的Pod之间

每一个Pod都有一个真实的全局IP地址,同一个Node内的不同Pod之间可以直接采用对方的IP地址通信,而且不需要使用DNS等其他发现机制。如图示例,Pod和Pod2都是通过veth连接在同一个Docker0网桥上的,它们的IP地址IP1、IP2都是从Docker0的网段上动态获取的,和网桥本身的IP3是同一个网段。

另外,在Pod1、Pod2的Linux协议栈上,默认路由都是Docker0的地址,也就是说所有非本地地址的网络数据,都会被默认发送到Docker0网桥上,由Docker0网桥直接中转。由于Pod1和Pod2都关联在同一个Docker0网桥上,位于同一个网段,它们之间是能直接通信的。

2.不同Node上的Pod之间

Pod的地址与Docker0在同一个网段,而Docker0与宿主机网卡又是两个完全不同的IP网段,并且不同Node之间的通信只能通过宿主机的物理网卡进行,因此要想实现位于不同Node上的Pod之间的通信,必须想办法通过主机的IP地址来进行寻址和通信。

另一方面,这些动态分配且藏在Docker0之后的“私有”IP地址也是可以找到的。Kubernetes会记录所有正在运行的Pod的IP地址分配信息,并将这些信息保存在etcd中(作为Service的Endpoint)。因为Kubernetes要求使用Pod的私有IP地址进行Pod之间的通信,所以首先要知道这些IP地址是什么。

此外,这些Pod的IP地址规划也很重要,不能发生冲突。只要没有冲突,就可以想办法在整个Kubernetes的集群中找到它。

总结来说,要想支持不同Node上的Pod间通信,要达到两个条件:

  • 在整个Kubernetes集群中对Pod的IP地址分配进行规划,不能有冲突。

  • 找到一种办法,将Pod的IP地址和所在Node的IP地址关联起来,让Pod之间可以互相访问。

根据第一个条件,需要在部署Kubernetes时,对Docker0的IP地址进行规划,保证每一个Node上的Docker0地址没有冲突。

根据第二个条件,Pod中的数据在发出时,需要有一个机制能够知道对方Pod的IP地址挂载在哪个具体的Node上。也就是说先要找到Node对应的宿主机IP地址,将数据发送到宿主机的网卡上,然后在宿主机上将应用的数据转到具体的Docker0上。一旦数据到达宿主机Node,则Node内部的Docker0便知道如何将数据发送到Pod。

如下图,IP 1对应的是Pod1,IP 2对应的是Pod2。在Pod1访问Pod2时,首先要将数据从源Node的eth0发送出去,找到并到达Node 2的eth0。也就是说先要从IP 3到IP 4,之后才是IP 4到IP 2的递送。

因此在实际环境中,除了部署Kubernetes和Docker,还需要额外的网络配置,甚至通过一些软件或插件来实现Kubernetes对网络的要求。之后,Pod之间才能无差别地透明通信。

3.Pod与Service之间的网络通信

Kubernetes里的Pod是不稳定的,它会由于各种原因被销毁和创造。比如在垂直扩容和滚动更新过程中,旧的Pod会被销毁,被新的Pod代替。在这期间,Pod的IP地址可能会发生变化。因此提供同一服务的Pod,其IP地址可能会发生变化,这也就使得前端无法通过访问Pod的IP地址的方式来获取服务,所以Kubernetes引入了Service的概念。

Service是一个抽象的实体,Kubernetes在创建Service实体时,为其分配了一个虚拟的IP,这个IP地址是相对固定的。当需要访问Pod里的容器所提供的功能时,不直接使用Pod的IP地址和端口,而是访问Service的这个虚拟IP和端口,再由Service把请求转发给它背后的Pod。如下图中的Service背后有3个Pod来承载。此外,Kubernetes还通过Service实现了负载均衡、服务发现和DNS等功能。

Kubernetes在创建Service时,根据Service的标签选择器(Label Selector)来查找Pod,据此创建与Service同名的EndPoints对象,Service的targetPort和Pod的IP地址都记录在与Service同名的EndPoints里。当Pod的地址发生变化时,EndPoints也随之变化。当Service接收到请求时,就能通过EndPoints找到请求需要转发的目标地址。

Service仅仅是一个抽象的实体,为其分配的IP地址也只是一个虚拟的IP地址,这背后真正负责转发请求的是运行在Node上的kube-proxy。

在Kubernetesv1.0中,kubeproxy运行在用户空间中。在Kubernetes v1.1中,添加了iptables代理,并成为自Kubernetes v1.2以来的默认操作模式。在Kubernetes v1.8.0-beta.0中,添加了ipvs代理。

因此,目前kubeproxy共有三种请求转发模式,分别为userspace、iptables和ipvs。

kubectl get services --all-namespaces -o wide
kubectl get svc --all-namespaces -o wide
# 其中有 ClusterIP,NodePort,LoadBalancer 几种service 类型
kubectl get svc -ntce $service-name  -o yaml
# 很多成熟的kubernetes 二次开发的项目,会在已有的资源类型基础上自定义添加一些资源类型 即 crd
kubectl get crd --all-namespaces -o wide
# 会以crd 为模板,创建对应的资源对象,eg ;vpcservices 就是 crd 中定义的模板
kubectl get vpcservice --all-namespaces -o wide 

(1)userspace模式

在userspace模式下,kube-proxy会监控Master对Service和Endpoints对象的添加和删除操作。创建Service时,Node节点上的kube-proxy会为其随机开放一个端口(称为代理端口),然后建立一个iptables规则,iptables会完成<服务虚拟IP,端口>与代理端口的流量转发,再从EndPoints里选择一个Pod,把代理端口的流量转给该Pod。

当EndPoints下有多个Pod时,选择Pod的算法有两种:一是依次循环,如果一个Pod没有响应,就试下一个;二是选择与请求来源IP地址更接近的Pod。

(2)iptables模式

在iptables模式下,创建Service时,Node节点上的kube-proxy会建立两个iptables规则,一个为Service服务,用于将<服务虚拟IP,端口>的流量转给后端,另一个为Endpoints创建,用于选择Pod。在默认情况下,后端的选择是随机的。

iptables模式下的kube-proxy不需要在用户空间和内核空间之间进行切换,这种模式下kube-proxy应该比userspace模式下运行得更快、更可靠。但与userspace模式下的kube-proxy不同的是,如果最初选择的Pod没有响应,则iptables模式下的kube-proxy无法自动重试另一个Pod。因此,使用iptables模式需要运行有readiness probes(准备情况探测器)。

kubelet使用readiness probes来了解容器何时可以开始接收流量。当一个Pod内部所有容器都准备就绪时,该Pod被认为已准备就绪接收流量。当Pod未就绪时,它将从服务负载平衡器中删除。因此,可将readiness probes用于控制那些Pod用作Service的后端。

(3)ipvs模式

在ipvs模式下,kube-proxy会调用netlink接口以创建相应的ipvs规则,并定期与Service和Endpoint同步ipvs规则,从而确保ipvs状态与期望一致。访问Service时,流量将被重定向到其中某一个后端Pod。与iptables类似,ipvs也基于netfilter hook函数。但不同的是,iptables规则为顺序匹配,当规则数量较多时,匹配时间将显著变长;而ipvs使用散列表作为底层数据结构,并在内核空间中工作,这使得规则匹配的时间较短。这也意味着ipvs可以更快地重定向流量,并且在同步代理规则时具有更好的性能。

此外,ipvs提供了多种负载均衡算法,例如:rr(round-robin,轮询调度算法),lc(least connection,最少连接数调度算法),dh(destination hashing,目的地址散列调度算法),sh(source hashing,源地址散列调度算法),sed(shortest expected delay,最短期望延迟调度算法),nq(never queue,永不排队调度算法)。

需要注意的是,ipvs模式需要Node上预先安装ipvs内核模块。当kube-proxy以ipvs模式启动时,kube-proxy将验证Node上是否安装了ipvs模块。如果未安装,则kube-proxy将使用iptables模式。

4.Kubernetes外界与Service之间的网络通信

根据应用场景的不同,Kubernetes提供了4种类型的Service:

  • ClusterIP:在集群内部的IP地址上提供服务,并且该类型的Service只能从集群内部访问。该类型为默认类型。

  • NodePort:通过每个Node IP上的静态端口(NodePort)来对外提供服务,集群外部可以通过访问:来访问对应的端口。在使用该模式时,会自动创建ClusterIP,访问NodePort的请求会最终路由到ClusterIP。

  • LoadBalancer:通过使用云服务提供商的负载均衡器对集群外部提供服务。使用该模式时,会自动创建NodePort和ClusterIP,集群外部的负载均衡器最终会将请求路由给NodePort和ClusterIP

  • ExternalName:将服务映射到集群外部的某个资源,例如foo.bar.example.com。使用该模式需要1.7版本或更高版本的kubedns。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值