业内喜欢用SLA (服务等级协议,全称:service level agreement)来衡量系统的稳定性,对互联网公司来说就是网站服务可用性的一个保证。9越多代表全年服务可用时间越长服务越可靠,停机时间越短。就以一个标准99.99%为例,停机时间52.6分钟,平均到每周也就是只能有差不多1分钟的停机时间,也就是说网络抖动这个时间可能就没了。保证一个系统四个9或者更高的五个9,需要一套全体共识严格标准的规章制度,没有规矩不成方圆。创建的规范有如下几种:
1、研发规范、自身稳定;
2、事务中不能包含远程调用;
3、超时时间和重试次数要合理;
4、表数据操作必须double check,合理利用索引,避免出现慢查询、分库分表不走分表键;
5、没有有效的资源隔离, 避免不同业务共用一个线程池或连接池;
6、合理的系统拓扑,禁止不合理服务依赖,能依赖就依赖,否则同步尽量改成异步弱依赖;
7、精简的代码逻辑;
8、核心路径流程必须进行资源隔离,确保任何突发情况主流程不能受影响。
关键字:开关可控、单一职责、服务隔离、异常兜底、监控发现!
对于稳定性来说,抛开整体系统架构设计,单就每个业务域服务的稳定性也是非常的重要。只有每个业务环节都稳如泰山,才能保障整个稳定性。单服务稳定可以从以下几个方面来进行:
1、禁用设计:应该提供控制具体功能是否开启可用的配置,在相应的功能服务出现故障时,快速下线局部功能,以保证整体服务的可用性;
2、必要的缓存:缓存是解决并发的利器,可以有效的提高系统的吞吐量。按照业务以及技术的纬度必要时可以增加多级缓存来保证其命中率;
3、接口无状态性:服务接口应是无状态的,当前接口访问不应该依赖上层接口的状态逻辑;
4、接口单一职责性:对于核心功能的接口,不应该过多的耦合不属于它的功能。如果一个接口做的事情太多应做拆分,保证单接口的稳定性和快速响应;
5、第三方服务隔离性:任何依赖于第三方的服务(不论接口还是中间件等),都应该做到熔断和降级,不能有强耦合的依赖;
6、业务场景兜底方案:核心业务场景要做到完整兜底方法,从前端到后端都应有兜底措施;
7、服务监控与及时响应:每个服务应做好对应监控工作,如有异常应及时响应,不应累积。
关键字:系统架构、部署发布、限流熔断、监控体系、压测机制!
对于集群维度的稳定性来说,稳定性保障会更加复杂。单服务是局部,集群是全局。一个见微知著,一个高瞻远瞩。
1、合理的系统架构:合理的系统架构是稳定的基石;
2、小心的代码逻辑:代码时刻都要小心,多担心一点这里会不会有性能问题,那里会不会出现并发,代码就不会有多少问题;
3、优秀的集群部署:一台机器永远会有性能瓶颈,优秀的集群部署,可以将一台机器的稳定放大无限倍,是高并发与大流量的保障;
4、科学的限流熔断:高并发来临时,科学的限流和熔断是系统稳定的必要条件;
5、精细的监控体系:没有监控体系,你永远不会知道你的系统到底有多少隐藏的问题和坑,也很难知道瓶颈在哪里;
6、强悍的压测机制:压测是高并发稳定性的试金石,能提前预知高并发来临时,系统应该出现的模样;
7、胆小的开发人员:永远需要一群胆小的程序员,他们讨厌bug,害怕error,不放过每一个波动,不信任所有的依赖。
专项指的是针对某些特定场景下的特定问题而梳理出对应的方案。下面是针对一些常见的稳定性专项的概述:
1、预案:分为定时预案和紧急预案,定时预案是大促常规操作对于一系列开关的编排,紧急预案是应对突发情况的特殊处理,都依赖于事前梳理;
2、预热:分为JIT代码预热和数据预热,阿里内部有专门的一个产品负责这块,通过存储线上的常态化流量或者热点流量进行回放来提前预热, 起源于某年双十一零点的毛刺问题,原因是访问了数据库的冷数据rt增高导致的一系列上层限流,现在预热已经成了大促之前的一个必要流程。
3、强弱依赖:梳理强弱依赖是一个偏人肉的过程,但是非常重要,这是一个系统自查识别潜在风险点并为后续整理开关限流预案和根因分析的一个重要参考,阿里内部有一个强弱依赖检测的平台,通过对测试用例注入RPC调用的延迟或异常来观察链路的依赖变化,自动梳理出强弱依赖关系。
4、限流降级熔断:应对突发流量防止请求超出自身处理能力系统被击垮的必要手段;
5、监控告警&链路追踪:监控分为业务监控、系统监控和中间件监控和基础监控,作为线上问题发现和排查工具,重要性不言而喻。
参考:06 Hot Key和Big Key引发的问题怎么应对?
Hot key。对于大多数互联网系统,数据是分冷热的。比如最近的新闻、新发表的微博被访问的频率最高,而比较久远的之前的新闻、微博被访问的频率就会小很多。而在突发事件发生时,大量用户同时去访问这个突发热点信息,访问这个 Hot key,这个突发热点信息所在的缓存节点就很容易出现过载和卡顿现象,甚至会被 Crash。
Hot key 引发缓存系统异常,主要是因为突发热门事件发生时,超大量的请求访问热点事件对应的 key,比如微博中数十万、数百万的用户同时去吃一个新瓜。数十万的访问请求同一个 key,流量集中打在一个缓存节点机器,这个缓存机器很容易被打到物理网卡、带宽、CPU 的极限,从而导致缓存访问变慢、卡顿。
引发 Hot key 的业务场景很多,比如明星结婚、离婚、出轨这种特殊突发事件,比如奥运、春节这些重大活动或节日,还比如秒杀、双12、618 等线上促销活动,都很容易出现 Hot key 的情况。
要解决这种极热 key 的问题,首先要找出这些 Hot key 来。Hot key可以分为两种,已知和未知两种。
对于重要节假日、线上促销活动、集中推送这些提前已知的事情,可以提前评估出可能的预热 key 来。
找到热 key 后,就有很多解决办法了。
多级缓存架构
其次,也可以 key 的名字不变,对缓存提前进行多副本+多级结合的缓存架构设计。
缓存实例 实时扩容
再次,如果热 key 较多,还可以通过监控体系对缓存的 SLA 实时监控,通过快速扩容来减少热 key 的冲击。
本地缓存
业务端还可以使用本地缓存,将这些热 key 记录在本地缓存,来减少对远程缓存的冲击。
令牌桶。100w的计数器,然后每次请求去获取令牌,拿到就请求,拿不到就丢弃或者等待,等待超时就丢弃。
然后面试官问怎么实现?我回答了,然后不满足他的要求,让我继续优化。
然后我说在代理层面做,他说不一定能抗住这么大的请求量,你的处理延时怎么处理。
然后我说分发在每个服务器上面做,每个服务器限小流,然后如果负载均衡就可以实现整体限流满足要求。
db读写分离
引入缓存,缓存的数据过期机制天然避免了陈旧数据对空间的占用。
这里我们如何设计Redis的key-value呢?我们不难发现,同一个用户一天发出的帖子数量是有限的,通常不超过10条,平均3条左右,单个用户一周发的帖子很难超过100KB,极端情况下1MB,远低于Redisvalue大小的上限。所以:
key:userId+时间戳(精确到每星期)
value:Redis为hash类型,field为postId,value为帖子内容
expire设置为一个星期,即最多同时存在两个星期的数据(假设每贴平均长度0.1KB,1亿用户每天发3贴预计数量为400GB)
对某个用户一段时间范围的查找变为针对该用户本周时间戳的hscan命令,用户发帖等操作同时同步更新DB和缓存,DB的变更操作记录保证一致性。
但是,有些热用户的follower数量极高,意味着这个热点用户所在Redis服务器的查询频率为1000万每秒。
当一些比较热点的用户查询比较频繁的时候,我们可以直接把热点用户存入本地缓存中。用来缓解服务端的缓存的压力
根据点赞业务特点可以发现:
如果只考虑点赞可以怎么做?
可以用MySQL做持久存储,Redis做缓存,读写操作落缓存,异步线程定期刷新DB。
counter表:id,postId,count
RedisKV存储:key:postId value:count
但是以微博为例,不仅有点赞还有转发数,评论数,阅读量等。所以,业务拓展性和效率问题是难点,如果这么设计,我们就需要多次查询Redis,多次查询DB。
假设首页有100条消息,就需要for循环每一条消息,每条消息要进行4次Redis访问进行拼装。
所以这里我们进行优化的话,可以在MySQL层面进行增加列优化
conunter表:id,postId,readCount,forwordCount,commentCount,PraiseCount
这样增加的话,缺点是如果增加一个计数服务的话,列就需要改变。那么不想改变列怎么办呢?
conunter表:id,postId,countKey(计数类型名称,比如readCount),countValue
Redis来获取业务的话,可以存储为hash用来计数。
https://www.cnblogs.com/wangtao_20/p/7115962.html
按用户id取模(分库分表) 思考优点和缺点
优点
订单水平分库分表,为什么要按照用户id来切分呢?
好处:查询指定用户的所有订单,避免了跨库跨表查询。
因为,根据一个用户的id来计算节点,用户的id是规定不变的,那么计算出的值永远是固定的(x库的x表)
那么保存订单的时候,a用户的所有订单,都是在x库x表里面。需要查询a用户所有订单时,就不用进行跨库、跨表去查询了。
缺点
缺点在于:数据分散不均匀,某些表的数据量特别大,某些表的数据量很小。因为某些用户下单量多,打个比方,1000-2000这个范围内的用户,下单特别多,
而他们的id根据计算规则,都是分到了x库x表。造成这个表的数据量大,单表的数据量撑到极限后,咋办呢?
总结一下:每种分库分表方案也不是十全十美,都是有利有弊的。目前来说,这种使用用户id来切分订单数据的方案,还是被大部分公司给使用。实际效果还不错。程序员省事,至于数据量暴涨,以后再说呢。毕竟公司业务发展到什么程度,不知道的,项目存活期多久,未来不确定。先扛住再说。
b2b平台,上面支持开店,买家和卖家都要能够登陆看到自己的订单。
先来看看,分表使用买家id分库分表和根据卖家id分库分表,两种办法出现的问题
如果按买家id来分库分表。有卖家的商品,会有n个用户购买,他所有的订单,会分散到多个库多个表中去了,卖家查询自己的所有订单,跨库、跨表扫描,性能低下。
如果按卖家id分库分表。买家会在n个店铺下单。订单就会分散在多个库、多个表中。买家查询自己所有订单,同样要去所有的库、所有的表搜索,性能低下。
所以,无论是按照买家id切分订单表,还是按照卖家id切分订单表。两边都不讨好。
淘宝的做法是拆分买家库和卖家库,也就是两个库:买家库、卖家库。
买家库,按照用户的id来分库分表。卖家库,按照卖家的id来分库分表。
实际上是通过数据冗余解决的:一个订单,在买家库里面有,在卖家库里面也存储了一份。下订单的时候,要写两份数据。先把订单写入买家库里面去,然后通过消息中间件来同步订单数据到卖家库里面去。
买家库的订单a修改了后,要发异步消息,通知到卖家库去,更改状态。
https://mp.weixin.qq.com/s/fVt7rvX-uurIEV7hMSAHtA
粉丝列表拆分为: 关注列表、粉丝列表
正常用户分库分表, 百万、千万等大V单独处理(单独分库分表)
一些大V账号,我们也可以进行服务器端的本地、中间件进行多级缓存。
1.分布式锁使请求串行化 缺点:请求积压过多 可能会使请求超时
3.canal+mq+redis
场景:读多写少,服务返回需求在 毫秒级
1.一二级缓存,local cache + redis cache
2.大map 可以分成多个小map,读写锁保证并发安全
binglog 传送,中继日志重放需要时间,所以理论上备库延迟只能减少
中继日志:复制架构中,备服务器用于保存主服务器的二进制日志中读取到的事件;用于实现mysql的主从复制。
1.业务强制走主库,会影响业务
2.业务上增加 强制延时(如转账后进度条一直转)
gtid:事物id global transcation id
业务上等待刷新(如银行转账等)
1)、架构方面
1.业务的持久化层的实现采用分库架构,mysql服务可平行扩展,分散压力。
2.单个库读写分离,一主多从,主写从读,分散压力。这样从库压力比主库高,保护主库。
3.服务的基础架构在业务和mysql之间加入memcache或者redis的cache层。降低mysql的读压力。
4.不同业务的mysql物理隔离,分散压力。
5.使用比主库更好的硬件设备作为slave,mysql压力小,延迟自然会变小。
2.硬件
3.对于要求更新立马能查到的,可以强制走主库查询数据
静态化 或 cdn 等暂不考虑
1.业务隔离
2.系统隔离(秒杀服务单独)
3.数据隔离 提前预热 (热点发现- 热点数据、操作 )
4.缓存+限流
1.减少序列化 (减少rpc调用) 合并部署
2.代码优化 控制堆栈日志输出
高读 高写场景
高读
分层次校验,滤掉无效请求
1.权限
2.秒杀是否结束
3.本地缓存
行记录排队
1.减后库存要大于0
2.设置数据库类型为无符号整数(unsigned)
type Category struct {
Id uint `json:"id" gorm:"column:id;type:int(10) unsigned not null AUTO_INCREMENT;primary_key"`
Title string `json:"title" gorm:"column:title;type:varchar(250) not null;default:''"`
Description string `json:"description" gorm:"column:description;type:varchar(250) not null;default:''"`
Content string `json:"content" gorm:"column:content;type:longtext default null"`
ParentId uint `json:"parent_id" gorm:"column:parent_id;type:int(10) unsigned not null;default:0;index:idx_parent_id"`
Status uint `json:"status" gorm:"column:status;type:tinyint(1) unsigned not null;default:0;index:idx_status"`
CreatedTime int64 `json:"created_time" gorm:"column:created_time;type:int(11) not null;default:0;index:idx_created_time"`
UpdatedTime int64 `json:"updated_time" gorm:"column:updated_time;type:int(11) not null;default:0;index:idx_updated_time"`
DeletedTime int64 `json:"-" gorm:"column:deleted_time;type:int(11) not null;default:0"`
}
1.流量消峰(1.答题、验证码 防作弊,提交时间验证(大于2s) 2.按钮置灰 3.排队)
降低rt,提高并发
1.降低数据库锁力度:分库分表
2.降低锁持有时间
请求 队列化,
比如将200ms 请求放在一个内存队列,起一个异步线程,将200ms所有商品库存进行扣减,
api请求最差为200ms
秒杀商品id计数器,如果当前并发超过阈值,会创建内存队列,会将内存队列中购买一并扣减,降低数据库io次数
假如上游库存服务 被调用超时,上游订单服务 会发条消息给库存服务,进行库存回滚,
库存服务会根据mq中订单号查库存流水,判断订单是否扣减库存
秒杀的高并发分析
insert和update需要先执行insert
因为update同一行会导致行级锁,而insert是可以并行执行的。
cache aside db更新 创建缓存
监听binlog 保持缓存一致性,业务代码不关心缓存了,另外一个模块去刷新
监听binlog时,设计时间窗口, 判断5s内有更新,在5s结束时刷新下redis
因为binlog会并行过来,所有后面的binlog可能呗先监听到,会有消息乱序的情况。
数据库字段加版本号,先判断redis版本号,binglog中 大于版本号才去更新
阿里云团队针对数据库
由业务队列改为数据库队列
签名机制
http 是一种无状态的协议, 服务端并不知道客户端发送的请求是否合法, 也并不知道请求中的参数是否正确
客户端生成 sign: 可使用 公私钥非对称加密 的方式, 也可以使用计算字符串 md5 或者 hash 值的方式, 这个被加密的字符串最好不是固定的,取时间戳, 请求参数等就可以, 每次客户端把生成sign传递给服务端
服务端根据约定好的算法验证sign是否正确就可以
设计了防篡改之后, 接口总算是安全了那么一点点, 但是还不够…还需要对接口设计防重放设置
客户端在请求中添加两个参数
1.1 添加一个随机不重复的字符串参数 比如uuid 至于怎么让他不重复,可以考虑拼接时间戳,md5随机数等
1.2 添加一个请求时间的参数 如 request_time 值就是发送请求时的 时间戳
服务端接到这个请求:
1 先验证 sign 签名是否合理,证明请求参数没有被中途篡改
2 再验证 timestamp 是否过期,证明请求是在最近 60s 被发出的
3 最后验证 uuid 是否已经有了,证明这个请求不是 60s 内的重放请求
把这些值按照账号来区分存储在redis增加过期时间就可以了。
客户端调用API时,需要在请求中添加计算的签名。API网关在收到请求后会使用同样的方法计算签名,同用户计算的签名进行比较,相同则验证通过,不同则认证失败。
在API网关的签名中,提供X-Ca-Timestamp、X-Ca-Nonce两个可选HEADER,客户端调用API时一起使用这两个参数,可以达到防止重放攻击的目的。
场景:一个数组,里面有100个标题id, 每一个都需要通过http请求拿标题
1.长链接
2.合并请求为一次, 网络io从多次变为1次
3.mq 削峰填谷(不符合),接口需返回标题
常见的限流方法:
分布式环境下,可以考虑用 Redis+Lua 脚本实现令牌桶。
如果请求量太大了,Redis 也撑不住怎么办?我觉得可以类似于分布式 ID 的处理方式,Redis 前面在增加预处理,比如每台及其预先申请一部分令牌,只有令牌用完之后才去 Redis。如果还是太大,是否可以垂直切分?按照流量的来源,比如地理位置、IP 之类的再拆开。
https://www.toutiao.com/article/7081542340891951657
需求背景:
春节活动除夕晚上 7 点半会开始烟火大会,是大流量集中发券的一个场景,钱包侧与算法策略配合进行卡券发放库存控制,防止超发。
具体实现:
(1)钱包资产中台维护每个卡券模板 ID 的消耗发放量。
(2)每次卡券发放前算法策略会读取钱包 sdk 获取该卡券模板 ID 的消耗量以及总库存数。同时会设置一个阈值,如果卡券剩余量小于 10%后不发这个券(使用兜底券或者祝福语进行兜底)。
(3) 同时钱包资产中台方向在发券流程累计每个券模板 ID 的消耗量(使用 Redis incr 命令原子累加消耗量),然后与总活动库存进行比对,如果消耗量大于总库存数则拒绝掉,防止超发,也是一个兜底流程。
在方案一实现的基础上进行优化,并且要考虑数字不断累加、节约成本与实现容灾方案。在写场景,通过本地缓存进行合并写请求进行原子性累加,读场景返回本地缓存的值,减少额外的存储资源占用。使用 Redis 实现中心化存储,最终大家读到的值都是一样的。
每个 docker 实例启动时都会执行定时任务,分为读 Redis 任务和写 Redis 任务。
读取流程:
写入流程:
注意点:
本方案调用 Redis 的流量是跟实例数成正比,经调研读取侧的服务为主会场实例数 2 万个,写入侧服务为资产中台实例数 8 千个,所以实际 Redis 要支持的 QPS 为 2.8 万/定时任务执行间隔(单位为 s),经压测验证 Redis 单实例可以支持单 key2 万 get,8k incr 的操作,所以设置定时任务的执行时间间隔是 1s,如果实例数更多可以考虑延长执行时间间隔。
redis 多个zset根据点击商品次数(热点),定时轮训做本地缓存,redis缓存
商品被修改, 删除缓存(会有网络抖动更新失败情况)
canal订阅binlog,消费mq,更新redis缓存, 广播模式重置本地缓存(兜底)
注意⚠️:更新缓存最好有版本号,防止多个线程同时修改导致缓存不一致问题
热点问题,本地缓存
1.同一进城内模式下 singleflight单飞模式 保证同一时间一样的请求 只有一次能够访问数据库
2.不同进程内,设置分布式锁,其它线程sleep+失败重试,