r91402 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r91401‎ | r91402 | r91403 >
Date:12:01, 4 July 2011
Author:tstarling
Status:ok (Comments)
Tags:
Comment:
Allow SqlBagOStuff data to be split over many tables, to avoid lock contention performance issues on servers with a high write load. See http://bugs.mysql.com/bug.php?id=61735 and http://bugs.mysql.com/bug.php?id=61736 .
Modified paths:
  • /trunk/phase3/includes/objectcache/SqlBagOStuff.php (modified) (history)

Diff [purge]

Index: trunk/phase3/includes/objectcache/SqlBagOStuff.php
@@ -19,6 +19,8 @@
2020 var $serverInfo;
2121 var $lastExpireAll = 0;
2222 var $purgePeriod = 100;
 23+ var $shards = 1;
 24+ var $tableName = 'objectcache';
2325
2426 /**
2527 * Constructor. Parameters are:
@@ -32,6 +34,16 @@
3335 * request. If this is set to zero, purging will never be
3436 * done.
3537 *
 38+ * - tableName: The table name to use, default is "objectcache".
 39+ *
 40+ * - shards: The number of tables to use for data storage. If this is
 41+ * more than 1, table names will be formed in the style
 42+ * objectcacheNNN where NNN is the shard index, between 0 and
 43+ * shards-1. The number of digits will be the minimum number
 44+ * required to hold the largest shard index. Data will be
 45+ * distributed across all tables by key hash. This is for
 46+ * MySQL bugs 61735 and 61736.
 47+ *
3648 * @param $params array
3749 */
3850 public function __construct( $params ) {
@@ -42,6 +54,12 @@
4355 if ( isset( $params['purgePeriod'] ) ) {
4456 $this->purgePeriod = intval( $params['purgePeriod'] );
4557 }
 58+ if ( isset( $params['tableName'] ) ) {
 59+ $this->tableName = $params['tableName'];
 60+ }
 61+ if ( isset( $params['shards'] ) ) {
 62+ $this->shards = intval( $params['shards'] );
 63+ }
4664 }
4765
4866 /**
@@ -72,11 +90,37 @@
7391 return $this->db;
7492 }
7593
 94+ /**
 95+ * Get the table name for a given key
 96+ */
 97+ protected function getTableByKey( $key ) {
 98+ if ( $this->shards > 1 ) {
 99+ $hash = hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
 100+ return $this->getTableByShard( $hash % $this->shards );
 101+ } else {
 102+ return $this->tableName;
 103+ }
 104+ }
 105+
 106+ /**
 107+ * Get the table name for a given shard index
 108+ */
 109+ protected function getTableByShard( $index ) {
 110+ if ( $this->shards > 1 ) {
 111+ $decimals = strlen( $this->shards - 1 );
 112+ return $this->tableName .
 113+ sprintf( "%0{$decimals}d", $index );
 114+ } else {
 115+ return $this->tableName;
 116+ }
 117+ }
 118+
76119 public function get( $key ) {
77120 # expire old entries if any
78121 $this->garbageCollect();
79122 $db = $this->getDB();
80 - $row = $db->selectRow( 'objectcache', array( 'value', 'exptime' ),
 123+ $tableName = $this->getTableByKey( $key );
 124+ $row = $db->selectRow( $tableName, array( 'value', 'exptime' ),
81125 array( 'keyname' => $key ), __METHOD__ );
82126
83127 if ( !$row ) {
@@ -92,7 +136,7 @@
93137 $db->begin();
94138 # Put the expiry time in the WHERE condition to avoid deleting a
95139 # newly-inserted value
96 - $db->delete( 'objectcache',
 140+ $db->delete( $tableName,
97141 array(
98142 'keyname' => $key,
99143 'exptime' => $row->exptime
@@ -129,7 +173,9 @@
130174 $db->begin();
131175 // (bug 24425) use a replace if the db supports it instead of
132176 // delete/insert to avoid clashes with conflicting keynames
133 - $db->replace( 'objectcache', array( 'keyname' ),
 177+ $db->replace(
 178+ $this->getTableByKey( $key ),
 179+ array( 'keyname' ),
134180 array(
135181 'keyname' => $key,
136182 'value' => $db->encodeBlob( $this->serialize( $value ) ),
@@ -150,7 +196,10 @@
151197
152198 try {
153199 $db->begin();
154 - $db->delete( 'objectcache', array( 'keyname' => $key ), __METHOD__ );
 200+ $db->delete(
 201+ $this->getTableByKey( $key ),
 202+ array( 'keyname' => $key ),
 203+ __METHOD__ );
155204 $db->commit();
156205 } catch ( DBQueryError $e ) {
157206 $this->handleWriteError( $e );
@@ -163,19 +212,24 @@
164213
165214 public function incr( $key, $step = 1 ) {
166215 $db = $this->getDB();
 216+ $tableName = $this->getTableByKey( $key );
167217 $step = intval( $step );
168218
169219 try {
170220 $db->begin();
171 - $row = $db->selectRow( 'objectcache', array( 'value', 'exptime' ),
172 - array( 'keyname' => $key ), __METHOD__, array( 'FOR UPDATE' ) );
 221+ $row = $db->selectRow(
 222+ $tableName,
 223+ array( 'value', 'exptime' ),
 224+ array( 'keyname' => $key ),
 225+ __METHOD__,
 226+ array( 'FOR UPDATE' ) );
173227 if ( $row === false ) {
174228 // Missing
175229 $db->commit();
176230
177231 return null;
178232 }
179 - $db->delete( 'objectcache', array( 'keyname' => $key ), __METHOD__ );
 233+ $db->delete( $tableName, array( 'keyname' => $key ), __METHOD__ );
180234 if ( $this->isExpired( $row->exptime ) ) {
181235 // Expired, do not reinsert
182236 $db->commit();
@@ -185,7 +239,7 @@
186240
187241 $oldValue = intval( $this->unserialize( $db->decodeBlob( $row->value ) ) );
188242 $newValue = $oldValue + $step;
189 - $db->insert( 'objectcache',
 243+ $db->insert( $tableName,
190244 array(
191245 'keyname' => $key,
192246 'value' => $db->encodeBlob( $this->serialize( $newValue ) ),
@@ -208,11 +262,14 @@
209263
210264 public function keys() {
211265 $db = $this->getDB();
212 - $res = $db->select( 'objectcache', array( 'keyname' ), false, __METHOD__ );
213266 $result = array();
214267
215 - foreach ( $res as $row ) {
216 - $result[] = $row->keyname;
 268+ for ( $i = 0; $i < $this->shards; $i++ ) {
 269+ $res = $db->select( $this->getTableByShard( $i ),
 270+ array( 'keyname' ), false, __METHOD__ );
 271+ foreach ( $res as $row ) {
 272+ $result[] = $row->keyname;
 273+ }
217274 }
218275
219276 return $result;
@@ -252,9 +309,14 @@
253310 $now = $db->timestamp();
254311
255312 try {
256 - $db->begin();
257 - $db->delete( 'objectcache', array( 'exptime < ' . $db->addQuotes( $now ) ), __METHOD__ );
258 - $db->commit();
 313+ for ( $i = 0; $i < $this->shards; $i++ ) {
 314+ $db->begin();
 315+ $db->delete(
 316+ $this->getTableByShard( $i ),
 317+ array( 'exptime < ' . $db->addQuotes( $now ) ),
 318+ __METHOD__ );
 319+ $db->commit();
 320+ }
259321 } catch ( DBQueryError $e ) {
260322 $this->handleWriteError( $e );
261323 }
@@ -264,9 +326,11 @@
265327 $db = $this->getDB();
266328
267329 try {
268 - $db->begin();
269 - $db->delete( 'objectcache', '*', __METHOD__ );
270 - $db->commit();
 330+ for ( $i = 0; $i < $this->shards; $i++ ) {
 331+ $db->begin();
 332+ $db->delete( $this->getTableByShard( $i ), '*', __METHOD__ );
 333+ $db->commit();
 334+ }
271335 } catch ( DBQueryError $e ) {
272336 $this->handleWriteError( $e );
273337 }
@@ -328,6 +392,27 @@
329393 wfDebug( __METHOD__ . ": ignoring query error\n" );
330394 $db->ignoreErrors( false );
331395 }
 396+
 397+ /**
 398+ * Create shard tables. For use from eval.php.
 399+ */
 400+ public function createTables() {
 401+ $db = $this->getDB();
 402+ if ( $db->getType() !== 'mysql'
 403+ || version_compare( $db->getServerVersion(), '4.1.0', '<' ) )
 404+ {
 405+ throw new MWException( __METHOD__ . ' is not supported on this DB server' );
 406+ }
 407+
 408+ for ( $i = 0; $i < $this->shards; $i++ ) {
 409+ $db->begin();
 410+ $db->query(
 411+ 'CREATE TABLE ' . $db->tableName( $this->getTableByShard( $i ) ) .
 412+ ' LIKE ' . $db->tableName( 'objectcache' ),
 413+ __METHOD__ );
 414+ $db->commit();
 415+ }
 416+ }
332417 }
333418
334419 /**

Follow-up revisions

RevisionCommit summaryAuthorDate
r91442MFT r91402: work around excessive locking in InnoDB by "sharding" across mult...tstarling07:04, 5 July 2011

Comments

#Comment by MaxSem (talk | contribs)   04:37, 5 July 2011

We have DB::duplicateTableStructure() to avoid building queries manually like that. Also works on every DB.

#Comment by Catrope (talk | contribs)   10:55, 13 August 2011

Untagging 1.17wmf1, already merged and deployed.

Tim, could you respond to MaxSem's comment?

#Comment by Tim Starling (talk | contribs)   10:58, 13 August 2011

Max's comment is correct. Note that the code in question is not called from anywhere.

#Comment by Catrope (talk | contribs)   11:00, 13 August 2011

Lol, I didn't notice that it was for eval.php only. Maybe it should be a maintenance script?

#Comment by Tim Starling (talk | contribs)   12:00, 13 August 2011

Maybe, but it's pretty likely nobody will ever run it. You would have to have a write rate similar to the whole of Wikimedia put together for this sharding option to make a performance difference. And hopefully it will be fixed in the next major version of MySQL, so it'll only be necessary for a few years.

Also, the chances that someone will have such a large wiki running on a DBMS other than MySQL and that that DBMS will happen to have the same bug requiring the same hackish workaround seem rather slim to me. That's another reason why I'm not too concerned about the MySQL-specific code here.

Status & tagging log