LBFactoryMulti.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <?php
  2. /**
  3. * Advanced generator of database load balancing objects for database farms.
  4. *
  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.
  9. *
  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.
  14. *
  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
  19. *
  20. * @file
  21. * @ingroup Database
  22. */
  23. namespace Wikimedia\Rdbms;
  24. use InvalidArgumentException;
  25. use UnexpectedValueException;
  26. /**
  27. * A multi-database, multi-master factory for Wikimedia and similar installations.
  28. * Ignores the old configuration globals.
  29. *
  30. * @ingroup Database
  31. */
  32. class LBFactoryMulti extends LBFactory {
  33. /** @var LoadBalancer[] */
  34. private $mainLBs = [];
  35. /** @var LoadBalancer[] */
  36. private $externalLBs = [];
  37. /** @var string[] Map of (hostname => IP address) */
  38. private $hostsByName = [];
  39. /** @var string[] Map of (database name => section name) */
  40. private $sectionsByDB = [];
  41. /** @var int[][][] Map of (section => group => host => load ratio) */
  42. private $groupLoadsBySection = [];
  43. /** @var int[][][] Map of (database => group => host => load ratio) */
  44. private $groupLoadsByDB = [];
  45. /** @var int[][] Map of (cluster => host => load ratio) */
  46. private $externalLoads = [];
  47. /** @var array Server config map ("host", "hostName", "load", and "groupLoads" are ignored) */
  48. private $serverTemplate = [];
  49. /** @var array Server config map overriding "serverTemplate" for external storage */
  50. private $externalTemplateOverrides = [];
  51. /** @var array[] Map of (section => server config map overrides) */
  52. private $templateOverridesBySection = [];
  53. /** @var array[] Map of (cluster => server config map overrides) for external storage */
  54. private $templateOverridesByCluster = [];
  55. /** @var array Server config override map for all main and external master servers */
  56. private $masterTemplateOverrides = [];
  57. /** @var array[] Map of (host => server config map overrides) for main and external servers */
  58. private $templateOverridesByServer = [];
  59. /** @var string[]|bool[] A map of section name to read-only message */
  60. private $readOnlyBySection = [];
  61. /** @var string An ILoadMonitor class */
  62. private $loadMonitorClass;
  63. /** @var string */
  64. private $lastDomain;
  65. /** @var string */
  66. private $lastSection;
  67. /**
  68. * Template override precedence (highest => lowest):
  69. * - templateOverridesByServer
  70. * - masterTemplateOverrides
  71. * - templateOverridesBySection/templateOverridesByCluster
  72. * - externalTemplateOverrides
  73. * - serverTemplate
  74. * Overrides only work on top level keys (so nested values will not be merged).
  75. *
  76. * Server config maps should be of the format Database::factory() requires.
  77. * Additionally, a 'max lag' key should also be set on server maps, indicating how stale the
  78. * data can be before the load balancer tries to avoid using it. The map can have 'is static'
  79. * set to disable blocking replication sync checks (intended for archive servers with
  80. * unchanging data).
  81. *
  82. * @see LBFactory::__construct()
  83. * @param array $conf Additional parameters include:
  84. * - hostsByName Optional (hostname => IP address) map.
  85. * - sectionsByDB Optional map of (database => section name).
  86. * For example:
  87. * [
  88. * 'DEFAULT' => 'section1',
  89. * 'database1' => 'section2'
  90. * ]
  91. * - sectionLoads Optional map of (section => host => load ratio); the first
  92. * host in each section is the master server for that section.
  93. * For example:
  94. * [
  95. * 'dbmaser' => 0,
  96. * 'dbreplica1' => 100,
  97. * 'dbreplica2' => 100
  98. * ]
  99. * - groupLoadsBySection Optional map of (section => group => host => load ratio);
  100. * any ILoadBalancer::GROUP_GENERIC group will be ignored.
  101. * For example:
  102. * [
  103. * 'section1' => [
  104. * 'group1' => [
  105. * 'dbreplica3 => 100,
  106. * 'dbreplica4' => 100
  107. * ]
  108. * ]
  109. * ]
  110. * - groupLoadsByDB Optional (database => group => host => load ratio) map.
  111. * - externalLoads Optional (cluster => host => load ratio) map.
  112. * - serverTemplate server config map for Database::factory().
  113. * Note that "host", "hostName" and "load" entries will be
  114. * overridden by "groupLoadsBySection" and "hostsByName".
  115. * - externalTemplateOverrides Optional server config map overrides for external
  116. * stores; respects the override precedence described above.
  117. * - templateOverridesBySection Optional (section => server config map overrides) map;
  118. * respects the override precedence described above.
  119. * - templateOverridesByCluster Optional (external cluster => server config map overrides)
  120. * map; respects the override precedence described above.
  121. * - masterTemplateOverrides Optional server config map overrides for masters;
  122. * respects the override precedence described above.
  123. * - templateOverridesByServer Optional (host => server config map overrides) map;
  124. * respects the override precedence described above
  125. * and applies to both core and external storage.
  126. * - loadMonitorClass Name of the LoadMonitor class to always use. [optional]
  127. * - readOnlyBySection Optional map of (section name => message text or false).
  128. * String values make sections read only, whereas anything
  129. * else does not restrict read/write mode.
  130. */
  131. public function __construct( array $conf ) {
  132. parent::__construct( $conf );
  133. $this->hostsByName = $conf['hostsByName'] ?? [];
  134. $this->sectionsByDB = $conf['sectionsByDB'];
  135. $this->groupLoadsBySection = $conf['groupLoadsBySection'] ?? [];
  136. foreach ( ( $conf['sectionLoads'] ?? [] ) as $section => $loadByHost ) {
  137. $this->groupLoadsBySection[$section][ILoadBalancer::GROUP_GENERIC] = $loadByHost;
  138. }
  139. $this->groupLoadsByDB = $conf['groupLoadsByDB'] ?? [];
  140. $this->externalLoads = $conf['externalLoads'] ?? [];
  141. $this->serverTemplate = $conf['serverTemplate'] ?? [];
  142. $this->externalTemplateOverrides = $conf['externalTemplateOverrides'] ?? [];
  143. $this->templateOverridesBySection = $conf['templateOverridesBySection'] ?? [];
  144. $this->templateOverridesByCluster = $conf['templateOverridesByCluster'] ?? [];
  145. $this->masterTemplateOverrides = $conf['masterTemplateOverrides'] ?? [];
  146. $this->templateOverridesByServer = $conf['templateOverridesByServer'] ?? [];
  147. $this->readOnlyBySection = $conf['readOnlyBySection'] ?? [];
  148. $this->loadMonitorClass = $conf['loadMonitorClass'] ?? LoadMonitor::class;
  149. }
  150. public function newMainLB( $domain = false, $owner = null ) {
  151. $section = $this->getSectionForDomain( $domain );
  152. if ( !isset( $this->groupLoadsBySection[$section][ILoadBalancer::GROUP_GENERIC] ) ) {
  153. throw new UnexpectedValueException( "Section '$section' has no hosts defined." );
  154. }
  155. $dbGroupLoads = $this->groupLoadsByDB[$this->getDomainDatabase( $domain )] ?? [];
  156. unset( $dbGroupLoads[ILoadBalancer::GROUP_GENERIC] ); // cannot override
  157. return $this->newLoadBalancer(
  158. array_merge(
  159. $this->serverTemplate,
  160. $this->templateOverridesBySection[$section] ?? []
  161. ),
  162. array_merge( $this->groupLoadsBySection[$section], $dbGroupLoads ),
  163. // Use the LB-specific read-only reason if everything isn't already read-only
  164. is_string( $this->readOnlyReason )
  165. ? $this->readOnlyReason
  166. : ( $this->readOnlyBySection[$section] ?? false ),
  167. $owner
  168. );
  169. }
  170. public function getMainLB( $domain = false ) {
  171. $section = $this->getSectionForDomain( $domain );
  172. if ( !isset( $this->mainLBs[$section] ) ) {
  173. $this->mainLBs[$section] = $this->newMainLB( $domain, $this->getOwnershipId() );
  174. }
  175. return $this->mainLBs[$section];
  176. }
  177. public function newExternalLB( $cluster, $owner = null ) {
  178. if ( !isset( $this->externalLoads[$cluster] ) ) {
  179. throw new InvalidArgumentException( "Unknown cluster '$cluster'" );
  180. }
  181. return $this->newLoadBalancer(
  182. array_merge(
  183. $this->serverTemplate,
  184. $this->externalTemplateOverrides,
  185. $this->templateOverridesByCluster[$cluster] ?? []
  186. ),
  187. [ ILoadBalancer::GROUP_GENERIC => $this->externalLoads[$cluster] ],
  188. $this->readOnlyReason,
  189. $owner
  190. );
  191. }
  192. public function getExternalLB( $cluster ) {
  193. if ( !isset( $this->externalLBs[$cluster] ) ) {
  194. $this->externalLBs[$cluster] =
  195. $this->newExternalLB( $cluster, $this->getOwnershipId() );
  196. }
  197. return $this->externalLBs[$cluster];
  198. }
  199. public function getAllMainLBs() {
  200. $lbs = [];
  201. foreach ( $this->sectionsByDB as $db => $section ) {
  202. if ( !isset( $lbs[$section] ) ) {
  203. $lbs[$section] = $this->getMainLB( $db );
  204. }
  205. }
  206. return $lbs;
  207. }
  208. public function getAllExternalLBs() {
  209. $lbs = [];
  210. foreach ( $this->externalLoads as $cluster => $unused ) {
  211. $lbs[$cluster] = $this->getExternalLB( $cluster );
  212. }
  213. return $lbs;
  214. }
  215. public function forEachLB( $callback, array $params = [] ) {
  216. foreach ( $this->mainLBs as $lb ) {
  217. $callback( $lb, ...$params );
  218. }
  219. foreach ( $this->externalLBs as $lb ) {
  220. $callback( $lb, ...$params );
  221. }
  222. }
  223. /**
  224. * @param bool|string $domain
  225. * @return string
  226. */
  227. private function getSectionForDomain( $domain = false ) {
  228. if ( $this->lastDomain === $domain ) {
  229. return $this->lastSection;
  230. }
  231. $database = $this->getDomainDatabase( $domain );
  232. $section = $this->sectionsByDB[$database] ?? self::CLUSTER_MAIN_DEFAULT;
  233. $this->lastSection = $section;
  234. $this->lastDomain = $domain;
  235. return $section;
  236. }
  237. /**
  238. * Make a new load balancer object based on template and load array
  239. *
  240. * @param array $serverTemplate Server config map
  241. * @param int[][] $groupLoads Map of (group => host => load)
  242. * @param string|bool $readOnlyReason
  243. * @param int|null $owner
  244. * @return LoadBalancer
  245. */
  246. private function newLoadBalancer( $serverTemplate, $groupLoads, $readOnlyReason, $owner ) {
  247. $lb = new LoadBalancer( array_merge(
  248. $this->baseLoadBalancerParams( $owner ),
  249. [
  250. 'servers' => $this->makeServerArray( $serverTemplate, $groupLoads ),
  251. 'loadMonitor' => [ 'class' => $this->loadMonitorClass ],
  252. 'readOnlyReason' => $readOnlyReason
  253. ]
  254. ) );
  255. $this->initLoadBalancer( $lb );
  256. return $lb;
  257. }
  258. /**
  259. * Make a server array as expected by LoadBalancer::__construct()
  260. *
  261. * @param array $serverTemplate Server config map
  262. * @param int[][] $groupLoads Map of (group => host => load)
  263. * @return array[] List of server config maps
  264. */
  265. private function makeServerArray( array $serverTemplate, array $groupLoads ) {
  266. // The master server is the first host explicitly listed in the generic load group
  267. if ( !$groupLoads[ILoadBalancer::GROUP_GENERIC] ) {
  268. throw new UnexpectedValueException( "Empty generic load array; no master defined." );
  269. }
  270. $groupLoadsByHost = $this->reindexGroupLoads( $groupLoads );
  271. // Get the ordered map of (host => load); the master server is first
  272. $genericLoads = $groupLoads[ILoadBalancer::GROUP_GENERIC];
  273. // Implictly append any hosts that only appear in custom load groups
  274. $genericLoads += array_fill_keys( array_keys( $groupLoadsByHost ), 0 );
  275. $servers = [];
  276. foreach ( $genericLoads as $host => $load ) {
  277. $servers[] = array_merge(
  278. $serverTemplate,
  279. $servers ? [] : $this->masterTemplateOverrides,
  280. $this->templateOverridesByServer[$host] ?? [],
  281. [
  282. 'host' => $this->hostsByName[$host] ?? $host,
  283. 'hostName' => $host,
  284. 'load' => $load,
  285. 'groupLoads' => $groupLoadsByHost[$host] ?? []
  286. ]
  287. );
  288. }
  289. return $servers;
  290. }
  291. /**
  292. * Take a group load array indexed by group then server, and reindex it by server then group
  293. * @param int[][] $groupLoads Map of (group => host => load)
  294. * @return int[][] Map of (host => group => load)
  295. */
  296. private function reindexGroupLoads( array $groupLoads ) {
  297. $reindexed = [];
  298. foreach ( $groupLoads as $group => $loadByHost ) {
  299. foreach ( $loadByHost as $host => $load ) {
  300. $reindexed[$host][$group] = $load;
  301. }
  302. }
  303. return $reindexed;
  304. }
  305. /**
  306. * @param DatabaseDomain|string|bool $domain Domain ID, or false for the current domain
  307. * @return string
  308. */
  309. private function getDomainDatabase( $domain = false ) {
  310. return ( $domain === false )
  311. ? $this->localDomain->getDatabase()
  312. : DatabaseDomain::newFromId( $domain )->getDatabase();
  313. }
  314. }