Nginx+Ingress-controller解决服务暴露和负载均衡
1 环境说明和准备
Ingress 使用开源的反向代理负载均衡器来实现对外暴露服务,比如 Nginx、Apache、Haproxy等。Nginx Ingress 一般有三个组件组成:
Nginx:反向代理负载均衡器
Ingress Controller :Ingress Controller 可以理解为控制器,它通过不断的跟 Kubernetes API 交互,实时获取后端 Service、Pod 等的变化,比如新增、删除等,然后结合 Ingress 定义的规则生成配置,然后动态更新上边的 Nginx 负载均衡器,并刷新使配置生效,来达到服务自动发现的作用。
Ingress: Ingress 则是定义规则,通过它定义某个域名的请求过来之后转发到集群中指定的 Service。它可以通过 Yaml 文件定义,可以给一个或多个 Service 定义一个或多个 Ingress 规则。
以上三者有机的协调配合起来,就可以完成 Kubernetes 集群服务的暴露。
拿前后端分离的frontend和testweb两个web服务为例,大致拓扑如下:
2 安装部署
2.1 安装部署default-backend
创建Deployment,副本为1,并创建service绑定pod。配置文件如下所示:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: default-http-backend
labels:
k8s-app: default-http-backend
namespace: kube-system
spec:
replicas: 1
template:
metadata:
labels:
k8s-app: default-http-backend
spec:
terminationGracePeriodSeconds: 60
containers:
- name: default-http-backend
# Any image is permissable as long as:
# 1. It serves a 404 page at /
# 2. It serves 200 on a /healthz endpoint
image: 10.57.26.15:4000/defaultbackend
livenessProbe:
httpGet:
path: /healthz
port: 8080
scheme: HTTP
initialDelaySeconds: 30
timeoutSeconds: 5
ports:
- containerPort: 8080
resources:
limits:
cpu: 10m
memory: 20Mi
requests:
cpu: 10m
memory: 20Mi
---
apiVersion: v1
kind: Service
metadata:
name: default-http-backend
namespace: kube-system
labels:
k8s-app: default-http-backend
spec:
ports:
- port: 80
targetPort: 8080
selector:
k8s-app: default-http-backend
执行
kubectl create -f default-backend.yaml
安装default-backend。
查看default-http-backend的service,deploy和pod
随后测试default-http-backend的访问,分别测试pod和service的访问,如下图所示:
这说明default backend可以正常访问了。
2.2 安装部署Nginx Ingress-Controller
采用官网的例子,创建ServiceAccount,ClusterRole,ClusterRoleBinding,创建Deployment,副本为1,通过–default-backend-service将之前创建的default-http-backend绑定上去。
注意:hostNetwork: true表示nginx-ingress与node共用网络命名空间,使用node的物理网卡直接转发。
apiVersion: v1
kind: ServiceAccount
metadata:
name: ingress
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: system:ingress
rules:
- apiGroups:
- ""
resources: ["configmaps","secrets","endpoints","events","services"]
verbs: ["list","watch","create","update","delete","get"]
- apiGroups:
- ""
- "extensions"
resources: ["services","nodes","ingresses","pods","ingresses/status"]
verbs: ["list","watch","create","update","delete","get"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: ingress
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:ingress
subjects:
- kind: ServiceAccount
name: ingress
namespace: kube-system
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-ingress-controller
labels:
k8s-app: nginx-ingress-controller
namespace: kube-system
spec:
replicas: 1
template:
metadata:
labels:
k8s-app: nginx-ingress-controller
annotations:
prometheus.io/port: '10254'
prometheus.io/scrape: 'true'
spec:
# hostNetwork makes it possible to use ipv6 and to preserve the source IP correctly regardless of docker configuration
# however, it is not a hard dependency of the nginx-ingress-controller itself and it may cause issues if port 10254 already is taken on the host
# that said, since hostPort is broken on CNI (https://github.com/kubernetes/kubernetes/issues/31307) we have to use hostNetwork where CNI is used
# like with kubeadm
hostNetwork: true
serviceAccountName: ingress
terminationGracePeriodSeconds: 60
containers:
- name: nginx-ingress-controller
image: 10.57.26.15:4000/nginx-ingress-controller:0.9.0-beta.10
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
args:
- /nginx-ingress-controller
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
# - --v=5
readinessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
livenessProbe:
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
timeoutSeconds: 1
ports:
- containerPort: 80
hostPort: 80
- containerPort: 443
hostPort: 443
执行
kubectl create -f nginx-ingress-controller.yaml
创建ingress
部署完成后,查看nginx-ingress-controller的service,deploy和pod
注意pod的IP和default-http-backend有所不同,是真实机器的ip地址,这就是hostNetwork: true的作用。
使用命令测试一下:
说明nginx-ingress-controller可以正常访问了,已经正常链接到了default-http-backend上了。
3 编写TCP/UDP端口转发规则实现L4层服务暴露
L4层暴露在平常开放使用非常多,比方说数据库暴露端口。
为将使用简单的mysql进行测试,将pod中的端口暴露出来。
输入命令kubectl get svc
查看mysql相关的service情况,可以看到,其占用TCP的3306端口,我们的目标就是把这个3306端口暴露出去。
3.1 配置TCP的configmap
还记得之前nginx-ingress-controller.yaml配置文件中的参数--tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
吗,这是对应TCP端口映射的。
编辑端口转发配置文件tcp-services-configmap.yaml写入下面内容,其中name和namespace对应–tcp-services-configmap中的参数:
kind: ConfigMap
apiVersion: v1
metadata:
name: tcp-services
namespace: kube-system
data:
13306: "default/mysql:3306"
非常好理解,将default/mysql这个service的TCP 3306端口暴露出来,为13306端口。
运行kubectl create -f tcp-services-configmap.yaml
命令创建configmap。
然后执行kubectl get cm -nkube-system
和kubectl describe cm tcp-services -nkube-system
查看创建的端口映射,如图所示:
3.2 验证TCP 端口的L4服务暴露
使用笔记本访问nginx-ingress-controller的ip,在这里如下图红框所示,是10.57.26.8。
在我的笔记本上安装了navicat,使用它配置之前创建的mysql,如图所示,注意左下角,test connection成功的绿色小灯。
登陆后可以看到建立的表以及里面的数据。
此时,外网笔记本到mysql服务的访问拓扑如下:
3.3 配置UDP的configmap
输入kubectl get svc -nkube-system
查看coredns的service,如下图所示红框为服务名和需要暴露的udp端口号。
nginx-ingress-controller.yaml配置文件中的参数--udp-services-configmap=$(POD_NAMESPACE)/udp-services
,这是对应UDP端口映射的。
编辑端口转发配置文件udp-services-configmap.yaml写入下面内容,其中name和namespace对应–udp-services-configmap中的参数:
kind: ConfigMap
apiVersion: v1
metadata:
name: udp-services
namespace: kube-system
data:
10053: "kube-system/kube-dns:53"
非常好理解,将kube-system/kube-dns这个service的UDP 53端口暴露出来,为10053端口。
运行kubectl create -f udp-services-configmap.yaml
命令创建configmap。
然后执行kubectl get cm -nkube-system
和kubectl describe cm udp-services -nkube-system
查看创建的端口映射,如图所示:
3.4 验证UDP 端口的L4服务暴露
位于外网的macbook上自带nslookup程序,直接用nslookup来对coredns集群解析进行测试。测试nginx-ingress-controller的ip(10.57.26.8)的UDP 10053端口访问:
我现有的k8s平台中有4个service服务,如下:
我随便找几个service来测试一下:
指定dns服务器为10.57.26.8(nginx-ingress地址),端口号为映射出来的10053,nslookup去解析default命名空间中的四个服务
nslookup tomcat.default.svc.cluster.local 10.57.26.8 -port=10053
nslookup hardy-rabbit-mongodb.default.svc.cluster.local 10.57.26.8 -port=10053
nslookup mysql.default.svc.cluster.local 10.57.26.8 -port=10053
nslookup kubernetes.default.svc.cluster.local 10.57.26.8 -port=10053
得到结果如下,与kubectl get svc
命令得到的服务CLUSTER-IP相同
此时,外网的笔记本到coredns服务的访问拓扑如下:
4 编写ingress规则实现L7层服务暴露
通过上面的default-http-backend以及nginx-ingress-controller安装,就可以来给服务定义ingress规则了。
我使用简单的tomcat和mysql进行测试。 在此给出测试的yaml文件,如下所示: 其中path表示host下的根路径,serviceName表示k8s上的服务,servicePort表示服务对应的端口。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress-tomcat-mysql
namespace: default
annotations:
ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: tomcat.tongdunzhy.com
http:
paths:
- path: /
backend:
serviceName: tomcat
servicePort: 8080
- host: mysql.tongdunzhy.com
http:
paths:
- path: /
backend:
serviceName: mysql
servicePort: 3306
- host: mix.tongdunzhy.com
http:
paths:
- path: /tomcat
backend:
serviceName: tomcat
servicePort: 8080
- path: /mysql
backend:
serviceName: mysql
servicePort: 3306
如图所示,这个yaml包含了下面将要介绍的两个例子:
4.1 基于不同域名的ingress访问
执行kubectl get ing
以及kubectl describe ing ingress-tomcat-mysql
可以看到如下的输出,红框部分为不同域名的ingress的规则,包括tomcat和mysql。
编辑hosts文件,加入域名tomcat.tongdunzhy.com和mysql.tongdunzhy.com,如图所示:
添加后就可以通过域名访问服务,使用curl命令快速测试一下,图中标红的地方为输出,可以看见,通过不同域名访问已经成功。
4.2 基于相同域名的ingress访问
执行kubectl get ing
以及kubectl describe ing ingress-tomcat-mysql
可以看到如下的输出,红框部分为相同域名的ingress的规则,包括tomcat和mysql。
编辑hosts文件,加入域名mix.tongdunzhy.com,如图所示:
添加后就可以通过域名访问服务,使用curl命令快速测试一下,图中标红的地方为输出,可以看见,通过相同域名的访问也成功。
注
ingress.kubernetes.io/rewrite-target: /
的作用
若不加此参数,输入curl mix.tongdunzhy.com/tomcat
会得到一个404错误,看上去是tomcat已经连上,但是不在根路径。估计变成了 /tomcat 子页面导致。
加入参数以后,会将target重写成/就可以避免这个问题。
5 更新ingress
假如想要向已有的ingress中增加一个新的Host,你可以编辑和更新该ingress:
kubectl edit ing test
这会弹出一个包含已有的yaml文件的编辑器,修改它,增加新的Host配置,就像编辑yaml一样。
保存它会更新API server中的资源,这会触发ingress controller重新配置loadbalancer。
注:在一个修改过的ingress yaml文件上调用kubectl replace -f命令一样可以达到同样的效果。
6 Nginx-ingress自身的扩容
如果访问用户过多,nginx-ingress自身压力很大怎么办?下面给出一些思路:
使用nodeSelector通过”定向调度”将nginx-ingress固定在特定范围的node上,利用k8s平台的优势进行弹性扩展。
然后使用软/硬负载均衡将用户流量分发到多个nginx-ingress,拓扑如下:
下面,我们来看看当前的nginx-ingress-ingress的pod和deploymet都只有1个:
下面我们来将nginx-ingress-ingress扩容到2个:
通过通用扩缩容命令
kubectl scale --replicas=2 deploy nginx-ingress-controller -nkube-system
进行扩容,通过下图可以看到红框部分的pod已经变成两个,分别位于k8s-02和k8s-03两个节点上,做负载均衡,同时,deployment也变成了2个。
7 Nginx-Ingress做web服务暴露的问题
这种使用nginx-ingress给web服务做外部服务暴露的玩法看上去很爽,但是这种搞法并不完美:
主要有以下四个问题:
- 由于微服务架构以及Docker技术和Kubernetes编排工具最近几年才开始逐渐流行,所以一开始的反向代理服务器比如Nginx/HAProxy并未提供其支持,毕竟他们也不是先知,所以才会出现IngressController这种东西来做Kubernetes和前端负载均衡器如Nginx/HAProxy之间做衔接,即Ingress Controller的存在就是为了能跟Kubernetes交互,又能写 Nginx/HAProxy配置,还能 reload 它,这是一种折中方案。
- 当你要添加了一个新的服务或hostname而需要升级你的ingress配置文件的时候,也就是说(你在一个持续集成的环境中)你要为你的pipeline去增加一个额外的步骤,让它去检查ingress的配置是否需要更新(这意味着在某种程度上你的所有microservices之间共享一个ingress)或者你是需要把你的pipeline分成两部分,其中一部分用来更新ingress(也就是说一个微服务的部署不是独立的,它还需要去更新ingress的配置才能生效)。
- 由于多了一层ingress controll,其性能损耗相对增加了许多。
- 没有一个直观的界面可以方便的查看其转发的规则和过程。
Traefik的诞生似乎很爽的解决了问题,它保留了nginx-ingress的所有优点,而且还解决了nginx-ingress的缺点。Traefik天生就是提供了对Kubernetes的支持,也就是说Traefik本身就能跟Kubernetes API交互,感知后端变化,因此在使用Traefik时,Ingress Controller已经没有卵用了。