Pod 是 kubernetes 中的基本單位,容器自己不會直接分配到主機上,而是會封裝到 Pod 對象中。一個 Pod 一般表示單個應用程序,有一個或者多個相關的容器組成,這些容器的生命週期都是相同的,並且會做爲一個總體在同一個 node 上調度起來,這些容器共享環境、存儲卷和 IP 控件。儘管 Pod 中可能存在多個容器,可是在 kubernetes 中是以 Pod 爲最小單位進行調度、伸縮並共享資源、管理生命週期。node
Pod 的基本操做
咱們先來看一下 Pod 的建立、查詢、修改和刪除操做。python
建立 Pod
# expod.yml
apiVersion: v1
kind: Pod
metadata:
name: expod
spec:
containers:
- name: expod-container
image: busybox
imagePullPolicy: IfNotPresent
command: ['sh', '-c']
args: ['echo "Hello Kubernetes!"; sleep 3600']
簡單的模板含義:nginx
apiVersion 表示 API 版本,v1 表示使用 kubernetes API 的穩定版本。git
kind 表示要建立的資源對象。github
metadata 表示該資源對象的元數據。能夠擁有多個元數據,name 表示當前資源的名稱。web
spec 表示該資源對象的具體設置。其中 containers 表示容器的集合,咱們這裏設置了一個簡單的容器。name: 要建立的容器名稱。image: 容器的鏡像地址。imagePullPolicy: 鏡像的下載策略,支持3種策略:Always、Never、IfNotPresent。command: 容器的啓動命令列表,不配置的話就使用鏡像內部的命令。args: 啓動參數列表docker
運行命令,建立 Pod。api
kubectl apply -f expod.yml
建立成功後,查詢一下當前運行的全部 Pod微信
kubectl get pod
查詢 Pod
Pod 信息查詢的命令有多個,查詢的詳細度也不同:網絡
查詢默認命名空間 default 的全部 Pod
kubectl get pod
查詢指定 Pod 的信息
kubectl get pod expod
對 Pod 狀態進行持續監控
kubectl get pod -w
顯示更多概要信息
kubectl get pod -o wide
比簡要信息多顯示集羣內 IP 地址、所屬 node
按照指定格式輸出 Pod 信息
kubectl get pod expod -o yaml
最詳細信息顯示,包括 Event
kubectl describe pod expod
這是最經常使用的資源信息查詢命令,顯示信息比較全面,包括資源的基本信息、容器信息、準備狀況、存儲卷信息和相關的事件列表。在資源部署的時候,若是遇到問題,能夠用這個命令查詢詳情,分析錯誤緣由。
查詢Pod的日誌信息
kubectl logs Pod名稱
修改 Pod
修改已存在的 Pod 屬性可使用 replace 命令
kubectl replace -f pod的yaml文件
咱們修改一下前面建立 Pod 的 yaml 文件,把輸出 「Hello Kubernetes!" 修改成 "Hello Kubernetes replaced!"。
# expod.yml
apiVersion: v1
kind: Pod
metadata:
name: expod
spec:
containers:
- name: expod-container
image: busybox
imagePullPolicy: IfNotPresent
command: ['sh', '-c']
args: ['echo "Hello Kubernetes replaced!"; sleep 3600']
Pod 有不少屬性是沒法修改的,若是必定要修改,須要加上 --force 參數,至關於重建 Pod。
kubectl replace -f expod.yml --force
能夠看一下命令輸出結果
先刪除了 expod 這個 Pod,而後建立一個替換的 expod 的 Pod,咱們再查看一下 Pod 的日誌。
kubectl logs expod
能夠看到 Pod 信息已經修改了。
刪除 Pod
刪除 Pod 很是簡單,執行如下命令便可:
kubectl delete pod expod
若是咱們是經過 Pod 模板文件建立的,推薦使用基於模板文件的刪除命令。
kubectl delete -f expod.yml
Pod 與容器
咱們可能已經發現了,Pod 模板和 Docker-Compose 很是類似,可是 Pod 模板能夠配置的參數更多、更復雜。
Pod 建立容器的方式
在 Pod 模板的 Containers 部分,指明容器的部署方式,在部署的過程當中會轉換成對應的容器運行命令,就以咱們最開始的 Pod 模板爲例:
apiVersion: v1
kind: Pod
metadata:
name: expod
spec:
containers:
- name: expod-container
image: busybox
imagePullPolicy: IfNotPresent
command: ['sh', '-c']
args: ['echo "Hello Kubernetes!"; sleep 3600']
在 kubernetes 進行調度的時候,會執行以下命令:
docker run --name expod-container busybox sh -c 'echo "Hello Kubernetes!"; sleep 3600'
command 和 args 的設置會分別覆蓋 Docker 鏡像中定義的 EntryPoint 與 CMD。
Pod 組織容器的方式
Pod 是由各個容器組成的一個總體,同時調度到某臺 Node 上,容器之間能夠共享資源、網絡環境和依賴,並擁有相同的生命週期。
容器如何組成一個 Pod
每一個 Pod 都包含一個或一組密切相關的業務容器,可是還有一個成爲」根容器「的特殊 Pause 容器。Pause 容器是屬於 Kubernetes 的一部分,若是一組業務容器做爲一個總體,咱們很難對整個容器進行判斷,假如一個業務組容器當中的某個容器掛載了能表明整個 Pod 都掛載了嗎?若是引入一個和業務無關的 Pause 容器,用它做爲 Pod 的根容器,用它的狀態表明整組容器的狀態,就能解決這個問題。並且一個 Pod 中的全部容器都共享 Pause 容器的 IP 地址及其掛載的存儲卷,這樣也簡化了容器之間的通訊和數據共享問題,相似於 Docker 容器網絡的 Container 模式。
咱們在開始建立的 Pod,能夠登陸上對應的 Node 機器,查看容器信息。
kubectl get pod -o wide
登陸 k8s-node2 之後,執行 docker ps 命令查看詳細信息:
能夠看到有三個 pause 容器,其中兩個是 flannel 和 proxy 容器,還有一個是咱們的 expod 的容器,它與另外一個 expod 容器共同組成了一個 Pod。
Pod 中的容器共享兩種資源-存儲和網絡。
存儲
在 Pod 裏面,咱們能夠指定一個或者多個存儲卷,Pod 中的全部容器均可以訪問這些存儲卷,存儲卷能夠分爲臨時和持久化兩種。
網絡
在 kubernetes 集羣中,每一個 Pod 都分配了惟一的 IP 地址,Pod 中的容器都共享網絡命名空間,包括 IP 地址和網絡端口。在 Pod 內部各容器之間能夠經過 localhost 互相通訊。咱們能夠對比一下 Docker 和 kubernetes 在網絡空間上的差別。
Docker的網絡空間
從圖中能夠看出,容器之間經過docker0網卡鏈接,每一個容器擁有獨立的內部網絡地址
kubernetes 的網絡空間
從圖中能夠看出,Pod 中的全部容器共享一個網絡地址
Pod 之間如何通訊
Pod 之間的通訊主要分爲兩種狀況:
同一個 Node 上 Pod 之間的通訊
同一個 Node 上的 Pod 使用的都是相同的網橋( docker0 )進行鏈接,至關於 Docker 的網絡空間,只不過是以 Pod 爲基礎。每一個 Pod 都有一個全局 IP 地址,同一個 Node 內不一樣 Pod 之間經過 veth 鏈接在同一個 docker0 網橋上,其 IP 地址都是從 docker0 網橋上動態獲取的,而且關聯在同一個 docker0 網橋上,地址段也相同,因此它們之間能直接通訊。
不一樣 Node 之間的 Pod 通訊
要實現不一樣 Node 的 Pod 之間通訊,首先要保證 Pod 在一個 kubernetes 集羣中擁有全局惟一的 IP 地址。又由於一個 Node 上的 Pod 是經過 Docker 網橋與外部進行通訊的,因此只要將不一樣 Node 上的 Docker 網橋配置成不一樣的 IP 地址段就能夠實現這個功能。Flannel 會配置 Docker 網橋,經過修改 Docker 的啓動參數 bip 來實現這一點,這樣就使得集羣中機器的 Docker 網橋就獲得了全局惟一的 IP 地址段,機器上所建立的容器也就擁有了全局惟一的 IP 地址。
跨 Node 的 Pod 之間的通訊
咱們在搭建 kubernetes 集羣的時候,在每一個 Node 機器上都建立了一個 kube-flannel 容器,這是在每一個 Node 機器上使用 Flannel 虛擬網卡接管容器並跨主機通訊,當一個節點的容器訪問另外一個節點的容器時,源節點上的數據會從 docker0 網橋路由到 flannel0 網卡,在目的節點處會從 flannel0 網卡路由到 docker0 網橋,而後再轉發給目標容器。Flannel 從新規劃了容器集羣的網絡,這樣既保證了容器 IP 的全局惟一性,又讓不一樣機器上的容器能經過內網 IP 地址互相通訊。可是,容器的 IP 地址並非固定的,Flannel 只分配子網段,因此容器的 IP 地址是在此網段的範圍內進行動態分配的。
由於 Pod 的 IP 地址自己是虛擬 IP,因此只有 kubernetes 集羣內部的機器( Master 和 Node )和其餘 Pod 能夠直接訪問這個 IP 地址,集羣以外的機器沒法直接訪問 Pod 的 IP 地址。咱們建立了一個 Nginx Pod:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: exnginx
image: nginx
imagePullPolicy: IfNotPresent
ports:
- name: nginxport
containerPort: 80
經過命令建立後查詢如下 IP 地址:
[root@k8s-master]# kubectl apply -f exnginx.yml
[root@k8s-master]# kubectl get pod -o wide
集羣中的全部機器和 Pod 均可以訪問這個虛擬地址和 containerPort 暴露的端口
[root@k8s-master]# curl http://10.244.1.6
同時咱們看到咱們有兩個 Pod,一個在 Node1 上,一個在 Node2 上,而 Node2 上的 Pod IP 地址是10.244.2.6,Node1 上的 Pod IP 地址是10.244.1.6,登陸到兩臺機器上,使用 ifconfig flannel.1 命令查看集羣子網段。
k8s-node1
K8s-node2
要使集羣外的機器訪問 Pod 提供的服務,後面咱們會介紹 Service 和 Ingress 來發布服務。
Pod 的生命週期
Pod 的狀態
一旦開始在集羣節點中建立 Pod,首先就會進入 Pending 狀態,只要 Pod 中的全部容器都已啓動並正常運行,則 Pod 接下來會進入 Running 狀態,若是 Pod 被要求終止,且全部容器終止退出時的狀態碼都爲0,Pod 就會進入 Succeeded 狀態。
若是進入 Failed 狀態,一般有如下3種緣由。
Pod 啓動時,只要有一個容器運行失敗,Pod 將會從 Pending 狀態進入 Failed 狀態。
Pod 正處於 Running 狀態,若 Pod 中的一個容器忽然損壞或者在退出時狀態碼不爲0,Pod 將會從 Running 進入 Failed 狀態。
在要求 Pod 正常關閉的時候,只要有一個容器退出的狀態碼不爲0,Pod 就會進入 Failed 狀態。
Pod 的重啓策略
在配置 Pod 的模板文件中有個 spec 模塊,其中有一個名爲 restartPolicy 的字段,字段的值爲 Always、OnFailure、Never。Node 上的 kubelet 經過 restartPolicy 執行重啓操做,由 kubelet 從新啓動的已退出容器將會以遞增延遲的方式(10s,20s,40s,...)嘗試從新啓動,上限時間爲 5min,延時的累加值會在成功運行 10min 後重置,一旦 Pod 綁定到某個節點上,就絕對不會從新綁定到另外一個節點上。
重啓策略對 Pod 狀態的影響以下:
假設有1個運行中的 Pod,包含1個容器,容器退出成功後。
Always:重啓容器,Pod 狀態仍爲 Running。
OnFailure:Pod 狀態變爲 Completed。
Never:Pod 狀態變爲 Completed。
假設有1個運行中的 Pod,包含1個容器,容器退出失敗後。
Always:重啓容器,Pod 狀態仍爲 Running。
OnFailure:重啓容器,Pod 狀態仍爲 Running。
Never:Pod 狀態變爲 Failed。
假設有1個運行中的 Pod,包含2個容器,第1個容器退出失敗後。
Always:重啓容器,Pod 狀態仍爲 Running。
OnFailure:重啓容器,Pod 狀態仍爲 Running。
Never:不會重啓容器,Pod 狀態仍爲 Completed。
假設第1個容器沒有運行起來,而第2個容器也退出了。
Always:重啓容器,Pod 狀態仍爲 Running。
OnFailure:重啓容器,Pod 狀態仍爲 Running。
Never:Pod 狀態變爲 Failed。
假設有1個運行中的 Pod,包含1個容器,容器發生內存溢出後。
Always:重啓容器,Pod 狀態仍爲 Running。
OnFailure:重啓容器,Pod 狀態仍爲 Running。
Never:記錄失敗事件,Pod 狀態變爲 Failed。
Pod 的建立於銷燬過程
Pod 的建立過程:
kubectl 命令將轉換爲對 API Server 的調用。
API Server 驗證請求並將其保存到 etcd 中。
etcd 通知 API Server。
API Server 調用調度器。
調度器決定在哪一個節點上運行 Pod,並將其返回給 API Server。
API Server 將其對應節點保存到 etcd 中。
etcd 通知 API Server。
API Server 在相應的節點中調用 kubelet。
kubelet 與容器運行時 API 發生交互,與容器守護進程通訊以建立容器。
kubelet 將 Pod 狀態更新到 API Server 中。
API Server 把最新的狀態保存到 etcd 中。
Pod 的銷燬過程:
用戶發送刪除 Pod 的命令。
將會更新 API Server 中的 Pod 對象,設定 Pod 被」銷燬「完成的大體時間(默認 30s),超出這個寬限時間 Pod 將被強制終止。
同時觸發如下操做:Pod 被標記爲 Terminating。kubelet 發現 Pod 已標記爲 Terminating 後,將會執行 Pod 關閉過程。Endpoint 控制器監控到 Pod 即將刪除,將溢出全部 Service 對象中與該 Pod 相關的 Endpoint。
若是 Pod 定義了 preStop 回調,則這會在 Pod 中執行,若是寬限時間到了 preStop 還在運行,則會通知 API Server增長少許寬限時間(2s)。
Pod 中的進程接收到 TERM 信號。
若是寬限時間過時,Pod 中的進行仍在運行,則會被 SIGKILL 信號終止。
kubelet 經過 API Server 設置寬限時間爲 0(當即刪除),完成 Pod 的刪除操做,Pod 從 API 中移除。
刪除操做的延遲時間默認爲 30s。kubectl delete 命令支持--grace-period=秒,用戶能夠自定義延遲時間。若是這個值設置爲0,則表示強制刪除 Pod,可是在使用--grace-period=0 時須要增長選項 --force 才能執行強制刪除。
Pod 的生命週期時間
Pod 在整個運行過程當中,會有兩個大的階段,第一階段是初始化容器運行階段,第二階段是正式容器運行階段,每一個階段都會有不一樣的事件
初始化容器運行階段
Pod 中能夠包含一個或者多個初始化容器,這些容器是在應用程序容器正式運行以前運行的,主要負責一些初始化工做,全部初始化容器執行完後才能執行應用程序容器,所以初始化容器不能是長期運行的容器,而是執行完必定操做後就必須結束的。若是是多個初始化容器,只能順序執行,不能同時運行。在應用程序容器開始前,全部初始化容器都必須正常結束。
初始化容器執行失敗時,若是 restartPolicy 是 OnFailure 或者 Always,那麼會重複執行失敗的初始化容器,直到成功;若是 restartPolicy 是 Never,則不會重啓失敗的初始化容器。
下面咱們舉一個例子,在部署應用程序前,檢測 db 是否就緒,並執行如下初始化腳本。
apiVersion: v1
kind: Pod
metadata:
name: expodinitcontainer
spec:
containers:
- name: maincontainer
image: busybox
command: ['sh', '-c']
args: ['echo "maincontainer is running!"; sleep 3600']
initContainers:
- name: initdbcheck
image: busybox
command: ['sh', '-c']
args: ['echo "checking db!"; sleep 30; echo "checking done!"']
- name: initscript
image: busybox
command: ['sh', '-c']
args: ['echo "init script exec!"; sleep 30; echo "init script exec done!"']
這裏包含一個主容器,兩個初始化容器,每一個初始化容器執行 30s,接下來咱們建立一下 Pod,再查看他們的狀態:
kubectl apply -f expodinitcontainers.yml
在 30s 內執行第一個初始化容器,因此 Pod 狀態是 Init:0/2,在 30s-60s 之間執行第二個初始化容器,因此 Pod 狀態是 Init:1/2,當全部初始化容器執行完畢後,狀態會先變爲 PodInitializing,而後變爲 Running 狀態。經過 kubectl describe 命令查看 Pod 詳細信息,能夠看到先執行 initdbcheck,而後執行 initscript,最後才運行 maincontainer。
正式容器運行階段
正式容器建立成功後,就會觸發 PostStart 事件,在容器運行的過程當中,能夠設置存活探針和就緒探針來持續監測容器的健康情況,在容器結束前,會觸發 PreStop 事件。
PostStart:容器剛建立成功後,觸發此事件,若是回調執行失敗,則容器會被終止,而後根據重啓策略決定是否要重啓該容器。
PreStop:容器開始和結束前,觸發此事件,不管執行結果如何,都會結束容器。
回調的方式有兩種:Exec 執行一段腳本和 HttpGet 執行特定的請求。
Exec 回調會執行特定的命令,若是 Exec 執行的命令最後正常退出,則表明執行成功;不然就認爲執行異常,配置方式以下:
postStart/preStop:
exec:
command: xxxxx # 命令列表
HttpGet 回調會執行特定的 Http 請求,經過返回的狀態碼來判斷該請求執行是否成功,配置方式以下:
postStart/preStop:
httpGet: host: xxxx # 請求的 IP 地址或域名 port: xx # 請求的端口號 path: xxxxxx # 請求的路徑,好比github.com/xingxingzaixian,"/xingxingzaixian"就是路徑
scheme: http/https # 請求的協議,默認爲 HTTP
咱們來舉例使用一下 PostStart 和 PreStop 事件
apiVersion: v1
kind: Pod
metadata:
name: expodpostpre
spec:
containers:
- name: postprecontainer
image: busybox
imagePullPolicy: IfNotPresent
command: ['sh', '-c']
args: ['echo "Hello Kubernetes!"; sleep 3600']
lifecycle:
postStart:
httpGet:
host: www.baidu.com
path: /
port: 80
scheme: HTTP
preStop:
exec:
command: ['sh', '-c', 'echo "preStop callback done!"; sleep 60']
postStart 中咱們執行 HttpGet 回調,訪問了百度首頁,preStop 則執行命令輸出一段文本,而後停留 60s。咱們執行建立命令,觀察一下 Pod 的狀態。
kubectl apply -f expodpostpre.yml
正常狀況下是和以前的是同樣的,咱們作一些測試操做,例如把 postStart 的網址寫一個不可訪問的網址,好比:
host: www.fackbook.com
建立後,查看日誌信息
kubectl logs expodpostpre
還能夠經過 kubectl describe 來查看詳細狀況
Pod 的健康檢查
在容器運行的過程當中,咱們能夠經過探針來持續檢查容器的情況,kubernetes 爲咱們提供了兩種探針:存活探針、就緒探針。
存活探針livenessProbe:檢測容器是否正在運行,若是返回 Failure,kubelet 會終止容器,而後容器會按照重啓策略執行。若是沒有提供存活探針,默認狀態就是 Success。
就緒探針readlinessProbe:檢測容器是否已經能夠啓動了應用服務,若是返回 Failure,Endpoint 控制器就會從全部 Service 的 Endpoint 中移除此 Pod 的 IP 地址。從容器啓動到第一次探測以前,默認的就緒狀態是 Failure。若是沒有提供就緒探針,默認狀態就是 Success。
容器配置當中有 3 種方法來執行探針檢測:exec、tcpSocket、httpGet。
exec:在容器內部執行指定的命令,若是命令以狀態碼「0」退出,則表示診斷成功。配置以下:
livenessProbe/readlinessProbe:
exec:
command: [xxxx] # 命令列表
tcpSocket:對容器的指定端口執行 TCP 檢測。若是端口是打開的,則診斷成功。配置以下:
livenessProbe/readlinessProbe:
tcpSocket:
port: Number # 指定端口號
httpGet:對容器內的 HTTP 服務進行檢測,若是響應的狀態碼範圍爲 200~400,則診斷成功。配置以下:
livenessProbe/readlinessProbe:
httpGet: port: Number # 指定的端口號
path: String # 指定路徑
下面舉幾個例子來展現一下探針的使用。
存活探針的使用
apiVersion: v1
kind: Pod
metadata:
name: expodlive
spec:
containers:
- name: livecontainer
image: busybox
imagePullPolicy: IfNotPresent
command: ['sh', '-c']
args: ['mkdir /files_dir; echo "Hello Kubernetes!" > /files_dir/newfile; sleep 3600']
livenessProbe:
exec:
command: ['cat', '/files_dir/newfile']
先建立一個文件夾 files_dir,再新建一個文件 newfile,寫入 Hello Kubernetes! 內容,而後在存活探針中使用 cat 查看文件內容。執行建立 Pod 命令:
kubectl apply -f expodlive.yml
目前 Pod 運行一切正常,如今咱們能夠作一些破壞性的操做,進入 Pod 內部,刪除 newfile 文件。
[root@k8s-master]# kubectl exec -it expodlive -- /bin/sh
/# rm -f /files_dir/newfile
/# exit
因爲探針按期檢查 /files_dir/newfile 文件是否存在,而咱們的 Pod 默認是異常後重啓,所以能夠經過如下命令查看 Pod 詳細信息:
kubectl describe pods expodlive
能夠看到 Event 中打印了存活探針的運行狀況:存活探針失敗,而且準備重啓。
就緒探針的使用
apiVersion: v1
kind: Pod
metadata:
name: expodread
spec:
containers:
- name: readcontainer
image: nginx
imagePullPolicy: IfNotPresent
ports:
- name: portnginx
containerPort: 80
readinessProbe:
httpGet:
port: 80
path: /
咱們建立了一個 Nginx 容器,經過 containerPort 屬性,將 80 端口暴露出來,而後設置一個就緒探針按期向 80 端口發送 HttpGet 請求,檢測響應範圍是否爲 200~400。使用 apply 命令建立成功後,咱們一樣要進行一些測試性的操做。
[root@k8s-master]# kubectl apply -f expodread.yml
[root@k8s-master]# kubectl exec -it expodread -- /bin/sh
/# nginx -s stop
執行 nginx -s stop 命令後,容器就會退出,由於 Nginx 容器是持續提供服務的,服務中止後,容器就異常了,而後 kubernetes 又會從新拉起一個容器,在這個過程中,咱們使用 kubectl get pod 命令就會發現,容器的狀態先變爲 Completed,而後變爲 CrashLoopBackOff,最後變爲 Running。
對於初學者來說,老是分不清存活探針和就緒探針的區別,什麼狀況下該使用存活探針?什麼狀況下應該使用就緒探針?我給的建議以下:
若是容器中的進程可以在遇到問題或異常的狀況下自行崩潰,就像剛纔的 Nginx 容器,那麼不必定須要存活探針,kubelet 會根據 Pod 的重啓策略自動執行正確的操做。
若是想在探測失敗時終止並重啓容器,則能夠指定存活探針,並將重啓策略設置爲 Always 或 OnFailure。
若是隻想在探針成功時纔對 Pod 發送網絡請求,則能夠指定就緒探針,例如 HttpGet。
若是容器須要在啓動期間處理大型數據、配置文件或遷移,就使用就緒探針。
對於每種探針,還能夠設置 5 個參數:
initialDelaySeconds:啓動容器後首次監控檢測的等待時間,單位爲秒。
timeoutSeconds:發送監控檢測請求後等待響應的超時時間,單位爲秒,若是超時就認爲探測失敗,默認值爲 10s。
periodSeconds:探針的執行週期,默認 10s 執行一次。
successThreshold:若是出現失敗,則須要連續探測成功屢次才能確認診斷成功。默認值爲1.
failureThreshold:若是出現失敗,則要連續失敗屢次才重啓 Pod(對於存活探針)或標記爲 Unready(對於就緒探針)。默認值爲3。
具體設置方法以下:
livenessProbe/readinessProbe:
exec/tcpSocket/httpGet: initialDelaySeconds: Number
timeoutSeconds: Number
periodSeconds: Number
successThreshold: Number
failureThreshold: Number
總結
這篇文章咱們主要講了 Pod 的增刪改查,Pod 與容器的關係,如何組織容器,Pod 的生命週期及對應事件,以及 Pod 的健康檢查機制。
本文分享自微信公衆號 - python爬蟲實戰之路(small_bud1989)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。