距离2017 年的见闻技术架构调整接近 2 年,随着业务线的发展,见闻技术部的项目数量、项目架构类型、基础设施规模、服务变动频率都在不断地增加,带给 SRE 的挑战是如何能更快地助力于开发人员更快更稳定地部署服务,保障线上服务的稳定。前端
咱们的后端开发团队仍然以 Golang 为主,不一样业务线的技术选型不尽相同,同时存在 Python,Java 服务,这就须要 SRE 提供更易接入的微服务基础组件,常见的方案就是为每种语言提供适配的微服务基础组件,但痛点是基础组件更新维护的成本较高。git
为了解决痛点,咱们将目光放到服务网格,它能利用基础设施下沉解决多语言基础库依赖问题,不一样的语言不须要再引入各类不一样的服务发现、监控等依赖库,只需简单的配置并运行在给定的环境下,就能享有以上功能,同时网络做为最重要的通讯组件,能够基于它实现不少复杂的功能,譬如根据不一样可用区进行的智能路由、服务熔断降级等。github
为此,咱们调研了一些服务网格方案,包括Istio、Linkerd,基于咱们的当前的后端架构特色:golang
对比下来,Istio 拥有更多活跃的开源贡献者,迭代速度快,以及 Istio 架构可行性讨论,咱们选择 Istio 做为实践方案。docker
这张图介绍了见闻典型的服务网格架构,左半图介绍了一个用户请求是如何处理,右半图介绍运维系统是如何监控服务,若无特殊说明,服务都是部署在腾讯云托管 Kubernetes。数据库
以上的流程能够观察到,服务之间通讯彻底依靠 Proxy 进程完成,Proxy 进程接管同一个 Pod 中服务的出入流量,完成请求的路由。apache
经过架构图以及以上流程,咱们拆分出如下关键组件,观察其性能、可用性、拓展性。后端
Istio Ingress 高性能,可拓展 Istio Ingress 用来处理用户入流量,使用 Envoy 实现,转发性能高。挂载在负载均衡后,经过增长实例实现可拓展。api
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 是具备可实施性的。
Pilot Discovery 负责 Istio 服务发现,支持在 Kubernetes 里部署,它读取 K8S 资源配置,并生成 Proxy 可用的路由表。如下面的 Service A 服务为例,介绍 Istio 如何进行精细路由。
要知道若是在 Istio 访问一个服务,必须得声明 K8S Service。
Istio 经过 K8S CRD拓展 K8S 已有的服务访问能力,咱们列举网络相关经常使用的配置:
如下举个例子介绍如何利用它们配置一样大小流量到服务的不一样版本,
# 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 有至关丰富的功能支持,同时也带来了至关的复杂度,建议用户根据平常的使用频率在后台实现相应的前端控制台,利用自动化来完成流量控制。
istio-injection : enabled
标签,这样实现该 namespace 下的全部 Pod 自动注入 Proxy;二是 deployment 能够设置 annotation 关闭自动注入。 若是 K8S 版本不够,能够利用命令行工具修改 Deployment 的配置。咱们目前的服务部署在腾讯云托管 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 框架,拥抱更轻量级的基础框架,这个框架只要支持:
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 是否初始化完成,见闻的实践是在基础镜像中打入一个探活程序:
# 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 资源要求
# 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
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: serviceA
spec:
hosts:
- serviceA
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
复制代码
yaml # 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
yaml # virtualService apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: serviceA spec: hosts: - serviceA http: - route: - destination: host: serviceA subset: v2
使用Istio Dashboard来实现上述流程
为何使用 Istio Ingress 做为新的 Ingress 方案?
过去咱们使用腾讯云托管的 Kubernetes Ingress,为了对 Ingress 流量控制而引入 Istio Ingress。咱们以前提到 Istio Ingress 是基于 Envoy,它读取 Istio 控制的配置进行路由,与其它内部服务同样方便地接入 Istio 全部功能。
除了 VirtualService 和 DestinationRule,Istio 定义了 Gateway 来控制实例支持的 Host 和证书。具体的流程是:
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、p9五、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 的目标依然是保障线上服务的健壮性。