• -------------------------------------------------------------
  • ====================================

k8s 内部负载均衡原理

docker dewbay 5年前 (2019-04-12) 2047次浏览 已收录 0个评论 扫描二维码

前言

个人理解有限,如有错误,请及时指正。

前前后后学习kubernetes已经有三个月了,一直想写一遍关于kubernetes内部实现的一系列文章来作为这三个月的总结,个人觉得kubernetes背后的架构理念以及技术会成为中大型公司架构的未来。我推荐可以先阅读下 Google 的Large-scale cluster management at Google with Borg技术文献,它是实现kubernetes的基石。

准备

在阐述原理之前我们需要先了解下kubernetes关于内部负载均衡的几个基础概念以及组件。

概念

Pod
k8s 内部负载均衡原理

1.PodKubernetes创建或部署的最小/最简单的基本单位。

2.如图所示,Pod的基础架构是由一个根容器Pause Container和多个业务Container组成的。

3.根容器的IP就是Pod IP,是由kubernetesetcd中取出相应的网段分配的, 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 IPService通过Label查询到相应的Pod组, 如果你的Pod是对外服务的那么还应该有一组endpoint,需要将endpoint绑到Service上,这样一个微服务就形成了。

Kubernetes CNI

CNI(Container Network Interface)是用于配置 Linux 容器的网络接口的规范和库组成,同时还包含了一些插件。CNI 仅关心容器创建时的网络分配,和当容器被删除时释放网络资源。

Ingress

1.俗称边缘节点,假如你的Service是对外服务的,那么需要将Cluster IP暴露为对外服务,这时候就需要将IngressServiceCluster IP与端口绑定起来对外服务。这样看来其实Ingress就是将外部流量引入到Kubernetes内部。

2.实现 Ingress 的开源组件有TraefikNginx-Ingress, 前者方便部署,后者部署复杂但是性能和灵活性更好。

组件

Kube-Proxy

1.Kube-Proxy是被内置在Kubernetes的插件。
2.当ServicePod Endpoint变化时,Kube-Proxy将会改变宿主机iptables, 然后配合Flannel或者Calico将流量引入Service.

Etcd

1.Etcd是一个简单的Key-Value存储工具。
2.Etcd实现了Raft协议,这个协议主要解决分布式强一致性的问题,与之相似的有PaxosRaftPaxos要容易实现。
3.Etcd用来存储Kubernetes的一些网络配置和其他的需要强一致性的配置,以供其他组件使用。
4.如果你想要深入了解Raft, 不放先看看raft 相关资料

Flannel

1.FlannelCoreOS团队针对Kubernetes设计的一个覆盖网络Overlay Network工具,其目的在于帮助每一个使用KuberentesCoreOS主机拥有一个完整的子网。
2.主要解决PODService,跨节点相互通讯的。

Traefik

1.Traefik是一个使得部署微服务更容易的现代 HTTP 反向代理、负载。
2.Traefik不仅仅是对Kubernetes服务的,除了Kubernetes他还有很多的Providers,如Zookeeper,Docker SwarmEtcd等等

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-GoSDK 来完成与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-ProxyTraefik实现原理很像,都是通过与kube-apiserver的交互来完成实时更新iptables的,这里就不细说了,以后会有一篇文章专门讲
kube-dnskube-proxyService的。

组件协同与负载均衡

简单描述流程,然后思考问题,最后考虑是否需要深入了解(取决于个人兴趣)

组件协同

用户通过访问Traefik提供的 L7 层端口, Traefik会转发流量到Cluster IPFlannel会将用户的请求准确的转发到相应的Node节点的Service上。(ps: Flannel初始化的时候宿主机会建立一个叫flannel0【这里的数字取决于你的 Node 节点数】的虚拟网卡)

负载均衡

上文讲述了kube-proxy是通过iptables来配合flannel完成一次用户请求的。

具体的流程我们只要看一个serviceiptables 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内部的负载均衡是通过iptablesprobability特性来做到的,这里就会有一个问题,当Pod副本数量过多时,iptables的表将会变得很大,这时会有性能问题。

总结

  • Traefik 通过默认的负载均衡(wrr)直接将流量通过Flannel送进POD.
  • kube-proxy 在没有 ipvs的情况下, 会通过iptables转发做负载均衡.

结尾

通过这篇文章我们简单的了解到内部负载均衡的机制,但是任然不够深入,你也可用通过这篇文章查漏补缺,觉得有什么错误的地方欢迎及时指正,我的邮箱shinemotec@gmail.com。下一篇将会讲KubernetesHPA工作原理。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网


露水湾 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:k8s 内部负载均衡原理
喜欢 (0)
[]
分享 (0)
关于作者:
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址