k8s 内部负载均衡原理

栏目: 服务器 · 发布时间: 6年前

内容简介:k8s 内部负载均衡原理

前言

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

前前后后学习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协议,这个协议主要解决分布式强一致性的问题,与之相似的有Paxos, RaftPaxos要容易实现。
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 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-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-dns, kube-proxy, Service的。

组件协同与负载均衡

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

组件协同

用户通过访问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工作原理。


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

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Concepts, Techniques, and Models of Computer Programming

Concepts, Techniques, and Models of Computer Programming

Peter Van Roy、Seif Haridi / The MIT Press / 2004-2-20 / USD 78.00

This innovative text presents computer programming as a unified discipline in a way that is both practical and scientifically sound. The book focuses on techniques of lasting value and explains them p......一起来看看 《Concepts, Techniques, and Models of Computer Programming》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器