[Django]django-compositepk-model

Googleサーチコンソールを見ると、Djangoの複合キーについて調べて、以下ページにアクセスしてくれる人が多い。

https://gijutsu.com/2021/01/19/django-composite-primary-key/

そこで、GitHubで公開している複合主キー対応のパッケージについて、今更ながらREADMEの日本語版を。

https://github.com/Arisophy/django-compositepk-model

django-compositepk-modelパッケージ

複合主キーの対応のためのDjangoのModelクラスを拡張した CPkModelというクラスを提供する。Query クラスに対しても、複数カラムでのlookupsへの対応を追加したCPkQueryというサブクラスを提供する

本パッケージにより、複合主キーを使っているレガシーDBテーブルについて、サロゲートキーを追加することなくDjangoで扱えるようになる。

これはticket373に関する自分なりの解決方法です。

特徴

1. 利用方法が簡単

複合主キーを持つテーブルに対しては、基底クラスをModelクラスからCPkModelクラスに変更して、 Metaにunique_togetherを設定し、主キーを構成する各フィールド定義に primary_key=Trueを設定する。

(モデル定義の例)

from django.db import models
from cpkmodel import CPkModel

# Normal Model
#   primary_key is auto 'id'
class Company(models.Model):
    name = models.CharField(max_length=100)
    established_date = models.DateField()
    company_code = models.CharField(max_length=100)

    class Meta:
        db_table = 'Company'

# Child Model (CpkModel)
#   composite primary key: company_id, country_code
class CompanyBranch(CPkModel):
    company = models.ForeignKey(
        Company,
        primary_key=True,       # for CompositePK
        on_delete=models.CASCADE,
    )
    country_code = models.CharField(
        max_length=100,
        primary_key=True,       # for CompositePK
    )
    name = models.CharField(max_length=100)
    established_date = models.DateField()

    class Meta:
        managed = False  # for CompositePK *1
        db_table = 'CompanyBranch'
        unique_together = (('company', 'country_code'),)  # for CompositePK

それだけで、それ以外の追加定義、仮想フィールドの追加は必要ない。

*1:  primary_key=Trueが複数カラムに設定されていると、マイグレーションが失敗するので、 managed = Falseとしている。レガシーDBのテーブルは既に存在するか、手でCREATEされると思うので。マイグレーションが使いたければ、migrationする時にprimary_key=Trueとmanaged=Falseをコメントアウトすること。

2. Admin画面が使える

CPkModelを継承したモデルも、DjangoのAdmin画面で使える。 複合キーの値は、 カンマ区切り表示される。 Change(Update)とDeleteは問題なく動く。 Add(Create) に関しては、 CreateViewがキー項目のそれぞれに対してユニークチェックをするので、最初の子レコードは登録できるけど、追加の子レコードが登録できなくなるという問題がある。 これは、CreateViewの問題なので、QuerySetやModelのメソッドを使うプログラムは問題ない。

3. 複数カラムでのlookupsに対応

複合主キーに対応するため、CPkQueryで複数カラムを指定したlookup(以下)に対応している。

obj = CompanyBranch.objects.get(pk=(1,'JP'))
qs = CompanyBranch.objects.filter(pk='1,JP')
qs = CompanyBranch.objects.filter(**{'company,country_code':(1,'JP')})
qs = CompanyBranch.objects.filter(**{'company_id,country_code':'1,JP'})

カンマの入ったLHS(左辺)は、主キーだけでなく、他のカラムの組み合わせもOK。

qs = CompanyBranch.objects.filter(
    **{'country_code,name':'JP,Google'})

IN句に対しても、複数カラムが使える。PostgreSQLは問題ないが、SQLite3についてはIN句に対応されていない(※Djangoの問題)のでエラーになる。

qs = CompanyBranch.objects.filter(
    pk__in=[(1,'JP'),(1,'US'),(2,'JP'),])
qs = CompanyBranch.objects.filter(
    **{'country_code,name__in':[('JP','HONDA'),('CN','SONY'),]})

4. bulk_updateが使える(v1.0.2)

bulk_updateメソッドも、PostgreSQLについて利用可能。SQLite3はサポートされていないので、不可。

    Album.objects.bulk_update(
        albums, ['num_stars',])

制約

1. マイグレーション(table作成)

primary_key=Trueが複数カラムに設定されていると、マイグレーションが失敗するので、 managed = Falseとしている。 レガシーなテーブルは既に存在するか、手でCREATEされるので。マイグレーションが使いたければ、migrationする時にはprimary_key=Trueとmanaged=Falseをコメントアウトすること。

2. Admin画面でのCREATE(CreateViewの問題)

CreateViewがキー項目のそれぞれに対してユニークチェックをするので、レコードが一部しか登録できなくなる。 これは、CreateViewの問題なので、QuerySetやModelのメソッドを使うプログラムには問題ない。

3. 外部キー

孫テーブルへのリレーションをサポートするにはForeignKeyからCPkForeignKeyというクラスを作る必要があるが、まだ未対応。

4. SQLiteでIN句が使えない

Djangoのモデルは 、SQLiteのIN句に対応していない。

5. レコード登録は、saveよりCREATEが良い

INSERTについては、CPKModelのsaveを使う方がより、CPKQuerySetのcreateメソッドを使う方がいい。何故なら、Modelのsaveでは、キー値が設定されている場合、最初にUPDATEを行うので(その次にINSERTを実行)。 オプションでforce_insert=Trueを指定することで、避けることは可能。

CompanyBranch.objects.create(**params)

 または 
 
obj = CompanyBranch(**params)
obj.save(force_insert=True)

インストール方法

pip install django-compositepk-model

リンク

https://code.djangoproject.com/ticket/373
https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys
https://gijutsu.com/2021/01/19/django-composite-primary-key/
https://gijutsu.com/2021/03/16/django-ticket373/

[Django]チケット#373について

チケット#373(Add support for multiple-column primary keys)については、Djangoの以下Wikiに詳しく書いてある。

https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys

これに関する自分の意見と、django-compositepk-modelではどのように実装したかを述べる。

0. django-compositepk-modelについて

複合主キーの対応については、Djangoのモデルに手を入れるのが良い実装になると思うが、対応されるのかもわからない状況で待てない。Djangoから分離して独自ブランチで進めるという手もあるが、先々のメンテナンスを考えるとそれも良くない。

ということで、拡張クラスとしてパッケージにした。直接手が入れられないため、1行を変更するために、オーバーライドして元ソースを丸ごとコピーして直すという間抜けな実装もあるが、基本的な部分は上手くフックできていると思う。

GitHub上にテスト用のデータ設定済みのSQLiteのデータも含まれていて、Admin画面も実行できるので、興味のある人は動かしてみて下さい。

https://github.com/Arisophy/django-compositepk-model/

1. WikiのMajor Issuesについて

https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys#MajorIssues

1.1 _meta.pkについて

1. A number of APIs use “obj._meta.pk” to access the primary key, on the assumption it is a single field (for example, to do “pk=whatever” lookups). A composite PK implementation would need to emulate this in some way to avoid breaking everything.

Django Community Wiki

1) multi field対応

CPKModelでは、_meta.pkに設定するものを、複数フィールドをまとめるCompositePkというFieldのサブクラスに変更した。

(compositekey.py)

class CompositeKey(Field):
    def __init__(self, keys, primary=False):
        names = tuple((f.name for f in keys))
        join_name = CPK_SEP.join(names)
        db_columns = tuple((f.db_column if f.db_column else f.name for f in keys))
        db_join_column = "(" + ",".join(db_columns) + ")" """! カンマ区切りに !"""
        super().__init__(
                name=join_name, 
                primary_key=primary,
                unique=True,
        )
        self.keys = keys
        self.attname = join_name
        self.column = join_name
        self.names = names
        self.model = keys[0].model

    def get_col(self, alias, output_field=None):
        return CompositeCol(alias, self, output_field)

(cpkmodel.py)

class CompositePk(CompositeKey):
    def __init__(self, keys):
        super().__init__(keys, primary=True)
:

class CPkModelBase(ModelBase):
    """ Metaclass for CompositePkModel."""
    def __new__(cls, name, bases, attrs, **kwargs):
            :
            super_new = super().__new__(cls, name, tuple(modelbases), attrs, **kwargs)
            meta = super_new._meta
            pkeys = tuple(f for f in meta.local_concrete_fields if f.primary_key)
            # change attributes
            if len(pkeys) > 1:
                super_new.has_compositepk = True
                meta.pk = CompositePk(pkeys)  """! _meta.pkにCompositePkを設定 !"""
                setattr(super_new, "pk", CPkModelMixin.cpk)
                setattr(super_new, "_get_pk_val", CPkModelMixin._get_cpk_val)
                setattr(super_new, "_set_pk_val", CPkModelMixin._set_cpk_val)
                setattr(super_new, meta.pk.attname, None)
                setattr(super_new, "_check_single_primary_key", CPkModelMixin._no_check)
                setattr(super_new, "delete", CPkModelMixin.delete)
            else:
                super_new.has_compositepk = False
            setattr(super_new, "get_pk_lookups", CPkModelMixin.get_pk_lookups)
            meta.base_manager._queryset_class = CPkQuerySet
            meta.default_manager._queryset_class = CPkQuerySet           
            super_new.pkeys = pkeys
            super_new.pkvals = CPkModelMixin.pkvals
            super_new._meta = meta
            return super_new

class CPkModel(CPkModelMixin, Model, metaclass=CPkModelBase):
    pass

従来のModelとCpkModelの設定値の違いは以下のようになる。※テスト画面から「CheckKeys」からも確認できる。

#Musician(Model)Album(CPkModel)
obj.pk11,1
_meta.pktest.Musician.idtest.Album.artist,album_no
_meta.pk ClassAutoFieldCompositePk
_meta.pk.keys(<django.db.models.fields.related.ForeignKey: artist>, <django.db.models.fields.IntegerField: album_no>)
_meta.pk.nameidartist,album_no
_meta.pk.attnameidartist,album_no
_meta.pk.columnidartist,album_no
_meta.pk.names(‘artist’, ‘album_no’)
_meta.pk.modelMusician objectAlbum object
_meta.pk.ColCol(Musician, test.Musician.id)CompositeCol(Album, test.Album.artist,album_no)

2) pk=”whatever” lookupsの対応

[pk=”whatever” lookups]のほとんどは、最終的には、Query.add_qで処理される。そこで、CompositePkが作った特殊なpkと、それに対応する複数の値を持つ”whatever”をここでうまく処理すればいい。

方針としては、以下で対応した。

2-1) 複合主キーの条件をadd_qの中で複数条件に変更

例えば、”pk=obj.pk”という条件は以下のように変更する。

Albumの場合、_meta.pk.attnameから左辺は、”artist,album_no”となる。右辺のobj.pkは”1,1″のような形になる。両辺をカンマで分解して二つのQ条件にする。

Q(artisit='1') & Q(album_no='1')

このクエリは、最終的に、以下のようなSQL条件文になる。

"Album"."artist_id" = 1 AND "Album"."album_no" = 1 

実装している部分は、以下である。

(cpkquery.py)

class CPkQueryMixin():
   :
    ###########################
    # override
    ###########################
     :
    def add_q(self, q_object):
       :
            def make_q(keys, vals):
                q = Q()
                for key, val in zip(keys, vals):
                    q.children.append((key, val))
                return q

            assert isinstance(obj, (Q, tuple))
            if isinstance(obj, Q):
                :
            else:
                # When obj is tuple,
                #  obj[0] is lhs(lookup expression)
                #       pk and multi column with lookup 'in' is nothing to do in this, it will change in 'names_to_path'. 
                #  obj[1] is rhs(values)
                #       valeus are separated in this method.
                names = obj[0].split(LOOKUP_SEP)
                if ('pk' in names and self.model.has_compositepk) or CPK_SEP in obj[0]:
                    # When composite-pk or multi-column
                    if len(names) == 1:
                        # change one Q to multi Q
                        keys = separate_key(self, obj[0]) """! キーを分ける !"""
                        vals = separate_value(keys, obj[1]) """! 値を分ける !"""
                        if len(keys) == len(vals):
                            return make_q(keys, vals) """! make multi conditions !"""
                        else:
                            raise ProgrammingError("Parameter unmatch : key={} val={}".format(keys, vals))
                    else:
                        # check the last name
                        last = names[-1]
                        if last == 'in':
                            :
                        elif last == 'pk' or CPK_SEP in last:
                            # change one Q to multi Q
                            #  example: ('relmodel__id1,id2', (valule1,value2))
                            #             |
                            #             V
                            #           ('relmodel__id1', valule1)
                            #           ('relmodel__id2', valule2)
                            before_path = LOOKUP_SEP.join(names[0:-1])
                            cols = separate_key(self, last)
                            keys = [before_path +  LOOKUP_SEP + col for col in cols] """! キーを分ける !"""
                            vals = separate_value(cols, obj[1])  """! 値を分ける !"""
                            return make_q(keys, vals)    """! make multi conditions !"""
                        else:
                            # another lookup is not supported.
                            raise NotSupportedError("Not supported multi-column with '{}' : {}".format(last,obj[0]))
                return obj

        new_q = transform_q(q_object)
        super().add_q(new_q)
2-2) 複数カラムIN句の対応

IN句については、pkはadd_qで加工はしないで、そのままCompositePkのままにする。値については、カンマ区切りのものは分解してタプルにする。

例えば、”pk__in=[‘1,JP’,’1,US’,’2,JP’,]”という条件は以下のように変更される。

pk__in=[(1,'JP'),(1,'US'),(2,'JP')])

DBカラム名を作成するColクラスは、CompositePkにおいては、CompositeColサブクラスにて処理されて、以下表現を返す。

("CompanyBranch"."company_id", "CompanyBranch"."country_code")

最終的に、以下のようなSQL条件文になる。

("CompanyBranch"."company_id", "CompanyBranch"."country_code") IN ((1,"JP"), (1,"US"), (2,"JP"))

なお、主キーだけでなく、任意のカラムでもカンマ区切りで複数指定ができるようにした。

(compositekey.py)

class CompositeCol(Col):
    def __init__(self, alias, target, output_field=None):
        super().__init__(alias, target, output_field)
        self.children = [Col(alias, key, output_field) for key in target.keys]

    def as_sql(self, compiler, connection):
        sqls = []
        for child in self.children:
            sql, _ = child.as_sql(compiler, connection)
            sqls.append(sql)
        return "(%s)" % ",".join(sqls), []

class CompositeKey(Field):
    :
    def get_col(self, alias, output_field=None):
        return CompositeCol(alias, self, output_field)   """! CompositeColを返す !"""

(cpkquery.py)

class CPkQueryMixin():
    :
    ###########################
    # override
    ###########################
    :
    def names_to_path(self, names, opts, allow_many=True, fail_on_missing=False):
        meta = self.get_meta()
        first_name = names[0]
        # name[0] is Multi-Column ?
        if (first_name == 'pk' and self.model.has_compositepk) or CPK_SEP in first_name:
            # get CompisteKey
            ckey = meta.pk
            if first_name != 'pk' and first_name != ckey.name:
                # IF Not PK, make another CompositeKey
                cols = [meta.get_field(col) for col in first_name.split(CPK_SEP)]
                ckey = CompositeKey(cols)    """! 複数カラムをCompositeKeyでまとめる !"""
            lookups = names[1:] if len(names) > 1 else []
            return [], ckey, (ckey,), lookups
        else:
            return super().names_to_path(names, opts, allow_many, fail_on_missing)

1.2 A number of things use (content_type_id, object_pk) tuples

2. A number of things use (content_type_id, object_pk) tuples to refer to some object — look at the comment framework, or the admin log API. Again, a composite PK system would need to somehow not break this.

Django Community Wiki

“object_pk” が CompositePkに変わっても、問題ないと思う。実際、CompositePkを使うCPkModelをAdmin画面で使っても問題は特に起きていない。

1.3 Admin URL

3. Admin URLs; they’re of the form “/app_label/module_name/pk/”; there would need to be a way to map URLs to objects with a set of columns for the primary key.

Django Community Wiki

CompositePkは、カンマ区切りの文字を返す。URLはエスケープされて問題なく使えている。

Django Admin for Composite PrimaryKey
Django Admin for Composite PrimaryKey

1.4 結論

_meta.pkを、CompositePkという複数フィールドを管理するサブクラスに置き換えるという考え方で、Major Issuesを簡単に解決できた。

2. 今後の課題

django-compositepk-modelの課題、制約は以下。

https://github.com/Arisophy/django-compositepk-model#limitations

https://github.com/Arisophy/django-compositepk-model/issues

CPkForegnKey以外は、Djangoのソースを直接いじれば、簡単に解決できる問題だと思う。

現状のままでも、自分のプロジェクトでは、今のところ問題なく使えている。また、今は、リリースに向けてパフォーマンス見直しおしており、ORMのリレーションは使わずに、逆にRawクエリに直しているところで、CPkForegnKeyは無くても困っていない。

いつか余裕ができたら、CPkForegnKeyの追加、あるいは、Djangoそのものに手を入れることも考えてみたい。

バックアップ設定

以下、バックアップの方針に基づいて、CentOSサーバー間で、バックアップ設定した手順。

0.環境

転送元サーバー:CentOS 8.2

  • ホスト名(例):www.xxservice.com
  • グローバルアドレス(例):222.222.222.222
  • プライベートネットワークアドレス(例):10.10.10.1
  • sshdポート(例):7777
  • バックアップを実行するユーザー(例):appuser

転送先サーバー1:CentOS 7.8

  • ホスト名(例):bak1.xxservice.com
  • プライベートネットワークアドレス(例):10.10.10.200
  • sshdポート(例):8888

転送先サーバー2:CentOS 7.8

  • ホスト名(例):bak2.xxservice.com
  • 転送元サーバーとは別拠点で、プライベトネットワーク未接続
  • sshdポート(例):9999

1.転送先サーバーの設定

バックアップ用ユーザー(例):backupuser

バックアップディレクトリ(例):/home/backupuser/xxservice/

1-1)sshd_configの修正

/etc/ssh/sshd_configの設定が以下となるようにする。

  • ホストベース認証を有効
  • ホストベース認証の既知ホストは、ユーザーディレクトリ下を許可
  • ユーザーディレクトリ下の.shostsを有効にする。
# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
HostbasedAuthentication yes
# Change to yes if you don't trust ~/.ssh/known_hosts for HostbasedAuthentication 
IgnoreUserKnownHosts no 
# Don't read the user's ~/.rhosts and ~/.shosts files 
IgnoreRhosts no

後、プライベートネットワークでは逆引きできない。余計なログを出さないようにUseDNSがyesの場合は、noに変更する。

UseDNS no

設定変更後に、設定の読み込み。

$ systemctl reload sshd

1-2)rootユーザーへのスイッチ禁止

/etc/pam.d/suで、以下を設定し、rootユーザへのスイッチを禁止する。

auth required pam_wheel.so use_uid root_only

1-3)バックアップ用ユーザー作成

$ adduser backupuser

1-4)バックアップ用ディレクトリ作成

$ su - backupuser
[backupuser ~]$ mkdir xxservice

1-5)既知ホストの追加

バックアップユーザーで、転送元サーバーにssh接続を試みて、公開鍵を取得して既知ホストに登録する。

※転送先サーバー1の場合
[backupuser ~]$ ssh -p 7777 10.10.10.1

※転送先サーバー2の場合
[backupuser ~]$ ssh -p 7777 www.xxservice.com

既知ホストに追加するかと聞いてくるので、yesで登録すると、~/.ssh/known_hostsに登録される。

ポート番号が入った形で登録されるので、viコマンドで修正する。

※転送先サーバー2の場合、以下のように修正する

[www.xxservice.com]:7777,[222.222.222.222]:7777 (公開鍵) 
↓
www.xxservice.com,222.222.222.222 (公開鍵)

1-6).shostsの設定

viコマンドで、.shostsファイルを作成し、中身は以下とする。転送元サーバーのIPアドレスを設定する。

※転送先サーバー1の場合

10.10.10.1 appuser 

※転送先サーバー2の場合

222.222.222.222 appuser 

パーミッションを変更して、.shostsファイルを見えないようにする。

[backupuser ~]$ chmod 600 .shosts

2.転送元サーバーの設定

2-1)ssh_configの修正

従来はssh_configを直接修正していたが、CentOS 8.2 では、以下ディレクトリにファイル追加するのが作法みたい。

/etc/ssh/ssh_config.d/99-hostbase.confを、以下内容で作成する。

HostbasedAuthentication yes
EnableSSHKeysign yes

2-2)既知ホストの追加と接続確認

以下コマンドで接続し、既知ホストに追加するかと聞いてくるので、yesで登録する。

※転送先サーバー1への接続
[appuser ~]$ ssh -p 8888 -l backupuser 10.10.10.200

※転送先サーバー2
[appuser ~]$ ssh -p 9999 -l backupuser bak2.xxservice.com

問題なければ、無事にログインできる。

2-3)バックアップ処理の登録

以下のコマンドを作成して、実行権を付けて、cron実行する。

2-3-1)バックアップコマンド

do_backupという名前で以下ファイルを作成する。timeコマンドで実行時間を計測しながら、低優先度でバックアップを実行する。

引数は、バックアップサーバーのアドレス、ポート、バックアップファイルの接尾辞で、このコマンドは、バッチから引数を渡されて実行される。

【do_backup】

#!/bin/sh
#
alias dotime='time ionice -c 2 -n 7 nice -n 19'

# param
if [ $# -ne 3 ]; then
  echo "param error!!"
  exit 1
fi
BACKUP_SERVER=$1
BACKUP_SERVER_PORT=$2
BACKUP_SUFFIX=$3

# file setting
BIN_DIR=(コマンドの配置ディレクトリ)
BACKUP_DIR=(バックアップファイルを置くディレクトリ)
BACKUP_LOG="$BACKUP_DIR/bak_$BACKUP_SUFFIX.log"

BACK_FILE_DB="$BACKUP_DIR/dbbak_$BACKUP_SUFFIX"

APP_DIR=(アプリケーションデータのディレクトリ)
BACK_APP_DIR="$BACKUP_DIR/appdata/"

TRNS_DB_DIR="/home/backupuser/xxservice/"
TRNS_APP_DIR="/home/backupuser/xxservice/appdata/"

#--------------------------------------------------
# Start
echo "---- START : `date`"  > $BACKUP_LOG

####################
# DB
echo "---- DB S : `date`"  >> $BACKUP_LOG
(dotime $BIN_DIR/do_db_backup $BACK_FILE_DB &>> $BACKUP_LOG) &>> $BACKUP_LOG

echo "---- DB GZIP S : `date`"  >> $BACKUP_LOG
(dotime gzip -f $BACK_FILE_DB &>> $BACKUP_LOG) &>> $BACKUP_LOG

####################
# APP
echo "---- APP S : `date`"  >> $BACKUP_LOG
(dotime rsync -auv --rsync-path="ionice -c 2 -n 7 nice -n 19 rsync" \
  $APP_DIR $BACK_APP_DIR &>> $BACKUP_LOG) &>> $BACKUP_LOG

####################
# Transfer

# transfer DB backup
echo "---- TRN DB S : `date`"  >> $BACKUP_LOG
(dotime scp -P $BACKUP_SERVER_PORT $BACK_FILE_DB.gz ubackup@$BACKUP_SERVER:$TRNS_DB_DIR \
  &>> $BACKUP_LOG) &>> $BACKUP_LOG

# transfer APP backup
echo "---- TRN APP S : `date`"  >> $BACKUP_LOG
(dotime rsync -auvz -e "ssh -p $BACKUP_SERVER_PORT" $BACK_APP_DIR \
  ubackup@$BACKUP_SERVER:$TRNS_APP_DIR  &>> $BACKUP_LOG) &>> $BACKUP_LOG

# End
echo "---- END : `date`"  >> $BACKUP_LOG

最初のrsyncで、–rsync-path=”ionice -c 2 -n 7 nice -n 19 rsync”オプションを付けているのは、受信側も同じアプリサーバーのため優先度を下げている。転送時には付けていないのは、転送先サーバーはバックアップ専用のため、優先度を気にしなくていいため。

2-3-2)DBバックアップコマンド

do_db_backupという名前で以下ファイルを作成する。3-1)バックアップコマンドから呼ばれるコマンドで、DBに応じたコマンドを作成する。

【do_db_backup】

※MariaDBの例

#!/bin/sh
#
#  $1: backup file name
#

mysqldump --user=(DBユーザー) --password=(DBパスワード) --single-transaction (データベース名) > $1

※PostgreSQLの例

#!/bin/sh
#
#  $1: backup file name
#

pg_dump -Fc -b (データベース名)> $1

2-3-3)実行コマンド

バッチで実行するための、以下の2つのファイルを作成する。

【BACKUP_HOURLY】

#!/bin/sh
#

BACKUP_SERVER=10.10.10.200
BACKUP_SERVER_PORT=8888
BACKUP_SUFFIX=h`date +%H`

(コマンドの配置ディレクトリ)/do_backup \
  $BACKUP_SERVER $BACKUP_SERVER_PORT $BACKUP_SUFFIX

【BACKUP_DAILY】

#!/bin/sh
#

BACKUP_SERVER=bak2.xxservice.com
BACKUP_SERVER_PORT=9999
BACKUP_SUFFIX=d`date +%d`

(コマンドの配置ディレクトリ)/do_backup \
  $BACKUP_SERVER $BACKUP_SERVER_PORT $BACKUP_SUFFIX

2-4)cron設定

“crontab -e”で、実行コマンドをスケジューリングする。

毎時42分と、毎日3時13分にバックアップする設定の例。

# m h dom mon dow command
42 * * * * (コマンドの配置ディレクトリ)/BACKUP_HOURLY
13 3 * * * (コマンドの配置ディレクトリ)/BACKUP_DAILY

バックアップについて

各種サービスのアプリデータのバックアップについての基本方針。

1.対象データ

データ1)DBデータ

MariaDB、PostgreSQL、SQLServerなどのデータ。

データ2)アプリデータファイル

サービス固有のデータファイル。細かいファイルがどんどん増えていく。

2.バックアップ先

異なるタイミングで2拠点にバックアップする。

転送先1)LAN内サーバー

同一センター内でプライベートネットワーク内の別サーバー

転送先2)別拠点サーバー

別事業者の別地域のサーバー。例えば、実サーバーがConoHaの東京リージョンの場合、転送先2としては、ConoHa以外の事業者(ニフクラやさくらインターネット等)で、東京以外(大阪とか)のサーバーにする。

3.バックアップのタイミング

1)1時間毎

 DBデータ、アプリデータファイルをバックアップフォルダにバックアップして、転送先1)LAN内サーバーに転送する。

2)日毎

 DBデータ、アプリデータファイルをバックアップフォルダにバックアップして、転送先2)別拠点サーバーに転送する。。

4.バックアップ方法

データ1)DBデータ

データベースアプリのバックアップ方法によりバックアップファイルを作成し、必要あればgzip等で圧縮する。

今のところ、フルバックアップでも問題ないため、すべてフルバックアップとする。

データ2)アプリデータファイル

rsyncコマンドにて、バックアップフォルダに差分コピーする。過去ファイルが残っていても問題ないなら、deleteオプションはつけない。

5.転送方法

データ1)DBデータは、scpによるファイル転送。

データ2)アプリデータファイルについては、rsync over sshで転送。

※プライベートネットワーク内であれば、rsyncプロトコルでもいいかもしれないが、ここもssh経由とする。

6.バックアップのローテート

データ1)DBデータ

転送先1)LAN内サーバーについては、バックアップファイル、またはディレクトリに時間(date +%H)を付けることで、24セット(24時間)でローテートする。

転送先2)別拠点サーバーについては、日にち(date +%d)を付けることで、翌月の同日にローテートする。

データ2)アプリデータファイル

1セットとしてローテートしない。

7.その他

1)ホストベース認証

バックアップ先のサーバーに、バックアップ専用のユーザーを作成し、そのユーザーに対してホストベース認証を許可する。

suコマンドによるrootユーザーへのスイッチは禁止しておく。

2)タスクの優先度

バックアップ元(アプリ運用中のサーバー)のバックアップ処理については、以下コマンドで実行することで、優先度を下げること。

ionice -c 2 -n 7 nice -n 19 コマンド

バックアップ先のサーバーについては、バックアップ専用のため、優先度は気にしない。

 

実際の設定は、次の記事。

サーバーの移行手順

サーバーの老朽化あるいはVPS業者の乗換えなどにより、サーバーを移行する場合の手順についてのメモ。

1.移行手順

DBサーバー移行時だけ計画停止(メンテナンス)するという条件で、 以下の手順で移行する。

1)新サーバーの環境構築

現行サーバーからプログラム、データ等をコピーして、新サーバーで環境を構築する。

新サーバーの環境構築

この時に、現行DBサーバーからデータをコピーして新DBサーバーに登録するまでの時間を覚えておくこと。この時間が、4)でのサービス停止時間(メンテナンス時間)を決めるのに必要となる。

テスト端末でhostsに以下の設定を追加することで、新サーバーの動作確認を行う。

(テスト端末のhosts設定)
222.222.222.222 XXService.com #新WebサーバーのIPアドレスを設定

Windows10でのhosts設定方法は以下。

新Webサーバーと新DBサーバーで、問題なく動作することを確認する。OS等のバージョンを上げている場合は、その影響がないか念入りに動作確認を行う。

2)新Webサーバー+現行DBの動作確認

新DBサーバーは一旦停止して、新Webサーバーから現行DBサーバーを見るようにする。現行DBサーバー側で、新Webサーバーからの接続を許可 (DB設定とファイアウォール)する 。

新Webサーバー+現行DBの動作確認

1)と同様に、新Webサーバーにアクセスして動作確認を行う。

3)DNSを変更してWebサーバー並行稼働(現行DB)

DNS設定を現行サーバーから新サーバーに変更することで、ユーザーが徐々に新Webサーバーにアクセスするようになる。

DNS設定を変更し、Webサーバー並行稼働(現行DB)

通常であれば1週間ほどでDNSキャッシュは全て更新されて、現行Webサーバーへの ユーザーからのアクセスは完全になくなる。この時間を短くしたい場合は、事前にDNS設定でTTL値を小さくしておく。

現行Webサーバーは、ユーザからのアクセスがなくなった時点でお役御免となる。

4)DBサーバーを移行し、新サーバーにて運用

事前に、計画停止についてユーザーへの告知(調整)をする。

計画停止時にDBサーバーの切り替え作業を行い、旧サーバーを停止する。

DBサーバーを移行し、新サーバーにて運用

計画停止時の作業内容としては、ざっくり以下。

  1. メンテナンス中として、DBアクセスを止める
  2. DBバックアップを取得し、新DBサーバーに投入する
  3. 旧サーバーを停止し、DBアクセス先を新DBサーバーに変更
  4. サービス再開

2.レプリケーション機能を使う場合

データ量が多くてDB移行時の停止時間が長くなりすぎる場合あるいは長い停止時間が取れない場合には、レプリケーション機能を使う。

2)と3)の間で、新DBをスレーブに設定することで、

3)Webサーバー並行稼働+現行DBサーバー(マスタ)+新DBサーバー(スレーブ)といった運用にする。

レプリケーション機能を使う場合

4)DBサーバー切り替えは、以下作業となり、僅かな停止時間で実施できる。

  • 新DBサーバーを、スレーブからマスタに昇格し
  • DB接続先を新DBサーバーに切り替える