前言
个人理解有限,如有错误,请及时指正。
前前后后学习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
的表将会变得很大,这时会有性能问题。
总结
结尾
通过这篇文章我们简单的了解到内部负载均衡的机制,但是任然不够深入,你也可用通过这篇文章查漏补缺,觉得有什么错误的地方欢迎及时指正,我的邮箱shinemotec@gmail.com
。下一篇将会讲Kubernetes
的HPA
工作原理。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网