php-doc-en/reference/mysqlnd_ms/quickstart.xml

1442 lines
50 KiB
XML
Raw Normal View History

<?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,
namelys 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>
<note>
<para>
The documentation has been updated to show the syntax used as of version
1.1.0-beta. PECL/mysqlnd_ms 1.1.0-beta introduces many
<link linkend="mysqlnd-ms.changes_one_one">changes</link>. Among
others, it is using a new <literal>JSON</literal>
based
<link linkend="mysqlnd-ms.plugin-ini-json">plugin configuration file</link>
format.
</para>
</note>
<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.
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>
The plugin uses its own configuration file. Use the PHP
configuration directive
<link linkend="ini.mysqlnd-ms.ini-file">mysqlnd_ms.ini_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).
</para>
<para>
<example>
<title>Enabling the plugin (php.ini)</title>
<programlisting role="ini">
<![CDATA[
mysqlnd_ms.enable=1
mysqlnd_ms.ini_file=/path/to/mysqlnd_ms_plugin.ini
]]>
</programlisting>
</example>
</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.ini-file">mysqlnd_ms.ini_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>
</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_MASTER_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 transaction aware. SQL transactions are
units of work to be run on a single server.
The plugin does not 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>
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 transactins, 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.
</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>
<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>Outlook: 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>
</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.
<link linkend="function.mysqlnd-ms-set-qos"><function>mysqlnd_ms_set_qos</function></link>
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 <link linkend="function.mysqlnd-ms-set-qos"><function>mysqlnd_ms_set_qos</function></link>.
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 &lt;= 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>
<note>
<title>Fail over logic is work in progress</title>
<para>
The details of the fail over logic may change in future versions.
</para>
</note>
</section>
<section xml:id="mysqlnd-ms.quickstart.gtid">
<title>Global transaction IDs</title>
<note>
<title>Version requirement</title>
<para>
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>
</note>
<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 comitted on the master.
Slaves replicate the table. The counter serves two puposes. 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 comitted 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 quering 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 <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", "root", "", "test");
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. After the example script has finished the global
transaciton 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", "root", "", "test");
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 mysqlnd_ms for a global transaction ID which
belongs to the last write transactions. The function <literal>mysqlnd_ms_get_last_gtid()</literal>
may be called after the GTID has been incremented. The
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. Applications are advices 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", "root", "", "test");
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 <literal>mysqlnd_ms_get_last_gtid()</literal>
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
<link linkend="function.mysqlnd-ms-set-qos"><function>mysqlnd_ms_set_qos</function></link>.
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>
</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
-->