解決gateway使用nacos重啟報(bào)503 Service Unavailable問(wèn)題

問(wèn)題描述

項(xiàng)目使用spring cloud gateway作為網(wǎng)關(guān),nacos作為微服務(wù)注冊(cè)中心侨艾,項(xiàng)目搭建好后正常訪問(wèn)都沒(méi)問(wèn)題,但是有個(gè)很煩人的小瑕疵:

  • 當(dāng)某個(gè)微服務(wù)重啟后,通過(guò)網(wǎng)關(guān)調(diào)用這個(gè)服務(wù)時(shí)有時(shí)會(huì)出現(xiàn)503 Service Unavailable(服務(wù)不可用)的錯(cuò)誤瞳腌,但過(guò)了一會(huì)兒又可以訪問(wèn)了,這個(gè)等待時(shí)間有時(shí)很長(zhǎng)有時(shí)很短镜雨,甚至有時(shí)候還不會(huì)出現(xiàn)
  • 導(dǎo)致每次重啟某個(gè)項(xiàng)目都要順便啟動(dòng)gateway項(xiàng)目才能保證立即可以訪問(wèn)嫂侍,時(shí)間長(zhǎng)了感覺(jué)好累,想徹底研究下為什么,并徹底解決

接下來(lái)介紹我在解決整個(gè)過(guò)程的思路挑宠,如果沒(méi)興趣菲盾,可以直接跳到最后的最終解決方案

gateway感知其它服務(wù)上下線

首先在某個(gè)微服務(wù)上下線時(shí),gateway的控制臺(tái)可以立即看到有對(duì)應(yīng)的輸出


某服務(wù)下線gateway輸出
某服務(wù)上線gateway輸出

這說(shuō)明nacos提供了這種監(jiān)聽功能各淀,在注冊(cè)中心服務(wù)列表發(fā)生時(shí)可以第一時(shí)間通知客戶端懒鉴,而在我們的依賴spring-cloud-starter-alibaba-nacos-discovery中顯然已經(jīng)幫我們實(shí)現(xiàn)了這個(gè)監(jiān)聽

所以也就說(shuō)明gateway是可以立即感知其它服務(wù)的上下線事件,但問(wèn)題是明明感知到某個(gè)服務(wù)的上線碎浇,那為什么會(huì)出現(xiàn)503 Service Unavailable的錯(cuò)誤临谱,而且上面的輸出有時(shí)出現(xiàn)了很久,但調(diào)用依然是503 Service Unavailable奴璃,對(duì)應(yīng)的某服務(wù)明明下線悉默,這是應(yīng)該是503 Service Unavailable狀態(tài),可有時(shí)確會(huì)有一定時(shí)間的500錯(cuò)誤

ribbon

為了調(diào)查事情的真相苟穆,我打開了gateway的debug日志模式抄课,找到了503的罪魁禍?zhǔn)?br>

503的控制臺(tái)輸出

在503錯(cuò)誤輸出前,有一行這樣的日志Zone aware logic disabled or there is only one zone雳旅,而報(bào)這個(gè)信息的包就是ribbon-loadbalancer跟磨,也就是gateway默認(rèn)所使用的負(fù)載均衡器

我的gateway配置文件路由方面設(shè)置如下

routes:
        - id: auth
          uri: lb://demo-auth
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1

其中在uri這一行,使用了lb:// ,代表使用了gateway的ribbon負(fù)載均衡功能岭辣,官方文檔說(shuō)明如下
Note that this example also demonstrates (optional) Spring Cloud Netflix Ribbon load-balancing (defined the lb prefix on the destination URI)

ribbon再調(diào)用時(shí)首先會(huì)獲取所有服務(wù)列表(ip和端口信息)吱晒,然后根據(jù)負(fù)載均衡策略調(diào)用其中一個(gè)服務(wù),選擇服務(wù)的代碼如下

package com.netflix.loadbalancer;
public class ZoneAwareLoadBalancer<T extends Server> extends DynamicServerListLoadBalancer<T> {
    // 選擇服務(wù)的方法
    public Server chooseServer(Object key) {
            if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
                logger.debug("Zone aware logic disabled or there is only one zone");
                return super.chooseServer(key);
            }
    ...     

這就是上面的Zone aware logic..這行日志的出處沦童,經(jīng)調(diào)試發(fā)現(xiàn)在getLoadBalancerStats().getAvailableZones()這一步返回的服務(wù)是空列表仑濒,說(shuō)明這里沒(méi)有存儲(chǔ)任何服務(wù)信息,所以才導(dǎo)致最終的503 Service Unavailable
繼續(xù)跟進(jìn)去看getAvailableZones的代碼偷遗,如下

public class LoadBalancerStats implements IClientConfigAware {
    // 一個(gè)緩存所有服務(wù)的map
    volatile Map<String, List<? extends Server>> upServerListZoneMap = new ConcurrentHashMap<String, List<? extends Server>>();
    // 獲取可用服務(wù)keys
    public Set<String> getAvailableZones() {
        return upServerListZoneMap.keySet();
    }

可以看到ribbon是在LoadBalancerStats中維護(hù)了一個(gè)map來(lái)緩存所有可用服務(wù)墩瞳,而問(wèn)題的原因也大概明了了:gateway獲取到了服務(wù)變更事件,但并沒(méi)有及時(shí)更新ribbon的服務(wù)列表緩存

ribbon的刷新緩存機(jī)制

現(xiàn)在的實(shí)際情況是:gateway獲取到了服務(wù)變更事件氏豌,但并沒(méi)有馬上更新ribbon的服務(wù)列表緩存喉酌,但過(guò)一段時(shí)間可以訪問(wèn)說(shuō)明緩存又刷新了,那么接下來(lái)就要找到ribbon的緩存怎么刷新的泵喘,進(jìn)而進(jìn)一步分析為什么沒(méi)有及時(shí)刷新

在LoadBalancerStats查找到更新緩存的方法是updateZoneServerMapping

public class LoadBalancerStats implements IClientConfigAware {
    // 一個(gè)緩存所有服務(wù)的map
    volatile Map<String, List<? extends Server>> upServerListZoneMap = new ConcurrentHashMap<String, List<? extends Server>>();
    // 更新緩存
    public void updateZoneServerMapping(Map<String, List<Server>> map) {
        upServerListZoneMap = new ConcurrentHashMap<String, List<? extends Server>>(map);
        // make sure ZoneStats object exist for available zones for monitoring purpose
        for (String zone: map.keySet()) {
            getZoneStats(zone);
        }
    }

那么接下來(lái)看看這個(gè)方法的調(diào)用鏈泪电,調(diào)用鏈有點(diǎn)長(zhǎng),最終找到了DynamicServerListLoadBalancer下的updateListOfServers方法纪铺,首先看DynamicServerListLoadBalancer翻譯過(guò)來(lái)"動(dòng)態(tài)服務(wù)列表負(fù)載均衡器"相速,說(shuō)明它有動(dòng)態(tài)獲取服務(wù)列表的功能,那我們的bug它肯定難辭其咎鲜锚,而updateListOfServers就是它刷新緩存的手段突诬,那么就看看這個(gè)所謂的"動(dòng)態(tài)服務(wù)列表負(fù)載均衡器"是如何使用updateListOfServers動(dòng)態(tài)刷新緩存的

public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
    // 封裝成一個(gè)回調(diào)
    protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
        @Override
        public void doUpdate() {
            updateListOfServers();
        }
    };
    // 初始化
    public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
                                         ServerList<T> serverList, ServerListFilter<T> filter,
                                         ServerListUpdater serverListUpdater) {
        ...
        this.serverListUpdater = serverListUpdater; // serverListUpdate賦值
        ...
        // 初始化時(shí)刷新服務(wù)
        restOfInit(clientConfig);
    }
    
    void restOfInit(IClientConfig clientConfig) {
        ...
        // 開啟動(dòng)態(tài)刷新緩存
        enableAndInitLearnNewServersFeature();
        // 首先刷新一遍緩存
        updateListOfServers();
        ...
    }
    
    // 開啟動(dòng)態(tài)刷新緩存
    public void enableAndInitLearnNewServersFeature() {
        // 把更新的方法傳遞給serverListUpdater
        serverListUpdater.start(updateAction);
    }

可以看到初始化DynamicServerListLoadBalancer時(shí)苫拍,首先updateListOfServers獲取了一次服務(wù)列表并緩存,這只能保證項(xiàng)目啟動(dòng)獲取一次服務(wù)列表旺隙,而真正的動(dòng)態(tài)更新實(shí)現(xiàn)是把updateListOfServers方法傳遞給內(nèi)部serverListUpdater.start方法绒极,serverListUpdater翻譯過(guò)來(lái)就是“服務(wù)列表更新器”,所以再理一下思路:

DynamicServerListLoadBalancer只所以敢自稱“動(dòng)態(tài)服務(wù)列表負(fù)載均衡器”蔬捷,是因?yàn)樗鼉?nèi)部有個(gè)serverListUpdater(“服務(wù)列表更新器”)垄提,也就是serverListUpdater.start才是真正為ribbon提供動(dòng)態(tài)更新服務(wù)列表的方法,也就是罪魁禍?zhǔn)?/p>

那么就看看ServerListUpdater到底是怎么實(shí)現(xiàn)的動(dòng)態(tài)更新周拐,首先ServerListUpdater是一個(gè)接口塔淤,它的實(shí)現(xiàn)也只有一個(gè)PollingServerListUpdater,那么肯定是它了速妖,看一下它的start方法實(shí)現(xiàn)

public class PollingServerListUpdater implements ServerListUpdater {
    @Override
    public synchronized void start(final UpdateAction updateAction) {
        if (isActive.compareAndSet(false, true)) {
            // 定義一個(gè)runable高蜂,運(yùn)行doUpdate放
            final Runnable wrapperRunnable = new Runnable() {
                @Override
                public void run() {
                    ....
                    try {
                        updateAction.doUpdate(); // 執(zhí)行更新服務(wù)列表方法
                        lastUpdated = System.currentTimeMillis();
                    } catch (Exception e) {
                        logger.warn("Failed one update cycle", e);
                    }
                }
            };

            // 定時(shí)執(zhí)行
            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,
                    initialDelayMs,
                    refreshIntervalMs, // 默認(rèn)30 * 1000
                    TimeUnit.MILLISECONDS
            );
        } else {
            logger.info("Already active, no-op");
        }
    }

至此真相大白了,原來(lái)ribbon默認(rèn)更新服務(wù)列表依靠的是定時(shí)任務(wù)罕容,而且默認(rèn)30秒一次备恤,也就是說(shuō)假如某個(gè)服務(wù)重啟了,gateway的nacos客戶端也感知到了锦秒,但是ribbon內(nèi)部極端情況需要30秒才會(huì)重新獲取服務(wù)列表露泊,這也就解釋了為什么會(huì)有那么長(zhǎng)時(shí)間的503 Service Unavailable問(wèn)題

而且因?yàn)槎〞r(shí)任務(wù),所以等待時(shí)間是0-30秒不等旅择,有可能你剛重啟完就獲取了正常調(diào)用沒(méi)問(wèn)題惭笑,也有可能剛重啟完時(shí)剛獲取完一次,結(jié)果就得等30秒才能訪問(wèn)到新的節(jié)點(diǎn)

解決思路

問(wèn)題的原因找到了生真,接下來(lái)就是解決了沉噩,最簡(jiǎn)單暴力的方式莫過(guò)于修改定時(shí)任務(wù)的間隔時(shí)間,默認(rèn)30秒柱蟀,可以改成10秒川蒙,5秒,1秒长已,只要你機(jī)器配置夠牛逼

但是有沒(méi)有更優(yōu)雅的解決方案畜眨,我們的gateway明明已經(jīng)感知到服務(wù)的變化,如果通知ribbon直接更新术瓮,問(wèn)題不就完美解決了嗎康聂,這種思路定時(shí)任務(wù)都可以去掉了,性能還優(yōu)化了

具體解決步驟如下

  • 寫一個(gè)新的更新器胞四,替換掉默認(rèn)的PollingServerListUpdater更新器
  • 更新器可以監(jiān)聽nacos的服務(wù)更新
  • 收到服務(wù)更新事件時(shí)恬汁,調(diào)用doUpdate方法更新ribbon緩存

接下來(lái)一步步解決

首先看上面DynamicServerListLoadBalancer的代碼,發(fā)現(xiàn)更新器是構(gòu)造方法傳入的撬讽,所以要找到構(gòu)造方法的調(diào)用并替換成自己信息的更新器

在DynamicServerListLoadBalancer構(gòu)造方法上打了個(gè)斷點(diǎn)蕊连,看看它是如何被初始化的(并不是gateway啟動(dòng)就會(huì)初始化,而是首次調(diào)用某個(gè)服務(wù)游昼,給對(duì)應(yīng)的服務(wù)創(chuàng)建一個(gè)LoadBalancer甘苍,有點(diǎn)懶加載的意思)

構(gòu)造方法斷點(diǎn)

debugger

看一下debugger的函數(shù)調(diào)用,發(fā)現(xiàn)一個(gè)doCreateBean>>>createBeanInstance的調(diào)用烘豌,其中createBeanInstance執(zhí)行到如下地方
createBeanInstance

熟悉spring源碼的朋友應(yīng)該看得出來(lái)DynamicServerListLoadBalancer是spring容器負(fù)責(zé)創(chuàng)建的载庭,而且是FactoryBean模式。

這個(gè)bean的定義在spring-cloud-netflix-ribbon依賴中的RibbonClientConfiguration類

package org.springframework.cloud.netflix.ribbon;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@Import({ HttpClientConfiguration.class, OkHttpRibbonConfiguration.class,
        RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class })
public class RibbonClientConfiguration {
    ...
    @Bean
    @ConditionalOnMissingBean
    public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
        return new PollingServerListUpdater(config);
    }
    ...
}

也就是通過(guò)我們熟知的@Configuration+@Bean模式創(chuàng)建的PollingServerListUpdater更新器廊佩,而且加了個(gè)注解@ConditionalOnMissingBean

也就是說(shuō)我們自己實(shí)現(xiàn)一個(gè)ServerListUpdater更新器囚聚,并加入spring容器,就可以代替PollingServerListUpdater成為ribbon的更新器

最終解決方案

我們的更新器是要訂閱nacos的标锄,收到事件做update處理顽铸,為了避免ribbon和nacos耦合抽象一個(gè)監(jiān)聽器再用nacos實(shí)現(xiàn)

1.抽象監(jiān)聽器
/**
 * @Author pq
 * @Date 2022/4/26 17:19
 * @Description 抽象監(jiān)聽器
 */
public interface ServerListListener {
    /**
     * 監(jiān)聽
     * @param serviceId 服務(wù)名
     * @param eventHandler 回調(diào)
     */
    void listen(String serviceId, ServerEventHandler eventHandler);

    @FunctionalInterface
    interface ServerEventHandler {
        void update();
    }
}
自定義ServerListUpdater
public class NotificationServerListUpdater implements ServerListUpdater {

    private static final Logger logger = LoggerFactory.getLogger(NotificationServerListUpdater.class);

    private final ServerListListener listener;

    public NotificationServerListUpdater(ServerListListener listener) {
        this.listener = listener;
    }

    /**
     * 開始運(yùn)行
     * @param updateAction
     */
    @Override
    public void start(UpdateAction updateAction) {
        // 創(chuàng)建監(jiān)聽
        String clientName = getClientName(updateAction);
        listener.listen(clientName, ()-> {
            logger.info("{} 服務(wù)變化, 主動(dòng)刷新服務(wù)列表緩存", clientName);
            // 回調(diào)直接更新
            updateAction.doUpdate();
        });
    }

    /**
     * 通過(guò)updateAction獲取服務(wù)名,這種方法比較粗暴
     * @param updateAction
     * @return
     */
    private String getClientName(UpdateAction updateAction) {
        try {
            Class<?> bc = updateAction.getClass();
            Field field = bc.getDeclaredField("this$0");
            field.setAccessible(true);
            BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) field.get(updateAction);
            return baseLoadBalancer.getClientConfig().getClientName();
        } catch (Exception e) {
            e.printStackTrace();
            throw new IllegalStateException(e);
        }
    }
實(shí)現(xiàn)ServerListListener監(jiān)控nacos并注入bean容器
@Slf4j
@Component
public class NacosServerListListener implements ServerListListener {

    @Autowired
    private NacosServiceManager nacosServiceManager;

    private NamingService namingService;

    @Autowired
    private NacosDiscoveryProperties properties;

    @PostConstruct
    public void init() {
        namingService =  nacosServiceManager.getNamingService(properties.getNacosProperties());
    }

    /**
     * 創(chuàng)建監(jiān)聽器
     */
    @Override
    public void listen(String serviceId, ServerEventHandler eventHandler) {
        try {
            namingService.subscribe(serviceId, event -> {
                if (event instanceof NamingEvent) {
                    NamingEvent namingEvent = (NamingEvent) event;
//                    log.info("服務(wù)名:" + namingEvent.getServiceName());
//                    log.info("實(shí)例:" + namingEvent.getInstances());
                    // 實(shí)際更新
                    eventHandler.update();
                }
            });
        } catch (NacosException e) {
            e.printStackTrace();
        }
    }
}
把自定義Updater注入bean
@Configuration
@ConditionalOnRibbonNacos
public class RibbonConfig {
    @Bean
    public ServerListUpdater ribbonServerListUpdater(NacosServerListListener listener) {
        return new NotificationServerListUpdater(listener);
    }
}

到此料皇,大工告成谓松,效果是gateway訪問(wèn)的某微服務(wù)停止后,調(diào)用馬上503践剂,啟動(dòng)后鬼譬,馬上可以調(diào)用

總結(jié)

本來(lái)想解決這個(gè)問(wèn)題首先想到的是nacos或ribbon肯定留了擴(kuò)展,比如說(shuō)改了配置就可以平滑感知服務(wù)下線逊脯,但結(jié)果看了文檔和源碼优质,并沒(méi)有發(fā)現(xiàn)對(duì)應(yīng)的擴(kuò)展點(diǎn),所以只能大動(dòng)干戈來(lái)解決問(wèn)題军洼,其實(shí)很多地方都覺(jué)得很粗暴巩螃,比如獲取clientName,但也實(shí)在找不到更好的方案匕争,如果誰(shuí)知道牺六,麻煩評(píng)論告訴我一下

實(shí)際上我的項(xiàng)目更新器還保留了定時(shí)任務(wù)刷新的邏輯,一來(lái)剛接觸cloud對(duì)自己的修改自信不足汗捡,二來(lái)發(fā)現(xiàn)nacos的通知都是udp的通知方式淑际,可能不可靠,不知道是否多余

nacos的監(jiān)聽主要使用namingService的subscribe方法扇住,里面還有坑春缕,還有一層緩存,以后細(xì)講

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末艘蹋,一起剝皮案震驚了整個(gè)濱河市锄贼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌女阀,老刑警劉巖宅荤,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件屑迂,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡冯键,警方通過(guò)查閱死者的電腦和手機(jī)惹盼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)惫确,“玉大人手报,你說(shuō)我怎么就攤上這事「幕” “怎么了掩蛤?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)陈肛。 經(jīng)常有香客問(wèn)我揍鸟,道長(zhǎng),這世上最難降的妖魔是什么句旱? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任蜈亩,我火速辦了婚禮,結(jié)果婚禮上前翎,老公的妹妹穿的比我還像新娘稚配。我一直安慰自己,他們只是感情好港华,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布道川。 她就那樣靜靜地躺著,像睡著了一般立宜。 火紅的嫁衣襯著肌膚如雪冒萄。 梳的紋絲不亂的頭發(fā)上新思,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天餐曼,我揣著相機(jī)與錄音辫诅,去河邊找鬼恕稠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛瑞妇,可吹牛的內(nèi)容都是我干的竭讳。 我是一名探鬼主播支子,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼钟哥,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼迎献!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起腻贰,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤吁恍,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冀瓦,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡伴奥,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了翼闽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拾徙。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖肄程,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情选浑,我是刑警寧澤蓝厌,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站古徒,受9級(jí)特大地震影響拓提,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜隧膘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一代态、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧疹吃,春花似錦蹦疑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至腔呜,卻和暖如春叁温,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背核畴。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工膝但, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谤草。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓跟束,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親丑孩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子泳炉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容