Talk is cheap in Open Source,  show me the way to achieve!

--大魏


世上没有完美的技术方案:“免费、稳定、性能好、出了问题有人扛雷“的情况是不存在的。需要结合自己的业务需求和预算情况,选择适合自己的方式。


在正式开始之前,我先将在OCP上几种Ingress的实现方式列出来,读者可以带着问题来阅读本文:

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java


问题1:OpenShift4(简称OCP4)上的Ingress Controller怎么实现的?

这个世界上,是OCP先有的router(容器化的haproxy),K8S后有的Ingress Controller。在Ingress这点上,K8S跟OCP学的,没错!


Kubernetes 1.0中没有Ingress的概念,因此将入站流量路由到Kubernetes Pod和Service是一项非常复杂、需要手工配置的任务。在OpenShift 3.0中,红帽开发了Router,以提供入口请求的自动负载平衡。Router是现在Kubernetes Ingress控制器的前身。


OCP4的入口流量还是使用HAproxy。OpenShift默认的Router本质上是一个以hostnetwork方式运行在节点上的容器化Haproxy,可提供HTTPHTTPSWebSocketsTLSwith SNI协议的访问。Router相当于IngressControllerRoute相当于Ingress对象。OpenShift使用社区提供的HaproxyIngress Controller,通过Ingress Operator实现部署。


OCP上的HAproxy上有很多有益的参数,既可以直接设置在router上,也可以在某一个route上设置。后者的设置可以覆盖前者的设置。这些参数用于OCP Ingress的性能调优。

https://docs.openshift.com/container-platform/4.3/networking/routes/route-configuration.html

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_02

OCP上的router只能实现7层的Http和https访问。但我们通过模板定制化,可以实现haproxy的七层。


问题2:OCP的4层Ingress需求是怎么产生的?

首先说,HAproxy是一个性能非常好的软负载,稳定性也强。OCP最初设计里面是:OCP中运行的前端的应用才需要对外暴露,前端和后端的应用访问,如果在一个OCP集群中,那通过K8S的Servcie;如果在OCP集群外的,如虚拟化环境,则通过NAT出去访问。


但随着OCP承载的应用越来越多,会有这样的需求:

OCP上有mysql,有DNS服务,我就需要直接通过tcp方式访问,这些服务不是给一个OCP集群中的web应用提供服务的。这就需要四层。


站在红帽的角度,4层的需求应少于7层。Ingress还用router,少量的四层入口访问需求,用nodeport就成。


但有的客户说:我们不想用nodeport,那玩意不好,怎么办?



问题3:如何让OCP原生的Ingress默认支持4层?

OCP3的HPproxy支持4层的方法如下:

通过OCP自带的机制或进行一定的定制化来实现。目前有如下几种方案:

  • TCP with TLS

  • 使用NodePort Service

  • 使用externalIP Service

  • 自定义Router支持四层

如果是使用TLS加密的TCP协议,而且客户端支持Server Name IndicationSNI),那么我们可以配置TLS Passthrough route,通过OpenShift Router访问。Passthrough route意味着在经过router不会解密数据,所以不关心使用的协议(只要是TLS)SNITLS的特性,就是客户端可以发送非加密的主机名作为初始连接到服务器的一部分,让负载平衡器或虚拟机在不解密数据的情况下,将流量路由到正确的后端。

SNI仅在支持的协议上可使用,其中TCP就可以通过SNI访问。

   OpenShift上的demo参见:https://github.com/pweil-/hello-sni

1.2    使用NodePort Service

NodePort Service的概念与K8S完全一致。一旦创建NodePort类型的ServiceOpenShift就会分配一个大端口(默认范围30000-32767),如31020。一旦分配,每个OpenShift节点都会监听31020端口,并将数据转发给Service

在集群中每个端口必须是唯一的,所以使用NodePort意味着需要在OpenShift中部署应用之后分配一个端口之后,才能配置客户端。通常还需要确保新分配的端口在客户端和服务器之间的防火墙允许访问。最后,如果这个服务需要高可用,可以通过配置一个负载均衡器来分发流量到多个节点上就达到冗余。

        NodePortService示例如下:

apiVersion: v1

kind: Service

metadata:

  name:mysql

  labels:

    name:mysql

spec:

  type:NodePort

  ports:

    - port:3036

     nodePort: 30036

      name: tcp

  selector:

name: mysql

注:nodeport可以在创建service的时候指定,但是端口必须在设定的范围内,默认30000-32767,范围可以通过master配置文件修改。

1.3    使用externalIP Service

OCP 3.3开始,添加了ExternalIPs的机制。当service请求External IP时,会从地址池中为其分配非sdnIP地址。假设已正确配置路由,将External IP的流量路由到节点,则OpenShift会将到该IP的所有流量转发到该serviceExternalIPs是一个很好的解决方案。

官方参考链接:https://docs.openshift.com/container-platform/3.11/dev_guide/expose_service/expose_internal_ip_service.html

1.3.1   操作步骤

1)  定义ExternalIP地址访问

修改maste配置文件

# vi /etc/origin/master/master-config.yaml

networkConfig:

 externalIPNetworkCIDRs:  ---列表类型,可以定义多个地址段,或者IP

  -<ip_address>/<cidr>

例如:

networkConfig:

 externalIPNetworkCIDRs:

  -192.168.120.0/24

重启master服务。

2)  创建服务的service

apiVersion: v1

kind: Service

metadata:

  name:mysql

  labels:

    name:mysql

spec:

  type: ClusterIP

  ports:

    - port:3036

      name:tcp

  selector:

name: mysql

3)  分配external IP

修改service,指定externalIPs

# oc patch svc <name> -p'{"spec":{"externalIPs":["<ip_address>"]}}'

注意:分配的ip_address必须在设定的范围内。

例如:

# oc patch svc mysql-55-rhel7 -p '{"spec":{"externalIPs":["192.168.120.10"]}}'

# oc get svc

NAME               CLUSTER-IP      EXTERNAL-IP     PORT(S)   AGE

mysql-55-rhel7     172.30.131.89   192.168.120.10  3306/TCP  13m

4)  在任意一个节点绑定externelIP

# ip address add <external-ip> dev<device>

例如:

ip address add 192.168.120.10 dev eth0

或者使用一个网卡绑定多个别名的方式设置externelIP

建议:externelip设置为与外部客户端可以连通的IP,即三层可达,这样客户端便可使用externelip地址192.168.120.10连接mysql

 

注意:开启节点的防火墙,允许对该external IP的访问。

 

1.4    修改router实现四层转发

    通过修改OpenShift Router相关的源代码以及haproxy-template.conf,通过route对象中的annotation段定义四层转发规则,把一个端口传入router配置文件中,让后端的第三方应用程序通过router节点对应的端口(Haproxy里的mode tcp)从而实现router代理4层协议的目的。

但是这会导致另一个问题,因为router的每一个端口只能映射给一个后台应用,比如3306端口,只能映射给一个MySQL,如果有两个开发人员都要调试MySQL,那第二个开发者的MySQL的映射端口肯定就得用除了3306以外的端口(比如3307、第三个人3308等)为了解决这个问题。

可以通过用给router所在节点添加网卡别名的方式解决,比如运行命令ifconfig eth0:1 10.10.xx.xx意思就是给eth0NIC加上了一个虚拟的IP 10.10.xx.xx,那么此时,这个网卡eth0就有两2IP,并且此时都能ping通。(当然实际实现的时候,需要把设置IP地址的代码集成到了我们修改过的OpenShift里)。然后routerhaproxy.confbind的时候就需要指定这个IP,端口号还用原来这个应用默认的端口号,如10.10.1.1:3306 10.10.1.2:3306。这样开发人员不用每次都记住不同的端口号。

这里还需要完成将分配的别名网卡的IP地址显示给开发人员,或者使用域名解析完成,定义一种域名规范,如<application_name>.<namespace>.xxxxx,每创建一个应用会自动创建可以解析到别名网卡IP的域名,开发人员直接使用域名和端口连接即可。

下面将给出这种方法实现的简化示例:

获取router配置文件模版

# oc get po -n default

NAME                      READY     STATUS    RESTARTS  AGE

router-2-40fc3            1/1       Running   0         11d

# oc rsh router-2-40fc3 cat haproxy-config.template >haproxy-config.template

# oc rsh router-2-40fc3 cat haproxy.config > haproxy.config

修改模版文件

frontend之前添加listen字段,如下红色标注部分:

# vi haproxy-config.template

……

  statsrealm Haproxy\ Statistics

  stats uri/

  stats auth{{.StatsUser}}:{{.StatsPassword}}

{{- end }}

{{- end }}

 

{{/* custom tcp support*/}}

{{ if .BindPorts -}}

{{- range $cfgIdx, $cfg :=.State }}

 {{- if isTrue (index $cfg.Annotations"haproxy.router.openshift.io/tcp-mode") }}

listen tcp_mode:{{$cfgIdx}}

{{- if (isInteger (index$cfg.Annotations "haproxy.router.openshift.io/tcp-port")) }}

 {{- if ne (index $cfg.Annotations"haproxy.router.openshift.io/tcp-bind-ip") ""}}

 {{- with $bind_ip := firstMatch $ipPattern (index $cfg.Annotations"haproxy.router.openshift.io/tcp-bind-ip") }}

 bind {{$bind_ip}}:{{ index $cfg.Annotations "haproxy.router.openshift.io/tcp-port"}}

 {{- end }} {{/* end with*/}}

 {{- else }}

 bind *:{{ index $cfg.Annotations"haproxy.router.openshift.io/tcp-port" }}

 {{- end }}  {{/*end if else*/}}

{{- end }} {{/*end tcp port*/}}

 mode tcp

 balance source

{{- if ne (env"ROUTER_SYSLOG_ADDRESS") ""}}

 option tcplog

{{- end }} {{/* end if*/}}

{{- range $serviceUnitName,$weight := $cfg.ServiceUnitNames }}

      {{- if ne $weight 0 }}

        {{- with $serviceUnit := index$.ServiceUnits $serviceUnitName }}

          {{- range $idx, $endpoint :=processEndpointsForAlias $cfg $serviceUnit (env"ROUTER_BACKEND_PROCESS_ENDPOINTS" "") }}

 server {{$endpoint.ID}} {{$endpoint.IP}}:{{$endpoint.Port}} weight{{$weight}}

            {{- if and (not$endpoint.NoHealthCheck) (gt $cfg.ActiveEndpoints 1) }} check inter{{firstMatch $timeSpecPattern (index $cfg.Annotations"router.openshift.io/haproxy.health.check.interval") (env"ROUTER_BACKEND_CHECK_INTERVAL") "5000ms"}}

            {{- end }}{{/* end else no healthcheck */}}

          {{- end }}{{/* end rangeprocessEndpointsForAlias */}}

        {{- end }}{{/* end get ServiceUnit fromserviceUnitName */}}

      {{- end }}{{/* end if weight != 0 */}}

{{- end }}{{/* end iterate overservices*/}}

{{- end }} {{/*end if istrue*/}}

{{- end }} {{/*end range.state*/}}

{{- end }} {{/*end bindport*/}}

 

 

{{ if .BindPorts -}}

frontend public

{{ if eq "v4v6" $router_ip_v4_v6_mode }}

……

此段配置依赖于route对象上添加特定的annotations才会触发,添加的参数说明如下:

  • haproxy.router.openshift.io/tcp-mode:设置该route是否开启TCP转发,true为开启TCP。如果参数不设置或者为空,则不开启TCP转发。

  • haproxy.router.openshift.io/tcp-port:在开启TCP转发之后,该参数生效。为了设置haproxy监听的端口,该参数为必选参数,主要是为了用户可以自定义端口,避免多个相同TCP服务导致端口冲突。

  • haproxy.router.openshift.io/tcp-bind-ip:在开启TCP转发之后,该参数生效。为了设置TCP端口绑定的地址,该参数为可选参数。如果不设置则默认绑定router所在节点的所有网卡。

由上述三个参数可以实现在OCP同时启动多个相同的TCP服务,端口冲突可以使用如下两种方法解决:

1  通过设置haproxy.router.openshift.io/tcp-port为不同的端口实现

2  通过设置haproxy.router.openshift.io/tcp-bind-ip为不同的ip地址实现。因为一个网卡可以绑定任意多的虚拟IP

使用ConfigMap替换模版文件

# oc project default

# oc create configmap customrouter--from-file=haproxy-config.template

# oc volume dc/router --add --overwrite \

    --name=config-volume \

   --mount-path=/var/lib/haproxy/conf/custom \

   --source='{"configMap": { "name":"customrouter"}}'

# oc set env dc/router \

TEMPLATE_FILE=/var/lib/haproxy/conf/custom/haproxy-config.template

创建route

假设router地址为192.168.182.191,且已提供*.apps.techcloud.comrouter地址的解析。

示例一:

# vi mysql-tcp-route-example1.yaml

apiVersion: v1

kind: Route

metadata:

  annotations:

   haproxy.router.openshift.io/tcp-mode: "true"

   haproxy.router.openshift.io/tcp-port: "3306"

   openshift.io/host.generated: "true"

  labels:

    app: mysql-ephemeral

    template: mysql-ephemeral-template

  name: mysql-tcp

spec:

  host:mysql-tcp-mysql.apps.techcloud.com

  port:

    targetPort: mysql

  to:

    kind: Service

    name: mysql

    weight: 100

  wildcardPolicy: None

# oc create -f mysql-tcp-route-example1.yaml

此示例中,会将3306端口绑定在router所在节点的所有网卡上。用户通过router ip或者定义的hostsmysql-tcp-mysql.apps.techcloud.com连接mysql

但是这样的话,在其他用户创建mysql服务时就必须通过haproxy.router.openshift.io/tcp-port指定其他的端口。

示例二:

# vi mysql-tcp-route-example2.yaml

apiVersion: v1

kind: Route

metadata:

  annotations:

   haproxy.router.openshift.io/tcp-mode: "true"

haproxy.router.openshift.io/tcp-port: "3306"

haproxy.router.openshift.io/tcp-bind-ip: 192.168.182.200

   openshift.io/host.generated: "true"

  labels:

    app: mysql-ephemeral

    template:mysql-ephemeral-template

  name: mysql-tcp

spec:

  host:mysql-tcp-mysql.apps.techcloud.com

  port:

    targetPort: mysql

  to:

    kind: Service

    name: mysql

    weight: 100

  wildcardPolicy: None

# oc create -f mysql-tcp-route-example2.yaml

此示例中,将mysql服务绑定在了192.168.182.200上(非router IP),这样用户可以通过这个IP地址连接到mysql

这里的Ip地址192.168.182.200router所在的节点上的一个虚拟IP(需要手动配置完成)或者是router前面负载负载均衡节点的VIP

这样做的好处在于,服务不需要修改端口,每个服务有一个虚拟IP用于连接。

 

注意:如果是在界面上创建route对象,不允许直接添加annotations,必须在创建后手动增加相应的annotations

开启防火墙

router所在的节点开启防火墙规则,允许tcp port连接。

# iptables -I OS_FIREWALL_ALLOW -p tcp -m state --state NEW -m tcp--dport 3306 -j ACCEPT

 

示例中目前的缺点

1  依赖于route对象,导致虽然实现了TCP访问,但在haproxy配置文件中依然会生成相关的http访问的规则

2  在界面上route地址依然显示http hosts

3  访问的端口和ip需要通过annotations配置和查看

2        总结

虽然方案2.4可以彻底解决四层访问问题,但是更好的实现方式需要修改源代码,工作量较大,而且后续在OCP升级过程中,每次都是完成代码合并,相对比较复杂。

而方案2.3显得更为简便通用,也是官方提供的方式。建议优先选择方案使用ExternalIP service


OCP4上INgress实现四层的方法:

定制haproxy template需要了解openshift router的一些原理要点

  • openshift router不仅仅是haproxy,它还有一个go程序,监听了openshift的配置,并且写入了一堆的map文件,这个文件是非常关键的配置haproxy template的配置文件。

  • openshift router里面的tls passthrough方式,对应到haproxy的配置里面,就是tcp的模式,我们的定制点就是在这里。

  • 定制过程集中在,屏蔽掉http/https 的edge和reencrypt部分,对于打annotation 的route,开放tls passthrough的frontend

  • route annotation 配置形式是 haproxy.router.openshift.io/external-tcp-port: "13306"

  • 当然,ocp4现在还不支持定制化route template,所以本文直接创建了一个route的deployment。

  • 现场实施的时候,注意更改router的image,每个版本的image可以去release.txt文件中找到。


这种方案的局限:

  • route annotation 定义的开放tcp端口,是手动定义,而且面向整个集群各个project开放,必然会导致tcp端口冲突。需要有端口管理方案。


测试步骤

测试步骤不复杂,就是创建一个新的router,然后就可以去其他project创建应用,给route打annotation就可以了。

本文的例子,包含两个应用,一个是web应用,一个是mysql,都通过tcp端口对外开放。

# tcp-router will install in the same project with openshift router

oc project openshift-ingress

# install the tcp-router and demo

oc create configmap customrouter-wzh --from-file=haproxy-config.template

oc apply -f haproxy.router.yaml

oc apply -f haproxy.demo.yaml

# test your tcp-router, replace ip with router ip, both command will success.

curl 192.168.7.18:18080

podman run -it --rm registry.redhat.ren:5443/docker.io/mysql mysql -h 192.168.7.18 -P 13306 -u user -D db -p

# if you want to delete the tcp-router and demo

oc delete -f haproxy.router.yaml

oc delete configmap customrouter-wzh

oc delete -f haproxy.demo.yaml

# oc set volume deployment/router-wzh --add --overwrite \

# --name=config-volume \

# --mount-path=/var/lib/haproxy/conf/custom \

# --source='{"configMap": { "name": "customrouter-wzh"}}'

# oc set env dc/router \

# TEMPLATE_FILE=/var/lib/haproxy/conf/custom/haproxy-config.template




问题4:OCP4是否支持Inginx controller,并通过ingress实现四层?

支持。

OCP4上现在有nginx的operator。从实现4层ingress的功能来说,nginx和nginx+是相同的。

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_03

具体的安装方法,我就不展示点鼠标的步骤了(太没技术含量)直接把cli贴出来。但是,需要注意的是,nginx controller service的创建,有三种方式:loadbalancer、 nodeport、loadbalancer-aws-elb。在私有云的OCP上、在不借助任何外置的LB情况下下,只能部署nodeport方式。使用这种部署方式,能够实现7层请求转发,通过配置,也能实现4层访问,但有个坑,这个后面说(第8个问题)。


可复现的执行步骤如下:

$ git clone https://github.com/nginxinc/kubernetes-ingress/

$ cd kubernetes-ingress/deployments

$ git checkout v1.8.0

配置RBAC

kubectl apply -f common/ns-and-sa.yaml

$ kubectl apply -f rbac/rbac.yaml

$ kubectl apply -f rbac/ap-rbac.yaml


如果需要 TLS certificate

$ kubectl apply -f common/default-server-secret.yaml

$ kubectl apply -f common/nginx-config.yaml


部署Controller:

$ kubectl apply -f deployment/nginx-ingress.yaml

# kubectl apply -f service/nodeport.yaml  (还有loadbalancer-aws-elb.yaml 和loadbalancer.yaml两个文件,我的OCP环境在私有云,而且没有外置LB,因此只能用Nodeport)


查看controller pod在运行:

$ kubectl get pods --namespace=nginx-ingress

nginx-ingress-65f97fcdb-rb4hm   1/1     Running            0          53m


$ kubectl get svc --namespace=nginx-ingress

nginx-ingress      NodePort    172.30.255.99    <none>        80:30698/TCP,443:30189/TCP   3h1m


问题5:如何验证OCP4上nginx 的七层生效?

在我的环境中,部署一个cafe的应用:

#cat cafe.yaml

apiVersion: v1

kind: Namespace

metadata:

  name: cafe

---

apiVersion: apps/v1

kind: Deployment

metadata:

  name: coffee

  namespace: cafe

spec:

  replicas: 2

  selector:

    matchLabels:

      app: coffee

  template:

    metadata:

      labels:

        app: coffee

    spec:

      containers:

      - name: coffee

        image: nginxdemos/nginx-hello:plain-text

        ports:

        - containerPort: 8080

---

apiVersion: v1

kind: Service

metadata:

  name: coffee-svc

  namespace: cafe

spec:

  ports:

  - port: 80

    targetPort: 8080

    protocol: TCP

    name: http

  selector:

    app: coffee

---

apiVersion: apps/v1

kind: Deployment

metadata:

  name: tea

  namespace: cafe

spec:

  replicas: 3

  selector:

    matchLabels:

      app: tea 

  template:

    metadata:

      labels:

        app: tea 

    spec:

      containers:

      - name: tea 

        image: nginxdemos/nginx-hello:plain-text

        ports:

        - containerPort: 8080

---

apiVersion: v1

kind: Service

metadata:

  name: tea-svc

  namespace: cafe

  labels:

spec:

  ports:

  - port: 80

    targetPort: 8080

    protocol: TCP

    name: http

  selector:

    app: tea


#oc apply -f cafe.yaml


部署完以后,查看pod:

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_04

然后,我们在解析中配置OCP Worker节点IP的解析,将其配置成:cafe.example.com。


然后curl两个应用(+URI):

[root@lb.weixinyucluster3 /david]# curl http://cafe.example.com:80/coffee

Server address: 10.129.2.16:8080

Server name: coffee-5f56ff9788-gbrhh

Date: 12/Aug/2020:16:20:40 +0000

URI: /coffee

Request ID: 59a5c37e99abc8e9256f330d07733f7e


[root@lb.weixinyucluster3 /david]# curl http://cafe.example.com:80/tea

Server address: 10.129.2.17:8080

Server name: tea-69c99ff568-9xspk

Date: 12/Aug/2020:16:20:49 +0000

URI: /tea

Request ID: 592ef43446c4a595d5cc89f480c119b7


我们看到nginx的配置成功了。



问题6:问题中的cafe应用,在nginx ingress上生成了什么具体的配置?

登录nginx的pod进行查看:

[root@lb.weixinyucluster3 /]# oc rsh nginx-ingress-65f97fcdb-rb4hm

$ cd /etc/nginx/conf.d

$ ls

cafe-cafe-ingress.conf

$ cat cafe-cafe-ingress.conf

# configuration for cafe/cafe-ingress



upstream cafe-cafe-ingress-cafe.example.com-coffee-svc-80 {

        zone cafe-cafe-ingress-cafe.example.com-coffee-svc-80 256k;

        random two least_conn;


        server 10.130.0.3:8080 max_fails=1 fail_timeout=10s max_conns=0;

        server 10.131.0.4:8080 max_fails=1 fail_timeout=10s max_conns=0;


}

upstream cafe-cafe-ingress-cafe.example.com-tea-svc-80 {

        zone cafe-cafe-ingress-cafe.example.com-tea-svc-80 256k;

        random two least_conn;


        server 10.130.0.7:8080 max_fails=1 fail_timeout=10s max_conns=0;

        server 10.131.0.3:8080 max_fails=1 fail_timeout=10s max_conns=0;

        server 10.131.0.6:8080 max_fails=1 fail_timeout=10s max_conns=0;


}



server {



        listen 80;





        server_tokens on;


        server_name cafe.example.com;






        location /tea {



                proxy_http_version 1.1;



                proxy_connect_timeout 60s;

                proxy_read_timeout 60s;

                proxy_send_timeout 60s;

                client_max_body_size 1m;

                proxy_set_header Host $host;

                proxy_set_header X-Real-IP $remote_addr;

                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

                proxy_set_header X-Forwarded-Host $host;

                proxy_set_header X-Forwarded-Port $server_port;

                proxy_set_header X-Forwarded-Proto $scheme;

                proxy_buffering on;



                proxy_pass http://cafe-cafe-ingress-cafe.example.com-tea-svc-80;



        }

        location /coffee {



                proxy_http_version 1.1;



                proxy_connect_timeout 60s;

                proxy_read_timeout 60s;

                proxy_send_timeout 60s;

                client_max_body_size 1m;

                proxy_set_header Host $host;

                proxy_set_header X-Real-IP $remote_addr;

                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

                proxy_set_header X-Forwarded-Host $host;

                proxy_set_header X-Forwarded-Port $server_port;

                proxy_set_header X-Forwarded-Proto $scheme;

                proxy_buffering on;



                proxy_pass http://cafe-cafe-ingress-cafe.example.com-coffee-svc-80;



        }



}


问题7:OCP4+nginx ingress的4层怎么实现?

通过nginx-ingress ns中的configmap实现。

我先基于我这个"简陋"的环境验证。


我的验证方式,是在nginx-ingress中部署一个coredns的应用,然后尝试访问tcp端口,看能否对kubernetes.io做dns解析。

[root@lb.weixinyucluster3 /david]# cat dns.yaml

apiVersion: v1

kind: ConfigMap

metadata:

  name: coredns

data:

  Corefile: |

    .:53 {

      forward . 8.8.8.8:53

      log

    }

---

apiVersion: apps/v1

kind: Deployment

metadata:

  name: coredns

spec:

  replicas: 2

  selector:

    matchLabels:

      app: coredns

  template:

    metadata:

      labels:

        app: coredns

    spec:

      containers:

      - name: coredns

        image: coredns/coredns:1.2.0

        args: [ "-conf", "/etc/coredns/Corefile" ]

        volumeMounts:

        - name: config-volume

          mountPath: /etc/coredns

          readOnly: true

        ports:

        - containerPort: 53

          name: dns

          protocol: UDP

        - containerPort: 53

          name: dns-tcp

          protocol: TCP

        securityContext:

          allowPrivilegeEscalation: false

          capabilities:

            add:

            - NET_BIND_SERVICE

            drop:

            - all

          readOnlyRootFilesystem: true

      volumes:

        - name: config-volume

          configMap:

            name: coredns

            items:

            - key: Corefile

              path: Corefile

---

apiVersion: v1

kind: Service

metadata:

  name: coredns

spec:

  selector:

   app: coredns

  ports:

  - name: dns

    port: 53

    protocol: UDP

  - name: dns-tcp

    port: 53

    protocol: TCP

---

apiVersion: v1

kind: Service

metadata:

  name: coredns-headless

spec:

  clusterIP: None

  selector:

   app: coredns

  ports:

  - name: dns

    port: 53

    protocol: UDP

  - name: dns-tcp

    port: 53

    protocol: TCP



#oc apply -f dns.yaml

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_05

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_06


接下来,我们配置一个cm,它的作用是,让nginx controller监听这个coredns的端口:

[root@lb.weixinyucluster3 /david]# cat cm.yaml

kind: ConfigMap

apiVersion: v1

metadata:

  name: nginx-config

  namespace: nginx-ingress

data:

    stream-snippets: |

      upstream dns-default-udp {

          server dns-default.openshift-dns.svc.cluster.local:53;

      }

      server {

          listen 5353 udp;

          proxy_pass dns-default-udp;

          proxy_responses 1;

      }

      upstream dns-default-tcp {

          server dns-default.openshift-dns.svc.cluster.local:53;

      }

      server {

          listen 5353;

          proxy_pass dns-default-tcp;

      } 


[root@lb.weixinyucluster3 /david]# oc apply -f cm.yaml

configmap/nginx-config configured


查看cm配置生效:

[root@lb.weixinyucluster3 /david]# oc describe cm nginx-config  

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_07

截止到目前,4层的coredns配置完成了。


问题8:如何验证4层生效?

然后,我们验证这个dns是否生效。

我们知道,OCP中的ovs是三层的,我们在OCP节点和外部是无法直接访问pod IP的。而我这里的配置,是通过nginx的pod ip做的监听。因此,我想要验证这个tcp配置生效,就需要到达与nginx pod的相同网络平面。


我们先查看现在nginx pod中的配置中是否已经包含我我们在问题6中的配置:

[root@lb.weixinyucluster3 /david]# oc rsh nginx-ingress-65f97fcdb-rb4hm

$

$ cat /etc/nginx/nginx.conf

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_08

已经有了。


接下来,我们查看nginx的pod ip:

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_09

是10.129.2.13。


我们ssh到OCP集群中的另外一个pod(我用的是ovs pod),然后dig 10.129.2.13加5353端口,看能否解析kubernetes.io(第一段展示udp方式解析,第二段展示tcp方式解析):

[centos@lb.weixinyucluster3 ~]$ oc rsh ovs-whc48

sh-4.2#

sh-4.2# dig @10.129.2.13 -p 5353 kubernetes.io


; <<>> DiG 9.11.4-P2-RedHat-9.11.4-16.P2.el7_8.6 <<>> @10.129.2.13 -p 5353 kubernetes.io

; (1 server found)

;; global options: +cmd

;; Got answer:

;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13849

;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1


;; OPT PSEUDOSECTION:

; EDNS: version: 0, flags:; udp: 512

;; QUESTION SECTION:

;kubernetes.io.                 IN      A


;; ANSWER SECTION:

kubernetes.io.          3487    IN      A       147.75.40.148


;; Query time: 18 msec

;; SERVER: 10.129.2.13#5353(10.129.2.13)

;; WHEN: Sat Aug 15 12:34:55 UTC 2020

;; MSG SIZE  rcvd: 71


sh-4.2# dig @10.129.2.13 -p 5353 kubernetes.io +tcp


; <<>> DiG 9.11.4-P2-RedHat-9.11.4-16.P2.el7_8.6 <<>> @10.129.2.13 -p 5353 kubernetes.io +tcp

; (1 server found)

;; global options: +cmd

;; Got answer:

;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27137

;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1


;; OPT PSEUDOSECTION:

; EDNS: version: 0, flags:; udp: 512

;; QUESTION SECTION:

;kubernetes.io.                 IN      A


;; ANSWER SECTION:

kubernetes.io.          3599    IN      A       147.75.40.148


;; Query time: 50 msec

;; SERVER: 10.129.2.13#5353(10.129.2.13)

;; WHEN: Sat Aug 15 12:35:03 UTC 2020

;; MSG SIZE  rcvd: 71



可以看到,tcp生效,靠谱。


但是,pod ip无法被外部直接访问这个坑怎么填?



问题9:pod ip无法被外部直接访问坑怎么填?

给nginx配置多网络平面,使用macvlan将pod第二个IP拉到与OCP worker节点一个平面成不?

不成,我已经试了。


那生产怎么整?最好的方法,就是配置Loadbalancer模式的nginx controller,例如将 Nginx Controller 创建了一个 external Load Balancer, 然后在在LB上,如F5 BIG-IP 上一个入口IP。这样,通过这个入口IP就可以实现直达ingress controller实现tcp四层了。具体配置可以咨询类似F5的厂商。


问题10.如果没有支持external IP的硬件LB怎么办?

如果将OCP部署到裸机上,则使用MetalLB解决这个问题(MetalLB is for bare-metal clusters)。它支持的平台如下,包含OCP:

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_10

MetalLB的官方地址:https://metallb.universe.tf/
MetalLB是使用标准路由协议的裸机Kubernetes/OpenShift集群的软负载均衡器,可以在物理机环境下实现对Service服务分配IP。


MetalLB支持两种声明模式:

  • Layer 2模式:ARP/NDP

Layer 2模式下,每个service会有集群中的一个node来负责。当服务客户端发起ARP解析的时候,对应的node会响应该ARP请求,之后,该service的流量都会指向该node(看上去该node上有多个地址)。

Layer 2模式并不是真正的负载均衡,因为流量都会先经过1个node后,再通过kube-proxy转给多个end points。如果该node故障,MetalLB会迁移 IP到另一个node,并重新发送免费ARP告知客户端迁移。现代操作系统基本都能正确处理免费ARP,因此failover不会产生太大问题。


Layer 2模式更为通用,不需要用户有额外的设备;但由于Layer 2模式使用ARP/ND,地址池分配需要跟客户端在同一子网,地址分配略为繁琐。同时Layer 2模式所有流量会进入到一个Node,该Node的带宽可能会成为一个网络瓶颈(我实验是用的这种方式,ping ip的时候,可以看到是从一个主机转过去的)。


  • BGP模式。 

BGP模式下,集群中所有node都会跟上联路由器建立BGP连接,并且会告知路由器应该如何转发service的流量。


BGP模式是真正的LoadBalancer。


MetalLB由两个组件组成:

  1. controller,负责IP地址的分配,以及service和endpoint的监听。

  2. speaker,负责保证service地址可达,例如Layer 2模式下,speaker会负责ARP请求应答。


[centos@lb.weixinyucluster3 ~]$ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/namespace.yaml

namespace/metallb-system created


# wget https://raw.githubusercontent.com/google/metallb/v0.8.1/manifests/metallb.yaml


下载下来后,删除文件中的runAsUser 65534,然后应用配置。



# oc adm policy add-scc-to-user privileged -n metallb-system -z speaker

查看部署好的pods:

 oc get pods -n metallb-system  -o wide

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_11

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_12


创建configmap配置,设置分配IP的网段。

 cat << EOF | kubectl apply -f -

apiVersion: v1

kind: ConfigMap

metadata:

  namespace: metallb-system

  name: config

data:

  config: |

    address-pools:

    - name: default

      protocol: layer2

      addresses:

      - 192.168.91.247-192.168.1.249

EOF


$ oc logs -f controller-748474fb67-892q9

确保没有分配IP地址报错。


接下来,我们部署loadbalance的nginx controller service:


# oc delete svc nginx-ingress

service "nginx-ingress" deleted


# cat loadbalancer.yaml

apiVersion: v1

kind: Service

metadata:

  name: nginx-ingress

  namespace: nginx-ingress

spec:

  externalTrafficPolicy: Local

  type: LoadBalancer

  ports:

  - port: 80

    targetPort: 80

    protocol: TCP

    name: http

  - port: 443

    targetPort: 443

    protocol: TCP

    name: https

  selector:

    app: nginx-ingress


应用配置

kubectl -f  loadbalancer.yaml

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_13

这时候,我们看到ingress的svc已经分配了IP地址:192.168.91.240


此时,在OCP节点层面已经可以ping通(我实验是用的L2,ping ip的时候,可以看到是从一个OCP主机转过去的)。

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_14


总结:

灵魂拷问x10:OpenShift 4层Ingress实现方式大全_java_15


1.OCP4上,如果Ingress大多数是7层请求,用默认的haproxy就好,性能好,有红帽支持。


2.OCP4上,如果有少量的4层ingress需求,用默认的haproxy+nodeport就可以,把端口设置的大些,没啥太大的影响,不要把nodeport想得太坏。我本人是最推荐使用这种方式。


3. OCP上的HAproxy默认支持7层,如果要支持四层,需要进行配置,方法本文已经给出。


3.通过nginx也可以实现ingress的4层。OCP上的nginx controller配置成loadbalancer这种方式的配置,还是需要通过全局的(nginx-ingress ns)configmap实现,需要手工写要暴露的应用的svc全名,这有一定工作量。


4.如果OCP部署在裸机上,又不想引入类似F5的硬件LB,那么使用MetalLB 为NGINX提供IP。中小规模使用Layer 2,规模大了需要打开BGP以保证性能。这种方式性价比较高,适合在开发测试环境使用。但需要关注MetalLB这个开源社区的发展。



总之,世上没有完美的技术方案:“免费、稳定、性能好、出了问题有人扛雷“的情况是不存在的。需要结合自己的业务需求和预算情况,选择适合自己的方式。