본문 바로가기
Server&OS/Linux

PHP 애플리케이션을 가장 빠르게, Part 2: PHP 애플리케이션을 프로파일링 하여 느려진 코드를 진단 및 빠르게 하기

by 백룡화검 2010. 4. 24.

PHP 애플리케이션이 느려진다면 프로파일러를 사용하여 어디에서 시간이 소비되는지를 파악합니다. 문, 루프, 함수, 클래스, 라이브러리 중 가장 느린 움직임을 보이는 것을 찾을 수 있습니다. 시간 보다는 메모리 사용이 문제가 될 경우에는 좋은 프로파일러가 컴포넌트 풋프린트도 발견해 낼 수 있습니다.

"PHP 애플리케이션을 가장 빠르게" 시리즈의 Part 1에 서는 PHP opcode 캐시(cache)인 XCache를 사용하여 사이트의 속도를 높이는 방법을 설명했다. XCache (캐싱 패키지들 중 하나)는 컴파일 프로세스의 결과를 보존하여 과잉 작업을 줄인다. 페이지가 바뀌지 않는 한, 캐싱 된 페이지는 프록시로서 충분한 역할을 한다. 페이지가 수정되면 캐싱 된 버전은 무효가 되고 다른 것으로 대체된다.

opcode 캐시와 opcode 옵티마이저는 사이트의 반응성을 높일 수 있는 저렴한 기술이라고 할 수 있다. 많은 캐시 패키지들이 프리 및 오픈 소스이기 때문에 코드를 수정할 필요가 없다.

물 론, 어떤 애플리케이션의 경우, PHP 소스 코드의 파일을 상응하는 opcode로 변환하는데 걸리는 시간은 실제 실행 시간과 비교해 본다면 그렇게 많은 시간이 걸리는 것은 아니다. 원격 데이터베이스 서버로 연결하고, 비효율적인 SQL 쿼리로 스토어를 쿼리하고, 데이터를 파싱 및 조작하는데 더 오랜 시간이 걸리며, 비용도 많이 든다. 좋은 네트워크 디자인과 똑똑한 데이터베이스 구현으로 지체된 시간과 느려진 쿼리를 보상할 수 있고, 필요에 따라서 여러분의 동료 전문가에게 의뢰할 수 있다. 하지만, 여러분의 코드가 느려진다면 여러분 스스로 이 문제를 해결해야 한다.

그런데, 어디서부터 시작해야 하는가? 코드가 완성되기 전에 코드를 조작하는 것은 무모한 짓이다. 첫 구현이 적절한 속도로 실행되더라도 마찬가지다. 여러분의 코드가 기능적이고 정확하기는 하지만 느리다면, 첫 번째로 해야 할 것은 성능을 정량화 또는 벤치마킹 하는 것이다. 이 같은 진단 노력 없이 코드를 최적화 한다는 것은 불가능하다.

간단한 성능 메트릭은 wall clock time 또는 페이지에 대한 요청과 완료된 렌더링 간 실제 지연 시간을 측정하는 것이다. 여러분의 워크스테이션에서 웹 서버, 데이터베이스, 브라우저를 로컬로 실행하는 경우, wall clock time이 유익하다. 하지만 wall clock time에는 네트워크 레이턴시, 트래픽에 걸린 활성 웹 서버, 활성 데이터베이스 등이 개입된다.

단일 소스 코드 문을 실행하는데 걸린 시간까지도 측정할 수 있는 훨씬 더 정확한 측정 방법은 코드 프로파일러를 사용하는 것이다. 일반적으로 PHP 런타임 엔진에 대한 확장으로서 구현되는 프로파일러는 문의 시작과 끝 사이의 델타(delta)를 기록하고, 프로시저의 시작과 끝 사이의 델타를 기록하고, 인커밍 요청에 대해 응답을 만드는데 필요한 총 시간을 파악한다. 이를 통해서 가장 느린 움직임을 보이는 문, 루프, 함수, 클래스, 라이브러리를 찾아낼 수 있다. 시간이 문제가 아니고 메모리 사용이 문제일 경우, 좋은 프로파일러라면 컴포넌트 풋프린트 까지도 발견해 낸다.

한 가지 대중적인 PHP용 프로파일러 중 Xdebug가 있다. 서버 후크(hook)를 사용하여 PHP 애플리케이션을 대화식으로 디버깅한다. (자세한 내용은 "더 나은 디버깅 방법"을 참조하라. 본 시리즈에서는 대화식(interactive) 디버깅과 고급 디버깅에 대해서도 다루고 있다.) Xdebug는 소스에서 구현하기 쉽고 Zend 확장으로서 설치된다. (일부 플랫폼에는 바이너리도 사용할 수 있다.) 설치가 되면 PHP 기반 페이지에 대한 각 요청은 KCacheGrind에서 볼 수 있는 데이터 세트를 생성한다.

Xdebug 구현 및 설치

여러분이 PHP 유틸리티 phpizephp-config에 액세스 했고, 여러분 시스템의 php.ini 설정 파일에 액세스 했다고 가정한다면, Xdebug를 설치 및 설정하는데 단 몇 분만 소요된다. 아래 제시된 지시 사항은 리눅스® 용이다. 물론 Mac OS X에서도 동일한 단계를 거친다. (Xdebug 웹 사이트에는 Microsoft® Windows®용 Xdebug 버전도 있다.)

Xdebug 최신 버전은 V2.0.0RC3이다. (최종 버전인 V2.0.0도 사용할 수 있다.) tarball을 다운로드 하여 압축을 풀고 소스 코드의 하위 디렉토리로 수정한다. phpizephp-config가 쉘의 PATH에 있는지를 확인하고 phpize로 구현할 준비를 한다.


Listing 1. Xdebug 설정하기
                
$ wget http://www.xdebug.org/files/xdebug-2.0.0RC3.tgz
$ tar xzf xdebug-2.0.0RC3.tgz
$ cd xdebug-2.0.0RC3/xdebug-2.0.0RC3
$ phpize
Configuring for:
PHP Api Version:         20020918
Zend Module Api No:      20020429
Zend Extension Api No:   20050606

phpize의 결과물은 하나의 스크립트이다. 정확히 말하면 configure이고, 이것은 나머지 빌드 프로세스를 설정한다. Xdebug를 구현하려면, ./configure를 타이핑 하고, 바로 다음에 make를 타이핑 한다.


Listing 2. Xdebug 구현하기
                
$ ./configure
checking build system type... i686-apple-darwin8.8.1
checking host system type... i686-apple-darwin8.8.1
checking for egrep... grep -E
...
$ make
...
Build complete.
(It is safe to ignore warnings about tempnam and tmpnam).

make 명령어는 Xdebug 확장인 xdebug.so를 만들어 낸다. 이제 남은 일은 sudo make install을 사용하여 설치하는 것이다.

$ sudo make install
Installing shared extensions: /usr/lib/php/extensions/no-debug-non-zts-20020429/

주: 터미널 윈도우에서 마지막 명령어를 실행했다면, 마지막 단계에서 만들어진 디렉토리를 선택하고 복사한다. 이것은 그 다음 단계에 필요하다.

마지막으로, 프로파일 데이터를 시각화 하려면, KCacheGrindGraphViz가 있어야 한다. K Desktop Environment (KDE)가 포함된 리눅스 배포판에는KCacheGrindGraphViz가 이미 있다. 그렇지 않을 경우, 각자 선호하는 리눅스 배포판에 맞는 버전을 찾으면 된다. Debian 사용자들은 Advanced Packaging Tool (APT)을 사용하여 KCacheGrind, GraphViz와 모든 패키지의 구성물들을 빠르게 설치할 수 있다.


Listing 3. KCacheGrind 설치하기
                
$ apt-cache search kcachegrind
valgrind-callgrind - call-graph skin for valgrind
kcachegrind - visualisation tool for valgrind profiling output
kcachegrind-converters - format converters for KCachegrind profiling visualisation tool
$ apt-cache search graphviz
graphviz - rich set of graph drawing tools
graphviz-dev - graphviz Libs and Headers against which to build applications
graphviz-doc - additional documentation for graphviz
libdeps-renderer-dot-perl - DEPS renderer plugin using GraphViz/dot
...
$ sudo apt-get install kcachegrind graphviz 
...

KDE가 시스템에 설치되지 않으면, KCacheGrind, GraphViz, 모든 사전 필수 항목들에 약90 MB의 디스크 공간이 필요하다.




위로


Xdebug 설정하기

Xdebug 확장이 설치되었다면, 확장을 실행 및 구성할 준비가 된 것이다. 텍스트 에디터에 php.ini를 열고 다음 라인을 추가한다.


Listing 4. 확장 실행 및 구성
                
zend_extension = /usr/lib/php/extensions/no-debug-non-zts-20020429/xdebug.so
xdebug.profiler_output_dir = "/tmp/xdebug/"
xdebug.profiler_enable = Off
xdebug.profiler_enable_trigger = 1

첫 번째 라인인 zend_extension은 Xdebug 확장을 로딩한다. 두 번째 라인은 프로파일러 아웃풋을 저장할 디렉토리의 이름을 정한다. 필요할 경우 네임드 디렉토리를 만들고 모드를 수정하여 웹 서버 사용자 쓰기 액세스를 허용한다.

세 번째 라인은 프로파일러를 실행 불가로 만든다. 하지만, 네 번째 라인은 HTTP GET 또는 POST 매개변수 XDEBUG_PROFILE가 설정될 때마다 프로파일러를 실행시킨다. (프로파일러를 계속해서 실행시키려면, 세 번째 라인의 OffOn으로 바꾼다.)

라인을 추가하고 아웃풋 디렉토리가 쓰기 가능한 것인지를 확인한 후에, 웹 서버를 재시작 한다. 다른 PHP 확장과 마찬가지로, Xdebug가 설치되어 사용할 수 있는지를 확인하려면, 단순한 PHP 프로그램을 만들어서 phpinfo()를 호출하고 그 결과를 분석해 본다. 그림 1과 비슷한 것을 보게 될 것이다. (단순함을 위해 전체 아웃풋은 뺐다.)


그림 1. Xdebug 설치 여부를 보여주는 Phpinfo


Zend 로고까지 스크롤을 내리는 방법도 있다. Xdebug가 올바르게 로딩되어 설정되었다면 Xdebug가 그 로고 옆에 나타난다.




위로


프로파일러 사용하기

코드를 프로파일링 하려면, 브라우저에서 PHP 애플리케이션으로 간다. 프로파일러가 트리거 당 케이스 별로 실행하도록 설정했다면 URL에 XDEBUG_PROFILE=1을 붙이거나, 폼 안에 매개변수를 삽입한다.

예를 들어, 간단한 ACME Fibonacci Maker인 fibonacci.php를 프로파일링 한다고 해보자. (Listing 5) XDEBUG_PROFILE 매개변수는 숨겨진 변수에 있는 폼 안에 설정된다. (코드가 실행 환경으로 이동할 때, Xdebug는 실행 불가가 되면서 이 변수는 어떤 해도 입히지 않는다.)


Listing 5. Fibonacci.php
                
<?php
  function fib($nth = 1) {
    if ( $nth < 2 ) {
      return( $nth ); 
    }
    
    return( fib( $nth - 1) + fib( $nth - 2 ) );
  }
?> 
  
<html>
  <head>
    <title>ACME Fibonacci Maker</title>
  </head>
  <body>
    <h2>Try the ACME Fibonacci Maker!</h2>
    <form action="fibonacci.php" method="POST">
    <input type="hidden" name="XDEBUG_PROFILE" value="1" />
    Enter a number: <input type="text" name="n"></input>
    </form>
    <hr />

<?php  
  if ( ! empty( $_REQUEST['n'] ) ) {
    $n = $_REQUEST['n'] % 10;
    $suffix = array( 1 => "st", 2 => "nd", 3 => "rd" );
    if ( $_REQUEST['n'] < 4 || $_REQUEST['n'] > 20 ) {
      $suffix = $suffix[$n];
    }
    else {
      $suffix = 'th';
    }
    
    echo '<p>The ' . $_REQUEST['n'] . $suffix .' Fibonacci number is ';
    echo fib( $_REQUEST['n'] ) . '</p>';
  }
?>
  </body>
</html>

브라우저를 통해 http://localhost/fibonacci.php (또는 알맞은 URL)로 가서 숫자(이를 테면, 16)를 입력한다. Fibonacci 시리즈의 16번째 엘리먼트가 나타날 것이다. (그림 2)


그림 2. Fibonacci 애플리케이션 샘플


프로파일러 아웃풋 디렉토리(php.ini)의 콘텐트를 리스팅 하면, cachegrind.out.951917687 같은 이름을 가진 파일을 볼 수 있다. 접두사 cachegrind.out.는 고정이다. 기본적으로 숫자 접미사는 fibonacci.php 파일에 대한 디렉토리 경로의 CRC32 해시이다. 따라서, 각 애플리케이션들이 고유의 디렉토리에 있다면, 각 애플리케이션에서 온 아웃풋이 파일 이름에 따라 분리된다. (아웃풋과 시간을 연결하고 싶다면,

xdebug.profiler_output_name = timestamp

위 라인을 php.ini 에 추가한다.)

터미널 윈도우에서, KCacheGrind를 시작하고 cachegrind.out.951917687을 연다. 그림 3과 비슷한 새로운 윈도우가 바로 열린다.


그림 3. KCacheGrind 애플리케이션


Callees 탭을 클릭하고, 소스 코드에서 하이라이트 된 부분을 더블 클릭 한 후, Grouping 리스트에서 Source File을 선택한다. 그림 4와 같은 모습의 뷰가 보인다.


그림 4. 결과 보기


여러분도 예상했듯이, 모든 프로세싱 시간(70,989 밀리초의 99.87%)이 fib() 함수의 3,193개 호출을 실행하는데 소비되었다. Fibonacci 시퀀스로 진행해 가면서 느려진 애플리케이션 속도를 빠르게 하기 위해, Fibonacci 넘버를 재 계산 해야 하는 "값비싼" 작업은 피해야 한다. 실제로 ACME Fibonacci Maker는 전산 재사용을 적용할 수 있는 좋은 기회이다.

fib() 함수의 업데이트 버전은 아래에 나와있다. 이 새로운 버전은 시간과 메모리를 교환한다. 나중에 사용하기 위해 중간 전산을 보유하기 때문이다. 그림 5는 분석 결과이다. 3,192개의 함수 호출 대신, 단 30개만 요구되고(이 중에서 절반만 결과를 계산했다.), 시간은 20밀리초로 줄어들었다.


Listing 6. 업데이트 된 fib() 함수
                
function fib($nth = 1) {
  static $fibs = array();

  if ( ! empty ($fibs[$nth] ) ) { 
    return( $fibs[$nth] );
  }
  
  if ( $nth < 2 ) {
    $fibs[$nth] = $nth;
  }
  else {  
    $fibs[$nth - 1] = fib( $nth - 1 );
    $fibs[$nth - 2] = fib( $nth - 2 );
    $fibs[$nth] = $fibs[$nth - 1] + $fibs[$nth -2];
  }
  
  return( $fibs[$nth] );
}
?>


그림 5. 더 빨라진 Fibonacci 함수


애플리케이션이 단일 실행을 통해 문제들을 드러낼 수 있지만(원래 애플리케이션에서 Fibonacci 시퀀스 중 50번째 엘리먼트를 실행하기), 일반적으로 여러 호출들에 대한 통계를 모아서 패턴을 분석한다.

기본적인 "crc32" 네이밍 스킴을 보유하고 있다면 fibonacci.php를 실행할 때마다 데이터 파일이 오버라이트 된다. 하지만, php.ini 에서 xdebug.profiler_append = 1을 설정함으로써 그 작동을 변경하고 후속 실행들을 같은 파일에 붙일 수 있다. 변경한 후에 웹 서버를 재시작 한다.

Fibonacci Maker를 세 번 실행 하여 수집된 데이터 예제는 그림 6에 나타나 있다. 걸린 시간은 2초 미만이다. 시간의 99.97 퍼센트가 fib()에 사용되었다. 그림 6은 Call Graph 탭을 보여주는데, 이는 GraphVizdot 유틸리티에 의해 생성된다. KCacheGrind의 상세한 사용법은 이 글에서는 설명하지 않겠다. 온라인에서 문서를 참조하기 바란다. KCacheGrind는 여러 가지 방법으로 데이터를 쪼개기 때문에 해결하고자 하는 문제를 찾아서 보면 된다.


그림 6. 프로파일링 데이터 모으기


더 나은 디버그 방법

PHP 애플리케이션을 프로파일링 하는 것 외에도, Xdebug 확장을 설정하여 에러가 발생할 때 상세한 스택 트레이스와 정보가 많은 에러 메시지와 대화식의 디버깅을 제공한다. 스택 트레이스와 에러 메시지들은 에러의 원인을 가리킬 수 있지만, 대화식 디버깅을 통해서 한번에 하나씩 검사하고, 프로그램의 유형과 값을 조사하고, 인커밍 요청 매개변수를 포함하여 모든 PHP 슈퍼글로벌(superglobal)을 검사할 수 있다.

본 시리즈의 다음 기술자료에서는 대화식 디버깅에 대해 보다 자세히 설명하겠다. 여러분은 여러 Xdebug 기능을 사용하여 에러가 발생할 때 애플리케이션의 상태를 파악할 수 있다.

  • xdebug.default_enable=On을 설정하여 프로그램에 에러가 생길 때마다 스택 트레이스를 드러내도록 한다. Xdebug 설치에 이미 많은 시간이 걸렸다면 코드를 작성 중일 때만이라도 이 기능을 실행한다.
  • xdebug.show_local_vars=1을 설정하여 최우선 범위에 모든 변수를 드러낸다.
  • xdebug.var_display_max_children, xdebug.var_display_max_data, xdebug.var_display_max_depth>xdebug.show_local_vars가 사용 중일 때 나타나는 변수들에 대한 많은 프로퍼티를 제어하는 관련 설정, 스트링의 길이, 중첩 깊이이다.

Xdebug 웹 사이트에서 자세한 내용을 참조하기 바란다.




위로


클래스 프로파일링

프 로파일링을 설명하려면 많은 코드가 필요하지만, 다음 예제에서는 예제 코드에서 만들어 낼 수 있는 정보의 양과 유형을 보여준다. Listing 7은 장난감 로켓을 조립하는 애플리케이션이다. 이 로켓은 여러 부분들로 구성되어 있고, 각 부분이 만들어 지려면 일정 시간이 필요하다. PHP에서, 하나의 클래스는 각 부분을 나타내고, 인스턴스 메소드는 각 부분의 구현 시간을 나타낸다. 장난감을 애플리케이션으로, 각 부분을 기능으로 생각하면 된다.


Listing 7. 장난감 조립 과정을 모방한 PHP 클래스
                
<?php
    define( 'BOOSTER', 5 );
    define( 'CAPSULE', 2 );
    define( 'MINUTE', 60 );
    define( 'STAGE', 3 );
    define( 'PRODUCTION', 1000 );
    
    class Part {
        function Part() {
            $this->build( MINUTE );
        }
        
        function build( $delay = 0 ) {
            if ( $delay <= 0 )
                return;
                
            while ( $delay-- > 0 ) {
            }
        }
    }
    
    class Capsule extends Part {
        function Capsule() {
          parent::Part();
            $this->build( CAPSULE * MINUTE );
        }
    }
    
    class Booster extends Part {
        function Booster() {
          parent::Part();
            $this->build( BOOSTER * MINUTE );
        }
    }
    
    class Stage extends Part {
        function Stage() {
          parent::Part();
          $this->build( STAGE * MINUTE );
        }
    }
    
    class SpaceShip {
        var $booster;
        var $capsule; 
        var $stages;
        
        function SpaceShip( $numberStages = 3 ) {
            $this->booster = new Booster();
            $this->capsule = new Capsule();
            $this->stages = array();
            
            while ( $numberStages-- >= 0 ) {
                $stages[$numberStages] = new Stage();
            }
        }
    }
    
    $toys = array();
    $count = PRODUCTION;
    
    while ( $count-- >= 0  ) {
      $toys[] = new SpaceShip( 2 );
    }
?>

<html>
<head>
<title>
Toy Factory Output
</title>
</head>
<body>
  <h1>Toy Production</h1>
  <p>Built <? echo PRODUCTION . ' toys' ?></p>
</body>
</html>

이 코드를 실행하면 새로운 데이터 파일이 만들어 진다. 데이터를 KCacheGrind로 로딩한다. SourceCall Graph 탭으로 전환하면 뷰는 그림 7과 같이 된다.


그림 7. 우주선 애플리케이션의 프로파일


Flat Profile(왼쪽)은 호출되는 모든 함수들(메소드)를 보여준다. 가장 왼쪽의 칼럼은 대략적인 누적 합계를 보여주고, 두 번째 칼럼은 각 메소드에 대한 개별적인 측정을 보여주며, 세 번째 칼럼은 메소드가 호출되는데 걸리는 시간을 나타낸다. 색깔이 칠해진 사각형은 호출 그래프를 반영하며 타이밍과 이벤트 순서를 쉽게 연관시킬 수 있다.

분명한 것은, 단계들을 구현하는 시간이 가장 비싸다는 것이다. 각 부분들(Part의 컨스트럭터로 나타남)을 구현하는데 필요한 오버헤드는 그 다음이다. 또한 PHP의 define() 함수에도 약간의 비용이 필요하다.

마지막으로, 메모리가 사용되었던 방식도 볼 수 있다. 상단 드롭다운 메뉴에서 MemoryClass를 선택하고, 위와 아래에 있는 TypesCaller Map으로 전환한다. 스크린은 그림 8처럼 보인다.


그림 8. 우주선 애플리케이션에서의 메모리 사용





위로


사이클 검색

소셜 북마크

mar.gar.in
Digg
del.icio.us
Slashdot

다 른 많은 PHP 확장과 마찬가지로 Xdebug는 즉각 구현되고, 빠르게 설치되며, 쉽게 설정된다. 이 모든 것이 10분 안에 끝난다. Apache가 이미 최적화 되고 애플리케이션이 캐싱 되고 있지만, 성능이 나아지지 않는다면 코드가 어떻게 실행되고 있는지를 봐야 한다. 알고리즘은 효율적인가? 코드가 너무 복잡한가? PHP가 이미 제공하고 있는 함수를 재구현 하고 있는 것은 아닌가?

병목 현상을 찾아낼 수 없다면, 느려짐의 원인을 찾아 픽스할 때이다. 바로 프로파일이 필요하다. 여러분의 귀중한 전산 사이클이 어떻게 소비되는지를 알게 된다면 놀라게 될 것이다.

잊지 말아야 할 것은 Xdebug가 실행 서버에서 실행된다면 오버헤드를 가중시키므로 실행 서버에서는 Xdebug를 실행시키지 말아야 한다.



참고자료

교육

제품 및 기술 얻기

출처 : http://www.ibm.com/developerworks/kr/library/os-php-fastapps2/index.html