Jeen - Yet anothere techlog

STFUAWSC

DBIx::Class and Replication - II

그러니까 17일이죠.

이렇게 몇줄만의 설정으로 간단하게 어플리케이션단에서 Replication 설정을 수행할 수 있습니다…만, 여러가지 운용이슈가 발생할 테고 그에 맞춰서 적당하게 답을 내야할 경우가 생기겠죠. 네, 뭐 그건 그때 가서…

라고 지난 글의 말미에서 얘기했습니다만, 네 금방 운용이슈가 발생했습니다.

넘겨준대로 설정해서 돌리려고 하는 데 이런 에러가 떴다는 것입니다.

~~~ bash

Class::MOP::Class:::around(): DBI Exception: DBD::mysql::db selectrow_hashref failed: Access denied; you need the SUPER,REPLICATION CLIENT privilege for this operation \[for Statement "show slave status"\] at /home/\*/perl5/perlbrew/perls/perl-5.14.2/lib/site_perl/5.14.2/i686-linux/Class/MOP/Method/Wrapped.pm line 162

~~~

딱 봐도 DB 접속계정의 권한문제인데, 가장 간단한 답은 접속계정에 해당 권한을 부여하면 되는 것이었는데요. 하지만 접속 계정을 추가해서 어떻게 하기에는 다른 운용이슈가 발생할 여지가 있다고 생각하기에(그럴리가…) 수정을 요구받았습니다.

결국 DBIx::Class::Storage::DBI::Replicated::* 아래의 소스를 파보도록 했습니다. 관련 동작은 connect_replicants 시에 validate_replicants 에서 각 Replicant 마다 is_replicating 체크를 하게 되는 것입니다.

일단 뭐 예, DBI::Replicated 시에는 기본적으로 그냥 뭐 자동 fail-over 까지 슥샥슥샥 해줄 수 있는데… 그냥 이런 거 다 필요없다라는 합의점에 이르러 결국은 해당부분을 회피하도록 했습니다.

~~~ perl

BEGIN {
    use DBIx::Class::Storage::DBI::Replicated::Pool;

    no warnings 'redefine';
    \*DBIx::Class::Storage::DBI::Replicated::Pool::validate_replicants = sub {
        return 1;
    };
};

~~~

추가한 부분은 위와 같습니다. 컴파일시에 미리 validate_replicants 를 재정의해버리는 것이었습니다.

전체 소스는 아래와 같습니다.

~~~ perl

BEGIN {
    use DBIx::Class::Storage::DBI::Replicated::Pool;

    no warnings 'redefine';
    \*DBIx::Class::Storage::DBI::Replicated::Pool::validate_replicants = sub {
        return 1;
    };
};

my $schema = MyApp::Schema->clone;
$schema->storage_type(\[
    '::DBI::Replicated' => {
        balancer_type => '::Random',
        balancer_args => {
            auto_validate_every => 10,
            master_read_weight  => 0,
        },
        pool_args => {
            maximum_lag => 2,
        }
    }
\]);
$schema->connection({
    dsn      => "dbi:mysql:myapp:127.0.0.50",
    user     => "user",
    password => "whatthehell",
    RaiseError        => 1,
    AutoCommit        => 1,
    mysql_enable_utf8 => 1,
    on_connect_do     => \[ "SET NAMES utf8" \],
    quote_char        => q{`},
});
$schema->storage->connect_replicants(\[
    {
        dsn      => "dbi:mysql:myapp:192.168.0.51",
        user     => "user",
        password => "whatthehell",
    },
    ....
\]);

~~~

그리고 그들은 행복하게 오래오래 살았답니다~. 끝.

Oh-my-zsh → Bash-it

한동안 zsh 을 사용하면서 oh-my-zsh 로 화려한 셸환경을 이용하고 있었는데요, 언제부턴가 grep warning 이 뜨기도 하고, bash 위주의 작업환경에 맞춘 기사들이 소개되기에 이르러 결국 zsh 을 버리고 다시 bash 로 돌아오게 되었습니다.

하지만 그래도 역시 oh-my-zsh 을 사용하면서 접한 경험들을 버릴 수가 없기에 bash 에서 그런 류의 확장이 없을까 검색해본 결과 bash-it 이라는 것이 있다는 것을 확인했습니다.

솔직히 oh-my-zsh 에 비해서는 그렇게 만족스럽지는 못합니다. :–(

DBIx::Class + Replication

일반적으로 회사에서 개발하는 규모에서는 Replication 을 크게 고려하지 않았었는데, 이번에 수주받은 프로젝트에서 DBIx::Class 사용시에 Replication 에 대해서 고려해야 할 부분이 있었습니다.

이전 회사에서 MySQL Replication 을 설정할 때 slave 서버들의 앞단에 keepalived 를 놓아서 로드 밸런서 역할로 사용했던 적이 있습니다. 말그대로 부하분산의 목적도 있고, 많은 slave 서버들의 커넥션정보를 일일이 웹 어플리케이션에서 설정할 필요도 없었죠. 하지만 IDC 에 로드밸런서 서버 한대를 더 우겨넣어야 했었죠.

뭐 그리고 어플리케이션 로직에서도 master, slave 를 명시적으로 지정해야할 필요도 있었습니다.

최근에는 HAProxy 를 사용해서 이런 저런 구성을 하고 있다는 얘기를 들었지만 마땅히 실제업무에서는 사용해보지 못해서 뭐라 말도 못하겠군요.

그나저나 DBIx::Class 에서는 0.081x 버젼부터 자체적으로 Replication 설정을 할 수 있게 되었습니다. 위에서 로드밸런서를 사용하는 것과는 달리 slave 서버들의 접속정보를 모두 적어야할 필요가 있습니다. 하지만 설정만으로 갱신/참조용 쿼리를 어플리케이션 로직에 상관없이 분리시킬 수 있습니다.

DBIx::Class::Storage::DBI::Replicated 의 SYNOPSIS 에 나와있는 대로 하면 됩니다.

....
my $schema = MyApp::Schema->clone;
$schema->storage_type([
    '::DBI::Replicated' => {
        balancer_type => '::Random',
        balancer_args => {
            auto_validate_every => 10,
            master_read_weight  => 0,
        },  
        pool_args => {
            maximum_lag => 2,
        }   
    }   
]);
$schema->connection({
    dsn      => "dbi:mysql:myapp:127.0.0.50",
    user     => "user",
    password => "whatthehell",
    RaiseError        => 1,
    AutoCommit        => 1,
    mysql_enable_utf8 => 1,
    on_connect_do     => ["SET NAMES utf8"],
    quote_char        => q{`},
});
$schema->storage->connect_replicants([
    {
        dsn      => "dbi:mysql:myapp:192.168.0.51",
        user     => "user",
        password => "whatthehell",
    },
    ....
]);

DBIx::Class 에서 위처럼, –>storage_type 에 DBI::Replicated 를 지정하고 그에 적합한 인수를 넘겨주면 됩니다. balancer_type 은 기본적으로 First, Random 이 있습니다. 일단 저는 Random을 사용하구요.

balancer_args에서 auto_validate_every 를 통해서 매초당 Replication 의 유효성을 확인합니다. 0으로 지정되어 있을 경우에는 뭐 안하겠다고 생각할 수 있지만… 그게 아니라 매번 쿼리를 날릴때마다 체크를 하게 됩니다.

pool_args 의 maximum_lags 는 Replication 서버에 대한 유효성 체크에서 해당 수치를 초과하면 자동으로 Replication Pool 에서 해당 서버에 대한 접속을 빼버리게 됩니다.

–>connection 에서 지정한 것은 master 서버, –>storage->connect_replicants 에서 복수의 slave 서버들을 지정합니다.

그럼 Replication 체크는 어떻게 하고 있을까요?

sub is_replicating {
    my $status = shift->_get_dbh->selectrow_hashref('show slave status');
    return ($status->{Slave_IO_Running} eq 'Yes') && ($status->{Slave_SQL_Running} eq 'Yes');
}

DBIx::Class::Storage::DBI::mysql 에서 확인할 수 있습니다만, show slave status 를 날려서 Slave_IO_Running, Slave_SQL_Running 값을 보고 Replication 체크를 수행하고 있는 것이죠. 뭐 물론 mysql 의 경우입니다만, 만약 DBD::* 에서 is_replicating 이 정의가 되어 있지 않다면 master 로의 fail-over 같은 건 동작하지 않습니다.

화면상에서 이뤄지는 각각의 쿼리들이 정말로 제대로 가고 있을까 확인해보고 싶을 경우도 있을 겁니다. 그때는…

$schema->storage->debug(1);

을 지정함으로, 어떤 쿼리가 어느 DB 서버로 향하는 지에 대한 정보가 화면에 뿌려지기에 눈으로 확인하기 좋겠죠.

만약 Replication 이 제대로 설정되지 않으면 자동으로 모든 쿼리는 master 서버로 향하게 됩니다.

Exception trying to ->ensure_connected for replicant dbi:mysql:bto:localhost, error is Class::MOP::Class:::around(): DBI Connection failed: DBI connect('my app:localhost','user',...) failed: Access denied for user 'user'@'localhost' (using password: YES) at /Users/jeen/perl5/perlbrew/perls/perl-5.14.2-llvm/lib/site_perl/5.14.2/DBIx/Class/Storage/DBI.pm line 1249. at /Users/jeen/perl5/perlbrew/perls/perl-5.14.2-llvm/lib/site_perl/5.14.2/darwin-2level/Class/MOP/Method/Wrapped.pm line 162
No Replicants validate, falling back to master reads.

이렇게 몇줄만의 설정으로 간단하게 어플리케이션단에서 Replication 설정을 수행할 수 있습니다…만, 여러가지 운용이슈가 발생할 테고 그에 맞춰서 적당하게 답을 내야할 경우가 생기겠죠. 네, 뭐 그건 그때 가서…