首先我们来看Zuul 2的启动代码:
try {
// 读取properties配置
ConfigurationManager.loadCascadedPropertiesFromResources("application");
// IOC容器依赖注入
Injector injector = InjectorBuilder.fromModule(new ZuulSampleModule()).createInjector();
BaseServerStartup serverStartup = injector.getInstance(BaseServerStartup.class);
// 获取已构建的服务
server = serverStartup.server();¡¡
long startupDuration = System.currentTimeMillis() - startTime;
System.out.println("Zuul Sample: finished startup. Duration = " + startupDuration + " ms");
// 启动服务
server.start(true);
} catch (Throwable t) {
...
} finally {
if (server != null) server.stop();
System.exit(exitCode);
}
是不是有一些似曾相识?
1 加载配置文件
ConfigurationManager#loadCascadedPropertiesFromResources方法是Netflix处理配置的公共方法,所以比较固执,该方法的入参是configName。
首先,它会加载configName.properties,其次,它会加载configName-deploymentEnvironment.properties,configName-deploymentEnvironment.properties会覆盖configName.properties中出现的属性。
// ① 加载配置属性
Properties props = loadCascadedProperties(configName);
// ② 如果是聚合配置类型
if (instance instanceof AggregatedConfiguration) {
// 使用可并发控制的配置类加载配置,然后放在配置实例中
ConcurrentMapConfiguration config = new ConcurrentMapConfiguration();
config.loadProperties(props);
((AggregatedConfiguration) instance).addConfiguration(config, configName);
} else {
// ③ 随意加载配置
ConfigurationUtils.loadProperties(props, instance);
}
- 就是很简单的拼装两个文件名称,然后用Thread的ClassLoader加载Properties,然后利用HashTable#putAll合并两个属性对象,是用configName.properties合并configName-deploymentEnvironment.properties。
- ConcurrentMapConfiguration是用ConcurrentHashMap的高吞吐和线程安全版本,Zuul 2以此来读写配置属性,在loadProperties时,也是非常小心的使用前置监听、后置监听、线程安全的容器进行加载。这里的AggregatedConfiguration的意思是,它拥有一个configList来存储各种配置。如果配置发生变更,会将配置放在list的尾部;如果配置没有发生变更,那么就将配置放在原处。
- 如果不是AggregatedConfiguration,那么我只需要用AbstractConfiguration的实现类来接受配置即可。
2 容器注入
Sample中,服务有以下注册需求:
@Override
protected void configure() {
// 启动器
bind(BaseServerStartup.class).to(SampleServerStartup.class);
// 一段自定义Eureka实例配置的代码
bind(EurekaInstanceConfig.class).to(MyDataCenterInstanceConfig.class);
// 基础NettyOriginManager,用于管理后端服务
bind(OriginManager.class).to(BasicNettyOriginManager.class);
// 添加ZuulFilterModule绑定类型
install(new ZuulFiltersModule());
// 恶汉单例模式的FilterFileManager
bind(FilterFileManager.class).asEagerSingleton();
// 服务器健康状态、发现状态管理
bind(ServerStatusManager.class);
// 请求接入时的Session装饰器
bind(SessionContextDecorator.class).to(ZuulSessionContextDecorator.class);
// 计数器注册
bind(Registry.class).to(DefaultRegistry.class);
// Request请求完成的处理器,也是用于计数
bind(RequestCompleteHandler.class).to(BasicRequestCompleteHandler.class);
// 服务发现客户端,实现的是Eureka
bind(AbstractDiscoveryClientOptionalArgs.class).to(DiscoveryClient.DiscoveryClientOptionalArgs.class);
// Request请求时间点计算与发布
bind(RequestMetricsPublisher.class).to(BasicRequestMetricsPublisher.class);
// Request日志生成,以及打一些Request基础日志
bind(AccessLogPublisher.class).toInstance(new AccessLogPublisher("ACCESS",
(channel, httpRequest) -> ClientRequestReceiver.getRequestFromChannel(channel).getContext().getUUID()));
}
Zuul整体使用的是Guice管理对象的依赖注入问题。
接下来我们进行逐个分析。
2.1 SampleServerStartup(基础服务启动类)
依赖注入的地方代码虽然庞大且杂乱,但是简单分析一下,就是我们之前看到的一些需要注入进IOC容器的对象。
@Inject
public SampleServerStartup(ServerStatusManager serverStatusManager, FilterLoader filterLoader,
SessionContextDecorator sessionCtxDecorator, FilterUsageNotifier usageNotifier,
RequestCompleteHandler reqCompleteHandler, Registry registry,
DirectMemoryMonitor directMemoryMonitor, EventLoopGroupMetrics eventLoopGroupMetrics,
EurekaClient discoveryClient, ApplicationInfoManager applicationInfoManager,
AccessLogPublisher accessLogPublisher, PushConnectionRegistry pushConnectionRegistry,
SamplePushMessageSenderInitializer pushSenderInitializer) {
super(serverStatusManager, filterLoader, sessionCtxDecorator, usageNotifier, reqCompleteHandler, registry,
directMemoryMonitor, eventLoopGroupMetrics, discoveryClient, applicationInfoManager,
accessLogPublisher);
// 维持Websocket和SSE渠道的客户端验证映射关系
this.pushConnectionRegistry = pushConnectionRegistry;
// 加入到ChannelPipeline中
this.pushSenderInitializer = pushSenderInitializer;
}
- 维持Websocket和SSE渠道的客户端验证映射关系,实现方式是ConcurrentHashMap,key是clientId,主要是为了为Websocket和SSE的消息push做一下客户端映射关系。
- 一个加入到ChannelPipeline中的简单实现,用于发送客户端请求的一些信息。
接下来实现来自超类BaseServerStartup#choosePortsAndChannels方法:
// 启动服务@Override
protected Map<Integer, ChannelInitializer> choosePortsAndChannels(
ChannelGroup clientChannels,
ChannelConfig channelDependencies) {
// 放入到ChannelPipeline中的渠道信息,key:端口,value:ChannelInitializer
Map<Integer, ChannelInitializer> portsToChannels = new HashMap<>();
// 获取Zuul服务的启动端口,默认为70001
int port = new DynamicIntProperty("zuul.server.port.main", 7001).get();
// 首先加载默认的Channel配置
ChannelConfig channelConfig = BaseServerStartup.defaultChannelConfig();
// 获取推送端口,默认的推送端口是7008
int pushPort = new DynamicIntProperty("zuul.server.port.http.push", 7008).get();
ServerSslConfig sslConfig;
// 下面这些配置的调整依赖于你是否在ELB Http监听器、TCP监听器或者直接放在互联网上
switch (SERVER_TYPE) {
case HTTP:
...
break;
case HTTP2:
...
break;
case HTTP_MUTUAL_TLS:
...
break;
case WEBSOCKET:
...
break;
case SSE:
...
break;
}
return portsToChannels;
}
也就是说SampleServerStartup是根据不同的使用场景,指定不同的ServerStartup。
那我们继续看下它的超类BaseServerStartup,它的核心方法是init():
@PostConstruct
public void init() throws Exception {
// Channel的一些配置管理
ChannelConfig channelDeps = new ChannelConfig();
// 添加一些Channel的依赖
addChannelDependencies(channelDeps);
// Channel组
ChannelGroup clientChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
// 客户端关闭钩子
clientConnectionsShutdown = new ClientConnectionsShutdown(clientChannels,
GlobalEventExecutor.INSTANCE, discoveryClient);
// 构建端口和指定的ChannelInitializer
portsToChannelInitializers = choosePortsAndChannels(clientChannels, channelDeps);
// 根据上面的信息构建Server
server = new Server(portsToChannelInitializers, serverStatusManager, clientConnectionsShutdown, eventLoopGroupMetrics);
}
- 是把我们之前声明的一些组件放入到ChannelConfig中,这些组件每个Channel都会用到。
- DefaultChannelGroup用于提供通知ChannelGroupFuture任务的服务组,而ChannelGroupFuture用于接受会影响ChannelGroup中Channel的I/O操作,是一个异步操作的结果集。
- 暂且理解它负责客户端关闭。
- choosePortsAndChannels()即为上面提到根据不同的协议,构建不一样的ChannelInitializer。
- 构建Server服务。
在默认配置中,我们会注入最大连接数,最大连接请求,空闲时间,读取时间等属性。
2.2 BasicNettyOriginManager(Origin管理者)
一个基础的基于Netty的Origin管理器,虽说是带Basic,但是也可以做一个复杂的Origin管理者。
一般在Zuul中,Origin代表边界之后的服务。
BasicNettyOriginManager使用ConcurrentHashMap来管理Origin,Key是Origin的名称,value是BasicNettyOrigin对象,BasicNettyOrigin是Origin的具体实现。
BasicNettyOrigin中会有一些请求执行、请求记录、响应记录的方法。再次暂时先不详细展开讲述。
2.3 ZuulFilterModule(过滤器)
ZuulFilterModule注入了一些我们可能需要的组件。
@Override
protected void configure() {
// Groovy动态脚本编译
bind(DynamicCodeCompiler.class).to(GroovyCompiler.class);
// 基于ZuulFilter的注入容器,获取出来的对象均是ZuulFilter类型
bind(FilterFactory.class).to(GuiceFilterFactory.class);
// ZuulFilter的一个使用次数的通知器
bind(FilterUsageNotifier.class).to(BasicFilterUsageNotifier.class);
}
- Groovy动态编译脚本,用于动态加载ZuulFilter。
- 注入一个ZuulFilter类型的IOC容器。
- 一个用于通知ZuulFilter使用次数的通知器。
同时它还会去加载一批已存储的过滤器,provideFilterFileManagerConfig():
@Provides
FilterFileManagerConfig provideFilterFileManagerConfig() {
// 获取filter的综合信息
final AbstractConfiguration config = ConfigurationManager.getConfigInstance();
String[] filterLocations = findFilterLocations(config);
String[] filterClassNames = findClassNames(config);
// 实例化已知的过滤器
FilterFileManagerConfig filterConfig = new FilterFileManagerConfig(filterLocations, filterClassNames, 5);
return filterConfig;
}
Zuul通过FilterFileManagerConfig来加载filters到Zuul中,FilterFileManagerConfig是FilterFileManger的内部类。FilterFileManagerConfig是FilterFileManager的管理类,会提供一些filter的位置、类名称的集合,以及轮询时间和关于文件名的过滤器。
FilterFileManager是用于管理轮询ZuulFilter变化和添加新Groovy过滤器的管理类,内部轮询和目录轮询一般都是轮询已指定的实现的类,然后还会一个轮询者来检查一些变化信息和新添加的东西。
2.4 ServerStatusManager(Server的状态管理者)
Server的状态管理者,通过status()方法来获取实例状态,这里会有一个trick。
它分为Server的本地状态和服务发现管理的远端状态,获取Server状态时,会同时对本地状态和远端状态进行校验:
public InstanceInfo.InstanceStatus status() {
InstanceInfo.InstanceStatus local = localStatus();
InstanceInfo.InstanceStatus remote = remoteStatus();
// 如果本地状态为ready,且远端状态并非未知,那么返回远端状态
// 否则返回本地状态
if (local == UP && remote != UNKNOWN) {
return remote;
} else {
return local;
}
}
- 如果同时存在本地状态和远端状态,并且本地状态为ready,则返回远端状态。
- 如果无法获取远端状态,直接返回本地状态。
2.5 ZuulSessionContextDecorator(Zuul的会话管理上下文包装)
ZuulSessionContextDecorator是对SessionContext的进行一些其他属性注入操作,核心方法是decorate()方法:
@Override
public SessionContext decorate(SessionContext ctx) {
ChannelHandlerContext nettyCtx = (ChannelHandlerContext) ctx.get(CommonContextKeys.NETTY_SERVER_CHANNEL_HANDLER_CONTEXT);
if (nettyCtx == null) {
return null;
}
Channel channel = nettyCtx.channel();
// 添加已经注入的OriginManager
ctx.put(CommonContextKeys.ORIGIN_MANAGER, originManager);
// 声明一个用于存储Request尝试信息的容器,默认是一个ArrayList
ctx.put(CommonContextKeys.REQUEST_ATTEMPTS, new RequestAttempts());
// 从当前Channel中获取记录RequestBody大小和Response大小的记录
ctx.set(CommonContextKeys.REQ_BODY_SIZE_PROVIDER, HttpBodySizeRecordingChannelHandler.getCurrentRequestBodySize(channel));
ctx.set(CommonContextKeys.RESP_BODY_SIZE_PROVIDER, HttpBodySizeRecordingChannelHandler.getCurrentResponseBodySize(channel));
// 获取请求通行证,它是基于纳秒时间记录的关于请求瞬时状态
CurrentPassport passport = CurrentPassport.fromChannel(channel);
ctx.set(CommonContextKeys.PASSPORT, passport);
// 生成对应的UUID
ctx.setUUID(UUID_FACTORY.generateRandomUuid().toString());
return ctx;
}
- 注入之前声明的BasicNettyOriginManager实例。
- 声明一个Request尝试信息的容器。它用于记录每次请求尝试的详情信息,它将放在Response的内部头中使用,对于跟踪和调试有这个很大的帮助。
- 根据Channel设置Request、Response的Body记录大小。
- 获取请求通行证,该对象存储了一个基于纳秒事件记录的请求瞬时状态,什么时间点进行了什么操作,它会记录一个起始的纳秒时间,而后的瞬时状态,都是基于它的增量纳秒。
- 生成唯一的UUID用作标识。
2.6 DefaultRegistry
DefaultRegistry在我看来就是一个简单的计数器统一管理者。
2.7 BasicRequestCompleteHandler
BasicRequestCompleteHandler是一个关于Request请求完成的计数器,实现了RequestCompleteHandler接口,核心方法是handle():
@Override
public void handle(HttpRequestInfo inboundRequest, HttpResponseMessage response) {
SessionContext context = inboundRequest.getContext();
// 请求计数
if (requestMetricsPublisher != null) {
requestMetricsPublisher.collectAndPublish(context);
}
}
而collectAndPublish()中则实现了具体的计数实现:
@Override
public void collectAndPublish(SessionContext context) {
// 请求的时间点
long totalRequestTime = context.getTimings().getRequest().getDuration();
long requestProxyTime = context.getTimings().getRequestProxy().getDuration();
// Origin的处理时间
int originReportedDuration = context.getOriginReportedDuration();
// Zuul 2处理Request的时间
long totalInternalTime = totalRequestTime - requestProxyTime;
// 计算一下路由到Origin的时间
// 如果值是-1,证明我们没有这个计数
long totalTimeAddedToOrigin = -1;
if (originReportedDuration > -1) {
totalTimeAddedToOrigin = totalRequestTime - originReportedDuration;
}
// 发布记录的时间
final String METRIC_TIMINGS_REQ_PREFIX = "zuul.timings.request.";
recordRequestTiming(METRIC_TIMINGS_REQ_PREFIX + "total", totalRequestTime);
recordRequestTiming(METRIC_TIMINGS_REQ_PREFIX + "proxy", requestProxyTime);
recordRequestTiming(METRIC_TIMINGS_REQ_PREFIX + "internal", totalInternalTime);
recordRequestTiming(METRIC_TIMINGS_REQ_PREFIX + "added", totalTimeAddedToOrigin);
}
- 计算当前请求的时间点。
- 计算origin后端节点处理请求的时间间隔。
- 计算Zuul 2处理整个请求的时间消耗。
- 计算请求路由到指定origin的时间。
- 最后记录下所有计算出的时间。
recordRequestTiming()则是以纳秒维度将数据记录到BasicTimer中,BasicTimer是Netflix在其开源项目中广泛应用的计数器。
2.8 AbstractDiscoveryClientOptionalArgs
AbstractDiscoveryClientOptionalArgs用于存储服务发现客户端的一些参数,参数组装来自EurekaInstanceConfig,在Zuul 2中,如果没有特殊指定EurekaInstanceConfig,默认走CloudEurekaInstanceConfig配置。
2.9 BasicRequestMetricsPublisher
BasicRequestMetricsPublisher依然是Request的计数器,上述BasicRequestCompleteHandler调用的collectAndPublish()方法即使BasicRequestMetricsPublisher的方法,在此不再赘述。
2.10 AccessLogPublisher
AccessLogPublisher的目的是是打一些关于Request的日志。
ZuulSampleModule的源码解析完毕,主要是为大家展示下Zuul启动时,依赖的组件。
3 获取已构建的服务
当我们构建完控制反转容器后,我们会从容器中获取BaseServerStartup实例,就是我们在讲容器注入时,第一个注入的部分。
获取BootServerStartup后,我们会构建Server服务,调用BaseServerStartup#server()方法,而Server的信息已经在实例初始化时构建完成(详情见SampleServerStartup讲解)。
4 启动服务
启动服务调用的是Netflix根据Netty重写的一个Server,我们看下start()方法:
public void start(boolean sync) {
// 设置服务组的接收线程数量和工作线程数量,以及计数器
serverGroup = new ServerGroup("Salamander", eventLoopConfig.acceptorCount(), eventLoopConfig.eventLoopCount(), eventLoopGroupMetrics);
// 创建模型线程池
serverGroup.initializeTransport();
try {
List<ChannelFuture> allBindFutures = new ArrayList<>();
for (Map.Entry<Integer, ChannelInitializer> entry : portsToChannelInitializers.entrySet()) {
// 根据端口和ChannelInitializer,添加对应的ChannelFuture任务
allBindFutures.add(setupServerBootstrap(entry.getKey(), entry.getValue()));
}
for (ChannelFuture f : allBindFutures) {
// 等待服务关闭
ChannelFuture cf = f.channel().closeFuture();
if (sync) {
cf.sync();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
- 根据EventLoopConfig和自定义计数器配置serverGroup。
- 创建对应的Boss线程池和Worer线程池。
- 然后将之前构建好的端口—>ChannelInitializer服务,一一赋给ChannelFuture进行处理。
- 服务启动,等待服务关闭命令。
接下来我们介绍下上面涉及到的核心概念
4.1 ServerGroup
ServerGroup是Server下面的子集,用于管理线程池和停止服务。
它使用initializeTransport()来构建线程池:
private void initializeTransport() {
// EventExecutor的选择器,用于轮询下一个EventExecutor
EventExecutorChooserFactory chooserFactory;
if (USE_LEASTCONNS_FOR_EVENTLOOPS.get()) {
chooserFactory = new LeastConnsEventLoopChooserFactory(eventLoopGroupMetrics);
} else {
chooserFactory = DefaultEventExecutorChooserFactory.INSTANCE;
}
ThreadFactory workerThreadFactory = new CategorizedThreadFactory(name + "-ClientToZuulWorker");
Executor workerExecutor = new ThreadPerTaskExecutor(workerThreadFactory);
// 如果使用epoll模式
if (USE_EPOLL.get()) {
clientToProxyBossPool = new EpollEventLoopGroup(
acceptorThreads,
new CategorizedThreadFactory(name + "-ClientToZuulAcceptor"));
// 客户端代理工作线程池使用的是epoll模型线程池
clientToProxyWorkerPool = new EpollEventLoopGroup(
workerThreads,
workerExecutor,
chooserFactory,
DefaultSelectStrategyFactory.INSTANCE
);
} else {
clientToProxyBossPool = new NioEventLoopGroup(
acceptorThreads,
new CategorizedThreadFactory(name + "-ClientToZuulAcceptor"));
// 否则,会使用NIO模式的线程池
clientToProxyWorkerPool = new NioEventLoopGroup(
workerThreads,
workerExecutor,
chooserFactory,
SelectorProvider.provider(),
DefaultSelectStrategyFactory.INSTANCE
);
// ③ 设置执行IO操作的比值,默认是50
((NioEventLoopGroup) clientToProxyWorkerPool).setIoRatio(90);
}
// ④ 创建完客户端的管理线程池和工作线程池的的后置处理
postEventLoopCreationHook(clientToProxyBossPool, clientToProxyWorkerPool);
}
- EventExecutorChooserFactory是EventExecutorChooser的实例工厂,而EventExecutorChooser用于选择下一个EventExecutor,而EventExecutor是EventExecutorGroup的子集。
- Netty提供了epoll和select两种模式,默认select模式。
- 设置的是执行I/O操作占CPU调度的百分比,这里会发现Zuul用的比较狠。
- 一个后置处理,默认没有实现,用于添加一些我们自定义的实现。
它的stop()方法用来停止服务组:
synchronized private void stop() {
LOG.warn("Shutting down");
if (stopped) {
LOG.warn("Already stopped");
return;
}
// 设置当前Server状态为DOWN
serverStatusManager.localStatus(InstanceInfo.InstanceStatus.DOWN);
// 优雅的停止客户端连接
// 核心思想方法是关闭所有的Channel,将Channel的close()事件放入到ChannelFuture中,然后等待所有ChannelFuture完成
clientConnectionsShutdown.gracefullyShutdownClientChannels();
LOG.warn("Shutting down event loops");
List<EventLoopGroup> allEventLoopGroups = new ArrayList<>();
allEventLoopGroups.add(clientToProxyBossPool);
allEventLoopGroups.add(clientToProxyWorkerPool);
for (EventLoopGroup group : allEventLoopGroups) {
// 优雅的停止EventLoopGroup
group.shutdownGracefully();
}
for (EventLoopGroup group : allEventLoopGroups) {
try {
// 等待20秒确认EventLoopGroup停止
group.awaitTermination(20, TimeUnit.SECONDS);
} catch (InterruptedException ie) {
LOG.warn("Interrupted while shutting down event loop");
}
}
stopped = true;
LOG.warn("Done shutting down");
}
- 优雅的停止线程池,就是将Channel关闭放入到异步事件中,然后一直阻塞直到关闭Channel完成。
- 等待一定时间,确认全部关闭,并将ServerGroup的状态置为已关闭。
4.2 setupServerBootstrap
setupServerBootstrap()是将每个Channel分别组装成ChannelFuture,然后返回给Server:
private ChannelFuture setupServerBootstrap(int port, ChannelInitializer channelInitializer)
throws InterruptedException {
// 使用Netty的ServerBootstrap构建主线程池和工作线程池
ServerBootstrap serverBootstrap = new ServerBootstrap().group(
serverGroup.clientToProxyBossPool,
serverGroup.clientToProxyWorkerPool);¡
// 处理socket参数
Map<ChannelOption, Object> channelOptions = new HashMap<>();
channelOptions.put(ChannelOption.SO_BACKLOG, 128);
//channelOptions.put(ChannelOption.SO_TIMEOUT, SERVER_SOCKET_TIMEOUT.get());
channelOptions.put(ChannelOption.SO_LINGER, -1);
channelOptions.put(ChannelOption.TCP_NODELAY, true);
channelOptions.put(ChannelOption.SO_KEEPALIVE, true);
// 判断是使用epoll还是NIO
if (USE_EPOLL.get()) {
LOG.warn("Proxy listening with TCP transport using EPOLL");
serverBootstrap = serverBootstrap.channel(EpollServerSocketChannel.class);
channelOptions.put(EpollChannelOption.TCP_DEFER_ACCEPT, Integer.valueOf(-1));
} else {
LOG.warn("Proxy listening with TCP transport using NIO");
serverBootstrap = serverBootstrap.channel(NioServerSocketChannel.class);
}
// 向serverBootstrap中注入socket参数
for (Map.Entry<ChannelOption, Object> optionEntry : channelOptions.entrySet()) {
serverBootstrap = serverBootstrap.option(optionEntry.getKey(), optionEntry.getValue());
}
// 注入通道初始化器
serverBootstrap.childHandler(channelInitializer);
// 校验Netty的ChannelHandler和工作线程是否存在
serverBootstrap.validate();
LOG.info("Binding to port: " + port);
// 绑定端口前先更改Server状态
serverStatusManager.localStatus(InstanceInfo.InstanceStatus.UP);
// 阻塞直到绑定端口成功
return serverBootstrap.bind(port).sync();
}
- 每个Channel都由统一的一个ServerGroup统一管理,所以构建的线程池也是同一个线程池。
- 注入当前Channel的socket参数和类型。
- 注入Channel初始化器。
- 做一些启动前的必要检查,主要是检查ChannelHandler和工作线程是否存在。
- 先将Server置为开启状态。
- 阻塞直到绑定端口成功。
经过Server#start()方法的循环处理,所有端口->Channel初始化器均已启动完毕。
5 源码环境搭建
我在研究Zuul 2的源码过程中,发现文档并非全部,进行了一些配置之后才搭建起源码调试环境,特此分享给大家。
运行zuul-sample项目下,SunshineBootstrap类即可运行Zuul 2。
Eureka服务注册位于zuul2-eureka项目下,运行EurekaBootstrap即可运行一个Eureka服务注册。
6 总结
Zuul 2由Zuul版本的阻塞式Servlet网关转换为现在的同步非阻塞,基于Netty构造的网关,核心思想是使用InboundChannelHandler和OutboundChannelHandler对请求进行处理及分发。
在Zuul 2的启动流程中,核心组件交由Guice的进行控制反转。
核心组件包括:启动器,Origin管理器,请求计数器,服务发现管理,与后端业务节点的长连接会话管理等组件。