高并发设计方案
高并发读
高并发读的设计思路主要是:加缓存 (Redis、MySQL 的 Slave、CDN)、并发读 (异步 RPC、Google 提出的冗余请求)、重写轻读、提前计算好多个表的关联查询 (定时计算、实时计算)、CQRS (Command Query Responsibility Separation 读写分离)
Google 的冗余请求是指:客户端首先发送一个请求,并等待服务器返回,如果一定时间内未返回,则马上给另外一台服务器发送同样的请求,客户端等待第一个响应到达之后,终止其他请求的处理。这个一定的时间是指:95% 请求的响应时间。
微博的重写轻读方案:
每个人的收件箱是存储在内存中的,需要为这个队列 (Redis 的 <key, list>
实现) 设置一个上限,比如 Twitter 设置的上限是 800。
超出 800 的微博放到 MySQL 中,可以按照 user_id
、time
等同时进行分片,然后可以再引入二级索引表:<user_id, month, count>
来查询某个用户在某个月份发表的微博的总数量,根据这个表可以快速定位到 offset = 5000 的微博发生在哪个月份,也就是数据库的分片。
至于粉丝数量比较大的,可以读的时候实时聚合,或者叫做拉。
一个人关注的人当中,有的人是推给他的,有的人是需要去拉的,需要聚合两者,再按时间排序,然后分页显示,这就是推拉结合。
读写分离的典型架构:
高并发写
一般采用的思路就是:数据分片 (分库分表)、任务分片 (Map/Reduce、Tomcat 1+N+M)、异步化 (通过队列发送短信验证码)、串行化+多进程单线程+异步I/O
发送短信验证码是异步的:
广告系统的扣费是异步化的:
LSM 树是异步落盘,提高写入性能的:
Pipeline 也属于异步化,Leader 一批批地处理消息:
高并发读资源优化思路
本节内容来源 阿里Sentinel原理解析
资源与slot chain
的对应关系存放在CtSph
类全局静态变量chainMap
中:
private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap
= new HashMap<ResourceWrapper, ProcessorSlotChain>();
这也意味着系统所有资源的访问都会经过chainMap
,这也意味着chainMap
是一个竞态热点访问数据。这就要求访问chainMap
是高性能的同时,chainMap
的更新也是线程安全的。
// 根据资源获取对应的SlotChain
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// Entry size limit.
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
chain = SlotChainProvider.newSlotChain();
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
// 切换引用即可
chainMap = newMap;
}
}
}
return chain;
}
我们看到代码没有对chainMap
加任何锁,只是在更新chainMap
时是通过额外加锁和复制替换的形式。这里面用到的技巧包括了volatile
特性、copyOnWrite
、synchronized
。这样高并发下读写操作是并行的,只有写写操作之间串行。但注意的是写操作是一个纯内存操作,只有第一次访问资源时才会触发,其时间花费只与资源的数量成正比,正常应用资源个数一般在数千以内,并且对象是共享的,这个花费的时间是非常的少。另外阿里也做了资源数量的限制: MAX_SLOT_CHAIN_SIZE = 6000
。所以写写操作也是非常的快,比例也很少。再加上volatile
关键字的特性,chainMap
更新后对所有线程都可见,线程安全。
为什么不用
ConcurrentHashMap
,是因为读远远多于写。每次读都要 CAS,是没有必要的。
高并发写资源优化思路 (分桶思想)
Sentinel 是以 Bucket
(桶)为单位记录一段时间内的请求总数、异常总数、总耗时的,而一个Bucket
可以是记录一秒内的数据,也可以是10毫秒内的数据,我们称这个时间区间为Bucket
的统计单位,是由使用者自定义的。
public class MetricBucket {
/**
* 存储各事件的计数,比如异常总数、请求总数等
*/
private final LongAdder[] counters;
/**
* 这段事件内的最小耗时
*/
private volatile long minRt;
}
有了Bucket之后,假设我们需要让Bucket存储一秒钟的数据,这样我们就能够知道每秒处理成功的请求数(成功QPS)、每秒处理失败的请求数(失败QPS),以及处理每个成功请求的平均耗时(avg RT)。但是我们如何才能确保Bucket存储的就是精确到1秒的数据呢?
Sentinel是这样实现的。它定义一个Bucket数组,根据时间戳来定位到数组下标。假设我们需要统计每1秒处理的请求数等数据,且只需要保存最近一分钟的数据。那么Bucket数组的大小就可以设置为60,每个Bucket的windowLengthInMs
窗口大小就是1000毫秒(1秒)。这个数组可以循环使用,永远只保存最近1分钟的数据。
容量规划
机器数量怎么计算?
机器数 = 预估总流量/单机容量
- 分母是预估(通过历史数据估算,过去24小时的调用量分布,取其中的峰值,再乘以一个系数,比如2倍、3倍)的值
- 分子通过压力测试得到
必须使用峰值测算,不能用均值,虽然持续时间短,可是没办法,的确需要这么多台机器,这也正是云计算 (弹性计算) 要解决的问题。