一、网关简介
1、网关背景
- 由于微服务“各自为政的特性”使微服务的使用非常麻烦。
- 通常公司会有一个“前台小姐姐”作为统一入口,这就是网关
2、网关作用
- 统一入口:为服务提供一个唯一的入口,网关起到外部和内部隔离的作用, 保障了后台服务的安全性。
- 鉴权校验:识别每个请求的权限,拒绝不符合要求的请求。
- 动态路由:动态的将请求路由到不同的后端集群中。
- 减少客户端与服务端的耦合:服务可以独立发展,通过网关层来做映射。
3、网关优势
- 路由和负载均衡:网关可以作为反向代理,接收客户端请求并将其路由到后端的多个服务器。通过使用负载均衡算法,网关可以根据服务器的负载情况分发请求,从而实现请求的平衡和优化。
- 安全性和访问控制:网关可以充当安全层,通过在请求到达应用程序之前进行身份验证、授权和访问控制来保护应用程序。它可以拦截和阻止恶意请求、DDoS攻击和SQL注入等常见的安全威胁。
- 协议转换和数据格式转换:网关可以处理不同的协议和数据格式之间的转换。它可以将来自客户端的请求从一种协议转换为另一种协议,或将后端服务的响应转换为适合客户端的数据格式,从而实现系统之间的无缝集成和通信。
- 监控和分析:网关可以收集和监控来自客户端和后端服务的请求和响应数据。它可以记录日志、统计数据和指标,提供关于请求流量、性能和错误的实时监控和分析。这些信息可以帮助开发人员和管理员识别问题、优化性能和做出决策。
4、网关劣势
- 在微服务这种去中心化架构中,成为瓶颈点:如果网关挂掉,将会影响整个系统的可用性。作为系统的入口点,网关的故障可能导致无法访问后端服务和应用程序。
- 服务如果不是异步或者同步非阻塞,耦合度高:如果通过网关调用后端服务不可用或者响应时间超时,我们的连接数量是有限的,这时候网关处理能力下降这时候可能造成其他服务的不可用,严重者甚至导致雪崩效应
二、常见网关的对比
1、Netflix Zuul1.0
- 中小厂落地案例丰富
- 基于同步阻塞I0,性能差
- Netflix已经停止了对Zuul 1.0的维护和开发
2、SpringCloud Gateway
- 是Spring Cloud生态系统中的官方网关解决方案,与其他Spring Cloud组件(如Eureka、Ribbon、Hystrix等)无缝集成
- 响应式支持:Spring Cloud Gateway基于Spring WebFlux框架,使用非阻塞式I/O和响应式编程模型,具有高性能和高吞吐量的特点
3、Netflix Zuul2.0
- 基于非阻塞式I/O模型,性能接近SpringCloud Gateway
- 它的开发和维护在2019年底停止了。Netflix推荐使用Spring Cloud Gateway作为替代方案,
4、综合对比
5、为什么自研网关
5.1 开源网关的不足
- 限制性:开源网关通常具有一些默认的行为和规则,可能无法满足特定业务需求的定制化要求。某些功能可能无法直接扩展或修改,需要深入了解和修改源代码,这对于非开发人员来说可能是一项挑战。
- 可靠性和稳定性:开源网关在稳定性和可靠性方面可能存在一些问题。尽管开源社区通常会及时修复和改进问题,但是在某些情况下,可能会遇到较长时间的修复周期或仍存在未解决的问题。这对于对高可用性和系统稳定性要求较高的应用来说可能是一个问题。
- 技术支持和文档:相比商业解决方案,开源网关通常缺乏官方的技术支持和详细的文档。虽然开源社区通常提供一定的支持,但可能无法提供即时和全面的帮助。这对于在生产环境中使用开源网关的企业来说可能是一个潜在的风险。
- 学习和培训成本:使用开源网关可能需要团队成员投入时间和精力来学习和理解其架构、配置和使用方式。这可能需要额外的培训和学习成本,特别是对于新加入的团队成员来说。
- 安全性和漏洞:尽管开源网关通常经过广泛的社区审查,但仍然存在潜在的安全漏洞。由于开源代码的公开性,攻击者可以更容易地发现和利用其中的漏洞。这要求团队及时更新和升级网关,以确保应用的安全性。
- 依赖和版本兼容性:开源网关可能依赖于其他的开源组件和库,可能需要处理版本兼容性和依赖冲突的问题。这可能需要额外的工作量来管理和升级依赖项,以确保系统的稳定性和一致性。
5.2 自研网关优势
- 定制化能力:自研网关可以根据具体业务需求进行定制和扩展,满足特定场景的需求。你可以根据自己的业务逻辑、安全需求、性能要求等来设计和实现网关,确保网关与整个系统的需求高度匹配。
- 灵活性和可扩展性:自研网关可以根据业务的发展和变化进行灵活调整和扩展。你可以根据需求添加新的功能、修改路由规则、引入新的协议等,而无需依赖第三方网关的更新和发布周期。
- 性能和吞吐量:自研网关可以根据需求进行性能优化,以满足高并发和低延迟的要求。你可以选择合适的技术栈、采用非阻塞式I/O模型、引入异步处理等,以提升网关的性能和吞吐量。
- 安全性和可控性:自研网关可以根据业务需求实现定制化的安全策略和访问控制机制。你可以集成各种认证和授权机制、请求过滤和防御机制,以确保系统的安全性,并根据具体需求进行调整和优化。
- 增强的监控和调试能力:自研网关可以针对自身的需求添加详细的监控指标和调试功能。你可以收集和展示请求流量、性能指标、错误日志等数据,以便进行监控、故障排查和性能优化。
- 技术栈选择:自研网关可以根据团队的技术栈和专长选择合适的技术和工具。你可以选择自己熟悉和喜欢的编程语言、框架和库,使网关的开发和维护更加高效和舒适。
三、网关整体设计
1、技术选型
1.1 基础框架
- SpringIOC ,SpringAOP,Springmvc
- Spring Boot
- 原生Java
1.2 网络框架
- 原生NIO
- Mina
- Netty
1.3 注册中心
- Zookeeper:Zookeeper是一个分布式协调系统,可以用于服务注册与发现。它提供了一个可靠的分布式数据存储,并支持高可用性和一致性。由于他是强一致所以不适合,大数据量的注册
- Eureka:Eureka是Netflix开源的服务注册与发现组件,被设计为在云环境中运行。它采用了基于REST的架构,具有简单的配置和易于使用的特点。Netflix在官方文档中已宣布停止对Eureka的更新和维护
- Consul:Consul是一个开源的服务网格解决方案,提供了服务注册与发现、健康检查、键值存储、分布式一致性和多数据中心功能等,但是他是GO语言实现的
- Nacos:Nacos是阿里巴巴开源的一个服务发现、配置管理和服务治理平台。它提供了统一的服务注册与发现、动态配置管理、服务健康监测和流量管理等功能,帮助构建和管理云原生应用和微服务架构
1.4 配置中心
- Spring Cloud Config:Spring Cloud Config是Spring Cloud提供的配置管理工具用于集中管理和分发应用程序的配置,但是不支持动态刷新
- Apollo
- Nacos
2、高性能要点
2.1异步化处理
- 单异步模式 (Future)
- 双异步模式 (completeFuture)
Future
接口在某些方面存在一些局限性,这些局限性包括:
- 阻塞式获取结果:
Future
的get()
方法是阻塞的,这意味着如果任务的结果还没有准备好,调用get()
方法的线程将会被阻塞,直到结果可用。这可能会导致应用程序的性能下降,特别是当需要同时处理多个Future
对象时。 - 无法取消任务:
Future
接口提供了cancel()
方法来取消任务的执行,但是这个方法并不能真正地取消任务的执行。它只是尝试去取消任务,并返回一个表示取消成功与否的布尔值。如果任务已经开始执行或已经完成,那么cancel()
方法将无效。 - 缺乏异常处理:
Future
接口的get()
方法会抛出InterruptedException
和ExecutionException
异常。然而,这些异常并不提供足够的信息来了解任务失败的原因。当任务抛出异常时,我们无法在Future
对象上捕获和处理这些异常。 - 无法组合多个
Future
对象:在某些情况下,我们可能需要组合多个异步任务的结果,例如并行执行多个任务并将它们的结果合并。Future
接口本身并不提供直接的支持来处理这种情况,需要使用其他的方法,例如使用CompletionService
或者CompletableFuture
来实现。
为了克服这些局限性,Java 8 引入了 CompletableFuture
类,它提供了更强大和灵活的功能,包括更好的异常处理、组合多个任务的结果以及异步任务的回调等。CompletableFuture
类提供了一种更现代和易于使用的方式来处理异步编程任务。
CompletableFuture
CompletableFuture是Java编程语言中的一个类,它在Java 8中引入作为CompletableFuture API的一部分。它属于java.util.concurrent包,并提供了一种执行异步编程和处理异步计算结果的方式。
CompletableFuture代表了一个可能在未来完成的计算,并允许你将多个操作链接在一起形成一个流水线。每个操作都是异步执行的,这意味着它可以与其他操作或任务并发运行。
以下是关于CompletableFuture的一些关键特性和概念:
- 异步执行:CompletableFuture提供了一种异步执行任务的方式,这意味着你可以启动一个任务并继续执行其他操作,而不需要等待任务完成。
- 组合:你可以将多个CompletableFuture实例链接在一起,形成一个操作的流水线。这使得你可以表达复杂的异步工作流和任务之间的依赖关系。
- 完成动作:CompletableFuture允许你指定在CompletableFuture完成时应执行的动作,无论是正常完成还是异常完成。你可以附加回调函数,在计算结果可用时执行,或者处理任何发生的异常。
- 结果合并:CompletableFuture提供了多种方法,在多个CompletableFuture都完成时合并它们的结果。例如,你可以使用
thenCombine
方法指定一个函数,将两个CompletableFuture的结果合并并产生一个新的结果。 - 异常处理:CompletableFuture提供了多种方法来处理计算过程中发生的异常。你可以使用
exceptionally
方法指定一个备用值,或者使用handle
方法处理异常并基于异常产生一个结果。 - 异步执行模型:CompletableFuture支持不同的执行模型,例如使用单独的线程异步执行任务,或者利用线程池执行任务。
package com.msb;
import java.util.concurrent.*;
public class FutureTaskTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建任务T2 FutureTask
FutureTask<String> ft2 = new FutureTask<>(new T2Task());
// 创建任务T1的 FutureTask
FutureTask<String> ft1 = new FutureTask<>(new T1Task(ft2));
// 线程1 执行T2任务
Thread t1 = new Thread(ft2);
t1.start();
// 线程2执行 任务他
Thread t2 = new Thread(ft1);
t2.start();
//等待返回结果
System.out.println(ft1.get());
}
}
// T1 任务 洗水壶,烧开水,泡茶
class T1Task implements Callable<String>{
FutureTask<String> ft2;
public T1Task(FutureTask<String> ft2) {
this.ft2 = ft2;
}
@Override
public String call() throws Exception {
System.out.println("T1 洗水壶。。。。。");
TimeUnit.SECONDS.sleep(1);
System.out.println("T1 烧开水。。。。。");
TimeUnit.SECONDS.sleep(2);
// 获取T2线程
String res = ft2.get();
System.out.println("T1 拿到茶叶。。。" + res);
System.out.println("T1 泡茶。。。。。");
return "喝茶" + res;
}
}
// T2任务 洗茶壶、洗茶杯、拿茶叶
class T2Task implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("T2 洗茶壶。。。。。");
TimeUnit.SECONDS.sleep(1);
System.out.println("T2 洗茶杯。。。。。");
TimeUnit.SECONDS.sleep(2);
System.out.println("T2 拿茶叶。。。。。");
TimeUnit.SECONDS.sleep(1);
return "铁观音";
}
}
package com.msb;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class CompletableFutureTest {
public static void main(String[] args) {
// 任务1: 洗水壶 -> 烧开水
CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> {
System.out.println("T1 洗水壶。。。。。");
sleep(1,TimeUnit.SECONDS);
System.out.println("T1 烧开水。。。。。");
sleep(2,TimeUnit.SECONDS);
});
// 任务2: 洗茶壶 -> 洗茶杯 -> 拿茶叶
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> {
System.out.println("T2 洗茶壶。。。。。");
sleep(1,TimeUnit.SECONDS);
System.out.println("T2 洗茶杯。。。。。");
sleep(2,TimeUnit.SECONDS);
System.out.println("T2 拿茶叶。。。。。");
sleep(1,TimeUnit.SECONDS);
return "铁观音";
});
// 任务3: 任务1和任务2完成后执行:泡茶
CompletableFuture<String> f3 = f1.thenCombine(f2, (__, tf) -> {
System.out.println("T1 拿到茶叶。。。。" + tf);
System.out.println("T1 泡茶");
return "喝茶:" + tf;
});
// 等待任务3的执行结果
System.out.println(f3.join());
}
static void sleep(int t ,TimeUnit u){
try {
u.sleep(t);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.2 使用缓存机制
尽量使用内容作为缓存(Map、Queue) ,比如加载的配置
2.3 合理利用串行和并行化机制
-
串行化使用场景
耗时较小,性能要求较高的场景
-
并行化使用场景
耗时较久,任务之间没有依赖关系,比如远程RPC调用场景