2021年3月18日 星期四

Laravel-S (Swoole 加速) 使用重點整理

在此所做之重點整理,係針對使用 hhxsv5/laravel-s 來對 Laravel 開發的網站提供 Swoole 加速功能,其專案網站為

基本資料

作業系統安裝的軟體
CentOS: 7.9.2009
Apache: 2.4.6
PHP: 7.0.33
Supervisor: 3.4.0-1
PHP Composer: 1.10.20
PHP Swoole: 2.2.0

透過 Composer 安裝 PHP package
LaravelS: 3.6.4
Laravel Framework: 5.1.46 (LTS)

Docker 設定

dockerfile
FROM centos:7.9.2009

RUN yum -y install http://rpms.famillecollet.com/enterprise/remi-release-7.rpm \
    && yum -y update \
    && yum -y install --enablerepo=remi,remi-php70 \
        httpd php php-pdo php-pgsql \
        php-xml php-mbstring php-mcrypt \
        php-opcache php-pecl-swoole2 php-pecl-inotify \
        unzip supervisor \
    && yum clean all \
    && rm -rf /var/cache/yum \
    && curl -o /usr/bin/composer https://getcomposer.org/download/1.10.20/composer.phar \
    && chmod a+x /usr/bin/composer

# add config file
COPY conf/httpd.conf /etc/httpd/conf/httpd.conf
COPY conf/supervisord.conf /etc/supervisord.conf
COPY conf/rev_proxy.conf /etc/httpd/conf.d/

#port and entry
EXPOSE 80

CMD ["/usr/bin/supervisord"]

其中,php-pecl-swoole2 為 PHP 加 Swoole 功能,php-pecl-inotify 則是為了偵測檔案變動重新載入程式。

rev_proxy.conf
<IfModule deflate_module>
    SetOutputFilter DEFLATE
    DeflateCompressionLevel 2
    AddOutputFilterByType DEFLATE text/html text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml application/x-httpd-php image/jpeg image/gif image/png font/ttf font/otf image/svg+xml
</IfModule>

RemoteIPHeader X-Forwarded-For

ProxyRequests Off
ProxyPreserveHost On

supervisord.conf
[supervisord]
nodaemon=true
logfile=/var/log/supervisord.log
pidfile=/var/run/supervisord.pid

[program:ntuocw_laravels]
command=php /var/www/html/ntu-ocw/web-src/bin/laravels start
redirect_stderr=true
autostart=true
autorestart=true
#user=http
numprocs=1
process_name=%(program_name)s_%(process_num)s
stdout_logfile=/var/www/html/ntu-ocw/storage/logs/bin_laravel-s-log1.log
stdout_logfile_maxbytes=1MB
stdout_logfile_backups=5


[program:httpd]
redirect_stderr=true
command=/usr/sbin/httpd -DFOREGROUND
process_name = httpd

對了,開發測試時,請手動下指令啟動,不要使用 supvervisord 啟動,若程式有問題,會陷入 reload 的無限迴圈。

httpd.conf 則只是要加上 "AllowOverride All",允許在目錄下使用 .htaccess 加上 rewrite rule。更重要的是基於安全,關閉目錄瀏覽功能 "Options -Indexes"。

使用 docker-compose 管理 docker,主要是下面三種指令
docker-compose build  ==> 建立 image
docker-compose up     ==> 啟動
docker-compose up -d  ==> 啟動,在背景執行
docker-compose down   ==> 停止

docker-compose.yml
version: '2'

services:
  web:
    build:
      context: ./
    #  dockerfile: Swoole_dockerfile
    restart: always
    working_dir: /var/www/html
    volumes:
      - ../web:/var/www/html
      - ../work_tmp:/work_tmp
    environment:
      COMPOSER_HOME: /work_tmp/Composer
    ports:
      - 8180:80


基本設定

以普通使用者的身份進入 container 中。
docker exec -it --user 1000:1000 ocw_docker_web_1 bash

進入後,建立 Laravel 5.1 的專案。
composer create-project laravel/laravel ntu-ocw "5.1.*"
在 composer.json 加入。
"require": {
    "php": ">=5.5.9",
    "laravel/framework": "5.1.*",
    "hhxsv5/laravel-s": "3.6.*"
},

然後執行 composer update,更新和安裝 PHP package

在 config/app.php,加入
'providers' => [
    //...
    Hhxsv5\LaravelS\Illuminate\LaravelSServiceProvider::class,
],

不然在執行下述指令時,會出現 Command "laravels" is not defined

php artisan laravels publish

若出現無法寫入 storage 的檔案時,
sudo chmod -R a+w storage/


執行 publish 指令後會複製下列 4個檔案
config/laravels.php
bin/laravels
bin/fswatch
bin/inotify


修改 .htaccess,為了確定修改正確,可將 index.php 刪除。
DirectoryIndex index.php   ==> Apache 預設為 index.html

# Handle Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$  http://localhost:10520/$1 [P,L]

在 .env 加入,再重新啟動 bin/laravels。測試時,worker_num 不用設太多。
# configs for Laravel-S
LARAVELS_LISTEN_PORT=10520
LARAVELS_WORKER_NUM=5
LARAVELS_INOTIFY_RELOAD=true
執行後,使用 ps -ax 看到的執行緒
  PID TTY      STAT   TIME COMMAND
      1 ?        Ss     0:01 /usr/bin/python /usr/bin/supervisord
      8 ?        Sl     0:00 /var/www/html/ntu-ocw/web-src laravels: master process
      9 ?        S      0:00 /usr/sbin/httpd -DFOREGROUND
     22 ?        S      0:00 /var/www/html/ntu-ocw/web-src laravels: manager process
     28 ?        S      0:00 /var/www/html/ntu-ocw/web-src laravels: worker process 0
     29 ?        S      0:00 /var/www/html/ntu-ocw/web-src laravels: worker process 1
     30 ?        S      0:00 /var/www/html/ntu-ocw/web-src laravels: worker process 2
     31 ?        S      0:00 /var/www/html/ntu-ocw/web-src laravels: worker process 3
     32 ?        S      0:10 /var/www/html/ntu-ocw/web-src laravels: worker process 4
     33 ?        S      0:00 /var/www/html/ntu-ocw/web-src laravels: inotify process
     72 pts/0    Ss     0:00 bash
    111 ?        S      0:00 /usr/sbin/httpd -DFOREGROUND
    203 ?        Z      0:00 [httpd] <defunct>
    227 ?        S      0:01 /usr/sbin/httpd -DFOREGROUND
      ........
    265 ?        S      0:00 /usr/sbin/httpd -DFOREGROUND
    266 pts/0    R+     0:00 ps -ax


可修改 config/laravels.php,增加監視的檔案 .env
'inotify_reload'           => [
        'enable'        => env('LARAVELS_INOTIFY_RELOAD', false),
        'watch_path'    => base_path(),
        'file_types'    => ['.php', '.env'],
        'excluded_dirs' => [],
        'log'           => true,
    ],

在 app/Providers/AppServiceProvider.php 的 boot() 中,加入 laravels.received_request 事件的處理程式片段,因為要處理的事情太多,將程式寫在另外的檔案中。
class AppServiceProvider extends ServiceProvider
{
    // Bootstrap any application services.
    public function boot()
    {
        // 送交 Laravel 前的處理
        \Event::listen('laravels.received_request', function (\Illuminate\Http\Request $req, $app) {
            // 這裡才是真的透過 browser瀏覽
            // 才有某些關於 client 的 SERVER 資料
            \App\Lib\SW_util::boot_setup($req, $app);
        });

        // Laravel 結束後的處理
        \Event::listen('laravels.generated_response', function (\Illuminate\Http\Request $req, \Symfony\Component\HttpFoundation\Response $rsp, $app) {
            // $rsp->headers->set('Content-Type', 'image/png');// Change header of response
        });
    }

為了處理使用 Laravel-S 帶來的差異性,把相關的程式碼統一放在 app\Lib\SW_util.php 中。下面的 boot_setup() 專門用來處理 boot 的事情。
// boot() 的啟始程序
public static function boot_setup(\Illuminate\Http\Request $req, $app)
{
    // 在這裡 print 的訊息,都會出現在執行 laravels start 的 console
    // echo "\n\n===== laravels.received_request =====\n";

    session()->clear();

    // 去除附加的預設執行檔 index.php
    $req_uri = $req->server->get("REQUEST_URI");
    $d_php = '/index.php';  // 配合 .htaccess 的 DirectoryIndex 設定

    if (starts_with($req_uri, $d_php)) {
        $req_uri = substr($req_uri, strlen($d_php));

        if ($req_uri == '') {
            $req_uri = '/';
        }

        $req->server->set("REQUEST_URI", $req_uri);
    }

    // 把 IP 改成實際的 remote IP
    $req->server->set("REMOTE_ADDR", $req->server->get("HTTP_X_FORWARDED_FOR"));

    // $_SERVER["SCRIPT_FILENAME"] =>
    // "/var/www/html/ajtest/ntu-ocw/web-src/bin/laravels"
    $pre_s = '/var/www/html/';
    $tail_s = '/web-src/bin/laravels';
    $url_path = substr($req->server->get("SCRIPT_NAME"), strlen($pre_s));
    $url_path = substr($url_path, 0, (0-strlen($tail_s)));

    \URL::forceRootUrl('http://'.$req->server->get('HTTP_HOST').'/'.$url_path);

    self::$dump_handler = null;
}


在 boot() 中的 laravels.received_request 的事件要處理的重點包括下列各點
  • 修改 SERVER['REQUEST_URI'],移除 index.php
  • session()->clear(),清除前一個連線留下的 session 資料
  • 使用 URL::forceRootUrl() 設定 host 的 url,url() 和 asset() 產生的網址才會正確
  • 以 SERVER ['HTTP_X_FORWARDED_FOR'] 取代 SERVER['REMOTE_ADDR'],這樣 Request::ip() 才能取得正確的使用者 IP
  • 假如有用 static variables 存放連線運作的變數,要記得在此重置

在剛開始設定 Laravel-S 可以連線時,要開啟目錄的預設網頁時,一直會出現無法 match route 的錯誤,真是讓人感到挫折灰心。後來根據錯誤訊息,追縱 Laravel 的原始碼,直接找到 Illuminate\Routing\Router.php,在出錯的那個 function,把傳過來 url 列印出來,才發現收到的 httpd 送來的預設檔名,index.html,後來自己改成 index.php。在執行 match route 之前必須將 index.php 移除。在 Symfony 中,有相關程式的片段。

Session 全部變成一樣了

將系統切換之後,在測試留言功能時,發現怎麼我第一次用,就有亂七八糟的留言內容,並且顯示驗證碼錯誤。再仔細檢查,不得了,所有的 session 內容全部變成一樣了,裡面還有我剛剛登入管理端的帳號身份。因為下班後才發現,只好把系統先關掉,將 session 全刪掉,再重新啟動系統。使用者不能留言,也就只好忍耐一下囉,終究先前曾經因系統設定錯誤,一兩個月留言功能都不正常,也沒人反應。

短時間內,找不到其他使用者的相關經驗分享,或是 Laravel-S 對 session 處理的相關說明。就先自己追蹤程式吧,發現是 Laravel 的 session/store.php 中,會將 attributes 陣列的資料與儲存的 session 資料合併。而 Session store 只在 laravel-s start 時 Instance 一次,因而其 attributes array 會保留前一次連線的 session 資料。幸好此系統僅是供使用者瀏覽,只有一個網頁讓使用者留言用,session 不正確沒有造成太大的影響。

Laravel 框架是使用 Symfony 的 Session,其 Session Store 是 Implement Symfony 的 SessionInterface。在實際運作時,建立的 session 資料是放在 attributes 陣列中,等到連線結束時,才寫回儲存裝置中。在緊急情況下,直接修改 store.php,在 function start() 中直接將 attributes 陣列清空。後來找到 interface 中有提供 function clear(),其功能即是將 attributes 陣列清空。

要取得 session 物件有幾種方式,session() 或 $req->session(),在 boot 時, session 還沒加進 $req 中,呼叫 $req->session() 會出現錯誤 "Session store not set on request"。還好可以正常呼叫 session()->clear(), 將 attributes 陣列清除。

依照上面的作法,將問題解決了。過了一陣子,隨意搜尋關於 Larave-S 的資訊,發現早就有人反應此問題,甚至在最早查資料時,就曾看過有關的敘述,只是那時一點概念都沒有,不曉得他在說什麼。而且 Larave-S 也解決了此問題,在 setting 中有相關說明,只是自己不能理解。

解決辦法很簡單,在 config/laravels.php 中的 'cleaners' 加入設定即可。

// Need to configure the following cleaners if you use the session/authentication/passport in your project
'cleaners' => [
    Hhxsv5\LaravelS\Illuminate\Cleaners\SessionCleaner::class,
    Hhxsv5\LaravelS\Illuminate\Cleaners\AuthCleaner::class,
],

這樣就可以了,就這麼簡單,真是踏破鐵鞋方覓得,嗯,頂多是多按了一些鍵盤啦。

調整目錄

調整後的目錄大致是這樣

ntu-ocw
   +- assets
   |     +- css, js, img, .....
   +- vendor
   |     +- hhxsv5, laravel, .....
   +- storage
   |     +- app, framework, logs, ....
   +- web-src
   |     +- bin, app, config, ....
   |     +- storage (僅供 laravels 存放資料 laravels.json  laravels.pid)
   + .htaccess, favicon.ico, robots.txt


因為改變了 vendor 的位置,修改 composer.json,然後再次執行 composer update
"config": {
    "preferred-install": "dist",
    "vendor-dir": "../vendor"
}

bin/laravels 的修改,將 $basePath 以 BASEDIR 取代
// $basePath = realpath(__DIR__ . '/../');
// BASEDIR, VENDORDIR 同時要定義在 artisan 中
define('BASEDIR', realpath(__DIR__ . '/../'));
define('VENDORDIR', realpath(__DIR__.'/../../vendor'));

$loader = new Psr4Autoloader();
$loader->register();

// Register laravel-s
$loader->addNamespace('Hhxsv5\LaravelS', VENDORDIR . '/hhxsv5/laravel-s/src');

// Register laravel-s dependencies
$loader->addNamespace('Symfony\Component\Console', VENDORDIR . '/symfony/console');
$loader->addNamespace('Symfony\Contracts\Service', VENDORDIR . '/symfony/service-contracts');
$loader->addNamespace('Symfony\Contracts', VENDORDIR . '/symfony/contracts');

$command = new Hhxsv5\LaravelS\Console\Portal(BASEDIR);
$input = new Symfony\Component\Console\Input\ArgvInput();
$output = new Symfony\Component\Console\Output\ConsoleOutput();
$code = $command->run($input, $output);
exit($code);

laravel-s 會呼叫 artisan,因此其亦要配合修改
define('BASEDIR', __DIR__.'');
define('VENDORDIR', realpath(__DIR__.'/../vendor'));

require BASEDIR.'/bootstrap/autoload.php';
$app = require_once BASEDIR.'/bootstrap/app.php';
$app->useStoragePath(realpath(BASEDIR.'/../storage'));

bootstrap/app.php 的修改,將 storage 移到 web-src 外部,便於備份程式。但是 laravel-s 將storage 的目錄寫死在程式,在 web-src 下保留一個 storage 目錄,只供 laravel-s使用。
$app = new Illuminate\Foundation\Application(
    realpath(__DIR__.'/../')
);

$app->useStoragePath(realpath(BASEDIR.'/../storage'));

bootstrap/autoload.php 的修改
require VENDORDIR.'/autoload.php';

效能測試

花了這麼多時間了解,查資料,追蹤,思考,測試,除錯,才把系統套到 Laravel-S,用上 Swoole 加速的功能,總會想了解花費的心血得到多少的收益。若是收效不大,大可不用,造成維護上的麻煩。

下面先用 siege 做壓力測試,看看網站每秒最多能處理多少連線。再用 curl 觀察個別連線花費的時間。

使用 siege 做壓力測試

PHP 7.0+Swoole 2.2,在對外開放的網站上,同時有其他使用者連上來
$ siege -c 100 -t 5s http://ocw.aca.ntu.edu.tw/ntu-ocw/ > /dev/null

** SIEGE 4.0.7
** Preparing 100 concurrent users for battle.
The server is now under siege...

Lifting the server siege...
Transactions:		         769 hits
Availability:		      100.00 %
Elapsed time:		        4.54 secs
Data transferred:	       14.84 MB
Response time:		        0.56 secs
Transaction rate:	      169.38 trans/sec
Throughput:		        3.27 MB/sec
Concurrency:		       94.54
Successful transactions:         769
Failed transactions:	           0
Longest transaction:	        1.65
Shortest transaction:	        0.03

PHP 7.0,在正式網站上,為了測試臨時開啟,未對外開放。
$ siege -c 100 -t 5s http://ocw.aca.ntu.edu.tw:8180/ntu-ocw/ > /dev/null

** SIEGE 4.0.7
** Preparing 100 concurrent users for battle.
The server is now under siege...

Lifting the server siege...
Transactions:		         863 hits
Availability:		      100.00 %
Elapsed time:		        4.28 secs
Data transferred:	       27.31 MB
Response time:		        0.40 secs
Transaction rate:	      201.64 trans/sec
Throughput:		        6.38 MB/sec
Concurrency:		       81.50
Successful transactions:         863
Failed transactions:	           0
Longest transaction:	        2.98
Shortest transaction:	        0.00

測試結果,很意外的,使用 Swoole 加速的網站,效能反而比較差。猜測可能的原因,網站會受到各種的安全防護的包圍過濾封包,防護裝置的效能也會影響效能。另外,其他使用者會佔用連線資料,如查詢資料庫,花較多的時間,也會擋到測試的連線。而專為測試開啟的伺服器,則不受其他使用者連線的影響。

為了排除其他使用者的影響,以及受安全防護裝置的拖累,另外在開發用的測試環境再跑一次壓力測試。不論正式及開發環境,都是使用 docker 提供服務。

PHP 7.0+Swoole 2.2,在開發用的測試網站上
$ siege -c 100 -t 5s http://10.161.86.117:8180/ntu-ocw/ > /dev/null

** SIEGE 4.0.7
** Preparing 100 concurrent users for battle.
The server is now under siege...

Lifting the server siege...
Transactions:		       30103 hits
Availability:		      100.00 %
Elapsed time:		        4.05 secs
Data transferred:	     1053.04 MB
Response time:		        0.01 secs
Transaction rate:	     7432.84 trans/sec
Throughput:		      260.01 MB/sec
Concurrency:		       98.84
Successful transactions:       30103
Failed transactions:	           0
Longest transaction:	        0.25
Shortest transaction:	        0.00

PHP 7.0,在開發用的測試網站上
$ siege -c 100 -t 5s http://10.161.86.117:8080/ajtest/ntu-ocw/ > /dev/null

** SIEGE 4.0.7
** Preparing 100 concurrent users for battle.
The server is now under siege...

Lifting the server siege...
Transactions:		        2664 hits
Availability:		      100.00 %
Elapsed time:		        4.03 secs
Data transferred:	      361.01 MB
Response time:		        0.15 secs
Transaction rate:	      661.04 trans/sec
Throughput:		       89.58 MB/sec
Concurrency:		       97.29
Successful transactions:        2664
Failed transactions:	           0
Longest transaction:	        0.31
Shortest transaction:	        0.03

在測試環境得到結果,使用 Swoole 加速每秒大約可以處理 10倍的連線。對了,在這網頁有用到 Laravel 的 cache 功能,會將資料庫查詢 cache 在檔案中,因此不受資料庫效能的影響。

使用 curl 測單一連線時間

另一方面,測試個別連線的花費時間
寫個簡單的 script,curltime
#!/bin/bash

curl -w @- -o /dev/null -s "$@" <<'EOF'
    time_namelookup:  %{time_namelookup}\n
       time_connect:  %{time_connect}\n
    time_appconnect:  %{time_appconnect}\n
   time_pretransfer:  %{time_pretransfer}\n
      time_redirect:  %{time_redirect}\n
 time_starttransfer:  %{time_starttransfer}\n
                    ----------\n
         time_total:  %{time_total}\n
EOF

PHP 7.0+Swoole 2.2,在對外開放的網站上,同時有其他使用者連上來
$ ./curltime http://140.112.161.116/ntu-ocw/
    time_namelookup:  20
       time_connect:  2286
    time_appconnect:  0
   time_pretransfer:  2303
      time_redirect:  0
 time_starttransfer:  19828
                    ----------
         time_total:  30103

PHP 7.0,在正式網站上,為了測試臨時開啟,未對外開放。
$ ./curltime http://140.112.161.116:8180/ntu-ocw/
    time_namelookup:  92
       time_connect:  2724
    time_appconnect:  0
   time_pretransfer:  2739
      time_redirect:  0
 time_starttransfer:  94360
                    ----------
         time_total:  101133

從結果的數來看,時間單位應是 microseconds。

為了減少 DNS 查詢時間,使用 IP 連線。花費的時間會一直變動,使用 Swoole 加速,每個連線花費的時間大約在 30ms 左右。而未加速時,花費時間大約為 100ms 左右。

以上使用正式開放的網站測試,可能受網站安全防護或其他使用者連線的影響。接著使用測試環境來測試,比較能看到純粹網站的效能。

PHP 7.0+Swoole 2.2,在開發用的測試網站上
$ ./curltime http://10.161.86.117:8180/ntu-ocw/
    time_namelookup:  89
       time_connect:  295
    time_appconnect:  0
   time_pretransfer:  400
      time_redirect:  0
 time_starttransfer:  13112
                    ----------
         time_total:  13402

PHP 7.0,在開發用的測試網站上
$ ./curltime http://10.161.86.117:8080/ajtest/ntu-ocw/
    time_namelookup:  17
       time_connect:  70
    time_appconnect:  0
   time_pretransfer:  110
      time_redirect:  0
 time_starttransfer:  23723
                    ----------
         time_total:  24125

使用 Swoole 加速,每個連線花費的時間大約在 15ms 左右。而未加速時,花費時間大約為 25ms 左右。

沒有留言:

張貼留言

網誌存檔