Sometimes, our application need to be opened in one instance, we can bind
to a port to make sure it. Sometimes, out application is dedicated to started in multiple instances and each one should have a different port for listening connection, we may better to let our application automatically choose in some ranges which can save much config pain.
Recently, we are trying to add health check endpoint in Syncer
(a Spring Boot
based data sync application), and we need to add the auto rebind
functionality with Spring Boot
.
Implementation
One of Spring Boot
's feature is the embedded server to ease the pain to deploy, but it also encapsulate too much, making it very hard to customize by ourself, because we don’t know how it works.
We searched like Spring Boot rebind if failed
, Spring Boot change server port
, but only found how to customize embedded container. So, we have to do by ourselves. In order to understand how it works, we start from exception:
Exception
We start two instances of our application, and one of instances failed with LifecycleException
:
2018-09-16 09:44:24,762 ERROR [] [syncer@dev@58.213.85.36] --- [main] o.apache.catalina.core.StandardService : Failed to start connector [Connector[HTTP/1.1-9999]]
org.apache.catalina.LifecycleException: Failed to start component [Connector[HTTP/1.1-9999]]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:167)
at org.apache.catalina.core.StandardService.addConnector(StandardService.java:225)
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.addPreviouslyRemovedConnectors(TomcatWebServer.java:255)
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.start(TomcatWebServer.java:197)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.startWebServer(ServletWebServerApplicationContext.java:300)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh(ServletWebServerApplicationContext.java:162)
// some stack trace removed ...
at org.springframework.boot.SpringApplication.run(SpringApplication.java:327)
at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:137)
at com.github.zzt93.syncer.SyncerApplication.main(SyncerApplication.java:54)
Caused by: org.apache.catalina.LifecycleException: Protocol handler start failed
at org.apache.catalina.connector.Connector.startInternal(Connector.java:1021)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
... 12 common frames omitted
Caused by: java.net.BindException: Address already in use
at sun.nio.ch.Net.bind0(Native Method)
at sun.nio.ch.Net.bind(Net.java:433)
at sun.nio.ch.Net.bind(Net.java:425)
at sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:223)
at sun.nio.ch.ServerSocketAdaptor.bind(ServerSocketAdaptor.java:74)
at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:210)
at org.apache.tomcat.util.net.AbstractEndpoint.start(AbstractEndpoint.java:1150)
at org.apache.coyote.AbstractProtocol.start(AbstractProtocol.java:591)
at org.apache.catalina.connector.Connector.startInternal(Connector.java:1018)
... 13 common frames omitted
The first thought is to catch the LifecycleException
in our code and retry with another port, but as we can see from the stack trace, Spring Boot
has already done much things (finishRefresh
), catch LifecycleException
in main
is not a good choice.
Internal
So, a better idea is to catch BindException
. In order to do this , we have a serial of candidate classes to override from the stack trace:
- Connector
- AbstractProtocol
- AbstractEndpoint
- NioEndpoint
We first try to override Endpoint
which seems affecting other part less, but fail to find a suitable way to inject our Endpoint
. Then we try to replace default Connect
with our customized, finally find we can only customize Protocol
.
Code
We first customize the Bean
TomcatServletWebServerFactory
to affect the web server:
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.setProtocol("com.github.zzt93.syncer.health.export.ReconnectProtocol");
factory.setTomcatConnectorCustomizers(Lists.newArrayList((TomcatConnectorCustomizer) connector -> {
connector.setProperty("retry", RETRY);
}));
And our protocol extends the default Http11NioProtocol
with start
method to catch bind exception and retry.
public class ReconnectProtocol extends Http11NioProtocol {
private int retry;
public void setRetry(int retry) {
this.retry = retry;
}
@Override
public void start() throws Exception {
for (int i = 0; i < retry; i++) {
try {
super.start();
} catch (BindException e) {
logger.warn("Fail to bind to {}, retry {}", getPort(), getPort()+1);
setPort(getPort() + 1);
}
}
}
}
Ref
- Official doc: customize embedded containers
- ReconnectProtocol: example
- TomcatServletWebServerFactory: example
Written with StackEdit.
评论
发表评论