docker 入門

http://dockone.io/article/277html

 

個人碎碎念:Docker入門指南

【編者的話】以前曾經翻譯過不少Docker入門介紹的文章,之因此再翻譯這篇,是由於Anders的角度很獨特,思路也很調理。你也能夠看下做者的演講稿 《Docker, DevOps的將來》。本文介紹了Docker的一些基本概念、誘人的特性、Docker的工做原理、平常管理基本操做,以及一些Docker的問題的解決方案。
docker.png

什麼是Docker,你應該知道些什麼?

相比不少人的解釋,我相信說Docker是一個輕量級的虛擬機更容易理解。另一種解釋是:Docker就是操做系統中的 chroot。若是你不知道 chroot是什麼的話,後一種解釋可能沒法幫助你理解什麼是Docker。

chroot是一種操做,能改變當前運行的進程和子進程的根目錄。 程序運行在這樣的一個被修改的環境中,它不能訪問這個環境目錄樹以外的文件和命令,這個被修改的環境就是「chroot牢籠」。


-- Arch Linux 的 wiki 中對 chroot 的解釋

虛擬機 vs. Docker

下面這張圖描述了虛擬機和Docker之間的差別。 在VM中,宿主OS上是 hypervisor(虛擬機監視器), 最上層是客戶機操做系統,而Docker則使用 Docker引擎和容器。 這樣解釋你能理解嗎? Docker引擎和hypervisor之間的區別又是什麼呢?你能夠列出運行在宿主OS上的進程來理解它們的區別。
vm-vs-docker.png

下面這個簡單的進程樹能夠看出它們的差別。雖然虛擬機中運行了不少進程,可是運行虛擬機的宿主機上卻只有一個進程。
# Running processes on Host for a VM
$ pstree VM

-+= /VirtualBox.app
|--= coreos-vagrant

而運行Docker引擎的主機上則能夠看到全部的進程。  容器進程是運行在宿主OS上的!,他們能夠經過普通的 pskill等命令進行檢查和維護。
# Docker在主機中的進程
$ pstree docker

-+= /docker
|--= /bin/sh
|--= node server.js
|--= go run app
|--= ruby server.rb
...
|--= /bin/bash

全部的東西都是透明的, 意味着什麼呢?意味着Docker容器比虛擬機更小,更快,更容易與其它東西集成。以下圖所示。
vm-vs-docker-table.png

安裝CoreOS的小型虛擬機竟然有1.2GB, 而裝上busybox的小型容器只有2.5MB。最快的虛擬機啓動時間也是分鐘級的,而容器的啓動時間一般不到一秒。在同一宿主機上安裝虛擬機須要正確的設置網絡, 而安裝Docker很是簡單。

這麼來看,容器是輕量、快速而且易集成,但這並非它的所有!

Docker 是一份合約

Docker仍是開發者和運維之間的「合約」。 開發和運維在選擇工具和環境時的姿態一般差異很大。開發者想要使用一些閃亮的新東西,好比Node.js、Rust、Go、微服務、Cassandra、Hadoop、blablabla.........而運維則傾向於使用以往用過的工具,由於事實證實那些舊的工具頗有效。

但這偏偏是Docker的亮點, 運維喜歡它,由於Docker讓他們只要關心一件事: 部署容器, 而開發者也同樣很開心,只要寫好代碼,而後往容器裏一扔,剩下的交給運維就完事了。
devs-loves-ops.png

不過別急,這還沒完。運維還能幫助開發者構建優化好的容器以便用於本地開發。

更好的資源利用

不少年前,那時候尚未虛擬化,當咱們須要建立一個新服務時,咱們必須申請實際的物理機硬件。 這可能要花上數月,依賴於公司的流程。一旦服務器到位,咱們建立好服務,不少時候它並無像咱們但願的那樣成功,由於服務器的CPU使用率只有5%。 太奢侈了。 

接着,虛擬化來了。它能夠在幾分鐘以內把一臺機器運轉起來,還能夠在同一硬件上運行多個虛擬機,資源使用率就不僅5%了。可是,咱們還須要給每一個服務分配一個虛擬機,所以咱們仍是不能如願的使用這臺機器。

容器化是演化進程的下一步。容器能夠在幾秒以內建立起來,並且還能以比虛擬機更小的粒度進行部署。

依賴

matrix-from-hell.jpg

Docker啓動速度真的很酷。 可是,咱們爲何不把全部的都服務部署到同一臺機器上呢? 緣由很簡單:依賴的問題。在同一臺機器上安裝多個獨立的服務,無論是真是機器仍是虛擬機都是一場災難。用Docker公司的說法是:地獄同樣的矩陣依賴。

而Docker經過在容器中保留依賴關係解決了矩陣依賴的問題。

速度

roadrunner.gif

快固然不錯,可是能快100倍就太難以想象了。速度讓不少事情成爲可能,增長了更多新的可能性。好比,如今能夠快速建立新的環境,若是須要從Clojure開發環境完整的切換到Go語言嗎?啓動一個容器吧。須要爲集成和性能測試提供生產環境DB ?啓動一個容器吧! 須要從Apache切換整個生產環境到Nginx?啓動容器吧!

Docker是怎麼工做的?

Docker是一個Client-Server結構的系統,Docker守護進程運行在主機上, 而後經過Socket鏈接從客戶端訪問, 客戶端和守護進程也能夠運行再同一主機上,但這不是必須的。Docker命令行客戶端也是相似的工做方式,但它一般經過Unix域套接字而不是TCP套接字鏈接。

守護進程從客戶端接受命令並管理運行在主機上的容器。
client-server.png

Docker 概念及相互做用

  • 主機, 運行容器的機器。
  • 鏡像,文件的層次結構,以及包含如何運行容器的元數據
  • 容器,一個從鏡像中啓動,包含正在運行的程序的進程
  • Registry, 鏡像倉庫
  • 卷,容器外的存儲
  • Dockerfile, 用於建立鏡像的腳本
    docker-interactions.png

    咱們能夠經過Dockerfile來構建鏡像, 還能夠經過commit一個運行的容器來建立一個鏡像,這個鏡像能夠會被標記,能夠推到Registry或者從Registry上拉下來,能夠經過建立或者運行鏡像的方式來啓動容器,能夠被stop,也能夠經過rm來移除它。

    鏡像

    鏡像是一種文件結構,包含如何運行容器的元數據。Dockerfile中的每條命令都會在文件系統中建立一個新的層次結構,文件系統在這些層次上構建起來,鏡像就構建於這些聯合的文件系統之上。
    docker-image.png

    當容器啓動後,全部鏡像都會統一合併到一個進程中。 聯合文件系統中的文件被刪除時, 它們只是被標記爲已刪除,但實際上仍然存在。
    # Commands for interacting with images
    $ docker images  # 查看全部鏡像.
    $ docker import  # 從tarball建立鏡像
    $ docker build   # 經過Dockerfile建立鏡像
    $ docker commit  # 從容器中建立鏡像
    $ docker rmi     # 刪除鏡像
    $ docker history # 列出鏡像的變動歷史
    

    鏡像大小

    這是一些常用的鏡像相關的數據:
  • scratch - 基礎鏡像, 0個文件,大小爲0
  • busybox - 最小Unix系統,2.5MB,10000個文件
  • debian:jessie - Debian最新版, 122MB, 18000 個文件
  • ubuntu:14.04 - 188MB,23000 個文件

建立鏡像

能夠經過 docker commit container-iddocker import url-to-tar或者 docker build -f Dockerfile .來建立鏡像。
先看commit的方式:
# 經過commit的方式來建立鏡像
$ docker run -i -t debian:jessie bash
root@e6c7d21960:/# apt-get update
root@e6c7d21960:/# apt-get install postgresql
root@e6c7d21960:/# apt-get install node
root@e6c7d21960:/# node --version
root@e6c7d21960:/# curl https://iojs.org/dist/v1.2.0/iojs-v1.2.0-
linux-x64.tar.gz -o iojs.tgz
root@e6c7d21960:/# tar xzf iojs.tgz
root@e6c7d21960:/# ls
root@e6c7d21960:/# cd iojs-v1.2.0-linux-x64/
root@e6c7d21960:/# ls
root@e6c7d21960:/# cp -r * /usr/local/
root@e6c7d21960:/# iojs --version
1.2.0
root@e6c7d21960:/# exit
$ docker ps -l -q
e6c7d21960
$ docker commit e6c7d21960 postgres-iojs
daeb0b76283eac2e0c7f7504bdde2d49c721a1b03a50f750ea9982464cfccb1e

從上面能夠看出,咱們能夠經過 docker commit來建立鏡像,可是這種方式有點凌亂並且很難複製, 更好的方式是經過Dockerfile來構建鏡像,由於它步驟清晰而且容易複製:
FROM debian:jessie
# Dockerfile for postgres-iojs

RUN apt-get update
RUN apt-get install postgresql
RUN curl https://iojs.org/dist/iojs-v1.2.0.tgz -o iojs.tgz
RUN tar xzf iojs.tgz
RUN cp -r iojs-v1.2.0-linux-x64/* /usr/local

而後用下面的命令來構建:
$ docker build -tag postgres-iojs .

Dockerfile中的每個命令都建立了新版的layer,一般把相似的命令放在一塊兒,經過&&和續行符號把命令組合起來:
FROM debian:jessie
# Dockerfile for postgres-iojs

RUN apt-get update && \
  apt-get install postgresql && \
  curl https://iojs.org/dist/iojs-v1.2.0.tgz -o iojs.tgz && \
  tar xzf iojs.tgz && \
  cp -r iojs-v1.2.0-linux-x64/* /usr/local

這些行中命令的順序很重要,由於Docker爲了加速鏡像的構建,會緩存中間的鏡像。 組織Dockerfile的順序時,注意把常常變化的行放在文件的底部,當緩存中相關的文件改變時,鏡像會從新運行,即便Dockerfile中的行沒有發生變化也是如此。

Dockerfile 中的命令

Dockerfile 支持13個命令, 其中一些命令用於構建鏡像,另一些用於從鏡像中運行容器,這是一個關於命令何時被用到的表格:
dockerfile-commands.png

BUILD 命令:

  • FROM - 新鏡像是基於哪一個鏡像的
  • MAINTAINER - 鏡像維護者的姓名和郵箱地址
  • COPY - 拷貝文件和目錄到鏡像中
  • ADD - 同COPY同樣,但會自動處理URL和解壓tarball壓縮包
  • RUN - 在容器中運行一個命令, 好比:apt-get install
  • ONBUILD - 當構建一個被繼承的Dockerfile時運行命令
  • .dockerignore - 不是一個命令, 但它能控制什麼文件被加入到構建的上下文中,構建鏡像時應該包含.git以及其它的不須要的文件。

RUN 命令:

  • CMD - 運行容器時的默認命令,能夠被命令行參數覆蓋
  • ENV - 設置容器內的環境變量
  • EXPOSE - 從容器中暴露出端口, 必須顯式的經過在主機上的RUN命令帶上-p或者-P來暴露端口
  • VOLUME - 指定一個在文件系統以後的存儲目錄。若是不是經過docker run -v設置的, 那麼將被建立爲/var/lib/docker/volumes
  • ENTRYPOINT - 指定一個命令不會被docker run image cmd命令覆蓋。經常使用於提供一個默認的可執行程序並使用命令做爲參數。

BUILD, RUN命令都有的命令:

  • USER - 爲RUN、CMD、ENTRYPOINT命令設置用戶
  • WORKDIR - 爲RUN、CMD、ENTRYPOINT、ADD、COPY命令設置工做目錄
    docker-image.png

運行的容器

容器啓動後,進程在它能夠運行的聯合文件系統中得到了新的可寫層。

從1.5版本起,它還可讓最頂層的layer設置爲只讀,強制咱們爲全部文件輸出(如日誌、臨時文件)使用卷。
# 用於與容器交互的命令
$ docker create  # 建立一個容器,但不啓動它
$ docker run     #  建立並啓動一個容器
$ docker stop    # 中止容器
$ docker start   #  啓動容器
$ docker restart # 重啓容器
$ docker rm      # 刪除容器
$ docker kill    #  給容器發送kill信號
$ docker attach  # 鏈接到正在運行的容器中
$ docker wait    # 阻塞直到容器中止爲止
$ docker exec    # 在運行的容器中執行一條命令

docker run

如上所述,  docker run是用戶啓動新容器的命令, 這裏是一些通用的運行容器的方法:
container.png

# 交互式運行容器
$ docker run -it --rm ubuntu

這是一個可讓你像普通的終端程序同樣交互式的運行容器的方法, 若是你想把管道輸出到容器中,可使用-t選項。
  • --interactive (-i) - 將標準輸入發送給進程
  • -tty (-t) - 告訴進程有終端鏈接。 這個功能會影響程序的輸出和它如何處理Ctrx-C等信號。
  • --rm - 退出時刪除鏡像。

    # 後臺運行容器
    $ docker run -d hadoop
    

    docker run -env

    # 運行一個命名容器並給它傳一些環境變量
    $ docker run \
    --name mydb \
    --env MYSQL_USER=db-user \
    -e MYSQL_PASSWORD=secret \
    --env-file ./mysql.env \
    mysql
    
  • --name - 給容器命名, 不然它是一個隨機容器
  • --env (-e)- 設置容器中的環境變量
  • --env-file - 從env-file中引入全部環境變量(同Linux下的source env-file 功能)
  • mysql - 指定鏡像名爲 mysql:lastest

docker run -publish

# 發佈容器的80端口到主機上的隨機端口
$ docker run -p 80 nginx

# 發佈容器端口80和主機上的8080端口
$ docker run -p 8080:80 nginx

# 發佈容器80端口到主機127.0.0.0.1的8080端口
$ docker run -p 127.0.0.1:8080:80 nginx

# 發佈全部容器中暴露的端口到主機的隨機端口上
$ docker run -P nginx

nginx 鏡像,好比暴露出80和443端口。
FROM debian:wheezy
  MAINTAINER NGINX "docker-maint@nginx.com"

 EXPOSE 80 443

docker run --link

# 啓動postgres容器,給它起名爲mydb
$ docker run --name mydb postgres

# 把mydb 連接到 myqpp 的db
$ docker run --link mydb:db myapp

鏈接容器須要設置容器到被鏈接的容器之間的網絡,有兩件事要作:
  • 經過容器的鏈接名,更新 /etc/hosts 。 在上面的例子中,鏈接名是db, 能夠方便的經過名字db來訪問容器。
  • 爲暴露的端口設置環境變量。這個好像沒啥實際用處,你也能夠經過 主機名:端口的形式訪問對應的端口。

docker run limits

還能夠經過run limits來限制容器可使用的主機資源
# 限制內存大小
$ docker run -m 256m yourapp

# 限制進程可使用的cpu份數(cpu shares)(總CPU份數爲1024)
$ docker run --cpu-shares 512 mypp

# 改變運行進程的用戶爲www,而不是root(出於安全考慮)
$ docker run -u=www nginx

設置CPU份數爲1024中的512份並不意味着可使用一半的CPU資源,這意味着在一個無任何限制的容器中,它最多可使用一半的份數。好比咱們有兩個有1024份的容器,和一個512份的容器(1024:1024:512) ,那麼512份的那個容器,就只能獲得1/5的總CPU份數

docker exec container

docker exec 容許咱們在已經運行的容器內部執行命令,這點在debug的時候頗有用。
# 使用id 6f2c42c0在容器內部運行shell
$ docker exec -it 6f2c42c0 sh

volumes.png

卷提供容器外的持久存儲。 這意味着若是你提交了新的鏡像,數據將不會被保存。
# Start a new nginx container with /var/log as a volume
$ docker run  -v /var/log nginx

若是目錄不存在,則會被自動建立爲:/var/lib/docker/valumes/ec3c543bc..535

實際的目錄名能夠經過命令: docker inspect container-id 找到。
# 啓動新的nginx容器,設置/var/log爲卷,並映射到主機的/tmp目錄下
$ docker run -v /tmp:/var/log nginx

還可使用 --valumes-from選項從別的容器中掛載卷。
# 啓動容器db
$ docker run -v /var/lib/postgresql/data --name mydb postgres

# 啓動backup容器,從mydb容器中掛載卷
$ docker run --volumes-from mydb backup

Docker Registry

Docker Hub是Docker的官方鏡像倉庫,支持私有庫和共有庫,倉庫能夠被標記爲 官方倉庫,意味着它由該項目的維護者(或跟它有關的人)策劃。 

Docker Hub 還支持自動化構建來自Github和Bitbucket的項目,若是啓用自動構建功能,那麼每次你提交代碼到代碼庫都會自動構建鏡像。

即便你不想用自動構建,你仍是能夠直接 docker push到Docker Hub,Docker pull則會拉取鏡像下來。 docker run 一個本地不存在的鏡像,則會自動開始 docker pull操做。 

你也能夠在任意地方託管鏡像,官方有 Registry的開源項目,可是,還有不少Bug。

此外,Quay、Tutum和Google 還提供私有鏡像託管服務。

檢查容器

檢查容器的命令有一大把:
$ docker ps      # 顯示運行的容器
$ docker inspect # 顯示容器信息(包括ip地址)
$ docker logs    # 獲取容器中的日誌
$ docker events  # 獲取容器事件
$ docker port    # 顯示容器的公開端口
$ docker top     # 顯示容器中運行的進程
$ docker diff    # 查看容器文件系統中改變的文件
$ docker stats   # 查看各類緯度數據、內存、CPU、文件系統等

下面詳細講一下 docker ps 和 docker inspect,這兩個命令最經常使用了。
# 列出全部容器,包括已中止的。
$ docker ps --all
CONTAINER ID   IMAGE            COMMAND    NAMES
9923ad197b65   busybox:latest   "sh"       romantic_fermat
fe7f682cf546   debian:jessie    "bash"     silly_bartik
09c707e2ec07   scratch:latest   "ls"       suspicious_perlman
b15c5c553202   mongo:2.6.7      "/entrypo  some-mongo
fbe1f24d7df8   busybox:latest   "true"     db_data


# Inspect the container named silly_bartik
# Output is shortened for brevity.
$ docker inspect silly_bartik
    1 [{
    2     "Args": [
    3         "-c",
    4         "/usr/local/bin/confd-watch.sh"
    5     ],
    6     "Config": {
   10         "Hostname": "3c012df7bab9",
   11         "Image": "andersjanmyr/nginx-confd:development",
   12     },
   13     "Id": "3c012df7bab977a194199f1",
   14     "Image": "d3bd1f07cae1bd624e2e",
   15     "NetworkSettings": {
       16         "IPAddress": "",
   18         "Ports": null
   19     },
   20     "Volumes": {},
   22 }]

技巧花招

獲取容器id。寫腳本時頗有用。
# Get the id (-q) of the last (-l) run container
# 獲取最後(-l)一個啓動的容器id(-q)
$ docker ps -l -q
c8044ab1a3d0

docker inspect能夠帶格式化的字符串----Go語言模板做爲參數,詳細描述所需的數據。寫腳本時同時有用。
$ docker inspect -f '{{ .NetworkSettings.IPAddress }}' 6f2c42c05500
172.17.0.11

使用 docker exec來跟運行中的容器進行交互。
# 獲取容器環境變量
$ docker exec -it 6f2c42c05500 env

PATH=/usr/local/sbin:/usr...
HOSTNAME=6f2c42c05500
REDIS_1_PORT=tcp://172.17.0.9:6379
REDIS_1_PORT_6379_TCP=tcp://172.17.0.9:6379
...

經過捲來避免每次運行時都重建鏡像, 下面是一個Dockerfile,每次構建時,會拷貝當前目錄到容器中。
1 FROM dockerfile/nodejs:latest
  2
  3 MAINTAINER Anders Janmyr "anders@janmyr.com"
  4 RUN apt-get update && \
  5   apt-get install zlib1g-dev && \
  6   npm install -g pm2 && \
  7   mkdir -p /srv/app
  8
  9 WORKDIR /srv/app
 10 COPY . /srv/app
 11
 12 CMD pm2 start app.js -x -i 1 && pm2 logs
 13

構建並運行鏡像:
$ docker build -t myapp .
$ docker run -it --rm myapp

爲避免重建,建立一次性鏡像並在運行時掛載本地目錄。

安全

security.jpg

你們可能據說過使用Docker不那麼安全。這不是假話,但這不成問題。 

目前Docker存在如下安全問題:
  • 鏡像簽名未被正確的核準。
  • 若是你在容器中擁有root權限,那你潛在的擁有對真個主機的root權限。

安全解決辦法:
  • 從你的私有倉庫中使用受信任的鏡像
  • 儘可能不要以root運行容器
  • 把容器中的root看成是主機上的root? 仍是把容器的根目錄設置爲容器內的根目錄 ?

若是服務器上全部的容器都是你的,那你不須要擔憂他們之間會有危險的交互。

「選擇」容器

我給選擇兩字加了引號, 由於目前根本沒有任何別的選擇, 可是不少容器愛好者想玩玩,好比Ubuntu的LXD、微軟的Drawbridge,還有 Rocket

Rocket由CoreOS開發,CoreOS是一個很大的容器平臺。 他們開發Rocket的理由是以爲Docker公司讓Docker變得臃腫,而且還和CoreOS有業務衝突。

他們在這個新的容器中,嘗試移除那些由於歷史緣由而留下來的Docker瑕疵。並經過 socket activation提供簡單的容器和完全的安全構建。
container-options.png

編排

當咱們把應用程序拆開到多個不一樣的容器中時,會產生一些新的問題。怎麼讓不一樣的部分進行通訊呢? 這些容器在單個主機上怎麼辦? 多個主機上又是怎麼處理? 

單個主機上,Docker經過鏈接來解決編排的問題。 

爲簡化容器的連接操做,Docker提供了一個叫 docker-compose的工具。(之前它叫 fig, 由另外一家公司開發,而後最近Docker收購了他們)

docker-compose

fig.png

docker-compose在單個 docker-compose.yml文件中聲明多個容器的信息。來看一個例子,管理web和redis兩個容器的配置文件:
1 web:
  2   build: .
  3   command: python app.py
  4   ports:
  5    - "5000:5000"
  6   volumes:
  7    - .:/code
  8   links:
  9    - redis
 10 redis:
 11   image: redis

啓動上述容器,可使用 docker-compose up命令
$ docker-compose up
  Pulling image orchardup/redis...
  Building web...
  Starting figtest_redis_1...
  Starting figtest_web_1...
  redis_1 | [8] 02 Jan 18:43:35.576 # Server
  started, Redis version 2.8.3
  web_1   |  * Running on http://0.0.0.0:5000/

也能夠經過detached模式(detached mode)啓動:  docker-compose up -d,而後能夠經過 docker-compose ps查看容器中跑了啥東西:
$ docker-compose up -d
Starting figtest_redis_1...
Starting figtest_web_1...
$ docker-compose ps
Name              Command                    State   Ports
------------------------------------------------------------
figtest_redis_1   /usr/local/bin/run         Up
figtest_web_1     /bin/sh -c python app.py   Up      5000->5000

還能夠同時讓命令在一個容器或者多個容器中同時工做。
# 從web容器中獲取環境變量
$ docker-compose run web env

# 擴展到多個容器中(Scale to multiple containers)
$ docker-compose scale web=3 redis=2

# 從全部容器中返回日誌信息
$ docker-compose logs

從以上命令能夠看出,擴展很容易,不過應用程序必須寫成支持處理多個容器的方式。在容器外,不支持負載均衡。

Docker託管

不少公司想作在雲中託管Docker的生意,以下圖。
docker-hosting-providers.png

這些提供商嘗試解決不一樣的問題, 從簡單的託管到作"雲操做系統"。其中有兩家比較有前景:

CoreOS

如上圖所示,CoreOS是能夠在CoreOS集羣中託管多個容器的一系列服務的集合:
core-os.png

  • CoreOS Linux發行版是裁剪的Linux,在初次啓動時使用114MB的RAM,沒有包管理器, 使用Docker或者它本身的Rocket運行一切程序。
  • CoreOS 使用Docker(或Rocket)在主機上安裝應用。
  • 使用systemd做爲init服務,它的性能超級好,還能很好的處理啓動依賴關係, 強大的日誌系統,還支持socket-activation。
  • etcd 是分佈式的,一致性 K-V 存儲用於配置共享和服務發現。
  • fleet,集羣管理器,是systemd的擴展,能與多臺機器工做,採用etcd來管理配置並運行在每個臺CoreOS服務器上。

AWS

Docker容器託管在Amazon有兩種途徑:
  • Elastic Beanstalk部署Docker容器,它工做的很好,但就是太慢了, 一次全新的部署須要好幾分鐘,感受跟通常的容器秒級啓動不大對勁。
  • ECS、Elastic Container Server是Amazon上游容器集羣解決方案, 目前還在預覽版3,看起來頗有前途,跟Amazon其它服務同樣,經過簡單的web service調用與它交互。

總結

  • Docker is here to stay
  • 解決了依賴問題
  • 容器各方面都很快
  • 有集羣解決方案,但不能無縫對接
相關文章
相關標籤/搜索