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@332949 c90b9560-bf6c-de11-be94-00142212c4b1
2097 lines
74 KiB
XML
Executable file
2097 lines
74 KiB
XML
Executable file
<?xml version="1.0" encoding="utf-8"?>
|
|
<!-- $Revision$ -->
|
|
<chapter xml:id="mysqlnd-ms.quickstart" xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
<title>Quickstart and Examples</title>
|
|
<para>
|
|
The mysqlnd replication load balancing 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. The quickstart tries to avoid discussing theoretical concepts
|
|
and limitations. Instead, it will link to the reference sections. 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>
|
|
The focus is on using PECL mysqlnd_ms for work with an asynchronous MySQL cluster,
|
|
namely MySQL replication. Generally speaking an asynchronous cluster is more
|
|
difficult to use than a synchronous one. Thus, users of, for example, MySQL Cluster
|
|
will find more information than needed.
|
|
</para>
|
|
<section xml:id="mysqlnd-ms.quickstart.configuration">
|
|
<title>Setup</title>
|
|
<para>
|
|
The plugin is implemented as a PHP extension. See also the
|
|
<link linkend="mysqlnd-ms.installation">installation instructions</link> to
|
|
install the
|
|
<link xlink:href="&url.pecl.package;mysqlnd_ms">PECL/mysqlnd_ms</link> extension.
|
|
</para>
|
|
<para>
|
|
Compile or configure the PHP MySQL extension (API) (<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_ms
|
|
is a plugin for the mysqlnd library. To use the plugin with any of the PHP
|
|
MySQL extensions, 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-ms.enable">mysqlnd_ms.enable</link>.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Enabling the plugin (php.ini)</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
mysqlnd_ms.enable=1
|
|
mysqlnd_ms.config_file=/path/to/mysqlnd_ms_plugin.ini
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The plugin uses its own configuration file. Use the PHP
|
|
configuration directive
|
|
<link linkend="ini.mysqlnd-ms.config-file">mysqlnd_ms.config_file</link>
|
|
to set the full file path to the plugin-specific configuration file.
|
|
This file must be readable by PHP (e.g., the web server user).
|
|
Please note, the configuration directive <link linkend="ini.mysqlnd-ms.config-file">mysqlnd_ms.config_file</link>
|
|
superseeds <link linkend="ini.mysqlnd-ms.ini-file">mysqlnd_ms.ini_file</link>
|
|
since 1.4.0. It is a common pitfall to use the old, no longer available
|
|
configuration directive.
|
|
</para>
|
|
<para>
|
|
Create a plugin-specific configuration file. Save the file to the path
|
|
set by the PHP configuration directive
|
|
<link linkend="ini.mysqlnd-ms.config-file">mysqlnd_ms.config_file</link>.
|
|
</para>
|
|
<para>
|
|
The plugins <link linkend="mysqlnd-ms.plugin-ini-json">configuration file</link>
|
|
is <acronym>JSON</acronym> based. It is divided into one or more sections.
|
|
Each section has a name, for example, <literal>myapp</literal>. Every section
|
|
makes its own set of configuration settings.
|
|
</para>
|
|
<para>
|
|
A section must, at a minimum, list the MySQL replication master server, and set
|
|
a list of slaves. The plugin supports using only one master server per section.
|
|
Multi-master MySQL replication setups are not yet fully supported.
|
|
Use the configuration setting
|
|
<link linkend="ini.mysqlnd-ms-plugin-config-v2.master">master</link>
|
|
to set the hostname, and the port or socket of the MySQL master server.
|
|
MySQL slave servers are configured using the
|
|
<link linkend="ini.mysqlnd-ms-plugin-config-v2.slave">slave</link>
|
|
keyword.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Minimal plugin-specific configuration file (mysqlnd_ms_plugin.ini)</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost"
|
|
}
|
|
},
|
|
"slave": [
|
|
|
|
]
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
Configuring a MySQL slave server list is required, although it may
|
|
contain an empty list. It is recommended to always configure at
|
|
least one slave server.
|
|
</para>
|
|
<para>
|
|
Server lists can use <link linkend="mysqlnd-ms.plugin-ini-json.server-list-syntax">
|
|
anonymous or non-anonymous syntax</link>. Non-anonymous
|
|
lists include alias names for the servers, such as <literal>master_0</literal>
|
|
for the master in the above example. The quickstart uses the
|
|
more verbose non-anonymous syntax.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Recommended minimal plugin-specific config (mysqlnd_ms_plugin.ini)</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "192.168.2.27",
|
|
"port": "3306"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
If there are
|
|
at least two servers in total, the plugin can start to load balance and switch
|
|
connections. Switching connections is not always transparent and can cause
|
|
issues in certain cases. The reference sections about
|
|
<link linkend="mysqlnd-ms.pooling">connection pooling and switching</link>,
|
|
<link linkend="mysqlnd-ms.transaction">transaction handling</link>,
|
|
<link linkend="mysqlnd-ms.failover">fail over</link>
|
|
<link linkend="mysqlnd-ms.loadbalancing">load balancing</link> and
|
|
<link linkend="mysqlnd-ms.rwsplit">read-write splitting</link> all provide
|
|
more details. And potential pitfalls are described later in this guide.
|
|
</para>
|
|
<para>
|
|
It is the responsibility of the application to handle potential issues caused
|
|
by connection switches, by configuring a master with at least one slave
|
|
server, which allows switching to work therefore related problems can be found.
|
|
</para>
|
|
<para>
|
|
The MySQL master and MySQL slave servers, which you configure, do not need to
|
|
be part of MySQL replication setup. For testing purpose you can use single
|
|
MySQL server and make it known to the plugin as a master and slave server
|
|
as shown below. This could help you to detect many potential issues with
|
|
connection switches. However, such a setup will not be prone to the issues
|
|
caused by replication lag.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Using one server as a master and as a slave (testing only!)</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "127.0.0.1",
|
|
"port": "3306"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The plugin attempts to notify you of invalid configurations. Since 1.5.0 it
|
|
will throw a warning during PHP startup if the configuration file cannot be read,
|
|
is empty or parsing the JSON failed. Depending on your PHP settings those
|
|
errors may appear in some log files only. Further validation is done when a connection
|
|
is to be established and the configuration file is searched for valid sections.
|
|
Setting <link linkend="ini.mysqlnd-ms.force-config-usage">mysqlnd_ms.force_config_usage</link>
|
|
may help debugging a faulty setup. Please, see also
|
|
<link linkend="mysqlnd-ms.plugin-ini-json.debug_config">configuration file debugging notes</link>.
|
|
</para>
|
|
|
|
</section>
|
|
|
|
<section xml:id="mysqlnd-ms.quickstart.usage">
|
|
<title>Running statements</title>
|
|
<para>
|
|
The plugin can be used with any PHP MySQL extension
|
|
(<link linkend="ref.mysqli">mysqli</link>,
|
|
<link linkend="ref.mysql">mysql</link>, and
|
|
<link linkend="ref.pdo-mysql">PDO_MYSQL</link>) that is
|
|
compiled to use the <link linkend="book.mysqlnd">mysqlnd</link> library.
|
|
PECL/mysqlnd_ms plugs into the <link linkend="book.mysqlnd">mysqlnd</link> library.
|
|
It does not change the API or behavior of those extensions.
|
|
</para>
|
|
<para>
|
|
Whenever a connection to MySQL is being opened, the plugin compares the host
|
|
parameter value of the connect call, with the section names
|
|
from the plugin specific configuration file. If, for example, the
|
|
plugin specific configuration file has a section <literal>myapp</literal> then
|
|
the section should be referenced by opening a MySQL connection to the
|
|
host <literal>myapp</literal>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Plugin specific configuration file (mysqlnd_ms_plugin.ini)</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "192.168.2.27",
|
|
"port": "3306"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Opening a load balanced connection</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* Load balanced following "myapp" section rules from the plugins config file */
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
$pdo = new PDO('mysql:host=myapp;dbname=database', 'username', 'password');
|
|
$mysql = mysql_connect("myapp", "username", "password");
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The connection examples above will be load balanced.
|
|
The plugin will send read-only statements to the MySQL slave server with the
|
|
IP <literal>192.168.2.27</literal> and will listen on port <literal>3306</literal>
|
|
for the MySQL client connection. All other statements will be directed to the
|
|
MySQL master server running on the host <literal>localhost</literal>. If on Unix like
|
|
operating systems, the master on <literal>localhost</literal> will be accepting
|
|
MySQL client connections on the Unix domain socket <literal>/tmp/mysql.sock</literal>,
|
|
while TCP/IP is the default port on Windows.
|
|
The plugin will use the user name <literal>username</literal> and the password
|
|
<literal>password</literal> to connect to any of the MySQL servers listed in
|
|
the section <literal>myapp</literal> of the plugins configuration file. Upon
|
|
connect, the plugin will select <literal>database</literal> as the current
|
|
schemata.
|
|
</para>
|
|
<para>
|
|
The username, password and schema name are taken from the connect
|
|
API calls and used for all servers. In other words: you must use the same
|
|
username and password for every MySQL server listed in a plugin configuration
|
|
file section. The is not a general limitation. As of PECL/mysqlnd_ms 1.1.0,
|
|
it is possible to set the
|
|
<link linkend="mysqlnd-ms.plugin-ini-json.server-config-keywords">username</link> and
|
|
<link linkend="mysqlnd-ms.plugin-ini-json.server-config-keywords">password</link> for any server in the
|
|
plugins configuration file, to be used instead of the credentials passed
|
|
to the API call.
|
|
</para>
|
|
<para>
|
|
The plugin does not change the API for running statements.
|
|
<link linkend="mysqlnd-ms.rwsplit">Read-write splitting</link>
|
|
works out of the box. The following example assumes that there is no
|
|
significant replication lag between the master and the slave.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Executing statements</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
/* Load balanced following "myapp" section rules from the plugins config file */
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (mysqli_connect_errno())
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* Statements will be run on the master */
|
|
if (!$mysqli->query("DROP TABLE IF EXISTS test")) {
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
}
|
|
if (!$mysqli->query("CREATE TABLE test(id INT)")) {
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
}
|
|
if (!$mysqli->query("INSERT INTO test(id) VALUES (1)")) {
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
}
|
|
|
|
/* read-only: statement will be run on a slave */
|
|
if (!($res = $mysqli->query("SELECT id FROM test")) {
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
} else {
|
|
$row = $res->fetch_assoc();
|
|
$res->close();
|
|
printf("Slave returns id = '%s'\n", $row['id'];
|
|
}
|
|
$mysqli->close();
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&example.outputs.similar;
|
|
<screen>
|
|
<![CDATA[
|
|
Slave returns id = '1'
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
</section>
|
|
|
|
<section xml:id="mysqlnd-ms.quickstart.connectionpooling">
|
|
<title>Connection state</title>
|
|
<para>
|
|
The plugin changes the semantics of a PHP MySQL connection handle.
|
|
A new connection handle represents a connection pool, instead of a
|
|
single MySQL client-server network connection. The connection pool consists
|
|
of a master connection, and optionally any number of slave connections.
|
|
</para>
|
|
<para>
|
|
Every connection from the connection pool has its own state. For example,
|
|
SQL user variables, temporary tables and transactions are part of the state.
|
|
For a complete list of items that belong to the state of a connection, see the
|
|
<link linkend="mysqlnd-ms.pooling">connection pooling and switching</link>
|
|
concepts documentation.
|
|
If the plugin decides to switch connections for load balancing, the
|
|
application could be given a connection which has a different state.
|
|
Applications must be made aware of this.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Plugin config with one slave and one master</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "192.168.2.27",
|
|
"port": "3306"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Pitfall: connection state and SQL user variables</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* Connection 1, connection bound SQL user variable, no SELECT thus run on master */
|
|
if (!$mysqli->query("SET @myrole='master'")) {
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
}
|
|
|
|
/* Connection 2, run on slave because SELECT */
|
|
if (!($res = $mysqli->query("SELECT @myrole AS _role"))) {
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
} else {
|
|
$row = $res->fetch_assoc();
|
|
$res->close();
|
|
printf("@myrole = '%s'\n", $row['_role']);
|
|
}
|
|
$mysqli->close();
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&example.outputs;
|
|
<screen>
|
|
<![CDATA[
|
|
@myrole = ''
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The example opens a load balanced connection and executes two statements.
|
|
The first statement <literal>SET @myrole='master'</literal> does not begin
|
|
with the string <literal>SELECT</literal>. Therefore the plugin does not
|
|
recognize it as a read-only query which shall be run on a slave. The
|
|
plugin runs the statement on the connection to the master. The statement
|
|
sets a SQL user variable which is bound to the master connection. The
|
|
state of the master connection has been changed.
|
|
</para>
|
|
<para>
|
|
The next statement is <literal>SELECT @myrole AS _role</literal>.
|
|
The plugin does recognize it as a read-only query and sends it to
|
|
the slave. The statement is run on a connection to the slave. This
|
|
second connection does not have any SQL user variables bound to it.
|
|
It has a different state than the first connection to the master.
|
|
The requested SQL user variable is not set. The example script prints
|
|
<literal>@myrole = ''</literal>.
|
|
</para>
|
|
<para>
|
|
It is the responsibility of the application developer to take care
|
|
of the connection state. The plugin does not monitor all
|
|
connection state changing activities. Monitoring all possible cases would
|
|
be a very CPU intensive task, if it could be done at all.
|
|
</para>
|
|
<para>
|
|
The pitfalls can easily be worked around using SQL hints.
|
|
</para>
|
|
</section>
|
|
|
|
<section xml:id="mysqlnd-ms.quickstart.sqlhints">
|
|
<title>SQL Hints</title>
|
|
<para>
|
|
SQL hints can force a query to choose a specific server from the connection pool.
|
|
It gives the plugin a hint to use a designated server, which can solve
|
|
issues caused by connection switches and connection state.
|
|
</para>
|
|
<para>
|
|
SQL hints are standard compliant SQL comments. Because
|
|
SQL comments are supposed to be ignored by SQL processing systems, they
|
|
do not interfere with other programs such as the MySQL Server, the MySQL Proxy,
|
|
or a firewall.
|
|
</para>
|
|
<para>
|
|
Three SQL hints are supported by the plugin: The
|
|
<constant>MYSQLND_MS_MASTER_SWITCH</constant> hint makes the plugin run a
|
|
statement on the master, <constant>MYSQLND_MS_SLAVE_SWITCH</constant>
|
|
enforces the use of the slave, and
|
|
<constant>MYSQLND_MS_LAST_USED_SWITCH</constant> will run a statement on
|
|
the same server that was used for the previous statement.
|
|
</para>
|
|
<para>
|
|
The plugin scans the beginning of a statement for the existence of an SQL
|
|
hint. SQL hints are only recognized if they appear at the beginning of
|
|
the statement.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Plugin config with one slave and one master</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "192.168.2.27",
|
|
"port": "3306"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>SQL hints to prevent connection switches</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (mysqli_connect_errno())
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* Connection 1, connection bound SQL user variable, no SELECT thus run on master */
|
|
if (!$mysqli->query("SET @myrole='master'")) {
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
}
|
|
|
|
/* Connection 1, run on master because of SQL hint */
|
|
if (!($res = $mysqli->query(sprintf("/*%s*/SELECT @myrole AS _role", MYSQLND_MS_LAST_USED_SWITCH)))) {
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
} else {
|
|
$row = $res->fetch_assoc();
|
|
$res->close();
|
|
printf("@myrole = '%s'\n", $row['_role']);
|
|
}
|
|
$mysqli->close();
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&example.outputs;
|
|
<screen>
|
|
<![CDATA[
|
|
@myrole = 'master'
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
In the above example, using <constant>MYSQLND_MS_LAST_USED_SWITCH</constant> prevents
|
|
session switching from the master to a slave when running the <literal>SELECT</literal>
|
|
statement.
|
|
</para>
|
|
<para>
|
|
SQL hints can also be used to run <literal>SELECT</literal> statements
|
|
on the MySQL master server. This may be desired if the MySQL slave servers
|
|
are typically behind the master, but you need current data from the cluster.
|
|
</para>
|
|
<para>
|
|
In version 1.2.0 the concept of a service level has been introduced to address
|
|
cases when current data is required. Using a service level requires less attention
|
|
and removes the need of using SQL hints for this use case. Please, find more
|
|
information below in the service level and consistency section.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Fighting replication lag</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* Force use of master, master has always fresh and current data */
|
|
if (!$mysqli->query(sprintf("/*%s*/SELECT critical_data FROM important_table", MYSQLND_MS_MASTER_SWITCH))) {
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
}
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
A use case may include the creation of tables on a slave.
|
|
If an SQL hint is not given, then the plugin will send <literal>CREATE</literal>
|
|
and <literal>INSERT</literal> statements to the master. Use the
|
|
SQL hint <constant>MYSQLND_MS_SLAVE_SWITCH</constant> if you want to
|
|
run any such statement on a slave, for example, to build temporary
|
|
reporting tables.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Table creation on a slave</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* Force use of slave */
|
|
if (!$mysqli->query(sprintf("/*%s*/CREATE TABLE slave_reporting(id INT)", MYSQLND_MS_SLAVE_SWITCH))) {
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
}
|
|
/* Continue using this particular slave connection */
|
|
if (!$mysqli->query(sprintf("/*%s*/INSERT INTO slave_reporting(id) VALUES (1), (2), (3)", MYSQLND_MS_LAST_USED_SWITCH))) {
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
}
|
|
/* Don't use MYSQLND_MS_SLAVE_SWITCH which would allow switching to another slave! */
|
|
if ($res = $mysqli->query(sprintf("/*%s*/SELECT COUNT(*) AS _num FROM slave_reporting", MYSQLND_MS_LAST_USED_SWITCH))) {
|
|
$row = $res->fetch_assoc();
|
|
$res->close();
|
|
printf("There are %d rows in the table 'slave_reporting'", $row['_num']);
|
|
} else {
|
|
printf("[%d] %s\n", $mysqli->errno, $mysqli->error);
|
|
}
|
|
$mysqli->close();
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The SQL hint <constant>MYSQLND_MS_LAST_USED</constant> forbids switching a
|
|
connection, and forces use of the previously used connection.
|
|
</para>
|
|
</section>
|
|
|
|
<section xml:id="mysqlnd-ms.quickstart.transactions">
|
|
<title>Transactions</title>
|
|
<para>
|
|
The current version of the plugin is not transaction safe by default,
|
|
because it is not aware of running transactions in all cases. SQL transactions are
|
|
units of work to be run on a single server. The plugin does not always know
|
|
when the unit of work starts and when it ends. Therefore, the plugin may
|
|
decide to switch connections in the middle of a transaction.
|
|
</para>
|
|
<para>
|
|
No kind of MySQL load balancer can detect transaction boundaries without any
|
|
kind of hint from the application.
|
|
</para>
|
|
<para>
|
|
You can either use SQL hints to work around this limitation. Alternatively,
|
|
you can activate transaction API call monitoring. In the latter case you
|
|
must use API calls only to control transactions, see below.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Plugin config with one slave and one master</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
[myapp]
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "192.168.2.27",
|
|
"port": "3306"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Using SQL hints for transactions</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* Not a SELECT, will use master */
|
|
if (!$mysqli->query("START TRANSACTION")) {
|
|
/* Please use better error handling in your code */
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
|
|
/* Prevent connection switch! */
|
|
if (!$mysqli->query(sprintf("/*%s*/INSERT INTO test(id) VALUES (1)", MYSQLND_MS_LAST_USED_SWITCH)))) {
|
|
/* Please do proper ROLLBACK in your code, don't just die */
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
if ($res = $mysqli->query(sprintf("/*%s*/SELECT COUNT(*) AS _num FROM test", MYSQLND_MS_LAST_USED_SWITCH)))) {
|
|
$row = $res->fetch_assoc();
|
|
$res->close();
|
|
if ($row['_num'] > 1000) {
|
|
if (!$mysqli->query(sprintf("/*%s*/INSERT INTO events(task) VALUES ('cleanup')", MYSQLND_MS_LAST_USED_SWITCH)))) {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
}
|
|
} else {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
if (!$mysqli->query(sprintf("/*%s*/UPDATE log SET last_update = NOW()", MYSQLND_MS_LAST_USED_SWITCH)))) {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
if (!$mysqli->query(sprintf("/*%s*/COMMIT", MYSQLND_MS_LAST_USED_SWITCH)))) {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
|
|
$mysqli->close();
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
Starting with PHP 5.4.0, the <literal>mysqlnd</literal> library allows the
|
|
plugin to monitor the status of the <literal>autocommit</literal> mode, if
|
|
the mode is set by API calls instead of using SQL statements such as
|
|
<literal>SET AUTOCOMMIT=0</literal>. This makes it possible for the plugin to
|
|
become transaction aware. In this case, you do not need to use SQL hints.
|
|
</para>
|
|
<para>
|
|
If using PHP 5.4.0 or newer, API calls that enable <literal>autocommit</literal> mode,
|
|
and when setting the plugin configuration option
|
|
<link linkend="ini.mysqlnd-ms-plugin-config-v2.trx-stickiness">trx_stickiness=master</link>,
|
|
the plugin can automatically disable load balancing and connection switches
|
|
for SQL transactions. In this configuration, the plugin stops load balancing
|
|
if <literal>autocommit</literal> is disabled and directs all statements to
|
|
the master. This prevents connection switches in the middle of
|
|
a transaction. Once <literal>autocommit</literal> is re-enabled, the plugin
|
|
starts to load balance statements again.
|
|
</para>
|
|
<para>
|
|
API based transaction boundary detection has been improved with PHP 5.5.0 and
|
|
PECL/mysqlnd_ms 1.5.0 to cover not only calls to <function>mysqli_autocommit</function>
|
|
but also <function>mysqli_begin</function>,
|
|
<function>mysqli_commit</function> and <function>mysqli_rollback</function>.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Transaction aware load balancing: trx_stickiness setting</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "127.0.0.1",
|
|
"port": "3306"
|
|
}
|
|
},
|
|
"trx_stickiness": "master"
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Transaction aware</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* Disable autocommit, plugin will run all statements on the master */
|
|
$mysqli->autocommit(FALSE);
|
|
|
|
if (!$mysqli->query("INSERT INTO test(id) VALUES (1)")) {
|
|
/* Please do proper ROLLBACK in your code, don't just die */
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
if ($res = $mysqli->query("SELECT COUNT(*) AS _num FROM test")) {
|
|
$row = $res->fetch_assoc();
|
|
$res->close();
|
|
if ($row['_num'] > 1000) {
|
|
if (!$mysqli->query("INSERT INTO events(task) VALUES ('cleanup')")) {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
}
|
|
} else {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
if (!$mysqli->query("UPDATE log SET last_update = NOW()")) {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
if (!$mysqli->commit()) {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
|
|
/* Plugin assumes that the transaction has ended and starts load balancing again */
|
|
$mysqli->autocommit(TRUE);
|
|
$mysqli->close();
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<note>
|
|
<title>Version requirement</title>
|
|
<para>
|
|
The plugin configuration option
|
|
<link linkend="ini.mysqlnd-ms-plugin-config-v2.trx-stickiness">trx_stickiness=master</link>
|
|
requires PHP 5.4.0 or newer.
|
|
</para>
|
|
</note>
|
|
<para>
|
|
Please note the restrictions outlined in the
|
|
<link linkend="mysqlnd-ms.transaction">transaction handling</link> concepts
|
|
section.
|
|
</para>
|
|
</section>
|
|
<section xml:id="mysqlnd-ms.quickstart.qos-consistency">
|
|
<title>Service level and consistency</title>
|
|
<note>
|
|
<title>Version requirement</title>
|
|
<para>
|
|
Service levels have been introduced in PECL mysqlnd_ms version 1.2.0-alpha.
|
|
<function>mysqlnd_ms_set_qos</function>
|
|
is available with PHP 5.4.0 or newer.
|
|
</para>
|
|
</note>
|
|
<para>
|
|
Different types of MySQL cluster solutions offer different service and
|
|
data consistency levels to their users. An asynchronous MySQL replication cluster
|
|
offers eventual consistency by default. A read executed on an asynchronous slave
|
|
may return current, stale or no data at all, depending on whether the slave
|
|
has replayed all changesets from the master or not.
|
|
</para>
|
|
<para>
|
|
Applications using an MySQL replication cluster need to be designed to work
|
|
correctly with eventual consistent data. In some cases, however, stale data
|
|
is not acceptable. In those cases only certain slaves or even only master accesses are
|
|
allowed to achieve the required quality of service from the cluster.
|
|
</para>
|
|
<para>
|
|
As of PECL mysqlnd_ms 1.2.0 the plugin is capable of selecting
|
|
MySQL replication nodes automatically that deliver session consistency or
|
|
strong consistency. Session consistency means that one client can read its writes.
|
|
Other clients may or may not see the clients' write. Strong consistency means
|
|
that all clients will see all writes from the client.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Session consistency: read your writes</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "127.0.0.1",
|
|
"port": "3306"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Requesting session consistency</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* read-write splitting: master used */
|
|
if (!$mysqli->query("INSERT INTO orders(order_id, item) VALUES (1, 'christmas tree, 1.8m')")) {
|
|
/* Please use better error handling in your code */
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
|
|
/* Request session consistency: read your writes */
|
|
if (!mysqlnd_ms_set_qos($mysqli, MYSQLND_MS_QOS_CONSISTENCY_SESSION))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* Plugin picks a node which has the changes, here: master */
|
|
if (!$res = $mysqli->query("SELECT item FROM orders WHERE order_id = 1"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
var_dump($res->fetch_assoc());
|
|
|
|
/* Back to eventual consistency: stale data allowed */
|
|
if (!mysqlnd_ms_set_qos($mysqli, MYSQLND_MS_QOS_CONSISTENCY_EVENTUAL))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* Plugin picks any slave, stale data is allowed */
|
|
if (!$res = $mysqli->query("SELECT item, price FROM specials"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
Service levels can be set in the plugins configuration file and at runtime
|
|
using <function>mysqlnd_ms_set_qos</function>.
|
|
In the example the function is used to enforce
|
|
session consistency (read your writes) for all future statements until further notice.
|
|
The <literal>SELECT</literal> statement on the <literal>orders</literal> table
|
|
is run on the master to ensure the previous write can be seen by the client.
|
|
Read-write splitting logic has been adapted to fulfill the service level.
|
|
</para>
|
|
<para>
|
|
After the application has read its changes from the <literal>orders</literal> table
|
|
it returns to the default service level, which is eventual consistency. Eventual
|
|
consistency puts no restrictions on choosing a node for statement execution.
|
|
Thus, the <literal>SELECT</literal> statement on the <literal>specials</literal>
|
|
table is executed on a slave.
|
|
</para>
|
|
<para>
|
|
The new functionality supersedes the use of SQL hints and the
|
|
<literal>master_on_write</literal> configuration option. In many cases
|
|
<function>mysqlnd_ms_set_qos</function> is easier to use, more powerful
|
|
improves portability.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Maximum age/slave lag</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "127.0.0.1",
|
|
"port": "3306"
|
|
}
|
|
},
|
|
"failover" : "master"
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Limiting slave lag</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* Read from slaves lagging no more than four seconds */
|
|
$ret = mysqlnd_ms_set_qos($mysqli,
|
|
MYSQLND_MS_QOS_CONSISTENCY_EVENTUAL,
|
|
MYSQLND_MS_QOS_OPTION_AGE, 4);
|
|
|
|
if (!$ret)
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* Plugin picks any slave, which may or may not have the changes */
|
|
if (!$res = $mysqli->query("SELECT item, price FROM daytrade"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
|
|
/* Back to default: use of all slaves and masters permitted */
|
|
if (!mysqlnd_ms_set_qos($mysqli, MYSQLND_MS_QOS_CONSISTENCY_EVENTUAL))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The eventual consistency service level can be used with an optional
|
|
parameter to set a maximum slave lag for choosing slaves. If set,
|
|
the plugin checks <literal>SHOW SLAVE STATUS</literal> for all
|
|
configured slaves. In case of the example, only slaves
|
|
for which <literal>Slave_IO_Running=Yes</literal>,
|
|
<literal>Slave_SQL_Running=Yes</literal> and
|
|
<literal>Seconds_Behind_Master <= 4</literal>
|
|
is true are considered for executing the statement
|
|
<literal>SELECT item, price FROM daytrade</literal>.
|
|
</para>
|
|
<para>
|
|
Checking <literal>SHOW SLAVE STATUS</literal> is done transparently from
|
|
an applications perspective. Errors, if any, are reported as
|
|
warnings. No error will be set on the connection handle. Even if all
|
|
<literal>SHOW SLAVE STATUS</literal> SQL statements executed by
|
|
the plugin fail, the execution of the users statement is not stopped, given
|
|
that master fail over is enabled. Thus, no application changes are required.
|
|
</para>
|
|
<note>
|
|
<title>Expensive and slow operation</title>
|
|
<para>
|
|
Checking <literal>SHOW SLAVE STATUS</literal> for all slaves adds overhead
|
|
to the application. It is an expensive and slow background operation.
|
|
Try to minimize the use of it. Unfortunately, a MySQL replication cluster
|
|
does not give clients the possibility to request a list of candidates
|
|
from a central instance.
|
|
Thus, a more efficient way of checking the slaves lag is not available.
|
|
</para>
|
|
<para>
|
|
Please, note the limitations and properties of <literal>SHOW SLAVE STATUS</literal>
|
|
as explained in the MySQL reference manual.
|
|
</para>
|
|
</note>
|
|
<para>
|
|
To prevent mysqlnd_ms from emitting a warning if no slaves can be found
|
|
that lag no more than the defined number of seconds behind the master,
|
|
it is necessary to enable master fail over in the plugins configuration file.
|
|
If no slaves can be found and fail over is turned on, the plugin
|
|
picks a master for executing the statement.
|
|
</para>
|
|
<para>
|
|
If no slave can be found and fail over is turned off, the plugin emits
|
|
a warning, it does not execute the statement and it sets an error
|
|
on the connection.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Fail over not set</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "127.0.0.1",
|
|
"port": "3306"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>No slave within time limit</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* Read from slaves lagging no more than four seconds */
|
|
$ret = mysqlnd_ms_set_qos($mysqli,
|
|
MYSQLND_MS_QOS_CONSISTENCY_EVENTUAL,
|
|
MYSQLND_MS_QOS_OPTION_AGE, 4);
|
|
|
|
if (!$ret)
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* Plugin picks any slave, which may or may not have the changes */
|
|
if (!$res = $mysqli->query("SELECT item, price FROM daytrade"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
|
|
/* Back to default: use of all slaves and masters permitted */
|
|
if (!mysqlnd_ms_set_qos($mysqli, MYSQLND_MS_QOS_CONSISTENCY_EVENTUAL))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&example.outputs;
|
|
<screen>
|
|
<![CDATA[
|
|
PHP Warning: mysqli::query(): (mysqlnd_ms) Couldn't find the appropriate slave connection. 0 slaves to choose from. Something is wrong in %s on line %d
|
|
PHP Warning: mysqli::query(): (mysqlnd_ms) No connection selected by the last filter in %s on line %d
|
|
[2000] (mysqlnd_ms) No connection selected by the last filter
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
</section>
|
|
<section xml:id="mysqlnd-ms.quickstart.gtid">
|
|
<title>Global transaction IDs</title>
|
|
<note>
|
|
<title>Version requirement</title>
|
|
<para>
|
|
A client-side global transaction ID injection has been introduced in mysqlnd_ms version 1.2.0-alpha.
|
|
The feature is not required for synchronous clusters, such as MySQL Cluster.
|
|
Use it with asynchronous clusters such as classical MySQL replication.
|
|
</para>
|
|
<para>
|
|
As of MySQL 5.6.5-m8 release candidate the MySQL server features built-in global transaction identifiers.
|
|
The MySQL built-in global transaction ID feature is supported by PECL/mysqlnd_ms 1.3.0-alpha or
|
|
later. However, the final feature set found in MySQL 5.6 production releases to date is not
|
|
sufficient to support the ideas discussed below in all cases. Please, see also the
|
|
<link linkend="mysqlnd-ms.gtid">concepts section</link>.
|
|
</para>
|
|
</note>
|
|
<para>
|
|
PECL/mysqlnd_ms can either use its own global transaction ID emulation or the
|
|
global transaction ID feature built-in to MySQL 5.6.5-m8 or later. From a developer
|
|
perspective the client-side and server-side approach offer the same features with
|
|
regards to service levels provided by PECL/mysqlnd_ms. Their differences
|
|
are discussed in the <link linkend="mysqlnd-ms.gtid">concepts section</link>.
|
|
</para>
|
|
<para>
|
|
The quickstart first demonstrates the use of the client-side global transaction ID emulation
|
|
built-in to PECL/mysqlnd_ms before its show how to use the server-side counterpart.
|
|
The order ensures that the underlying idea is discussed first.
|
|
</para>
|
|
<para>
|
|
<emphasis role="bold">Idea and client-side emulation</emphasis>
|
|
</para>
|
|
<para>
|
|
In its most basic form a global transaction ID (GTID) is a counter in a table on the
|
|
master. The counter is incremented whenever a transaction is committed on the master.
|
|
Slaves replicate the table. The counter serves two purposes. In case of a
|
|
master failure, it helps the database administrator to identify the most recent slave
|
|
for promoting it to the new master. The most recent slave is the one with the
|
|
highest counter value. Applications can use the global transaction ID to search
|
|
for slaves which have replicated a certain write (identified by a global transaction ID)
|
|
already.
|
|
</para>
|
|
<para>
|
|
PECL/mysqlnd_ms can inject SQL for every committed transaction to increment a GTID counter.
|
|
The so created GTID is accessible by the application to identify an applications
|
|
write operation. This enables the plugin to deliver session consistency (read your writes)
|
|
service level by not only querying masters but also slaves which have replicated
|
|
the change already. Read load is taken away from the master.
|
|
</para>
|
|
<para>
|
|
Client-side global transaction ID emulation has some limitations. Please,
|
|
read the <link linkend="mysqlnd-ms.gtid">concepts section</link>
|
|
carefully to fully understand the principles and ideas
|
|
behind it, before using in production environments. The background knowledge
|
|
is not required to continue with the quickstart.
|
|
</para>
|
|
<para>
|
|
First, create a counter table on your master server and insert a record into it.
|
|
The plugin does not assist creating the table.
|
|
Database administrators must make sure it exists. Depending on the error
|
|
reporting mode, the plugin will silently ignore the lack of the table or bail out.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Create counter table on master</title>
|
|
<programlisting role="sql">
|
|
<![CDATA[
|
|
CREATE TABLE `trx` (
|
|
`trx_id` int(11) DEFAULT NULL,
|
|
`last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
) ENGINE=InnoDB DEFAULT CHARSET=latin1
|
|
INSERT INTO `trx`(`trx_id`) VALUES (1);
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
In the plugins configuration file set the SQL to update the
|
|
global transaction ID table using <literal>on_commit</literal>
|
|
from the <literal>global_transaction_id_injection</literal>
|
|
section. Make sure the table name used for the <literal>UPDATE</literal>
|
|
statement is fully qualified. In the example,
|
|
<literal>test.trx</literal> is used to refer to table <literal>trx</literal>
|
|
in the schema (database) <literal>test</literal>. Use the table that was created in
|
|
the previous step. It is important to set the fully qualified table name
|
|
because the connection on which the injection is done may use a different
|
|
default database. Make sure the user that opens the connection
|
|
is allowed to execute the <literal>UPDATE</literal>.
|
|
</para>
|
|
<para>
|
|
Enable reporting of errors that may occur when mysqlnd_ms does global
|
|
transaction ID injection.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Plugin config: SQL for client-side GTID injection</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "127.0.0.1",
|
|
"port": "3306"
|
|
}
|
|
},
|
|
"global_transaction_id_injection":{
|
|
"on_commit":"UPDATE test.trx SET trx_id = trx_id + 1",
|
|
"report_error":true
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Transparent global transaction ID injection</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* auto commit mode, transaction on master, GTID must be incremented */
|
|
if (!$mysqli->query("DROP TABLE IF EXISTS test"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* auto commit mode, transaction on master, GTID must be incremented */
|
|
if (!$mysqli->query("CREATE TABLE test(id INT)"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* auto commit mode, transaction on master, GTID must be incremented */
|
|
if (!$mysqli->query("INSERT INTO test(id) VALUES (1)"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* auto commit mode, read on slave, no increment */
|
|
if (!($res = $mysqli->query("SELECT id FROM test")))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
var_dump($res->fetch_assoc());
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&example.outputs;
|
|
<screen>
|
|
<![CDATA[
|
|
array(1) {
|
|
["id"]=>
|
|
string(1) "1"
|
|
}
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The example runs three statements in auto commit mode on the master, causing
|
|
three transactions on the master. For every such statement, the plugin will
|
|
inject the configured <literal>UPDATE</literal> transparently before executing
|
|
the users SQL statement. When the script ends the global
|
|
transaction ID counter on the master has been incremented by three.
|
|
</para>
|
|
<para>
|
|
The fourth SQL statement executed in the example, a <literal>SELECT</literal>,
|
|
does not trigger an increment. Only transactions (writes) executed on a master
|
|
shall increment the GTID counter.
|
|
</para>
|
|
<note>
|
|
<title>SQL for global transaction ID: efficient solution wanted!</title>
|
|
<para>
|
|
The SQL used for the client-side global transaction ID emulation is inefficient.
|
|
It is optimized for clearity not for performance. Do not use it for production
|
|
environments. Please, help finding an efficient solution for inclusion in the manual.
|
|
We appreciate your input.
|
|
</para>
|
|
</note>
|
|
<para>
|
|
<example>
|
|
<title>Plugin config: SQL for fetching GTID</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "127.0.0.1",
|
|
"port": "3306"
|
|
}
|
|
},
|
|
"global_transaction_id_injection":{
|
|
"on_commit":"UPDATE test.trx SET trx_id = trx_id + 1",
|
|
"fetch_last_gtid" : "SELECT MAX(trx_id) FROM test.trx",
|
|
"report_error":true
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Obtaining GTID after injection</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* auto commit mode, transaction on master, GTID must be incremented */
|
|
if (!$mysqli->query("DROP TABLE IF EXISTS test"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
printf("GTID after transaction %s\n", mysqlnd_ms_get_last_gtid($mysqli));
|
|
|
|
/* auto commit mode, transaction on master, GTID must be incremented */
|
|
if (!$mysqli->query("CREATE TABLE test(id INT)"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
printf("GTID after transaction %s\n", mysqlnd_ms_get_last_gtid($mysqli));
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
&example.outputs;
|
|
<screen>
|
|
<![CDATA[
|
|
GTID after transaction 7
|
|
GTID after transaction 8
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
Applications can ask PECL mysqlnd_ms for a global transaction ID which
|
|
belongs to the last write operation performed by the application.
|
|
The function <function>mysqlnd_ms_get_last_gtid</function> returns the
|
|
GTID obtained when executing the SQL statement from
|
|
the <literal>fetch_last_gtid</literal> entry of the
|
|
<literal>global_transaction_id_injection</literal> section from
|
|
the plugins configuration file. The function may be called
|
|
after the GTID has been incremented.
|
|
</para>
|
|
<para>
|
|
Applications are adviced not to run the SQL
|
|
statement themselves as this bares the risk of accidently causing an implicit
|
|
GTID increment. Also, if the function is used, it is easy to migrate
|
|
an application from one SQL statement for fetching a transaction ID to another,
|
|
for example, if any MySQL server ever features built-in global transaction ID support.
|
|
</para>
|
|
<para>
|
|
The quickstart shows a SQL statement which will return a GTID equal or greater
|
|
to that created for the previous statement. It is exactly the GTID created
|
|
for the previous statement if no other clients have incremented the GTID in the
|
|
time span between the statement execution and the <literal>SELECT</literal>
|
|
to fetch the GTID. Otherwise, it is greater.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Plugin config: Checking for a certain GTID</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "127.0.0.1",
|
|
"port": "3306"
|
|
}
|
|
},
|
|
"global_transaction_id_injection":{
|
|
"on_commit":"UPDATE test.trx SET trx_id = trx_id + 1",
|
|
"fetch_last_gtid" : "SELECT MAX(trx_id) FROM test.trx",
|
|
"check_for_gtid" : "SELECT trx_id FROM test.trx WHERE trx_id >= #GTID",
|
|
"report_error":true
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Session consistency service level and GTID combined</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* auto commit mode, transaction on master, GTID must be incremented */
|
|
if (!$mysqli->query("DROP TABLE IF EXISTS test") ||
|
|
!$mysqli->query("CREATE TABLE test(id INT)") ||
|
|
!$mysqli->query("INSERT INTO test(id) VALUES (1)"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* GTID as an identifier for the last write */
|
|
$gtid = mysqlnd_ms_get_last_gtid($mysqli);
|
|
|
|
/* Session consistency (read your writes): try to read from slaves not only master */
|
|
if (false == mysqlnd_ms_set_qos($mysqli, MYSQLND_MS_QOS_CONSISTENCY_SESSION, MYSQLND_MS_QOS_OPTION_GTID, $gtid)) {
|
|
die(sprintf("[006] [%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
|
|
/* Either run on master or a slave which has replicated the INSERT */
|
|
if (!($res = $mysqli->query("SELECT id FROM test"))) {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
|
|
var_dump($res->fetch_assoc());
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
A GTID returned from <function>mysqlnd_ms_get_last_gtid</function>
|
|
can be used as an option for the session consistency service level.
|
|
Session consistency delivers read your writes. Session consistency can
|
|
be requested by calling
|
|
<function>mysqlnd_ms_set_qos</function>.
|
|
In the example, the plugin will execute the <literal>SELECT</literal>
|
|
statement either on the master or on a slave which has replicated
|
|
the previous <literal>INSERT</literal> already.
|
|
</para>
|
|
<para>
|
|
PECL mysqlnd_ms will transparently check every configured slave if
|
|
it has replicated the <literal>INSERT</literal> by checking the slaves
|
|
GTID table. The check is done running the SQL set with the
|
|
<literal>check_for_gtid</literal> option from the
|
|
<literal>global_transaction_id_injection</literal> section of
|
|
the plugins configuration file. Please note, that this is a slow and
|
|
expensive procedure. Applications should try to use it sparsely and only
|
|
if read load on the master becomes to high otherwise.
|
|
</para>
|
|
<para>
|
|
<emphasis role="bold">Use of the server-side global transaction ID feature</emphasis>
|
|
</para>
|
|
<note>
|
|
<title>Insufficient server support in MySQL 5.6</title>
|
|
<para>
|
|
The plugin has been developed against a pre-production version of MySQL 5.6.
|
|
It turns out that all released production versions of MySQL 5.6 do not provide
|
|
clients with enough information to enforce session consistency based on GTIDs.
|
|
Please, read the <link linkend="mysqlnd-ms.gtid">concepts section</link>
|
|
for details.
|
|
</para>
|
|
</note>
|
|
<para>
|
|
Starting with MySQL 5.6.5-m8 the MySQL Replication system features server-side
|
|
global transaction IDs. Transaction identifiers are automatically generated and
|
|
maintained by the server. Users do not need to take care of maintaining them.
|
|
There is no need to setup any tables in advance, or for setting
|
|
<literal>on_commit</literal>. A client-side emulation is no longer needed.
|
|
</para>
|
|
<para>
|
|
Clients can continue to use global transaction identifier to achieve
|
|
session consistency when reading from MySQL Replication slaves in some cases but not all!
|
|
The algorithm works as described above. Different SQL statements must be configured for
|
|
<literal>fetch_last_gtid</literal> and <literal>check_for_gtid</literal>.
|
|
The statements are given below. Please note, MySQL 5.6.5-m8 is a development
|
|
version. Details of the server implementation may change in the future and require
|
|
adoption of the SQL statements shown.
|
|
</para>
|
|
<para>
|
|
Using the following configuration any of the above described functionality can
|
|
be used together with the server-side global transaction ID feature.
|
|
<function>mysqlnd_ms_get_last_gtid</function> and <function>mysqlnd_ms_set_qos</function>
|
|
continue to work as described above. The only difference is that the server
|
|
does not use a simple sequence number but a string containing of a server identifier
|
|
and a sequence number. Thus, users cannot easily derive an order from GTIDs returned
|
|
by <function>mysqlnd_ms_get_last_gtid</function>.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Plugin config: using MySQL 5.6.5-m8 built-in GTID feature</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "127.0.0.1",
|
|
"port": "3306"
|
|
}
|
|
},
|
|
"global_transaction_id_injection":{
|
|
"fetch_last_gtid" : "SELECT @@GLOBAL.GTID_DONE AS trx_id FROM DUAL",
|
|
"check_for_gtid" : "SELECT GTID_SUBSET('#GTID', @@GLOBAL.GTID_DONE) AS trx_id FROM DUAL",
|
|
"report_error":true
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
</section>
|
|
<section xml:id="mysqlnd-ms.quickstart.cache">
|
|
<title>Cache integration</title>
|
|
<note>
|
|
<title>Version requirement, dependencies and status</title>
|
|
<para>
|
|
Please, find more about version requirements, extension load order dependencies and the current status
|
|
in the <link linkend="mysqlnd-ms.concept_cache">concepts section</link>!
|
|
</para>
|
|
</note>
|
|
<para>
|
|
Databases clusters can deliver different levels of consistency. As of
|
|
PECL/mysqlnd_ms 1.2.0 it is possible to advice the plugin to consider only
|
|
cluster nodes that can deliver the consistency level requested. For example,
|
|
if using asynchronous MySQL Replication with its cluster-wide eventual
|
|
consistency, it is possible to request session consistency (read your writes)
|
|
at any time using <function>mysqlnd_ms_set_quos</function>. Please, see also the
|
|
<link linkend="mysqlnd-ms.quickstart.qos-consistency">service level and consistency</link>
|
|
introduction.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Recap: quality of service to request read your writes</title>
|
|
<programlisting role="php">
|
|
/* Request session consistency: read your writes */
|
|
if (!mysqlnd_ms_set_qos($mysqli, MYSQLND_MS_QOS_CONSISTENCY_SESSION))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
Assuming PECL/mysqlnd has been explicitly told to deliver no consistency level
|
|
higher than eventual consistency, it is possible to replace a database node
|
|
read access with a client-side cache using time-to-live (TTL) as its invalidation
|
|
strategy. Both the database node and the cache may or may not serve
|
|
current data as this is what eventual consistency defines.
|
|
</para>
|
|
<para>
|
|
Replacing a database node read access with a local cache access can improve
|
|
overall performance and lower the database load. If the cache entry is every
|
|
reused by other clients than the one creating the cache entry,
|
|
a database access is saved and thus database load is lowered. Furthermore,
|
|
system performance can become better if computation and delivery
|
|
of a database query is slower than a local cache access.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Plugin config: no special entries for caching</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "127.0.0.1",
|
|
"port": "3306"
|
|
}
|
|
},
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Caching a slave request</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
if (!$mysqli->query("DROP TABLE IF EXISTS test") ||
|
|
!$mysqli->query("CREATE TABLE test(id INT)") ||
|
|
!$mysqli->query("INSERT INTO test(id) VALUES (1)"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* Explicitly allow eventual consistency and caching (TTL <= 60 seconds) */
|
|
if (false == mysqlnd_ms_set_qos($mysqli, MYSQLND_MS_QOS_CONSISTENCY_EVENTUAL, MYSQLND_MS_QOS_OPTION_CACHE, 60)) {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
|
|
/* To make this example work, we must wait for a slave to catch up. Brute force style. */
|
|
$attempts = 0;
|
|
do {
|
|
/* check if slave has the table */
|
|
if ($res = $mysqli->query("SELECT id FROM test")) {
|
|
break;
|
|
} else if ($mysqli->errno) {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
/* wait for slave to catch up */
|
|
usleep(200000);
|
|
} while ($attempts++ < 10);
|
|
|
|
/* Query has been run on a slave, result is in the cache */
|
|
assert($res);
|
|
var_dump($res->fetch_assoc());
|
|
|
|
/* Served from cache */
|
|
$res = $mysqli->query("SELECT id FROM test");
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The example shows how to use the cache feature. First, you have to set
|
|
the quality of service to eventual consistency and explicitly allow for caching.
|
|
This is done by calling <function>mysqlnd_ms_set_qos</function>.
|
|
Then, the result set of every read-only statement is cached for upto that
|
|
many seconds as allowed with <function>mysqlnd_ms_set_qos</function>.
|
|
</para>
|
|
<para>
|
|
The actual TTL is lower or equal to the value set
|
|
with <function>mysqlnd_ms_set_qos</function>. The value passed to the
|
|
function sets the maximum age (seconds) of the data delivered. To calculate
|
|
the actual TTL value the replication lag on a slave is checked and subtracted
|
|
from the given value. If, for example, the maximum age is set to 60 seconds and
|
|
the slave reports a lag of 10 seconds the resulting TTL is 50 seconds.
|
|
The TTL is calculated individually for every cached query.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Read your writes and caching combined</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
if (!$mysqli->query("DROP TABLE IF EXISTS test") ||
|
|
!$mysqli->query("CREATE TABLE test(id INT)") ||
|
|
!$mysqli->query("INSERT INTO test(id) VALUES (1)"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* Explicitly allow eventual consistency and caching (TTL <= 60 seconds) */
|
|
if (false == mysqlnd_ms_set_qos($mysqli, MYSQLND_MS_QOS_CONSISTENCY_EVENTUAL, MYSQLND_MS_QOS_OPTION_CACHE, 60)) {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
|
|
/* To make this example work, we must wait for a slave to catch up. Brute force style. */
|
|
$attempts = 0;
|
|
do {
|
|
/* check if slave has the table */
|
|
if ($res = $mysqli->query("SELECT id FROM test")) {
|
|
break;
|
|
} else if ($mysqli->errno) {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
/* wait for slave to catch up */
|
|
usleep(200000);
|
|
} while ($attempts++ < 10);
|
|
|
|
assert($res);
|
|
|
|
/* Query has been run on a slave, result is in the cache */
|
|
var_dump($res->fetch_assoc());
|
|
|
|
/* Served from cache */
|
|
if (!($res = $mysqli->query("SELECT id FROM test")))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
var_dump($res->fetch_assoc());
|
|
|
|
/* Update on master */
|
|
if (!$mysqli->query("UPDATE test SET id = 2"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* Read your writes */
|
|
if (false == mysqlnd_ms_set_qos($mysqli, MYSQLND_MS_QOS_CONSISTENCY_SESSION)) {
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
}
|
|
|
|
/* Fetch latest data */
|
|
if (!($res = $mysqli->query("SELECT id FROM test")))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
var_dump($res->fetch_assoc());
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The quality of service can be changed at any time to avoid further cache usage.
|
|
If needed, you can switch to read your writes (session consistency). In that case,
|
|
the cache will not be used and fresh data is read.
|
|
</para>
|
|
</section>
|
|
<section xml:id="mysqlnd-ms.quickstart.failover">
|
|
<title>Failover</title>
|
|
<para>
|
|
By default, the plugin does not attempt to fail over if connecting to a host
|
|
fails. This prevents pitfalls related to
|
|
<link linkend="mysqlnd-ms.quickstart.connectionpooling">connection state</link>.
|
|
It is recommended to manually handle connection errors in a way similar to a failed
|
|
transaction. You should catch the error, rebuild the connection state and rerun your
|
|
query as shown below.
|
|
</para>
|
|
<para>
|
|
If connection state is no issue to you, you can alternatively enable automatic
|
|
and silent failover. Depending on the configuration, the automatic and silent failover
|
|
will either attempt to fail over to the master before issuing and error or, try to
|
|
connect to other slaves, given the query allowes for it, before attempting to connect
|
|
to a master. Because <link linkend="mysqlnd-ms.failover">automatic failover</link> is
|
|
not fool-proof, it is not discussed in the quickstart. Instead, details are given
|
|
in the concepts section below.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Manual failover, automatic optional</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "simulate_slave_failure",
|
|
"port": "0"
|
|
},
|
|
"slave_1": {
|
|
"host": "127.0.0.1",
|
|
"port": 3311
|
|
}
|
|
},
|
|
"filters": { "roundrobin": [] }
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Manual failover</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "username", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
$sql = "SELECT 1 FROM DUAL";
|
|
|
|
/* error handling as it should be done regardless of the plugin */
|
|
if (!($res = $link->query($sql))) {
|
|
/* plugin specific: check for connection error */
|
|
switch ($link->errno) {
|
|
case 2002:
|
|
case 2003:
|
|
case 2005:
|
|
printf("Connection error - trying next slave!\n");
|
|
/* load balancer will pick next slave */
|
|
$res = $link->query($sql);
|
|
break;
|
|
default:
|
|
/* no connection error, failover is unlikely to help */
|
|
die(sprintf("SQL error: [%d] %s", $link->errno, $link->error));
|
|
break;
|
|
}
|
|
}
|
|
if ($res) {
|
|
var_dump($res->fetch_assoc());
|
|
}
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
</section>
|
|
<section xml:id="mysqlnd-ms.quickstart.partitioning">
|
|
<title>Partitioning and Sharding</title>
|
|
<para>
|
|
Database clustering is done for various reasons. Clusters can improve availability,
|
|
fault tolerance, and increase performance by applying a divide and conquer approach
|
|
as work is distributed over many machines. Clustering is sometimes combined with
|
|
partitioning and sharding to further break up a large complex task into
|
|
smaller, more manageable units.
|
|
</para>
|
|
<para>
|
|
The mysqlnd_ms plugin aims to support a wide variety of MySQL database clusters. Some flavors of
|
|
MySQL database clusters have built-in methods for partitioning and sharding,
|
|
which could be transparent to use. The plugin supports the two most
|
|
common approaches: MySQL Replication table filtering, and Sharding
|
|
(application based partitioning).
|
|
</para>
|
|
<para>
|
|
MySQL Replication supports partitioning as filters that allow you to
|
|
create slaves that replicate all or specific databases of the master, or tables.
|
|
It is then in the responsibility of the application
|
|
to choose a slave according to the filter rules. You can either use the
|
|
mysqlnd_ms <literal><link linkend="ini.mysqlnd-ms-plugin-config-v2.filter-node-groups">node_groups</link></literal>
|
|
filter to manually support this, or use the experimental table filter.
|
|
</para>
|
|
<para>
|
|
Manual partitioning or sharding is supported through the
|
|
node grouping filter, and SQL hints as of 1.5.0. The node_groups filter
|
|
lets you assign a symbolic name to a group of master and slave servers.
|
|
In the example, the master <literal>master_0</literal> and <literal>slave_0</literal>
|
|
form a group with the name <literal>Partition_A</literal>. It is entirely
|
|
up to you to decide what makes up a group. For example, you may use node
|
|
groups for sharding, and use the group names to address shards
|
|
like <literal>Shard_A_Range_0_100</literal>.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Cluster node groups</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"master": {
|
|
"master_0": {
|
|
"host": "localhost",
|
|
"socket": "\/tmp\/mysql.sock"
|
|
}
|
|
},
|
|
"slave": {
|
|
"slave_0": {
|
|
"host": "simulate_slave_failure",
|
|
"port": "0"
|
|
},
|
|
"slave_1": {
|
|
"host": "127.0.0.1",
|
|
"port": 3311
|
|
}
|
|
},
|
|
"filters": {
|
|
"node_groups": {
|
|
"Partition_A" : {
|
|
"master": ["master_0"],
|
|
"slave": ["slave_0"]
|
|
}
|
|
},
|
|
"roundrobin": []
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Manual partitioning using SQL hints</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
function select($mysqli, $msg, $hint = '') {
|
|
/* Note: weak test, two connections to two servers may have the same thread id */
|
|
$sql = sprintf("SELECT CONNECTION_ID() AS _thread, '%s' AS _hint FROM DUAL", $msg);
|
|
if ($hint) {
|
|
$sql = $hint . $sql;
|
|
}
|
|
if (!($res = $mysqli->query($sql))) {
|
|
printf("[%d] %s", $mysqli->errno, $mysqli->error);
|
|
return false;
|
|
}
|
|
$row = $res->fetch_assoc();
|
|
printf("%d - %s - %s\n", $row['_thread'], $row['_hint'], $sql);
|
|
return true;
|
|
}
|
|
|
|
$mysqli = new mysqli("myapp", "user", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* All slaves allowed */
|
|
select($mysqli, "slave_0");
|
|
select($mysqli, "slave_1");
|
|
|
|
/* only servers of node group "Partition_A" allowed */
|
|
select($mysqli, "slave_1", "/*Partition_A*/");
|
|
select($mysqli, "slave_1", "/*Partition_A*/");
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
<screen>
|
|
<![CDATA[
|
|
6804 - slave_0 - SELECT CONNECTION_ID() AS _thread, 'slave1' AS _hint FROM DUAL
|
|
2442 - slave_1 - SELECT CONNECTION_ID() AS _thread, 'slave2' AS _hint FROM DUAL
|
|
6804 - slave_0 - /*Partition_A*/SELECT CONNECTION_ID() AS _thread, 'slave1' AS _hint FROM DUAL
|
|
6804 - slave_0 - /*Partition_A*/SELECT CONNECTION_ID() AS _thread, 'slave1' AS _hint FROM DUAL
|
|
]]>
|
|
</screen>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
By default, the plugin will use all configured master and slave servers for
|
|
query execution. But if a query begins with a SQL hint like
|
|
<literal>/*node_group*/</literal>, the plugin will only consider the servers
|
|
listed in the <literal>node_group</literal> for query execution. Thus,
|
|
<literal>SELECT</literal> queries prefixed with <literal>/*Partition_A*/</literal>
|
|
will only be executed on <literal>slave_0</literal>.
|
|
</para>
|
|
|
|
</section>
|
|
|
|
|
|
<section xml:id="mysqlnd-ms.quickstart.mysql_fabric">
|
|
<title>MySQL Fabric</title>
|
|
<note>
|
|
<title>Version requirement and status</title>
|
|
<para>
|
|
Work on supporting MySQL Fabric started in version 1.6. Please,
|
|
consider the support to be of pre-alpha quality. The manual may
|
|
not list all features or feature limitations. This is work in progress.
|
|
</para>
|
|
<para>
|
|
Sharding is the only use case supported by the plugin to date.
|
|
</para>
|
|
</note>
|
|
<note>
|
|
<title>MySQL Fabric concepts</title>
|
|
<para>
|
|
Please, check the MySQL reference manual for more information about MySQL Fabric
|
|
and how to set it up. The PHP manual assumes that you are familiar
|
|
with the basic concepts and ideas of MySQL Fabric.
|
|
</para>
|
|
</note>
|
|
<para>
|
|
MySQL Fabric is a system for managing farms of MySQL servers to achive
|
|
High Availability and optionally support sharding. Technically, it is a
|
|
middleware to manage and monitor MySQL servers.
|
|
</para>
|
|
<para>
|
|
Clients query MySQL Fabric to obtain lists of MySQL servers,
|
|
their state and their roles. For example, clients can can request a list of
|
|
slaves for a MySQL Replication group and whether they are ready to
|
|
handle SQL requests. Another example is a cluster of sharded MySQL servers
|
|
where the client seeks to know which shard to query for a given
|
|
table and shard key. If configured to use Fabric, the plugin uses XML RCP over HTTP
|
|
to obtain the list at runtime from a MySQL Fabric host. The XML remote
|
|
procedure call itself is done in the background and transparent from a
|
|
developers point of view.
|
|
</para>
|
|
<para>
|
|
Instead of listing MySQL servers directly in the plugins configuration file
|
|
it contains a list of one or more MySQL Fabric hosts
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Plugin config: Fabric hosts instead of MySQL servers</title>
|
|
<programlisting role="ini">
|
|
<![CDATA[
|
|
{
|
|
"myapp": {
|
|
"fabric": {
|
|
"hosts": [
|
|
{
|
|
"host" : "127.0.0.1",
|
|
"port" : 8080
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
Users utilize the new functions
|
|
<link linkend="function.mysqlnd-ms-fabric-select-shard">
|
|
<function>mysqlnd_ms_fabric_select_shard</function></link> and
|
|
<link linkend="function.mysqlnd-ms-fabric-select-global">
|
|
<function>mysqlnd_ms_fabric_select_global</function></link> to switch to
|
|
the set of servers responsible for a given shard key. Then, the
|
|
plugin picks an appropriate server for running queries on.
|
|
When doing so, the plugin takes care of additional
|
|
load balancing rules set.
|
|
</para>
|
|
<para>
|
|
The below example assumes that MySQL Fabric has been setup
|
|
to shard the table <literal>test.fabrictest</literal> using
|
|
the <literal>id</literal> column of the table as a shard key.
|
|
</para>
|
|
<para>
|
|
<example>
|
|
<title>Manual partitioning using SQL hints</title>
|
|
<programlisting role="php">
|
|
<![CDATA[
|
|
<?php
|
|
$mysqli = new mysqli("myapp", "user", "password", "database");
|
|
if (!$mysqli)
|
|
/* Of course, your error handling is nicer... */
|
|
die(sprintf("[%d] %s\n", mysqli_connect_errno(), mysqli_connect_error()));
|
|
|
|
/* Create a global table - a table available on all shards */
|
|
mysqlnd_ms_fabric_select_global($mysqli, "test.fabrictest");
|
|
if (!$mysqli->query("CREATE TABLE test.fabrictest(id INT NOT NULL PRIMARY KEY)"))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* Switch connection to appropriate shard and insert record */
|
|
mysqlnd_ms_fabric_select_shard($mysqli, "test.fabrictest", 10);
|
|
if (!($res = $mysqli->query("INSERT INTO fabrictest(id) VALUES (10)")))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
|
|
/* Try to read newly inserted record */
|
|
mysqlnd_ms_fabric_select_shard($mysqli, "test.fabrictest", 10);
|
|
if (!($res = $mysqli->query("SELECT id FROM test WHERE id = 10")))
|
|
die(sprintf("[%d] %s\n", $mysqli->errno, $mysqli->error));
|
|
?>
|
|
]]>
|
|
</programlisting>
|
|
</example>
|
|
</para>
|
|
<para>
|
|
The example creates the sharded table, inserts a record and reads
|
|
the record thereafter. All SQL data definition language (DDL)
|
|
operations on a sharded table must be applied to the so called global server group.
|
|
Prior to creatingor altering a sharded table,
|
|
<link linkend="function.mysqlnd-ms-fabric-select-global">
|
|
<function>mysqlnd_ms_fabric_select_global</function></link> is called
|
|
to switch the given connection to the corresponsing servers of the global
|
|
group. Data manipulation (DML) SQL statements must be sent to the shards
|
|
directly. The <link linkend="function.mysqlnd-ms-fabric-select-shard">
|
|
<function>mysqlnd_ms_fabric_select_shard</function></link> switches a
|
|
connection to shards handling a certain shard key.
|
|
</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
|
|
-->
|