rui312/9ccを写経する-その2

https://github.com/rui314/9cc/commit/2f62e5267a1c2874dcfa674cf8654e0cb3f189d6

コミットコメントが仰々しい感じがする。

test.sh

とりあえずここを見れば次に実装することがわかる。 コードを見る限り、そこまで追加機能はないようだが……?

int pos = 0;
enum {
  ND_NUM = 256,     // Number literal
};
typedef struct Node {
  int ty;           // Node type
  struct Node *lhs; // left-hand side
  struct Node *rhs; // right-hand side
  int val;          // Number literal
} Node;

posはプログラムの読み込み位置かな?NodeはTokenとどう違うんだろう?

Node *new_node(int op, Node *lhs, Node *rhs)
Node *new_node_num(int val)

んーまだよくわからない。

Node *number()

数字のtokenから数字のnodeを作っている?

Node *expr()

再帰的なNodeを作っているようだ。

char *regs[] = {"rdi", "rsi", "r10", "r11", "r12", "r13", "r14", "r15", NULL};

んーアセンブラの変数(レジスタ)の名前列?これは予約語なのかなあ、単純に数字が増加しているだけではないし、よくわからない。 アセンブラを少し学ぶ必要がありそうだ。

https://qiita.com/edo_m18/items/83c63cd69f119d0b9831

によるとrdiシステムコールに使われるレジスタ。でも他はそうではないっぽい。

http://milkpot.sakura.ne.jp/note/x86.html

によると汎用レジスタの一覧にすべて収まる。

おそらく、システムコールを使うときにはrdiは意味を持つが、そうでなければ汎用的に使えるレジスタ、ということかな? レジスタは、とりあえず8個使うようだ。

char *gen(Node *node)

nodeを再帰的にたどって、node毎に利用するレジスタを割り当てるっぽい。 そのうえで、node->tyからアセンブラの足し算引き算コードを作り出す。 レジスタはnodeに割り当てられたものを使う。

  printf("  mov rax, %s\n", gen(node));

これはつまりトップレベルに割り当てられたレジスタを終了ステータスレジスタに割り当てているので、 最終的にこのレジスタに全ての計算結果が返ってきているはずということか! coolだ。

ここまで写経してきて、ふと思い立って1+2+3+4+5+6+7+8の出力アセンブラを見比べてみた。

before

.intel_syntax noprefix
.global main
main:
  mov rax, 1
  add rax, 2
  add rax, 3
  add rax, 4
  add rax, 5
  add rax, 6
  add rax, 7
  add rax, 8
  ret

after

.intel_syntax noprefix
.global main
main:
  mov rdi, 1
  mov rsi, 2
  add rdi, rsi
  mov r10, 3
  add rdi, r10
  mov r11, 4
  add rdi, r11
  mov r12, 5
  add rdi, r12
  mov r13, 6
  add rdi, r13
  mov r14, 7
  add rdi, r14
  mov r15, 8
  add rdi, r15
  mov rax, rdi
  ret

んー違いはあるけど恩恵はまだなさそうな感じがする。次行ってみよう。

https://github.com/rui314/9cc/commit/dca0fdb71df98d8500170ff8178eaac0c8f9edaa

test.sh

try 153 '1+2+3+4+5+6+7+8+9+10+11+12+13+14+15+16+17'

んーこれまたhttps://github.com/rui314/9cc/commit/916237396164ae25557b62bf0d1ca9d9cf8c2070からのメリットが見えない感じ。 https://github.com/rui314/9cc/commit/2f62e5267a1c2874dcfa674cf8654e0cb3f189d6から予想できるのは、おそらくレジスタを有効に使って計算が長くても対応できるようにしそうなこと。

// Intermediate representation
enum {
  IR_IMM,
  IR_MOV,
  IR_RETURN,
  IR_KILL,
  IR_NOP,
};
typedef struct {
  int op;
  int lhs;
  int rhs;
} IR;

名前からして"内部表現"……?token -> node -> irという変化だろうか? mrubyでの経験に当てはめると、MOVで変数をレジスタに割り当てたり、RETURNで関数の返り値を決めれるようにしたりという感じかな? IMMはimmediateで即値を表す?KILLは分からない。NOPは何もしない命令っぽい。

データ構造からして、命令と対象と引数でアセンブラと言うか機械命令語っぽい感じ?

IR *ins[1000];
int inp;
int regno;

Intermediateが複数のsでinsかな?これもtokens[1000]と同じく雑に1000個空間を用意しているっぽい。 同じくinsのポインタpでinp、regnoレジスタナンバーというところかな?

int gen_ir_sub(Node *node)

名前からしてirを作るサブ関数っぽい。 関数の中も見ていく。

  if (node->ty == ND_NUM) {
    int r = regno++;
    ins[inp++] = new_ir(IR_IMM, r, node->val);
    return r;
  }

レジスタ番号regnoとnodeの値node->valをIMM命令としてinsに登録してポインタinpを進める。 多分レジスタrにnode->valを割り当てるんだろうと思う。 割り当てたレジスタ番号を返すのはなんでだろう?あとでアセンブラとして書き出すのかな?

assert(node->ty == '+' || node->ty == '-');

Cのassertって個人的には好きで、バグ検知しつつここから先のコードの制限というか説明になっていて良い。 Rubyでいうとraise unless expr だと思ってる。

  int lhs = gen_ir_sub(node->lhs);
  int rhs = gen_ir_sub(node->rhs);
  ins[inp++] = new_ir(node->ty, lhs, rhs);
  ins[inp++] = new_ir(IR_KILL, rhs, 0);

node->tyは'+'とか'-'とかが入っている。これもirの命令として流用するようだ。 gen_ir_subを再帰的に使っている。たしかnode自体が再帰的にツリー状になっていたはず。 返り値はレジスタ番号だったので、lhsrhsレジスタ番号だろう。 足し算引き算の左右の値をirで纏めて、おそらく後でアセンブラに変換するんだろう。 ここでKILL命令。おそらく再帰の終わりを表している?まだわからない。

int gen_ir(Node *node) {
  int r = gen_ir_sub(node);
  ins[inp++] = new_ir(IR_RETURN, r, 0);
}

これまでのgen_ir_subと違うのは、最後にRETURN命令をつけていることだけ。おそらくこれまで再帰的に処理してきた全結果が渡ってきたレジスタ番号を、関数の返り値として設定するんだろう。

char *regs[] = {"rdi", "rsi", "r10", "r11", "r12", "r13", "r14", "r15"};
bool used[8];
int reg_map[1000];

regsは前回と変わらない。 usedとreg_mapから察するに、レジスタ番号それぞれを、使ってるor使ってないで管理するためのものなんだろう。 reg_map[1000]はinsとかに1:1で対応するんだろうか?

int alloc(int ir_reg)

mallocやallocaを連想させる名前。未割り当てのレジスタを返してほしい関数かな? 引数はレジスタ番号?中身も見ていく。

  if (reg_map[ir_reg] != -1) {
    int r = reg_map[ir_reg];
    assert(used[r]);
    return r;
  }

if文からir_regは0〜999の数字なんだろう。-1はまだ未登場の値。未使用を表すのかな? assert文から察するにused=trueのレジスタ番号rを返すっぽい。used=trueでいいのか?

  for (int i = 0; i < sizeof(regs) / sizeof(*regs); i++) {
    if (used[i])
      continue;
    used[i] = true;
    reg_map[ir_reg] = i;
    return i;
  }

regsは8つのレジスタだった。これをループで回す。条件はregs[i]でもいい気はする。もしくはusedで8って決め打ちしちゃってるしi < 8とか……。 まあわかりやすさを重視した結果かな? used=trueなら飛ばして次へ。used=falseならused=trueにしてreg_map[ir_reg]に使っているregsのindexを割り当てて番号を返す。 さっき出てきたreg_map[ir_reg]には-1は割り当てられえない。あとで初期化するのかな?それともfree的な関数が出るのかな? ともかくこの関数が返すのは、regsのindex番号のようだ。 0〜999のなにかしらの番号ir_regが与えられた時、使えるregsのindexを返す。このindexからアセンブラを書き出すんだろう。

void kill(int r) {
  assert(used[r]);
  used[r] = false;
}

usedフラグをoffにするだけ。レジスタの再利用のためかな。

void alloc_regs()

長いので見ていこう。allocとどう違うんだ。

  for (int i = 0; i < inp; i++) {
    IR *ir = ins[i];

inpはinsのindexだった。これまで進んだins全てでループしているっぽい。

    switch (ir->op) {

命令毎になんかする。

    case IR_IMM:
      ir->lhs = alloc(ir->lhs);
      break;

allocはとにかく使えるregsのindexを返す関数(という予想)だった。 IR_IMMgen_ir_subで出てきた。lhsはregnoというインクリメントする謎の値だった。 この時点では使えるかどうかわからないから使えるレジスタindexを割り当てているんだろう。 ここから、おそらくalloc_regs関数はalloc関数のirレベルで適用するものと予想。

    case IR_MOV:
    case '+':
    case '-':
      ir->lhs = alloc(ir->lhs);
      ir->rhs = alloc(ir->rhs);
      break;

IR_MON'+''-'が一緒になっているのに面食らったが、おそらく全て左右の2つのレジスタを使う命令なんだろう。

    case IR_RETURN:
      kill(reg_map[ir->lhs]);
      break;

IR_RETURNは関数の返り値と予想している。killは使ってないフラグを設定するんだった。 関数の返り値は再利用すると思われるんだがkillするからには再利用はしない。予想は外れていることになる。 これまでのcommitからアセンブラの最後のプロセスの終了ステータスを設定するやつか?

    case IR_KILL:
      kill(reg_map[ir->lhs]);
      ir->op = IR_NOP;
      break;

IR_KILLgen_ir_subの最後に登場していた。 例えば足し算では左と右のレジスタの値を足して左のレジスタに入れる。そうすると右側のレジスタは再利用可能になる。ということかな。

void gen_x86()

なまえからしアセンブラを吐きそうな感じだ(勘)。またまた見ていく。

  for (int i = 0; i < inp; i++) {
    IR *ir = ins[i];

alloc_regsと同じくinsのループのようだ。

switch (ir->op) {

ちなみに実行順はmain関数を見るにgen_ir->alloc_regs->gen_x86のようだから予想は合っている。 ここでの命令は全てalloc_regsが終わったあとの世界のようだ。

    case IR_IMM:
      printf("  mov %s, %d\n", regs[ir->lhs], ir->rhs);
      break;

IR_IMMは使えるレジスタ左に即値のnode-valが右に入っていた。 これでmov reg1 123のように出力されるんだろう。

    case IR_MOV:
      printf("  mov %s, %s\n", regs[ir->lhs], regs[ir->rhs]);
      break;

単純に右から左へのコピー。変数とかを使えるようにするための布石か? ならこのテストコードではこの命令自体がいらないような……。作るところもないし。

    case IR_RETURN:
      printf("  mov rax, %s\n", regs[ir->lhs]);
      printf("  ret\n");
      break;

これは予想通り最終的な結果をプロセスの終了ステータスに割り当てている。

    case '+':
      printf("  add %s, %s\n", regs[ir->lhs], regs[ir->rhs]);
      break;
    case '-':
      printf("  sub %s, %s\n", regs[ir->lhs], regs[ir->rhs]);
      break;

左右に使えるレジスタ番号が入っているので、ここで使えることが保証される。

    case IR_NOP:
      break;

IR_KILLとかが変化してIR_NOPになったものだろう。 何もしないことが便利なときもある。

int main(int argc, char **argv)

一応main関数も見ていこう。

  for (int i = 0; i < sizeof(reg_map) / sizeof(*reg_map); i++)
    reg_map[i] = -1;

やはりreg_mapallocで予想したとおり-1で初期化されていた。

  tokenize(argv[1]);
  Node* node = expr();

  gen_ir(node);
  alloc_regs();

  printf(".intel_syntax noprefix\n");
  printf(".global main\n");
  printf("main:\n");
  gen_x86();

やはりtoken -> node -> irと変換していき、レジスタ割当を行って、アセンブラを吐いておわり。

token = 空白など余計な文字を除いた文字の列

node = 構文的な計算順序の組み立て

ir = このプログラミング言語VM的なもの

ということかな。

んー、やはりテストコードからは、実装が先の世界に行き過ぎてる感がある。

アセンブラはこうなった

.intel_syntax noprefix
.global main
main:
  mov rdi, 1
  mov rsi, 2
  add rdi, rsi
  mov rsi, 3
  add rdi, rsi
  mov rsi, 4
  add rdi, rsi
  mov rsi, 5
  add rdi, rsi
  mov rsi, 6
  add rdi, rsi
  mov rsi, 7
  add rdi, rsi
  mov rsi, 8
  add rdi, rsi
  mov rsi, 9
  add rdi, rsi
  mov rsi, 10
  add rdi, rsi
  mov rsi, 11
  add rdi, rsi
  mov rsi, 12
  add rdi, rsi
  mov rsi, 13
  add rdi, rsi
  mov rsi, 14
  add rdi, rsi
  mov rsi, 15
  add rdi, rsi
  mov rsi, 16
  add rdi, rsi
  mov rsi, 17
  add rdi, rsi
  mov rax, rdi
  ret

rui314/9ccを写経する-その1

8ccという有名なCコンパイラがあるが、これを書いたrui314さんが新たに9ccというリポジトリを上げていた。

https://note.mu/ruiu/n/n00ebc977fd60 を読むに、これは8ccをさらにわかりやすく、Cコンパイラ自作の教材として作っているものに違いないと勝手に判断し、写経してみることにした。 写経なので、書いているコードは同じ。自分が理解していった記録をここに残す。

僕のスペックは、普段はRubyでWebアプリを書くお仕事をしている。 Cコンパイラは一切書いたことはなく、Brainf*ckぐらいは書いたことがあるレベルだ。

ではスタート。

https://github.com/rui314/9cc/commit/56e94442ae8844688d5390851e5b29ba0c946e2f

9cc.c

main関数でprintfしているだけのコードのようだが、いきなりアセンブラっぽいコードを発見。 アセンブラは一切書いたことがないのでこれが初アセンブラだ。

.intel_syntax noprefixググるアセンブラのお作法としての記法を宣言するコードのようだ。アセンブラには複数の記法があるのだろう。

.global main
main:

なんとなくだけど関数宣言みたいなものかな?Cで言うmain関数宣言みたいなものだろう。

  mov rax, %d
  ret

うーんこの辺は全然わからん。多分変数raxに数字を入れてreturn的な感じか?

Makefile

9cc: 9cc.c

なんとたったこれだけで cc 9cc.c -o 9cc taskを書いたことになるようだ。 *.c taskがdefaultで用意されてるのかな?深くはわからない。

test.sh

テストコードっぽい。

  ./9cc "$input" > tmp.s

これから作るCコンパイラ9ccに引数をあたえて標準出力を書き出す。 いまのところ単にアセンブラをprintfするコードだったので、tmp.sにはアセンブラのコードが書き出されるのだろう。

  gcc -static -o tmp tmp.s

これはmacでは実行不可能だった。-staticgcc特有のオプションっぽい? clangでも同じことができなくはないだろうけど、コードの読み替えなどはしたくなかったのでDockerでlinux環境を用意した。

Dockerfile

FROM ubuntu:18.10

RUN apt-get update
RUN apt-get install -y make gcc lld

docker-compose.yml

version: "3"
services:
  9cc:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./:/9cc
    working_dir: /9cc

これで写経したコードが

docker-compose run 9cc

で動くようになった。docker便利。

  ./tmp
  actual="$?"

おそらく出力されたアセンブラを元に、仮の実行バイナリtmpを作って実行しているんだろう。 $?はプロセスの終了ステータスだった。あのアセンブラは、終了ステータスをセットするものだったようだ。 この終了ステータスが入力と同じならOKとしている。

初っ端から初アセンブラを書いた。新言語習得である。

https://github.com/rui314/9cc/commit/916237396164ae25557b62bf0d1ca9d9cf8c2070

いきなり足し算引き算の登場。どうやって実装するんだろう。おそらくアセンブラコードを拡張していくはずだ。

9cc.c

まず最初に数字を読み込むが、strtolは初めて知った。 第一引数にポインタを渡したら勝手に文字列を数字として読み取ってくれるっぽい。 第二引数にポインタのポインタを渡すと、読み取り終わり位置までポインタが指す位置を進めておいてくれるようだ。便利。 テストコードから察するに、これで20のような2桁の場合でも読み込めるようだ。 次は1文字ずつ読み込んで、+だったらaddアセンブラ命令、-だったらsubアセンブラ命令を書き出すようだ。 たぶんこれで足し算引き算になるんだろう。

test.sh

写経通りやると動いた。 試しに123456のような数字を書いてみると、どうやら結果が255を超えることができない。 多分終了ステータスの最大値だと思われるが、このコンパイラは現状255が限界のようだ。

https://github.com/rui314/9cc/commit/42adde9b5e0ed09c0d76bb84da38d136be8390c1

おそらく空白を入れることができるようにする変更。これまでは空白文字を入れるとSEGVしていた。 ついでにtokenizeということで、空白を除去したプログラム構文を作り出すんだろう。本格的になってきた。

9cc.c

enum {
  TK_NUM = 256, // Number literal
  TK_EOF,       // End marker
};

tokenの種類を定義しているようだけど、何故256から始まるんだろう?0からじゃだめなのかな?とりあえずこのまま進める。

typedef struct {
  int ty;      // Token type
  int val;     // Number literal
  char *input; // Token string (for error reporting)
} Token;

tokenの構造体のようだ。tokenのty(種類)とval(値)とinput(入力文字)。valがint型なので、おそらく数字の足し引きしかできないだろう。これでいいんだ感満載である。

Token tokens[100];

これは正直ちょっと笑ってしまった。 あまりに単純で「これでいいんだw」というズコーな感じと、単純なところから始めることへの徹底されたこだわりまで感じる。 極限まで研ぎ澄まされた職人芸という感じだ。

void tokenize(char *p)

これも結局は1文字ずつ読み込んでif文で分岐しているだけ。 「これでいいんだ」と何回思わせるんだ。職人芸だ。

tyに直接'+''-'を入れている。 そうか、tyに直接文字列が入ることを想定すると、どの文字ともぶつからないようにするために TK_NUM = 256 となっていたんだな。

main関数ではこのtokenを使って、やはりif文で分岐してアセンブラを書き出す。

空白を飛ばすだけにしてはやや仰々しいが、これぐらいはいいでしょと飛ばしているんだろう。

test.sh

無事、空白を無視することができるようになった。

embulk-decoder-execつくった

github.com

経緯

現在fluentdからlzoファイル形式で圧縮して定期的に溜めてるJSONデータが既にある。 これを別のストレージにサッと移せたらできること広がりそうだなーと考えた。

問題点

bulk処理といえばembulk、ということでembulkを触ってみて、どうやら圧縮ファイルを展開するのはdecoderと言うらしいことがわかった。 decoderをinputのオプションとして行うことで、あとにつながる処理に渡すようだ。

embulkはpluginアーキテクチャだと分かっていたので、embulk-decoder-lzoがすでにあるかな?と思ったらあった。

github.com

これでいけるじゃんと思って組み込んでみたが、以下のエラーが出てきた。

Error: java.io.IOException: Compressed with incompatible lzo version: 0x20a0 (expected 0x2050)

よくわからないけど、lzo形式のversionの違いでincompatibilityがあるっぽい? そして手元のデータは0x20a0の方で、0x2050は展開しているライブラリの表示のようだ。

github.com

んーということはこのライブラリをlzo version0x20a0対応すればいいのか? しかしJavaもlzoプロトコルも全くと言っていいほどわからない。 ましてやversion upにともなう互換性はどうすればいいんだとかも全くわからない。

提案手法

俺はlzoファイルを展開したいだけなんだ!

$ cat in.lzo | lzop -dc > out.json

したいだけなんだ!

と思って、「embulkから任意の外部プロセスを立ててdecodeするplugin」というアイデアに至った。

かくしてembulkのpluginを書こうとしたが、どうやらdecoder pluginは現状javaでしか書けないっぽい。 なんとかがんばってjrubyで書けるようにする道もあるかもしれないが、どうせならjavaに挑戦してみることにした。

javaは新卒のころにだましだまし書いてた記憶がおぼろげにある。 まだプログラミングの楽しさが分かっていなかったときのことだ。多分8年前?

そんなわけで全くの初心者ではないが、ほぼ初心者と言っていいだろう。

embulkにはpluginの雛形を生成するコマンドがあったので使ってみる。

$ embulk new java-decoder exec

これで生成されたjavaのコードをなんとか読んでみると、どうやらjavaではrubyで言うIOInputStreamとかOutputStreamという名前で扱われているようだった。

embulkのdecoder pluginはinputから渡ってきたInputStreamに対して、readメソッドを実装したclassのオブジェクトを返すと、embulkがよしなに読み込んでくれるようだ。 このclassにロジックを書けばいい。

参考はgzipとbzip2のdecode pluginがcoreにあった。

embulk/GzipFileDecoderPlugin.java at 627afede81ff547eda0db8a06a0d5fd53c8d586c · embulk/embulk · GitHub

コードは大変短い。java.util.zip.GZIPInputStreamというやつは、名前から察するに組み込みのclassっぽかった。

(このjavaは1ファイル1classでimportすることで使えるみたいなのも最初は戸惑ったがgolangみたいなもんだろと思ったら読めた。)

java.util.zip.GZIPInputStreamは最終的にはInputStreamを継承している。

どうやらこのInputStream classをつかったインターフェースはjavaの世界ではかなり一般的なもののようだ。 javaは割と堅いイメージを持ってたけど、特定のインターフェースさえ実装すればいいのはgolangっぽくて柔軟な印象を得た。javaいいじゃん。時代はjava

そんなこんなでjavaの標準的な動かし方を調べつつ、任意のコマンド指定でプロセスを立てて、stdinとstdoutをpipeから渡してやる実装ができた。 javaの作法はよくわからないので、Threadを立ててprocessのstdinに対してwirteしまくり、いつでもstdoutからreadしてねと言う感じで実装した。

試しにS3 inputに対してlzopで展開してPostgreSQLにoutputするサンプルを書いてみたら、指定のs3 dir以下のファイルを全部読み込んで展開して書き出しまでちゃんと動いた。

$ cat tmp.yml
in:
  type: s3
  bucket: bucket
  path_prefix: path
  auth_method: default
  decoders:
    - type: exec
      mode: pipe
      command: lzop -dc
  parser:
    type: jsonl
    columns:
      ...

out:
  type: postgresql
  database: test
  host: postgres
  port: 5432
  user: postgres
  password: password
  mode: replace
  table: test
  column_options:
    ...

(なんかmodeオプションとかいらない気もするが、pipe以外の使い方もあるかなと思ってこうなってる。。。)

考察

かくしてjavaのlzoライブラリに依存することなく、手元のlzopコマンドで動くなら大丈夫な状態ができた。 こんな感じで、ガンガン外部コマンドやパイプに頼る実装はunixっぽくて好き。

この仕組を応用すれば、encoderも作れそうだなあ(予定がない)。

まとめ

「で、これはproductionで使えるの?」と言うと、実はこれを書き出した時点で、自分のやっていたプロジェクトが一旦ペンディングになった。。。 僕の実装が予想より遅すぎたためである。面目ない。

いったんプロジェクトから離れはするが、必ずここに戻ってくる。 そういう誓いを立てるためにも、このpluginは取りあえず動く状態にしたのでインターネットに保存しておくのでどなたかのお役にもたてれば。

必ず戻ってくるぞ!

logs

RubyKaigi2018 in 仙台に行ってきた

rubykaigi.org

RubyKaigiは京都も広島も行っていなくて、仙台で3年ぶりの参加だった。

どのセッションも裏番組が面白そうすぎて、血涙を流しながら見にいっていた。

セッションを聞いて「こんな事ができたんだ」「それならこんな事もできるかな」みたいにアレコレ考えるのが楽しかった。

明日から使えるtipsを学ぶというよりも、自分の考えを拡張するため、あんまり話が想像できないセッションにも積極的に参加した。

どのセッションも面白かったというほかないんだけど、これだけはどうしても内に秘めたままにできない……。

「どうして俺は発表できないんだッ!くやしいッ!!!」

また次の機会にがんばります。RubyKaigi2018に関わったすべての皆様に感謝。

Logs

After Kaigi

終わってからは家族で観光した。

家族ばかり撮ってたのでネットに上げられるような写真はあまりない。

はじめてfluent-pluginを書いた

ようするに

github.com

fluentdでちょっと溜めて、postgresにbulk insertするやつです。

そもそも

fluentdが何をするやつなのかいまいちよく分かっていなかった。 「ログを転送する……。それで??」みたいな。ふわっとした理解だった。

いろいろ調べていくうちに、「この考え方だとすんなり理解できるな」というポイントを発見した。

Linuxで言うIOモデルをプロセス化したやつ」

とイメージすることで全体を理解しやすくなった。 更に雑に言うと「ちょっと溜めてなんかするやつ」である。

「何でも出来る」と言われてもよくわからなかったけど、inputをちょっと溜めて(buffer)、変換してoutputするイメージを掴むことで、fluentdの動作イメージがわいた。

例えば、アプリケーションの各プロセスで1レコードずつDBにinsertするとwrite負荷が高いけど、一箇所にある程度溜めてからDBにbulk insertすれば、DBへの接続は溜めた分まとめてできるので負荷が減る。fluentdならこれができる。

アプリケーションプロセスからのinsertが1、batch処理を100としたら、5とか10の単位で処理できる。 fluentdによってエンジニアが使える武器が増えた感じがする。

加えて「n秒に1回」みたいなこともできるので、マイクロバッチ処理も出来る。「何でも出来るちょっとしたサーバー」としても使いやすい。

postgres

今回はギョームで紆余曲折を経てpostgresを使うことにしたが、結構なデータ量を扱うのでinsert負荷が懸念された。 そこで、既に弊社でヘビーに使われているfluentdのラインにpostgresにもデータを送る経路を追加して、ある程度まとめてinsertしようと考えた。

ちょっと探したところ、 https://github.com/uken/fluent-plugin-postgres というpluginがすでにあるが、与えられたsqlをprepareして1レコードずつexec_preparedするやつだったので、sqlが自由にかけて柔軟ではあるけど、パフォーマンスに懸念があった。(実際に図ったところ、データ量によっては10倍〜100倍の差があった。)

あとは https://github.com/choplin/fluent-plugin-pgjson はテーブルスキーマが固定だったので要件に合わない。

そこで fluent-plugin-mysqlのbulkのやつ のpostgres版を作ってみたのが経緯。

本日production入りして、弊社の流量でもキビキビinsertしているっぽい。

fluent-plugin

他のpluginを参考にしつつ、結構雰囲気で書けた。 output-pluginなら、writeメソッドをよしなに実装すればいい。

ただ、formatメソッドの有無でchunkの挙動が変わってくるあたりに歴史的経緯を感じたのがちょっとハマりポイント。 1 plugin 1 classにしているところとかよく設計されているなあと思った。

testは https://docs.fluentd.org/v1.0/articles/plugin-test-code を参考に書いてみたけどうまく動かなくて力技で書いた。 (dirverからeventsが取れなかった)

豆知識

ON CONFLICT DO UPDATE

postgresではinsert文にON CONFLICT DO UPDATEをつけることでupsertできる。

しかしながら、同じinsert文内でON CONFLICTに指定したkeyが重複すると、postgres側では「どっちやねん」となってinsertに失敗するようだった。

https://www.postgresql.jp/document/9.6/html/sql-insert.html

ON CONFLICT DO UPDATE句のあるINSERTは「決定論的な」文です。 これは、そのコマンドが既存のどの行に対しても、2回以上影響を与えることが許されない、ということを意味します。 これに反する状況が発生した時は、カーディナリティ違反のエラーが発生します。 挿入されようとする行は、競合解決インデックスあるいは制約により制限される属性の観点で、複製されてはなりません。

これではunique indexを貼っている場合に不便……。 かと思いきや、多分fluentdからinsertするからには履歴テーブルとして扱うのがベストプラクティスなんだろうと思う。

送り側としては、受け側の事情は気にしたくない。(ただでさえ、既に複数のストレージに様々な事情でデータをpostしているのだ) ガンガン同じデータをpostしてもいいようにしたほうが運用が楽そうだと考えた。 そこで、unique indexはすべて外して、送られてくるがままを受け入れるようにした。

定期的に重複するデータを消したりする必要も出てくるかもしれないが(無視できる量なら大丈夫だけど)、多分エラーだ何だで困るより楽だと思う。

最大values数

postgresはprepareするとき$1 $2のような記号を使うが、これは$65535が最大。 つまり insert文で送れるvaluesは65535個までのようだ。

下記commitによると、16-bit unsigned integerでいまのところ固定のようだった。

https://github.com/postgres/postgres/commit/f86e6ba40c9cc51c81fe1cf650b512ba5b19c86b

pluginでは65535を超えそうなら、クエリを分割するように工夫しているので安心。

反省

  • fluentd自体に触れるのが初めてだったので、簡単な機能なのにdeployまで結構時間がかかってしまった。
  • あとから気がついたけど https://github.com/fluent/fluent-plugin-sql を使っても同じことはできたっぽい?

RejectKaigi2018でMVPを取った結果www

はい、というわけでね、RejectKaigi2018に行って話してきたわけですけどもね。

なんと、MVPとして選ばれ、見事(?)乾杯の音頭を取らせていただきました 🎉

というわけで今回はYouTuber風を意識して発表してみた。

伝わったかどうかは微妙だけど、自分もテンション上げて話せた気がする(根が暗いのでプラマイ0かも)。

自分の好きなYouTuberってゲーム実況者しかいないので、一般的なYouTuberってどんなんだろうとヒカキンさんの動画も見てみたけど、これは正直自分には合わなかった。 なので単に自分の好きなゲーム実況YouTuberを思い出して勇気をもらったのだった。

RejectのMVPと、なんとも残念賞的な感じではあるけど、何もCFPを出さなかったよりは & 何も話さないよりは、自分としてはがんばったと思おう。

CFPはどうも激戦で、ちょっとでも熱量が低いとダメだったようだ。 熱量。次はがんばっていこう。

じゃーねー、バイバイ!

スピコラ考察

最近スピコラを全ルール全ステージで使ってS前後をウロウロしている。(A+は適当にやっても勝てるが、S+0にはボコボコにされる程度のウデマエ) スピコラの特徴を整理する。

立ち回り

最大の特徴は万能性にあると思う。 塗りをやらせても前衛をやらせても、そこそこ動けるので、ブキとしてはスシやZAPに近い立ち位置なると思う。 味方の構成と動きを見て、足りないポジションを補完できる。

射程と連射力が相まって塗りは強い。エリア塗りのような瞬間的な塗りが強く、スペシャルもそれなりに貯まる。 カーリングで強制的に塗れるのも塗り役として相性がいい。適当に後ろから流していてもラッキーキルが狙える。(カーリング強すぎだろ……。) 味方に前衛職が多ければ、ひたすら塗って裏方に徹するのもいいだろう。

対面も強くて、射程はスシ・ZAPより2キャラ分ぐらい長く、集弾性も悪くないので結構対面で打ち勝てる事が多い。ZAPなどと同じく確定4発なのでエイムは必要。 メインの射程はちょうどデボンエリアの味方屋根から敵屋根に届くぐらい。 カーリングを使って裏で暴れても結構いい感じ。後ろで暴れて雨も裏から打つと、敵ラインが前に出れなくなるので効果的だ。 味方に前衛職が少なければ、前に出るのもいいだろう。 しかし、どうしてもメインにタメが必要なのでインファイトや奇襲はあまり得意ではない。本職がいれば任すべきだろう。

苦手なこと

高低差が激しいステージだと、上にいる敵に対してほぼ為す術がない。 できるだけこちらが先に高台を取っていかなければならない。

また、スシより前衛力は低いし、わかば・モデよりは塗りが弱い。あくまで万能性を利用した柔軟な立ち回りを生かさなくては勝てない。

ほしいギア

ヒト速はデフォルトで1.3つけてる。 あとはメインインクがつくと動きやすく感じた。

スペ増積めばスペシャル型として立ち回れるが、その分前に出る意識が下がってしまうので特徴となる万能性が薄れてしまう。雨が打ちたければもみじがあるのでスピコラを持つ意味は薄まる。むしろスプスピに持ち替えてミサイル役に徹するのもいいかもしれない。 ホコやインクアーマー対策に対物積むのもいいし。エリアならサブインク積んでカーリング流しているだけでも現状維持・打開の援護射撃になる。(カーリング強すぎだろ……。)

https://twitter.com/_ksss_/status/984356619587764224