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工作原理。


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

查看所有标签

猜你喜欢:

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

Java Web入门经典

Java Web入门经典

王国辉、陈英 / 机械工业出版社 / 2013-6 / 69.00元

《Java Web入门经典》以初学者为核心,全面介绍了JavaWeb开发中常用的各种技术。内容排列上由浅入深,让读者循序渐进掌握编程技术;在内容讲解上结合丰富的图解和形象的比喻,帮助读者理解“晦涩难懂”的技术;在内容形式上附有大量的提示、技巧、说明等栏目,夯实读者编程技术,丰富编程经验。全书共分4篇19章,其中,第一篇为“起步篇”,主要包括开启JavaWeb之门、不可不知的客户端应用技术、驾驭Ja......一起来看看 《Java Web入门经典》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试