100万用户,抢10万优惠券,如何设计?
一、10Wqps抢券系统的功能分析
首先看看,电商系统(Electronic Commerce Systems)的促销手段:
-
优惠券
-
拼团
-
砍价
-
老带新
说到电商平台上,无人不知优惠券体系,它是一种常见的促销方式,所以说, 优惠券是电商常见的营销手段,具有灵活的特点。
优惠券 的功能:在规定的周期内,购买对应商品类型和额度商品时,下单结算时会减免一定金额。
优惠券既可以作为促销活动的载体,也是重要的引流入口。
一般来说,优惠券系统是 商城营销模块中一个重要组成部分,哪怕早在 商城单体应用时代,优惠券就是其中核心模块之一。
在电商系统中 优惠券的种类,主要有:
-
代金券
可以直接抵扣商品金额,不找零;任意金额可用(可能会导致零元单,看业务是否能接受);可以同非商品券叠加
-
满减券
可以抵扣商品金额,有使用门槛;门槛金额需大于优惠金额;可以同非商品券叠加
-
折扣券
可以抵扣商品金额,有使用门槛,需要设置封顶金额;可以同非商品券叠加
-
运费券
不可以抵扣商品金额;只能够抵扣运费;可以同非商品券叠加
在电商系统中 优惠券 涉及到两个核心功能:
(1)发放券
-
谁能领?
所有用户 or 指定的用户
-
领取上限
一个优惠券最多能领取多少张?
-
领取方式
用户主动领取 or 自动发放被动领取
(2)核销券
-
作用范围
商品、商户、类目
-
计算方式
是否互斥、是否达到门槛等
V1.0版本的抢券系统需求拆解
最为简单的一个抢券系统的功能,保护两个部分:
-
tob部分:配置券,会涉及到券批次(券模板)创建,券模板的有效期以及券的库存信息
-
toc部分:发券,会涉及到券记录的创建和管理(过期时间,状态)
因此,我们可以将需求先简单拆解为两部分:
同时,无论是券模板还是券记录,都需要开放查询接口,支持券模板/券记录的查询。
V1.0版本的模块介绍
优惠券系统主要分为tob部分 运营端的运营配置管理模块和toc部分执行端的执行核心模块。
tob部分 优惠券运营配置管理模块提供给运营人员操作、管理券权益,回收数据分析。包括
-
模板库存管理、
-
外部券管理、
-
策略规则管理、
-
数据报表等功能。
toc部分 优惠券执行核心模块包括:
-
优惠券的兑换发放、
-
使用核销、
-
状态流转等功能,
-
提供统一的API接口,供上游各业务系统调用,参与到各产品功能环节。
同时在运行中有完善的运行监控和有效的策略管控能力,提供保障。
优惠券模板管理的设计
优惠券信息的定义是基于对应模板的,一张优惠券一般有这些要素:
-
券的名称描述等信息
-
券的类型
-
应该在什么条件下能使用
-
使用时优惠减免多少。
这些要素被抽象为券模板,在不同模板中定义了券的不同元素。
模板主要包含了两部分信息:模板配置和运营管理。
模板配置主要定义了模板使用的业务配置,比如模板类型、基础信息、发放信息、激活条件、使用条件、展示信息、兑换码管理、外部券管理等。
运营管理是对模板的使用、运行进行的监控统计分析,包含监控告警、数据统计、策略管控。
抢券系统的业务流程设计
优惠券系统提供统一的 API 接口和 MQ 消息,提供配券、发券、查券、用券的能力,接入到上游各业务系统中。
优惠券的使用规则设计
1.叠加规则
-
一般情况下,商品券我们分为商家自有券和平台发放券,都为商品券,平台的自有券是可以和商家的自行发放的券进行叠加的,但是商家券与 商家券不能叠加,平台券和平台券不能叠加,所以有
商家券+平台券
-
商品券和运费券是可以叠加的,那么我们可以知道,叠加规则可以
商家券+平台券+运费券
-
有时候,双十一等大促期间,电商平台还会出一些神券,这些券也是可以和上边的券叠加的,给到用户更多的优惠
商家券+平台券+运费券+神券
2.命中规则
对于用户的优惠券来说,可能有很多,但是最优的搭配只能有一个,所以在选中最优的优惠券时,我们需要有一些排序条件
命中规则考虑的因素比较多
-
金额
金额是用户最关心的一个点,到底能有多大优惠,一定是体现在金额上,所以我们在搭配优惠券时,一定要把金额最大的,排在第一位
-
时间
优惠券都有有效时间,有可能有两张券,他们的面额、使用条件等,都一样,那么过期时间就作为一个排序条件,最先过期的优先
-
类型
可能我们的多张优惠券计算出来的金额都一样,时间也都一样,那么券的类型就很重要了。
一般来说满减券要优先,折扣券次之,代金券最后。
因为满减券有门槛,有最大值,但是折扣券可能封顶的优惠更大,而代金券又没有门槛。
也就是说,满减券是限制最多的,优先使用掉。
-
商品
有些券有商品的适用范围,有些券是全品类的,那么我们优先使用限制商品的券。
-
渠道
有些券限制了APP还是小程序使用,有些是全渠道通用的券,那么优先使用限制渠道的券
优惠卷的生命周期设计
优惠券各状态说明
券状态 | 说明 |
待激活 | 发放给用户时券的初始状态,需用户完成任务或达到一定条件后自动触发激活券,激活后才能使用。 |
待使用 | 发放给用户时券的初始状态;由用户自己激活的券;可直接在各场景下使用。 |
已使用 | 用户已使用的券。 |
已过期 | 发放给用户,直到过期都未使用的券。 |
锁定 | 被锁定的券,在订单流转过程中会出现。比如已下单未付款时,使用的券被锁定。 |
已回收 | 被回收的券,用户不可使用。比如活动资格被取消后,回收已发放的券。 |
V2.0版本的抢券系统需求演进
V1.0版本 优惠券最早和商城耦合在一个系统中
随着商城的不断发展,营销活动力度加大,优惠券使用场景增多,优惠券系统逐渐开始“力不从心”,暴露了很多问题:
-
海量优惠券的发放,达到优惠券单库、单表存储瓶颈。
-
与商城系统的高耦合,直接影响了商城整站接口性能。
-
优惠券的迭代更新受限于商城的版本安排。
-
针对多品类优惠券,技术层面没有沉淀通用优惠券能力。
为了解决以上问题,V2.0优惠券系统进行了系统独立,提供通用的优惠券服务,独立后的系统架构如下:
V2.0优惠券系统覆盖了优惠券的4个核心要点:创、发、用、计。
-
“创”指优惠券的创建,包含各种券规则和使用门槛的配置。
-
“发”指优惠券的发放,优惠券系统提供了多种发放优惠券的方式,满足针对不同人群的主动发放和被动发放。
-
“用”指优惠券的使用,包括正向购买商品及反向退款后的优惠券回退。
-
“计”指优惠券的统计,包括优惠券的发放数量、使用数量、使用商品等数据汇总。
除了提供常见的优惠券促销玩法外,优惠券系统还以优惠券的形式作为其他一些活动或资产的载体,比如手机类商品的保值换新、内购福利、与外部广告商合作发放优惠券等。
以下为V2.0版本的抢券系统的展示:
优惠券的依赖方和调用方
依赖方
-
商品
好多优惠券都是限制哪些商品可用,商品服务作为一个基础服务,是优惠券系统需要依赖的
-
商户
有些券是商家券,限制了哪个商家可用,比如lining可用,nike不可用,我们要识别到是哪个商户,所以需要依赖商户系统
-
职能
在权限控制和用户的有效性判断上,需要依赖职能系统,做校验相关的处理
-
审批流
在创建优惠券的时候,我们需要有流程化的管控,需要对接审批流
调用方
-
APP、小程序
C端作为流量的入口,需要展示优惠券的信息数据,承接了优惠券展示的所有能力
从领券开始,到提交订单时的查询可以使用的优惠券,到提交订单后的核销优惠券,最后在我的账户里边查询个人优惠券信息,完成了一整个C端的生命周期处理,属于最关键的调用方。
-
订单
订单系统,在提交订单时,需要向优惠券系统来校验,券的状态等数据,如果订单金额符合满额返券的金额。那么还需要调用优惠券系统来向用户发券
-
营销工具
一些营销工具多种多样,比如新人礼包等,需要调用优惠券系统来获取券的信息
二、抢券系统的非功能需求
如何从零搭建10万级 QPS 大流量、高并发优惠券系统?
大促期间,多个业务方都有发放优惠券的需求,且对发券的 QPS 量级有明确的需求。
所有的优惠券发放、核销、查询都需要一个高并发架构 来承载。
因此,需要设计、开发一个能够支持十万级 QPS 的券系统,并且对优惠券完整的生命周期进行维护。
NFR难题1:如何实现10Wqps的领券能力?
以多点APP为例,来看看如何实现10Wqps的领券能力?
优惠券并不是每个人都可以领取的,有很多限制条件,比如”有些优惠券只有新人可以领“,”有些优惠券一天只能领一张“,”有些券需要完成任务或达到一定门槛才可以领“,等等。
我们简单看下优惠券领取的流程图:
优惠券的领取,逻辑相对简单些,需要关注券模板、领取记录,用户标签等几个维度校验。
如果满足条件就可以给用户发放优惠券。
券模板校验:
-
优惠券模板本身设置领取时间,只有在区间时间内才有机会领取优惠券,当然是否能最终领取还取决于其他条件。
-
券库存。正如商品一样,没有库存的商品是无法下单售卖
-
当日发放限制。为了防止用户对券哄抢,同时为了延长活动的黏性效果,会限量每天发放优惠券数量。
优惠券不是随便乱发的,毕竟要计入公司的支出成本,一旦涉及到钱的问题便是个严肃的问题。
定位好用户群体,什么样的优惠券发给什么类型的用户才能获得最大收益。所以我们在创建优惠券时,除了设置适用的商品范围,还要限制优惠券的用户类型。人尽其用,物尽其才。
适用商品范围一般在页面展示或下单时才用到。而用户类型则相反,需要前置控制,也就是说在用户领取时校验,一旦用户领取成功就属于用户个人财产,只要没有过期都可以使用。
用户领取记录校验:
-
根据券模板的领取条件限制,以及用户的领取记录,判断是否还可以领取
用户标签校验:
-
有些券限制只有新人才可以领取。如:新人专享活动
-
券也可以跟用户等级挂钩,只有达到设置的等级才可以领取
-
黑名单用户。用风控挂钩,识别风险刷券用户
-
自定义用户。可以给用户打一些特定标签。并针对该类型的用户发放。
技术挑战:如何实现10Wqps的领券能力?
-
风控如何接入,领券接口的10Wqps,对风控接口性能有较高要求
-
券缓存如何设计。一般会按变化的频率做拆分。券模板本身内容可以封装一个缓存模型。其中的券库存由于经常变化,需要单独剥离处理,采用缓存+数据库。但如何保证两者的数据一致性需要我们特别关注
-
领取记录同样采用缓存+数据库
-
用户标签。由于不用的优惠券会限制发放给不用的用户人群,所以我们会根据券模板设置的用户标,采用策略模式,调用外部服务,实时查询用户是否满足领取条件。
如何实现10Wqps的领券能力?
NFR难题2:如何实现10Wqps的算券能力?
以 多点APP为例,来看看如何实现10Wqps的算券能力?
用户对勾选的商品(也可能是购物车批量商品下单)创建订单时,会计算有哪些优惠券可以使用。
如上图所示,页面输出信息很简单,只显示可以当前可用且按优惠力度排序的优惠券,但如何计算出满足当前订单的优惠券,后台涉及哪些复杂业务逻辑,又会遇到哪些技术难点挑战,下面来我们来分析下。
整体的架构思想还是采用过滤机制,首先查出用户下所有的优惠券,然后根据优惠的各种标记位做相应的策略处理。
渠道过滤:
上面的图是多点APP的券包截图,该券限制使用条件必须是”多点配送(到家可用)“,如果是”门店自提“则订单无法使用该优惠券。
所以我们可以清楚判断,优惠券请求接口势必要传入配送方式参数。
下单时计算可用的优惠券,首要一条就是判断订单中的商品是否在优惠券的商品范围里。
常见的商品作用范围有哪些:
-
单品:创建单品优惠券,限制只有该商品才可以使用
-
部分商品:可以与活动绑定,也可以独立设置,只有指定的这些商品才可以适用优惠券
-
所有商品:全场券,所有售卖的商品都可使用。
-
店铺商品:店铺优惠券,限制只有购买本店铺的商品才可以使用,当然要满足金额门槛
-
商品分类:类目券,根据商品的类目属性做适用条件
-
商品品牌:按品牌决定是否可以使用优惠券
-
商品标签:为指定商品打上自定义标签,然后优惠券模板中配置
-
指定商品/排除特殊商品:针对特殊商品做给运营人员的灵活配置。
-
渠道商品:根据商品进货或者销售渠道定义优惠券范围
-
区域商品:根据商品销售区域划分,此类优惠券社区电商用的较多。
-
订单范围就是订单金额满减、满赠、包邮等条件下可使用的优惠券。
上图优惠券就是商家券,除了”商家券“的标记外,还会设置商家的id,也就是下单的商品中属于这家的商品才会累计校验金额门槛。
如何实现10Wqps的算券能力?
NFR难题3:如何实现10Wqps的核销能力?
以多点APP为例,来看看如何实现10Wqps的核销能力?
优惠券核销的流程,主要如下:
如何实现10Wqps的核销能力?
NFR难题4:如何实现10Wqps的抢券事务?
TCC,Try-Confirm-Cancel,目前分布式事务主流解决方案。
-
阶段一:Try
对资源进行冻结,预留业务资源
创建订单时,将优惠券状态改为 “冻结”
-
阶段二:Confirm
确认执行业务操作,做真正提交,将第一步 Try 中冻结的资源,真正扣减
订单支付成功,将优惠券状态改为 “已使用”
-
阶段三:Cancel
取消执行业务操作,取消 Try 阶段预留的业务资源
支付失败/超时或订单关闭情况,将优惠券状态改为 “未使用”
如何实现10Wqps的抢券事务?
三、抢券系统的微服务架构
抢券的微服务划分
模版模块:提供给运营人员,运营人员根据要求构建优惠券模版
分发模块:面向用户,提供查看、领取、结算、核销优惠券等功能。需要依赖模版微服务与结算微服务
结算模块:结算优惠券,对优惠券规则进行计算
1 模版微服务
运营人员通过设定条件创建优惠券模版,之后生成对应数量优惠券,最后用户才可以去领取优惠券
核心功能:运营人员设定好条件(名称、logo、数量等)创建优惠券模版,后台异步创建对应数量优惠券。创建优惠券过程比较耗时,http接口不返回不是一种很好的体验,所以使用异步操作
使用Redis保存优惠券码,提高效率
查询优惠券信息功能,用于提供给其他微服务调用
2 分发微服务
面向用户,提供查看优惠券、领取优惠券、核销优惠券与结算优惠券功能
查看优惠券
根据不同信息查询对应优惠券模版信息,需要依赖模版微服务
领取优惠券
通过验证,即优惠券模版是可领取的,且成功获取到优惠券码,就将优惠券写入MySQL与Redis
核销优惠券
标记优惠券状态为已使用,更新MySQL与Redis数据
3 结算微服务
调用结算微服务计算优惠券规则
四、抢券系统的表设计
与系统架构对应的,我们需要建立对应的 MySQL 数据存储表。
五: 抢券系统的10Wqps高并发架构亮点
1 服务器架构(应用服务器、数据库、中间件等)
业务从发展的初期到逐渐成熟,服务器架构也是从相对单一到集群,再到分布式服务。
可以支持高并发的服务少不了好的服务器架构,需要有均衡负载,数据库需要主从集群,NoSQL缓存需要主从集群,静态文件需要上传CDN,这些都是能让业务程序流畅运行的强大后盾
2 MQ缓冲队列(削峰填谷、系统解耦)
大量需要处理的各种请求涌入到服务端,无论是nosql还是sql都支撑不住,那么就要考虑如何解决这些请求,让他们不会搞垮我们的服务端应用甚至搞垮我们的数据库
由于券模板/券记录都需要展示过期状态,并且根据不同的状态进行业务逻辑处理,因此有必要引入延迟消息队列来对券模板/券状态进行处理。
RocketMQ 支持延时消息,因此我们选用 RocketMQ 作为消息队列。
3 高并发存储
由于券模板、券记录这些都是需要持久化的数据,同时还需要支持条件查询,所以我们选用通用的结构化存储 MySQL 作为存储中间件。
但是需要做数据分片,数据分库
4 网关(令牌桶)
解决前端到服务端的不确定流量以及瞬时的超大流量,防止服务器血崩
5 缓存(静态缓存、动态缓存)
-
由于发券时需要券模板信息,大流量情况下,不可能每次都从 MySQL 获取券模板信息,因此考虑引入缓存
-
同理,券的库存管理,或者叫库存扣减,也是一个高频、实时的操作,因此也考虑放入缓存中
主流的缓存 Redis 可以满足我们的需求,因此我们选用 Redis 作为缓存中间件。
当然,缓存不单单只是Redis,还需要应用内部的三级缓存等等
6 应用内的代码设计(多线程、锁等等)
7 异常流量的抵御(缓存攻击、数据库攻击等)
布隆过滤器
8 并发测试
高并发相关的业务,需要进行并发的测试,通过大量的数据分析评估出整个架构可以支撑的并发量。
测试高并发可以使用第三方服务器或者自己测试服务器,利用测试工具进行并发请求测试,分析测试数据得到可以支撑并发数量的评估,这个可以作为一个预警参考,俗话说知己自彼百战不殆
其实高并发系统涉及到的知识点还有很多很多,网络抖动?灾备?动态伸缩?等等
接下来,我们首先来看看,10Wpqs 抢券系统的核心逻辑的高并发设计
六:10Wpqs 抢券 的核心流程的高并发设计
1 结合redis 高并发缓存,完成领券逻辑设计
领券流程分为三部分:参数校验、幂等校验、库存扣减。
幂等操作用于保证发券请求不正确的情况下,业务方通过重试、补偿的方式再次请求,可以最终只发出一张券,防止资金损失。
2 大流量 10W qps 场景下的问题及解决方案
实现了系统的基本功能后,我们来讨论一下,如果在大流量、高并发的场景下,系统可能会遇到的一些问题及解决方案。
存储瓶颈及解决方案
瓶颈:
在系统架构中,我们使用了 MySQL、Redis 作为存储组件。
我们知道,单个服务器的 I/O 能力终是有限的,在实际测试过程中,能够得到如下的数据:
-
单个 MySQL 的每秒写入在 4000 QPS 左右,超过这个数字,MySQL 的 I/O 时延会剧量增长。
-
MySQL 单表记录到达了千万级别,查询效率会大大降低,如果过亿的话,数据查询会成为一个问题。
-
Redis 单分片的写入瓶颈在 2w 左右,读瓶颈在 10w 左右
解决方案:
-
读写分离。在查询券模板、查询券记录等场景下,我们可以将 MySQL 进行读写分离,让这部分查询流量走 MySQL 的读库,从而减轻 MySQL 写库的查询压力。
-
分治。在软件设计中,有一种分治的思想,对于存储瓶颈的问题,业界常用的方案就是分而治之:流量分散、存储分散,即:分库分表。
-
发券,归根结底是要对用户的领券记录做持久化存储。对于 MySQL 本身 I/O 瓶颈来说,我们可以在不同服务器上部署 MySQL 的不同分片,对 MySQL 做水平扩容,这样一来,写请求就会分布在不同的 MySQL 主机上,这样就能够大幅提升 MySQL 整体的吞吐量。
-
给用户发了券,那么用户肯定需要查询自己获得的券。基于这个逻辑,我们以 user_id 后四位为分片键,对用户领取的记录表做水平拆分,以支持用户维度的领券记录的查询。
-
每种券都有对应的数量,在给用户发券的过程中,我们是将发券数记录在 Redis 中的,大流量的情况下,我们也需要对 Redis 做水平扩容,减轻 Redis 单机的压力。
容量预估:
基于上述思路,在要满足发券 12w QPS 的需求下,我们预估一下存储资源。
a. MySQL 资源
在实际测试中,单次发券对 MySQL 有一次非事务性写入,MySQL 的单机的写入瓶颈为 4000,据此可以计算我们需要的 MySQL 主库资源为:
120000/4000 = 30
b. Redis 资源
假设 12w 的发券 QPS,均为同一券模板,单分片的写入瓶颈为 2w,则需要的最少 Redis 分片为:
120000/20000 = 6
3 redis 热点库存问题及解决方案
问题
大流量发券场景下,如果我们使用的券模板为一个,那么每次扣减库存时,访问到的 Redis 必然是特定的一个分片,因此,一定会达到这个分片的写入瓶颈,更严重的,可能会导致整个 Redis 集群不可用。
解决方案
热点库存的问题,业界有通用的方案:即分而治之,扣减的库存 key 不要集中在某一个分片上。
如何保证这一个券模板的 key 不集中在某一个分片上呢,我们拆 key(拆库存)即可。
如图:
在业务逻辑中,我们在建券模板的时候,就将这种热点券模板做库存拆分,后续扣减库存时,也扣减相应的子库存即可。
建券
4:数据分片场景下的库存扣减方案设计
这里还剩下一个问题,即:扣减子库存,每次都是从 1 开始进行的话,那对 Redis 对应分片的压力其实并没有减轻,因此,我们需要做到:每次请求,随机不重复的轮询子库存。
以下是本项目采取的一个具体思路:
Redis 子库存的 key 的最后一位是分片的编号,如:xxx_stock_key1、xxx_stock_key2……,在扣减子库存时,我们先生成对应分片总数的随机不重复数组,如第一次是[1,2,3],第二次可能是[3,1,2],这样,每次扣减子库存的请求,就会分布到不同的 Redis 分片上,缓轻 Redis 单分片压力的同时,也能支持更高 QPS 的扣减请求。
这种思路的一个问题是,当我们库存接近耗尽的情况下,很多分片子库存的轮询将变得毫无意义,因此我们可以在每次请求的时候,将子库存的剩余量记录下来,当某一个券模板的子库存耗尽后,随机不重复的轮询操作直接跳过这个子库存分片,这样能够优化系统在库存即将耗尽情况下的响应速度。
业界针对 Redis 热点 key 的处理,除了分 key 以外,还有一种 key 备份的思路:即,将相同的 key,用某种策略备份到不同的 Redis 分片上去,这样就能将热点打散。这种思路适用于那种读多写少的场景,不适合应对发券这种大流量写的场景。在面对具体的业务场景时,我们需要根据业务需求,选用恰当的方案来解决问题。
券模板获取失败问题及解决方案
问题
高 QPS,高并发的场景下,即使我们能将接口的成功率提升 0.01%,实际表现也是可观的。
现在回过头来看下整个发券的流程:查券模板(Redis)-->校验-->幂等(MySQL)--> 发券(MySQL)。
在查券模板信息时,我们会请求 Redis,这是强依赖,在实际的观测中,我们会发现,Redis 超时的概率大概在万分之 2、3。因此,这部分发券请求是必然失败的。
解决方案
为了提高这部分请求的成功率,我们有两种方案。
-
一是从 Redis 获取券模板失败时,内部进行重试;
-
二是将券模板信息缓存到实例的本地内存中,即引入二级缓存。
内部重试可以提高一部分请求的成功率,但无法从根本上解决 Redis 存在超时的问题,同时重试的次数也和接口响应的时长成正比。
二级缓存的引入,可以从根本上避免 Redis 超时造成的发券请求失败。因此我们选用二级缓存方案:
当然,引入了本地缓存,我们还需要在每个服务实例中启动一个定时任务来将最新的券模板信息刷入到本地缓存和 Redis 中,将模板信息刷入 Redis 中时,要加分布式锁,防止多个实例同时写 Redis 给 Redis 造成不必要的压力。
引入了本地缓存的还有一个问题,要保证本地缓存与redis 之间的数据一致性。
库存扣减,属于读少写多的场景,其实不适合使用二级缓存。最好的方案,Redis分段锁方案。
5 参考秒杀Redis分段锁,设计分库库存扣减方案
七:10Wpqs 抢券的缓存优化
本节的方案来自于: 达达集团 千万级QPS-优惠券系统架构设计
优惠券系统C端使用Redis承接高流量,对Redis的深度了解和学习是优化实战的重要一部分。
下面会分别介绍常见的实战优化经验,选取的常用的优化点:大Key、热Key、过期策略。
1 消灭缓存大Key问题
Redis主线程是单线程模型。大Key表面理解为单个存储Key的Value比较大,参考值:String类型长度大于8000,对于List、Set、Hash、Zset的元素数量大于8000。该数据仅供参考,实际业务不同也可有一定调整。
如果一次操作比较大,会导致主线程处理时间变长,单点阻塞;另外大Key的删除或是过期,也会导致节点阻塞,极端情况会导致主从出现问题,Redis无法响应,所以在使用层面根据业务应拆尽拆。
如果是复杂度比较低的应用,定位大Key是比较简单的事情,通常开发人员根据经验就能定位到,但是优惠券系统有200多缓存Key场景,靠经验值很难定位,针对大Key问题下面拿具体案例进行说明,如何定位、如何解决。
案例背景(优惠券适用门店缓存优化)
1.高峰期响应时长高,所有接口都出现问题。
2.某分片新建连接数飙升、流出量高,并且问题分片每天都变化。
3.定位场景复杂:200多个接口无法确定某个业务;缓存Key和接口之间存在多对多关系。
4.Redis没有慢日志,定位问题困难。
5.大Key存在但是不一定是问题Key;热Key也存在但也不一定是问题,如何找到影响性能具体的Key。
排查过程:
1.根据方法耗时最终可以定位到具体集群,但是不能定位到某个缓存Key,因为Redis是单点阻塞,所以表现为该集群所有缓存都变慢。
2.寻找该集群涉及数据源代码中重复调用过程优化,优化后无效。
3.降级+限流无效。
4.使用主从切换方式定位到问题Key,发现分片流出量从主分片到了从分片。定位到券适用门店缓存,此缓存是CouponId为Key,Value是门店ID,接近上万门店,用来判断券和门店是否适用。
解决方案:
将缓存结构Key是CouponId,Value是适用门店结构,改造为缓存Key是CouponId+门店的维度,B端进行一次写入,C端只判断有没有来做到用户已领取的券在不在该门店展示。
结果:总的每秒流出量由几G/s直接降到几十M/s,单分片由几百M/s降低为几M/s。
总结如何发现大Key:
1.业务经验,假设问题Key。
2.客户端代理,收集大Key信息。
3.查看Redis记录的慢日志。
4.在从集群Scan大Key,以免影响生产环境稳定。
5.读写分离笨办法,对怀疑的Key一个一个进行主从分片切换,观察节点的流出量,这个方法虽然不能快速定位,但是一定能帮助定位到问题。
总结解决大Key:
1.根据业务场景解决,散列足够均匀,足够小。个人认为根据场景解决为首选。
2.对于大Key,要想办法进行业务拆解,无法业务拆分的考虑使用技术手段路由拆分。
3.精简缓存Key的长度,使用缩写替代,例如coupon_info_base改为c_i_b。
4.对字段属性进行精简,使用单字母表示,例如couponName对应存储字母a。
5.数据优先使用整数,比字符串省空间。
6.对缓存数据进行冷热分离,比较热的字段放入一个Key,减少请求的网络流出量。
7.在清理大Key数据时,特别注意不要直接删除,也会造成单点阻塞,使用Scan进行清理。4.0以上引入了unlink,异步删除。
2 解决缓存热Key问题
Redis单节点可以承受10万+QPS,如果突然有几十万请求访问某一固定Key,那会达到网卡上限,导致节点无法响应。当然这里的量级也是参考值,和实际场景的Key大小和机器性能许多因素都有关系。常见的突出业务场景是秒杀,其他的业务场景也会存在请求量大的热Key,热Key的发现和处理与大Key有些共通之处,也有不一样之处。
热Key发现:
1.业务经验,假设问题Key。
2.抽样抓取请求,这里分为服务端和客户端两个维度抓取,客户端便向集群维度抓取,服务端便向单节点维度抓取。例如:尝试抓10s内的请求然后按请求量对Key排序。
3.Redis自带命令,Redis 4+版本后提供热点Key发现。
4.查看Redis记录的慢日志。
5.读写分离笨办法,对怀疑的Key一个一个进行主从分片切换,观察节点QPS变化,这个和大Key发现思路一致。
解决热Key:
1.业务优化减少请求量,例如秒杀,网上有各种降流量措施。
2.缓存隔离,减少热Key对其他业务影响。
3.散列多份,放许多份,读取数据时进行随机读取一份,这个方法比较通用。
4.对于热Key前置缓存到应用服务器上,尽量是占用空间小并且不怎么发生变的数据进行前置,不然GC也是件比较麻烦的事情。
注意:实际生产环境会比较复杂,会存在一些场景比较难以定位。例如:不是大Key也不是热Key,但是会是阻塞节点的主要流量Key。
定位方法可以参考读写分离方法,观察节点出流量以及QPS变化。
八:10Wpqs 抢券系统的高并发DB设计
1 优惠券分库分表
随着优惠券发放量越来越大,单表已经达到瓶颈。为了支撑业务的发展,综合考虑,对用户优惠券数据进行分库分表。
关键字:技术选型、分库分表因子
参考之前项目经验,采用了公司中间件团队提供的自研框架。原理是引入自研的MyBatis的插件,根据自定义的路由策略计算不同的库表后缀,定位至相应的库表。
用户优惠券与用户id关联,并且用户id是贯穿整个系统的重要字段,因此使用用户id作为分库分表的路由因子。这样可以保证同一个用户路由至相同的库表,既有利于数据的聚合,也方便用户数据的查询。
假设共分N个库M个表,分库分表的路由策略为:
库后缀databaseSuffix = hash(userId) / M %N
表后缀tableSuffix = hash(userId) % M
2 DB的读写分离操作
以 vivo 亿级优惠券系统为例,介绍一下优惠券系统 DB的读写分离操作
除了上述所说的分库分表外,在此基础上还做了读写分离操作。
主库负责执行数据更新请求,然后将数据变更实时同步到所有从库,用从库来分担查询请求,解决数据库写入影响查询的问题。
主从同步存在延迟,正常情况下延迟不超过1ms,优惠券的领取或状态变更存在一个耗时的过程,主从延迟对于用户来说无感知。
3 用户维度优惠券字段冗余
查询用户相关的优惠券数据是优惠券最频繁的查询操作之一,用户优惠券数据做了分库分表,在查询时无法关联券规则表进行查询,为了减少IO次数,用户优惠券表中冗余了部分券规则的字段。
优惠券规则表字段较多,冗余的字段不能很多,要在性能和字段数之间做好平衡。
4 优惠券V1.0-》V2.0 独立的DB迁移方案
以 vivo 亿级优惠券系统为例,介绍一下优惠券系统独立迁移方案
如何将优惠券从商城系统迁移出来,并兼容已对接的业务方和历史数据,也是一大技术挑战。
系统迁移有两种方案:停机迁移和不停机迁移。
我们采用的是不停机迁移方案:
-
迁移前,运营停止与优惠券相关的后台操作,避免产生优惠券静态数据。
静态数据:优惠券后台生成的数据,与用户无关。
动态数据:与用户有关的优惠券数据,含用户领取的券、券和订单的关系数据等。
-
配置当前数据库开关为单写,即优惠券数据写入商城库(旧库)。
-
优惠券系统上线,通过脚本迁移静态数据。迁完后,验证静态数据迁移准确性。
-
配置当前数据库开关为双写,即线上数据同时写入商城库和优惠券新库。此时服务提供的数据源依旧是商城库。
-
迁移动态数据。迁完后,验证动态数据迁移准确性。
-
切换数据源,服务提供的数据源切换到新库。验证服务是否正确,出现问题时,切换回商城数据源。
-
关闭双写,优惠券系统迁移完成。
迁移后优惠券系统请求拓扑图如下:
九:10Wqps场景的微服务治理
系统开发完成后,还需要通过一系列操作保障系统的可靠运行。
-
超时设置。优惠券系统是一个 RPC 服务,因此我们需要设置合理的 RPC 超时时间,保证系统不会因为上游系统的故障而被拖垮。例如发券的接口,我们内部执行时间不超过 100ms,因此接口超时我们可以设置为 500ms,如果有异常请求,在 500ms 后,就会被拒绝,从而保障我们服务稳定的运行。
-
监控与报警。对于一些核心接口的监控、稳定性、重要数据,以及系统 CPU、内存等的监控,我们会在 Grafana 上建立对应的可视化图表,在春节活动期间,实时观测 Grafana 仪表盘,以保证能够最快观测到系统异常。同时,对于一些异常情况,我们还有完善的报警机制,从而能够第一时间感知到系统的异常。
-
限流。优惠券系统是一个底层服务,实际业务场景下会被多个上游服务所调用,因此,合理的对这些上游服务进行限流,也是保证优惠券系统本身稳定性必不可少的一环。
-
资源隔离。因为我们服务都是部署在 docker 集群中的,因此为了保证服务的高可用,服务部署的集群资源尽量分布在不同的物理区域上,以避免由集群导致的服务不可用。
-
依赖外部接口隔离熔断
优惠券内部依赖了第三方的系统, 为了防止因为依赖方服务不可用,产生连锁效应,最终导致优惠券服务雪崩的事情发生,优惠券对依赖外部接口做了隔离和熔断。
十:优惠券系统高并发压测设计
做完了上述一系列的工作后,是时候检验我们服务在生产环境中的表现了。
当然,新服务上线前,首先需要对服务进行压测。这里总结一下压测可能需要注意的一些问题及压测结论。
注意事项
-
首先是压测思路,由于我们一开始无法确定 docker 的瓶颈、存储组件的瓶颈等。
-
所以我们的压测思路一般是:
- 找到单实例瓶颈
- 找到 MySQL 一主的写瓶颈、读瓶颈
- 找到 Redis 单分片写瓶颈、读瓶颈
得到了上述数据后,我们就可以粗略估算所需要的资源数,进行服务整体的压测了。
-
压测资源也很重要,提前申请到足量的压测资源,才能合理制定压测计划。
-
压测过程中,要注意服务和资源的监控,对不符合预期的部分要深入思考,优化代码。
-
适时记录压测数据,才能更好的复盘。
-
实际的使用资源,一般是压测数据的 1.5 倍,我们需要保证线上有部分资源冗余以应对突发的流量增长。
十一:优惠券系统高可用架构设计
优惠券业务量级的高速发展,对后台系统架构的可用性要求越来越高。在保障微信优惠券业务体验的前提下,优惠券后台系统进行了一系列高可用方面的优化设计。
要保障优惠券系统架构的高可用性,可以从多个方面进行优化和设计。以下是一些详细说明:
-
系统架构设计:
-
信息流、业务流、资金流分离:优惠券系统由信息流、业务流、资金流三部分组成,这三部分在组织架构上应由不同的后台团队完成,以提高系统的模块化和可维护性。
-
微服务架构:采用微服务架构,将优惠券系统拆分成多个独立的服务,每个服务负责特定的功能,这样可以提高系统的可扩展性和容错性。
-
无状态设计:确保优惠券系统的服务是无状态的,即服务不需要保存用户的会话信息,这样可以提高系统的并发处理能力和可靠性。
-
-
高可用性策略:
-
冗余部署:将系统的关键组件部署在多台服务器上,通过搭建主备或者集群的架构来实现冗余。当主服务器出现故障时,备用服务器能够自动接管,保证系统的可用性。
-
负载均衡:通过将流量分发到多台服务器上,均衡系统的请求负载,提高系统的可用性和扩展性。负载均衡可以通过硬件(如负载均衡器)或者软件(如Nginx、HAProxy)实现。
-
服务容器化:使用容器技术(如Docker、Kubernetes)将应用程序与其依赖项打包为容器,实现快速部署、弹性扩展和自动化管理。容器化可以提高系统的可移植性、弹性和可伸缩性,从而增加系统的高可用性。
-
-
数据保障:
-
数据备份与恢复:定期对关键数据进行备份,并确保备份的数据可用性。这样,在发生数据丢失或损坏时,可以快速恢复数据,减少系统停机时间。
-
分布式缓存:使用多级缓存技术(如Redis、Memcached等),将数据分别存储在内存缓存、本地缓存和分布式缓存中,以提高访问速度和降低数据库压力。
-
数据一致性:使用分布式锁技术来保护优惠券的领取操作,确保每个用户只能领取一次优惠券。同时,为了保证数据一致性,可以采用消息队列等技术实现请求的异步处理和结果的返回。
-
-
监控与告警:
-
系统监控:对优惠券系统的关键指标(如请求量、响应时间、错误率等)进行实时监控,以便及时发现和解决潜在问题。
-
告警机制:设置合理的告警阈值,当系统出现异常情况时,及时发送告警通知给相关人员,以便快速响应和处理。
-
-
安全性保障:
通过以上措施的综合应用,可以大大提高优惠券系统架构的高可用性,确保系统在高峰时段能够稳定、高效地运行。
-
访问控制:实施严格的访问控制策略,确保只有授权的用户才能访问优惠券系统。
-
数据加密:对敏感数据进行加密存储和传输,以防止数据泄露和篡改。
-
安全审计:定期对优惠券系统进行安全审计和漏洞扫描,及时修复发现的安全漏洞和隐患。
-
十二:10Wqps高并发系统的架构思考
从零搭建一个大流量、高并发的优惠券系统,首先应该充分理解业务需求,然后对需求进行拆解,根据拆解后的需求,合理选用各种中间件;
本文主要是要建设一套优惠券系统,因此会使用各类存储组件和消息队列,来完成优惠券的存储、查询、过期操作;
在系统开发实现过程中,对核心的发券、券过期实现流程进行了阐述,并针对大流量、高并发场景下可能遇到的存储瓶颈、热点库存、券模板缓存获取超时的问题提出了对应的解决方案。
其中,我们使用了分治的思想,对存储中间件进行水平扩容以解决存储瓶颈;采取库存拆分子库存思路解决热点库存问题;引入本地缓存解决券模板从 Redis 获取超时的问题。
最终保证了优惠券系统在大流量高并发的情景下稳定可用;
除开服务本身,我们还从服务超时设置、监控报警、限流、资源隔离等方面对服务进行了治理,保障服务的高可用;
压测是一个新服务不可避免的一个环节,通过压测我们能够对服务的整体情况有个明确的了解,并且压测期间暴露的问题也会是线上可能遇到的,通过压测,我们能够对新服务的整体情况做到心里有数,对服务上线正式投产就更有信心了。
1000W长连接,如何建立和维护?千万用户IM ,如何架构设计?
一、IM的重大价值
1、IM的广泛使用场景
即时通信(Instant Messaging,简称 IM)是一种通过网络进行实时通信的系统,允许用户之间快速传递信息。
无论是文字消息、文件,还是语音和视频交流,IM 系统都能即时响应。
即时通信(IM)功能模块具有广泛的应用场景,可以集成到多种系统中,以增强沟通和协作能力。
以下是一些常见的即时通信(IM)应用场景:
1. 企业内部沟通系统
-
协作工具:如 Slack、Microsoft Teams 等,为团队提供即时消息、文件共享、视频会议等功能,提升工作效率。
-
企业管理系统(ERP、CRM):整合 IM 功能,方便员工快速交流、协作,以及与客户或合作伙伴沟通。
2. 社交平台
-
社交网络:如 Facebook、Twitter 等,可以通过 IM 功能让用户私下交流或创建群组讨论。
-
在线社区和论坛:提供用户之间的即时消息功能,增强互动性和用户粘性。
3. 电子商务平台
-
客户支持:如 Shopify、Amazon 等电商平台,通过 IM 实现客户与客服的实时沟通,解决售前咨询或售后服务问题。
-
买卖双方沟通:买家与卖家可以直接通过 IM 沟通,协商交易细节。
4. 教育系统
-
在线学习平台:如 Coursera、Udemy,师生之间可以通过 IM 进行实时交流,辅导和答疑。
-
校园管理系统:学校内部系统中,师生或学生之间可以通过 IM 进行联系,安排活动或布置作业。
5. 医疗系统
-
远程医疗:患者与医生之间可以通过 IM 进行初步沟通,预约咨询或远程诊断。
-
医院管理系统:医院内部工作人员之间可以通过 IM 实时沟通,协调工作和安排。
6. 在线游戏
-
多人在线游戏:如 MMORPG、MOBA 类游戏,通过 IM 实现玩家之间的实时沟通和策略讨论。
-
游戏社交平台:玩家之间可以通过 IM 进行交流、组队或分享游戏经验。
7. 金融系统
-
在线银行和支付系统:如支付宝、微信支付,通过 IM 功能实现用户与客服的沟通,解决支付问题或账户管理。
-
投资和交易平台:投资者之间或与金融顾问之间通过 IM 进行市场讨论和交易建议交流。
这些 IM 功能模块能够融入不同系统,极大地提升了用户的交互体验和系统的服务能力。
2、IM的巨大学习价值
由于IM的广泛使用场景广泛,所以IM的学习价值巨大。
3、技术要点
1)、网络传输协议
IM系统传输即时消息无外乎使用UDP、TCP、基于TCP的HTTP这几种协议中的一种或几种
-
UDP协议实时性更好,但是如何处理安全可靠的传输并且处理不同客户端之间的消息交互是个难题,实现起来过于复杂;
-
HTTP协议属于扩展支持,数据传输量大;
-
TCP协议如果有海量用户的需求。如何保证单机服务器高并发量,如何做到灵活,扩展的架构。
2)、数据通信格式(协议)
-
自定义二进制:信息体积小,占用带宽低,传输效率高,缺点是处理不同客户端之间的消息交互较复杂。
-
提供序列化和反序列化库的开源协议:protocol buffers, json,Thrift。是一种流行的通用数据格式,扩展相当方便,序列化和反序列化相当方便。
-
文本化协议:序列化,反序列化容易(库支持),可视化强;缺点:相对于二进制存储占用体积大
3)、如何保证实时消息的有序性、可用性
-
IM中消息的投递中要保证发送方发送顺序与接收方展现顺序一致。
-
IM中如何保证在线实时消息的可靠投递,即消息的不丢失和不重复。
-
TCP 自带TCP Keepalive 无法满足需求。
4)、如何处理群发消息以及离线消息
5)、如何进行消息持久化
6)、如何避免数据中心级故障
二、10万用户->100万用户->1000万用户 的架构演进
10万用户 | 100万用户 | 1000万用户 | |
存储架构 | MySQL主备 Redis主备 | MySQL 主备 Redis cluster MongoDB | MySQL 主备 Redis cluster HBASE存储平台 |
接入架构 | 2台 Nginx 负载均衡 | 2台 Nginx/LVS 负载均衡三级缓存 | 2台 Nginx/LVS 负载均衡三级缓存 / 多数据中心部署 |
可扩展架构 | 2个单体架构/直连 | 3个服务微服务/异步广播 | 3个服务微服务/异步精准路由 |
高可用 | MySQL 主备跨机房复制,只保证基础数据不丢失 | 同城双中心 | 同城双活/异地双活 |
大数据架构 | MongoDB | 大数据平台 flink + ES+ HBASE | |
基础设施 | 快速扩展(辅助需求) | 全面完善(基础技术) |
三、10万用户IM架构,如何进行架构设计?
1. 多协议支持
支持TCP、WebSocket。使用 Netty 和 Protobuf 实现高性能IM 通信模块,能够结合 Netty 的高性能网络通信框架和 Protobuf 的高效序列化机制,构建一个高效、可扩展的即时通信系统。
要构建一个高性能的 IM 通信模块,使用 Netty 和 Protobuf 并支持 TCP 和 WebSocket 协议,需要设计一个能够处理多种协议、灵活扩展、并且能够保证低延迟和高吞吐量的架构。
-
TCP 通信:
-
ChannelPipeline 配置:通过 Netty 的
ChannelPipeline
配置解码器(ProtobufDecoder)、编码器(ProtobufEncoder)和自定义的ChannelHandler
。 -
TCP Server:通过
ServerBootstrap
启动 TCP 服务,绑定端口并监听连接。
-
-
WebSocket 通信:
-
HTTP 协议升级:首先通过 HTTP 进行握手,然后升级为 WebSocket 协议。
Netty 提供了
WebSocketServerProtocolHandler
来处理协议升级。 -
消息处理:在升级为 WebSocket 后,使用 WebSocketFrame 来传递消息,再通过 Protobuf 进行编解码。
-
WebSocket Server:与 TCP 类似,使用
ServerBootstrap
启动并配置 WebSocket 服务。
-
2. 消息编解码设计
消息编解码:利用 Protobuf 进行消息的序列化和反序列化,减少数据传输的大小,并保证高效的数据传输。
-
Protobuf 生成器:通过
.proto
文件定义消息结构,并使用protoc
编译生成相应的 Java 类。 -
Protobuf 编解码:
-
ProtobufDecoder:将收到的二进制数据解码为 Protobuf 消息对象。
-
ProtobufEncoder:将消息对象编码为二进制格式,准备发送。
-
3. 用户管理与消息路由
-
用户连接管理:
-
使用数据结构(如
ConcurrentHashMap
)来管理用户的连接映射,跟踪每个用户的Channel
实例。 -
使用redis 来管理 user 和node 的映射关系,跟踪每个用户的 node 实例。
-
多设备支持:如果一个用户可以有多个设备在线,需要设计一个映射来跟踪每个设备的连接。
-
-
消息路由与分发:
-
实现消息的路由逻辑,根据接收者的 ID 查找对应的
Channel
并将消息发送。 -
支持单播、组播(群聊)和广播消息。
-
4. 心跳机制
-
TCP 心跳:通过 Netty 的
IdleStateHandler
检测连接的空闲状态,如果超过一定时间没有读写操作,可以发送心跳包或断开连接。 -
WebSocket 心跳:使用 WebSocket 的 PING/PONG 帧来维持连接活跃状态。
5. 高可用性设计
实现服务的集群化部署,使用 ZooKeeper 等工具进行服务注册和发现,保证集群的高可用性。
6. 扩展性与容错性设计
-
分布式架构:
IM 服务实例可以水平扩展,设计 Router 服务,实现IM 服务实例 直接的 负载均衡。
-
负载均衡架构:
同时可以把IM 节点分组,通过NG负载均衡,保证 接入层的高可用性。
四、100万用户IM架构,如何进行升级?
1、高扩展架构:功能的细粒度、分布式解耦
在构建高性能 IM 系统时,将不同的功能模块解耦是一个关键的设计策略,可以提高系统的可维护性、可扩展性和容错能力。
通过将 IM 服务拆分为多个独立的微服务,每个服务专注于特定的功能模块,能够更好地分配资源、隔离故障并简化开发和部署流程。
使IM服务node的不同功能模块解耦,如连接管理服务、业务逻辑服务等。
连接管理服务职责:
-
管理用户的长连接,处理 TCP 和 WebSocket 连接的建立与断开。
-
维护用户的在线状态,管理链接的 heartbeat 心跳。
-
连接管理服务可以组成集群,每个连接管理节点可以维护一部分用户的连接(分片管理),并通过一个注册中心(如 ZooKeeper)来协调和管理节点间的负载。
Logic 业务逻辑 服务 包括:
-
消息处理服务
-
用户认证服务
-
群组管理
-
聊天记录管理
-
消息过滤等。
2、基于RocketMQ做异步消息推送
1. 架构设计
-
生产者-消费者模型:IM 系统的消息生产者(如发送消息的客户端或服务)将消息推送到 RocketMQ主题中,消费者(如消息接收者的客户端或服务)从 RocketMQ中拉取消息进行处理。
-
分区与副本:RocketMQ主题可以分为多个分区,分区内的消息是有序的;同时,为了实现高可用性,每个分区可以有多个副本,分布在不同的 RocketMQ Broker 上。
2. 消息生产
-
消息队列设计:根据业务需求,设计 RocketMQ主题的分区策略。例如,可以按照用户 ID 或聊天会话 ID 来进行分区,以保证相关消息的有序性。
-
消息格式:设计消息的序列化格式(如 JSON、Avro、Protobuf),并确保消息内容包括必要的元数据,如消息 ID、时间戳、发送者和接收者 ID。
3. 消息消费
-
消费者组:接收消息的消费者可以被组织为一个消费者组,RocketMQ会将主题的分区分配给消费者组内的不同消费者,确保每个分区的消息只被一个消费者处理,避免重复消费。
-
消费确认与偏移量管理:消费者在处理消息后,需要提交消费偏移量(Offset)到 Kafka,以便在重启或故障恢复时从正确的位置继续消费。可以选择自动提交(自动提交可能有消息丢失的风险)或手动提交(需要在消息处理完成后手动提交,保证消息处理的准确性)。
4. 消息持久化与顺序性
-
消息持久化:RocketMQ自带持久化机制,消息可以保留一定时间(可配置),即使消费者临时不可用,消息也不会丢失。
-
消息顺序:如果需要保证消息的顺序性,可以将相关消息(如同一会话中的消息)发送到同一分区中。
5. 故障处理与重试机制
-
消息重试:在消息处理失败时,可以将消息重新放回 RocketMQ队列或记录到专用的失败队列,进行后续重试或人工处理。
-
幂等性处理:在消费端实现幂等性逻辑,确保消息即使被多次处理也不会引发不一致问题。
3、100万用户IM架构总体架构
4、ConnectManager服务
用户代理服务器,用于客户端的连接
1) 客户端首先连接到ConnectManager
服务,ConnectManager 发送消息到 RocketMQ。
2) Logic 业务逻辑 服务 收到消息,进行 消息的处理
3) 如果是收到登录的消息,Logic 校验用户的合法性
4) 如果 校验通过,Logic 会返回一个 token 给 RocketMQ, 这是一个广播消息, 该 token 成为该连接的安全通讯的令牌
5) ConnectManager 收到令牌,标识登录成功,并且给客户端正确的响应, 后面可以开始正常的聊天消息处理,
6) 客户端接下来可以发心跳包给ConnectManager
,ConnectManager 会转发到 RocketMQ,Logic 业务逻辑处理心跳包。
7) 客户端发送消息给ConnectManager,ConnectManager 会转发到 rocketmq , Logic 业务逻辑 服务 将MQ
的消息处理完成后, 处理结果发送到 rocketmq ,ConnectManager 再将其转发到对应的客户端。
8) 这里的RocketMQ下行消息是广播模式,同一个下行的消息,会发送给所有的ConnectManager ,ConnectManager 收到属于当前节点的回复消息,再将其转发到对应的客户端,如果 收到消息不是当前节点的消息,ConnectManager 抛弃。
5、Logic 业务逻辑 服务:主要业务处理模块
1)、订阅RocketMQ MQ,处理Comet对Logic的远程RPC调用的处理,如用户登录注册等。
2)、启用rest 服务,监听business 微服务上 B端推送的IM的消息,将这些聊天相关的业务发生到 Kafka。
6、100万用户IM架构中的跨node的会话路由
1)这里,分为local-session 本地会话 、Distrubute-session 分布式会话。
其中,local-session使用map来维护本地会话。分布式session 使用redis 维护全局的会话映射关系,分布式会话映射关系清晰,但是模式比较重。
100万用户IM,其实使用 local-session 本地会话 ,其实就可以了。跨node的会话路由,通过RocketMQ做广播消息解决。
2)跨node的会话路由,100W连接的场景,可以简单的使用的事件广播,由各节点自行判断是否持有local-session 会话。
3)当然,当有1000W级链接,node节点较多时,所有节点均被广播,资源浪费。
跨node的会话路由 整体流程如下:
1)客户端与网关的任何一个ConnectManager节点建立长连接,节点会将其加入到内存中的长连接队列。客户端会定期向服务端发送心跳消息,若超过设定时间还未收到心跳,则认为客户端与服务端的长连接已断开,服务端会关闭连接,清理内存中的会话。
2)当业务系统需要向客户端推送数据时,通过提供的HTTP接口将数据发送至服务 Logic 服务 。
3)Logic 服务在收到推送请求后,服务会将消息写入RocketMQ。
4)ConnectManager 服务作为消费者,以广播模式消费消息,所有ConnectManager 节点都能收到消息。
5)ConnectManager节点在收到消息后会判断推送的消息目标是否在其内存 local-session map中维护的长连接队列里,如果存在则通过长连接推送数据,否则直接忽略。
7、100万用户的数据存储架构
-
存储平台:SQL 平台,NoSQL 平台,文件存储。
-
大数据平台:离线处理,在线处理,推荐系统。
这里的NoSQL平台使用MongoDB。
五:1000万用户IM架构,如何再造升天?
1、高扩展架构
更进一步的 功能的细粒度解耦, 实现无限可扩展的架构
在构建1000w用户高性能 IM 系统时,将不同的 Logic 功能模块解耦是一个关键的设计策略,可以提高系统的可维护性、可扩展性和容错能力。
通过将 Logic 服务拆分为多个独立的微服务,每个服务专注于特定的功能模块,能够更好地分配资源、隔离故障并简化开发和部署流程。
logic 业务逻辑 服务负责:
-
消息处理服务
-
消息过滤等。
UserAuth 服务负责:
-
用户认证服务
group 服务负责:
-
群组管理
storage 服务负责:
-
聊天记录管理
其他服务负责:
-
风控等等。
不同的服务,使用不同的消费者group:
-
消费者组:接收消息的消费者可以被组织为一个消费者组,Rocketmq会将主题的分区分配给消费者组内的不同消费者,确保每个分区的消息只被一个消费者处理,避免重复消费。
-
消费确认与偏移量管理:消费者在处理消息后,需要提交消费偏移量(Offset)到 Kafka,以便在重启或故障恢复时从正确的位置继续消费。可以选择自动提交(自动提交可能有消息丢失的风险)或手动提交(需要在消息处理完成后手动提交,保证消息处理的准确性)。
2、1000万用户IM架构中的跨node的会话路由
在处理大规模连接和高并发的场景时,跨节点的会话路由是一个关键的技术挑战。
1000万用户,跨node的会话路由,通过RocketMQ做广播消息,会造成网络资源浪费。
这里需要用到 RocketMQ集群消息 + Distrubute-session 分布式会话。
1)这里,使用 Distrubute-session 分布式会话。
local-session使用map来维护本地会话。但这里还要结合 使用 分布式session 使用redis 维护全局的会话映射关系,分布式会话映射关系清晰,但是模式比较重。
2)跨node的会话路由,1000W连接的场景,可以采用的 RocketMQ集群消息 ,每个节点的消息,发到这个节点对应的topic。
3) Rocketmq Topic设计:
-
为每个节点创建一个独立的Topic,确保消息可以定向发送到正确的节点。
-
考虑使用分区(Partition)来提高并发处理能力。
4) Producer 设计:
-
可以根据会话ID或其他业务标识符,来确定node 对应的topic。
-
配置Producer 以发送消息到正确的Topic。
5) Consumer配置设计:
-
每个node 节点上的Consumer订阅对应的Topic。
-
根据业务需求配置并发数量和消费策略。
3、如何避免数据中心级故障?
数据中心级故障 故障的原因很多,比如:
在 IM 系统中实现异地多活(Active-Active)架构,并确保消息数据同步,是为了提高系统的可用性和容灾能力,同时降低网络延迟对用户体验的影响。
异地多活意味着多个数据中心在不同的地理位置同时提供服务,这些数据中心之间需要保持数据的一致性,尤其是消息数据的同步。以下是实现异地多活架构及消息数据同步的关键点和设计策略。
异地多活(Active-Active) 架构设计原则
-
全局分布式架构:将 IM 系统部署在多个地理位置不同的数据中心,每个数据中心都能够独立处理用户的连接和消息,但所有数据中心之间需要实现高效的数据同步和一致性保障。
-
数据分区与路由:可以根据用户的地理位置将其连接路由到离用户最近的数据中心,减少网络延迟。同时,系统需要能够处理跨数据中心的消息传递。
-
高可用性和容错性:任何一个数据中心的故障不应该影响整体服务的可用性,其他数据中心应当能够无缝接管业务。
异地多活(Active-Active) 数据同步机制
-
消息队列同步:使用分布式消息队列(RocketMQ)在数据中心之间传递消息。每个数据中心的消息队列, 设置sync-Storage 微服务,负责将本地消息复制到其他数据中心,保证消息的一致性。
-
数据库同步:
使用分布式数据库(如 Cassandra、CockroachDB)或数据库复制技术(如 MySQL 的 GTID 复制、主从复制)来实现数据同步。
这里采用了方式1:每个数据中心 设置sync-Storage 微服务,负责将本地消息复制到其他数据中心,保证消息的一致性。
4、1000万用户的数据存储架构
-
存储平台:SQL 平台,NoSQL 平台,文件存储。
-
大数据平台:离线处理,在线处理,推荐系统。
这里的NoSQL平台使用 HBase。