Kubernetes 存活、就緒和啟動探針

Kubernetes主要有三中探針:存活(Liveness)、就緒(Readiness)和啟動(Startup)探針。

  • kubelet 使用存活探針來確定什么時候要重啟容器。 例如,存活探針可以探測到應用死鎖(應用程序在運行,但是無法繼續執行后面的步驟)情況。 重啟這種狀態下的容器有助于提高應用的可用性,即使其中存在缺陷。

  • kubelet 使用就緒探針可以知道容器何時準備好接受請求流量,當一個 Pod 內的所有容器都就緒時,才能認為該 Pod 就緒。 這種信號的一個用途就是控制哪個 Pod 作為 Service 的后端。 若 Pod 尚未就緒,會被從 Service 的負載均衡器中剔除。

  • kubelet 使用啟動探針來了解應用容器何時啟動。 如果配置了這類探針,你就可以控制容器在啟動成功后再進行存活性和就緒態檢查, 確保這些存活、就緒探針不會影響應用的啟動。 啟動探針可以用于對慢啟動容器進行存活性檢測,避免它們在啟動運行之前就被殺掉。

注意:
存活探針是一種從應用故障中恢復的強勁方式,但應謹慎使用。 你必須仔細配置存活探針,確保它能真正標示出不可恢復的應用故障,例如死鎖。

說明:
錯誤的存活探針可能會導致級聯故障。 這會導致在高負載下容器重啟;例如由于應用程序無法擴展,導致客戶端請求失敗;以及由于某些 Pod 失敗而導致剩余 Pod 的工作負載增加。了解就緒探針和存活探針之間的區別, 以及何時為應用程序配置使用它們非常重要。

存活探測

定義存活命令

許多長時間運行的應用最終會進入損壞狀態,除非重新啟動,否則無法被恢復。 Kubernetes 提供了存活探針來發現并處理這種情況。

在本練習中,你會創建一個 Pod,其中運行一個基于 registry.k8s.io/busybox 鏡像的容器。 下面是這個 Pod 的配置文件。

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-exec
spec:
  containers:
  - name: liveness
    image: registry.k8s.io/busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 600
    livenessProbe:
      exec:
        command:
        - cat
        - /tmp/healthy
      initialDelaySeconds: 5
      periodSeconds: 5

在這個配置文件中,可以看到 Pod 中只有一個 Container。 periodSeconds 字段指定了 kubelet 應該每 5 秒執行一次存活探測。 initialDelaySeconds 字段告訴 kubelet 在執行第一次探測前應該等待 5 秒。 kubelet 在容器內執行命令 cat /tmp/healthy 來進行探測。 如果命令執行成功并且返回值為 0,kubelet 就會認為這個容器是健康存活的。 如果這個命令返回非 0 值,kubelet 會殺死這個容器并重新啟動它。

當容器啟動時,執行如下的命令:

/bin/sh -c "touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 600"

這個容器生命的前 30 秒,/tmp/healthy 文件是存在的。 所以在這最開始的 30 秒內,執行命令 cat /tmp/healthy 會返回成功代碼。 30 秒之后,執行命令 cat /tmp/healthy 就會返回失敗代碼。

創建 Pod:

kubectl apply -f https://k8s.io/examples/pods/probe/exec-liveness.yaml

在 30 秒內,查看 Pod 的事件:

kubectl describe pod liveness-exec

輸出結果表明還沒有存活探針失敗:

Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  11s   default-scheduler  Successfully assigned default/liveness-exec to node01
  Normal  Pulling    9s    kubelet, node01    Pulling image "registry.k8s.io/busybox"
  Normal  Pulled     7s    kubelet, node01    Successfully pulled image "registry.k8s.io/busybox"
  Normal  Created    7s    kubelet, node01    Created container liveness
  Normal  Started    7s    kubelet, node01    Started container liveness

35 秒之后,再來看 Pod 的事件:

kubectl describe pod liveness-exec

在輸出結果的最下面,有信息顯示存活探針失敗了,這個失敗的容器被殺死并且被重建了。

  Type     Reason     Age                From               Message
  ----     ------     ----               ----               -------
  Normal   Scheduled  57s                default-scheduler  Successfully assigned default/liveness-exec to node01
  Normal   Pulling    55s                kubelet, node01    Pulling image "registry.k8s.io/busybox"
  Normal   Pulled     53s                kubelet, node01    Successfully pulled image "registry.k8s.io/busybox"
  Normal   Created    53s                kubelet, node01    Created container liveness
  Normal   Started    53s                kubelet, node01    Started container liveness
  Warning  Unhealthy  10s (x3 over 20s)  kubelet, node01    Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
  Normal   Killing    10s                kubelet, node01    Container liveness failed liveness probe, will be restarted

再等 30 秒,確認這個容器被重啟了:

kubectl get pod liveness-exec

輸出結果顯示 RESTARTS 的值增加了 1。 請注意,一旦失敗的容器恢復為運行狀態,RESTARTS 計數器就會增加 1:

NAME            READY     STATUS    RESTARTS   AGE
liveness-exec   1/1       Running   1          1m

定義一個存活態 HTTP 請求接口

另外一種類型的存活探測方式是使用 HTTP GET 請求。 下面是一個 Pod 的配置文件,其中運行一個基于 registry.k8s.io/liveness 鏡像的容器。

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-http
spec:
  containers:
  - name: liveness
    image: registry.k8s.io/liveness
    args:
    - /server
    livenessProbe:
      httpGet:
        path: /healthz
        port: 8080
        httpHeaders:
        - name: Custom-Header
          value: Awesome
      initialDelaySeconds: 3
      periodSeconds: 3

在這個配置文件中,你可以看到 Pod 也只有一個容器。 periodSeconds 字段指定了 kubelet 每隔 3 秒執行一次存活探測。 initialDelaySeconds 字段告訴 kubelet 在執行第一次探測前應該等待 3 秒。 kubelet 會向容器內運行的服務(服務在監聽 8080 端口)發送一個 HTTP GET 請求來執行探測。 如果服務器上/healthz 路徑下的處理程序返回成功代碼,則 kubelet 認為容器是健康存活的。 如果處理程序返回失敗代碼,則 kubelet 會殺死這個容器并將其重啟。

返回大于或等于 200 并且小于 400 的任何代碼都標示成功,其它返回代碼都標示失敗。

你可以訪問 server.go 閱讀服務的源碼。 容器存活期間的最開始 10 秒中,/healthz 處理程序返回 200 的狀態碼。 之后處理程序返回 500 的狀態碼。

http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
    duration := time.Now().Sub(started)
    if duration.Seconds() > 10 {
        w.WriteHeader(500)
        w.Write([]byte(fmt.Sprintf("error: %v", duration.Seconds())))
    } else {
        w.WriteHeader(200)
        w.Write([]byte("ok"))
    }
})

kubelet 在容器啟動之后 3 秒開始執行健康檢測。所以前幾次健康檢查都是成功的。 但是 10 秒之后,健康檢查會失敗,并且 kubelet 會殺死容器再重新啟動容器。

創建一個 Pod 來測試 HTTP 的存活檢測:

kubectl apply -f https://k8s.io/examples/pods/probe/http-liveness.yaml

10 秒之后,通過查看 Pod 事件來確認存活探針已經失敗,并且容器被重新啟動了。

kubectl describe pod liveness-http

在 1.13 之前(包括 1.13)的版本中,如果在 Pod 運行的節點上設置了環境變量 http_proxy(或者 HTTP_PROXY),HTTP 的存活探測會使用這個代理。 在 1.13 之后的版本中,設置本地的 HTTP 代理環境變量不會影響 HTTP 的存活探測。

定義 TCP 的存活探測

第三種類型的存活探測是使用 TCP 套接字。 使用這種配置時,kubelet 會嘗試在指定端口和容器建立套接字鏈接。 如果能建立連接,這個容器就被看作是健康的,如果不能則這個容器就被看作是有問題的。

apiVersion: v1
kind: Pod
metadata:
  name: goproxy
  labels:
    app: goproxy
spec:
  containers:
  - name: goproxy
    image: registry.k8s.io/goproxy:0.1
    ports:
    - containerPort: 8080
    readinessProbe:
      tcpSocket:
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 10
    livenessProbe:
      tcpSocket:
        port: 8080
      initialDelaySeconds: 15
      periodSeconds: 20

如你所見,TCP 檢測的配置和 HTTP 檢測非常相似。 下面這個例子同時使用就緒和存活探針。kubelet 會在容器啟動 5 秒后發送第一個就緒探針。 探針會嘗試連接 goproxy 容器的 8080 端口。 如果探測成功,這個 Pod 會被標記為就緒狀態,kubelet 將繼續每隔 10 秒運行一次探測。

除了就緒探針,這個配置包括了一個存活探針。 kubelet 會在容器啟動 15 秒后進行第一次存活探測。 與就緒探針類似,存活探針會嘗試連接 goproxy 容器的 8080 端口。 如果存活探測失敗,容器會被重新啟動。

kubectl apply -f https://k8s.io/examples/pods/probe/tcp-liveness-readiness.yaml

15 秒之后,通過看 Pod 事件來檢測存活探針:

kubectl describe pod goproxy

定義 gRPC 存活探針

特性狀態: Kubernetes v1.24 [beta]

如果你的應用實現了 gRPC 健康檢查協議, kubelet 可以配置為使用該協議來執行應用存活性檢查。 你必須啟用 GRPCContainerProbe 特性門控 才能配置依賴于 gRPC 的檢查機制。

這個例子展示了如何配置 Kubernetes 以將其用于應用程序的存活性檢查。 類似地,你可以配置就緒探針和啟動探針。

下面是一個示例清單:

apiVersion: v1
kind: Pod
metadata:
  name: etcd-with-grpc
spec:
  containers:
  - name: etcd
    image: registry.k8s.io/etcd:3.5.1-0
    command: [ "/usr/local/bin/etcd", "--data-dir",  "/var/lib/etcd", "--listen-client-urls", "http://0.0.0.0:2379", "--advertise-client-urls", "http://127.0.0.1:2379", "--log-level", "debug"]
    ports:
    - containerPort: 2379
    livenessProbe:
      grpc:
        port: 2379
      initialDelaySeconds: 10

要使用 gRPC 探針,必須配置 port 屬性。 如果要區分不同類型的探針和不同功能的探針,可以使用 service 字段。 你可以將 service 設置為 liveness,并使你的 gRPC 健康檢查端點對該請求的響應與將 service 設置為 readiness 時不同。 這使你可以使用相同的端點進行不同類型的容器健康檢查(而不需要在兩個不同的端口上偵聽)。 如果你想指定自己的自定義服務名稱并指定探測類型,Kubernetes 項目建議你使用使用一個可以關聯服務和探測類型的名稱來命名。 例如:myservice-liveness(使用 - 作為分隔符)。

說明:
與 HTTP 和 TCP 探針不同,gRPC 探測不能使用按名稱指定端口, 也不能自定義主機名。

配置問題(例如:錯誤的 port 和 service、未實現健康檢查協議) 都被認作是探測失敗,這一點與 HTTP 和 TCP 探針類似。

kubectl apply -f https://k8s.io/examples/pods/probe/grpc-liveness.yaml

15 秒鐘之后,查看 Pod 事件確認存活性檢查并未失敗:

kubectl describe pod etcd-with-grpc

在 Kubernetes 1.23 之前,gRPC 健康探測通常使用 grpc-health-probe 來實現,如博客 Health checking gRPC servers on Kubernetes(對 Kubernetes 上的 gRPC 服務器執行健康檢查)所描述。 內置的 gRPC 探針行為與 grpc-health-probe 所實現的行為類似。 從 grpc-health-probe 遷移到內置探針時,請注意以下差異:

  • 內置探針運行時針對的是 Pod 的 IP 地址,不像 grpc-health-probe 那樣通常針對 127.0.0.1 執行探測; 請一定配置你的 gRPC 端點使之監聽于 Pod 的 IP 地址之上。
  • 內置探針不支持任何身份認證參數(例如 -tls)。
  • 對于內置的探針而言,不存在錯誤代碼。所有錯誤都被視作探測失敗。
  • 如果 ExecProbeTimeout 特性門控被設置為 false,則 grpc-health-probe 不會考慮 timeoutSeconds 設置狀態(默認值為 1s), 而內置探針則會在超時時返回失敗。

使用命名端口

對于 HTTP 和 TCP 存活檢測可以使用命名的 port(gRPC 探針不支持使用命名端口)。

例如:

ports:
- name: liveness-port
  containerPort: 8080
  hostPort: 8080

livenessProbe:
  httpGet:
    path: /healthz
    port: liveness-port

啟動探針

使用啟動探針保護慢啟動容器,有時候,會有一些現有的應用在啟動時需要較長的初始化時間。針對 HTTP 或 TCP 檢測,可以通過將 failureThreshold * periodSeconds 參數設置為足夠長的時間來應對糟糕情況下的啟動時間。

這樣,前面的例子就變成了:

ports:
- name: liveness-port
  containerPort: 8080
  hostPort: 8080

livenessProbe:
  httpGet:
    path: /healthz
    port: liveness-port
  failureThreshold: 1
  periodSeconds: 10

startupProbe:
  httpGet:
    path: /healthz
    port: liveness-port
  failureThreshold: 30
  periodSeconds: 10

應用程序將會有最多 5 分鐘(30 * 10 = 300s)的時間來完成其啟動過程。 一旦啟動探測成功一次,存活探測任務就會接管對容器的探測,對容器死鎖作出快速響應。 如果啟動探測一直沒有成功,容器會在 300 秒后被殺死,并且根據 restartPolicy 來執行進一步處置。

就緒探針

有時候,應用會暫時性地無法為請求提供服務。 例如,應用在啟動時可能需要加載大量的數據或配置文件,或是啟動后要依賴等待外部服務。 在這種情況下,既不想殺死應用,也不想給它發送請求。 Kubernetes 提供了就緒探針來發現并緩解這些情況。 容器所在 Pod 上報還未就緒的信息,并且不接受通過 Kubernetes Service 的流量。

說明:
就緒探針在容器的整個生命周期中保持運行狀態。

注意:
存活探針不等待就緒性探針成功。 如果要在執行存活探針之前需要等待,應該使用 initialDelaySecondsstartupProbe

就緒探針的配置和存活探針的配置相似。 唯一區別就是要使用 readinessProbe 字段,而不是 livenessProbe 字段。

readinessProbe:
  exec:
    command:
    - cat
    - /tmp/healthy
  initialDelaySeconds: 5
  periodSeconds: 5

HTTP 和 TCP 的就緒探針配置也和存活探針的配置完全相同。

就緒和存活探測可以在同一個容器上并行使用。 兩者共同使用,可以確保流量不會發給還未就緒的容器,當這些探測失敗時容器會被重新啟動。

探針配置

Probe 有很多配置字段,可以使用這些字段精確地控制啟動、存活和就緒檢測的行為:

  • initialDelaySeconds:容器啟動后要等待多少秒后才啟動啟動、存活和就緒探針, 默認是 0 秒,最小值是 0。
  • periodSeconds:執行探測的時間間隔(單位是秒)。默認是 10 秒。最小值是 1。
  • timeoutSeconds:探測的超時后等待多少秒。默認值是 1 秒。最小值是 1。
  • successThreshold:探針在失敗后,被視為成功的最小連續成功數。默認值是 1。 存活和啟動探測的這個值必須是 1。最小值是 1。
  • failureThreshold:探針連續失敗了 failureThreshold 次之后, Kubernetes 認為總體上檢查已失敗:容器狀態未就緒、不健康、不活躍。 對于啟動探針存活探針而言,如果至少有 failureThreshold 個探針已失敗, Kubernetes 會將容器視為不健康并為這個特定的容器觸發重啟操作。kubelet 會考慮該容器的 terminationGracePeriodSeconds 設置。 對于失敗的就緒探針kubelet 繼續運行檢查失敗的容器,并繼續運行更多探針; 因為檢查失敗,kubeletPodReady 狀況設置為 false。
  • terminationGracePeriodSeconds:為 kubelet 配置從為失敗的容器觸發終止操作到強制容器運行時停止該容器之前等待的寬限時長。 默認值是繼承 Pod 級別的 terminationGracePeriodSeconds 值(如果不設置則為 30 秒),最小值為 1。 更多細節請參見探針級別 terminationGracePeriodSeconds。

說明:
在 Kubernetes 1.20 版本之前,exec 探針會忽略 timeoutSeconds: 探針會無限期地持續運行,甚至可能超過所配置的限期,直到返回結果為止。

這一缺陷在 Kubernetes v1.20 版本中得到修復。你可能一直依賴于之前錯誤的探測行為, 甚至都沒有覺察到這一問題的存在,因為默認的超時值是 1 秒鐘。 作為集群管理員,你可以在所有的 kubelet 上禁用 ExecProbeTimeout 特性門控 (將其設置為 false),從而恢復之前版本中的運行行為。之后當集群中所有的 exec 探針都設置了 timeoutSeconds 參數后,移除此標志重載。 如果你有 Pod 受到此默認 1 秒鐘超時值的影響,你應該更新這些 Pod 對應的探針的超時值, 這樣才能為最終去除該特性門控做好準備。

當此缺陷被修復之后,在使用 dockershim 容器運行時的 Kubernetes 1.20+ 版本中,對于 exec 探針而言,容器中的進程可能會因為超時值的設置保持持續運行, 即使探針返回了失敗狀態。

注意:
如果就緒態探針的實現不正確,可能會導致容器中進程的數量不斷上升。 如果不對其采取措施,很可能導致資源枯竭的狀況。

HTTP 探測

HTTP Probes 允許針對 httpGet 配置額外的字段:

  • host:連接使用的主機名,默認是 Pod 的 IP。也可以在 HTTP 頭中設置 "Host" 來代替。
  • scheme:用于設置連接主機的方式(HTTP 還是 HTTPS)。默認是 "HTTP"。
  • path:訪問 HTTP 服務的路徑。默認值為 "/"。
  • httpHeaders:請求中自定義的 HTTP 頭。HTTP 頭字段允許重復。
  • port:訪問容器的端口號或者端口名。如果數字必須在 1~65535 之間。

對于 HTTP 探測,kubelet 發送一個 HTTP 請求到指定的路徑和端口來執行檢測。 除非 httpGet 中的 host 字段設置了,否則 kubelet 默認是給 Pod 的 IP 地址發送探測。 如果 scheme 字段設置為了 HTTPSkubelet 會跳過證書驗證發送 HTTPS 請求。 大多數情況下,不需要設置 host 字段。 這里有個需要設置 host 字段的場景,假設容器監聽 127.0.0.1,并且 Pod 的 hostNetwork 字段設置為了 true。那么 httpGet 中的 host 字段應該設置為 127.0.0.1。 可能更常見的情況是如果 Pod 依賴虛擬主機,你不應該設置 host 字段,而是應該在 httpHeaders 中設置 Host。

針對 HTTP 探針,kubelet 除了必需的 Host 頭部之外還發送兩個請求頭部字段:User-AgentAccept。這些頭部的默認值分別是 kube-probe/{{ skew currentVersion >}}(其中 1.27 是 kubelet 的版本號)和 */*

你可以通過為探測設置 .httpHeaders來重載默認的頭部字段值;例如:

livenessProbe:
  httpGet:
    httpHeaders:
      - name: Accept
        value: application/json

startupProbe:
  httpGet:
    httpHeaders:
      - name: User-Agent
        value: MyUserAgent

你也可以通過將這些頭部字段定義為空值,從請求中去掉這些頭部字段。

livenessProbe:
  httpGet:
    httpHeaders:
      - name: Accept
        value: ""

startupProbe:
  httpGet:
    httpHeaders:
      - name: User-Agent
        value: ""

TCP 探測

對于 TCP 探測而言,kubelet 在節點上(不是在 Pod 里面)發起探測連接, 這意味著你不能在 host 參數上配置服務名稱,因為 kubelet 不能解析服務名稱。

探針層面的 terminationGracePeriodSeconds

特性狀態: Kubernetes v1.27 [stable]
在 1.21 發行版之前,Pod 層面的 terminationGracePeriodSeconds 被用來終止存活探測或啟動探測失敗的容器。 這一行為上的關聯不是我們想要的,可能導致 Pod 層面設置了 terminationGracePeriodSeconds 時容器要花非常長的時間才能重新啟動。

在 1.21 及更高版本中,用戶可以指定一個探針層面的 terminationGracePeriodSeconds 作為探針規約的一部分。 當 Pod 層面和探針層面的 terminationGracePeriodSeconds 都已設置,kubelet 將使用探針層面設置的值。

說明:
從 Kubernetes 1.25 開始,默認啟用 ProbeTerminationGracePeriod 特性。 選擇禁用此特性的用戶,請注意以下事項:

ProbeTerminationGracePeriod 特性門控只能用在 API 服務器上。 kubelet 始終優先選用探針級別 terminationGracePeriodSeconds 字段 (如果它存在于 Pod 上)。
如果你已經為現有 Pod 設置了 terminationGracePeriodSeconds 字段并且不再希望使用針對每個探針的終止寬限期,則必須刪除現有的這類 Pod。
當你(或控制平面或某些其他組件)創建替換 Pod,并且特性門控 ProbeTerminationGracePeriod 被禁用時,即使 Pod 或 Pod 模板指定了 terminationGracePeriodSeconds 字段, API 服務器也會忽略探針級別的 terminationGracePeriodSeconds 字段設置。
例如:

spec:
  terminationGracePeriodSeconds: 3600  # Pod 級別設置
  containers:
  - name: test
    image: ...

    ports:
    - name: liveness-port
      containerPort: 8080
      hostPort: 8080

    livenessProbe:
      httpGet:
        path: /healthz
        port: liveness-port
      failureThreshold: 1
      periodSeconds: 60
      # 重載 Pod 級別的 terminationGracePeriodSeconds
      terminationGracePeriodSeconds: 60

探針層面的 terminationGracePeriodSeconds 不能用于就緒態探針。 這一設置將被 API 服務器拒絕。

完整示例

apiVersion: apps/v1
kind: Deployment
metadata:                        # metadata字段包含對Deployment的描述信息
  name: pipeline-test-deployment
  namespace: test
  labels:
    app: pipeline-test-pod      # 標簽字段用于識別Pod
spec:
  replicas: 2                   # 定義副本數量
  selector:
    matchLabels:
      app: pipeline-test-pod
  template:
    metadata:
      labels:
        app: pipeline-test-pod
    spec:
      containers:
      # 定義nginx容器
      - name: pipeline-test
        image: 192.168.232.7:80/repository/pipeline-test:v1.0.0
        imagePullPolicy: Always # 定義拉取鏡像的方式(每次都拉取)
        ports:
        - containerPort: 80
          protocol: TCP
        resources:
          requests:
            cpu: 100m            # 請求時申請CPU資源為0.2核
            memory: 256Mi        # 請求時申請內存資源為256M
          limits:
            cpu: 500m            # 限定CPU資源上限為0.5核
            memory: 512Mi        # 限定內存資源上限為512M
        livenessProbe:           # 定義存活探測
          httpGet:
            path: /              # 探測路徑
            port: 80             # 探測端口
            httpHeaders:         # 定義請求頭
              - name: Custom-Header
                value: Awesome
          initialDelaySeconds: 10 # 第一次探活前,延遲10秒
          periodSeconds: 10       # 每間隔10秒進行一次探活
          timeoutSeconds: 3       # 每次探測的超時時間
          failureThreshold: 5     # 探針連續失敗了 5 次之后Kubernetes認為服務死亡(容器狀態未就緒、不健康、不活躍)
        readinessProbe:           # 定義就緒探測
          httpGet:
            path: /              # 探測路徑
            port: 80             # 探測端口
            httpHeaders:         # 定義請求頭
              - name: Custom-Header
                value: Awesome
          initialDelaySeconds: 10 # 第一次探活前,延遲10秒
          periodSeconds: 10       # 每間隔10秒進行一次探活
          timeoutSeconds: 3       # 每次探測的超時時間
          failureThreshold: 5     # 探針連續失敗了 5 次之后Kubernetes認為服務死亡(容器狀態未就緒、不健康、不活躍)
          successThreshold: 1     # 探針在失敗后,連續1次探測到成功就任務服務恢復

k8s官方文檔

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容