给gRPC-spring-boot-starter一个pr的说明

简介: 为了更好的说明给gRPC-spring-boot-starter项目提交bug修复的pr的原因,解答作者的问题。以博文的形式记录了整个过程的上下文,目前pr未合并还在沟通处理中,希望此博文可以更清楚描述问题

前言

为了更好的说明给gRPC-spring-boot-starter项目提交bug修复的pr的原因,解答作者的问题。以博文的形式记录了整个过程的上下文,目前pr未合并还在沟通处理中,希望此博文可以更清楚描述问题

pr地址:https://github.com/yidongnan/grpc-spring-boot-starter/pull/454

gRPC-spring-boot-starter是什么?

这是一个spring-boot-starter项目,用来在spring boot框架下,快速便捷的使用grpc技术,开箱即用。它提供如下等功能特性:

  • 在 spring boot 应用中,通过@GrpcService自动配置并运行一个嵌入式的 gRPC 服务。

  • 使用@GrpcClient自动创建和管理您的 gRPC Channels 和 stubs

  • 支持Spring Cloud(向ConsulEurekaNacos注册服务并获取 gRPC 服务端信息)

  • 支持Spring Sleuth作为分布式链路跟踪解决方案(如果brave-instrument-grpc存在)

  • 支持全局和自定义的 gRPC 服务端/客户端拦截器

  • 支持Spring-Security

  • 支持metric (基于micrometer/actuator)

  • 也适用于 (non-shaded) grpc-netty

选型gRPC-spring-boot-starter

博主新入职公司接手的项目采用grpc做微服务通讯框架,项目底层框架采用的spring boot,然后grpc的使用是纯手工配置的,代码写起来比较繁琐, 而且这种繁琐的模板化代码充斥在每个采用了grpc的微服务项目里。所以技术选型后找到了gRPC-spring-boot-starter 这个开源项目,这个项目代码质量不错,非常规范,文档也比较齐全。但是鉴于之前工作经验遇到过开源项目的问题(博主选型的原则,如果有合适的轮子,就摸透这个轮子,然后基于这个轮子二开,没有就自己造一个轮子),而且一般解决周期比较长,所以 最后,我们没有直接采用他们的发行包,而是fork了项目后,打算自己维护。正因为如此,才为后面迅速解决问题上线成为可能。也验证了二开这个选择是正确的。

bug出现,grpc未优雅下线

风风火火重构了所有代码,全部换成gRPC-spring-boot-starter后就上线了,上线后一切都非常好,但是项目在第二次需求上线投产时发生了一些问题。 这个时候还不确定是切换grpc实现导致的问题,现象就是,线上出现了大量的请求异常。上线完成后,异常就消失了。后面每次滚动更新都会出现类似的异常。 这个时候就很容易联系到是否切换grpc实现后,grpc未优雅下线,导致滚动更新时,大量的进行中的请求未正常处理,导致这部分流量异常?因为我们线上 流量比较大,几乎每时每刻都有大量请求,所以我们要求线上服务必须支持无缝滚动更新。如果流量比较小,这个问题可能就不会暴露出来,这也解释了之前和同事讨论的点,为什么这么明显的问题没有被及早的发现。不过都目前为止,这一切都只是猜测,真相继续往下。

定位bug,寻找真实原因

有了上面的猜测,直接找到了gRPC-spring-boot-starter管理维护GrpcServer生命周期的类GrpcServerLifecycle,这个类实现了spring的SmartLifecycle接口,这个接口是用来注册SpringContextShutdownHook的钩子用的,它的实现如下:

@Slf4j
public class GrpcServerLifecycle implements SmartLifecycle {
    private static AtomicInteger serverCounter = new AtomicInteger(-1);

    private volatile Server server;
    private volatile int phase = Integer.MAX_VALUE;
    private final GrpcServerFactory factory;

    public GrpcServerLifecycle(final GrpcServerFactory factory) {
        this.factory = factory;
    }

    @Override
    public void start() {
        try {
            createAndStartGrpcServer();
        } catch (final IOException e) {
            throw new IllegalStateException("Failed to start the grpc server", e);
        }
    }

    @Override
    public void stop() {
        stopAndReleaseGrpcServer();
    }

    @Override
    public void stop(final Runnable callback) {
        stop();
        callback.run();
    }

    @Override
    public boolean isRunning() {
        return this.server != null && !this.server.isShutdown();
    }

    @Override
    public int getPhase() {
        return this.phase;
    }

    @Override
    public boolean isAutoStartup() {
        return true;
    }

    /**
     * Creates and starts the grpc server.
     *
     * @throws IOException If the server is unable to bind the port.
     */
    protected void createAndStartGrpcServer() throws IOException {
        final Server localServer = this.server;
        if (localServer == null) {
            this.server = this.factory.createServer();
            this.server.start();
            log.info("gRPC Server started, listening on address: " + this.factory.getAddress() + ", port: "
                    + this.factory.getPort());

            // Prevent the JVM from shutting down while the server is running
            final Thread awaitThread = new Thread(() -> {
                try {
                    this.server.awaitTermination();
                } catch (final InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }, "grpc-server-container-" + (serverCounter.incrementAndGet()));
            awaitThread.setDaemon(false);
            awaitThread.start();
        }
    }

    /**
     * Initiates an orderly shutdown of the grpc server and releases the references to the server. This call does not
     * wait for the server to be completely shut down.
     */
    protected void stopAndReleaseGrpcServer() {
        final Server localServer = this.server;
        if (localServer != null) {
            localServer.shutdown();
            this.server = null;
            log.info("gRPC server shutdown.");
        }
    }

}

也就是说当spring容器关闭时,会触发ShutdownHook,进而关闭GrpcServer服务,问题就出现在这里,从stopAndReleaseGrpcServer()方法可知,Grpc进行shudown()后,没有进行任何操作,几乎瞬时就返回了,这就导致了进程在收到kill命令时,Grpc的服务会被瞬间回收掉,而不会等待执行中的处理完成,这个判断可以从shutdown()的文档描述中进一步得到确认,如:

  /**
   * Initiates an orderly shutdown in which preexisting calls continue but new calls are rejected.
   * After this call returns, this server has released the listening socket(s) and may be reused by
   * another server.
   *
   * <p>Note that this method will not wait for preexisting calls to finish before returning.
   * {@link #awaitTermination()} or {@link #awaitTermination(long, TimeUnit)} needs to be called to
   * wait for existing calls to finish.
   *
   * @return {@code this} object
   * @since 1.0.0
   */
  public abstract Server shutdown();

文档指出,调用shutdown()后,不在接收新的请求流量,进行中的请求会继续处理完成,但是请注意,它不会等待现有的调用请求完成,必须使用awaitTermination()方法等待请求完成,也就是说,这里处理关闭的逻辑里,缺少了awaitTermination()等待处理中的请求完成的逻辑。

模拟环境,反复验证

验证方法:

这个场景的问题非常容易验证,只需要在server端模拟业务阻塞耗时长一点,然后kill掉java进程,看程序是否会立刻被kill。正常优雅下线关闭的话,会等待阻塞的时间后进程kill。否则就会出现不管业务阻塞多长时间,进程都会立马kill。

验证定位的bug

先验证下是否如上面所说,不加awaitTermination()时,进程是否立马就死了。直接使用gRPC-spring-boot-starter里自带的demo程序,在server端的方法里加上如下模拟业务执行耗时的代码:

@GrpcService public class GrpcServerService extends SimpleGrpc.SimpleImplBase {

    @Override
  public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
        HelloReply reply = HelloReply._newBuilder_().setMessage("Hello ==> " \+ req.getName()).build();
 try {
            System._err_.println("收到请求,阻塞等待");
  TimeUnit._MINUTES_.sleep(1);
  System._err_.println("阻塞完成,请求结束");
  } catch (InterruptedException e) {
            e.printStackTrace();
  }
        responseObserver.onNext(reply);
  responseObserver.onCompleted();
  }
}

上面代码模拟的执行一分钟的方法,然后触发grpc client调用。接着找到server端的进程号,直接kill掉。发现进程确实立马就kill了。继续加大阻塞的时间,从一分钟加大到六分钟,重复测试,还是立马就kill掉了,没有任何的等待。

验证修复后的效果

先将上面的代码修复下,正确的关闭逻辑应该如下,在Grpc发出shutdown指令后,阻塞等待所有请求正常结束,同时,这里阻塞也会夯住主进程不会里面挂掉。

    protected void stopAndReleaseGrpcServer() {
        final Server localServer = this.server;
        if (localServer != null) {
            localServer.shutdown();
            try {
                this.server.awaitTermination();
            } catch (final InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            this.server = null;
            log.info("gRPC server shutdown.");
        }
    }

同样,如上述步骤验证,当kill掉java进程后,此时java进程并没有立马就被kill,而是被awaitTermination()阻塞住了线程,直到业务方法中模拟的业务阻塞结束后,java进程才被kill掉,这正是我们想要达到的优雅下线关闭的效果。被kill时的,线程堆栈如下:

即使被kill了,还是能打印如下的日志【阻塞完成,请求结束】,进一步验证了修复后确实解决了问题:

目录
相关文章
|
3月前
|
Java Spring 容器
SpringBoot自动配置的原理是什么?
Spring Boot自动配置核心在于@EnableAutoConfiguration注解,它通过@Import导入配置选择器,加载META-INF/spring.factories中定义的自动配置类。这些类根据@Conditional系列注解判断是否生效。但Spring Boot 3.0后已弃用spring.factories,改用新格式的.imports文件进行配置。
751 0
|
7月前
|
前端开发 Java 数据库
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 介绍
本课介绍Spring Boot集成Thymeleaf模板引擎。Thymeleaf是一款现代服务器端Java模板引擎,支持Web和独立环境,可实现自然模板开发,便于团队协作。与传统JSP不同,Thymeleaf模板可以直接在浏览器中打开,方便前端人员查看静态原型。通过在HTML标签中添加扩展属性(如`th:text`),Thymeleaf能够在服务运行时动态替换内容,展示数据库中的数据,同时兼容静态页面展示,为开发带来灵活性和便利性。
326 0
|
7月前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于 xml 的整合
本教程介绍了基于XML的MyBatis整合方式。首先在`application.yml`中配置XML路径,如`classpath:mapper/*.xml`,然后创建`UserMapper.xml`文件定义SQL映射,包括`resultMap`和查询语句。通过设置`namespace`关联Mapper接口,实现如`getUserByName`的方法。Controller层调用Service完成测试,访问`/getUserByName/{name}`即可返回用户信息。为简化Mapper扫描,推荐在Spring Boot启动类用`@MapperScan`注解指定包路径避免逐个添加`@Mapper`
319 0
|
7月前
|
Java 测试技术 微服务
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——少量配置信息的情形
本课主要讲解Spring Boot项目中的属性配置方法。在实际开发中,测试与生产环境的配置往往不同,因此不应将配置信息硬编码在代码中,而应使用配置文件管理,如`application.yml`。例如,在微服务架构下,可通过配置文件设置调用其他服务的地址(如订单服务端口8002),并利用`@Value`注解在代码中读取这些配置值。这种方式使项目更灵活,便于后续修改和维护。
101 0
|
7月前
|
SQL Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— application.yml 中对日志的配置
在 Spring Boot 项目中,`application.yml` 文件用于配置日志。通过 `logging.config` 指定日志配置文件(如 `logback.xml`),实现日志详细设置。`logging.level` 可定义包的日志输出级别,例如将 `com.itcodai.course03.dao` 包设为 `trace` 级别,便于开发时查看 SQL 操作。日志级别从高到低为 ERROR、WARN、INFO、DEBUG,生产环境建议调整为较高级别以减少日志量。本课程采用 yml 格式,因其层次清晰,但需注意格式要求。
640 0
|
3月前
|
缓存 JSON 前端开发
第07课:Spring Boot集成Thymeleaf模板引擎
第07课:Spring Boot集成Thymeleaf模板引擎
405 0
第07课:Spring Boot集成Thymeleaf模板引擎
|
7月前
|
缓存 NoSQL Java
基于SpringBoot的Redis开发实战教程
Redis在Spring Boot中的应用非常广泛,其高性能和灵活性使其成为构建高效分布式系统的理想选择。通过深入理解本文的内容,您可以更好地利用Redis的特性,为应用程序提供高效的缓存和消息处理能力。
545 79
|
4月前
|
Prometheus 监控 Cloud Native
Spring Boot 可视化监控
本文介绍了如何通过Spring Actuator、Micrometer、Prometheus和Grafana为Spring Boot应用程序添加监控功能。首先创建了一个Spring Boot应用,并配置了Spring Actuator以暴露健康状态和指标接口。接着,利用Micrometer收集应用性能数据,并通过Prometheus抓取这些数据进行存储。最后,使用Grafana将Prometheus中的数据可视化,展示在精美的仪表板上。整个过程简单易行,为Spring Boot应用提供了基本的监控能力,同时也为后续扩展更详细的监控指标奠定了基础。
690 2
|
7月前
|
Java Spring
SpringBoot自动配置原理
本文深入解析了SpringBoot的核心功能——自动配置,重点探讨了`org.springframework.boot.autoconfigure`及相关注解的工作机制。通过分析`@SpringBootApplication`、`@EnableAutoConfiguration`等注解,揭示了SpringBoot如何基于类路径和条件自动装配Bean
346 7
|
7月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 的使用
本文介绍了 Thymeleaf 在 Spring Boot 项目中的使用方法,包括访问静态页面、处理对象和 List 数据、常用标签操作等内容。通过示例代码展示了如何配置 404 和 500 错误页面,以及如何在模板中渲染对象属性和列表数据。同时总结了常用的 Thymeleaf 标签,如 `th:value`、`th:if`、`th:each` 等,并提供了官方文档链接以供进一步学习。
486 0
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 的使用