# Istio 负载均衡的区域感知

Envoy/Istio 1.1 中有个有趣的新特性：负载均衡提供了[区域感知](https://istio.io/help/ops/traffic-management/locality-load-balancing/)的能力。简单说来，就是在分区部署的较大规模的集群，或者公有云上，Istio 负载均衡可以根据节点的区域标签，对调用目标做出就近选择。在跨区部署的应用中，原始的 Kubernetes 负载均衡可能会把来自 A 区的请求发送给远在 B 区的服务，造成高成本的跨区调用。要缩减这种损耗，通常都需要实现更多的逻辑，Istio 的区域感知特性在某种程度上提供了一种解决办法。

## 准备工作

接下来首先做一些琐碎的安装工作，这里选择了常见的 GCP 作为测试环境，Istio 版本为 1.1.2。

1. 在 GCP 的 `us-central1` 创建一个区域集群：

    ~~~shell
    $ gcloud beta container clusters create "standard-cluster-1" \
    ...
     --no-enable-basic-auth \
     --cluster-version "1.12.6-gke.10" \
     --machine-type "n1-standard-1" --image-type "COS" \
    ...
     --num-nodes "2" \
     --no-enable-cloud-logging --no-enable-cloud-monitoring \
    ...--no-enable-ip-alias \
     --addons HorizontalPodAutoscaling \
     --enable-autoupgrade --enable-autorepair
    ~~~

1. 获取本地认证，为 `kubectl` 生成 `context`：

    ~~~shell
    $ gcloud beta container clusters get-credentials \
    standard-cluster-1 --region us-central1 \
    --project dustise-mesh-lab
    ~~~

1. 查看节点标签，这里会看到不同的节点会使用[区域标签](https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#failure-domain-beta-kubernetes-io-region)进行标识：

    ~~~shell
    $ kubectl get nodes --show-labels

    ...
    failure-domain.beta.kubernetes.io/region=us-central1,failure-domain.beta.kubernetes.io/zone=us-central1-a
    ...
    failure-domain.beta.kubernetes.io/region=us-central1,failure-domain.beta.kubernetes.io/zone=us-central1-c
    ...
    ~~~

1. 为 Istio 准备 RBAC：

    ~~~shell
    $ kubectl create clusterrolebinding cluster-admin-binding \
        --clusterrole=cluster-admin \
        --user=$(gcloud config get-value core/account)
    ~~~

1. 初始化 Istio CRD：

    ~~~shell
    $ kubectl create namespace istio-system
    $ helm template install/kubernetes/helm/istio-init --name istio-init --namespace istio-system | kubectl apply -f -

    configmap/istio-crd-10 created
    configmap/istio-crd-11 created
    serviceaccount/istio-init-service-account created
    clusterrole.rbac.authorization.k8s.io/istio-init-istio-system created
    clusterrolebinding.rbac.authorization.k8s.io/istio-init-admin-role-binding-istio-system created
    job.batch/istio-init-crd-10 created
    job.batch/istio-init-crd-11 created
    ~~~

1. [安装 Isto](https://istio.io/docs/setup/kubernetes/install/helm/)：

    ~~~shell
    $ helm template install/kubernetes/helm/istio \
        --name istio --namespace istio-system \
        --values install/kubernetes/helm/istio/values-istio-demo-auth.yaml | kubectl apply -f -
    ......
    handler.config.istio.io/kubernetesenv created
    rule.config.istio.io/kubeattrgenrulerule created
    rule.config.istio.io/tcpkubeattrgenrulerule created
    kubernetes.config.istio.io/attributes created
    destinationrule.networking.istio.io/istio-policy created
    destinationrule.networking.istio.io/istio-telemetry created
    ~~~

1. 标记 `default` 命名空间，启动自动注入：

~~~shell
$ kubectl label namespaces default istio-injection=enabled --overwrite
kubectl namespace/default labeled
~~~

## 部署应用

为了方便演示，我们给惯用的 flaskapp 和 sleep 加上 `NodeSelector`，要求按照版本分布到不同区域的节点上，例如：

~~~yaml
nodeSelector:
        failure-domain.beta.kubernetes.io/zone: us-central1-f
~~~

标签内容可以参照上文 `kubectl get nodes --show-labels` 的显示结果。

修改了部署清单之后，就可以部署了：

~~~shell
$ kubectl apply -f flaskapp/flaskapp.istio.yaml
service/flaskapp created
deployment.extensions/flaskapp-v1 created
deployment.extensions/flaskapp-v2 created
$ kubectl apply -f sleep/sleep.istio.yaml
service/sleep created
deployment.extensions/sleep-v1 created
deployment.extensions/sleep-v2 created
deployment.extensions/sleep-v3 created
~~~

稍候片刻，查看部署结果：

~~~shell
$ kubectl get pods -o wide
NAME                          READY   STATUS    RESTARTS   AGE   IP          NODE                                                NOMINATED NODE
flaskapp-v1-b9644bd75-g82nj   2/2     Running   0          92m   10.40.4.9   gke-standard-cluster-1-default-pool-0570ecb1-lm7q   <none>
flaskapp-v2-77d648fbd-cvfql   2/2     Running   0          92m   10.40.3.5   gke-standard-cluster-1-default-pool-f2347d89-q79k   <none>
sleep-v1-84c85c8946-c7bvc     2/2     Running   0          91m   10.40.1.3   gke-standard-cluster-1-default-pool-0570ecb1-1qnq   <none>
sleep-v2-57cf55db78-vrvtc     2/2     Running   0          92m   10.40.3.7   gke-standard-cluster-1-default-pool-f2347d89-q79k   <none>
~~~

和上文比较，可以看到，sleep 和 flaskapp 的 v1、v2 两个版本，分别运行在 `us-central1-a` 和 `us-central1-f` 中。

## 验证路由的区域感知功能

接下来分别从网格内部和 Ingress Gateway 来验证这一功能。

### 服务网格内部请求

~~~shell
$ kubectl exec -it -c sleep sleep-v1-84c85c8946-c7bvc bash
# for i in {1..10}; do http --body  http://flaskapp/env/version ; done
v1
v2
v2
v1
...
~~~

可以看到，请求被随机分配到不同的版本，也就是说，此时的调用是无视分区的。接下来我们设置 Pilot 的环境变量，启用区域感知功能，过程很简单，给它的 Pod 加入环境变量 `PILOT_ENABLE_LOCALITY_LOAD_BALANCING`，并任意赋值即可，例如：

~~~yaml
- name: PILOT_TRACE_SAMPLING
  value: "100"
...
- name: PILOT_ENABLE_LOCALITY_LOAD_BALANCING
  value: "1"
...
~~~

再次进入 Pod 访问 flaskapp 服务：

~~~shell
$ kubectl exec -it -c sleep sleep-v1-84c85c8946-c7bvc bash
# for i in {1..10}; do http --body  http://flaskapp/env/version ; done
v1
v1
v1
...
# exit
$ kubectl exec -it -c sleep sleep-v2-57cf55db78-vrvtc bash
# for i in {1..10}; do http --body  http://flaskapp/env/version ; done
v2
v2
v2
...
~~~

可以看到，果然按照我们预想的情况，不同区域的请求，会交由不同区域的服务来进行响应。如果此时删除同区的目标负载，会发现开始平均访问其它区的服务。

### Ingress 网关

Ingress 网关控制器在网格内同样也会分配到不同的节点上，因此也同样会受到区域的影响。例如我们为 flaskapp 创建一个 `VirtualService` + `Gateway` 的组合，引入外部流量：

~~~yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: flaskapp-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "flaskapp.example.com"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - "flaskapp.example.com"
  gateways:
  - flaskapp-gateway
  http:
  - route:
    - destination:
        port:
          number: 80
        host: flaskapp
~~~

提交后，可以在外使用 `curl --resolve` 来验证：

~~~shell
$ export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
$ for i in {1..10}; do curl --resolve flaskapp.example.com:80:$INGRESS_HOST http://flaskapp.example.com/env/version ; done
v3v3v3v3v3v3v3v3v3v3
~~~

可以看到，对 Ingress Gateway 进入的流量，区域感知功能也是同样生效的。

## 区域间分流

如果只是简单的就近原则，虽然方便，但也难免有些枯燥，例如我的集群中的三个分区之间存在优先次序，或者强行指派一个区的请求需要由指定的其它分区的服务进行处理，又该怎样呢？

istio-system 中有个叫做 istio 的 configmap，其中包含了 [Istio 的一些核心配置](https://istio.io/docs/reference/config/istio.mesh.v1alpha1/)，里面的 [LocalityLoadBalancerSetting](https://istio.io/docs/reference/config/istio.mesh.v1alpha1/#LocalityLoadBalancerSetting)，包含了对区域感知负载均衡的一些行为配置。例如我们分配所有分区的流量，都分配到前面两个区域：

~~~yaml
    localityLbSetting:
      distribute:
      - from: us-central1/us-central1-a/*
        to:
          us-central1/us-central1-a/*: 90
          us-central1/us-central1-b/*: 10
      - from: us-central1/us-central1-b/*
        to:
          us-central1/us-central1-a/*: 90
          us-central1/us-central1-b/*: 10
      - from: us-central1/us-central1-f/*
        to:
          us-central1/us-central1-a/*: 90
          us-central1/us-central1-b/*: 10
~~~

应用之后，重启 Galley、Pilot、Injector，并重新注入应用，再次在不同分区的 sleep 容器中进行测试。会发现其中的请求呈现了符合配置要求的分配，并且没有发送到 us-central1-b 区。

> 事实上本次测试，并没有发现比率生效，仅达到有或无的区别。

## 结论

目前的分区域分流功能似乎还有些问题，但是不失为一个新的服务亲和思路。
分区是基于 Kubernetes Node 标签完成的，通过对标签的调整（例如机柜、楼层），能够比较方便的在**无侵入**的情况下，实现就近调用，对服务的跨区 HA，有一定的辅助作用。

## 参考链接

1. Istio 负载均衡的区域感知：https://istio.io/help/ops/traffic-management/locality-load-balancing/
1. 使用 Helm 安装 Istio：https://istio.io/docs/setup/kubernetes/install/helm/
1. Kubernetes 区域标签：https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#failure-domain-beta-kubernetes-io-region

