Atomic Package Source Syncing

Deepin 15 發佈後, 使用者一下衝了上來, 造成目前公司的 IT 基礎建設及實務作法中, 平時不在意的問題就會被放大. Package source 的同步是其中必須及早解決的問題.

Linux distro 的軟件發佈方式, 基本都是打完包後放上 mirror site 或 CDN. 舉 Debian (Deepin 基於它) 為例, packages 在更新時, 除了要將相應的 .deb 放上, 還要將整個 package source 的 metadata 也一併更新. 這樣, user 就可以 apt-get update 下載 metadata, 知道有哪些 packages 哪些版本可用. 再 apt-get install 下載真正的 .deb 安裝.

問題出現在, 當 metadata 跟實際的 .deb file 不對應時, 就會造成 apt-get install 失敗. 原因是, 整體 package source 的更新並非一步到位的操作 (atomic operation), 使用者看到了中間的過程, 例如 metadata 被更新成新版的了, 但相應的 .deb 還沒上傳, 或是反過來也有一樣的問題.

所以, 解決問題的大前提是, 要避免 user 看到更新中的 package source, 要不下載到舊的, 要不就下載到新的, 沒有中間狀態.

我們可以這麼做, 每次更新的 package source 都放在不同的目錄下, 以日期及時間命名之, 例如

$ mkdir 20160115/
$ touch 20160115/file1
$ touch 20160115/file2

$ stat -c %n,%i 20160115/*
20160115/file1,452745
20160115/file2,452746

file1file2 用做要被更新的檔案, inode number 分別是 452745 及 452746.

另外, 因為對外要維持相同的 URL, 可對 20160115 產生個 symlink

$ ln -s 20160115/ deepin

對外提供下載服務的目錄 (以上例而言為 20160115) 在更新完成後就不再變動 (read only), 如此可避免上述問題, 但, 要怎麼進行更新?

很簡單, copy 一份, 更新時對複本進行.

Copy 時可以 hard link 的方式進行 (shallow copy, 一般是增加同一個 inode 的 reference count), 而非完整 copy (deep copy, 連內容一起, 同時產生新的 inode number), 可大大的減少空間的佔用及 I/O 量. cp 指令配合參數 --link | -l 可滿足我們的需要, 例如

$ cp -al 20160115/ 20160116/
$ stat -c %n,%i 20160116/*
20160116/file1,452745
20160116/file2,452746

接著準備公司內部要更新到外部 package source 的內容. 假設 file1 沒有變動, file2 被 drop 了, file3 則是新增的 package

$ mkdir internal
$ cp -a 20160115/file1 internal/
$ touch internal/file3

然後就可以用 rsync 同步了

$ rsync -av --delete internal/ 20160116/
sending incremental file list
deleting file2
./
file3

sent 146 bytes  received 47 bytes  386.00 bytes/sec
total size is 0  speedup is 0.00
$ ls 20160116/ 
file1  file3

同步完以後, 就可以將 symlink 切換過去了. 簡單做可用 ln 指令完成

$ ln -sfn 20160115/ deepin

但這麼做其實無法達到 atomic 的要求. 在 POSIX spec 中要求 symlink() 本身是 atomic 操作, 但只有在新建時是這樣. 當目標 (這裡是 deepin) 存在的情況下, 需要先 unlink()symlink(), 如下例

$ strace ln -sfn 20160116/ deepin
execve("/bin/ln", ["ln", "-sfn", "20160116/", "deepin"], [/* 53 vars */]) = 0
...                                = 0
lstat("deepin", {st_mode=S_IFLNK|0777, st_size=9, ...}) = 0
lstat("deepin", {st_mode=S_IFLNK|0777, st_size=9, ...}) = 0
stat("20160116/", {st_mode=S_IFDIR|0755, st_size=4, ...}) = 0
symlink("20160116/", "deepin")          = -1 EEXIST (File exists)
unlink("deepin")                        = 0
symlink("20160116/", "deepin")          = 0
...

解決方案是, 先在同一個 file system 中建立新的 symlink, 配合 mv 命令重命名為 deepin, 因為 mv 用到的 rename() 是 atomic 的操作, 可滿足我們的要求

$ ln -s 20160116/ .deepin
$ mv .deepin deepin