3 * Advanced generator of database load balancing objects for database farms.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
24 namespace Wikimedia\Rdbms
;
26 use InvalidArgumentException
;
27 use UnexpectedValueException
;
30 * A multi-database, multi-master factory for Wikimedia and similar installations.
31 * Ignores the old configuration globals.
35 class LBFactoryMulti
extends LBFactory
{
36 /** @var LoadBalancer[] */
37 private $mainLBs = [];
38 /** @var LoadBalancer[] */
39 private $externalLBs = [];
41 /** @var string[] Map of (hostname => IP address) */
42 private $hostsByName = [];
43 /** @var string[] Map of (database name => section name) */
44 private $sectionsByDB = [];
45 /** @var int[][][] Map of (section => group => host => load ratio) */
46 private $groupLoadsBySection = [];
47 /** @var int[][][] Map of (database => group => host => load ratio) */
48 private $groupLoadsByDB = [];
49 /** @var int[][] Map of (cluster => host => load ratio) */
50 private $externalLoads = [];
51 /** @var array Server config map ("host", "hostName", "load", and "groupLoads" are ignored) */
52 private $serverTemplate = [];
53 /** @var array Server config map overriding "serverTemplate" for external storage */
54 private $externalTemplateOverrides = [];
55 /** @var array[] Map of (section => server config map overrides) */
56 private $templateOverridesBySection = [];
57 /** @var array[] Map of (cluster => server config map overrides) for external storage */
58 private $templateOverridesByCluster = [];
59 /** @var array Server config override map for all main and external master servers */
60 private $masterTemplateOverrides = [];
61 /** @var array[] Map of (host => server config map overrides) for main and external servers */
62 private $templateOverridesByServer = [];
63 /** @var string[]|bool[] A map of section name to read-only message */
64 private $readOnlyBySection = [];
66 /** @var string An ILoadMonitor class */
67 private $loadMonitorClass;
75 * Template override precedence (highest => lowest):
76 * - templateOverridesByServer
77 * - masterTemplateOverrides
78 * - templateOverridesBySection/templateOverridesByCluster
79 * - externalTemplateOverrides
81 * Overrides only work on top level keys (so nested values will not be merged).
83 * Server config maps should be of the format Database::factory() requires.
84 * Additionally, a 'max lag' key should also be set on server maps, indicating how stale the
85 * data can be before the load balancer tries to avoid using it. The map can have 'is static'
86 * set to disable blocking replication sync checks (intended for archive servers with
89 * @see LBFactory::__construct()
90 * @param array $conf Additional parameters include:
91 * - hostsByName Optional (hostname => IP address) map.
92 * - sectionsByDB Optional map of (database => section name).
95 * 'DEFAULT' => 'section1',
96 * 'database1' => 'section2'
98 * - sectionLoads Optional map of (section => host => load ratio); the first
99 * host in each section is the master server for that section.
103 * 'dbreplica1' => 100,
104 * 'dbreplica2' => 100
106 * - groupLoadsBySection Optional map of (section => group => host => load ratio);
107 * any ILoadBalancer::GROUP_GENERIC group will be ignored.
112 * 'dbreplica3 => 100,
113 * 'dbreplica4' => 100
117 * - groupLoadsByDB Optional (database => group => host => load ratio) map.
118 * - externalLoads Optional (cluster => host => load ratio) map.
119 * - serverTemplate server config map for Database::factory().
120 * Note that "host", "hostName" and "load" entries will be
121 * overridden by "groupLoadsBySection" and "hostsByName".
122 * - externalTemplateOverrides Optional server config map overrides for external
123 * stores; respects the override precedence described above.
124 * - templateOverridesBySection Optional (section => server config map overrides) map;
125 * respects the override precedence described above.
126 * - templateOverridesByCluster Optional (external cluster => server config map overrides)
127 * map; respects the override precedence described above.
128 * - masterTemplateOverrides Optional server config map overrides for masters;
129 * respects the override precedence described above.
130 * - templateOverridesByServer Optional (host => server config map overrides) map;
131 * respects the override precedence described above
132 * and applies to both core and external storage.
133 * - loadMonitorClass Name of the LoadMonitor class to always use. [optional]
134 * - readOnlyBySection Optional map of (section name => message text or false).
135 * String values make sections read only, whereas anything
136 * else does not restrict read/write mode.
138 public function __construct( array $conf ) {
139 parent
::__construct( $conf );
141 $this->hostsByName
= $conf['hostsByName'] ??
[];
142 $this->sectionsByDB
= $conf['sectionsByDB'];
143 $this->groupLoadsBySection
= $conf['groupLoadsBySection'] ??
[];
144 foreach ( ( $conf['sectionLoads'] ??
[] ) as $section => $loadByHost ) {
145 $this->groupLoadsBySection
[$section][ILoadBalancer
::GROUP_GENERIC
] = $loadByHost;
147 $this->groupLoadsByDB
= $conf['groupLoadsByDB'] ??
[];
148 $this->externalLoads
= $conf['externalLoads'] ??
[];
149 $this->serverTemplate
= $conf['serverTemplate'] ??
[];
150 $this->externalTemplateOverrides
= $conf['externalTemplateOverrides'] ??
[];
151 $this->templateOverridesBySection
= $conf['templateOverridesBySection'] ??
[];
152 $this->templateOverridesByCluster
= $conf['templateOverridesByCluster'] ??
[];
153 $this->masterTemplateOverrides
= $conf['masterTemplateOverrides'] ??
[];
154 $this->templateOverridesByServer
= $conf['templateOverridesByServer'] ??
[];
155 $this->readOnlyBySection
= $conf['readOnlyBySection'] ??
[];
157 $this->loadMonitorClass
= $conf['loadMonitorClass'] ?? LoadMonitor
::class;
160 public function newMainLB( $domain = false, $owner = null ) {
161 $section = $this->getSectionForDomain( $domain );
162 if ( !isset( $this->groupLoadsBySection
[$section][ILoadBalancer
::GROUP_GENERIC
] ) ) {
163 throw new UnexpectedValueException( "Section '$section' has no hosts defined." );
166 $dbGroupLoads = $this->groupLoadsByDB
[$this->getDomainDatabase( $domain )] ??
[];
167 unset( $dbGroupLoads[ILoadBalancer
::GROUP_GENERIC
] ); // cannot override
169 return $this->newLoadBalancer(
171 $this->serverTemplate
,
172 $this->templateOverridesBySection
[$section] ??
[]
174 array_merge( $this->groupLoadsBySection
[$section], $dbGroupLoads ),
175 // Use the LB-specific read-only reason if everything isn't already read-only
176 is_string( $this->readOnlyReason
)
177 ?
$this->readOnlyReason
178 : ( $this->readOnlyBySection
[$section] ??
false ),
183 public function getMainLB( $domain = false ) {
184 $section = $this->getSectionForDomain( $domain );
186 if ( !isset( $this->mainLBs
[$section] ) ) {
187 $this->mainLBs
[$section] = $this->newMainLB( $domain, $this->getOwnershipId() );
190 return $this->mainLBs
[$section];
193 public function newExternalLB( $cluster, $owner = null ) {
194 if ( !isset( $this->externalLoads
[$cluster] ) ) {
195 throw new InvalidArgumentException( "Unknown cluster '$cluster'" );
198 return $this->newLoadBalancer(
200 $this->serverTemplate
,
201 $this->externalTemplateOverrides
,
202 $this->templateOverridesByCluster
[$cluster] ??
[]
204 [ ILoadBalancer
::GROUP_GENERIC
=> $this->externalLoads
[$cluster] ],
205 $this->readOnlyReason
,
210 public function getExternalLB( $cluster ) {
211 if ( !isset( $this->externalLBs
[$cluster] ) ) {
212 $this->externalLBs
[$cluster] =
213 $this->newExternalLB( $cluster, $this->getOwnershipId() );
216 return $this->externalLBs
[$cluster];
219 public function getAllMainLBs() {
221 foreach ( $this->sectionsByDB
as $db => $section ) {
222 if ( !isset( $lbs[$section] ) ) {
223 $lbs[$section] = $this->getMainLB( $db );
230 public function getAllExternalLBs() {
232 foreach ( $this->externalLoads
as $cluster => $unused ) {
233 $lbs[$cluster] = $this->getExternalLB( $cluster );
239 public function forEachLB( $callback, array $params = [] ) {
240 foreach ( $this->mainLBs
as $lb ) {
241 $callback( $lb, ...$params );
243 foreach ( $this->externalLBs
as $lb ) {
244 $callback( $lb, ...$params );
249 * @param bool|string $domain
252 private function getSectionForDomain( $domain = false ) {
253 if ( $this->lastDomain
=== $domain ) {
254 return $this->lastSection
;
257 $database = $this->getDomainDatabase( $domain );
258 $section = $this->sectionsByDB
[$database] ?? self
::CLUSTER_MAIN_DEFAULT
;
259 $this->lastSection
= $section;
260 $this->lastDomain
= $domain;
266 * Make a new load balancer object based on template and load array
268 * @param array $serverTemplate Server config map
269 * @param int[][] $groupLoads Map of (group => host => load)
270 * @param string|bool $readOnlyReason
271 * @param int|null $owner
272 * @return LoadBalancer
274 private function newLoadBalancer( $serverTemplate, $groupLoads, $readOnlyReason, $owner ) {
275 $lb = new LoadBalancer( array_merge(
276 $this->baseLoadBalancerParams( $owner ),
278 'servers' => $this->makeServerArray( $serverTemplate, $groupLoads ),
279 'loadMonitor' => [ 'class' => $this->loadMonitorClass
],
280 'readOnlyReason' => $readOnlyReason
283 $this->initLoadBalancer( $lb );
289 * Make a server array as expected by LoadBalancer::__construct()
291 * @param array $serverTemplate Server config map
292 * @param int[][] $groupLoads Map of (group => host => load)
293 * @return array[] List of server config maps
295 private function makeServerArray( array $serverTemplate, array $groupLoads ) {
296 // The master server is the first host explicitly listed in the generic load group
297 if ( !$groupLoads[ILoadBalancer
::GROUP_GENERIC
] ) {
298 throw new UnexpectedValueException( "Empty generic load array; no master defined." );
301 $groupLoadsByHost = $this->reindexGroupLoads( $groupLoads );
302 // Get the ordered map of (host => load); the master server is first
303 $genericLoads = $groupLoads[ILoadBalancer
::GROUP_GENERIC
];
304 // Implictly append any hosts that only appear in custom load groups
305 $genericLoads +
= array_fill_keys( array_keys( $groupLoadsByHost ), 0 );
308 foreach ( $genericLoads as $host => $load ) {
309 $servers[] = array_merge(
311 $servers ?
[] : $this->masterTemplateOverrides
,
312 $this->templateOverridesByServer
[$host] ??
[],
314 'host' => $this->hostsByName
[$host] ??
$host,
317 'groupLoads' => $groupLoadsByHost[$host] ??
[]
326 * Take a group load array indexed by group then server, and reindex it by server then group
327 * @param int[][] $groupLoads Map of (group => host => load)
328 * @return int[][] Map of (host => group => load)
330 private function reindexGroupLoads( array $groupLoads ) {
333 foreach ( $groupLoads as $group => $loadByHost ) {
334 foreach ( $loadByHost as $host => $load ) {
335 $reindexed[$host][$group] = $load;
343 * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
346 private function getDomainDatabase( $domain = false ) {
347 return ( $domain === false )
348 ?
$this->localDomain
->getDatabase()
349 : DatabaseDomain
::newFromId( $domain )->getDatabase();