高并发设计方案

高并发设计方案

高并发读

高并发读的设计思路主要是:加缓存 (Redis、MySQL 的 Slave、CDN)、并发读 (异步 RPC、Google 提出的冗余请求)、重写轻读、提前计算好多个表的关联查询 (定时计算、实时计算)、CQRS (Command Query Responsibility Separation 读写分离)

Google 的冗余请求是指:客户端首先发送一个请求,并等待服务器返回,如果一定时间内未返回,则马上给另外一台服务器发送同样的请求,客户端等待第一个响应到达之后,终止其他请求的处理。这个一定的时间是指:95% 请求的响应时间

微博的重写轻读方案:

每个人的收件箱是存储在内存中的,需要为这个队列 (Redis 的 <key, list> 实现) 设置一个上限,比如 Twitter 设置的上限是 800。

超出 800 的微博放到 MySQL 中,可以按照 user_idtime 等同时进行分片,然后可以再引入二级索引表:<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特性、copyOnWritesynchronized。这样高并发下读写操作是并行的,只有写写操作之间串行。但注意的是写操作是一个纯内存操作,只有第一次访问资源时才会触发,其时间花费只与资源的数量成正比,正常应用资源个数一般在数千以内,并且对象是共享的,这个花费的时间是非常的少。另外阿里也做了资源数量的限制: MAX_SLOT_CHAIN_SIZE = 6000。所以写写操作也是非常的快,比例也很少。再加上volatile关键字的特性,chainMap更新后对所有线程都可见,线程安全。

为什么不用 ConcurrentHashMap,是因为读远远多于写。每次读都要 CAS,是没有必要的。

高并发写资源优化思路 (分桶思想)

本节内容来自 Alibaba Sentinel是如何统计QPS实现限流的

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倍)的值
  • 分子通过压力测试得到

必须使用峰值测算,不能用均值,虽然持续时间短,可是没办法,的确需要这么多台机器,这也正是云计算 (弹性计算) 要解决的问题。