本文档记录,k8s如何暴露公网服务以及负载均衡
主要涉及两个知识点:k8s service、k8s ingress controller
对比传统单体垂直的架构,可以使用k8s service、k8s ingress controller 进行替代HAproxy+Nginx
K8s Service
基本每次创建k8s pod最好都提前定义好service对象,其实service的概念可以同等于CMDB中的服务组的概念。
直接通过Pod的IP地址+端口可以访问到容器应用,但是Pod的IP是动态的,当发生故障时k8s会重新调度,这时候Service服务自发现的作用就体现出来了。
其实这个概念和微服务非常类似。一个Serivce
下面包含的Pod
集合一般是由Label Selector
来决定的。
要想外部可以访问创建的应用,就使用service吧。
K8S暴露服务的方法有3种:
- ClusterIP:集群内可访问,但外部不可访问
- NodePort:通过NodeIP:NodePort方式可以在集群内访问,结合EIP或者云服务VPC负载均衡也可在集群外访问,但开放NodePort一方面不安全,另一方面随着应用的增多不方便管理
- LoadBalancer:某些云服务提供商会直接提供LoadBalancer模式,将服务对接到负载均衡,其原理是基于kubernetes的controller做二次开发,并集成到K8S集群,使得集群可以与云服务SDK交互
我们也了解到Pod的生命是有限的,死亡过后不会复活了。我们后面学习到的RC和Deployment可以用来动态的创建和销毁Pod。尽管每个Pod都有自己的IP地址,但是如果Pod重新启动了的话那么他的IP很有可能也就变化了。这就会带来一个问题:比如我们有一些后端的Pod的集合为集群中的其他前端的Pod集合提供API服务,如果我们在前端的Pod中把所有的这些后端的Pod的地址都写死,然后去某种方式去访问其中一个Pod的服务,这样看上去是可以工作的,对吧?但是如果这个Pod挂掉了,然后重新启动起来了,是不是IP地址非常有可能就变了,这个时候前端就极大可能访问不到后端的服务了。
遇到这样的问题该怎么解决呢?在没有使用Kubernetes之前,我相信可能很多同学都遇到过这样的问题,不一定是IP变化的问题,比如我们在部署一个WEB服务的时候,前端一般部署一个Nginx作为服务的入口,然后Nginx后面肯定就是挂载的这个服务的大量后端,很早以前我们可能是去手动更改Nginx配置中的upstream选项,来动态改变提供服务的数量,到后面出现了一些服务发现的工具,比如Consul、ZooKeeper还有我们熟悉的etcd等工具,有了这些工具过后我们就可以只需要把我们的服务注册到这些服务发现中心去就可以,然后让这些工具动态的去更新Nginx的配置就可以了,我们完全不用去手工的操作了。
在k8s体系中”有三种IP”
- Node IP: Node节点的IP
- Pod IP:Pod的IP
- Cluster IP:service的IP
k8s 提供两种负载分发策略,RoundRobin、SessionAffinity(默认是RoundRobin)
集群外部访问pod或service
将容器应用端口映射到物理机
- 通过设置容器级别的hostport
- 设置pod级别的HostNetwork=true
将Service的端口映射到物理机(type=NodePort)
Service分类:
名称 | 说明 |
---|---|
ClusterIP | 只在集群内部可达的私网地址,集群外部无法访问 |
NodePort | 通过节点端口映射使得集群外部可达,是在ClusterIP基础上添加一层节点端口映射,访问路径:集群外客户端–>节点ip:port–>ClusterIP:port–>PodIP:port |
LoadBalancer | 如果客户端只访问一个节点,会对该节点造成很大压力,一般在NodePort之前加入负载均衡,加入负载均衡器有两种方式:一是使用公有云的LBaaS,二是自己搭建负载均衡器 |
ExternalName | 当Pod客户端想访问集群之外的服务时,使用前几种类型Service是无法绕过集群直接访问外部服务的,使用ExternalName类型的主机名或域名映射方式可以和集群外通信 |
HeadLess | 特殊类型Service(无头服务),直接把服务名称映射到Pod的Ip上 |
默认创建的类型是:ClusterIP
kubernetes网络分为三类:node network、pod network、cluster network,前两种是真实存在的,而cluster network是虚拟的,仅用于Service资源。
发布服务 —— 服务类型
对一些应用(如 Frontend)的某些部分,可能希望通过外部(Kubernetes 集群外部)IP 地址暴露 Service。
Kubernetes ServiceTypes
允许指定一个需要的类型的 Service,默认是 ClusterIP
类型。
Type
的取值以及行为如下:
ClusterIP
:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的ServiceType
。NodePort
:通过每个 Node 上的 IP 和静态端口(NodePort
)暴露服务。NodePort
服务会路由到ClusterIP
服务,这个ClusterIP
服务会自动创建。通过请求<NodeIP>:<NodePort>
,可以从集群的外部访问一个NodePort
服务。LoadBalancer
:使用云提供商的负载均衡器,可以向外部暴露服务。外部的负载均衡器可以路由到NodePort
服务和ClusterIP
服务。ExternalName
:通过返回CNAME
和它的值,可以将服务映射到externalName
字段的内容(例如,foo.bar.example.com
)。 没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的kube-dns
才支持。
服务发现
Kubernetes 支持2种基本的服务发现模式 —— 环境变量和 DNS。
环境变量
当 Pod
运行在 Node
上,kubelet 会为每个活跃的 Service
添加一组环境变量。 它同时支持 Docker links 兼容 变量(查看 makeLinkVariables)、简单的 {SVCNAME}_SERVICE_HOST
和 {SVCNAME}_SERVICE_PORT
变量,这里 Service
的名称需大写,横线被转换成下划线。
举个例子,一个名称为 "redis-master"
的 Service 暴露了 TCP 端口 6379,同时给它分配了 Cluster IP 地址 10.0.0.11,这个 Service 生成了如下环境变量:
1 | REDIS_MASTER_SERVICE_HOST=10.0.0.11 |
这意味着需要有顺序的要求 —— Pod
想要访问的任何 Service
必须在 Pod
自己之前被创建,否则这些环境变量就不会被赋值。DNS 并没有这个限制
DNS
一个可选(尽管强烈推荐)集群插件 是 DNS 服务器。 DNS 服务器监视着创建新 Service
的 Kubernetes API,从而为每一个 Service
创建一组 DNS 记录。 如果整个集群的 DNS 一直被启用,那么所有的 Pod
应该能够自动对 Service
进行名称解析。
例如,有一个名称为 "my-service"
的 Service
,它在 Kubernetes 集群中名为 "my-ns"
的 Namespace
中,为 "my-service.my-ns"
创建了一条 DNS 记录。 在名称为 "my-ns"
的 Namespace
中的 Pod
应该能够简单地通过名称查询找到 "my-service"
。 在另一个 Namespace
中的 Pod
必须限定名称为 "my-service.my-ns"
。 这些名称查询的结果是 Cluster IP。
Kubernetes 也支持对端口名称的 DNS SRV(Service)记录。 如果名称为 "my-service.my-ns"
的 Service
有一个名为 "http"
的 TCP
端口,可以对 "_http._tcp.my-service.my-ns"
执行 DNS SRV 查询,得到 "http"
的端口号。
Kubernetes DNS 服务器是唯一的一种能够访问 ExternalName
类型的 Service 的方式。 更多信息可以查看 DNS Pod 和 Service。
创建ClusterIP类型的Service资源
创建ClusterIP类型的Service资源配置文件service-clusterIP-demo.yml,这个Service资源关联使用Deployment创建的3个Pod,内容如下:
1 | # ---------------------- Deployment资源 ---------------------- |
1 | # 查看select是否匹配 |
创建 NodePort类型的Service资源
NodePort类型是ClusterIp类型的增强版,是外部网络打通集群边界的一种方式。客户端访问流程:集群外的客户端 –> NodeIP:NodePort –> ClusterIP:servicePort –> PortIP:containerPort。
创建NodePort类型的Service资源配置文件service-notePort-demo.yml,这个Service资源关联使用Deployment创建的3个Pod,内容如下:
1 | apiVersion: v1 |
测试在集群外部浏览器 http://<集群节点ip>:30080 是否可以访问。
集群外部访问服务
Service 的 ClusterIP 是 Kubernetes 内部的虚拟 IP 地址,无法直接从外部直接访问。但如果需要从外部访问这些服务该怎么办呢,有多种方法
- 使用 NodePort 服务在每台机器上绑定一个端口,这样就可以通过
<NodeIP>:NodePort
来访问该服务。 - 使用 LoadBalancer 服务借助 Cloud Provider 创建一个外部的负载均衡器,并将请求转发到
<NodeIP>:NodePort
。该方法仅适用于运行在云平台之中的 Kubernetes 集群。对于物理机部署的集群,可以使用 MetalLB 实现类似的功能。 - 使用 Ingress Controller 在 Service 之上创建 L7 负载均衡并对外开放。
K8s Ingress Controller
Service属于四层网络模型,是在网络协议TCP或UDP之上,而http或https属于七层网络模型,无法使用Service来解析七层的http或https,为了解决这个问题,在节点运行一个代理服务的Pod,这个Pod里的容器共享宿主机网络(host network),外部可以直接访问,并且运行属于七层网络模型代理服务(Nginx、Traefik、Envoy),所有的客户端的请求都先经过代理服务的Pod,代理服务Pod把请求转发给其他Pod,而且各个Pod之间是同一个网段,可以直接通信。为了解决代理服务Pod单点问题,使用DaemonSet类型的Pod控制器,DaemonSet控制的Pod会在每个node节点运行一个代理服务Pod,如果集群的节点很多的话,选择3个节点专门用来运行服务代理即可,不必每个节点都运行代理服务。
这种代理服务的Pod属于ingress controller,ingress controller是独立于集群master中manager controller。而ingress是集群的一种特殊资源,定义ingress资源时需要指明ingress controller的前端是虚拟主机或url映射。集群中运行很多组的pod(例如:用户、电商、社交、交易、物流等),ingress controller怎么知道哪些请求是指向哪组Pod?可以使用虚拟主机或url映射(/usr/、/shop/等),因为Pod是有生命周期的,Pod的Ip地址有可能随时改变,ingress controller经过url映射的Pod就保证存在呢?解决方法是借助headless ClusterIp类型的Service资源,Service不是用来代理,只用来对Pod进行分组,ingress实时监控Service中Pod的ip地址是否变化,如果有变化,ingress实时获取到变化后Pod的ip信息,然后注入到ingress controller的配置文件中,并触发ingress controller中容器进程重载配置文件。
而ingress controller目前有几种:
- k8s社区提供的ingress
- nginx社区提供的ingress
- Traefik
- …
k8s-nginx-ingress 、nginxinc-ingress两者之间的区别
https://github.com/nginxinc/kubernetes-ingress/blob/master/docs/nginx-ingress-controllers.md
Ingress 架构图
Ingress-Nginx实现
参考文档:
https://kubernetes.github.io/ingress-nginx/deploy/
1 | kubectl create -f /etc/ansible/manifests/ingress/nginx-ingress/nginx-ingress.yaml # 创建ingress |
1 | # 查看ingress-nginx各类资源列表 |
添加外网接入的service资源
在本地测试为了接入集群外的请求,需要另外创建一个NodePort类型service做转发,如果是在公有云部署的集群,建议使用LoadBalancer类型service,参考service。
创建NodePort类型的service资源配置文件ingress-nginx-serivce.yml,内容如下:
1 | YAMLapiVersion: v1 |
查看调度器nginx ingress controller是否工作正常:
1 | # 所有从集群外进来的http使用30080,https使用30443 |
ingress nginx代理http示例
开始部署app,创建headless类型的service资源和Deployment资源配置文件myapp-service-deployment.yml,其中service资源只用来对Deployment的Pod进行分组的,获取到Pod的ip给ingress资源使用,内容如下:
1 | YAMLapiVersion: v1 |
创建ingress资源配置文件http-myapp-ingress.yml,ingress会监听service服务myapp-svc,一旦发现myapp-svc发生改变,ingress会从myapp-svc获取最新的Pod信息,把最新配置信息注入到nginx配置信息。内容如下:
1 | YAMLapiVersion: extensions/v1beta1 |
测试ingress是否能够动态注入nginx.conf,并触发nginx重载配置信息:
1 | BASH# 获取ingress nginx的pod名称 |
创建成功后,Ingress会将信息注入到ingress-controller里面去,即:会自动转换为nginx的配置文件,可以看到pod内的nginx.conf文件确实被注入了myapp相关的信息。此时,在本地机器绑定myapp.test.com为两个node的ip,就可以在本地机器的浏览器中访问 http://myapp.test.com:30080/ 网页了。
配置阿里云SLB,添加nginx-ingress组,将带有nginx-ingress的node节点添加至阿里云SLB
traefik ingress实现
部署 traefik ingress-controller
1 | kubectl create -f /etc/ansible/manifests/ingress/traefik/traefik-ingress.yaml |
验证 traefik ingress-controller
1 | # kubectl get deploy -n kube-system traefik-ingress-controller |
测试ingress
首先创建测试用K8S应用,并且该应用服务不用nodePort暴露,而是用ingress方式让外部访问
1 | kubectl run test-hello --image=nginx --expose --port=80 |
然后为这个应用创建 ingress对象
1 | kubectl create -f /etc/ansible/manifests/ingress/test-hello.ing.yaml |
集群内部尝试访问: curl -H Host:hello.test.com 10.68.69.170(traefik-ingress-service的服务地址) 能够看到欢迎页面 Welcome to nginx!;
在集群外部尝试访问(假定集群一个NodeIP为 192.168.1.1): curl -H Host:hello.test.com 192.168.1.1:23456,也能够看到欢迎页面 Welcome to nginx!,说明ingress测试成功
traefik WEB 管理页面
1 | # traefik-ui.ing.yaml内容 |