写点什么

转:基于 Springcloud 的服务治理落地实践

作者:小江
  • 2022 年 6 月 13 日
  • 本文字数:5351 字

    阅读完需:约 18 分钟

本文转自 http://www.uml.org.cn/wfw/202112034.asp


前言


在微服务盛行的今天,提起服务治理,相信大家都已经不再陌生,许多公司都有自己内部的一套定制化的实现方案, Access 也不例外, 接下来, 我来为大家介绍一下我们的一套基于 Springcloud 的服务治理方案,在本文中,涉及的内容主要包括注册发现/健康检查/灰度发布/访问鉴权等。


演进



业务初期,我们所有的业务运行在一个 PHP 服务中,通过公有云的 ECS 虚拟机与 Nginx 来提供服务,这个并没有持续很久,因为马上我们的系统性能就已经跟不上业务的需求,但凡遇到电商大促等活动,系统都是随时宕机的状态,大规模的扩容也徒增了高额的运维成本。


2019 年底,我们开始了正式的重构,一方面将 PHP 项目重构为基于 SpringCloud 框架的 Springboot 应用,另一方面, 从 ECS 部署迁移到了灵活的 K8S 部署,在此基础上引入了 Consul 注册中心来实现微服务间的注册与发现。


2020 年中,我们进行了整体服务的云迁移,由于迁移过程的需要,我们去除了 Consul 注册中心,将注册发现机制下沉,由 K8S 的 Service 与 Ingress 来实现,我们发现, Consul 在这里确实是有点多余。


好景不长,到 2020 年底,这个架构已经产生了许多治理相关的痛点不能解决,最终,我们在此基础上,引入了全新自研的 Sun 服务治理平台,到目前为止,我们已经基于此平台实现了:实时的注册发现机制;可靠的访问鉴权机制;全链路灰度发布;精准的监控告警等等治理能力。


背景


在开始之前,请允许我再介绍一下咱们在服务治理之前的技术栈背景。


语言框架:我们经过 2 年多的重构改造,从开始的 PHP 转型为 Java 语言,并且所有服务都基于内部的 Springcloud 脚手架搭建,使用 Maven 进行依赖管理, 整体语言和规范统一度较高。


开发规范: 我们所有服务都继承内部定制的父 pom 文件,架构组定制化包装了诸多开源组件,如:spring-webmvc 等,也自定义了许多必选组件,如:consumer / provider 等,并且这些基础组件版本受父 pom 管理且版本号统一,这意味着基础架构组可以便捷的切入所有业务服务完成一系列的扩展和升级,对于业务服务来说,大部分时候只需要升级父 pom 版本即可完成一次升级。


部署运维:我们使用 CCE 云容器引擎进行部署运维, CCE 是基于 K8S 提供的企业级 Kubernetes 集群。


流量架构:我们的流量链路大体上分为两个部分, 一是公网请求流量链路, 二是内部调用流量链路。


对于公网流量,我们使用公网 ELB(LVS+Nginx)集群,将流量引入我们内部的 SpringcloudGateway 网关集群,内部 gateway 对流量进行过滤后路由至业务服务上。



对于内部流量,我们通过 K8s ingress,为每一个服务配置了一个专属的域名,服务间通过专属域名进行调用,意味着我们的注册发现/健康检查/负载均衡都是借助于 K8s 实现的。



痛点


那么,基于以上的技术方案和背景,我们遇到了哪些痛点呢?


发布不平滑

由于依赖 K8S 集群的注册发现与健康检查机制,虽然我们已经采取了滚动发布与容器就绪检查,但是容器在退出时并没有提前从服务列表中剔除该实例,ingress 后端服务器未及时更新,这导致发布期间还是会出现少量 502 请求异常,并且由于容器没有预热和延迟上线,所以新启动的容器 RT 非常高,在一些对 RT 比较敏感的核心链路中产生了较大影响。


不支持灰度发布

作为业务处在快速增长期的发展中公司来说,经常会有比较大的项目需要上线或改动,一次项目发布上线涉及二十个服务都是十分常见的,涉及的服务越多,代码改动越多,意味着发布存在的风险也就越大。这个时候,大家一定能想到一个词:灰度发布。可是基于我们目前的现状,流量统一走 ingress 路由进行负载,别说全链路灰度了,就连实现单个应用的灰发都是天方夜谭。


敏感接口无鉴权

在众多微服务中,不可避免会存在一些敏感的数据与服务,比如财务/用户信息相关的服务,因为在集群内部网络都是互通的,如果不加以鉴权的话,很容易造成的敏感数据泄露,这种事情,没发生的时候都不以为意,一旦发生才后悔莫及。


注册中心选型


针对以上问题,我们意识到,只有重新引入注册发现机制,这些问题才有机会解决。Consul 是我们最先考虑的,但是它的几个特性却让我们望而却步。



部署成本:由于高可用集群部署架构下,每一个服务实例容器下都需要运行一个 Consul Agent 守护进程,来监控客户端实例的状态,每一个 Agent 进程都需要额外占用运维资源,细算下来是一笔不小的成本。


有状态:Consul 服务节点间基于 Raft 协议集群部署,这意味着各个节点需要提前了解其他所有存在节点,它使得我们在对 Consul 进行扩缩容或发布迭代时显得不那么灵活。


非 Java:我们知道 Consul 是由 Go 语言开发而成,这与我们的语言栈多少有些不和谐,因为后期我们需要针对性的扩展一系列治理功能,以及集成我们的基础组件,我们知道 Golang 很牛,但还是算了吧~


Eureka 怎么样,我们选它了吗?我们没有~



健康检查:Eureka 采用的是定时心跳的健康检查机制,当服务端超过一定时间未收到心跳,则认为此客户端实例已经下线,基于这个机制,如果客户端实例意外宕机,在很长一段时间内,调用端仍然会去请求这个实例,导致线上稳定性被破坏。当大批量实例重启或发布时,这个问题也会被放大。


有状态:相对于 Consul 来说,Eureka 的集群部署模式简单明了,但它依然是有状态的,与 Consul 存在类似的痛点。


Nacos 是一个十分强大的开源项目,它实现了动态服务发现、服务配置、服务元数据及流量管理等多项能力,可以说它能满足目前几乎所有常见的治理需求,并且没有明显的缺点。



可是对于我们来说,它也不是一个很好的选择,正因为 Nacos 的强大,使得此项目变得很重,它包含了太多我们不需要的功能与代码,比如动态配置的能力,我们已经有了自己的配置中心,这部分代码就是多余的,而且我们需要的不仅仅是一个注册中心,而是基于注册中心的一整套服务治理解决方案,在 Nacos 上进行二开的成本不亚于自研一个轻量级的注册中心服务。


Sun 的诞生


经过多方选型,我们最终决定自研注册中心服务,它具备几个核心特性:无状态/高可用/宕机感知等,并基于此开发了一套集注册发现/健康检查/灰度发布/访问鉴权/监控告警等功能于一体的服务治理平台,我们给它起名为 Sun。


下面是 Sun 服务治理平台的基础架构:



在这个架构中,主要存在三个模块:客户端(sdk) / 服务端(server) / 管理端(portal)


客户端 SDK 以组件的方式运行在每一个业务服务中。

它主要负责与服务端进行通信(Websocket),完成自身的注册,并接收服务端的列表下发和策略下发。

其次,它扩展了 Feign 与 RestTemplate,并在请求头中添加了自身信息作为来源应用,这样服务提供方收到请求时就可以清楚的知道调用方是谁了。


服务端的主要职责是管理好连接到自己节点上的客户端实例,接受它们的注册,完成对它们的健康检查,并将他们所订阅的服务列表同步给客户端,同时还会将管理端配置的一些流量策略同步给客户端,下面的图片展示了服务端内部的工作原理。



图中客户端表示我们的业务服务,每一个业务服务实例与一个 sun 节点建立 WebSocket 长链接。


客户端发送注册消息(消息中携带自己的 group 分组与 version 版本号),服务端会将消息解析后转发到特定的 Action 处理器更新数据中心,并新增一个健康检查任务,对此实例进行定期的健康检查。


当数据中心产生数据变更时,会触发相应的 Listener,将变更信息下发给其他的客户端实例,当然,如果部分客户端未订阅变更的服务,那么也不需要同步给它们。


服务端是支持单机与集群两种部署方式,在集群模式下需要额外配置外部数据源(ZK)来协助服务端实现集群间的数据同步工作,而在单机模式下,可以不需要配置外部数据源。


除了注册与下发服务列表以外,服务端的数据中心中还存储着流量策略信息,这些流量策略由管理端(sun-portal)进行维护, 并通过数据中心的 Listener 机制将变更下发给每一个客户端。


这里的流量策略是什么呢?


简单的说就是一个路由规则,告诉客户端在哪些情况下选择哪些服务实例进行调用。比如: 在 header 中存在 version:2.0 的情况下,选择服务列表中 version 版本号为 2.0 的实例。


zookeeper 数据源中的结构如下:



/sunNodes 节点负责管理所有的注册中心节点,这可以使得注册中心集群中的节点可以互相感知对方的状态,我们可以利用这个信息来实现注册中心长链接的负载均衡,以及部分节点宕机后的实时感知与处理。


/clients 节点就是负责存储我们的客户端实例数据了,可以看到客户端实例被划分到了不同的注册中心节点(sunId)下面,并且在这套注册中心体系中,不存在消费者与提供者的概念,人人都是消费者,人人皆可提供者。


/config 节点负责存储我们的灰度流量策略,以及服务鉴权相关的配置。


问题解决


平台基本背景介绍完了,下面我们来看下基于这个平台,如何真正的解决上面的几个痛点。


发布不平滑

实现平滑发布需要实现两大要素:平滑下线/平滑上线。



实现平滑下线相对是比较简单粗暴的,只需要基于 Spring 的 ContextClosedEvent 事件,在 Spring 容器退出前,向 Sun 服务端发送注销消息,由于我们设计的注册中心模块是基于 WebSocket 长链接的主动推送机制的,所以实例下线的消息几乎可以在瞬间就可以同步到所有订阅方,并且客户端在发送注销消息后,会执行一个 sleep 操作,以保证退出之前,所有订阅方都已经感知,且进行中的请求完成响应。


上面的平滑下线是有限制的,对于未能正常发送注销消息的实例,调用方的感知是有延迟的 (由于我们的注册中心采用的是长链接的方案,所以即时没有收到注销消息,当实例与服务端断开连接时,服务端仍然能感知并将状态变更通知到所有调用方,但是会有延迟),所以我们利用 K8s 的停止前处理能力,在容器退出调用脚本主动告知注册中心此节点已下线。


平滑下线关键词:延迟退出/断开感知/停止前禁用。


关于平滑上线,在客户端实例注册到注册中心后,不会立即提供服务(不管注册时客户端状态是否健康,起始状态都被标记为不健康状态,经过一轮健康检查后,方可更新为健康状态), 这可以防止服务在未就绪时接受请求。


其次,服务在启动完成时,如果直接接受请求,那么这些请求的 RT 会明显增高,这个是因为服务启动时,线程池/连接池/对象池/本地缓存等资源都还未加载,需要花费大量 CPU 和时间同步去加载这些资源,针对这个问题,客户端 SDK 中在服务 SpringReady 事件中主动预热了多个内部资源(比如 RibbonContext 等),同时提供了自定义预热注解,业务服务只需要在任意方法上添加注解,即可在启动时完成对注解方法的预热,至此可以将服务启动首次请求 RT 从 3s~5s 降低至 300ms。


平滑上线关键词:延迟上线/启动预热


做到了平滑下线+平滑上线,从流量层面与性能方面算是实现了发布的平滑,但是如果新发布的代码存在 bug,依然会导致发布问题,对用户产生不好的体验,接下来我们继续使用灰度发布来解决这个问题。


不支持灰度发布

实现灰度发布的前提是服务需要有版本的概念,并且需要获得对流量完全的控制。


前面提到,我们每个客户端都有一个 group 与 version 属性(默认值为:default 与 -1,可以通过启动参数等方式指定),同时,group 与 version 信息会在注册时同步到注册中心,随后下发到调用方,这样一来,调用方就可以区分这些服务实例了。


接下来,服务端需要给这些调用方下发一个流量策略,让其知道在什么情况下调用哪些实例,期望的效果是,圈定一批用户群体,或者圈定一定比例的用户,让这些用户来访问特定的灰度服务。


具体方案如下:



首先我们在标签服务中创建一个灰度标签,并给这个标签下添加一批用户(意味着这些用户拥有这个灰度标签),我们发布一个新的服务实例,添加启动参数来指定此实例的 group 与 version,随后我们在管理端配置流量策略,策略的规则是:当 header 中包含 User-tag:grey 时,优先进入灰度的 group 服务(v2)。


APP 在用户登入或跳转页面时,会从服务端拉取此用户所携带的标签,并在所有请求头中添加此标签信息,如:User-tag: grey。


请求到达 Gateway 网关,客户端 SDK 会发现请求头匹配了流量策略的规则,遂将请求转发到了灰度的服务 S1(v2),同样的,S1 服务收到请求后,客户端 SDK 会在其调用 S2 之前扩展请求头,将 User-tag 信息添加到 RPC 请求头中,随后,客户端 SDK 根据当前请求头又将请求转发到了灰度的 S2(v2)。


至此,灰度发布就实现了,事实上其中还有许多细节,比如:如何保证多线程环境下 header 信息不丢?如何保证更改灰度用户群体时前后端能同步等等?


敏感接口无鉴权

针对敏感接口,我们设计了一套基于注解与动态配置的鉴权方案,它通过为接口或接口类添加注解来定义所属资源,并通过动态配置来指定这些资源允许被哪些服务所调用,完成以上配置与定义相对是比较简单的,接下来的问题是当 SDK 拦截到 RPC 请求后,如何知道来源应用是谁?



为了防止恶意修改请求头中的来源应用信息,我们在请求中引入了加密 Token 机制,客户端在首次启动注册时从服务端获取专属 Token(Token 由应用名与时间信息加密而成,只能被服务端加密或解密),并定期从服务端更新 Token,在发起 RPC 请求时,在请求头中添加 Token 信息,服务端收到请求后可借助服务端来进行 Token 解析,根据应用名与有效期来进行校验,如果 Token 解析失败或是超过有效期,亦或是应用名不在接口访问白名单中,则拒绝访问,否则正常处理请求。


结语


至此,我们通过自研的服务治理平台解决了三个痛点,当然,这不是全部,我们还可以基于此来实现更多好玩有用的功能,比如: AB 测试等。此外,我们还引入了 javaagent 技术来实现对服务的监控/告警/全链路追踪/无感 dump 等功能,下次再来给大家分享吧,拜拜~

用户头像

小江

关注

~做一个安静的码男子~ 2019.02.11 加入

软件工程师,目前在电商公司做研发效能平台,中间件维护开发相关工作

评论

发布
暂无评论
转:基于Springcloud的服务治理落地实践_Spring Cloud_小江_InfoQ写作社区