時計を壊せ

駆け出してからそこそこ経ったWebプログラマーの雑記

Mustache Templateの実装を書いた

この記事はPerl Advent Calendar 15日目の記事です。

qiita.com

さて

ということで、ひさしぶりにCPANizeしました。なんか早速bug fixが見つかって早々に0.02です。

metacpan.org

今のPCでは初めてのCPANizeだったようで ~/.pause がなくてちょっと焦った。

なんで?

人生色々。様々があります。 たとえば、やることに追われたり、悩ましい考え事に苛まれたり、やることに追われたり、やることに追われたり、様々があります。

一方でISUCONも迫っていました。PerlでISUCONに勝ちたいので日頃の仕事でなかなか書く機会が減ってしまったPerlを素振りしたくなってきます。 気分転換がてら、Perlでちまちまなんかを作ってみるのは良いかもしれないと思いました。

そんななか、趣味でプログラミングするのにちょうど良さそうな題材として、Mustache Templateが出てきました。

Template::Mustacheが1.0系になってめっちゃ遅くなったことに困っていそうな人をみかけたのです。 確かにめちゃくちゃ遅くなったのは知っていて、たしかに調べてみても代替モジュールによさげなのがなかったのです。

まあ簡単そうだしちょうどよいだろう。ミニマルなものを作っていい感じにしよう。と実装しながらMustache Templateの仕様を追っていきます。

すると、なんとも色々な仕様がでてくるではありませんか。

lambdaとか継承テンプレートとかmustacheで1回も使ったことないぞ!などとと思いましたが、それは自分はText::XslateやText::MicroTemplateを使っていたのでまあそれはそう。

まあ必要なひともいるんだろう、そうなるとこれもたぶんほしいんだろう、これを実装するならこれもまあ実装されていて然るべきだろう、などとうっかり色々実装していくと、あれよあれよと芋づる式に全部の仕様を実装してしまいました。

なんで????

実装

Lexerでトークンに分割して、ParserでAST*1を作り、Compilerでコンパイルするという王道な実装となっております。

Lexerといってもそんなに丁寧なものではなくて、素朴なLexerとしてはたとえば "foo{{bar}}" があったときに、これを ["foo", "{{", "bar", "}}"] と分割するのが一般的かと思います。 もうちょっとParser寄りな感じで、["foo", "{{bar}}"] という感じでタグの構文そのものだけ解釈してトークン分割をする実装にしました。

これにはちょっと理由があって、Mustacheって{{}}というデリミタでタグを構成するのがデフォルトなのですが、このデリミタを変更するタグが存在するので、Lexerはそれを解釈しないと正しくトークン分割ができないのです。すると、まあそこまでしてるのに {{bar}} を分ける必要もなかろうということで自ずとそれらを一緒のトークンにまとめて扱うことにしました。

そして、Parserはその一列のトークンを解釈してMustache Templateとしての文法の構造をデータ構造上の構造に構造化します。いわゆるASTというやつですね。 たとえば、Mustacheには{{#condition}}yes{{/condition}}みたいな複数のタグを使って1つの構造を作るみたいな文法をもっているので、これはAST上もこの分岐の子要素みたいな感じで整理してあげます。

ASTを使うとめっちゃ簡単にレンダリング処理が実装できます。 素朴にレンダリングする事もできると思いますが、今回はせっかくなのでキャッシュしたテンプレートの実行効率を高めるべくCompilerとして実装しようと思います。

Compilerってなんやねんとなると思いますが、これはText::MicroTemplateよろしくテンプレートに合わせたパターンの文字列を生成するPerlのコードを文字列で作り上げてevalすることで、事前に分岐などが最適化されたコードを作る操作を指しています。

Perlコードの生成時はASTを解釈して細かい分岐をたくさん辿りますが、生成されたPerlコードはある程度の分岐が事前に解決されたものになるので直にASTを毎度解釈するより高速に実行することができるという寸法です。

Apache::LogFormat::Compilerなど様々なCPANモジュールでこのアプローチは取られています。 なんなら、こういうコードを簡単に作るためのSub::Quoteってやつまであり、これはMooの実装で使われていたりもします。

ほか、render というテンプレートをパースして即レンダリングするというインターフェースもあるんですが、このケースではコンパイルしたものが1度実行したら廃棄される前提がありかつコンテキスト変数*2がコンパイル前にすでに決定しているという特徴があるので、コンテキスト変数をヒントにASTを最適化することが可能です。 ということで、そういう処理を挟んであり、うまくいくケースではこの最適化をやらない場合と比べて3倍くらい速くなるようになることがわかっています。

ほかにも、正規表現を最適化していたりなど、地味な高速化を測っている箇所がちょいちょいあったりします。

出来栄え

github.com

このテストケースをすべてPASSしています。やったね。 ついでにテストカバレッジもステートメントレベルでは100%*3で、ほかも90%台後半まで網羅してあります。

パフォーマンスも簡単なテンプレートをレンダリングするベンチマークで比較してみました。 Template::Mustacheに比べてパースの速度が10倍、レンダリングする速度がパース済のテンプレートをキャッシュしているケースで3倍、キャッシュしていないケースで40倍となることがわかっています。 やったね。

なお、Mustache::Simpleというモジュールがあるのですが、これはテストケースをpassしなかったので比較していません。(存在しない変数を無視せずにエラーにしてくる仕様非準拠な挙動だけ確認してあとは確認してない)

ということで、まあそれなりに実用に耐える品質ではあろうと思ったのでCPANnizeしておきました。

これを作る時間でISUCONの素振りすればよかったのでは?

ISUCONの素振りってまとまった時間が必要じゃないですか……。 これ細切れの微妙な時間でちまちま進めたのです。素振りもしたかった……。

そんなわけで

まあ、完成したのでもしご入用の方はお試し下さい。

*1:Abstract Syntax Tree

*2:テンプレートに与える変数

*3:普通は絶対にこのパスは通らないところとかは除いているのでちょっとズルかも

ISUCON13にPerlで挑んだ

この記事はPerl Advent Calendar 14日目の記事です。

qiita.com

チーム「銀河鉄道の昼」としてISUCON13にPerlで挑みました。 最終的に2台構成で最終スコア7083、最高スコアは1台構成での9381でした。

上位30チームが50000点弱以上なので、残念ながら惨敗と言って差し支えない結果です。 くやしい……。上位チームの皆さんおめでとうございます。

何をやったのか

なんやかんや、足回りを整えたりトラブルシューティングしたりで2時間くらい食ってたきがします。

StatsHandlerのN+1解消

順位を計算する処理があり、その順位とはtipの総額とリアクションの総数によって順位付けられていました。 この際のもとの実装は以下のような感じです。

    # ランク算出
    my $ranking = [];
    for my $livestream ($livestreams->@*) {
        my $reactions = $app->dbh->select_one(
            q[
                SELECT COUNT(*) FROM livestreams l
                INNER JOIN reactions r ON r.livestream_id = l.id
                WHERE l.id = ?
            ],
            $livestream->id
        );

        my $total_tips = $app->dbh->select_one(
            q[
                SELECT IFNULL(SUM(l2.tip), 0) FROM livestreams l
                INNER JOIN livecomments l2 ON l2.livestream_id = l.id
                WHERE l.id = ?
            ],
            $livestream->id
        );

        my $score = $reactions + $total_tips;
        push $ranking->@* => Isupipe::Entity::LivestreamRankingEntry->new(
            livestream_id => $livestream->id,
            score => $score,
        );
    }

    my @sorted_ranking = sort {
        if ($a->score == $b->score) {
            $a->livestream_id <=> $b->livestream_id;
        }
        else {
            $a->score <=> $b->score;
        }
    } $ranking->@*;

    $ranking = \@sorted_ranking;

    my $rank = 1;
    for (my $i = scalar $ranking->@* - 1; $i >= 0; $i--) {
        my $entry = $ranking->[$i];
        if ($entry->livestream_id == $livestream_id) {
            last;
        }
        $rank++;
    }

うーん、では中間テーブルを作りましょう。

RedisのSortedSetを使うことも考えましたが、scoreが同順であった場合はlivestream_idの昇順となるように実装されているためそのへんをうまくやれるだけのRedis力が僕にはなかったです。 (出来ないと思っていた。実はできるという話を聞いたけどどうやってやるのか知らないので知っているひとはコメントで教えてください。)

中間テーブルをこんな感じで作って

-- スコア
CREATE TABLE `user_scores` (
  `user_id` BIGINT NOT NULL PRIMARY KEY,
  `score` BIGINT NOT NULL,
  `user_name` VARCHAR(255) NOT NULL,
  INDEX ranking_idx (`score` DESC, `user_name` DESC)
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;

CREATE TABLE `livestream_scores` (
  `livestream_id` BIGINT NOT NULL PRIMARY KEY,
  `score` BIGINT NOT NULL,
  INDEX ranking_idx (`score` DESC, `livestream_id` DESC)
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;

更新時によしなに更新していくと、以下のような感じで順位がSQLだけで取得できるようになりました。

    $app->dbh->query('SET @r = 0');
    my $rank = $app->dbh->select_one(q!SELECT `rank` FROM (SELECT livestream_id, score, @r := @r+1 AS `rank` FROM livestream_scores ORDER BY score DESC, livestream_id DESC) a WHERE a.livestream_id = ?!, $livestream_id);

Window関数を使って DENSE_RANK() でなんとかする手もあるかもでしたが、うまくインデックスが効かないのでこういう感じにしました。

たしかまあまあスコアは上がった気がします。 しかし、なんやかんやハマって2~3時間くらいこれで溶かしてしまった気がします。

reservation slotsのチューニング

元々の実装はこんな感じでした。

    # 予約枠をみて、予約が可能か調べる
    # NOTE: 並列な予約のoverbooking防止にFOR UPDATEが必要
    my $slots = $app->dbh->select_all_as(
        'Isupipe::Entity::ReservationSlot',
        'SELECT * FROM reservation_slots WHERE start_at >= ? AND end_at <= ? FOR UPDATE',
        $params->{start_at},
        $params->{end_at},
    );

    for my $slot ($slots->@*) {
        my $count = $app->dbh->select_one(
            'SELECT slot FROM reservation_slots WHERE start_at = ? AND end_at = ?',
            $slot->start_at,
            $slot->end_at,
        );
        infof('%d ~ %d予約枠の残数 = %d', $slot->start_at, $slot->end_at, $slot->slot);
        if ($count < 1) {
            $c->halt(HTTP_BAD_REQUEST, sprintf("予約期間 %d ~ %dに対して、予約区間 %d ~ %dが予約できません", TERM_START_AT, TERM_END_AT, $params->{start_at}, $params->{end_at}));
        }
    }

この手のはロックする範囲をいかに絞れるかがキモっぽいという直感と、幸いにもslotは減ることはあっても増えることがない仕様であることがわかったため、 すでに空になっている予約枠を予め除外してロックを取ることでロックの競合をなるべく抑えるようにしてみました。

    # 予約枠をみて、予約が可能か調べる
    my %slots = @{
        $app->dbh->selectcol_arrayref(
            'SELECT id, slot FROM reservation_slots WHERE start_at >= ? AND end_at <= ?',
            { Columns => [ 1, 2 ] }, $params->{start_at}, $params->{end_at},
        )
    };
    if (any { $_ == 0 } values %slots) {
        $c->halt(HTTP_BAD_REQUEST, sprintf("予約期間 %d ~ %dに対して、予約区間 %d ~ %dが予約できません", TERM_START_AT, TERM_END_AT, $params->{start_at}, $params->{end_at}));
    }

    my $txn = $app->dbh->txn_scope;

    # NOTE: 並列な予約のoverbooking防止にFOR UPDATEが必要
    my $overed_slots = $app->dbh->select_one('SELECT COUNT(*) FROM reservation_slots WHERE id IN (?) AND slot = 0 FOR UPDATE', [keys %slots]);
    if ($overed_slots) {
        $c->halt(HTTP_BAD_REQUEST, sprintf("予約期間 %d ~ %dに対して、予約区間 %d ~ %dが予約できません", TERM_START_AT, TERM_END_AT, $params->{start_at}, $params->{end_at}));
    }

これも、たしかまあまあスコアは上がった気がします。

どうして複数台構成にした途端にスコアが落ちたのか

MySQLを2台目に追い出したのですが、どうやらその途端にDNS Serverのスループットが改善してDNS水責め攻撃の餌食となったようです。 2台構成が動いたのが終了10分前とかだったのでそこから元に戻すのは諦めました……。

次回に向けて

今回は素振りがほとんど出来なかったので素振りする時間をなんとか確保して、トラブルシューティングに費やす時間を減らし、チューニングの手札を増やしておきたいです。 今回はnginxでキャッシュさせるといったこともほとんど出来ず、またnginx luaを使えば楽にいきそうな部分もあったのでそういったこともできるようにしていきたい。

次回もPerlで挑む覚悟で、実装移植がなかったら自前でいちからコードを書いてでもPerlで戦おうと思います。

感想

移植はモダンなPerlの機能がふんだんに使われており、面白い実装でした。(Entityクラスは正直いらなかった気がしますが……。) 問題もボリューム満点で面白く、素朴なチューニングでスコアがちゃんと上がるような問題に仕上がっていてスコアが上がる楽しさも感じました。 運営の皆さんには感謝の気持ちでいっぱいです。ありがとうございました。

Linux以外ではDockerでIPv6が扱えないので簡易なTCP Reverse Proxy Serverを書いた

まずは公式ドキュメントをご覧ください。

docs.docker.com

IPv6 is only supported on Docker daemons running on Linux hosts.

残念!

Docker Desktop for Macなどでローカル開発をしているときに、ローカルで立ち上げたプロセスからDocker内にあるコンテナに通信したいことは割りとよくあるユースケースだと思う。

こういうときは、基本的には宛先をIPv4のLoopback Addressである127.0.0.1に向けてあげて、 IPv6を使わないようにしてあげるとよい。 localhostを使ってしまうと、名前解決でIPv6のLoopback Addressに名前解決されるケースがあり、そうなればIPv6に対して接続しようとしてしかしIPv4でしかlisten(2)されていないのでコケる。

しかし、世の中には親切(?)にも127.0.0.1localhostに正規化するようなコードが存在する。*1 困る。

ローカルの問題なので/etc/hostsを手で書き換えてlocalhostからIPv6に解決されないようにすればOK!という感じではあるのだが、たとえばチームで開発する基盤としてのローカル開発環境だとしてそれをチームメンバーに強いるにはトラブルの元になりそうでちょっともにょる。 なにより、プロジェクトローカルな事情で環境の設定を変えてしまうのは目的に対してtoo muchな策であり、副作用が大きくあんまり良い解決策とはいえないだろう。

では、nginxのTCP Reverse Proxy Server機能をつかってIPv6で待ち受けたやつをIPv4にProxyしてあげればよかろう。 と思いきや、nginxはDockerで建てるには楽だがローカルで建てるにはだるい!

ということで、Perlさえあればどこでも動く、簡易なTCP Reverse Proxy Serverを書きました。 PerlはだいたいのOSに最初から入ってるのでお手軽に使えるし、コピーしてプロジェクトのリポジトリに同梱しちゃえば各環境でのインストールも不要。お手軽!

github.com

簡易、というのは、素直なTCP Reverse Proxyとは異なりTCPパケットレベルでProxyしているわけではなくて、いったんTCPのペイロードを受け取ってからそれをまたTCPで流すということをやっているため。 たとえば、ACKが返ってきたからといって、それがReverse Proxyされた先でもACKされているとは限らないので、厳密にはL4 Proxyと言ってはいけない代物ではある。が、おそらく実用上はそんなに問題はないでしょう。というか、SOCK_RAWで頑張るには目的に対して大変すぎる……。

ニッチなケースで役に立つと思うのでお試しください。

裏話

もともと、これをIO::Socket::IPで書いて、IO::Socket::INETと違ってIO::Socket::IPではIPv4もIPv6も使えて便利!というLTをShonan.pm #1でやったのですが、実際にはIO::Socket::IPでpeeraddrやsockaddrなどをうまく取得できず(undefになる)、あきらめて標準関数とHash::Util::FieldHashを使って実装しなおしたという微妙な経緯があります。(そんなわけで。Shonan.pm #1のLT資料は動くでしょうって前提の間違いが微妙に散りばめられており、公開しておりません。あしからず。)

FAQ(追記)

ncでいいのでは?

2つの課題があって、ひとつはncではこれと同等のことを実現することは実はあんまり楽ではないこと。 たとえば、proxy modeを使うにしても送信側のproxy対応が必要になって(アプリケーションが小さければ楽かもしれないが少なくとも自分の関わっていたプロダクトでは対応箇所がどうしても多くなってしまって)面倒で、具体的にはhttpではなくgrpcなやつとかもいたりするから単にHTTP_PROXY環境変数を入れておしまいとはならないからsocks proxyを使わねばならず、まあ面倒。 nc -l とかを繋げたワンライナーっぽいので素朴にやることも考えられるが、複数のリクエストをそれも並行して扱えないとあんまり意味がない場面(まあまああると思う)ではまともにつかえないのでこれもやはり無し。

もうひとつは移植性の問題があるのでスクリプトみたいな形にまとめて使いづらいこと。

*1:厳密にはそうではないかもしれないが、外形的な挙動としてはそのような挙動に見えるケースに当たったことがある。

YAPC::Kyoto 2020~2023の思い出

3年前、2020年の2月にこんな記事を書きました。

blog.perlassociation.org

まだその脅威の全貌が明らかにならないままに刻一刻とタイムリミットは迫り、 YAPC::Kyoto 2020のために積み上げてきたものとそれをその状況下で実施することのリスクを直視して、頭を抱えました。

自分たちの本業は当然ながらイベントの運営ではありません。 その一方で、イベントの開催には様々な準備が必要になってくるため、それなりの労力と時間がかかります。 それを誰もが持て余しているわけがないように、スタッフも暇だからそれをやっているわけではないのです。

では、それをどこから捻出するかというと、趣味や大切な家族との時間を削ったり、睡眠時間と体力を削ったり、業務調整をして同僚に助けてもらうなど、人によってその程度の差はあれど全く何も犠牲にしていないという人は少ないでしょう。 そういった、掛け替えのない時間をもらうことによってYAPC::Kyoto 2020の準備は進められてきたことを、自分はよく理解していました。

イベントは一点物です。 イベントの準備のほとんどは「その日」のためにしか使えないものであり、つまりイベントを中止するという判断はそのために準備したもののほとんどを無意味なものにしてしまうことを意味します。 例を挙げると、イベントの内容は会場や日程に大きく左右されます。必ずしも同じスペースが同じ条件で借りられるとは限りませんし、どこでどのようなことをやろうとしていたかという構想も簡単に崩れてしまいます。ゲストとして呼ぶ人々の都合がつくかもわかりません。

和気あいあいと、こんなことが出来たら面白そうだ、こんなことができるとみんな嬉しいのではないか、という具合で盛り上がっていた運営メンバーの様子を思い浮かべながら、 中止を考えざるを得ないこの変化にどのように対処するか、中止以外になにか打つ手はないか、現状でベストな打ち手はなにか、といったことをひたすら考えていました。

そして、2時間近い話し合いをオンラインで行って結論を出し、以下のようなブログエントリを書きました。

blog.perlassociation.org

あえて「開催見送り」「延期」という言葉を使い、「中止」とは書きませんでした。

イベントの延期は、それを言うこと自体は簡単ですが、それを実際に行うことは簡単ではありません。 その準備のほとんどが無駄になったことにもう一度前向きに取り組むためには意思が必要です。 そして、延期を提案するということは「いままでと同じくらいの労力と時間をもう一度ください」と言うことに等しいので、延期したいということへの共感が必要です。

しかしながら、イベントのスタッフをやった報酬は、イベントを開催してそこに参加したり関わった方々からの感謝、それだけです。 そして、これは開催することで初めてそれを得ることができます。まだそれがない状況で、延期としましょうと言ったところでそれは本当にできるのでしょうか? 最初はよくても、燃え尽きる、折れる、といった形容をされるような状態になってしまっても不思議ではないと思います。

とりあえず言っておけば良いという意見もあるかもしれませんが、もし期待値を高めてそれを裏切るという行為になってしまうと、信頼を毀損してしまいます。 自分自身のことであればそれでもよいかもしれません(よくはない)が、YAPCという世界中で何代にも渡って主宰が交代してきたカンファレンスは自分だけが築き上げたものではない、これまでYAPCをつくりあげてきた人々から受け継いだ大切な財産です。

「延期」と言うからにはたとえ誰も手伝ってくれなくてももう一度やる覚悟を持つ、そういう気持ちが自分にとっては必要でした。 YAPC::Kyoto 2020のメンバーは延期に対して前向きに向き合ってくれて、延期と決めた話し合いでも「絶対やるぞ!」となって終わるくらいに前向きでいてくれたので、それに背中を押してもらったことであまり迷わずに延期と言うことができました。

そうなれば、コロナ禍でイベントがやりづらくなっても、なんとかYAPCを開催するための土台を維持したいとなります。 コミュニティは生き物であり、集まるきっかけが失われると死んでしまいます。

オンラインでそれをどうするか、悩んだ末にJapan.pm 2021、YAPC::Japan::Online 2022というイベントをやりました。 このあたり葛藤とそこからどうしたかはイベント振り返り Meetup #2というイベントでも話したとおりです。

speakerdeck.com

そして、結果的にはこのようにYAPC::Kyoto 2023として復活を果たしました。 いろいろと不安を書いていましたが、実際はどうだったか。

なんと、YAPC::Kyoto 2020を手伝ってくれていたほとんどのスタッフが引き続き手伝ってくれました。そうでない方も3年のあいだに状況が変わって手伝えなくなってしまった、当日の予定が合わなくなってしまった。といった方ばかりで、自分の知る限りでは100%のメンバーが延期開催の実現に引き続き前向きでいてくれました。 先に書いたような不安は幸いにも杞憂に終わり、id:azumakuniyuki さん id:papix さんを中心に多くのメンバーが高いモチベーションでまた新しいカンファレンスを作ることに尽力してくれたのです。

結果的に、(ブランクが空いたことなどを加味しても)残念ながらトラブルは少なくはありませんでしたが、 それを踏まえても「とても良かった」と参加した人々に言ってもらえるような、多くの人の心に残るカンファレンスとなったのではないかと思います。 ぼくも、とても良いカンファレンスになったのではないかと思います。

この彼らの前向きさとこの結果に力をもらって、自分たちにとって掛け替えのない場をこれからも続けるために、自分にできることを引き続きやっていきたいと僕も心を新たにすることができました。 そんなわけで、YAPC::Kyoto 2023を作り上げてくれたメンバーに僕は感謝をするばかりです。本当にありがとうございました。


いろいろと思い出して感慨深いものがありました。という話でした。

YAPC::Kyoto 2023ではぼくはスタッフ業として溢れたタスクを拾ったり、あるいは #ぶつかり稽古 のリバイバルイベントの司会進行兼実況解説役やLTもやりましたが、その話はまた別で書こうかと思います。

UNIVERGE IX2105で宅内ネットワークを構築した

とある分譲マンションを購入して入居したところ、棟一括契約のインターネット回線以外は引けないようで、それを使わざるを得なかった。 宅内への配線は1000BASE-Tが1本あり、宅内に設置されたスイッチングハブを通じて各部屋へ配線されている。 ネットワーク的には少なくとも他住居のネットワークへルーティングされることはないようだった。 速度も実測で下り600~700Mbpsが出ていて概ね問題がない。ということで普通に使うぶんにはほとんど不満はなかった。

唯一心配なのがマンション側のネットワーク構成がブラックボックスであることで、まあおそらくそんなに問題ない構成にはしているのだろうとは思いつつコントロールの範囲外で安全性が保たれていることにはちょっとだけ不安がある。 あと、NASを使うにあたってDHCPでNASのMACアドレスに対してIPを固定アサインしたい(でないと使いづらい)のだが、ルーターは家の外なので権限があろうはずもなく、まあIPが変わる機会もそうそうないはずではあるのだがこれもまたちょっと不安。 ということで、明確にネットワーク的に宅内と宅外を分離しつつ外からのパケットを一定ブロックしたい。ということで家の中の最前段にルーターを設置したくなった。

なお、それまではどうしていたのかというと、光回線やCATVなどの終端装置にAterm WX3000HPを置いていた。 しかし、引っ越しに伴って家のレイアウトが変わったことでアクセスポイントを置きたい位置と配線の来る位置が変わったので、これは単なるアクセスポイント兼スイッチングハブにしてルーターは別途調達しようという考えになった。

社内で雑に相談したところ、中古のUNIVERGEがよさそうというオススメをもらい、中古で型落ちなら値段も手頃だったのでそれでやってみることにした。 ということで、UNIVERGE IX2105を購入したのでした。ついでにネットワークのお勉強もできる。

構成

超ざっくり

WAN(※)--->[IX2105]--->[WX3000HP]

※便宜上、WANとしているが、実態はたぶんマンション共用部に構成させたネットワークになっていそう

やったこと

まず、コンソールケーブルを持っていなかったので初期設定に困った。 いまどきRS-232Cのコネクタなどそうそう載っていないわけで、うちの場合は家にMac系しかなかったのでUSB-Cからなんとかする必要があった。

そこで買ったのがこちらです。

RS-232Cのコネクタを経由せずに内部的にコンソール用のRJ45に合わせてよしなにしてくれて便利。 コネクタの仕様はよく知らないけど様々なルーターに対応しているようだし、業界標準的な仕様があるのでしょう。知らんけど。

ドライバはここから入手した。

ftdichip.com

家にM1(arm)のMacしかないなか、macOS向けにはx86_64のドライバしかなくて、しかもダウンロードしてインストールしようとしたらエラーが出て焦ったけど、 よくよく読んだらApplicationフォルダに入れて実行しろということらしく、ほんまかいなと思いつつ言われるがままに入れて実行したらあっさり成功して問題なく使えた。 なんかmacOSによってよしなになっているのでしょうたぶん。

あとはマニュアルを見ながらよしなに設定。 ぐぐって出てきた設定みても結局なぜそういう設定になるのかが分からないので、ちゃんとマニュアルを読んで基礎的な操作方法や概念を理解してから設定するほうが早かった。

構成的にPPPoEとかもする必要もなくいので普通に設定をしていく。

WAN側のインターフェースはDHCPでIPを受け取りつつ、Proxy DNSとNAPTを有効化したり、 パケットフィルターやキャッシュを設定したり、NetMeisterに登録してファームウェア・アップデートをかけたりなど。

NTPはホスト名で指定できないのがちょっと微妙で悩ましかった。

よし

まあ、とりあえず良い感じになったと思うので、しばらくこれで運用してみる。 速度も元のスイッチングハブでやってたときと同等の速度がでているし、パケットフィルタもいれたことでちゃんとネットワーク的にも分離したうえで必要のないアクセスをある程度は排除できていそうで当初の目的が達成できてよかった。

Google Cloud Workflowのエミュレータを作り始めた

github.com

Google Cloud Workflowのエミュレータを作り始めた。 標準関数などもだいたいひとまずの実装が終わってきて、ある程度動くようになっている。

Cloud Workflowを検証・導入する機会があって、便利な一方でエミュレータが提供されておらずローカルでの動作確認に困ったのもあり、いっそ自作してみるかという気分になった。 あと、一度は処理系っぽいものを作ってみたかったというのもあって、それもそこそこ大きなモチベーションだった。

Cloud WorkflowはYAMLかJSONで手続きを記述するとそれをクラウド上で動かせるというもので、 Cloud Functionなどとの違いとしては制約が多く複雑なことがやりづらい(できなくはない)のでオーケストレーター的な役割などに集中させやすいこと、 また冗長性とフォールトトレランスに力がいれられておりゾーン障害などが起きても実行の継続が保証されたりリトライが簡単に記述できるあたりが良いと思う。

詳しくはドキュメントを見るとよさそう:

cloud.google.com

どうつくったかというと、GoだとCIからバイナリを落としてくるだけで動く世界観になってインストールが楽そう && 慣れているのでGoで作ることにした。

Cloud Workflowの構文はYAMLとJSONで書けるのだが、お馴染みの github.com/goccy/go-yaml が実は YAMLToJSON というインターフェースが提供されており、さらに一定のパターンは持つものの決定的に構造が定まるわけではないので一部を json.RawMessage にしてその親の構造に応じて遅延評価するような実装ができると楽そうだと踏んでそういう感じで実装していった。

JSONのデータ構造を解釈して、それの実行順序を組み、そのとおりに実行していく簡単なインタプリタを基本としている。

ほか、式や変数を扱えたり for によってスコープが作れたり、関数呼び出しが式のなかでも行えたり、演算子に優先順位があったり動的型付けながら型の概念もあったりなど、諸々をうまく解決していく必要があってなかなか大変だったし勉強になった。このあたりは機会があればどこかで話したい。

というわけで、OAuth2を使って(Google Cloud APIではなくGoogle CalendarなどのAPIである)Google APIを叩けないなど一部困った問題はあるが、 おおまか動くっぽい感じになってきたので、Cloud Workflowを使っている皆様におかれましては、ぜひ試してみてほしいです。(まだまだテスト不足でバグってるところも多いかとは思いますが。。)