以下内容是在学习过程中的一些笔记,难免会有错误和纰漏的地方。如果造成任何困扰,很抱歉。
理论基础
首先是高并发的理论基础:吞吐量 * 响应时间 = 并发数
- 吞吐量:单位时间内处理的请求数;
- 响应时间:处理每个请求所需的时间;
- 并发数:服务器同时并行处理的请求个数;
对于单机最大的QPS计算方法,实际上我认为最有效的计算方式还是压力测试,实践出真理。
一、熔断与限流
首先聊聊限流,限流的维度大概分为两种
- 限制系统的最大资源使用数:如Nginx、Linux中的并发连接数limit.conf;
- 限制速率:分为单机限流和中央限流,单机限流如令牌桶算法、Nginx的限流模块等,或者打造一个中央限流系统;
漏桶算法与令牌桶算法
图解说明
-
令牌桶算法
下面通过google的RateLimiter实现,就不自己造轮子了,首先是依赖引入
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.0.1-jre</version> </dependency>
上代码
import com.google.common.util.concurrent.RateLimiter; public class DemoApplication { public static void main(String[] args) { // 每秒产生100个令牌,还可以多两个入参,设置预热时间 RateLimiter rateLimiter = RateLimiter.create(100); // 从这个RateLimiter获得给定数量的许可证 阻塞 直到请求可以被授予 返回速率 double acquire = rateLimiter.acquire(1); // 从这个RateLimiter获得给定数量的许可证 非阻塞 返回布尔 boolean tryAcquire = rateLimiter.tryAcquire(1); } }
-
漏桶算法
个人理解的方式:数据包放在一个容器,由其它线程定时(一定的输出速率)去容器中取出数据包,我这里利用了队列的性质,首先是准备数据容器,然后是数据的取出,这里还引入了超时处理机制,我认为这里面的设计更多的包含了Reactor的处理思想。
import com.google.common.util.concurrent.SimpleTimeLimiter; import org.apache.commons.lang3.ObjectUtils; import java.util.concurrent.*; public class DemoApplication { private static ThreadPoolExecutor pool = null; private static BlockingQueue<String> consumerList = new ArrayBlockingQueue<>(1000000); public static void main(String[] args) { try { // 判断队列中是否有数据 if (consumerList.size() > 0) { // 取走排在首位的对象 若不能立即取出 则可以等time参数规定的时间 取不到时返回null String data = consumerList.poll(10, TimeUnit.MILLISECONDS); // 对象非空判断 if (ObjectUtils.isNotEmpty(data)) { // 业务逻辑处理 .... 此处引入简易计时器应对超时处理 SimpleTimeLimiter timeLimiter = SimpleTimeLimiter.create(pool); Callable<String> workPlan = new Callable<String>() { /** * Computes a result, or throws an exception if unable to do so. * @return computed result * @throws Exception if unable to compute a result */ @Override public String call() throws Exception { // todo return "my data"; } }; timeLimiter.callWithTimeout(workPlan, 3, TimeUnit.MINUTES); } } } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } catch (ExecutionException e) { throw new RuntimeException(e); } catch (TimeoutException e) { throw new RuntimeException(e); } } }
另外我们也可以通过google的ListenableFuture
来处理并发问题,顾名思义,它可以帮助我们监听结果是否完成,不再细说。
Reactor
额外聊一下反应器设计模式,Reactor 翻译过来就是反应器,简单理解就是对接收到的请求进行高效的自反应处理,不过这么说还是太敷衍了,看看下面
The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.
反应器设计模式 是一种事件处理模式,用于处理由一个或多个输入并发地交付给服务处理程序的服务请求。然后,服务处理程序将传入请求解复用,并将它们同步地分发到相关的请求处理程序。
Reactor是基于NIO模型实现的,为什么不用BIO模型理由也是非常容易说明,这是关于单线程阻塞IO
跟多路复用IO
两种模型的比较
由于 Reactor 是一个设计模型,所以有三种实现方案,方案具体使用进程还是线程,要看使用的编程语言以及平台有关
- Java 语言一般使用线程,比如 Netty
- C 语言使用进程和线程都可以,例如 Nginx 使用的是进程,Memcache 使用的是线程
首先看看单线程Reactor下的基本模型

应用程序中的对象的作用
- Reactor 对象的作用是监听和分发事件
- Acceptor 对象的作用是获取连接
- Handler 对象的作用是处理业务
接下来,介绍下「单 Reactor 单进程」这个方案:
- Reactor 对象通过
select (IO 多路复用接口)
监听事件,收到事件后通过 Dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型; - 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
- 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
它另外还有两种方案以及优缺点不做说明
- 单Reactor + 多进程/线程
- 多Reactor + 多进程/线程
基于时间窗口的统计
基于时间窗口的统计就是统计每个时间窗口的请求量,单位时间可以是1秒、1分钟,然后把单位时间内的请求量
与设置的阈值
做对比,这里的请求量是一个count不断累加的过程,先列举一个简单的代码
import org.springblade.common.tool.ThreadPoolUtils;
import org.springblade.netty.NettyServerInboundHandler;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 简易限流器
* @author 李家民
*/
@Component
public class SimpleLimitedTraffic {
/** 轮询时长 毫秒 */
private static long durationPolling = 10000;
/** 单位时间内请求最大次数 */
private static int maximum = 20;
/** 流量桶 */
private static ConcurrentHashMap<String, AtomicInteger> bucketTraffic = new ConcurrentHashMap<>();
/**
* 添加流量
* @param address
*/
public static void addTrafficOnBucket(String address) {
if (bucketTraffic.containsKey(address)) {
AtomicInteger atomicInteger = bucketTraffic.get(address);
atomicInteger.addAndGet(1);
bucketTraffic.put(address,atomicInteger);
}else {
bucketTraffic.put(address, new AtomicInteger(0));
}
}
/**
* 移除地址
* @param address
*/
public static void removeAddressBucket(String address) {
bucketTraffic.remove(address);
}
/**
* 桶监听
*/
@PostConstruct
private void bucketListening() {
CompletableFuture.runAsync(() -> {
try {
while (true) {
Thread.sleep(durationPolling);
bucketTraffic.forEach((k, v) -> {
if (v.get() > maximum) {
NettyServerInboundHandler.closeGameSession(k);
removeAddressBucket(k);
} else {
bucketTraffic.put(k, new AtomicInteger(0));
}
});
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}, ThreadPoolUtils.getThreadPool());
}
}
中央限流/熔断/超时重试
-
中央限流:顾名思义,通过中央限流系统去分配及限制请求量,一般通过网关去做,本人业务涉及不到,不做说明。
-
熔断:策略大致有两种
- 单位时间内,根据请求失败率做熔断
- 单位时间内,根据请求响应时间做熔断
常见的处理方案如阿里巴巴的Sentinel。
-
超时重试:例如谷歌的SimpleTimeLimiter。
二、灰度发布、备份与回滚
灰度发布(又名金丝雀发布)的几种类型
- 金丝雀发布
- 滚动发布
- 蓝绿发布
方案之一是可以通过Nginx反向代理,并使用内嵌的LUA脚本解析cookie是否符合灰度发布要求的流量进行引流,但是最为重要的在于出现问题后的回滚,涉及到:
- 功能回滚:比较好解决,通过代码恢复 or 安装包恢复处理;
- 数据回滚:同时使用同一个数据库的情况下如何处理脏数据?同时KV存储中的脏数据如何处理?
可以从数据来源标识、快照恢复(全量增量)、备份恢复进行着手。
三、高可用问题提出
首先根据书中提出的问题:
- 如何实现故障探测?---心跳
- 如何解决脑裂?---新纪元新版本
- 如何做到数据一致性?---强弱一致性的权衡
- 如何做到对客户端透明?---虚拟IP代理
- 如何解决高可用依赖的连环套问题?
接入层网关高可用
根据实际业务场景划分
- DNS广域网
- 网关
- Nginx
- Tomcat
待补充
业务微服务高可用
待补充
存储高可用
待补充