consul+upsync 实现ingress controller 无损更新

背景

ingress-controller 实现了集群内部服务的负载均衡,对于公有云环境,我们可以通过LoadBalance 类型的Service实现ingress-controller 的负载均衡。但是私有云环境,对于负载均衡的支持有限,虽然有MetalLB这样的开源的解决方案。 但是真正在生产应用的案例并不多。而且对于已有一套完整架构的公司,直接将ingress-controller 暴露给公网业务或者内部调用的情况并不多,大部分都是在ingress-controller 作为upstream 挂载为一组负载均衡(nginx)的后端来提供服务。因此。 如何实现ingress-controller 的无损更新也就成了如何实现upstream 的server 如何无损摘除的问题。这里我们采用的是微博的upsync 插件和 consul 来实现。

具体实现

大致说明

  1. 集群中创建单独的node节点(配置可以略低),仅用来运行ingress-controller(daemonset),通过给节点打标签和污点的方式实现,ingress-controller采用HostNetwork方式占用宿主机端口,并创建ClusterIP 的service。
  2. 集群中部署consul-sync-catalog 服务,consul-sync-catalog 可以将kubernetes中的service 同步到consul 集群中注册为服务
  3. nginx 通过微博的upsync 组件动态获取consul 中服务对应instance 的ip 和端口。当kubernetes 中的endpoints 发生变动时,consul-sync-catalog同步对应的变动到consul,upsync 组件自动变更upstream对应的server列表,无需reload nginx。

拓扑图

操作步骤

集群操作

  • 设置node节点role,并打污点

    $ kubectl label  node pg-k8s-node-01   node-role.kubernetes.io/edge=edge
    $ kubectl label  node pg-k8s-node-02   node-role.kubernetes.io/edge=edge
    $ kubectl taint node pg-k8s-node-01 node-role.kubernetes.io/edge:NoSchedule
    $ kubectl taint node pg-k8s-node-02 node-role.kubernetes.io/edge:NoSchedule
    
  • 修改ingress-controller 为daemonset(我们使用的是istio的ingressgateway),修改部分见yaml

    apiVersion: apps/v1
    kind: DaemonSet
    metadata:
      labels:
        app: istio-ingressgateway
        istio: ingressgateway
        operator.istio.io/component: IngressGateways
        operator.istio.io/managed: Reconcile
        operator.istio.io/version: 1.5.0
        release: istio
      name: istio-ingressgateway
      namespace: istio-system
    spec:
      revisionHistoryLimit: 10
      selector:
        matchLabels:
          app: istio-ingressgateway
          istio: ingressgateway
      template:
        metadata:
          annotations:
            kubectl.kubernetes.io/restartedAt: "2020-09-10T10:50:07+08:00"
            sidecar.istio.io/inject: "false"
          creationTimestamp: null
          labels:
            app: istio-ingressgateway
            chart: gateways
            heritage: Tiller
            istio: ingressgateway
            release: istio
            service.istio.io/canonical-name: istio-ingressgateway
            service.istio.io/canonical-revision: "1.5"
        spec:
          containers:
          - args:
            - proxy
            - router
            - --domain
            - $(POD_NAMESPACE).svc.cluster.local
            - --proxyLogLevel=warning
            - --proxyComponentLogLevel=misc:error
            - --log_output_level=default:info
            - --drainDuration
            - 45s
            - --parentShutdownDuration
            - 1m0s
            - --connectTimeout
            - 10s
            - --serviceCluster
            - istio-ingressgateway
            - --zipkinAddress
            - zipkin.istio-system:9411
            - --proxyAdminPort
            - "15000"
            - --statusPort
            - "15020"
            - --controlPlaneAuthPolicy
            - NONE
            - --discoveryAddress
            - istio-pilot.istio-system.svc:15012
            - --trust-domain=cluster.local
            env:
            - name: SERVICE_NAME
              value: ingress-test
            - name: JWT_POLICY
              value: first-party-jwt
            - name: PILOT_CERT_PROVIDER
              value: istiod
            - name: ISTIO_META_USER_SDS
              value: "true"
            - name: CA_ADDR
              value: istio-pilot.istio-system.svc:15012
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: spec.nodeName
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.namespace
            - name: INSTANCE_IP
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: status.podIP
            - name: HOST_IP
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: status.hostIP
            - name: SERVICE_ACCOUNT
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: spec.serviceAccountName
            - name: ISTIO_META_WORKLOAD_NAME
              value: istio-ingressgateway
            - name: ISTIO_META_OWNER
              value: kubernetes://apis/apps/v1/namespaces/istio-system/deployments/istio-ingressgateway
            - name: ISTIO_META_MESH_ID
              value: cluster.local
            - name: ISTIO_AUTO_MTLS_ENABLED
              value: "true"
            - name: ISTIO_META_POD_NAME
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.name
            - name: ISTIO_META_CONFIG_NAMESPACE
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.namespace
            - name: ISTIO_META_ROUTER_MODE
              value: sni-dnat
            - name: ISTIO_META_CLUSTER_ID
              value: Kubernetes
            image: dockerhub.piggy.xiaozhu.com/istio/proxyv2:1.5.0
            imagePullPolicy: IfNotPresent
            lifecycle:
              preStop:
                exec:
                  command:
                  - /bin/sh
                  - -c
                  - sleep 40
            name: istio-proxy
            ports:
            - containerPort: 15020
              hostPort: 15020
              protocol: TCP
            - containerPort: 80
              hostPort: 80
              protocol: TCP
            - containerPort: 443
              hostPort: 443
              protocol: TCP
            - containerPort: 15029
              hostPort: 15029
              protocol: TCP
            - containerPort: 15030
              hostPort: 15030
              protocol: TCP
            - containerPort: 15031
              hostPort: 15031
              protocol: TCP
            - containerPort: 15032
              hostPort: 15032
              protocol: TCP
            - containerPort: 15443
              hostPort: 15443
              protocol: TCP
            - containerPort: 15011
              hostPort: 15011
              protocol: TCP
            - containerPort: 8060
              hostPort: 8060
              protocol: TCP
            - containerPort: 853
              hostPort: 853
              protocol: TCP
            - containerPort: 15090
              hostPort: 15090
              name: http-envoy-prom
              protocol: TCP
            readinessProbe:
              failureThreshold: 30
              httpGet:
                path: /healthz/ready
                port: 15020
                scheme: HTTP
              initialDelaySeconds: 1
              periodSeconds: 2
              successThreshold: 1
              timeoutSeconds: 1
            resources:
              limits:
                cpu: "2"
                memory: 1Gi
              requests:
                cpu: 100m
                memory: 128Mi
            terminationMessagePath: /dev/termination-log
            terminationMessagePolicy: File
            volumeMounts:
            - mountPath: /var/run/secrets/istio
              name: istiod-ca-cert
            - mountPath: /var/run/ingress_gateway
              name: ingressgatewaysdsudspath
            - mountPath: /etc/istio/pod
              name: podinfo
            - mountPath: /etc/istio/ingressgateway-certs
              name: ingressgateway-certs
              readOnly: true
            - mountPath: /etc/istio/ingressgateway-ca-certs
              name: ingressgateway-ca-certs
              readOnly: true
          dnsPolicy: ClusterFirstWithHostNet  # 如果不修改,pod 的dns-server 会变成宿主机的,无法访问集群内部的svc
          hostNetwork: true # pod 使用宿主机网络
          nodeSelector:
            node-role.kubernetes.io/edge: edge   # 进部署在edge(边缘)节点
          tolerations:
          - key: "node-role.kubernetes.io/edge"
            operator: "Exists"
            effect: "NoSchedule"  # 增加容忍
          restartPolicy: Always
          schedulerName: default-scheduler
          securityContext: {}
          serviceAccount: istio-ingressgateway-service-account
          serviceAccountName: istio-ingressgateway-service-account
          terminationGracePeriodSeconds: 30
          volumes:
          - configMap:
              defaultMode: 420
              name: istio-ca-root-cert
            name: istiod-ca-cert
          - emptyDir: {}
            name: data
          - configMap:
              defaultMode: 420
              name: consul-client-config
            name: config
          - downwardAPI:
              defaultMode: 420
              items:
              - fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.labels
                path: labels
              - fieldRef:
                  apiVersion: v1
                  fieldPath: metadata.annotations
                path: annotations
            name: podinfo
          - emptyDir: {}
            name: ingressgatewaysdsudspath
          - name: ingressgateway-certs
            secret:
              defaultMode: 420
              optional: true
              secretName: istio-ingressgateway-certs
          - name: ingressgateway-ca-certs
            secret:
              defaultMode: 420
              optional: true
              secretName: istio-ingressgateway-ca-certs
      updateStrategy:
        rollingUpdate:
          maxUnavailable: 1
        type: RollingUpdate
    
  • 部署consul-sync-catalog 服务

    $ helm repo add hashicorp https://helm.releases.hashicorp.com # helm添加consul源
    $ cat config.yaml
    syncCatalog:
      enabled: true  # 默认不安装sync,需要手动开启
      toConsul: true # 开启k8s->consul的同步
      toK8S: false # 关闭consul->k8s的同步
      default: false # If true, all valid services in K8S are synced by default. If false, the service must be annotated properly to sync. In either case an annotation can override the default
    $ helm install consul hashicorp/consul --set global.name=consul -n consul -f config.yaml # 会安装consul,consul-server,consul-sync-catalog
    
  • 通过helm 安装的consul-server pod 处于pending 中,因为他需要pvc 而我本地并没有,我们将需要持久化的数据目录修改为emptydir 来解决。并修改consul-server 的svc 类型为nodeport 以便外部访问

  • 给ingressgateway 的svc 添加annotations

    apiVersion: v1
    kind: Service
    metadata:
      annotations:
        consul.hashicorp.com/service-name: ingressgateway
        consul.hashicorp.com/service-port: http2
        consul.hashicorp.com/service-sync: "true"
    ...
    
  • 从consu-ui 中查看service ,已经有一个名为ingressgateway 的服务注册成功了。包含两个instances。

负载均衡配置

  • 安装upsync 插件

    $ cd /root 
    $ yum install git pcre-devel openssl-devel # 安装openresty 依赖相关包
    $ git clone https://github.com/weibocom/nginx-upsync-module.git # 下载upsync 插件
    $ wget https://openresty.org/download/openresty-1.17.8.1.tar.gz -o openresty-1.17.8.1.tar.gz && tar zxf openresty-1.17.8.1.tar.gz && cd openresty-1.17.8.1
    $ ./configure --prefix=/usr/local/openresty --add-module=../nginx-upsync-module/
    $ gmake -j 4 && gmake install 
    
  • 配置upstream 从consul 获取server列表

    upstream app {
       upsync 10.4.10.176:8500/v1/catalog/service/ingressgateway upsync_timeout=6m upsync_interval=1000ms upsync_type=consul_services strong_dependency=off;
       upsync_dump_path /tmp/servers_app.conf;
       include /tmp/servers_app.conf;
       server 0.0.0.0:80 down;   # 如果不加这一行,第一次reload 会因为没有server 而报错。
    }
    server {
      listen       80;
      server_name  api.itanony.com;
      charset utf-8;
    
      location /upstream_list {
          upstream_show;
      }
      location = /api/v1/products {
          proxy_pass http://app;
          proxy_http_version 1.1;
      }
    
    }
    

相关问题

502

在ingressgateway滚动更新过程中进行压测,使用如下命令,日志中还是会有502 的情况, 怀疑是consul-sync-catalog同步不及时。

for i in `seq 1 1000`; do  curl -o /dev/null -s -w "%{time_total}:%{http_code}\n"  http://api.xiaozhu.com/api/v1/products| tee -a 1.log; done

查看到官方文档 中有consulWriteInterval的配置

consulWriteInterval (string: null) - Override the default interval to perform syncing operations creating Consul services.

这个参数应该是控制consul-sync-catalog向consul 集群同步间隔的(看来consul-sync-catalog不是实时的)。这种情况下, 我们可以通过调小这个间隔或者通过给ingressgateway添加一段prestop来解决。注意sleep 的时间要大于consulWriteInterval的值

最后的解决方法:

$ cat config.yaml
syncCatalog:
  enabled: true
  toConsul: true
  toK8S: false
  default: false
  consulWriteInterval: 10s
$ helm upgrade consul hashicorp/consul --set global.name=consul -n consul -f config.yaml
$ kubectl get daemonsets.apps  -n istio-system  istio-ingressgateway  -o yaml
...
        lifecycle:
          preStop:
            exec:
              command:
              - /bin/sh
              - -c
              - sleep 40
...

一些思考

  • 这种方式的好处是ingress-controller 直接通过宿主机网络来实现流量收发。性能相对比nodeport 要高,而且可以避免nodeport 的一些弊端。相比lvs 方案, 也可以避免vrrp 切换中间的流量损失
  • 为什么不用采用NodePort 方式暴露ingress?在测试中。将NodePort的外部流量策略改为Local 或者 Cluster 的情况下。consul-sync-catalog 均会根据pod 的分布,将没有pod 处于不可用状态的node节点从instance 中摘除。但是NodePort 相比直接采用宿主机网络会经过一次目的地址转换。效率自然相比宿主机网络模式要低一点。
  • 弊端:虽然保证了pod滚动更新情况下的完全无损。但是。如果pod 因为一些原因,就绪探针失败而从svc 上摘除, 这块还是没法实现完全无损,不考虑同步的网络延时。这个故障间隔最大是consulWriteInterval。 这里可以通过nginx_upstream_check_module 在负载层做健康监测主动摘除减低影响。 或者应用+负载层的重试来避免。
  • 如果企业内部已经通过consul 来做服务发现。 那其实我们可以借助一些非cluster network plugins如macvlan来实现内部业务的无缝迁移kubernetes。仅使用kubernetes 的调度和资源编排能力, 业务的服务发现和服务注册依然通过consul 来实现。
  • 对于同一个svc中定义了多个端口的服务,consul-sync-catalog默认会以第一个端口作为instance 的port。 其他端口通过metadata 的形式写入consul中。 当然我们也可以通过添加consul.hashicorp.com/service-port 的注解来显式指定哪个端口作为instance 的端口。其他端口想同步。 那我们就再定义几个服务吧。。。

参考

https://www.consul.io/docs/k8s/service-sync#syncing-kubernetes-and-consul-services

https://www.consul.io/docs/k8s/helm

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343