TECH PLAY

Perl

イベント

該当するコンテンツが見つかりませんでした

マガジン

技術ブログ

はじめに 現在メルカリでは CoreDB と呼ばれる巨大な MySQL を TiDB に移行しています[^1]. この記事内でも紹介されていますが, 私たちは移行するために MySQL と TiDB を DM というツールで差分同期を行っています. 本記事ではこの DM を利用しつつ DDL(Data Definition Language) をどの様に実行しているかについて紹介します. メルカリでの MySQL への DDL 実行 まず, メルカリにおける MySQL への DDL 実行は下記の通り場合分けして実行しています: それぞれの条件について簡単に解説しますが, 基本的に source – replica の replication 遅延を最小限に抑えるために場合分けしています. メタデータのみの変更 まず最初の条件は「メタデータのみの変更かどうか」です. これは Online DDL[^2] のページで [In Place] & ![Rebuilds Table] & [Permit Concurrent DML] & [Only Modifies Metadata] なものが該当します. 例えば Table 名の変更や Column の default 値の変更, ENUM の追加[^3] などです. これはテーブルの再構築などが不要で一瞬で完了するためそのまま source 側で実行します. metadata のみの変更でも注意すること metadata のみの変更とはいえ注意すべきこと, それは DDL とクエリのロック競合です. 公式ドキュメント[^4]にあるとおり MySQL では table へのアクセス/変更時に一貫性を保証するために metadata lock を取得しますが, この metadata lock が DDL とアプリケーションが発行するクエリで競合し意図しない影響を及ぼす可能性があります: -- session 1 mysql> BEGIN; Query OK, 0 rows affected (0.00 sec) mysql> SELECT * FROM foo; Empty set (0.00 sec) -- // このまま transaction を保持したままにする -- session 2 mysql> ALTER TABLE foo ALTER COLUMN id SET DEFAULT 0, ALGORITHM=INSTANT; -- // metadata のみの変更にもかかわらず実行がブロックされる -- session 3 mysql> SELECT * FROM foo; -- // DDL の後続のクエリも待たされる このように session 2 移行の同一テーブルへのアクセスがブロックされていますが, ここで processlist と metadata lock の関係を見てみます: mysql> SELECT -> t.PROCESSLIST_ID AS process_id, -> t.PROCESSLIST_USER AS user, -> t.PROCESSLIST_DB AS db, -> t.PROCESSLIST_TIME AS time, -> t.PROCESSLIST_STATE AS state, -> t.PROCESSLIST_INFO AS query, -> ml.LOCK_TYPE, -> ml.LOCK_DURATION, -> ml.LOCK_STATUS -> FROM performance_schema.metadata_locks ml -> JOIN performance_schema.threads t -> ON ml.OWNER_THREAD_ID = t.THREAD_ID -> WHERE ml.OBJECT_TYPE = 'TABLE' -> AND ml.OBJECT_SCHEMA = 'test' -> AND ml.OBJECT_NAME = 'foo' -> AND t.PROCESSLIST_ID IS NOT NULL -> ORDER BY ml.LOCK_STATUS, process_id; +------------+------+------+------+---------------------------------+------------------------------------------------------------------+-------------------+---------------+-------------+ | process_id | user | db | time | state | query | LOCK_TYPE | LOCK_DURATION | LOCK_STATUS | +------------+------+------+------+---------------------------------+------------------------------------------------------------------+-------------------+---------------+-------------+ | 8 | root | test | 516 | NULL | NULL | SHARED_READ | TRANSACTION | GRANTED | | 9 | root | test | 514 | Waiting for table metadata lock | alter table foo alter column id set default 0, ALGORITHM=INSTANT | SHARED_UPGRADABLE | TRANSACTION | GRANTED | | 9 | root | test | 514 | Waiting for table metadata lock | alter table foo alter column id set default 0, ALGORITHM=INSTANT | EXCLUSIVE | TRANSACTION | PENDING | | 16 | root | test | 512 | Waiting for table metadata lock | SELECT * FROM foo | SHARED_READ | TRANSACTION | PENDING | +------------+------+------+------+---------------------------------+------------------------------------------------------------------+-------------------+---------------+-------------+ この様に DDL(id=9)が Exclusive lock を取ろうとして親の transaction(id=8)を待っていて, DDL の後続(id=16)が更に待たされている事がわかります. これを避けるために, たとえ metadata lock のものであっても下記のように lock_wait_timeout を十分短い値に指定することでこの例の後続のクエリになるべく影響を与えない様に実行することが重要です. 例えばここでは 5s に設定した場合のそれぞれの挙動を確認します: -- session 1 mysql> BEGIN; Query OK, 0 rows affected (0.00 sec) mysql> SELECT * FROM foo; Empty set (0.02 sec) -- session 2 mysql> SET SESSION lock_wait_timeout=5; mysql> SELECT NOW(); ALTER TABLE foo ALTER COLUMN id SET DEFAULT 0, ALGORITHM=INSTANT; SELECT NOW(); +---------------------+ | NOW() | +---------------------+ | 2026-03-05 01:59:32 | +---------------------+ 1 row in set (0.01 sec) ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction +---------------------+ | NOW() | +---------------------+ | 2026-03-05 01:59:37 | +---------------------+ 1 row in set (0.00 sec) -- session 3 mysql> SELECT NOW(); SELECT * FROM foo; SELECT NOW(); +---------------------+ | NOW() | +---------------------+ | 2026-03-05 01:59:34 | +---------------------+ 1 row in set (0.01 sec) Empty set (3.03 sec) +---------------------+ | NOW() | +---------------------+ | 2026-03-05 01:59:37 | +---------------------+ 1 row in set (0.00 sec) 1秒未満で完了するか 続いての条件は「1秒未満で完了するかどうか」です. これについては具体的なケースを紹介することは難しいのですが, 例えば CREATE TABLE 文, 空もしくは十分少ないレコード数に対するメタデータの変更で完結しない DDL[^5] などがこれに当たります. ここでなぜ「1秒未満」という条件を付与しているかについてですが, MySQL の DDL はたとえ online としても replication を構成しているときには下記の制限があります[^6]: Long running online DDL operations can cause replication lag. An online DDL operation must finish running on the source before it is run on the replica. Also, DML that was processed concurrently on the source is only processed on the replica after the DDL operation on the replica is completed. online DDL としても replica で実行される前に source で完了している必要があり, また source で並行で実行されている DML は replica での DDL が完了した後に実行されます. つまり, source で並行実行されている DML は replica では DDL が完了するまでブロックされるということです. これは source 側で(online) DDL による source 側の table definition の変更と並行実行されている DML の対応が replica 側でも同様にして再現される必要があります. そのため replica 側でも table defintion の変更と DML の整合性を担保するためにこのような仕組みとなっています. 冒頭の記事のようにメルカリのデータサイズは非常に巨大で中には DDL 完了に1日以上かかるものもあり, そのようなテーブルへの DDL による replication 遅延を防ぐために, 基本的に即座に終わらない DDL はそのまま実行しないようにしています. 実際に DDL が 1s 未満で完了するかどうかは経験則に基づくことが多いですが, 例えばサービスで稼働していないホストで SET sql_log_bin=0 を実行してバイナリログに出力しないようにして実測するなどで計測可能です. また, DDL 自体が 1s 未満で完了するとしても先の metadata lock による影響は考慮する必要があるため, この場合も同様に lock_wait_timeout を調整する必要があります. gh-ost で対応可能か 時間のかかる DDL を実行する場合, メルカリでは gh-ost を採用しています . gh-ost を利用できるかどうかについてはリソース的な制限(テーブルコピーを伴うため余剰なディスクサイズが必要, 通常の更新と gh-ost による backfill/差分同期による replication 遅延影響)や機能的な制限(UNIQUE KEY の一部に ENUM が含まれる場合の性能劣化[^7])がない限り gh-ost を利用しており, DDL 実行オペレーションで最も数の多いケースです. もしそれらを満たさない場合には Rolling Upgrade, つまり全 replica に対して DDL を実行し最後に source を切り替えるといったことを行います. なぜ ENUM を含む場合に性能劣化するのか MySQL の 公式ドキュメント によると ENUM は文字列順ではなく内部 index 順でソートされますが, これは Go 言語[^8]の文字列によるハンドリングと異なります. この ENUM の取り扱いの不一致による iteration 時のデータ欠損を避けるために, gh-ost では UNIQUE KEY に ENUM が含まれる場合には CONCAT(...) により明示的な文字列として取り扱われます[^9]. この時, ORDER BY CONCAT(...) ASC が実行されることになり結果として iteration のたびに Creating Sort Index が発生し性能, 負荷ともに劣化する可能性があります. DM を用いた DDL 実行 ここから TiDB の話に移ります. 前述の通りメルカリでは移行に伴う停止時間をなるべく短くするために DM を用いて MySQL と TiDB で差分同期をしつつ切り替えを進めています: この時, 前述の条件 3 の場合にどのような挙動になるかを考えてみます. こちらのドキュメント の通り, DM には online-ddl というフラグがあり pt-osc や gh-ost といった online migration tool のユースケースをカバーしています. ここからは gh-ost を例にして説明していきます. まず, gh-ost で test database の foo table に DDL が適応される流れについて説明します: メタデータテーブル(ghc) を作成 Create /* gh-ost */ table test._foo_ghc realtable をもとに切り替え後のテーブル(gho)を作成 Create /* gh-ost */ table test._foo_gho like test.foo gho に DDL の適応 ALTER /* gh-ost */ table test._foo_gho ... realtable から gho への backfill backfill 完了後 realtable から gho への差分同期 差分同期完了後 RENAME 文を使って cutover RENABLE TABLE foo TO _foo_del, _foo_gho TO foo このフローで online-ddl が有効化されている場合に DM はどのような挙動になるでしょうか. メタデータテーブル(ghc) を作成 DM は ghc テーブルを作成しない realtable をもとに切り替え後のテーブル(gho)を作成 DM は gho テーブルを作成しない, その代わりにメタデータテーブル dm_meta.{task_name}_onlineddl を初期化する DELETE FROM dm_meta.{task_name}_onlineddl WHERE id = {server_id} and ghost_schema = {ghost_schema} and ghost_table = {ghost_table}; gho に DDL の適応 DM が実行される DDL をメタデータテーブル dm_meta.{task_name}_onlineddl に保存する この DDL は後に利用される realtable から gho への backfill DM は realtable への更新のみ TiDB に同期する gho への更新はすべて破棄される backfill 完了後 realtable から gho への差分同期 4 と同様に realtable への更新のみ同期 差分同期完了後 RENAME 文を使って cutover DM は cutover の RENAME 文を分割し, gho table から realtable への RENAME 実行の際に下記を実施する 3 で保存した DDL を取得 DDL の gho を realtable に置換 置換された DDL を実行 ALTER table test.foo ...; このようにして DM は OnlineDDL ツールを利用する際に realtable から gho への同期に伴う処理を削減しています( online-ddl=false を指定した場合は通常通り gho/ghc などが作成される). まとめると下記のようになります: MySQL TiDB メタデータテーブル(ghc) を作成 ghc テーブルは作成しない realtable をもとに切り替え後のテーブル(gho)を作成 gho テーブルは作成せず DM のメタデータテーブルを初期化 gho に DDL の適応 実行予定の DDL をメタデータテーブルに保存 realtable から gho への backfill realtable への更新のみ TiDB に同期 backfill 完了後 realtable から gho への差分同期 realtable への更新のみ TiDB に同期 差分同期完了後 RENAME 文を使って cutover RENAME 文を分割し保存していた DDL を実際に realtable に対して実行 gh-ost 利用時の DM の挙動の比較 DDL 実行時のトラブル メルカリでは online-ddl=true & gh-ost による DDL 実行で運用しており, 特に大きな問題もなく運用できていました. しかしある時の DDL 実行で問題が発生します. その問題とは DM の replication lag が通常 1~2s 未満だったものが突如 1h 以上遅延するという事象でした. DM の status は RUNNING にも関わらずこの様に Replication QPS(DM syncer の完了した Job 数)が 16:00 頃から 0 になっており, DML が何も実行されなくなっていました: TiDB のサポートチームとともに調査したところ, この QPS が停止したタイミングで下記の DDL が実行されていたことがわかりました. ALTER TABLE `mercari`.`foo` MODIFY COLUMN `bar_id` int(10) UNSIGNED NOT NULL; この DDL は先の MySQL における DDL 実行パターンにおいて gh-ost を利用するものであったため, 先で紹介した cutover( RENAME TABLE ) 実行時に ALTER が実行されておりこの DDL により DM の遅延が発生していることがわかりました. 原因はわかりましたが, なぜ DM が遅延したのでしょうか. これは先に紹介した Online DDL の replica で実行される制限と同じ理由で, TiDB が直接 MySQL の binlog を読めないため DM が先の source/replica での table defnition と DML の整合性担保を保証するための仕組みとなっています. では v8.5.3 を例に実際にコードの中をみてみましょう[^10]: // dm/syncer/syncer.go#L713 func (s *Syncer) Process(ctx context.Context, pr chan pb.ProcessResult) { // ... err := s.Run(newCtx) if err != nil { // returned error rather than sent to runFatalChan // cancel goroutines created in s.Run cancel() } // ... } // dm/syncer/syncer.go#L1741 func (s *Syncer) Run(ctx context.Context) (err error) { // ... s.runWg.Add(1) go s.syncDML() s.runWg.Add(1) go func() { defer s.runWg.Done() // also need to use a different ctx. checkpointFlushWorker worker will be closed in the first defer s.checkpointFlushWorker.Run(s.tctx) }() s.runWg.Add(1) go s.syncDDL(adminQueueName, s.ddlDBConn, s.ddlJobCh) // ... } // dm/syncer/syncer.go#L1419 func (s *Syncer) syncDDL(queueBucket string, db *dbconn.DBConn, ddlJobChan chan *job) { defer s.runWg.Done() var err error for { ddlJob, ok := <-ddlJobChan if !ok { return } // ... if !ignore { // ... affected, err = db.ExecuteSQLWithIgnore(s.syncCtx, s.metricsProxies, errorutil.IsIgnorableMySQLDDLError, ddlJob.ddls) // ... } } } // dm/syncer/ddl.go#L221 func (ddl *DDLWorker) HandleQueryEvent(ev *replication.QueryEvent, ec eventContext, originSQL string) (err error) { // ... if err = ddl.flushJobs(); err != nil { return err } return ddl.strategy.handleDDL(qec) } // dm/syncer/syncer.go#L3210 func (s *Syncer) flushJobs() error { flushJobSeq := s.getFlushSeq() s.tctx.L().Info("flush all jobs", zap.Stringer("global checkpoint", s.checkpoint), zap.Int64("flush job seq", flushJobSeq)) job := newFlushJob(s.cfg.WorkerCount, flushJobSeq) _, err := s.handleJobFunc(job) return err } // dm/syncer/syncer.go#L1115 func (s *Syncer) handleJob(job *job) (added2Queue bool, err error) { // ... s.addJob(job) // ... } // dm/syncer/syncer.go#L1016 func (s *Syncer) addJob(job *job) { // ... tp := job.tp switch tp { case flush: s.jobWg.Add(1) s.dmlJobCh <- job case ddl: s.updateJobMetrics(false, adminQueueName, job) s.jobWg.Add(1) startTime := time.Now() s.ddlJobCh <- job s.metricsProxies.AddJobDurationHistogram.WithLabelValues("ddl", s.cfg.Name, adminQueueName, s.cfg.SourceID).Observe(time.Since(startTime).Seconds()) // ... } } // dm/syncer/syncer.go#L1617 func (s *Syncer) syncDML() { defer s.runWg.Done() dmlJobCh := s.dmlJobCh if s.cfg.Compact { dmlJobCh = compactorWrap(dmlJobCh, s) } causalityCh := causalityWrap(dmlJobCh, s) flushCh := dmlWorkerWrap(causalityCh, s) for range flushCh { s.jobWg.Done() } } // dm/syncer/syncer.go#L1419 func (s *Syncer) syncDDL(queueBucket string, db *dbconn.DBConn, ddlJobChan chan *job) { defer s.runWg.Done() var err error for { ddlJob, ok := <-ddlJobChan if !ok { return } // ... if !ignore { // ... affected, err = db.ExecuteSQLWithIgnore(s.syncCtx, s.metricsProxies, errorutil.IsIgnorableMySQLDDLError, ddlJob.ddls) // ... } } } この部分で binlog から DDL イベントが来たときに, まず DML の flush を行います. これは DML queue に入っている DML を一度すべて下流(TiDB)にて実行するため, s.jobWg.Wait() それらの完了を待機します. その後 DDL job を DDL queue に投入し, 再び s.jobWg.Wait() で下流(TiDB)で DDL 実行が完了するまで待機します. この DDL による wait で待機している間 binlog イベント処理ループはブロックされるため, DDL が完了するまで DM は後続のイベントを処理しないということになります. online-ddl=true 設定時に gh-ost を利用した DDL の実行は下記のようになります: この様に gh-ost の cut-over(RENAME) 実行時に downstream 側で実際の DDL が発行されそれが完了するまで DML はブロックされるため, DDL が完了するまで replication が遅延することになります. DM を利用しつつ安全に DDL を実行するには この様に online-ddl は gh-ost など online schema change tool の利用時に同期に必要な無駄を削減させるためのものなため, 指定したとしても下流(TiDB)での DDL の実行完了を待つ必要があります. これは冒頭の無停止で段階的に移行していく際にいくつか問題がある場合があります. MySQL から TiDB へ移行する際にはサービスへの影響が少ないもの(分析基盤のための CDC など)から読み込みを移行させていきますが, これらの中には大きな遅延が許容できないものもあるため, 数時間実行にかかる DDL は影響を与える可能性があります. 一方でメルカリのデータベースはいずれも巨大であり TiDB Dumpling や TiDB Lightning による export/import でも数日以上かかり, DDL 実行のために DM を停止, つまり TiDB Cluster を再作成するというオペレーションも現実的ではありません. そのため, メルカリでは下記のように DDL の実行方法をケース分けしています: 先の紹介のように online-ddl を有効化すると MySQL 上で cutover( RENAME TABLE ) されたタイミングで TiDB 側で DDL が実行されます. 仮にこの DDL が例えば MODIFY COLUMN のように何度でも実行可能な場合は, 先に TiDB 側のみ DDL を実行しておいて gh-ost をすることで2回目の(実質意味がない) DDL では即座に終了するため, 実際の DDL が長時間でも DM の遅延を最小に抑えることが可能です. 前のブログ にあるようなレコード数が多くさらに INDEX で多数利用されているような column に対する MODIFY COLUMN などには有用と言えます. それ以外の場合(例えば INDEX 追加など)は実行時間やビジネス要件によってかわりますが, 遅延を許容して gh-ost を実行する, Binlog Event Filter により該当の DDL だけ除外するなど対応が考えられます. まとめ 今回は DM で MySQL と TiDB を同期しているときに DDL をなるべく安全に実行する方法を紹介しました. DM を利用する際に参考にしていただけると幸いです. 現在メルカリでは DBRE の EM を募集しています, 詳しくは こちら をご覧ください. [^1]: ref: https://pingcap.co.jp/case-study/mercari-tidb-cloud/   [^2]: ref: https://dev.mysql.com/doc/refman/8.4/en/innodb-online-ddl-operations.html   [^3]: 末尾追加かつ要素の個数が byte を跨がない場合のみ該当 [^4]: ref: https://dev.mysql.com/doc/refman/8.4/en/metadata-locking.html   [^5]: 例えば INDEX や column の操作(追加/削除/タイプ変更) [^6]: ref: https://dev.mysql.com/doc/refman/8.4/en/innodb-online-ddl-limitations.html   [^7]: ref: https://github.com/github/gh-ost/blob/master/doc/requirements-and-limitations.md   [^8]: ref: gh-ost は Go 言語, pt-osc は Perl で実装されている [^9]: ref: https://github.com/github/gh-ost/issues/273   [^10]: ref: https://github.com/pingcap/tiflow/blob/v8.5.3/  
LifeKeeperの『困った』を『できた!』に変える!サポート事例から学ぶトラブルシューティング&再発防止策 こんにちは、SCSKの前田です。 いつも TechHarmony をご覧いただきありがとうございます。 システムのリプレースやハードウェア更新のタイミングで訪れる、「ミドルウェアのバージョンアップ」。 「サポート切れ(EOS)対応」や「魅力的な新機能の追加」など、OSのパッチ適用とはまた違った、期待と緊張が入り混じる一大イベントではないでしょうか。 しかし、HAクラスターソフトウェアであるLifeKeeperにおいて、この「バージョンアップ」は単なるファイルの置き換えではありません。 「インストーラーを実行して、バージョン番号が上がれば完了!」 ……そう思い込んで作業を進めた結果、再起動後に設定ファイルが初期値に戻っていたり、長年動いていたスクリプトが突然エラーを吐き始めたりといった、予期せぬトラブルに直面することがあります。 本連載企画「LifeKeeper の『困った』を『できた!』に変える!サポート事例から学ぶトラブルシューティング&再発防止策」では、サポートセンターに蓄積された「生のトラブル事例」を元に、安定運用のための実践的な知恵を共有していきます。 はじめに:成功へのロードマップを描く 連載第2回となる今回は、LifeKeeper本体やDataKeeperのバージョンアップに焦点を当てます。 バージョンアップ作業において、最も怖いのは 「見えない変化」 です。 インストーラーは便利ですが、それが裏側でどの設定を引き継ぎ、どの設定をリセットするのか、また新しいバージョンが古い設定をどう解釈するのかは、リリースノートの細部を読み込まない限り見えてきません。 今回は、実際にサポートへ寄せられた「バージョンアップ失敗事例」を以下の3つの「落とし穴(Trap)」に分類しました。 設定と環境の「サイレント変化」 過去の「まあいいか」が牙をむく 近道は「急がば回れ」 これらの事例から「なぜ失敗したのか」を学び、確実に成功させるためのチェックポイント(ロードマップ)を解説します。 その他の連載企画は以下のリンクからどうぞ! 【リソース起動・フェイルオーバー失敗の深層 #1】EC2リソースが起動しない!クラウド連携の盲点とデバッグ術 – TechHarmony 【リソース起動・フェイルオーバー失敗の深層 #2】ファイルシステムの思わぬ落とし穴:エラーコードから原因を読み解く – TechHarmony 【リソース起動・フェイルオーバー失敗の深層 #3】設定ミス・通信障害・バージョン違いの深層と再発防止策 – TechHarmony 【OS・LKバージョンアップで泣かないために #1】OSバージョンは変えていないのに!?カーネル更新の「落とし穴」と互換性の真実 – TechHarmony 【実録】LifeKeeperバージョンアップの「困った!」事例ファイル ここからは、実際のサポート問い合わせをベースにしたケーススタディです。 「自分の環境でも起こりうるかもしれない」 という視点でご覧ください。 Trap 1:設定と環境の「サイレント変化」 バージョンアップによって、今まで使っていた設定や環境が 「静かに」 変わってしまう ケースです。 ■ケースA:アップデートしたら設定が消えた!?(Linux版) 「v9.5.2からv9.9.1へアップデートしました。エラーなく完了したのですが、再起動後になぜかプロキシ設定などが効かなくなっています…」 発生状況:  アップデート作業自体は成功したものの、LifeKeeperの動作設定ファイル( /etc/default/LifeKeeper )に記述していたカスタム設定が消失していました。 原因: LifeKeeperの仕様により、更新インストール時に 一部の設定ファイルがデフォルト値に戻る(上書きされる) 挙動となっていました。 教訓:  「設定はすべて自動的に引き継がれるはず」という思い込みは危険です。特にメジャーバージョンをまたぐ更新では、バックアップと復元手順が必須です。 ■ケースB:スクリプトが動かない!Perlの罠(Windows版) 「v8.9からv8.10.2へ上げようとしています。パラメータ変更は不要と聞きましたが、本当にそのまま上げて大丈夫でしょうか?」 リスク:  調査の結果、LifeKeeperに同梱されているPerlのバージョンが、v8.10.1以降で「5.8系」から「5.32系」へと一気にアップグレードされていることが判明しました。 教訓:  GenericリソースなどでPerlスクリプトを使用している場合、言語仕様の変更によりスクリプトが動作しなくなる可能性があります。アプリケーションだけでなく、ミドルウェアが依存する 「言語環境」の変化 も重要なチェックポイントです。 Trap 2:過去の「まあいいか」が牙をむく 古いバージョンでは許容されていた(あるいは無視されていた)設定の不備が、新しいバージョンの「厳格なチェック」によってエラーとして顕在化するケースです。 ■ケースC:亡霊IPがアラートを引き起こす 「OSとLifeKeeperを更新した後、ログに failed quickCheck due to ALRM signal というエラーが多発するようになりました。以前は出ていなかったのですが…」 原因:  IPリソースの監視リスト( pinglist )に、既に撤去済みで疎通できない古いIPアドレスが残っていました。 なぜ今?:  バージョンアップに伴い チェック処理やリトライの挙動が変化(厳格化) し、古いIPへの応答待ちが積み重なった結果、タイムアウト(ALRM signal)が発生していました。 教訓:  「今は使っていないけど残っている設定」は、バージョンアップ時の最大の敵です。更新作業はゴミ掃除の絶好の機会と捉えましょう。 ■ケースD:ディスクに名前がない!?(UUID問題) 「バージョンアップ後、DataKeeperリソースで『一意の識別子がない』という警告が出たり、パーティション情報が正しく取得できなくなりました」 原因:  LifeKeeper/DataKeeperの新しいバージョンでは、ディスクを一意に特定するために 「UUID」の使用が必須化(または厳格化) されました。古い環境で「MBR形式」のパーティションを使っていたため、UUIDを持たず、新しいバージョンの要件を満たせなくなっていました。 教訓:  ソフトウェアの進化に合わせて、インフラ側(パーティションテーブル等)もGPT形式へのモダン化が必要になることがあります。 Trap 3:近道は「急がば回れ」 手順を省略したり、無理なアップデートパスを通ろうとして失敗するケースです。 ■ケースE:飛ばしすぎたバージョンアップ 「v9.6.xからv9.8.1へ一気にアップデートしようとしたら失敗しました。2世代前からの更新はサポートされているはずですが…」 原因:  基本的には直接アップデートが可能ですが、 この特定のバージョン(v9.8.1)においては 内部パッケージの大幅な構成変更 があったため、例外的にv9.7やv9.8.0を経由する「ステップアップグレード」が必要でした。 教訓:  「いつものルール(N-2までOKなど)が今回も適用される」とは限りません。リリースノートには必ず 「アップグレードの注意点」や「例外的なパス」 が記載されていますので、慣れている作業でも必ず目を通しましょう。 ■ケースF:GUIの表示がおかしい 「DataKeeper更新後、GUI上でジョブ情報が表示されなくなりました」 解決策: 調査の結果、内部情報の不整合が起きていました。このケースでは、無理に修正するよりも「再インストールしてジョブを再作成」した方が、結果として早く、確実に解消しました。 教訓:   バージョンが大きく離れている場合や挙動がおかしい場合 は、上書きアップデートに固執せず、設定バックアップをとった上での「作り直し(再作成)」が最短ルートになることがあります。 「再発させない!」成功へのチェックリスト 上記の失敗事例から導き出した、バージョンアップ作業前に確認すべき「転ばぬ先の杖」チェックリストです。計画段階でぜひご活用ください。 ■LifeKeeper/DataKeeper バージョンアップ事前確認シート [  ] 【パスの確認】 現在のバージョンからターゲットバージョンへ「直接」アップデート可能ですか?(中継バージョンが必要ありませんか?) [  ] 【設定ファイルのバックアップ】 更新時に初期化されるファイル( /etc/default/LifeKeeper 等)を特定していますか? それらの手動バックアップを取得し、更新後に復元する手順を組み込みましたか? [  ] 【言語・環境の差異】 PerlやPythonなど、LifeKeeperが利用するランタイムのバージョンに変更はありませんか?(自作スクリプトへの影響確認) ディスクのパーティション形式は新しいバージョンの要件(UUID必須/GPT推奨など)を満たしていますか? [  ] 【不要設定の削除(ゴミ掃除)】 IPリソースの pinglist に、現在疎通できない「亡霊IP」が残っていませんか? [  ] 【OSとの整合性】 OS自体のカーネルアップデートを行う場合、LifeKeeperの再インストールやモジュール再コンパイルの手順を確認しましたか? [  ] 【リカバリプラン】 更新インストールが不整合を起こした場合に備え、一度アンインストールして「新規インストール+設定復元(または再作成)」に切り替える判断基準を持っていますか? ベストプラクティス:成功への近道 トラブルを防ぐために、明日からできる「ベストプラクティス」を3つ提案します。 「リリースノート」は宝の地図 リリースノートを「バグ修正のリスト」だと思っていませんか? 本当に見るべきは「制限事項 (Known Issues)」 と 「変更点 (Migration/Changes)」です。ここには「設定ファイルが初期化される」「UUIDが必須になる」といった重要情報が必ず書かれています。 ステージング環境でのリハーサルは必須 机上の確認だけでは、「pinglistのタイムアウト」や「Perlの互換性」といった環境依存のトラブルは見抜けません。本番環境のクローン(または同等構成)を用意し、実際にバージョンアップ手順を流すリハーサルを行ってください。 大幅な更新は「再作成」も視野に 数年前のバージョンから一気に最新版にするような場合、継ぎ接ぎのアップデートを繰り返すより、「設定情報を控えて、クリーンインストール後に再設定する」方が、潜在的なゴミが残らず、結果的に安定稼働につながることが多々あります。「上書き」にこだわらない柔軟性も重要です。 まとめ バージョンアップ時のトラブルは、多くの場合「準備不足」や「思い込み」の隙間に入り込むものです。 しかし、今回ご紹介した事例のように、事前に 「何が変わるのか」「何が消えるのか」「今の設定にゴミはないか」 を確認しておけば、そのほとんどは防げます。 「LifeKeeperの『困った』を『できた!』に変える」 今回のロードマップを参考に、ぜひ安心・安全なバージョンアップ計画を立ててください。 次回予告 次回からは新章に突入! テーマは「クラウド環境特有の落とし穴:AWS/Azure連携でハマるポイント」です。 クラウドならではの「オンプレミスと同じ感覚で設定したら動かない!?」というトラブル。 その第一弾として、 AWS環境でのLifeKeeper安定稼働術 にフォーカスします。 「Route53のDNSが切り替わらない!」「便利なはずの『Auto Recovery』がLifeKeeperと喧嘩する!?」といったAWS特有の事例と、EC2・S3連携の注意点を徹底解説します。お楽しみに! 詳しい内容をお知りになりたいかたは、以下のバナーからSCSK LifeKeeper公式サイトまで
こんにちは、エンジニアの id:mp0liiu です。 非常に遅くなってしまいましたが、昨年の7/4にPerlの最新安定バージョンである5.42がリリースされたので新機能や変更点についてまとめます。 source::encoding プラグマが追加され、デフォルトで有効に スコープ内のソースコードに期待する文字コードの指定をするプラグマ source::encoding が追加されました。 指定できるのは ascii と utf8 のみです。 use source::encoding 'ascii' するとスコープ内のソースコードに非ASCII文字が存在している場合、コンパイルエラーが発生するようになります。 use source::encoding 'utf8' は use utf8 と同等です。 v5.41 以降の feature bundle 1 ではデフォルトで use source::encoding 'ascii' が有効になります。 従来では次のように use utf8 していないのに非ASCII文字を扱うようなコードは、エラーになることなく意図していない挙動をしてしまうことがありました。 say length "あいうえお" ; # 本当は5文字だが, use utf8 していないので 15 が出力される use source::encoding 'ascii' することで、そのようなコードはコンパイルエラーになるので事前に気づくことができるようになります。 use v5.42 ; # use source::encoding 'ascii' も有効になる say length "あいうえお" ; # コンパイルエラー: Use of non-ASCII character 0xE3 illegal when 'use source::encoding "ascii"' これからは日本語など非ASCII文字を使うコードはきちんと use utf8 してから書くようにしましょう。 なお、 source::encoding 'ascii' はコメントやPODでも非ASCII文字があるとコンパイルエラーになるので注意が必要です。 __DATA__ , __END__ セクション以降に書く分には問題ありません。 any, all 演算子の追加 List::Util の any, all と同じ挙動をします。 演算子として実装されているためコードブロックのスタックフレームが作られずより高速に実行できるとのことでしたが、ベンチマークをいろいろとってみたところ大きいパフォーマンスの差はないものの、コードブロック内の処理やリストの要素数によってどちらの方がパフォーマンスがよいかが変わってしまいました。 なのでこだわる場合は自分で該当部分のパフォーマンスを計測することをおすすめします。 参考までに List::Util と関数と演算子とでそれぞれベンチマークをとったので参考にしてみてください。 keywordのほうがパフォーマンスが良い場合(any) Benchmark: running keyword_any, list_util_any for at least 1 CPU seconds... keyword_any: 2 wallclock secs ( 1.13 usr + 0.00 sys = 1.13 CPU) @ 495.58/s (n=560) list_util_any: 1 wallclock secs ( 1.11 usr + 0.00 sys = 1.11 CPU) @ 355.86/s (n=395) Rate list_util_any keyword_any list_util_any 356/s -- -28% keyword_any 496/s 39% -- keywordのほうがパフォーマンスが良い場合(all) Benchmark: running keyword_all, list_util_all for at least 1 CPU seconds... keyword_all: 1 wallclock secs ( 1.11 usr + 0.00 sys = 1.11 CPU) @ 503.60/s (n=559) list_util_all: 1 wallclock secs ( 1.04 usr + 0.00 sys = 1.04 CPU) @ 358.65/s (n=373) Rate list_util_all keyword_all list_util_all 359/s -- -29% keyword_all 504/s 40% -- List::Utilのほうがパフォーマンスが良い場合(any) Benchmark: running keyword_any, list_util_any for at least 1 CPU seconds... keyword_any: 2 wallclock secs ( 1.06 usr + 0.00 sys = 1.06 CPU) @ 16.04/s (n=17) list_util_any: 1 wallclock secs ( 1.06 usr + 0.00 sys = 1.06 CPU) @ 17.92/s (n=19) Rate keyword_any list_util_any keyword_any 16.0/s -- -11% list_util_any 17.9/s 12% -- List::Utilのほうがパフォーマンスが良い場合(all) Benchmark: running keyword_all, list_util_all for at least 1 CPU seconds... keyword_all: 1 wallclock secs ( 1.05 usr + 0.00 sys = 1.05 CPU) @ 16.19/s (n=17) list_util_all: 1 wallclock secs ( 1.06 usr + 0.00 sys = 1.06 CPU) @ 17.92/s (n=19) Rate keyword_all list_util_all keyword_all 16.2/s -- -10% list_util_all 17.9/s 11% -- ベンチマークに使用したコードはこちら use v5.42 ; use Benchmark qw( timethese cmpthese ) ; use List::Util (); use utf8 ; binmode STDOUT, ':encoding(UTF-8)' ; my @ary = ( 1 .. 1000000 ); say "keywordのほうがパフォーマンスが良い場合(any)" ; cmpthese( timethese(- 1 , +{ keyword_any => sub { use experimental qw( keyword_any ) ; any { $_ == 500 } @ary ; }, list_util_any => sub { List::Util::any { $_ == 500 } @ary ; }, }) ); print " \n " ; say "keywordのほうがパフォーマンスが良い場合(all)" ; cmpthese( timethese(- 1 , +{ keyword_all => sub { use experimental qw( keyword_all ) ; all { $_ < 500 } @ary ; }, list_util_all => sub { List::Util::all { $_ < 500 } @ary ; }, }) ); print " \n " ; say "List::Utilのほうがパフォーマンスが良い場合(any)" ; cmpthese( timethese(- 1 , +{ keyword_any => sub { use experimental qw( keyword_any ) ; any { $_ == @ary / 2 } @ary ; }, list_util_any => sub { List::Util::any { $_ == @ary / 2 } @ary ; }, }) ); print " \n " ; say "List::Utilのほうがパフォーマンスが良い場合(all)" ; cmpthese( timethese(- 1 , +{ keyword_all => sub { use experimental qw( keyword_all ) ; all { $_ < @ary / 2 } @ary ; }, list_util_all => sub { List::Util::all { $_ < @ary / 2 } @ary ; }, }) ); print " \n " ; レキシカルなメソッドを宣言できるようになった my sub のように my method でスコープ内でのみ呼び出すことのできる、レキシカルなメソッドを宣言できるようになりました。 また、レキシカルメソッドを呼び出すための演算子 ->& も追加されました。 use v5.42 ; use experimental 'class' ; class Point { my method hoge { say "hoge" ; } method wrap { $self -> &hoge ; } } Point->new->wrap(); # hoge $self->&method は method($self) の糖衣構文です。 同じクラス内でもスコープが違えば呼び出すことはできないですし、継承先のクラスから呼び出すこともできません。 switch 機能とスマートマッチング演算子の削除が無期限の延期に Perl5.38 で非推奨となり、5.42で削除予定だったswitch 機能(given-when構文)とスマートマッチング演算子は削除が無期限に延期となり、これらを使っても実験的機能であることの警告は発生しないようになりました。 switch機能はデフォルトでは無効になっており、個別に有効にするか、v5.34 までの feature bundle で有効になります。 v5.35 以降の feature bundle では無効になります。 { use v5.10 ; given ( 100 ) { when ( $_ % 2 == 0 ) { print " $_ is even" ; } default { print " $_ is odd" ; } } } { use v5.36 ; given ( 100 ) { # syntax error when ( $_ % 2 == 0 ) { print " $_ is even" ; } default { print " $_ is odd" ; } } } { use feature 'switch' ; given ( 100 ) { when ( $_ % 2 == 0 ) { print " $_ is even" ; } default { print " $_ is odd" ; } } } スマートマッチングはデフォルトで有効ですが、 smartmatch 機能として有効/無効を切り替えられるようになりました。 v5.40 までの feature bundle では有効になっており、v5.42 以降では無効となります。 { use v5.41 ; print 'A' ~~ [ 'A' .. 'D' ] ? 'Included' : 'Not included' ; # syntax error } { use feature 'smartmatch' ; print 'A' ~~ [ 'A' .. 'D' ] ? 'Included' : 'Not included' ; } あくまで後方互換性のためのことを考えると削除が難しかったので残しておいている、という感じがするのでこれからswitch機能やスマートマッチングを多用するコードを書くのはおすすめできないです。 特にスマートマッチングはオペランドごとの挙動を覚えるのが難しいのでやめておいたほうが良いでしょう。 switch機能は直接条件式を記述するなどスマートマッチングを利用しないように使う限りにおいては使用しても問題ないかなと思いますが、 だとしても代替として match構文 が提案されており、実験的実装も作られ後々実装される可能性があるので、今まで通りコードを書くのが一番無難かなと思います。 フィールド変数の attribute :writer が追加 クラス構文のフィールド変数の値を更新する setter を自動生成する attribute が追加されました。 スカラ変数のみ対応しています。 class Point { field $x :writer :param; field $y :writer :param; } my $p = Point->new( x => 20 , y => 40 ); $p->set_x ( 60 )->set_y( 100 ); # writerはインスタンス自身を返すのでメソッドチェーンも可能 引数を指定すると指定した名前で setter を生成します。 Moose系のクラスビルダーのアクセサと違ってフィールド名と同名のアクセサで getter / setter 両方として使えるようにできないので注意が必要です。 パッケージの区切り文字としてのアポストロフィを無効にできるようになった Perlではパッケージ名の区切り文字に :: を利用しますが、Perl4の頃は ' を利用しており、その互換性を保つためPerl5になってからもずっと ' をパッケージ名の区切り文字として利用できるようになっていました。 ' をパッケージ名の区切り文字として利用することは Perl5.38 で非推奨となり、 Perl5.41.3 で一旦削除されましたが、議論の後にデフォルトでは復活し、プラグマで有効/無効を切り替えられるようになりました。 以下のように apostrophe_as_package_separator 機能として有効/無効を切り替えられるようになっています。 use feature 'say' ; use POSIX; use feature 'apostrophe_as_package_separator' ; say $ POSIX' VERSION ; # $POSIX::VERSION と同じ no feature 'apostrophe_as_package_separator' ; say $ POSIX' VERSION ; # コンパイルエラー apostrophe_as_package_separator は use v5.42 で無効になります。 use v5.42 ; say $ POSIX' VERSION ; # コンパイルエラー chdir が CORE:: 名前空間に追加された コア関数と同名の関数がパッケージ内に定義されている際、曖昧さを避けて呼べるよう CORE:: にいくつか組み込み関数が追加されていってるのですが、その流れの1つかと思われます。 二項演算子で左項が否定されるのが不自然な場合に警告が発生するように !$x < $y のようなコードがあったとして、比較演算子で左項を本当に否定したいことはまずなくて、通常は条件式全体を否定したい場合が多いと思います。 そのような場合に警告が発生するようになりました。 ! $x < $y # 警告が発生: Possible precedence problem between ! and numeric lt (<) 拘束演算子(=~など)、 cmp 、 <=> 以外の比較演算子、isa演算子でこの警告が発生するようです。 このような場合は否定された演算子を使うか、かっこで優先順位を明示するか、優先順位の低い論理否定演算子 not を利用するようにしましょう。 $x >= $y !( $x < $y ) not $x < $y builtin モジュールの indexed 関数で配列のインデックスと値の組のリストを生成し、2変数のforループでイテレーションするコードのパフォーマンスが改善 Perl5.40で追加された、組み込み関数を提供する builtin モジュールの indexed 関数を利用することで、配列のindexと要素の列挙が楽に書けるようになっていました。 use v5.40 ; my @array = qw( red blue green ) ; for my ( $index , $value ) (indexed @array ) { say " $index => $value " ; } ただし、これは配列のインデックスと値のリストを実際に生成する点など、通常の for (@array) のようなループ文と比べて効率的でないコードとなっていました。 5.42からは内部的には配列のインデックスと値のリストを実際に生成するのではなく、通常の for (@array) と同じ方法で配列をイテレーションするようになりました。 まとめ source::encoding プラグマが追加されことは影響が大きそうで、 use utf8 していないころに書かれたコードは見直したほうがいいかもしれません。 その他は今回も細かい改善点が多いといった感じですが、Perlに足りなかった機能が追加されたりPerl4のころの構文を無効にできるようになったりと確実に過去のバージョンより使いやすくなっています。 次のバージョンでは名前付き引数が追加されるなど、大きな変更がありそうで楽しみです。 この記事では書けなかったこともあるので詳しいことが気になった方は 公式ドキュメント もぜひ読んでみてください。 use v5.42; のような構文のことです。Perlでは後方互換性を保つため古い機能は無効にできるように、新しい機能は有効にできるようになっていますが、 feature bundle はそういった各機能を、バージョンごとに推奨されるものをまとめて有効/無効にしてくれるプラグマになっています。 ↩

動画

該当するコンテンツが見つかりませんでした

書籍