在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

記錄一次RPC服務有損上線的分析過程

京東云 ? 來源:jf_75140285 ? 作者:jf_75140285 ? 2024-07-30 09:58 ? 次閱讀

1. 問題背景

某應用在啟動完提供JSF服務后,短時間內出現了大量的空指針異常。

分析日志,發現是服務依賴的藏經閣配置數據未加載完成導致。即所謂的有損上線或者是直接發布應用啟動時,service還沒加載完,就開始對外提供服務,導致失敗調用

關鍵代碼如下

數據的初始化加載是通過實現CommandLineRunner接口完成的

@Component
public class LoadSystemArgsListener implements CommandLineRunner {

    @Resource
    private CacheLoader cjgConfigCacheLoader;

    @Override
    public void run(String... args) {
        // 加載藏經閣配置
        cjgConfigCacheLoader.refresh();

    }
}


cjgConfigCacheLoader.refresh()方法內部會將數據加載到內存中

/** 藏經閣配置數據 key:租戶 value:配置數據 */
public static Map cjgRuleConfigMap = new HashMap();

如果此時還未加載完數據,調用cjgRuleConfigMap.get("301").getXX(),則會報空指針異常

總結根因:JSF Provider發布早于服務依賴的初始化數據加載,導致失敗調用

2. 問題解決

在解決此問題前,我們需要先回憶并熟悉下Spring Boot的啟動過程、JSF服務的發布過程

1)Spring Boot的啟動過程(版本2.0.7.RELEASE)

run方法,主要關注refreshContext(context)刷新上下文

public ConfigurableApplicationContext run(String... args) {
    // 創建 StopWatch 實例:用于計算啟動時間
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection exceptionReporters = new ArrayList();
    configureHeadlessProperty();

    // 獲取SpringApplicationRunListeners:這些監聽器會在啟動過程的各個階段發送對應的事件
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);

        // 創建并配置Environment:包括準備好對應的`Environment`,以及將`application.properties`或`application.yml`中的配置項加載到`Environment`中
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        configureIgnoreBeanInfo(environment);

        // 打印Banner:如果 spring.main.banner-mode 不為 off,則打印 banner
        Banner printedBanner = printBanner(environment);

        // 創建應用上下文:根據用戶的配置和classpath下的配置,創建合適的`ApplicationContext`
        context = createApplicationContext();
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);

        // 準備上下文:主要是將`Environment`、`ApplicationArguments`等關鍵屬性設置到`ApplicationContext`中,以及加載`ApplicationListener`、`ApplicationRunner`、`CommandLineRunner`等。
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);

        // 刷新上下文:這是Spring IoC容器啟動的關鍵,包括Bean的創建、依賴注入、初始化,發布事件等
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        // 打印啟動信息:如果 spring.main.log-startup-info 為 true,則打印啟動信息
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
        }
        // 發布 ApplicationStartedEvent:通知所有的 SpringApplicationRunListeners 應用已經啟動
        listeners.started(context);
        
        // 調用 Runner:調用所有的ApplicationRunner和CommandLineRunner
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        // 運行中:通知所有的 SpringApplicationRunListeners 應用正在運行
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

refreshContext(context)內部調用refresh()方法,此方法主要關注
finishBeanFactoryInitialization(beanFactory) 實例化Bean 早于 finishRefresh() 發生

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // 準備刷新的上下文環境:設置啟動日期,激活上下文,清除原有的屬性源
        prepareRefresh();

        // 告訴子類啟動 'refreshBeanFactory()' 方法,創建一個新的bean工廠。
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // 為 BeanFactory 設置上下文特定的后處理器:主要用于支持@Autowired和@Value注解
        prepareBeanFactory(beanFactory);

        try {
            // 為 BeanFactory 的處理提供在子類中的后處理器。
            postProcessBeanFactory(beanFactory);

            // 調用所有注冊的 BeanFactoryPostProcessor Bean 的處理方法。
            invokeBeanFactoryPostProcessors(beanFactory);

            // 注冊 BeanPostProcessor 的處理器,攔截 Bean 創建。
            registerBeanPostProcessors(beanFactory);

            // 為此上下文初始化消息源。
            initMessageSource();

            // 為此上下文初始化事件多播器。
            initApplicationEventMulticaster();

            // 在特定的上下文子類中刷新之前的進一步初始化。
            onRefresh();

            // 檢查監聽器 Bean 并注冊它們:注冊所有的ApplicationListenerbeans
            registerListeners();

            // 實例化所有剩余的(非延遲初始化)單例。
            finishBeanFactoryInitialization(beanFactory);

            // 完成刷新:發布ContextRefreshedEvent,啟動所有Lifecyclebeans,初始化所有剩余的單例(lazy-init 單例和非延遲初始化的工廠 beans)。
            finishRefresh();
        }
        ...
    }


實例化Bean中,需熟悉Bean的生命周期(重要)

wKgZomanddCAEkqGAAMW50WLXmA889.jpg

??

2)JSF Provider的發布過程(版本1.7.5-HOTFIX-T6)


com.jd.jsf.gd.config.spring.ProviderBean調用方法com.jd.jsf.gd.config.ProviderConfig#export進行發布

JSF源碼地址:
http://xingyun.jd.com/codingRoot/jsf/jsf-sdk?

public class ProviderBean extends ProviderConfig implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware {
    
    // 此處代碼省略...

    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ContextRefreshedEvent && this.isDelay() && !this.exported && !CommonUtils.isUnitTestMode()) {
            LOGGER.info("JSF export provider with beanName {} after spring context refreshed.", this.beanName);
            if (this.delay < -1) {
                Thread thread = new Thread(new Runnable() {
                    public void run() {
                        try {
                            Thread.sleep((long)(-ProviderBean.this.delay));
                        } catch (Throwable var2) {
                        }

                        ProviderBean.this.export();
                    }
                });
                thread.setDaemon(true);
                thread.setName("DelayExportThread");
                thread.start();
            } else {
                this.export();
            }
        }

    }

    private boolean isDelay() {
        return this.supportedApplicationListener && this.delay < 0;
    }

    public void afterPropertiesSet() throws Exception {
        // 此處代碼省略...

        if (!this.isDelay() && !CommonUtils.isUnitTestMode()) {
            LOGGER.info("JSF export provider with beanName {} after properties set.", this.beanName);
            this.export();
        }

    }
}

public synchronized void export() throws InitErrorException {
    if (this.delay > 0) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep((long)ProviderConfig.this.delay);
                } catch (Throwable var2) {
                }

                ProviderConfig.this.doExport();
            }
        });
        thread.setDaemon(true);
        thread.setName("DelayExportThread");
        thread.start();
    } else {
        this.doExport();
    }

}

可以看出Provider發布有兩個地方

Ⅰ、Bean的初始化過程(delay>=0)

實現InitializingBean接口,重寫afterPropertiesSet方法。這里會判斷是否延遲發布,如果大于等于0,則會此處進行發布。具體在export方法中,當delay>0,則會延遲發布,如配置5000,表示延遲5秒發布;當delay=0,則立即發布。

Ⅱ、監聽ContextRefreshedEvent事件觸發(delay<0)

實現ApplicationListener接口,重寫onApplicationEvent方法。屬于事件ContextRefreshedEvent,當delay<-1,則會延遲發布,如配置-5000,表示延遲5秒發布;反之,則立即發布。

3)解決方案

場景1:XML方式自動發布Provider(常用)

由上面的介紹,了解到執行順序1.Bean初始化 > 2.ContextRefreshedEvent事件觸發 > 3.調用ApplicationRunner或CommandLineRunner;

上面已經知道Provider發布處于1、2過程,需避免使用方式3進行數據的初始化。

前提建議:delay默認配置為-1,可以不配置,或者配置負數。則JSF Provider發布則處于過程2,即監聽ContextRefreshedEvent事件觸發

方式1:Bean的初始化過程中

解決方法:使用@PostConstruct注解、實現InitializingBean接口、配置init-method方法均可

@Component
public class DataLoader {

    @PostConstruct
    @Scheduled(cron = "${cron.config}")
    public void loadData() {
        // 數據加載
        System.out.println("數據加載工作");
    }

}

注意:該Bean如果依賴了其他Bean,需確保依賴Bean已實例化,否則會報空指針異常。

方式2:ContextRefreshedEvent事件觸發

ContextRefreshedEvent事件是如何發布的

調用過程
AbstractApplicationContext#finishRefresh -> AbstractApplicationContext#publishEvent-> SimpleApplicationEventMulticaster#multicastEvent

public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
   ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
   for (final ApplicationListener listener : getApplicationListeners(event, type)) {
      Executor executor = getTaskExecutor();
      if (executor != null) {
         executor.execute(() -> invokeListener(listener, event));
      }
      else {
         invokeListener(listener, event);
      }
   }
}


SimpleApplicationEventMulticaster的multicastEvent方法中調用invokeListener()進行事件發布getTaskExecutor()默認值是null(除自定義設置Executor對象),所有ApplicationListener實現類串行執行onApplicationEvent方法。

getApplicationListeners(event, type)獲取所有的實現類,繼續向下看內部會調用
AnnotationAwareOrderComparator.sort(allListeners)對所有ApplicationListener進行排序,allListeners 是待排序的對象列表。該方法將根據對象上的排序注解或接口來確定排序順序,并返回一個按照指定順序排序的對象列表。具體來說,排序的規則如下:

1.首先,根據對象上的 @Order 注解的值進行排序。@Order 注解的值越小,排序優先級越高

2.如果對象上沒有 @Order 注解,或者多個對象的 @Order 注解值相同,則根據對象是否實現了 Ordered 接口進行排序。實現了 Ordered 接口的對象,可以通過 getOrder() 方法返回一個排序值。

3.如果對象既沒有 @Order 注解,也沒有實現 Ordered 接口,則使用默認的排序值 LOWEST_PRECEDENCE(Integer.MAX_VALUE)。特別的:如果BeanA和BeanB排序值都是默認值,則保持原順序,即Bean的加載順序

總結:默認情況所有ApplicationListener實現類串行執行onApplicationEvent方法,而順序取決于
AnnotationAwareOrderComparator.sort(allListeners),@Order 注解的值越小,排序優先級越高

解決方法:使用@Order注解保證執行順序早于ProviderBean

@Component
@Order(1)
public class DataLoader implements ApplicationListener {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 數據準備
        System.out.println("初始化工作");
        
    }
}

此外帶有@SpringBootApplication的啟動類中實現也是可以的(在Spring Boot中默認使用基于注解的方式進行配置和管理Bean,所以注解定義的Bean會在XML定義的Bean之前被加載)

@SpringBootApplication
public class DemoApplication implements ApplicationListener {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        System.out.println("初始化工作");
    }
}

場景2:API方式發布Provider(較少使用)

應用啟動完成后,先做初始化動作,完成后再手動發布Provider。這種就可以通過實現接口ApplicationRunner或接口CommandLineRunner去執行初始化。

@Component
public class DataLoader implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 數據準備
        System.out.println("初始化工作");

        // 發布provider
        // 參考:https://cf.jd.com/pages/viewpage.action?pageId=296129902
    }
}

場景3:XML方式手動發布(不常用)

provider的dynamic屬性設置為false

標簽

屬性

類型

是否必填

默認值

描述

provider

dynamic

boolean

true

是否動態注冊Provider,默認為true,配置為false代表不主動發布,需要到管理端進行上線操作

3. 總結

RPC服務(如JSF、Dubbo)進行優雅上線,常用的兩種方式:1、延遲發布 2、手動發動

如果你的服務需要一些初始化操作后才能對外提供服務,如初始化緩存(不限與藏經閣、ducc、mysql、甚至調用其他jsf服務)、redis連接池等相關資源就位,可以參考本文中介紹的幾種方式。

此文是筆者通過讀取源碼+本地驗證得出的結論,如有錯誤遺漏或者更好的方案還煩請各位指出共同進步!

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 接口
    +關注

    關注

    33

    文章

    8596

    瀏覽量

    151145
  • RPC
    RPC
    +關注

    關注

    0

    文章

    111

    瀏覽量

    11534
收藏 人收藏

    評論

    相關推薦

    一次電源與二電源有什么不同

    在電力系統和電子設備的供電領域中,一次電源與二電源是兩個至關重要的概念。它們各自承擔著不同的功能和角色,共同確保電力供應的穩定性和可靠性。本文將對一次電源與二電源的定義、區別以及它
    的頭像 發表于 10-10 14:10 ?1887次閱讀

    一次電池分類以及應用場景詳解

    01 一次電池簡介 一次電池即原電池(primarycell、primarybattery)(俗稱干電池),是放電后不能再充電使其復原的電池,通電電池有正極、負極電解以及容器和隔膜等組成。 一次電池
    的頭像 發表于 09-30 17:52 ?682次閱讀
    <b class='flag-5'>一次</b>電池分類以及應用場景詳解

    Dubbo源碼淺析()—RPC框架與Dubbo

    、什么是RPC 1.1 RPC概念 RPC,Remote Procedure Call 即遠程過程調用,與之相對的是本地
    的頭像 發表于 08-16 15:18 ?733次閱讀
    Dubbo源碼淺析(<b class='flag-5'>一</b>)—<b class='flag-5'>RPC</b>框架與Dubbo

    如何手搓個自定義的RPC 遠程過程調用框架

    1、RPC(遠程過程調用概述) 遠程過程調用(RPC, Remote Procedure Call)是種通過網絡從遠程計算機程序上請求
    的頭像 發表于 07-22 12:17 ?889次閱讀
    如何手搓<b class='flag-5'>一</b>個自定義的<b class='flag-5'>RPC</b> 遠程<b class='flag-5'>過程</b>調用框架

    TSMaster RPC 基礎入門:編程指導和使用說明

    介紹RPC模塊前,我們先淺聊RPC的相關說明,以及在什么樣的情況下需要了解本文。1.RPC說明遠程過程調用(
    的頭像 發表于 07-13 08:21 ?866次閱讀
    TSMaster <b class='flag-5'>RPC</b> 基礎入門:編程指導和使用說明

    記錄一次使用easypoi時與源碼博弈的過程

    、背景介紹 最近剛剛接手了保險線之聲平臺的開發和維護工作,第個需要修復的問題是:平臺的事件導出成excel功能在經過一次上線之后突然不
    的頭像 發表于 07-03 16:33 ?344次閱讀
    <b class='flag-5'>記錄</b><b class='flag-5'>一次</b>使用easypoi時與源碼博弈的<b class='flag-5'>過程</b>

    一次消諧器的構造

    今天來給大家介紹一下一次消諧器的構造。 一次消諧器是種用于消除電力系統中的諧波及無功功率的裝置,它由感性元件和電容器構成,感性元件用于吸收系統中的無功功率,而電容器則用于補償系統中的感性無功功率
    的頭像 發表于 05-30 14:55 ?415次閱讀

    鴻蒙OS開發:【一次開發,多端部署】(視頻應用)

    提供了“一次開發,多端部署”的系統能力,讓開發者可以基于一次開發,快速構建不同類型終端上的應用,降低開發成本,提高開發效率。
    的頭像 發表于 05-25 16:29 ?4544次閱讀
    鴻蒙OS開發:【<b class='flag-5'>一次</b>開發,多端部署】(視頻應用)

    HarmonyOS開發案例:【一次開發,多端部署(視頻應用)】

    提供了“一次開發,多端部署”的系統能力,讓開發者可以基于一次開發,快速構建不同類型終端上的應用,降低開發成本,提高開發效率。
    的頭像 發表于 05-11 15:41 ?1465次閱讀
    HarmonyOS開發案例:【<b class='flag-5'>一次</b>開發,多端部署(視頻應用)】

    基波是一次諧波么 基波與一次諧波的區別

    基波是一次諧波么 基波與一次諧波的區別? 基波和一次諧波是兩個不同的概念。 基波是在諧波分析中指的是頻率最低且沒有任何諧波成分的波形,它是構成復雜波形的基礎。在正弦波中,基波就是正弦波
    的頭像 發表于 04-08 17:11 ?7617次閱讀

    鴻蒙OS跨進程IPC與RPC通信

    便是為了突破這點。IPC和RPC通常采用客戶端-服務器(Client-Server)模型,在使用時,請求服務的(Client)端進程可獲
    發表于 02-17 14:20

    HarmonyOS跨進程通信—IPC與RPC通信開發

    便是為了突破這點。IPC和RPC通常采用客戶端-服務器(Client-Server)模型,在使用時,請求服務的(Client)端進程可獲
    的頭像 發表于 02-02 17:47 ?1283次閱讀
    HarmonyOS跨進程通信—IPC與<b class='flag-5'>RPC</b>通信開發

    DDoS攻擊規模最大的一次

    有史以來DDoS攻擊規模最大的是哪一次? Google Cloud團隊在2017年9月披露了一次此前未公開的DDoS攻擊,其流量達 2.54Tbps,是迄今為止有記錄以來最大的DDoS攻擊。 在同時
    的頭像 發表于 01-18 15:39 ?458次閱讀

    記錄一次K8s pod被殺的排查過程

    今天下午運維反饋說我們這個pod天重啟了8,需要排查下原因。看Kiban日志,jvm沒有拋出過任何錯誤,服務就直接重啟了。
    的頭像 發表于 01-18 09:57 ?755次閱讀

    一次消諧器類型與選擇

    一次消諧器:類型與選擇 在電力系統中的設備運行過程中,諧波的存在往往會對設備產生不良影響。為了解決這問題,消諧器應運而生。今天,我們將為大家詳細介紹一次消諧器的類型與選擇。
    的頭像 發表于 01-09 10:14 ?605次閱讀
    主站蜘蛛池模板: 久久国产美女| 久草视频在线免费看| 国产卡一卡2卡三卡免费视频| 天天干夜夜想| 一级美女片| 日产精品卡二卡三卡四卡乱码视频 | 一级特黄aa毛片免费观看| 欧美日韩国产成人精品| 久久综合色综合| 日韩在线影院| 男男浪荡性受高hnp肉| 欧美性f| 日本全黄视频| 禁漫画羞羞动漫入口| 久久久久999| 黄乱色伦短篇小说h| 亚洲国产成人久久一区久久| 国模论坛| 午夜啪啪网站| 色色色色色色色色色色色色| 亚洲天堂va| 久久精品亚洲热综合一本奇米| 特级毛片免费视频观看| 18年大片免费在线观看| 中年艳妇乱小玩| 免费激情网址| 国产午夜精品视频| 最近最新视频中文字幕4| 国产一区二区三区影院| 在线视频黄| 日本a级片在线观看| 人人插人人| 午夜免费福利片| 偷自在线| 中文在线最新版天堂| www.三级.com| 美女露出尿口让男人桶爽网站| 黄色小毛片| 欧美午夜网站| 午夜精品久久久久蜜桃| 午夜小视频在线观看|