This is the blog section. It has two categories: News and Releases.
Files in these directories will be listed in reverse chronological order.
This is the blog section. It has two categories: News and Releases.
Files in these directories will be listed in reverse chronological order.
– 转载自掘金
PolarisMesh 官网:PolarisMesh
PolarisMesh Github:polarismesh/polaris
polaris-server 作为PolarisMesh的控制面,该进程主要负责服务数据、配置数据、治理规则的管理以及下发至北极星SDK以及实现了xDS的客户端。
polaris-server 是如何处理客户端的服务实例的心跳请求的呢?心跳数据是怎么存储的呢?带着这个疑问,我们来探究看下polaris-server的健康检查模块,看看北极星是实现的。
从上一篇文章我们看到,客户端在注册实例之后,会维护实例的心跳上报任务
public InstanceRegisterResponse registerInstance(InstanceRegisterRequest request, RegisterFunction registerFunction,
HeartbeatFunction heartbeatFunction) {
// 将注册请求发送至北极星服务端
InstanceRegisterResponse instanceRegisterResponse = registerFunction.doRegister(request,
createRegisterV2Header());
// 当前实例的注册管理状态进行本地保存
RegisterState registerState = RegisterStateManager.putRegisterState(sdkContext, request);
if (registerState != null) {
// 首次存入实例的注册状态时,为该实例注册创建定期心跳上报动作任务
registerState.setTaskFuture(asyncRegisterExecutor.scheduleWithFixedDelay(
() -> doRunHeartbeat(registerState, registerFunction, heartbeatFunction), request.getTtl(),
request.getTtl(), TimeUnit.SECONDS));
}
return instanceRegisterResponse;
}
客户端会按照用户实际设置的 TTL 间隔进行心跳定期上报,而客户端和服务端的心跳上报通信协议基于 gRPC,具体的定义信息如下:
service PolarisGRPC {
...
// 被调方上报心跳
rpc Heartbeat(Instance) returns (Response) {}
}
那么当服务端收到心跳后,是如何处理心跳数据,以及如何根据 TTL 进行实例的健康状态检查的呢?这里我们先通过一个简单的数据图来对北极星服务端对于服务实例心跳包的处理有一个大致的理解后,我们在逐步针对每个细节进行分析。
从流程图可知,客户端发起服务实例心跳包到北极星服务端后,先后经过 apiserver -> resource auth filter -> service -> healthcheck -> checker plugin。这里来分析下每个部份它们的职能:
如果细心察看流程图的读者会发现,healthcheck 内部还有一层缓存,这层缓存的数据来源于北极星的 cache 模块,并且该缓存只会保存开启了健康检查的实例信息,为什么需要这一层缓存呢?由于北极星的服务实例健康检查开关可根据用户实际需要选择开启或关闭,只有开启了健康检查的引擎实例才能上报心跳数据,如果没有开启健康检查的实例,上报心跳数据是一个无效数据,如果对这类的数据不加以拦截,那么会导致 checker plugin 内部保存了无效的心跳包数据,同时也会在有限的服务端资源内,和正常的心跳数据请求争抢服务端资源。因此这里做了一个心跳请求的判断处理。
当服务端将客户端的实例心跳数据请求处理之后,接下来就是如何检查服务实例的健康状态是否处于正常状态、心跳包是否在 3TTL 内正常上报,北极星集群间各节点如何协同工作,一起完成服务实例的健康状态检查。这些都在 service 下的 healthcheck 模块完成。
这里先来看看 healthcheck 在 polaris-server.yaml 中的相关配置
healthcheck:
# 是否开启该北极星节点的健康检查组件
open: true
# 北极星健康检查集群的服务名
service: polaris.checker
# 设置时间轮参数
slotNum: 30
# 设置最小的实例健康检查周期
minCheckInterval: 1s
# 设置最大的实例健康检查周期
maxCheckInterval: 30s
# 由于一个北极星 SDK 实例会将当前 SDK 实例的相关信息上报到北极星服务段,因此这里也需要做一个额外检查
clientReportInterval: 120s
batch:
# 心跳数据批量修改的控制器
heartbeat:
open: true
queueSize: 10240
waitTime: 32ms
maxBatchCount: 32
concurrency: 64
# 健康检查的类型插件
checkers:
# 单机场景,基于 map 的心跳检查插件
- name: heartbeatMemory
# 集群场景,基于 DB 选主的分布式心跳检查插件
- name: heartbeatLeader
这里来分析下 healthcheck 中每个部份的职能
接着再通过一个时序图来看看 healthcheck 是如何完成上述相关任务的
通过 Dispatcher 组件的一致性 hash 环对所有开启了健康检查服务实例进行责任节点分发,使得北极星的健康检查可以进行很好的水平扩展,同时将具体的健康检查执行判断以及健康检查数据CURD操作独立成插件
在北极星开源服务治理平台中,用户可以通过前端WEB界面来进行服务管理、流量管理、配置管理、故障容错和可观测性查看,本篇Blog主要从源码分析的角度,来看polaris的前端功能如何与后端进行交互和调用。
在 polaris 部署中,主要包括下面4个组件:
本篇Blog,重点分析 polaris-console组件与 polaris-server 组件的交互和调用, 整体交互流程图如下:
polaris-console 中主要包括2部分源码内容:
在 polaris-console工程中, 并没有使用传统的 React前端 + Nginx反向代理 这样的方案, 而是通过 Gin + Golang的 httputil ReverseProxy实现了一个简单的反向代理, 这样带来的好处是能够通过golang编码的方式,来实现很多权限校验,请求重定向, 请求/响应校验 等功能。
在polaris-console中,主要实现了4中类型的反向代理:
类型一 ReverseProxyForLogin
: 主要 为登录接口提供反向代理,登录成功后,会将反参中的 UserID
, Token
作为 claims生成Jwt Token, 并将这个Jwt Token作为Cookie 写入Http Response中。
func ReverseProxyForLogin(polarisServer *bootstrap.PolarisServer, conf *bootstrap.Config) gin.HandlerFunc {
return func(c *gin.Context) {
c.Request.Header.Add("Polaris-Token", polarisServer.PolarisToken)
c.Request.Header.Del("Cookie")
director := func(req *http.Request) {
// 请求入参处理
}
modifyResp := func(resp *http.Response) error {
// 响应反参处理 与 Cookie刷新
}
proxy := &httputil.ReverseProxy{Director: director, ModifyResponse: modifyResp}
proxy.ServeHTTP(c.Writer, c.Request)
}
}
类型二 ReverseProxyForServer
: 主要 为大部分需要鉴权的接口提供反向代理功能,一般是登录成功后做其他操作时,走该反向代理。 该反向代理 首先从 Cookie中解析出 Jwt Claims, 从Claims中得到 UserID
与Token
属性, 然后通过调用 polaris-server的 /user/token
接口,比对接口返回的 AuthToken
是否与Jwt中的一致 ,如果一致, 则校验通过, 反向代理将请求代理到 polaris-server的接口, 并且刷新 Cookie 中Jwt的 Expire时间。
func ReverseProxyForServer(polarisServer *bootstrap.PolarisServer, conf *bootstrap.Config) gin.HandlerFunc {
return func(c *gin.Context) {
if !verifyAccessPermission(c, conf) { // 权限校验方法
return
}
c.Request.Header.Add("Polaris-Token", polarisServer.PolarisToken)
c.Request.Header.Del("Cookie")
director := func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = polarisServer.Address
req.Host = polarisServer.Address
}
proxy := &httputil.ReverseProxy{Director: director}
proxy.ServeHTTP(c.Writer, c.Request)
}
}
类型三 ReverseProxyNoAuthForServer
: 不需要鉴权的反向代理, 主要提供给 /apidocs.json
Swagger描述使用。
func ReverseProxyNoAuthForServer(polarisServer *bootstrap.PolarisServer, conf *bootstrap.Config) gin.HandlerFunc {
return func(c *gin.Context) {
c.Request.Header.Add("Polaris-Token", polarisServer.PolarisToken)
c.Request.Header.Del("Cookie")
director := func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = polarisServer.Address
req.Host = polarisServer.Address
}
proxy := &httputil.ReverseProxy{Director: director}
proxy.ServeHTTP(c.Writer, c.Request)
}
}
类型四 ReverseProxyForMonitorServer
: 代理到 Prometheus的地址。
func ReverseProxyForMonitorServer(monitorServer *bootstrap.MonitorServer) gin.HandlerFunc {
return func(c *gin.Context) {
director := func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = monitorServer.Address // Prometheus地址
req.Host = monitorServer.Address
}
proxy := &httputil.ReverseProxy{Director: director}
proxy.ServeHTTP(c.Writer, c.Request)
}
}
polaris-server 在启动时, 会启动一个 httpserver
, 其内部使用 go-restful
框架来处理前端的 Restful 请求 。
httpserver
在 go-restful 的Container 级别增加了全局 Filter, 包括 preFilter 与 postFilter 。
此处需要说明, 在 go-restful 的preFilter中,并没有对权限进行校验。 真正的权限校验放在了服务层。
polaris-server 收到 Restful 请求后, 主要由如下几部分组件处理:
auth
, config
, namespace
, service
等逻辑处理接口及实现。主要包括:
namespace
, service
, instance
, routing
, ratelimit
, circuitbreaker
等主要的功能接口。configfile
配置文件管理接口。通过上面的分析, 梳理了polaris-console
前端组件通过 Restful 调用后台 polaris-server
组件的逻辑,可以帮助大家更好的理解 polaris 的前后端模块的交互过程。
北极星是腾讯开源的一款服务治理平台,用来解决分布式和微服务架构中的服务管理、流量管理、配置管理、故障容错和可观测性问题。 在分布式和微服务架构的治理领域,目前国内比较流行的还包括 Spring Cloud,Apache Dubbo 等。在 Kubernetes 的技术领域,也有以 Istio 为代表的 ServiceMesh 技术。 本篇 Blog 主要分析北极星的优势,及其服务注册发现的技术实现。
要做好一个服务治理框架, 核心功能和要素至少包括以下几点:
从功能实现方面来看, 不管是 SpringCloud,Apache Dubbo, Istio 还是北极星,基本都实现了这些功能,那它们的实现思路有什么不同呢?
SpringCloud 是完全架构在 SpringBoot 基础上的,是 SpringBoot 开发框架很自然的延申,因此继承了 SpringBoot 框架所有的优点,可以非常灵活的集成各类服务注册发现、服务治理、可观测组件。 例如 SpringCloud 框架自身开发了 Spring-Cloud-Config和 Spring-Cloud-Sleuth 组件,分别提供了配置和部分可观测性的能力;在其他能力方面,早期版本主要由 Netflix 提供的 Eureka, Ribbon, Hystrix等组件提供了服务注册发现,服务治理的能力; 随着Netflix很多组件生命周期结束, SpringCloud 又通过自定义的 Abstract LoadBalance/Route 接口及实现, 以及集成的Resilience4J/Sentinel 等熔断限流组件,继续为用户提供统一的服务注册发现,服务治理等方面的能力。
SpringCloud 在实际使用过程中, 可能会给人一种没有一个统一的服务治理模型的错觉, 这是因为 SpringCloud 保持了自身的胶水框架的特性和思路, 可以集成和融合各类治理组件,例如熔断限流就提供了 Hystrix、Resilience4J、Sentinel 等方式。这样的框架特性,优势是灵活,可以融合各类框架, 劣势是抽象统一的治理模型比较困难, 因此 SpringCloud 并没有提供一个统一的服务治理控制面,即使是 Spring-Cloud-Admin 的扩展,也更多是基于 SpringBoot Actuator 提供可观测的管理,并没有提供服务治理的控制面能力。
ServiceMesh 解耦了业务逻辑和服务治理逻辑, 它将服务治理能力下沉到基础设施,在服务的消费者和提供者两侧以独立进程的方式部署。这种方式提升了整体架构的灵活性,减少对业务的侵入性并很好的解决了多语言支持的复杂性。劣势是一方面服务通过sidecar的调用多了一道 iptables/ipvs 的转发,降低了一些性能,Sidecar 也增加了少量的资源使用; 另一方面是中小团队很难对框架灵活扩展,虽然 Envoy 提供了 WASM 机制,其自身也能通过 C++ 扩展 Filter,但是不管那种方式,都需要团队有一定经验的人员来完成,中小团队很难提供这样的人员保障。
另外,Istio 虽然也能提供基于虚拟机/物理机的部署, 但是本身还是基于Kubernetes 设计的,也对 Kubernetes 部署最友好,对于未使用 Kubernetes 的团队有一定的挑战。
PolarisMesh 从个人的角度看,是融合和兼容了很多技术的解决方案。
一方面,它可以看作是 SpringCloud 服务治理实践的一种自顶向下的正向思考过程, SpringCloud 是自底向上的一种构建思路,它提供了各类服务发现、服务治理、可观测组件的集成和融合,但是并没有提供统一的顶层治理模型(或者仅提供了一部分); 而 Polaris是先基于 下一代架构基金会所制定的 服务治理标准, 制定和扩展了服务治理的模型, 然后基于该模型,分别构建了服务治理的控制面和数据面(各类语言的SDK、开发框架、JavaAgent、Kubernetes Controller)。 当然,基于该模型,也能很好的对接到 ServiceMesh 的治理方式, 这样就给未来的发展也留足了空间。
另一方面,PolarisMesh 也通过 插件机制, 为框架扩展预留了空间,如果当前的开源Polaris不满足你的需求,可以较灵活的进行扩展。
本篇 Blog 重点对 Polaris-Go SDK的服务的注册和发现做下技术分析, 以及源码阅读。 主要是服务注册和发现是各类服务治理框架最基础和核心的功能,因此先从它开始吧~
在客户端 SDK, 不论是 服务注册的 API, 还是服务发现的 SDK,其内部都是封装了 SDKContext 的上下文, SDKContext 内部构成如下图所示:
ConfigurationImpl: 主要是 客户端 polaris.yaml
配置文件的映射, 分为4个部分,分别是 GlobalConfig, ConsumerConfig, ProviderConfig 和 ConfigFileConfig 。
ConnectionManager: gRPC 连接的连接池管理接口。
plugin.Manager: 插件管理接口,SDK 内部实现的各类功能- 熔断,限流,配置,健康检查,路由,负载均衡等都是按照插件的方式实现, 插件需要实现 Plugin 接口,通过 PluginProxy 包装后交给 plugin.Manager 管理。
Engine: SDK 执行的各类动作,都交由 Engine 处理, 例如 服务的注册发现, 限流,路由,熔断等, 都是调用 Engine 内的 API 实现。 也就是SDK能执行的功能,都是由 Engine API 统一实现。
服务注册的粗略流程如下图所示 :
go客户端SDK的整体服务注册流程比较线性, 没有涉及特别复杂的逻辑, 相关 gRPC service 如下:
service PolarisGRPC {
// 客户端上报
rpc ReportClient(Client) returns (Response) {}
// 被调方注册服务实例
rpc RegisterInstance(Instance) returns (Response) {}
// 被调方反注册服务实例
rpc DeregisterInstance(Instance) returns (Response) {}
// 统一发现接口
rpc Discover(stream DiscoverRequest) returns (stream DiscoverResponse) {}
// 被调方上报心跳
rpc Heartbeat(Instance) returns (Response) {}
// 上报服务契约
rpc ReportServiceContract(ServiceContract) returns (Response) {}
}
服务发现流程相对于服务注册流程要复杂很多, 主要原因是 北极星的服务发现 会涉及到 本地Cache 与 远程server端 信息的懒加载同步,同时加载的服务信息也比较复杂,包括实例信息,服务信息,路由信息,限流信息等内容。
服务发现的粗略流程如下图所示 :
可以看到,服务发现中的关键点包括:
go SDK 服务发现的 gRPC 接口如下:
// DiscoverClient 服务发现客户端接口
type DiscoverClient interface {
// Send 发送服务发现请求
Send(*apiservice.DiscoverRequest) error
// Recv 接收服务发现应答
Recv() (*apiservice.DiscoverResponse, error)
// CloseSend 发送EOF
CloseSend() error
}
上面的技术分析因为时间有限,难免有错误和遗漏,欢迎大家指正, 也特别感谢社区 @chuntaojun 的帮助。 北极星通过对 服务治理标准 的实现,提供了完善的服务发现和治理方案。 同时, SDK客户端 与server服务端 的数据同步与交互,也有设计良好的服务治理模型和健壮的通信机制提供了可靠的保障。 此外,通过插件机制,SDK框架 也提供了灵活扩展的能力 。
蓝色光标是一家营销领域的科技公司,其业务涵盖数字广告投放平台,SaaS服务,以及基于营销科技的智慧经营服务等,2022年公司营业收入超过了366亿人民币,服务了约3,000个国内外品牌客户。由于营销领域的客户需求多种多样,这也决定了公司的产品形态并非大一统,不同的业务战线具有非常显著的个体差异性,同时也对技术架构提出了不同的要求,其中典型的场景如下:
不同的业务场景,对技术架构的要求也不同,而各个团队的选择也往往是先快速支持业务,因此随着时间推移,难免会出现技术栈不统一,各自造轮子的情况。在实施服务治理前,我们遇到的一些问题如下:
北极星虽然提供了完善的服务治理能力,但是也并不是立即就可以在我们业务落地,也需要看业务是否已经准备好了。在实施服务治理前,我们需要首先解决以下前置问题:
综上所述,我们首先对现有的服务构建和发布流程做了一次全面的重构,并通过重新设计的发布平台整合了VM/K8s部署的全流程。在此基础上,利用北极星实现了内部的服务治理能力。
由于各个业务的异构性,对于单个业务的具体痛点的改造往往收效不大,且不具备普适性。因此,我们在整个架构变革的初期,选定了以发布平台作为全流程的切入点,原因如下:
蓝色光标内部的统一服务发布平台取名是Atlantis,后续我们将用atlantis来代替发布平台,我们在atlantis上实现了以下核心能力:
通常在技术团队内,都会定义一套完整的持续发布流程,通过全自动的构建,测试,发布流程,提升研发效能。这套流程实施的方法论不属于本文的探讨重点,本节主要描述这套持续集成流程与发布系统的是如何整合的。 首先,先简要描述下我们对构建流程的几个目标:
为了达到以上目标,我们在发布系统里增加了"构建管理"的功能,管理服务的所有构建相关配置。
通过将构建和服务部署信息整合到一个系统,并且在构建时实时下发模板,我们就可以轻松的实现类似以下的功能:
当部署环境有变化时,只需要调整发布平台上的配置,无需修改构建脚本,能够自动同步到构建流程中。
业务在往K8s迁移的过程是一个持续的过程,通常伴随着新老系统并存的问题,因此我们重点解决了以下几个问题:
架构图如下所示:
如上文所描述,北极星对异构系统的兼容能够很好的匹配蓝色光标的业务形态。我们将北极星作为服务治理的底座,通过和atlantis的结合,快速构建起了同时支持测试和生产环境的服务治理平台。
由于我们的业务是跨地域,多集群部署,且不同地域之间的网络通常是不互通(针对需要跨地域互调的场景,单独走专线),因此,我们在每个地域(或者说,每个局域网内部)都部署了一套北极星组件,用于集群内部的服务发现。
无论是部署在哪个集群,北极星server对外暴露的api地址都为同一个,通过DNS配置,实现每个集群的server只访问本集群的北极星server。这样可以简化业务服务的配置,无需根据部署地域,配置不同的北极星地址;实际部署架构图如下所示:
分布式部署北极星能提高集群内的服务发现稳定性和效率,但是如果服务存在跨多集群部署的情况,由于每个集群的北极星server都有一个管理console,如果让用户通过直接访问console进行流量的管理,会需要登录多个console,成本高且容易出错。并且我们也希望能基于服务的视角,统一管理生产,预发布,测试环境的流量。对这个问题的解决方案是这样的:
我们将发布平台atlantis对接所有已部署的北极星(这些北极星都部署在不同的地域,集群里),将常用的流量管理功能嵌入atlantis,以服务的视角,支持跨集群管理流量,如下图所示:
具体流程如下:
如下图所示,同一个服务即使部署在多集群,多环境,也能够通过atlantis统一管理流量:
北极星提供了多种语言的sdk,且对服务框架没有严格限制,可以让业务服务快速接入。为了更好的适配现有业务,我们将自身的服务定义和北极星服务定义进行了融合,并在北极星sdk基础上做了一层封装。
实际业务场景里,一个服务有可能会监听多个端口,每个端口提供不同的服务。例如,一个passport_server在8080端口提供了业务服务rpc_service,同时在8081端口提供了管理控制功能control_service。对于这种情况,我们在北极星会注册两个服务,名称分别为: passport_server.rpc_service, passport_server.control_service。主调需要通过完整的servername进行服务寻址,即类似passport_server.rpc_service这样的格式。
为了实现环境隔离,标签选址等功能,服务在往北极星注册,以及查询被调服务地址时,通常需要带上自己的部署信息,例如环境类型,集群名称等。关于这一点,我们是通过atlantis发布平台来实现。如下图所示,atlantis在服务发布时,根据所发布的环境,注入不同的环境变量。服务在调用北极星api时,自动携带所注入的环境变量,从而实现环境隔离,标签路由等功能。
在我们使用北极星作为服务发现平台后,通常情况下,对被调服务的寻址和路由都是动态的,即根据被调实例的可用性和权重进行自动的选择,这些选择往往基于预先设定的路由规则,主调一般不会hard code被调的地址。但是在一些特殊场景下,我们有可能需要override北极星的功能,直接指定被调的地址。例如,服务开发中,我们希望直接指定某个下游服务的地址进行请求,或者我们希望单独测试某个被调节点的性能时。
为了实现此需求,我们对业务服务框架进行了扩展,使之同时支持自定义寻址和北极星两种方式,并且自定义寻址的优先级要高于北极星寻址,服务的配置如下所示:
[polaris]
token_server_addr=bfo_scrm_token_server.token_thrift_service
material_server_Addr=bfo_scrm_material_server.material_thrift_service@10.0.0.215:8080;10.0.0.214:8000
我们通过占位符"@“标明是否使用自定义配置,配置格式为:IP:PORT;IP:PORT当存在自定义配置时,将根据配置文件中明确定义的地址进行请求,如果配置了多个,则进行负载均衡。通过这样的设计,使得业务代码本身不需要做任何修改就可以快速响应流量定向请求的需求。 流量定向请求的功能主要应用场景还是在各类联调和测试中,线上稳定运行的服务通常不会开启。
为了更好适配我们对北极星的使用姿势,减少业务重复代码,我们在北极星sdk基础上做了一层封装,主要增加了以下功能:
测试环境的管理是一个老生常谈的问题,简言之,测试环境治理的核心目标是:减少测试环境维护成本,环境之间按需隔离/互通,使用门槛足够低。当我们把测试环境治理的需求抽象成一个最小集,即可得到如下的示意图:
对以上场景的描述为:
我们基于北极星的测试环境管理基于以下5个点:
整体调用流程示例如下:
以上是蓝色光标基于北极星的服务治理实践,截止目前,我们主要利用的是北极星服务发现和标签路由的功能。通过使用北极星,很好的解决了我们业务跨多集群, 混合架构部署的问题,使得我们可以加快将业务服务从虚拟机往K8s迁移的过程。当然,我们目前只是迈出了第一步,后续我们计划和北极星做更深入的整合,将灰度发布,全链路追踪等能力也集成到现有平台。
部门早在2020年便积极响应公司自研上云的号召,借用云原生的思想,对老系统进行了重构,使用 Spring Cloud 框架将原来的单体服务拆分为一个个业务微服务,并对这些微服务进行容器化多实例部署,使得整个系统的弹性伸缩能力、容错能力得到了增强,能够面对更加复杂多样化的业务场景。现如今,部门拆分的微服务数量已高达200多个,这些微服务各司其职,共同为部门的业务保驾护航。
在系统重构之初,部门的业务场景还比较简单。对于服务治理的要求,仅仅只需要微服务能够自动注册并互相发现即可,对于流量管理、故障容错等没有太大的要求,因此,选择了当时较为成熟稳定的 Consul 作为微服务的服务注册中心。但是后来随着业务规模的不断扩大,服务间的调用关系也越来越复杂,接入的用户数量越来越多,对系统的稳定性要求也越来越高。Consul 作为服务注册中心已经不能够满足实际的运营要求,给开发运维工作带来了巨大的挑战。
由于 Consul 不支持流量管理、故障容错等功能,因此当需要进行服务治理时,会引入相关的 Spring Cloud 治理组件,比如 Resilience4j、Ribbion 等等,相关组件的治理规则会配置在本地 Application.yml 文件中,但是可能各个服务引入的治理组件不一样,配置的规则也不一样,那么在 Application.yml 展示的内容就各不相同;如果服务负责研发同学调整,就有可能需要重新理解相关的服务治理规则配置,增加维护的理解成本。
由于部门负责的业务非常关键,所以,一旦发生代码变更,除了需要在测试环境进行充分验证外,还需要进行现网灰度。现网灰度如果只按照实例节点灰度的话,意味着现网所有业务的流量均可流到这个变更的代码上,一旦变更的代码有点小 Bug,很可能会对业务的服务器误操作,从而造成生产事故,因此,部门此前都是通过增加额外的代码逻辑来实现按业务灰度流量的,但这样的操作不但费时还耗人力。
为了保障现网服务的稳定性,部门正在考虑对一些关键服务进行多区部署。多区部署虽然能够避免因为一个可用区故障而导致整个服务宕机情况的发生,但同时也带来一些问题,多区部署的服务,在进行服务间调用时,很容易因为两个服务不在同一个可用区而导致调用时间增加从而影响整个链路的性能,严重情况下,可能会导致请求超时。另外,也会增加跨区访问的网络成本。因此,在多区部署的情况下,流量的就近路由就很有必要。
Consul 仅提供服务注册发现的能力,缺乏针对业务流量的治理,因此部门希望可以引入服务治理中心组件,并且可以针对不同语言、不同微服务开发框架都能够有一样的服务治理体验;同时希望引入服务治理组件后不会增加太多的运维成本。经过对业内流行的几款开源组件调研后,最终选择了北极星(Polaris Mesh)。选择北极星(Polaris Mesh)主要看中其一站式的服务治理中心设计:包括服务管理、流量管理、故障容错、配置管理、可观测性;仅需一个组件就可以获得微服务场景下所需的全部服务治理能力,减少对于服务治理规则理解成本;同时北极星支持无侵入式的 JavaAgent 以及 Sidecar,也便于后续进行 Mesh 化的改造。
结合团队原本的微服务开发框架选型,Spring Cloud Tencent 成为了部门的首要选择,并且 Spring Cloud Tencent 将北极星的服务治理能力,和 Spring Cloud 的扩展点进行了很好的整合,只需要引入对应的服务治理能力 Starter,就可以启用对应的服务治理能力。
为了实现系统新上线的功能能够按照不同业务进行灰度,可以在北极星控制台中配置服务路由规则,让不同的业务请求能够路由到指定的实例节点上。比如 Callee 服务有新功能上线,则需配置如下图所示的路由规则。
该路由规则表示,对于所有访问 Callee 的服务请求,只要请求头中带有 x-client-id=businessA 的请求会优先路由到带有标签 Verison=v2 的 Callee 服务的实例中。
该路由规则表示,对于所有访问 Callee 的服务请求,只要请求头中没有带 x-client-id=businessA 的请求会优先路由到带有标签 Verison=v1 的 Callee 服务的实例中。
配置好了上述路由规则后,通过北极星 SDK 提供的服务打标功能,给新版本的 Callee 服务打上 Version=v2 的标签。这样,在灰度 Callee 服务节点时,灰度的服务节点会带上 Version=v2 的标签,而没有灰度的节点则还是之前的老标签 Version=v1。至此,按不同业务进行流量灰度的功能便得以实现。
为了解决服务就近路由的问题,北极星 SDK 通过提供环境变量、接口重写等方式让服务实例能够轻松地上报自身的地域信息到北极星服务端,方便其它服务实例能够获取。在进行服务间调用时,主调服务会获取被调服务所有的健康实例,然后根据健康实例的地域信息与自身的地域信息进行匹配,相同地域信息的被调服务实例会优先访问,从而实现就近路由功能。
注册中心作为微服务架构中最为核心的组件,更换注册中心并非易事,只有当所有服务同一时刻同时更换注册中心时,才能保证服务间的访问不受影响。很显然,这种要求是无法做到的。庆幸的是,北极星注册中心提供了双注册中心的机制,可以让服务同时注册在北极星注册中心及其它类型的注册中心上,这样一来,业务便可利用这种双注册的机制帮助业务平滑地从其它类型的注册中心迁移到北极星注册中心来。
利用双注册机制实现注册中心迁移可分为二步三阶段:
经过系统重构和北极星的加持后,让部门的系统焕然一新,服务治理能力得到了有力的提升,能够承载越来越多的业务以及越来越复杂的业务场景。现如今,部门200多个微服务依托于北极星丰富的服务治理能力为业务提供持续稳定的服务,将后来,随着业务规模的继续扩大,北极星的服务治理能力也将进一步得到体现。
– 转载自掘金
polaris-server 作为PolarisMesh的控制面,该进程主要负责服务数据、配置数据、治理规则的管理以及下发至北极星SDK以及实现了xDS的客户端。
polaris-server 是如何处理客户端的服务注册请求的呢?服务数据是怎么存储的呢?带着这个疑问,我们来探究看下polaris-server的启动流程,看看北极星是实现的。
polaris-java
InstanceRegisterRequest request = new InstanceRegisterRequest();
// 设置实例所属服务信息
request.setService(service);
// 设置实例所属服务的命名空间信息
request.setNamespace(namespace);
// 设置实例的 host 信息
request.setHost(host);
// 设置实例的端口信息
request.setPort(port);
// 可选,资源访问Token,即用户/用户组访问凭据,仅当服务端开启客户端鉴权时才需配置
request.setToken(token);
// 设置实例版本
request.setVersion(version);
// 设置实例的协议
request.setProtocol(protocol);
// 设置实例权重
request.setWeight(weight);
// 设置实例的标签
request.setMetadata(metadata);
// 设置实例地理位置 zone 信息
request.setZone(zone);
// 设置实例地理位置 region 信息
request.setRegion(region);
// 设置实例地理位置 campus 信息
request.setCampus(campus);
// ttl超时时间,如果节点要调用heartbeat上报,则必须填写,否则会400141错误码,单位:秒
request.setTtl(ttl);
polaris-go
// InstanceRegisterRequest 注册服务请求
type InstanceRegisterRequest struct {
// 必选,服务名
Service string
// 必选,命名空间
Namespace string
// 必选,服务监听host,支持IPv6地址
Host string
// 必选,服务实例监听port
Port int
// 可选,资源访问Token,即用户/用户组访问凭据,仅当服务端开启客户端鉴权时才需配置
ServiceToken string
// 以下字段可选,默认nil表示客户端不配置,使用服务端配置
// 服务协议
Protocol *string
// 服务权重,默认100,范围0-10000
Weight *int
// 实例提供服务版本号
Version *string
// 用户自定义metadata信息
Metadata map[string]string
// 该服务实例是否健康,默认健康
Healthy *bool
// 该服务实例是否隔离,默认不隔离
Isolate *bool
// ttl超时时间,如果节点要调用heartbeat上报,则必须填写,否则会400141错误码,单位:秒
TTL *int
// Location 当前注册实例的地理位置信息,主要用于就近路由
Location *Location
// 可选,单次查询超时时间,默认直接获取全局的超时配置
// 用户总最大超时时间为(1+RetryCount) * Timeout
Timeout *time.Duration
// 可选,重试次数,默认直接获取全局的超时配置
RetryCount *int
}
可以先通过官方的 SDK 使用手册来看看是如何使用SDK的服务注册。
这里我们已 polaris-java 为例,看看 polaris-java 如何将服务实例注册请求发送至北极星服务端。
ProviderAPI providerAPI = DiscoveryAPIFactory.createProviderAPI();
InstanceRegisterRequest registerRequest = new InstanceRegisterRequest();
// 初始化服务实例注册信息
...
InstanceRegisterResponse registerResp = providerAPI.registerInstance(registerRequest);
通过这个简单示例代码可知,服务实例的注册动作是通过 polaris-java 中 ProviderAPI(负责服务实例注册相关方法的调用) 的 registerInstance 方法完成。
通过 IDEA 或者 vscode,查看这个方法的具体实现:
@Override
public InstanceRegisterResponse registerInstance(InstanceRegisterRequest req) throws PolarisException {
if (req.getTtl() == null) {
req.setTtl(DEFAULT_INSTANCE_TTL);
}
return registerFlow.registerInstance(req, this::doRegister, this::heartbeat);
}
当调用 providerAPI.registerInstance 后,SDK 内部会自动设置实例的 TTL 周期,然后交由 RegisterFlow 这个负责注册动作的流程编排者执行。因此接着看看这个 RegisterFlow 的定义
public class RegisterFlow {
// 异步注册header key
private static final String HEADER_KEY_ASYNC_REGIS = "async-regis";
// 最大连续心跳失败阈值
private static final int HEARTBEAT_FAIL_COUNT_THRESHOLD = 2;
// SDK 上下文,包括SDK配置、SDK插件实例管理、内部任务流程编排等等
private final SDKContext sdkContext;
// 发送实例心跳
private final ScheduledThreadPoolExecutor asyncRegisterExecutor;
...
}
其实 RegisterFlow 就干两件事件
public InstanceRegisterResponse registerInstance(InstanceRegisterRequest request, RegisterFunction registerFunction,
HeartbeatFunction heartbeatFunction) {
// 将注册请求发送至北极星服务端
InstanceRegisterResponse instanceRegisterResponse = registerFunction.doRegister(request,
createRegisterV2Header());
// 当前实例的注册管理状态进行本地保存
RegisterState registerState = RegisterStateManager.putRegisterState(sdkContext, request);
if (registerState != null) {
// 首次存入实例的注册状态时,为该实例注册创建定期心跳上报动作任务
registerState.setTaskFuture(asyncRegisterExecutor.scheduleWithFixedDelay(
() -> doRunHeartbeat(registerState, registerFunction, heartbeatFunction), request.getTtl(),
request.getTtl(), TimeUnit.SECONDS));
}
return instanceRegisterResponse;
}
来看看 registerFunction.doRegister 的主要流程以及如何将请求发送到服务端
// com.tencent.polaris.discovery.client.flow.RegisterFlow#registerInstance
private InstanceRegisterResponse doRegister(InstanceRegisterRequest req, Map<String, String> customHeader) {
checkAvailable("ProviderAPI");
Validator.validateInstanceRegisterRequest(req);
// 填充注册实例的地理位置信息
enrichInstanceLocation(req);
...
// 调用协议插件,发起网络调用
CommonProviderResponse response = serverConnector.registerInstance(request, customHeader);
...
}
// com.tencent.polaris.plugins.connector.grpc.GrpcConnector#registerInstance
public CommonProviderResponse registerInstance(CommonProviderRequest req, Map<String, String> customHeader)
throws PolarisException {
...
try {
waitDiscoverReady();
// 从连接池中获取一个链接
connection = connectionManager
.getConnection(GrpcUtil.OP_KEY_REGISTER_INSTANCE, ClusterType.SERVICE_DISCOVER_CLUSTER);
req.setTargetServer(connectionToTargetNode(connection));
// 根据 Connection 创建一个 gRPC Stub
PolarisGRPCGrpc.PolarisGRPCBlockingStub stub = PolarisGRPCGrpc.newBlockingStub(connection.getChannel());
...
// 向服务端发起 gRPC 请求,完成服务实例的注册
ResponseProto.Response registerInstanceResponse = stub.registerInstance(buildRegisterInstanceRequest(req));
...
}
当实例注册请求从北极星 SDK 发出之后,数据流就来到了北极星服务端处,由北极星的 apiserver 层进行接收并处理,由于北极星 SDK 和服务端的数据通信走的是 gRPC 协议,因此这里请求就会在基于 gRPC 实现的 apiserver 插件中进行处理
// RegisterInstance 注册服务实例
func (g *DiscoverServer) RegisterInstance(ctx context.Context, in *apiservice.Instance) (*apiservice.Response, error) {
// 需要记录操作来源,提高效率,只针对特殊接口添加operator
rCtx := grpcserver.ConvertContext(ctx)
rCtx = context.WithValue(rCtx, utils.StringContext("operator"), ParseGrpcOperator(ctx))
// 客户端请求中带了 token 的,优先已请求中的为准
if in.GetServiceToken().GetValue() != "" {
rCtx = context.WithValue(rCtx, utils.ContextAuthTokenKey, in.GetServiceToken().GetValue())
}
grpcHeader := rCtx.Value(utils.ContextGrpcHeader).(metadata.MD)
if _, ok := grpcHeader["async-regis"]; ok {
rCtx = context.WithValue(rCtx, utils.ContextOpenAsyncRegis, true)
}
out := g.namingServer.RegisterInstance(rCtx, in)
return out, nil
}
在 gRPC apiserver 层处理好请求之后,接着就调用 g.namingServer.RegisterInstance(rCtx, in) 将服务实例数据写进北极星集群中。
// CreateInstance create a single service instance
func (s *Server) CreateInstance(ctx context.Context, req *apiservice.Instance) *apiservice.Response {
...
data, resp := s.createInstance(ctx, req, &ins)
...
// 发布实例上线事件 & 记录实例注册操作记录
s.sendDiscoverEvent(*event)
s.RecordHistory(ctx, instanceRecordEntry(ctx, req, svc, data, model.OCreate))
...
}
// createInstance store operate
func (s *Server) createInstance(ctx context.Context, req *apiservice.Instance, ins *apiservice.Instance) (
*model.Instance, *apiservice.Response) {
// 自动创建实例所在的服务信息
svcId, errResp := s.createWrapServiceIfAbsent(ctx, req)
if errResp != nil {
log.Errorf("[Instance] create service if absent fail : %+v, req : %+v", errResp.String(), req)
return nil, errResp
}
if len(svcId) == 0 {
log.Errorf("[Instance] create service if absent return service id is empty : %+v", req)
return nil, api.NewResponseWithMsg(apimodel.Code_BadRequest, "service id is empty")
}
// 根据 CMDB 插件填充实例的地域信息
s.packCmdb(ins)
// 如果没有开启实例批量注册,则同步调用存储层接口将实例数据进行持久化
if namingServer.bc == nil || !namingServer.bc.CreateInstanceOpen() {
return s.serialCreateInstance(ctx, svcId, req, ins) // 单个同步
}
// 如果开启了实例批量注册,则会将注册请求丢入一个异步队列进行处理
return s.asyncCreateInstance(ctx, svcId, req, ins) // 批量异步
}
func (s *Server) serialCreateInstance(
ctx context.Context, svcId string, req *apiservice.Instance, ins *apiservice.Instance) (*model.Instance, *apiservice.Response) {
...
instance, err := s.storage.GetInstance(ins.GetId().GetValue())
...
// 如果存在,则替换实例的属性数据,但是需要保留用户设置的隔离状态,以免出现关键状态丢失
if instance != nil && ins.Isolate == nil {
ins.Isolate = instance.Proto.Isolate
}
// 直接同步创建服务实例
data := instancecommon.CreateInstanceModel(svcId, ins)
// 创建服务实例时,需要先锁住服务,避免在创建实例的时候把服务信息删除导致出现错误的数据
_, releaseFunc, errCode := s.lockService(ctx, req.GetNamespace().GetValue(),
req.GetService().GetValue())
if errCode != apimodel.Code_ExecuteSuccess {
return nil, api.NewInstanceResponse(errCode, req)
}
defer releaseFunc()
// 调用存储层直接写实例信息
if err := s.storage.AddInstance(data); err != nil {
log.Error(err.Error(), utils.ZapRequestID(rid), utils.ZapPlatformID(pid))
return nil, wrapperInstanceStoreResponse(req, err)
}
return data, nil
}
func (s *Server) asyncCreateInstance(
ctx context.Context, svcId string, req *apiservice.Instance, ins *apiservice.Instance) (
*model.Instance, *apiservice.Response) {
allowAsyncRegis, _ := ctx.Value(utils.ContextOpenAsyncRegis).(bool)
// 将实例注册请求放入异步任务池中
future := s.bc.AsyncCreateInstance(svcId, ins, !allowAsyncRegis)
// 等待任务完成
if err := future.Wait(); err != nil {
if future.Code() == apimodel.Code_ExistedResource {
req.Id = utils.NewStringValue(ins.GetId().GetValue())
}
return nil, api.NewInstanceResponse(future.Code(), req)
}
return instancecommon.CreateInstanceModel(svcId, req), nil
}
北极星的存储层是插件化设计,单机模式下是采用 boltdb,集群模式则是依赖 MySQL,这里只说集群模式下的存储层处理。
依赖 MySQL 的存储层实现中,针对实例信息,北极星将其拆分成了三个表
实例主要信息
CREATE TABLE `instance`
(
`id` varchar(128) NOT NULL comment 'Unique ID',
`service_id` varchar(32) NOT NULL comment 'Service ID',
`vpc_id` varchar(64) DEFAULT NULL comment 'VPC ID',
`host` varchar(128) NOT NULL comment 'instance Host Information',
`port` int(11) NOT NULL comment 'instance port information',
`protocol` varchar(32) DEFAULT NULL comment 'Listening protocols for corresponding ports, such as TPC, UDP, GRPC, DUBBO, etc.',
`version` varchar(32) DEFAULT NULL comment 'The version of the instance can be used for version routing',
`health_status` tinyint(4) NOT NULL DEFAULT '1' comment 'The health status of the instance, 1 is health, 0 is unhealthy',
`isolate` tinyint(4) NOT NULL DEFAULT '0' comment 'Example isolation status flag, 0 is not isolated, 1 is isolated',
`weight` smallint(6) NOT NULL DEFAULT '100' comment 'The weight of the instance is mainly used for LoadBalance, default is 100',
`enable_health_check` tinyint(4) NOT NULL DEFAULT '0' comment 'Whether to open a heartbeat on an instance, check the logic, 0 is not open, 1 is open',
`logic_set` varchar(128) DEFAULT NULL comment 'Example logic packet information',
`cmdb_region` varchar(128) DEFAULT NULL comment 'The region information of the instance is mainly used to close the route',
`cmdb_zone` varchar(128) DEFAULT NULL comment 'The ZONE information of the instance is mainly used to close the route.',
`cmdb_idc` varchar(128) DEFAULT NULL comment 'The IDC information of the instance is mainly used to close the route',
`priority` tinyint(4) NOT NULL DEFAULT '0' comment 'Example priority, currently useless',
`revision` varchar(32) NOT NULL comment 'Instance version information',
`flag` tinyint(4) NOT NULL DEFAULT '0' comment 'Logic delete flag, 0 means visible, 1 means that it has been logically deleted',
`ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP comment 'Create time',
`mtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment 'Last updated time',
PRIMARY KEY (`id`),
KEY `service_id` (`service_id`),
KEY `mtime` (`mtime`),
KEY `host` (`host`)
) ENGINE = InnoDB;
实例健康检查的类型
CREATE TABLE `health_check`
(
`id` varchar(128) NOT NULL comment 'Instance ID',
`type` tinyint(4) NOT NULL DEFAULT '0' comment 'Instance health check type',
`ttl` int(11) NOT NULL comment 'TTL time jumping',
PRIMARY KEY (`id`),
CONSTRAINT `health_check_ibfk_1` FOREIGN KEY (`id`) REFERENCES `instance` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB;
实例的元数据
CREATE TABLE `instance_metadata`
(
`id` varchar(128) NOT NULL comment 'Instance ID',
`mkey` varchar(128) NOT NULL comment 'instance label of Key',
`mvalue` varchar(4096) NOT NULL comment 'instance label Value',
`ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP comment 'Create time',
`mtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP comment 'Last updated time',
PRIMARY KEY (`id`, `mkey`),
KEY `mkey` (`mkey`),
CONSTRAINT `instance_metadata_ibfk_1` FOREIGN KEY (`id`) REFERENCES `instance` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB;
因此在操作存储层持久化服务实例信息时,需要做以下几个操作
// 添加实例主信息
if err := batchAddMainInstances(tx, instances); err != nil {
log.Errorf("[Store][database] batch add main instances err: %s", err.Error())
return err
}
// 添加实例的健康检查信息
if err := batchAddInstanceCheck(tx, instances); err != nil {
log.Errorf("[Store][database] batch add instance check err: %s", err.Error())
return err
}
// 先清理实例原先的 metadata 信息数据,确保不会遗留脏数据
if err := batchDeleteInstanceMeta(tx, instances); err != nil {
log.Errorf("[Store][database] batch delete instance metadata err: %s", err.Error())
return err
}
// 添加实例的 metadata 信息
if err := batchAddInstanceMeta(tx, instances); err != nil {
log.Errorf("[Store][database] batch add instance metadata err: %s", err.Error())
return err
}
富融银⾏是⼀家⽴⾜于⾹港,⾯向全球业务的虚拟银⾏,创立以来先后斩获 2021年-杰出虚拟银行服务大奖、2022年-[领航9+2粤港澳大湾区奖项]粤港澳大湾区最佳银行 等荣誉。
富融银⾏以⼤数据、云计算等技术为驱动,为用户提供存款、贷款、转账、理财、营销等⼀站式的⾦融服务。
富融银行的核⼼系统是处理银⾏业务存款、贷款和中间件业务等最基本业务的IT系统。为了⽀持银⾏业务的⾼速发展,核⼼系统涵盖了外购、⾃研2⼤类系统,其中外购系统不具备⼆次开发能⼒,需要供应商⽀持。
为了保障业务的持续发展,需要改进核⼼系统服务治理⽔平,来应对业务挑战和⾦融监管,因此核⼼业务需要引入服务治理组件,能够平滑顺利地解决容灾、系统集成、流控、服务发现、服务治理、故障容错等问题。接下来,我们来看看富融银行是如何应对挑战,实现业务系统升级的。
随着业务不断扩展,自研系统+外购系统带来了一定的挑战:通讯协议上的多样性,报文格式的差异,云上的安全机制,混合云的容灾机制等,北极星的到来,帮助核心研发团队低成本高效率应对上述各种挑战。
上面提到过,为了⽀撑银⾏业务发展,核心系统涵盖了外购、⾃研2⼤类系统,外购系统不具备⼆次开发能⼒,需要供应商⽀持。
核⼼服务供应商A适配各种银⾏的集成需求,提供私有化RPC协议解决模块之间的调⽤,提供服务⽹关解决外部系统的调⽤问题。核⼼服务供应商B,基于Spring体系,提基于Http+Json的通讯协议,并基于Netty定制Http组件,便于配置。
但不同供应商系统再加上⾃建系统,增加系统集成难度:
北极星社区提供多种数据面,能够很好地兼容现在主流的技术栈,目前富融银行核心系统使用的是 Spring Cloud Tencent、Spring Boot Polaris 和 Polaris Java SDK。
基于 Spring Boot 体系开发的服务全部使⽤ Spring Cloud Tencent,其中,Spring Boot 选用 2.4.5 版本,Spring Cloud Tecent 选用 2020.5 版本。统⼀版本,可维护性⾼。
没有使用 Spring Boot 体系的服务,需要在开发框架中集成 Polaris Java SDK。我们参考 Spring Boot Polaris 实现了服务和 Polaris Java SDK 的集成插件,必须通过插件进行服务发现和路由,选择性进行服务注册,低成本地接⼊北极星体系。⽆法使⽤ SDK 进行服务注册的服务,可以在北极星控制台上注册。
使⽤统⼀的polaris.yml,统⼀北极星服务接⼊、就近路由、降级措施和主动探测机制。
引⼊统⼀的pom依赖,管理⾃定义组件和北极星版本。
改造结果如下:
⾹港银⾏同业结算有限公司(HKICL)要求所有接⼊转数快(FPS)的⾦融机构实现MQ队列的⾃动容灾。在主队列出现异常时,能在5分钟之内切换到备⽤队列。
早期设计思路是⽆法做到⾃动切换队列的场景。
新服务的架构变为:
默认情况下同机房80%流量、跨机房20%流量。保证在队列异常、专线异常、转发服务异常时,服务不中断。
北极星丰富的服务和流量管理能⼒,给了我们更多的选择。
⾦融业务上公有云,需要考虑后云上安全,需要加密传输、数字签名等机制,直接修改外购系统不现实。折中⽅案是将这些系统,放在严格管理的区域,然后开放有限⼊⼝对外提供服务。⽹关可以作为服务⼊⼝承担此任务。北极星提供spring-cloud-tencent-gateway-plugin插件打通SpringCloudGateway(简称SCG)和北极星,将北极星的服务治理能力与⽹关集成在⼀起。⼀旦服务能注册到北极星上,SCG就可路由到对应服务。如果外购系统不⽀持⾃动注册到北极星,可使⽤⼿动注册+北极星主动探测的机制,及时熔断异常服务。
北极星维护服务对应的IP列表,可以借此做授权管理。通过 服务名->[⽩名单URL]、服务名->[⿊名单URL],可有效控制流量。IP地址的变更与服务注册完美绑定在⼀起,⽆需再⽹关⼿动维护。
默认情况下,北极星服务中的IpPort就是被调⽅地址。
在特定业务中,需要与公⽹服务进行通讯。服务方或者提供IpPort或者URL。如果IpPort,可直接使⽤北极星负载均衡。但如果是URL则需要引⼊Http代理服务器。
内网的主调⽅通过代理服务访问被调⽅。容灾可在主调⽅处理,也可以通过http代理服务应对。考虑到运维统⼀性,选择通过北极星进⾏管理。默认情况下北极星路由地址是http代理地址,因此需要重写访问地址。利⽤ApacheHttpClient提供的DefaultRoutePlanner,可解决此问题。通过重写DefaultRoutePlanner的 determineProxy接⼝,达到 返回实际代理(proxyHost)并重写⽬标地址(targetHost)。
此外利⽤北极星SDK⾃带的探活机制,定期检查链路,在链路(Http代理->专线->被调⽅) 出现任何异常时,都可切换到可⽤链路。
北极星是腾讯新⼀代服务治理的核⼼组件,它的到来极⼤改善了富融银⾏核⼼业务的服务治理⽔平。促使富融银行逐步摆脱了私有化的⼚商RPC协议,以统⼀的Https/Http+Json+Fegin模式降低系统间的集成难度,北极星灵活可靠的路由规则,低成本的⽀持银⾏容灾演练,上报与探测功能,也保证了服务的可⽤性。
借由这个机会,向北极星开源团队表示感谢。当我们摸索北极星,开源团队积极耐⼼回答我们的疑惑。我们提出的建议,也能得到团队回应。
从2021年到现在,⻅证了开源北极星的诸多变化,基础功能不断优化,⽂档质量逐步提升,企业级功能逐步完善,对运维⼈员也越来越友好,开源的道路上,愿它乘⻛破浪。
总部位于苏州的这家国际物流SaaS公司,已经借助云原生能力,实现了技术架构的全面升级。 海管家,这家创立于2015年的年轻科技公司,不到8年时间,将服务的客户数量做到超百万级,遍布全球各地,成长速度让人咂舌。 得益于公司在AI、大数据、云计算等技术领域的超前布局,海管家率先在物流领域推出多个变革性产品,为港口、船公司、货代企业、船代企业提供领先的系统解决方案和数据对接服务,在无纸化码头系统领域有丰富的项目经验。 目前,海管家的产品矩阵涵盖了可视化、电子单证发送、SaaS货代操作系统、跨境业务系统、获客和IM工具为主的综合性SaaS服务,并提供在线报关、线上代订舱(E-BOOKING)等公共物流服务。 那么,海管家又是如何通过云原生,在物流 SaaS领域实现业务创新,为客户提供更加稳定、可靠的服务的,并进一步帮助企业优化资源和人力成本的呢?带着这些疑问,我们深入带你了解这家企业的技术变革历程。
随着海管家 SaaS 业务的上线,注册认证用户呈现出了爆发式的增长,用户的使用场景也不断扩大。在这个过程中,SaaS 的用户使用体验变得愈发重要,如何在用户规模高速增长的同时可以保证 SaaS 的稳定性、敏捷性, SaaS 的微服务开发效率如何保证,这些都给研发团队带来了一定的挑战。
最大的挑战之一,来自于SaaS用户场景需求的增加,越来越多的功能等待发开、发布上线,对迭代频率的要求越来越高,但由于 SaaS 服务技术架构偏向于传统的应用开发方式,不能够像微服务、模块化架构一样,进行多线并行开发。同时,对于应用发布,缺少灰度发布能力,为了保障业务稳定性,每次发布客户只能选择在凌晨的业务低峰期进行,开发、运维、测试同学苦不堪言,对于发版无损发布能力的需求声音越来越大。
还有一个,疫情物流承压、货代数字化成大趋势,但数字化如何在国际物流落地,海管家提出了自己的标准。 “国际物流跨境角色多、链条长,一个提供国际货代服务的 SaaS 公司如果要做数字化,一条产品线至少要提升20%到30%的效能,才可实现商业的快速复制、扩张以及落地,进而才能发展为 SaaS 公司的核心业务线。“海管家CEO金忠国表示。 而且除了效能问题,国际货代的SaaS服务的本质其实是要解决信息、数据和相关业务的标准化问题,而这些需要行业各相关角色的协同,单个公司靠自己无法解决标准化问题。 作为一家 SaaS 服务商,海管家选择的发展路径是跟着行业节奏逐点击破、连点成线,最终达到平台的融合。 可以预计,未来国际物流SaaS平台企业一定会以『数据服务化』、『全渠道服务化』和『新业务拓展敏捷化』的交融与创新为发展方向,这对团队的业务架构能力与技术架构能力提出来比较高的要求。
此外,在市场需求的快速变化下,产品功能创新和迭代效率问题也是对技术架构的一大挑战。
”我们从创业的一开始就基于云原生,可以说是国内第一批云住民,上云用云就和血液一样,云造就了我们,也发展了我们。”海管家技术负责人徐红维告诉鹅厂技术派。 云原生,它是先进软件架构技术和管理方法的思想集合,通过容器、微服务、持续交付等一系列技术,实现了信息系统由烟囱状、重装置和低效率的架构向分布式、小型化和自动化的新一代软件架构的转变。 同时,云原生架构具备松耦合、分布式、高韧性三大特点,能够以业务应用为中心,充分利用云计算优势,实现敏捷交付、价值聚焦的核心目标。 “有没有发现,上述问题及现状的解法和云原生架构带来的核心能力不谋而合。” “因此,海管家很早就笃定要将主要的业务应用,包括前端 Web 容器、网关、后端微服务、大数据等等通过 Kubernetes 集群部署,以云原生的方式帮助业务快速迭代,灵活响应商业需求。”徐红维补充道。
都在说微服务,但是微服务也要对症下药。对于微服务的治理、改造,海管家的团队更加看重的是改造的复杂度、侵入性、稳定性等方面,海管家技术团队对目前市面上的几款开源产品进行调研以及和相关团队进行深入的沟通。 经过大量的预研后,最终选择了腾讯云北极星(Polaris Mesh),主要看重一下几个特性:强大的控制面、无侵入、稳定性高、丰富的可观测能力、混合云支持、兼容Kubernetes等。 基于北极星(Polaris Mesh)的服务管理、流量管理、故障容错、配置管理和可观测性五大功能,以及容器服务的基础运行能力,海管家重新架构了业务的技术架构如下图。
与容器化改造几乎同步进行的是对微服务架构的统一。 在此之前,海管家的各个业务单元多种技术栈并存,彼此之间相互通讯复杂度高,项目成员的交接往往要耗费巨大的精力,极大程度上阻碍了业务发展的进展,因此微服务架构统一势在必行。 海管家经历了 2 年多时间完成了这一项艰巨的工作,虽然投入精力巨大,但收益很大,在技术框架上都有统一的标准可以遵循,各团队之间统一技术栈,研发效率成倍提升。 关系到未来多年的技术战略,在微服务架构的选型上,开放性、成熟度、普适性标准缺一不可,考虑到海管家以 Java 为主要开发语言,Spring Cloud Tencent 就成为了微服务框架的新的选择。同时,海管家也将自研的基于Spring Cloud + Dubbo开发标准的基础服务框架与Spring Cloud Tencent、Polaris Mesh进行兼容整合。
架构的变更需要有一个演进过程。云原生其实源自于PaaS,所以在应用云原生架构的时候,在PaaS层也遇到了平滑演进的问题。如何让产品和开发者即能保留以前的习惯,同时又能去尝试新的交付、开发方式?在传统的模式中,大家习惯于交付代码包,习惯于基于虚拟机的运维,而云原生时代以容器镜像作为交付载体,而运行实例则是镜像实例化容器。 无论是基于传统架构的PaaS,还是基于kubernetes的PaaS,实现主要操作都是一样的,包括:建站、发布、重启、扩容/缩容、下线等等,实现两套无疑非常浪费资源,也增加了维护成本,对于产品和开发者来说干的事情是一样的。所以海管家技术团队用kubernetes实现了所有公共部分,包括:统一元数据、统一运维操作、统一资源抽象。在产品层和运维方式上,提供两种控制面。 在进行技术架构演技的过程中,会面临新老系统并存的问题,老(遗留)系统的架构技术栈老旧,改造、重构成本较大,海管家通过Mesh的方式统一解决这个问题。新系统,Mesh是Pod里的Sidecar,但老系统因为一般情况下是没有运行在kubernetes上,所以不支持Pod和Sidecar的运维模式,需要用Java Agent的模式来管理Mesh进程,使得Mesh能够帮助老架构下的应用完成服务化改造,并支持新老架构下服务的统一管理。
海管家 SaaS 研发团队意识到,随着业务发展的向好,这些挑战也会也越来越大。 在业务快速发展中,既要保证好已有业务的稳定性,又要快速地迭代新功能,并且需要保证开发的效率并不会随着业务增长而大幅降低。在新的微服务体系下,海管家的业务开发人员更加专注在业务本身,从繁杂的技术栈中脱离出来,也就能解决两大关键性的问题:系统稳定性、研发效率。
以下是我们在微服务探索过程中的一些经验分享,希望能够帮到正在阅读本文的同行。
在实际的开发过程中,一个微服务架构系统下的不同微服务可能是由多个团队进行开发与维护的,每个团队只需关注所属的一个或多个微服务,而各个团队维护的微服务之间可能存在相互调用关系。 如果一个团队在开发其所属的微服务,调试的时候需要验证完整的微服务调用链路,此时需要依赖其他团队的微服务,如何部署开发联调环境就会遇到以下问题: 首先,如果所有团队都使用同一套开发联调环境,那么一个团队的测试微服务实例无法正常运行时,会影响其他依赖该微服务的应用也无法正常运行。 其次,如果每个团队有单独的一套开发联调环境,那么每个团队不仅需要维护自己环境的微服务应用,还需要维护其他团队环境的自身所属微服务应用,效率大大降低。同时,每个团队都需要部署完整的一套微服务架构应用,成本也随着团队数的增加而大大上升。 此时,可以使用测试环境路由的架构来帮助部署一套运维简单且成本较低开发联调环境。测试环境路由是一种基于服务路由的环境治理策略,核心是维护一个稳定的基线环境作为基础环境,测试环境仅需要部署需要变更的微服务。多测试环境有两个基础概念,如下所示:
1.基线环境(Baseline Environment): 完整稳定的基础环境,是作为同类型下其他环境流量通路的一个兜底可用环境,用户应该尽量保证基线环境的完整性、稳定性。
2.测试环境(Feature Environment): 一种临时环境,仅可能为开发/测试环境类型,测试环境不需要部署全链路完整的服务,而是仅部署本次有变更的服务,其他服务通过服务路由的方式复用基线环境服务资源。
部署完成多测试环境后,开发者可以通过一定的路由规则方式,将测试请求打到不同的测试环境,如果测试环境没有相应的微服务处理链路上的请求,那么会降级到基线环境处理。因此,开发者需要将开发新测试的微服务部署到对应的测试环境,而不需要更新或不属于开发者管理的微服务则复用基线环境的服务,完成对应测试环境的测试。 虽然测试环境路由是一个相对成熟的开发测试环境解决方案,但是能够开箱即用的生产开发框架却不多,往往需要开发者二次开发相应的功能。因此需要一个相对完善的解决方案来帮助实现测试环境路由,简化开发难度并提升开发效率。 基于上述的想法,海管家有十几条产品线,并且产品线之间存在着错综复杂的关联,并线开发、联调等问题一直被产研团队吐槽和诟病。 基于北极星微服务引擎的能力,结合Spring Cloud Tencent 的开发框架,与社区进行合作开发以下的方案,测试环境路由的样例实现以下图为例,一共有两个测试环境以及一个基线环境。流量从端到端会依次经过以下组件:App(前端) -> 网关 -> 通行证中心 -> 订单交易中心 -> 支付结算中心。
为了达到测试环境路由的能力,开发工作需要做三件事情:
其中在流量染色的环节,海管家结合着Spring Cloud Tencent的开发组件的能力,使用客户端染色 + 网关动态染色。
客户端染色 (推荐)
如下图所示,在客户端发出的 HTTP 请求里,新增 X-Polaris-Metadata-Transitive-featureenv=v2 请求头即可实现染色。该方式是让开发者在请求创建的时候根据业务逻辑进行流量染色。
网关动态染色(推荐) 动态染色是开发者配置一定的染色规则,让流量经过网关时自动染色,使用起来相当方便。例如把区域编号 area_code=shanghai 用户的请求都转发到 feature env v2 环境,把区域编号 area_code=beijing 的用户的请求都转发到 feature env v3 环境。只需要配置一条染色规则即可实现。
随着业务的发展、客户需求的增多、行业应用场景的多样化,产线平均每天几十次发布。为了不影响白天业务高峰以及用户群体的特殊性(面对B端的SaaS系统),每次较大发版只能选择在凌晨业务低峰期进行。 想象一下,如果产品、研发、运维人员、中台支持人员每次都集中在晚上发布,太不人性化。“移动互联网的时代,谁还玩停机维护那一套呢?” 如果晚上选择较少的人参与发布,那么当出问题的时候会『耽误救治』的最佳时机,故障责任也不好划分。 北极星,在灰度发布这方面提供了很大的支持和帮助,能够满足海管家现阶段灰度发布的场景:首先用户体验不能中断的业务,其次微服务业务的存在关联关系的多个微服务的特性变更。 可以基于域名分离的方式实现全链路灰度,通过不同的域名区分灰度环境和稳定环境。前端客户的请求通过灰度域名访问到灰度版本的服务,通过稳定域名访问到稳定版本的服务。
灰度请求通过灰度域名接入到网关,网关通过域名识别到灰度请求后,将请求优先路由到灰度版本的服务,并通过请求头的方式进行灰度染色。后续微服务之间,服务框架通过请求头识别到灰度请求,会优先将请求路由到灰度版本服务,如果寻址不到灰度版本,则路由到稳定版本服务。 对于全链路灰度发布,海管家不仅需要将流量进行灰度,还需要将后端的数据库、缓存、消息队列等等基础服务也支持灰度,这里还需要跟北极星社区进行更加深度的合作和开发。
“看的远,才能走的稳。看得远映射到平台化,走的稳映射到系统重构,这已然成为海管家的重要技术战略。”金忠国说。 “未来,我们将继续进行云原生架构升级探索,持续提高SaaS业务系统的稳定性和敏捷性,随着对云原生架构的理解的深入,我们将继续与腾讯云原生团队进一步的探索和研究,给客户创造更多的价值。”
– 转载自掘金
polaris-server 作为PolarisMesh的控制面,该进程主要负责服务数据、配置数据、治理规则的管理以及下发至北极星SDK以及实现了xDS的客户端。
polaris-server 是如何同时对外提供服务注册发现、配置管理、服务治理的功能呢?又是如何同时支持北极星基于gRPC的私有协议、兼容eureka协议以及xDS协议呢?带着这些疑问,我们来探究看下polaris-server的启动流程,看看北极星是实现的。
# server启动引导配置
bootstrap:
# 全局日志
logger:
${日志scope名称,主要支持 config/auth/store/naming/cache/xdsv3/default}:
rotateOutputPath: ${日志文件位置}
errorRotateOutputPath: ${专门记录error级别的错误日志文件}
rotationMaxSize: ${单个日志文件大小最大值, 默认 100, 单位为 mb}
rotationMaxBackups: ${最大保存多少个日志文件, 默认 10}
rotationMaxAge: ${单个日志文件最大保存天数, 默认 7}
outputLevel: ${日志输出级别,debug/info/warn/error}
# 按顺序启动server
startInOrder:
open: true # 是否开启,默认是关闭
key: sz # 全局锁,锁的名称,根据锁名称进行互斥启动
# 注册为北极星服务
polaris_service:
probe_address: ${北极星探测地址,用于获取当前北极星server自身对外的IP, 默认为 ##DB_ADDR##}
enable_register: ${是否允许当前北极星进程进行自注册,即将自身的系统级服务注册通过北极星的服务注册能力进行注册,默认为 true}
isolated: ${注册的实例是否需要处理隔离状态,默认为 false}
services:
- name: polaris.checker # 北极星健康检查需要的系统级服务,根据该服务下的实例,进行健康检查任务的 hash 责任划分
protocols: # 注册的实例需要暴露的协议,即注册端口号
- service-grpc
# apiserver,北极星对外协议实现层
apiservers:
- name: service-eureka # 北极星支持 eureka 协议的协议层插件
option:
listenIP: "0.0.0.0" # tcp server 的监听 ip
listenPort: 8761 # tcp server 的监听端口
namespace: default # 设置 eureka 服务默认归属的北极星命名空间
refreshInterval: 10 # 定期从北极星的cache模块中拉取数据,刷新 eureka 协议中的数据缓存
deltaExpireInterval: 60 # 增量缓存过期周期
unhealthyExpireInterval: 180 # 不健康实例过期周期
connLimit: # 链接限制配置
openConnLimit: false # 是否开启链接限制功能,默认 false
maxConnPerHost: 1024 # 每个IP最多的连接数
maxConnLimit: 10240 # 当前listener最大的连接数限制
whiteList: 127.0.0.1 # 该 apiserver 的白名单 IP 列表,英文逗号分隔
purgeCounterInterval: 10s # 清理链接行为的周期
purgeCounterExpired: 5s # 清理多久不活跃的链接
- name: api-http # 北极星自身 OpenAPI 协议层
option:
listenIP: "0.0.0.0" # tcp server 的监听 ip
listenPort: 8090 # tcp server 的监听端口
enablePprof: true # 是否开启 golang 的 debug/pprof 的数据采集
enableSwagger: true # 是否开启 swagger OpenAPI doc 文档数据生成
api: # 设置允许开放的 api 接口类型
admin: # 运维管理 OpenAPI 接口
enable: true
console: # 北极星控制台 OpenAPI 接口
enable: true
include: [default] # 需要暴露的 OpenAPI 分组
client: # 北极星客户端相关 OpenAPI 接口
enable: true
include: [discover, register, healthcheck]
config: # 北极星配置中心相关 OpenAPI 接口
enable: true
include: [default]
- name: service-grpc # 北极星基于 gRPC 协议的客户端通信协议层,用于注册发现、服务治理
option:
listenIP: "0.0.0.0"
listenPort: 8091
enableCacheProto: true # 是否开启 protobuf 解析缓存,对于内容一样的protobuf减少序列化
sizeCacheProto: 128 # protobuf 缓存大小
tls: # 协议层支持 tls 的配置
certFile: ""
keyFile: ""
trustedCAFile: ""
api:
client:
enable: true
include: [discover, register, healthcheck]
- name: config-grpc # 北极星基于 gRPC 协议的客户端通信协议层,用于配置中心
option:
listenIP: "0.0.0.0"
listenPort: 8093
connLimit:
openConnLimit: false
maxConnPerHost: 128
maxConnLimit: 5120
api:
client:
enable: true
- name: xds-v3 # 北极星实现的 xDSv3 协议层
option:
listenIP: "0.0.0.0"
listenPort: 15010
connLimit:
openConnLimit: false
maxConnPerHost: 128
maxConnLimit: 10240
# 核心逻辑的配置
auth:
# 鉴权插件
name: defaultAuth
option:
# token 加密的 salt,鉴权解析 token 时需要依靠这个 salt 去解密 token 的信息
# salt 的长度需要满足以下任意一个:len(salt) in [16, 24, 32]
salt: polarismesh@2021
# 控制台鉴权能力开关,默认开启
consoleOpen: true
# 客户端鉴权能力开关, 默认关闭
clientOpen: false
namespace:
# 是否允许自动创建命名空间
autoCreate: true
naming:
auth:
open: false
# 批量控制器
batch:
${批量控制器配置,支持 register/deregister/clientRegister/clientDeregister}:
open: true # 是否开启该批量控制器
queueSize: 10240 # 暂存任务数量
waitTime: 32ms # 任务未满单次 Batch 数量的最大等待时间,时间到直接强制发起 Batch 操作
maxBatchCount: 128 # 单次 Batch 数量
concurrency: 128 # 处于批任务的 worker 协程数量
dropExpireTask: true # 是否开启丢弃过期任务,仅用于 register 类型的批量控制器
taskLife: 30s # 任务最大有效期,超过有效期则任务不执行,仅用于 register 类型的批量控制器
# 健康检查的配置
healthcheck:
open: true # 是否开启健康检查功能模块
service: polaris.checker # 参与健康检查任务的实例所在的服务
slotNum: 30 # 时间轮参数
minCheckInterval: 1s # 用于调整实例健康检查任务在时间轮内的下一次执行时间,限制最小检查周期
maxCheckInterval: 30s # 用于调整实例健康检查任务在时间轮内的下一次执行时间,限制最大检查周期
clientReportInterval: 120s # 用于调整SDK上报实例健康检查任务在时间轮内的下一次执行时间
batch: # 健康检查数据的批量写控制器
heartbeat:
open: true
queueSize: 10240
waitTime: 32ms
maxBatchCount: 32
concurrency: 64
checkers: # 健康检查启用插件列表,当前支持 heartbeatMemory/heartbeatRedis,由于二者属于同一类型健康检查插件,因此只能启用一个
- name: heartbeatMemory # 基于本机内存实现的健康检查插件,仅适用于单机版本
- name: heartbeatRedis # 基于 redis 实现的健康检查插件,适用于单机版本以及集群版本
option:
kvAddr: ##REDIS_ADDR## # redis 地址,IP:PORT 格式
# ACL user from redis v6.0, remove it if ACL is not available
kvUser: ##REDIS_USER# # 如果redis版本低于6.0,请直接移除该配置项
kvPasswd: ##REDIS_PWD## # redis 密码
poolSize: 200 # redis 链接池大小
minIdleConns: 30 # 最小空闲链接数量
idleTimeout: 120s # 认为空闲链接的时间
connectTimeout: 200ms # 链接超时时间
msgTimeout: 200ms # redis的读写操作超时时间
concurrency: 200 # 操作redis的worker协程数量
withTLS: false
# 配置中心模块启动配置
config:
# 是否启动配置模块
open: true
cache:
#配置文件缓存过期时间,单位s
expireTimeAfterWrite: 3600
# 缓存配置
cache:
open: true
resources:
- name: service # 加载服务数据
option:
disableBusiness: false # 不加载业务服务
needMeta: true # 加载服务元数据
- name: instance # 加载实例数据
option:
disableBusiness: false # 不加载业务服务实例
needMeta: true # 加载实例元数据
- name: routingConfig # 加载路由数据
- name: rateLimitConfig # 加载限流数据
- name: circuitBreakerConfig # 加载熔断数据
- name: users # 加载用户、用户组数据
- name: strategyRule # 加载鉴权规则数据
- name: namespace # 加载命名空间数据
- name: client # 加载 SDK 数据
# 存储配置
store:
# 单机文件存储插件
name: boltdbStore
option:
path: ./polaris.bolt
## 数据库存储插件
# name: defaultStore
# option:
# master: # 主库配置, 如果要配置 slave 的话,就把 master 替换为 slave 即可
# dbType: mysql # 数据库存储类型
# dbName: polaris_server # schema 名称
# dbUser: ##DB_USER## # 数据库用户
# dbPwd: ##DB_PWD## # 数据库用户密码
# dbAddr: ##DB_ADDR## # 数据库连接地址,HOST:PORT 格式
# maxOpenConns: 300 # 最大数据库连接数
# maxIdleConns: 50 # 最大空闲数据库连接数
# connMaxLifetime: 300 # 单位秒 # 连接的最大存活时间,超过改时间空闲连接将会呗释放
# txIsolationLevel: 2 #LevelReadCommitted
# 插件配置
plugin: # 本次暂不涉及,后续文章在详细说明
我们先来看下,北极星服务端源码的组织形式
➜ polaris-server git:(release-v1.12.0) tree -L 1
.
├── apiserver # 北极星对外协议实现层
├── auth # 北极星的资源鉴权层
├── bootstrap # 负责将北极星各个功能模块初始化、逐个启动
├── cache # 北极星的资源缓存层,对于弱一致性读接口进行加速
├── cmd # 简单实现几个 command 命令,start:启动北极星,version: 查询当前北极星进程版本
├── common # 公共模块,放置api接口对象定义、功能模块的工具函数
├── config # 北极星的配置中心
├── main.go # main 函数所在文件,polaris-server 进程启动的入口
├── maintain # 北极星自身运维能力模块
├── namespace # 北极星命名空间模块,主要用于服务注册发现以及配置中心
├── plugin # 北极星小功能插件模块,主要集中了各个旁路能力的默认插件实现
├── plugin.go # 北极星的插件注册文件,利用 golang 的 init 机制
├── polaris-server.yaml # polaris-server 进程启动所需要的配置文件
├── service # 北极星的服务注册发现中心、治理规则中心
├── store # 北极星的存储层,已插件化,存在两个默认实现插件,一个是基于boltdb实现的单机存储插件,一个是基于MySQL实现的集群存储插件
├── tool # 北极星的相关脚本,包含启动、关闭
└── version # 编译期间注入版本信息
从源码的组织中可以看出,北极星各个功能模块的划分还是很清晰的,其核心的模块主要是以下六个
我们先来看看,是如何在bootstrap中完成对北极星各个功能模块的初始化以及逐个启动的
先看看 bootstrap 下的源码文件组织
➜ bootstrap git:(release-v1.12.0) tree -L 1
.
├── config # bootstrap 在 polaris-server.yaml 中对应的配置对象
├── run_darwin.go # 用于 drawin 内核,阻塞 polaris-server 主协程不退出,并监听相应的os.Signal
├── run_linux.go # 用于 linux 内核,阻塞 polaris-server 主协程不退出,并监听相应的os.Signal
├── run_windows.go # 用于 window 内核,阻塞 polaris-server 主协程不退出,并监听相应的os.Signal
├── self_checker.go # 北极星服务端自身的心跳上报流程,保持自身注册的相关内置服务实例的健康
└── server.go # 北极星启动核心逻辑文件
既然 server.go 是服务端进程启动核心逻辑所在的文件,那我们就直接从他入手。
来到 server.go 文件中,立马就看到一个 Start(configFilePath string) 方法,毋庸置疑,这肯定就是北极星服务端启动的核心入口。我们来简单看看,server.go#Start(configFilePath string) 主要做了哪些事情
func Start(configFilePath string) {
// 根据所给定的配置文件路径信息,加载对应的配置文件内容, 这里指的就是 polaris-server.yaml 中的内容
cfg, err := boot_config.Load(configFilePath)
...
// 根据配置文件内容中对于日志模块的配置, 初始化日志模块
err = log.Configure(cfg.Bootstrap.Logger)
// 初始化相关指标收集器
metrics.InitMetrics()
// 设置插件配置
plugin.SetPluginConfig(&cfg.Plugin)
// 初始化存储层
store.SetStoreConfig(&cfg.Store)
// 开启进入启动流程,初始化插件,加载数据等
var tx store.Transaction
// 根据 ${bootstrap.startInOrder.key} 从存储层获取一把锁,如果锁获取成功,则继续执行
tx, err = StartBootstrapInOrder(s, cfg)
if err != nil {
// 多次尝试加锁失败
fmt.Printf("[ERROR] bootstrap fail: %v\n", err)
return
}
// 启动北极星服务端的功能模块(服务发现、服务治理、配置中心等等)
err = StartComponents(ctx, cfg)
...
// 启动北极星的 apiserver 插件,对于 polaris-server.yaml 中配置的 apiserver 均会进行启动
servers, err := StartServers(ctx, cfg, errCh)
// 北极星服务端自注册逻辑,方便其他节点感知到自己的存在
if err := polarisServiceRegister(&cfg.Bootstrap.PolarisService, cfg.APIServers); err != nil {
fmt.Printf("[ERROR] register polaris service fail: %v\n", err)
return
}
// 服务端启动完成,发起请求到存储层,释放名为${bootstrap.startInOrder.key}的锁
// 其他北极星节点便可以获取到锁之后继续完成自己的启动逻辑
_ = FinishBootstrapOrder(tx)
fmt.Println("finish starting server")
// 简单的死循环逻辑,监听 os.Signal 完成 apiserver 重启、服务端优雅下线逻辑
RunMainLoop(servers, errCh)
}
简单的梳理 server.go#Start(configFilePath string) 中逻辑,主要就是做了这么几个事情
接着我们来看下功能模块是如何逐个开启的。
北极星的功能模块主要有三个
北极星的旁路功能模块则为
北极星的 APIServer 层,通过插件化的设计,将北极星的能力通过各个协议对外提供,以及对其他注册中心组件的协议兼容。APIServer 的定义如下
type Apiserver interface {
// GetProtocol API协议名
GetProtocol() string
// GetPort API的监听端口
GetPort() uint32
// Initialize API初始化逻辑
Initialize(ctx context.Context, option map[string]interface{}, api map[string]APIConfig) error
// Run API服务的主逻辑循环
Run(errCh chan error)
// Stop 停止API端口监听
Stop()
// Restart 重启API
Restart(option map[string]interface{}, api map[string]APIConfig, errCh chan error) error
}
可以看到,APIServer interface 只是定义了 APIServer 插件的相关生命周期定义,并没有具体限制 APIServer 改如何处理数据请求,因此使得 APIServer 相关插件实现,即可以将北极星的能力通过 gRPC、HTTP 协议对外提供,同时也可以通过 APIServer 插件对 eureka、xds 等第三方协议进行适配,将其转换为北极星的相关能力接口以及数据模型。 当前北极星 APIServer 已有的插件列表如下
// StartComponents start health check and naming components
func StartComponents(ctx context.Context, cfg *boot_config.Config) error {
var err error
...
// 初始化缓存模块
if err := cache.Initialize(ctx, &cfg.Cache, s); err != nil {
return err
}
}
缓存层模块初始化的触发在 StartComponents 流程中,在初始化过程中,会根据 polaris-server.yaml 配置文件中关于 cache 配置的 resources 列表,按需启动相关资源的 cache 实现
// 构建 CacheManager 对象实例,并构造所有资源的 Cache 接口实现实例
func newCacheManager(_ context.Context, cacheOpt *Config, storage store.Store) (*CacheManager, error) {
SetCacheConfig(cacheOpt)
mgr := &CacheManager{
storage: storage,
caches: make([]Cache, CacheLast),
comRevisionCh: make(chan *revisionNotify, RevisionChanCount),
revisions: map[string]string{},
}
// 构建服务实例缓存 cache
ic := newInstanceCache(storage, mgr.comRevisionCh)
// 构建服务缓存 cache
sc := newServiceCache(storage, mgr.comRevisionCh, ic)
mgr.caches[CacheService] = sc
mgr.caches[CacheInstance] = ic
// 构建路由规则缓存 cache
mgr.caches[CacheRoutingConfig] = newRoutingConfigCache(storage, sc)
// 构建限流规则缓存 cache
mgr.caches[CacheRateLimit] = newRateLimitCache(storage)
// 构建熔断规则缓存 cache
mgr.caches[CacheCircuitBreaker] = newCircuitBreakerCache(storage)
notify := make(chan interface{}, 8)
// 构建用户列表缓存 cache
mgr.caches[CacheUser] = newUserCache(storage, notify)
// 构建鉴权策略缓存 cache
mgr.caches[CacheAuthStrategy] = newStrategyCache(storage, notify, mgr.caches[CacheUser].(UserCache))
// 构建命名空间缓存 cache
mgr.caches[CacheNamespace] = newNamespaceCache(storage)
// 构建SDK实例信息缓存 cache
mgr.caches[CacheClient] = newClientCache(storage)
if len(mgr.caches) != CacheLast {
return nil, errors.New("some Cache implement not loaded into CacheManager")
}
...
// 根据 polaris-server.yaml 配置完成最终的缓存模块启动
if err := mgr.initialize(); err != nil {
return nil, err
}
return mgr, nil
}
// initialize 缓存对象初始化
func (nc *CacheManager) initialize() error {
for _, obj := range nc.caches {
var option map[string]interface{}
// 根据配置文件中的 resource 列表,按需启动相关的 cache
for _, entry := range config.Resources {
if obj.name() == entry.Name {
option = entry.Option
break
}
}
if err := obj.initialize(option); err != nil {
return err
}
}
return nil
}
// StartComponents start health check and naming components
func StartComponents(ctx context.Context, cfg *boot_config.Config) error {
...
cacheMgn, err := cache.GetCacheManager()
if err != nil {
return err
}
// 初始化鉴权层
if err = auth.Initialize(ctx, &cfg.Auth, s, cacheMgn); err != nil {
return err
}
}
资源鉴权模块初始化的触发在 StartComponents 流程中,由于资源鉴权模块主要任务是根据配置的鉴权规则,针对每次请求都进行一次策略计算,因此为了节省查询相关规则的时间,以及鉴权规则信息、用户信息变化不频繁的假定之下,资源鉴权模块默认从资源缓存模块中获取相关对象,执行计算并返回最终的资源鉴权结果。
// StartComponents start health check and naming components
func StartComponents(ctx context.Context, cfg *boot_config.Config) error {
...
// 初始化命名空间模块
if err := namespace.Initialize(ctx, &cfg.Namespace, s, cacheMgn); err != nil {
return err
}
}
命名空间模块初始化的触发在 StartComponents 流程中,polaris 的服务注册发现、配置中心的模型设计中都依赖命名空间,因此将命名空间相关能力独立出来。 命名空间模块相关的数据操作不是非常频繁,数据操作都是直接和数据存储层进行交互,而依赖缓存模块则是为了解决在创建服务、配置时触发的命名空间自动创建动作,为了减少对数据存储层的调用,通过缓存存在性判断以及 singleflight.Group 组件来实现。
// StartComponents start health check and naming components
func StartComponents(ctx context.Context, cfg *boot_config.Config) error {
...
// 初始化服务发现模块相关功能
if err := StartDiscoverComponents(ctx, cfg, s, cacheMgn, authMgn); err != nil {
return err
}
}
服务注册发现、服务治理模块初始化的触发在 StartComponents 流程中的 StartDiscoverComponents 方法。StartDiscoverComponents 具体做的事情如下
// StartComponents start health check and naming components
func StartComponents(ctx context.Context, cfg *boot_config.Config) error {
...
// 初始化配置中心模块相关功能
if err := StartConfigCenterComponents(ctx, cfg, s, cacheMgn, authMgn); err != nil {
return err
}
}
配置中心模块初始化的触发在 StartComponents 流程中的 StartConfigCenterComponents 方法。StartConfigCenterComponents 具体做的事情如下
注册中心作为微服务架构的核心,承担服务调用过程中的服务注册与寻址的职责。注册中心的演进是随着业务架构和需求的发展而进行演进的。腾讯当前内部服务数超百万级,日调用量超过万亿次,使用着统一的注册中心——北极星。腾讯注册中心也经历3个阶段演进历程,下文主要分享腾讯内部注册中心的演进历程,以及根据运营过程中的优化实践。
2008年,zookeeper诞生,作为最早被广泛使用的注册中心,提供了可自定义的基于树形架构的数据存储模型。业界常见的微服务使用场景是dubbo框架,使用zookeeper进行服务数据的管理;同时在大数据场景下,kafka/hadoop使用zookeeper进行集群和分区的管理,属于泛服务注册发现的用法。
由于zookeeper没有服务模型的概念,各框架使用约定的树形路径进行服务数据的存取,在使用和维护上比较复杂。2014年开始,随着微服务架构的大规模应用,具备统一的服务模型和控制台的注册中心得到广泛的使用,最常用的包括eureka、consul、nacos等。
2015年起,k8s开始大规模使用,k8s基于etcd+coredns提供了基于域名的服务发现能力,应用直接基于DNS即可进行服务发现。
总体来说,服务注册发现有两种实现方案。
zookeeper/eureka/nacos/consul:优势在于作为独立注册中心,与具体应用部署环境无关,管理和使用都比较方便。局限在于这些注册中心架构都是存储计算合一的,单集群性能有限,无法单独针对读或者写进行水平扩展。
k8s service:服务数据基于etcd进行存储,通过coredns进行服务发现。优势在于服务注册中心内置在k8s平台中,无需额外维护注册中心。局限点在于:
在2012年以前,腾讯内部主流开发语言是C++,业务技术栈主要是LAMP(Linux+Apache+MySQL+PHP)模式和CGI+独立后端模式,前者常见于逻辑简单的单体Web应用,后者常见于大型偏计算类的应用,比如社交应用的后端等。
在这2种模式下,业务的远程调用(PHP寻址MySQL,CGI寻址后端节点),需要依赖寻址。在没有注册中心之前,业务往往通过VIP(四层负载均衡)的方式进行寻址,存在以下问题:
变更难度大:VIP本身基于四层负载均衡设备实现,设备本身存在一个裁撤变更的风险,一旦出现VIP变更,业务的代码需要进行变更,影响面比较广。
单点问题:VIP本身成为了一个单点,一旦四层负载均衡设备出现问题,会导致服务不可用。 在2007年,为了解决VIP寻址所带来的问题,腾讯自研了一款注册中心产品L5,提供了基于控制台和SDK的服务注册发现、负载均衡功能,服务调用可以通过L5寻址后直连访问,无需再依赖VIP。
L5是一套使用C++开发的一体化注册中心,基于数据库进行服务数据的存储,客户端通过Agent与服务端进行访问。L5的架构存在以下问题:
在2013年后,随着移动互联网的高速发展,社交、娱乐等业务的用户量也在增长,分布式服务架构进入快速发展期,对注册中心也提出了一些需求,比如支持健康检查、支持轻量化、支持与基础设施或发布平台打通等。但是由于原有L5缺乏定制性,而且L5的运维团队缩减,导致业务提的需求无法满足,因此部分业务都开始自研注册中心,其中使用比较广泛的是以下几个注册中心:
使用不同注册中心的业务团队要进行互通,服务提供者需要把服务同时注册到多个注册中心,才能支持各个业务团队之间服务相互发现。
这种使用方式会带来以下问题:
服务发布的复杂度提升:服务的部署和发布一般和发布系统绑定,对接多个注册中心,对发布系统的实现会比较复杂,容易出现数据不一致的问题。
业务开发复杂度提升:不同注册中心有不同的服务发现API,业务开发者在调用服务之前,需要先确认服务注册在哪个注册中心,选用不同的API接口来进行服务发现。
到了2018年,公司内部业务经过进一步的发展,节点数已经达到了百万级,日调用量也超过了万亿,内部服务跨业务团队之间互相访问成为了常态。使用多注册中心所引发的问题,愈发成为了影响业务开发和部署效率的瓶颈。
2018年到2019年初,在公司开源协同的背景下,为解决多注册中心带来的复杂性问题,多个业务团队的同学们聚在一起,基于原有注册中心的运营和设计经验,通过内部开源的方式,协同共建了可以支撑百万级服务体量的注册中心——北极星。
新的注册中心上线,但是大量的存量服务节点在老注册中心上,业务团队需要进行渐进式迁移,但是对于业务团队来说,不希望有任何的代码改造,否则这个迁移过程成本太高。因此北极星支持以下零代码改造的的渐进式迁移方式:
对于Java类应用,北极星通过提供JavaAgent的方式,支持已迁移的服务通过零改造的方式,进行双注册和双发现,同时存量注册中心也有全量的服务,未迁移服务也可发现已迁移服务。
对于非Java类应用,北极星通过插件化提供协议兼容能力,兼容已有注册中心的接口,新服务变更一下注册中心地址即可实现迁移。同时,为了解决未迁移服务访问已迁移服务的问题,通过扩展存量注册中心的方式,实现存量服务数据的单向同步以及对已迁移服务的增量拉取。
北极星在2018年底开始进行设计和开发,在2019年初上线,到现在已运营了超过3年时间,支撑了腾讯内部不同形态的业务接入,也经历过大大小小的运营活动的洗礼,架构和稳定性也得到了打磨。从技术设计上,北极星解决了业界注册中心存在的单集群性能,水平扩展的问题,同时产品形态上也有自己的一些思考,下文会对这部分内容进行分享:
注册中心关键的点是服务数据的一致性,根据一致性模型,注册中心会分为2类:
注册中心集群中所有节点的数据都保证强一致,客户端从集群中同一时刻任意一个节点获取到的数据都是相同的。强一致性集群中,各个节点有自己的角色,一般分为leader和follower。leader统一协调数据的写入与同步,follower负责处理客户端的读请求,常见的数据一致性协议有Zab,Raft等。
zookeeper和consul都属于强一致性注册中心,其局限点在于单集群的写性能受制于一致性协议,必须等待leader写入成功且集群中大多数的follower都同步成功,才完成一次写操作。当leader节点网络故障,需要重新选主,通常耗时30~120秒,选举期间集群不提供服务,这不符合可用性原则。
注册中心集群所有节点都是无状态对等的,服务数据可以在任意节点写入,写入后通过远程请求同步到集群其他节点。客户端从集群中同一时刻不同节点获取到的数据可能会存在差异,但是最终会保持一致。
eureka和nacos都属于最终一致性注册中心,其局限点在于集群每个节点都参与服务数据写入、同步、以及读取,计算存储合一。单集群的读性能会受到写操作的影响,写操作过于频繁引起高负载问题,对读操作性能存在影响。
考虑到作为注册中心,可用性、性能和吞吐量是比较关键的指标,短期的数据不一致是可以忍受的,因此在架构模型上,北极星采用的是最终一致性的架构模型。
腾讯内部服务调用链路是一个网状结构,各个BG、各个业务之间都存在相互调用,比如游戏业务需要对接支付系统,视频业务需要对接存储系统等。为了简化用户的使用方式,北极星集群需要支撑内部所有的服务接入,以及随着业务的发展,集群的性能也要可以支持水平扩展。
因此,北极星采用计算存储分离的架构,控制面代码拆分成cache(缓存)层和store(存储)层,cache层提供高性能缓存的能力,store层负责对接后端存储,支持关系数据库和本地磁盘;客户端及控制台把服务数据注册到store层,store层通过异步的方式将数据写入数据库或者磁盘;cache层订阅到变更后,从store层增量拉取服务数据,并更新本地缓存;客户端直接从cache层获取到所需的服务数据。控制面cache本身无状态,这意味着对于读写来说都能很好的水平扩展,在腾讯内部,从小规模集群(万级以下服务)到大规模公共集群(百万级服务)都有着生产案例。
为了提升控制面性能,观察到大部分注册中心在服务发现过程中,都有着模型转换和编解码的过程,这一过程CPU基本都是消耗在了protobuf的编解码动作中(占70%)。因此,基于空间换时间的思想,我们在公共服务缓存基础上,增加了协议层热点缓存,对返回的编码后的数据进行复用,省去编解码过程的损耗;经现网实际数据验证,协议层缓存命中率高达98%,性能相比之前提升了一倍,同时也优于同类注册中心。
我们针对北极星控制面进行了压测,对于不同规格下的北极星三节点集群、eureka集群、consul集群,压测数据如下:
通过对北极星的注册以及发现功能,从接口层到存储层全调用链路的优化,从最终的压测结果可以看出:
针对企业不同的微服务规模,北极星提供3种形态的集群组网:
-大规模集群组网,按功能拆分集群部署,可支持百万级的服务接入; -小规模集群组网,全功能对等集群,可支持十万级以下服务接入; -单机版本,提供轻量化的单机版本,供开发人员本地测试联调使用。
北极星控制面是支持模块化组装的,各部分功能和接口,比如注册、发现、配置等,可以通过单独开关控制开启和关闭。为了提升功能的可用性,实现故障隔离,腾讯内部实践中,对北极星按照功能模块进行集群拆分,服务注册、服务发现、控制台操作划分为不同集群来进行处理,各集群相互独立,可按照请求量独立扩展。客户端通过埋点集群的二次寻址机制,为接口寻址到目标集群进行功能接口的调用。
大规模集群部署流程相对比较复杂,且耗费资源较多,为了简化部署过程,北极星支持全功能集群,可支撑万级以下级别的服务接入。每个北极星节点都是具备了全部的功能,如果当前北极星集群的负载高的话,只需对北极星集群执行水平扩容操作。
开发人员在程序开发联调过程中,往往需要依赖注册中心,而依赖公共集群注册中心容易产生环境冲突,导致联调结果不准确。北极星也提供单机版能力,开发人员可以在桌面机启动一个个人独占全功能的北极星注册中心进行调测,调测完可随时销毁。
早期的注册中心,如zookeeper, eureka,只提供了服务注册和发现功能,对于简单的服务调用来说是比较适合的。但是在应用的整个生命周期中,服务调用往往涉及复杂的调度场景,比如在腾讯内部,应用测试阶段需要涉及多测试环境的隔离、在发布阶段需要进行灰度发布和金丝雀发布,在生产环境需要进行基于生产流量的A/B测试。这时候就需要基于请求特性进行流量调度的能力,将服务流量进行精细化导入到对应的服务分组中,也就是现在常说的服务网格。
要实现服务网格,除需要依赖注册中心下发服务数据外,还需要进行网格规则(流量的调度策略等)的管理下发。此外,应用自身的运行所需要的业务配置信息,也需要依赖配置中心进行管理及订阅下发。
为了简化用户对服务网格的使用,业界像consul 2.0提供了注册中心、配置中心和服务网格的解决方案。与consul类似,北极星控制面将注册中心、服务网格、配置中心功能进行了整合,提供了一体式的服务网格控制面,同时也提供异构数据面及业界主流框架(Spring Cloud,gRPC,TARS等)扩展,便于应用集成。
腾讯内部服务架构的发展经历了从单体到分布式再到微服务的发展历程,而服务架构的核心组件注册中心,也经历了从L5单体注册中心,到多注册中心共存,最终统一到北极星的发展历程。
北极星作为腾讯现阶段的企业级注册中心,支撑了腾讯万级服务,以及百万服务实例的日常业务请求调用。根据业务不同的需要,支持大规模集群、小规模集群、单机版等多种部署形态,在运营过程中经过多次的迭代优化,注册和发现性能均优于同等规格的开源注册中心。
北极星已对外开源(开源后名字为Polaris Mesh),支持Spring Cloud、Spring Boot、gRPC、dubbo等主流框架应用直接接入,
PolarisMesh 是腾讯开源的百万级服务发现和治理中心,积累了腾讯从虚拟机到容器时代的分布式服务治理经验。作为分布式和微服务架构中的核心组件,PolarisMesh 提供服务寻址、流量调度、故障容错和访问控制等一系列能力,在K8s 和虚拟机环境中可以无差别使用,支持主流的开发模式,兼容grpc、spring cloud和servicemesh等开源生态,帮助用户快速构建扩展性强、可用性高的业务架构,实现从传统架构到云原生架构的转型。
当某个服务下部署了多个应用程序副本时,一个请求如何发送给某一副本上进行处理呢?当应用部署于kubernetes时,请求流量先经过Service,再由Service将流量转发至某一个Pod进行处理;而应用部署在VM或者物理机时,会额外部署一个用于流量转发的组件,请求将先经过该组件,再由该组件将流量转发至某一台机器进行请求处理。
上述请求调用的方式,都是将所有的流量通过LVS等四层负载均衡组件进行转发,即某个服务下的所有服务实例都挂载到一个VIP(Virtual IP)下,所有的请求都先经过此VIP,再由VIP转发,但是这种方式使用起来会遇到以下几个明显的问题:
如果要解决VIP所带来的问题,那么通用的解决方式,就是去除负载均衡组件到业务进程这一跳的网络请求链路,直接将主调方的请求流量发送至对应业务进程进行处理。这种思路存在两种解决方案。
在上文提到的filebeat上报ES、组件Proxy统一接入地址、数据库主备节点统一接入地址等几个场景中,由于涉及到开源及第三方组件,这些组件无法通过改造的方式集成注册中心SDK,因此无法实施侵入式方案,只能通过无侵入式的方案来解决,下面我们看看如何实现无侵入的内网DNS方案。
上述所提到的内网DNS方案,主要有集中式和分布式两种实现方式,北极星对这两种方式均提供支持。下面向大家分享在前期北极星对于这两种方案的设计思考。
集中式DNS模式,需要北极星服务端实现DNS协议,然后需要修改业务进程所在环境的/etc/resolv.conf 配置,将北极星服务端所在IP作为第一个 nameserver 地址即可,这样业务进程所有的DNS请求都将发往北极星服务端,北极星服务端会根据域名解析出对应的服务以及命名空间,将相关实例地址信息数据进行DNS回包。
可见,在时间驱动型场景中,相比执行内容而言,业务更关注的任务是定时执行还是周期执行、执行具体时间点准确性、周期频率的长短等时间因素。
分布式DNS模式,则是通过在业务进程所在环境运行 polaris-sidecar 进程,然后需要修改业务进程所在环境的/etc/resolv.conf 配置,将 127.0.0.1 所在IP作为第一个 nameserver 地址,这样业务的DNS请求将全部由polaris-sidecar进程代为处理,polaris-sidecar在将自身缓存的服务地址列表进行DNS回包。polaris-sidecar 自身的服务地址信息缓存会定时和北极星服务端进行同步,从而拉取服务的最新实例地址列表。
这里再给出一个北极星对于集中式DNS与分布式DNS,在能力支持上的对比表格:
功能 | 集中DNS | 分布式DNS |
---|---|---|
服务发现 | 支持 | 支持 |
服务注册 | 不支持 | 不支持 |
标签路由 | 不支持 | 支持 |
就近路由 | 不支持 | 支持 |
故障熔断 | 支持 | 支持 |
限流 | 不支持 | 部分支持 |
鉴权 | 不支持 | 支持 |
集中式DNS:DNS请求只能携带域名名称信息,提供的数据信息非常有限,北极星在此基础上只能做到服务发现,无法满足用户就近路由、分区路由等服务治理的需求。
分布式DNS:由于 polaris-sidecar 与业务进程在同一部署环境下,因此可以将一些静态信息注入为环境变量,polaris-sidecar 会将这些环境变量信息作为服务治理所需要的数据,每次处理DNS请求时可以通过这些静态标签执行相应的服务治理流程;因此相比集中式DNS服务发现,分布式DNS方案可以享受到更多的北极星服务治理能力。
选型依据:北极星服务端插件化的设计,要支持集中式DNS的方案十分便捷,仅需要在北极星服务端接入层中添加一个DNS协议的接入插件即可。但是北极星定位是一个服务治理中心,因此北极星在支持DNS协议的服务发现基础之上,同时还希望能将治理能力带入到DNS协议中,因此,最终北极星采用分布式DNS方案实现内网DNS寻址的能力。
PolarisMesh 是腾讯开源的百万级服务发现和治理中心,积累了腾讯从虚拟机到容器时代的分布式服务治理经验。作为分布式和微服务架构中的核心组件,PolarisMesh 提供服务寻址、流量调度、故障容错和访问控制等一系列能力,在K8s 和虚拟机环境中可以无差别使用,支持主流的开发模式,兼容grpc、spring cloud和servicemesh等开源生态,帮助用户快速构建扩展性强、可用性高的业务架构,实现从传统架构到云原生架构的转型。
Eureka是Netflix开源的一款基于Java语言的服务发现框架,2014年发布了第一个版本,现在业界广泛使用的是与Spring Cloud结合的Spring Cloud Neflix的版本。
Eureka主要功能是为应用之间跨进程的RPC调用提供服务注册发现,以及故障实例剔除的功能,其工作原理如下图所示:
Eureka Server本身支持集群化部署,通过推拉结合的方式进行数据的同步,达成最终一致性。
Eureka支持多语言客户端SDK,官方提供了Java版本的SDK(spring-cloud-netflix-eureka-client),社区提供了其他语言的SDK(Go,Python等),应用可以按照各自的开发语言集成客户端从而使用Eureka的功能。
大部分开发者在使用Eureka时都会遇到这样或那样的问题,我们通过收集社区以及用户的反馈,归纳总结为以下3类常见的问题:
从上文的描述可知,Eureka各个server之间是通过异步请求的方式进行写请求的同步,写请求包括注册/反注册/心跳续约的请求,同步失败会进行重试。这种异步同步模式,在客户端集群规模较大、或者网络情况不好触发了重试风暴的情况下,容易因为处理过多的同步续约请求,导致server端高负载。
下面是当时某个客户的一个现网场景,4个Eureka server,跨区进行高可用部署,客户端数有2000+,其中一个区的Eureka server出现网络异常,导致续约的同步请求都重试到其他区的服务端,导致服务端高负载,出现大量请求超时,超时情况下会继续重试,从而导致高负载问题蔓延到其他区。
下图是当时的一个监控截图,在1小时内,服务端平均负载飙升到80%,续约请求的时延也出现10s的峰值,导致大量服务健康状态出现异常,严重影响了现网的服务运营质量。
下图是Eureka的运维控制台,大部分用户反馈该控制台在实际使用过程中存在以下问题,从而影响了运维效率:
在2018年,Eureka社区发了通告,停止对Eureka 2.0的开源更新工作,对于使用Eureka的现网业务来说,增加了运维风险和成本。
北极星是腾讯开源的服务发现和治理组件,在服务注册发现基础上,提供了流量调度,故障剔除等治理能力,其功能可完整覆盖Eureka的使用场景。同时北极星提供了存算分离的架构,以及全功能的服务治理控制台。在性能和可运维性上相比Eureka都有不少的提升。
相当一部分使用Eureka的用户,希望可以接入北极星,以解决他们在使用eureka过程中出现的问题。但是,他们接入过程中,遇到以下几个阻碍:
针对问题一:北极星在新版本1.5.0实现了Eureka Server API的全兼容,做到支持各个语言、不同版本的Eureka Client进行直接接入。
北极星服务端基于一套开放的服务治理模型来实现各个治理功能,在API层,提供了插件化的机制来进行其他服务模型的对接转换。为满足Eureka的兼容性需求,只需要在API层实现一个对接插件即可完成:
针对问题二:北极星在服务端通过服务数据单向同步,以及关联查询的方式,实现了新老服务的互访,用户可以按自己的节奏将服务从Eureka注册中心迁移到北极星。
用户将服务数据迁移到北极星后,可以解决上文描述的使用的Eureka Server所碰到的3个问题:
通过监控曲线可以看到,北极星(Eureka)的整体CPU使用情况稳定在2.29核,并且整体的CPU利用率稳定在57%左右(5W注册实例,并发进行全量数据拉取及实时心跳续约)
并且,当实例规模从1w到5w中,CPU的利用率都是稳定速率增加的,没有出现说CPU利用率陡然增加,整体的内存利用了也没有出现大幅度的变动。
在注册规模进一步提升时,由于北极星设计之初就是存算彻底分离的架构设计,因此当北极星出现高负载时,可以快速的对北极星进行水平扩容。
从控制台的易用性来看,北极星控制台经过腾讯内部的多次打磨,在易用性上对用户更容易上手,且可支持多种的查询条件方便进行各种服务信息的检索。同时北极星本身带有全功能的服务治理能力,可以服务设置服务治理规则,实现包含Eureka客户端在内的多种框架的统一服务治理。
北极星开源社区当前由腾讯主导维护,正常每月发布1个迭代版本,每双周会进行开发者例会的技术分享,BUG问题在社区提问后,不超过1周时间会得到回复及修复。详情可了解:https://github.com/polarismesh
PolarisMesh 是腾讯开源的百万级服务发现和治理中心,积累了腾讯从虚拟机到容器时代的分布式服务治理经验。作为分布式和微服务架构中的核心组件,PolarisMesh 提供服务寻址、流量调度、故障容错和访问控制等一系列能力,在 K8s 和虚拟机环境中可以无差别使用,支持主流的开发模式,兼容 grpc、spring cloud 和 servicemesh 等开源生态,帮助用户快速构建扩展性强、可用性高的业务架构,实现从传统架构到云原生架构的转型。
腾讯云高级研发工程师
腾讯北极星(PolarisMesh)开源项目、弹性微服务引擎 TSE 核心研发,10 + 年从业经验,从事云计算及中间件 7 年有余。热爱开源、崇尚技术,希望能够使用技术使软件的应用变得简单、高效和美好。
PolarisMesh 以服务注册中心为基础,提供了服务注册发现,健康检查等能力;同时扩展了服务治理控制面,支持了流量调度(动态路由、负载均衡)、熔断降级、访问控制(限流)等功能。
在客户端接入上,PolarisMesh 提供了多语言 Proxyless(Go,Java,C++)以及 Sidecar(envoy,Java agent)的接入方式,供不同形态的应用进行接入。同时,PolarisMesh 也提供了生态组件,以提升已使用了生态框架的应用的接入效率,下文主要介绍的是 Go 语言相关的生态组件(gRPC-Go)的使用及实现。
在 Go 语言社区,发展最成熟,使用最广泛的 RPC 框架就是 gRPC-Go(https://github.com/grpc/grpc-go)。
gRPC 是一个高性能的二进制 RPC 框架,通过统一定义的 RPC 服务描述,配合多语言的 SDK,可以轻松实现跨语言的 RPC 调用。
gRPC-Go 是 gRPC 框架的 Go 语言的实现,核心层提供在 RPC 调用过程中的寻址,消息编解码,网络收发、连接管理,故障重试等功能,并且可以通过插件的方式,扩展服务发现、负载均衡、鉴权以及链路跟踪等服务治理的高级能力:
然而,假如将 gRPC 作为一个服务框架使用的话,以当前的能力,部分服务治理相关的场景还是满足不了。比如:
就近路由场景 :用户希望应用可以进行就近访问,以降低链路时延,同时在区域故障的时候,可以进行跨区的容灾切换
故障熔断场景 : 在节点出现 down 机时,能对故障节点进行及时剔除以避免雪崩效应
优雅下线场景 : 为了保服务调用成功率不受节点裁撤影响,节点在裁撤前,需要先进行流量的剔除,再进行裁撤下线
流量限制场景 : 在应用进行营销活动时,为了保障活动的进场流量在预估范围之内,需要将异常流量通过全局方式进行限制。
为解决上述这些服务治理相关的场景诉求,需要将 PolarisMesh 的能力与 gRPC 进行整合,是的 gRPC 能真正成为一个全功能的服务框架。
PolarisMesh 通过插件扩展的方式,将 go 语言客户端(polaris-go)与 gRPC-Go 进行整合,整合后的整体架构如下图所示:
扩展后 gRPC-Go 服务调用和核心流程如下:
需要指定 “polaris://EchoServerGRPC/” 以明确使用 PolarisMesh 进行服务发现
import (
"google.golang.org/grpc"
polaris "github.com/polarismesh/grpc-go-polaris"
)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
conn, err := grpc.DialContext(ctx, "polaris://EchoServerGRPC/", grpc.WithInsecure(),
grpc.WithDefaultServiceConfig(polaris.LoadBalanceConfig))
if err != nil {
log.Fatal(err)
}
defer conn.Close()
//使用conn正常进行gRPC客户端方法调用
echoClient := pb.NewEchoServerClient(conn)
echoClient.Echo(ctx, &pb.EchoRequest{Value: value})
需要使用 polaris.NewServer 以明确使用 PolarisMesh 作为服务注册中心
import (
"google.golang.org/grpc"
polaris "github.com/polarismesh/grpc-go-polaris"
)
srv := grpc.NewServer()
pb.RegisterEchoServerServer(srv, &EchoService{})
address := fmt.Sprintf("0.0.0.0:%d", listenPort)
listen, err := net.Listen("tcp", address)
if err != nil {
log.Fatalf("Failed to addr %s: %v", address, err)
}
// 执行北极星的注册命令
pSrv, err := polaris.Register(srv, listen, polaris.WithServerApplication("EchoServerGRPC"))
if nil != err {
log.Fatal(err)
}
go func() {
c := make(chan os.Signal)
signal.Notify(c)
s := <-c
log.Printf("receive quit signal: %v", s)
// 执行北极星的反注册命令
pSrv.Deregister()
srv.GracefulStop()
}()
err = srv.Serve(listen)
if nil != err {
log.Printf("listen err: %v", err)
}
快速入门样例请参考:https://github.com/polarismesh/grpc-go-polaris/tree/main/examples/quickstart
除了 gRPC-Go 以外,PolarisMesh 还对接了其他的开源框架,助力这些框架能获取到全功能的服务治理能力,以下为已完成对接的框架以及示例:
grpc-go:https://github.com/polarismesh/grpc-go-polaris
dubbo-go:https://github.com/apache/dubbo-go/tree/master/registry/polaris
go-zero:https://github.com/zeromicro/zero-contrib/tree/main/zrpc/registry/polaris
GoFrame:https://github.com/gogf/polaris
grpc-java-polaris:https://github.com/polarismesh/grpc-java-polaris
spring-cloud-tencent:https://github.com/Tencent/spring-cloud-tencent
本文转载自:infoQ
9 月 8 日,腾讯云面向所有开发者,正式宣布开源北极星(Polaris Mesh),开放了应用在大规模生产环境中的源代码,推进以微服务为核心的开源生态建设,并希望帮助业界更好地进行分布式或者微服务架构转型。
目前很多企业在微服务实施和演化过程中,都会面临技术栈的多样性问题。整个微服务领域逐渐沉淀出了无数个相关组件,大家在选择上更加困难,也为企业的基础设施建设不断带来挑战。腾讯也曾面临这样的痛点,因此从 2019 年开始腾讯开创了统一的微服务解决方案“北极星”(Polaris Mesh),通过北极星对这些组件进行抽象和整合,打造公司标准化的服务发现和治理方案,帮助业务提升研发效率和运营质量。
经过两年的发展,北极星在腾讯内部注册服务数量超过百万,服务实例数量超过五百万,接口日调用量超过三十万亿,腾讯音乐、腾讯视频、腾讯会议、腾讯文档、企业微信、微信支付和王者荣耀等重点产品均在使用。
9 月 8 日,腾讯云面向所有开发者,正式宣布开源北极星(Polaris Mesh),开放了应用在大规模生产环境中的源代码,推进以微服务为核心的开源生态建设,并希望帮助业界更好地进行分布式或者微服务架构转型。
最近十几年,业务架构经历了从单体到分布式再到微服务的演进。单体架构的所有代码都在一个应用中,适合小规模或者初创期的业务。如果应用模块和开发人员的数量很少,单体架构容易开发、测试、部署和伸缩。随着应用模块和开发人员增加,单体架构面临众多问题,例如:
任何修改都需要重新编译和部署整个系统,变更风险大,测试成本高,编译速度慢。 如果某个业务模块存在缺陷,也会影响其他业务模块,降低整个系统的可用性。 如果每个业务模块的请求量不均匀,无法针对某些热点模块进行水平扩展。 为了解决这些问题,分布式和微服务架构将业务模块拆分成为独立的服务,但是整个系统的复杂度也急剧上升,如果没有配套的技术组件,分布式和微服务架构很难落地。作为微服务方向的开发人员,我们都知道服务发现和治理是分布式和微服务架构中的关键技术,可以很好的帮助大家解决服务寻址、流量调度、故障容错、访问控制和可观测性等问题,但这个关键技术目前在业界的开源解决方案却各有利弊,并不完美。
我们认为上述三种方案各有优劣,不是谁取代谁的问题,而是互相融合,满足不同的业务需求。腾讯内部绝大部分核心业务使用第一种方案,也有不少业务在 Kubernetes 上使用其他两种方案,但是依然存在跨部门业务系统间数据无法打通、缺少标准化的服务治理的问题。
为了能够融合上述三种解决方案的优点,同时规避它们的缺点,我们开创了统一的解决方案——北极星,致力于打造腾讯新一代服务发现和治理中心,解决原有平台存在的问题,并且支持无缝迁移,实现公司服务的互联互通和统一治理。目前,北极星的注册服务数量超过百万,服务实例数量超过五百万,接口日调用量超过三十万亿,腾讯音乐、腾讯视频、腾讯会议、腾讯文档、企业微信、微信支付和王者荣耀等重点业务均在使用。
北极星(Polaris Mesh)是腾讯自研的服务发现和治理中心,以服务注册中心为基础,扩展了服务治理功能以及相应的控制面,提供多语言的客户端实现,不同的开发框架可以集成使用。随着容器化和云原生的推进,北极星也支持了 Kubernetes 服务和网格 Sidecar 的自动接入,实现了它们之间互联互通和统一治理。
北极星主要有五大功能:
北极星系统组件分为核心和生态两个部分:
为了降低业务的使用成本,北极星提供三种类型的生态组件。第一类用于各种开发框架和北极星数据面的无缝集成,框架用户不需要直接调用北极星数据面,减少开发的侵入性;第二类用于各种网关和北极星数据面的无缝集成,支持网关将请求直接转发到北极星服务;第三类生态组件只有 polaris-controller,支持 Kubernetes 服务和网格 Sidecar 的自动接入。
目前,腾讯常用的框架、网关和容器平台已经集成北极星,形成了以北极星为核心的服务发现和治理体系。下面介绍北极星在腾讯的最佳实践:
第一,作为公司统一的服务发现平台,实现公司内网服务的互联互通。北极星采用计算和存储分离的架构,计算层可以随着客户端数量的增加平行扩展,轻松支持百万级客户端接入。同时服务端提供同城多中心或者跨城多中心等多种部署模式,满足不同的容灾要求。
第二,为不同的开发语言和框架提供统一的服务发现和治理功能。腾讯业务线众多,开发语言和框架也众多,北极星数据面支持多语言 SDK 和 Sidecar 两种模式。框架可以直接集成相应语言的 SDK,不需要部署 Sidecar,不会增加运维成本,没有性能和资源损耗。
第三,作为网关到内网服务的连接器。网关可以集成北极星,将请求直接转发到北极星服务,实现微服务网关的能力。
第四,现有的开源组件主要分为两个体系,一个围绕服务注册中心和开发框架打造,一个围绕 Kubernetes 服务和网格打造。两个体系各自有各自的亮点和局限,随着容器化和云原生的推进,越来越多企业同时使用两个体系。但是两个体系的实现存在割裂,给业务增加了不必要的使用成本。北极星对两个体系进行了融合,为虚拟机和容器环境、开发框架和网格提供一体化的服务发现和治理方案。
北极星客户端可以集成到各种框架中,让裸的开发框架快速升级为分布式和微服务框架,具备完整的服务发现和治理功能。
腾讯业务常用的框架均已集成北极星,其中除了自研框架,还有 gRPC、Spring 和 Gin 等开源框架。如上所述,这些集成也会作为北极星的生态组件开源,框架用户可以直接引入,逻辑代码不需要任何改动。
网关和框架的情况类似,北极星也可以和常见的开源网关集成使用。
随着容器化和云原生的推进,越来越多企业开始使用 Kubernetes 部署服务,腾讯也不例外。
在 Kubernetes 环境上,除了注册中心和框架,还有两种服务发现和治理方案:
北极星提供 polaris-controller,支持 Kubernetes 服务和网格 Sidecar 自动注入,实现三种方案的联通和统一治理。
北极星是在满足腾讯业务需求的过程中,不断演进和发展起来的,积累了腾讯超大规模服务发现和治理的经验,没有一个开源组件的形态和北极星完全类似。腾讯的业务线众多,包含即时通信、音乐视频、金融科技和企业服务等,北极星面临的问题和相应的解决方案具有很强的通用性。我们相信北极星也可以帮助其他企业更好地进行分布式或者微服务架构转型,提高业务的研发效率和运营质量。
北极星开源版本直接来自腾讯的生产代码,我们已经将主体部分提交到社区。期待更多感兴趣、有能力的开发者参与共建,后续计划包括但不限于:
截至目前,腾讯共对外开源超过 130 个优质项目,代码贡献者超过 2000 人,开源项目 star 总数超过 37 万个。北极星作为微服务领域新推出的开源项目,也非常欢迎感兴趣的小伙伴在北极星 Github 上提交 issue 与 PR 进行讨论和贡献,或加入北极星社区群参与社区讨论。
北极星 GitHub:https://github.com/polarismesh/polaris
北极星官网地址:https://polarismesh.cn/