一個面向未來的數據庫應使用哪些技術?

本文爲PingCAP聯合創始人兼CTO 黃東旭在TiDB DevCon 2019 上的演講實錄,分享了其對數據庫行業大趨勢以及未來數據庫技術的看法。

PingCAP其實並不是一個特別擅長髮明名詞的公司,我記得我們第一次使用HTAP 這個詞是在2016年左右。當時,市場部的同事還跟我們說 HTAP 這個詞從來沒人用過,都是論文裏的詞,大家都不知道,你把你們公司的產品定位改成這個別人都不知道怎麼辦?我們後來仔細想,還是覺得 HTAP 這個方向是一個更加適合我們的方向,所以還是選了 HTAP 這個詞。現在很欣喜的看到各種友商、後來的一些數據庫,都開始爭相說 HTAP,得到了同行的認可。

那麼在 HTAP 的未來應該是一個什麼樣子,我希望能夠在今年這個 Talk 裏面先說一說,但是這個題目起的有點不太謙虛,所以我特地加了一個「Near」, 分享一下這一年、兩年、三年我們想做什麼,和對行業大趨勢的展望。

image

今天我們的分享的一個主題就是:「我們只做用戶想要的東西,並不是要去做一個完美的東西」。其實很多工程師包括我們自己,都會有一個小小的心理潔癖,就是想要做一個超級快、超級牛的東西,但是做出來一個數據庫,單機跑分一百萬 TPS ,其實用戶實際業務就需要 3000,然後所有的用戶還會說我需要這些東西,比如需要 Scalability(彈性擴展), Super Large 的數據量,最好是我的業務一行代碼都不用改,而且 ACID 能夠完全的滿足,怎麼踹都踹不壞,機器壞了可以高可用,業務層完全不用動, 另外可以在跑 OLTP 的同時,完全不用擔心任何資源隔離地跑 OLAP(這裏不是要說大家的願望不切實際,而是非常切實際,我們也覺得數據庫本身就應該是這樣的)。

本質上來說用戶的需求就是「大一統」。看過《魔戒》的同學都知道這句話 :ONE RING TO RULE THEM ALL,就是一套解決方案去解決各種問題。

過去很多人,包括一些行業的大佬都認爲在各種環境下都要出一個數據庫來解決特定的一個問題,但是我們想走的方案還是儘可能在一個平臺裏面,儘可能大範圍去解決用戶的問題。因爲不同的產品之間去做數據的交互和溝通,其實是蠻複雜的。

image

圖 2 理想中的「賽道」

這張圖(圖 2)什麼意思呢?就是很多人設計系統的時候,總是會陷入跑分思維,在實驗室或者說在一個特定的 Workload 下,跑得巨快無比。如果大家去看一下大概 2000 年以後關於數據庫的論文,很多在做一個新的模型或者新的系統的時候,都會說 TPCC 能夠跑到多大,然後把 Oracle 摁在地上摩擦,這樣的論文有很多。但是大家回頭看看 Oracle 還是王者。所以大多數實驗室的產品和工程師自己做的東西都會陷入一個問題,就是想象中的我的賽道應該是一個圖 2 那樣的,但實際上用戶的業務環境是下面這樣的(圖 3)。很多大家在廣告上看到特別牛的東西,一放到生產環境或者說放到自己的業務場景裏面就不對了,然後陷入各種各樣的比較和糾結的煩惱之中。

image

圖 3 實際上用戶的業務環境

TiDB 的定位或者說我們想做的事情,並不是在圖 2 那樣的賽道上,跑步跑得巨快,全世界沒人在短跑上跑得過我,我們不想做這樣。或者說,我們其實也能跑得很快,但是並不想把所有優勢資源全都投入到一個用戶可能一輩子都用不到的場景之中。我們其實更像是做鐵人三項的,因爲用戶實際應用場景可能就是一個土路。這就是爲什麼 TiDB 的設計放在第一位的是「穩定性」。

我們一直在想能不能做一個數據庫,怎麼踹都踹不壞,然後所有的異常的狀況,或者它的 Workload 都是可預期的。我覺得很多人遠遠低估了這個事情的困難程度,其實我們自己也低估了這種困難程度。大概 4 年前出來創業的時候,我們就是想做這麼一個數據庫出來,我跟劉奇、崔秋三個人也就三個月做出來了。但是到現在已經 4 年過去了,我們的目標跟當年還是一模一樣。不忘初心,不是忘不掉,而是因爲初心還沒達到,怎麼忘?其實把一個數據庫做穩,是很難很難的。

image

圖 4 近年來硬件的發展

而且我們這個團隊的平均年齡可能也就在二十到三十歲之間,爲什麼我們如此年輕的一個團隊,能夠去做數據庫這麼古老的一件事情。其實也是得益於整個 IT 行業這幾年非常大的發展。圖 4 是這幾年發展起來的 SSD,內存越來越大,萬兆的網卡,還有各種各樣的多核的 CPU,虛擬化的技術,讓過去很多不可能的事情變成了可能。

舉一個例子吧,比如極端一點,大家可能在上世紀八九十年代用過這種 5 寸盤、3 寸盤,我針對這樣的磁盤設計一個數據結構,現在看上去是個笑話是吧?因爲大家根本沒有人用這樣的設備了。在數據庫這個行業裏面很多的假設,在現在新的硬件的環境下其實都是不成立的。比如說,爲什麼 B-Tree 就一定會比 LSM-Tree 要快呢?不一定啊,我跑到 Flash 或者 NVMe SSD 、Optane 甚至未來的持久化內存這種介質上,那數據結構設計完全就發生變化了。過去可能需要投入很多精力去做的數據結構,現在暴力就好了。

image

圖 5 近年來軟件變革

同時在軟件上也發生了很多很多的變革,圖 5 左上角是 Wisckey 那篇論文裏的一個截圖,還有一些分佈式系統上的新的技術,比如 2014 年 Diego 發表了 Raft 這篇論文,另外 Paxos 這幾年在各種新的分佈式系統裏也用得越來越多。

所以我覺得這幾年我們趕上了一個比較好的時代,不管是軟件還是硬件,還是分佈式系統理論上,都有了一些比較大突破,所以我們基礎才能夠打得比較好。

image

圖 6 Data Type

除了有這樣的新的硬件和軟件之外,我覺得在業務場景上也在發生一些比較大變化。過去,可能十年前就是我剛開始參加工作的時候,線上的架構基本就是在線和離線兩套系統,在線是 Oracle 和 MySQL,離線是一套 Hadoop 或者一個純離線的數據倉庫。但最近這兩年越來越多的業務開始強調敏捷、微服務和中臺化,於是產生了一個新的數據類型,就是 warm data,它需要像熱數據這樣支持 transaction、支持實時寫入,但是需要海量的數據都能存在這個平臺上實時查詢, 並不是離線數倉這種業務。

所以對 warm data 來說,過去在 TiDB 之前,其實是並沒有太好的辦法去很優雅的做一層大數據中臺架構的,「the missing part of modern data processing stack」,就是在 warm data 這方面,TiDB 正好去補充了這個位置,所以纔能有這麼快的增長。當然這個增長也是得益於 MySQL 社區的流行。

image

圖 7 應用舉例

想象一下,我們如果在過去要做這樣很簡單的業務(圖 7),比如在美國的訂單庫跟在中國的訂單庫可能都是在不同的數據庫裏,用戶庫可能是另外一個庫,然後不同的業務可能是操作不同的庫。如果我想看看美國的消費者裏面有哪些在中國有過消費的,就是這麼一條 SQL。過去如果沒有像 TiDB 這樣的東西,大家想象這個東西該怎麼做?

image

圖 8 過去的解決方案

假如說這兩邊的數據量都特別大,然後已經分庫分表了。過去可能只能第二天纔可以看到前一天的數據,因爲中間比如說一個 T+1 要做一個 ETL 到一個 data ware house 裏。或者厲害一點的架構師可能會說,我可以做一套實時的 OLAP 來做這個事情,怎麼做呢?比如說 MySQL 中間通過一個 MQ 再通過 Hadoop 做一下 ETL,然後再導到 Hadoop 上做一個冷的數據存儲,再在上面去跑一個 OLAP 做實時的分析。先不說這個實時性到底有多「實時」,大家仔細算一算,這套架構需要的副本數有多少,比如 M 是我的業務數,N 是每一個系統會存儲的 Replica,拍腦袋算一下就是下面這個數字(圖 9 中的 R )。

image

圖 9 過去解決方案裏需要的 Replica 數量

所以大家其實一開始在過去說,TiDB 這個背後這麼多 Replica 不好,但其實你想想,你自己在去做這個業務的時候,大家在過去又能怎麼樣呢?所以我覺得 TiDB 在這個場景下去統一一箇中臺,是一個大的趨勢。今天在社區實踐分享上也看到很多用戶都要提到了 TiDB 在中臺上非常好的應用。

image

圖 10 現在的解決方案

回顧完行業和應用場景近年來的一些變化之後,我們再說說未來。假設要去做一個面向未來的數據庫,會使用哪些技術?

1. Log is the new database

第一個大的趨勢就是日誌,「log is the new database」 這句話應該也是業界的一個共識吧。現在如果有一個分佈式數據庫的複製協議,還是同步一個邏輯語句過去,或者做 binlog 的複製,那其實還算比較 low 的。

image

圖 11 Log is the new database

上面圖 11 左半部分是 Hyper,它是慕尼黑工業大學的一個實驗性數據庫項目,它做了一些分析,第一個柱形是正常的 SQL 語句的執行時間,比如說直接把一語句放到另外一個庫裏去執行,耗時這麼多。第二個柱形是用邏輯日誌去存放,耗時大概能快 23%,第三個柱形能看到如果是存放物理日誌能快 56%。所以大家仔細想想,TiDB 的架構裏的 TiFlash 其實同步的是 Raft 日誌,而並不是同步 Binlog 或者其他的。

上面圖 11 右半部分是 Aurora,它的架構就不用說了,同步的都是 redo log 。其實他的好處也很明顯,也比較直白,就是 I/O 更小,網絡傳輸的 size 也更小,所以就更快。

然後在這一塊 TiDB 跟傳統的數據庫有點不一樣的就是,其實如果很多同學對 TiDB 的基礎架構不太理解的話就覺得, Raft 不是一個一定要有 Index 或者說是一定強順序的一個算法嗎?那爲什麼能做到這樣的亂序的提交?其實 TiDB 並不是單 Raft 的架構,而是一個多 Raft 的架構,I/O 可以發生在任何一個 Raft Group 上。傳統的單機型數據庫,就算你用更好的硬件都不可能達到一個線性擴展,因爲無論怎麼去做,都是這麼一個架構不可改變。比如說我單機上 Snapshot 加 WAL,不管怎麼寫, 總是在 WAL 後面加,I/O 總是發生在這。但 TiDB 的 I/O 是分散在多個 Raft Group、多個機器上,這是一個很本質的變化,這就是爲什麼在一些場景下,TiDB 能夠獲取更好的吞吐。

2. Vectorized

第二個大趨勢是全面的向量化。向量化是什麼意思?我舉個簡單的例子。比如我要去算一個聚合,從一個表裏面去求某一列的總量數據,如果我是一個行存的數據庫,我只能把這條記錄的 C 取出來,然後到下一條記錄,再取再取再取,整個 Runtime 的開銷也好,還有去掃描、讀放大的每一行也好,都是很有問題的。但是如果在內存裏面已經是一個列式存儲,是很緊湊的結構的話,那會是非常快的。

image

圖 12 TiDB 向量化面臨的挑戰

這裏面其實也有一些挑戰。我們花了大概差不多 2018 年一年的時間去做向量化的改造,其實還挺難的。爲什麼?首先 TiDB SQL 引擎是用了 Volcano 模型,這個模型很簡單,就是遍歷一棵物理計劃的樹,不停的調 Next,每一次 Next 都是調用他的子節點的 Next,然後再返回結果。這個模型有幾個問題:第一是每一次都是拿一行,導致 CPU 的 L1、L2 這樣的緩存利用率很差,就是說沒有辦法利用多 CPU 的 Cache。第二,在真正實現的時候,它內部的架構是一個多級的虛函數調用。大家知道虛函數調用在 Runtime 本身的開銷是很大的,在《MonetDB/X100: Hyper-Pipelining Query Execution》(http://cidrdb.org/cidr2005/papers/P19.pdf) 裏面提到,在跑 TPC-H 的時候,Volcano 模型在 MySQL 上跑,大概有 90% 的時間是花在 MySQL 本身的 Runtime 上,而不是真正的數據掃描。所以這就是 Volcano 模型一個比較大的問題。第三,如果使用一個純靜態的列存的數據結構,大家知道列存特別大問題就是它的更新是比較麻煩的, 至少過去在 TiFlash 之前,沒有一個列存數據庫能夠支持做增刪改查。那在這種情況下,怎麼保證數據的新鮮?這些都是問題。

image

圖 13 TiDB SQL 引擎向量化

TiDB 已經邁出了第一步,我們已經把 TiDB SQL 引擎的 Volcano 模型,從一行一行變成了一個 Chunk 一個 Chunk,每個 Chunk 裏面是一個批量的數據,所以聚合的效率會更高。而且在 TiDB 這邊做向量化之外,我們還會把這些算子推到 TiKV 來做,然後在 TiKV 也會變成一個全向量化的執行器的框架。

3. Workload Isolation

另外一個比較大的話題,是 Workload Isolation。今天我們在演示的各種東西都有一箇中心思想,就是怎麼樣儘可能地把 OLTP 跟 OLAP 隔離開。這個問題在業界也有不同的聲音,包括我們的老前輩 Google Spanner,他們其實是想做一個新的數據結構,來替代 Bigtable-Like SSTable 數據結構,這個數據結構叫 Ressi,大家去看 2018 年 《Spanner: Becoming a SQL System》這篇 Paper 就能看到。它其實表面上看還是行存,但內部也是一個 Chunk 變成列存這樣的一個結構。但我們覺得即使是換一個新的數據結構,也沒有辦法很好做隔離,因爲畢竟還是在一臺機器上,在同一個物理資源上。最徹底的隔離是物理隔離。

image

圖 14 TiFlash 架構

我們在 TiFlash 用了好幾種技術來去保證數據是更新的。一是增加了 Raft Leaner,二是我們把 TiDB 的 MVCC 也實現在了 TiFlash 的內部。第三在 TiFlash 這邊接觸了更新(的過程),在 TiFlash 內部還有一個小的 Memstore,來處理更新的熱數據結果,最後查詢的時候,是列存跟內存裏的行存去 merge 並得到最終的結果。TiFlash 的核心思想就是通過 Raft 的副本來做物理隔離。

這個有什麼好處呢?這是我們今天給出的答案,但是背後的思考,到底是什麼原因呢?爲什麼我們不能直接去同步一個 binlog 到另外一個 dedicate 的新集羣上(比如 TiFlash 集羣),而一定要走 Raft log?最核心的原因是,我們認爲 Raft log 的同步可以水平擴展的。因爲 TiDB 內部是 Mult-Raft 架構,Raft log 是發生在每一個 TiKV 節點的同步上。大家想象一下,如果中間是通過 Kafka 溝通兩邊的存儲引擎,那麼實時的同步會受制於中間管道的吞吐。比如圖 14 中綠色部分一直在更新,另一邊併發寫入每秒兩百萬,但是中間的 Kafka 集羣可能只能承載 100 萬的寫入,那麼就會導致中間的 log 堆積,而且下游的消費也是不可控的。而通過 Raft 同步, Throughput 可以根據實際存儲節點的集羣大小,能夠線性增長。這是一個特別核心的好處。

4. SIMD

說完了存儲層,接下來說一說執行器。TiDB 在接下來會做一個很重要的工作,就是全面地 leverage SIMD 的計算。我先簡單科普一下 SIMD 是什麼。

image

圖 15 SIMD 原理舉例(1/2)

如圖 15,在做一些聚合的時候,有這樣一個函數,我要去做一個求和。正常人寫程序,他就是一個 for 循環,做累加。但是在一個數據庫裏面,如果有一百億條數據做聚合,每一次執行這條操作的時候,CPU 的這個指令是一次一次的執行,數據量特別大或者掃描的行數特別多的時候,就會很明顯的感受到這個差別。

image

圖 16 SIMD 原理舉例(2/2)

現代的 CPU 會支持一些批量的指令,比如像 _mm_add_epi32,可以一次通過一個32 位字長對齊的命令,批量的操作 4 個累加。看上去只是省了幾個 CPU 的指令,但如果是在一個大數據量的情況下,基本上能得到 4 倍速度的提升。

順便說一句,有一個很大的趨勢是 I/O 已經不是瓶頸了,大家一定要記住我這句話。再過幾年,如果想去買一塊機械磁盤,除了在那種冷備的業務場景以外,我相信大家可能都要去定製一塊機械磁盤了。未來一定 I/O 不會是瓶頸,那瓶頸會是什麼?CPU。我們怎麼去用新的硬件,去儘可能的把計算效率提升,這個纔是未來我覺得數據庫發展的重點。比如說我怎麼在數據庫裏 leverage GPU 的計算能力,因爲如果 GPU 用的好,其實可以很大程度上減少計算的開銷。所以,如果在單機 I/O 這些都不是問題的話,下一個最大問題就是怎麼做好分佈式,這也是爲什麼我們一開始就選擇了一條看上去更加困難的路:我要去做一個 Share-nothing 的數據庫,並不是像 Aurora 底下共享一個存儲。

5. Dynamic Data placement

image

圖 17 Dynamic Data placement (1/2)
分庫分表方案與 TiDB 對比

在今天大家其實看不到未來十年數據增長是怎樣的,回想十年前大家能想到現在我們的數據量有這麼大嗎?不可能的。所以新的架構或者新的數據庫,一定要去面向我們未知的 Scale 做設計。比如大家想象現在有業務 100T 的數據,目前看可能還挺大的,但是有沒有辦法設計一套方案去解決 1P、2P 這樣數據量的架構?在海量的數據量下,怎麼把數據很靈活的分片是一個很大的學問。

爲什麼分庫分表在對比 TiDB 的時候,我們會覺得分庫分表是上一代的方案。這個也很好理解,核心的原因是分庫分表的 Router 是靜態的。如果出現分片不均衡,比如業務可能按照 User ID 分表,但是發現某一地方/某一部分的 User ID 特別多,導致數據不均衡了,這時 TiDB 的架構有什麼優勢呢?就是 TiDB 徹底把分片這個事情,從數據庫裏隔離了出來,放到了另外一個模塊裏。分片應該是根據業務的負載、根據數據的實時運行狀態,來決定這個數據應該放在哪兒。這是傳統的靜態分片不能相比的,不管傳統的用一致性哈希,還是用最簡單的對機器數取模的方式去分片(都是不能比的)。

在這個架構下,甚至未來我們還能讓 AI 來幫忙。把分片操作放到 PD 裏面,它就像一個 DBA 一樣,甚至預測 Workload 給出數據分佈操作。比如課程報名數據庫系統,系統發現可能明天會是報名高峯,就事先把數據給切分好,放到更好的機器上。這在傳統方案下是都需要人肉操作,其實這些事情都應該是自動化的。

image

圖 18 Dynamic Data placement (2/2)

Dynamic Data placement 好處首先是讓事情變得更 flexible ,對業務能實時感知和響應。另外還有一點,爲什麼我們有了 Dynamic Placement 的策略,還要去做 Table Partition(今天上午申礫也提到了)?Table Partition 在背後實現其實挺簡單的。相當於業務這邊已經告訴我們數據應該怎麼分片比較好,我們還可以做更多針對性的優化。這個 Partition 指的是邏輯上的 Partition ,是可能根據你的業務相關的,比如說我這張表,就是存着 2018 年的數據,雖然我在底下還是 TiDB 這邊,通過 PD 去調度,但是我知道你 Drop 這個 Table 的時候,一定是 Drop 這些數據,所以這樣會更好,而且更加符合用戶的直覺。

但這樣架構仍然有比較大的挑戰。當然這個挑戰在靜態分片的模型上也都會有。比如說圍繞着這個問題,我們一直在去嘗試解決怎麼更快的發現數據的熱點,比如說我們的調度器,如果最好能做到,比如突然來個秒殺業務,我們馬上就發現了,就趕緊把這塊數據挪到好的機器上,或者把這塊數據趕緊添加副本,再或者把它放到內存的存儲引擎裏。這個事情應該是由數據庫本身去做的。所以爲什麼我們這麼期待 AI 技術能夠幫我們,是因爲雖然在 TiDB 內部,用了很多規則和方法來去做這個事情,但我們不是萬能的。

6. Storage and Computing Seperation

image

圖 19 存儲計算分離

還有大的趨勢是存儲計算分離。我覺得現在業界有一個特別大的問題,就是把存儲計算分離給固化成了某一個架構的特定一個指代,比如說只有長的像 Aurora 那樣的架構纔是存儲計算分離。那麼 TiDB 算存儲計算分離嗎?我覺得其實算。或者說存儲計算分離本質上帶來的好處是什麼?就是我們的存儲依賴的物理資源,跟計算所依賴的物理資源並不一樣。這點其實很重要。就用 TiDB 來舉例子,比如計算可能需要很多 CPU,需要很多內存來去做聚合,存儲節點可能需要很多的磁盤和 I/O,如果全都放在一個組件裏 ,調度器就會很難受:我到底要把這個節點作爲存儲節點還是計算節點?其實在這塊,可以讓調度器根據不同的機型(來做決定),是計算型機型就放計算節點,是存儲型機型就放存儲節點。

7. Everything is Pluggable

image

圖 20 Everything is Pluggable

今天由於時間關係沒有給大家演示的插件平臺。未來 TiDB 會變成一個更加靈活的框架,像圖 20 中 TiFlash 是一個 local storage,我們其實也在祕密研發一個新的存儲的項目叫 Unitstore,可能明年的 DevCon 就能看到它的 Demo 了。在計算方面,每一層我們未來都會去對外暴露一個非常抽象的接口,能夠去 leverage 不同的系統的好處。今年我其實很喜歡的一篇 Paper 是 F1 Query 這篇論文,基本表述了我對一個大規模的分佈式系統的期待,架構的切分非常漂亮。

8. Distributed Transaction

image

圖 21 Distributed Transaction(1/2)

說到分佈式事務,我也分享一下我的觀點。目前看上去,ACID 事務肯定是必要的。我們仍然還沒有太多更好的辦法,除了 Google 在這塊用了原子鐘,Truetime 非常牛,我們也在研究各種新型的時鐘的技術,但是要把它推廣到整個開源社區也不太可能。當然,時間戳,不管是用硬件還是軟件分配,仍然是我們現在能擁有最好的東西, 因爲如果要擺脫中心事務管理器,時間戳還是很重要的。所以在這方面的挑戰就會變成:怎麼去減少兩階段提交帶來的網絡的 round-trips?或者如果有一個時鐘的 PD 服務,怎麼能儘可能的少去拿時間戳?

image

圖 22 Distributed Transaction(2/2)

我們在這方面的理論上有一些突破,我們把 Percolator 模型做了一些優化,能夠在數學上證明,可以少拿一次時鐘。雖然我們目前還沒有在 TiDB 裏去實現,但是我們已經把數學證明的過程已經開源出來了,我們用了 TLA+ 這個數學工具去做了證明(https://github.com/pingcap/tla-plus/blob/master/OptimizedCommitTS/OptimizedCommitTS.tla)。 此外在 PD 方面,我們也在思考是不是所有的事務都必須跑到 PD 去拿時間戳?其實也不一定,我們在這上面也已有一些想法和探索,但是現在還沒有成型,這個不劇透了。另外我覺得還有一個非常重要的東西,就是 Follower Read。很多場景讀多寫少,讀的業務壓力很多時候是要比寫大很多的,Follower Read 能夠幫我們線性擴展讀的性能,而且在我們的模型上,因爲沒有時間戳 ,所以能夠在一些特定情況下保證不會去犧牲一致性。

9. Cloud-Native Architecture

image

圖 23 Cloud-Native

另外一點就是 Cloud-Native。剛剛中午有一個社區小夥伴問我,你們爲什麼不把多租戶做在 TiDB 的系統內部?我想說「數據庫就是數據庫」,它並不是一個操作系統,不是一個容器管理平臺。我們更喜歡模塊和結構化更清晰的一個做事方式。而且 Kubernetes 在這塊已經做的足夠好了 ,我相信未來 K8s 會變成集羣的新操作系統,會變成一個 Linux。比如說如果你單機時代做一個數據庫,你會在你的數據庫裏面內置一個操作系統嗎?肯定不會。所以這個模塊抽象的邊界,在這塊我還是比較相信 K8s 的。《Large-scale cluster management at Google with Borg》這篇論文裏面提到了一句話,BigTable 其實也跑在 Borg 上。

image

圖 24 TiDB 社區小夥伴的願望列表

當然最後,大家聽完這一堆東西以後,回頭看我們社區小夥伴們的願望列表(圖 24),就會發現對一下 TiDB 好像還都能對得上 。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章