聊聊 Docker 容器的資源管理

Docker 上手很容易,但如果將其應用於生產環境,則需要對它有更深入的理解。只有這樣,才能確保應用符合我們的預期,或在遇到問題時可及時解決。所以,要想真正掌握 Docker 的核心知識,只靠網絡上零散的信息往往是不夠的,必須系統性地學習。

容器,作為 Docker 的核心特性之一,是 Docker 使用者們無法迴避的重要知識點。要想了解容器的核心原理,甚至自己動手寫容器,不深入了解容器資源管理的相關的內容是絕對不行的。

本文將以容器資源管理為主題,解決以下三個問題:

  • 哪些分配給容器的資源可被我們管理?
  • 容器實際使用了多少資源?
  • 如何對容器使用的資源進行管理?

資源類型

對於第一個問題,當我們啟動一個容器的時候,它可以使用一些系統資源,這與我們在物理機上啟動程序基本是一致的。比如主要的幾類:

  • CPU
  • 內存
  • 網絡
  • I/O
  • GPU

這些系統資源是在我們啟動容器時,需要考慮和可被我們管理的。比如,我們可以執行 docker run --help 查看 docker run 命令所支持的全部參數。現在 docker run 命令所支持的參數已超過 90 項,這裡就不一一列出了。

查看容器佔用資源

docker stats

Docker 提供了一個很方便的命令 docker stats,可供我們查看和統計容器所佔用的資源情況。

我們仍然啟動一個 Redis 容器作為示例。

  # 啟動一個容器  (MoeLove) ➜  ~ docker run -d redis  c98c9831ee73e9b71719b404f5ecf3b408de0b69aec0f781e42d815575d28ada  # 查看其所佔用資源的情況  (MoeLove) ➜  ~ docker stats --no-stream $(docker ps -ql)  CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS  c98c9831ee73        amazing_torvalds    0.08%               2.613MiB / 15.56GiB   0.02%               3.66kB / 0B         0B / 0B             4

這裡傳遞了一個 --no-stream 的參數,是因為 docker stats 命令默認是一個持續的動態流式輸出(每秒一次),給它傳遞 --no-stream 參數後,它就只輸出一次便會退出了。

接下來我為你介紹下它輸出內容的含義:

  • Container ID:容器的 ID,也是一個容器生命周期內不會變更的信息。
  • Name:容器的名稱,如果沒有手動使用 --name 參數指定,則 Docker 會隨機生成一個,運行過程中也可以通過命令修改。
  • CPU %:容器正在使用的 CPU 資源的百分比,這裏面涉及了比較多細節,下面會詳細說。
  • Mem Usage/Limit:當前內存的使用及容器可用的最大內存,這裡我使用了一台 16G 的電腦進行測試。
  • Mem %:容器正在使用的內存資源的百分比
  • Net I/O:容器通過其網絡接口發送和接受到的數據量。
  • Block I/O:容器通過塊設備讀取和寫入的數據量。
  • Pids:容器創建的進程或線程數。

docker top

除了上面提到的 docker stats 命令外,Docker 也提供了另一個比較簡單的命令 docker top,與我們平時用的 ps 命令基本一致, 也支持 ps 命令的參數。

  (MoeLove) ➜  ~ docker top $(docker ps -ql)  UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD  systemd+            6275                6248                0                   16:50               ?                   00:00:24            redis-server *:6379  # 可以使用 ps 命令的參數  (MoeLove) ➜  ~ docker top $(docker ps -ql)  -o pid,stat,cmd  PID                 STAT                CMD  6275                Ssl                 redis-server *:6379

管理容器的 CPU 資源

在我們使用容器的時候,CPU 和內存是我們尤為關注的資源。不過,對於 CPU 資源的管理,涉及的內容會比較偏底層一些,有些涉及到了內核的 CPU 調度器,比如 CFS(Completely Fair Scheduler)等。

我們可以先來查看下 Docker 提供了哪些控制 CPU 資源相關的參數。使用 docker run --help |grep CPU 即可查看。

(MoeLove) ➜  ~ docker run --help |grep CPU        --cpu-period int                 Limit CPU CFS (Completely Fair Scheduler) period        --cpu-quota int                  Limit CPU CFS (Completely Fair Scheduler) quota        --cpu-rt-period int              Limit CPU real-time period in microseconds        --cpu-rt-runtime int             Limit CPU real-time runtime in microseconds    -c, --cpu-shares int                 CPU shares (relative weight)        --cpus decimal                   Number of CPUs        --cpuset-cpus string             CPUs in which to allow execution (0-3, 0,1)  

這裡暫時先不對參數的具體含義進行深入展開,我們直接以幾個示例來分別進行說明,幫助大家理解。

默認無限制

備註:我這裡以一個 4 核 CPU 的電腦進行演示。

現在我們啟動一個容器,我們以體積很小的 Alpine Linux 為例好了。

(MoeLove) ➜  ~ docker run --rm -it alpine / #

在另一個窗口,執行上面介紹的查看容器資源的命令:

  (MoeLove) ➜  ~ docker stats --no-stream $(docker ps -ql)  CONTAINER ID        NAME                    CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS  106a24399bc9        friendly_varahamihira   0.00%               1.047MiB / 15.56GiB   0.01%               5.01kB / 0B         1.67MB / 0B         1

可以看到,當前容器內沒有過多的 CPU 消耗,且 PIDS 為 1,表示當前只有一個進程。

現在我們回到剛才啟動的容器,執行以下命

sha256sum /dev/zero
  • sha256sum 是一個用於計算和檢查 SHA256 信息的命令行工具;
  • /dev/zero 是 Linux 系統上一個特殊的設備,在讀它時,它可以提供無限的空字符串(NULL 或者 0x00 之類的)。

所以上面的命令,會**讓 sha256sum 持續地讀 /dev/zero 產生的空串,並進行計算。**這將迅速地消耗 CPU 資源。

我們來看看此時容器的資源使用情況:

(MoeLove) ➜  ~ docker stats --no-stream $(docker ps -ql)  CONTAINER ID        NAME                    CPU %               MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDS  106a24399bc9        friendly_varahamihira   100.59%             1.5MiB / 15.56GiB   0.01%               14.4kB / 0B         1.99MB / 0B         2  (MoeLove) ➜  ~ docker top $(docker ps -ql) -o pid,c,cmd  PID                 C                   CMD  825                 0                   /bin/sh  965                 99                  sha256sum /dev/zero  

可以看到當前的 CPU 使用率已經在 100% 左右了。

我們再新打開一個窗口,進入容器內,執行相同的命令:

(MoeLove) ➜  ~ docker exec -it $(docker ps -ql) sh  / # sha256sum /dev/zero  

查看容器使用資源的情況:

(MoeLove) ➜  ~ docker stats --no-stream $(docker ps -ql)  CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS  f359d4ff6fc6        nice_zhukovsky      200.79%             1.793MiB / 15.56GiB   0.01%               4.58kB / 0B         0B / 0B             4  (MoeLove) ➜  ~ docker top $(docker ps -ql) -o pid,c,cmd  PID                 C                   CMD  825                 0                   /bin/sh  965                 99                  sha256sum /dev/zero  1236                0                   sh  1297                99                  sha256sum /dev/zero

可以看到現在兩個進程,已經讓兩個 CPU 滿負載運行了。這裡需要額外說明的是,選擇 sha256sum 作為示例,是因為它是單線程程序,每次啟動一個 sha256sum 並不會消耗其他 CPU 核的資源。

由此可以得出的結論是,如果不對容器內程序進行 CPU 資源限制,其可能會消耗掉大量 CPU 資源,進而影響其他程序或者影響系統的穩定。

分配 0.5 CPU

那接下來,我們對這個容器進行 CPU 資源的限制,比如限制它只可以使用 0.5 CPU。

我們可以重新啟動一個容器,在 docker run 時,為它添加資源限制。

但我來給你介紹一種動態更改資源限制的辦法,使用 docker update 命令。例如,在此例子中,我們使用如下命令,限制該容器只能使用 0.5 CPU。

(MoeLove) ➜  ~ docker update --cpus "0.5" $(docker ps -ql)f359d4ff6fc6

為了方便,我們直接關閉剛才的 sha256sum 進程,按 Ctrl+c 終止進程。然後重新運行該命令

# 終止進程/ # sha256sum /dev/zero    ^C# 啟動程序/ # sha256sum /dev/zero

查看資源佔用情況:

(MoeLove) ➜  ~ docker stats --no-stream $(docker ps -ql)  CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS  f359d4ff6fc6        nice_zhukovsky      49.87%              1.777MiB / 15.56GiB   0.01%               112kB / 0B          1.59MB / 0B         3    (MoeLove) ➜  ~ docker top $(docker ps -ql) -o pid,c,cmd  PID                 C                   CMD  825                 0                   /bin/sh  1236                0                   sh  7662                49                  sha256sum /dev/zero  

可以看到,該進程使用了 50% 左右的 CPU。我們接下來再啟動另一個 sha256sum 的進程:

/ # sha256sum /dev/zero

查看資源使用情況:

(MoeLove) ➜  ~ docker stats --no-stream $(docker ps -ql)  CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS  f359d4ff6fc6        nice_zhukovsky      49.87%              1.777MiB / 15.56GiB   0.01%               112kB / 0B          1.59MB / 0B         3    (MoeLove) ➜  ~ docker top $(docker ps -ql) -o pid,c,cmd  PID                 C                   CMD  825                 0                   /bin/sh  1236                0                   sh  7662                49                  sha256sum /dev/zero  

可以看到,該容器整體佔用了 50% 的 CPU,而其中的兩個 sha256sum 進程則各佔了 25%。

我們已經成功的按預期為它分配了 0.5 CPU。

分配 1.5 CPU

接下來,重複上述步驟,但是為它分配 1.5 CPU,來看看它的實際情況如何。

# 更新配置,使用 1.5 CPU(MoeLove) ➜  ~ docker update --cpus "1.5" $(docker ps -ql)f359d4ff6fc6

分別使用之前的兩個窗口,執行 sha256sum /dev/zero 進行測試:

/ # sha256sum /dev/zero

查看資源使用情況:

(MoeLove) ➜  ~ docker stats --no-stream $(docker ps -ql)  CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS  f359d4ff6fc6        nice_zhukovsky      49.87%              1.777MiB / 15.56GiB   0.01%               112kB / 0B          1.59MB / 0B         3    (MoeLove) ➜  ~ docker top $(docker ps -ql) -o pid,c,cmd  PID                 C                   CMD  825                 0                   /bin/sh  1236                0                   sh  7662                49                  sha256sum /dev/zero  

可以看到,結果與我們的預期基本相符,150% 左右的 CPU,而兩個測試進程,也差不多是平分了 CPU 資源。

指定可使用 CPU 核

可以使用 --cpuset-cpus 來指定分配可使用的 CPU 核,這裡我指定為 0,表示使用第一個 CPU 核。

(MoeLove) ➜  ~ docker update --cpus "1.5" --cpuset-cpus 0  $(docker ps -ql)f359d4ff6fc6

分別使用之前的兩個窗口,執行 sha256sum /dev/zero 進行測試:

/ # sha256sum /dev/zero

查看資源情況:

(MoeLove) ➜  ~ docker stats --no-stream $(docker ps -ql)  CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS  f359d4ff6fc6        nice_zhukovsky      50.92%              1.891MiB / 15.56GiB   0.01%               113kB / 0B          1.59MB / 0B         4    (MoeLove) ➜  ~ docker top $(docker ps -ql) -o pid,c,cmd  PID                 C                   CMD  825                 0                   /bin/sh  1236                0                   sh  10106               25                  sha256sum /dev/zero  11999               25                  sha256sum /dev/zer

可以看到,雖然我們仍然使用 --cpus 指定了 1.5 CPU,但由於使用 --cpuset-cpus 限制只允許它跑在第一個 CPU 上,所以這兩個測試進程也就只能評分該 CPU 了。

本文節選自專欄

小結

通過上述的示例,我介紹了如何通過 --cpus 參數限制容器可使用的 CPU 資源;通過 --cpuset-cpus 參數可指定容器內進程運行所用的 CPU 核心;通過 docker update 可直接更新一個正在運行的容器的相關配置。

現在我們回到前面使用 docker run --help | grep CPU,查看 Docker 支持的對容器 CPU 相關參數的選項:

  (MoeLove) ➜  ~ docker run --help |grep CPU        --cpu-period int                 Limit CPU CFS (Completely Fair Scheduler) period        --cpu-quota int                  Limit CPU CFS (Completely Fair Scheduler) quota        --cpu-rt-period int              Limit CPU real-time period in microseconds        --cpu-rt-runtime int             Limit CPU real-time runtime in microseconds    -c, --cpu-shares int                 CPU shares (relative weight)        --cpus decimal                   Number of CPUs        --cpuset-cpus string             CPUs in which to allow execution (0-3, 0,1)

--cpus 是在 Docker 1.13 時新增的,可用於替代原先的 --cpu-period--cpu-quota。這三個參數通過 cgroups 最終會實際影響 Linux 內核的 CPU 調度器 CFS(Completely Fair Scheduler, 完全公平調度算法)對進程的調度結果。

一般情況下,推薦直接使用 --cpus,而無需單獨設置 --cpu-period--cpu-quota,除非你已經對 CPU 調度器 CFS 有了足夠多的了解,提供 --cpus 參數也是 Docker 團隊為了可以簡化用戶的使用成本增加的,它足夠滿足我們大多數的需求。

--cpu-shares 選項,它雖然有一些實際意義,但卻不如 --cpus 來的直觀,並且它會受到當前系統上運行狀態的影響,為了不因為它給大家帶來困擾,此處就不再進行介紹了。

--cpu-rt-period--cpu-rt-runtime 兩個參數,會影響 CPU 的實時調度器。但實時調度器需要內核的參數的支持,並且配置實時調度器也是個高級或者說是危險的操作,有可能會導致各種奇怪的問題,此處也不再進行展開。

管理容器的內存資源

前面已經介紹了如何管理容器的 CPU 資源,接下來我們看看如何管理容器的內存資源。相比 CPU 資源來說,內存資源的管理就簡單很多了。

同樣的,我們先看看有哪些參數可供我們配置,對於其含義我會稍後進行介紹:

(MoeLove) ➜  ~ docker run --help |egrep 'memory|oom'      --kernel-memory bytes            Kernel memory limit  -m, --memory bytes                   Memory limit      --memory-reservation bytes       Memory soft limit      --memory-swap bytes              Swap limit equal to memory plus swap: '-1' to enable unlimited swap      --memory-swappiness int          Tune container memory swappiness (0 to 100) (default -1)      --oom-kill-disable               Disable OOM Killer      --oom-score-adj int              Tune host's OOM preferences (-1000 to 1000)

OOM

在開始進行容器內存管理的內容前,我們不妨先聊一個很常見,又不得不面對的問題:OOM(Out Of Memory)。

當內核檢測到沒有足夠的內存來運行系統的某些功能時候,就會觸發 OOM 異常,並且會使用 OOM Killer 來殺掉一些進程,騰出空間以保障系統的正常運行。

這裡簡單介紹下 OOM killer 的大致執行過程,以便於大家理解後續內容。

內核中 OOM Killer 的代碼,在 torvalds/linux/mm/oom_kill.c 可直接看到,這裡以 Linux Kernel 5.2 為例。

引用其中的一段注釋:

If we run out of memory, we have the choice between either killing a random task (bad), letting the system crash (worse).

OR try to be smart about which process to kill. Note that we don't have to be perfect here, we just have to be good.

翻譯過來就是,當我們處於 OOM 時,我們可以有幾種選擇,隨機地殺死任意的任務(不好),讓系統崩潰(更糟糕)或者嘗試去了解可以殺死哪個進程。注意,這裡我們不需要追求完美,我們只需要變好(be good)就行了。

事實上確實如此,無論隨機地殺掉任意進程或是讓系統崩潰,那都不是我們想要的。

回到內核代碼中,當系統內存不足時,out_of_memory() 被觸發,之後會調用 select_bad_process() 函數,選擇一個 bad 進程來殺掉。

那什麼樣的進程是 bad 進程呢?總是有些條件的。select_bad_process() 是一個簡單的循環,其調用了 oom_evaluate_task() 來對進程進行條件計算,最核心的判斷邏輯是其中的 oom_badness()。

而為了能夠最快地進行選擇,這裡的邏輯也是儘可能的簡單,除了明確標記不可殺掉的進程外,直接選擇內存佔用最多的進程。(當然,還有一個額外的 oom_score_adj 可用於控制權重)

這種選擇的最主要的兩個好處是:

  1. 可以回收很多內存;
  2. 可以避免緩解 OOM 後,該進程後續對內存的搶佔引發後續再次的 OOM。

我們將注意力再回到 Docker 自身,在生產環境中,我們通常會用 Docker 啟動多個容器運行服務。當遇到 OOM 時,如果 Docker 進程被殺掉,那對我們的服務也會帶來很大的影響。

所以 Docker 在啟動的時候默認設置了一個 -500 的 oom_score_adj 以儘可能地避免 Docker 進程本身被 OOM Killer 給殺掉。

如果我們想讓某個容器,儘可能地不要被 OOM Killer 殺掉,那我們可以給它傳遞 --oom-score-adj 配置一個比較低的數值。

但是注意:不要通過 --oom-kill-disable 禁用掉 OOM Killer,或者給容器設置低於 dockerd 進程的 oom_score_adj 值,這可能會導致某些情況下系統的不穩定。除非你明確知道自己的操作將會帶來的影響。

管理容器的內存資源

介紹完了 OOM,相比你已經知道了內存耗盡所帶來的危害,我們來繼續介紹如何管理容器的內存資源。

(MoeLove) ➜  ~ docker run --help |egrep 'memory|oom'        --kernel-memory bytes            Kernel memory limit    -m, --memory bytes                   Memory limit        --memory-reservation bytes       Memory soft limit        --memory-swap bytes              Swap limit equal to memory plus swap: '-1' to enable unlimited swap        --memory-swappiness int          Tune container memory swappiness (0 to 100) (default -1)        --oom-kill-disable               Disable OOM Killer        --oom-score-adj int              Tune host's OOM preferences (-1000 to 1000)

可用的配置參數有上述幾個,我們通常直接使用 --memory 參數來限制容器可用的內存大小。我們同樣使用幾個示例進行介紹:

啟動一個容器,並傳遞參數 --memory 10m 限制其可使用的內存為 10 m

(MoeLove) ➜  ~ docker run --rm -it --memory 10m alpine/ #

那我們如何驗證它的可用內存大小是多少呢?在物理機上,我們通常使用 free 工具進行查看。但在容器環境內,它還是否生效呢?

  / # free -m               total       used       free     shared    buffers     cached  Mem:         15932      14491       1441       1814        564       3632  -/+ buffers/cache:      10294       5637  Swap:         8471        693       7778

很明顯,使用 free 得到的結果是宿主機上的信息。當然,我們前面已經介紹了 docker stats 命令,我們使用它來查看當前的資源使用情況:

(MoeLove) ➜  ~ docker stats --no-stream $(docker ps -ql)     CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDSe260e91874d8        busy_napier         0.00%               1.172MiB / 10MiB    11.72%              16.1kB / 0B         0B / 0B             1

可以看到 MEM USAGE / LIMIT 那一列中的信息已經生效,是我們預期的樣子。

那我們是否還有其他方式查看此信息呢?當然有:

(MoeLove) ➜  ~ docker stats --no-stream $(docker ps -ql)  CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT   MEM %               NET I/O             BLOCK I/O           PIDS  e260e91874d8        busy_napier         0.00%               1.172MiB / 10MiB    11.72%              16.1kB / 0B         0B / 0B             1  

或者可以在宿主機上執行以下命令:

  (MoeLove) ➜  ~ cat  /sys/fs/cgroup/memory/system.slice/docker-$(docker inspect --format '{{ .Id}}' $(docker ps -ql)).scope/memory.limit_in_bytes  10485760  

注意:以上命令在 Linux 5.2 內核下測試通過,不同版本之間目錄結構略有差異。

更新容器內存資源限制

當容器運行一段時間,其中的進程使用的內存變多了,我們想允許容器使用更多內存資源,那要如何操作呢?

我們仍然可以用前面介紹的 docker update 命令完成。

比如使用如下命令,將可用內存擴大至 20m:

(MoeLove) ➜  ~ docker update --memory 20m $(docker ps -ql)  e260e91874d8  # 驗證是否生效  (MoeLove) ➜  ~ docker stats --no-stream $(docker ps -ql)  CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT   MEM %               NET I/O      BLOCK I/O           PIDS  e260e91874d8        busy_napier         0.00%               1.434MiB / 20MiB    7.17%               35.3kB / 0B  0B / 0B             1

如果還不夠,需要擴大至 100m 呢?

(MoeLove) ➜  ~ docker update --memory 100m $(docker ps -ql)  Error response from daemon: Cannot update container e260e91874d8181b6d0078c853487613907cd9ada2af35d630a7bef204654982: Memory limit should be smaller than already set memoryswap limit, update the memoryswap at the same time

會發現這裡有個報錯信息。大意是 memory limit 應該比已經配置的 memoryswap limit 小,需要同時更新 memoryswap。

你可能會困惑,之前我們只是限制了內存為 10m,並且擴大至 20m 的時候是成功了的。為什麼到 100m 的時候就會出錯?

這就涉及到了這些參數的特定行為了,我來為你一一介紹。

內存限制參數的特定行為

這裡的特定參數行為,主要是指我們前面使用的 --memory 和未介紹過的 --memory-swap 這兩個參數。

1. --memory 用於限制內存使用量,而 --memory-swap 則表示內存和 Swap 的總和。

這解釋了上面「Memory limit should be smaller than already set memoryswap limit」,因為 --memory-swap 始終應該大於等於 --memory (畢竟 Swap 最小也只能是 0 )。

2. 如果只指定了 --memory 則最終 --memory-swap 將會設置為 --memory 的兩倍。也就是說,在只傳遞 --memory 的情況下,容器只能使用與 --memory 相同大小的 Swap。

這也解釋了上面「直接擴大至 20m 的時候能成功,而擴大到 100m 的時候會出錯」,在上述場景中只指定了 --memory 為 10m,所以 --memory-swap 就默認被設置成了 20m。

3. 如果 --memory-swap--memory 設置了相同值,則表示不使用 Swap。

4. 如果 --memory-swap 設置為 -1 則表示不對容器使用的 Swap 進行限制。

5. 如果設置了 --memory-swap 參數,則必須設置 --memory 參數。

總結

至此,我你介紹了容器資源管理的核心內容,包括管理容器的 CPU 資源和內存資源。為容器進行合理的資源控制,有利於提高整體環境的穩定性,避免資源搶佔或大量內存佔用導致 OOM,進程被殺掉等情況。

對 CPU 進行管理時,建議使用 --cpus,語義方面會比較清晰。如果是對 Linux 的 CPU 調度器 CFS 很熟悉,並且有強烈的定製化需求,這種情況下再使用 --cpu-period--cpu-quota 比較合適。

對內存進行管理時,有個 --memory-swappiness 參數也需要注意下,它可設置為 0~100 的百分比,與我們平時見到的 swappiness 行為基本一致,設置為 0 表示不使用匿名頁面交換,設置為 100 則表示匿名頁面都可被交換。如果不指定的話,它默認會從主機上繼承。

在本文中,關於在宿主機上查看容器的內存限制,我給出了一個命令,它具體是什麼含義呢?下篇《深入剖析容器》中我將詳細說明。

  (MoeLove) ➜  ~ cat  /sys/fs/cgroup/memory/system.slice/docker-$(docker inspect --format '{{ .Id}}' $(docker ps -ql)).scope/memory.limit_in_bytes  10485760

本文節選自專欄

點擊閱讀原文,查看更多試讀內容