[Dbix-class] DBIx::Class::Storage::DBI::Replicated - read from master

John Napiorkowski jjn1056 at yahoo.com
Wed Jun 9 20:15:03 GMT 2010




>
>From: Bill Moseley <moseley at hank.org>
>To: dbix-class <dbix-class at lists.scsys.co.uk>
>Sent: Tue, June 8, 2010 12:31:16 AM
>Subject: [Dbix-class] DBIx::Class::Storage::DBI::Replicated - read from master
>
>
>Sanity check.
>
>
>I need to have DBIC work with an existing database that includes slaves.  As the Replicated docs note, there's often a replication delay from master to slaves.  So, when a write happens I want to direct all reads to the master for some period of time (for the specific user that did the write).  In addition, I need a way to communicate to other applications/processes that they also need to use the master for any requests by that user.

Interesting idea.  The way DBIC Replication works is that by default when a slave falls too far behind it gets dropped from the pool until it catches up.  Anything wrapped inside a transaction automatically does both reads and writes to the master.  I didn't think about a strategy where after an insert access to slaves would be temporarily suspended.  Seems a bit heavy handed to me, but I guess that could be grafted in.

>
>
>The communication between applications is done via memcached.
>
>
>I'm using Postgresql and Slony.  I have not looked into implementing lag_behind_master (as I'm not clear how that works in ::Replicated).
>

The current replication only works with MySQL native replication.

If you look inside DBIC::Storage::DBI::mysql.pm you'll see two methods, "is_replicating" and "lag_behind_master" which is probably pretty specific to MySQL.  When I originally wrote this stuff I had little to no idea how replication works under other databases so was at a loss really to figure out how to properly abstract this.  'lag_behind_master' is a numeric value that is supposed to be the number of seconds a given slave is behind the master.  In practice the number is pretty relative.  In DBIC replication support  this number is used to determined if a slave is 'too stale' to use.

>
>
>The existing (non-DBIC) application will set a flag in memcached when a write happens.  This is keyed by user id. And each request memcached is checked to see if the current user needs to read from the master.  I'm looking at a way to duplicate that behavior with ::Replicated.
>

Well, you can flip replication off on a per query basis:


my $RS = $schema->resultset('Source')->search(undef, {force_pool=>'master'});

I'd probably hack into my Catalyst model to add that bit to search resultset automatically.  

>I'd like to know if this seems like a reasonable approach, and if anyone sees any gotchas that I need to be aware of.
>
>
>First, I subclass ::Replicated in Catalyst::Model::DBIC::Schema config via storage_type => 'MyApp::DB::Replicated'.  The point of this subclass is two things: 1) set a flag in memcached and 2) force all reads to the master for the remainder of the request.
>
>
>This subclass looks like:
>
>
>package MyApp::DB::Replicated;
>>use Moose;
>>extends 'DBIx::Class::Storage::DBI::Replicated';
>>use namespace::autoclean;
>>
>>
>>has flag_write => ( is => 'rw' );
>>
>>
>>
>>
>>my @methods = qw/
>>    insert
>>    insert_bulk
>>    update
>>    delete
>>/;
>>
>>
>>after \@methods => sub {
>>    my $self = shift;
>>    $self->flag_write->();
>>    $self->set_reliable_storage;
>>};
>>
>>
>>__PACKAGE__->meta->make_immutable;
>>1;
>
>
>So, after the methods listed a sub is called to flag (in memcached) that a write happened, and then set_reliable_storage is forced on to make any subsequent reads go to the master (for the remainder of the request).
>
>
>Replicated will force all reads to the master for reads inside a transaction, but a single request might span multiple transactions (and selects outside of a txn_do), so forcing it for the reminder of the request seems the best option.
>
>
>Now, in the Catalyst Model I need a way to force reads to the master, and also set memcached when a write to the master happens:
>
>
>before 'ACCEPT_CONTEXT' => sub {
>    my ( $self, $c ) = @_;
>
>
>    my $schema  = $self->schema;
>    my $storage = $schema->storage;
>
>
>
>
>    return if $c->stash->{_replicated_set}++
>    || !$storage->isa( 'DBIx::Class::Storage::DBI::Replicated' );
>
>
>    # callback to flag that reads go to master.
>    $storage->flag_write( sub { $self->force_master($c, 1) } );
>
>
>    # Should all reads go to master?
>>    if ( $self->force_master( $c ) ) {
>        $storage->set_reliable_storage;
>    }
>    else {
>        $storage->set_balanced_storage;
>    }
>
>
>};
>
>
>That doesn't feel bullet proof by any means, but does this seem like a good way to hook into DBIC for this?
>
>

Seems there's a bit more here than you need.  Like I mentioned above you got that force_pool=>'master' thing.  Honestly I'd probably start by adding that force_pool stuff to my controllers and make sure it works and then back it into the model.  I'm not so excited by the "set_reliable_storage" and its counterpart since the way it flips state is a bit shaky in my mind.  That was my first attempt at giving the user this kind of control and I left it in for backcompat.  You should also take a look at the test for replication (in the t directory) and make heavy used of DBIC_TRACE=1 to see what is going on.  I override this output so you can see what slaves (or the master) are picking up which bit.

I'm very excited to see that postgresql replication of some sort supported.  I only did mysql since $work was paying for just that.  I was also thinking about about a cross database replication support system for DBIC, but that's really a lot of work to get right.

>
>
>BTW -- Do you think ::Replicated should load any class specified by "storage_type"?  I'm having to explicitly "use" my subclass.

There was a reason for this but I can't recall.  Anybody else remember?

>
>
>Thanks,
>
>
>
>-- 
>Bill Moseley
>moseley at hank.org
>


      



More information about the DBIx-Class mailing list