コツを覚えたというのもあるかもしれんが,3区間ともタイム更新できた.また,1時間足らずのところで TSS 100 ということは,FTP ちょっと上がった?
SaaS 屋さんブログ記事 – NFS+sqliteで苦労した話から学ぶ、問題解決の考え方
ええ記事やな.後でちゃんと読む.
https://techblog.raccoon.ne.jp/archives/1635140633.html
Klipsch R-40M インストール
去る 9/28 に,注文していた Klipsch R-40M が届いた.当日開梱してインストールした時の記録写真を数点掲載.
今日のは楽
ハードで 9 out of 10 とか脅されたが,全然楽.今のところ,#3 がいちばんしんどい.
Zwift で 100km
台風が以前の予報より早く過ぎ去り,明日は絶好のライド日和になるはずだが走れないので,今日のうちに長いめを…と思い 7:30 からのイベントに参加した.
2.3-2.5W/kg と advertise されていたのでぎりぎりもつかどうか?と予想.最初の30分くらいはソリッドに Zone 3 で,これは途中でリタイアかなと思っていたら,その後少し脚が回るようになってきたのと,全体のパワーが少し落ちてきたのとでなんとか完走できた.平坦基調のコースも幸いしたが,たまにくる登りは思い切って standing で 250W くらい踏んだ方が,集団内の位置も良くなって後しばらく楽できるのと,脚も少し軽くなる効果がある気がする.
リーダーの指示も良く,脱落しそうになることもなく結局 100km を2時間半ちょっとで完走.Zwift で 100km は二回目の達成.平均時速が思ったより速く,3時間かからずそれも助かった.平均出力 163W(2.43W/kg),TSS 175.
フラットファイル + シェルスクリプト「で」トランザクション
ファイルシステム「の」トランザクションではなく,データリポジトリにフラットファイルを使用して,ビジネスアプリケーションをシェルスクリプトで組むときにどうやって「トランザクション」を実現するかという話.
トランザクションは「データ(のセット)を(業務上)整合性のとれた一貫性のある状態に保ちながら更新すること」と言って良いと思う.これを実現するために,「コミット」や「ロールバック」の手段を整備することが必要になる.
SQL の場合,こんな感じにできるらしい.
BEGIN;
UPDATE accounts SET balance = balance + 10000 WHERE name = 'Jiro';
UPDATE accounts SET balance = balance - 10000 WHERE name = 'Taro';
COMMIT;
二つ目の UPDATE 文で,balance に「0または自然数」制約みたいなものが付いていて,なおかつ Taro の口座に 10,000円入っていなかった場合は,二つ目の UPDATE は失敗する.このとき DBMS は勝手に BEGIN の時の状態にデータを戻してくれる – Jiro の口座への 10,000円追加を無かったことにしてくれる(ロールバック).
これに加えて上記の SQL コードには陽に現れていないが,DBMS ではこのトランザクションが開始されると他のプログラムから Taro や Jiro の口座データが更新することはできない仕組みになっている(排他制御).同等のことを,フラットファイル + シェルスクリプトでどれだけシステマティックに実現できるかがテーマである.
排他制御をどう実現するか
ファイルアクセスの排他制御というと「ファイルロック」だが,UNIX では「アドバイザリロック」を使用するのが一般的だ.「マンダトリ(強制)ロック」がサポートされる OS もあるようだ(この辺のことはこちらを参照)が,ここではアドバイザリロックを前提にする.アドバイザリロックというのはあくまで「アドバイザリ」なので,カーネルがプログラムのファイルアクセスを禁止しているわけではない.従って,排他が問題になるプログラム群は,共通の排他制御のルールに従う(簡単に言うと,ファイルアクセスの際にいちいちロックの有無を確認→ロックする→アクセスする→ロックを解除する)ことが要求される.
アドバイザリロックを実現するにも何通りかのやり方がある.字面上いちばん名が通っているのが flock(2) である.ただ flock(2) は C プログラマ向けに用意された API であり,シェルスクリプトから利用するにはなんらかのパッケージを介在させる必要がある.Linux の場合は flock(1) コマンドが用意されているが,他の UNIX OS では一般に利用できない.また flock(1) も flock(2) も NFS に対応していないという仕様上の制限もある.
そういったことから,歴史的に「ロックファイル」の方式がよく使われる.これは,一つのファイルシステム上にパスが同じファイルは複数存在できないという仕様を利用しており,「ファイルAをロックする」ということを「(ファイルAに対応した)ロックファイルaを作成する」ということに読み替えてアドバイザリロックを擬似的に実現するものである.「ロックファイルaの作成を試みたが,できなかった」ということは「ファイルAは他のプログラムで使用されている」ということなので,プログラムはファイルAへのアクセスをいったん諦めて,時間をおいて再挑戦するなり,エラー終了するなりという挙動をするようプログラマがロジックを組む責任がある.
ところで「ファイルロック」を実現するのに「ロックファイル」を使用する,というのが用語上混乱をきたすことがあるかもしれない(自分がそうだったので).「セマフォファイル」という呼称にするとその心配がなくなるかもしれないが,本来セマフォは共有資源にアクセスできるプログラムの数を制限するためのものなので,排他制御の文脈で使うと(共有資源同時アクセス数 1 とすれば排他制御にもなるが)それはそれで混乱するかもしれない.
コミットをどう実現するか
フラットファイル + シェルスクリプトにおいて,データの更新方法はいくつかある.ここではフラットファイルがレコード(行)xフィールド(列)の二次元形式を取っていると想定すると,
レコード追記型更新
new_record="$(<処理>)"
printf "%s\n" "$new_record">> <フラットファイル>
非追記型(ランダム更新 – レコード挿入,削除,フィールド値更新)
<処理> < <フラットファイル> > <一時ファイル>
mv <一時ファイル> <フラットファイル>
こんなところだろう.ビジネスアプリケーションにおいては,レコード追記型更新で全てを済ませられる場合は少ないだろうから,ここは非追記型を想定する.非追記型はレコード追記型にも使用することができる.また,非追記型の更新は「更新試行」→「確定」というトランザクションのコミットの動きそのものである.あとは,トランザクションに関係する複数ファイルを「全て mv で更新する・しない」,言い方を変えると,複数ファイルのうち一部だけ mv で更新されてしまわないようにする制御を加えれば,トランザクションのコミットは実現可能ということになる.
なお mv による更新の魅力は,一時ファイルと本番ファイルが同一ファイルシステムに格納されている場合,それらを置き換えるにあたって I/O が発生しないことである(ファイルシステムメタデータの更新のため微量の I/O は発生するが,データを複製するための I/O は発生しない).この場合,一時ファイルによるフラットファイルの置き換えは一瞬で済ませることができる.
ロールバックをどう実現するか
コミットの考察で,非追記型更新を前提にすると述べた.この場合,ロールバックとは何かというと「mv で一時ファイルをフラットファイルに置き換えないままプログラムを終了すること」で良かろう.ただし単一ファイルの更新ならそれで良いのだが,複数ファイルの更新を行うトランザクションでは少々他の考慮も必要である.
2つのファイルAとBを更新するトランザクションがあったとする.AとBにロックを施して排他を宣言し,ファイルAとファイルBをそれぞれ更新していったん一時ファイルAと一時ファイルBを作成までできた(ここまででエラーが発生した場合は,一時ファイルAもBもない,や,一時ファイルAだけ生成されているという状況だから,そのままプログラムを終了すればファイルAとBの内容は整合性がとれたままである).
問題はこの後で,次は
- 一時ファイルAをファイルA に mv
- 一時ファイルBをファイルB に mv
するわけだが,前者が成功して後者が失敗した場合は,ファイルAを更新前の状態に戻さなければならない.ここが工夫のしどころとなる.
ファイルAの更新前の状態を保存するためにまず思いつくのは,トランザクション開始時に cp でバックアップを取っておくことである.
cp ファイルA ファイルA.org
そうして,一時ファイルBをファイルBに mv する操作が失敗したら,
mv ファイルA.org ファイルA
これでロールバックができた,ということになる(この操作が失敗したらどうするかというのは考慮事項だが,それはアプリケーションの要件によって変わってくるのでここでは議論しない.ロールバックが失敗したという通知をさせ,またはログを吐かせ,いったんシステムを停止する – ユーザーからのアクセスを遮断して管理者だけが操作可能にする – というのが定石だとは思う)
問題は,cp によるバックアップでデータ I/O が発生してしまうため,トランザクション開始時のオーバーヘッドが大きいということである.ファイルが数GBあった場合,トランザクション開始操作に数10秒を要することも想定される(RAM を大量に搭載していてファイルシステムキャッシュが潤沢に使えている場合は一瞬で完了する可能性はある – が毎回それは期待できない).
これを解決するのがハードリンクである.大学で共用計算機の SunOS を利用する際に「リンク」は基本「シンボリックリンク」を使え -「ハードリンク」は使ってはならぬと教え込まれたので,その理由もよく考えないまま(今から考えると,複数のファイルシステムにホームディレクトリが収容されていたからとかそういうことなんでは,と思うが)ハードリンクには馴染みのないままこれまで過ごしてきたが,cp の代替手段としてこのハードリンクが使える.
ln ファイルA ファイルA.org
これで「ファイルA」のデータは「ファイルA.org」という名前でも参照できるようになった.内部ではファイルシステム上で同じデータに違うラベルが追加された,ということなので,データ I/O は発生しない.この操作をおこなった後,「一時ファイルA」を「ファイルA」に mv して更新すると,「ファイルA」を指すと更新されたデータが得られるが,「ファイルA.org」を指すと更新前のデータが得られる.
従って,この後「一時ファイルB」を「ファイルB」に mv して更新する操作が失敗した場合,mv ファイルA.org ファイルA でファイルAを元の内容に戻すことが可能である.
定式(決め事)化
トランザクションでのデータ更新,コミット,ロールバックをどう実現するかの骨子は以上である.あとは以上の事柄をプログラマにできるだけ簡単になおかつ確実に実行してもらうための仕組み(決め事)作りである.
現在のところ,以下のような決め事とし,それをサポートする関数 INIT(), BEGIN(), COMMIT(), ROLLBACK(), END() を実装している(関数やファイル名の字面は実際とは異なる)
- INIT() でトランザクションIDを設定して,本トランザクションで一時ファイル等を置く一時ディレクトリを作成
- files-to-protect(トランザクション中,他のプログラムからのアクセスを遮断しなければいけないファイル群)を定義
- files-to-update (トランザクション中更新するファイルと,更新一時ファイル名)を定義
- BEGIN() で排他ファイルロックとハードリンク作成によるバックアップを実施
- トランザクション処理はサブシェル内に記述し,エラーが発生すれば non-zero exit させる
- non-zero exit (異常終了)した場合は ROLLBACK() する(ただし,この場合の ROLLBACK() は実質ほぼ何もしない – バックアップハードリンクを消すだけである)
- zero exit (正常終了)した場合は COMMIT() するが.ここで失敗すると ROLLBACK() する
- END() で排他ファイルロックを解除,バックアップハードリンク消去,トランザクション用一時ディレクトリを削除する
# トランザクションの初期化
INIT || exit 1
# このトランザクション中,排他アクセスが必要なファイルをリスト
cat <<EOF > files-to-protect || exit 1
ledger-Taro
ledger-Jiro
EOF
# このトランザクション中,更新するファイルとその一時ファイル名をリスト
cat <<EOF > files-to-update || exit 1
ledger-Taro ledger-Taro.new
ledger-Jiro ledger-Jiro.new
EOF
# トランザクション開始
BEGIN || exit 1
dt="$(date "+%Y/%m/%d %H:%M")" || exit 1
name=フリコミ
amount=10000
# トランザクション処理(事前に名前を決めておいた一時ファイルに更新データを出力する)
(
# Taro の通帳更新
printf "%s\t%s\t%d\n" "$dt" "$name" "-$amount" > /tmp/transaction || exit 1
cat ledger-Taro /tmp/transaction > ledger-Taro.new || exit 1
# Jiro の通帳更新
printf "%s\t%s\t%d\n" "$dt" "$name" "+$amount" > /tmp/transaction || exit 1
cat ledger-Jiro /tmp/transaction > ledger-Jiro.new || exit 1
)
es=$?
if [ "$es" -ne 0 ] ; then
# トランザクション処理が失敗した場合 ROLLBACK() が一時ファイルを消去する
ROLLBACK || exit 1
else
# トランザクション処理が成功した場合 COMMIT() が一時ファイルを本番ファイルに mv する
COMMIT || {
# コミットに失敗した場合 ROLLBACK() がバックアップハードリンクから本番ファイルをリストアする
ROLLBACK || exit 1
}
fi
# トランザクション終了(END() がファイルロック解除,バックアップハードリンク消去を行う)
END || exit 1
Saris Cup 2回目
なんとか第二集団についていこうとすると,5分くらいパワーグラフが赤々していたのでどっかで脱落してまうな…と思っていたが,そのうち集団のパワーも落ちてきて,なんとか粘れた.黄色領域でも多少は回復❤️🩹できるんか?などと頭が巡ることもあるが,ちょっとした数十メートルののぼりがくると,220W程度出さねばならず,それがまぁ辛い.こんな平坦基調のコースでもコレだから,上りが目立つコースだとイッパツで脱落するに違いない.Zwift Academy でスプリンター判定が出てしまったので,今回は最後の300mくらいを頑張って踏み込んだ.結果,第二集団では先頭ゴール.
(Zwift 上の) FTP が 200 → 203 になってもうた.ワークアウトしんどなるわ…
Klipsch R-40M をポチる
「高能率スピーカーは生々しい」という話がある.
小型スピーカーは基本的に低音が出ないので,わざと高音を出にくくして(高音のドライバーに入る入力を小さくするようネットワークをチューニングして)やるというのが定石のようなのだが,そうすると高音の音の解像度が下がってしまうということらしい.現在使用中の Dali Zensor 1 の場合は 87dB/2.83V/m というカタログスペックだが,大体 80dB 代後半が小型ブックシェルフスピーカーの定番値だ.室内楽でもう少し生々しい音が聴けないか(たとえばヴァイオリンで弓が弦に当たる音が聴こえるとか)?ということで高能率で評判の Klipsch スピーカーをポチった.モデルはいくつかあるが,先日サブウーファーを導入したので,ウーファーはもう極小で良いという判断から最小モデルの R-40M にした.
やっぱり来たら数字を見てわかったような気になりたいので,以下にスペックをダンプしておく.
項目 | Dali Zensor 1 | Klipsch R-40M |
形式 | 2ウェイ2スピーカー・バスレフ | 2ウェイ2スピーカー・バスレフ |
HF | 25mm ドーム型ツイーター | 2.54cm アルミニウム LTS ツイーター |
LF | 130mm コーン型ウーファー | 10.16cm TCP ウーファー |
インピーダンス | 6Ω | 8Ω |
再生周波数帯域 | 53Hz – 26.5kHz | 71Hz – 21kHz |
クロスオーバー | 2.9kHz | 1,770Hz |
出力音圧レベル | 87dB/2.83V/m | 91dB @ 2.83V/1M |
サイズ(WHD, cm) | 16.2 x 27.4 x 22.8 | 14.6 x 29.8 x 21.6 |
質量 | 4.2kg | 3.2kg |
定価(ペア,税抜) | 37,800円 | 43,000円 |
発売年 | 2011 | 2022 |
Zwift Academy 二発目
昨晩のワークアウト.TSS 60 代ならまだ余裕ある.
ところで,baseline の脚質が出た.
Medium 区間は様子見してしもたからな…Long はこれでいっぱいいっぱいだと思う.伸びはあまり期待しない.
Zwift Academy 一発目
この先どうなんのか…不安しかない.2月にやった 4wk FTP boost よりはマシだと信じたい.