gitlab-ci+docker+supervisor+uwsgi部署踩坑,使用uwsgi,如何查看500错误?
需求:搭建了autotest自动化工程,结合了flask,目的是将autotest中的方法开放接口出去(存在跨语言调用),并且开放了自动化测试接口出去(pytest+allure),以便可以实现调用接口跑case。
部署:gitlab-ci+docker+supervisor+uwsgi
docker和docker-compose
docker容器必须以前台进程启动
这里踩了个坑,一直尝试在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是一个很强大的进程管理工具, 也可以用它来管理脚本任务,比如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
即 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