内容简介:k8s 内部负载均衡原理
前言
个人理解有限,如有错误,请及时指正。
前前后后学习kubernetes
已经有三个月了,一直想写一遍关于kubernetes
内部实现的一系列文章来作为这三个月的总结,个人觉得kubernetes
背后的架构理念以及技术会成为中大型公司架构的未来。我推荐可以先阅读下Google的Large-scale cluster management at Google with Borg技术文献,它是实现kubernetes
的基石。
准备
在阐述原理之前我们需要先了解下kubernetes
关于内部负载均衡的几个基础概念以及组件。
概念
Pod
1.Pod
是Kubernetes
创建或部署的最小/最简单的基本单位。
2.如图所示,Pod
的基础架构是由一个根容器Pause Container
和多个业务Container
组成的。
3.根容器的IP
就是Pod IP
,是由kubernetes
从etcd
中取出相应的网段分配的, Container IP
是由docker
分配的,同样这些IP
相对应的IP
网段是被存放在etcd
里。
4.业务Container
暴露出来端口并且映射到相应的根容器Pause Container
端口,映射出来的端口叫做endpoint
。
5.业务Container
的生命周期就是POD
的生命周期,任何一个与之相关联的Container
死亡,POD
也应该随之消失
Service
1.Service
是定义一系列Pod以及访问这些Pod的策略的一层抽象。Service
通过Label
找到Pod
组。因为Service
是抽象的,所以在图表里通常看不到它们的存在,这也就让这一概念更难以理解。
2.Kubernetes
也会分给Service
一个内部的Cluster IP
,Service
通过Label
查询到相应的Pod
组, 如果你的Pod
是对外服务的那么还应该有一组endpoint
,需要将endpoint
绑到Service
上,这样一个微服务就形成了。
Kubernetes CNI
CNI(Container Network Interface)是用于配置 Linux 容器的网络接口的规范和库组成,同时还包含了一些插件。CNI仅关心容器创建时的网络分配,和当容器被删除时释放网络资源。
Ingress
1.俗称边缘节点,假如你的Service
是对外服务的,那么需要将Cluster IP
暴露为对外服务,这时候就需要将Ingress
与Service
的Cluster IP
与端口绑定起来对外服务。这样看来其实Ingress
就是将外部流量引入到Kubernetes
内部。
2.实现Ingress的开源组件有Traefik
和Nginx-Ingress
, 前者方便部署,后者部署复杂但是性能和灵活性更好。
组件
Kube-Proxy
1.Kube-Proxy
是被内置在Kubernetes
的插件。
2.当Service
与Pod
Endpoint
变化时,Kube-Proxy
将会改变宿主机iptables
, 然后配合Flannel
或者Calico
将流量引入Service
.
Etcd
1.Etcd
是一个简单的Key-Value
存储工具。
2.Etcd
实现了Raft
协议,这个协议主要解决分布式强一致性
的问题,与之相似的有Paxos
, Raft
比Paxos
要容易实现。
3.Etcd
用来存储Kubernetes
的一些网络配置和其他的需要强一致性的配置,以供其他组件使用。
4.如果你想要深入了解Raft
, 不放先看看raft相关资料
Flannel
1.Flannel
是CoreOS
团队针对Kubernetes
设计的一个覆盖网络Overlay Network
工具,其目的在于帮助每一个使用Kuberentes
的CoreOS
主机拥有一个完整的子网。
2.主要解决POD
与Service
,跨节点
相互通讯的。
Traefik
1.Traefik
是一个使得部署微服务更容易的现代HTTP反向代理、负载。
2.Traefik
不仅仅是对Kubernetes
服务的,除了Kubernetes
他还有很多的Providers
,如Zookeeper
,Docker Swarm
, Etcd
等等
Traefik工作原理
授人以鱼不如授人以渔,我想通过我看源码的思路来抛砖引玉,给大家一个启发。
思考
在我要深度了解一个组件的时候通常会做下面几件事情
-
组件扮演的角色
-
手动编译一个版本
-
根据语言特性来了解组件初始化流程
-
看单元测试,了解函数具体干什么的
- 手动触发一个流程,在关键步骤处记录日志,单步调试
Traefik初始化流程
1.在github.com/containous/traefik/cmd/traefik
下由一个名为traefik.go
的文件是该组件的入口。main()
方法里有这样一段代码
// 加载 Traefik全局配置
traefikConfiguration := cmd.NewTraefikConfiguration()
// 加载providers的配置
traefikPointersConfiguration := cmd.NewTraefikDefaultPointersConfiguration()
...
// 加载store的配置
storeConfigCmd :=storeconfig.NewCmd(traefikConfiguration, traefikPointersConfiguration)
// 获取命令行参数
f := flaeg.New(traefikCmd, os.Args[1:])
// 解析参数
f.AddParser(reflect.TypeOf(configuration.EntryPoints{}), &configuration.EntryPoints{})
...
// 初始化Traefik
s := staert.NewStaert(traefikCmd)
// 加载配置文件
toml := staert.NewTomlSource("traefik", []string{traefikConfiguration.ConfigFile, "/etc/traefik/", "$HOME/.traefik/", "."})
...
// 启动服务
if err := s.Run(); err != nil {
fmtlog.Printf("Error running traefik: %s\n", err)
os.Exit(1)
}
os.Exit(0)
上面就是组件初始化流程,当我们看完初始化流程的时候应该会想到下面几个问题:
-
当我们手动或者自动伸缩
Pods
时,Traefik
是怎么知道的?假设你已经知道
Kubernets
是一个C/S
架构,所有的组件都要通过kube-apiserver
来了解其他节点或者组件的运行状态。当然
Traefik
也不例外,他是通过Kubernetes
开源的Client-Go
SDK来完成与kube-apiserver
交互的。我们来找找源码:
github.com/containous/traefik/provider/kubernetes
是关于Kubernetes
的源码。我们看看到底干了啥。// client.go type Client interface { // 检测Namespaces下的所有变动 WatchAll(namespaces Namespaces, stopCh <-chan struct{}) (<-chan interface{}, error) // 获取边缘节点 GetIngresses() []*extensionsv1beta1.Ingress // 获取Service GetService(namespace, name string) (*corev1.Service, bool, error) // 获取秘钥 GetSecret(namespace, name string) (*corev1.Secret, bool, error) // 获取Endpoint GetEndpoints(namespace, name string) (*corev1.Endpoints, bool, error) // 更新Ingress状态 UpdateIngressStatus(namespace, name, ip, hostname string) error }
显而易见,这里通过订阅
kube-apiserver
,来实时的知道Service
的变化,从而实时更新Traefik
。
我们再来看看具体实现// kubernetes.go func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { ... // 初始化一个kubernets client k8sClient, err := p.newK8sClient(p.LabelSelector) if err != nil { return err } .... // routines 连接池,这里的routines实现的很优雅,有心的同学看下 pool.Go(func(stop chan bool) { operation := func() error { for { stopWatch := make(chan struct{}, 1) defer close(stopWatch) // 监视和更新namespaces下的所有变动 eventsChan, err := k8sClient.WatchAll(p.Namespaces, stopWatch) .... for { select { case <-stop: return nil case event := <-eventsChan: // 从kubernestes 那边接收到的事件 log.Debugf("Received Kubernetes event kind %T", event) // 加载默认template配置 templateObjects, err := p.loadIngresses(k8sClient) ... // 对比最后一次的和这次的配置有什么不同 if reflect.DeepEqual(p.lastConfiguration.Get(), templateObjects) { // 相同的话,滤过 log.Debugf("Skipping Kubernetes event kind %T", event) } else { // 否则更新配置 p.lastConfiguration.Set(templateObjects) configurationChan <- types.ConfigMessage{ ProviderName: "kubernetes", Configuration: p.loadConfig(*templateObjects), } } } } } }
Kubernets
返回给Traefik
的数据结构大致是这样的:{"service":{"pod_name":{"domain":"ClusterIP"}}}
看过上述的代码分析应该就对Traefik有一个大致的了解了。
Kube-Poxy工作原理
Kube-Proxy
与Traefik
实现原理很像,都是通过与kube-apiserver
的交互来完成实时更新iptables
的,这里就不细说了,以后会有一篇文章专门讲kube-dns
, kube-proxy
, Service
的。
组件协同与负载均衡
简单描述流程,然后思考问题,最后考虑是否需要深入了解(取决于个人兴趣)
组件协同
用户通过访问Traefik
提供的L7层端口, Traefik
会转发流量到Cluster IP
,Flannel
会将用户的请求准确的转发到相应的Node
节点的Service
上。(ps: Flannel
初始化的时候宿主机会建立一个叫flannel0
【这里的数字取决于你的Node节点数】的虚拟网卡)
负载均衡
上文讲述了kube-proxy
是通过iptables
来配合flannel
完成一次用户请求的。
具体的流程我们只要看一个service
的iptables rules
就知道了。
// 只截取了一小段,假设我们起了两个Pods
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
// 流量跳转至 KUBE-SVC-ILP7Z622KEQYQKOB
-A KUBE-SERVICES -d 10.111.182.127/32 -p tcp -m comment --comment "pks/car-info-srv:http cluster IP" -m tcp --dport 80 -j KUBE-SVC-ILP7Z622KEQYQKOB
// 50%的几率跳转至KUBE-SEP-GDPUTEQG2YTU7YON
-A KUBE-SVC-ILP7Z622KEQYQKOB -m comment --comment "pks/car-info-srv:http" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-GDPUTEQG2YTU7YON
// 流量转发至真正的Service Cluster IP
-A KUBE-SEP-GDPUTEQG2YTU7YON -s 10.244.1.57/32 -m comment --comment "pks/car-info-srv:http" -j KUBE-MARK-MASQ
-A KUBE-SEP-GDPUTEQG2YTU7YON -p tcp -m comment --comment "pks/car-info-srv:http" -m tcp -j DNAT --to-destination 10.244.1.57:80
可以很明显的看出来,kubernetes
内部的负载均衡是通过iptables
的probability
特性来做到的,这里就会有一个问题,当Pod
副本数量过多时,iptables
的表将会变得很大,这时会有性能问题。
总结
Traefik
通过默认的负载均衡(wrr)直接将流量通过Flannel
送进POD
.kube-proxy
在没有ipvs
的情况下, 会通过iptables
转发做负载均衡.
结尾
通过这篇文章我们简单的了解到内部负载均衡的机制,但是任然不够深入,你也可用通过这篇文章查漏补缺,觉得有什么错误的地方欢迎及时指正,我的邮箱shinemotec@gmail.com
。下一篇将会讲Kubernetes
的HPA
工作原理。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java Web入门经典
王国辉、陈英 / 机械工业出版社 / 2013-6 / 69.00元
《Java Web入门经典》以初学者为核心,全面介绍了JavaWeb开发中常用的各种技术。内容排列上由浅入深,让读者循序渐进掌握编程技术;在内容讲解上结合丰富的图解和形象的比喻,帮助读者理解“晦涩难懂”的技术;在内容形式上附有大量的提示、技巧、说明等栏目,夯实读者编程技术,丰富编程经验。全书共分4篇19章,其中,第一篇为“起步篇”,主要包括开启JavaWeb之门、不可不知的客户端应用技术、驾驭Ja......一起来看看 《Java Web入门经典》 这本书的介绍吧!