如何根据需求设计一个能扛住一定量QPS且接口延时稳定(高可用)的服务,其中需要注意很多细节。以真实项目中的服务设计为例,探讨一下当前的高并发低延时服务的设计以及更多需要改进的地方。
提供用户数据的RPC/HTTP服务。业务方通过用户的ID,获取当前业务被授权的用户数据。
用户量级10亿+,pb压缩后T级别存储,服务需要满足的几个点包括:
- 50w+的峰值QPS
- P99<20ms的接口延时
- 分钟级延时的用户数据
- 用户数据权限控制
接口设计大致如下,这里以设备的ID为例,给定其中的一个接口定义。
public interface OnlineService {
/**
* 通过设备ID获取用户标签
* @param deviceId 设备ID,即idfa、imei、iqid
* @param idType PC, MOBILE or TV
* @param auth 授权认证
* @return tag_field, value: tagInfo
*/
Map<String, Tags> getSingleDeviceTags(String deviceId, DeviceIdType idType, String auth);
}
参考大数据的经典Lambda框架的分层设计,将离线数据和实时数据分开处理,大致框架图如下图。
我们将系统分成了Batch层、Speed层和Server层。
Batch层即离线批量数据层,每天定期将HBase中的用户数据导入Hikv。Hikv和Couchbase作为数据存储介质。
Speed层即实时数据层,接收来自用户实时事件总线(我们的一个内部服务)数据,加工成用户标签,提供用户的实时标签数据。
Server层主要做数据的合并逻辑,包括数据的封装和权限的筛选,将数据结果返回给到用户。
离线数据使用HBase存储无疑是最合适的,HBase的列存储以及批量读写的性质,适合用户标签数据。
数据缓存选择了Hikv和Couchbase,Hikv是基于SSD的KV数据存储解决方案,特点是容量大,但性能相对Couchbase会弱一些。Couchbase是内存数据库解决方案,特点是延时低,但容量相对有限制。
我们的用户数据量达到10亿级别,存储量在T级别,公司内部独立的Redis或Couchbase集群都无法支撑如此大的容量,因此这里使用了两级缓存的设计,用Hikv做底层缓存,Couchbase作为上层缓存。
我们同时提供RPC和HTTP的接口。RPC是基于Apache Dubbo实现,Dubbo无论是在功能上或是性能上,无疑都是很优秀的RPC服务框架。
考虑到在线服务主要是用于线上的用户定向以及一些推荐场景中的用户召回,因此我们舍弃了一部分用户数据,只保留了月活用户的特定标签数据。
这样的做法是为了在让数据充分服务业务的条件下,降低存储压力。
高并发的服务中,常见的缓存问题包括:缓存击穿、缓存穿透、缓存雪崩的现象。这里把CB当成缓存,Hikv当成传统数据库,来探讨一下系统设计中如何避免这些问题。
先简单描述一下几种case:
**缓存穿透:**缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
常用的解决方案是接口层的校验和空数据设置。
在这里,我们会做用户ID和用户权限的校验;在都没有数据的情况下,主动构造一条空数据写入缓存中,设置较短的TTL,避免这样的ID重复调用。
**缓存击穿:**缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
常用的解决方案是热点数据永不过期或加上互斥锁。
在这里,我们不太用担心这个问题,因为线上用户请求可以认为是均匀的,因此不会大量的同时过期,另外Hikv的性能也是相对OK的,高并发读写Hikv是可以支持的,加锁反而会影响效率。有一个可能的场景是,流量高峰到来之前,进行缓存预热。
**缓存雪崩:**缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
常用的解决办法是设置缓存过期时间随机。即数据进缓存时,加上一个随机的时间戳。
在这里我们两层缓存时间使用的是异步的数据Set,若CB没查到数据,而Hikv有,则直接从Hikv中取到数据返回。
服务基于Docker容器化部署,可灵活的伸缩扩容。
服务使用多机房、多数据中心的形式部署,借助Dubbo的负载均衡以及内部扩展的同区域路由的功能,满足线上不同机房的业务调用。
面对大QPS的情况(如过年抢红包),有如下建议。
服务端:接入Sentinel保障微服务的稳定性。在某个服务可能过多地占用资源,我们需要可配置方式地进行限制某个服务的调用数,并且在服务发生故障时,不级联影响到其他核心功能的正常使用,对这类服务进行快速失败或者是触发备用降级功能。
业务调用方:做好及时熔断和服务降级。
服务接入接口的监控、调用链路监控、日志监控。
在目前框架的设计下,Speed层是存在比较明显的性能瓶颈的。实时数据的更新必然造成数据的频繁读写。
目前的设计,在上游的“实时事件总线服务”中,会对数据流进行一次清洗和聚合,保证数据的有效性,也可以削弱数据流量。另外,实时的标签仅限于时间敏感性高且业务效益大的标签,如时间敏感标签,包括观影时间,或是行为敏感标签,如银行卡的支付绑定行为。
在数据流量稳定且可控的情况下,存储的选择可以考虑redis,redis的数据结构会让数据的操作变得更简单高效,目前独立的新客数据服务也是这么做的。
couchbase独立集群,三地部署,单集群容量2T,可用容量1.5T,常驻item数量<1亿,缓存利用率~40% - 50%。qps峰值10w+,cpm峰值(20w+18w+15w)* 10
hikv,两地部署,全量数据pb压缩后~2T+
服务,三地部署,rpc服务:bj机房40实例,其余机房36实例,单实例配置4core-8g;http服务:20实例
布隆过滤器解析:跳转
布隆过滤器可以帮助我们过滤无效的用户ID,减少打到数据库上的请求。考虑在10亿级用户的场景中,布隆过滤器的初始化是比较不灵活的。且我们在防缓存穿透时的做法,数据库是能正常应付的。综合考虑,内部数据接口的调用,id可以认为是规范的,因此我们没有选择使用布隆过滤器。
redis是好东西,选用主要取决于公司的技术栈和基础设施。
高并发场景下一种常见的挑战是”秒杀场景“,常因为一些业务原因会出现这样的突发流量,最近一次例如过年的到点抢红包活动,同一时间会有大量用户请求服务。
主要存在的问题包括:
- 高并发:短期内突然激增的QPS
- 超卖:100个卖了101个?
- 恶意请求:程序自动带来的模拟出来的请求
- 链接暴露:秒杀链接露出
- 数据库:数据库的吞吐量瓶颈
主要的一些设计点包括:
- 服务单一职责:微服务的设计思想,分布式部署,服务分离。
- 秒杀链接加盐:URL动态化,如通过MD5之类的加密算法实现。
- Nginx:加机器,负载均衡,在网关层将恶意请求拦截。
- 前端操作:资源静态化,按钮控制,点击限流等
- 限流&降级&熔断&隔离:限流组件,如阿里的Sentinel。服务降级、熔断、隔离(取决于服务内调用)。
- 削峰填谷:加入MQ
- 若是使用redis,考虑Lua。
Lua 脚本功能是 Reids在 2.6 版本的最大亮点, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。
另外,库存为负数的问题,可以将update操作加锁,且把udpate语句写在前边,先把数量-1,之后select出库存如果>-1就commit,否则rollback。
附上过程参考图。