深入了解 Docker 存儲驅動

MadSturgess 8年前發布 | 15K 次閱讀 Docker 文件系統 操作系統

如果你曾經上手用過Docker,你可能已經見過存儲驅動(storage driver)這個詞。或者你已經偶然聽過graphdriver這個詞,并心想這到底是個什么鬼?或者你聽過大牛們談論的話題中出現的aufs和devicemapper這樣的詞,你一定想知道他們都在講的是什么? 最近,我協助The New Stack編輯了他們的容器生態系列叢書(container ecosystem series)中關于存儲、網絡和安全的第四本電子書。該書有一個標題為《處理容器存儲的方式》(http://thenewstack.io/methods-dealing-container-storage/)的章節,章節開頭花了幾頁講了一些Docker中存儲驅動相關的一般性主題,但是很快地話題就轉移到了比較火熱的持久型存儲上面。介于該書中的介紹太簡短,我想用本文對它做一點補充,并嘗試著為諸君理清相關概念,同時介紹一些Docker引擎中相關的關鍵背景。

首先厘清一個事情:有很多的資源能幫你理解持久性存儲,volume API和一些插件如ClusterHQ推出的Flocker,EMC的rexray/libstorage項目,Portworx等等,它們都是在處理Docker生態中的持久性存儲。但是這邊文章僅僅關注的是當你運行docker run時,在將鏡像分層組裝進根文件系統過程中,目前有哪些選擇可以做本地容器鏡像存儲。

在過去已經有一些圍繞這個主題的文章。Red Hat幾年前發表了“graphdriver實現概概覽”。在2015年,Jér?me Petazzoni做了一個關于Docker中各種graphdriver的歷史和實現的演講。今年早些時候,Jess Frazelle發表了她“特別實誠的graphdriver指南”,提供了當時所有選項一個很好而簡短的介紹。但是從那時起已經有一些事情發生了變化:Docker 1.12中推出了一個新的驅動叫做“overlay2”,較之最初的overlay的實現有重大的提升,并且最近一些graphdriver的選項中增加了一些關于配額的支持。

背景介紹就這么多,我想有必要關注并更加深入了解以下話題:

  1. 為什么Docker中存在graphdriver,它的角色是什么?

  2. 為什么一種graphdriver不夠用,要有這么多種選擇?

  3. 這么多種類的graphdriver,我該怎么選?

Graphdriver是何物?

要開始理解graphdriver這個概念,我們首先得理解本地的Docker引擎有一個Docker鏡像層的緩存。 層疊鏡像模型(layered image model)是Docker引擎最具特色的特性之一。 它能允許一個或者多個容器進程共享文件系統內容。這些分層的緩存會在 docker pull 或者 docker build 命令執行的時候,顯式地進行構建。當 docker run 運行的時候,因為分層在本地不存在需要從注冊表中拉取的時候,也會向緩存中隱式地添加。要在運行的時候管理這些分層,需要一個支持一組特定能力的驅動 – 這組能力被抽象成接口可以在Docker引擎代碼里找到 - 來將這些分層掛載到一個合并的根文件系統里面。因為分層內容的“鏡像圖(image graph)”代表了各種分層之間的關系,用來處理這些分層的驅動就被叫做“圖驅動(graphdriver)”。

在運行的時候用來處理這個 分層圖 有兩個非常重要的概念。一個概念是聯合文件系統(union filesystem),它最好的定義位于維基百科詞條中。( 譯注:該維基詞條說的是,“在操作系統中,聯合掛載(union mounting)是一種將多個目錄結合成一個目錄的方式,這個目錄看起來就像包含了他們結合的內容一樣。” ) 聯合文件系統的實現將這種文件系統內容合并來,形成一個單一的掛載點。除非只讀的根文件系統已經能滿足你的需求,聯合文件系統的實現通常輔有“寫時復制(CoW)”的實現技術,這樣任何對于底層文件系統分層的更改都會被“向上拷貝”到文件系統的一個臨時、工作、或高層的分層里面。這個可寫的層然后可以被看做是一個“改動(diff)”,能將之應用到下層只讀的層,而這些層很可能作為底層被很多容器的進程中共享。這是一個很重要的點。一個Docker中使用分層文件系統的好處就是,1000個運行著 bash 的 ubuntu:latest 容器的副本,會共享一個底層的鏡像,而并不會產生1000個文件系統的副本(vfs是個例外,請參考下邊vfs部分)。并且同樣重要的是,對于aufs和overlay的實現,用來讀取或執行共享庫的共享內存也在所有運行的容器之間共享,大大的減少了通用庫如’libc’的內存占用。這是一個分層策略的巨大優勢,同時也是Docker的graphdriver是引擎中相當重要的一部分的原因之一。

現在諸君應該清楚了graphdriver是何物了,并且為什么Dokcer引擎要實現這個特性。現在讓我們接著看看為什么Docker中有眾多graphdriver的選擇吧。

都有哪些Graphdriver?

在最近的Docker引擎1.12版本中,會發現如下的graphdriver:vfs、aufs、overlay、overlay2、btrfs、zfs、devicemapper和windows。將這個列表歸一下類有助于對它們進行更好地定義。

特別的白雪公主:vfs

首先,讓我們挑出一個特別的graphdriver – vsf。vfs是接口的“原生”的實現,完全沒有使用聯合文件系統或者寫時復制技術,而是將所有的分層依次拷貝到靜態的子文件夾中,然后將最終結果掛載到容器的根文件系統。它并不適合實際或者生產環境使用,但是對于需要進行簡單驗證的場景,或者需要測試Docker引擎的其他部件的場景,是很有價值的。對于在Docker中運行Docker的場景也很有用,要知道graphdriver嵌套起來,可能會讓你丈二和尚摸不著頭腦。順便一提:Docker引擎開發者用來構建Docker自己所使用的 Dockerfile ,也是采用vfs來作為里邊Docker的graphdriver。

聯合文件系統

有意思的是,現有的graphdriver中只有少部分是真正的有寫時復制語義的聯合文件系統:Overlay的兩個版本,從Docker早期就存在的aufs。記住聯合文件系統只是一個基于文件的接口,通過把一組目錄交錯起來來,形成一個單一視圖。所以與它不是一個真正的文件系統,如ext4或者xfs,它僅僅是站在一個已有的文件系統上提供了這些功能。在一些場景,對于底層文件系統是有要求的,并且Docker也會同時檢查請求的聯合文件系統和底層的文件系統,來保證它們是兼容的。

特定文件系統的實現

剩下的graphdriver都是建立在具體文件系統實現的基礎上,需要依賴其內置特性(如快照)能提供必需的能力。這些包含devicemaper、zfs和btrfs驅動。在這每一個情形中,你都需要新建一個磁盤并用該文件系統格式化磁盤(或者為了快速測試,用循環掛載的文件作為磁盤),來使用這些選項作為Docker引擎的存儲后端。

Graphdriver必須要執行什么操作?

首先我們要簡要地解釋一下graphdriver必須執行什么操作。相關信息在已經被代碼化在了守護進程代碼庫中 ProtoDriver 和 Driver 接口的定義中。同時值得注意的是,有一個ProtoDriver接口的包裝器實現叫做NativeDiffDriver。對于那些無法通過原生處理方式來得出分層差異或改動的文件系統,該包裝器能通過借用歸檔軟件包,和驅動實現一起來提供這些計算差異的特性。 除了(計算)差別和改動相關的方法,graphdriver最重要的能力是 Get、 Put、 Create 和 Remove 方法 。要幫助理解graphdriver的API,我們需要簡單地談一下這個API的消費者。在Docker中的實現被稱作 layerStore(分層倉庫) 。當終端用戶使用Docker客戶端或者API下載或導入鏡像時,分發(注冊表)客戶端代碼用“分層倉庫”來進行添加或者刪除層的操作。我們知道鏡像可以包含多個分層,并且這些分層有存在父級子級的關系。“分層倉庫”的代碼利用graphdriver驅動,采用最適合該文件系統實現中類似聯合和寫時復制(union+CoW-like)的疊層技術,來保存這些分層和它們之間的關系。要處理這些分層鏡像的創建和解開(un-tar)操作,以及將鏡像解開的內容放到創建的位置,會用到graphdriver的 Create 和 ApplyDiff 接口。顯然,當鏡像從本地緩存刪除的時候需要執行的相反的操作,“分層倉庫”會調用graphdriver的 Remove 接口來將分層的內容從系統中刪除。

經歷了上面這些過程,graphdriver現在已經包含了很多分層的本地緩存,同時包含下載的具名鏡像之間的關系。容器需要運行時,在容器啟動之前這些必須被組裝成可運行的根文件系統。graphdriver的 Get 方法會被調用并帶上一個特定的標識符,此時根據graphdriver特定文件系統的實現,需要根據父級連接關系遍歷并且使用該文件系統提供的相應技術來將分層堆疊成一個單獨的掛載點,并創建可寫的上層或者頂部分層來滿足容器更改文件系統的需要。 Put 方法來告知graphdriver,某掛載的資源沒有用了,并在絕大多數的場景下卸載相關的層。

現在的Graphdriver概覽

知道了graphdriver在意圖解決什么問題之后,讓我們快速概覽一下在當下Docker 1.12引擎中可以有哪些選擇。對于那些將要嘗試,或者已經嘗試過不同的grpahdriver的人來說,因為每一個graphdriver的分層存儲是依賴具體實現的,當更改了graphdriver并重啟了Docker引擎后,之前拉取或者構建的任何鏡像將無法繼續使用。這是一個之前廣為人知的會讓用戶困惑的地方,但是不要害怕;切換回之前的graphdriver會喚回之前的鏡像或者容器,它們沒有消失,只是在你切換了不同的graphdrirver之后從你的視野中躲起來了而已。

AUFS

歷史:aufs驅動老早就在Docker中存在了!其實,他在使用 graphdriver 這個名字之前久存在了。如果你查看項目在那(即首次使用graphdriver名稱)提交之前的歷史,之前項目中當時只有一個aufs的實現。下邊devicemapper部分會講到更多關于graphdriver這個名稱誕生的歷史。

實現:Aufs最初代表的意思“另一個聯合文件系統(another union filesystem)”,試圖對當時已經存在的UnionFS實現進行重寫。正如你期望的那樣,它是一個傳統意義的上層覆蓋,通過利用aufs稱作為“分支(branch)”的特性,讓堆疊的目錄合并成一個堆疊內容單一掛載點視圖。此驅動會將父級信息組合一個有序列表,并把它作為掛載參數,然后把重活移交給aufs來把這些分層組裝成一個聯合視圖。更多的細節信息可以在aufs的幫助文檔(http://aufs.sourceforge.net/aufs3/man.html)上看到。

優點:這可能是歷史最久且測試最完善的graphdriver后端了。它擁有不錯的性能,也比較穩定,適用于廣泛的場景。盡管它只在Ubuntu或者Debian的內核上才可以啟用(下邊有說明),但是這兩個發行版和Docker一起使用的場景已經非常多,這讓它在廣闊的環境中得到了驗證。同時,通過讓不同的容器從同一個分層里面加載相同的庫(因為他們在磁盤上是相同的inode)達到了共享內存頁的效果。

缺點:Aufs從來沒有被上游Linux內核社區接受。多年來Ubuntu和Debian都需要往內核集成一個歷史久遠的補丁包,且原作者已經放棄了讓它被內核采納的努力。可能與IPV4和IPv6的辯論有些類似,人們擔心某一天內核更新后會出現難以整合aufs的補丁的情況,從而導致aufs沒得玩。但是就如IPv6,替換aufs勢在必行的決心講了一年又一年。除此之外,它面臨著很多其他比較棘手的問題。其中一個最麻煩的、也是比較有歷史的問題(盡管某種程度上這是一個安全的特性),是關于在高層更改向上拷貝的文件的權限的,這個問題困擾了不少用戶。最終在2015年早期的時候通過編號為#11799(http://dockone.io/docker/docker#11799)的PR使用aufs的 dirperm1 特性修復了。自然,這需要內核中有具有 dirperm1 能力aufs,然而這在今天任何較新版本的Ubuntu或者Debian上都已經不成問題了。

總結:如果你在使用Ubtuntu或者Debian,那默認的graphdriver就是aufs,它能滿足你絕大多數需求。有人期望有一天它能被overlay的實現取代,但是考慮到overlay文件系統的諸多問題,以及在上游內核中的成熟程度等挑戰,這尚未實現。最后,aufs中沒有配額的支持。

Overlay

歷史:2014年8月,Red Hat的 Alex Larsson在編號為453552c8384929d8ae04dcf1c6954435c0111da0的代碼提交中添加了針對OverlayFS(最初的上游內核的名稱)的graphdriver。

實現:Overlay是一個聯合文件系統,它的概念較之aufs的分支模型更為簡單。Overlay通過三個概念來實現它的文件系統:一個“下層目錄(lower-dir)”,一個“上層目錄(upper-dir)”,和一個做為文件系統合并視圖的“合并(merged)”目錄。受限于只有一個“下層目錄”,需要額外的工作來讓“下層目錄”遞歸嵌套(下層目錄自己又是另外一個overlay的聯合),或者按照Docker的實現,將所有位于下層的內容都硬鏈接到“下層目錄”中。正是這種可能潛在的inode爆炸式增長(因為有大量的分層和硬連接)阻礙了很多人采用Overlay。Overlay2通過利用更高內核(4.0以及以上的版本)中提供了的更優雅處理多個位于下層分層的機制解決了這個問題。

優點:Overlay作為一個合并進主線Linux內核的一個有完整支持的聯合文件系統有望成為人們的焦點。與aufs類似,通過使用磁盤上相同的共享庫,它也能讓分散的容器實現內存共享。Overlay同時有很多的上游Linux內核基于現代的應用場景,如Docker,被持續開發(參看overlay2)。

缺點:硬鏈接的實現方式已經引發了 inode耗盡(http://dockone.io/docker/docker#10613)的問題,這阻礙了它的大規模采用。inode耗盡并不是唯一的問題,還有其他一些與用戶命名空間、SELinux支持有關的問題,且整體的成熟狀況不足也阻礙著overlay直接取代aufs成為Docker默認的graphdriver。隨著很多問題的解決,特別是在最新的內核發新版中,overlay的可用度越來越高了。如今出現的Overlay2修復了inode耗盡的問題,應該是從Docker 1.12版本之后的焦點,成為overlay驅動的后續開發對象。出于向后兼容的原因, overlay 驅動將會繼續留在Docker引擎中繼續支持現有的用戶。

總結:考慮到aufs沒有足夠多的發行版的支持,能有一個上游集成的聯合文件系統且擁有Linux內核文件系統社區的支持,overlay驅動的加入是一個重大進步。Overlay在過去的18-24個月已經成熟了很多,并且隨著overlay2的出現,它之前一些麻煩的問題已經解決了。希望overlay(或者更具可能性的overlay2)會成為未來默認的graphdriver。為了overlay最好的體驗,上游內核社區在4.4.x的內核系列里面修復了很多overlay實現中存在的問題;選擇該系列中更新的版本可以獲得overlay更好的性能和穩定性。

Overlay2

歷史:Derek McGowan在編號為#22126(https://github.com/docker/docker/pull/22126)的PR中添加了overlay2的graphdriver,在2016年6月被合并進Docker 1.12版本,正如該PR的標題注明的,要取代之前overlay的主要原因是它能“支持多個下層目錄”,能解決原先驅動中inode耗盡的問題。

實現:在上面的overlay部分已經講述了Linux內核中的Overlay的框架。上面鏈接的PR中改進了原有的設計,基于Linux內核4.0和以后版本中overlay的特性,可以允許有多個下層的目錄。

優點:overlay2解決了一些因為最初驅動的設計而引發的inode耗盡和一些其他問題。Overlay2繼續保留overlay已有的優點,包括在同一個引擎的多個容器間從同一個分層中加載內庫從而達到內存共享。

缺點:現在可能唯一能挑出overlay2的問題是代碼庫還比較年輕。很多早期的問題已經在早期測試過程中發現并被及時解決了。但是Docker 1.12是第一個提供overlay2的發行版本,隨著使用量的增長,相信可能還會發現其他問題。

總結:將Linux內核中的一個現代的、廣受支持的聯合文件系統,和一個和Docker中一個性能優秀的graphdriver結合起來,這應該是Docker引擎未來打造默認的graphdriver最好的道路,只有這樣才能獲得各種Linux發行版廣泛的支持。

Btrfs

歷史:2013年12月較晚的時候,Red Hat公司的Alex Larsson在編號為e51af36a85126aca6bf6da5291eaf960fd82aa56的提交中,讓使用btrfs作為管理 /var/lib/docker 的文件系統成為可能。

實現:Btrfs的原生特性中,有兩個是“子卷(subvolumes)”和“快照(snapshots)”。 (譯注:根據Wikipedia,“子卷在btrfs中不是一個塊設備,也不應該被當做是一個塊設備。相反,子卷可以被想象成POSIX文件的命名空間。這個命名空間可以通過頂層的子卷來訪問到,也可以獨立地被掛載。快照在Btrfs中實際上是一個子卷,通過使用Btrfs的寫時復制來和其他的子卷共享數據,對快照的更改不會影響原先的子卷。” ) graphdriver實現中主要結合了這兩個能力,從而提供了堆疊和類似寫時復制的特性。當然,graphdriver的根(默認情況下是: /var/lib/docker )需要是一個被btrfs文件系統格式化的磁盤。

優點:Btrfs幾年前發布的時候(2007-2009時代),它被視作一個未來的Linux文件系統并受到了大量的關注(https://lwn.net/Articles/342892/)。如今在上游Linux內核中,該文件系統已經比較健壯,并受到良好的支持,是眾多可選的文件系統之一。

缺點:但是Btrfs并沒有成為Linux發行版的主流選擇,所以你不大可能已經有一個btrfs格式化的磁盤。因為這種在Linux發行版中采用不足的原因,它并沒有受到類似其他graphdriver一樣的關注和采用。

總結:如果你正在使用btrfs,那很顯然的這個graphdriver應該迎合了你的需求。在過去幾年有過很多Bug,并且有一段時間缺乏對SELinux的支持,但是這已經被修復了。同時,對btrfs配額的支持也直接加進了docker守護進程中,這是Zhu Guihua在編號為#19651(http://dockone.io/docker/docker#19651)的PR中添加的,這個特性包含在了Docker 1.12版本中。

Devicemapper

歷史:Devicemapper很早就以C代碼的包裝器面貌存在了,用來和libdevmapper進行交互; 是2013的9月Alex Larsson在編號為 739af0a17f6a5a9956bbc9fd1e81e4d40bff8167的代碼提交中添加的。幾個月后的重構了才誕生了我們現在所知道的“graphdriver”這個詞;Solomon Hykes在2013年10月份早期代碼合并的注釋中說:將devmapper和aufs整合進通用的“graphdriver”框架。

實現:devicemapper這個graphdriver利用了Linux中devicemapper代碼中眾多特性之一,“輕配置(thin provisioning)”,或者簡稱為“thinp”。 (譯注:根據Wikipedia,“thin provisioning是利用虛擬化技術,讓人覺得有比實際可用更多的物理資源。如果系統的資源足夠,能同時滿足所有的虛擬化的資源,那就不能叫做thin-provisioned。”)  這與之前提到的聯合文件系統不同,因為devicemapper是基于塊設備的。這些“輕配置(thin-provisioned)”的塊設備帶來的是如聯合文件系統所提供的一樣輕量的行為,但是最重要的一點是,他們不是基于文件的(而是基于塊設備的)。正如你能推測的,這讓計算分層之間的差別變得不再容易,也喪失了通過在容器間使用同樣的庫片段而共享內存的能力。

優點:Devicemapper在過去的年間也被一些人感到不屑,但是它提供的一個非常重要的能力讓紅帽系(Fedora,RHEL,Project Atomic)也有了一個graphdriver。因為它是基于塊設備而不是基于文件的,它有一些內置的能力如配額支持,而這在其他的實現中是不容易達到的。

缺點:使用devicemapper沒有辦法達到開箱立即唾手可得很好的性能。你必須遵循安裝和配置指示才能得到性能還可以的配置。并且最重要的是,在任何需要用Docke引擎來做點正事的地方,都不要使用“虛擬設備(loopback)”模式(對于運行有devicemapper且負載高的系統,如延遲刪除( deferred removal)這樣的特性絕對有必要的,這能減少引擎看起來好似夯住了一樣的悲劇。)。它的一些特性依賴libdevmaper特定的版本,并且需要比較高級的技能來驗證系統上所有的設置。同時,如果Docker Engine的二進制是靜態編譯的話,devicemapper會完全無法工作,因為它需要udev sync的支持,而這不能被靜態編譯進引擎中。

總結:對于紅帽類發行版本來說,devicemapper已經成為“可以直接用”的選擇,并且在過去幾年間里得到了紅帽團隊的大力支持和改進。它質量上有優點也有缺點,如果安裝/配置過程中沒有特別格外注意的話,可能導致和其他選項比較起來性能低下、質量不高。鑒于overlay和overlay2受到了Fedora和RHEL最新的內核的支持,并且擁有SELinux的支持,除非在Red Hat場景中有某種必須使用devicemapper的需求,我想隨著用戶的成熟他們會轉向overlay的懷抱。

Zfs

歷史:ZFS的graphdriver是由Arthur Gautier和J?rg Thalheim一起在#9411(http://dockone.io/docker/docker#9411)的PR中實現的,在2014年的5月被合并進了Docker引擎里面,并且從Docker 1.7版本開始用戶可以使用。該實現依賴Go的一個三方包go-zfs(https://github.com/mistifyio/go-zfs)進行相關zfs命令的交互。

實現:與btrfs和devicemapper類似,要使用zfs驅動必需要有一個ZFS格式化的塊設備掛載到graphdriver路徑(默認是/var/lib/docker)。同時也需要安裝好zfs工具(在絕大多數的發行版上是一個名為zfs-utils的包)供zfs Go庫調用來執行相關操作。ZFS有能力創建快照(與btrfs類似),然后以快照的克隆作為分享層的途徑(在ZFS的實現中成了一個快照)。因為ZFS不是一個基于文件的實現,aufs和overlay中所擁有的內存共享能力在ZFS是沒有的。

優點:ZFS正在受到越來越多的歡迎,在Ubuntu 16.04中,在Ubuntu的LXC/LXD中已經被使用。最初由Sun創建,ZFS已經存在很長的時間了,并且在Solaris和很多BSD的衍生版中使用,并且它的Linux移植版實現看起來也比較穩定,對于容器文件系統的場景也有足夠合理性能。 ZFS graphdriver也很及時的在Dockr 1.12中通過PR #21946(http://dockone.io/docker/docker#21946)添加了配額的支持,這讓它在配額支持方面和btrfs、devicemapper站在了同一起跑線上。

缺點:除了沒有基于文件(inode)的共享達到內庫共享之外,很難說ZFS和其它同樣基于塊設備的實現相比有什么缺點。通過比較,ZFS看起來歡迎程度越來越高。對于那些完全支持或者正在使用ZFS的Linux發行版或者UNIX衍生版而言,zfs graphdriver可以是一個非常好的選擇。

總結:ZFS的支持為Docker引擎中穩定的graphdriver加了分。對于那些ZFS的使用者,或者那些ZFS扮演了更要角色的發行版來說,Docker能直接支持該文件系統,對這些社區來說是一個好消息。對于那些默認文件系統是ext4和xfs的發行版,默認采用overlay驅動的用戶來說,時間會告訴我們他們是否會對zfs驅動產生更多的興趣。

更深層次的細節

要真的需要深挖每一個文件系統如何通過graphdriver來運作的需要更多的篇幅。更重要的是,Docker社區已經將一部分寫成了文檔,可以在官方的存儲驅動文檔查看。如果對任何graphdriver的細節有不清楚的,可以點過去看一下。這里是官方文檔中有關每個graphdriver的鏈接:aufs、devicemapper、overlay、zfs和btrfs。

細心的讀者會發現,我在開頭提到了“windows”的graphdriver,但是之后再也沒有提到。很顯然windows graphdriver是最近Docker向Windows Sever 2016移植中使用的graphdriver,這個消息是這周在Atlanta的MS Ignite宣布的。我本人沒有足夠多的細節,但希望以后我們能寫一篇相關的文章或者鏈接到微軟團隊講述該驅動是如何在Windows上運作的。

 

 

來自:https://mp.weixin.qq.com/s?__biz=MzA5OTAyNzQ2OA==&mid=2649692382&idx=1&sn=a880d1220eefaa6d9a4ce39590af4e1d&chksm=889327bdbfe4aeab16e7fc1984d5e2ae72c9538daa0d0c54adcab16e1ea48c9374630edb609e&mpshare=1&scene=1&srcid=1101He3tp0eYRzE9OX75cUTw&pass_ticket=Ftr/CG7ZOlqH4VDW8cvl/MCaMrXKWNz3Jx94eDvx0 M=

 

 本文由用戶 MadSturgess 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!