DockOne微信分享(二三六):扇贝 Service Mesh 发展历程


【编者的话】伴随 Kubernetes 的兴起,越来越多的公司开始实践 Service Mesh,以应对规模越来越大的微服务间通信问题。本文主要介绍了 Service Mesh 演进的常见形式,扇贝 Service Mesh 选型落地 Envoy 的过程,以及如何基于 go-control-plane 搭建自己的 Envoy xDS 服务端。希望能对在大家落地 Service Mesh 的过程中提供一点思路。

扇贝业务现状

首先跟大家简单说明一下扇贝的业务状况,扇贝三年前就开始实行 Docker 化和微服务化来应对需求的快速变更。随着公司规模扩大,组织结构也相应的调整拆分,业务团队之间变得独立,微服务数量开始快速上升,因此 2018 年初我们的技术架构也开始转向 Kubernetes 来应对挑战,Service Mesh 的落地便是伴随着转型 Kubernetes 开始的。

目前扇贝的微服务有五百多个,整个 Kubernetes 集群有七十多个节点,运行了五千多 Pod,承担每天数十亿次的内外部 API 调用,峰值 QPS 过万。

Service Mesh 的演进形式

Service Mesh 一词出现的时间虽然较晚(2016年9月 https://www.infoq.cn/article/kHBVYZIf2V3VpHGkCkkL),但宽泛的来讲,他所追求的通信与业务解耦的理念却有很长的发展的历程,通常会有下面几种演进形式:
  • 较原始的时候,开发者需要在业务代码中实现服务发现、负载均衡、熔断重试等功能,这些都是脏活累活,本不属于业务的核心功能,却需要开发者花费大量的心血。
  • 第二阶段,自然而然的我们就会想到把这部分功能剥离出来,抽象成可复用的类库,比如Java世界中的 Spring Cloud/Netflix OSS。但是这些框架从进程角度来看,还是跟业务是耦合的,而且还存在着跨语言困难,版本升级难以推进,框架庞大业务同学学习成本高等问题。
  • 第三阶段,服务通信的技术栈进一步下沉,通过 Nginx,HAProxy,Apache 等反向代理的使用引入 Proxy 层,分离业务与服务通信。Proxy层的引入是一个承上启下的阶段。有了 Proxy 层,服务注册发现、负载均衡、重试熔断等机制得以脱离业务语言和框架,开始热闹起来。但是以传统反向代理为核心的,服务变化需要重启进程甚至更改配置文件的 Proxy 层,在面对 Kubernetes 时代微服务频繁重启、漂移的状况时就显得力不从心了。
  • 进入 Kubernetes 时代,Service Mesh 概念开始出现,通信网络分为了控制平面和数据平面,Linked、Envoy、Istio 等产品登上历史舞台。并由第一代的 proxy per node(Linked v1 ) 模式快速进化到 Sidecar 形式,使得通信层离真实业务足够进,控制力度足够细,同时又做到了业务的完全无感知


扇贝 Service Mesh 的选型落地

1.png

扇贝早期的微服务架构采用的是传统的 Proxy 层方式,由 Nginx + Consul + Consul-Template 来实现。微服务启动时向 Consul 进行注册,同时提供健康检查入口。然后 Consul 会进行主动健康检测,Consul-Template 通过长链接到 Consul 监测服务变化,渲染 Nginx 的配置模板,在监测到发生变动时根据模板生成新的配置文件,发送 signal 热重启 Nginx。这种架构在服务变动较少时还是挺好用的。但是当我们需要蓝绿部署、灰度测试、流量镜像等高级一点的功能和更详细的统计指标时就比较难办了。

从早期的微服务模式演进到 Kubernetes 时,公司的技术方案是比较开放激进的,想要 Kubernetes 和 Service Mesh 同时推进。当时可选的方案有 Linked 和 Istio 两种,考虑到 Istio 背后的 Google 和社区繁荣程度,我们认为 Istio 会成为主流,所以在技术预研和测试的时候部署了 Istio,令人尴尬的是,当时 Istio 太早期了,版本还是 0 点几,我们发现服务非常不稳定,各种莫名其妙的 5xx 错误。但是我们又实在不想放弃这种符合未来趋势的服务通信层方式,所以我们做折中选择——只使用它的数据平面组件 Envoy。一方面我们获得的 Envoy 技术积累待 Istio 稳定后在切换过去也还是有用的,另一方面保留 Envoy 也使我们拥有了更先进的 Proxy 层,能够应对 Kubernetes 架构下服务频繁快速变化的状况。

之所以说 Envoy 是现代化的代理,是因为相比于比于传统的 Nginx 代理,Envoy 有原生的 GRPC/HTTP2(我们内部服务调用是基于 gRPC 的)支持,能够提供非常丰富的统计指标(比如基础的请求延时,重试次数等),以及我们后面会提到的能够运行时动态更新配置的 xDS 协议。

Envoy 在扇贝的部署架构

2.png

由上图可以看出,扇贝使用的并不是严格意义上 Service Mesh 所定义的 sidecar 模式,而是以daemonset 的形式部署的 Envoy,这是考虑到我们脱离了 Istio 控制平面,自己做 Sidecar 模式对技术团队来说难度过大。使用 DaemonSet 的部署形式降低了我们要管理的代理数量,能减少资源占用,升级 Envoy 也相对容易些。当然,缺点也是显而易见的,无法做到 Sidecar 那样细粒度的流量管控,好在目前我们还能接受这一点。

另外一个可以从图中看出的点是,我们的 Envoy 其实是分成两组的。一组承担了 API Gateway 的功能,代理集群南北流量。另一组则代理了集群内的东西流量。API Gateway 启用了 jwt 解析,ratelimit 等功能。内部服务代理则实现了服务鉴权功能

xDS 协议介绍

前面提到我们只保留了作为数据平面的 Envoy,没有控制平面的话看起来与传统的 Proxy 层也没有太大差别。这里就要提到注入灵魂的 xDS 协议了。xDS 是一组协议的统称,包括:
  • EDS: Endpoint Discovery Service – 节点发现服务
  • CDS: Cluster Discovery Service – 集群发现服务
  • RDS: Route Discovery Service – 路由规则发现服务
  • LDS: Listener Discovery Service – 监听器发现服务


等,这些协议是 Envoy 为实现动态配置而的定义接口。xDS 协议有v1,v2两个版本。v1 基于RESTful JSON API,在 1.8 版本之后已被彻底淘汰。v2 版本则基于 gRPC 和 protobuf3(也可以通过 rest api 访问)。我们以 eds 为例来详细看一下 xDS 协议。

eds 期望我们实现如下 gRPC 接口:
POST /envoy.api.v2.EndpointDiscoveryService/StreamEndpoints

message DiscoveryRequest {
string version_info = 1;
core.Node node = 2;
repeated string resource_names = 3;
string type_url = 4;
string response_nonce = 5;
google.rpc.Status error_detail = 6;


该接口应该返回类似下面的响应:
version_info: "0"
nonce: A
resources:
- "@type": type.googleapis.com/envoy.api.v2.ClusterLoadAssignment
cluster_name: some_service
endpoints:
- lb_endpoints:
- endpoint:
    address:
      socket_address:
        address: 127.0.0.2
        port_value: 1234

xDS 使用的是双向流式 gRPC,每个 xDS 流以 DiscoveryRequest 开始,通过 type_url 来区分请求的不同资源,具体到 eds 的类型是 type.googleapis.com/envoy.api.v2.ClusterLoadAssignment。version_info 和 nonce 表示的是上一次成功应用的 response 的版本号和 nonce 值。如果 Envoy 拒绝配置更新 X,则回复 error_detail 及前一个的版本号。简化过的 Request 和 Response 的顺序可以用下图来表示:
3.png

其中字母缩写的含义为:
  • Request: (V=version_info,R=resource_names,N=response_nonce,T=type_url)
  • Response: (V=version_info,R=resources,N=nonce,T=type_url)


基于 go-control-plane 实现xDS服务

上面简略的示意图只展示了最基础的情况,如果有多个 xDS Server 端(响应不一致)和和多种类型的 xDS 资源时(有依赖关系),正确的处理请求响应是一件很有挑战性的工作。而 go-control-plane 则帮我们处理好了这些通信细节,我们只需要实现资源的更新即可。go-control-plane 是使用 Go 语言开发 Service Mesh 控制平面的基础设施(Istio 也用它), 主要包含了 Configuration Cache 和 API Server两个组件。

API Server 是 Envoy 定义的 data-plane-api 的一个实现,可以直接用于生产环境。Configuration Cache 用于存储 Envoy 的配置信息。比如 endpoints、clusters 等,是我们实现自定义 xDS server 时主要操作的部分。

下面我们通过几段代码展示一下如何实现一个简单的 eds server。
4.png

这一部分首先实例化了一个 cache,这个 cache 的维护是要我们重点编码实现的地方。然后创建运行了一个 grcp server,我们向 server 注册了 eds 服务,当然我们也可以再注册 cds、rds等。
5.png

这段代码示例了如何操作 snapshot cache。我们通过 Kubernetes 提供的 endpoints watch api 去监听集群内 service endpoints 的变化事件(ADDED,MODIFIED,DELETED),根据事件类型和包含的 objects,具体到这个场景就是 Kubernetes endpoints,转换成符合 envoy 格式的 endpoints,生成新的 snapshot,更新缓存。这样一个基本可用的 eds server 搭建起来了。

Q&A

Q:为啥自己实现 xds 服务器?有现成的为啥不用?例如istio-pilot/rotor,istio-pilot可以单独使用的。
A:其实是早期遗留问题,一开始我们用的 Istio 不稳定,后续的选择就趋于保守了。当时 rotor 应该还没有 release 版本。

Q:如何保证 Envoy 对业务性能影响最小?
A:Envoy 本身性能没什么问题,要注意的是配置的 filter 这些跟外部服务交互的地方,比如 ratelimit 之类的,要配置好超时时间以及失败后的策略。

Q:你们的服务是基于 gRPC 还是 REST/http1.1 的?gRPC 要求至少 http2,如果需要把 gRPC 服务暴露给外部,对于 Ingress Controller 你有什么推荐?你们服务部署的是阿里云吗?Edge Proxy 用的阿里的服务?
A:我们对外的 rest,内部服务是 gRPC。Envoy 也是有做 Ingress Controller 的产品的,比如 Contour,不过我们没有实践过,谈不上推荐。Edge Proxy 用的是阿里云,最外面有一层阿里云的 slb。

Q:Istio 不好用,为什么不考虑 Sofa Mesh 来代替?
A:早期 0 点几版本的时候不稳定哈。现在不是特别在意大集群性能问题的话,Istio 是可用的。

Q:请问如何实现的灰度发布和蓝绿部署?
A:灰度发布也是基于配置不同 Endpoints 权重这个思路来的,也可以部署不同的 Deployment,基于 Pod 比例来做。蓝绿部署实践不多哈。

Q:你们服务 tracing 和流量监控是怎么做的呢?
A:traceing 目前还是原始的日志方式,在研究替代方案,后续有进展可以再分享一下。流量监控我们有做 Pod 的进出流量统计,也有从 Envoy metrics 获取的请求统计。

以上内容根据2019年11月12日晚微信群分享内容整理。 分享人李有才,扇贝基础服务工程师。负责公司 Kubernetes 和 Service Mesh 的推进、运维工作。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiese,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

0 个评论

要回复文章请先登录注册