Кэширование генератора миниатюрных изображений в PHP: как правильно установить заголовки If-Last-Modified / Max-Age / Last-Modified в заголовке PHP? - PullRequest
4 голосов
/ 11 марта 2011

Даже после очень высокой оценки Google PageSpeed ​​( 97 ) и Yahoo! Да, медленно ( 92 ) сгенерированные PHP миниатюры, похоже, не приходят пассивно из старого кэша: они, похоже, генерируются каждый раз снова ... и снова ... свежеиспеченные, отнимающие много времени. .

Этот вопрос будет сосредоточен только и конкретно на том, как решить проблему CACHE в коде PHP , который генерирует большие пальцы:

Взгляните на эти крошечные миниатюрные миниатюры размером всего 3 ~ 5 кб каждый!

Водопад в деталях: http://www.webpagetest.org/result/110328_AM_8T00/1/details/

Любые и все предложения - +1 помощь для меня и горячо приветствуются, так как в последние месяцы я очень отчаялся по этому вопросу. Спасибо тысячу!

Использование или отсутствие Modrewrite не влияет на скорость: оба одинаковы. Я использую эти условия перезаписи: RewriteCond %{REQUEST_URI} ^/IMG-.*$ & RewriteCond %{REQUEST_FILENAME} !-f

Исходный URL-адрес по умолчанию , а также переписанный URL-адрес вызывают одинаковые задержки !! Так что давайте не будем указывать на ошибку молниеносного Apache: это PHP Cache / заголовки, которые каким-то образом неправильно закодированы ...

enter image description here


Предупреждение от webpagetest.org: Использование в браузере кэширования статических ресурсов: 69/100

FAILED - (нет максимального срока действия или истекает): http://aster.nu/imgcpu?src=aster_bg/124.jpg&w=1400&h=100&c=p


После каждого обновления вы увидите одно из этих двух случайных предупреждений на REDbot.org enter image description here enter image description here


Соответствующие части кода:

// Script is directly called
if(isset($_GET['src']) && (isset($_GET['w']) || isset($_GET['h']) || isset($_GET['m']) || isset($_GET['f']) || isset($_GET['q']))){
    $ImageProcessor = new ImageProcessor(true);
    $ImageProcessor->Load($_GET['src'], true);
    $ImageProcessor->EnableCache("/var/www/vhosts/blabla.org/httpdocs/tmp/", 345600);
    $ImageProcessor->Parse($quality);
}

/* Images processing class
 * - create image thumbnails on the fly
 * - Can be used with direct url imgcpu.php?src=
 * - Cache images for efficiency 
 */
class ImageProcessor
{
    private $_image_path;      # Origninal image path
    protected $_image_name;    # Image name   string
    private $_image_type;      # Image type  int    
    protected $_mime;          # Image mime type  string    
    private $_direct_call = false;   # Is it a direct url call?  boolean        
    private $_image_resource;  # Image resource   var Resource      
    private $_cache_folder;    # Cache folder strig
    private $_cache_ttl;        # Cache time to live  int
    private $_cache = false;    # Cache on   boolean
    private $_cache_skip = false;   # Cache skip   var boolean

    private function cleanUrl($image){   # Cleanup url
        $cimage = str_replace("\\", "/", $image);
        return $cimage;
    }   

    /** Get image resource
     * @access private, @param string $image, @param string $extension, @return resource  */
    private function GetImageResource($image, $extension){
        switch($extension){
            case "jpg":
                @ini_set('gd.jpeg_ignore_warning', 1);
                $resource = imagecreatefromjpeg($image);
                break;
        }
        return $resource;
    }


    /* Save image to cache folder
     * @access private, @return void  */
    private function cacheImage($name, $content){

        # Write content file
        $path = $this->_cache_folder . $name;
        $fh = fopen($path, 'w') or die("can't open file");
        fwrite($fh, $content);
        fclose($fh);

        # Delete expired images
        foreach (glob($this->_cache_folder . "*") as $filename) {
            if(filemtime($filename) < (time() - $this->_cache_ttl)){
                unlink( $filename );
            }
        }
    }

    /* Get an image from cache
     * @access public, @param string $name, @return void */
    private function cachedImage($name){
        $file = $this->_cache_folder . $name;
        $fh = fopen($file, 'r');
        $content = fread($fh,  filesize($file));
        fclose($fh);
        return $content;
    }

    /* Get name of the cache file
     * @access private, @return string  */
    private function generateCacheName(){
        $get = implode("-", $_GET);
        return md5($this->_resize_mode . $this->_image_path . $this->_old_width . $this->_old_height . $this->_new_width . $this->_new_height . $get) . "." . $this->_extension;
    }

    /* Check if a cache file is expired
     * @access private,  @return bool  */
    private function cacheExpired(){
        $path = $this->_cache_folder . $this->generateCacheName();
        if(file_exists($path)){
            $filetime = filemtime($path);
            return $filetime < (time() - $this->_cache_ttl);
        }else{
            return true;
        }
    }

    /* Lazy load the image resource needed for the caching to work
     * @return void */
    private function lazyLoad(){
        if(empty($this->_image_resource)){
            if($this->_cache && !$this->cacheExpired()){
                $this->_cache_skip = true;
                return;
            }
            $resource = $this->GetImageResource($this->_image_path, $this->_extension);
            $this->_image_resource = $resource;
        }    
    }

    /* Constructor
     * @access public, @param bool $direct_call, @return void */
    public function __construct($direct_call=false){

    # Check if GD extension is loaded
        if (!extension_loaded('gd') && !extension_loaded('gd2')) {
            $this->showError("GD is not loaded");
        }

        $this->_direct_call = $direct_call;
    }

    /* Resize
     * @param int $width, @param int $height, @param define $mode
     * @param bool $auto_orientation houd rekening met orientatie wanneer er een resize gebeurt */
    public function Resize($width=100, $height=100, $mode=RESIZE_STRETCH, $auto_orientation=false){

        // Validate resize mode
        $valid_modes = array("f", "p");
        }
                     // .... omitted .....

        // Set news size vars because these are used for the
        // cache name generation
                 // .... omitted .....          
        $this->_old_width = $width;
        $this->_old_height = $height;

        // Lazy load for the directurl cache to work
        $this->lazyLoad();
        if($this->_cache_skip) return true;

        // Create canvas for the new image
        $new_image = imagecreatetruecolor($width, $height);

        imagecopyresampled($new_image, $this->_image_resource, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h);

             // .... omitted .....

        $this->_image_resource = $new_image;
    }

    /* Create image resource from path or url
     * @access public, @param string $location, @param bool $lazy_load, @return */
    public function Load($image,$lazy_load=false){

        // Cleanup image url
        $image = $this->cleanUrl($image);

        // Check if it is a valid image
        if(isset($mimes[$extension]) && ((!strstr($image, "http://") && file_exists($image)) || strstr($image, "http://")) ){

            // Urlencode if http
            if(strstr($image, "http://")){
                $image = str_replace(array('http%3A%2F%2F', '%2F'), array('http://', '/'), urlencode($image));
            }
            $image = str_replace("+", "%20", $image);

            $this->_extension = $extension;
            $this->_mime = $mimes[$extension];
            $this->_image_path = $image;
            $parts = explode("/", $image);
            $this->_image_name = str_replace("." . $this->_extension, "", end($parts));

            // Get image size
            list($width, $height, $type) = getimagesize($image);
            $this->_old_width = $width;
            $this->_old_height = $height;
            $this->_image_type = $type;
        }else{
            $this->showError("Wrong image type or file does not exists.");
        }
        if(!$lazy_load){
            $resource = $this->GetImageResource($image, $extension);
            $this->_image_resource = $resource;
        }           
    }

    /* Save image to computer
     * @access public, @param string $destination, @return void  */
    public function Save($destination, $quality=60){
        if($this->_extension == "png" || $this->_extension == "gif"){
            imagesavealpha($this->_image_resource, true); 
        }
        switch ($this->_extension) {
            case "jpg": imagejpeg($this->_image_resource,$destination, $quality);   break;
            case "gif": imagegif($this->_image_resource,$destination);      break;
            default: $this->showError('Failed to save image!');             break;
        }           
    }

    /* Print image to screen
     * @access public, @return void */
    public function Parse($quality=60){
        $name = $this->generateCacheName();
        $content = "";
        if(!$this->_cache || ($this->_cache && $this->cacheExpired())){
            ob_start();
            header ("Content-type: " . $this->_mime);
            if($this->_extension == "png" || $this->_extension == "gif"){
                imagesavealpha($this->_image_resource, true); 
            }

            switch ($this->_extension) {
                case "jpg": imagejpeg($this->_image_resource, "", $quality);    break;
                case "gif": imagegif($this->_image_resource);   break;
                default: $this->showError('Failed to save image!');             break;
            }

            $content = ob_get_contents();
            ob_end_clean();
        }else{

            if (isset ($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
                if (strtotime ($_SERVER['HTTP_IF_MODIFIED_SINCE']) < strtotime('now')) {
                    header ('HTTP/1.1 304 Not Modified');
                    die ();
                }
            }

            // change the modified headers
            $gmdate_expires = gmdate ('D, d M Y H:i:s', strtotime ('now +10 days')) . ' GMT';
            $gmdate_modified = gmdate ('D, d M Y H:i:s') . ' GMT';

            header ("Content-type: " . $this->_mime);
            header ('Accept-Ranges: bytes');
            header ('Last-Modified: ' . $gmdate_modified);
            header ('Cache-Control: max-age=864000, must-revalidate');
            header ('Expires: ' . $gmdate_expires);

            echo $this->cachedImage($name);
            exit();
        }

        // Save image content
        if(!empty($content) && $this->_cache){
            $this->cacheImage($name, $content);
        }

        // Destroy image
        $this->Destroy();

        echo $content;
        exit();
    }

    /* Destroy resources
     * @access public,  @return void */
    public function Destroy(){
        imagedestroy($this->_image_resource); 
    }


    /* Get image resources
     * @access public,  @return resource */
    public function GetResource(){
        return $this->_image_resource;
    }

    /* Set image resources
     * @access public, @param resource $image, @return resource */
    public function SetResource($image){
        $this->_image_resource = $image;
    }

    /* Enable caching
     * @access public, @param string $folder, @param int $ttl,   * @return void */
    public function EnableCache($folder="/var/www/vhosts/blabla.org/httpdocs/tmp/", $ttl=345600){
        if(!is_dir($folder)){
            $this->showError("Directory '" . $folder . "' does'nt exist");
        }else{
            $this->_cache           = true;
            $this->_cache_folder    = $folder;
            $this->_cache_ttl       = $ttl;
        }
        return false;
    }
}

Первоначальный автор предоставил мне разрешение на размещение частей кода здесь для решения этой проблемы.


Ответы [ 5 ]

5 голосов
/ 11 марта 2011

Если я правильно понимаю вопрос, этого вполне следует ожидать.Работа с изображениями медленная.

Желтый - браузер, отправляющий запрос.Зеленый - это ваш браузер, ожидающий на сервере фактического создания миниатюры, что занимает очень много времени, независимо от того, какую библиотеку использует сервер.Синий - это сервер, отправляющий ответ, на который, в отличие от предыдущих шагов, влияет размер файла.

Ничего не поделаешь с присущей медлительностью манипуляции изображениями.Было бы целесообразно кэшировать эти миниатюры, чтобы они генерировались только один раз, а затем обслуживались статически.Таким образом, очень немногим из ваших пользователей когда-либо придется смириться с этой зеленой задержкой, и ваш сервер тоже будет счастлив.

РЕДАКТИРОВАТЬ: Если проблема в том, что файлы существуют вэти URL-адреса, но ваш RewriteRule все равно запускается, имейте в виду, что по умолчанию правила запускаются без проверки, существует ли файл.

Используйте следующее условие над вашим RewriteRule, чтобы убедиться, что файлсуществует.

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule # ...etc...
3 голосов
/ 14 марта 2011

Apache может обслуживать файлы с вашего жесткого диска намного быстрее, чем PHP, и кажется, что вы делаете последнее для обработки кэширования:

 /**
     * Get an image from cache
     * 
     * @access public
     * @param string $name
     * @return void
     */
    private function cachedImage($name){
        $file = $this->_cache_folder . $name;
        $fh = fopen($file, 'r');
        $content = fread($fh,  filesize($file));
        fclose($fh);
        return $content;
    }

Есть лучший способ сделать то, что делает эта функция (passthru), но лучший вариант - установить регулярное выражение, которое будет переписывать запрос к вашему сценарию эскизов, только если файл еще не существует:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^images/.*$ - [NC,L]
RewriteRule ^images/(.*)$ /imgcpu.php/$1 [NC,L]

А затем вводите логику для анализа запроса к изображению и форматирования его соответственно. Например, вы можете сказать, что большие пальцы должны быть названы в честь оригинального файла и иметь размеры W x H, добавленные как «stackoverflow_logo_100x100.jpg».

Имеет смысл?


Для запроса (в комментарии) описание флагов «s», «l» и «d» выглядит следующим образом (цитируя документы):

'- d' (является каталогом). TestString как путь и тесты существует ли он, и является ли он каталог.

'- s' (есть обычный файл, с размером) Относится к TestString как путь и тесты существует ли он, и является ли он обычный файл с размером больше чем нуль.

'- l' (является символической ссылкой) TestString как путь и тесты существует ли он, и является ли он символическая ссылка.

3 голосов
/ 12 марта 2011

imgcpu.php? Src = foo / foo.jpg & w = 100 & h = 100

, поэтому imgcpu.php работает для каждого запроса изображения?

В этом случае, если вы беспокоитесь о производительности, выполните следующие действия:

  • сценарию необходимо выполнить некоторое кэширование созданных им эскизов.Если он изменяет размер содержимого при каждом запросе, это ваша проблема.

  • сценарию необходимо отправить некоторые заголовки кэширования в браузер - чистый сценарий PHP этого не сделает и будетобновляться при каждой загрузке страницы

  • вызов session_start() внутри скрипта PHP может привести к проблемам параллелизма из-за блокировки сеанса.

youнужно будет показать немного кода PHP.Может быть, в отдельном вопросе, хотя.

2 голосов
/ 29 марта 2011

Проверка вашего заголовка и кэша HTTP_IF_MODIFIED_SINCE ПОСЛЕ того, как вы генерируете изображение, чтобы оно генерировалось и кэшировалось при каждой загрузке страницы. Вы получите значительное сокращение времени, если переместите эти проверки ближе к началу выполнения, прежде чем начинать обработку изображения.

0 голосов
/ 26 марта 2011

Матчу дал вам ответ, почему.Если вы хотите исправить это, сохраните созданные эскизы, чтобы они не создавались заново при каждом запросе.Я использую простую страницу 404, которая ловит запрос к миниатюрам, которые еще не были созданы, этот скрипт вычисляет необходимые размеры и файл по URL-адресу - например, /thumbs/100x100/cat.png означает создание миниатюры 100x100 из /images/cat.png.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...