您好,登錄后才能下訂單哦!
這篇文章主要介紹“提交gRPC-spring-boot-starter項(xiàng)目出現(xiàn)bug如何修復(fù)”,在日常操作中,相信很多人在提交gRPC-spring-boot-starter項(xiàng)目出現(xiàn)bug如何修復(fù)問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”提交gRPC-spring-boot-starter項(xiàng)目出現(xiàn)bug如何修復(fù)”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
這是一個(gè)spring-boot-starter項(xiàng)目,用來在spring boot框架下,快速便捷的使用grpc技術(shù),開箱即用。它提供如下等功能特性:
在 spring boot 應(yīng)用中,通過@GrpcService自動配置并運(yùn)行一個(gè)嵌入式的 gRPC 服務(wù)。
使用@GrpcClient自動創(chuàng)建和管理您的 gRPC Channels 和 stubs
支持Spring Cloud(向Consul或Eureka或Nacos注冊服務(wù)并獲取 gRPC 服務(wù)端信息)
支持Spring Sleuth作為分布式鏈路跟蹤解決方案(如果brave-instrument-grpc存在)
支持全局和自定義的 gRPC 服務(wù)端/客戶端攔截器
支持Spring-Security
支持metric (基于micrometer/actuator)
也適用于 (non-shaded) grpc-netty
博主新入職公司接手的項(xiàng)目采用grpc做微服務(wù)通訊框架,項(xiàng)目底層框架采用的spring boot,然后grpc的使用是純手工配置的,代碼寫起來比較繁瑣, 而且這種繁瑣的模板化代碼充斥在每個(gè)采用了grpc的微服務(wù)項(xiàng)目里。所以技術(shù)選型后找到了gRPC-spring-boot-starter 這個(gè)開源項(xiàng)目,這個(gè)項(xiàng)目代碼質(zhì)量不錯(cuò),非常規(guī)范,文檔也比較齊全。但是鑒于之前工作經(jīng)驗(yàn)遇到過開源項(xiàng)目的問題(博主選型的原則,如果有合適的輪子,就摸透這個(gè)輪子,然后基于這個(gè)輪子二開,沒有就自己造一個(gè)輪子),而且一般解決周期比較長,所以 最后,我們沒有直接采用他們的發(fā)行包,而是fork了項(xiàng)目后,打算自己維護(hù)。正因?yàn)槿绱?,才為后面迅速解決問題上線成為可能。也驗(yàn)證了二開這個(gè)選擇是正確的。
風(fēng)風(fēng)火火重構(gòu)了所有代碼,全部換成gRPC-spring-boot-starter后就上線了,上線后一切都非常好,但是項(xiàng)目在第二次需求上線投產(chǎn)時(shí)發(fā)生了一些問題。 這個(gè)時(shí)候還不確定是切換grpc實(shí)現(xiàn)導(dǎo)致的問題,現(xiàn)象就是,線上出現(xiàn)了大量的請求異常。上線完成后,異常就消失了。后面每次滾動更新都會出現(xiàn)類似的異常。 這個(gè)時(shí)候就很容易聯(lián)系到是否切換grpc實(shí)現(xiàn)后,grpc未優(yōu)雅下線,導(dǎo)致滾動更新時(shí),大量的進(jìn)行中的請求未正常處理,導(dǎo)致這部分流量異常?因?yàn)槲覀兙€上 流量比較大,幾乎每時(shí)每刻都有大量請求,所以我們要求線上服務(wù)必須支持無縫滾動更新。如果流量比較小,這個(gè)問題可能就不會暴露出來,這也解釋了之前和同事討論的點(diǎn),為什么這么明顯的問題沒有被及早的發(fā)現(xiàn)。不過都目前為止,這一切都只是猜測,真相繼續(xù)往下。
有了上面的猜測,直接找到了gRPC-spring-boot-starter管理維護(hù)GrpcServer生命周期的類GrpcServerLifecycle,這個(gè)類實(shí)現(xiàn)了spring的SmartLifecycle接口,這個(gè)接口是用來注冊SpringContextShutdownHook的鉤子用的,它的實(shí)現(xiàn)如下:
@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."); } } }
也就是說當(dāng)spring容器關(guān)閉時(shí),會觸發(fā)ShutdownHook,進(jìn)而關(guān)閉GrpcServer服務(wù),問題就出現(xiàn)在這里,從stopAndReleaseGrpcServer()方法可知,Grpc進(jìn)行shudown()后,沒有進(jìn)行任何操作,幾乎瞬時(shí)就返回了,這就導(dǎo)致了進(jìn)程在收到kill命令時(shí),Grpc的服務(wù)會被瞬間回收掉,而不會等待執(zhí)行中的處理完成,這個(gè)判斷可以從shutdown()的文檔描述中進(jìn)一步得到確認(rèn),如:
/** * 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();
文檔指出,調(diào)用shutdown()后,不在接收新的請求流量,進(jìn)行中的請求會繼續(xù)處理完成,但是請注意,它不會等待現(xiàn)有的調(diào)用請求完成,必須使用awaitTermination()方法等待請求完成,也就是說,這里處理關(guān)閉的邏輯里,缺少了awaitTermination()等待處理中的請求完成的邏輯。
驗(yàn)證方法:
這個(gè)場景的問題非常容易驗(yàn)證,只需要在server端模擬業(yè)務(wù)阻塞耗時(shí)長一點(diǎn),然后kill掉java進(jìn)程,看程序是否會立刻被kill。正常優(yōu)雅下線關(guān)閉的話,會等待阻塞的時(shí)間后進(jìn)程kill。否則就會出現(xiàn)不管業(yè)務(wù)阻塞多長時(shí)間,進(jìn)程都會立馬kill。
先驗(yàn)證下是否如上面所說,不加awaitTermination()時(shí),進(jìn)程是否立馬就死了。直接使用gRPC-spring-boot-starter里自帶的demo程序,在server端的方法里加上如下模擬業(yè)務(wù)執(zhí)行耗時(shí)的代碼:
@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("阻塞完成,請求結(jié)束"); } catch (InterruptedException e) { e.printStackTrace(); } responseObserver.onNext(reply); responseObserver.onCompleted(); } }
上面代碼模擬的執(zhí)行一分鐘的方法,然后觸發(fā)grpc client調(diào)用。接著找到server端的進(jìn)程號,直接kill掉。發(fā)現(xiàn)進(jìn)程確實(shí)立馬就kill了。繼續(xù)加大阻塞的時(shí)間,從一分鐘加大到六分鐘,重復(fù)測試,還是立馬就kill掉了,沒有任何的等待。
先將上面的代碼修復(fù)下,正確的關(guān)閉邏輯應(yīng)該如下,在Grpc發(fā)出shutdown指令后,阻塞等待所有請求正常結(jié)束,同時(shí),這里阻塞也會夯住主進(jìn)程不會里面掛掉。
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."); } }
同樣,如上述步驟驗(yàn)證,當(dāng)kill掉java進(jìn)程后,此時(shí)java進(jìn)程并沒有立馬就被kill,而是被awaitTermination()阻塞住了線程,直到業(yè)務(wù)方法中模擬的業(yè)務(wù)阻塞結(jié)束后,java進(jìn)程才被kill掉,這正是我們想要達(dá)到的優(yōu)雅下線關(guān)閉的效果。被kill時(shí)的,線程堆棧如下:
即使被kill了,還是能打印如下的日志【阻塞完成,請求結(jié)束】,進(jìn)一步驗(yàn)證了修復(fù)后確實(shí)解決了問題:
到此,關(guān)于“提交gRPC-spring-boot-starter項(xiàng)目出現(xiàn)bug如何修復(fù)”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。