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-systemkubectl 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-systemkubectl 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服务做外部服务暴露的玩法看上去很爽,但是这种搞法并不完美:

主要有以下四个问题:

  1. 由于微服务架构以及Docker技术和Kubernetes编排工具最近几年才开始逐渐流行,所以一开始的反向代理服务器比如Nginx/HAProxy并未提供其支持,毕竟他们也不是先知,所以才会出现IngressController这种东西来做Kubernetes和前端负载均衡器如Nginx/HAProxy之间做衔接,即Ingress Controller的存在就是为了能跟Kubernetes交互,又能写 Nginx/HAProxy配置,还能 reload 它,这是一种折中方案。
  2. 当你要添加了一个新的服务或hostname而需要升级你的ingress配置文件的时候,也就是说(你在一个持续集成的环境中)你要为你的pipeline去增加一个额外的步骤,让它去检查ingress的配置是否需要更新(这意味着在某种程度上你的所有microservices之间共享一个ingress)或者你是需要把你的pipeline分成两部分,其中一部分用来更新ingress(也就是说一个微服务的部署不是独立的,它还需要去更新ingress的配置才能生效)。
  3. 由于多了一层ingress controll,其性能损耗相对增加了许多。
  4. 没有一个直观的界面可以方便的查看其转发的规则和过程。

Traefik的诞生似乎很爽的解决了问题,它保留了nginx-ingress的所有优点,而且还解决了nginx-ingress的缺点。Traefik天生就是提供了对Kubernetes的支持,也就是说Traefik本身就能跟Kubernetes API交互,感知后端变化,因此在使用Traefik时,Ingress Controller已经没有卵用了。

8 扩展阅读

nginx-ingress官方文档

kubernetes-crash-course中关于ingress的