Статьи

Настройка производительности PHP-приложений для Windows с помощью Wincache

Несколько недель назад я написал пост, в котором показано, как повысить производительность приложений PHP в Windows с помощью модуля кэширования вывода IIS. Использование модуля кэширования вывода может оказать значительное положительное влияние на производительность приложения, поскольку страницы обслуживаются из кэша без выполнения какого-либо кода PHP. Однако эта сила также может быть недостатком в зависимости от того, как построено ваше приложение. Поскольку целые страницы кэшируются, использование кэширования вывода может быть не идеальным для страниц с несколькими источниками данных. Wincacheрасширение к PHP обеспечивает альтернативу (и / или комплимент) кешированию вывода. В этой статье я расскажу о том, какие улучшения производительности вы получаете «бесплатно», просто включив расширение Wincache, а также о том, как вы можете кэшировать пользовательские объекты, чтобы получить более высокую степень детализации кэширования, чем обеспечивает выходное кэширование.

Что Wincache делает для «бесплатно»

Просто добавив файл php_wincache.dll в каталог расширений и добавив extension = php_wincache.dll в файл php.ini , вы получите 3 бесплатных повышения производительности (я цитирую документацию Wincache на php.net ):

  • Кэширование кода операции PHP : PHP — это механизм обработки сценариев, который считывает входной поток данных, содержащий текстовые и / или инструкции PHP, и создает другой поток данных, чаще всего в формате HTML. Это означает, что на веб-сервере механизм PHP читает, анализирует, компилирует и выполняет сценарий PHP каждый раз, когда он запрашивается веб-клиентом. Операции чтения, синтаксического анализа и компиляции создают дополнительную нагрузку на процессор и файловую систему веб-сервера и, таким образом, влияют на общую производительность веб-приложения PHP. Кэш байт-кода PHP (код операции) используется для хранения байт-кода скомпилированного скрипта в общей памяти, чтобы его можно было повторно использовать движком PHP для последующего выполнения того же скрипта. (Вы можете получить метаданные о кэшированных кодах операций, используя wincache_ocache_fileinfo иФункции wincache_ocache_meminfo .)
  • Кэширование файлов : даже с включенным кэшем кодов операций PHP, движок PHP должен обращаться к файлам скриптов в файловой системе. Когда PHP-скрипты хранятся на удаленном общем файловом ресурсе UNC, файловые операции приводят к значительному снижению производительности. Расширение Windows Cache для PHP включает в себя файловый кеш, который используется для хранения содержимого файлов сценариев PHP в общей памяти, что уменьшает количество операций файловой системы, выполняемых механизмом PHP. (Вы можете получить метаданные о кэшированных файлах, используя функции wincache_fcache_fileinfo и wincache_fcache_meminfo .)
  • Разрешите кэширование пути к файлу : PHP-скрипты очень часто включают или работают с файлами, используя относительные пути к файлам. Каждый путь к файлу должен быть нормализован до абсолютного пути к файлу движком PHP. Когда приложение PHP использует много файлов PHP и обращается к ним по относительным путям, операция разрешения путей может отрицательно повлиять на производительность приложения. Расширение Windows Cache для PHP предоставляет кэш Resolve File Path, который используется для хранения сопоставлений между относительными и абсолютными путями файлов, тем самым уменьшая количество разрешений путей, которые должен выполнять механизм PHP. (Вы можете получить метаданные о кэшированных путях, используя функции wincache_rplist_fileinfo и wincache_rplist_meminfo .)

Я видел, как большие приложения (такие как WordPress и Drupal) обрабатывают в 3 раза больше запросов страниц в секунду с включенным Wincache, чем без него. (Я проводил тесты на своем ноутбуке. YMMV) Простое включение Wincache должно быть частью любой стратегии повышения производительности приложений PHP на Windows . И, если вы хотите выжать больше из Wincache (и, следовательно, из вашего приложения), вы можете использовать функции пользовательского кэша, которые предлагает Wincache…

Кэширование пользовательских объектов

Как я упоминал во введении, расширение Wincache позволяет вам кэшировать «части» страницы с несколькими источниками данных вместо всей страницы (как это делает модуль кэширования вывода). Например, если запрос к базе данных был одним источником данных на странице, Wincache можно использовать для кэширования набора результатов, что позволило бы сохранить обратную передачу в базу данных. Чтобы увидеть это в действии, я изменил пример приложения , включенного в документацию по SQL Server Driver for PHP . Пример приложения очень прост; он использует AdventureWorks2008R2база данных, позволяющая пользователям искать товары, просматривать обзоры товаров и отправлять обзоры товаров (мое измененное приложение прилагается к этому сообщению). Я добавил логику, которая использует Wincache для кэширования наборов результатов базы данных на основе пользовательского ввода. Я расскажу вам немного кода здесь.

Приведенная ниже функция возвращает массив продуктов на основе условий поиска, предоставленных пользователем. На мой взгляд, стоит отметить следующее:

  • The wincache_ucache_get function attempts to get an object from cache by looking for a supplied key (the first parameter). If the object is found in the cache, the second parameter is set to true. Notice that the key is a concatenation of the string “products” and the user’s search terms. I’m attaching the “products” string to the key in order to distinguish the object from other cached objects that may not be a result set of products.
  • The $from_cache variable is not important for caching. I’m using it to determine if data was pulled from the cache or not (as you will see in the screen shots below).
  • The wincache_ucache_add function adds an object to the cache with the specified key only if the object is not already in the cache. Note that I’m setting the time-to-live (ttl) for this object to 300 seconds. You may want to expire objects from the cache much more quickly or slowly depending on your application.
function GetProducts($search_terms, &$from_cache)
{
    $results = array();
    // Get results from cache if possible. Otherwise, get results from database.
    $results = wincache_ucache_get("products".$search_terms, $success);
    $from_cache = "true";
    if(!$success)
    {
        $from_cache = "false";
        // Get results from the database.
        $conn = ConnectToDB();
        $tsql = "SELECT ProductID, Name, Color, Size, ListPrice 
                 FROM Production.Product 
                 WHERE Name LIKE '%' + ? + '%' AND ListPrice > 0.0";
        $params = str_replace(" ", "%", $search_terms);               
        $stmt = sqlsrv_query($conn, $tsql, array($params));
        if ( $stmt === false )
            die( FormatErrors( sqlsrv_errors() ) );
 
        if(sqlsrv_has_rows($stmt))
        {
            while( $row = sqlsrv_fetch_array( $stmt, SQLSRV_FETCH_ASSOC))
            {
                $results[] = $row;
            }
        }
                
        // Add array of search results to user cache
        wincache_ucache_add("products".$search_terms, $results, 300);
    }
    return $results;
}

Those two functions (wincache_ucache_get and wincache_ucache_add) are the bread and butter of the Wincache extension and will take you a long way toward improving performance. However, you need to think carefully about when you use them. For example, when a result set of product reviews (for a particular product ID) are returned from the database, I want to cache the result set in the same way I did for products. But what happens, when  a user submits a new product review? A typical scenario might be 1) insert the new review, then 2) show the new review in the context of other reviews for the selected product. If I’m caching product review result sets by product ID, then after inserting a new review I’ll retrieve reviews from cache, which won’t include the newly added review! So, I need to account for this in my application logic.

The function below returns an array of product reviews based on a product ID. The things worth pointing out are the following:

  • The wincache_ucache_exists function is used to simply determine if a particular key exists in the cache. This allows me to construct logic that only retrieves reviews from the cache if a new review has not been submitted. (When a new review is submitted, I’ll simply pass true for the value of $new_review.)
  • The $from_cache variable is not important for caching. I’m using it to determine if data was pulled from the cache or not (as you will see in the screen shots below).
  • The wincache_ucache_set function adds a new object to the cache if it doesn’t already exist. But, unlike wincache_ucache_add, it overwrites an object if it does already exist (which we want in the case where a new review was added). Again, note the ttl of 300 seconds (with the same caveat I mentioned above).
    function GetReviews($productID, &$from_cache, $new_review = false)
    {
        $reviews = array();
        // Get reviews from cache if a new review hasn't been submitted.
        if(wincache_ucache_exists("reviews".$productID) && !$new_review)
        {
            $reviews = wincache_ucache_get("reviews".$productID);
            $from_cache = "true";
        }
        else
        {
            // Get reviews from the database.
            $conn = ConnectToDB();
            $tsql = "SELECT ReviewerName, CONVERT(varchar(32), ReviewDate, 107) AS [ReviewDate], Rating, Comments 
                     FROM Production.ProductReview 
                     WHERE ProductID = ? 
                     ORDER BY ReviewDate DESC";
            $stmt = sqlsrv_query( $conn, $tsql, array($productID));
            if( $stmt === false )
                die( FormatErrors( sqlsrv_errors() ) );
            
            if(sqlsrv_has_rows($stmt))
            {
                while ( $row = sqlsrv_fetch_array( $stmt, SQLSRV_FETCH_ASSOC ) )
                {
                    $reviews[] = $row;
                }
                // Add array of reviews to cache. Overwrite cached object if it already exists.
                wincache_ucache_set(array("reviews".$productID => $reviews), null, 300);
            }
        }
        return $reviews;
    }

With that work done, it’s time to take a look at how it improves performance of the application. I’ve added some function calls and flags to show when a result set is retrieved from cache. I’ve also used the Wincache metadata functions (wincache_ucache_info and wincache_ucache_meminfo) to surface cache information in the UI.

In this screen shot, I’ve searched for product with the key word “gloves”. Since this is the first search based on that keyword, the results are not retrieved from the cache. But, the results have been added to the cache (notice the key_name: productgloves in the cache metadata). You can see the page load time in the screen shot along with other information about the cache:

образ

By searching for “gloves” again, the results are returned from the cache and my page load time has decreased dramatically:

образ

You’ll see similar results when retrieving reviews for a product:

образ

Refresh that page and the results will be retrieved from the cache:

образ

However, if you submit a review for this product, you want the reviews to be retrieved from the database after the new review has been submitted. Because of the logic built into the GetReviews function, this is exactly what happens:

образ

And, because our logic overwrites the cache entry for reviews when a new review is added, this result set will be served from the cache the next time reviews are requested for this product (if the request happens with in the specified ttl, 300 seconds in my case).

Note: If you are looking at that last screen shot carefully you may notice that the object with key productgloves appears to still be cached even though it has been in the cache longer than its ttl. A background process periodically removes expired objects from the cache. From a functional standpoint, this object does not exist in the cache. i.e. If that same key is requested again, it will not be served from cache and a new object (with the same key) will be added to the cache.

Other Wincache Performance Enhancements Options

In addition to being an object cache, with minimal work Wincache can also store session data in shared memory. Having session data in shared memory improves application performance by saving on the time it takes to read/write session data from/to files. To enable this feature, make the following modification to your php.ini file:

session.save_handler = wincache
session.save_path = C:\inetpub\temp\session\

For more information about caching session data with Wincache, see Wincache Session Handler in the official documentation.

Conclusion

The Wincache extension provides several ways to significantly improve PHP application performance on Windows. The largest performance gains can be made by simply enabling the Wincache extension (you get opcode, file, and resolved path caching for “free”). You can further enhance performance by leveraging the wincache_ucache_* functions to cache user objects (these functions are especially useful when IIS output caching does not provide the granularity you need for caching data). And finally, you can squeeze a few more drops of performance out of your application by enabling Wincache session handling.

Have fun performance tuning!