写点什么

vivo 自研鲁班分布式 ID 服务实践

  • 2023-06-29
    广东
  • 本文字数:5505 字

    阅读完需:约 18 分钟

作者:vivo IT 平台团队- An Peng


本文介绍了什么是分布式 ID,分布式 ID 的业务场景以及 9 种分布式 ID 的实现方式,同时基于 vivo 内部 IT 的业务场景,介绍了自研鲁班分布式 ID 服务的实践。

一、方案背景

1.1 分布式 ID 应用的场景

随着系统的业务场景复杂化、架构方案的优化演进,我们在克服问题的过程中,也总会延伸出新的技术诉求。分布式 ID 也是诞生于这样的 IT 发展过程中,在不同的关联模块内,我们需要一个全局唯一的 ID 来让模块既能并行地解耦运转,也能轻松地进行整合处理。以下,首先让我们一起回顾这些典型的分布式 ID 场景。

1.1.1 系统分库分表

随着系统的持续运作,常规的单库单表在支撑更高规模的数量级时,无论是在性能或稳定性上都已经难以为继,需要我们对目标逻辑数据表进行合理的物理拆分,这些同一业务表数据的拆分,需要有一套完整的 ID 生成方案来保证拆分后的各物理表中同一业务 ID 不相冲突,并能在后续的合并分析中可以方便快捷地计算。


以公司的营销系统的订单为例,当前不但以分销与零售的目标组织区别来进行分库存储,来实现多租户的数据隔离,并且会以订单的业务属性(订货单、退货单、调拔单等等)来进一步分拆订单数据。在订单创建的时候,根据这些规则去构造全局唯一 ID,创建订单单据并保存在对应的数据库中;在通过订单号查询时,通过 ID 的规则,快速路由到对应的库表中查询;在 BI 数仓的统计业务里,又需要汇总这些订单数据进行报表分析。

1.1.2 系统多活部署

无论是面对着全球化的各国数据合规诉求,还是针对容灾高可用的架构设计,我们都会对同一套系统进行多活部署。多活部署架构的各单元化服务,存储的单据(如订单/出入库单/支付单等)均带有部署区域属性的 ID 结构去构成全局唯一 ID,创建单据并保存在对应单元的数据库中,在前端根据单据号查询的场景,通过 ID 的规则,可快速路由到对应的单元区域进行查询。对应多活部署架构的中心化服务,同步各单元的单据数据时,单据的 ID 是全局唯一,避免了汇聚数据时的 ID 冲突。


在公司的系统部署中,公共领域的 BPM 、待办、营销领域的系统都大范围地实施多活部署。

1.1.3 链路跟踪技术

在微服务架构流行的大背景下,此类微服务的应用对比单体应用的调用链路会更长、更复杂,对问题的排查带来了挑战,应对该场景的解决方案,会在流量入口处产生全局唯一的 TraceID,并在各微服务之间进行透传,进行流量染色与关联,后续通过该全局唯一的 TraceID,可快速地查询与关联全链路的调用关系与状态,快速定位根因问题。


在公司的各式各样的监控系统、灰度管理平台、跨进程链路日志中,都会伴随着这么一个技术组件进行支撑服务。

1.2 分布式 ID 核心的难点

  • 唯一性:保持生成的 ID 全局唯一,在任何情况下也不会出现重复的值(如防止时间回拔,时钟周期问题)。

  • 高性能:ID 的需求场景多,中心化生成组件后,需要高并发处理,以接近 0ms 的响应大规模并发执行。

  • 高可用:作为 ID 的生产源头,需要 100%可用,当接入的业务系统多的时候,很难调整出各方都可接受的停机发布窗口,只能接受无损发布。

  • 易接入:作为逻辑上简单的分布式 ID 要推广使用,必须强调开箱即用,容易上手。

  • 规律性:不同业务场景生成的 ID 有其特征,例如有固定的前后缀,固定的位数,这些都需要配置化管理。

1.3 分布式 ID 常见的方案

常用系统设计中主要有下图 9 种 ID 生成的方式:



1.4 分布式 ID 鲁班的方案

我们的系统跨越了公共、生产制造、营销、供应链、财经等多个领域。在分布式 ID 诉求下还有如下的特点

  • 在业务场景上除了常规的 Long 类型 ID,也需要支持“String 类型”、“MixId 类型”(后详述)等多种类型的 ID 生成,每一种类型也需要支持不同的长度的 ID。

  • 在 ID 的构成规则上需要涵盖如操作类型、区域代理等业务属性的标识;需要集中式的配置管理。

  • 在一些特定的业务上,基于安全的考虑,还需要在尾部加上随机数来保证 ID 不能被轻易猜测。


综合参考了业界优秀的开源组件与常用方案均不能满足,为了统一管理这类基础技术组件的诉求,我们选择基于公司业务场景自研一套分布式 ID 服务:鲁班分布式 ID 服务

二、系统架构

2.1 架构说明

三、 设计要点

3.1 支持多种类型的 ID 规则

目前鲁班分布式 ID 服务共提供"Long 类型"、“String 类型”、“MixId 类型”等三种主要类型的 ID,相关 ID 构成规则与说明如下:

3.1.1 Long 类型

(1)构成规则

静态结构由以下三部分数据组成,组成部分共 19 位

  • 固定部分(4 位):由 FixPart+ServerPart 组成。

① FixPart(4 位):由大区 zone 1 位/代理 agent 1 位/项目 project 1 位/应用 app 1 位,组成的 4 位数字编码。

② ServerPart(4 位):用于定义产生全局 ID 的服务器标识位,服务节点部署时动态分配。

  • 动态部分 DynPart(13 位):System.currentTimeMillis()-固定配置时间的 TimeMillis (可满足使用 100 年)。

  • 自增部分 SelfIncreasePart(2 位):用于在全局 ID 的客户端 SDK 内部自增部分,由客户端 SDK 控制,业务接入方无感知。共 2 位组成。


(2)降级机制

主要自增部分在服务器获取初始值后,由客户端 SDK 维护,直到自增 99 后再次访问服务端获取下一轮新的 ID 以减少服务端交互频率,提升性能,服务端获取失败后抛出异常,接入业务侧需介入进行处理。


(3)样例说明

3.1.2 String 类型

(1)构成规则

静态结构由以下五部分数据组成,组成部分共 25~27 位

  • 固定部分操作位 op+FixPart(9~11 位)

① 操作位 op(2~4 位):2~4 位由业务方传入的业务类型标识字符。

② FixPart(7 位):业务接入时申请获取,由大区 zone 1 位,代理 agent 2 位,项目 project 2 位,应用 app 2 位组成。

  • 服务器标识部分 ServerPart(1 位): 用于定义产生全局 ID 的服务器标识位,服务节点部署时动态分配 A~Z 编码。

  • 动态部分 DynPart(9 位):System.currentTimeMillis()-固定配置时间的 TimeMillis ,再转换为 32 进制字符串(可满足使用 100 年)。

  • 自增部分 SelfIncreasePart(3 位):用于在全局 ID 的客户端 SDK 内部自增部分,由客户端 SDK 控制,业务接入方无感知。

  • 随机部分 secureRandomPart(3 位):用于在全局 ID 的客户端 SDK 的随机部分,由 SecureRandom 随机生成 3 位 0-9,A-Z 字母数字组合的安全随机数,业务接入方无感知。


(2)降级机制

主要自增部分由客户端 SDK 内部维护,一般情况下只使用 001–999 共 999 个全局 ID。也就是每向服务器请求一次,都在客户端内可以自动维护 999 个唯一的全局 ID。特殊情况下在访问服务器连接出问题的时候,可以使用带字符的自增来做服务器降级处理,使用产生 00A, 00B... 0A0, 0A1,0A2....ZZZ. 共有 36 * 36 * 36 - 1000 (999 纯数字,000 不用)= 45656 个降级使用的全局 ID


(3)样例说明

3.1.3 MixId 类型

(1)构成规则

静态结构由以下三部分数据组成,组成部分共 17 位

  • 固定部分 FixPart(4~6 位)

① 操作位 op(2~4 位):2~4 位由业务方传入的业务类型标识字符

② FixPart(2 位):业务接入时申请获取由代理 agent 2 位组成。

  • 动态部分 DynPart(6 位): 生成 ID 的时间,年(2 位)月(2 位)日(2 位)。

  • 自增部分 SelfIncreasePart(7 位):用于在全局 ID 的客户端 SDK 内部自增部分,由客户端 SDK 控制,业务接入方无感知。


(2)降级机制

无,每次 ID 产生均需到服务端请求获取,服务端获取失败后抛出异常,接入业务侧需介入进行处理。


(3)样例说明

3.2 业务自定义 ID 规则实现

鲁班分布式 ID 服务内置“Long 类型”,“String 类型”,“MixId 类型”等三种长度与规则固定的 ID 生成算法,除以上三种类型的 ID 生成算法外,业务侧往往有自定义 ID 长度与规则的场景诉求,在鲁班分布式 ID 服务内置 ID 生成算法未能满足业务场景时,为了能在该场景快速支持业务,鲁班分布式 ID 服务提供了业务自定义接口并通过 SPI 机制在服务运行时动态加载,以实现业务自定义 ID 生成算法场景的支持,相关能力的实现设计与接入流程如下:


(1)ID 的构成部分主要分 FixPart、DynPart、SelfIncreasePart 三个部分。


(2)鲁班分布式 ID 服务的客户端 SDK 提供 LuBanGlobalIDClient 的接口与 getGlobalId(...)/setFixPart(...)/setDynPart(...)/setSelfIncreasePart(...)等四个接口方法


(3)业务侧实现 LuBanGlobalIDClient 接口内的 4 个方法,通过 SPI 机制在业务侧服务进行加载,并向外暴露出 HTTP 或 DUBBO 协议的接口。


(4)用户在鲁班分布式 ID 服务管理后台对自定义 ID 生成算法的类型名称与服务地址信息进行配置,并关联需要使用的 AK 接入信息。


(5)业务侧使用时调用客户端 SDK 提供的 LuBanGlobalIDClient 的接口与 getGlobalId 方法,并传入 ID 生成算法类型与 IdRequest 入参对象,鲁班分布式 ID 服务接收请求后,动态识别与路由到对应 ID 生产算法的实现服务,并构建对象的 ID 返回给客户端,完成整个 ID 生成与获取的过程。

3.3 保证 ID 生成不重复方案

3.4 ID 服务无状态无损管理

服务部署的环境在虚拟机上,ip 是固定,常规的做法是在配置表里配置 ip 与机器码的绑定关系(这样在服务扩缩容的时候就需要人为介入操作,存在一定的遗漏配置风险,也带来了一定的运维成本),但在容器的部署场景,因为每次部署时 IP 均是动态变化的,以前通过配置表里 ip 与机器码的映射关系的配置实现方式显然不能满足运行在容器场景的诉求,故在服务端设计了通过心跳上报实现机器码动态分配的机制,实现服务端节点 ip 与机器码动态分配、绑定的能力,达成部署自动化与无损发布的目的。


相关流程如下:


【注意】

服务端节点可能因为异常,非正常地退出,对于该场景,这里就需要有一个解绑的过程,当前实现是通过公司平台团队的分布式定时任务服务,检查持续 5 分钟(可配置)没有上报心跳的机器码分配节点进行数据库绑定信息清理的逻辑,重置相关机器码的位置供后续注册绑定使用。

3.5 ID 使用方接入 SDK 设计

SDK 设计主要以"接入快捷,使用简单"的原则进行设计。


(1)接入时:

鲁班分布式 ID 服务提供了 spring-starter 包,应用只需再 pom 文件依赖该 starter,在启动类里添加 @EnableGlobalClient,并配置 AK/SK 等租户参数即可完成接入。


同时鲁班分布式 ID 服务提供 Dubbo & Http 的调用方式,通过在启动注解配置 accessType HTTP/DUBBO 来确定,SDK 自动加载相关依赖。


(2)使用时:

根据"Long"、"String"、"MixId"等三种 id 类型分别提供 GlobalIdLongClientGlobalIdStringClientGlobalIdMixIDClient 等三个客户端对象,并封装了统一的入参 RequestDTO 对象,业务系统使用时只需构建对应 Id 类型的 RequestDTO 对象(支持链式构建),并调用对应 id 类型的客户端对象 getGlobalID(GlobalBaseRequestDTO globalBaseRequestDTO)方法,即可完成 ID 的构建。


Long 类型 Id 获取代码示例:

package com.vivo.it.demo.controller; import com.vivo.it.platform.luban.id.client.GlobalIdLongClient;import com.vivo.it.platform.luban.id.dto.GlobalLongIDRequestDTO;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping; @RequestMapping("/globalId")public class GlobalIdDemoController {     @Autowired    private GlobalIdLongClient globalIdLongClient;     @RequestMapping("/getLongId")    public String getLongId() {        GlobalLongIDRequestDTO globalLongIDRequestDTO = GlobalLongIDRequestDTO.Builder()                .setAgent("1") //代理,接入申请时确定                .setZone("0") //大区,接入申请时确定                .setApp("8") //应用,接入申请时确定                .setProject("7") //项目,接入申请时确定                .setIdNumber(2); //当次返回的id数量,只对getGlobalIDQueue有效,对getGlobalID(...)无效        long longId = globalIdLongClient.getGlobalID(globalLongIDRequestDTO);        return String.valueOf(longId);    }}
复制代码

3.6 关键运行性能优化场景

3.6.1 内存使用优化

在项目上线初时,经常发生 FGC,导致服务停顿,获取 ID 超时,经过分析,鲁班分布式 ID 服务的服务端主要为内存敏感的应用,当高并发请求时,过多对象进入老年代从而触发 FGC,经过排查主要是 JVM 内存参数上线时是使用默认的,没有经过优化配置,JVM 初始化的内存较少,高并发请求时 JVM 频繁触发内存重分配,相关的对象也流程老年代导致最终频繁发送 FGC。


对于这个场景的优化思路主要是要相关内存对象在年轻代时就快速经过 YGC 回收,尽量少的对象进行老年代而引起 FGC。


基于以上的思路主要做了以下的优化:

  • 增大 JVM 初始化内存(-Xms,容器场景里为-XX:InitialRAMPercentage)

  • 增大年轻代内存(-Xmn)

  • 优化代码,减少代码里临时对象的复制与创建

3.6.2 锁颗粒度优化

客户端 SDK 再自增值使用完或一定时间后会向服务端请求新的 id 生成,这个时候需要保证该次请求在多线程并发时是只请求一次,当前设计是基于用户申请 ID 的接入配置,组成为 key,去获取对应 key 的对象锁,以减少同步代码块锁的粒度,避免不同接入配置去在并发去远程获取新的 id 时,锁粒度过大,造成线程的阻塞,从而提升在高并发场景下的性能。

四、 业务应用

当前鲁班分布式 ID 服务日均 ID 生成量亿级,平均 RT 在 0~1ms 内,单节点可支持 万级 QPS,已全面应用在公司 IT 内部营销订单、支付单据、库存单据、履约单据、资产管理编码等多个领域的业务场景。

五、未来规划

在可用性方面,当前鲁班分布式 ID 服务仍对 Redis、Mysql 等外部 DB 组件有一定的依赖(如应用接入配置信息、MixId 类型自增部分 ID 计数器),规划在该依赖极端宕机的场景下,鲁班分布式 ID 服务仍能有一些降级策略,为业务提供可用的服务。


同时基于业务场景的诉求,支持标准形式的雪花算法等 ID 类型。

六、 回顾总结

本文通过对分布式 ID 的 3 种应用场景,实现难点以及 9 种分布式 ID 的实现方式进行介绍,并对结合 vivo 业务场景特性下自研的鲁班分布式 id 服务从系统架构,ID 生成规则与部分实现源码进行介绍,希望对本文的阅读者在分布式 ID 的方案选型或自研提供参考。

发布于: 刚刚阅读数: 2
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020-07-10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
vivo 自研鲁班分布式 ID 服务实践_雪花算法_vivo互联网技术_InfoQ写作社区