天天看點

資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

作者:阿裡雲瑤池資料庫

文 / 阿裡雲瑤池資料庫PolarDB MySQL團隊:胡慶達、張海平、袁理祥

DDL是資料庫所有SQL操作中最繁重的一種,本文總結介紹了雲原生資料庫PolarDB中DDL全鍊路MDL鎖治理的經驗和進展,持續優化使用者的使用體驗,為使用者打造最佳的雲原生資料庫。

1. 概述

在日常資料庫操作中,使用者總是談DDL色變,原因在于總是擔心DDL的執行會影響業務SQL,這裡面最核心的因素在于DDL持有的MDL表鎖導緻的鎖堵塞問題。另一方面,由于DDL類型衆多,使用者難以區分不同類型DDL的鎖行為,無法判斷執行DDL可能導緻的後果,這進一步加劇了該問題的複雜度。通過多年大量線上執行個體的經驗積累, 我們非常了解使用者在面對這類MDL鎖問題時的困惑。

本文整理總結了雲原生資料庫PolarDB MySQL核心團隊在全鍊路MDL鎖治理方面的經驗和進展,鞭策我們為“DDL無鎖”、為使用者可以毫無擔憂地執行DDL而持續努力。針對MDL鎖的背景知識,我們有持續的核心月報在介紹相關原理,感興趣的讀者可以自行查詢《常用SQL語句的MDL加鎖及源碼分析》[1]和《MDL鎖實作分析》[2]。在開始全文前,我們首先回顧使用者主要關注哪些方面的DDL鎖問題:

1.什麼時候拿鎖

很不幸的是,無論是MySQL核心原生的DDL,還是各種第三方插件(gh-ost、pt-osc,以及雲廠商們的“無鎖變更”),幾乎所有的DDL都會申請表級别的MDL互斥鎖。這裡的核心原因在于:DDL的目标是表結構/表定義變更,它必然會修改中繼資料/字典資訊,是以DDL依賴MDL鎖來完成元資訊、檔案操作和相應緩存資訊的正确更新。當DDL修改中繼資料時,它申請表級别的MDL互斥鎖,進而堵塞并發的中繼資料查詢/修改操作,繼而可以線程安全地更新元資訊緩存,進而保證所有線程用正确版本的中繼資料解析對應版本的表資料。

說到這裡,很多熟悉MySQL的讀者一定會問,那為什麼gh-ost等第三方插件在做DDL時似乎呈現出一種類似“無鎖”的表現呢?其實這裡的核心差别在于,MySQL核心和第三方插件,在處理“拿不到鎖”這個問題時采用了完全不一樣的政策。

2.拿不到鎖會導緻什麼問題(雪崩vs饑餓,本文關注的核心問題)

相比于第三方插件,MySQL核心的MDL拿鎖機制簡單粗暴:當DDL申請MDL-X(互斥鎖)時,如果目标表存在未送出的長事務或大查詢,DDL将持續等待擷取MDL-X鎖。由于MDL-X鎖具有最高的優先級,DDL在等待MDL-X鎖的過程中将阻塞目标表上所有的新事務,這将導緻業務連接配接的堆積和阻塞,繼而可能帶來整個業務系統「雪崩」的嚴重後果。

為了避免這個問題,MySQL社群開發了很多外部工具,比如pt-osc和github的gh-ost。它們均采用拷表方式實作,即建立一個空的新表,通過select + insert的方式拷貝存量資料,然後通過觸發器或者Binlog的方式拷貝增量資料,最後通過rename操作切換新表和舊表。雲廠商的各種工具,例如DMS的無鎖變更也與這些外部工具原理類似。但很遺憾,這種方式也存在明顯的劣勢:

1. 可能由于大事務/大查詢的存在,DDL持續拿不到鎖,持續等待直到反複失敗(「饑餓」);

2. 不管是Instant DDL(例如秒級加列),還是僅增加二級索引,第三方工具都無腦選擇了全表重建的方式,通過大幅犧牲性能來追求穩定性。我們之前的測試表明(月報連結[3]),相比于核心原生的DDL執行方式(INSTANT / INPLACE / COPY),gh-ost有着10倍甚至幾個數量級的性能下降,這在資料量快速增長的今天是完全無法忍受的。

資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

不管是第三方插件,還是MySQL核心,很遺憾,任何一種方式都不能在所有場景裡都達到最優。PolarDB MySQL核心團隊嘗試在保留最佳性能的前提下,同時解決雪崩和饑餓這兩個問題。

3.拿到鎖又會導緻什麼問題(持有鎖的時間,Fast DDL将在後續文章中介紹)

在解決了「拿不到鎖」的問題後,我們同樣要解決「拿到鎖後」會有什麼問題,即如果互斥鎖持有時間過久,同樣會導緻業務堆積雪崩等問題。

熟悉MySQL的使用者都知道,MySQL有三種DDL類型,分别是「INSTANT DDL」、「INPLACE DDL」和「COPY DDL」。其中,Online DDL(使用者常說的“非鎖表”DDL,包括INSTANT DDL和絕大多數INPLACE DDL)在執行DDL期間絕大多數時刻并不鎖表,隻在修改中繼資料時短暫持有表的MDL-X鎖(持有時間一般秒級),使用者體驗良好。

目前的MySQL 8.0已經實作了常見高頻DDL的Online能力,例如增加索引、秒級加列,加減主鍵等等。但是,因為涉及一些SQL層的操作,目前依然存在COPY類型的DDL,它在執行DDL期間「全程鎖表」(隻能讀不能寫),例如修改表的字元集、修改列類型等操作。針對這類COPY DDL,PolarDB MySQL的解決方案是擴充Online DDL(不鎖表)的範圍,例如支援Instant Modify Column(秒級修改列類型),例如嘗試在SQL層支援所有DDL的Online能力,我們将這類能力統稱為「Fast DDL」,筆者後續會統一介紹這方面的工作,本文不再贅述。

相比于MySQL,PolarDB的叢集架構使得這一問題變得更加複雜:MDL鎖不僅要關注單個節點,更要關注叢集多個節點/叢集同步鍊路上的鎖問題,需要叢集次元的全鍊路解決方案。熟悉MySQL的使用者,對基于Binlog的MySQL主備叢集一定非常熟悉。在依賴Binlog的MySQL主備複制叢集上,主備節點是邏輯隔離的。也就是說,主節點的MDL鎖行為,并不會對備節點的MDL鎖有任何影響,是以MySQL隻需要考慮單個節點的MDL鎖問題。然而,PolarDB MySQL是基于共享存儲的架構。以一寫多讀叢集為例,寫節點和多個隻讀節點共享同一個分布式存儲,依賴實體複制完成不同節點之間的資料同步。寫節點在做DDL操作時,多個隻讀節點都會看到DDL過程中的實時資料。是以,PolarDB的MDL表鎖,是一個叢集次元的分布式鎖,需要考慮多節點上的鎖堵塞問題。

資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

基于PolarDB的架構特征,結合多年線上運維經驗,我們認為從叢集次元看,要實作使用者體驗良好的DDL鎖機制,需要達到以下幾個目标:

1. 解決雪崩問題。不管是RW寫節點上的大事務/大查詢,還是隻讀節點叢集上任何一個節點的大事務/大查詢,抑或是RW->RO實體複制鍊路上任何可能的堵塞點,都可能導緻DDL拿不到鎖,進而觸發業務雪崩。針對這類問題,PolarDB MySQL在去年釋出了Non-Block DDL功能(使用者文檔[4],月報連結[3]),可以保證即使在無法獲得MDL-X鎖的情況下,依然允許新事務通路目标表,進而保證整個業務系統的穩定。該功能受到了很多客戶的歡迎,多個客戶認為這個功能是執行DDL的剛需能力;

2. 解決饑餓問題。Non-Block DDL在拿不到鎖時,通過Retry等方式避免DML的堆積和雪崩。然而如果存在大事務或者大查詢,DDL可能一直拿不到鎖而持續失敗。進一步的,随着PolarDB MySQL的大客戶越來越多,單執行個體不乏10+個隻讀節點的使用者,這大大增加了叢集次元出現大查詢/大事務的機率,導緻DDL拿不到鎖。針對這類問題,PolarDB MySQL最近推出了Preemptive DDL能力(使用者文檔[5]),即賦予DDL最高的MDL鎖權限,在滿足條件的情況下主動kill堵塞它的事務/查詢,保證DDL的順利執行;

3. 解決表「資料變更」、「元資訊/元資訊緩存變更」和「檔案操作」 這三者之間的資料一緻性和實時性問題。衆所周知,TP資料庫對事務的要求極高,而DDL過程中涉及的資料變更、表結構變更和檔案操作這三者之間需要在任何一個時間點都要滿足Consistency的要求。而在基于共享存儲的PolarDB MySQL中,這一問題變得更加複雜:不僅在所有階段(正常資料同步、資料庫Recovery、按時間點還原等等)需要滿足多節點在資料變更、表結構變更和檔案操作這三者的一緻性要求,而且需要保證良好的性能,滿足強實時性的要求。針對這類問題,PolarDB MySQL做了一系列的優化,由于這部分内容要求的資料庫背景和對代碼的了解要求過高,并且使用者業務無需感覺,本文不展開介紹這一部分的工作;

4. 解決DDL過程中RW->RO實體複制鍊路的堵塞問題。上線五年以來,PolarDB MySQL支援了大量行業,不同行業的業務場景對DDL的要求是不同,具體表現在:

  • 高頻DDL導緻的高性能MDL鎖需求,例如SaaS等行業場景,DDL是個非常常見和高頻的操作。PolarDB需要避免分布式MDL鎖和實體複制的耦合性,避免因為鎖堵塞等行為影響整個叢集的資料同步;
  • DDL伴随高負載的業務壓力,例如在大壓力場景下加索引。這種場景會産生大量的redo日志,PolarDB需要保證DDL過程下實體複制鍊路的穩定性、低延遲。

針對上述問題,PolarDB MySQL在實體複制全鍊路做了優化(使用者文檔[6]),采用了異步線程池和回報機制,解耦了MDL鎖同步和實體複制的強耦合性,并優化了DDL過程中redo日志的同步&複制速度(使用者文檔[7]),滿足了大壓力DDL場景下的同步要求;

5. 持續演進的能力:DDL & DML MVCC。如前文所述,在極限情況下,使用者依然需要手動執行Preemptive DDL來解決饑餓問題。我們一直在想,有沒有更理想的方式,使用者可以完全無感覺MDL鎖的存在。熟悉InnoDB的讀者一定知道,InnoDB提供了行級别的MVCC能力,即使修改某行資料的事務沒有送出,這時候另一個事務查詢同一行資料時,事務根據它的時間戳,通過undo list建構出對應的版本,無需等待鎖的釋放。

細心的讀者一定會問,為什麼DDL沒有提供DDL和DML互不堵塞這種MVCC的能力?原因在于,DDL操作涉及了檔案操作/表資料/元資訊/表結構緩存等多種資訊的變更,是以為了達到DDL & DML的MVCC能力,涉及大量的子產品/代碼修改,帶來的代碼切口過大,穩定性風險較高。但是為了滿足客戶的訴求,PolarDB核心團隊一直在這條路徑上試圖找到工程上的最優路徑。在PolarDB 8.0.2的下個版本中,我們将提供給使用者這一實驗室功能,即滿足Instant Add Column這種高頻DDL與DML的「MVCC」能力,後續我們會陸續支援Add Index等高頻DDL與DML的MVCC能力。

2. Non-Block DDL(雪崩問題)

2.1 功能概述

如前文所述,非阻塞DDL(使用者文檔[4],月報連結[3])用于解決因MDL鎖堵塞而導緻的業務雪崩問題。非阻塞DDL功能采用了和第三方插件(gh-ost、pt-osc)類似的拿鎖邏輯:當DDL操作擷取MDL鎖失敗時,拿鎖線程會進入短暫的Sleep階段,接着重新嘗試擷取MDL鎖。通過此種方式,非阻塞DDL保證了DDL執行過程中,業務真正的online。非阻塞DDL目前已經灰階一段時間,受到大量使用者的歡迎,後面會嘗試預設開啟此功能。此外,我們将在8.0.2的2.2.15版本中,支援叢集次元的Non-Block DDL:如果主節點已經擷取MDL鎖,但是隻讀節點同步MDL鎖堵塞(目前預設堵塞時間為50s,由參數loose_replica_lock_wait_timeout控制),Non-Block DDL會在叢集次元重試拿鎖的操作,進而實作叢集次元的非阻塞DDL。

2.2 測試效果

可以通過設定參數loose_polar_nonblock_ddl_mode為ON來打開非阻塞DDL功能(使用者文檔[4]),下面給出使用sysbench模拟使用者業務,對比開啟Non-Block DDL功能和使用原生DDL功能對業務的影響。

1.在目标表sbtest1上開啟一個事務但不送出,該事務将持有目标表sbtest1的MDL鎖。

begin;
select * from sbtest1;           

2. 在新會話中,分别在開啟和關閉Non-Block DDL情況下,對表sbtest1進行加列操作,觀察TPS的變化情況。

# 由于目前session 1大查詢持有MDL鎖,目前DDL無法擷取MDL鎖,被堵塞
alter table sbtest1 add column d int;           

▶︎ 關閉Non-Block DDL功能

TPS持續跌零,預設逾時時間為31536000,嚴重影響使用者業務。

資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

▶︎ 開啟Non-Block DDL功能

TPS周期性下降,但未跌零。對使用者業務影響較小,能保證業務系統的穩定。

資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

3. Preemptive DDL (饑餓問題)

3.1 功能概述

上文非阻塞DDL解決了DDL擷取MDL鎖阻塞導緻的業務雪崩問題,但是如果DDL遲遲無法擷取MDL鎖,會導緻DDL執行頻繁失敗。目前線上值班偶爾會遇到由于RO上面存在大查詢、長事務導緻的DDL執行失敗問題,并傳回錯誤ERROR 8007 (HY000): Fail to get MDL on replica during DDL synchronize。由于此報錯與PolarDB共享存儲的架構相關,與傳統MySQL不一緻,使用者經常會一頭霧水,無從下手。

目前已有官方文檔(執行DDL操作提示“擷取不到MDL鎖”[8])介紹這類問題的解決方案,使用者可以根據此文檔找到隻讀節點上持有表MDL鎖的事務,手動進行Kill,來保證DDL同步MDL鎖的成功。但是這種方式在部分場景下依然非常晦澀,一方面使用者進行kill操作的時間視窗有限(目前同步MDL鎖逾時時間為50秒,可通過loose_replica_lock_wait_timeout進行調整),另一方面随着PolarDB上面客戶不斷增多,出現了許多10+個隻讀節點的叢集,手動kill操作顯得狼狽且低效,為此我們提供了搶占式DDL功能。

當隻讀節點通過實體複制,解析到目前表上有DDL操作時,隻讀節點會嘗試擷取表的MDL鎖。如果此時表上存在大查詢或長事務時,開啟Preemptive DDL後(使用者文檔[5]),如果隻讀節點在預期時間内無法獲得MDL鎖,便會嘗試kill掉占有MDL鎖的線程,進而保證MDL鎖同步的成功,解決DDL的饑餓問題。

3.2 測試效果

可以通過設定參數loose_polar_support_mdl_sync_preemption為ON來打開搶占式DDL功能。下面給出DDL同步MDL鎖被隻讀節點長事務堵塞時,開啟和關閉搶占式DDL的實驗效果。

▶︎ 關閉搶占式DDL功能

1. 在隻讀節點上查詢test.t1:

mysql> use test
Database changed
#大查詢,執行100s
mysql> select sleep(100) from t1;           

2.在主節點進行加列操作,被block,執行失敗:

mysql > alter table t1 add column c int;
ERROR 8007 (HY000): Fail to get MDL on replica during DDL synchronize           
資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

由于隻讀節點存在大查詢,同步MDL鎖失敗,DDL執行失敗,并復原。

▶︎ 開啟搶占式DDL功能

1.在隻讀節點上查詢test.t1:

mysql> use test
Database changed
#大查詢,執行100s
mysql> select sleep(100) from t1;           

2.在主節點進行加列操作操作,被block,等待一段時間,發生搶占,執行成功:

mysql> alter table t1 add column c int;
Query OK, 0 rows affected (11.13 sec)
Records: 0  Duplicates: 0  Warnings: 0           
資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

開啟搶占式DDL功能後,加列操作完成,同時可以看到隻讀節點(右圖),大查詢連接配接已經斷開。

4. 多版本DD:DDL & DML的MVCC能力

4.1 功能概述

不管是Non-Block DDL還是Preemptive DDL,都是在有互斥鎖的場景下,盡可能最優地滿足使用者的DDL變更需求。然而,使用者在部分場景下依然要感覺MDL鎖的存在,例如在極限場景下,使用者依然需要手動觸發Preemptive DDL,來解決DDL饑餓的問題。我們一直在探索,是否可以實作DDL與DML更細粒度的并發控制,類似于InnoDB MVCC能力。

然而,如前文所述,DDL是個複雜操作,其執行過程涉及檔案操作/表資料變更/元資訊變更/表緩存處理等一系列流程。是以,考慮到MySQL代碼的強耦合性,我們對這一目标做了切分,在控制代碼切口和穩定性風險的情況下,逐漸支援這一能力。在第一階段,我們優先支援線上高頻DDL與DML的MVCC能力,即按照statement次元,滿足Instant Add Column與DML的MVCC能力(使用者文檔待新版本8.0.2上線)。為了相容MySQL的預設表現,我們不僅支援DDL和未送出事務的并發,而且支援DD的readview,使得跨越了DDL的DML事務可以選擇以RC或者RR的隔離級别讀取表結構資訊,進而讓使用者自行決定使用新或者舊的表定義。

4.2 測試效果

具體的效果如下:

步驟一:開啟會話A,建立一個新的表t1并插入一些資料;随後開啟一個新事務,在事務中進行資料的插入和更新操作,但事務不送出:

資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

步驟二(DDL不會被未送出的事務所堵塞):開啟一個新的會話B,查詢performance_schema,此時t1的MDL正被會話A中未送出的事務持有。進行DDL操作(add column),該操作可以立即完成,而不會被未送出的事務阻塞。

資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

步驟三(跨DDL的事務可以選擇通路表時使用的隔離級别):回到第一個會話A,将表通路的隔離級别參數table_def_isolation設定REPEATABLE-READ,因為DDL的執行在該事務之後,是以新增的列c不可見,該事務将始終看到與事務開始時一緻的表定義。

資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

将table_def_isolation設定為READ-COMMITTED,因為DDL已經送出,列c将對該事務可見。

資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

送出事務後,DD的readview随之釋放,随後将隻能看到最新的表結構。

資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

5.全鍊路優化的分布式MDL鎖(多節點資料同步問題)

目前的雲原生資料庫,不論是PolarDB,或者其它廠商資料庫,都以“存算分離”+“共享存儲”的形态提供一寫多讀的能力。對這類架構感興趣的讀者,可以閱讀我們之前的相關月報(PolarDB 實體複制解讀[9],PolarDB 實體複制熱點頁優化[10])。對這類針對存算分離場景下IO優化感興趣的讀者,可以閱讀我們去年發表在VLDB上的相關論文(CloudJump: Optimizing Cloud Databases for Cloud Storages[11])。

簡單來說,雲原生資料庫依賴實體複制(Redo日志)完成不同節點之間的資料同步,而DDL觸發的中繼資料/表資料/檔案變更同樣随着實體複制完成多節點的同步,這三者之間依賴分布式MDL鎖提供實時&一緻性的保證。然而,當MDL鎖和實體複制相耦合時,會産生一系列的問題,尤其是日志流 / 鎖同步 / 檔案操作這三者之間的一緻性問題。這裡,我們介紹與使用者密切相關的兩類問題:

資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

5.1 異步中繼資料鎖同步

高頻DDL場景下分布式MDL鎖的穩定性&實時性。尤其是在MDL鎖被堵塞時,不能影響正常實體日志的進行。為了解決這個問題,PolarDB設計了全新的分布式MDL鎖機制(使用者文檔[6],已預設開啟),主要展現在以下兩個方面:

  • 異步MDL鎖複制:将分布式MDL鎖與實體複制互相解耦,實作了即使在等待MDL鎖時,隻讀節點仍能繼續解析并應用實體日志,保證了實體複制的實時性;
  • 并行MDL鎖:為了優化高頻DDL場景下分布式MDL鎖的性能,我們采用一組線程池來并發響應MDL鎖的需求。即使某個MDL鎖被堵塞,也不會影響其它線程去擷取MDL鎖,并且這部分線程池會随着DDL的情況動态調整,保證了MDL鎖同步的高并發。

5.2 DDL實體複制優化

高壓力DDL場景下實體複制的穩定性&實時性。PolarDB中的資料是通過B-Tree來維護索引的,然而大部分Slow DDL操作(如增加主鍵或二級索引、Optimize Table等)往往需要重建或新增B-Tree索引,導緻大量實體日志的産生。而針對實體日志進行的操作往往出現在DDL執行的關鍵路徑上,增加了DDL操作的執行時間。此外,實體複制技術要求隻讀節點解析和應用這些新生成的實體日志,DDL操作而産生的大量實體日志可能嚴重影響隻讀節點的日志同步程序,甚至導緻隻讀節點不可用等問題。針對上述問題,PolarDB提供了DDL實體複制優化功能(使用者文檔[7],已預設開啟),主要展現在以下兩個方面:

  • 主節點加快DDL寫日志速度:在主節點寫實體日志和隻讀節點應用實體日志的關鍵路徑上做了全面的優化,使得主節點在執行建立主鍵DDL操作的執行時間最多可減少20.6%;
  • 隻讀節點加快實體複制速度:隻讀節點解析DDL的複制延遲時間最多約可減少至原來的0.4%,并且明顯降低了CPU / Memory / IO的硬體開銷。以下面測試資料為例,在主節點上不論執行1個DDL還是8個DDL,隻讀節點非常穩定,沒有明顯抖動。
資料庫核心那些事|深度解析PolarDB DDL鎖的優化和演進

6. 總結

DDL是PolarDB所有SQL操作中最繁重的一種,DDL的易用性是PolarDB良好使用體驗非常重要的一環。本文總結介紹了PolarDB在全鍊路MDL鎖治理的經驗和進展,把簡單留給客戶,把複雜留給自己,持續優化使用者的使用體驗。後續将總結介紹PolarDB在Fast DDL方面的工作,PolarDB核心團隊将始終如一地為使用者打造最佳的雲原生資料庫。

參考

[1] 常用SQL語句的MDL加鎖及源碼分析: http://mysql.taobao.org/monthly/2018/02/01/

[2] MDL鎖實作分析: http://mysql.taobao.org/monthly/2015/11/04/

[3] 非阻塞DDL月報介紹 : http://mysql.taobao.org/monthly/2022/10/01/

[4] 非阻塞DDL使用者文檔 : https://help.aliyun.com/document_detail/436462.html

[5] 搶占式DDL使用者文檔 : https://help.aliyun.com/document_detail/2326304.html

[6] PolarDB 并行中繼資料鎖同步 : https://help.aliyun.com/document_detail/200678.html

[7] DDL實體複制優化 : https://help.aliyun.com/document_detail/198213.html

[8] 執行DDL操作提示“擷取不到MDL鎖” : https://help.aliyun.com/document_detail/611732.html

[9] PolarDB 實體複制解讀 : http://mysql.taobao.org/monthly/2018/12/05/

[10] PolarDB 實體複制熱點頁優化 : http://mysql.taobao.org/monthly/2021/03/04/

[11] VLDB論文連結CloudJump: Optimizing Cloud Databases for Cloud Storages https://www.vldb.org/pvldb/vol15/p3432-chen.pdf

福利TIME:免費試用PolarDB

雲原生資料庫PolarDB MySQL版開通免費試用啦!

阿裡雲推出“飛天免費試用計劃”,面向國内1000萬雲上開發者,提供雲産品免費試用。雲原生資料庫PolarDB MySQL版現推出3個月【免費試用】,快來領取吧!

點選下方連結即刻開啟雲上實踐之旅!

阿裡雲免費試用 - 阿裡雲

繼續閱讀