華為云 TaurusDB 性能挑戰(zhàn)賽賽題總結(jié)

1 前言

image-20190902204538276

回顧第一次參加性能挑戰(zhàn)賽--第四屆阿里中間件性能挑戰(zhàn)賽,那時(shí)候真的是什么都不會(huì),只有一腔熱情,借著比賽學(xué)會(huì)了 Netty、學(xué)會(huì)了文件 IO 的最佳實(shí)踐,到了這次華為云舉辦的 TaurusDB 性能挑戰(zhàn)賽,已經(jīng)是第三次參加比賽了,同時(shí)也是最“坎坷”的一次比賽。經(jīng)過(guò)我和某位不愿意透露姓名的 96 年小迷妹的不懈努力,最終跑分排名為第 3 名。

如果要挑選一個(gè)詞來(lái)概括這次比賽的核心內(nèi)容,那非”計(jì)算存儲(chǔ)分離“莫屬了,通過(guò)這次比賽,自己也對(duì)計(jì)算存儲(chǔ)分離架構(gòu)有了比較直觀的感受。為了比較直觀的體現(xiàn)計(jì)算存儲(chǔ)分離的優(yōu)勢(shì),以看電影來(lái)舉個(gè)例子:若干年前,我總是常備一塊大容量的硬盤(pán)存儲(chǔ)小電影,但自從家里帶寬升級(jí)到 100mpbs 之后,我從來(lái)不保存電影了,要看直接下載/緩沖,基本幾分鐘就好了。這在幾年前還不可想象,如今是觸手可及的事實(shí),歸根到底是隨著互聯(lián)網(wǎng)的發(fā)展,網(wǎng)絡(luò) IO 已經(jīng)不再是瓶頸了。

計(jì)算存儲(chǔ)分離架構(gòu)相比傳統(tǒng)本地存儲(chǔ)架構(gòu)而言,具有更加靈活、成本更低等特性,但架構(gòu)的復(fù)雜性也會(huì)更高,也會(huì)更加考驗(yàn)選手的綜合能力。

計(jì)算存儲(chǔ)分離架構(gòu)的含義:

  • 存儲(chǔ)端有狀態(tài),只存儲(chǔ)數(shù)據(jù),不處理業(yè)務(wù)邏輯。
  • 計(jì)算端無(wú)狀態(tài),只處理邏輯,不持久化存儲(chǔ)數(shù)據(jù)。

2 賽題概覽

比賽整體分成了初賽和復(fù)賽兩個(gè)部分,初賽要求實(shí)現(xiàn)一個(gè)簡(jiǎn)化、高效的本地 kv 存儲(chǔ)引擎,復(fù)賽在初賽的基礎(chǔ)上增加了計(jì)算存儲(chǔ)分離的架構(gòu),計(jì)算節(jié)點(diǎn)需要通過(guò)網(wǎng)絡(luò)傳輸將數(shù)據(jù)遞交給存儲(chǔ)節(jié)點(diǎn)存儲(chǔ)。

public interface KVStoreRace {
   
   public boolean init(final String dir, final int thread_num) throws KVSException;
   
   public long set(final String key, final byte[] value) throws KVSException;
   
   public long get(final String key, final Ref<byte[]> val) throws KVSException;
}

計(jì)算節(jié)點(diǎn)和存儲(chǔ)節(jié)點(diǎn)共用上述的接口,評(píng)測(cè)程序分為 2 個(gè)階段:

正確性評(píng)測(cè)

此階段評(píng)測(cè)程序會(huì)并發(fā)寫(xiě)入隨機(jī)數(shù)據(jù)(key 8B、value 4KB),寫(xiě)入數(shù)據(jù)過(guò)程中進(jìn)行任意次進(jìn)程意外退出測(cè)試,引擎需要保證異常中止不影響已經(jīng)寫(xiě)入的數(shù)據(jù)正確性。
異常中止后,重啟引擎,驗(yàn)證已經(jīng)寫(xiě)入數(shù)據(jù)正確性和完整性,并繼續(xù)寫(xiě)入數(shù)據(jù),重復(fù)此過(guò)程直至數(shù)據(jù)寫(xiě)入完畢。
只有通過(guò)此階段測(cè)試才會(huì)進(jìn)入下一階段測(cè)試。

性能評(píng)測(cè)

隨機(jī)寫(xiě)入:16 個(gè)線程并發(fā)隨機(jī)寫(xiě)入,每個(gè)線程使用 Set 各寫(xiě) 400 萬(wàn)次隨機(jī)數(shù)據(jù)(key 8B、value 4KB)
順序讀?。?6 個(gè)線程并發(fā)按照寫(xiě)入順序逐一讀取,每個(gè)線程各使用 Get 讀取 400 萬(wàn)次隨機(jī)數(shù)據(jù)
熱點(diǎn)讀?。?6 個(gè)線程并發(fā)讀取,每個(gè)線程按照寫(xiě)入順序熱點(diǎn)分區(qū),隨機(jī)讀取 400 萬(wàn)次數(shù)據(jù),讀取范圍覆蓋全部寫(xiě)入數(shù)據(jù)。熱點(diǎn)的邏輯為:按照數(shù)據(jù)的寫(xiě)入順序按 10MB 數(shù)據(jù)粒度分區(qū),分區(qū)逆序推進(jìn),在每個(gè) 10MB 數(shù)據(jù)分區(qū)內(nèi)隨機(jī)讀取。隨機(jī)讀取次數(shù)會(huì)增加約 10%。

語(yǔ)言限定

CPP & Java,一起排名

3 賽題剖析

看過(guò)我之前《PolarDB數(shù)據(jù)庫(kù)性能大賽Java選手分享》的朋友應(yīng)該對(duì)題目不會(huì)感到陌生,基本可以看做是在 PolarDB 數(shù)據(jù)庫(kù)性能挑戰(zhàn)賽上增加一個(gè)網(wǎng)絡(luò)通信的部分,所以重頭戲基本是在復(fù)賽網(wǎng)絡(luò)通信的比拼上。初賽主要是文件 IO 和存儲(chǔ)架構(gòu)的設(shè)計(jì),如果對(duì)文件 IO 常識(shí)不太了解,可以先行閱讀 《文件IO操作的一些最佳實(shí)踐》。

image-20190902214231821

3.1 架構(gòu)設(shè)計(jì)

計(jì)算節(jié)點(diǎn)只負(fù)責(zé)生成數(shù)據(jù),在實(shí)際生產(chǎn)中計(jì)算節(jié)點(diǎn)還承擔(dān)額外的計(jì)算開(kāi)銷(xiāo),由于計(jì)算節(jié)點(diǎn)是無(wú)狀態(tài)的,所以不能夠聚合數(shù)據(jù)寫(xiě)入、落盤(pán)等操作,但可以在 Get 觸發(fā)網(wǎng)絡(luò) IO 時(shí)一次讀取大塊數(shù)據(jù)用作緩存,減少網(wǎng)絡(luò) IO 次數(shù)。

存儲(chǔ)節(jié)點(diǎn)負(fù)責(zé)存儲(chǔ)數(shù)據(jù),考驗(yàn)了選手對(duì)磁盤(pán) IO 和緩存的設(shè)計(jì),可以一次使用緩存寫(xiě)入/讀取大塊數(shù)據(jù),減少磁盤(pán) IO 次數(shù)。

所以選手們將會(huì)圍繞網(wǎng)絡(luò) IO、磁盤(pán) IO 和緩存設(shè)計(jì)來(lái)設(shè)計(jì)整體架構(gòu)。

3.2 正確性檢測(cè)

賽題明確表示會(huì)進(jìn)行 kill -9 并驗(yàn)證數(shù)據(jù)的一致性,正確性檢測(cè)主要影響的是寫(xiě)入階段。

存儲(chǔ)節(jié)點(diǎn)負(fù)責(zé)存儲(chǔ)數(shù)據(jù),需要保證 kill -9 不丟失數(shù)據(jù),但并不要求斷電不丟失,這間接地闡釋了一點(diǎn):我們可以使用 PageCache 來(lái)做寫(xiě)入緩存;正確性檢測(cè)對(duì)于計(jì)算節(jié)點(diǎn)與存儲(chǔ)節(jié)點(diǎn)之間通信影響便是:每次寫(xiě)入操作都必須 ack,所以選手必須保證同步通信,類似于 ping/pong 模型。

3.3 性能評(píng)測(cè)

性能評(píng)測(cè)由隨機(jī)寫(xiě)、順序讀、熱點(diǎn)讀(隨機(jī)讀取熱點(diǎn)數(shù)據(jù))三部分構(gòu)成。

隨機(jī)寫(xiě)階段與 PolarDB 的評(píng)測(cè)不同,TaurusDB 隨機(jī)寫(xiě)入 key 的 16 個(gè)線程是隔離的,即 A 線程寫(xiě)入的數(shù)據(jù)只會(huì)由 A 線程讀出,可以認(rèn)為是彼此獨(dú)立的 16 個(gè)實(shí)例在執(zhí)行評(píng)測(cè),這大大簡(jiǎn)化了我們的架構(gòu)。

順序讀階段的描述也很容易理解,需要注意的是這里的順序是按照寫(xiě)入順序,而不是 Key 的字典序,所以隨機(jī)寫(xiě)可以轉(zhuǎn)化為順序?qū)?,也方便了選手去設(shè)計(jì)順序讀的架構(gòu)。

熱點(diǎn)讀階段有點(diǎn)故弄玄虛了,其實(shí)就是按照 10M 數(shù)據(jù)為一個(gè)分區(qū)進(jìn)行逆序讀,同時(shí)在 10M 數(shù)據(jù)范圍內(nèi)摻雜一些隨機(jī)讀,由于操作系統(tǒng)的預(yù)讀機(jī)制只會(huì)順序預(yù)讀,無(wú)法逆序預(yù)讀,PageCache 將會(huì)在這個(gè)環(huán)節(jié)會(huì)失效,考驗(yàn)了選手自己設(shè)計(jì)磁盤(pán) IO 緩存的能力。

4 架構(gòu)詳解

4.1 全局架構(gòu)

image-20190903130239656

計(jì)算存儲(chǔ)分離架構(gòu)自然會(huì)分成計(jì)算節(jié)點(diǎn)和存儲(chǔ)節(jié)點(diǎn)兩部分來(lái)介紹。計(jì)算節(jié)點(diǎn)會(huì)在內(nèi)存維護(hù)數(shù)據(jù)的索引表;存儲(chǔ)節(jié)點(diǎn)負(fù)責(zé)存儲(chǔ)持久化數(shù)據(jù),包括索引文件和數(shù)據(jù)文件;計(jì)算節(jié)點(diǎn)與存儲(chǔ)節(jié)點(diǎn)之間的讀寫(xiě)都會(huì)經(jīng)過(guò)網(wǎng)絡(luò) IO。

4.2 隨機(jī)寫(xiě)架構(gòu)

image-20190903134509621

隨機(jī)寫(xiě)階段,評(píng)測(cè)程序調(diào)用計(jì)算節(jié)點(diǎn)的 set 接口,發(fā)起網(wǎng)絡(luò) IO,存儲(chǔ)節(jié)點(diǎn)接受到數(shù)據(jù)后不會(huì)立刻落盤(pán),針對(duì) data 和 index 的處理也會(huì)不同。針對(duì) data 部分,會(huì)使用一塊緩沖區(qū)(如圖:Mmap Merge IO)承接數(shù)據(jù),由于 Mmap 的特性,會(huì)形成 Merge File 文件,一個(gè)數(shù)據(jù)緩沖區(qū)可以聚合 16 個(gè)數(shù)據(jù),當(dāng)緩沖區(qū)滿后,將緩沖區(qū)的數(shù)據(jù)追加到數(shù)據(jù)文件后,并清空 Merge File;針對(duì) index 部分,使用 Mmap 直接追加到索引文件中。

F: 1. data 部分為什么搞這么復(fù)雜,需要聚合 16 個(gè)數(shù)據(jù)再刷盤(pán)?

Q: 針對(duì)此次比賽的數(shù)據(jù)盤(pán),實(shí)測(cè)下來(lái) 16 個(gè)數(shù)據(jù)刷盤(pán)可以打滿 IO。

F: 2. 為什么使用 Mmap Merge IO 而不直接使用內(nèi)存 Merge IO?

Q: 正確性檢測(cè)階段,存儲(chǔ)節(jié)點(diǎn)可能會(huì)被隨機(jī) kill,Mmap 做緩存的好處是操作系統(tǒng)會(huì)幫我們落盤(pán),不會(huì)丟失數(shù)據(jù)

F: 3. 為什么 index 部分直接使用 Mmap,而不和 data 部分一樣處理?

Q: 這需要追溯到 Mmap 的特點(diǎn),Mmap 適合直接寫(xiě)索引這種小數(shù)據(jù),所以不需要聚合。

4.3 熱點(diǎn)讀&順序讀架構(gòu)

image-20190903134612617

熱點(diǎn)讀取階段 & 順序讀取階段 ,這兩個(gè)階段其實(shí)可以認(rèn)為是一種策略,只不過(guò)一個(gè)正序,一個(gè)逆序,這里以熱點(diǎn)讀為例介紹。我們采取了貪心的思想,一次讀取操作本應(yīng)該只會(huì)返回 4kb 的數(shù)據(jù),但為了做預(yù)讀緩存,我們決定會(huì)存儲(chǔ)節(jié)點(diǎn)返回 10M 的數(shù)據(jù),并緩存在計(jì)算節(jié)點(diǎn)中,模擬了一個(gè)操作系統(tǒng)預(yù)讀的機(jī)制,同時(shí)為了能夠讓計(jì)算節(jié)點(diǎn)精確知道緩存是否命中,會(huì)同時(shí)返回索引數(shù)據(jù),并在計(jì)算節(jié)點(diǎn)的內(nèi)存中維護(hù)索引表,這樣便減少了成噸的網(wǎng)絡(luò) IO 次數(shù)。

4.4 存儲(chǔ)設(shè)計(jì)

image-20190903133433218

站在每個(gè)線程的視角,可以發(fā)現(xiàn)在我們的架構(gòu)中,每個(gè)線程都是獨(dú)立的。評(píng)測(cè)程序會(huì)對(duì)每個(gè)線程寫(xiě)入 400w 數(shù)據(jù),最終形成 16 * 16G 的數(shù)據(jù)文件和 16 * 32M 左右的索引文件。

數(shù)據(jù)文件不停追加 MergeFile,相當(dāng)于一次落盤(pán)單位是 64K(16 個(gè)數(shù)據(jù)),由于自行聚合了數(shù)據(jù),所以可以采用 Direct IO,減少操作系統(tǒng)的 overhead。

索引文件由小數(shù)據(jù)構(gòu)成,所以采用 Mmap 方式直接追加寫(xiě)

計(jì)算節(jié)點(diǎn)由于無(wú)狀態(tài)的特性,只能在內(nèi)存中維護(hù)索引結(jié)構(gòu)。

4.5 網(wǎng)絡(luò)通信設(shè)計(jì)

image-20190903193128706

我們都知道 Java 中有 BIO(阻塞 IO)和 NIO(非阻塞 IO)之分,并且大多數(shù)人可能會(huì)下意識(shí)覺(jué)得:NIO 就是比 BIO 快。而這次比賽恰恰是要告訴大家,這兩種 IO 方式?jīng)]有絕對(duì)的快慢之分,只有在合適的場(chǎng)景中選擇合適的 IO 方式才能發(fā)揮出最佳性能。

稍微分析下這次比賽的通信模型,寫(xiě)入階段由于需要保證每次 set 不受 kill 的影響,所以需要等到同步返回后才能進(jìn)行下一次 set,而 get 本身依賴于返回值進(jìn)行數(shù)據(jù)校驗(yàn),所以從通信模型上看只能是同步 ping/pong 模型;從線程數(shù)上來(lái)看,只有固定的 16 個(gè)線程進(jìn)行收發(fā)消息。以上兩個(gè)因素暗示了 BIO 將會(huì)非常契合這次比賽。

在很多人的刻板印象中,阻塞就意味著慢,非阻塞就意味著快,這種理解是完全錯(cuò)誤的,快慢取決于通信模型、系統(tǒng)架構(gòu)、帶寬、網(wǎng)卡等因素。我測(cè)試了 NIO + CountDownLatch 和 BIO 的差距,前者會(huì)比后者整體慢 100s ~ 130s。

5 細(xì)節(jié)優(yōu)化點(diǎn)

5.1 最大化磁盤(pán)吞吐量

但凡是涉及到磁盤(pán) IO 的比賽,首先需要測(cè)試便是在 Direct IO 下,一次讀寫(xiě)多大的塊能夠打滿 IO,在此基礎(chǔ)上,才能進(jìn)行寫(xiě)入緩沖設(shè)計(jì)和讀取緩存設(shè)計(jì),否則在這種爭(zhēng)分奪秒的性能挑戰(zhàn)賽中不可能取得較好的名次。測(cè)試方法也很簡(jiǎn)單,如果能夠買(mǎi)到對(duì)應(yīng)的機(jī)器,直接使用 iostat 觀察不同刷盤(pán)大小下的 iops 即可,如果比賽沒(méi)有機(jī)器,只能祭出調(diào)參大法,不停提交了,這次 TaurusDB 的盤(pán)實(shí)測(cè)下來(lái) 64k、128K 都可以獲得最大的吞吐量。

5.2 批量回傳數(shù)據(jù)

計(jì)算節(jié)點(diǎn)設(shè)計(jì)緩存是一個(gè)比較容易想到的優(yōu)化點(diǎn),按照常規(guī)的思路,索引應(yīng)該是維護(hù)在存儲(chǔ)節(jié)點(diǎn),但這樣做的話,計(jì)算節(jié)點(diǎn)在 get 數(shù)據(jù)時(shí)就無(wú)法判斷是否命中緩存,所以在前文的架構(gòu)介紹中,我們將索引維護(hù)在了計(jì)算節(jié)點(diǎn)之上,在第一次 get 時(shí),順便恢復(fù)索引。批量返回?cái)?shù)據(jù)的優(yōu)勢(shì)在于增加了緩存命中率、降低總網(wǎng)絡(luò) IO 次數(shù)、減少上行網(wǎng)絡(luò) IO 數(shù)據(jù)量,是整個(gè)比賽中分量較重的一個(gè)優(yōu)化點(diǎn)。

5.3 流控

image-20190903201156406

在比賽中容易出現(xiàn)的一個(gè)問(wèn)題,在批量返回 10M 數(shù)據(jù)時(shí)經(jīng)常會(huì)出現(xiàn)網(wǎng)絡(luò)卡死的情況,一時(shí)間無(wú)法定位到問(wèn)題,以為是代碼 BUG,但有時(shí)候又能跑出分?jǐn)?shù),不得以嘗試過(guò)一次返回較少的數(shù)據(jù)量,就不會(huì)報(bào)錯(cuò)。最后還是機(jī)智的小迷妹定位到問(wèn)題是 CPU 和 IO 速率不均等導(dǎo)致的,解決方案便是在一次 pong 共計(jì)返回 10M 的基礎(chǔ)上,將報(bào)文拆分成 64k 的小塊,中間插入額外的 CPU 操作,最終保證了程序穩(wěn)定性的同時(shí),也保障了最佳性能。

額外的 CPU 操作例如:for(int i=0;i<700;i++),不要小看這個(gè)微不足道的一個(gè) for 循環(huán)哦。

流控其實(shí)也是計(jì)算存儲(chǔ)分離架構(gòu)一個(gè)常見(jiàn)設(shè)計(jì)點(diǎn),存儲(chǔ)節(jié)點(diǎn)與計(jì)算節(jié)點(diǎn)的寫(xiě)入速度需要做一個(gè)平衡,避免直接打垮存儲(chǔ)節(jié)點(diǎn),也有一種”滑動(dòng)窗口“機(jī)制專門(mén)應(yīng)對(duì)這種問(wèn)題,不在此贅述了。

5.4 預(yù)分配文件

在 Cpp 中可以使用 fallocate 預(yù)先分配好文件大小,會(huì)使得寫(xiě)入速度提升 2s。在 Java 中沒(méi)有 fallocate 機(jī)制,但是可以利用評(píng)測(cè)程序的漏洞,在 static 塊中事先寫(xiě)好 16 * 16G 的文件,同樣可以獲得 fallocate 的效果。

5.5 合理設(shè)計(jì)索引結(jié)構(gòu)

get 時(shí)需要根據(jù) key 查詢到文件偏移量,這顯示是一個(gè) Map 結(jié)構(gòu),在這個(gè) Map 上也有幾個(gè)點(diǎn)需要注意。以 Java 為例,使用 HashMap 是否可行呢?當(dāng)然可以,但是缺點(diǎn)也很明顯,其會(huì)占用比較大的內(nèi)存,而且存取性能不好,可以使用 LongIntHashMap 來(lái)代替,看過(guò)我之前文章的朋友應(yīng)該不會(huì)對(duì)這個(gè)數(shù)據(jù)結(jié)構(gòu)感到陌生,它是專門(mén)為基礎(chǔ)數(shù)據(jù)類型設(shè)計(jì)的 Map 容器。

每個(gè)線程 400w 數(shù)據(jù),每個(gè)線程獨(dú)享一個(gè)索引 Map,為了避免出現(xiàn)擴(kuò)容,需要合理的設(shè)置擴(kuò)容引子和初始化容量:new LongIntHashMap(410_0000, 0.99);

5.6 Direct IO

最終進(jìn)入決賽的,有三支 Java 隊(duì)伍,相比較 Cpp 得天獨(dú)厚的對(duì)操作系統(tǒng)的靈活控制性,Java 選手更像是帶著鐐銬在舞蹈,幸好有了上次 PolarDB 比賽的經(jīng)驗(yàn),我提前封裝好了 Java 的 Direct IO 類庫(kù):https://github.com/lexburner/kdio,相比 FileChannel,它能夠使得磁盤(pán) IO 效率更高。得知有 Java 選手真的在比賽中使用了我的 Direct IO 類庫(kù),也是比賽中實(shí)實(shí)切切的樂(lè)趣之一。

6 失敗的優(yōu)化點(diǎn)

6.1 預(yù)讀線程先行

考慮到網(wǎng)絡(luò) IO 還是比本地磁盤(pán) IO 要慢的,一個(gè)本以為可行的方案是單獨(dú)使用預(yù)讀線程進(jìn)行存儲(chǔ)節(jié)點(diǎn)的磁盤(pán) IO,設(shè)計(jì)一個(gè) RingBuffer,不斷往前預(yù)讀,直到環(huán)滿,計(jì)算階段 get 時(shí)會(huì)消費(fèi) RingBuffer 的一格緩存,從而使得網(wǎng)絡(luò) IO 和磁盤(pán) IO 不會(huì)相互等待。實(shí)際測(cè)試下來(lái),發(fā)現(xiàn)瓶頸主要還是在于網(wǎng)絡(luò) IO,這樣的優(yōu)化徒增了不少代碼,不利于進(jìn)行其他的優(yōu)化嘗試,最終放棄。

6.2 計(jì)算節(jié)點(diǎn)聚合寫(xiě)入緩沖

既然在 get 階段時(shí)存儲(chǔ)節(jié)點(diǎn)批量返回?cái)?shù)據(jù)給計(jì)算節(jié)點(diǎn)可以提升性能,那 set 階段聚合批量的數(shù)據(jù)再發(fā)送給存儲(chǔ)節(jié)點(diǎn)按理來(lái)說(shuō)也能提升性能吧?的確如此,如果不考慮正確性檢測(cè),這的確是一個(gè)不錯(cuò)的優(yōu)化點(diǎn),但由于 kill 的特性使得我們不得不每一次 set 都進(jìn)行 ACK。但是!可以對(duì)將 4/8/16 個(gè)線程編為一組進(jìn)行聚合呀!通過(guò)調(diào)整參數(shù)來(lái)確定該方案是否可行。

image-20190903215024344

然后事與愿違,該方案并沒(méi)有取得成效。

7 聊聊比賽吧

之前此類工程性質(zhì)的性能挑戰(zhàn)賽只有阿里一家互聯(lián)網(wǎng)公司承辦過(guò),作為熱衷于中間件性能優(yōu)化的參賽選手而言,非常高興華為也能夠舉辦這樣性質(zhì)的比賽。雖然比賽中出現(xiàn)了諸多的幺蛾子,但畢竟是第一次承辦比賽,我也就不表了。

如果你同樣也是性能挑戰(zhàn)賽的愛(ài)好者,想要在下一次中間件性能挑戰(zhàn)賽中有一群小伙伴一起解題、組隊(duì),體驗(yàn)沖分的樂(lè)趣,歡迎關(guān)注我的微信公眾號(hào):【Kirito的技術(shù)分享】,也歡迎加入微信技術(shù)交流群進(jìn)行交流~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容