生涯未熟

生涯未熟

プログラミングをちょこちょこと。

runtime.Goexitを改めて理解する

序文

runtime packageには Goexit() というfunctionがあります。

// Goexit terminates the goroutine that calls it. No other goroutine is affected.
// Goexit runs all deferred calls before terminating the goroutine. Because Goexit
// is not a panic, any recover calls in those deferred functions will return nil.
//
// Calling Goexit from the main goroutine terminates that goroutine
// without func main returning. Since func main has not returned,
// the program continues execution of other goroutines.
// If all other goroutines exit, the program crashes.

// Goexitはそれを呼び出すgoroutineを終了します。 その他のゴルーチンは影響を受けません。
// Goexitは、goroutineを終了する前にすべてのdefer呼び出しを実行します。 Goexitはpanicではないので、これらのdefer関数の中のすべてのrecover呼び出しはnilを返します。
// メインのgoroutineからGoexitを呼び出すと、メインgoroutineに戻ることなくそのgoroutineが終了します。 func mainは返されていないので、プログラムは他のgoroutineの実行を続けます。
// その他のゴルーチンがすべて終了すると、プログラムがクラッシュします。
func Goexit() {
    // Run all deferred functions for the current goroutine.
    // This code is similar to gopanic, see that implementation
    // for detailed comments.

        // 現在のゴルーチンのすべてのdefer関数を実行します。
        // このコードはgopanicに似ています。詳細なコメントの実装を参照してください。
    gp := getg()
    for {
        d := gp._defer
        if d == nil {
            break
        }
        if d.started {
            if d._panic != nil {
                d._panic.aborted = true
                d._panic = nil
            }
            d.fn = nil
            gp._defer = d.link
            freedefer(d)
            continue
        }
        d.started = true
        reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
        if gp._defer != d {
            throw("bad defer entry in Goexit")
        }
        d._panic = nil
        d.fn = nil
        gp._defer = d.link
        freedefer(d)
        // Note: we ignore recovers here because Goexit isn't a panic
    }
    goexit1()
}

コメントにあるように、以下の特性を持ちます。

  • 呼び出し元のgoroutineを終了する
  • すべてのgoroutineが終了すると、 deadlock を起こしクラッシュする
  • panic処理と違い、defer function内のrecoverはnilを返す

なかなか使い所の難しい要素ではありますが、Goでは FailNow() などで使われたりしています。
さて、このGoexitですがそこまで使われていないっぽくて、自分自身使ってみたことがないので試してみました。

試行

それでは軽く試してみましょう。

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    go func() {
        defer fmt.Println("Done")
        time.Sleep(1 * time.Second)
        runtime.Goexit()
    }()

    fmt.Printf("goroutines: %v\n", runtime.NumGoroutine())
    time.Sleep(2 * time.Second)
    fmt.Printf("goroutines: %v\n", runtime.NumGoroutine())
}

// goroutines: 2
// Done
// goroutines: 1

正常に終了し、goroutineの数が減っていることがわかりますね。
ただ、これだけでは単純に return するのと変わりません。次は少し捻ってみましょう。

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    go func() {
        defer fmt.Println("Done")
        HelloAndExit()
    }()

    time.Sleep(1 * time.Second)
}

func HelloAndExit() {
    fmt.Println("Hello")
    runtime.Goexit()
}

// Hello
// Done

Hello と出力してからgoroutineを終了させるパターンです。
ただし、これだけだと

func HelloAndExit() {
    fmt.Println("Hello")
}

としても同じ結果になります。
それでは、 return との違いを試してみましょう。

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    go func() {
        defer fmt.Println("Done HelloAndExit goroutine")
        fmt.Println("Start HelloAndExit goroutine")
        HelloAndExit()
        HelloAndExit()
    }()

    go func() {
        defer fmt.Println("Done HelloAndReturn goroutine")
        fmt.Println("Start HelloAndReturn goroutine")
        HelloAndReturn()
        HelloAndReturn()
    }()

    time.Sleep(1 * time.Second)
}

func HelloAndExit() {
    fmt.Println("HelloAndExit")
    runtime.Goexit()
}

func HelloAndReturn() {
    fmt.Println("HelloAndReturn")
    return
}

// Start HelloAndReturn goroutine
// HelloAndReturn
// HelloAndReturn
// Done HelloAndReturn goroutine

// Start HelloAndExit goroutine
// HelloAndExit
// Done HelloAndExit goroutine

このように、 return の場合では後続処理が走り、 runtime.Goexit では実行された時点で呼び出し元goroutineが終了していることが分かりますね。
こんな風にネストを深くしても、

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    go func() {
        defer fmt.Println("Done")
        Wrapper()
    }()

    time.Sleep(2 * time.Second)
}

func Wrapper() {
    fmt.Println("Wrapper")
    HelloAndExit()
    time.Sleep(1 * time.Second)
    fmt.Println("Wake up")
}

func HelloAndExit() {
    fmt.Println("HelloAndExit")
    runtime.Goexit()
}

// Wrapper
// HelloAndExit
// Done

runtime.Goexit が呼び出された時点で、Wrapperの処理を打ち切ってgoroutineが終了されています。

まとめ

面白い機能なんですが、利用シーンを考えてみるとなかなか難しくて、「これだ!」と思うような実践的な実装が思いつきませんでした・・・
実装例を探そうとしても、そもそもGoexitを使っている人が居なかっt(ry

こんな感じで使うと良いよっていうのを、どなたかデキルGopherな方が提示してくれるのを待つしか無い・・・

「Go言語でつくるインタプリタ」を読んだ

読んだので思ったことを書く。

Go言語でつくるインタプリタ
Thorsten Ball
オライリージャパン
売り上げランキング: 9,186

どんな感じの本なのさ?

詳しくはこちら

Writing An Interpreter In Goを読んだ | SOTA

本書は、
字句解析器→構文解析器(トップダウン)→評価器→さらなるインタプリタの改良→【付録】マクロの実装
という感じの構成になってました。

昔、Rubyで小さなインタプリタを書いた自分にとってはPratt構文解析器の説明など学びがある感じの本でした。

めちゃくちゃ丁寧な説明、なのに300Pもない

この本、めちゃくちゃ解説が丁寧です。特にGo以外の事前知識が無くても大丈夫なくらいです。
そこまで丁寧に書かれてるのに総ページ数が300もいかないという薄さ。サックリ読めてしまう薄さです。
1000P超えるdragon bookと比べると、お手軽さが段違いですね。

丁寧は丁寧なんですが、バカ真面目に書かれた本でもなく、合間合間に小気味なジョークを挟んでくるので読んでいてあまり飽きもきません。

省くところは省く簡潔さ

本当にインタプリタを作ることだけにフォーカスしているため、余計なところは出来る限り省いています。
そして、それを何故省くのか?といったところも勿論説明してくれているため、余計な疑問を持たずに読み進められる。

TDD形式のコーディング

きちんと事前にテストを書いてから進める進行なので、どういった挙動をするプログラムを組めばいいのか?というのが事前に分かりやすい。

おわり

色々な実装を削っても、この規模のインタプリタを作るの大変なんだなぁ・・・という感想。
いつかインタプリタを作りたくなった時に再び本書を紐解こうと思う。

Cassandraが墓石まみれになり死

TombStoneが出てこなくて、ついTotemと言ってしまった私です。
今回はTombStoneで死んだ話をします。

状況

Apache SparkでCassandraに向けて、1000万件程のデータ を流し込む処理を実行していたのですが、途中で処理が完全に止まってしまい死にました。

何故に

何ぞ?と思い、 cqlsh でQuery投げてみましたが、何を投げても以下のようなエラーが。

cassandra.ReadFailure: code=1300 [Replica(s) failed to execute read] message="Operation failed - received 0 responses and 1 failures" info={'required_responses': 1, 'received_responses': 0, 'failures': 1, 'consistency': 'ONE'}

ほげ〜〜〜〜となって調べてみると、こんな記事が。

stackoverflow.com

You are exceeding the tombstone_failure_threshold. It defaults to 100'000.

いやいや、そんなTombStoneが10万もおっ立てるほど削除処理回してへんで、となっておもむろに nodetool cfstats hoge.fuga を実行したところ完全にTombStoneが10万超えてました・・・

なんでやねん

opencredo.com

この記事読んで知ったんですが、columnのnullでもTombStoneって立つんですね・・・
たしかにテーブル内のデータを見ると、nullのデータがちらほらあって色々と納得。

どうすんねん

とりあえずTombStoneを掃除するために、Compactionの設定を変えることにしました。
Tableの WITH COMPACTION をこんな感じに。

'tombstone_compaction_interval': '60' :TombStoneのCompaction間隔秒数(デフォルトは1日)
'tombstone_threshold': '0.01' :Compaction実行のしきい値となる、すべての列とTombStoneが含まれた列の比率

docs.datastax.com

上限値も変えようかなとは思ったんですが、それやっちゃうと上限値の最適値を探る作業が入っちゃうので面倒だなと思いCompaction設定をいじりました。

まとめ

TombStoneで死ぬこともあるので気を付けよう。

参考資料

Cassandra導入事例と現場視点での苦労したポイント cassandra summit2014jpn

Null bindings on prepared statements and undesired tombstone creation

Common Problems with Cassandra Tombstones - OpenCredo

How is data deleted?

cassandra.yaml構成ファイル

Compaction subproperties

saramaでAsyncProducerを使う時に気を付けなければいけないたったひとつのこと

AsyncCloseを使うな


どういうこと?

AsyncProducerには

  • AsyncClose
  • Close

の2種類がある。

なんの違いが?

それぞれのコメント読みましょ。

AsyncClose

AsyncClose triggers a shutdown of the producer. The shutdown has completed when both the Errors and Successes channels have been closed. When calling AsyncClose, you must continue to read from those channels in order to drain the results of any messages in flight.


AsyncCloseがプロデューサのシャットダウンをトリガします。 エラーと成功の両方のチャネルが閉じられたときにシャットダウンが完了しました。 AsyncCloseを呼び出すときは、飛行中のメッセージの結果を排除するために、それらのチャネルからの読み込みを続ける必要があります。

Close

Close shuts down the producer and waits for any buffered messages to be flushed. You must call this function before a producer object passes out of scope, as it may otherwise leak memory. You must call this before calling Close on the underlying client.


Closeはプロデューサをシャットダウンし、バッファされたメッセージがフラッシュされるのを待ちます。 プロデューサオブジェクトがスコープの外に出る前に、この関数を呼び出す必要があります。 基になるクライアントでCloseを呼び出す前にこれを呼び出す必要があります。

よくわからん🤔

コードを読もう

AsyncClose

func (p *asyncProducer) AsyncClose() {
    go withRecover(p.shutdown)
}

withRecover()shutdown というメソッドを引数にとって実行しています。

func withRecover(fn func()) {
    defer func() {
        handler := PanicHandler
        if handler != nil {
            if err := recover(); err != nil {
                handler(err)
            }
        }
    }()

    fn()
}

単純にpanicしたときにrecoverしてerrorを吐き出してる感じのdeferをつけて、引数のメソッドを実行しているだけですね。

func (p *asyncProducer) shutdown() {
    Logger.Println("Producer shutting down.")
    p.inFlight.Add(1)
    p.input <- &ProducerMessage{flags: shutdown}

    p.inFlight.Wait()

    if p.ownClient {
        err := p.client.Close()
        if err != nil {
            Logger.Println("producer/shutdown failed to close the embedded client:", err)
        }
    }

    close(p.input)
    close(p.retries)
    close(p.errors)
    close(p.successes)
}

で、肝心のshutdown処理なのですが、一度シャットダウンのための値を p.input に渡して、Producer上で実行中のメッセージの受け渡しが終わり次第、Producerをcloseしようとしています。

Close

func (p *asyncProducer) Close() error {
    p.AsyncClose()

    if p.conf.Producer.Return.Successes {
        go withRecover(func() {
            for range p.successes {
            }
        })
    }

    var errors ProducerErrors
    if p.conf.Producer.Return.Errors {
        for event := range p.errors {
            errors = append(errors, event)
        }
    } else {
        <-p.errors
    }

    if len(errors) > 0 {
        return errors
    }
    return nil
}

1行目から AsyncClose 呼んでますね・・・
その後は、brokerへのメッセージ送信の成否を示す SuccessesErrors をClose側でhandlingしています。
p.successesp.errors で値を受け取り続けているのは、最初の説明で書いてあった "AsyncCloseを呼び出すときは、飛行中のメッセージの結果を排除するために、それらのチャネルからの読み込みを続ける必要があります。" に従っているわけですね。
この飛行中ってのはイコール、メッセージの送信中って意味で、それを表しているのが inFlight というわけです。

inFlight 自体は sync.WaitGroup で、メッセージを送信する際に Add(1) , 送信し終わったら Done() します。
今回で言うと、 shutdown 処理の際に、安全にシャットダウンするために inFlight を使ってますね。

まとめ

というわけで、 AsyncClose のみを使用するときには結局 Close でやっているような処理を実装しなくてはならないので、それなら Close 使った方がいいよ。という結論に至りました。
なら何故 AsyncClose は外部からも読めるようになってるんだ?という感じもするかもしれませんが、 p.successes とかは処理内容がNoopなので、自分で色々やりたい人はどうぞ、っていうキャパシティを残してるんですかね?🤔

OS Xのnetstatに立ち向かう

ある時、手持ちのMacからTIME_WAITが出てるか調べようと netstat を叩いた所、想定していた挙動と全く違ったので、メモ代わりに書いておく。

何が違ったのか?

  • -p オプションの挙動
  • TIME_WAITの扱い

-p オプションの挙動

これは完全にLinuxBSD系であるOSXの違いなんですが、 -p の扱いが両者で全く違います。
完全にPIDと実行プログラム名が出ると思って -p 付けて叩いたら

ʕ ◔ϖ◔ʔ % netstat -anp                                                                                                                                                 (net-tools)-[master]
netstat: option requires an argument -- p
Usage:  netstat [-AaLlnW] [-f address_family | -p protocol]
    netstat [-gilns] [-f address_family]
    netstat -i | -I interface [-w wait] [-abdgRtS]
    netstat -s [-s] [-f address_family | -p protocol] [-w wait]
    netstat -i | -I interface -s [-f address_family | -p protocol]
    netstat -m [-m]
    netstat -r [-Aaln] [-f address_family]
    netstat -rs [-s]

なんて出たもので。BSD系では -pプロトコル指定のオプションなんですね。
なので、実行プログラム名とか見たいって場合は netstat よりも lsof 使った方がいいのかもしれません。

TIME_WAITの扱い

OS Xの場合、netstat ではTIME_WAITが表示されません。
なので、確認したい場合は

ʕ ◔ϖ◔ʔ % sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 0

を実行しましょう。

おわりに

OS XがTIME_WAITを頑なに隠してるのが謎・・・
どっかのファイルにTCPコネクションを吐いていてくれたら、なんとかツール組んで出来るんじゃないかなーと思ったり。

あと、調査する中で面白かったのはnetstatアマチュア無線パケット通信プロトコルであるAX25とかをサポートしてたりとか、謎な知識が知れたことですね。
今回、netstatのコードも読んだんですが一度読んでみると様々な発見があるのでお薦めです。

github.com

参考資料

blog.kamipo.net

OSX: Where are my TIME_WAIT ?!