20.Kubernetes共享存儲

Kubermetes對於有狀態的容器應用或者對數據須要持久化的應用,不只須要將容器內的目錄掛載到宿主機的目錄或者emptyDir臨時存儲卷,並且須要更加可靠的存儲來保存應用產生的重要數據,以便容器應用在重建以後,仍然可使用以前的數據。不過,存儲資源和計算資源(CPU/內存)的管理方式徹底不一樣。爲了可以屏蔽底層存儲實現的細節,讓用戶方便使用,同時能讓管理員方便管理, Kubernetes從v1.0版本就引入PersistentVolumePersistentVolumeClaim兩個資源對象來實現對存儲的管理子系統。html

PersistentVolume (PV)是對底層網絡共享存儲的抽象,將共享存儲定義爲一種「資源」,好比節點(Node) 也是一種容 器應用能夠「消費」的資源。PV由管理員進行建立和配置,它與共享存儲的具體實現直接相關,例如GlusterFS、iSCSI、 RBD或GCB/AWS公有云提供的共享存儲,經過插件式的機制完成與共享存儲的對接,以供應用訪問和使用。node

PersistentVolumeClaim (PVC)則是用戶對於存儲資源的一個「申請」。就像Pod「消費」Node的資源一-樣, PVC會「消費」PV資源。PVC 能夠申請特定的存儲空間和訪問模式。nginx

StorageClass,使用PVC「申請」到必定的存儲空間仍然不足以知足應用對於存儲設備的各類需求。一般應用程序都會對存儲設備的特性和性能有不一樣的要求,包括讀寫速度、併發性能、數據冗餘等更高的要求,Kubernetes 從v1.4版本開始引入了-一個新的資源對象StorageClass,用於標記存儲資源的特性和性能。到v1.6版本時,StorageClass動態資源供應的機制獲得了完善,實現了存儲卷的按需建立,在共享存儲的自動化管理進程中實現了重要的一步。git

經過StorageClass的定義,管理員能夠將存儲資源定義爲某種類別(Class), 正如存儲設備對於自身的配置描述(Profile),例如「快速存儲」「慢速存儲」「有數據冗餘」「無數據冗餘」等。用戶根據StorageClas的描述就可以直觀得知各類存儲資源的特性,就能夠根據應用對存儲資源的需求去申請存儲資源了。github

下面對Kubermetes的PVPVCStorageClass動態資源供應等共享存儲管理機制進行詳細說明。web

PV 詳解

PV做爲存儲資源,主要包括存儲能力、訪問模式、存儲類型、回收策略、後端存儲類型等關鍵信息的設置。下面的例子聲明的PV具備以下屬性: 5Gi 存儲空間,訪問模式爲「ReadWriteOnce」,存儲類型爲「slow" (要求系統中已存在名爲slow的StorageClass),回收策略爲「Recycle",而且後端存儲類型爲「nfs」(設置了NFS Server的IP地址和路徑):json

apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: pv1
  spec:
    capacity:
      storage: 5Gi
    accessModes:
      - ReadWriteOnce persistentVolumeReclaimPolicy: Recycle storageClassName: slow nfs: path: /tmp server: 172.17.0.2 

Kubernetes支持的PV類型以下。後端

  • gcePersistentDisk: GCE公有云提供的PersistentDisk。
  • AWSElasticBlockStore: AWS公有云提供的ElasticBlockStore.
  • AzureFile: Azure公有云提供的File。
  • AzureDisk: Azure 公有云提供的Disk。
  • FC ( Fibre Channel)。
  • Flocker。
  • NFS:網絡文件系統。
  • iSCSI。
  • RBD (Rados Block Device); Ceph塊存儲。
  • CephFS。
  • Cinder: OpenStack Cinder塊存儲。
  • GlusterFS。
  • VsphereVolume.
  • Quobyte Volumes。
  • VMware Photon。
  • Portworx Volumes。
  • ScaleIO Volumes。
  • HostPath:宿主機目錄,僅用於單機測試。

每種存儲類型都有各自的特色,在使用時須要根據它們各自的參數進行設置。centos

一、Capacity(容量)

描述存儲設備具有的能力,目前僅支持對存儲空間的設置( storage= =xx),將來可能加入api

二、訪問模式(Access Modes)

  • ReadWriteOnce (簡寫爲 RWO): 讀寫權限,而且只能被單個Node掛載。
  • ReadOnlyMany (簡寫爲 ROX): 只讀權限,容許被多個Node掛載。
  • ReadWriteMany (簡寫爲 RWX): 讀寫權限,容許被多個Node掛載。

注意:即便volume支持不少種訪問模式,但它同時只能使用一種訪問模式。好比,GCEPersistentDisk能夠被單個節點映射爲ReadWriteOnce,或者多個節點映射爲ReadOnlyMany,但不能同時使用這兩種方式來映射。

Volume Plugin ReadWriteOnce ReadOnlyMany ReadWriteMany
AWSElasticBlockStore - -
AzureFile
AzureDisk - -
CephFS
Cinder - -
FC -
FlexVolume -
Flocker - -
GCEPersistentDisk -
Glusterfs
HostPath - -
iSCSI -
PhotonPersistentDisk - -
Quobyte
NFS
RBD -
VsphereVolume - -
PortworxVolume -
ScaleIO -

三、存儲類別(Class)

PV能夠設定其存儲的類別(Class),經過storageClassName參數指定一個StorageClass資源對象的名稱。具備特定「類別」的PV只能與請求該「類別」的PVC進行綁定。未設定「類別」的PV則只能與不請求任何「類別」的PVC進行綁定。

四、回收策略(Reclaim Policy)

目前支持以下三種回收策略。

  • 保留(Retain): 保留數據,須要手工處理。
  • 回收空間( Recycle):簡單清除文件的操做(例如執行m -rf /thevolume/*命令)。
  • 刪除(Delete):與PV相連的後端存儲完成volume的刪除操做(如AWS EBS、GCE PD、Azure Disk、OpenStack Cinder等設備的內部volume清理)。

目前,只有NFS和HostPath兩種類型的存儲支持「Recycle」策略; AWS EBS、GCE PD、Azure Disk和Cinder volumes支持「Delete」策略。

2. PV生命週期的各個階段( Phase )

某個PV在生命週期中,可能處於如下4個階段之一。

  • Available: 可用狀態,還未與某個PVC綁定。
  • Bound: 已與某個PVC綁定。
  • Released: 綁定的PVC已經刪除,資源已釋放,但沒有被集羣回收。
  • Failed: 自動資源回收失敗。

3. PV的掛載參數( Mount Options )

在將PV掛載到一個Node上時,根據後端存儲的特色,可能須要設置額外的掛載參數,目前能夠經過在PV的定義中,設置一個名爲「volume.beta.kubernetes.io/mount-options"的annotation來實現。下面的例子對一個類型爲gcePersistentDisk的PV設置了掛載參數「discard":

apiVersion: "v1" kind: "PersistentVolume" metadata : name: gce-disk-1 annotations: volume.beta.kubernetes.io/mount-options: "discard" spec: capacity: storage : "10Gi」 accessModes : - 」ReadWriteOnce」 gcePersistentDisk: fsType: "ext4" pdName : "gce-disk-1 

並不是全部類型的存儲都支持設置掛載參數。從Kubernetes v1.6版本開始,如下存儲類型支持設置掛載參數。

  • gcePersistentDisk。
  • AWSElasticBlockStore.
  • AzureFile。
  • AzureDisk。
  • NFS。
  • iSCSI。
  • RBD
  • (Rados Block Device): Ceph 塊存儲。
  • CephFS。
  • Cinder: OpenStack 塊存儲。
  • GlusterFS。
  • VsphereVolume.
  • Quobyte Volumes.
  • VMware Photon。

PVC詳解

PVC 做爲用戶對存儲資源的需求申請,主要包括存儲空間請求、訪問模式、PV選擇條件和存儲類別等信息的設置。下面的例子中聲明的PVC具備以下屬性:申請8Gi存儲空間,訪問模式爲"ReadWriteOnce",PV選擇條件爲包含標籤"release=stable"而且包含條件爲"environment In [dev]"的標籤,存儲類別爲"slow"(要求系統中已存在名爲slow的StorageClass)。

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: myclaim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 8Gi
  storageClassName: slow
  selector:
    matchLabels:
      release: "stable" matchExpressions: - {key: environment, operator: In, values: [dev]} 

PVC的關鍵配置參數說明以下:

  • 資源請求(Resources):描述對存儲資源的請求,目前僅支持request.storage的設置,即存儲空間大小。
  • 訪問模式(Access Modes):PVC也能夠設置訪問模式,用於描述用戶應用對存儲資源的訪問權限。能夠設置的三種訪問模式與PV相同。
  • PV選擇條件(Selector):經過Label Selector的設置,可以使PVC對於系統中已存在的各類PV進行篩選。系統將根據標籤選擇出合適的PV與該PVC進行綁定。選擇條件可使用matchLabels和matchExpressions進行設置。若是兩個條件都設置了,則Selector的邏輯是兩組條件同時知足才能完成匹配。
  • 存儲類別(Class):PVC在定義時能夠設定須要的後端存儲的"類別"(經過storageClassName字段指定),以下降對後端存儲特性的詳細信息的依賴。只有設置了該Class的PV才能被系統選出,並與該PVC進行綁定。

PVC也能夠不設置Class需求。若是storageClassName字段的值被設置爲空(storageClassName=""),則表示該PVC不要求特定的Class,系統將只選擇未設定Class的PV與之匹配和綁定。PVC也能夠徹底不設置storageClassName字段,此時將根據系統是否啓用了名爲"DefaultStorageClass"的admission controller進行相應的操做。

  • 未啓用DefaultStorageClass:等效於PVC設置storageClassName的值爲空,即只能選擇未設定Class的PV與之匹配和綁定。

  • 啓用了DefaultStorageClass:要求集羣管理員已定義默認的StorageClass。若是系統中不存在默認的StorageClass,則等效於不啓用DefaultStorageClass的狀況。若是存在默認的StorageClass,則系統將自動爲PVC建立一個PV(使用默認StorageClass的後端存儲),並將它們進行綁定。集羣管理員設置默認StorageClass的方法爲,在StorageClass的定義中加上一個annotation "storageclass.kubernetes.io/is-default-class=true"。若是管理員將多個StorageClass都定義爲default,則因爲不惟一,系統將沒法爲PVC建立相應的PV。

注意,PVC和PV都受限於namespace,PVC在選擇PV時受到namespace的限制,只有相同namespace中的PV纔可能與PVC綁定。Pod在引用PVC時一樣受namespace的限制,只有相同namespace中的PVC才能掛載到Pod內。

當Selector和Class都進行了設置時,系統將選擇兩個條件同時知足的PV與之匹配。

另外,若是資源供應使用的是動態模式,即管理員沒有預先定義PV,僅經過StorageClass交給系統自動完成PV的動態建立,那麼PVC再設定Selector時,系統將沒法爲其供應任何存儲資源了。

在啓動動態供應模式的狀況下,一旦用戶刪除了PVC,與之綁定的PV將根據其默認的回收策略"Delete"也會被刪除。若是須要保留PV(用戶數據),則在動態綁定成功後,用戶須要將系統自動生成PV的回收策略從"Delete"改爲"Retain"。

PV和PVC的生命週期

PV能夠看做可用的存儲資源,PVC則是對存儲資源的需求,PV和PVC的相互關係遵循下圖所示的生命週期。

screenshot

1 資源供應(Provisioning)

k8s支持兩種資源的供應模式:靜態模式(Static)動態模式(Dynamic)。資源供應的結果就是建立好的PV。

  • 靜態模式:集羣管理員手工建立許多PV,在定義PV時須要將後端存儲的特性進行設置。
  • 動態模式:集羣管理員無須手工建立PV,而是經過StorageClass的設置對後端存儲進行描述,標記爲某種「類型(Class)」。此時要求PVC對存儲類型進行聲明,系統將自動完成PV的建立及與PVC的綁定。PVC能夠聲明Class爲"",說明該PVC禁止使用動態模式。

2 資源綁定(Binding)

在用戶定義好PVC以後,系統將根據PVC對存儲資源的請求(存儲空間和訪問模式)在已存在的PV中選擇一個知足PVC要求的PV,一旦找到,就將該PV與用戶定義的PVC進行綁定,而後用戶的應用就可使用這個PVC了。若是系統中沒有知足PVC要求的PV,PVC就會無限期處於Pending狀態,直到等到系統管理員建立了一個符合其要求的PV。PV一旦綁定到某個PVC上,就被這個PVC獨佔,不能再與其餘PVC進行綁定了。在這種狀況下,當PVC申請的存儲空間與PV的少時,整個PV的空間都會被PVC所用,可能會形成資源的浪費。若是資源供應使用的是動態模式,則系統在爲PVC找到合適的StorageClass後,將自動建立一個PV並完成與PVC的綁定。

3 資源使用(Using)

Pod使用volume的定義,將PVC掛載到容器內的某個路徑進行使用。volume的類型爲"persistentVolumeClaim",在後面的示例中再進行詳細說明。在容器應用掛載了一個PVC後,就能被持續獨佔使用。不過,多個Pod能夠掛載同一個PVC,應用程序須要考慮多個實例共同訪問同一塊存儲空間的問題。

4 資源釋放(Releasing)

當用戶對存儲資源使用完畢後,用戶能夠刪除PVC,與該PVC綁定的PV將會被標記爲「已釋放」,但還不能馬上與其餘PVC進行綁定。經過以前PVC寫入的數據可能還留在存儲設備上,只有在清除以後該PV才能再次使用。

5 資源回收(Reclaimig)

對於PV,管理員能夠設定回收策略(Reclaim Policy),用於設置與之綁定的PVC釋放資源以後,對於遺留數據如何處理。只有PV的存儲空間完成回收,才能供新的PVC綁定和使用。

下面經過兩張圖分別對在靜態資源供應模式和動態資源供應模式下,PV、PVC、StorageClass及Pod使用PVC的原理進行說明。

在靜態資源供應模式下,經過PV和PVC完成綁定,並供Pod使用的存儲管理機制。
screenshot
在動態資源供應模式下,經過StorageClass和PVC完成資源動態綁定(系統自動生成PV),並供Pod使用的存儲管理機制。
screenshot

StorageClass詳解

StorageClass做爲對存儲資源的抽象定義,對用戶設置的PVC申請屏蔽後端存儲的細節。一方面減輕用戶對於存儲資源細節的關注,另外一方面也減輕管理員手工管理PV的工做,由系統自動完成PV的建立和綁定,實現動態的資源供應。使用基於StorageClass的動態資源供應模式將逐步成爲雲平臺的標準存儲配置模式。

StorageClass的定義主要包括名稱、後端存儲的提供者(Provisioner)和後端存儲的相關參數配置。StorageClass一旦被建立出來,就將沒法修改,只能刪除原StorageClass的定義重建。下面的例子中定義了一個名爲「standard"的StorageClass,提供者爲aws-ebs,其參數設置了一個type=gp2。

kind: StorageClass
apiVersion: storage.k8s.io/v1 metadata: name: standard provisioner: kubernetes.io/aws-ebs parameters: type: gp2 

1 StorageClass的關鍵配置參數

1)提供者(Provisioner)

描述存儲資源的提供者,也能夠看做後端存儲驅動。
目前k8s支持的Provisioner都以"kubernetes.io/"爲開頭,用戶也可使用自定義的後端存儲提供者。爲了符合StorageClass的用法,自定義Provisioner須要符合存儲卷的開發規範,詳見該連接的說明:https://github.com/kubernetes/community/blob/master/contributors/design-proposals/storage/volume-provisioning.md

2)參數(Parameters)

後端存儲資源提供者的參數設置,不一樣的Provisioner包括不一樣的參數設置。某些參數能夠不顯示設定,Provisioner將使用其默認值。

3)下面介紹幾種經常使用的Provisioner對StorageClass的定義進行詳細說明

AWS EBS存儲卷
kind: StorageClass
apiVersion: storage.k8s.io/v1 metadata: name: slow provisioner: kubernetes.io/aws-ebs parameters: type: io1 zone: us-east-id iopsPerGB: "10" 

參數說明以下:

  • type:可選項爲io1, gp2, sc1, st1, 默認值爲gp2
  • zone:AWS zone的名稱
  • iopsPerGB:僅用於io1類型的volume,意爲每秒每GiB的I/O操做數量
  • encrypted:是否加密
  • kmsKeyId:加密時的Amazon Resource Name
GCE PD存儲卷
kind: StorageClass
apiVersion: storage.k8s.io/v1 metadata: name: slow provisioner: kubernetes.io/gce-pd parameters: type: pd-standard zone: us-centrall-a 

參數說明:

  • type:可選項爲pd-standard, pd-ssd, 默認值爲pd-standard
  • zone:GCE zone名稱
GlusterFS存儲卷
kind: StorageClass
apiVersion: storage.k8s.io/v1 metadata: name: slow provisioner: kubernetes.io/glusterfs parameters: resturl: "https://127.0.0.1:8081" clusterid: "sadfa2435hfghsrg462345" restauthenabled: "true" restuser: "admin" secretNamespace: "default" secretName: "heketi-secret" gidMin: "40000" gidMax: "50000" volumetype: "replicate:3" 

參數說明以下(詳細說明請參考GlusterFS和Heketi的文檔)。

  • resturl: Gluster REST服務(heketi)的URL地址,用於自動完成GlusterFSvolume的設置。
  • restauthenabled: 是否對Gluster REST服務啓用安全機制。
  • restuser: 訪問Gluster REST服務的用戶名。
  • secretNamespace和secretName: 保存訪問Gluster REST服務密碼的Secret資源對象名。
  • clusterid: GlusterFS的Cluster ID
  • gidMin和gidMAX: StorageClass的GID範圍,用於動態資源供應時爲PV設置的GID。
  • volumetype: GlusterFS的volume類型設置,例如replicate:3(Replicate類型,3副本);disperse:4:2(Disperse類型,數據4份,冗餘2份);none(Distribute類型)。
OpenStack Cinder存儲卷
kind: StorageClass
apiVersion: storage.k8s.io/v1 metadata: name: gold provisioner: kubernetes.io/cinder parameters: type: fast availability: nova 

參數說明以下:

  • type: Cinder的VolumeType, 默認值爲空。
  • availability: Availability Zone, 默認值爲空。

其餘Provisioner的StorageClass相關參數設置請參考它們各自的配置手冊。

2 設置默認的StorageClass

要在系統中設置一個默認的StorageClass,首先須要啓動名爲"DefaultStorageClass"admission controller, 即在kube-apiserver的命令行參數--admission-controll中增長:
--admission-control=...,DefaultStorageClass

而後,在StorageClass的定義中設置一個annotation:

kind: StorageClass
apiVersion: storage.k8s.io/v1 metadata: name: gold annotations: storageclass.beta.kubernetes.io/is-default-class="true" provisioner: kubernetes.io/cinder parameters: type: fast availability: nova 

經過kubectl create命令建立成功後,查看StorageClass列表,能夠看到名爲gold的StorageClass被標記爲"default":

# kubectl get sc gold (default) kubernetes.io/cinder 

動態存儲管理實戰:GlusterFS

本節以GlusterFS爲例,從定義StorageClass、建立GlusterFS和Heketi服務、用戶申請PVC到建立Pod使用存儲資源,對StorageClass和動態資源分配進行詳細說明,進一步剖析k8s的存儲機制。

1 準備工做

首先在用於部署GlusterFS的三個節點上安裝GlusterFS客戶端:

yum -y install glusterfs glusterfs-fuse 

GlusterFS管理服務容器須要以特權模式運行,中kube-apiserver的啓動參數中確認已經打開了:

--allow-privileged=true 

給要部署GlusterFS管理服務的節點打上"storagenode=glusterfs"的標籤,這樣能夠將GlusterFS容器定向部署到安裝了GlusterFS的Node上:

[k8s@kube-server harbor]$ kubectl label node kube-node1 storagenode=glusterfs node "kube-node1" labeled [k8s@kube-server harbor]$ kubectl label node kube-node2 storagenode=glusterfs node "kube-node2" labeled [k8s@kube-server harbor]$ kubectl label node kube-node3 storagenode=glusterfs node "kube-node3" labeled 

2 建立GlusterFS服務容器集羣

GlusterFS服務容器以DaemonSet的方式進行部署,確保每臺Node上都運行一個GlusterFS管理服務,glusterfs-daemonset.yaml內容以下。參照 https://github.com/gluster/gluster-kubernetes。

1)在各個Node節點的啓動參數中增長如下選項,由於GlusterFS須要使用容器的特權模式運行

--allow-privileged 

生效:

systemctl daemon-reload systemctl restart kubelet systemctl status kubelet 

2)給每一個運行GlusterFS的Node節點增長一塊數據磁盤

注意數據盤掛載後,在系統中使用的設備描述符,須要在下一步配置中使用到。

3)編輯topology.json拓樸文件

獲取一份安裝資源:

git clone https://github.com/gluster/gluster-kubernetes.git

[k8s@kube-server deploy]$ pwd /home/k8s/gluster-kubernetes/deploy [k8s@kube-server deploy]$ ls gk-deploy heketi.json.template kube-templates ocp-templates topology.json 

至少須要3個節點,按下面格式對該文件進行更新:

[k8s@kube-server deploy]$ cat topology.json { "clusters": [ { "nodes": [ { "node": { "hostnames": { "manage": [ "kube-node1" ], "storage": [ "172.16.10.101" ] }, "zone": 1 }, "devices": [ "/dev/sdb" ] }, { "node": { "hostnames": { "manage": [ "kube-node2" ], "storage": [ "172.16.10.102" ] }, "zone": 1 }, "devices": [ "/dev/sdb" ] }, { "node": { "hostnames": { "manage": [ "kube-node3" ], "storage": [ "172.16.10.103" ] }, "zone": 1 }, "devices": [ "/dev/sdb" ] } ] } ] } 

4)在k8s上部署 GlusterFS + heketi

須要先檢查下環境:

  • 至少須要3個節點
  • 每一個節點上至少提供一個裸塊存儲設備;
  • 確保如下端口沒有被佔用:2222,24007, 24008, 49152~49251
  • 在系統中加載如下模塊: modprobe dm_snapshot && modprobe dm_mirror && modprobe dm_thin_pool
  • 安裝依賴包:yum -y install glusterfs-fuse

執行部署命令:

[k8s@kube-server deploy]$ ./gk-deploy -g 

注:-g參數表示要建立出一套glusterfs集羣服務。

若是一切順利,在結束時會看到下面的輸出:

....
service "heketi" created
deployment.extensions "heketi" created
Waiting for heketi pod to start ... OK Flag --show-all has been deprecated, will be removed in an upcoming release heketi is now running and accessible via https://172.30.86.3:8080 . To run administrative commands you can install 'heketi-cli' and use it as follows: # heketi-cli -s https://172.30.86.3:8080 --user admin --secret '<ADMIN_KEY>' cluster list You can find it at https://github.com/heketi/heketi/releases . Alternatively, use it from within the heketi pod: # /opt/k8s/bin/kubectl -n default exec -i heketi-75dcfb7d44-vj9bk -- heketi-cli -s https://localhost:8080 --user admin --secret '<ADMIN_KEY>' cluster list For dynamic provisioning, create a StorageClass similar to this: --- apiVersion: storage.k8s.io/v1beta1 kind: StorageClass metadata: name: glusterfs-storage provisioner: kubernetes.io/glusterfs parameters: resturl: "https://172.30.86.3:8080" Deployment complete! 

查看下都建立出了哪些服務實例:

[k8s@kube-server deploy]$ kubectl get pods -o wide
NAME                        READY     STATUS    RESTARTS   AGE       IP NODE glusterfs-88469 1/1 Running 0 2h 172.16.10.102 kube-node2 glusterfs-lwm4n 1/1 Running 0 2h 172.16.10.103 kube-node3 glusterfs-pfgwb 1/1 Running 0 2h 172.16.10.101 kube-node1 heketi-75dcfb7d44-vj9bk 1/1 Running 0 1h 172.30.86.3 kube-node2 my-nginx-86555897f9-2kn92 1/1 Running 2 8h 172.30.49.2 kube-node1 my-nginx-86555897f9-d95t9 1/1 Running 4 2d 172.30.48.2 kube-node3 [k8s@kube-server deploy]$ kubectl get svc -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR heketi ClusterIP 10.254.42.129 <none> 8080/TCP 1h glusterfs=heketi-pod heketi-storage-endpoints ClusterIP 10.254.4.122 <none> 1/TCP 1h <none> kubernetes ClusterIP 10.254.0.1 <none> 443/TCP 7d <none> my-nginx ClusterIP 10.254.191.237 <none> 80/TCP 5d run=my-nginx [k8s@kube-server deploy]$ kubectl get deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE heketi 1 1 1 1 1h my-nginx 2 2 2 2 5d [k8s@kube-server deploy]$ kubectl get secret NAME TYPE DATA AGE default-token-p5wjd kubernetes.io/service-account-token 3 7d heketi-config-secret Opaque 3 1h heketi-service-account-token-mrtsx kubernetes.io/service-account-token 3 2h kubelet-api-test-token-gdj7g kubernetes.io/service-account-token 3 6d [k8s@kube-server deploy]$ 

5)使用示例

在能夠調用kubectl管理k8s集羣的節點上,安裝一個heketi客戶端:

yum -y install heketi-client 

建立個1GB的PV存儲卷:

[k8s@kube-server deploy]$ export HEKETI_CLI_SERVER=https://172.30.86.3:8080 [k8s@kube-server deploy]$ heketi-cli volume create --size=1 --persistent-volume --persistent-volume-endpoint=heketi-storage-endpoints | kubectl create -f - persistentvolume "glusterfs-900fb349" created 

回到Dashboard上看看這個剛建立的存儲卷:
screenshot
經過heketi服務查看和管理GlusterFS集羣: 查看集羣列表:

[root@kube-node1 ~]# curl 10.254.42.129:8080/clusters {"clusters":["ada54ffbeac15a5c9a7521e0c7d2f636"]} 

查看集羣詳情:

[root@kube-node1 ~]# curl 10.254.42.129:8080/clusters/ada54ffbeac15a5c9a7521e0c7d2f636 {"id":"ada54ffbeac15a5c9a7521e0c7d2f636","nodes":["49ac6f56ef21408bcad7c7613cd40bd8","bdf51ae46025cd4fcf134f7be36c32de","fc21262379ec3636e3eadcae15efcc94"],"volumes":["42b01b9b08af23b751b2359fb161c004","900fb349e56af275f47d523d08fdfd6e"],"block":true,"file":true,"blockvolumes":[]} 

查看節點詳情:

[root@kube-node1 ~]# curl 10.254.42.129:8080/nodes/49ac6f56ef21408bcad7c7613cd40bd8 {"zone":1,"hostnames":{"manage":["kube-node3"],"storage":["172.16.10.103"]},"cluster":"ada54ffbeac15a5c9a7521e0c7d2f636","id":"49ac6f56ef21408bcad7c7613cd40bd8","state":"online","devices":[{"name":"/dev/sdb","storage":{"total":8253440,"free":5087232,"used":3166208},"id":"2f6b2f6c289a2f6bf48fbec59c0c2009","state":"online","bricks":[{"id":"2ea90ebd791a4230e927d233d1c8a7d1","path":"/var/lib/heketi/mounts/vg_2f6b2f6c289a2f6bf48fbec59c0c2009/brick_2ea90ebd791a4230e927d233d1c8a7d1/brick","device":"2f6b2f6c289a2f6bf48fbec59c0c2009","node":"49ac6f56ef21408bcad7c7613cd40bd8","volume":"42b01b9b08af23b751b2359fb161c004","size":2097152},{"id":"4c98684d878ffe7dbfc1008336460eed","path":"/var/lib/heketi/mounts/vg_2f6b2f6c289a2f6bf48fbec59c0c2009/brick_4c98684d878ffe7dbfc1008336460eed/brick","device":"2f6b2f6c289a2f6bf48fbec59c0c2009","node":"49ac6f56ef21408bcad7c7613cd40bd8","volume":"900fb349e56af275f47d523d08fdfd6e","size":1048576}]}]} 
  • state 爲 online說明節點正常

6)建立一個使用GlusterFS動態存儲供應服務的nginx應用

注:在本示例中的用戶認證是未啓用的,若是要啓動用戶認證服務,則能夠建立一個secret,而後經過StorageClass配置參數傳遞給Gluster動態存儲供應服務。
下面是一個存儲類的示例,它將請求2GB的按需存儲,用於在咱們的HelloWorld應用程序中使用。

gluster-storage-class.yaml apiVersion: storage.k8s.io/v1beta1 kind: StorageClass metadata: name: gluster-heketi provisioner: kubernetes.io/glusterfs parameters: resturl: "https://10.254.42.129:8080" restuser: "joe" restuserkey: "My Secret Life" 
  • name,StorageClass名稱
  • provisioner,存儲服務提供者
  • resturl,Heketi REST Url
  • restuser,由於未啓用認證,因此這個參數無效
  • restuserkey,同上

建立該存儲類:

[k8s@kube-server ~]$ kubectl create -f gluster-storage-class.yaml storageclass.storage.k8s.io "gluster-heketi" created [k8s@kube-server ~]$ kubectl get storageclass NAME PROVISIONER AGE gluster-heketi kubernetes.io/glusterfs 43s 

建立PersistentVolumeClaim(PVC)以請求咱們的HelloWorld應用程序的存儲:
咱們將建立一個要求2GB存儲空間的PVC,此時,Kubernetes Dynamic Provisioning Framework和Heketi將自動配置新的GlusterFS卷並生成Kubernetes PersistentVolume(PV)對象。

gluster-pvc.yaml
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: gluster1 annotations: volume.beta.kubernetes.io/storage-class: gluster-heketi spec: accessModes: - ReadWriteOnce resources: requests: storage: 2Gi 
  • annotations,Kubernetes存儲類註釋和存儲類的名稱

    [k8s@kube-server ~]$ kubectl create -f gluster-pvc.yaml
    persistentvolumeclaim "gluster1" created

能夠看到PVC是綁定到一個動態供給的存儲捲上的:

[k8s@kube-server ~]$ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE gluster1 Bound pvc-53e824cf-7eb7-11e8-bf5c-080027395360 2Gi RWO gluster-heketi 53s [k8s@kube-server ~]$ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE glusterfs-900fb349 1Gi RWX Retain Available 2h pvc-53e824cf-7eb7-11e8-bf5c-080027395360 2Gi RWO Delete Bound default/gluster1 gluster-heketi 1m 

建立一個使用該PVC的nginx實例:

nginx-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod1
  labels:
    name: nginx-pod1
spec:
  containers:
  - name: nginx-pod1 image: nginx:1.7.9 ports: - name: web containerPort: 80 volumeMounts: - name: gluster-vol1 mountPath: /usr/share/nginx/html volumes: - name: gluster-vol1 persistentVolumeClaim: claimName: gluster1 

claimName,要使用的PVC的名稱

[k8s@kube-server ~]$ kubectl create -f nginx-pod.yaml pod "nginx-pod1" created [k8s@kube-server ~]$ kubectl get pods -o wide|grep nginx-pod nginx-pod1 1/1 Running 0 33s 172.30.86.3 kube-node2 

登陸到該Pod中並建立一個網頁文件:

[k8s@kube-server ~]$ kubectl exec -it nginx-pod1 /bin/bash root@nginx-pod1:/# df -h Filesystem Size Used Avail Use% Mounted on rootfs 41G 7.1G 34G 18% / overlay 41G 7.1G 34G 18% / tmpfs 64M 0 64M 0% /dev tmpfs 496M 0 496M 0% /sys/fs/cgroup /dev/mapper/centos_bogon-root 41G 7.1G 34G 18% /dev/termination-log shm 64M 0 64M 0% /dev/shm /dev/mapper/centos_bogon-root 41G 7.1G 34G 18% /etc/resolv.conf /dev/mapper/centos_bogon-root 41G 7.1G 34G 18% /etc/hostname /dev/mapper/centos_bogon-root 41G 7.1G 34G 18% /etc/hosts /dev/mapper/centos_bogon-root 41G 7.1G 34G 18% /var/cache/nginx 172.16.10.101:vol_1b6e32efd9b6f07e2b056bed2ce6cc73 2.0G 53M 2.0G 3% /usr/share/nginx/html tmpfs 496M 12K 496M 1% /run/secrets/kubernetes.io/serviceaccount tmpfs 64M 0 64M 0% /proc/kcore tmpfs 64M 0 64M 0% /proc/keys tmpfs 64M 0 64M 0% /proc/timer_list tmpfs 64M 0 64M 0% /proc/timer_stats tmpfs 64M 0 64M 0% /proc/sched_debug tmpfs 496M 0 496M 0% /proc/scsi tmpfs 496M 0 496M 0% /sys/firmware root@nginx-pod1:/# cd /usr/share/nginx/html dex.htmlnx-pod1:/usr/share/nginx/html# echo 'Hello World from GlusterFS!!!' > in root@nginx-pod1:/usr/share/nginx/html# ls index.html root@nginx-pod1:/usr/share/nginx/html# cat index.html Hello World from GlusterFS!!! root@nginx-pod1:/usr/share/nginx/html# exit exit 

訪問一下咱們的網頁:

[k8s@kube-server ~]$ curl https://172.30.86.3 Hello World from GlusterFS!!! 

再檢查一下gluster pod,找到咱們剛寫入的index.html文件,登陸任一個gluster pod:

screenshot

[root@kube-node1 brick]# pwd /var/lib/heketi/mounts/vg_f8776d0d92102fc3e272f2ec899e5f18/brick_6e016b1ed8e16b6a28839f1670a56d00/brick [root@kube-node1 brick]# ls index.html [root@kube-node1 brick]# cat index.html Hello World from GlusterFS!!! 

7)刪除 glusterfs 集羣配置

curl -X DELETE 10.254.42.129:8080/devices/46b2685901f56d6fe0cc85bf3d37bf75 # 使用的是device id,刪除device curl -X DELETE 10.254.42.129:8080/nodes/8bd8497a8dcda0708508228f4ae8c2ae #使用的是node id, 刪除node 

每一個節點都要刪除掉device才能再刪除node

cluster 列表下的全部節點都刪除後 才能刪除cluster: curl -X DELETE 10.254.42.129:8080/clusters/ada54ffbeac15a5c9a7521e0c7d2f636 

Heketi服務

GlusterFS 是個開源的分佈式文件系統,而 Heketi 在其上提供了 REST 形式的 API,兩者協同爲 Kubernetes 提供了存儲卷的自動供給能力。Heketi還支持GlusterFS多集羣管理。當一個集羣中同時有多種規格、性能和容量特色的存儲資源時,Heketi能夠經過接入多個存儲集羣,在每一個集羣中又按資源特性劃分出多個Zone來進行管理。

screenshot

1)在k8s中部署Heketi服務前須要爲其建立一個ServiceAccount帳號

咱們繼續看一下前面例子中使用到的一些配置資源:

[k8s@kube-server deploy]$ pwd /home/k8s/gluster-kubernetes/deploy [k8s@kube-server deploy]$ cat ./kube-templates/heketi-service-account.yaml apiVersion: v1 kind: ServiceAccount metadata: name: heketi-service-account labels: glusterfs: heketi-sa heketi: sa [k8s@kube-server deploy]$ kubectl get sa | grep heketi heketi-service-account 1 4h 

2)經過Deployment部署Heketi服務

[k8s@kube-server deploy]$ kubectl get deployment
NAME       DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
heketi     1         1         1            1           3h

[k8s@kube-server kube-templates]$ pwd
/home/k8s/gluster-kubernetes/deploy/kube-templates [k8s@kube-server kube-templates]$ cat heketi-deployment.yaml --- kind: Service apiVersion: v1 metadata: name: heketi labels: glusterfs: heketi-service heketi: service annotations: description: Exposes Heketi Service spec: selector: glusterfs: heketi-pod ports: - name: heketi port: 8080 targetPort: 8080 --- kind: Deployment apiVersion: extensions/v1beta1 metadata: name: heketi labels: glusterfs: heketi-deployment heketi: deployment annotations: description: Defines how to deploy Heketi spec: replicas: 1 template: metadata: name: heketi labels: glusterfs: heketi-pod heketi: pod spec: serviceAccountName: heketi-service-account containers: - image: heketi/heketi:dev imagePullPolicy: IfNotPresent name: heketi env: - name: HEKETI_USER_KEY value: ${HEKETI_USER_KEY} - name: HEKETI_ADMIN_KEY value: ${HEKETI_ADMIN_KEY} - name: HEKETI_EXECUTOR value: ${HEKETI_EXECUTOR} - name: HEKETI_FSTAB value: ${HEKETI_FSTAB} - name: HEKETI_SNAPSHOT_LIMIT value: '14' - name: HEKETI_KUBE_GLUSTER_DAEMONSET value: "y" - name: HEKETI_IGNORE_STALE_OPERATIONS value: "true" ports: - containerPort: 8080 volumeMounts: - name: db mountPath: "/var/lib/heketi" - name: config mountPath: /etc/heketi readinessProbe: timeoutSeconds: 3 initialDelaySeconds: 3 httpGet: path: "/hello" port: 8080 livenessProbe: timeoutSeconds: 3 initialDelaySeconds: 30 httpGet: path: "/hello" port: 8080 volumes: - name: db glusterfs: endpoints: heketi-storage-endpoints path: heketidbstorage - name: config secret: secretName: heketi-config-secret 

定義了兩個volumes,其中"db"是使用的glusterfs提供的存儲卷,而"config"的volume則是使用的"secret"

查看一下被看成volume使用的secret的內容:

[k8s@kube-server kube-templates]$ kubectl describe secret heketi-config-secret
Name:         heketi-config-secret
Namespace:    default
Labels:       glusterfs=heketi-config-secret
 heketi=config-secret Annotations: <none> Type: Opaque Data ==== heketi.json: 909 bytes private_key: 0 bytes topology.json: 1029 bytes 

能夠看到這個secret中是包含了三個配置文件

這個secret是使用腳本建立的:

[k8s@kube-server deploy]$ pwd /home/k8s/gluster-kubernetes/deploy [k8s@kube-server deploy]$ ls gk-deploy heketi.json.template kube-templates ocp-templates topology.json [k8s@kube-server deploy]$ grep heketi-config-secret gk-deploy eval_output "${CLI} create secret generic heketi-config-secret --from-file=private_key=${SSH_KEYFILE} --from-file=./heketi.json --from-file=topology.json=${TOPOLOGY}" eval_output "${CLI} label --overwrite secret heketi-config-secret glusterfs=heketi-config-secret heketi=config-secret" [k8s@kube-server deploy]$ 

3)爲Heketi設置GlusterFS集羣

使用一個topology.json的配置文件來完成各個GlusterFS節點和設備的定義。
Heketi要求一個GlusterFS集羣中至少有3個節點。在topology.json配置文件的hostnames字段中的manage中填寫主機名,在storage上填寫IP地址,devices要求是未建立文件系統的裸設備(支持多塊磁盤)。這樣Heketi就能夠自動完成PV、VG和LV的建立。

topology.json文件的內容在前面已經提供了,再也不重複。

當使用gluster-kubernetes項目提供的腳本工具和模板建立glusterfs和heketi服務時,不須要額外的手動干預。但若是是手動建立這些服務,則能夠按下面的方法使用Heketi加載topology配置,完成GlusterFS服務集羣的建立。
能夠登陸進入Hekiti容器執行如下命令:

# export HEKETI_CLI_SERVER=https://localhost:8080 # heketi-cli topology load --json=topology.json 

這樣Heketi就完成了GlusterFS集羣的建立,同時在GlusterFS集羣的各個節點的/dev/sdb盤上成功建立了PV和VG。
此時,查看Heketi的topology信息,能夠看到很詳細的Node和Device信息:

[k8s@kube-server deploy]$ heketi-cli topology info

Cluster Id: ada54ffbeac15a5c9a7521e0c7d2f636

    Volumes:

    Name: vol_1b6e32efd9b6f07e2b056bed2ce6cc73
    Size: 2
    Id: 1b6e32efd9b6f07e2b056bed2ce6cc73
    Cluster Id: ada54ffbeac15a5c9a7521e0c7d2f636
    Mount: 172.16.10.103:vol_1b6e32efd9b6f07e2b056bed2ce6cc73
    Mount Options: backup-volfile-servers=172.16.10.102,172.16.10.101 Durability Type: replicate Replica: 3 Snapshot: Disabled Bricks: Id: 6e016b1ed8e16b6a28839f1670a56d00 Path: /var/lib/heketi/mounts/vg_f8776d0d92102fc3e272f2ec899e5f18/brick_6e016b1ed8e16b6a28839f1670a56d00/brick Size (GiB): 2 Node: fc21262379ec3636e3eadcae15efcc94 Device: f8776d0d92102fc3e272f2ec899e5f18 Id: c7acfcecaf85aada98b0c0208798440f Path: /var/lib/heketi/mounts/vg_2f6b2f6c289a2f6bf48fbec59c0c2009/brick_c7acfcecaf85aada98b0c0208798440f/brick Size (GiB): 2 Node: 49ac6f56ef21408bcad7c7613cd40bd8 Device: 2f6b2f6c289a2f6bf48fbec59c0c2009 Id: e2acc8268f17f05e940ebe679dacfa3a Path: /var/lib/heketi/mounts/vg_e100120226d5d9567ed0f92b9810236c/brick_e2acc8268f17f05e940ebe679dacfa3a/brick Size (GiB): 2 Node: bdf51ae46025cd4fcf134f7be36c32de Device: e100120226d5d9567ed0f92b9810236c Name: heketidbstorage Size: 2 Id: 42b01b9b08af23b751b2359fb161c004 Cluster Id: ada54ffbeac15a5c9a7521e0c7d2f636 Mount: 172.16.10.103:heketidbstorage Mount Options: backup-volfile-servers=172.16.10.102,172.16.10.101 Durability Type: replicate Replica: 3 Snapshot: Disabled Bricks: Id: 2ea90ebd791a4230e927d233d1c8a7d1 Path: /var/lib/heketi/mounts/vg_2f6b2f6c289a2f6bf48fbec59c0c2009/brick_2ea90ebd791a4230e927d233d1c8a7d1/brick Size (GiB): 2 Node: 49ac6f56ef21408bcad7c7613cd40bd8 Device: 2f6b2f6c289a2f6bf48fbec59c0c2009 Id: 9dc7238db3240146f12189dd28320227 Path: /var/lib/heketi/mounts/vg_f8776d0d92102fc3e272f2ec899e5f18/brick_9dc7238db3240146f12189dd28320227/brick Size (GiB): 2 Node: fc21262379ec3636e3eadcae15efcc94 Device: f8776d0d92102fc3e272f2ec899e5f18 Id: cb68cbf4e992abffc86ab3b5db58ef56 Path: /var/lib/heketi/mounts/vg_e100120226d5d9567ed0f92b9810236c/brick_cb68cbf4e992abffc86ab3b5db58ef56/brick Size (GiB): 2 Node: bdf51ae46025cd4fcf134f7be36c32de Device: e100120226d5d9567ed0f92b9810236c Name: vol_900fb349e56af275f47d523d08fdfd6e Size: 1 Id: 900fb349e56af275f47d523d08fdfd6e Cluster Id: ada54ffbeac15a5c9a7521e0c7d2f636 Mount: 172.16.10.103:vol_900fb349e56af275f47d523d08fdfd6e Mount Options: backup-volfile-servers=172.16.10.102,172.16.10.101 Durability Type: replicate Replica: 3 Snapshot: Disabled Bricks: Id: 4c98684d878ffe7dbfc1008336460eed Path: /var/lib/heketi/mounts/vg_2f6b2f6c289a2f6bf48fbec59c0c2009/brick_4c98684d878ffe7dbfc1008336460eed/brick Size (GiB): 1 Node: 49ac6f56ef21408bcad7c7613cd40bd8 Device: 2f6b2f6c289a2f6bf48fbec59c0c2009 Id: 7c82d03c88d73bb18d407a791a1053c2 Path: /var/lib/heketi/mounts/vg_e100120226d5d9567ed0f92b9810236c/brick_7c82d03c88d73bb18d407a791a1053c2/brick Size (GiB): 1 Node: bdf51ae46025cd4fcf134f7be36c32de Device: e100120226d5d9567ed0f92b9810236c Id: 822266f7ad3cf62b1e45686265cf7268 Path: /var/lib/heketi/mounts/vg_f8776d0d92102fc3e272f2ec899e5f18/brick_822266f7ad3cf62b1e45686265cf7268/brick Size (GiB): 1 Node: fc21262379ec3636e3eadcae15efcc94 Device: f8776d0d92102fc3e272f2ec899e5f18 Nodes: Node Id: 49ac6f56ef21408bcad7c7613cd40bd8 State: online Cluster Id: ada54ffbeac15a5c9a7521e0c7d2f636 Zone: 1 Management Hostname: kube-node3 Storage Hostname: 172.16.10.103 Devices: Id:2f6b2f6c289a2f6bf48fbec59c0c2009 Name:/dev/sdb State:online Size (GiB):7 Used (GiB):5 Free (GiB):2 Bricks: Id:2ea90ebd791a4230e927d233d1c8a7d1 Size (GiB):2 Path: /var/lib/heketi/mounts/vg_2f6b2f6c289a2f6bf48fbec59c0c2009/brick_2ea90ebd791a4230e927d233d1c8a7d1/brick Id:4c98684d878ffe7dbfc1008336460eed Size (GiB):1 Path: /var/lib/heketi/mounts/vg_2f6b2f6c289a2f6bf48fbec59c0c2009/brick_4c98684d878ffe7dbfc1008336460eed/brick Id:c7acfcecaf85aada98b0c0208798440f Size (GiB):2 Path: /var/lib/heketi/mounts/vg_2f6b2f6c289a2f6bf48fbec59c0c2009/brick_c7acfcecaf85aada98b0c0208798440f/brick Node Id: bdf51ae46025cd4fcf134f7be36c32de State: online Cluster Id: ada54ffbeac15a5c9a7521e0c7d2f636 Zone: 1 Management Hostname: kube-node2 Storage Hostname: 172.16.10.102 Devices: Id:e100120226d5d9567ed0f92b9810236c Name:/dev/sdb State:online Size (GiB):7 Used (GiB):5 Free (GiB):2 Bricks: Id:7c82d03c88d73bb18d407a791a1053c2 Size (GiB):1 Path: /var/lib/heketi/mounts/vg_e100120226d5d9567ed0f92b9810236c/brick_7c82d03c88d73bb18d407a791a1053c2/brick Id:cb68cbf4e992abffc86ab3b5db58ef56 Size (GiB):2 Path: /var/lib/heketi/mounts/vg_e100120226d5d9567ed0f92b9810236c/brick_cb68cbf4e992abffc86ab3b5db58ef56/brick Id:e2acc8268f17f05e940ebe679dacfa3a Size (GiB):2 Path: /var/lib/heketi/mounts/vg_e100120226d5d9567ed0f92b9810236c/brick_e2acc8268f17f05e940ebe679dacfa3a/brick Node Id: fc21262379ec3636e3eadcae15efcc94 State: online Cluster Id: ada54ffbeac15a5c9a7521e0c7d2f636 Zone: 1 Management Hostname: kube-node1 Storage Hostname: 172.16.10.101 Devices: Id:f8776d0d92102fc3e272f2ec899e5f18 Name:/dev/sdb State:online Size (GiB):7 Used (GiB):5 Free (GiB):2 Bricks: Id:6e016b1ed8e16b6a28839f1670a56d00 Size (GiB):2 Path: /var/lib/heketi/mounts/vg_f8776d0d92102fc3e272f2ec899e5f18/brick_6e016b1ed8e16b6a28839f1670a56d00/brick Id:822266f7ad3cf62b1e45686265cf7268 Size (GiB):1 Path: /var/lib/heketi/mounts/vg_f8776d0d92102fc3e272f2ec899e5f18/brick_822266f7ad3cf62b1e45686265cf7268/brick Id:9dc7238db3240146f12189dd28320227 Size (GiB):2 Path: /var/lib/heketi/mounts/vg_f8776d0d92102fc3e272f2ec899e5f18/brick_9dc7238db3240146f12189dd28320227/brick 

總結:使用Kubernetes的動態存儲供應模式,配置StorageClass和Heketi共同搭建基於GlusterFS的共享存儲,相對於靜態模式至少有兩大優點。

  1. 一個是管理員無須預先建立大量的PV做爲存儲資源
  2. 用戶在申請PVC時也沒法保證容量與預置PV的容量可以一致。所以,從k8s v1.6開始,建議用戶優先考慮使用StorageClass的動態存儲供應模式進行存儲管理。
連接:https://www.orchome.com/1284 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索