k8s内部负载均衡原理

前言

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

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

准备

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

概念

Pod

alt

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注