Docker 從入門到入迷

Docker 從入門到入魔😈

之魔鬼藏在細節裡

這裡會講到

  • Docker
  • Docker Compose

不會講到

以及其他很複雜但除範例之外
不知道怎麼跑起來的方案😏

大家都說容器化是未來趨勢...

但我們只有一堆Copy & Paste的Server端舊程式...

要用怎樣的方法來最小幅度改造舊程式, 以便能夠用 Docker + Docker Compose的組合, 配合內部架設 Docker庫來做開發時期的維護及營運時期的運行。

首先來說為何要容器化...

來源:http://blog.gameanalytics.com/blog/what-analysing-400-games-taught-us.html

從正式上線營運開始後三星期到一個月之間是
關鍵💰觀察期

流量特性:

  • 一開始會有暴衝流量(衝首發)
  • 如果紅了還會繼續暴衝(病毒式傳播)
  • 如果不紅就會掉得很快(該撤收了)
  • 真的不紅主動減少維護人力硬體等開銷
    (自動營運)

如何根據實際情況有效配置硬體資源讓營運成本降低?

讓伺服器配置有彈性

  • 安裝佈署的方法要很容易&自動化
  • 遷移到更↑或↓配置的硬體流程簡易&快

  • 最好還不用多花錢💸

Game Table

  • 遊戲系統中佔很大部分的設定檔
  • 遊戲商業邏輯的核心資料
  • 至少一半bug是這類資料沒設定正確導致
  • 要有辦法能在發現錯誤之後快速回溯上個版本重新佈署 (退版)

使用Docker做容器化

  • Ops:
    • 伺服器的部署方法完全一致統一化
    • 多個服務要(縮減規模)同時部署在單台硬體上較簡單
      (port mapping且容器內的程式不需改設定)
    • 萬一要退版,藉由Docker的 Union File System 特性可快速退回先前的狀態來回溯服務狀態
  • Dev:
    • 開發環境≒營運環境,而且不用花時間自己安裝
    • 不怕過了很久以後要修改程式碼找不到舊版本函式庫&套件工具

Docker
(docker-engine)
是什麼

Docker
(docker-engine)
是什麼

不管它是什麼,要做的就是:

  1. 將應用程式&執行環境進行包裝產生可佈署的『image』
  2. 『image』藉由一些步驟導入到目標端機器的docker上
  3. 『image』在目標端的docker上啟動執行該應用

幾個概念

  1. client-server架構
  2. Running Container (run time)
    v.s.  Fixed Image (build time)
  3. Union File System分層構造
    (for static data & code or exe)
  4. Volume for state data
    • 多個容器可以共享單一volume。
    • 容器volume可以對應到實體機器FS路徑位置。
    • image產生時可以定義內部路徑位置的volume,並由另一個容器啟動時mount上去使用。
      (--volumes-from)

Docker Compose
是什麼

  • docker的client端操作單用指令列打命令落落長
  • 運作環境的配置通常不會只有單一個容器
  • 根據執行環境有不同的環境變數&設定檔對應

需要有類似Bash Script批次化一堆手工命令操作步驟的工具

                        
version: '2'

services:
  varnish:
    extends:
      file: docker-compose.base.yml
      service: varnish-base
    env_file:
      - ./conf/varnish-cache/env/web.env
    volumes:
      - ./conf/varnish-cache/vcl:/etc/varnish/
    links:
      - default_site:normal_site
    ports:
      - "80:80"
    networks:
      - back_end

  # application container
  default_site:
    extends:
      file: docker-compose.base.yml
      service: nginx-base
    volumes:
      - ./conf/nginx/config:/etc/nginx
    links:
      - phpfpm
    volumes_from:
      - phpfpm
    networks:
      - back_end

  phpfpm:
    extends:
      file: docker-compose.base.yml
      service: phpfpm-base
    volumes:
      - ./conf/phpfpm/config:/usr/local/etc
      - ./src/php:/var/www/html
    links:
      - redis0cache
    networks:
      - back_end

  #cache container
  redis0cache:
    extends:
      file: docker-compose.base.yml
      service: redis-base
    volumes:
      - ./conf/redis/config:/usr/local/etc/redis
      - ./data/redis:/data
    ports:
      - "6379:6379"
    networks:
      - back_end

networks:
  back_end:
                        
                    

使用Docker Compose的優點

容器化的步驟

既有應用程式docker化的方法:

  1. 寫dockerfile檔以docker build建立出『image』
  2. 寫docker-compose.yml檔配置組態
  3. docker-compose up,打完收工

這麼簡單piece of cake🍰?!大家可以去吃點心了?!

當然不是!😓

寫dockerfile檔以docker build建立出『image』

如何分層?

既有業務流程
  • 開發技術
  • 商業邏輯
  • DevOps流程
容器化技術特性
  • 單一容器功能要單一
  • image可多場合reuse
  • 方便&快速deploy

Layering...


Ops mode


Dev mode

整體流程

開發時期:

  1. 根據docker hub上提供的官方image切分服務,不夠的再自行根據Base OS image寫Dockerfile自建。
  2. 撰寫Dockerfile及docker-compose.yml,建出配合舊有應用程式所需的image,設定檔的部分也先用volume mapping的方式餵進去。
  3. 使用volume mapping的方式將原本建置流程產生的執行檔直接餵進去讓docker容器使用,進行開發測試。
    軟體開發動力學第0定律—可以跑就好🙃
  4. 封裝開發時期產出物:
    • 撰寫新Dockerfile繼承自原本開發時期使用的docker image,新增的ADD/COPY指令動作將建置成果執行檔加入image。
    • 採用『data volume container』的方式把設定檔
      (不含Game Table資料)封裝產生docker image。
    • 以上兩種docker image要release時記得根據版本號加tag

整體流程

Game Table資料導入&驗證審核:

  1. 遊戲企劃使用除了Game Table目錄資料由volume mapping導入,其他都是由開發團隊釋出的docker image搭建起來的系統上,實際執行測試遊戲,即時修改及驗證Game Table的資料正確性。
    所以需要程式內先做 B A Konami Code來加快效率才不會測得太辛苦🙃
  2. 資料測試無誤後:
    • data volume container的方式將Game Table資料封裝進 docker image
    • docker image釋出時加tag版本號。


以上所有可發佈docker image都會push到內部Docker庫準備deploy

整體流程

運營時期:

  1. 運營環境的Linux伺服器上有設定環境變數,對應docker image正確配置的版本號tag。
  2. 營運人員由docker-compose.yml檔執行docker-compose pull把對應tag版本的docker image下載到伺服器上。
  3. 執行docker-compose up,啟動服務。


  • 要進版/退版:修改環境變數後再docker-compose pull抓取對應版本啟動服務應用。
  • 實際使用時配合寫好的shell script執行,在script內預先備份/建立好啟動時需要的docker volume。
  • 甚至容器執行需要的設定檔、docker-compose的yml檔都是塞進一個專用docker image,運營人員pull/start該image以便取出使用。

實務上既有系統容器化有5種類型的坑:

app要跟著轉變,時間會說明一切 (Microservice)
設裡面設外面 (Isolation)
過個水變權(貴) (Boundary)
網路卡無極限 (Networking)
Infra自幹有點累 (OS API/Service)
根本不是code的罪 (Environment)

其他無法歸類的坑也很多......

設裡面設外面  (Isolation)


設定是應用程式本身的?
設定給執行環境(OS)的?
容器化的隔離技術是怎麼實現的??(該K的底層功課逃不了)

過個水變權 (貴)  (Boundary)

  • Docker預設跑在容器內的使用者權限是【Root】!

  • 容器內的程式假如寫入到volume mapping對應的主機檔案,原本的檔案權限都亂掉了~

  • Workaround:手動在啟動容器時額外塞外部的$UID、$GID環境變數,然後容器內最後要多做指令:
    chown -R ${UID}:${GID}
    把權限 回來。
  •  
  • 正解:資安角度強烈建議運營環境的docker容器內不要用預設執行權限!
    使用v1.10之後的 User Namespace mapping功能。

網路卡無極限  (Networking)

    • Docker容器最簡單可供外界存取服務的配置方式:
      透過port mapping對應到Host主機特定埠上
    • 某些較早Linux Kernel版本(Ubuntu 12.04)且安裝多張網卡的環境,非預設路由網卡進來的連線會無法存取對應容器mapping port的服務。
  •  
    • 容器內應用服務listening [address:port]的設定寫成IPv6格式,在安裝太古早網卡硬體的hosting環境下服務會無法正常啟動。

   無解,人品不好就不要用太舊的軟硬體跑Docker😵

Infra自幹有點累  (OS API/Service)

Docker容器內的應用需要用到原本倚賴的作業系統服務
(例:Unix/Linux的crontab排程)
怎麼辦?

自幹一個!💪

當然更好的解法是程式改寫成使用其他外部服務

根本不是code的罪  (Environment)

  1. Docker storage bug:
  2.  
  3. Hosting environment bug:

 使用的工具 & 環境  複合因素造成的地雷還不少......

app要跟著轉變,時間會說明一切  (Microservice)

實際範例Demo

結論


很想說的是:


但要說的是: Concept    Idea     Technique


來源:仮面ライダーフォーゼ episode 3

   只是別這樣:

Do it Right!
Thank you~

投影片

範例程式


參考資料

常用docker & docker-compose指令

                        
#List all container(running or not running):
docker ps -a

#Run container(it will download image -> create container -> run it):
docker run image_name [command to run in container]

#Start a  stopped state container:
docker start [container_ID|container_Name]

#Check container detail information(include virtual IP address):
docker inspect [container_ID|container_Name]

#Get container's IP address:
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' [container_ID|container_Name]

#See container's information filter by certain "JSON node":
docker inspect --format='{{json .[JSON_node_name]}}' [container_ID|container_Name]

#Console into running container:
docker exec -it [container_ID|container_Name] bash

#When using docker-compose, this is create new container and run with command:
docker-compose [-f the_yml_file] run yml_defined_service_name bash

#See container's running log:
docker logs [container_ID|container_Name]

#Save container version:
docker commit [container_ID|container_Name] The_name_you_want

#Stop running container:
docker stop

#Show current docker daemon's images:
docker images

#Remove all containers:
docker rm -f $(docker ps -aq)

#Remove all images currently in host:
docker rmi -f $(docker images -aq)

#Remove all volumes in current host:
docker volume rm $(docker volume ls -q)

#Remove all untagged images (images has <none> in REPOSITORY field as example):
docker rmi $(docker images -q -f=dangling=true)
                        
                    

redis容器設定的demo

另開新頁 會發現居然kernel設定是要設在跑docker-engine的Host上,redis有個啟動warning才會消失
😱

使用Process Manager + tail指令

  • 用來把原本程式行為是Hard Code寫入到固定位置檔案的log,導入到docker-engine的預設log機制裡。
  •  
  • 設定時要注意 Pid 1問題,以免docker容器有停下來要非常久的問題。
  •  
  • 以下用另一個Python的Process manager:  circus 的設定範例:
                        
[watcher:show_log_worker]
cmd = /usr/bin/tail --retry -f /php_log/access.log --retry -f /php_log/exception.log --retry -f /php_log/error.log --retry -f /php_log/slow.log
copy_env = True
stdout_stream.class = StdoutStream
stderr_stream.class = StdoutStream

[watcher:phpfpm_worker]
cmd = /usr/local/sbin/php-fpm
working_dir=/var/www/html
numprocesses = 1
warmup_delay = 3
graceful_timeout = 10
copy_env = True
stdout_stream.class = StdoutStream
stderr_stream.class = StdoutStream