gitlab-ci+docker+supervisor+uwsgi部署踩坑,使用uwsgi,如何查看500错误?

5 篇文章 0 订阅
3 篇文章 0 订阅

需求:搭建了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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值