1. 程式人生 > >拉開大變革序幕(上):在浪潮之巔觀望Docker

拉開大變革序幕(上):在浪潮之巔觀望Docker

Docker says: an open platform to build, ship, and run any app, anywhere

這裡寫圖片描述

Docker Service Overview

(as far as I study)

  • CaaS/PaaS/lightweight IaaS

    • developer oriented
    • connect code manage and cloud machine, to build, deploy and manage
  • Use as VM

    • Tencent ten thousand machines
    • rebuild in-house system
    • promote DevOps
    • provide CI/CD
  • Software Architecture

    • help microservice
    • baozoumanhua.com

Hello Docker, hello SDUer

現在正是雲端計算‘容器化’的潮流。Docker越來越成為雲端計算和分散式系統的寵兒和基石。

我們可以從 Docker Hub 或其他registry,如 DockerPool阿里雲Docker映象庫, pull下已有的映象,也可以自己寫Dockerfile檔案,自己建立映象。有了映象,就可以去RUN它。下面依次介紹了RUN一個映象(docker run

),自己建立映象(Dockerfile語法docker build)。在介紹它的最基本用法之後,開始初步深入它的原理和核心技術,不求理解,只求一個印象 :-)。深入部分會越來越細緻,不斷完善。之後也會增加Docker Runtime metrics的介紹和命令,這樣對Performance的分析也會有幫助。

感謝大家一起幫助博主完善這篇blog。

照個相

先粗略介紹docker常用的幾個基本命令:

run

執行容器,如果映象不存在則先下載

pull

從映象庫上下載容器映象

start/stop

啟動/停止一個container

rm

刪除容器

rmi

刪除容器映象

commit

將容器中的修改提交至映象中

logs

顯示容器執行的控制檯輸出

build

從 Dockerfile 構建一個映象

inspect

顯示容器執行引數

images

顯示當前宿主機上的所有映象

docker run 灑灑水

$ sudo docker run [OPTIONS] IMAGE[:TAG] [COMMAND] [ARG...]

docker run命令有兩個引數,一個是映象名,一個是要在映象中執行的命令。
正確的命令:

$ docker run learn/tutorial echo "hello word"
  • -d:containter將會執行在後臺模式
  • –name:給container命名,對於一個container來說有個name會非常方便,因為你可以當你需要link其它容器時或者其他類似需要區分其它容器時,使用容器名稱會簡化操作
  • –link:連線兩個container之間的通訊,通過埠。

–link (name or id): alias

$ docker run -d -P --name web --link db:db training/webapp python app.py

上面命令連線了web和db兩個container,注意link的引數 db:db,前一個db是容器名,後一個db是alias。

如果一個名為web的container被連線到db container上, –link db:webdb,那麼Docker就會在web這個container中建立環境變數 WEBDB_NAME=/web/webdb。其中<alias>_NAME = WEBDB_NAME。

  • -P:container會開放部分埠到host,只要對方可以連線到host,就可以連線到container內部。當使用-P時,docker會查詢一個未被佔用的埠繫結到container。你可以使用docker port來查詢這個隨機繫結埠
docker run --name mongo_001 -d -P mongo
  • -p:指定要對映的埠,並且,在一個指定埠上只可以繫結一個容器。支援的格式有 ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort

對映所有介面地址

使用 hostPort:containerPort 格式本地的 5000 埠對映到容器的 5000 埠,可以執行

$ sudo docker run -d -p 5000:5000 training/webapp python app.py

此時預設會繫結本地所有介面上的所有地址。

對映到指定地址的指定埠

可以使用 ip:hostPort:containerPort 格式指定對映使用一個特定地址,比如 localhost 地址
127.0.0.1

$ sudo docker run -d -p 127.0.0.1:5000:5000 training/webapp python app.py

對映到指定地址的任意埠

使用 ip::containerPort 繫結 localhost 的任意埠到容器的 5000 埠,本地主機會自動分配一個埠。

$ sudo docker run -d -p 127.0.0.1::5000 training/webapp python app.py

還可以使用 udp 標記來指定 udp 埠

$ sudo docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py

檢視對映埠配置

使用 docker port 來檢視當前對映的埠配置,也可以檢視到繫結的地址

$ docker port nostalgic_morse 5000
127.0.0.1:49155.

再看這個例子:

sudo docker run –i –t –v /opt/ubuntutmp:/opt/ubuntutmp ubuntu:14.04 /bin/bash

建立基於Ubuntu 14.04映象的容器,並掛載主機/opt/ubuntutmp,目錄作為容器的資料卷

  • -i:建立互動性連線,佔用容器的標準輸出
  • -t:在容器中建立一個偽終端或者終端
  • -v:將主機的目錄或者檔案掛載為容器資料卷或者在容器中增加資料卷

進入容器用 docker attach <container id>

退出一個映象的bash,而不終止它

Ctrl-p Ctrl-q

Dockerfile語法:事兒多

RUN cmd 在base image中執行命令,一般為linux shell命令

mkdir -p /data/db

這裡補充一點,RUN預設使用的是/bin/sh,通過/bin/sh -c執行。有一次我用的機器上/bin/sh -c有問題,不能識別後續的cmd,導致映象建立失敗。這個時候可以用另一種RUN的形式:
RUN [“executable”, “param1”, “param2”] (exec形式)

RUN ["/bin/bash", "-c", "mkdir /data/db"]

VOLUME [“mountpoint”] 將本地資料夾或者其他container的資料夾掛載到container中

# Define mountable directories.
VOLUME ["/data/db"]

WORKDIR /path/to/workdir 切換目錄用,可以多次切換(相當於cd命令)

# Define working directory.
WORKDIR /data

CMD [“executable”,”param1”,”param2”] container啟動時執行的命令,但是一個Dockerfile中只能有一條CMD命令,多條則只執行最後一條CMD,用於在構建過程中執行命令

# Define default command.
CMD ["mongod"]

EXPOSE port 把這個埠暴露在外,這樣容器外可以看到這個埠並與其通訊

# Expose ports.
#   - 27017: process
#   - 28017: http
EXPOSE 27017
EXPOSE 28017

ENV key value 設定環境變數

ENV APP_NAME app.js

ADD <原始檔> <目標檔案> 用來將一個檔案或目錄新增到 Docker 映象中,前面是原始檔,後面是目標檔案(原始檔必須使用相對路徑)

ADD device.jar /device.jar

ENTRYPOINT 【執行命令】 用來指定執行 Docker 容器時,在容器中執行的命令是什麼。如果需要執行多個命令,可以通過 Supervisor 來執行

ENTRYPOINT java -jar /device.jar

示例(構建mongodb映象的Dockerfile)

#
# MongoDB Dockerfile
#

# Pull base image
FROM       ubuntu:latest

MAINTAINER LIU Qiu Shan <qsliubj@cn.ibm.com>


# Install MongoDB
# The real logic
# Add 10gen official apt source to the sources list
RUN \
  apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 && \
  echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' > /etc/apt/sources.list.d/mongodb.list && \
  apt-get update && \
  apt-get install -y mongodb-org && \
  rm -rf /var/lib/apt/lists/*

# Define mountable directories
VOLUME ["/data/db"]

# Define working directory
WORKDIR /data

# Define default mongodb command
CMD ["mongod"]

# Expose the process port of mongodb
EXPOSE 27017

# Expose the http port of mongodb
#EXPOSE 28017

docker build 如此easy

docker build -t="dockerfile/mongodb" .

在當前目錄下有名為Dockerfile的檔案,執行上述命令,構建名為dockerfile/mongodb的映象。

映象:等主人完善…

檢視所有映象 $ docker images

刪除指定映象 $ docker rmi image_name

注意:
清空/var/lib/docker/devicemapper這個目錄後,需要重新build所有用到的映象,包括用到的作業系統映象,如ubuntu:latest

docker pull ubuntu

/var/lib/docker 啥玩意

這是docker的配置資料夾。Docker用/var/lib/docker作為預設的目錄,所有docker相關的檔案,包括images和掛在卷(volumes)都存在這個目錄下。

我們可以為一個容器建立一個卷,這個卷存放在/var/lib/docker/volume目錄下,卷名是基於UUID命名的,所以很難與容器名稱聯絡在一起。任何卷中的資料都能在主機作業系統中瀏覽和編輯。

注意:
在使用mongodb時,建立多個mongodb的containers後,volumes目錄下就會存滿mongodb的掛在卷,如果過多,會佔用大量磁碟空間,如果出現空間不足的錯誤,可以把不用的volumes刪掉。

Docker Daemon

在Docker架構中,Docker Client通過特定的協議與Docker Daemon進行通訊,而Docker Daemon主要承載了Docker執行過程中的大部分工作。Docker Daemon是Docker架構中執行在後臺的守護程序,大致可以分為Docker Server、Engine和Job三部分。

Docker Daemon架構示意圖

Docker Client先通知Docker Daemon建立container,創建出後Docker Daemon會通知Docker Client已經建立好,這時Docker Client會再次發出start container的請求。收到請求的Docker Daemon會使用以下的Start函式來完成容器啟動的所有過程。

if err := container.Start(); err != nil {
        return job.Errorf("Cannot start container %s: %s", name, err)
}

“Start函式實現了容器的啟動。更為具體的描述是:Start函式實現了程序的啟動,另外在啟動程序的同時為程序設定了名稱空間(namespace),啟動完畢之後為程序完成了資源使用的控制,從而保證程序以及之後程序的子程序都會在同一個名稱空間內,且受到相同的資源控制。如此一來,Start函式建立的程序,以及該程序的子程序,形成一個程序組,該程序組處於資源隔離和資源控制的環境,我們習慣將這樣的程序組環境稱為容器,也就是這裡的Docker Container。”(《Docker原始碼分析》)

容器編排

“container orchestration specifically is now a hot area” —— Nati Shalom on June 11, 2015

容器編排是為了幫助人們自動的構建和管理眾多容器,尤其在分散式叢集中。有很多編排工具,不只是容器編排,根據具體場景可以選擇相應的,例如,如果你只使用Docker容器就可以使用Docker提供的編排系統Swarm,如果你在設計微服務,Kubernetes是不錯的選擇。

另外,Mesos和Mesosphere DCOS是專門設計用來管理大規模容器的。這些高效能的系統在一些世界上最大的資料中心的生產環境中已經身經百戰很多年了,例如Twitter,幾乎完全是在Mesos上面執行的。
在去年(2014)12月份的歐洲DockerCon上,Docker首席技術官Solomon Hykes說Mesos是生產環境下執行大規模可擴充套件容器叢集的黃金標準。

Docker Swarm可以無縫管理容器叢集, 是真正的為了讓企業使用者能夠部署和管理大規模容器。

“我們認為Docker Swarm釋出最酷的部分應該是“batteries included but swappable(可插拔式的架構)”。簡單的說,這個意思就是當你需要規模化生產的時候,你可以開始使用Docker Swarm,並“換入(swap in)”Mesosphere。我們認為他們做了一個偉大的社群決定, 就是鼓勵使用者在容器叢集排程和協調上面可以自我選擇和創新,而不是隻規定一種方式。”

“Mesos上的Docker Swarm直接使用Mesos的API,這就意味著它可以完全相容Mesosphere DCOS,這也使得Docker Swarm同Marathon、 Cronos、Spark、Storm、Hadoop以及Cassandra一樣成為Mesos和Mesosphere生態系統裡面的一等公民。”

總之,這部分文字簡單描述了Docker Swarm, Kubernetes和Mesos的聯絡,它們也是在Docker在分散式叢集上最火熱的話題,值得去探討。

再看一下Docker Swarm,先來看一張它的圖:
這裡寫圖片描述

Swarm發現Docker叢集中的節點,依靠discovery模組,在發現之前需要先註冊(一個Docker Node在Swarm節點上註冊,僅僅是註冊了Docker Node的IP地址以及Docker監聽的埠號)。當發現所有存在的節點時,當Swarm接收到具體的docker管理請求,swarm會通過filter模組決策到底哪些Node滿足要求,並通過一定的strategy將請求轉發至具體的一個Node中。

Docker Compose

通過YAML檔案自動構建container。

在64bit Red Ha上安裝Compose,

# curl -L https://github.com/docker/compose/releases/download/1.1.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

#chmod +x /usr/local/bin/docker-compose

執行 # docker-compose,顯示如下:
這裡寫圖片描述

這裡展示一個YAML檔案示例,我們通過執行命令 $ docker-compose up -d 依據這個YAML檔案的描述構建出各個docker containers。

下面的,hystrix是要構建的container名,image是用到的映象,ports是指定埠,links是該container要link到哪些containers上。具體語法大家可以google或baidu。

hystrix:
  image: kbastani/hystrix-dashboard
  ports:
   - "7979:7979"
  links:
   - gateway
   - discovery
discovery:
  image: kbastani/discovery-microservice
  ports:
   - "8761:8761"
configserver:
  image: kbastani/config-microservice
  ports:
   - "8888:8888"
  links:
   - discovery
gateway:
  image: kbastani/api-gateway-microservice
  ports:
   - "10000:10000"
  links:
   - discovery
   - configserver
   - user
   - movie
   - recommendation
user:
  image: kbastani/users-microservice
  links:
   - discovery
   - configserver
movie:
  image: kbastani/movie-microservice
  links:
   - discovery
   - configserver
recommendation:
  image: kbastani/recommendation-microservice
  links:
   - discovery
   - configserver
moviesui:
  image: kbastani/movies-ui
  ports:
     - "9006:9006"
  links:
   - discovery
   - configserver

Docker Machine

讓你輕鬆部署Docker例項到很多不同的平臺。

Docker Machine提供了多平臺多Docker主機的集中管理,包括本地虛擬機器和私有云、公有云,如VirtualBox、 Digital Ocean、Microsoft Azure(只支援有限的幾個平臺),只需要一條命令便可搭建好Docker主機。所以它是為了簡化部署的複雜性而生的。

“Docker官方是這樣介紹Machine的初衷的:
之前,Docker的安裝流程非常複雜,使用者需要登入到相應的主機上,根據官方的安裝和配置指南來安裝Docker,並且不同的作業系統的安裝步驟也是不一樣的。而有了Machine後,不管是在筆記本、虛擬機器還是公有云例項上,使用者僅僅需要一個命令….當然那你需要先安裝Machine。”

這裡引入一張經典圖片:
這裡寫圖片描述

我已經安裝了 VirtualBox,並且要建立一個叫“qianyu”的虛擬機器:

$  docker-machine create --driver virtualbox qianyu
INFO[0000] Creating SSH key...                          
INFO[0000] Creating VirtualBox VM...                    
INFO[0006] Starting VirtualBox VM...                    
INFO[0006] Waiting for VM to start...                   
INFO[0038] "qianyu" has been created and is now the active machine. 
INFO[0038] To point your Docker client at it, run this in your shell: $(docker-machine env testing)

這樣就在本地起了一個virtualbox虛擬機器,並在裡面部署好了docker。同樣我們可以指定雲,比如digitalocean,不過你需要提供你的賬戶資訊,這樣就會在雲上也創建出docker。而這所有的一切都是在一臺本地物理機器上完成的,每一個平臺的部署僅需一條或幾條命令。

“雖然官方只支援幾種特定平臺,但其它平臺的相容留給那些愛Docker的第三方廠商以及開發者去做。所以接下來一定會有很多的廠商跟進,比如國內阿里雲之類的,他們根據官方的介面開發個Driver即可加入Machine的能力。”—— https://linux.cn/article-4393-1.html

深入Docker大山坡

  • Docker概貌

”Docker有兩方面的技術非常重要,第一是Linux容器方面的技術,第二是Docker映象的技術。從技術本身來講,兩者的可複製性很強,不存在絕對的技術難點,然而Docker Hub由於存在大量的資料的原因,導致Docker Hub的可複製性幾乎不存在,這需要一個生態的營造。“

容器這種系統級的虛擬化運用了一項技術叫namespace isolation:Namespace isolation使主機能夠給每個容器一個虛擬的namespace,容器在這個虛擬的namespace內,只能看到它應該看到的資源。但為了提升效率,許多作業系統檔案、目錄和執行的服務在容器間共享,並對映到每個容器的namespace。僅當應用在它的容器內改變這些資源時,比如修改一個已存在的檔案或建立新檔案,容器會從宿主作業系統得到一份副本——利用Docker的“copy-on-write”優化,僅僅複製發生變化的部分。這一共享特性,是在一臺主機上高效部署多個容器的技術之一。還有一項技術叫cgroup,利用它可以實現對資源的限制和配置,如限制CPU的使用率。

Dockerfile 是軟體的原材料,Docker 映象是軟體的交付品,而 Docker 容器則可以認為是軟體的執行態。

Dockerfile中的四條命令 FROM, ADD, VOLUME, CMD, 這四條命令可以構建出一個映象來,分別對應四個映象層,見下圖。

這裡寫圖片描述

Docker最大的創新點在於Docker映象的設計,下面是張Docker映象的層次圖,分層的檔案系統,一層層地搭建出一個完整的容器執行環境:
這裡寫圖片描述

  • Cgroup/程序/物理資源/隔離/虛擬化/Docker

Cgroups可以限制、記錄、隔離程序組所使用的物理資源(包括:CPU、memory、IO等),為容器實現虛擬化提供了基本保證,是構建Docker等一系列虛擬化管理工具的基石,最初由Google工程師(Paul Menage和Rohit Seth)於2006年提出。

Cgroups可以對程序組使用的資源總額進行限制,如設定應用執行時使用記憶體的上限,一旦超過這個配額就發出OOM(Out of Memory)。通過分配的CPU時間片數量及硬碟IO頻寬大小,實際上就相當於控制了程序執行的優先順序。 cgroups可以統計系統的資源使用量,如CPU使用時長、記憶體用量等等,這個功能非常適用於計費。cgroups可以對程序組執行掛起、恢復等操作。

Cgroups也是LXC為實現虛擬化所使用的資源管理手段,可以說沒有cgroups就沒有LXC。從單個程序的資源控制,到實現作業系統層次的虛擬化(OS Level Virtualization)。

“根據Docker佈道師Jerome Petazzoni的說法,Docker約等於LXC+AUFS(之前只支援ubuntu時)。其中LXC負責資源管理,AUFS負責映象管理;而LXC又包括cgroup、namespace、chroot等元件,並通過cgroup進行資源管理。所以只從資源管理這條線來看的話,Docker、LXC、CGroup三者的關係是:Cgroup在最底層落實資源管理,LXC在cgroup上封裝了一層,Docker又在LXC封裝了一層,關係圖如圖1.b所示。”
(出自http://speakingbaicai.blog.51cto.com/5667326/1352962

這裡寫圖片描述
(a)
這裡寫圖片描述
(b)

圖1 Docker-LXC-CGroup結構圖

Docker的本質實際上是宿主機上的一個程序,通過namespace實現了資源隔離,通過cgroup實現了資源限制,通過UnionFS實現了Copy on Write的檔案操作。

  • Cgroup的術語與規則

術語表

task(任務):cgroups的術語中,task就表示系統的一個程序。
cgroup(控制組):cgroups 中的資源控制都以cgroup為單位實現。cgroup表示按某種資源控制標準劃分而成的任務組,包含一個或多個子系統。一個任務可以加入某個cgroup,也可以從某個cgroup遷移到另外一個cgroup。
subsystem(子系統):cgroups中的subsystem就是一個資源排程控制器(Resource Controller)。比如CPU子系統可以控制CPU時間分配,記憶體子系統可以限制cgroup記憶體使用量。
hierarchy(層級樹):hierarchy由一系列cgroup以一個樹狀結構排列而成,每個hierarchy通過繫結對應的subsystem進行資源排程。hierarchy中的cgroup節點可以包含零或多個子節點,子節點繼承父節點的屬性。整個系統可以有多個hierarchy

規則1: 同一個hierarchy可以附加一個或多個subsystem。如下圖1,cpu和memory的subsystem附加到了一個hierarchy。
這裡寫圖片描述
圖1 同一個hierarchy可以附加一個或多個subsystem

規則2: 一個subsystem可以附加到多個hierarchy,當且僅當這些hierarchy只有這唯一一個subsystem。如下圖2,小圈中的數字表示subsystem附加的時間順序,CPU subsystem附加到hierarchy A的同時不能再附加到hierarchy B,因為hierarchy B已經附加了memory subsystem。如果hierarchy B與hierarchy A狀態相同,沒有附加過memory subsystem,那麼CPU subsystem同時附加到兩個hierarchy是可以的。
這裡寫圖片描述
圖2 一個已經附加在某個hierarchy上的subsystem不能附加到其他含有別的subsystem的hierarchy上

規則3: 系統每次新建一個hierarchy時,該系統上的所有task預設構成了這個新建的hierarchy的初始化cgroup,這個cgroup也稱為root cgroup。對於你建立的每個hierarchy,task只能存在於其中一個cgroup中,即一個task不能存在於同一個hierarchy的不同cgroup中,但是一個task可以存在在不同hierarchy中的多個cgroup中。如果操作時把一個task新增到同一個hierarchy中的另一個cgroup中,則會從第一個cgroup中移除。在下圖3中可以看到,httpd程序已經加入到hierarchy A中的/cg1而不能加入同一個hierarchy中的/cg2,但是可以加入hierarchy B中的/cg3。實際上不允許加入同一個hierarchy中的其他cgroup野生為了防止出現矛盾,如CPU subsystem為/cg1分配了30%,而為/cg2分配了50%,此時如果httpd在這兩個cgroup中,就會出現矛盾。
這裡寫圖片描述
圖3 一個task不能屬於同一個hierarchy的不同cgroup

規則4: 程序(task)在fork自身時建立的子任務(child task)預設與原task在同一個cgroup中,但是child task允許被移動到不同的cgroup中。即fork完成後,父子程序間是完全獨立的。如下圖4中,小圈中的數字表示task 出現的時間順序,當httpd剛fork出另一個httpd時,在同一個hierarchy中的同一個cgroup中。但是隨後如果PID為4840的httpd需要移動到其他cgroup也是可以的,因為父子任務間已經獨立。總結起來就是:初始化時子任務與父任務在同一個cgroup,但是這種關係隨後可以改變。
這裡寫圖片描述

  • namespace, cgroup & AuFS

While namespaces are responsible for isolation between host and container, control groups implement resource accounting and limiting. In addition to the above components, Docker has been using AuFS (Advanced Multi-Layered Unification Filesystem) as a filesystem for containers. AuFS is a layered filesystem that can transparently overlay one or more existing filesystems. When a process needs to modify a file, AuFS creates a copy of that file. AuFS is capable of merging multiple layers into a single representation of a filesystem. This process is called copy-on-write.

  • Cgroups & Docker

Docker既可以使用LXC,也可以使用libcontainer,後者是新的而且是預設的。通過它們我們可以對container進行限制。比如,我們要將container鎖定在第一個core上,在 docker run 命令上加上 –cpuset-cpus=0。
另外 –cpu-shares 引數會定下 share 一個CPU的百分比。下面是CloudSigma上的一個示例:

$ docker run -d \
    --name='low_prio' \
    --cpuset-cpus=0 \
    --cpu-shares=20 \
    busybox md5sum /dev/urandom
$ docker run -d \
    --name='high_prio' \
    --cpuset-cpus=0 \
    --cpu-shares=80 \
    busybox md5sum /dev/urandom

這裡寫圖片描述

如果在一個host上管理很多個Docker container,就要藉助Cgroups,Docker支援兩個Cgroup driver,分別是LXC,libcontainer,各有優勢。

工業界對Docker使用

  • 騰訊萬臺規模的Docker應用實踐
    “... Docker在資源管理緯度方面只有CPU和記憶體兩個維度,這對於共享的雲環境下需要完善,也是目前相對於虛擬機器不足的地方。Gaia引入磁碟容量管理,網路出入頻寬控制以及磁碟IO的控制維護。 ...”

  • 基於容器的自動構建——Docker在美團的應用
    “... 該應用只利用了Docker最核心的容器功能,並沒有使用Docker叢集管理、排程、自動擴容等高階的功能。 ...”

  • Otto奧托集團的架構選型之路
    “... 主要講解otto.de的微服務架構 … 為了簡化不同微服務的部署和操作問題,每一個伺服器執行在獨立的Docker容器中。...”

Slides

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述