赞
踩
最近做项目,需要一个代理逻辑,实际上这种代理NGINX最好,但是有些额外功能的开发,NGINX就需要额外能力支持,比如lua脚本,常见的做法有kong,apisix等,据说apisix的性能较强,界面较好,不过如果需要Java开发(方便二次开发),那么zuul也是可以的,实际上gateway相对主流,但是实现逻辑相对复杂,而且跟zuul(配置连接池和线程)性能差不多,只不过zuul不再被Spring Cloud支持,需要自己维护,但是servlet貌似也没啥维护的了。
zuul的设计之初是为了微服务网关,但是如果做TCP、websocket等转发就需要自己实现,实际上开源的goproxy就是一个性能较强的代理,go-gateway等,但是开发语言最终选择zuul,因为定制性极强。
zuul默认需要注册注册中心,需要把这一部分剥离出来,做成插件,需要的时候才会注册,拿到zuul starter源码,发现
默认加载这2个配置类,因为zuul被Spring Cloud废弃,所以没有Spring Boot新版本的引入配置类的方式import模式
Cloud模式,支持Cloud的负载均衡,熔断等
域名或者Host模式,可以使用域名,或者APP端负载均衡,限流等
zuul的注入依赖EnableZuulProxy还是EnableZuulServer,EnableZuulProxy的能力更强,原因如下
- @Target(ElementType.TYPE)
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- @Import(ZuulServerMarkerConfiguration.class)
- public @interface EnableZuulServer {
-
- }
-
- @EnableCircuitBreaker
- @Target(ElementType.TYPE)
- @Retention(RetentionPolicy.RUNTIME)
- @Import(ZuulProxyMarkerConfiguration.class)
- public @interface EnableZuulProxy {
-
- }
Import的marker决定的,注意proxy模式有EnableCircuitBreaker注解,这个是过时注解,而且依赖hystrix等熔断器,这个需要去掉,限流熔断自己实现吧,或者依赖Cloud的自定义实现
Server的能力,依赖marker类的bean创建,来源于上面的Enable注解,所以注解开启不同功能,
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
这个之上的注解是会执行的,毕竟需要开启ZuulProperties的注入
Proxy的能力更强, 因为继承(简单粗暴)
逻辑很简单,实际只需要把robbin的filter和相关的支持类剥离即可
剥离后注册中心相关的可以单独加载,以http转https转发为例,流程分析
- @SpringBootApplication
- //@EnableDiscoveryClient
- @EnableZuulProxy
- public class ZuulMain {
- public static void main(String[] args) {
- SpringApplication.run(ZuulMain.class, args);
- }
- }
因为剥离注册中心,就不需要服务发现了,但是只能转发Host、IP或者域名
配置转发博客为例:
- zuul:
- routes:
- rule1:
- path: /demo/**
- url: https://blog.csdn.net/
笔者很早讲了SCI模式,不通过配置文件注入servlet和filter,那么zuul也是这种方式注入的
安装加载顺序一般情况下注入servlet,而不是filter,那么在http请求时
经过类型,然后执行zuulfilter
以http为例,笔者访问http://localhost:8766/demo/hello ,返回了csdn的地址
关键逻辑1:路径匹配
org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter
预处理会设置后面需要的url转发
threadlocal线程安全,在post的filter执行后会被uset
然后再取出使用转发
逻辑简单就是一条链传递,filter各个阶段的转发,中间做逻辑处理,收到Http请求,在通过“httpclient”发送出去。
笔者使用的是Apache的httpclient 4.5,实际上现在最新版本是5.2.x,可能略有不同,研究
CloseableHttpClient
的用法,连接池的用法,连接是怎么关闭的,看看Spring怎么做的
- @PostConstruct
- private void initialize() {
- if (!customHttpClient) {
- this.connectionManager = newConnectionManager();
- this.httpClient = newClient();
- this.connectionManagerTimer.schedule(new TimerTask() {
- @Override
- public void run() {
- if (SimpleHostRoutingFilter.this.connectionManager == null) {
- return;
- }
- SimpleHostRoutingFilter.this.connectionManager
- .closeExpiredConnections();
- }
- }, 30000, 5000);
- }
- }
3步:
1. 创建连接池
2. 创建httpclient(可关闭)
3. 定时关闭过期连接(实际是应该pool自己定时清除)
但是没看到在优雅停机时关闭连接池的代码,只有关闭定时器的代码
- @PreDestroy
- public void stop() {
- this.connectionManagerTimer.cancel();
- }
或者可以在这个里面加入关闭连接池的代码,但是流量能不能做到无损就需要外部支持了,不让外部进,内部流量消耗完
- protected HttpClientConnectionManager newConnectionManager() {
- return connectionManagerFactory.newConnectionManager(
- !this.sslHostnameValidationEnabled,
- this.hostProperties.getMaxTotalConnections(),
- this.hostProperties.getMaxPerRouteConnections(),
- this.hostProperties.getTimeToLive(), this.hostProperties.getTimeUnit(),
- null);
- }
过度代码,封装参数,zuul的调参数就可以调这里参数,注意
!this.sslHostnameValidationEnabled
坑啊,sslenable取反,表示ssl不验证,Spring Cloud commons封装创建流程
- public HttpClientConnectionManager newConnectionManager(boolean disableSslValidation,
- int maxTotalConnections, int maxConnectionsPerRoute, long timeToLive,
- TimeUnit timeUnit, RegistryBuilder registryBuilder) {
- if (registryBuilder == null) {
- //支持HTTP,注册的是map,可以注册多种协议
- registryBuilder = RegistryBuilder.<ConnectionSocketFactory>create()
- .register(HTTP_SCHEME, PlainConnectionSocketFactory.INSTANCE);
- }
- if (disableSslValidation) {//刚刚的标记,不验证ssl
- try {
- final SSLContext sslContext = SSLContext.getInstance("SSL");
- sslContext.init(null,
- new TrustManager[] { new DisabledValidationTrustManager() },
- new SecureRandom());//不验证信任
- registryBuilder.register(HTTPS_SCHEME, new SSLConnectionSocketFactory(
- sslContext, NoopHostnameVerifier.INSTANCE));
- }
- catch (NoSuchAlgorithmException e) {
- LOG.warn("Error creating SSLContext", e);
- }
- catch (KeyManagementException e) {
- LOG.warn("Error creating SSLContext", e);
- }
- }
- else {
- //验证信任,默认是验证的
- registryBuilder.register("https",
- SSLConnectionSocketFactory.getSocketFactory());
- }
- final Registry<ConnectionSocketFactory> registry = registryBuilder.build();
- //连接池,相对的就是basic模式,单链接
- PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(
- registry, null, null, null, timeToLive, timeUnit);
- connectionManager.setMaxTotal(maxTotalConnections);//最大连接数
- connectionManager.setDefaultMaxPerRoute(maxConnectionsPerRoute);//每个路由最大连接
-
- return connectionManager;
- }
先看看怎么验证的
- public static SSLConnectionSocketFactory getSocketFactory() throws SSLInitializationException {
- return new SSLConnectionSocketFactory(SSLContexts.createDefault(), getDefaultHostnameVerifier());
- }
读取火狐的认证的后缀:Public Suffix List - MozillaWiki
验证逻辑,host和x509,如果是自定义证书,比如我们自己做的jdk或者openssl,可以自定义验证,或者不验证
再看看创建PoolingHttpClientConnectionManager的过程,创建了CPool,继承自AbstractConnPool,有创建和回收方法,池子就可以循环
- public PoolingHttpClientConnectionManager(
- final HttpClientConnectionOperator httpClientConnectionOperator,
- final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
- final long timeToLive, final TimeUnit timeUnit) {
- super();
- this.configData = new ConfigData();
- //默认值每个路由最大2个连接,最大20个连接
- this.pool = new CPool(new InternalConnectionFactory(
- this.configData, connFactory), 2, 20, timeToLive, timeUnit);
- this.pool.setValidateAfterInactivity(2000);
- this.connectionOperator = Args.notNull(httpClientConnectionOperator, "HttpClientConnectionOperator");
- this.isShutDown = new AtomicBoolean(false);
- }
居然没用Apache的commons-pools,自己实现了,造轮子
- protected CloseableHttpClient newClient() {
- final RequestConfig requestConfig = RequestConfig.custom()
- .setConnectionRequestTimeout(
- this.hostProperties.getConnectionRequestTimeoutMillis())
- .setSocketTimeout(this.hostProperties.getSocketTimeoutMillis())
- .setConnectTimeout(this.hostProperties.getConnectTimeoutMillis())
- .setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();
- return httpClientFactory.createBuilder().setDefaultRequestConfig(requestConfig)
- .setConnectionManager(this.connectionManager).disableRedirectHandling()
- .build();
- }
核心是通过刚刚创建的连接管理对象创建Client,执行client的时候可以创建连接和回收复用,里面封装的很复杂,考虑
涉及权限和user agent,尤其是user agent,这个在很多地方有限制,比如浏览器
发送请求需要封装method,jdk8自带的urlconnection不能支持patch:[JDK-8207840] HTTPUrlConnection does not accept PATCH method - Java Bug System,所以jdk8只能使用httpclient或者okhttp,httpclient使用serversocket自己实现的
根据实际verb写入method
那么在哪里去池子获取连接的呢,httpclient是自己封装的,装载获取连接超时
异常关闭
那么正常情况下呢,response的body流关闭时
参考API,实际上就是读取流结束,关闭response的输入流
还包括经常用的toString的API
toString
那么zuul呢,注释写的很明白,释放连接
线程栈,zuul是读取结束就直接关闭连接了,实际上是EofSensorWatcher在生效,httpclient的ResponseEntityProxy实现了EofSensorWatcher接口
- "http-nio-8766-exec-4@8603" daemon prio=5 tid=0x1c nid=NA runnable
- java.lang.Thread.State: RUNNABLE
- at org.apache.http.impl.execchain.ResponseEntityProxy.releaseConnection(ResponseEntityProxy.java:76)
- at org.apache.http.impl.execchain.ResponseEntityProxy.eofDetected(ResponseEntityProxy.java:121)
- at org.apache.http.conn.EofSensorInputStream.checkEOF(EofSensorInputStream.java:199)
- at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:136)
- at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:148)
- at org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.writeResponse(SendResponseFilter.java:259)
- at org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.writeResponse(SendResponseFilter.java:162)
- at org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.run(SendResponseFilter.java:112)
- at com.netflix.zuul.ZuulFilter.runFilter(ZuulFilter.java:117)
流在读取结束时也会调起钩子,实际上流关闭也是同理
这个设计真不错,各个环节解决内存泄漏问题,有C++的味道
流关闭时
实际上,对于技术而言,无论使用任何框架,设计思路都是有异曲同工的地方,对于HTTP代理,无论是zuul(servlet)还是gateway(netty),或者NGINX;本质处理逻辑还是IO的区别,HTTPS协议对于所有的逻辑都是一样的,关键在于定制化吧,zuul对于简单应用还是很不错的,方便定制化,也可以使用gateway,相对要复杂一点。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。