基于Kubernetes的微服务可观测性与Istio服务网格
本系列文章将分为两部分,在此我们将探讨Istio服务网格中一部分,即可观测性工具集。这些工具包含了Jaeger、Kiali、Prometheus以及Grafana。为辅助我们此行探索,我们将在GCP上部署基于Go的微服务参考平台到GKE上去。
由Cindy Sridharan撰写,O’Reilly出版的《分布式系统可观测性[1]》一书在第四章节中详细地介绍了“可观测性的三大支柱”。在继续下文之前,我强烈建议你阅读下这篇在线摘录。关于可观测性资讯的另一大来源是honeycomb.io,一个生产系统可观测性工具的开发商,由知名的行业思想领袖Charity Majors领导。这个站点上面发布了很多关于可观测性的文章、博文、白皮书和博客。
随着现代分布式系统变得越发复杂,想具备观察这些系统的能力需要同样在设计之初就考虑到适配如此复杂环境的现代化的工具。传统的日志记录和监控系统通常与当今的混合和多云、基于多种语言、事件驱动、基于容器和无服务器、可无限扩展的、临时计算的平台相竞争。
像Istio服务网格这类工具试图通过提供与几种同类最佳的开源遥测工具的原生集成方案来解决可观测性带来的挑战。Istio的集成包括Jaeger用于分布式跟踪,Kiali用于分布式系统可视化,Prometheus和Grafana用于度量指标收集、监控和警报。同时结合云平台原生的监控和日志服务,例如谷歌云平台上Google Kubernetes Engine(GKE)的Stackdriver,我们为现代化的分布式应用打造完整的可观测平台。
该参考平台旨在生成基于HTTP的服务到服务,基于TCP的服务到数据库以及基于TCP的服务到队列再到服务(RabbitMQ)的IPC(进程间通信)。服务A调用服务B和C,服务B调用服务D和E,服务D生产消息到RabbitMQ队列供服务F来消费并写入到MongoDB,依此类推。当这个系统部署到运行Istio服务网格的Kubernetes集群中时,可以使用Istio的可观测性工具来观察这些分布式通信。
服务响应
在该平台上,每个上游服务通过返回一个小的JSON信息负载(在源码中称为问候语)来响应下游服务的请求。
这些请求响应在调用链中聚合,进而产生给边缘服务的一系列服务响应,并返回给UI界面,最终展示在用户的浏览器中。响应聚合功能只是简单验证服务到服务间通信,Istio组件和遥测工具都能否正常工作。
所有的Go微服务都包含一个/ping和/health端点。/health端点用于配置Kubernetes的存活以及就绪探针。此外,边缘服务A使用响应头access-control-allow-origin: *用来配置Cross-Origin Resource Sharing(CORS)[2]。CORS允许用于浏览器来调用位于与UI不同子域的/ping端点。相关服务A的源码可以在此[3]查看。
在本次演示中,MongoDB数据库将被托管到GCP的外部,即MongoDB Atlas,它是一个基于云的MongoDB即服务平台。同样的,RabbitMQ队列服务将被托管到CloudAMQP,这是一个基于云的RabbitMQ即服务平台。我曾在之前的几篇文章中使用过这两种Saas供应商。使用外部服务将帮助我们了解Istio和它的可观测性工具是如何为位于Kubernetes集群中和外部系统的通信提供遥测的。(如果你想深入快速学习Kubernetes,可以报名参加我们组织的为期3天的Kubernetes实战培训,一线资深讲师带你从0开始上手Kubernetes。)
服务F[4]消费来自RabbitMQ队列的消息,服务D将消息写入队列并写入到MongoDB中,这些服务都能通过对应链接打开直接查看源码。
git clone --branch master --single-branch --depth 1 --no-tags \
https://github.com/garystafford/k8s-istio-observe-backend.git
Kubernetes资源中引用到的相关Go服务和UI界面的容器镜像,都可以在Docker Hub上找到。Go服务镜像是使用官方的Golang Alpine来作为基础镜像,包含了Go 1.12.0版本。使用Alpine镜像来编译源码将能保证容器镜像尽可能地小并且包含更小的可攻击面。
系统要求
创建MongoDB Atlas数据库集群
创建CloudAMQP RabbitMQ集群
根据自己的环境调整Kubernetes的资源文件和脚本
在GCP上创建GKE集群
使用Helm在GKE集群中部署Istio 1.1.0版本
为平台中需要暴露访问的资源创建DNS记录
部署所有Go微服务,Angular UI和与GKE相关联的资源
测试或为你的平台排障
在第二篇中查看观察的结果
MongoDB Atlas集群
MongoDB Atlas目前提供有四种定价方案,包含免费版、基础版、高级版和企业版。这些方案范围从最小的共享内存和512MB存储的M0规模的MongoDB集群,到最高具有488GB内存和3TB存储的大型M400 MongoDB集群。
在本文中,我在GCP的us-central1地区创建了一个M2大小的MongoDB集群,并为该演示创建了一个数据库账户,该帐户将用于连接GKE上运行的八个基于Go的微服务的其中四个。
最初我使用了一个M0大小的集群,但发现计算资源不足以支撑来自这些Go语言的微服务。在此我建议至少使用M2大小乃至更大的集群。
配置MongoDB Atlas的Istio ServiceEntry
在external-mesh-mongodb-atlas.yaml文件中添加MongoDB Atlas主机地址。此文件允许从GKE上的四个微服务到外部MongoDB Atlas集群的出口流量。
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: mongodb-atlas-external-mesh
spec:
hosts:
- {{ your_host_goes_here }}
ports:
- name: mongo
number: 27017
protocol: MONGO
location: MESH_EXTERNAL
resolution: NONE
配置CloudAMQP RabbitMQ的Istio ServiceEntry
在external-mesh-cloudamqp.yaml文件中添加CloudAMQP主机地址,此文件允许从两个微服务到CloudAMQP群集的出口流量。
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: cloudamqp-external-mesh
spec:
hosts:
- {{ your_host_goes_here }}
ports:
- name: rabbitmq
number: 5672
protocol: TCP
location: MESH_EXTERNAL
resolution: NONE
IstioGateway和VirtualService
通过Istio你可以有许多策略用于配置路由流量。此文中我使用了example-api.com这个域名,还有4组子域名。一组子域用于Angular UI,分别是dev命名空间(ui.dev.example-api.com)和test命名空间(ui.test.example-api.com)。另一组子域用于边缘API微服务,即被UI界面调用的服务A(api.dev.example-api.com和api.test.example-api.com)。流量将根据URL路由到特定的Kubernetes服务去。
Istio中所定义的Gateway对象描述了一个在网格边缘操作的负载均衡器,它负责接收传入或传出的HTTP/TCP连接。修改Istio ingress Gateway,在该hosts部分中插入你自己的域名或子域,这些是端口80上允许进入网格的主机。
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: demo-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- ui.dev.example-api.com
- ui.test.example-api.com
- api.dev.example-api.com
- api.test.example-api.com
Istio中的VirtualService定义了一组主机域名被寻址时应用的流量路由规则。一个VirtualService与Gateway绑定来控制到达特定主机和端口的流量的转发。修改项目中的4个VirtualServices,将你的域名或子域添加进去。以下是在istio-gateway.yaml中一个VirtualService的例子。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: angular-ui-dev
spec:
hosts:
- ui.dev.example-api.com
gateways:
- demo-gateway
http:
- match:
- uri:
prefix: /
route:
- destination:
port:
number: 80
host: angular-ui.dev.svc.cluster.local
KubernetesSecret
该项目中包含了一个Kubernetes Secret对象,go-srv-demo.yaml文件中指定了两个键值,一个用于MongoDB Atlas的连接,一个用于CloudAMQP,Kubernetes中的Secret的值需要经过base64加密。
apiVersion: v1
kind: Secret
metadata:
name: go-srv-config
type: Opaque
data:
mongodb.conn: {{ your_base64_encoded_secret }}
rabbitmq.conn: {{ your_base64_encoded_secret }}
在Linux和Mac上,你能直接使用base64程序来对此连接字符串进行编码。
> echo -n "mongodb+srv://username:[email protected]/test?retryWrites=true" | base64
bW9uZ29kYitzcnY6Ly91c2VybmFtZTpwYXNzd29yZEBhdGxhcy1jbHVzdGVyLmdjcC5tb25nb2RiLm5ldC90ZXN0P3JldHJ5V3JpdGVzPXRydWU=
> echo -n "amqp://username:[email protected]/cluster" | base64
YW1xcDovL3VzZXJuYW1lOnBhc3N3b3JkQHJtcS5jbG91ZGFtcXAuY29tL2NsdXN0ZXI=
Bash脚本变量
# 对应修改这些常量
readonly PROJECT='{{ your_gcp_project_goes_here }}'
readonly CLUSTER='go-srv-demo-cluster'
readonly REGION='us-central1'
readonly MASTER_AUTH_NETS='72.231.208.0/24'
readonly GKE_VERSION='1.12.5-gke.5'
readonly MACHINE_TYPE='n1-standard-2'
脚本part4_install_istio.sh[8]中包含了ISTIO_HOME这个变量,该值应与你本地的Istio 1.1.0的路径一致。下面是在我本地的Mac上的值:
readonly ISTIO_HOME='/Applications/istio-1.1.0'
部署GKE集群
该脚本使用本地Istio 1.1.0中install/kubernetes/helm/istio目录中的Helm Chart安装Istio。该安装脚本使用--set命令行标志覆写了Istio Helm Chart中的多个默认值。相关可用的配置项在GitHub project[9]中有详细说明。这些选项启用了Istio的可观测性功能,包含Kiali、Grafana、Prometheus和Jaeger,这些我们将在本系列的第二篇中探索。
helm install ${ISTIO_HOME}/install/kubernetes/helm/istio-init \
--name istio-init \
--namespace istio-system
helm install ${ISTIO_HOME}/install/kubernetes/helm/istio \
--name istio \
--namespace istio-system \
--set prometheus.enabled=true \
--set grafana.enabled=true \
--set kiali.enabled=true \
--set tracing.enabled=true
kubectl apply --namespace istio-system \
-f ./resources/secrets/kiali.yaml
如下图,我们可以看到与Istio相关联的负载已经运行在集群中,包含了可观测性工具集。
同样,我们能看到集群中相应的Istio相关的Service资源。
修改DNS记录
部署GKE集群和Istio会触发创建一个Google Load Balancer,四个IP地址和所有必要的防火墙规则。与转发规则相关联的四个IP地址之一(如下所示)将与负载均衡器的前端相关联。
如下所示,我们能看到一个新的负载均衡器,包含了前端IP地址和三个GKE集群工作节点的后端VM池。每个节点都被分配了一个IP地址。
接着我使用了Google Cloud DNS创建了四个子域并将所有子域与负载均衡器前端的IP绑定。到这些地址的入口流量将通过Istio ingress Gateway和四个Istio VirtualService然后路由到适当的Kubernetes Service资源去。使用你选定的DNS管理工具来创建四个A类型的DNS记录。
部署脚本将所有资源部署到两个Kubernetes命名空间,分别是dev和test。这将使我们能够在使用可观测性工具时区分不同的命名空间。
下图展示刚刚部署的与Istio相关的资源,它们包括Istio Gateway,四个Istio VirtualService和两个Istio ServiceEntry资源。
接着是在集群上运行此平台的工作负载(Kubernetes Deployment资源)。在这我们可以看到每个工作负载有两个Pod,共有18个Pod,在dev命名空间中运行。每个Pod都包含已部署的微服务或UI组件,以及Istio的Envoy Proxy的副本。
下图我们看到dev命名空间中创建了对应的Kubernetes Service资源。
下图test命令空间中的资源与dev命名空间的类似。
UI界面要求你输入服务A的主机名,即API的边缘服务。由于你无法使用我的子域,并且JavaScript代码是在你的Web浏览器本地运行,因此该选项允许你提供自己的主机域。这与你在VirtualService中为UI配置的域名相同。此域名将API调用路由到dev命名空间中运行的Service A服务的FQDN service-a.dev.svc.cluster.local,或test命名空间service-a.test.svc.cluster.local。
你还可以使用性能测试工具对平台进行负载测试。很多问题在平台出现负载前都很难发现。最近我开始使用hey,一个现代化的负载生成工具,作为Apache Bench的替代品,它不像ab,hey支持HTTP/2端点,这是测试运行在包含了Istio组件的GKE集群上的平台工作所必需的。下面我直接在Google Cloud Shell使用hey工具,将模拟25个并发用户,为服务A生成总共1000个基于HTTP/2的GET请求。
四个DNS记录中存在错误,它们不是解析到负载均衡器的前端IP地址上
没有为四个Istio的VirtualService资源配置正确的子域名
基于Go的微服务无法访问外部MongoDB Atlas和CloudAMQP RabbiqMQ集群。可能是Kuberetes Secret资源结构不对或者是两个ServiceEntry中包含了那些到外部集群的错误的主机信息
我建议通过使用cURL或Postman工具直接调用服务A(API的边缘服务)来开始排障。如果你能看到类似下图的JSON格式的响应,那则表明问题在于UI而非API。
接下来,确认为服务D、F、G和H创建了四个MongoDB数据库。另外确保新文档被正确写入数据库。
另外使用CloudAMQP RabbitMQ管理控制台确认已创建新的RabbitMQ队列。服务D产生信息,服务F从队列中取出并消费。
最后查看Stackdriver日志以查看是否存在任何明显错误。
由于集群只需几分钟即可完成创建和部署资源,如果需要清除集群请运行part6_tear_down.sh[12]脚本。
以上所有仅代表我个人观点,与现任、前任雇主和客户无关。
相关链接:
https://www.oreilly.com/library/view/distributed-systems-observability/9781492033431/
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
https://gist.github.com/garystafford/c87c2fcd7bd3da1438ae74e8f68fac44#file-main-go
https://gist.github.com/garystafford/870ee61929591e3b6b374a917132c8bf#file-main-go
https://github.com/garystafford/k8s-istio-observe-backend
https://github.com/garystafford/k8s-istio-observe-frontend
https://github.com/garystafford/golang-srv-demo/blob/master/part3_create_gke_cluster.sh
https://github.com/garystafford/golang-srv-demo/blob/master/part4_install_istio.sh
https://github.com/istio/istio/tree/master/install/kubernetes/helm/istio#configuration
https://github.com/garystafford/golang-srv-demo/blob/master/part5a_deploy_resources.sh
https://github.com/garystafford/golang-srv-demo/blob/master/part5b_delete_resources.sh
-
https://github.com/garystafford/golang-srv-demo/blob/master/part6_tear_down.sh