华尔街见闻基于istio的服务网格实践
距离2017年的见闻技术架构调整接近2年,随着业务线的发展,见闻技术部的项目数量、项目架构类型、基础设施规模、服务变更频率都在不断地增长,带给SRE的挑战是如何能更快地助力于开发人员更快更稳定地部署服务,保障线上服务的稳定。
我们的后端开发团队仍然以Golang为主,不同业务线的技术选型不尽相同,同时存在Python,Java服务,这就需要SRE提供更易接入的微服务基础组件,常见的方案就是为每种语言提供适配的微服务基础组件,但痛点是基础组件更新维护的成本较高。
为了解决痛点,我们将目光放到服务网格,它能利用基础设施下沉解决多语言基础库依赖问题,不同的语言不需要再引入各种不同的服务发现、监控等依赖库,只需简单的配置并运行在给定的环境下,就能享有以上功能,同时网络作为最重要的通信组件,可以基于它实现很多复杂的功能,譬如根据不同可用区进行的智能路由、服务熔断降级等。
为此,我们调研了一些服务网格方案,包括Istio、Linkerd,基于我们的当前的后端架构特点:
- 服务通信协议主要基于gRPC、HTTP
- 基于Kubernetes的Docker部署
- 拥抱开源,使用了Prometheus、Grafana作为监控,Zipkin作为链路追踪等
对比下来,Istio拥有更多活跃的开源贡献者,迭代速度快,以及Istio架构可行性讨论,我们选择Istio作为实践方案。
架构
这张图介绍了见闻典型的服务网格架构,左半图介绍了一个用户请求是如何处理,右半图介绍运维系统是如何监控服务,若无特殊说明,服务都是部署在腾讯云托管Kubernetes。
组件一览
- Go(1.11)
后端语言。 - Docker(17.12.1-ce)
容器技术。 - Kubernetes(1.10.5,托管于腾讯云容器平台)
容器编排工具。 Istio(1.0.0)
服务网格开源方案,支持与Kubernetes集成。- Dashboard
基于Kubernetes Dashboard二次开发的Istio Dashboard,负责管理Istio服务发布,配置变更等。 - Grafana
负责监控数据可视化。 - Prometheus
时序数据库,常用于监控系统。 - Jaeger
负责服务链路追踪,组件包括collector、Jaeger UI。 - Cassandra
分布式NoSQL数据库,用于Jaeger指标数据存储。
用户请求分析
- 我们先从用户请求端开始,用户的请求通过Tencent 4层LB转发到基于Envoy的Istio Ingress,Ingress根据配置将请求路由到Service A所在的Pod
Service A所在Pod接收Ingress请求
- 访问Service A的请求会先到达Proxy再由它转发到Service A进程
- Service A向Service B发出的请求被iptables路由到Proxy(下文会提到iptables的初始化)
Proxy进程发起对Service B所在Pod的请求
- Proxy进程同步请求Mixer Policy服务,检查是否允许访问Service B,检查通过,开始请求
- Proxy进程记录请求的指标(QPS,Latency,Status Code分布等),异步并批量上报到Mixer Telemetry服务,这里是客户端指标。
Service B所在Pod接收请求
Service B Proxy接收请求并路由到Service B所在进程
- Proxy进程记录请求的指标(QPS,Latency,Status Code分布等),异步并批量上报到Mixer Telemetry服务,这里是服务端指标。
- Service B进程处理完请求并返回
- 数据原路返回到用户端
以上的流程可以观察到,服务之间通信完全依靠Proxy进程完成,Proxy进程接管同一个Pod中服务的出入流量,完成请求的路由。
架构可行性
通过架构图以及以上流程,我们拆分出以下关键组件,观察其性能、可用性、拓展性。
- Istio Ingress高性能,可拓展
Istio Ingress用来处理用户入流量,使用Envoy实现,转发性能高。挂载在负载均衡后,通过增加实例实现可拓展。
- Istio Proxy随应用部署,轻微性能损耗,可随应用数量拓展
Istio Proxy以Sidecar形式随应用一起部署,增加2次流量转发,存在性能损耗。
性能: 4核8G服务器,上面运行Proxy服务和API服务,API服务只返回ok字样。(此测试只测试极限QPS)
单独测试API服务的QPS在59k+,平均延时在1.68ms,CPU占用4核。
通过代理访问的QPS7k+,平均延时14.97ms,代理CPU占用2核,API服务CPU占用2核。
CPU消耗以及转发消耗降低了QPS,增加了延时,通过增加机器核数并增加服务部署数量缓解该问题,经过测试环境测试,延时可以接受。
可用性:基于Envoy,我们认为Envoy的可用性高于应用。依赖Pilot Discovery进行服务路由,可用性受Pilot Discovery影响。
拓展性:Sidecar形式,随应用数拓展
- Istio Policy服务可拓展,但同步调用存在风险
Istio Policy需要在服务调用前访问,是同步请求,会增加服务调用延时,通过拓展服务数量增加处理能力。属于可选服务,见闻生产未使用该组件。
性能: 未测试
可用性:若开启Policy,必须保证Policy高可用,否则正常服务将不可用
拓展性:增加实例数量进行拓展
- Istio Telemetry监控收集服务
性能: 从监控上观察Report 5000qps,使用25核,响应时间p99在72ms。异步调用不影响应用的响应时间。
可用性:Telemetry不影响服务可用性
拓展性:增加实例数量进行拓展
- Pilot Discovery
性能: 服务发现组件1.0.5版本经过监控观察,300个Service,1000个Pod,服务变更次数1天100次,平均CPU消耗在0.01核,内存占用在1G以内。
可用性: 在服务更新时需要保证可用,否则新创建的Pod无法获取最新路由规则,对于已运行Pod由于Proxy存在路由缓存不受Pilot Discovery关闭的影响。
拓展性:增加实例数量可以增加处理量。
可以看到各个组件的可用性、拓展性都有相应的策略达到保障,我们认为Istio是具有可实施性的。
Istio流量控制背后
Pilot Discovery: Istio的交通大脑
Pilot Discovery负责Istio服务发现,支持在Kubernetes里部署,它读取K8S资源配置,并生成Proxy可用的路由表。以下面的Service A服务为例,介绍Istio如何进行精细路由。
- Discovery与对应K8S集群的API server相连,拉取全量资源信息,并使用Watch方法对增量变更进行同步。
- 根据Service A的配置,生成对应Service A的路由配置,通过gRPC形式的ADS接口提供给Proxy。
- Proxy同步到最新配置,热更新后配置生效。
要知道如果在Istio访问一个服务,必须得声明K8S Service。
Istio通过K8S CRD拓展K8S已有的服务访问能力,我们列举网络相关常用的配置:
- Gateway
控制Istio Ingress的路由转发及TLS证书绑定。
- VirtualService
服务流量控制,实现如A/B测试、错误注入、服务保护等。
- DestinationRule
用于目标服务的版本管理,根据Pod的Label区分目标服务的版本,联合VirtualService进行流量控制。
以下举个例子介绍如何利用它们配置同样大小流量到服务的不同版本,
# serviceA.yaml kind: Service apiVersion: v1 metadata: name: serviceA labels: app: serviceA spec: ports: - name: http-8080 protocol: TCP port: 8080 targetPort: 8080 selector: app: serviceA type: ClusterIP # virtualServiceA.yaml apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: serviceA spec: hosts: - serviceA http: - route: - destination: host: serviceA subset: v1 - route: - destination: host: serviceA subset: v2 --- # destinationRule apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: serviceA spec: host: serviceA subsets: - labels: version: v1 name: v1 - labels: version: v2 name: v2
以上实现了Istio服务调用serviceA时,会随机地50%概率到serviceA的v1版本,50%概率到serviceA的v2版本。
可以看到,VirtualService通过hosts关联serviceA,在http区域有两个route,分别是subset v1, subset v2,v1,v2依赖DestinationRule来定义,同样用host来标注该DestinationRule控制哪个host的访问,以及通过pod label中version来划分不同版本。
流量控制方面,Istio有相当丰富的功能支持,同时也带来了相当的复杂度,建议用户根据日常的使用频率在后台实现相应的前端控制台,利用自动化来完成流量控制。
Proxy工作机制
- 自动注入
在K8S 1.9之后的版本,Istio利用K8S提供的MutatingAdmissionWebhook在K8S创建Pod前回调Istio提供的istio-sidecar-injector动态修改Pod配置,添加以Sidecar形式运行的Proxy。这里开启自动注入有两个维度,一是namespace,namespace需要添加istio-injection : enabled
标签,这样实现该namespace下的所有Pod自动注入Proxy;二是deployment可以设置annotation关闭自动注入。
如果K8S版本不够,可以利用命令行工具修改Deployment的配置。
- 接管Pod流量
Service A所在Pod至少运行Service A应用容器以及用于代理的Envoy容器,创建Pod时proxy-init命令负责获取Pod监听的端口和具体协议,以此初始化网络,利用iptables将容器网络出入流量都转发到Proxy监听的localhost端口。
若Service A的Pod声明servicePort为8080:HTTP,最终Proxy将会接收8080端口的Pod入流量和全部的Pod出流量。
- 服务发现
Proxy基于Envoy,与Pilot Discovery连接,动态同步Kubernetes集群中所有的服务信息:服务与Pod IP、端口之间的映射表,通过路由信息实现智能路由,从而使服务发现从业务代码中剥离。
- 链路追踪
Proxy支持设置Zipkin URL,异步上报链路追踪数据。
- 服务质量监控
Proxy将属性包上报给Telemetry服务,Telemetry根据用户的配置生成指标数据并由Prometheus收集。
适配Istio
我们目前的服务部署在腾讯云托管Kubernetes,节点使用16核32G的网络增强型机器,所有的后端服务都以Docker部署,K8S集群外部署高可用ETCD支持集群内服务发现,数据库以MySQL、Cassandra、MongoDB为主,消息队列采用Kafka、NSQ。在应用Istio的过程中,我们对基础库进行了修改,删减了Istio已提供的功能并完成了对Istio的适配。
前情提要
见闻旧后端服务架构,所有Golang服务以打包成Docker镜像,以"gRPC"协议通信。
去"框架"
见闻Golang后端使用go-micro框架,一个支持多插件的Golang微服务框架,作者将组件分成transport,server,client,registry,codec等,通过组合不同类型的组件非常灵活地配置微服务技术栈。对于有定制需求的微服务架构,是值得推荐的选择。
通信协议作为服务互通的基石,Istio对gRPC和HTTP非常友好,根据协议Istio能解析HTTP头中的信息,支持提取指标以供分析。go-micro只是利用HTTP和gRPC作为通信协议,但是协议的解析逻辑是协议无关的,所以可以说它只是用了这些通信协议的外壳,传输的报文内容是"micro方言",这就导致了Golang暴露的服务无法被其它语言其它框架调用。为了将协议能在多语言中完全统一,也为了更好地使用Istio的监控统计功能,这个时候我们开始对go-micro的存留有一些新的思考,我们是否还需要go-micro?经过近2年的生产实践,我们是不是可以更精简我们的框架?
经过这些思考过后,我们的决定是去go-micro框架,拥抱更轻量级的基础框架,这个框架只要支持:
- gRPC
纯原生即可
- Istio
支持Istio的基础功能,譬如一些HTTP header转发等
- 改动尽量小
我们已经存在上百个Golang项目,避免改动Golang项目代码,将改动放到基础库为佳
go-micro通过定义自制protobuf插件的方式在stub代码中集成框架功能,经过对逻辑的梳理,我们决定复写protobuf插件,生成兼容micro的stub代码,通过对micro接口的向后兼容,开发人员不需要修改代码,只需要在CI阶段运行protoc即时生成新版代码。
详情可见再见,micro
运维流程
右半图描述运维人员如何利用运维后台运维Kubernetes集群,Istio的运维必须有自动化的工具来减少人工配置带来的错误,见闻的旧运维后台基于腾讯云容器平台暴露的开放API,在引入Istio后,功能依赖于更细节的label以及CRD(Custom Resource Definition),于是得依托更细粒度的Kubernetes API,新的后台需要能完成基本的Kubernetes运维,而且结合Istio的实际进行日常更新,经过选型,见闻基于Kubernetes Dashboard二次开发了Istio部分的一些功能(APP部署、更新,Istio配置更新等),利用Istio Dashboard实现APP创建、部署接口,并由此重构原有的运维后台。
最终,SRE提供两个后台,精细控制的Istio Dashboard;提供给开发人员日常更新使用的简化版后台。
服务发布
日常最重要、最高频的功能,服务版本变更。
服务创建
服务创建包括对老服务的改造,一个K8S服务要经过一些配置上的更新才能成为Istio APP。一个Kubernetes服务需要满足以下要求来获得Istio的功能支持:
- Service资源声明服务监听的端口号以及协议
这里的服务端口声明中name字段值需要以协议名为起始值,譬如grpc、http、tcp、udp等,istio识别前缀,用于初始化Proxy,譬如grpc-svc
,http-svc
,不正确的端口命名会引起Proxy的异常行为,以及监控服务无法捕获该协议下的指标。
- 服务探活接口
每个后端服务提供一个HTTP探活接口,这样服务启动时,不至于让其它服务访问到未就绪的状态。
对于HTTP探活接口的定义包括Proxy以及APP是否初始化完成,见闻的实践是在基础镜像中打入一个探活程序:
- 检测Proxy是否初始化
通过Proxy的配置同步时间与Pilot Discovery的配置更新时间对比,相同时认为其就绪。
- APP自定义接口
APP可以在指定端口提供就绪API。
# serviceA.yaml kind: Service apiVersion: v1 metadata: name: serviceA namespace: default labels: app: serviceA spec: ports: - name: grpc protocol: TCP port: 10088 targetPort: 10088 selector: app: serviceA type: ClusterIP
Deployment资源要求
- 有必要的两个label:app和version,明确定义流量控制中的目标服务。
可以看例子中deployment的app为serviceA,version为v1。 - 声明对外服务的端口,要求Proxy对指定端口入流量的接管。
例子中ports声明了10088端口。
- 有必要的两个label:app和version,明确定义流量控制中的目标服务。
# deploymentA.yaml kind: Deployment apiVersion: extensions/v1beta1 metadata: name: serviceA-v1 labels: app: serviceA version: v1 spec: replicas: 1 selector: matchLabels: app: serviceA version: v1 template: metadata: labels: app: serviceA version: v1 spec: containers: - name: serviceA image: 'some-image' ports: - containerPort: 10088 protocol: TCP resources: requests: cpu: 1000m livenessProbe: httpGet: path: /health port: 54321 scheme: HTTP initialDelaySeconds: 1 timeoutSeconds: 2 periodSeconds: 5 successThreshold: 1 failureThreshold: 3 terminationMessagePath: /dev/termination-log terminationMessagePolicy: File imagePullPolicy: Always securityContext: privileged: false restartPolicy: Always terminationGracePeriodSeconds: 30 dnsPolicy: ClusterFirst
符合以上要求,服务能正确接入Istio系统并获得服务发现和监控的能力。
服务更新
Istio提供流量控制,给运维带来方便的A/B测试,用于根据指定规则迁移流量。
见闻的服务更新依靠Istio流量迁移功能,发布服务的新版本,并将流量通过Istio规则迁移到新版本,实际细节如下:
更新流量控制将流量指向已有版本
以下实例利用VirtualService将ServiceA的服务流量全部指向已存在的v1版本# virtualService apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: serviceA spec: hosts:
http: - route: - destination: host: serviceA subset: v1 --- # destinationRule apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: serviceA spec: host: serviceA subsets: - labels: version: v1 name: v1 ```
- 部署新版本的Deployment
查找符合app label的deployment,运维人员基于该deployment创建v2版本的deployment,并向destinationRule中增加v2版本。
# destinationRule apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: serviceA spec: host: serviceA subsets: - labels: version: v1 name: v1 - labels: version: v2 name: v2
- 更新流量控制将流量指向新版本
以下实例利用VirtualService将ServiceA的服务流量全部指向v2版本
# virtualService apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: serviceA spec: hosts: - serviceA http: - route: - destination: host: serviceA subset: v2
- 下线v1版本的deployment,删除DestinationRule中的v1
使用Istio Dashboard来实现上述流程
Ingress配置
为什么使用Istio Ingress作为新的Ingress方案?
过去我们使用腾讯云托管的Kubernetes Ingress,为了对Ingress流量控制而引入Istio Ingress。我们之前提到Istio Ingress是基于Envoy,它读取Istio控制的配置进行路由,与其它内部服务一样方便地接入Istio所有功能。
除了VirtualService和DestinationRule,Istio定义了Gateway来控制实例支持的Host和证书。具体的流程是:
- 创建Istio Ingress提供的Deployment和Service
创建Deployment ingressgateway时,以ConfigMap的形式挂载Ingress需要的证书。
- 配置Gateway
配置Ingress接收具体域名(如wallstreetcn.com)的流量,以及对应的TLS证书位置,这里的证书路径已经挂在到Ingress的Deployment上。以下是一个典型的Gateway配置。
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: wallstreetcn-com namespace: istio-system spec: selector: istio: ingressgateway servers: - hosts: - wallstreetcn.com port: name: http number: 80 protocol: HTTP - hosts: - wallstreetcn.com port: name: https number: 443 protocol: HTTPS tls: mode: SIMPLE privateKey: /etc/istio/ingressgateway-certs/tls.key serverCertificate: /etc/istio/ingressgateway-certs/tls.crt
配置完成后,再配合VirtualService的路由控制,控制Ingress的反向代理到default命名空间下的gateway服务80端口,如下所示:
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: wallstreetcn-com namespace: istio-system spec: gateways: - wallstreetcn-com hosts: - wallstreetcn.com http: - route: - destination: host: gateway.default.svc.cluster.local port: number: 80
监控指标
Istio支持Prometheus拉取集群指标,并提供Grafana看板展示。这里建议初期使用Istio自带的Grafana看板配置,并且注意Kubernetes主机的类型划分,Prometheus服务适合放在内存型机器。可以与Dashboard集成,在发布服务过程中即时查看指标。
服务质量
Istio自带一些默认的Grafana面板,统计所有可以被访问的HTTP/gRPC服务的返回码以及延时情况。
对于返回码,认为5xx为错误,并在面板上使用label_join((sum(rate(istio_requests_total{reporter="destination", response_code!~"5.*"}[1m])) by (destination_workload, destination_workload_namespace) / sum(rate(istio_requests_total{reporter="destination"}[1m])) by (destination_workload, destination_workload_namespace)), "destination_workload_var", ".", "destination_workload", "destination_workload_namespace")
计算服务错误率。
对于延时情况采用histogram_quantile
获取多维度p50、p90、p95、p99的延时分布。
链路追踪
之前提到Proxy由Envoy实现,Envoy支持设置Zipkin上报API,Proxy在收发请求时将链路指标上报到Zipkin,为了实现链路追踪,Proxy在流量转发中解析协议中的HTTP或gRPC请求头,找出其中的追踪头,组装成指标。
所以应用端需要在收到调用方请求时解析出请求头,并持续携带该请求头向后传递。
由于见闻在Ingress之后映射一个HTTP gateway,请求从Ingress转发到HTTP gateway,再发送到后续的gRPC服务,所以HTTP gateway有段代码生成gRPC请求头。
import ( "github.com/labstack/echo" gmeta "google.golang.org/grpc/metadata" ) // Create a gRPC context from Echo. func NewContextFromEcho(ec echo.Context) context.Context { md := gmeta.MD{} for _, header := range []string{ "x-request-id", "x-b3-traceid", "x-b3-spanid", "x-b3-parentspanid", "x-b3-sampled", "x-b3-flags", "x-ot-span-context", } { md.Set(header, ec.Request().Header.Get(header)) } md.Set("x-b3-parentspanid", ec.Request().Header.Get("x-b3-spanid")) return gmeta.NewOutgoingContext(context.Background(), md) }
在后续的gRPC服务调用中使用该Context,至于gRPC服务之间的调用,我们发现会自动将context传递到下一个服务,所以没有做类似处理。
这里追踪的数据如果全量捕获将会是非常大的,并且对于监控来说也不必要,所以可以设置抽样率,Istio提供ConfigMap中设置抽样率,一般来说设置成1%即可。
实践中的宝贵经验
在Istio实践过程中,有哪些需要注意的问题。
- API server的强依赖,单点故障
Istio对Kubernetes的API有很强的依赖,诸如流量控制(Kubernetes资源)、集群监控(Prometheues通过Kubernetes服务发现查找Pod)、服务权限控制(Mixer Policy)。所以需要保障API server的高可用,我们曾遇到Policy组件疯狂请求Kubernetes API server使API server无法服务,从而导致服务发现等服务无法更新配置。
* 为避免这种请求,建议使用者了解与API server直接通信组件的原理,并尽量减少直接通信的组件数量,增加必要的Rate limit。 * 尽量将与API server通信的服务置于可以随时关闭的环境,这是考虑如果部署在同一Kubernetes集群,如果API server挂掉,无法关闭这些有问题的服务,导致死锁(又想恢复API server,又要依靠API server关闭服务)
- 服务配置的自动化
服务配置是Istio部署后的重头戏,避免使用手动方式更改配置,使用代码更新配置,将常用的几个配置更新操作做到运维后台,相信手动一定会犯错的事实。
- 关于Pilot Discovery
Pilot Discovery 1.0.0版本有很大的性能问题,1.0.4有很大的性能提升,但引入了一个新bug,所以请使用1.0.5及以上的版本,该版本在见闻的平均CPU负载从10核降到了0.5核,大大降低了Proxy同步配置的延时。
- 关于Mixer Policy 1.0.0
这个组件曾导致API server负载过高(很高的list pods请求),所以我们暂时束之高阁,慎用。
- 性能调优
在使用Proxy、Telemetry时,默认它们会打印访问日志,我们选择在生产上关闭该日志。
时刻观察Istio社区的最新版本,查看新版本各个组件的性能优化以及bug修复情况,将Istio当做高度模块化的系统,单独升级某些组件。上面就提到我们在Istio1.0的基础上使用了1.0.5版本的Policy、Telemetry、Pilot Discovery等组件。
- 服务平滑更新和关闭
Istio依靠Proxy来帮助APP进行路由,考虑几种情况会出现意外的状态:
* APP启动先于Proxy,并开始调用其它服务,这时Proxy尚未初始化完毕,APP调用失败。 * Service B关闭时,调用者Service A的Proxy尚未同步更新Service A关闭的状态,向Service B发送请求,调用失败。
第一种情况要求APP有重试机制,能适当重试请求,避免启动时的Proxy初始化与APP初始化的时差。
第二种情况,一种是服务更新时,我们使用新建新服务,再切流量;一种是服务异常退出,这种情况是在客户端重试机制。希望使用Istio的开发人员有更好的解决方案。
下一步计划
见闻Istio化已于去年10月份完成并上线,我们的线上集群中Istio和非Istio的APP混合部署,上千的Pod数量曾对不够健壮的服务发现组件造成巨大的压力,在这期间曾遇到Istio的一些惊喜,并不断总结经验,希望给之后使用Istio的同学一些借鉴。之后的过程中,SRE的目标依然是保障线上服务的健壮性。
- Istio Dashboard
优化APP部署流程,考虑自动部署的功能,通过服务指标自动完成灰度发布和流量迁移。
- Prometheus
Prometheus的高可用、可拓展方案的探索。