Tomcat中部署多GRPC Netty服务,应用重部署端口占用问题
这个问题在非Spring Boot的Spring+Tomcat环境里非常典型——当你重部署应用时,旧的Web应用实例没有被彻底清理,导致gRPC服务器绑定的Netty线程还在占用6565/7575端口,新实例启动时就会抛出java.net.BindException: Address already in use。下面是几个可靠的解决方案:
1. 为gRPC服务器Bean添加销毁回调
你用@PostConstruct启动了gRPC服务器,但没有对应的销毁逻辑来关闭它。Spring提供了@PreDestroy注解(或实现DisposableBean接口),可以在Bean被销毁时主动关闭gRPC服务器,释放端口和线程:
@Component public class GrpcServerBean implements DisposableBean { private Server grpcServer; @PostConstruct public void startGrpcServer() throws IOException { // 初始化并启动gRPC服务器的业务代码 this.grpcServer = ServerBuilder.forPort(6565) .addService(new MasterDataServiceImpl()) .build() .start(); } @Override public void destroy() throws InterruptedException { if (grpcServer != null) { // 先优雅关闭,等待正在处理的请求完成 grpcServer.shutdown(); // 设置30秒超时,确保服务器有足够时间终止 if (!grpcServer.awaitTermination(30, TimeUnit.SECONDS)) { // 超时后强制关闭剩余连接 grpcServer.shutdownNow(); } } } }
Tomcat在卸载旧应用时,会触发destroy()方法,保证gRPC服务器能正常释放资源。
2. 使用Servlet上下文监听器兜底
有时候Bean的销毁顺序可能受Spring容器影响,改用ServletContextListener可以更可靠地监听Web应用的生命周期,确保在应用销毁时关闭gRPC服务器:
@WebListener public class GrpcServerContextListener implements ServletContextListener { private Server grpcServer; @Override public void contextInitialized(ServletContextEvent sce) { try { this.grpcServer = ServerBuilder.forPort(6565) .addService(new MasterDataServiceImpl()) .build() .start(); } catch (IOException e) { throw new RuntimeException("Failed to start gRPC server", e); } } @Override public void contextDestroyed(ServletContextEvent sce) { if (grpcServer != null) { grpcServer.shutdown(); try { if (!grpcServer.awaitTermination(30, TimeUnit.SECONDS)) { grpcServer.shutdownNow(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } }
记得在web.xml里注册这个监听器,或者确保Spring配置能扫描到@WebListener注解。
3. 配置Tomcat减少类加载器泄漏
类加载器泄漏是导致旧线程残留的常见原因,你可以修改Tomcat的context.xml,添加以下参数优化类加载器回收:
<Context antiResourceLocking="true" antiJARLocking="true"> <!-- 其他原有配置 --> </Context>
antiResourceLocking会让Tomcat在部署时复制JAR文件,避免文件锁定;antiJARLocking则防止JAR被占用,帮助类加载器正确回收旧应用的资源。
4. 开发环境临时方案(不建议生产使用)
如果只是开发阶段想快速绕过问题,可以给gRPC服务器设置端口重用选项,让新实例能绑定已被旧进程占用的端口:
this.grpcServer = ServerBuilder.forPort(6565) .addService(new MasterDataServiceImpl()) .withChildOption(ChannelOption.SO_REUSEADDR, true) .build() .start();
这个选项允许操作系统快速重用TIME_WAIT状态的端口,但生产环境还是要依赖前面的正确关闭逻辑,避免资源泄漏隐患。
内容的提问来源于stack exchange,提问作者Kasun Kariyawasam

