架构设计方法(二)高可靠与高可用

195 阅读6分钟

以下内容是在学习过程中的一些笔记,难免会有错误和纰漏的地方。如果造成任何困扰,很抱歉。

理论基础

首先是高并发的理论基础:吞吐量 * 响应时间 = 并发数

  • 吞吐量:单位时间内处理的请求数;
  • 响应时间:处理每个请求所需的时间;
  • 并发数:服务器同时并行处理的请求个数;

对于单机最大的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

待补充

业务微服务高可用

待补充

存储高可用

待补充

结束