고성능 PHP application server - Road Runner 와 라라벨 연동하기


Road Runner 는 Spiral scout 라는 Agency 에서 만드는 high performance PHP Application Server 입니다.

(외국에서 웹 에이전시를 하려면 이 정도 수준의 기술력을 갖춰야 경쟁력이 있는건지 잠시 좌절감을 느끼게 되네요.)


golang 으로 작성했고 go routine 으로 PHP worker 와 동작하고 gRPC 로 통신을 합니다.


PHP 의 장점이자 단점은 Request 가 끝나면 worker 가 초기화되어 버리는 것입니다.

이는 memory 나 resource 관리에 개념이 없는 초보 개발자가 PHP 를 사용해서 잘못 코딩해도 leak 이 발생하지 않으므로 안정적인 서비스가 가능했지만 이로 인해 framework 의 booting time 등이 매번 발생하므로 대규모 처리가 어려운 문제가 있습니다.

특히 laravel 같이 덩치가 큰 framework 일 경우 그 무거운 초기화를 매 요청시마다 해야 하지만 road runner 를 사용하면 worker 를 memory 에 유지해서 빠른 처리가 가능해진다고 합니다.



사전 준비

https://github.com/spiral/roadrunner-binary/releases 에 연결한 후에  road runner 바이너리를 laravel 프로젝트 루트에 다운받아 놓습니다. 


다음은 2.0.1 Linux 버전을 다운받는 예제입니다.

$ curl -L -O https://github.com/spiral/roadrunner-binary/releases/download/v2.0.1/roadrunner-2.0.1-linux-amd64.tar.gz


OSX 용 바이너리는 다음 명령어로 받으면 됩니다.

$ curl -L -O https://github.com/spiral/roadrunner-binary/releases/download/v2.0.1/roadrunner-2.0.1-darwin-amd64.zip

다운받은 바이너리의 압축을 풀어서 road runner 실행 파일인 rr 을 라라벨 프로젝트 루트에 복사해 둡니다.

$ tar -zxvf roadrunner-2.0.1-linux-amd64.tar.gz
$ mv roadrunner-2.0.1-linux-amd64/rr .


road runner 바이너리가 동작하는지 실행해 봅니다.

$ ./rr

Usage:
  rr [command]

Available Commands:
  help        Help about any command
  reset       Reset workers of all or specific RoadRunner service
  serve       Start RoadRunner server
  workers     Show information about active roadrunner workers
...



설치

컴포저로 road runner 라라벨 bridge 프로젝트를 설치합니다.

$ composer require spiral/roadrunner-laravel "^4.0"


vendor 설정 파일을 퍼블리싱합니다.

$ php ./artisan vendor:publish --provider='Spiral\RoadRunnerLaravel\ServiceProvider' --tag=config



라라벨 프로젝트 root 폴더에 road runner 설정 파일인 .rr.yaml 을 만들고 다음 내용을 추가해 줍니다.

server:
  command: "php ./vendor/bin/rr-worker start --relay-dsn unix:///var/run/rr/rr-rpc.sock"
  relay: "unix:///var/run/rr/rr-rpc.sock"

http:
  address: 0.0.0.0:8080
  middleware: ["headers", "static", "gzip"]
  pool:
    max_jobs: 64 # feel free to change this
    supervisor:
      exec_ttl: 60s
  headers:
    response:
      X-Powered-By: "RoadRunner"
  static:
    dir: "public"
    forbid: [".php"]


일반 사용자로 Road Runner 를 띄울 경우 /var/run 폴더에 Unix Domain socket 을 만들 권한이 없어서 에러가 나므로 /var/run 밑에 폴더(Ex: rr)를 만들고 Road Runner 를 뛰울 사용자 소유로 변경해 줍니다. 

$ sudo mkdir /var/run/rr/
$ sudo chown lesstif /var/run/rr/


이제 다음 명령어로 Road Runner 를 구동하면 됩니다.

$ ./rr serve


nginx 연결

nginx 를 웹 서버로 사용한다면 가상 호스트 설정에 proxy_pass 를 추가해 줍니다.

server {
    listen 80;
    listen 443 ssl;
    server_name rr.local;
    root "/var/www/lesstif/road-runner/public";
    location / {
        proxy_pass http://127.0.0.1:8080;
	    proxy_set_header Host $host;
	    proxy_set_header X-Real-IP $remote_addr;     
	}

    ssl_certificate     /etc/nginx/ssl/rr.local.crt;
    ssl_certificate_key /etc/nginx/ssl/rr.local.key;
}


wrk 로 테스트

제 desktop 인 fedora 33 에 순정 설치 laravel 의 landing 페이지를 부하 측정 도구인 wrk 에 다음 옵션을 줘서 테스트를 해보았습니다.

$ wrk -t 4 -c 50 http://rr.local

nginx + php-fpm

먼저 nginx에서 fastcgi 로 연결한 php-fpm 을 테스트해 보았습니다. nginx 설정은 다음과 같습니다.

 location ~ \.php$ {        
       
        # Mitigate https://httpoxy.org/ vulnerabilities
        fastcgi_param HTTP_PROXY "";
        
        fastcgi_pass unix:/run/php-fpm/www.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors off;
        fastcgi_buffer_size 16k;
        fastcgi_buffers 4 16k;
}
$ wrk -t 4 -c 50 http://rr.local

Running 10s test @ http://rr.local
  4 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   155.95ms  282.79ms   1.84s    88.59%
    Req/Sec   174.36     55.54   330.00     70.50%
  6953 requests in 10.01s, 123.36MB read
  Socket errors: connect 0, read 0, write 0, timeout 2
Requests/sec:    694.44
Transfer/sec:     12.32MB

초당 약 7백여개의 요청을 처리했습니다.

nginx + road runner

이제 road runner 와 nginx 을 reverse proxy 로 연결하고 wrk 로 동일한 조건으로 요청을 전송했습니다.

$ wrk -t 4 -c 50 http://rr.local

Running 10s test @ http://rr.local
  4 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   166.61ms  286.86ms   1.99s    91.16%
    Req/Sec   126.10     95.12   720.00     79.38%
  4968 requests in 10.01s, 88.37MB read
  Socket errors: connect 0, read 0, write 0, timeout 9
Requests/sec:    496.12
Transfer/sec:      8.83MB

초당 496 정도 나오는데 생각보다 너무 성능이 안 나와서 살펴보다가 soft max open file 이 1024인 것을 발견하고 max open file 을 Hard, Soft 둘 다 524,288 로 수정했습니다.

$ ulimit -Hn
524288
$ ulimit -Sn
524288

그리고 다시 wrk 로 테스트를 해보았는데 크게 성능 차이가 없었습니다.


$ wrk -t 4 -c 50 http://rr.local

Running 10s test @ http://rr.local
  4 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   179.50ms  296.11ms   1.72s    90.19%
    Req/Sec   128.81    108.37   707.00     81.00%
  5002 requests in 10.01s, 88.98MB read
  Socket errors: connect 0, read 0, write 0, timeout 1
Requests/sec:    499.61
Transfer/sec:      8.89MB


그래서 road runner 를 구동시 console 에서 구동해서 로그를 화면에 뿌리느라 성능에 영향을 줄수 있을 것 같아서 중지하고 출력을 화면에 뿌리도록 수정했습니다.

$ ./rr serve &> rr.log


그리고 다시 wrk 를 실행해 보아도 큰 차이가 없었습니다.

road runner 에 직접 연결

nginx 를 사용하지 않고 road runner 에 직접 연결해 보았습니다.

$ wrk -t 4 -c 50 http://rr.local:8080

Running 10s test @ http://rr.local:8080
  4 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   182.63ms  281.43ms   1.77s    90.31%
    Req/Sec   113.31     77.94   540.00     75.77%
  4470 requests in 10.01s, 79.23MB read
Requests/sec:    446.52
Transfer/sec:      7.91MB

역시 크게 차이를 못 느끼겠네요.


PHP-FPM이 더 빠른 걸로 봐서는 아마 제가 설정을 잘못한 게 아닐까 싶네요.

PHP.ini 에서 성능에 영향을 줄 설정은 다음과 같습니다.

  • memory_limit => 512M => 512M
  • opcache.enable => On => On
  • opcache.enable_cli => On => On


같이 보기


Ref