Laravel octane 使用蓝绿部署方案实现0停机部署

最近在尝试使用 octane swoole 驱动的 Laravel 项目时出现了一个问题:在更新代码后使用 octane:reload 重新启动 workers 时新代码不生效。

我的项目是通过 deployer 部署的,通过符号链接的形式将项目目录指向新的代码,但 octane:reload 后新代码并没有生效。

之后发现通过符号链接指向新目录、composer引入新的库、.env 在重启后都不生效,服务运行过程中动态加载的文件支持 reload,在服务执行前就载入的文件代码不支持reload。

之后我们部署代码后通过执行 octane::stopoctane::start 启动服务,这样代码是生效的,但这样并非热部署,每次执行上述命令服务都会中断几秒,导致此时进来的请求 502 报错。

解决方法

参考蓝绿部署形式

对于同一个项目(同一个代码目录)启动两套 octane swoole 服务,通过 nginx 指向新的服务的形式实现 0 downtime 部署。

启动 服务1、服务2 ,首次将 nginx 指向 服务1,在更新代码后通过stop&start重启服务2,将nginx指向服务2,reload nginx即实现热部署;在下次更新代码后重启服务1,将 nginx 指向 服务1 并 reload。

解决步骤

1.1、修改 octane 启动代码,使 octane 可以通过不同端口启动两套服务

当然你也可以在服务器上部署两套代码,这样就不需要该脚本了

新增 Artisan 命令行,自定义 octane 启动脚本

php artisan make:command Octane/StartOctane

修改脚本:

在原有 octane 服务启动脚本基础上修改不同端口对应不用的状态文件,这样就可以启动多个服务了

<?php

namespace App\Console\Commands\Octane;

use Laravel\Octane\Commands\StartCommand;
use Laravel\Octane\Swoole\ServerStateFile as SwooleServerStateFile;

class StartOctane extends StartCommand
{
    /**
     * The command's signature.
     *
     * @var string
     */
    public $signature = 'Octane:StartOctane
                    {--server= : The server that should be used to serve the application}
                    {--host=127.0.0.1 : The IP address the server should bind to}
                    {--port= : The port the server should be available on [default: "8000"]}
                    {--rpc-host= : The RPC IP address the server should bind to}
                    {--rpc-port= : The RPC port the server should be available on}
                    {--workers=auto : The number of workers that should be available to handle requests}
                    {--task-workers=auto : The number of task workers that should be available to handle tasks}
                    {--max-requests=500 : The number of requests to process before reloading the server}
                    {--rr-config= : The path to the RoadRunner .rr.yaml file}
                    {--watch : Automatically reload the server when the application is modified}
                    {--poll : Use file system polling while watching in order to watch files over a network}';

    /**
     * The command's description.
     *
     * @var string
     */
    public $description = '无停机部署 Octane server';

    /**
     * Handle the command.
     *
     * @return int
     */
    public function handle()
    {
        //  在原有octane 服务启动脚本基础上新增如下代码 start
        // 修改不同端口对应不用的状态文件,这样就可以启动多个服务了
        $port = $this->option('port') ?: 8012;

        app()->bind(SwooleServerStateFile::class, function($app) use ($port){
            return new SwooleServerStateFile($app['config']->get(
                'octane.state_file',
                storage_path('logs/octane-server-state' . $port . '.json')
            ));
        });
        // end

        $server = $this->option('server') ?: config('octane.server');

        return match ($server) {
            'swoole' => $this->startSwooleServer(),
            'roadrunner' => $this->startRoadRunnerServer(),
            default => $this->invalidServer($server),
        };
    }
}

1.2、服务启动脚本

若我们想启动两套服务,端口分别是 8012、8013

我们的服务使用 supervisorctl 管理:

octane:start 换为我们上一步创建的脚本 Octane:StartOctane

[program:server_8012]
command =   /bin/php -d variables_order=EGPCS /www/server/current/artisan Octane:StartOctane --max-requests=5000 --workers=4 --task-workers=4 --port=8012
user=www
process_name            = %(program_name)s_%(process_num)s
numprocs                = 1
autostart               = true
autorestart             = true
startretries            = 1000
stdout_logfile          = /log/supervisor/server.log
stdout_logfile_maxbytes = 1000MB
redirect_stderr=true

[program:server_8013]
command =   /bin/php -d variables_order=EGPCS /www/server/current/artisan Octane:StartOctane --max-requests=5000 --workers=4 --task-workers=4 --port=8013
user=www
process_name            = %(program_name)s_%(process_num)s
numprocs                = 1
autostart               = true
autorestart             = true
startretries            = 1000
stdout_logfile          = /log/supervisor/server.log
stdout_logfile_maxbytes = 1000MB
redirect_stderr=true

1.3、nginx 配置

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;
    server_name test.net;
    server_tokens off;
    root /www/server/current/public;

    index index.html index.htm index.php default.html default.htm default.php;

    charset utf-8;

    error_page 404 /index.php;

    location /index.php {
        try_files /not_exists @octane;
    }

    location / {
        try_files $uri $uri/ @octane;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
    {
        expires      30d;
    }

    location ~ .*\.(js|css)?$
    {
        expires      12h;
    }

    location ~ /.well-known {
        allow all;
    }

    location ~ /\.
    {
        deny all;
    }

    # 在此处引入配置文件,部署脚本通过修改配置文件设置 nginx 指向的服务
    include /www/server/current/nginx_octane.conf;
}

nginx_octane_8012.conf

    location @octane {
        set $suffix "";

        if ($uri = /index.php) {
            set $suffix ?$query_string;
        }

        proxy_http_version 1.1;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header SERVER_PORT $server_port;
        proxy_set_header REMOTE_ADDR $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_pass http://127.0.0.1:8012$suffix;
    }

nginx_octane_8013.conf


    location @octane {
        set $suffix "";

        if ($uri = /index.php) {
            set $suffix ?$query_string;
        }

        proxy_http_version 1.1;
        proxy_set_header Host $http_host;
        proxy_set_header Scheme $scheme;
        proxy_set_header SERVER_PORT $server_port;
        proxy_set_header REMOTE_ADDR $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_pass http://127.0.0.1:8013$suffix;
    }

1.4、服务部署流程

我们是通过 deployer 脚本部署的代码,如上次部署用的 8012 端口的服务,本次部署用 8013 端口服务,服务重启脚本如下:

这样在 8013 端口重启后,在将 nginx 指向8013 即可实现0停机部署

task('supervisor:reload', function(){
    $env = get('labels')['env'];
    if ($env == 'pre8012’) {
        # 重启8012端口服务
        run('sudo /usr/bin/supervisorctl restart server_8012:');
        # 将 nginx 指向 8012 端口
        run('cd {{release_path}} && /usr/bin/cp ./nginx_octane_8012.conf ../../shared/nginx_octane.conf');
    }
    if ($env == 'pre8013') {
        # 重启8013端口服务
        run('sudo /usr/bin/supervisorctl restart server_8013:');
        # 将 nginx 指向 8013 端口
        run('cd {{release_path}} && /usr/bin/cp ./nginx_octane_8013.conf ../../shared/nginx_octane.conf');
    }
    # 热重启 nginx
    run('sudo /usr/bin/lnmp nginx reload');
});

本次执行命令:

dep deploy pre8013 -vvv

引用链接

[1] deployer : https://deployer.org/