需求:搭建了autotest自動化工程,結合了flask,目的是将autotest中的方法開放接口出去(存在跨語言調用),并且開放了自動化測試接口出去(pytest+allure),以便可以實作調用接口跑case。
部署:gitlab-ci+docker+supervisor+uwsgi
docker和docker-compose
docker容器必須以前台程序啟動
CMD 容器啟動指令
這裡踩了個坑,一直嘗試在Dockerfile中以背景程序運作服務,結果發現容器啟動了就會立馬退出,所依賴的服務也不在。這是因為沒有了解好容器中應用在前台執行和背景執行的問題。
Docker 不是虛拟機,容器中的應用都應該以前台執行,而不是像虛拟機、實體機裡面那樣,用 systemd 去啟動背景服務,容器内沒有背景服務的概念。
一些初學者将 CMD 寫為:
CMD service nginx start
然後發現容器執行後就立即退出了。甚至在容器内去使用 systemctl 指令結果卻發現根本執行不了。這就是因為沒有搞明白前台、背景的概念,沒有區分容器和虛拟機的差異,依舊在以傳統虛拟機的角度去了解容器。
對于容器而言,其啟動程式就是容器應用程序,容器就是為了主程序而存在的,主程序退出,容器就失去了存在的意義,進而退出,其它輔助程序不是它需要關心的東西。
而使用 service nginx start 指令,則是希望 upstart 來以背景守護程序形式啟動 nginx 服務。而剛才說了 CMD service nginx start 會被了解為 CMD [ “sh”, “-c”, “service nginx start”],是以主程序實際上是 sh。那麼當 service nginx start 指令結束後,sh 也就結束了,sh 作為主程序退出了,自然就會令容器退出。
正确的做法是直接執行 nginx 可執行檔案,并且要求以前台形式運作。
比如:
CMD ["nginx", "-g", "daemon off;"]
docker-compose中services下webapp的名字需要指定為自己服務的名字
docker-compose模闆中的command會覆寫容器啟動後預設執行的指令。
我的實體機上之前利用docker-compose啟動個應用伺服器叫server,然後這次想啟動第二個服務,發現兩個服務發版後一直互相覆寫。
up
格式為
docker-compose up [options] [SERVICE...]
該指令十分強大,它将嘗試自動完成包括建構鏡像,(重新)建立服務,啟動服務,并關聯服務相關容器的一系列操作。
連結的服務都将會被自動啟動,除非已經處于運作狀态。
可以說,大部分時候都可以直接通過該指令來啟動一個項目。
預設情況,docker-compose up 啟動的容器都在前台,控制台将會同時列印所有容器的輸出資訊,可以很友善進行調試。
當通過 Ctrl-C 停止指令時,所有容器将會停止。
如果使用 docker-compose up -d,将會在背景啟動并運作所有的容器。一般推薦生産環境下使用該選項。(直接運作Dockerfile中的前台程序)
預設情況,如果服務容器已經存在,docker-compose up 将會嘗試停止容器,然後重新建立(保持使用 volumes-from 挂載的卷),以保證新啟動的服務比對 docker-compose.yml 檔案的最新内容。如果使用者不希望容器被停止并重新建立,可以使用 docker-compose up --no-recreate。這樣将隻會啟動處于停止狀态的容器,而忽略已經運作的服務。如果使用者隻想重新部署某個服務,可以使用
docker-compose up --no-deps -d <SERVICE_NAME>
來重新建立服務并背景停止舊服務,啟動新服務,并不會影響到其所依賴的服務。
看到
<SERVICE_NAME>
後,想到docker-compose會為啟動的每個容器服務命名(注意這裡不是容器名字,容器名稱可以通過
container_name
指定),于是差別了下兩個compose模闆檔案中的容器服務名字,問題解決。
supervisor
前台說到docker必須前台程序方式啟動,是以supervisord服務也必須以前台方式啟動,下面附上配置:
supervisor.conf
[program:autotest]
# supervisor執行的指令
command=uwsgi --ini /autotest/app/deploy/supervisor/uwsgi.ini
# 項目的目錄
directory = /autotest
# 程式需要保持running狀态startsecs秒,才被判定為啟動成功
startsecs=0
# 停止的時候等待多少秒
stopwaitsecs=3
; # 自動開始
; autostart=true
; # 程式挂了後自動重新開機
; autorestart=true
# 輸出的log檔案
stdout_logfile=/autotest/app/log/supervisor.log
# 輸出的錯誤檔案
stderr_logfile=/autotest/app/log/supervisor.err
[supervisord]
nodaemon=true # 關閉daemon,使supervisord在前台運作,不然的話docker程序啟動後會立馬退出
loglevel=debug
user=root
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
docker-compose
version: '2'
services:
autotest:
image: {鏡像位址}
build:
context: .
dockerfile: Dockerfile-server
ports:
- "5000:5000"
networks:
- dev_network
restart: always
container_name: autotest
command: bash -c "supervisord -c app/deploy/supervisor/supervisor.conf ; tail -f app/log/track.log ; tail -f app/log/supervisor.err"
networks:
dev_network:
driver: bridge
這裡說一下,supervisor是一個很強大的程序管理工具,https://blog.csdn.net/windy135/article/details/87248948
也可以用它來管理腳本任務,比如command、cron,
[program:token-schedule]
command=python /autotest/service/schedule.py
directory = /autotest
startsecs=0
stopwaitsecs=3
autostart=true
autorestart=true
stdout_logfile=/autotest/app/log/supervisor.log
stderr_logfile=/autotest/app/log/supervisor.err
uwsgi
上述supervisord在docker容器中是以前台程序方式啟動,那關于supervisord管理的程序,即子程序沒有保活程式的原因有如下:
1、command中執行的程式是 背景程序、或者是立刻結束的shell腳本,或者是cron表達式,這些command馬上就結束的,supervisor會認為程式已結束,并且重試3次(預設),發現始終起不來,就不再守護程序。supervisorctl指令能看出程序的監控狀态,RUNNING是正常的。
2、看配置檔案裡面有木有設定autostart=true
上述supervisor.conf中可以看到
command=uwsgi --ini /autotest/app/deploy/supervisor/uwsgi.ini
,關于uwsgi前台程序還是背景程序啟動踩了寫坑。
一開始uwsgi是以前台程序啟動的,即
[uwsgi]
pidfile = /var/run/uwsgi.pid
http = :5000
daemonize = /autotest/app/log/uwsgi.log
chdir = /autotest
module = app.manager # chdir保持在項目的根目錄,是以這裡的module可以 app.manager來指定,這樣uwsgi就可以找到app變量了(這樣可以友善的引用autotest的腳本方法了)
wsgi-file = /autotest/app/manager.py
callable = app
master = True
logdate = True
memory-report = True
enable-threads = True
single-interpreter = True
harakiri = 40
harakiri-verbose = True
processes = 2
buffer-size = 65536
reload-on-rss = 256
add-header = Connection: Keep-Alive
so-keepalive = True
http-keepalive = True
這樣supervisord可以捕捉到了uwsgi的前台程序,是以uwsgi應用伺服器啟動成功,flask接口也能通路成功。
後來因為使用了docker部署,且本地代碼運作沒有問題,docker上服務接口報了500,是以欲debug該問題,查了supervisord.log、supervisor.err都沒有包含具體錯誤代碼資訊(supervisord.log隻有子程序的啟動日志,supervisor.err隻有請求接口的status_code500日志),後來發現需要配置uwsgi日志,是以在上述uwsgi,ini配置中加入了
daemonize = /autotest/app/log/uwsgi.log
。
接着使用docker-compose啟動,發現supervisord父程序一直在重新開機uwsgi子程序,接口不是不可通路的。原因就是加上了上面那個uwsgi.log配置後,uwsgi是以daemon方式運作的,supervisord捕捉不到該背景程序,由于supervisor.conf中一開始打開了
# 自動開始
autostart=true
# 程式挂了後自動重新開機
autorestart=true
于是就導緻了上面說的那個現象。注釋這兩個選項後,supervisord隻啟動uwsgi一次後就結束了,是以uwsgi正常的在docker容器中以supervisord的背景子程序運作着了。
logging
檢視日志發現,500接口依然是隻有一條日志資訊列印,後來明白了。
uwsgi是應用伺服器,隻能監聽請求日志。(這裡補充幾點,nginx是反向代理,将域名轉發到應用伺服器的程序,nginx的access.log和error.log都隻能監聽到請求也包含狀态碼,supervisor.err日志也隻能監聽請求500的列印)。
是以就要在flask web服務架構及代碼引入logging子產品來進行日志捕捉列印,可以主動捕捉log資訊,沒有捕捉到的程式異常(500)也可以列印出來。到此檢視到時Allure在docker中目錄不存在的問題,問題解決。
即 nginx、uwsgi日志隻會列印http協定、uwsgi協定請求日志,無法列印到具體的500代碼報錯,具體代碼報錯可以借助logging日志子產品捕捉。
關于上面加入uwsgi.log配置後,uwsgi以daemon方式運作,嘗試了http/socket兩種方式運作,遇到了兩種有意思的錯誤,記錄下,
supervisor捕捉不到前台程序,是以一直會自動重新開機uwsgi: uwsgi使用deamon模式,以http形式啟動,uwsgi頻繁重新開機,flask app 通路不了。 uwsgi使用deamon模式,以socket形式啟動,uwsgi重新開機會報端口号占用,因為重新開機前的socket(對應到uwsgi module子產品的端口号)還在。
http 和 http-socket的使用上有一些差別:
http: 自己會産生一個http程序(可以認為與nginx同一層)負責路由http請求給worker, http程序和worker之間使用的是uwsgi協定。
http-socket: 不會産生http程序, 一般用于在前端webserver不支援uwsgi而僅支援http時使用, 他産生的worker使用的是http協定
是以, http 一般是作為獨立部署的選項; http-socket 在前端webserver不支援uwsgi時使用。
如果前端webserver支援uwsgi, 則直接使用socket即可(tcp or unix)。
至此部署完成,可以看出supervisord是以docker前台主程序存在的,supervisord前台程序管理着uwsgi背景子程序,同時也管理着背景cron程序-token-schedule。
gitlab-ci
最後說下ci配置,注意docker build的上下文環境即可,不然docker鏡像越來越臃腫。
# The [runners.cache] section One of: s3, gcs.
# Docker in docker
image: youpy/docker-compose-git
services:
- docker:18-dind
variables:
IMAGE: {images}
ENV: ${ENV}
SERVICE: ${SERVICE}
before_script:
- docker login -u="${DOCKER_USER}" -p="${DOCKER_TOKEN}" {docker domain};
stages:
- build
- deploy
- test
build:
stage: build
tags:
- docker
only:
- dev
- master
script:
- >
echo "docker build ENV: ${ENV}";
echo "docker build start";
docker pull ${IMAGE};
cd app/deploy && docker build -t ${IMAGE} --build-arg ENV=${ENV} -f Dockerfile-server . ;
docker push ${IMAGE};
echo "docker build finish";
deploy:
stage: deploy
tags:
- docker
only:
- dev
- master
script:
- >
docker-compose -f /builds/{你的檔案目錄}/docker-compose.yaml up --no-deps -d
test:
stage: test
tags:
- docker
only:
- dev
- master
script:
- >
docker exec autotest bash -c "pytest testcases/ -m ${SERVICE} --verbosity=2"
附上docker常用的批量删除鏡像和容器的指令:
批量删除容器:
查詢所有的容器,過濾出Exited狀态的容器,列出容器ID,删除這些容器:
docker rm `docker ps -a|grep Exited|awk '{print $1}'`
删除所有未運作的容器(已經運作的删除不了,未運作的就一起被删除了):
docker rm $(sudo docker ps -a -q)
批量删除鏡像:
删除所有名字中帶 “none” 關鍵字的鏡像,即可以把所有編譯錯誤的鏡像删除:
docker rmi $(docker images | grep "none" | awk '{print $3}')
docker 批量删除 鏡像指令:
docker ps -a | grep "Exited" | awk '{print $1 }'|xargs docker stop
docker ps -a | grep "Exited" | awk '{print $1 }'|xargs docker rm
docker images|grep none|awk '{print $3 }'|xargs docker rmi