zhangguanzhang's Blog

IngressController使用和它的高可用落地

字数统计: 3.6k阅读时长: 14 min
2018/10/06

从之前对ingress controller到现在了解架构和一些经验总结下,顺带给人科普少走弯路
需要看懂本文要具备一下知识点

  • svc实现原理和会应用
  • 知道反向代理原理,了解nginx和apache的vhost概念
  • 了解svc的几种类型(Nodeport,clusterip,LB)
  • 四层和七层区别(不明白就这样去理解,七层最常见就是应用层的http,也就是url,四层是传输层,为tcp/udp端口)
  • 域名解析,/etc/hosts等基础知识
  • 知道pod互斥或者ds的使用

Ingress Controller介绍与部署

本文档不具备时效性,请各位看概念就行了

介绍

Ingress Controller是一个统称,并不只有一个,如下

  • Ingress NGINX: Kubernetes 官方维护的方案,也是本次安装使用的 Controller。
  • F5 BIG-IP Controller: F5 所开发的 Controller,它能够让管理员通过 CLI 或 API 让 Kubernetes 与 OpenShift 管理 F5 BIG-IP 设备。
  • Ingress Kong: 著名的开源 API Gateway 方案所维护的 Kubernetes Ingress Controller。
  • Traefik: 是一套开源的 HTTP 反向代理与负载均衡器,而它也支援了 Ingress。
  • Voyager: 一套以 HAProxy 为底的 Ingress Controller。
  • 而 Ingress Controller 的实现不只上面这些方案,还有很多可以在网络上找到这里不一一列出来了

我们部署在集群里的服务的svc想暴露出来的时候,从长久眼光看和易于管理维护都是用的Ingress Controller来处理,clusterip非集群主机无法访问,Nodeport不方便长久管理和效率,LB服务多了不方便因为需要花费额外的钱,externalIPS不好用(后面有空写文章会说它)

这里我们设想下,假如世界上没有 ingress controller 的时候,你自己开始造这个东西的时候的历程。拿你的 vmware workstation,虚机网络是nat模式的,只有宿主机能访问虚机的ip,和宿主机同一局域网的其他机器是无法访问你 pc 上的虚拟机的。pod 的 overlay 网络就是集群的node之间组了个自嗨的内网的cidr网络,集群的成员(k8s的node)之间才能访问pod ip和svc的clusterip(非nodePort),非集群成员无法访问 pod 的 ip 和 svc 的cluster IP。你想到了在node上跑一个进程做转发,集群里七层服务居多,四层很少。暴露大量集群的7层http就得有个东西在node上转发,代理node 的host网络到pod的overlay 网络上,你最熟悉nginx,毕竟它7层+四层都可以代理。
因为 svc 的 ip 是变化的:所以初期你想到的是:

  • nginx 部署在 k8s的机器上,这样能访问svcip和podip,dns 配置成coredns的,手动写nginx的配置文件决定哪些svc需要被反向代理。

后期,开发反应他自己实体服务+nginx压测的响应时间间隔整体比你这个高,你意识到你这个nginx先走svc,再是pod。改出了第二版:

  • nginx + lua 连kube-apiserver,(虽然配置文件里写的还是svc名字,但是底层是)获取集群svc的end point,upsteam动态变动,时刻写入最新的endpoint也就是pod的ip

再后期,你意识到每添加一个暴露服务都要手动改配置文件,非常繁琐,查看文档知道k8s有annotation这个东西,能够在这里写注释自己nginx + lua读取,你可能写到svc的annotation上。也就是下面类似:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Service
metadata:
annotations:
conf:|
more_set_headers "Request-Id: $req_id";
...
...

后期随着你对k8s的api越来越熟悉,意识到写svc上似乎太繁琐,毕竟这样增删改查可贼鸡儿麻烦。可不可以自己创建一个资源对象来和svc解耦,就像kind: Pod那样,于是你整出了一个crd,kind: xxx(也就是kind: Ingress)
实际上这就是ingress controller的由来和工作方式和由来

我们跑的大多服务都是应用层http(s),Ingress Controller使用svc或者pod的网络将它暴露在集群外,然后它反向代理集群内的七层服务,通过vhost子域名那样路由到后端的服务,Ingress Controller工作架构如下,借用traefik官方的图

ingresscontroller

你可以将api.domain.com进来的流量路由到集群里api的pod,你可以将backoffice.domain.com流量路由到backoffice的一组pod上
虽说我们可以自己搭建一个nginx来代替掉Ingress Controller,但是要增加代理的svc长期来看维护很不方便,在使用上Ingress Controller后可以用一种抽象的对象告诉controller添加对应的代理,也就是kind: Ingress. 它里面描述了从Ingress Controller访问进来的ServerName和web的url要代理到集群里哪个svc(以及svc的port)等等具体信息
而官方的Ingress Nginx可以视为一个魔改的nginx,拥有集群赋予的RBAC权限后,能够有监听集群Ingress相关的变化能力,用户创建了kind: Ingress,
例如上面trafik图里的Ingress大致就是下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: my-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
rules:
- host: api.mydomain.com
http:
paths:
- backend:
serviceName: api
servicePort: 80
- host: domain.com
http:
paths:
- path: /web/*
backend:
serviceName: web
servicePort: 8080
- host: backoffice.domain.com
http:
paths:
- backend:
serviceName: backoffice
servicePort: 8080

只要创建了上面的Ingress后,ingress controller里会监听到从而生成对应的配置段后动态reload配置文件

部署

部署非常简单,几条命令创建即可,yml来源于 https://github.com/kubernetes/ingress-nginx/tree/master/deploy/static

1
2
3
curl -o mandatory.yaml https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml
sed -ri 's#quay.io#quay.azk8s.cn#g;/authorization/s#v1beta1#v1#g' mandatory.yaml
kubectl apply -f mandatory.yaml

该yaml缺少向群外暴露的方式,我们先使用externalIPs方式创建svc来让它能从集群外面访问,因为externalIP新版本的arp不再像之前那样了,所以此处不建议读者跟着部署,先只需要看看来学工作原理,后面再讲生产的高可用




$INGRESS_VIP选取一个和宿主机同一个段没使用过的IP即可(实际上Ingress Nginx bind的端口不止80和443,这里不讨论,有兴趣的同学可以看容器里的默认配置文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Service
metadata:
name: ingress-nginx
namespace: ingress-nginx
labels:
app: ingress-nginx
spec:
type: LoadBalancer
externalIPs:
- $INGRESS_VIP
ports:
- port: 80
targetPort: 80
selector:
app: ingress-nginx

上面的yaml里后面详细解释我们需要关注的配置项,先来创建ingress对象试试

测试http 7层负载

部署了官方的ingress nginx后,我部署了一个nginx的pod,为它创建了一个名为nginx的svc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx:alpine
name: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80

然后创建对应的一个ingress对象来暴露集群里这个nginx的http服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: nginx-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: nginx.testdomain.com
http:
paths:
- backend:
serviceName: nginx
servicePort: 80

找到ingress nginx的pod名字后通过命令查看里面nginx配置文件能找到有对应的配置段生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ kubectl -n ingress-nginx exec nginx-ingress-controller-6cdcfd8ff9-t5sxl -- cat /etc/nginx/nginx.conf
...
## start server nginx.testdomain.com
server {
server_name nginx.testdomain.com ;

listen 80;

set $proxy_upstream_name "-";

location / {

set $namespace "default";
set $ingress_name "nginx-ingress";
set $service_name "nginx";
set $service_port "80";
set $location_path "/";
........
## end server nginx.testdomain.com
...

找一台非集群的Windows机器(也可以mac,主要是有图形界面且非集群内机器),设置hosts文件把域名nginx.testdomain.com设置到对svc的那个externalIPs的ip上,打开浏览器访问nginx.testdomain.com即可发现集群内的nginx已经暴露在集群外

  • 注意: Ingress Controller虽然调用的是svc,看起来按照nginx来理解转发是client–nginx–svc–pod; 实际上转发是client–nginx–pod,因为已经魔改了不能按照nginx的来理解,是直接负载到svc的endpoint上面的
  • 另外低版本的ingress nginx的args参数--default-backend-service=$(POD_NAMESPACE)/default-http-backend,该参数指定ingress nginx的同namespace下名为default-http-backend的svc作为默认访问的时候页面,通常那个时候是创建一个404页面的的pod和对应svc,如果ingress nginx启动的时候没找到这个svc会无法启动,新版本不是必须了,好像也自带404页面了

另外ingress也能多路径,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spec:
rules:
- host: xxxx.xxxx.xxx
http:
paths:
- backend:
serviceName: service-index
servicePort: 80
path: /
- backend:
serviceName: service-test-api
servicePort: 80
path: /api/

如何来4层负载

我们可以看到ingress nginx的args里有这两行

1
2
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services

从选项和值可以猜测出,要想代理四层(tcp/udp),得写同namespace里一个名为tcp-serviceudp-service的两个configmap的数据
四层的话这边我们创建一个mysql的pod,来代理3306端口到集群外面,则需要写tcp-services这个configmap

1
2
3
4
5
6
7
kind: ConfigMap
apiVersion: v1
metadata:
name: tcp-services
namespace: ingress-nginx
data:
3306: "default/mysql:3306"

四层写这两个cm的data即可,按照这样去写即可out_port: namespaces/svc_name:port,要给每个ingress加一些nginx里的配置可以查看官方的annotation字段以及值(traefik同理)

Ingress Controller高可用

这里来讨论下Ingress Controller的高可用
Ingress Controller到集群内的路径这部分都有负载均衡了,我们比较关注部署了Ingress Controller后,外部到它这段的流量路径怎么高可用?
上面的例子里svc我使用的externalIPs,但是代理四层的时候会新加端口,需要每次人为去介入增加暴露端口?
流量从入口到Ingress Controller的pod有下面几种方式

  • type为LoadBalancer的时候手写externalIPs很鸡肋,后面会再写文章去讲它
  • type为LoadBalancer的时候只有云厂商支持分配公网ip来负载均衡,LoadBalancer 公开的每项服务都将获得自己的 IP 地址,但是需要收费,自己建立集群想使用它的话得部署metaLB。
  • 不创建svc,pod直接用hostport,效率等同于hostNetwork,如果不代理四层端口还好,代理了的话每增加一个四成端口都需要修改pod的template来滚动更新来让nginx bind的四层端口能映射到宿主机上
  • Nodeport,端口不是web端口(但是可以修改Nodeport的范围改成web端口),如果进来流量负载到Nodeport上可能某个流量路线到某个node上的时候因为Ingress Controller的pod不在这个node上,会走这个node的kube-proxy转发到Ingress Controller的pod上,多走一趟路
  • 不创建svc,效率最高,也能四层负载的时候不修改pod的template,唯一要注意的是hostNetwork: true下pod会继承宿主机的网络协议,也就是使用了主机的dns,会导致svc的请求直接走宿主机的上到公网的dns服务器而非集群里的dns server,需要设置pod的dnsPolicy: ClusterFirstWithHostNet即可解决
    1
    2
    3
    4
    5
    6
    ...
    spec:
    containers:
    - xxx
    dnsPolicy: ClusterFirstWithHostNet
    hostNetwork: true
    已经部署的deploy的话使用patch修改为hostNetwork
    1
    2
    3
    kubectl -n ingress-nginx \
    patch deploy nginx-ingress-controller \
    -p '{"spec":{"template":{"spec":{"dnsPolicy":"ClusterFirstWithHostNet","hostNetwork":true}}}}'

部署方式没多大区别开心就好

  • daemonSet + nodeSeletor
  • deploy设置replicas数量 + nodeSeletor + pod互斥

拓扑为下面,client通过域名,最后导向到SLB,负载到三个node上,三个node都部署了hostNetwork的ingress controller,然后ingress controller根据server_name反向代理相关的svc的http服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                                            +-------+
| |
-+---------->+ |
/ +-------+ node1
/
/
/ +-------+
+--------+ / | |
| client +-----------> SLB -------------->+ |
+--------+ \ +-------+ node2
\
\
\ +-------+
------------->+ |
| |
+-------+ node3
  • 所以可以一个vip飘在拥有存活的controller的宿主机上,云上的话就用slb来负载代替vip,自己有条件有F5之类的硬件LB一样可以代替VIP
  • 最后说说域名请求指向它,如果部署在内网或者办公室啥的,内网有dns server的话把ing的域名全部解析到ingress controller的宿主机ip(或者VIP,LB的ip)上,否则要有人访问每个人设置/etc/hosts才能把域名解析来贼麻烦,如果没有dns server可以跑一个external-dns,它的上游dns是公网的dns服务器,办公网内机器的dns server指向它即可,云上的话把域名请求解析到对应ip即可
  • traefik和ingress nginx类似,不过它用go实现的并且好像它不支持四层代理,如果上微服务可以上istio,没接触过它,不知道原理是否如此
  • ingress nginx的log里会一直刷找不到ingress-nginx的svc不处理的话会狂刷log导致机器load过高,创建一个同名的svc即可解决,例如创建一个不带选择器clusterip为null的
  • get ing输出的时候ADDRESS一栏会为空,ingress-nginx加参数--report-node-internal-ip-address即可解决
  • 使用了rancher在负载均衡也就是ingress页面,ingress状态不为Active的话在ingress-nginx的参数配置--publish-service--publish-status-address

Multiple Ingress Controllers

当然,一个集群可以多组ingress controller,或者不同的ingress controller。拿ingress nginx举例。
假如公司的内网组了vpn,办公网和机房的node的网络打通,我们就需要两组ingress controller了(不一定需要一样,例如ingress nginx和traefik),例如下面

  • 硬件F5对外暴露了一组业务的ingress给用户
  • 而it的研发听说k8s部署方便,想把服务部署到k8s上,然后这些服务想对办公网的同事让http访问,压力不高。我们使用低成本的keepalived飘在内网组的node上
  • 两组deploy使用nodeSelector固定在两组不同的label的node上
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
                                                +---------------------------+
    | |
    -+---------->+ hostNetwork的ingress nginx|
    / +---------------------------+ node1
    /
    /
    / +---------------------------+
    +--------+ / | |
    | client +-----------> F5 -------------->+ hostNetwork的ingress nginx|
    +--------+ \ +---------------------------+ node1
    \
    \
    \ +---------------------------+
    ------------->+hostNetwork的ingress nginx |
    | |
    +---------------------------+ node3

    +---------------------------+
    | |
    -+---------->+ hostNetwork的ingress nginx|
    / +---------------------------+ node4
    /
    /
    / +---------------------------+
    +--------+ / | |
    | client +-----------> SLB(or VIP) -------->+ hostNetwork的ingress nginx|
    +--------+ \ +---------------------------+ node5
    \
    \
    \ +---------------------------+
    ------------->+ hostNetwork的ingress nginx|
    | |
    +---------------------------+ node6
    我们需要一个svc供内网服务,只需要创建该ingress的时候添加一个annotation
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    apiVersion: networking.k8s.io/v1beta1
    kind: Ingress
    metadata:
    name: it-work
    annotations:
    kubernetes.io/ingress.class: "nginx-internal"
    spec:
    tls:
    - secretName: tls-secret
    rules:
    - http:
    paths:
    - backend:
    serviceName: it-work-svc
    servicePort: 8080
    而两组的ingress controller的该选项的值不同即可
    1
    2
    3
    4
    5
    $ docker run --rm --entrypoint /nginx-ingress-controller \
    quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1 --help |& grep -A2 -- --ingress-class
    --ingress-class string Name of the ingress class this controller satisfies.
    The class of an Ingress object is set using the annotation "kubernetes.io/ingress.class".
    All ingress classes are satisfied if this parameter is left empty.
    这个参数内网的那组我们就写--ingress-class=nginx-internal,面向公网的不能写和这个值一样,定义成另一个即可
CATALOG
  1. 1. Ingress Controller介绍与部署
    1. 1.1. 介绍
    2. 1.2. 部署
    3. 1.3. 测试http 7层负载
    4. 1.4. 如何来4层负载
    5. 1.5. Ingress Controller高可用
    6. 1.6. Multiple Ingress Controllers