PHP 7.4 preload

PHP 7.4

마지막 글 업데이트: 2019 12 09

2019년 11월 29일에 php 7.4가 릴리즈 되었다.
여러가지 기능이 업데이트 되었고 기다렸던 property type도 추가되었는데
가장 눈에 띄이는 기능은 preload 기능이였다.

Preload?

PHP는 기본적으로 CGI 형식으로 동작합니다. 매번 페이지를 읽을 때마다 PHP는 실행하기 전에 모든 코드를 재컴파일합니다. 심지어 Python의 장난감 프레임워크의 개발 서버도 이런 식으로 돌지는 않습니다.

PHP: 잘못된 디자인의 프랙탈 – 웹 프레임워크

이 대단한 PHP는 하나의 리퀘스트가 들어올 때 마다 모든 PHP스크립트를 다시 컴파일 한다. 그러다보니 코드가 커질 수록 느려질 수 밖에 없는 구조를 가지고있다.

이를 해결 하기 위한 방법으로, 프레임워크의 클래스, 상수들은 이번리퀘스트든 다음 리퀘스트든 동일하게 사용되기에 이를 미리 컴파일 하여 메모리에 미리 올려두어 컴파일 시간을 없에 빠른 응답이 가능하게 한다.

Test

테스트 환경은
OS: ubuntu 18.04 – WSL
PHP: 7.4.0 으로 진행하였다.

/etc/php/7.4/cli/php.ini의 preload 설정을 바꾸어 줘야하는데,
(만약, fpm으로 테스트한다면 /etc/php/7.4/fpm/php.ini)

; Specifies a PHP script that is going to be compiled and executed at server
; start-up.
; http://php.net/opcache.preload
opcache.preload=/home/silnex/test/test.php

php.ini 파일에서 opcache.preload 옵션에 미리 컴파일할 PHP코드를 지정해 두게 되면 해당 파일은 PHP가 맨처음 실행될 때 미리 컴파일 되어 메모리에 올라가있게 된다.

TEST 1. echo

간단한 echo구문을 통해 어떻게 동작하는지 보자.
파일은 2가지로 preload설정이 되어있는 test.php
preload 설정이 되지 않은 test2.php이다.

<?php
echo 1234;

위와같은 코드 test.php와 test2.php에 쓰고 php -S localhost:8080을 통해 cli서버를 시작해주자.
그리고 http://localhost:8080/test.php를 접속하면 아래와 같이 출력된다.

위에서 입력한 echo 1234;가 그대로 출력된다.
물론 test2.php도 동일하게 뜨게 되는데, 이때 test.php와 test2.php파일을 아래와 같이 수정해보자.

<?php
echo 12345;

이렇게 수정한 후 test.php에 들어가게되도 표시되는 1234는 변경되지 않는다.
하지만 test2.php에 들어가면 12345로 출력되는 것을 확인 할 수 있다.

즉, 미리 컴파일되어 메모리에 올라가있기 때문에 php를 수정해도 php서버를 재시작 하기 전까지는 적용되지 않는다.

php -S로 시작했던 서버를 Ctrl+c를 눌러 종료하고 재시작하면 test.php에서 12345가 출력되는 것을 확인 할 수 있다.

TEST 2. include, require

그럼 컴파일된 php파일을 컴파일되지 않는 php에서 include, require하면,
즉, test2.php에서 test.php를 include 혹은 require로 불러오게 되면 어떻게 되는지 확인해보자.

<?php
# test.php
echo 1234;

# test2.php
include('test.php');

위 처럼 test.php와 test2.php를 작성하고 http://localhost:8080/test2.php로 접속하면,
다음과 같이 출력된다.

이 처럼 출력되는 모습이 확인되면 이제 test.php와 test2.php를 아래처럼 바꿔주자.

<?php
# test.php
echo 12345;

# test2.php
include('test.php');
echo 'php preload test';

test.php의 수정사항이 적용되지 않은 1234php preload test가 출력된다.

즉, include나 require를 통해 preload된 php를 불러오게되면 해당 파일을 다시 컴파일 하는 것이 아닌 메모리상에서 불러오는 것을 알 수 있다.

+그 반대는?

위에서의 테스트는 preload가 안된 test2.php에서 test.php를 include한 경우이고,
반대로 preload된 test.php에서 test2.php를 include하면 어떻게 될까.

<?php
# test.php
include('test2.php');

# test2.php
echo 1234;

이렇게 test.php와 test2php를 작성하고 서버를 실행시킨 후에
접속해보면 당연히 1234가 출력될것이다.

이제 test2.php를 echo 1234;가 아닌 echo 12345;로 수정한 후 접속하면 재미있는 결과가 나오게된다.

localhost:8080/test.php
localhost:8080/test2.php

특이하게도 test.php와 test2.php가 모두 바뀌지 않게 된다.
즉, test.php에서 include된 php는 따로 opcache.preload 옵션에 설정되있지 않더라도 preload되어 메모리상에 올라가게 된다는 뜻이다.

이를 이용해 php.ini엔 하나의 파일만을 넣어두고, 그외엔 모두 php파일 안에서 처리가 가능하게 할 수있다.

이러한 특징을 이용해 preload.php를 생성해주는 composer 패키지도 존재한다.

TEST 3. Class ( feat. function )

A.php에서의 선언된 class와 B.php에서의 선언된 class는 다른 파일이기에 같은 이름이여도 각각 다르게 컴파일되어 큰 문제는 없었다.

하지만 preload된 파일에서 선언된 class는 다른 php에 영향을 끼치게 될 수 있다.
아래의 예제로 확인해보자.

<?php
# test.php
class Test
{
    public function __construct()
    {
        echo 1234;
    }
}

$test = new Test();

test.php를 위와 같이 작성하고 localhost:8080/test.php로 접속하면,
1234가 출력된다.
그리고 test2.php에 아래와 같이 Testclass를 선언한 후 test2.php에 접근해보자.

<?php
# test2.php
class Test
{ }
500 Error

위 처럼 500에러가 뜨게되며 php -S를 통해 서버를 실행 했던 곳에선
127.0.0.1:56421 [500]: GET /test2.php - Cannot declare class Test, because the name is already in use in /home/silnex/test/test2.php on line 2
이미 Test 란 이름의 Class가 사용중이며, 재 선언이 불가능하다고 출력되며 에러를 표시한다.

class 뿐만 아니라 function 또한 preload된 php에서 선언된 함수와 동일한 이름의 함수를 선언할 경우
127.0.0.1:56450 [500]: GET /test2.php - Cannot redeclare test() (previously declared in /home/silnex/test/test.php:2) in /home/kerberos/test/test2.php on line 2 에러를 출력한다.

Test 4. $value

위에 예시들을 보면 뭔가 “모든 파일들에 preload된 php가 include되어 동작된다.”라는 느낌이 든다.

그래서 변수를 통해 진짜 include되는지 확인해 보자.

<?php
# test.php
$preload = 123;
var_dump($preload);
<?php
# test2.php
var_dump($preload);
localhost:8080/test.php
localhost:8080/test2.php

위에서 햇던 예상이 틀렷다는 것을 보여준다.
즉, 변수는 preload 되더라도 미리 선언 되어지는것은 아니다.

++ 혹시나 싶어서 test.php를 아래와 같이 변수에 static을 붙여보았지만 결과는 동일한것으로 보여진다.

<?php
# test.php
static $preload = 123;
var_dump($preload);

Test 4. define()

그렇다면 define을 통해 선언을 하게 되면 어떻게 될까

<?php
# test.php
define(TEST, 1234);

# test2.php
var_dump(defined('TEST'));
test2.php

define을 이용한 선언도 다른 php에서는 불러지지 않는 것으로 보여진다.

마치며

PHP 7.4에 preload 기능은 단순히 활성화 하는 것만으로 10% ~ 19%정도 속도 향상을 보인다고 한다.
물론 그에 따른 메모리 소모는 감수해야 겠지만 그만큼 퍼포먼스 측면에서의 장점이 분명한것같다.

또한 composer측에서도 preload.php를 생성해주는 기능을 제공했으면 한다는 이슈가 github에 올라와 있지만, 적용 자체는 불투명 한것같다.
개인적으로는 preload.php에 대한 표준을 composer에서 잡아주어 패키지를 좀 더 preload할 수 있는 방향으로 나아갔으면한다.

분명 적용에 어려움이 있는 것은 맞으나, 앞으로 유용하게 사용될 기능임은 확실하고,
PHP 8.0 JIT을 위한 밑바탕으로써의 역할은 확실히 보여주는것 같다.
한가지 팁을 공유하면 opcache_get_status() 함수를 통해 opcache에서 자주 캐싱되는 파일만을 preload 하는것만으로도 충분한 효과를 누릴 수 있을 것이다.

PHP를 사용하다보면, 글 처음에 언급한 “PHP 잘못된 디자인의 프랙탈”에 대해 자주 듣지만, 저 글을 부정하지도, 그렇다고 수용하지도 않는다.
PHP는 점점 더 모던해지고 있고, 최신 트렌드를 따라가고 있다고 생각한다.
그게 아니여도 PHP가 나아지고 있다는 것은 사실인 것 같다.

PHP개발 하면서 주변에 물어볼사람도 없고, PHP욕밖에 못듣는다면 (제가 그랫습니다.)
Modern PHP User Group에서 함께 공부했으면 좋겠습니다.

참고글
https://medium.com/swlh/composer-how-it-should-preload-in-php-7-4-3f8d19fda40
https://medium.com/swlh/preloading-your-php-7-4-project-in-one-line-9ede756f292c
https://stitcher.io/blog/preloading-in-php-74

해당 글은 제가 테스트 해보고 싶은게 생각나면 계속 업데이트 될 예정입니다.

글의 문제가 있다면 댓글을 달아 주세요.

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.