takaha.siの技術メモ

勉強したことをお伝えします。ちょっとでも誰かの役に立てればいいな…

Firestoreの新しいライブラリを思いついたんだけど

思いつきのメモなんですが

FirebaseのFirestoreを読み書きするTypeScriptのコードってちょっと書くとすぐごちゃごちゃし始めて脳負荷が高いコードになりがちじゃないですか?

テーブル?の定義とセーブロードのロジックが混じり合ってカオスになるのがとっても汚らしいと思うんですよ。

なので、アノテーションつかってこういうふうにかけるライブラリあったらよくないかなとおもいました。

class Game extends FSModel {
    // このアノテーションで/usr/games/ 以下に保存される
  @definePath("/user/games")
  
  // これでFireStoreのエントリを定義する
  // storeEntityアノテーションがつけられたメンバー変数は
  // toFirestore(), fromFirestore()で自動的に保存される
  @storeEntity
  public gameTitle: string;

  @storeEntity
  public gameID: GameID;

  @storeEntity
  private published: boolean = false;

  public setPublish() {
    this.published = true;
  }
}

const e = new Game();
e.gameID = "Foo";
e.gameTitle = "Bar";

e.setPublish();

// これでFireStoreに保存
e.toFirestore();

// FireStoreから取ってくる
const ref = Game.fromFirestore();
ref.gameTitle = "Hoge";
ref.gameID = "Fuga";

// もっかい保存しなおす
ref.toFirestore();

こんな感じで書けると嬉しい。

ロシアの軍事ドローンのオルラン10(Orlan-10)ってまともに使えないんじゃない?

Twitterにも書いたんだけど、これの補足。

ロシアの軍事ドローンオルラン10(Orlan-10)。偵察用に使うものなのかな?こいつの中身が結構な割合で日本製の部品を使用しているとのこと。カメラ、エンジン周りは全部日本製。民生品を徹底利用してる。

この動画をソフト屋さんの観点からみて「このドローン。画像はさておき動画を見るのは辛いんじゃないか?」という疑惑がふつふつ湧いてきたのでそれについて考えを書きなぐる。

まず、気になったのは民生品のカメラからどうやって画像とか動画とか取り出してるの?っていうところ。カメラはCanonのEOS Kiss X9iが使われてるらしい。私もCanonユーザやって長い(ずっと5dユーザでmark II, mark IIIと持ってる)ですが、Canonのカメラからリアルタイムで動画を取り出すのは結構しんどい。

選択肢はかなり限られて私が思いつく限り以下の2つ。

  1. Webカメラモード
  2. HDMIパススルーモード

ここで、まず(1)はないかなと。Webカメラモードは画質がかなり制限される。これを使うのであればEOS Kissみたいないいカメラ+レンズを使う必要は全く無いので違うのでは?と。

次に(2)は有り得そうかなと。HDMIパススルーモードはかなり良い画質の動画が取れます。しかし、このHDMIパススルーモードを備えたラインナップは限定的で、少なくともEOS Kiss X9iにはその機能はなかったはず。もっとハイエンドの(5d系統以上)カメラじゃないとだめ。非公式の改造ファーム入れるとできるけど…それを使ってるのかしら?

などとぼんやり考えてたら、友達からUnboxing動画があると教えてもらったので見てみた。結構詳しくバラしてて内部基盤とかもちゃんと見れる

www.youtube.com

それで、6:40秒あたりのこれ。多分これが答えかなーと。

多分これが答え。この端子をCanonのカメラに指してSDカード経由で。つまりFATレイヤー経由で取ってきてるんだと。。。おおーなるほど賢い!日曜電子工作勢みたいなことやってるんですね。

ここから考察です。このドローン画像はさておき動画を見るのはかなり辛いんじゃないの?と思いました。見れてもブツブツ途切れてまともに見れなさそう…

そう考える理由ですが、まずFATレイヤーをHookして取り出すとなるとSDカードにデータが降りてくるタイミング、つまりカメラ側がfsync発行するタイミングが完全にカメラ依存になり、そこは制御することはできない。これが結構問題。

すると、再エンコーディングとかで必要なbufferingのタイミングの自由度がかなり下がりそうです。動画を電波で飛ばすための再エンコーディングは必須だと思いますが、その時、通信帯域やCPUパワーが空いてるにも関わらずカメラ側からデータが降りてこず完全に待ちぼうけをくらい、次の瞬間はCPUパワーや通信帯域が一度にさばききれない量のデータがカメラ側から(fsyncされて)どかっと降りてくるみたいなことが普通に起こりそうです。

こういった状況下で安定したストリーミングを行うのは大変そうです。安定配信には輻輳制御はかなり重要で、各々のコンポーネントが互いに調整しあわないとすぐに不安定になると思います。しかし、肝心のカメラ側の書き出しタイミングがいじれない以上選択肢が大幅にさがり、安定した動画再生は難しいのでは?と感じました。

割り切って動画は見れたら嬉しい!基本的に画像だけでなんとかする!みたいな運用をしてるんでしょうかね?

現物を隅から隅までみたわけではないので、動画用のカメラは別にあるとかも考えられると思いますが、以上考察でした。

追記。教えてもらったんですが、このOrlan-10から撮ったであろう動画見るとやっぱりかなりガクガクしてますよね。カメラを安定させるジンバルが不安定である可能性もありますが。。。さて。。。

WSL2でCentOS9Streamを動かしたあと一般ユーザでログインしたい

blog.yamk.net

ここに書かれてるように、アンオフィシャルなCentOS9StreamとかをWSL2にimportしたとしよう。

importしたままだと、WSLを起動したときrootユーザになってしまう。これを自分で作った一般ユーザでログインできるようにしたい。

例えばCentOS9Streamという名前でimportしたWSL環境にtakahashiという一般ユーザでログインしたい場合。Power Shellで以下のようにすればいい

wsl --distribution CentOS9Stream -u takahashi

次回。Windows Terminalから起動したときはtakahashiでログインするようになる。

zfsはcloseとwriteが遅いです

Linuxzfsの話です。zfsのI/O特性を知っておきましょう。

結論から言います。zfsは「書く量を増やせば増やすほど、それにつられてclose(2)とwrite(2)が顕著に遅くなる」という性質があります。ちなみにLVMではこれは見られません(zfsのほうが色々付加価値があるので、フェアな比較ではないことは重々承知しています)

これを検証するためにちょっとしたマイクロベンチマークを書いてみました。以下のGistにありますので見てください。

write_bench.c · GitHub

このプログラムは、指定されたファイルに対して、特定の大きさのブロック(bs)を、特定の大きさまで(total_size)for文でひたすらseekしてwriteするという単純なプログラムです。

20GBのzvolを/dev/pool/20gbに作って、書き込み量のtotal_sizeを変えながらclosewriteのレイテンシの結果を見てみましょう。ちなみに各システムコールのレイテンシはstrace -c -fで集計した値を使っています。

total_size (GB) write (usec) close (usec)
1 37 454748 (454.748 ms)
5 47 1277080 (1277.08 ms)
10 47 1693095 (1693.095 ms)
15 50 1730856 (1730.856 ms)
19 53 2232082 (2232.082 ms)

total_size (書き込み量)を増やせば増やすほど。write, closeともにレイテンシが上昇していくのが見られます。特にcloseのレイテンシの上昇はとても大きいです。

一方、LVMではこの特性は見られません。書き込み量を増やしても、大体同じぐらいのレイテンシに収まります。

total_size (GB) write (usec) close (usec)
1 93 81
5 91 3
10 98 90
15 96 79
19 92 91

若干遅くはなってますが、zfsほどではありませんね。

今回close, writeの測定しかしていませんが、fioで色々実験した限りですと、非同期I/Oのpwriteでもio_submitでも同様の傾向が見られます。

というわけで、LVMと同じI/O特性を期待してzfsを使うと痛い目を見ます。特にcloseのレイテンシの増加率はLVMに比べるととても大きいです。書き込み量を増やすときはなるべくcloseは発行しないようにしましょう。

p.s. 書き込んだ量ではなくて、汚したブロックの量(つまり同じ場所に書き込みまくったらレイテンシは変わらないのではないか?)によって変わるのではないかという疑惑ありますが、それの検証はまた今度。

WSL2でDockerが動かない

WSL2最高ですね。WSL2上のLinuxjavaとかnodeとかrubyとかいれれば、IntelliJとかWebstormとかRubyMineからふつープロジェクトインポートして開発に使えます。もうx86_64ではないMacを使う必要はなくなるわけです。バイバイMac。ARMの本番環境がまだ普及していない以上開発機としてはもう使えません。

WSL2のUbuntu 20.04.4 LTS上でDockerを動かしたいと思うわけですが、公式ドキュメントの通りインストールはスムーズに行くんですけど、どうもDockerが起動しない!

$ docker ps
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

デーモンが起動してないのかしら?どうやらWSL2のUbuntu上だといちいち手動でDockerデーモンをあげてやらないと使えないっぽいですね。

$ sudo service docker start
 * Starting Docker: docker
$ sudo docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:bfea6278a0a267fad2634554f4f0c6f31981eea41c553fdf5a83e95a41d40c38
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

O_DIRECTでopenするときはmemalign使わないとだめだよ

O_DIRECTopenしてwriteしたいときがたまにあると思います。

通常のopenしてwriteだとカーネル内部でページキャッシュが使われて、カーネル内部でのメモリコピーが何回か行われちゃいます。が、O_DIRECTだとページキャッシュをバイパスできて、その結果カーネル内部でのメモリコピー回数が減ります。

通常O_DIRECTは使わないんですけども、自前でバッファリングとかしてるアプリケーションとかだと(データベースとか)、カーネルのページキャッシュ層と競合してわけわかんないことになったりするのでOFFにしたりします。

あとは、ベンチマークプログラムでファイルシステムの生の性能を知りたいときなどもコレを使います。

んで、僕知らなかったんですがO_DIRECTでopenしたfdにread/writeするときってalignされたメモリ空間じゃないと受け付けないんですね。つまり、通常のmallocだとだめなんです。これで30分ぐらいハマりました。

普通のmallocで確保したバッファをwrite(2)に渡そうとすると、perror()Invalid argumentエラーが出ちゃいます。

write
: Invalid argument

なんでmallocじゃなくてposix_memalignとか使いましょうって話です。こんな感じ。

  if (posix_memalign((void **)&buf, 512, bs)) {
    perror("posix_memalign");
    exit(EXIT_FAILURE);
  }

ページ空間をまるごと直接アロケーションしちゃってるんですかね?

まあ、manにはちゃんと書いてあります。ちゃんと読めって話ですね。

   O_DIRECT
       The O_DIRECT flag may impose alignment restrictions on the length
       and address of user-space buffers and the file offset of I/Os.

man7.org

すべてのコードは以下の通り:

gist.github.com

O_DIRECT tipsでした。

docker-composeで作ったコンテナ同士をネットワークでつなげる

github.com

ファイルはここに。

f:id:tkh86:20220325011310p:plain
複数Docker間で通信

図のようにdockerコンテナを2つ(図中のmy_clientとmy_server)つくって、それらコンテナ間で通信したい。

単一のdocker-compose.ymlの中に2つ以上のコンテナが定義されてる場合は別に問題ない。同一ymlファイル内部でコンテナの名前がホスト名として使えるので、それを指定して通信できる。単一のymlファイルに定義されたDockerコンテナはすべて同一ネットワーク(Bridge)に所属できるのでコンテナ間通信ができるわけ。

問題はこれら2つのdockerコンテナが別々のdocker-compose.ymlファイルで定義されてるとき。この場合は、自分で独自のネットワーク(図中のmy_network)を構築した上で、接続したいコンテナをその独自ネットワーク(my_network)に所属させる必要があります。それで、コンテナ同士が接続がコンテナ名をホスト名として相互に通信できます。

(ちなみにBridgeとは、コンテナ内部のみで通信できるNATみたいなものが生成されて、そのNATネットワーク内部でコンテナが動く。BridgeなのにNATとはこれ如何に?)

ネットワークはdocker-compose upする前に自分で生成しときましょう。以下のように:

#!/bin/sh

# make a network
docker network create --driver bridge my_network

cd ./client && docker-compose -f docker-compose.yml up -d
cd -
cd ./server && docker-compose -f docker-compose.yml up -d

こんな感じで。docker-compose.ymlファイルは以下の2つ。

version: '3.8'
services:
  my_client:
    build:
      context: ./
      dockerfile: Dockerfile
    networks:
      - my_network

networks:
  my_network:
    external: true
version: '3.8'
services:
  my_server:
    build:
      context: ./
      dockerfile: Dockerfile
    networks:
      - my_network

networks:
  my_network:
    external: true

これでmy_clientコンテナに入って通信してみましょう。

kazushi@dev2:~$ docker ps
CONTAINER ID   IMAGE              COMMAND                  CREATED          STATUS          PORTS     NAMES
62af57565a5a   server_my_server   "/bin/sh -c 'while :…"   35 minutes ago   Up 35 minutes             server-my_server-1
7adf566c7103   client_my_client   "/bin/sh -c 'while :…"   35 minutes ago   Up 35 minutes             client-my_client-1
kazushi@dev2:~$ docker exec -it 7adf566c7103 /bin/bash
root@7adf566c7103:/client#
root@7adf566c7103:/client# ping my_server
PING my_server (172.25.0.3) 56(84) bytes of data.
64 bytes from server-my_server-1.my_network (172.25.0.3): icmp_seq=1 ttl=64 time=0.043 ms
64 bytes from server-my_server-1.my_network (172.25.0.3): icmp_seq=2 ttl=64 time=0.048 ms
64 bytes from server-my_server-1.my_network (172.25.0.3): icmp_seq=3 ttl=64 time=0.047 ms
64 bytes from server-my_server-1.my_network (172.25.0.3): icmp_seq=4 ttl=64 time=0.047 ms
64 bytes from server-my_server-1.my_network (172.25.0.3): icmp_seq=5 ttl=64 time=0.048 ms
64 bytes from server-my_server-1.my_network (172.25.0.3): icmp_seq=6 ttl=64 time=0.037 ms
64 bytes from server-my_server-1.my_network (172.25.0.3): icmp_seq=7 ttl=64 time=0.047 ms
64 bytes from server-my_server-1.my_network (172.25.0.3): icmp_seq=8 ttl=64 time=0.047 ms

--- my_server ping statistics ---
8 packets transmitted, 8 received, 0% packet loss, time 7162ms
rtt min/avg/max/mdev = 0.037/0.045/0.048/0.003 ms
root@7adf566c7103:/client#

通信できてますねping my_serverで通信できます。

external: trueの意味

ちなみのymlファイル内に記述されてるexternal: trueの意味ですが、docker-composeですでに作成済みのnetworkを再利用するときはtrueを指定します。するとdocker-compose -f docker-compose.yml upのときに通常生成される独自ネットワーク(Bridge)が生成されず、あり物のmy_networkを探して使用するようになります。逆言うと、my_networkが作られてない場合はエラーが出てコンテナが起動しません。上述したように事前にdocker network create --driver bridge my_networkで作っておきましょう。