mirror of
https://github.com/sigmasternchen/php-doc-en
synced 2025-03-19 10:28:54 +00:00

git-svn-id: https://svn.php.net/repository/phpdoc/en/trunk@325489 c90b9560-bf6c-de11-be94-00142212c4b1
1732 lines
54 KiB
XML
1732 lines
54 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<!-- $Revision$ -->
|
|
|
|
<chapter xml:id="mysqlnd-qc.quickstart" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
<title>Quickstart and Examples</title>
|
|
<para>
|
|
The mysqlnd query cache plugin is easy to use.
|
|
This quickstart will demo typical use-cases, and provide practical advice on getting
|
|
started.
|
|
</para>
|
|
<para>
|
|
It is strongly recommended to read the reference sections in addition to the
|
|
quickstart. It is safe to begin with the quickstart.
|
|
However, before using the plugin in mission critical
|
|
environments we urge you to read additionally the background information from the
|
|
reference sections.
|
|
</para>
|
|
<para>
|
|
Most of the examples use the <link linkend="ref.mysqli">mysqli</link> extension
|
|
because it is the most feature complete PHP MySQL extension. However, the plugin
|
|
can be used with any PHP MySQL extension that is using the
|
|
<link linkend="book.mysqlnd">mysqlnd</link> library.
|
|
</para>
|
|
|
|
<section xml:id="mysqlnd-qc.quickstart.concepts" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
<title>Architecture and Concepts</title>
|
|
<para>
|
|
The query cache plugin is implemented as a PHP extension.
|
|
It is written in C and operates under the hood of PHP. During the
|
|
startup of the PHP interpreter, it gets registered as a
|
|
<link linkend="book.mysqlnd">mysqlnd</link> plugin to replace selected
|
|
mysqlnd C methods. Hereby, it can change the behaviour of any
|
|
PHP MySQL extension (<link linkend="ref.mysqli">mysqli</link>,
|
|
<link linkend="ref.pdo-mysql">PDO_MYSQL</link>,
|
|
<link linkend="ref.mysql">mysql</link>) compiled to use the
|
|
mysqlnd library without changing the extensions API. This makes
|
|
the plugin compatible with each and every PHP MySQL application.
|
|
Because existing APIs are not changed, it is almost transparent
|
|
to use. Please, see the
|
|
<link linkend="mysqlnd.plugin">mysqlnd plugin API description</link>
|
|
for a discussion of the advantages of the plugin architecture and
|
|
a comparison with proxy based solutions.
|
|
</para>
|
|
<para>
|
|
<emphasis role="bold">Transparent to use</emphasis>
|
|
</para>
|
|
<para>
|
|
At PHP run time PECL/mysqlnd_qc can proxy queries send from PHP
|
|
(<link linkend="book.mysqlnd">mysqlnd</link>) to the MySQL server.
|
|
It then inspects the statement string to find whether it shall cache
|
|
its results. If so, result set is cached using a storage handler and
|
|
further executions of the statement are served from the cache for
|
|
a user-defined period. The Time to Live (TTL) of the cache entry
|
|
can either be set globally or on a per statement basis.
|
|
</para>
|
|
<para>
|
|
A statement is either cached if the plugin is instructed to cache all
|
|
statements globally using a or, if the query string starts with the SQL hint
|
|
(<literal>/*qc=on*/</literal>). The plugin is capable of caching any
|
|
query issued by calling appropriate API calls of any of the existing
|
|
PHP MySQL extensions.
|
|
</para>
|
|
<para>
|
|
<emphasis role="bold">Flexible storage: various storage handler</emphasis>
|
|
</para>
|
|
<para>
|
|
Various storage handler are supported to offer different scopes for cache
|
|
entries. Different scopes allow for different degrees in sharing cache
|
|
entries among clients.
|
|
</para>
|
|
<para>
|
|
<itemizedlist>
|
|
<listitem>
|
|
<para>
|
|
<literal>default</literal> (built-in): process memory, scope: process, one or more web requests depending on PHP deployment model used
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
<literal>APC</literal>: shared memory, scope: single server, multiple web requests
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
<literal>SQLite</literal>: memory or file, scope: single server, multiple web requests
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
<literal>MEMCACHE</literal>: main memory, scope: single or multiple server, multiple web requests
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
<literal>user</literal> (built-in): user-defined - any, scope: user-defined - any
|
|
</para>
|
|
</listitem>
|
|
</itemizedlist>
|
|
</para>
|
|
<para>
|
|
Support for the <literal>APC</literal>, <literal>SQLite</literal> and <literal>
|
|
MEMCACHE</literal> storage handler has to be enabled at compile time. The <literal>
|
|
default</literal> and <literal>user</literal> handler are built-in. It is possible
|
|
to switch between compiled-in storage handlers on a per query basis at run time.
|
|
However, it is recommended to pick one storage handler and use it for all cache entries.
|
|
</para>
|
|
<para>
|
|
<emphasis role="bold">Built-in slam defense to avoid overloading</emphasis>
|
|
</para>
|
|
<para>
|
|
To avoid overload situations the cache plugin has a built-in slam defense mechanism.
|
|
If a popular cache entries expires many clients using the cache entries will try
|
|
to refresh the cache entry. For the duration of the refresh many clients may
|
|
access the database server concurrently. In the worst case, the database server
|
|
becomes overloaded and it takes more and more time to refresh the cache entry, which
|
|
in turn lets more and more clients try to refresh the cache entry. To prevent
|
|
this from happening the plugin has a slam defense mechanism. If slam defense is
|
|
enabled and the plugin detects an expired cache entry it extends the life time
|
|
of the cache entry before it refreshes the cache entry. This way other concurrent
|
|
accesses to the expired cache entry are still served from the cache for a certain
|
|
time. The other concurrent accesses to not trigger a concurrent refresh. Ideally,
|
|
the cache entry gets refreshed by the client which extended the cache entries lifespan
|
|
before other clients try to refresh the cache and potentially cause an overload
|
|
situation.
|
|
</para>
|
|
<para>
|
|
<emphasis role="bold">Unique approach to caching</emphasis>
|
|
</para>
|
|
<para>
|
|
PECL/mysqlnd_qc has a unique approach to caching result sets that is superior
|
|
to application based cache solutions. Application based solutions first fetch
|
|
a result set into PHP variables. Then, the PHP variables are serialized for
|
|
storage in a persistent cache, and then unserialized when fetching. The mysqlnd
|
|
query cache stores the raw wire protocol data sent from MySQL to PHP in its cache
|
|
and replays it, if still valid, on a cache hit. This way, it saves an extra
|
|
serialization step for a cache put that all application based solutions have to
|
|
do. It can store the raw wire protocol data in the cache without having to
|
|
serialize into a PHP variable first and deserializing the PHP variable for storing
|
|
in the cache again.
|
|
</para>
|
|
</section>
|
|
|
|
<section xml:id="mysqlnd-qc.quickstart.configuration">
|
|
<title>Setup</title>
|
|
<para>
|
|
The plugin is implemented as a PHP extension. See also the
|
|
<link linkend="mysqlnd-qc.installation">installation instructions</link> to
|
|
install the
|
|
<link xlink:href="&url.pecl.package;mysqlnd_qc">PECL/mysqlnd_qc</link> extension.
|
|
</para>
|
|
<para>
|
|
Compile or configure the PHP MySQL extension (<link linkend="ref.mysqli">mysqli</link>,
|
|
<link linkend="ref.pdo-mysql">PDO_MYSQL</link>,
|
|
<link linkend="ref.mysql">mysql</link>) that you plan to use with support
|
|
for the <link linkend="book.mysqlnd">mysqlnd</link> library. PECL/mysqlnd_qc
|
|
is a plugin for the mysqlnd library. To use the plugin with any of the existing PHP
|
|
MySQL extensions (APIs), the extension has to use the mysqlnd library.
|
|
</para>
|
|
<para>
|
|
Then, load the extension into PHP and activate the plugin in the PHP configuration
|
|
file using the PHP configuration directive named
|
|
<link linkend="ini.mysqlnd-qc.enable-qc">mysqlnd_qc.enable_qc</link>.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Enabling the plugin (php.ini)</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.enable_qc=1
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
</section>
|
|
|
|
<section xml:id="mysqlnd-qc.quickstart.caching">
|
|
<title>Caching queries</title>
|
|
<para>
|
|
There are four ways to trigger caching of a query.
|
|
<itemizedlist>
|
|
<listitem>
|
|
<simpara>Use of SQL hints on a per query basis</simpara>
|
|
</listitem>
|
|
<listitem>
|
|
<simpara>
|
|
User supplied callbacks to decide on a per query basis, for example, using <function>mysqlnd_qc_is_select</function>
|
|
</simpara>
|
|
</listitem>
|
|
<listitem>
|
|
<simpara>
|
|
<function>mysqlnd_set_cache_condition</function> for rule based automatic per query decisions
|
|
</simpara>
|
|
</listitem>
|
|
<listitem>
|
|
<simpara>
|
|
<literal><link linkend="mysqlnd-qc.configuration">mysqlnd_qc.cache_by_default = 1</link></literal>
|
|
to cache all queries blindly
|
|
</simpara>
|
|
</listitem>
|
|
</itemizedlist>
|
|
</para>
|
|
<para>
|
|
Use of SQL hints and
|
|
<literal><link linkend="mysqlnd-qc.configuration">mysqlnd_qc.cache_by_default = 1</link></literal>
|
|
are explained below. Please, refer to the function reference on
|
|
<function>mysqlnd_qc_is_select</function> for a description of using a callback and,
|
|
<function>mysqlnd_qc_set_cache_condition</function> on how to set rules for automatic
|
|
caching.
|
|
</para>
|
|
<para>
|
|
A SQL hint is a SQL standards compliant
|
|
comment. As a SQL comment it is ignored by the database. A statement is considered
|
|
eligible for caching if it either begins with the SQL hint enabling caching
|
|
or it is a <literal>SELECT</literal> statement.
|
|
</para>
|
|
<para>
|
|
An individual query which shall be cached must begin with the SQL hint
|
|
<literal>/*qc=on*/</literal>. It is recommended to use the PHP constant
|
|
<literal><link linkend="mysqlnd-qc.constants">MYSQLND_QC_ENABLE_SWITCH</link></literal>
|
|
instead of using the string value.
|
|
</para>
|
|
<para>
|
|
<itemizedlist>
|
|
<listitem>
|
|
<para>
|
|
not eligible for caching and not cached: <literal>INSERT INTO test(id) VALUES (1)</literal>
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
not eligible for caching and not cached: <literal>SHOW ENGINES</literal>
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
eligible for caching but uncached: <literal>SELECT id FROM test</literal>
|
|
</para>
|
|
</listitem>
|
|
<listitem>
|
|
<para>
|
|
eligible for caching and cached: <literal>/*qc=on*/SELECT id FROM test</literal>
|
|
</para>
|
|
</listitem>
|
|
</itemizedlist>
|
|
</para>
|
|
<para>
|
|
The examples <literal>SELECT</literal> statement string is prefixed with the
|
|
<literal><link linkend="mysqlnd-qc.constants">MYSQLND_QC_ENABLE_SWITCH</link></literal>
|
|
SQL hint to enable caching of the statement. The SQL hint must be given at
|
|
the very beginning of the statement string to enable caching.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Using the <literal>MYSQLND_QC_ENABLE_SWITCH</literal> SQL hint</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.enable_qc=1
|
|
]]>
|
|
</programlisting>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* Connect, create and populate test table */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
$mysqli->query("DROP TABLE IF EXISTS test");
|
|
$mysqli->query("CREATE TABLE test(id INT)");
|
|
$mysqli->query("INSERT INTO test(id) VALUES (1), (2)");
|
|
|
|
/* Will be cached because of the SQL hint */
|
|
$start = microtime(true);
|
|
$res = $mysqli->query("/*" . MYSQLND_QC_ENABLE_SWITCH . "*/" . "SELECT id FROM test WHERE id = 1");
|
|
|
|
var_dump($res->fetch_assoc());
|
|
$res->free();
|
|
|
|
printf("Total time uncached query: %.6fs\n", microtime(true) - $start);
|
|
|
|
/* Cache hit */
|
|
$start = microtime(true);
|
|
$res = $mysqli->query("/*" . MYSQLND_QC_ENABLE_SWITCH . "*/" . "SELECT id FROM test WHERE id = 1");
|
|
|
|
var_dump($res->fetch_assoc());
|
|
$res->free();
|
|
|
|
printf("Total time cached query: %.6fs\n", microtime(true) - $start);
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs.similar;
|
|
<screen>
|
|
<![CDATA[
|
|
array(1) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
}
|
|
Total time uncached query: 0.000740s
|
|
array(1) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
}
|
|
Total time cached query: 0.000098s
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
If nothing else is configured, as it is the case in the quickstart example,
|
|
the plugin will use the built-in <literal>default</literal> storage handler.
|
|
The <literal>default</literal> storage handler uses process memory to hold a cache entry.
|
|
Depending on the PHP deployment model, a PHP process may serve one or more
|
|
web requests. Please, consult the web server manual for details.
|
|
Details make no difference for the examples given in the quickstart.
|
|
</para>
|
|
<para>
|
|
The query cache plugin will cache all queries regardless if
|
|
the query string begins with the SQL hint which enables caching or not,
|
|
if the PHP configuration directive
|
|
<literal><link linkend="mysqlnd-qc.configuration">mysqlnd_qc.cache_by_default</link></literal>
|
|
is set to <literal>1</literal>. The setting
|
|
<literal><link linkend="mysqlnd-qc.configuration">mysqlnd_qc.cache_by_default</link></literal>
|
|
is evaluated by the core of the query cache plugins.
|
|
Neither the built-in nor user-defined storage handler can overrule the setting.
|
|
</para>
|
|
<para>
|
|
The SQL hint <literal>/*qc=off*/</literal> can be used to disable caching
|
|
of individual queries if
|
|
<literal><link linkend="mysqlnd-qc.configuration">mysqlnd_qc.cache_by_default = 1</link></literal>
|
|
It is recommended to use the PHP constant
|
|
<literal><link linkend="mysqlnd-qc.constants">MYSQLND_QC_DISABLE_SWITCH</link></literal>
|
|
instead of using the string value.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Using the <literal>MYSQLND_QC_DISABLE_SWITCH</literal> SQL hint</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.enable_qc=1
|
|
mysqlnd_qc.cache_by_default=1
|
|
]]>
|
|
</programlisting>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* Connect, create and populate test table */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
$mysqli->query("DROP TABLE IF EXISTS test");
|
|
$mysqli->query("CREATE TABLE test(id INT)");
|
|
$mysqli->query("INSERT INTO test(id) VALUES (1), (2)");
|
|
|
|
/* Will be cached although no SQL hint is present because of mysqlnd_qc.cache_by_default = 1*/
|
|
$res = $mysqli->query("SELECT id FROM test WHERE id = 1");
|
|
var_dump($res->fetch_assoc());
|
|
$res->free();
|
|
|
|
$mysqli->query("DELETE FROM test WHERE id = 1");
|
|
|
|
/* Cache hit - no automatic invalidation and still valid! */
|
|
$res = $mysqli->query("SELECT id FROM test WHERE id = 1");
|
|
var_dump($res->fetch_assoc());
|
|
$res->free();
|
|
|
|
/* Cache miss - query must not be cached because of the SQL hint */
|
|
$res = $mysqli->query("/*" . MYSQLND_QC_DISABLE_SWITCH . "*/SELECT id FROM test WHERE id = 1");
|
|
var_dump($res->fetch_assoc());
|
|
$res->free();
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs;
|
|
<screen>
|
|
<![CDATA[
|
|
array(1) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
}
|
|
array(1) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
}
|
|
NULL
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
PECL/mysqlnd_qc forbids caching of statements for which at least one
|
|
column from the statements result set shows no table name in its meta data by default.
|
|
This is usually the case for columns originating from SQL functions such as
|
|
<literal>NOW()</literal> or <literal>LAST_INSERT_ID()</literal>. The policy
|
|
aims to prevent pitfalls if caching by default is used.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Example showing which type of statements are not cached</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.enable_qc=1
|
|
mysqlnd_qc.cache_by_default=1
|
|
]]>
|
|
</programlisting>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* Connect, create and populate test table */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
$mysqli->query("DROP TABLE IF EXISTS test");
|
|
$mysqli->query("CREATE TABLE test(id INT)");
|
|
$mysqli->query("INSERT INTO test(id) VALUES (1)");
|
|
|
|
for ($i = 0; $i < 3; $i++) {
|
|
|
|
$start = microtime(true);
|
|
|
|
/* Note: statement will not be cached because of NOW() use */
|
|
$res = $mysqli->query("SELECT id, NOW() AS _time FROM test");
|
|
$row = $res->fetch_assoc();
|
|
|
|
/* dump results */
|
|
var_dump($row);
|
|
|
|
printf("Total time: %.6fs\n", microtime(true) - $start);
|
|
|
|
/* pause one second */
|
|
sleep(1);
|
|
}
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs.similar;
|
|
<screen>
|
|
<![CDATA[
|
|
array(2) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
["_time"]=>
|
|
string(19) "2012-01-11 15:43:10"
|
|
}
|
|
Total time: 0.000540s
|
|
array(2) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
["_time"]=>
|
|
string(19) "2012-01-11 15:43:11"
|
|
}
|
|
Total time: 0.000555s
|
|
array(2) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
["_time"]=>
|
|
string(19) "2012-01-11 15:43:12"
|
|
}
|
|
Total time: 0.000549s
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
It is possible to enable caching for all statements including those
|
|
which has columns in their result set for which MySQL reports no table, such as
|
|
the statement from the example. Set
|
|
<link linkend="ini.mysqlnd-qc.cache-no-table"><literal>mysqlnd_qc.cache_no_table = 1</literal></link>
|
|
to enable caching of such statements. Please, note the difference in the
|
|
measured times for the above and below examples.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Enabling caching for all statements using the <literal>mysqlnd_qc.cache_no_table</literal> ini setting</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.enable_qc=1
|
|
mysqlnd_qc.cache_by_default=1
|
|
mysqlnd_qc.cache_no_table=1
|
|
]]>
|
|
</programlisting>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* Connect, create and populate test table */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
$mysqli->query("DROP TABLE IF EXISTS test");
|
|
$mysqli->query("CREATE TABLE test(id INT)");
|
|
$mysqli->query("INSERT INTO test(id) VALUES (1)");
|
|
|
|
for ($i = 0; $i < 3; $i++) {
|
|
|
|
$start = microtime(true);
|
|
|
|
/* Note: statement will not be cached because of NOW() use */
|
|
$res = $mysqli->query("SELECT id, NOW() AS _time FROM test");
|
|
$row = $res->fetch_assoc();
|
|
|
|
/* dump results */
|
|
var_dump($row);
|
|
|
|
printf("Total time: %.6fs\n", microtime(true) - $start);
|
|
|
|
/* pause one second */
|
|
sleep(1);
|
|
}
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs.similar;
|
|
<screen>
|
|
<![CDATA[
|
|
array(2) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
["_time"]=>
|
|
string(19) "2012-01-11 15:47:45"
|
|
}
|
|
Total time: 0.000546s
|
|
array(2) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
["_time"]=>
|
|
string(19) "2012-01-11 15:47:45"
|
|
}
|
|
Total time: 0.000187s
|
|
array(2) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
["_time"]=>
|
|
string(19) "2012-01-11 15:47:45"
|
|
}
|
|
Total time: 0.000167s
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<note>
|
|
<para>
|
|
Although <link linkend="ini.mysqlnd-qc.cache-no-table"><literal>mysqlnd_qc.cache_no_table = 1</literal></link>
|
|
has been created for use with
|
|
<literal><link linkend="mysqlnd-qc.configuration">mysqlnd_qc.cache_by_default = 1</link></literal>
|
|
it is bound it. The plugin will evaluate the
|
|
<link linkend="ini.mysqlnd-qc.cache-no-table"><literal>mysqlnd_qc.cache_no_table</literal></link>
|
|
whenever a query is to be cached, no matter whether caching has been enabled using a
|
|
SQL hint or any other measure.
|
|
</para>
|
|
</note>
|
|
</section>
|
|
|
|
<section xml:id="mysqlnd-qc.per-query-ttl" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
<title>Setting the TTL</title>
|
|
<para>
|
|
The default invalidation strategy of the query cache plugin is Time to Live
|
|
(<literal>TTL</literal>). The built-in storage handlers will use the default
|
|
<literal>TTL</literal> defined by the PHP configuration value
|
|
<literal><link linkend="mysqlnd-qc.configuration">mysqlnd_qc.ttl</link></literal>
|
|
unless the query string contains a hint for setting a different
|
|
<literal>TTL</literal>. The <literal>TTL</literal> is specified in seconds.
|
|
By default cache entries expire after <literal>30</literal> seconds
|
|
</para>
|
|
<para>
|
|
The example sets <literal>mysqlnd_qc.ttl=3</literal> to cache
|
|
statements for three seconds by default. Every second it updates
|
|
a database table record to hold the current time and executes
|
|
a <literal>SELECT</literal> statement to fetch the record from the
|
|
database. The <literal>SELECT</literal> statement is cached for
|
|
three seconds because it is prefixed with the SQL hint enabling
|
|
caching. The output verifies that the query results are taken
|
|
from the cache for the duration of three seconds before they
|
|
are refreshed.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Setting the TTL with the <literal>mysqlnd_qc.ttl</literal> ini setting</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.enable_qc=1
|
|
mysqlnd_qc.ttl=3
|
|
]]>
|
|
</programlisting>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* Connect, create and populate test table */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
$mysqli->query("DROP TABLE IF EXISTS test");
|
|
$mysqli->query("CREATE TABLE test(id VARCHAR(255))");
|
|
|
|
for ($i = 0; $i < 7; $i++) {
|
|
|
|
/* update DB row */
|
|
if (!$mysqli->query("DELETE FROM test") ||
|
|
!$mysqli->query("INSERT INTO test(id) VALUES (NOW())"))
|
|
/* Of course, a real-life script should do better error handling */
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* select latest row but cache results */
|
|
$query = "/*" . MYSQLND_QC_ENABLE_SWITCH . "*/";
|
|
$query .= "SELECT id AS _time FROM test";
|
|
if (!($res = $mysqli->query($query)) ||
|
|
!($row = $res->fetch_assoc()))
|
|
{
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
}
|
|
$res->free();
|
|
printf("Wall time %s - DB row time %s\n", date("H:i:s"), $row['_time']);
|
|
|
|
/* pause one second */
|
|
sleep(1);
|
|
}
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs.similar;
|
|
<screen>
|
|
<![CDATA[
|
|
Wall time 14:55:59 - DB row time 2012-01-11 14:55:59
|
|
Wall time 14:56:00 - DB row time 2012-01-11 14:55:59
|
|
Wall time 14:56:01 - DB row time 2012-01-11 14:55:59
|
|
Wall time 14:56:02 - DB row time 2012-01-11 14:56:02
|
|
Wall time 14:56:03 - DB row time 2012-01-11 14:56:02
|
|
Wall time 14:56:04 - DB row time 2012-01-11 14:56:02
|
|
Wall time 14:56:05 - DB row time 2012-01-11 14:56:05
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
As can be seen from the example, any <literal>TTL</literal> based cache
|
|
can serve stale data. Cache entries are not automatically invalidated,
|
|
if underlying data changes. Applications using the default
|
|
<literal>TTL</literal> invalidation strategy must be able to work correctly
|
|
with stale data.
|
|
</para>
|
|
<para>
|
|
A user-defined cache storage handler can implement any invalidation strategy
|
|
to work around this limitation.
|
|
</para>
|
|
<para>
|
|
The default <literal>TTL</literal> can be overruled using the SQL hint
|
|
<literal>/*qc_tt=seconds*/</literal>. The SQL hint must be appear immediately
|
|
after the SQL hint which enables caching. It is recommended to use the PHP constant
|
|
<literal><link linkend="mysqlnd-qc.constants">MYSQLND_QC_TTL_SWITCH</link></literal>
|
|
instead of using the string value.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Setting TTL with SQL hints</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$start = microtime(true);
|
|
|
|
/* Connect, create and populate test table */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
$mysqli->query("DROP TABLE IF EXISTS test");
|
|
$mysqli->query("CREATE TABLE test(id INT)");
|
|
$mysqli->query("INSERT INTO test(id) VALUES (1), (2)");
|
|
|
|
printf("Default TTL\t: %d seconds\n", ini_get("mysqlnd_qc.ttl"));
|
|
|
|
/* Will be cached for 2 seconds */
|
|
$sql = sprintf("/*%s*//*%s%d*/SELECT id FROM test WHERE id = 1", MYSQLND_QC_ENABLE_SWITCH, MYSQLND_QC_TTL_SWITCH, 2);
|
|
$res = $mysqli->query($sql);
|
|
|
|
var_dump($res->fetch_assoc());
|
|
$res->free();
|
|
|
|
$mysqli->query("DELETE FROM test WHERE id = 1");
|
|
sleep(1);
|
|
|
|
/* Cache hit - no automatic invalidation and still valid! */
|
|
$res = $mysqli->query($sql);
|
|
var_dump($res->fetch_assoc());
|
|
$res->free();
|
|
|
|
sleep(2);
|
|
|
|
/* Cache miss - cache entry has expired */
|
|
$res = $mysqli->query($sql);
|
|
var_dump($res->fetch_assoc());
|
|
$res->free();
|
|
|
|
printf("Script runtime\t: %d seconds\n", microtime(true) - $start);
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs.similar;
|
|
<screen>
|
|
<![CDATA[
|
|
Default TTL : 30 seconds
|
|
array(1) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
}
|
|
array(1) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
}
|
|
NULL
|
|
Script runtime : 3 seconds
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
</section>
|
|
|
|
<section xml:id="mysqlnd-qc.pattern-based-caching" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
<title>Pattern based caching</title>
|
|
<para>
|
|
An application has three options for telling PECL/mysqlnd_qc whether a particular
|
|
statement shall be used. The most basic approach is to cache all statements by
|
|
setting <literal><link linkend="mysqlnd-qc.configuration">
|
|
mysqlnd_qc.cache_by_default = 1</link></literal>. This approach is often of little
|
|
practical value. But it enables users to make a quick estimation about
|
|
the maximum performance gains from caching. An application designed to
|
|
use a cache may be able to prefix selected statements with the appropriate SQL
|
|
hints. However, altering an applications source code may not always be possible
|
|
or desired, for example, to avoid problems with software updates. Therefore,
|
|
PECL/mysqlnd_qc allows setting a callback which decides if a query is to be
|
|
cached.
|
|
</para>
|
|
<para>
|
|
The callback is installed with the <function>mysqlnd_qc_set_is_select</function>
|
|
function. The callback is given the statement string of every statement
|
|
inspected by the plugin. Then, the callback can decide whether to cache
|
|
the function. The callback is supposed to return &false;
|
|
if the statement shall not be cached. A return value of &true;
|
|
makes the plugin try to add the statement into the cache. The cache entry
|
|
will be given the default TTL (<literal><link linkend="mysqlnd-qc.configuration">
|
|
mysqlnd_qc.ttl</link></literal>). If the callback returns
|
|
a numerical value it is used as the TTL instead of the global default.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Setting a callback with <function>mysqlnd_qc_set_is_select</function></title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.enable_qc=1
|
|
mysqlnd_qc.collect_statistics=1
|
|
]]>
|
|
</programlisting>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* callback which decides if query is cached */
|
|
function is_select($query) {
|
|
static $patterns = array(
|
|
/* true - use default from mysqlnd_qc.ttl */
|
|
"@SELECT\s+.*\s+FROM\s+test@ismU" => true,
|
|
/* 3 - use TTL = 3 seconds */
|
|
"@SELECT\s+.*\s+FROM\s+news@ismU" => 3
|
|
);
|
|
|
|
/* check if query does match pattern */
|
|
foreach ($patterns as $pattern => $ttl) {
|
|
if (preg_match($pattern, $query)) {
|
|
printf("is_select(%45s): cache\n", $query);
|
|
return $ttl;
|
|
}
|
|
}
|
|
printf("is_select(%45s): do not cache\n", $query);
|
|
return false;
|
|
}
|
|
/* install callback */
|
|
mysqlnd_qc_set_is_select("is_select");
|
|
|
|
/* Connect, create and populate test table */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
$mysqli->query("DROP TABLE IF EXISTS test");
|
|
$mysqli->query("CREATE TABLE test(id INT)");
|
|
$mysqli->query("INSERT INTO test(id) VALUES (1), (2), (3)");
|
|
|
|
/* cache put */
|
|
$mysqli->query("SELECT id FROM test WHERE id = 1");
|
|
/* cache hit */
|
|
$mysqli->query("SELECT id FROM test WHERE id = 1");
|
|
/* cache put */
|
|
$mysqli->query("SELECT * FROM test");
|
|
|
|
$stats = mysqlnd_qc_get_core_stats();
|
|
printf("Cache put: %d\n", $stats['cache_put']);
|
|
printf("Cache hit: %d\n", $stats['cache_hit']);
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs.similar;
|
|
<screen>
|
|
<![CDATA[
|
|
is_select( DROP TABLE IF EXISTS test): do not cache
|
|
is_select( CREATE TABLE test(id INT)): do not cache
|
|
is_select( INSERT INTO test(id) VALUES (1), (2), (3)): do not cache
|
|
is_select( SELECT id FROM test WHERE id = 1): cache
|
|
is_select( SELECT id FROM test WHERE id = 1): cache
|
|
is_select( SELECT * FROM test): cache
|
|
Cache put: 2
|
|
Cache hit: 1
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The examples callback tests if a statement string matches a pattern.
|
|
If this is the case, it either returns &true; to cache
|
|
the statement using the global default TTL or an alternative TTL.
|
|
</para>
|
|
<para>
|
|
To minimize application changes the callback can put into and registered
|
|
in an auto prepend file.
|
|
</para>
|
|
</section>
|
|
|
|
<section xml:id="mysqlnd-qc.slam-defense" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
<title>Slam defense</title>
|
|
<para>
|
|
A badly designed cache can do more harm than good. In the worst case a cache
|
|
can increase database server load instead of minimizing it. An overload situation
|
|
can occur if a highly shared cache entry expires (cache stampeding).
|
|
</para>
|
|
<para>
|
|
Cache entries are shared and reused to a different degree depending on
|
|
the storage used. The default storage handler stores cache entries in process memory.
|
|
Thus, a cache entry can be reused for the life-span of a process. Other PHP
|
|
processes cannot access it. If Memcache is used, a cache entry can be shared
|
|
among multiple PHP processes and even among multiple machines, depending on the
|
|
set up being used.
|
|
</para>
|
|
<para>
|
|
If a highly shared cache entry stored, for example, in Memcache expires, many
|
|
clients gets a cache miss. Many client requests can no longer be served from
|
|
the cache but try to run the underlying query on the database server. Until
|
|
the cache entry is refreshed, more and more clients contact the database server.
|
|
In the worst case, a total lost of service is the result.
|
|
</para>
|
|
<para>
|
|
The overload can be avoided using a storage handler which limits the reuse of cache
|
|
entries to few clients. Then, at the average, its likely that only a limited number
|
|
of clients will try to refresh a cache entry concurrently.
|
|
</para>
|
|
<para>
|
|
Additionally, the built-in slam defense mechanism can and should be used. If
|
|
slam defense is activated an expired cache entry is given an extended life time.
|
|
The first client getting a cache miss for the expired cache entry tries to
|
|
refresh the cache entry within the extended life time. All other clients requesting
|
|
the cache entry are temporarily served from the cache although the original
|
|
<literal>TTL</literal> of the cache entry has expired. The other clients will
|
|
not experience a cache miss before the extended life time is over.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Enabling the slam defense mechanism</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.slam_defense=1
|
|
mysqlnd_qc.slam_defense_ttl=1
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The slam defense mechanism is enabled with the PHP configuration directive
|
|
<link linkend="ini.mysqlnd-qc.slam-defense"><literal>mysqlnd_qc.slam_defense</literal></link>.
|
|
The extended life time of a cache entry is set with
|
|
<link linkend="ini.mysqlnd-qc.slam-defense-ttl"><literal>mysqlnd_qc.slam_defense_ttl</literal></link>.
|
|
</para>
|
|
<para>
|
|
The function
|
|
<function>mysqlnd_qc_get_core_stats</function> returns an array of
|
|
statistics. The statistics <literal>slam_stale_refresh</literal> and
|
|
<literal>slam_stale_hit</literal> are incremented if slam defense takes place.
|
|
</para>
|
|
<para>
|
|
It is not possible to give a one-fits-all recommendation on the slam defense
|
|
configuration. Users are advised to monitor and test their setup and derive
|
|
settings accordingly.
|
|
</para>
|
|
</section>
|
|
|
|
<section xml:id="mysqlnd-qc.cache-candidates" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
<title>Finding cache candidates</title>
|
|
<para>
|
|
A statement should be considered for caching if it is executed often and has
|
|
a long run time. Cache candidates are found by creating a list of statements
|
|
sorted by the product of the number of executions multiplied by the
|
|
statements run time. The function
|
|
<function>mysqlnd_qc_get_query_trace_log</function>
|
|
returns a query log which help with the task.
|
|
</para>
|
|
<para>
|
|
Collecting a query trace is a slow operation. Thus, it is disabled by default.
|
|
The PHP configuration directive
|
|
<link linkend="ini.mysqlnd-qc.collect-query-trace"><literal>mysqlnd_qc.collect_query_trace</literal></link>
|
|
is used to enable it. The functions trace contains one entry for every
|
|
query issued before the function is called.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Collecting a query trace</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.enable_qc=1
|
|
mysqlnd_qc.collect_query_trace=1
|
|
]]>
|
|
</programlisting>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* connect to MySQL */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
|
|
/* dummy queries to fill the query trace */
|
|
for ($i = 0; $i < 2; $i++) {
|
|
$res = $mysqli->query("SELECT 1 AS _one FROM DUAL");
|
|
$res->free();
|
|
}
|
|
|
|
/* dump trace */
|
|
var_dump(mysqlnd_qc_get_query_trace_log());
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs;
|
|
<screen>
|
|
<![CDATA[
|
|
array(2) {
|
|
[0]=>
|
|
array(8) {
|
|
["query"]=>
|
|
string(26) "SELECT 1 AS _one FROM DUAL"
|
|
["origin"]=>
|
|
string(102) "#0 qc.php(7): mysqli->query('SELECT 1 AS _on...')
|
|
#1 {main}"
|
|
["run_time"]=>
|
|
int(0)
|
|
["store_time"]=>
|
|
int(25)
|
|
["eligible_for_caching"]=>
|
|
bool(false)
|
|
["no_table"]=>
|
|
bool(false)
|
|
["was_added"]=>
|
|
bool(false)
|
|
["was_already_in_cache"]=>
|
|
bool(false)
|
|
}
|
|
[1]=>
|
|
array(8) {
|
|
["query"]=>
|
|
string(26) "SELECT 1 AS _one FROM DUAL"
|
|
["origin"]=>
|
|
string(102) "#0 qc.php(7): mysqli->query('SELECT 1 AS _on...')
|
|
#1 {main}"
|
|
["run_time"]=>
|
|
int(0)
|
|
["store_time"]=>
|
|
int(8)
|
|
["eligible_for_caching"]=>
|
|
bool(false)
|
|
["no_table"]=>
|
|
bool(false)
|
|
["was_added"]=>
|
|
bool(false)
|
|
["was_already_in_cache"]=>
|
|
bool(false)
|
|
}
|
|
}
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
Assorted information is given in the trace. Among them
|
|
timings and the origin of the query call. The origin property
|
|
holds a code backtrace to identify the source of the query.
|
|
The depth of the backtrace can be limited with
|
|
the PHP configuration directive
|
|
<link linkend="ini.mysqlnd-qc.query-trace-bt-depth"><literal>mysqlnd_qc.query_trace_bt_depth</literal></link>.
|
|
The default depth is <literal>3</literal>.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Setting the backtrace depth with the <literal>mysqlnd_qc.query_trace_bt_depth</literal> ini setting</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.enable_qc=1
|
|
mysqlnd_qc.collect_query_trace=1
|
|
]]>
|
|
</programlisting>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* connect to MySQL */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
$mysqli->query("DROP TABLE IF EXISTS test");
|
|
$mysqli->query("CREATE TABLE test(id INT)");
|
|
$mysqli->query("INSERT INTO test(id) VALUES (1), (2), (3)");
|
|
|
|
/* dummy queries to fill the query trace */
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$res = $mysqli->query("SELECT id FROM test WHERE id = " . $mysqli->real_escape_string($i));
|
|
$res->free();
|
|
}
|
|
|
|
$trace = mysqlnd_qc_get_query_trace_log();
|
|
$summary = array();
|
|
foreach ($trace as $entry) {
|
|
if (!isset($summary[$entry['query']])) {
|
|
$summary[$entry['query']] = array(
|
|
"executions" => 1,
|
|
"time" => $entry['run_time'] + $entry['store_time'],
|
|
);
|
|
} else {
|
|
$summary[$entry['query']]['executions']++;
|
|
$summary[$entry['query']]['time'] += $entry['run_time'] + $entry['store_time'];
|
|
}
|
|
}
|
|
|
|
foreach ($summary as $query => $details) {
|
|
printf("%45s: %5dms (%dx)\n",
|
|
$query, $details['time'], $details['executions']);
|
|
}
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs.similar;
|
|
<screen>
|
|
<![CDATA[
|
|
DROP TABLE IF EXISTS test: 0ms (1x)
|
|
CREATE TABLE test(id INT): 0ms (1x)
|
|
INSERT INTO test(id) VALUES (1), (2), (3): 0ms (1x)
|
|
SELECT id FROM test WHERE id = 0: 25ms (1x)
|
|
SELECT id FROM test WHERE id = 1: 10ms (1x)
|
|
SELECT id FROM test WHERE id = 2: 9ms (1x)
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
</section>
|
|
|
|
<section xml:id="mysqlnd-qc.cache-efficiency" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
<title>Measuring cache efficiency</title>
|
|
<para>
|
|
PECL/mysqlnd_qc offers three ways to measure the cache efficiency.
|
|
The function
|
|
<function>mysqlnd_qc_get_normalized_query_trace_log</function>
|
|
returns statistics aggregated by the normalized query string,
|
|
<function>mysqlnd_qc_get_cache_info</function>
|
|
gives storage handler specific information which includes a list
|
|
of all cached items, depending on the storage handler. Additionally, the
|
|
core of PECL/mysqlnd_qc collects high-level summary statistics aggregated
|
|
per PHP process. The high-level statistics are returned by
|
|
<function>mysqlnd_qc_get_core_stats</function>.
|
|
</para>
|
|
<para>
|
|
The functions
|
|
<function>mysqlnd_qc_get_normalized_query_trace_log</function>
|
|
and
|
|
<function>mysqlnd_qc_get_core_stats</function>
|
|
will not collect data unless data collection has been
|
|
enabled through their corresponding PHP configuration directives. Data collection
|
|
is disabled by default for performance considerations. It is configurable with the
|
|
<link linkend="ini.mysqlnd-qc.time-statistics">mysqlnd_qc.time_statistics</link>
|
|
option, which determines if timing information should be collected.
|
|
Collection of time statistics is enabled by default
|
|
but only performed if data collection as such has been enabled.
|
|
Recording time statistics causes extra system calls. In most cases,
|
|
the benefit of the monitoring outweighs any potential performance penalty of
|
|
the additional system calls.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Collecting statistics data with the <literal>mysqlnd_qc.time_statistics</literal> ini setting</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.enable_qc=1
|
|
mysqlnd_qc.collect_statistics=1
|
|
]]>
|
|
</programlisting>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* connect to MySQL */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
$mysqli->query("DROP TABLE IF EXISTS test");
|
|
$mysqli->query("CREATE TABLE test(id INT)");
|
|
$mysqli->query("INSERT INTO test(id) VALUES (1), (2), (3)");
|
|
|
|
/* dummy queries */
|
|
for ($i = 1; $i <= 4; $i++) {
|
|
$query = sprintf("/*%s*/SELECT id FROM test WHERE id = %d", MYSQLND_QC_ENABLE_SWITCH, $i % 2);
|
|
$res = $mysqli->query($query);
|
|
|
|
$res->free();
|
|
}
|
|
|
|
var_dump(mysqlnd_qc_get_core_stats());
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs.similar;
|
|
<screen>
|
|
<![CDATA[
|
|
array(26) {
|
|
["cache_hit"]=>
|
|
string(1) "2"
|
|
["cache_miss"]=>
|
|
string(1) "2"
|
|
["cache_put"]=>
|
|
string(1) "2"
|
|
["query_should_cache"]=>
|
|
string(1) "4"
|
|
["query_should_not_cache"]=>
|
|
string(1) "3"
|
|
["query_not_cached"]=>
|
|
string(1) "3"
|
|
["query_could_cache"]=>
|
|
string(1) "4"
|
|
["query_found_in_cache"]=>
|
|
string(1) "2"
|
|
["query_uncached_other"]=>
|
|
string(1) "0"
|
|
["query_uncached_no_table"]=>
|
|
string(1) "0"
|
|
["query_uncached_no_result"]=>
|
|
string(1) "0"
|
|
["query_uncached_use_result"]=>
|
|
string(1) "0"
|
|
["query_aggr_run_time_cache_hit"]=>
|
|
string(2) "28"
|
|
["query_aggr_run_time_cache_put"]=>
|
|
string(3) "900"
|
|
["query_aggr_run_time_total"]=>
|
|
string(3) "928"
|
|
["query_aggr_store_time_cache_hit"]=>
|
|
string(2) "14"
|
|
["query_aggr_store_time_cache_put"]=>
|
|
string(2) "40"
|
|
["query_aggr_store_time_total"]=>
|
|
string(2) "54"
|
|
["receive_bytes_recorded"]=>
|
|
string(3) "136"
|
|
["receive_bytes_replayed"]=>
|
|
string(3) "136"
|
|
["send_bytes_recorded"]=>
|
|
string(2) "84"
|
|
["send_bytes_replayed"]=>
|
|
string(2) "84"
|
|
["slam_stale_refresh"]=>
|
|
string(1) "0"
|
|
["slam_stale_hit"]=>
|
|
string(1) "0"
|
|
["request_counter"]=>
|
|
int(1)
|
|
["process_hash"]=>
|
|
int(1929695233)
|
|
}
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
For a quick overview, call
|
|
<function>mysqlnd_qc_get_core_stats</function>. It delivers
|
|
cache usage, cache timing and traffic related statistics. Values are aggregated
|
|
on a per process basis for all queries issued by any PHP MySQL API call.
|
|
</para>
|
|
<para>
|
|
Some storage handler, such as the default handler, can report cache entries,
|
|
statistics related to the entries and meta data for the underlying query through the
|
|
<function>mysqlnd_qc_get_cache_info</function>
|
|
function. Please note, that the information returned depends
|
|
on the storage handler. Values are aggregated on a per process basis.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Example <function>mysqlnd_qc_get_cache_info</function> usage</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.enable_qc=1
|
|
]]>
|
|
</programlisting>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* connect to MySQL */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
$mysqli->query("DROP TABLE IF EXISTS test");
|
|
$mysqli->query("CREATE TABLE test(id INT)");
|
|
$mysqli->query("INSERT INTO test(id) VALUES (1), (2), (3)");
|
|
|
|
/* dummy queries to fill the query trace */
|
|
for ($i = 1; $i <= 4; $i++) {
|
|
$query = sprintf("/*%s*/SELECT id FROM test WHERE id = %d", MYSQLND_QC_ENABLE_SWITCH, $i % 2);
|
|
$res = $mysqli->query($query);
|
|
|
|
$res->free();
|
|
}
|
|
|
|
var_dump(mysqlnd_qc_get_cache_info());
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs.similar;
|
|
<screen>
|
|
<![CDATA[
|
|
array(4) {
|
|
["num_entries"]=>
|
|
int(2)
|
|
["handler"]=>
|
|
string(7) "default"
|
|
["handler_version"]=>
|
|
string(5) "1.0.0"
|
|
["data"]=>
|
|
array(2) {
|
|
["Localhost via UNIX socket
|
|
3306
|
|
root
|
|
test|/*qc=on*/SELECT id FROM test WHERE id = 1"]=>
|
|
array(2) {
|
|
["statistics"]=>
|
|
array(11) {
|
|
["rows"]=>
|
|
int(1)
|
|
["stored_size"]=>
|
|
int(71)
|
|
["cache_hits"]=>
|
|
int(1)
|
|
["run_time"]=>
|
|
int(391)
|
|
["store_time"]=>
|
|
int(27)
|
|
["min_run_time"]=>
|
|
int(16)
|
|
["max_run_time"]=>
|
|
int(16)
|
|
["min_store_time"]=>
|
|
int(8)
|
|
["max_store_time"]=>
|
|
int(8)
|
|
["avg_run_time"]=>
|
|
int(8)
|
|
["avg_store_time"]=>
|
|
int(4)
|
|
}
|
|
["metadata"]=>
|
|
array(1) {
|
|
[0]=>
|
|
array(8) {
|
|
["name"]=>
|
|
string(2) "id"
|
|
["orig_name"]=>
|
|
string(2) "id"
|
|
["table"]=>
|
|
string(4) "test"
|
|
["orig_table"]=>
|
|
string(4) "test"
|
|
["db"]=>
|
|
string(4) "test"
|
|
["max_length"]=>
|
|
int(1)
|
|
["length"]=>
|
|
int(11)
|
|
["type"]=>
|
|
int(3)
|
|
}
|
|
}
|
|
}
|
|
["Localhost via UNIX socket
|
|
3306
|
|
root
|
|
test|/*qc=on*/SELECT id FROM test WHERE id = 0"]=>
|
|
array(2) {
|
|
["statistics"]=>
|
|
array(11) {
|
|
["rows"]=>
|
|
int(0)
|
|
["stored_size"]=>
|
|
int(65)
|
|
["cache_hits"]=>
|
|
int(1)
|
|
["run_time"]=>
|
|
int(299)
|
|
["store_time"]=>
|
|
int(13)
|
|
["min_run_time"]=>
|
|
int(11)
|
|
["max_run_time"]=>
|
|
int(11)
|
|
["min_store_time"]=>
|
|
int(6)
|
|
["max_store_time"]=>
|
|
int(6)
|
|
["avg_run_time"]=>
|
|
int(5)
|
|
["avg_store_time"]=>
|
|
int(3)
|
|
}
|
|
["metadata"]=>
|
|
array(1) {
|
|
[0]=>
|
|
array(8) {
|
|
["name"]=>
|
|
string(2) "id"
|
|
["orig_name"]=>
|
|
string(2) "id"
|
|
["table"]=>
|
|
string(4) "test"
|
|
["orig_table"]=>
|
|
string(4) "test"
|
|
["db"]=>
|
|
string(4) "test"
|
|
["max_length"]=>
|
|
int(0)
|
|
["length"]=>
|
|
int(11)
|
|
["type"]=>
|
|
int(3)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
It is possible to further break down the granularity of statistics
|
|
to the level of the normalized statement string.
|
|
The normalized statement string is the statements string with all parameters
|
|
replaced with question marks. For example, the two statements
|
|
<literal>SELECT id FROM test WHERE id = 0</literal> and
|
|
<literal>SELECT id FROM test WHERE id = 1</literal> are normalized into
|
|
<literal>SELECT id FROM test WHERE id = ?</literal>. Their both
|
|
statistics are aggregated into one entry for
|
|
<literal>SELECT id FROM test WHERE id = ?</literal>.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Example <function>mysqlnd_qc_get_normalized_query_trace_log</function> usage</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_qc.enable_qc=1
|
|
mysqlnd_qc.collect_normalized_query_trace=1
|
|
]]>
|
|
</programlisting>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* connect to MySQL */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
$mysqli->query("DROP TABLE IF EXISTS test");
|
|
$mysqli->query("CREATE TABLE test(id INT)");
|
|
$mysqli->query("INSERT INTO test(id) VALUES (1), (2), (3)");
|
|
|
|
/* dummy queries to fill the query trace */
|
|
for ($i = 1; $i <= 4; $i++) {
|
|
$query = sprintf("/*%s*/SELECT id FROM test WHERE id = %d", MYSQLND_QC_ENABLE_SWITCH, $i % 2);
|
|
$res = $mysqli->query($query);
|
|
|
|
$res->free();
|
|
}
|
|
|
|
var_dump(mysqlnd_qc_get_normalized_query_trace_log());
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs.similar;
|
|
<screen>
|
|
<![CDATA[
|
|
array(4) {
|
|
[0]=>
|
|
array(9) {
|
|
["query"]=>
|
|
string(25) "DROP TABLE IF EXISTS test"
|
|
["occurences"]=>
|
|
int(0)
|
|
["eligible_for_caching"]=>
|
|
bool(false)
|
|
["avg_run_time"]=>
|
|
int(0)
|
|
["min_run_time"]=>
|
|
int(0)
|
|
["max_run_time"]=>
|
|
int(0)
|
|
["avg_store_time"]=>
|
|
int(0)
|
|
["min_store_time"]=>
|
|
int(0)
|
|
["max_store_time"]=>
|
|
int(0)
|
|
}
|
|
[1]=>
|
|
array(9) {
|
|
["query"]=>
|
|
string(27) "CREATE TABLE test (id INT )"
|
|
["occurences"]=>
|
|
int(0)
|
|
["eligible_for_caching"]=>
|
|
bool(false)
|
|
["avg_run_time"]=>
|
|
int(0)
|
|
["min_run_time"]=>
|
|
int(0)
|
|
["max_run_time"]=>
|
|
int(0)
|
|
["avg_store_time"]=>
|
|
int(0)
|
|
["min_store_time"]=>
|
|
int(0)
|
|
["max_store_time"]=>
|
|
int(0)
|
|
}
|
|
[2]=>
|
|
array(9) {
|
|
["query"]=>
|
|
string(46) "INSERT INTO test (id ) VALUES (? ), (? ), (? )"
|
|
["occurences"]=>
|
|
int(0)
|
|
["eligible_for_caching"]=>
|
|
bool(false)
|
|
["avg_run_time"]=>
|
|
int(0)
|
|
["min_run_time"]=>
|
|
int(0)
|
|
["max_run_time"]=>
|
|
int(0)
|
|
["avg_store_time"]=>
|
|
int(0)
|
|
["min_store_time"]=>
|
|
int(0)
|
|
["max_store_time"]=>
|
|
int(0)
|
|
}
|
|
[3]=>
|
|
array(9) {
|
|
["query"]=>
|
|
string(31) "SELECT id FROM test WHERE id =?"
|
|
["occurences"]=>
|
|
int(4)
|
|
["eligible_for_caching"]=>
|
|
bool(true)
|
|
["avg_run_time"]=>
|
|
int(179)
|
|
["min_run_time"]=>
|
|
int(11)
|
|
["max_run_time"]=>
|
|
int(393)
|
|
["avg_store_time"]=>
|
|
int(12)
|
|
["min_store_time"]=>
|
|
int(7)
|
|
["max_store_time"]=>
|
|
int(25)
|
|
}
|
|
}
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The source distribution of PECL/mysqlnd_qc contains a directory
|
|
<literal>web/</literal> in which web based monitoring
|
|
scripts can be found which give an example how to write a cache monitor.
|
|
Please, follow the instructions given in the source.
|
|
</para>
|
|
<para>
|
|
Since PECL/mysqlnd_qc 1.1.0 it is possible to write statistics into a
|
|
log file. Please, see <literal><link linkend="ini.mysqlnd-qc.collect-statistics-log-file">
|
|
mysqlnd_qc.collect_statistics_log_file</link></literal>.
|
|
</para>
|
|
</section>
|
|
|
|
<section xml:id="mysqlnd-qc.set-user-handlers" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
<title>Beyond TTL: user-defined storage</title>
|
|
<para>
|
|
The query cache plugin supports the use of user-defined storage handler.
|
|
User-defined storage handler can use arbitrarily complex invalidation
|
|
algorithms and support arbitrary storage media.
|
|
</para>
|
|
<para>
|
|
All user-defined storage handlers have to provide a certain interface.
|
|
The functions of the user-defined storage handler will be called by the
|
|
core of the cache plugin. The necessary interface consists of seven
|
|
public functions. Both procedural and object oriented user-defined storage
|
|
handler must implement the same set of functions.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Using a user-defined storage handler</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* Enable default caching of all statements */
|
|
ini_set("mysqlnd_qc.cache_by_default", 1);
|
|
|
|
/* Procedural user defined storage handler functions */
|
|
|
|
$__cache = array();
|
|
|
|
function get_hash($host_info, $port, $user, $db, $query) {
|
|
global $__cache;
|
|
printf("\t%s(%d)\n", __FUNCTION__, func_num_args());
|
|
|
|
return md5(sprintf("%s%s%s%s%s", $host_info, $port, $user, $db, $query));
|
|
}
|
|
|
|
function find_query_in_cache($key) {
|
|
global $__cache;
|
|
printf("\t%s(%d)\n", __FUNCTION__, func_num_args());
|
|
|
|
if (isset($__cache[$key])) {
|
|
$tmp = $__cache[$key];
|
|
if ($tmp["valid_until"] < time()) {
|
|
unset($__cache[$key]);
|
|
$ret = NULL;
|
|
} else {
|
|
$ret = $__cache[$key]["data"];
|
|
}
|
|
} else {
|
|
$ret = NULL;
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
function return_to_cache($key) {
|
|
/*
|
|
Called on cache hit after cached data has been processed,
|
|
may be used for reference counting
|
|
*/
|
|
printf("\t%s(%d)\n", __FUNCTION__, func_num_args());
|
|
}
|
|
|
|
function add_query_to_cache_if_not_exists($key, $data, $ttl, $run_time, $store_time, $row_count) {
|
|
global $__cache;
|
|
printf("\t%s(%d)\n", __FUNCTION__, func_num_args());
|
|
|
|
$__cache[$key] = array(
|
|
"data" => $data,
|
|
"row_count" => $row_count,
|
|
"valid_until" => time() + $ttl,
|
|
"hits" => 0,
|
|
"run_time" => $run_time,
|
|
"store_time" => $store_time,
|
|
"cached_run_times" => array(),
|
|
"cached_store_times" => array(),
|
|
);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
function query_is_select($query) {
|
|
printf("\t%s('%s'): ", __FUNCTION__, $query);
|
|
|
|
$ret = FALSE;
|
|
if (stristr($query, "SELECT") !== FALSE) {
|
|
/* cache for 5 seconds */
|
|
$ret = 5;
|
|
}
|
|
|
|
printf("%s\n", (FALSE === $ret) ? "FALSE" : $ret);
|
|
return $ret;
|
|
}
|
|
|
|
function update_query_run_time_stats($key, $run_time, $store_time) {
|
|
global $__cache;
|
|
printf("\t%s(%d)\n", __FUNCTION__, func_num_args());
|
|
|
|
if (isset($__cache[$key])) {
|
|
$__cache[$key]['hits']++;
|
|
$__cache[$key]["cached_run_times"][] = $run_time;
|
|
$__cache[$key]["cached_store_times"][] = $store_time;
|
|
}
|
|
}
|
|
|
|
function get_stats($key = NULL) {
|
|
global $__cache;
|
|
printf("\t%s(%d)\n", __FUNCTION__, func_num_args());
|
|
|
|
if ($key && isset($__cache[$key])) {
|
|
$stats = $__cache[$key];
|
|
} else {
|
|
$stats = array();
|
|
foreach ($__cache as $key => $details) {
|
|
$stats[$key] = array(
|
|
'hits' => $details['hits'],
|
|
'bytes' => strlen($details['data']),
|
|
'uncached_run_time' => $details['run_time'],
|
|
'cached_run_time' => (count($details['cached_run_times']))
|
|
? array_sum($details['cached_run_times']) / count($details['cached_run_times'])
|
|
: 0,
|
|
);
|
|
}
|
|
}
|
|
|
|
return $stats;
|
|
}
|
|
|
|
function clear_cache() {
|
|
global $__cache;
|
|
printf("\t%s(%d)\n", __FUNCTION__, func_num_args());
|
|
|
|
$__cache = array();
|
|
return TRUE;
|
|
}
|
|
|
|
/* Install procedural user-defined storage handler */
|
|
if (!mysqlnd_qc_set_user_handlers("get_hash", "find_query_in_cache",
|
|
"return_to_cache", "add_query_to_cache_if_not_exists",
|
|
"query_is_select", "update_query_run_time_stats", "get_stats", "clear_cache")) {
|
|
|
|
printf("Failed to install user-defined storage handler\n");
|
|
}
|
|
|
|
|
|
/* Connect, create and populate test table */
|
|
$mysqli = new mysqli("host", "user", "password", "schema", "port", "socket");
|
|
$mysqli->query("DROP TABLE IF EXISTS test");
|
|
$mysqli->query("CREATE TABLE test(id INT)");
|
|
$mysqli->query("INSERT INTO test(id) VALUES (1), (2)");
|
|
|
|
printf("\nCache put/cache miss\n");
|
|
|
|
$res = $mysqli->query("SELECT id FROM test WHERE id = 1");
|
|
var_dump($res->fetch_assoc());
|
|
$res->free();
|
|
|
|
/* Delete record to verify we get our data from the cache */
|
|
$mysqli->query("DELETE FROM test WHERE id = 1");
|
|
|
|
printf("\nCache hit\n");
|
|
|
|
$res = $mysqli->query("SELECT id FROM test WHERE id = 1");
|
|
var_dump($res->fetch_assoc());
|
|
$res->free();
|
|
|
|
printf("\nDisplay cache statistics\n");
|
|
var_dump(mysqlnd_qc_get_cache_info());
|
|
|
|
printf("\nFlushing cache, cache put/cache miss");
|
|
var_dump(mysqlnd_qc_clear_cache());
|
|
|
|
$res = $mysqli->query("SELECT id FROM test WHERE id = 1");
|
|
var_dump($res->fetch_assoc());
|
|
$res->free();
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&examples.outputs.similar;
|
|
<screen>
|
|
<![CDATA[
|
|
query_is_select('DROP TABLE IF EXISTS test'): FALSE
|
|
query_is_select('CREATE TABLE test(id INT)'): FALSE
|
|
query_is_select('INSERT INTO test(id) VALUES (1), (2)'): FALSE
|
|
|
|
Cache put/cache miss
|
|
query_is_select('SELECT id FROM test WHERE id = 1'): 5
|
|
get_hash(5)
|
|
find_query_in_cache(1)
|
|
add_query_to_cache_if_not_exists(6)
|
|
array(1) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
}
|
|
query_is_select('DELETE FROM test WHERE id = 1'): FALSE
|
|
|
|
Cache hit
|
|
query_is_select('SELECT id FROM test WHERE id = 1'): 5
|
|
get_hash(5)
|
|
find_query_in_cache(1)
|
|
return_to_cache(1)
|
|
update_query_run_time_stats(3)
|
|
array(1) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
}
|
|
|
|
Display cache statistics
|
|
get_stats(0)
|
|
array(4) {
|
|
["num_entries"]=>
|
|
int(1)
|
|
["handler"]=>
|
|
string(4) "user"
|
|
["handler_version"]=>
|
|
string(5) "1.0.0"
|
|
["data"]=>
|
|
array(1) {
|
|
["18683c177dc89bb352b29965d112fdaa"]=>
|
|
array(4) {
|
|
["hits"]=>
|
|
int(1)
|
|
["bytes"]=>
|
|
int(71)
|
|
["uncached_run_time"]=>
|
|
int(398)
|
|
["cached_run_time"]=>
|
|
int(4)
|
|
}
|
|
}
|
|
}
|
|
|
|
Flushing cache, cache put/cache miss clear_cache(0)
|
|
bool(true)
|
|
query_is_select('SELECT id FROM test WHERE id = 1'): 5
|
|
get_hash(5)
|
|
find_query_in_cache(1)
|
|
add_query_to_cache_if_not_exists(6)
|
|
NULL
|
|
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
</section>
|
|
</chapter>
|
|
|
|
<!-- Keep this comment at the end of the file
|
|
Local variables:
|
|
mode: sgml
|
|
sgml-omittag:t
|
|
sgml-shorttag:t
|
|
sgml-minimize-attributes:nil
|
|
sgml-always-quote-attributes:t
|
|
sgml-indent-step:1
|
|
sgml-indent-data:t
|
|
indent-tabs-mode:nil
|
|
sgml-parent-document:nil
|
|
sgml-default-dtd-file:"~/.phpdoc/manual.ced"
|
|
sgml-exposed-tags:nil
|
|
sgml-local-catalogs:nil
|
|
sgml-local-ecat-files:nil
|
|
End:
|
|
vim600: syn=xml fen fdm=syntax fdl=2 si
|
|
vim: et tw=78 syn=sgml
|
|
vi: ts=1 sw=1
|
|
-->
|