Spring Cloud zuul与CloseableHttpClient连接池,TLS证书认证

前言

最近做项目,需要一个代理逻辑,实际上这种代理NGINX最好,但是有些额外功能的开发,NGINX就需要额外能力支持,比如lua脚本,常见的做法有kong,apisix等,据说apisix的性能较强,界面较好,不过如果需要Java开发(方便二次开发),那么zuul也是可以的,实际上gateway相对主流,但是实现逻辑相对复杂,而且跟zuul(配置连接池和线程)性能差不多,只不过zuul不再被Spring Cloud支持,需要自己维护,但是servlet貌似也没啥维护的了。

zuul

zuul的设计之初是为了微服务网关,但是如果做TCP、websocket等转发就需要自己实现,实际上开源的goproxy就是一个性能较强的代理,go-gateway等,但是开发语言最终选择zuul,因为定制性极强。

zuul改造

zuul默认需要注册注册中心,需要把这一部分剥离出来,做成插件,需要的时候才会注册,拿到zuul starter源码,发现

默认加载这2个配置类,因为zuul被Spring Cloud废弃,所以没有Spring Boot新版本的引入配置类的方式import模式

Cloud模式,支持Cloud的负载均衡,熔断等

域名或者Host模式,可以使用域名,或者APP端负载均衡,限流等

zuul源码分析

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转发为例,流程分析

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,自己实现了,造轮子

httpclient的创建 

	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,相对要复杂一点。

猜你喜欢

转载自blog.csdn.net/fenglllle/article/details/132761316