Nacos 配置實時更新原理分析

上篇文章《Nacos 配置中心原理分析》我和大家分析了 Nacos 的配置中心原理眶明,主要分析了 Nacos 客戶端是如何感知到服務(wù)端的配置變更的泛烙,但是只是從客戶端的角度進(jìn)行了分析,并沒有從服務(wù)端的角度進(jìn)行分析,本篇文章我將結(jié)合服務(wù)端從兩個角度來分析配置變更是如何通知到客戶端的。

PS:文章有點長鞍历,因為涉及到多個細(xì)節(jié)需要闡述,如果看不下去的話肪虎,可以直接轉(zhuǎn)到文末看結(jié)論即可劣砍。

一、客戶端

從上篇文章中我們已經(jīng)知道了 Nacos 的客戶端維護(hù)了一個長輪詢的任務(wù)扇救,去檢查服務(wù)端的配置信息是否發(fā)生變更刑枝,如果發(fā)生了變更香嗓,那么客戶端會拿到變更的 groupKey 再根據(jù) groupKey 去獲取配置項的最新值即可。

每次都靠客戶端去發(fā)請求装畅,詢問服務(wù)端我所關(guān)注的配置項有沒有發(fā)生變更靠娱,那請求的間隔改設(shè)置為多少才合適呢?

如果間隔時間設(shè)置的太長的話有可能無法及時獲取服務(wù)端的變更洁灵,如果間隔時間設(shè)置的太短的話,那么頻繁的請求對于服務(wù)端來說無疑也是一種負(fù)擔(dān)掺出。

所以最好的方式是客戶端每隔一段長度適中的時間去服務(wù)端請求徽千,而在這期間如果配置發(fā)生變更,服務(wù)端能夠主動將變更后的結(jié)果推送給客戶端汤锨,這樣既能保證客戶端能夠?qū)崟r感知到配置的變化双抽,也降低了服務(wù)端的壓力。

客戶端長輪詢

現(xiàn)在讓我們再次回到客戶端長輪詢的部分闲礼,也就是 LongPollingRunnable 中的 checkUpdateDataIds 方法牍汹,該方法就是用來訪問服務(wù)端的配置是否發(fā)生變更的,該方法最終會調(diào)用如下圖所示的方法:

check-update-config.jpg

請注意圖中紅框部分的內(nèi)容柬泽,客戶端是通過一個 http 的 post 請求去獲取服務(wù)端的結(jié)果的慎菲,并且設(shè)置了一個超時時間:30s。

這個信息很關(guān)鍵锨并,為什么客戶端要等待 30s 才超時呢露该?不應(yīng)該越快得到結(jié)果越好嗎,我們來驗證下該方法是不是真的等待了 30s第煮。

在 LongPollingRunnable 中的 checkUpdateDataIds 方法前后加上時間計算解幼,然后將所消耗的時間打印出來,如下圖所示:

print-cost-check-update-config.jpg

然后我們啟動客戶端包警,觀察打印的日志撵摆,如下圖所示:

long-polling-cost-result.jpg

從打印出來的日志可以看出來,客戶端足足等了29.5+s害晦,才請求到服務(wù)端的結(jié)果特铝。然后客戶端得到服務(wù)端的結(jié)果之后,再做一些后續(xù)的操作壹瘟,全部都執(zhí)行完畢之后苟呐,在 finally 中又重新調(diào)用了自身,也就是說這個過程是一直循環(huán)下去的俐筋。

長輪詢時修改配置

現(xiàn)在我們可以確定的是牵素,客戶端向服務(wù)端發(fā)起一次請求,最少要29.5s才能得到結(jié)果澄者,當(dāng)然啦笆呆,這是在配置沒有發(fā)生變化的情況下请琳。

如果客戶端在長輪詢時配置發(fā)生變更的話,該請求需要多長時間才會返回呢赠幕,我們繼續(xù)做一個實驗俄精,在客戶端長輪詢時修改配置,結(jié)果如下圖所示:

long-polling-cost-result-2.jpg

上圖中紅框中就是我在客戶端一發(fā)起請求時就更新配置后打印的結(jié)果榕堰,從結(jié)果可以看出來該請求并沒有等到 29.5s+ 才返回竖慧,而是一個很短的時間就返回了,具體多久需要從服務(wù)端的實現(xiàn)中查詢答案逆屡。

到目前為止我們已經(jīng)知道了客戶端執(zhí)行長輪詢的邏輯圾旨,以及每次請求的響應(yīng)時間會隨著服務(wù)端配置是否變更而發(fā)生變化,具體可以用下圖描述:

nacos-client-request.jpg

二魏蔗、服務(wù)端

分析完客戶端的情況砍的,接下來要重點分析服務(wù)端是如何實現(xiàn)的,并且要帶著幾個問題去尋找答案:

客戶端長輪詢的響應(yīng)時間會受什么影響

為什么更改了配置信息后客戶端會立即得到響應(yīng)

客戶端的超時時間為什么要設(shè)置為30s

帶著以上這些問題我們從服務(wù)端的代碼中去探尋結(jié)論莺治。

首先我們從客戶端發(fā)送的 http 請求中可以知道廓鞠,請求的是服務(wù)端的 /v1/cs/configs/listener 這個接口。

我們找到該接口對應(yīng)的方法谣旁,在 ConfigController 類中床佳,如下圖所示:

com.alibaba.nacos.config.server.controller.ConfigController.java

config-controller-listener.jpg

Nacos 的服務(wù)端是通過 spring 對外提供的 http 服務(wù),對 HttpServletRequest 中的參數(shù)進(jìn)行轉(zhuǎn)換后榄审,然后交給一個叫 inner 的對象去執(zhí)行夕土。

下面我們進(jìn)入這個叫 inner 的對象中去,該 inner 對象是 ConfigServletInner 類的實例瘟判,具體的方法如下所示:

com.alibaba.nacos.config.server.controller.ConfigServletInner.java

do-polling-config.jpg

可以看到該方法是一個輪詢的接口怨绣,除了支持長輪詢外還支持短輪詢的邏輯,這里我們只關(guān)心長輪詢的部分拷获,也就是圖中紅框中的部分篮撑。

再次進(jìn)入 longPollingService 的 addLongPollingClient 方法,如下圖所示:

com.alibaba.nacos.config.server.service.LongPollingService.java

add-long-polling-client.jpg

從該方法的名字我們可以知道匆瓜,該方法主要是將客戶端的長輪詢請求添加到某個東西中去赢笨,在方法的最后一行我們得到了答案:服務(wù)端將客戶端的長輪詢請求封裝成一個叫 ClientLongPolling 的任務(wù),交給 scheduler 去執(zhí)行驮吱。

但是請注意我用紅框圈出來的代碼茧妒,服務(wù)端拿到客戶端提交的超時時間后,又減去了 500ms 也就是說服務(wù)端在這里使用了一個比客戶端提交的時間少 500ms 的超時時間左冬,也就是 29.5s桐筏,看到這個 29.5s 我們應(yīng)該有點興奮了。

PS:這里的 timeout 不一定一直是 29.5拇砰,當(dāng) isFixedPolling() 方法為 true 時梅忌,timeout 將會是一個固定的間隔時間狰腌,這里為了描述簡單就直接用 29.5 來進(jìn)行說明。

接下來我們來看服務(wù)端封裝的 ClientLongPolling 的任務(wù)到底執(zhí)行的什么操作牧氮,如下圖所示:

com.alibaba.nacos.config.server.service.LongPollingService.ClientLongPolling.java

client-long-polling.jpg

ClientLongPolling 被提交給 scheduler 執(zhí)行之后琼腔,實際執(zhí)行的內(nèi)容可以拆分成以下四個步驟:

1.創(chuàng)建一個調(diào)度的任務(wù),調(diào)度的延時時間為 29.5s

2.將該 ClientLongPolling 自身的實例添加到一個 allSubs 中去

3.延時時間到了之后踱葛,首先將該 ClientLongPolling 自身的實例從 allSubs 中移除

4.獲取服務(wù)端中保存的對應(yīng)客戶端請求的 groupKeys 是否發(fā)生變更丹莲,將結(jié)果寫入 response 返回給客戶端

整個過程可以用下面的圖進(jìn)行描述:

client-long-polling-process.jpg

這里出現(xiàn)了一個很關(guān)鍵的 allSubs 對象,該對象是一個 ConcurrentLinkedQueue 隊列尸诽,ClientLongPolling 將自身添加到隊列中去肯定是有原因的甥材,這里需要對 allSubs 留個心眼。

調(diào)度任務(wù)

我們先不管 allSubs 隊列具體做了什么事逊谋,先來看下服務(wù)端過了 29.5s 的延時時間后擂达,執(zhí)行調(diào)度任務(wù)時做了什么土铺,也就是上圖中對應(yīng)的第三胶滋、第四步。

首先將自身從 allSubs 隊列中刪除掉悲敷,也就是如注釋中說的:刪除訂閱關(guān)系究恤,從這里我們可以知道 allSubs 和 ClientLongPolling 之間維持了一種訂閱關(guān)系,而 ClientLongPolling 是被訂閱的后德。

PS:刪除掉訂閱關(guān)系之后部宿,訂閱方就無法對被訂閱方進(jìn)行通知了。

然后服務(wù)端對客戶端提交上來的 groupKey 進(jìn)行檢查瓢湃,如果發(fā)現(xiàn)某一個 groupKey 的 md5 值還不是最新的理张,則說明客戶端的配置項還沒發(fā)生變更,所以將該 groupKey 放到一個 changedGroupKeys 列表中绵患,最后將該 changedGroupKeys 返回給客戶端雾叭。

對于客戶端來說,只要拿到 changedGroupKeys 即可落蝙,后續(xù)的操作我在上一篇文章中已經(jīng)分析過了织狐。

服務(wù)端數(shù)據(jù)變更

服務(wù)端直到調(diào)度任務(wù)的延時時間到了之前,ClientLongPolling 都不會有其他的任務(wù)可做筏勒,所以在這段時間內(nèi)移迫,該 allSubs 隊列肯定有事情需要進(jìn)行處理。

回想到我們在客戶端長輪詢期間管行,更改了配置之后厨埋,客戶端能夠立即得到響應(yīng),所以我們有理由相信捐顷,這個隊列可能會跟配置變更有關(guān)系揽咕。

現(xiàn)在我們找一下在 dashboard 上修改配置后悲酷,調(diào)用的請求,可以很容易的找到該請求對應(yīng)的 url為:/v1/cs/configs 并且是一個 POST 請求亲善,具體的方法是 ConfigController 中的 publishConfig 方法设易,如下圖所示:

publish-config.jpg

我只截取了重要的部分,從紅框中的代碼可以看出蛹头,修改配置后顿肺,服務(wù)端首先將配置的值進(jìn)行了持久化層的更新,然后觸發(fā)了一個 ConfigDataChangeEvent 的事件渣蜗。

具體的 fireEvent 的方法如下圖所示:

com.alibaba.nacos.config.server.utils.event.EventDispatcher.java

fire-event.jpg

fireEvent 方法實際上是觸發(fā)的 AbstractEventListener 的 onEvent 方法屠尊,而所有的 listener 是保存在一個叫 listeners 對象中的。

被觸發(fā)的 AbstractEventListener 對象則是通過 addEventListener 方法添加到 listeners 中的耕拷,所以我們只需要找到 addEventListener 方法在何處被調(diào)用的讼昆,就知道有哪些 AbstractEventListener 需要被觸發(fā) onEvent 回調(diào)方法了。

可以找到是在 AbstractEventListener 類的構(gòu)造方法中骚烧,將自身注冊進(jìn)去了浸赫,如下圖所示:

com.alibaba.nacos.config.server.utils.event.EventDispatcher.AbstractEventListener.java

abstract-event-listener.jpg

而 AbstractEventListener 是一個抽象類,所以實際注冊的應(yīng)該是 AbstractEventListener 的子類,所以我們需要找到所以繼承自 AbstractEventListener 的類,如下圖所示:

abstract-event-listener-subclass.jpg

可以看到 AbstractEventListener 所有的子類中驹尼,有一個我們熟悉的身影筝家,他就是我們剛剛一直在研究的 LongPollingService。

所以到這里我們就知道了,當(dāng)我們從 dashboard 中更新了配置項之后,實際會調(diào)用到 LongPollingService 的 onEvent 方法。

現(xiàn)在我們繼續(xù)回到 LongPollingService 中传惠,查看一下 onEvent 方法,如下圖所示:

on-event.jpg

com.alibaba.nacos.config.server.service.LongPollingService.DataChangeTask.java

發(fā)現(xiàn)當(dāng)觸發(fā)了 LongPollingService 的 onEvent 方法時稻扬,實際是執(zhí)行了一個叫 DataChangeTask 的任務(wù)卦方,應(yīng)該是通過該任務(wù)來通知客戶端服務(wù)端的數(shù)據(jù)已經(jīng)發(fā)生了變更,我們進(jìn)入 DataChangeTask 中看下具體的代碼腐螟,如下圖所示:

data-change-task.jpg

代碼很簡單愿汰,可以總結(jié)為兩個步驟:

1.遍歷 allSubs 的隊列

首先遍歷 allSubs 的隊列,該隊列中維持的是所有客戶端的請求任務(wù)乐纸,需要找到與當(dāng)前發(fā)生變更的配置項的 groupKey 相等的 ClientLongPolling 任務(wù)

2.往客戶端寫響應(yīng)數(shù)據(jù)

在第一步找到具體的 ClientLongPolling 任務(wù)后衬廷,只需要將發(fā)生變更的 groupKey 通過該 ClientLongPolling 寫入到響應(yīng)對象中,就完成了一次數(shù)據(jù)變更的 “推送” 操作了

如果 DataChangeTask 任務(wù)完成了數(shù)據(jù)的 “推送” 之后汽绢,ClientLongPolling 中的調(diào)度任務(wù)又開始執(zhí)行了怎么辦呢吗跋?

很簡單,只要在進(jìn)行 “推送” 操作之前,先將原來等待執(zhí)行的調(diào)度任務(wù)取消掉就可以了跌宛,這樣就防止了推送操作寫完響應(yīng)數(shù)據(jù)之后酗宋,調(diào)度任務(wù)又去寫響應(yīng)數(shù)據(jù),這時肯定會報錯的疆拘。

可以從 sendResponse 方法中看到蜕猫,確實是這樣做的:

send-response.jpg

問題解答

現(xiàn)在讓我們回到剛開始的時候提的幾個問題,相信大家已經(jīng)有了答案了哎迄。

客戶端長輪詢的響應(yīng)時間會受什么影響

客戶端長輪詢的響應(yīng)時間回右,設(shè)置的是30s,但是有時響應(yīng)很快漱挚,有時響應(yīng)很慢翔烁,這取決于服務(wù)端的配置有沒有發(fā)生變化。當(dāng)配置發(fā)生變化時旨涝,響應(yīng)很快就會返回蹬屹,當(dāng)配置一直沒有發(fā)生變化時,會等到 29.5s 之后再進(jìn)行響應(yīng)白华。

為什么更改了配置信息后客戶端會立即得到響應(yīng)

因為服務(wù)端會在更改了配置信息后慨默,找到具體的客戶端請求中的 response,然后直接將結(jié)果寫入 response 中衬鱼,就像服務(wù)端對客戶端進(jìn)行的數(shù)據(jù) “推送” 一樣业筏,所以客戶端會很快得到響應(yīng)憔杨。

客戶端的超時時間為什么要設(shè)置為30s

這應(yīng)該是一個經(jīng)驗值鸟赫,該超時時間關(guān)系到服務(wù)端調(diào)度任務(wù)的等待時間,服務(wù)端在前29.5s 只需要進(jìn)行等待消别,最后的 0.5s 才進(jìn)行配置變更檢查抛蚤。

如果設(shè)置的太短,那服務(wù)端等待的時間就太短寻狂,如果這時配置變更的比較頻繁岁经,那很可能無法在等待期對客戶端做推送,而是滑動到檢查期對數(shù)據(jù)進(jìn)行檢查后才能將數(shù)據(jù)變更發(fā)回給客戶端蛇券,檢查期相比等待期需要進(jìn)行數(shù)據(jù)的檢查缀壤,涉及到 IO 操作,而 IO 操作是比較昂貴的纠亚,我們應(yīng)該盡量在等待期就將數(shù)據(jù)變更發(fā)送給客戶端塘慕。

http 請求本來就是無狀態(tài)的,所以沒必要也不能將超時時間設(shè)置的太長蒂胞,這樣是對資源的一種浪費(fèi)图呢。

結(jié)論

1、客戶端的請求到達(dá)服務(wù)端后,服務(wù)端將該請求加入到一個叫 allSubs 的隊列中蛤织,等待配置發(fā)生變更時 DataChangeTask 主動去觸發(fā)赴叹,并將變更后的數(shù)據(jù)寫入響應(yīng)對象,如下圖所示:

nacos-config-update-1.jpg

2指蚜、與此同時服務(wù)端也將該請求封裝成一個調(diào)度任務(wù)去執(zhí)行乞巧,等待調(diào)度的期間就是等待 DataChangeTask 主動觸發(fā)的,如果延遲時間到了 DataChangeTask 還未觸發(fā)的話摊鸡,則調(diào)度任務(wù)開始執(zhí)行數(shù)據(jù)變更的檢查摊欠,然后將檢查的結(jié)果寫入響應(yīng)對象,如下圖所示:

nacos-config-update-2.jpg

基于上述的分析柱宦,最終總結(jié)了以下結(jié)論:

1.Nacos 客戶端會循環(huán)請求服務(wù)端變更的數(shù)據(jù)些椒,并且超時時間設(shè)置為30s,當(dāng)配置發(fā)生變化時掸刊,請求的響應(yīng)會立即返回免糕,否則會一直等到 29.5s+ 之后再返回響應(yīng)

2.Nacos 客戶端能夠?qū)崟r感知到服務(wù)端配置發(fā)生了變化。

3.實時感知是建立在客戶端拉和服務(wù)端“推”的基礎(chǔ)上忧侧,但是這里的服務(wù)端“推”需要打上引號石窑,因為服務(wù)端和客戶端直接本質(zhì)上還是通過 http 進(jìn)行數(shù)據(jù)通訊的,之所以有“推”的感覺蚓炬,是因為服務(wù)端主動將變更后的數(shù)據(jù)通過 http 的 response 對象提前寫入了松逊。

至此,正如標(biāo)題所說的肯夏,推+拉打造 Nacos 配置信息的實時更新的原理已經(jīng)分析清楚了经宏。

逅弈逐碼,專注于原創(chuàng)分享驯击,用通俗易懂的圖文描述源碼及原理

作者:逅弈

鏈接:http://www.reibang.com/p/acb9b1093a54

來源:簡書

著作權(quán)歸作者所有烁兰。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處徊都。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沪斟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子暇矫,更是在濱河造成了極大的恐慌主之,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件李根,死亡現(xiàn)場離奇詭異槽奕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)朱巨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門史翘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事琼讽”胤澹” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵钻蹬,是天一觀的道長吼蚁。 經(jīng)常有香客問我,道長问欠,這世上最難降的妖魔是什么肝匆? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮顺献,結(jié)果婚禮上旗国,老公的妹妹穿的比我還像新娘。我一直安慰自己注整,他們只是感情好能曾,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肿轨,像睡著了一般寿冕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上椒袍,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天驼唱,我揣著相機(jī)與錄音,去河邊找鬼驹暑。 笑死玫恳,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的岗钩。 我是一名探鬼主播纽窟,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼肖油,長吁一口氣:“原來是場噩夢啊……” “哼兼吓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起森枪,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤视搏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后县袱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浑娜,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年式散,在試婚紗的時候發(fā)現(xiàn)自己被綠了筋遭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖漓滔,靈堂內(nèi)的尸體忽然破棺而出编饺,到底是詐尸還是另有隱情,我是刑警寧澤响驴,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布透且,位于F島的核電站,受9級特大地震影響豁鲤,放射性物質(zhì)發(fā)生泄漏秽誊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一琳骡、第九天 我趴在偏房一處隱蔽的房頂上張望锅论。 院中可真熱鬧,春花似錦楣号、人聲如沸棍厌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽耘纱。三九已至,卻和暖如春毕荐,著一層夾襖步出監(jiān)牢的瞬間束析,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工憎亚, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留员寇,地道東北人。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓第美,卻偏偏與公主長得像蝶锋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子什往,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

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

  • 上篇文章《Nacos 配置中心原理分析》我和大家分析了 Nacos 的配置中心原理扳缕,主要分析了 Nacos 客戶端...
    逅弈閱讀 48,323評論 17 48
  • Nacos 另一個非常重要的特性就是服務(wù)注冊與發(fā)現(xiàn),說到服務(wù)的注冊與發(fā)現(xiàn)相信大家應(yīng)該都不陌生别威,在微服務(wù)盛行的今天躯舔,...
    駱孝宇閱讀 1,139評論 0 2
  • 動態(tài)配置管理是 Nacos 的三大功能之一,通過動態(tài)配置服務(wù)省古,我們可以在所有環(huán)境中以集中和動態(tài)的方式管理所有應(yīng)用程...
    逅弈閱讀 75,790評論 9 101
  • Nacos主要有兩大功能:配置中心和服務(wù)注冊 配置中心 我們知道客戶端會有一個長輪訓(xùn)的任務(wù)去檢查服務(wù)器端的配置是否...
    WEIJAVA閱讀 16,473評論 1 1
  • 黑色的海島上懸著一輪又大又圓的明月豺妓,毫不嫌棄地把溫柔的月色照在這寸草不生的小島上惜互。一個少年白衣白發(fā)布讹,悠閑自如地倚坐...
    小水Vivian閱讀 3,102評論 1 5