jetpants 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/README.rdoc +88 -0
- data/bin/jetpants +442 -0
- data/doc/commands.rdoc +119 -0
- data/doc/configuration.rdoc +27 -0
- data/doc/plugins.rdoc +120 -0
- data/doc/requirements.rdoc +54 -0
- data/etc/jetpants.yaml.sample +58 -0
- data/lib/jetpants.rb +100 -0
- data/lib/jetpants/callback.rb +131 -0
- data/lib/jetpants/db.rb +122 -0
- data/lib/jetpants/db/client.rb +103 -0
- data/lib/jetpants/db/import_export.rb +330 -0
- data/lib/jetpants/db/privileges.rb +89 -0
- data/lib/jetpants/db/replication.rb +226 -0
- data/lib/jetpants/db/server.rb +79 -0
- data/lib/jetpants/db/state.rb +212 -0
- data/lib/jetpants/host.rb +396 -0
- data/lib/jetpants/monkeypatch.rb +74 -0
- data/lib/jetpants/pool.rb +272 -0
- data/lib/jetpants/shard.rb +311 -0
- data/lib/jetpants/table.rb +146 -0
- data/lib/jetpants/topology.rb +144 -0
- data/plugins/simple_tracker/db.rb +23 -0
- data/plugins/simple_tracker/pool.rb +70 -0
- data/plugins/simple_tracker/shard.rb +76 -0
- data/plugins/simple_tracker/simple_tracker.rb +74 -0
- data/plugins/simple_tracker/topology.rb +66 -0
- data/tasks/promotion.rb +260 -0
- metadata +191 -0
data/doc/commands.rdoc
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
= Jetpants command suite
|
2
|
+
|
3
|
+
Run <tt>jetpants</tt> without any parameters to see a list of all commands available in the suite.
|
4
|
+
|
5
|
+
Please note that all \Jetpants commands that take a database node require you to specify it <b>by IP address</b>, rather than hostname. It's usually a good practice to list databases in your application configuration by IP anyway, to remove DNS as a potential bottleneck and single point of failure. By also keeping all commands in \Jetpants by IP, it's easier to confirm that your commands had the expected results by looking at the diff of your regenerated application config file, for example.
|
6
|
+
|
7
|
+
Here's a more thorough description of the commands, grouped by function:
|
8
|
+
|
9
|
+
== Slave cloning
|
10
|
+
|
11
|
+
<b><tt>jetpants clone_slave</tt></b> copies data from a standby slave onto one or more fresh nodes, turning them into additional standby slaves of the same master.
|
12
|
+
|
13
|
+
\Jetpants copies data sets by shutting down the MySQL daemon on the source node and then copying all MySQL files. This is the fastest way to clone MySQL data sets, and is part of the reason why we recommend having 2 standby slaves per pool for high availability.
|
14
|
+
|
15
|
+
The copy method in \Jetpants uses a combination of tar, netcat (nc), and pigz. It does not use encryption; we assume you are transferring over a secure local network. When copying to multiple destinations, \Jetpants creates a "copy chain" using tee and a fifo. For more information on this technique, please see {our post on the Tumblr engineering blog}[http://engineering.tumblr.com/post/7658008285/efficiently-copying-files-to-multiple-destinations].
|
16
|
+
|
17
|
+
This command does not require an asset tracker plugin, but DOES require that all your database nodes have pigz (a parallel gzip tool) installed. This may become configurable in a future release.
|
18
|
+
|
19
|
+
|
20
|
+
== Master/slave state changes
|
21
|
+
|
22
|
+
These commands change the type of a slave, or promote a slave to be a master. <tt>jetpants promotion</tt> does not require an asset tracker, but the other commands all do.
|
23
|
+
|
24
|
+
<b><tt>jetpants promotion</tt></b> changes which node in a pool is the master by performing a full MySQL master promotion. This is usable even if the old master is offline or unavailable. All nodes in the pool will now slave off of the new master. If the old master is online/available, it will become a standby slave of the new master.
|
25
|
+
|
26
|
+
Please note that the master promotion process enables global READ_ONLY mode on the old master. This is a required step of the standard MySQL master promotion technique. After doing a promotion in \Jetpants, you'll need to update/deploy your application's configuration as quickly as possible, if a plugin doesn't do it automatically for you.
|
27
|
+
|
28
|
+
<b><tt>jetpants activate_slave</tt></b> turns a standby slave into an active slave. Use this if you want to generate an app configuration that now sends read queries to a slave that formerly did not receive them.
|
29
|
+
|
30
|
+
<b><tt>jetpants weigh_slave</tt></b> alters the weight of an active slave. This is only useful if application supports having read slaves with different selection weights.
|
31
|
+
|
32
|
+
<b><tt>jetpants pull_slave</tt></b> turns an active slave into a standby slave. Use this if you want to generate an app configuration file that stops sending read queries to a slave.
|
33
|
+
|
34
|
+
<b><tt>jetpants destroy_slave</tt></b> removes a standby slave from its pool entirely, stopping and then resetting replication on the node. Use this prior to recycling or canceling a node that is no longer needed.
|
35
|
+
|
36
|
+
|
37
|
+
== Interactive REPL
|
38
|
+
|
39
|
+
<b><tt>jetpants console</tt></b> starts an <tt>irb</tt> session that is already in the Jetpants module namespace.
|
40
|
+
|
41
|
+
If you're using an asset tracker, this is an easy way to programmatically interact with your database topology.
|
42
|
+
This is especially true because the Jetpants module delegates missing methods to its Jetpants::Topology singleton. For example:
|
43
|
+
|
44
|
+
# print summary data on all pools
|
45
|
+
> pools.each &:summary
|
46
|
+
|
47
|
+
# print condensed info on shard pools only
|
48
|
+
> shards.each {|s| print "[%-12s] %8s to %-11s = %-4s\n" % [s.ip, s.min_id, s.max_id, s.data_set_size(true)]}
|
49
|
+
|
50
|
+
The first time you install \Jetpants and an asset tracker, you can enter your pool information through the console:
|
51
|
+
|
52
|
+
> p = Pool.new('my-pool-name', '1.2.3.4') # supply name and master IP
|
53
|
+
> p.sync_configuration # update the asset tracker data
|
54
|
+
|
55
|
+
Or if you're deploying a brand new pool in an existing topology:
|
56
|
+
|
57
|
+
> master = claim_spare
|
58
|
+
> slaves = claim_spares(2)
|
59
|
+
> p = Pool.new('my-new-pool', master)
|
60
|
+
> slaves.each {|s| s.change_master_to(master); s.start_replication}
|
61
|
+
> p.sync_configuration
|
62
|
+
|
63
|
+
== Shard rebalancing
|
64
|
+
|
65
|
+
\Jetpants allows you to split a shard into N pieces. This is a fairly complex process; the steps look roughly like this:
|
66
|
+
|
67
|
+
1. Create N new slaves of the shard being split (the "parent" shard). These new slaves will become masters of their own "child" shards later in the process.
|
68
|
+
2. Reduce the data set on those slaves so that each contains a different subset of the data. This is done by exporting the data subset, dropping the tables, re-creating the tables, and importing the data subset back in.
|
69
|
+
3. Move app reads from the parent shard to the appropriate child shards. You must move reads before moving writes in order to maintain consistency in your application. Writes will continue to go to the parent shard, which then replicates to the child shards.
|
70
|
+
4. Move app writes from the parent shard to the appropriate child shards.
|
71
|
+
5. Stop replicating writes from the parent shard, and then take the parent pool offline entirely.
|
72
|
+
6. Remove rows that replicated to the wrong child shard. This data will be sparse, since it's only the writes that were made since the shard split process started.
|
73
|
+
|
74
|
+
For more information, including diagrams of each step, please see {our presentation at Velocity Europe 2011}[https://github.com/tumblr/jetpants/blob/master/doc/VelocityEurope2011Presentation.pdf?raw=true].
|
75
|
+
|
76
|
+
Separately, \Jetpants also allows you to alter the range of the last shard in your topology. In a range-based sharding scheme, the last shard has a range of X to infinity; eventually this will be too large of a range, so you need to truncate that shard range and create a "new" last shard after it. We call this process "shard cutover".
|
77
|
+
|
78
|
+
These commands all require an asset tracker in order to function.
|
79
|
+
|
80
|
+
<b><tt>jetpants shard_split</tt></b> performs the first steps of the split: spins up the new shard pools with the appropriate portions of the data set. The new shards will still be slaving from their parent though. This step potentially takes several hours, so you may want to use a tool like screen to prevent SSH timeouts.
|
81
|
+
|
82
|
+
<b><tt>jetpants shard_split_child_reads</tt></b> regenerates your application config file to move reads to the new child shards.
|
83
|
+
|
84
|
+
<b><tt>jetpants shard_split_child_writes</tt></b> regenerates your application config file to move writes to the new child shards. Be sure to move reads first.
|
85
|
+
|
86
|
+
<b><tt>jetpants shard_split_cleanup</tt></b> tears down replication between the parent and child shards, makes the parent shard non-readable and non-writable, and then cleans up data that replicated to the wrong child. This may take a little while to run, although typically much less than the initial <tt>jetpants shard_split</tt> takes since data will be sparse.
|
87
|
+
|
88
|
+
<b><tt>jetpants shard_cutover</tt></b> truncates the current last shard range, and adds a new shard after it. You need to pick what ID is being used for the cutover; this ID <b>MUST</b> be one "in the future", ie, one your application hasn't hit yet. The cutover process does NOT move any data, since it assumes the new shard it's creating has no data yet. What the cutover process will do is allocate the new shard's hardware, set up replication, create your sharded tables, and update your app configuration file.
|
89
|
+
|
90
|
+
|
91
|
+
== Shard degradation
|
92
|
+
|
93
|
+
With an asset tracker, \Jetpants allows you to mark a shard as read-only or completely offline, assuming your application supports these notions. (Which, of course, it should -- this is one of the major benefits of a sharded infrastructure!)
|
94
|
+
|
95
|
+
<b><tt>jetpants shard_read_only</tt></b> marks a shard as not writable.
|
96
|
+
|
97
|
+
<b><tt>jetpants shard_offline</tt></b> marks a shard as completely unavailable.
|
98
|
+
|
99
|
+
<b><tt>jetpants shard_online</tt></b> marks a shard as being fully online again, after a prior call to <tt>jetpants shard_read_only</tt> or <tt>jetpants shard_offline</tt>.
|
100
|
+
|
101
|
+
|
102
|
+
== Informational commands
|
103
|
+
|
104
|
+
These commands do not require an asset tracker. They may be fleshed out further in a future release of \Jetpants.
|
105
|
+
|
106
|
+
<b><tt>jetpants node_info</tt></b> displays information on a MySQL instance.
|
107
|
+
|
108
|
+
<b><tt>jetpants show_master</tt></b> tells you what node is the master of a given MySQL instance.
|
109
|
+
|
110
|
+
<b><tt>jetpants show_slaves</tt></b> displays a list of slaves (along with current slave lag) of a MySQL instance.
|
111
|
+
|
112
|
+
|
113
|
+
== Miscellaneous
|
114
|
+
|
115
|
+
<b><tt>jetpants rebuild_slave</tt></b> exports all data on a standby slave, drops tables, recreates tables, and then re-imports the data. This is useful for defragmenting a node. Currently it only works on shard pool slaves; this may change in a future release.
|
116
|
+
|
117
|
+
<b><tt>jetpants regen_config</tt></b> regenerates your application's configuration file, assuming you are using an asset tracker. This isn't terribly useful in most cases, because any commands that make configuration changes will do this automatically. However, you may want to run this after manually making topology changes via <tt>jetpants console</tt>.
|
118
|
+
|
119
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
= Jetpants Configuration
|
2
|
+
|
3
|
+
\Jetpants supports a global configuration file at <tt>/etc/jetpants.yaml</tt>, as well as per-user configuration files at <tt>~/.jetpants.yaml</tt>.
|
4
|
+
|
5
|
+
At least one of these files must exist for \Jetpants to function properly.
|
6
|
+
|
7
|
+
If both exist, the user configuration will be merged on top of the global configuration, but please note that this is not a "deep" merge. So if the user configuration defines a "plugins" section, this will used as-is and the global "plugins" section will be ignored. This may change in a future release.
|
8
|
+
|
9
|
+
For an example global configuration file, please see the included <tt>etc/jetpants.yaml.sample</tt> file.
|
10
|
+
|
11
|
+
== Configuration settings
|
12
|
+
|
13
|
+
max_concurrency:: Maximum threads to use in import/export operations, or equivalently the maximum connection pool size per database host (default: 40)
|
14
|
+
standby_slaves_per_pool:: Minimum number of standby slaves to keep in every pool (default: 2)
|
15
|
+
mysql_schema:: database name (mandatory)
|
16
|
+
mysql_app_user:: mysql user that your application uses (mandatory)
|
17
|
+
mysql_app_password:: mysql password that your application uses (mandatory)
|
18
|
+
mysql_repl_user:: mysql user for replication (mandatory)
|
19
|
+
mysql_repl_password:: mysql password for replication (mandatory)
|
20
|
+
mysql_root_password:: mysql root password (default: false, indicating that \Jetpants should use /root/.my.cnf instead)
|
21
|
+
mysql_grant_ips:: mysql user manipulations are applied to these IPs (array; mandatory)
|
22
|
+
mysql_grant_privs:: mysql user manipulations grant this set of privileges by default (array; default: \['ALL'])
|
23
|
+
export_location:: directory to use for data dumping (default: '/tmp')
|
24
|
+
verify_replication:: raise exception if the actual replication topology differs from Jetpants' understanding of it (ie, disagreement between asset tracker and probed state), or if MySQL's two replication threads are in different states (one running and the other stopped) on a DB node. (default: true. master promotion tool ignores this, since the demoted master may legitimately be dead/offline)
|
25
|
+
plugins:: hash of plugin name => arbitrary plugin data, usually a nested hash of settings (default: \{})
|
26
|
+
ssh_keys:: array of SSH private key file locations, if not using standard id_dsa or id_rsa. Passed directly to Net::SSH.start's :keys parameter (default: nil)
|
27
|
+
sharded_tables:: array of name => \{sharding_key=>X, chunks=>Y} hashes, describing all tables on shards. Required by shard split/rebuild processes (default: \[])
|
data/doc/plugins.rdoc
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
= Jetpants Plugins
|
2
|
+
|
3
|
+
\Jetpants offers an extensible plugin system. Plugins are Ruby code (such as stand-alone gems) that add to \Jetpants by supplying callback methods, and/or overriding core methods.
|
4
|
+
|
5
|
+
== Recommended Uses
|
6
|
+
|
7
|
+
It is highly recommended that you tie \Jetpants into your site's asset tracker (ie, hardware management system) by writing a custom plugin. This will allow Jetpants to automatically know what database pools and shards are present, and to make topological changes immediately be reflected in your site's configuration.
|
8
|
+
|
9
|
+
Other recommended uses of plugins include integration with your site's monitoring system, trending system, query killers, and environment-specific overrides to various core methods.
|
10
|
+
|
11
|
+
== Asset tracker
|
12
|
+
|
13
|
+
=== Example
|
14
|
+
|
15
|
+
We supply a sample plugin called simple_tracker, demonstrating how to go about writing a very basic asset-tracking plugin. This plugin just uses an internal JSON file to keep track of database topology/state, and separately writes app configuration to a YAML file. This isn't actually suitable for production use, but should provide a reasonable starting point for learning the plugin system and building a real asset tracker.
|
16
|
+
|
17
|
+
To use simple_tracker, be sure to define its <tt>tracker_data_file_path</tt> (where it saves its internal asset-tracking data) and <tt>app_config_file_path</tt> (where it exports web app config files). If you're wondering why these are kept separate, it's to allow you to support pool topology changes that don't immediately impact your application, in case you're doing a complex multi-step migration.
|
18
|
+
|
19
|
+
For an example of how to configure the plugin, please see the bottom of etc/jetpants.yaml.sample.
|
20
|
+
|
21
|
+
When you first start using simple_tracker, there will be no pools, shards, or spare machines. You can start to add some via <tt>jetpants console</tt>, or you can write a custom script to import data from another source or config file. Either way, some examples of how to add things include:
|
22
|
+
|
23
|
+
# Create a global pool (functional partition)
|
24
|
+
p = Pool.new('some-name', '10.45.67.220')
|
25
|
+
|
26
|
+
# The slaves of the master will be detected automatically,
|
27
|
+
# but they will all be assumed to be standby slaves by
|
28
|
+
# default. You have to tell Jetpants which ones are active
|
29
|
+
# slaves:
|
30
|
+
p.has_active_slave('10.45.67.226')
|
31
|
+
|
32
|
+
# save it
|
33
|
+
p.sync_configuration
|
34
|
+
|
35
|
+
# Create a shard + save it
|
36
|
+
s = Shard.new(1, 100000, '10.45.67.160', :ready)
|
37
|
+
s.sync_configuration
|
38
|
+
|
39
|
+
# add some spare machines
|
40
|
+
Jetpants.topology.tracker.spares << '10.45.67.130'
|
41
|
+
Jetpants.topology.tracker.spares << '10.45.67.132'
|
42
|
+
|
43
|
+
# If you're using jetpants console, this works too, since console runs in Jetpants
|
44
|
+
# module namespace, which then delegates missing methods to its topology object:
|
45
|
+
tracker.spares << '10.45.67.134'
|
46
|
+
|
47
|
+
|
48
|
+
=== Methods to override
|
49
|
+
|
50
|
+
If you're writing your own asset-tracker plugin, you will need to override the following methods:
|
51
|
+
|
52
|
+
* Jetpants::Topology#load_pools
|
53
|
+
* Reads data from an asset tracker to initialize all pools and shards.
|
54
|
+
* Jetpants::Topology#write_config
|
55
|
+
* Writes a configuration file for your webapp, or commits configuration changes to a service, depending on how your site handles configuration knowledge
|
56
|
+
* Jetpants::Topology#claim_spares
|
57
|
+
* Obtains spare database nodes and marks them as no longer being spare
|
58
|
+
* Jetpants::Topology#count_spares
|
59
|
+
* Returns a count of spare database nodes
|
60
|
+
* Jetpants::Pool#sync_configuration
|
61
|
+
* Updates the asset tracker with the current status of a pool.
|
62
|
+
* This should update the asset tracker's internal knowledge of the database topology immediately, but not necessarily cause the application's config file to be regenerated immediately.
|
63
|
+
|
64
|
+
You may also want to override or implement these, though it's not strictly mandatory:
|
65
|
+
|
66
|
+
* Jetpants::DB#for_backups?
|
67
|
+
* Defines how to tell the different between standby slaves and backup slaves
|
68
|
+
* Skip this if your architecture doesn't include backup slaves, or if the default implementation (hostnames starting with "backup") is sufficient
|
69
|
+
* Jetpants::DB#after_probe_slaves
|
70
|
+
* In order to support master promotions when a master has failed, you will need to implement this to set a slave list from your asset tracker if a MySQL instance isn't available
|
71
|
+
* Jetpants::DB#after_probe_master
|
72
|
+
* In order to support operations on a slave that has failed, you will need to implement this to set @master based on information in your asset tracker if a MySQL instance isn't available
|
73
|
+
* Jetpants::Pool#after_remove_slave!
|
74
|
+
* If your implementation of Jetpants::Pool#sync_configuration works by iterating over the current slaves of the pool's master, it won't correctly handle the <tt>jetpants destroy_slave</tt> command,
|
75
|
+
or anything else that calls Jetpants::DB#disable_replication! which does a RESET SLAVE. You can instead handle that special case here.
|
76
|
+
|
77
|
+
== How to write a plugin
|
78
|
+
|
79
|
+
=== Name and location
|
80
|
+
|
81
|
+
If you define a plugin named "foo" in your \Jetpants config file, on startup \Jetpants will first attempt to require 'foo/foo', and failing that simply 'foo'.
|
82
|
+
|
83
|
+
Plugins may be located anywhere on your Ruby load path, so packing them as standard gems should work perfectly. You may want to prefix the gem name with "\jetpants_" to avoid conflicts with other gems. \Jetpants also adds its plugins directory to its load path automatically, so that any pre-bundled plugins (like simple_tracker) can be loaded easily.
|
84
|
+
|
85
|
+
=== Recommended format
|
86
|
+
|
87
|
+
The \Jetpants plugin system is designed to be as light and simple as possible. Plugins are just normal Ruby code that gets required after the rest of \Jetpants has been initialized. This allows you to override any method in any \Jetpants class. Simply re-open the class as normal in Ruby.
|
88
|
+
|
89
|
+
The base of your plugin may use the Jetpants::Plugin module as an outer namespace, but doing so is not required.
|
90
|
+
|
91
|
+
=== Callbacks
|
92
|
+
|
93
|
+
To make it easier to chain behavior before or after existing \Jetpants methods, all core \Jetpants classes include a mix-in (Jetpants::CallbackHandler) that adds automatic callback functionality. For example, any time you call method Jetpants::DB#foo, it will magically first call method Jetpants::DB#before_foo if it exists. Similarly, after the foo method finishes, it will magically call method Jetpants::DB#after_foo if it exists.
|
94
|
+
|
95
|
+
A particularly nice aspect of callbacks is that they "stack". For example, if two plugins both implement Jetpants::DB#before_foo, they will <b>both</b> be called before Jetpants::DB#foo. You can even specify the priority of a callback (higher = called first, default is 100) by preceding it with the Jetpants::CallbackHandler.callback_priority decorator. For example:
|
96
|
+
|
97
|
+
module Jetpants
|
98
|
+
class DB
|
99
|
+
callback_priority 150
|
100
|
+
def after_query(*args)
|
101
|
+
puts "After calling DB#query, I am called next due to my high priority"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
module Jetpants
|
106
|
+
class DB
|
107
|
+
callback_priority 85
|
108
|
+
def after_query(*args)
|
109
|
+
puts "After calling DB#query, other plugins' callbacks with higher priorities get called, then I get called"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
Callbacks receive the same parameter list as the base method was called with. Their return values are ignored.
|
115
|
+
|
116
|
+
Callbacks may interrupt the control flow by raising a Jetpants::CallbackAbortError exception:
|
117
|
+
|
118
|
+
* If this is raised in a before_foo method, all lower-priority before_foo stacked callbacks will be skipped, as will the call to foo and any after_foo callbacks. The exception is automatically caught and is not fatal. The return value of foo will be nil.
|
119
|
+
|
120
|
+
* If this is raised in an after_foo method, all subsequent lower-priority after_foo stacked callbacks will be skipped. The return value of foo will be unaffected.
|
@@ -0,0 +1,54 @@
|
|
1
|
+
= Jetpants Requirements and Assumptions
|
2
|
+
|
3
|
+
The base classes of \Jetpants currently make a number of assumptions about your environment and database topology.
|
4
|
+
|
5
|
+
Plugins may freely override these assumptions, and upstream patches are very welcome to incorporate support for alternative configurations. We're especially interested in plugins or pull requests that add support for: Postgres and other relational databases; Redis and other non-relational data stores; non-Redhat Linux distributions or *BSD operating systems; master-master topologies; multi-instance-per-host setups; etc. We have attempted to design \Jetpants in a way that is sufficiently flexible to eventually support a wide range of environments.
|
6
|
+
|
7
|
+
== Environment
|
8
|
+
|
9
|
+
* Using MySQL (or Percona Server), specifically version 5.1 or higher.
|
10
|
+
* Using a RHEL/CentOS distribution of Linux.
|
11
|
+
* It should be easy to write a plugin supporting another distribution. The main change might be overriding Jetpants::Host#service, if your distribution doesn't have <tt>/sbin/service</tt>.
|
12
|
+
* Using InnoDB / Percona XtraDB for storage engine. \Jetpants has not been tested with MyISAM, since \Jetpants is geared towards huge tables, and MyISAM is generally a bad fit.
|
13
|
+
* All MySQL instances run on port 3306, with only one instance per logical machine.
|
14
|
+
* A plugin could override this easily, but would require you to use the --report-host option on all slaves, so that crawling the replication topology is possible. It would also have to override various methods that specify the MySQL init script location, config file location, data directory, etc.
|
15
|
+
* Since there's no "standard" layout for multi-instance MySQL, this won't ever be part of the \Jetpants core, but we may include one implementation as a bundled plugin in a future release.
|
16
|
+
* Master-Master replication is not in use. It presents a large number of additional failure states which make automation quite difficult, since transaction IDs are not global in MySQL.
|
17
|
+
* Unusual replication topologies -- special-purpose slaves, ring topologies, hierarchical replication -- are not in use. Plugins could certainly override this by adding additional types of slaves, but there are too many options for this to be part of the \Jetpants core.
|
18
|
+
* You must be running \Jetpants as a UNIX user with access to an SSH private key, capable of connecting as root to any host in your database topology. If this key has a nonstandard name or location, you must specify this using the <tt>ssh_keys</tt> option in the Jetpants configuration file. SSH agent forwarding should work fine automatically, but be careful of its interaction with UNIX tools like <tt>screen</tt>.
|
19
|
+
* The root MySQL user must be able to access the database on localhost via MySQL's UNIX socket. You may specify the root password with Jetpants' <tt>mysql_root_password</tt> option in the configuration file. Or if all of your database hosts has a <tt>/root/.my.cnf</tt> file specifying the root password, you can omit <tt>mysql_root_password</tt> in the configuration file.
|
20
|
+
* You must have <tt>pigz</tt> (an open-source parallel gzip tool by Mark Adler) installed on all database hosts. A future version of \Jetpants will allow pluggable compression tools, but at present we strictly use <tt>pigz</tt> for compression in all file copy operations.
|
21
|
+
|
22
|
+
|
23
|
+
== Database topology
|
24
|
+
|
25
|
+
We define a database <b>pool</b> as 1 master and 0 or more slaves. Each slave falls into one of three classifications:
|
26
|
+
* Active slaves: replicas that actively receive reads from your application(s)
|
27
|
+
* Standby slaves: replicas that exist for high availability, to replace failed nodes or to quickly spin up new standby replacements.
|
28
|
+
* Backup slaves: replicas that never receive application queries and will never be promoted to another class. For example, might be a different hardware spec.
|
29
|
+
|
30
|
+
At Tumblr we ensure that all pools have exactly 2 standby slaves. This establishes a minimum level of redundancy at 3 copies of each row. If a master or active slave fails, we use \Jetpants to promote one standby slave in its place, and use the other standby slave to quickly clone a replacement. (\Jetpants clones nodes by stopping the MySQL daemon and then copying the data set; this is substantially faster than using a hot-copy solution, especially on very large and/or very actively written data sets.)
|
31
|
+
|
32
|
+
We no longer use dedicated backup slaves at Tumblr, although we still offer support in \Jetpants for them.
|
33
|
+
|
34
|
+
== Sharding scheme
|
35
|
+
|
36
|
+
We define a database <b>shard</b> as a specialized type of pool, always consisting of 1 master and 2 standby slaves. All shards contain the same set of tables, but each stores a different partition of the data set, divided by ID ranges of a global sharding key.
|
37
|
+
|
38
|
+
For example, say your application's natural sharding key is <tt>user_id</tt>. The <tt>users</tt> table itself might not be shardable (since it may require global lookup patterns like find-by-email), but any other table containing a <tt>user_id</tt> column could be sharded. One shard might contain all sharded table data for users 1 through 1000000; another shard would contain data for users 1000001 through 2000000; etc. These ranges may be of uneven sizes. When disk usage approaches capacity or I/O utilization gets too high, \Jetpants can be used to split the shard into N new "child" shards of equal or unequal ranges.
|
39
|
+
|
40
|
+
We prefer this range-based scheme due to its simplicity. The ranges can be expressed in a language-neutral JSON or YAML format, which can be shared between application stacks easily. \Jetpants solves all data rebalancing problems for you. There's no need to "pre-allocate" thousands of tiny shards, nor do you need an external lookup service which would be a bottleneck and a single point of failure.
|
41
|
+
|
42
|
+
Under this sharding scheme, there will always be a "last" shard, with a range of X to infinity. All new users (or whatever your sharding key is) will have their data placed on this shard. Once there are too many users on this shard, it becomes necessary to truncate its range such that it handles IDs X through Y, where Y is an ID "in the future" (ie, sufficiently higher than your current max ID such that you won't hit ID Y for another few days). You must then add a new last shard handles IDs (Y+1) to infinity. We call this process "shard cutover", and it is a supported operation in the Jetpants command suite.
|
43
|
+
|
44
|
+
|
45
|
+
== Assumptions for shard-split logic
|
46
|
+
|
47
|
+
In order for \Jetpants to be able to rebalance your shards efficiently, we also must assume the following:
|
48
|
+
|
49
|
+
* No auto_increment columns in any sharded table. Use an external ID generation service instead. Build your own (it's a hundred lines of C) or use Memcached or Redis or Snowflake or any other number of options.
|
50
|
+
* Primary key of all sharded tables begins with the sharding key. This takes advantage of InnoDB clustered indexes to allow efficient export/import of data by ranges. This isn't strictly mandatory, but it makes an absolutely huge performance difference versus using a secondary index.
|
51
|
+
* Uniform MySQL configuration between masters and slaves: log-slave-updates, unique server-id, generic log-bin and relay-log names, replication user/grants everywhere.
|
52
|
+
* Redundant rows temporarily on the wrong shard don’t matter to your application. Hopefully these would only matter if you're doing aggregate queries (like COUNTs) over the whole data set... but if your data set is big enough to shard, these already don't scale, so it shouldn't really matter.
|
53
|
+
|
54
|
+
For more information on the shard split process implemented by \Jetpants, including diagrams of each stage of the process, please see {Evan Elias's presentation at Velocity Europe 2011}[https://github.com/tumblr/jetpants/blob/master/doc/VelocityEurope2011Presentation.pdf?raw=true], starting at slide 19.
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Jetpants supports a global settings file at /etc/jetpants.yaml,
|
2
|
+
# as well as user-specific settings files at ~/.jetpants.yaml
|
3
|
+
|
4
|
+
mysql_schema: mydb
|
5
|
+
mysql_app_user: myuser
|
6
|
+
mysql_app_password: foo
|
7
|
+
mysql_repl_user: replication_user
|
8
|
+
mysql_repl_password: bar
|
9
|
+
|
10
|
+
# Users created by Jetpants are given access from these IPs
|
11
|
+
mysql_grant_ips: [10.%, 192.168.%]
|
12
|
+
|
13
|
+
# Users created by Jetpants are given these MySQL privileges
|
14
|
+
mysql_grant_privs:
|
15
|
+
- SELECT
|
16
|
+
- INSERT
|
17
|
+
- UPDATE
|
18
|
+
- DELETE
|
19
|
+
- REFERENCES
|
20
|
+
- CREATE TEMPORARY TABLES
|
21
|
+
- LOCK TABLES
|
22
|
+
- EXECUTE
|
23
|
+
|
24
|
+
# Define what directory to put exported files in. If you have /var/lib/mysql
|
25
|
+
# on an SSD RAID mount and the rest of your OS on a rotational disk mount,
|
26
|
+
# it's fine to put the export_location on the rotational disk, which probably
|
27
|
+
# has higher capacity anyway.
|
28
|
+
export_location: /some/path/on/root/partition
|
29
|
+
|
30
|
+
# List all tables defined on the shards, along with what column name corresponds
|
31
|
+
# to your app's sharding key (to determine which shard a given row lives on),
|
32
|
+
# and how many "chunks" to split the data set into when doing an import or
|
33
|
+
# export.
|
34
|
+
sharded_tables:
|
35
|
+
posts:
|
36
|
+
sharding_key: user_id
|
37
|
+
chunks: 200
|
38
|
+
likes:
|
39
|
+
sharding_key: from_user_id
|
40
|
+
chunks: 100
|
41
|
+
some_small_table:
|
42
|
+
sharding_key: user_id
|
43
|
+
# chunks defaults to 1 if omitted.
|
44
|
+
following:
|
45
|
+
sharding_key: [from_user_id, to_user_id]
|
46
|
+
# multiple sharding key columns: each row may have to exist on two shards
|
47
|
+
# omit chunks since cannot use chunked export/import with multiple sharding keys
|
48
|
+
|
49
|
+
plugins:
|
50
|
+
simple_tracker:
|
51
|
+
tracker_data_file_path: /var/jetpants/assets.json
|
52
|
+
app_config_file_path: /var/www/your-web-app/databases.yaml
|
53
|
+
|
54
|
+
plugin_with_no_settings:
|
55
|
+
|
56
|
+
plugin_with_settings:
|
57
|
+
foo: bar
|
58
|
+
flim: flam
|
data/lib/jetpants.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'net/ssh'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Jetpants; end
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), 'jetpants'), File.join(File.dirname(__FILE__), '..', 'plugins')
|
8
|
+
%w(callback table host db pool topology shard monkeypatch).each {|g| require g}
|
9
|
+
|
10
|
+
# Since Jetpants is extremely multithreaded, we need to force uncaught exceptions to
|
11
|
+
# kill all threads in order to have any kind of sane error handling.
|
12
|
+
Thread.abort_on_exception = true
|
13
|
+
|
14
|
+
# Namespace for the Jetpants toolkit.
|
15
|
+
module Jetpants
|
16
|
+
|
17
|
+
# Establish default configuration values, and then merge in whatever we find globally
|
18
|
+
# in /etc/jetpants.yaml and per-user in ~/.jetpants.yaml
|
19
|
+
@config = {
|
20
|
+
'max_concurrency' => 40, # max threads/conns per database
|
21
|
+
'standby_slaves_per_pool' => 2, # number of standby slaves in every pool
|
22
|
+
'mysql_schema' => 'test', # database name
|
23
|
+
'mysql_app_user' => false, # mysql user for application
|
24
|
+
'mysql_app_password' => false, # mysql password for application
|
25
|
+
'mysql_repl_user' => false, # mysql user for replication
|
26
|
+
'mysql_repl_password' => false, # mysql password for replication
|
27
|
+
'mysql_root_password' => false, # mysql root password. omit if specified in /root/.my.cnf instead.
|
28
|
+
'mysql_grant_ips' => ['192.168.%'], # mysql user manipulations are applied to these IPs
|
29
|
+
'mysql_grant_privs' => ['ALL'], # mysql user manipulations grant this set of privileges by default
|
30
|
+
'export_location' => '/tmp', # directory to use for data dumping
|
31
|
+
'verify_replication' => true, # raise exception if the 2 repl threads are in different states, or if actual repl topology differs from Jetpants' understanding of it
|
32
|
+
'plugins' => {}, # hash of plugin name => arbitrary plugin data (usually a nested hash of settings)
|
33
|
+
'ssh_keys' => nil, # array of SSH key file locations
|
34
|
+
'sharded_tables' => [], # array of name => {sharding_key=>X, chunks=>Y} hashes
|
35
|
+
}
|
36
|
+
%w(/etc/jetpants.yaml ~/.jetpants.yml ~/.jetpants.yaml).each do |path|
|
37
|
+
overrides = YAML.load_file(File.expand_path path) rescue {}
|
38
|
+
@config.merge! overrides
|
39
|
+
end
|
40
|
+
|
41
|
+
class << self
|
42
|
+
# A singleton Jetpants::Topology object is accessible from the global
|
43
|
+
# Jetpants module namespace.
|
44
|
+
attr_reader :topology
|
45
|
+
|
46
|
+
# Returns true if the specified plugin is enabled, false otherwise.
|
47
|
+
def plugin_enabled?(plugin_name)
|
48
|
+
!!@config['plugins'][plugin_name]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a hash containing :user => username string, :pass => password string
|
52
|
+
# for the MySQL application user, as found in Jetpants' configuration. Plugins
|
53
|
+
# may freely override this if there's a better way to obtain this password --
|
54
|
+
# for example, if you already distribute an application configuration or
|
55
|
+
# credentials file to all of your servers.
|
56
|
+
def app_credentials
|
57
|
+
{user: @config['mysql_app_user'], pass: @config['mysql_app_password']}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns a hash containing :user => username string, :pass => password string
|
61
|
+
# for the MySQL replication user, as found in Jetpants' configuration. Plugins
|
62
|
+
# may freely override this if there's a better way to obtain this password --
|
63
|
+
# for example, by parsing master.info on a slave in your topology.
|
64
|
+
def replication_credentials
|
65
|
+
{user: @config['mysql_repl_user'], pass: @config['mysql_repl_password']}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Proxy missing top-level Jetpants methods to the configuration hash,
|
69
|
+
# or failing that, to the Topology singleton.
|
70
|
+
def method_missing(name, *args, &block)
|
71
|
+
if @config.has_key? name.to_s
|
72
|
+
@config[name.to_s]
|
73
|
+
elsif name.to_s[-1] == '=' && @config.has_key?(name.to_s[0..-2])
|
74
|
+
var = name.to_s[0..-2]
|
75
|
+
@config[var] = args[0]
|
76
|
+
elsif @topology.respond_to? name
|
77
|
+
@topology.send name, *args, &block
|
78
|
+
else
|
79
|
+
super
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def respond_to?(name, include_private=false)
|
84
|
+
super || @config[name] || @topology.respond_to?(name)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Load plugins at this point, to allow them to override any of the previously-defined methods
|
89
|
+
# in any of the loaded classes
|
90
|
+
(@config['plugins'] || {}).each do |name, attributes|
|
91
|
+
begin
|
92
|
+
require "#{name}/#{name}"
|
93
|
+
rescue LoadError
|
94
|
+
require name
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Finally, initialize topology object
|
99
|
+
@topology = Topology.new
|
100
|
+
end
|