zoukankan      html  css  js  c++  java
  • laravel后端项目的Docker镜像打包

    对读者的要求

    • 后端开发基础知识
    • 掌握Docker基础用法以及docker-compose用法(有laradock使用经验为佳)
    • Laravel基础

    简介

    在第一篇文章《纯前端项目的Docker镜像打包》中,提到后端项目的镜像打包方面比较复杂,需要独立一篇。我们的目标仍然是执行一条命令就要能完成应用的启动。纯前端项目使用的是docker run,后端项目需要用到镜像编排(docker-comopse),启动命令变成:

    docker-compose up -d --renew-anon-volumes
    

    docker-compose使用最新版本,否则--renew-anon-volumes参数可能不支持

    docker-compose的内部结构如下图的“应用”部分所示,里面编排了4个任务;而后端需要的数据库缓存队列对象存储以及安全配置则由外部提供,docker-compose不关心它们的来源:

    下面我们一步步了解,为什么会形成这样的图,以及怎么打包以满足图中所示的架构。

    概念

    在谈论后端的镜像打包和镜像测试之前,首先要认识到Docker要用得好,需要满足两大特征:

    1. 无状态
    2. 单一职责

    无状态是指容器不会保存任何东西,不论往容器里面写入任何东西,再次执行docker-compose up -d,启动后的容器是找不到原来写入的东西的。有些场景如果需要保存数据,比如数据库和存储,它们需要通过挂载保证写入的数据不因为重启而丢失,这类容器称为有状态容器。

    要保证应用的水平扩展快速迁移,所有的应用在启动时必须是无状态的。试想一下,你的应用原本在A机器上执行一句docker-comopse up -d就行了,现在要改到B机器上运行,就在B机器上执行一句docker-compose up -d
    如果它是有状态的(比如A在启动应用的时候,将存储挂载到主机,以便用户上传的图片和应用产生的日志可以永久保留),那么迁移到B机器时,它的日志和图片仍然在A机器上,快速迁移受到了限制。
    另一个更常见的场景是,A,B两台机器同时启动以便负载均衡,如果A和B是有状态的,就会出现跟迁移一样的问题,A和B都会遗漏用户上传的图片,这就限制了水平扩展。因此,应用必须是无状态的。

    单一职责简单说就是只干一件事,反映到Docker上,就是最好只启动一个进程(取决于对职责的解释,也可能启动多个进程)。比如php-fpm镜像只启动php-fpmnginx镜像只启动nginx。要做到将nginxphp-fpm都在同一个里容器里启动当然是可以的,但是它带来的代价往往比比好处更大。越多不同职责的功能塞到同一个容器里,代价就越大。
    Laravel工程为例,有4个不同职责的进程:nginxphp-fpmphp队列cron定时任务。混在一起以后,第一个问题是:任何一块有更新,都要重新打包这个镜像,而通常这个镜像里面的内容比较多,通常会很大;第二个问题是职责的混合导致它很难与已有的系统结合,典型的是我们只需要php-fpmnginx由外部提供,如果打包到一起,nginx会产生干扰;另一个典型的场景是暂时不需要用到php队列cron定时任务,它们也必须开着;第三个则是日志信息的混乱,nginxphp-fpm的日志混在一起无法有效地提取。

    后端应用要满足这2个特征,就带来2个直接的问题:

    • 后端访问的数据库、队列和存储放哪里?
    • nginx、php-fpm、php队列、定时任务怎么编排?

    所有后端应用的配置都是比较敏感,比如数据库密码,是不能打包到镜像中的,于是就有了第3个问题:

    • 敏感信息怎么保护?

    关键问题的分析

    后端镜像的打包,重点在于思考清楚这3个问题。下面我们一个个问题的分析。

    Q:后端要访问的数据库、缓存、队列和存储放哪里?
    A:这三个需要与应用分开单独考虑,在生产环境下,一般使用一台独立的机器,或者直接使用云服务商提供的数据库、缓存等服务。这样不论应用迁移到哪里,都能使用同一份数据库、缓存和存储。

    Q:nginx、php-fpm、php队列、php定时任务怎么编排?
    A:单一职责在实践上是有争议的。如果是要求容器只启动一个进程的角度,将nginxlaravel php-fpmlaravel队列laravel定时任务独立成4个任务,如果要检查nginx证书是否过期,还得有certbot,共5个任务。一个简单的后台应用要有5个任务,听起来头就很大。

    另外一种对单一职责的解释则是从就应用的范围去解释。nginxphp-fpm管的东西八竿子打不着,所以肯定是独立的两个镜像容器。但是上面的nginxcertbot要合在一起,而laravel php-fpmlaravel 队列laravel 定时任务也要合在一起,镜像编排时只启动2个容器。

    这两种解释目前我们都接受,应用小的时候可以采用后一种方式,应用变大时要过度到第一种方式。

    Q:敏感信息怎么保护?
    A:通过环境变量的方式注入。云容器有提供配置环境变量的地方。如果自己启动docker-compose,环境变量可以通过直接读取独立的环境变量文件,或者从vault等管理密钥的平台读取。

    解决这3个问题以后,最后形成的镜像结构就如“简介”中的图所示。

    目录结构

    脚本文件比较多,并不适合直接放到后端代码里面。要么将后端与脚本独立成2个项目,要么按如下调整项目的总体目录结构:

    backend/
    - Dockerfile 
    - scripts/
        - start.sh
        - crontab
        - worker.conf
        - build.sh
        - push.sh
    - ...其他Laravel文件直接忽略
    scripts/
    - nginx/ # nginx 镜像制作 
        - Dockerfile
        - app.conf
        - nginx.conf
        - build.sh
        - push.sh
    - prod/ # 生产环境的镜像编排测试
        - docker-compose.yml
        - update.sh
        - app-backend.env # 不能放git仓库
    - prod-local/ # 本地镜像编排测试
        - docker-compose.yml
        - update.sh
        - app-backend.env # 不能放git仓库
    

    步骤

    制作php-fpm镜像配置

    先直接看Dockerfile,然后对这个文件做详细解释:

    FROM pheye/php-fpm:latest
    
    MAINTAINER LIUWENCAN <phenye@gmail.com>
    
    # 将源码拷到镜像中
    COPY . /var/www/backend
    # 确保没有将.env打包进去
    RUN if [ -e .env ] ; then rm .env; fi
    
    # 启动脚本,除了php-fpm还有一些额外的配置
    COPY scripts/start.sh /start.sh
    RUN chmod +x /start.sh
    # 用于任务调度的任务
    COPY scripts/crontab /etc/cron.d/www
    # 用于支持worker的启动
    ADD ./scripts/worker.conf /etc/supervisor/conf.d/worker.conf
    
    # 修改属主,确保与php-fpm的用户一致
    RUN chown -R www /var/www/backend
    
    VOLUME /var/www/backend
    
    CMD ["/start.sh"]
    

    关于这个文件,首先要了解php-fpm的基础镜像,官方有提供,不过php-fpm的许多扩展需要自己配置,很容易出现遗漏,性能调优也需要自己配置,cronsupervisord等用于支持任务调度和队列的包也需要自己安装,整个配置过程是非常繁琐的,因此这里我使用自己在生产环境验证过的php-fpm作为基础镜像,有做调整时就更新该包即可

    其次是VOLUME /var/www/backend这一句,非常重要,它会开放一个匿名挂载供nginx使用,否则nginx容器里面将会是空的,没有任何应用源码。

    最后是,Dockerfile中出现了start.shworker.confcrontab这3个文件,下面要针对这3个文件做个详细解释。

    start.sh是启动脚本,正常来讲,只需要启动php-fpm就能工作,但是默认情况没有考虑到迁移文件的需求,该脚本可以做更多必要的工作。

    #!/bin/sh
    
    # 用于启动性能采集
    # nohup tideways-daemon &
    
    # 执行migration
    cd /var/www/backend
    php artisan migrate --force
    if [ ! -f "public/storage" ] ; then php artisan storage:link; fi
    
    # 下面这2个被注释的命令有助于提高性能,但是可能导致应用不可用,根据需要自己启动
    # php artisan optimize 
    # php artisan api:cache 
    if [ $? -eq 0 ] ; then
        # 启动php-fpm
        php-fpm
    else
       exit 1
    fi
    

    worker.confsupersivord的配置文件,用于确保php队列的可靠启动和对队列的进程数量做精确控制

    [program:worker]
    process_name=%(program_name)s_%(process_num)02d
    command=php /var/www/backend/artisan queue:work --sleep=3 --tries=3 --daemon
    user=www
    autostart=true
    autorestart=true
    numprocs=2
    stdout_logfile=/dev/fd/1
    stdout_logfile_maxbytes=0
    stderr_logfile=/dev/fd/2
    stderr_logfile_maxbytes=0
    

    crontab用于执行任务调度

    * * * * * www php /var/www/backend/artisan schedule:run > /dev/null 2>&1
    

    构建php-fpm镜像

    docker build -t  app-backend:latest  .
    

    制作nginx镜像配置

    nginx的制作与《培训-纯前端项目的Docker镜像打包》几乎一样, 这里就不再赘述。只简单提下不同的几个小点:

    1. Dockerfile不需要添加任何应用的源码;
    2. app.conf与前端不一样,完整代码见下面;
    3. app.conf中的fastcgi_pass backend:9000;需要特别注意,backend是实际启动的php-fpm的容器名。

    Dockerfile

    FROM nginx:alpine
    
    MAINTAINER LIUWENCAN <phenye@gmail.com>
    
    RUN adduser -D -H -u 5000 -s /bin/sh www
    RUN rm /etc/nginx/conf.d/default.conf
    ADD nginx.conf /etc/nginx/
    ADD backend.conf /etc/nginx/sites-available/
    
    VOLUME /var/www
    
    CMD ["nginx"]             
    

    区别只在于app.conf的内容不一样:

    server {
        listen 80;
        listen [::]:80;
    
        server_tokens off;
        server_name demo-app.store.codefriend.top;
        root /var/www/backend/public;
        index index.php index.html index.htm;
    
        location ~* .*.(gif|jpg|jpeg|png|bmp|swf|js|css)$ {
            expires      30d;
            add_header Cache-Control "public";
        }
    
        location / {
             try_files $uri $uri/ /index.php$is_args$args;
        }
    
        location ~ .php$ {
            internal;
            try_files $uri /index.php =404;
            fastcgi_pass app-backend:9000; # 这个名字需要注意
            fastcgi_index index.php;
            fastcgi_buffers 16 16k;
            fastcgi_buffer_size 32k;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
        }
    
        location ~ /.ht {
            deny all;
        }
    
        location /.well-known/acme-challenge/ {
            root /var/www/letsencrypt/;
            log_not_found off;
        }
    
    }
    

    构建nginx镜像

    docker build -t  app-nginx:latest .
    

    本地镜像编排

    本地镜像编排docker-comopse.yml

    version: '2'
    services:
      backend:
        image: app-backend:latest
        env_file: "./app-backend.env"
      cron:
        image: app-backend:latest
        env_file: "./app-backend.env"
        command: ['cron', '-f']
      worker:
        image: app-backend:latest
        env_file: "./app-backend.env"
        command: ['/usr/bin/supervisord', '-n', '-c', '/etc/supervisor/supervisord.conf']
      nginx:
        image: app-nginx:latest
        volumes_from:
          - backend
        depends_on:
          - backend
        ports:
          - "8888:80"
    

    镜像编排中有两个重点:

    1. 出现了app-backend.env这个文件,这个文件就是Laravel.env,这些敏感一般放在保密的对象存储上,或者通过云容器的环境配置、或者放在vault这样的密钥管理里面。
    2. nginxvolumes_from来自backend,这一句非常重要,它确保nginx可以直接读取静态文件,没有这一句的话,nginx没有任何应用的文件,读取静态文件时将直接报错。

    测试镜像

    docker-compose up -d --renew-anon-volumes
    

    --renew-anon-volumes这个参数用于更新匿名挂载,非常重要。因为nginx里面本身没源码,源码是跟着php-fpm,如果没有这个参数,当php-fpm那边的代码更新以后,nginx这边仍然是旧代码。

    docker-compose ps看下各个任务是否正常启动,如果已经正常启动。直接访问http://localhost:8888应该能够正常进入。

    如果不能正常进入,结合docker-compose logdocker-compose top以及通过docker-compose exec进到容器内部排查。

    进阶配置1-版本控制

    前面制作的镜像,版本都是latest,这种玩法在生产环境是有问题的,要是制作的镜像不能用,一启动起来就是人间惨剧。因此制作的镜像都需要指定版本(一般通过CI/CD自动生成),布署出问题的时候就回滚。下面是怎么方便指定版本的脚本

    php-fpm构建支持指定版本的生产镜像

    php-fpm部分,增加backend/scripts/build.sh

    #!/bin/sh
    
    if [ $# -gt 1 ] ; then
        docker build -t app-backend:$1 -t  app-backend:latest  .
    else
        docker build -t  app-backend:latest  .
    fi
    

    要制作镜像时就进入backend目录,指定版本构建,如果不指定,就是latest版本:

    ./scripts/build.sh v1.0.0
    

    nginx构建支持指定版本的生产镜像

    nginx部分,与前面的类似,增加scripts/nginx/build.sh:

    #!/bin/sh
    
    if [ $# -gt 1 ] ; then
    docker build -t app-nginx:$1 -t  app-nginx:latest  .
    else
    docker build -t  app-nginx:latest  .
    fi
    

    要制作镜像时就进入scripts/nginx目录,指定版本构建,如果不指定,就是latest版本:

    ./build.sh v1.0.0
    

    进阶配置2-推送镜像与编排测试

    本地构建的镜像在本地测试,只能说它没有问题。但是要实际使用,需要推送到公共的镜像仓库以供其他人获取。https://hub.docker.com/是官方提供的免费镜像仓库,放公开的镜像是极好的。

    公司的应用,一般都要放在私有镜像仓库。私有镜像仓库,国内阿里云,国外AWS都有提供,自己要搭建的话,可以使用harbor。本文以阿里云作为例子,演示私有镜像仓库的推送和拉取。

    php-fpm的推送镜像脚本

    创建backend/scripts/push.sh:

    #!/bin/bash
    
    pwd=${ALIYUN_REGISTRY_PASSWORD}
    docker login --username=phenye -p $pwd registry.cn-hangzhou.aliyuncs.com
    docker tag app-backend:latest registry.cn-hangzhou.aliyuncs.com/phenye/app-backend:latest
    docker push registry.cn-hangzhou.aliyuncs.com/phenye/app-backend:latest
    
    
    if [ $# -gt 0 ] ; then
      tag=$1
      docker tag app-backend:latest registry.cn-hangzhou.aliyuncs.com/phenye/app-backend:${tag}
      docker push registry.cn-hangzhou.aliyuncs.com/phenye/app-backend:${tag}
    fi
    

    进入backend,推送指定版本,不指定则总是推成(latest):

    ./scripts/push.sh v1.0.0
    

    nginx的推送镜像脚本

    创建scripts/nginx/push.sh:

    #!/bin/sh
    pwd=${ALIYUN_REGISTRY_PASSWORD}
    docker login --username=phenye -p $pwd registry.cn-hangzhou.aliyuncs.com
    docker tag app-nginx:latest registry.cn-hangzhou.aliyuncs.com/phenye/app-nginx:latest
    docker push registry.cn-hangzhou.aliyuncs.com/phenye/app-nginx:latest
    
    
    if [ $# -gt 0 ] ; then
      tag=$1
      docker tag app-nginx:latest registry.cn-hangzhou.aliyuncs.com/phenye/app-nginx:${tag}
      docker push registry.cn-hangzhou.aliyuncs.com/phenye/app-nginx:${tag}
    fi
    

    进入scripts/nginx,推送指定脚本,不指定总是推送latest:

    ./push.sh v1.0.0
    

    镜像编排测试

    对推送的镜像做编排测试:

    version: '2'
    services:
      backend:
        image: registry.cn-hangzhou.aliyuncs.com/phenye/app-backend:latest
        env_file: "./baas.env"
      cron:
        image: registry.cn-hangzhou.aliyuncs.com/phenye/app-backend:latest
        env_file: "./baas.env"
        command: ['cron', '-f']
      worker:
        image: registry.cn-hangzhou.aliyuncs.com/phenye/app-backend:latest
        env_file: "./baas.env"
        command: ['/usr/bin/supervisord', '-n', '-c', '/etc/supervisor/supervisord.conf']
      nginx:
        image: registry.cn-hangzhou.aliyuncs.com/phenye/baas-nginx:latest
        volumes_from:
          - backend
        depends_on:
          - backend
        ports:
          - "8080:80"
    
    docker-compose up -d --renew-anon-volumes
    

    优化镜像编排的升级

    前面的镜像编排都存在一个问题,都是使用latest版本,指定版本并不方便,因此创建update.sh脚本,方便快速升级:

    #!/bin/bash
    
    # ./update.sh <version>
    
    if [ $# -gt 0 ] ; then
        hash=$1
    else
        hash=latest
    fi
    
    echo "version: $hash"
    sed -i.bak "/app-backend:/s/(app-backend:)([^"]*)/1${hash}/" docker-compose.yml
    sed -i.bak "/app-nginx:/s/(app-nginx:)([^"]*)/1${hash}/" docker-compose.yml
    
    docker-compose pull
    # 暂不支持蓝绿布署
    docker-compose up -d --renew-anon-volumes
    

    要启动或者更换成某个版本(不指定版本就是latest):

    ./udpate.sh v1.0.0
    

    一般来说,通过蓝绿布署做到零秒停机的升级是必要的,但是我们这一节的目标主要是讲镜像打包,而不是讲布署;第二个我们的布署基本推荐在K8S或者云容器上,基于docker-compose的布署场景变得很少,没有专门写优化的脚本。

    附录

    Laravel自身的问题

    本文谈的镜像打包的问题主要由Docker的特征导致,但是Laravel应用本身也有许多问题(其他语言的后台应用也有类似的问题),会使镜像打包变得复杂:

    • 升级版本时,数据库迁移怎么做?
    • 日志放哪里?
    • 性能数据的采集放哪里?

    这些问题在这里抛出由读者思考,以后有机会我们专门讲讲,本文镜像打包对其中的一些问题做了处理。

  • 相关阅读:
    sql 查出一张表中重复的所有记录数据
    几种常见SQL分页方式效率比较
    Redis命令参考之复制(Replication)
    Redis-benchmark使用总结
    redis压力测试详解
    c#:ThreadPool实现并行分析,并实现线程同步结束
    C#多线程学习 之 线程池[ThreadPool]
    [C#基础]ref和out的区别
    C#:ref和out的联系及区别。
    生产环境中使用Docker Swarm的一些建议
  • 原文地址:https://www.cnblogs.com/pheye/p/12873465.html
Copyright © 2011-2022 走看看