QNAPのNASをOpenTelemetryで監視したいときはSNMP Receiverを使えるが、設定を書き連ねるのが大変

なぜやりたいか

自宅にQNAPのNASを導入したことは以前の記事で触れた。このNASは便利に使っているのだけれど、何の監視もせずに運用していくのは心配。気付いたらディスク容量がパンパンで大慌て、みたいなことは避けたい。

ふと調べてみると、OpenTelemetryにはSNMP Receiverがあるではないか。NAS側がSNMPを使えることはわかっていたので、お手軽に監視できるのでは?ということで、試してみることにした。

どうやったか

NAS側の設定

以下を辿ればできる。簡単便利。

docs.qnap.com

OpenTelemetry Collectorを動かす

動かし方はいろいろある。本当は、Raspberry Piなどで外部から問い合わせるのが手堅いと思うが、とりあえず手軽にやりたかったので、NASにあるContainer Stationという機能*1を使った。手触りとしてはDocker Composeのラッパーという感じ。NASにあるデータと連携させたいような常駐アプリケーションを置いておくにはよいと思う。あと、データがNASのバックアップ機能によって自然と行われるのも魅力。

OpenTelemetry CollectorのイメージはDockerHubにあるので、これをpullしてきて動かす*2

OpenTelemetry CollectorがNASにアクセスしてSNMPの値を取得するように設定する

コンテナ内の/etc/otelcol-contrib/config.yamlを編集することで行える*3。設定の方法は以下のページを参照。

github.com

つらいのは、OIDを調べるところである。snmpwalkを使って調べようとした時代もあったが、挫折。以下のサイトを参照している。

https://mibs.observium.org/mib/NAS-MIB/

基本的に、テーブルの列を指すOIDを指定すればよいようだ。

お好みの送信先に送る

Collectorが取れるようになれば、あとはOpenTelemetryの仕組みの中でやりたい放題である。今のところ、最近オブザーバビリティプラットフォーム*4として活発な開発が行われていてAPMにも対応した*5Mackerel*6でもよいし、Grafanaなどでもよいだろう。今のところ、自分は上記の二つのプラットフォームに送信してみている。

今後の課題

MIBを調べて一個ずつ設定を書いていくのはあまりにつらい!必要最低限の項目だけならともかく、網羅的にやるならもっといい方法はないのか……。と思っていたら、こんな記事を見つけた。

zenn.dev

snmpwalkはPrometheusのSNMP exporterと組み合わせる(設定例)

walkができるのなら、個別のマッピングを人間が書かなくてもいい……?詳しくは調べていないので、今度試してみようと思う。

*1:https://www.qnap.com/ja-jp/software/container-station

*2:https://opentelemetry.io/docs/collector/installation/

*3:目下のところ変更管理はできていない。よくない……

*4:https://mackerel.io/ja/blog/entry/announcement/mackerel-as-observability-platform

*5:https://mackerel.io/ja/blog/entry/announcement/20250203

*6:筆者はMackerel開発元である株式会社はてなの従業員であるものの、本記事は個人の見解に基づいて執筆されている

ぼくの考えた最強のアルゴリズムを雑に検証してみたら、想定通り駄目なアイディアだった話

この記事は、はてなエンジニア Advent Calendar 2024の 29日目の記事です。昨日は id:todays_mitsui さんの「React の className と SolidJS の class」でした。

頼りないアイディア

日の下に新しきものなしという有名な言葉があります。いろんな使われ方がありますが、私は、これを「思いついたことの先行事例がないか考えてみよう」という意味でも捉えています。

さて、私はかねて思っていました。モデル層で書いた状態を判定するためのロジックを永続化層へのクエリでも管理しないといけないのがつらいな、と。我々は、アクティブな状態のユーザーを検索したり、ユーザー管理画面を開いたときユーザーの表に「アクティブ」と表示させたいのであって、別にrepository.findActiveUsersで返ってくるレコード全てに対してuser.isActiveがtrueであることを確認したりしたいわけではないはずです。そんなわけで、なんとかSQLはシンプルに保ちつつ、アプリケーション側でフィルタする方向に倒せないかなと考えていたのですが、ある日思いついたのです。

「ゆるくSQLで絞り込んでからアプリケーションできちんと絞り込む。欲しい件数に足りていなければ、カーソルを動かしてもう一回クエリ、もう一度絞り込む。件数が十分になったら打ち切って結果を返す。どうだ!これなら完璧なのでは?」

自分は天才なんじゃないかと5秒くらい思いましたが、日の下に新しきものなし、です。いいアイディアなら、誰かが思いついて実用されているんじゃないか?調べてみると、先行事例を見つけることはできませんでした。どうにも雲行きが怪しいですね。自分はヘンテコな思いつきをしてしまったのではないか?

まあ、冷静になって机上で考えただけでも不穏な点はあります。例えば、テーブル全体を走査して結果が0件になるような状況なら、レコード数 / ページサイズ だけのクエリが発行されることになるでしょう。基本的にはすぐ見つかるが、時々例外的に除外したいものが混じっている、程度の状況にしか使えないのではないかという予想はすぐにつきます。

それでも試してみる

予想を立てて諦めるのもいいのですが、目の前にあるキーボードをちょっと叩けば、この程度のアイディアの検証はすぐできるはずです。ちょうどGoの勉強をしたかったこともあり、実際にやってみることにしました。

github.com

1回のクエリに100ms掛かる検索処理で一定数のレコードを取得したあと、乱数によって一定確率でアプリケーション側のフィルタによって絞られる。件数が必要数に足りなければ、もう一度クエリを投げて同じことをする。必要数を満足するまでこれを繰り返す。コードにすると、以下のような処理です(全体は上記リポジトリ参照)。

func Find(after FooId, limit uint, isActiveThreshould uint) []Foo {
    r := rand.New(rand.NewSource(time.Now().UnixNano()))
    randomNumber := r.Intn(100)
    rng := lo.Range(int(limit) * 2)
    time.Sleep(QUERY_EXECUTION_TIME_SECOND)
    return lo.Map(rng, func(item int, index int) Foo {
        return Foo{
            ID:       FooId(item),
            IsActive: randomNumber < int(isActiveThreshould),
        }
    })
}

このようなアルゴリズムによって得られた結果がこちらになります。

アプリケーション層フィルタ処理通過率と処理時間の関係

急激に処理時間が延びていく様子がわかります。うーん。厳しい!通過率50%までは要件によってはギリギリ許容されるかな~という気もしますが、そもそもクエリをちゃんと書いていたら不要な心配なわけで、あまり検討されないのもむべなるかな、という結果に終わりました。予想を裏切らない結果ですね。

感想

このように手軽にベンチマークが書けるエコシステムが整っているのはGoのいいところですね。考え込む前にまず実験!という動きをするにはとても便利です。

今回考えたアイディアは日の目を見ませんでした。似たような、しかし微妙に違う絞り込みロジックが沢山あるとき、それぞれのクエリに名前を付けていくのが結構大変なんですよね。しかし、そうした問題を解決する方法としては、別の方法を考えなくてはいけないようです。モデル層のロジックとクエリそれぞれを考えなくてはいけないことは受け入れて、片方を変えたときもう一方の変更が漏れないような方法を工夫する方が筋がよいのかもしれません。

はてなエンジニア Advent Calendar 2024、明日は id:yutailang0119です。

LambdaでS3イベント通知を受け取ってHTTPリクエストを送信したいとき気をつけること

S3に何らかのファイルが置かれたとき、そのことをアプリケーションに通知したいとき、S3イベント通知を使うと便利なのだが、いくつかハマりどころがあったので、備忘しておく。

S3イベント通知は、特定のバケットにおいて何らかの操作がなされたとき、そのことを連携しているサービスに通知してくれる。コンソールをポチポチするだけでイベントオブジェクトをLambdaが処理してくれるようになるので大変便利だ。

docs.aws.amazon.com

このイベントオブジェクトの構造は以下のようになっている。*1

{  
   "Records":[  
      {  
         "eventVersion":"2.2",
         "eventSource":"aws:s3",
         "awsRegion":"us-west-2",
         "eventTime":"The time, in ISO-8601 format, for example, 1970-01-01T00:00:00.000Z, when Amazon S3 finished processing the request",
         "eventName":"event-type",
         "userIdentity":{  
            "principalId":"Amazon-customer-ID-of-the-user-who-caused-the-event"
         },
         "requestParameters":{  
            "sourceIPAddress":"ip-address-where-request-came-from"
         },
         "responseElements":{  
            "x-amz-request-id":"Amazon S3 generated request ID",
            "x-amz-id-2":"Amazon S3 host that processed the request"
         },
         "s3":{  
            "s3SchemaVersion":"1.0",
            "configurationId":"ID found in the bucket notification configuration",
            "bucket":{  
               "name":"bucket-name",
               "ownerIdentity":{  
                  "principalId":"Amazon-customer-ID-of-the-bucket-owner"
               },
               "arn":"bucket-ARN"
            },
            "object":{  
               "key":"object-key",
               "size":"object-size in bytes",
               "eTag":"object eTag",
               "versionId":"object version if bucket is versioning-enabled, otherwise null",
               "sequencer": "a string representation of a hexadecimal value used to determine event sequence, only used with PUTs and DELETEs"
            }
         },
         "glacierEventData": {
            "restoreEventData": {
               "lifecycleRestorationExpiryTime": "The time, in ISO-8601 format, for example, 1970-01-01T00:00:00.000Z, of Restore Expiry",
               "lifecycleRestoreStorageClass": "Source storage class for restore"
            }
         }
      }
   ]
}

なので、追加されたファイルに何らかの処理をしたいのならば、Records[].s3.object.keyをもとにバケットにアクセスし、あれこれ触ればよい。

どこにハマったか

LambdaのJavaScriptランタイムに手軽なHTTP通信用のライブラリは組み込まれていない

コンソールでポチポチしてちょっとしたHTTPリクエストを送るものを探索的に実装したかったので、JavaScriptでLambdaを書いていた。こういう感じなので、組み込まれていないライブラリに依存したコードは書きたくない。こんなとき、Lambdaではどうしたらいいのだろう?

最新のNodeJSにはfetch APIが登場しているが、LambdaのNodeJSランタイムのバージョンは20で、ギリギリ安定化されていないので、使えない。11月には22のランタイムが登場するようなので、ギリギリ間に合わなかったようだ。

docs.aws.amazon.com

そんなわけで、httpsというモジュールを使う必要がある。

nodejs.org

使い心地については、リンク先にある以下のサンプルコードがわかりやすいと思う。

const https = require('node:https');

const options = {
  hostname: 'encrypted.google.com',
  port: 443,
  path: '/',
  method: 'GET',
};

const req = https.request(options, (res) => {
  console.log('statusCode:', res.statusCode);
  console.log('headers:', res.headers);

  res.on('data', (d) => {
    process.stdout.write(d);
  });
});

req.on('error', (e) => {
  console.error(e);
});
req.end();

見ての通り大分低レベルなAPIで、手軽なスクリプトを書く目的からするとtoo muchな感じがある。自分は、最後にreq.end()を呼ばないと通信が終了しないことに気付かず、Lambdaを何度かタイムアウトさせてしまったことをここに懺悔したい。

httpsでリクエストを組み立てるとき、Content-Lengthの計算方法には工夫が要る

上記の通り、だいぶ低レベルなAPIだから、POSTリクエストを送るときにリクエストボディを見てContent-Lengthを良い感じに設定してくれる仕組みなどはないようだ。なので、自分で計算する必要がある。

このとき、サンプルコードなどを見ながら、何気なくこんな書き方をして大失敗をした。

const payload = JSON.stringify(payloadObject);
const options = {
  method: 'POST',
  headers: {
    'Content-Length':  payload.length
  }
};

なぜか?Content-LengthとString.lengthの仕様を見ればわかる。

Content-Length エンティティヘッダーは、受信者に送信されるエンティティ本文の長さをバイト単位で示します。 developer.mozilla.org

length データプロパティは、String オブジェクトの文字列長を UTF-16 コード単位の数で表します。 developer.mozilla.org

長さの定義が揃っていないのである!このため、うまくペイロードを送信できず、受付先の処理が失敗していた。この不具合は、ASCII文字のみのファイル名では顕在化しないため、原因箇所に気付くのが遅れてしまった。

解決するには、Buffer.byteLength(payload, 'utf8') のように書けばよいようだ。

nodejs.org

S3イベント通知では、keyに日本語が含まれる場合、エスケープされる

読んで字のごとしである。keyに日本語が含まれないケースでは成功するのに、日本語が含まれると失敗するという不具合を調べていたところ、S3イベント通知のオブジェクト内ではkeyがUTF-8エスケープ文字列に変換されていた。これをそのままHTTPリクエストに詰めて別アプリケーションに送信すると、実際と異なるキーにアクセスしにいって空振りしてしまう。これを回避するには、Lambda内で文字列のエスケープを解除してやるとよい。エスケープの解除は、decodeURIを使えばよい。

developer.mozilla.org

まとめ

あまり触ったことのない道具を使ったら火傷した、という話に集約されると思う。手元でちょっとしたお試し、という段階でこういうことに気づけたのはよかったけれど、ドキュメントを斜め読みせず見ていたらもっと事前に気づけたところもありそう。なので、ドキュメントをもっときちんと読めると良いですね、という教訓で締めくくりたい。

「初めてのGo言語」を読んだ

Goと仲良くしていく必要性を感じていたので、書店でいろいろ見比べて買ってきた。プログラミングしたことない人向けの記述は不要だったので、Go特有の概念や考え方がよくまとめられているのは非常に使いやすいと感じた。通読しても良いし、必要なときにリファレンスとして紐解くにも便利な構成になっているから、今後も机のそばに置いておきたい。

以下は本書に基づくGoに対する第一印象

基本的にはふむふむという感じで読んでいた。エラーを例外として扱わず関数の戻り値として返すコンセプトなどは今も仕事でやっていて困っていないし、よいのではないか。あまり並列処理を明に扱った経験はないのだけど、ゴルーチンは便利そう。以前別の言語で軽く触ったときは、同期処理とつなぎこむところがもう少しおまじないめいていて難しかった記憶がある。うまい抽象化ができているんだと思う。

なるべく安全に寄せて、便利な自動変換は予期せぬ不具合の温床になる、明示的にできるものは明示的にしようという考え方自体は理解できた。しかし、イミュータブルみたいな話になると、「ここは人間を信頼しよう」みたいな話になるのが呑み込みきれなかった。もう一つのコンセプトとして高速にコンパイルしたいという事情があって、トレードオフの結果ということなんだろうか?仮にそうだとすると、一般的な開発環境の計算速度が向上したり、コンパイル最適化の技術が進歩していけば、明示的にイミュータブルであると宣言できるようになるのだろうか*1

さて、ここまでは机上の話。今度は実際に手を動かして何かを作ってみたい。

*1:実際、本書によればGoの初期にジェネリクスがなかったのは、コンパイル速度を犠牲にせず実現する方法がなかったからだそうだし

初のNASを迎え入れて対峙したいくつかの課題とその打開策など

まとめ

  • NASを買った
  • バックアップにはS3 Glacier Deep Archiveを採用したが、いくつか注意点がある
  • IOPSが足りない気がして設定をあれこれ見直したけど、効果を定量的に測定しきれていない
  • 折角買ったのでいろいろ活用していきたいね

なぜNASを買ったか

ネットワーク機器スペース(通称わくわくインターネットランド)に鎮座するNAS

同人活動などもやっている都合、大容量のファイルをずっとアーカイブしたい事情があった。これを今まではDropboxでやってきていたのだけど、いい加減なんだか複雑な気持ちになってきたのだ。

もちろん、Dropboxは素晴らしいサービスだ。ファイルの共有も簡単にできるし、バージョン管理されているから、操作ミスでファイルを壊してしまってもなんとかなる可能性が高い。お金さえ払えば、それなりのディスク容量も提供してくれる。

ただし、あくまでデータは人に預けたものだし、毎月1000円以上のお金を払う必要はある。その価値はあるけれど、今後もずっと払い続けていく固定費と考えると悩ましいものがあった。

とはいえ今まではそうしてきたし、大きな不満はなかった。ではなぜNASを買ったかというと、以下のような事情がある。

  • ライフステージが変わり、同人活動以外でも共有すべきデータが増えてきた
  • 音楽や写真などのデータが溜まってきていて、Dropboxにバックアップすべきデータとそうでないデータに仕分けるのが苦痛になってきた
  • 職業柄、フルマネージドのサービスばかり触っていてもよくないと思った

どんなNASを買ったか

こんなのを買った。ディスク容量は4TBだが、もちろん増設はできる。RAIDは今のところ組んでいない。これによる信頼性の低下については、後述するがS3 Glacierへのバックアップによって担保したいと考えている。

なぜこれにしたかというと、以下のようなことを考えたからだ。

  • HDDの交換が簡単そうな作りになっていた
  • マルチギガビット対応なので、10Gbps環境のおうちネットワーク環境を最大限生かせる
  • 機械学習による画像のタグ付け機能などが充実していた

課題と対応

バックアップをどうするか

NAS一台では故障のリスクは無視できない。ディスク破損によりデータが飛んだりしたら最悪だ。このリスクは、RAIDを組んでも低減しきれないと考えたので、RAIDは組まないことにした。代わりに採用したのが、S3 Glacier Deep Archiveへのバックアップだ。

masawada.hatenablog.jp

上記の記事を大いに参考にした。ただし、この後NASのOSがアップデートされたのか、AWS側でライフサイクルポリシーを変更しなくても、直接S3 Glacier Deep Archiveのストレージクラスを指定してアップロードできるようになったようだ。便利便利。

クラウドストレージを脱出して結局クラウドストレージに依存してるじゃん、という気はするけれど、コストを計算してみると月々数ドルとかになりそうだったので許容した。

ただし、この点については二つ失敗をしたのでここに書き留めておく。注意されたい。

初回のバックアップはそれなりに費用が掛かる

当たり前の話ではある。しかし、初回は試行錯誤も発生するので尚更お金が掛かりやすいということに留意したほうがよい。多分、最初はあまりデータ量のない状態で何度か試すとよかったのだと思うけど、一度入れてしまってからバックアップを取り始めたから後の祭りである。

アップロードに失敗した場合にゴミデータが残り続けることに注意

コスト状況を確かめようとCost Explorerを見ていたら、DeepArchiveStagingStorageという見慣れない費用が随分掛かっていた。

ある日のコスト発生状況抜粋

なんだこれ?調べてみると、マルチパートアップロードが途中で止まった場合に残ったデータたちらしい。

Glacier Deep Archive ストレージクラスのオブジェクトの CompleteMultipartUpload (複数アップロード完了) リクエストが完了する前に、マルチパートオブジェクトに使用されているバイト数。

docs.aws.amazon.com

何度かバックアップに失敗したので、そのときに作られてしまったのだろう。これはしかるべきライフサイクルポリシーを設定することで、定期的に削除することができる。

ゴミデータを削除するためのライフサイクルルール

この点にメスを入れることで、一日0.2USD程度で大量のデータを保存できるようになった。やったね。

なんか転送が遅いんだけど

SMBでマウントしていると、なんだか転送が遅い気がした。具体的に言うと、RPGツクール製のゲームを起動するときめちゃくちゃ待たされる、など*1スループットは数百Mbps前後出ていそうなので、IOPSが足りていないのではないか*2。そう考えて、非同期IOを有効化するなどの手を打ったらだいぶ改善した気がする。これについてはあまり定量的な議論ができておらず、やや歯がゆい。うまいモニタリングの方法はないだろうか。

非同期IOはクラッシュ時などにデータ消失のリスクもあるはずだけれど、今回はそれを容認することにした。大量のファイルを転送するときはカット&ペーストでなくコピペしてから削除するような小技を使うことになるかもしれない。

NASを導入して嬉しかったこと

メディアサーバーが家で稼働する形になり、家のオーディオ機器から手軽に音楽が再生できるようになったのはよかった。オーディオ機器からの再生の都合でストリーミングサービスに頼りがちになっていたけれど、これでおうちの音源を手軽に流すことができる。

あとは、Dropboxの容量節約のためにクラウドストレージを複数管理するような苦しみから解き放たれたのも嬉しいですね。

今後やりたいこと

NASのモニタリング

今はほぼモニタリングできていない。Mackerelなどを導入して稼働状況を監視できるようにしたい。Mackerelのインストールはコマンド一発というわけにはいかないようだけど、以下のようなグッズを提供してくださっている方がいるところまでは調べることができた。

github.com

SSDキャッシュの導入

SATAポートが一つ余っているので、SSDキャッシュを使えば高速化が図れるのではないか。そのうち懐が暖まったらやってみたい。

UPSの導入

なんとUPSがない。絶対いい状態ではないけれど、結構高いのよね……。SSDキャッシュよりは優先度高いと思う。おすすめUPS情報お待ちしています。

アプリケーションを動かしてみる

QNAPのNASはコンテナイメージを持ってきて稼働させることができるようだ。折角なので、この辺をうまく使って面白いことができないか模索したい。

*1:ゲームはローカルに置けという主張は正しいけど、軽いゲームくらいならNASに置けないかなと思ったのだ

*2:今時のツクールはJavaScriptで動いているので、結構な数の.jsファイルを転送していそう

Androidアプリ開発に入門してみた

サーバサイドのアプリケーションやコマンドラインツールは作ったことがあるけれど、考えてみるとモバイルアプリというものは作ったことがなかった。何事も経験ということでやってみる。なぜAndroidかというと、普段使っているのがAndroidであるため。

環境構築

プライベートではWSLで開発しているので、以下の記事を参考にさせていただいた。

zenn.dev

usbipdのインターフェイスは少し変わっているようで、上記の記事通りにやると怒られた。

$ usbipd: error: The 'wsl' subcommand has been removed. Learn about the new syntax at https://learn.microsoft.com/windows/wsl/connect-usb#attach-a-usb-device.

最初にWSLと共有可能なように設定し、その後バインドという流れになるようだ。共有可能とするには管理者権限が必要。sudoが欲しい!!!

$ usbipd bind -b 2-1
$ usbipd attach --busid=2-1 --wsl

インストーラの表示がまあまあグリッチしていて不穏。後でWSLをアップデートしたら治ったので一安心である。しかし、どうにも動きがもっさりだし、うまくデバイスを認識してくれている様子がない。心配。バッテリーも心もとなくなってきたので、Hello, worldするには至らなかった。

何を作るか

環境構築をしながらつらつら考えてみる。自分は何かを作るときには日常のお困りを解決するグッズを作ってみることにしているのだが、いまいち思い浮かばない。正確に言うと思い浮かばないは嘘なのだけど、サーバサイドとの連携が必要なアイディアしかなかった。それはそれで悪くないのだけど、初手でやることではないな、と思った。 要するに、スマートフォンに何ができるのかよくわかってないのだな。

今後すること

  • やっぱりこのノートPCでは限界がありそう。家のデスクトップPCとかでやる必要があるのではないか
  • 最初の一冊的な本を見つける
  • モバイルアプリの特徴は、端末にくっついている様々な機能を使えることだと思うので、どんなものがあるか調べる

この関数を使われるんですか? はい、それでしたらまず関数利用申込書を記入いただいて……

世の中には、軽々に使ってほしくないメソッドというものがありますよね。通常のフローと異なる経路でデータを削除するメソッドとか。 手っ取り早くdeprecatedにしてしまえればいいんですけれど、何かしら理由があれば許可し続けたいものもあるんですよね。

同僚がこういうことで困っていて、ジャストアイディアを口にしたら参考になったと喜んで貰えたのでメモっておきます。

TL,DR;

  # 緊急時に通常のバリデーション等を無視して削除するメソッド
  # 理由なしに緊急削除をしようとしたら実行時エラー
  #
  # 引数なしにおける関数定義
  # 引数がなければ実行時エラーする
  @spec emergency_delete :: none
  def emergency_delete do
    "Do not use this without a reason!" |> raise
  end

  # 理由があるなら許すよ
  # でも空文字列は許さん
  @spec emergency_delete(String.t()) :: :ok
  def emergency_delete(reason) when is_bitstring(reason) and byte_size(reason) > 0 do
    "Deletion completed without any validation!"
    |> IO.puts
  end

思いついた背景

紙の書類などの文化で、「例外的な手続きをするので書類のここに理由を書いてね」みたいなものがよくある気がする。これをコードで素直に表現してしまえ。

コメントと比べて何が嬉しいの?

  • コメントを見ずにうっかり使った人を確実に弾ける
  • コードレビューする側としても、呼び出し先のコメントを見に行くより気付きやすい
  • lintツールが十分整備されていない状況でも使える
  • 型に依存しないので、動的型付け言語でも使える

課題

  • 実行時エラーなのがイマイチ。コンパイルエラーやlintなどで落としたいですよね
  • コードの中に長い自然言語が現れるので頻出するとノイジーかも

(おまけ)なんでテストコードがElixirなの?

同僚がElixirわからんと言っていたので、ついでに話題にしたかった。一旦どの辺がわからないのか、などの叩き台にしたいので、自分が特徴的だと思う部分を雑に話題にしておく。Supervisorがどうのという話もあるけれど、今は一旦構文の話をしておく。

  • |>はパイプライン演算子で、左辺の評価結果を右辺にある関数の第一引数に代入する
  • 同じ名前の関数を複数定義した場合は上から順にパターンマッチを試していって、マッチした関数が適用される
    • パターンマッチは引数の数意外にも細々条件が付けられる
  • 何一つマッチしなければFunctionClauseErrorが発生する
  • Erlangのモジュールはセミコロンを先頭に付けた形で呼び出す(例: :rand.uniform(n))
  • @spec... で型を註釈することで静的解析してくれる