ISUCON13 に参加した

前回の様子: ISUCON12 の予選に参加した - きりきりやま

こんいす〜

www.youtube.com

今年も unasuke, lime1024 とチーム「たんぽぽの上の刺身」で Ruby 実装で参加した。いつメン。

最終スコアは 15,549 点の 139 位。100 位切りたかったな〜。

タイムライン

10:00 - 13:00

  • 計測ツールのインストール
    • alp とか pt-query-digest とか
  • デプロイの準備
  • マニュアル読む
    • マニュアル読むからヘッドホン外すねって言ってるのに unasuke が無に向かってしゃべっている映像が流れてくるから毎回ヘッドホンつけないといけなくて大変だった

13:00 - 16:00

  • isupipe の DB を 2 台目に
  • PowerDNS のレコードに TTL をつける
    • 適当に 3600 にした
    • そういえば www とか mail とかよくありそうなサブドメインに対してもクエリあったんだけどなんだったんだろう、水責めとは別だとおもうんだけど
  • スキーマの変更をデプロイで反映していないことに気がついた
    • 毎年やっているはずなのに完全に記憶を失っている

16:00 - 18:00

  • PowerDNS の DB を 3 台目に
    • /etc/powerdns/pdns.dgmysql-host が指定されているのにしばらく気がつかなくて大変だった
  • ログ止めて再起動試験

いろいろ

  • 動作確認がベンチありきだったからベンチ動かない時間がつらかった
    • デプロイ後に /initialize とか叩いて最低限の動作確認ができるといいのかもしれない
  • ずっと PowerDNS の DB 重かったんだけど、まさかインデックスないなんて思いもしなかった
    • 枯れたソフトウェアだと思って信じ切っていた
  • 環境構築にわりと時間がかかっちゃう
    • 足回りとインフラは全部やるからみんな好きにやってくれ!とは思っているんだけど、それはそれとしてもう少し短縮してアプリも見たい
    • 記憶のあるうちに来年に向けて秘伝のタレを煮詰めておく
  • 最近あんまりコード書いてなかったんだけど、ISUCON 楽しくてまたコード書く気持ちになった

Cisco C841M の IOS イメージが突然いなくなった

ある日突然インターネットがこわれたので確認にいくとルーターにしている C841M の様子がなにやらおかしい。 普段はピカピカしているのに SYS ランプだけがゆっくりと点滅している1。 とりあえず電源を引っこ抜いて再起動しつつコンソールでつないでみる。

Readonly ROMMON initialized
loadprog: bad file magic number:      0x0
boot: cannot load "flash:"

IOS が見つからないらしい。rommon2はじめて見た。

rommon > dir flash:
Directory of flash:

20169    383361    -rw-     crashinfo_20221112-180230-UTC

たしかにイメージがない。crashinfo が残されているけど rommon には cat がなくて確認できなかった。

仕方がないので USB メモリ3にイメージをいれてそこから起動してみる。 TFTP でもよかったんだけどルーターが死んでいる状態でつなぐのが大変そうな気がしたからやめた。

rommon > boot usbflash0:c800m-universalk9-mz.SPA.155-3.M10.bin

起動した、こんにちは。とりあえず crashinfo を見る。

Queued messages:
001693: Nov 12 18:02:12.224 UTC: %SYS-3-LOGGER_FLUSHING: System pausing to ensure console debugging output.

001629: Nov 12 18:00:01.372 UTC: %SYS-3-CPUHOG: Task is running for (2000)msecs, more than (2000)msecs (0/0),process = NBAR timer tick task.
-Traceback= 0x4024F4Cz 0x772C9F0z 0x7729DE4z 0x772A440z 0x7765344z 0x772A2A4z 0x772A954z 0x772B988z 0x4816E50z 0x47FDD80z

001692: Nov 12 18:02:07.396 UTC: %SYS-3-CPUHOG: Task is running for (128000)msecs, more than (2000)msecs (6/0),process = NBAR timer tick task.
-Traceback= 0x42F89C4z 0x772CA18z 0x7729DE4z 0x772A1F0z 0x772A954z 0x772B988z 0x4816E50z 0x47FDD80z
001693: Nov 12 18:02:07.400 UTC: %SYS-2-WATCHDOG: Process aborted on watchdog timeout, process = NBAR timer tick task.
-Traceback= 0x42F8838z 0x772CB40z 0x7729ED4z 0x772A284z 0x772A954z 0x772B988z 0x4816E50z 0x47FDD80z
001694: Nov 12 18:02:30.520 UTC: %SYS-3-LOGGER_FLUSHED: System was paused for 00:00:81604378623 to ensure console debugging output.


 18:02:30 UTC Sat Nov 12 2022: Unexpected exception to CPU: vector 1500, PC = 0x48143FC , LR = 0x48143FC

crashinfo から抜粋。なんか大変だったことはわかった。

flash が逝ったのかと思いきや問題なく crashinfo は読めるし、試しにイメージを copy して reload してみる。

Router#copy usbflash0:c800m-universalk9-mz.SPA.155-3.M10.bin flash:
Router#reload

起動した、おかえり。

前々からたまにクラッシュする子だったんだけど、クラッシュついでに自らを消し去っていくのは流石にやめてほしい。


  1. GIG4 もだったかも
  2. めっちゃ雑に言うと BIOS みたいなやつ
  3. たしか FAT32 でフォーマットした

Go で DB にいれるデータを暗号化したい

センシティブなデータを DB に保存するにあたって、Go で暗号化を実装した。 その過程で暗号化についていろいろ調べたので、Go で暗号化したいけど暗号化のことはよくわからないという人を対象にまとめる。

今回は鍵の導出に PBKDF2 を、暗号アルゴリズムに AES-256-GCM を採用したのでこれをベースに説明していく1

PBKDF2

前提として、暗号化には鍵と呼ばれるデータが必要になる。 暗号文 (暗号化の結果) は、暗号アルゴリズムと平文、そして鍵の組み合わせで決定される。 また、鍵は復号にも用いられるため第三者に知られてはならない。

この鍵に human-readable な文字列を使うと辞書攻撃や総当たり攻撃に弱いという問題がある。 対策として、文字列のハッシュ化を繰り返し行うストレッチングと呼ばれる処理をすることで攻撃を困難にする。

PBKDF2 は任意のハッシュ関数に基づいて上記を行うアルゴリズムで、これによる鍵の導出の実装はこうなる。

package main

import (
    "crypto/sha256"

    "golang.org/x/crypto/pbkdf2"
)

func main() {
    password := []byte("password")
    salt := []byte("salt")

    // func pbkdf2.Key(password []byte, salt []byte, iter int, keyLen int, h func() hash.Hash) []byte
    key := pbkdf2.Key(password, salt, 1000, 32, sha256.New)
}

今回はベースとなるハッシュ関数 h を SHA-256、ストレッチング回数 iter を 1000 とした2。 鍵長 keyLen を 32 bytes にしていること、また salt については後述する。

AES-256-GCM

次に AES-256-GCM について。このままだと難しいので AES と 256 と GCM に分解する。

AES

共通鍵暗号のブロック暗号アルゴリズム。このままだと難しいので共通鍵暗号とブロック暗号に分解する。

共通鍵暗号3

暗号化と復号に同一の鍵を用いる暗号方式のこと。 暗号化に用いた鍵があればデータを復号できる。

ブロック暗号4

固定長のデータ (ブロックと呼ばれる) を単位に暗号化する暗号のこと。

256

AES で暗号化する際に用いる鍵のサイズ (ビット数)。 256 の他に 128 と 192 がある。 長ければ長いほど計算量が増えて安全になる。

GCM

ブロック暗号では固定長のデータしか扱えないため、ブロック長より長いデータを暗号化するためにはブロック暗号を繰り返して適用する必要がある。 この繰り返しの方法を暗号利用モードといって、GCM はそのひとつ。

ここで、AES-256-GCM を扱う実装についてまとめる。

package main

import (
    "crypto/aes"
    "crypto/cipher"
)

func main() {
    var key [32]byte

    // 鍵として 256 bits (= 32 bytes) のバイトスライスを引数に渡すと AES-256 が選択される
    block, err := aes.NewCipher(key[:])
    if err != nil {
        panic(err)
    }

    aead, err := cipher.NewGCM(block)
    if err != nil {
        panic(err)
    }
}

コメントにも書いたが aes.NewCipher() に 32 bytes の鍵を渡すことで AES-256 が選択される。 PBKDF2 で導出する鍵長を 32 bytes にしていたのはこのためだ。

また cipher.NewGCM() の戻り値に aead と名前をつけているが、次はこれについて説明する。

AEAD

暗号化に加えて、認証を同時に行う暗号利用モードを AEAD という。 認証によって、暗号文が改ざんされていないこと、また正規に生成されたものであることが保証される5

GCM は認証付きの暗号利用モードのため、cipher.NewGCM()cipher.AEAD を返す。 この cipher.AEAD を用いた暗号化の実装は以下になる。

package main

import (
    "crypto/cipher"
    "crypto/rand"
    "io"
)

func main() {
    var aead cipher.AEAD

    plaintext := []byte("plaintext")

    iv := make([]byte, aead.NonceSize())
    if _, err := io.ReadFull(rand.Reader, iv); err != nil {
        panic(err)
    }

    encryptedData := aead.Seal(iv, iv, plaintext, nil)
}

今度は iv という名前がいっぱい出てきた。

IV

暗号文は暗号アルゴリズムと平文と鍵の組み合わせで決定されると前述した。 これは組み合わせが一致していれば暗号文は常に同じになるということであり、暗号初期化ベクトルは役に立つのか?|徳丸 浩|note で紹介されているような危険性がある。 そこで、IV (Initial Vector) と呼ばれる一意な値を付加することで6、組み合わせが一致していても異なる暗号文が出力されるようにする7

復号には暗号化のときに付加した IV が必要になるのだが、それを踏まえて先ほどのコードの

   encryptedData := aead.Seal(iv, iv, plaintext, nil)

の部分を詳しくみていく。まずは Seal()シグネチャを以下に示す8

// nonce はここでは IV を指す。IV は一意である必要があるため、使い捨てとなる。このような値を nonce (number used once) と呼ぶ。
// 戻り値は dst に暗号文を append したバイトスライスになる。
func (cipher.AEAD).Seal(dst []byte, nonce []byte, plaintext []byte, additionalData []byte) []byte

ここで引数 dstiv を渡すことによって、戻り値が IV と暗号文を連結したバイトスライスになる。 IV のサイズは aead.NonceSize() で取得可能なため、このバイトスライスを暗号データとして保存しておくことで復号時に以下で IV を取得できる。

package main

func main() {
    var encryptedData []byte

    iv, ciphertext := encryptedData[:aead.NonceSize()], encryptedData[aead.NonceSize():]
}

また、この IV と暗号文をもとに復号するコードは以下になる。

package main

import "crypto/cipher"

func main() {
    var aead cipher.AEAD
    var iv, ciphertext []byte

    plaintext, err := aead.Open(nil, iv, ciphertext, nil)
    if err != nil {
        panic(err)
    }
}

まとめ

PBKDF2 による鍵の導出と AES-256-GCM による暗号化を Go で実装した。

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "crypto/sha256"
    "fmt"
    "io"

    "golang.org/x/crypto/pbkdf2"
)

func main() {
    password := []byte("password")
    salt := []byte("salt")

    key := pbkdf2.Key(password, salt, 1000, 32, sha256.New)

    block, err := aes.NewCipher(key)
    if err != nil {
        panic(err)
    }

    aead, err := cipher.NewGCM(block)
    if err != nil {
        panic(err)
    }

    plaintext := []byte("plaintext")

    iv := make([]byte, aead.NonceSize())
    if _, err := io.ReadFull(rand.Reader, iv); err != nil {
        panic(err)
    }

    // encrypt
    encryptedData := aead.Seal(iv, iv, plaintext, nil)

    // decrypt
    iv2, ciphertext := encryptedData[:aead.NonceSize()], encryptedData[aead.NonceSize():]
    plaintext2, err := aead.Open(nil, iv2, ciphertext, nil)
    if err != nil {
        panic(err)
    }

    fmt.Printf("%s\n", plaintext2)
}

  1. Rails を参考にした

  2. Rails を参考にした

  3. 他に、暗号化と復号に異なる鍵を用いる公開鍵暗号が存在する

  4. 他に、ビットやバイト単位で逐次暗号化するストリーム暗号が存在する

  5. 細工した暗号文を復号させることで情報を得る選択暗号文攻撃に対して安全になるらしい (よくわかっていない)

  6. AES-GCM の安全性は IV が重複しないことが前提となっている。例示したコードは乱数の生成回数が増えると衝突する可能性があり、本当は怖いAES-GCMの話 - ぼちぼち日記 では IV をカウンター値にするといった対策が挙げられていた。

  7. パスワードのハッシュ化においても、似たような目的で salt と呼ばれる一意な値を付加する。pbkdf2.Key() に渡していた salt がこれにあたる。

  8. 引数 additionalData に渡したデータは暗号文に含まれないが認証の対象にはなる。つまり復号時に同じデータを渡さないと復号に失敗するということだが、ユースケースがよくわかっていない。

ISUCON12 の予選に参加した

今年も unasuke, lime1024 とチーム「たんぽぽの上の刺身」で ISUCON に参加した。このチームでの参加は 3 回目になる。

おととしは ISUCON10 の予選に参加した - きりきりやま で、昨年はあまりにも無力でブログを書くことができなかった。

今年は Ruby 実装で参加して、最終スコアは 3765 点。自分がやったのは以下。

  • app と DB をわける
  • ID 採番を UUID にする
  • MySQL に移行しようとして失敗する
  • シャーディングしようとして失敗する

無力。

MySQL 移行の判断のため、エンドポイント毎にアクセスしているテーブルをスプシにまとめたら便利だった。 ちょっと大変だけど、コードの理解の助けにもなるので来年もやってもいいかもしれない。 毎年、コードの読み込みが足りないと感じる。

3 年目ともなると秘伝のタレが溜まってきて、環境構築はかなり最適化されてきたと思う。 来年は有力になりたい。

2021年

畑をはじめた

昨年、庭にプランターで家庭菜園をはじめたけど、今年は近所の貸農園になった。

家庭菜園むずかしいとか昨年は言っていたんだけど、細かいこと気にしなければほっといたら育つ (というか育ちすぎる) し、物量で殴って最終的に辻褄をあわせるのがよいという結論になった。 この方針でやっていると食べきれない量の野菜ができて困る。

f:id:kirikiriyamama:20220106173628j:plain f:id:kirikiriyamama:20220106173642j:plain f:id:kirikiriyamama:20220106173922p:plain

作業環境がよくなった

ディスプレイアームを買ったりマイクを買ったりした。

f:id:kirikiriyamama:20220106173350j:plain

ディスプレイは左右に並べていたら首が痛くなっちゃったから上下にした。

車を買った

my new gear...

f:id:kirikiriyamama:20220106173305j:plain

人間に水を飲ませた

最近また飲んでいない。

kirikiriyamama.hatenablog.com

スチームオーブンレンジを買った

ボタンがすくなくて操作がむずかしい。性能面はわりと満足していて、特に酒かんモードがいい。

panasonic.jp

これはグリルでつくったからあげ。説明書に従わないとこういうことになる、味はいい。

M1 MBA を買った

はやい。Twitter しかしていないからこれで開発するとどうなるかは知らない。

1U サーバを買った

競馬

日曜日になると TL の全員が競馬やっている世界線にきてしまった。有馬記念 3 連単あたってうれしい。

年間収支はマイナスである。

人間に水を飲ませる技術

妻が水を飲まない。健康のために水を飲むと自ら宣言したのに飲まない1。毎日「今日はこれを飲む」と言って 2L のペットボトルを仕事部屋に持っていくのに 500ml も飲んでいない。https://twitter.com/mtkasr/status/1384856756963135488 を見てぴよログで水を飲んだことを記録しはじめたがそれも 3 日で終わった。

f:id:kirikiriyamama:20210518234835p:plain
リマインドするが無力

常に「今日も水を飲めなかった…」とか言ってるのでとにかく記録させるために物理的なボタンを用意することにした。それで機械にリマインドさせたい。


できたもの。

用意したボタン。

www.switch-science.com

仕組み。

  • M5StickC のボタンを押すと GAS の Web Apps にリクエストが飛ぶ
  • Web Apps で Spreadsheets に水を飲んだことを記録する
  • 水を飲んでいなかったら GAS の cron で Slack にリマインドする

あと、一日のおわりにその日飲んだ量を Slack に流している。Spreadsheets で集計しているんだけど、普段あんまり Spreadsheets 使わないからこれが一番むずかしかった2

M5StickC は Arduino (だいたい C 言語) で実装した。久しぶりの C 言語は文字列をフォーマットするだけでわあ大変ってなった。これが初期のコード。

#include <M5StickC.h>

#include <HTTPClient.h>
#include <WiFi.h>

const char* ssid = "xxxxx";
const char* passphrase = "xxxxx";

const char* url = "https://example.com/";

void setup() {
  M5.begin();

  Serial.begin(9600);
  delay(1000); // wait for serial initialization
  Serial.print("\n");

  M5.Axp.ScreenBreath(10);
  M5.Lcd.setRotation(1);
  M5.Lcd.setTextSize(1);
  M5.Lcd.fillScreen(BLACK);

  M5.Lcd.print("connecting...");
  WiFi.begin(ssid, passphrase);
  // TODO: timeout
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
  M5.Lcd.println("ok");
  Serial.println(WiFi.localIP());
}

void loop() {
  M5.update();

  if (M5.BtnA.wasReleased()) {
    M5.Lcd.print("sending...");

    HTTPClient http;

    http.begin(url);
    http.addHeader("Content-Type", "application/json");
    int code = http.POST("{}");
    Serial.println(http.getString());

    // TODO: follow redirects
    if (code == 302) {
      M5.Lcd.println("ok");
    } else {
      M5.Lcd.println("failed");

      if (code > 0) {
        M5.Lcd.println(code);
      } else {
        M5.Lcd.println(http.errorToString(code).c_str());
      }
    }

    http.end();
  }

  delay(1);
}

周辺の技術がわかっていないのもあり、いまいち一次情報を見つけられなくて苦労した。以下のサイトは日本語で情報がまとまっていてとてもお世話になった。

あと M5StickC で調べるとそんなに情報が出てこないんだけど、中身としては ESP32 という SoC の上で FreeRTOS が動いているので、そのあたりの単語をつかって調べるとよかった。

このあと余計なことをしだして、バッテリー節約のために一定時間操作がなかったら Wi-Fi を切断するとか、送信に失敗したら LED を点滅させる (これを非同期でやりたい、愚直にやると操作をブロックしてしまう) とかやっていたら割り込みや排他処理と向き合わないといけなくなって大変だった。ボタンが手元にないときのために Slack の Slash Commands もつくって、最終的にこうなった3

https://gist.github.com/kirikiriyamama/79c47a74e861e6a16d1b4106c70ec937

これでわりと水を飲むようにはなったんだけど、お昼ごはんのときにいっぱい飲んだり、一日の終わりに駆け込んでむりやり目標を達成したりしていて、そういうことじゃない気がしている。がんばってほしい。


  1. よく知らないけどいっぱい飲むといいらしい

  2. ARRAYFORMULA を使っているせいで GAS で行挿入するコードがむずかしい、もう少しうまいやり方がある気がしている

  3. いま気づいたんだけどラクダとヘビがめちゃくちゃなことになっている

2020年

L2SW を買った

ついにラックマウントタイプの機器を買ってしまった、うるさい。

家庭菜園をはじめた

なんか思っていたよりだいぶ難しくて、まずパラメータがとても多い。 必要になる栄養素がたくさんある上にそれらをバランスのとれた状態に保つ必要があって、例えば窒素が多すぎるとカルシウムが不足して実がぼろぼろになってしまう。 栄養状態を計測することも難しくて、作物の状態を見て少しずつ肥料で調整していくしかない*1。 農薬を使わないとすぐ虫や病気にやられてしまうし、天候にも大きく左右される。 短期間に何回も試行することができないという難しさもある。 農業で生きている人はすごい。

今年は初めてにしてはうまく育てられたと思う、おいしくできた。 大葉などの薬味も一緒に育てて、好きなときにいつでも薬味を収穫できる生活はかなり体験がよかった。

sunsetcellars.jp

正直いままでワインをおいしいとおもったこと一度もなかったんだけど、SUNSET CELLARS は全部おいしい。

chichibubeer.shop-pro.jp

おいしい。

野菜の定期便をはじめた

毎週わりと大量の野菜が届いて、それに合わせて献立を考える生活になった。 呼吸をしていると勝手に野菜が届くのでスーパーにいってあれこれ考えるよりだいぶ楽。 たまに名前も聞いたことのないような野菜が届いてどう食べたらいいかまったくわからなかったりするんだけど、それはそれで楽しい。

数ヶ月の無職期間を経て転職した

無職すごい楽しくて、野菜育てたりコード書いたりしてたらあっという間だった。

いまは BizteX というところで Go を書いている。 LSP の補助に感動しつつ、たまに Ruby の表現力が恋しくなっている。

ISUCON

ISUCON10 の予選に参加した - きりきりやま

ドラム式洗濯機を買った

洗濯物を干す行為が嫌いだったのでうれしい。

洗剤自動投入機能がついているんだけど、これが完全に "顧客が本当にほしかったもの" だった。 洗濯物をいれてボタンを押すだけで済むのはかなり体験がよくて、洗剤をいれる行為がこんなにも面倒なことだなんて思いもよらなかった。 使う前は洗剤が自動で投入されるだけでしょ?としか思っていなかったし、顧客は本当にほしいものを知らなすぎる。

開発環境を Windows にした

ここ数年は Linux をメインにしていたんだけど、デスクトップ環境をあれこれすることにはあんまり興味がなくてとにかくいい感じに動いてほしいと思っていたところに WSL2 が台頭したので、思い切って移行してみた。 久しぶりの Windows、使いやすくなっていてびっくりした。

いくつか気になるところはあるものの*2*3*4、開発環境として問題なく使えているし満足している。 Windows で開発する日がくるなんて思ってもみなかった。

サーバラックを買った

*1:肥料によっては効くまでに 1 ヶ月くらいかかる

*2:フォントが汚い、HiDPI で殴ると解決する

*3:ConPTY がつらい、今後に期待している

*4:WSL と Windows でファイルのやりとりをするのがちょっとめんどう