Yabu.log

ITなどの雑記

GolangでUDPサーバー&クライアントを作成してWiresharkで検証する

Goならわかるシステムプログラミング第7章 UDPソケットを使ったマルチキャスト通信に書かれている内容をWiresharkを使ったり、デバッグでライブラリ(netパッケージ中心)のソースを読んだりして検証してみました

サーバー側

  • net.ListenPacket()の戻り値のnet.PacketConnインターフェースを使う
    • TCPの時のnet.Listenerインタフェースのようなクライアントを待つインタフェースではない
    • ReadFromメソッドで受信内容を読み取る
      • 引数で渡しているbufferに通信内容を格納している
      • remoteAddressに通信して来たクライアントのアドレスを受け取っている
      • bufferの中身はあくまでudpパケットの中身しか入らない
        • だからIPヘッダの情報などはReadだけでは読み取れず、ReadFromで別途IPアドレスなどを取得する必要がある
package main

import (
    "fmt"
    "net"
)

func main() {
    fmt.Println("Server is runnign at localhost:8888")
    conn, err := net.ListenPacket("udp", "localhost:8888")
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    buffer := make([]byte, 1500)
    for {
        length, remoteAddress, err := conn.ReadFrom(buffer)

    if err != nil {
            panic(err)
        }

        fmt.Printf("Received from %v: %v\n",remoteAddress, string(buffer[:length]))

    _, err = conn.WriteTo([]byte("Hello from Server"), remoteAddress)

        if err != nil {
            panic(err)
        }

    }
}

ReadFrom関数の実装が気になったので確認してみた

//udpsock.go
func (c *UDPConn) ReadFrom(b []byte) (int, Addr, error) {
    if !c.ok() {
        return 0, nil, syscall.EINVAL
    }
    n, addr, err := c.readFrom(b)
    if err != nil {
        err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
    }
    if addr == nil {
        return n, nil, err
    }
    return n, addr, err
}
//udpsock_posix.go
func (c *UDPConn) readFrom(b []byte) (int, *UDPAddr, error) {
    var addr *UDPAddr
    n, sa, err := c.fd.readFrom(b)
    switch sa := sa.(type) {
    case *syscall.SockaddrInet4:
        addr = &UDPAddr{IP: sa.Addr[0:], Port: sa.Port}
    case *syscall.SockaddrInet6:
        addr = &UDPAddr{IP: sa.Addr[0:], Port: sa.Port, Zone: zoneCache.name(int(sa.ZoneId))}
    }
    return n, addr, err
}
//fd_unix.go
func (fd *netFD) readFrom(p []byte) (n int, sa syscall.Sockaddr, err error) {
    n, sa, err = fd.pfd.ReadFrom(p)
    runtime.KeepAlive(fd)
    return n, sa, wrapSyscallError("recvfrom", err)
}
//syscall_unix.go
func Recvfrom(fd int, p []byte, flags int) (n int, from Sockaddr, err error) {
    var rsa RawSockaddrAny
    var len _Socklen = SizeofSockaddrAny
    if n, err = recvfrom(fd, p, flags, &rsa, &len); err != nil {
        return
    }
    if rsa.Addr.Family != AF_UNSPEC {
        from, err = anyToSockaddr(&rsa)
    }
    return
}
//zsyscall_darwin_amd64.go
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT

func recvfrom(fd int, p []byte, flags int, from *RawSockaddrAny, fromlen *_Socklen) (n int, err error) {
    var _p0 unsafe.Pointer
    if len(p) > 0 {
        _p0 = unsafe.Pointer(&p[0])
    } else {
        _p0 = unsafe.Pointer(&_zero)
    }
    r0, _, e1 := Syscall6(SYS_RECVFROM, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(flags), uintptr(unsafe.Pointer(from)), uintptr(unsafe.Pointer(fromlen)))
    n = int(r0)
    if e1 != 0 {
        err = errnoErr(e1)
    }
    return
}

このSyscall6の中身は見れなかった。名前的にもこれはネットワーク系のシステムコールなのでしょうか。 という訳で、コールスタックは私の場合は

syscall.recvfrom (/usr/local/go/src/syscall/zsyscall_darwin_amd64.go:146)
syscall.Recvfrom (/usr/local/go/src/syscall/syscall_unix.go:262)
internal/poll.(*FD).ReadFrom (/usr/local/go/src/internal/poll/fd_unix.go:215)
net.(*netFD).readFrom (/usr/local/go/src/net/fd_unix.go:208)
net.(*UDPConn).readFrom (/usr/local/go/src/net/udpsock_posix.go:47)
net.(*UDPConn).ReadFrom (/usr/local/go/src/net/udpsock.go:121)
main.main (/User/Study/go-sys/chap7/udp_server/udpServer.go:18)

となった。

クライアント側

package main

import (
    "fmt"
    "net"
)

func main() {
    conn, err := net.Dial("udp4", "localhost:8888")
    if err != nil {
        panic(err)
    }

    defer conn.Close()
    fmt.Println("Sending to server")
    _, err = conn.Write([]byte("Hello from client"))
    if err != nil {
        panic(err)
    }

    fmt.Println("Receiving from server")
    buffer := make([]byte, 1500)
    length, err := conn.Read(buffer)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Received: %s\n", string(buffer[:length]))
}

net.Dialでnet.Connインターフェース(実装はnet.UDPConn)を取得してそちらにWrite,Readで送受信ができる。 とてもシンプルですね。

キャプチャ結果

この2つのソースで送受信それぞれ1つづつしかパケットが飛んでいない。

f:id:yuyubu:20180917005441p:plain

UDPはとてもシンプルですが、TCPが信頼性を担保するためにいかに頑張ってくれているか、というのが実感できるのではないでしょうか。

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング

Golangで書かれたWebサーバーでHTTP/1.1のkeep-aliveを検証する

HTTP/1.1にはKeep-Aliveという通信方法があります。HTTP/1.0の頃は1セットの通信が終わるたびに接続・切断処理が入っていたので非効率だったため、複数のリクエストが来た場合にコネクションを使い回す機能になります。 今回はGolangで書かれたサーバーを実行してWiresharkで通信の様子をキャプチャしています

コード

keep-aliveの実装コードの一部です。Requestを終わりまで読み取り、コネクションを再利用します。

//Accept後のソケットでなんども応答を返すためにループ
for {
  //タイムアウトを設定
  conn.SetReadDeadline(time.Now().Add(5 * time.Second))
  //リクエストを読み込む
  request, err := http.ReadRequest(bufio.NewReader(conn))
  if err != nil {
    //タイムアウトもしくはソケットクローズ時は終了
    //それ以外はエラーにする
    neterr, ok := err.(net.Error) //ダウンキャスト
    if ok && neterr.Timeout() {
      fmt.Println("timeout")
      break
    } else if err == io.EOF {
      break
    }
    panic(err)
  }
  //リクエストを表示
  dump, err := httputil.DumpRequest(request, true)
  if err != nil {
    panic(err)
  }
  fmt.Println(string(dump))
  content := "Hello World\n"

  //レスポンスを書き込む
  //HTTP/1.1かつ,ContentLengthの設定が必要
  response := http.Response{
    StatusCode:    200,
    ProtoMajor:    1,
    ProtoMinor:    1,
    ContentLength: int64(len(content)),
    Body: ioutil.NopCloser(
      strings.NewReader(content)),
  }
  response.Write(conn)
}

通信内容

curlで2回リクエストを投げています。成功すると2回hello worldが帰ってきます

$ curl localhost:8888 localhost:8888
hello world
hello world

キャプチャ結果

続いてWiresharkのキャプチャ結果を貼りますちょっと分かり辛いかもしれませんが、

  • 「接」がTCPの接続処理(3Way HandShake)です
  • 「通」がデータの通信処理です
  • 「断」がTCPの切断処理です(FIN)

f:id:yuyubu:20180915230426p:plain
図1.keep aliveなしの場合

f:id:yuyubu:20180915230429p:plain
図2.keep aliveありの場合

keep aliveが無い方は2回のリクエストに対してそれぞれ接続、切断処理を行なっていますが、keep aliveがある方はコネクションを使いまわして全部で1度しか接続、切断処理をしていないことがわかります。

Golangで作成したWEBサーバーのTCP通信(HTTP GET)をWiresharkで検証

ソースコード

サーバー側のソースコードです8888ポートでクライアントからの送信を待ち受けます

package main

import (
    "bufio"
    "fmt"
    "io/ioutil"
    "net"
    "net/http"
    "net/http/httputil"
    "strings"
)

func main() {
    listener, err := net.Listen("tcp", "localhost:8888")
    if err != nil {
        panic(err)
    }
    fmt.Println("Server is running at localhost:8888")
    for {
        conn, err := listener.Accept()
        if err != nil {
            panic(err)
        }
        go func() {
            //fmt.Printf("Accept %v\n",conn.RemoteAddr())
            //リクエストを読みこむ
            request, err := http.ReadRequest(
                bufio.NewReader(conn))
            if err != nil {
                panic(err)
            }
            dump, err := httputil.DumpRequest(request, true)
            if err != nil {
                panic(err)
            }
            fmt.Println(string(dump))

            //レスポンスを書き込む
            response := http.Response{
                StatusCode: 200,
                ProtoMajor: 1,
                ProtoMinor: 0,
                Body: ioutil.NopCloser(
                    strings.NewReader("hello world\n")),
            }
            response.Write(conn)
            conn.Close()
        }()
    }
}

クライアントの方は後述のcurlを利用します

通信方法

サーバーの方で8888のポートで受信を受け付けているのでこちらにcurlで通信を試みています

$ curl localhost:8888
hello world

サーバーの方でリクエストを受け取るとhello worldという文字列を返すようにしているのでコマンドを実行するとこちらの文字列が帰ります

localhostに対する送受信をキャプチャする準備

yuyubu.hatenablog.com

こちらの記事を参考にLoopback:Io0を選択しています

netstatコマンドを利用したTCPコネクションの状態

以下のタイミングでnetstatコマンドを実行してコネクションの状態を確認しました

サーバー起動時

サーバーは起動したタイミングではnetstatコマンドの出力に8888portの情報は含まれていません。

listner.Accept()メソッドに置いたブレイクポイントで処理を止めたタイミング

tcp4      78      0  localhost.ddi-tcp-1    localhost.53315        ESTABLISHED
tcp4       0      0  localhost.53315        localhost.ddi-tcp-1    ESTABLISHED

クライアント側のポート番号として53315が選ばれていることがわかります

なおこの状態でパケットをキャプチャすると以下の情報が得られました。 サーバーがGETを受け取ってOKを返す直前という感じです。

f:id:yuyubu:20180915165701p:plain

接続終了時

netstatコマンドにport8888の出力は含まれませんでした

次の記事でHTTP/1.1のKeep-Aliveを検証するのでこのnetstatの情報の差分は興味深い結果になると思います

キャプチャ結果

Wiresharkを起動しフィルタをtcp.port==8888に設定して通信内容を記録しました。すると下のような通信内容が得られました。

f:id:yuyubu:20180915165058p:plain

解説

  • 色枠1のところでTCPの3way HandShakeをしています
  • 色枠2のところでデータを送受信しています
  • 色枠3のところで切断処理をしています

3Way HandShake

TCPの接続開始を示すこの3つのパケットのやりとりを3 Way HandShakeと言います。

f:id:yuyubu:20180915165237p:plain

データ送受信

f:id:yuyubu:20180915165250p:plain

  • データ送信直後に、受信側が送信するパケットのACKがデータを受け取った分だけ増えていることが確認できます。
  • データ受信確認の後、送信側から送ったもので受信が確認できたオクテット数だけSEQが増えていることが確認できます

切断

f:id:yuyubu:20180915165307p:plain

サーバーから送ったFINのパケットに対してクライアントもFINのパケットを送り返しています。 こちらは確認応答のたびにACKを+1しています

感想

キャプチャ結果冒頭*1の[SYN]パケットを送った後の[RST,ACK]が送り返されて再度SYNパケットを送信している理由がわかりませんでした。 一般的な挙動には見えないのですが。。。これはGoのListnerの実装などを読めばわかるのでしょうか。次回はKeep-Aliveを実験してみたいと思います

参考

  • Goならわかるシステムプログラミング第6章TCPソケットとHTTPの実装
  • コンピュータネットワーク第5版
  • マスタリングTCP/IP入門

*1:①の上に位置する2つのパケット

Wiresharkでlocalhost(127.0.0.1)宛のパケット送受信をキャプチャする方法

「Goならわかるシステムプログラミング」のネットワークプログラミングの章でプログラムの検証をやりたかったのでLoopbackアドレスに対するパケット送受信ができないのか調べてみたところ、日本語ではあまりズバリな解決策*1が出なかったため英語で調べると解決した。

wireshark how to capture localhost」でググったところ以下の投稿を参考にwiresharkwikiにたどり着いた

https://stackoverflow.com/questions/5847168/wireshark-localhost-traffic-capture

https://wiki.wireshark.org/CaptureSetup/Loopback

Summary: you can capture on the loopback interface on Linux, on various BSDs including Mac OS X, and on Digital/Tru64 UNIX, and you might be able to do it on Irix and AIX, but you definitely cannot do so on Solaris, HP-UX, or Windows.

上記引用にある通り、WiresharkNIC選択一覧に出ているものの中には、loopbackのパケットをキャプチャできるものがある。

f:id:yuyubu:20180913133347p:plain
Loopback:Io0を利用した

今回はこちらを選択することで無事localhost宛のパケットをキャプチャすることができた

f:id:yuyubu:20180913133516p:plain
TCPハンドシェイクを含むHTTP通信をキャプチャした様子

ただしmacOSではこの方法が使えたが

では使えないので該当OSユーザーの方は別の方法を模索してください

*1:ルーティングテーブルを書き換えろとか、別のツールを入れろとか

Effective SQL 読書会(5)に参加

参加者が少なかったので木村さんの話がいつもより多く聞けました。 サポートやコンサル、DBMSの制作などをされている方なのでその膨大な知見に毎度驚かされます。

今回は

  • (続き)第2章 プログラム可能性とインデックスの設計
    • 項目17 計算値をインデックスで使用する状況を理解する
  • 第3章 設計を変更できないときはどうするか
    • 項目18 変更できないものはビューを使って単純化する
    • 項目19 ETLを使って非リレーショナルデータを情報に変える
    • 項目20 サマリテーブルを作成して管理する

を読みました

Effective SQL

Effective SQL

  • SQLServerの計算処理はC言語のライブラリを使っている

    • ビルド(32bit or 64bit,win/linux/mac)によって微妙に計算結果が違っていたらしする
    • 最近はOS間の違い(Cのライブラリ?)が少なくなってきている
  • SQLServerANSI_NULLS

過去のSQLServerではNULLが値として扱われていた。 そのためNULL is NULLじゃなくてNULL = NULLでもTrueとなる ANSI_NULLSオプションをオンにするとこのようなおかしな挙動を防げる 最近だとデフォルトで有効化されているので安心。

  • MySqlのTIMESTANP型はタイムゾーンを考慮する
  • 関数ベースのインデックスは決定関数でしか使えない
  • 関数か決定的かどうかは関数定義時に特殊なキーワードを使う
    • DETERMINISTIC句(Oracle)
    • IMMUTABLE句(PostgreSQL)
    • その関数が決定的かどうかはコンパイル時にわからない。
      • インデックスに利用する関数が決定的であることはプログラマが責任を持たなければならない。
  • SQLliteの専門的な知見を持っている日本人はかなり少ない
    • らしいです
  • ETLツール
    • 静的なデータファイルをDBにインポートするツール
    • それらしいものは何度か触ったことがあるが、ETL(Extract, Transform, Load)という名称は知らなかった
    • 商用のものだとData Spiderなんかも有名
    • PostgreSQLではTALEND(OSS)なんかが利用可能
  • 計算量の大きいデータをキャッシュする仕組み
    • マテリアライズドビュー
    • サマリーテーブル
      • サマリーテーブルは有用だがあまり活用されないらしい
  • MySQLのイベントスケジューラー

  • Raspberry Pi

    • 最近は64bitのOSもある
      • 最近までARMは32bitのOSが殆どだったらしい
    • 並列計算にも利用されている

ラズパイは正直、あまり大規模ではない組み込み用途か教育用でしか利用できないと思っていたが、 分散システムのようなものを作ってパフォーマンスがシビアな状況でも利用しているらしい

次回は「項目21 UNIONを使って非正規化データをアンピボットする」から

プログラマのためのSQL 読書会(26)に参加

今回もやたら難解なクエリが多く、輪読がはかどらず、もくもく会になってしまう場面が多かったです*1

プログラマのためのSQL 第4版

プログラマのためのSQL 第4版

検証環境:

  • MySQL:Ver 14.14 Distrib 5.7.22, for osx10.13 (x86_64) using EditLine wrapper
  • PostgreSQL:PostgreSQL 10.1 on x86_64-apple-darwin13.4.0

FETCH FIRST ROWS

fetch first 3 rowsを使って結果から「N行までのデータ」という取り方をすることができます。 それぞれのDBMS実装依存でTOPやLIMITなども使えますが、一応SQL標準(ANSI 2008)の行数制限指定はこちらですので、 標準SQLにこだわる方はこちらの存在を知っておいた方が良いです。

list_seq  | cat_string
----------+------------
        1 | abc
        2 | bcd
        3 | cde
        4 | def
        5 | efg
        6 | fgh
        7 | ghi
SELECT * FROM make_string FETCH FIRST 3 ROWS ONLY;
 list_seq | cat_string
----------+------------
        1 | abc
        2 | bcd
        3 | cde
(3 rows)

limit句とfetch first句の対応状況です。

SQL-対応DB Oracle MySQL PostgreSQL DB2 SQLServer
limit × ×
fetch first

SQLServerでは標準化される前はTOPという構文を使っていたようです。

p.547のクエリが動くのはPostgresだけ

らしいです

--▼P.547


CREATE TABLE Make_String
(list_seq INTEGER NOT NULL PRIMARY KEY,
 cat_string VARCHAR NOT NULL);--MySQLで試す場合はVARCHARのサイズ指定が必要
---

INSERT INTO Make_String
VALUES (1, 'abc'), (2, 'bcd'), (3, 'cde'),
       (4, 'def'), (5, 'efg'), (6, 'fgh'), (7, 'ghi');

---

WITH RECURSIVE
String_Tail(list_seq, cat_string)
AS
(SELECT list_seq, cat_string
   FROM Make_String
  WHERE list_seq > 1),
String_Head(list, max_list_seq, list_seq)
AS
(SELECT cat_string, 1, list_seq
   FROM Make_String
UNION ALL
SELECT String_Head.list || ', ' || String_Tail.cat_string,
       String_Head.max_list_seq + 1,
       String_Head.list_seq
  FROM String_Tail, String_Head
 WHERE String_Tail.list_seq = String_Head.max_list_seq + 1)
SELECT list
  FROM String_Head
 WHERE list_seq = 1
   AND max_list_seq = (SELECT MAX(list_seq)
                         FROM Make_String);

Postgresで試した結果

list                
-----------------------------------
abc, bcd, cde, def, efg, fgh, ghi
(1 row)

確かにMySQLではエラーになった。

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'RECURSIVE
String_Tail(list_seq, cat_string)
AS
(SELECT list_seq, cat_string
   F' at line 1

p551のクエリに関して

CREATE TABLE Performance
(portfolio_id CHAR(7) NOT NULL,
 execute_date DATE NOT NULL,
 rate_of_return DECIMAL(13,7) NOT NULL);

---

INSERT INTO Performance
VALUES ('001', '2006-01-06', 0.5),
       ('001', '2006-01-07', 0.3),
       ('001', '2006-01-08', 0.6),
       ('001', '2006-01-09', 0.8),
       ('001', '2006-01-10', 0.1);


---


CREATE TABLE BigPi
(execute_date DATE NOT NULL,
 day_1 INTEGER NOT NULL,
 day_2 INTEGER NOT NULL,
 day_3 INTEGER NOT NULL,
 day_4 INTEGER NOT NULL,
 day_5 INTEGER NOT NULL);

---

INSERT INTO BigPi
VALUES ('2006-01-06', 1, 0, 0, 0, 0),
       ('2006-01-07', 0, 1, 0, 0, 0),
       ('2006-01-08', 0, 0, 1, 0, 0),
       ('2006-01-09', 0, 0, 0, 1, 0),
       ('2006-01-10', 0, 0, 0, 0, 1);
SELECT portfolio_id,
       (SUM((1.00 + P1.rate_of_return) * M1.day_1) *
        SUM((1.00 + P1.rate_of_return) * M1.day_2) *
        SUM((1.00 + P1.rate_of_return) * M1.day_3) *
        SUM((1.00 + P1.rate_of_return) * M1.day_4) *
        SUM((1.00 + P1.rate_of_return) * M1.day_5)) AS product
  FROM Performance AS P1, BigPi AS M1
 WHERE M1.execute_date = P1.execute_date
  AND P1.execute_date BETWEEN '2006-01-06' AND '2006-01-10'
 GROUP BY portfolio_id;

途中結果がsumを経由しているので値として0になっているのか、nullが含まれているから0担っているのかわからない、という問題があり対策のCASE式として以下のものが書籍中に紹介されていた

CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_N)=0.00
  THEN CAST(NULL AS DECIMAL(13,7))
  ELSE SUM((1.00 + P1.rate_of_return) * M1.day_N)
END

この対策は結局P1.rate_of_returnに-1のものが入ればNULLが伝搬して結果がNULLになってしまうのでは?ということを主張しました(が検証しきれなかった)

上記を適用したクエリと全体は以下のようになる

SELECT portfolio_id,
       (       
       CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_1)=0.00
         THEN CAST(NULL AS DECIMAL(13,7))
         ELSE SUM((1.00 + P1.rate_of_return) * M1.day_1)
       END *

       CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_2)=0.00
         THEN CAST(NULL AS DECIMAL(13,7))
         ELSE SUM((1.00 + P1.rate_of_return) * M1.day_2)
       END *

       CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_3)=0.00
         THEN CAST(NULL AS DECIMAL(13,7))
         ELSE SUM((1.00 + P1.rate_of_return) * M1.day_3)
       END *

       CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_4)=0.00
         THEN CAST(NULL AS DECIMAL(13,7))
         ELSE SUM((1.00 + P1.rate_of_return) * M1.day_4)
       END *

       CASE WHEN SUM((1.00 + P1.rate_of_return) * M1.day_5)=0.00
         THEN CAST(NULL AS DECIMAL(13,7))
         ELSE SUM((1.00 + P1.rate_of_return) * M1.day_5)
       END
        ) AS product
  FROM Performance AS P1, BigPi AS M1
 WHERE M1.execute_date = P1.execute_date
  AND P1.execute_date BETWEEN '2006-01-06' AND '2006-01-10'
 GROUP BY portfolio_id;

とりあえず実行してみたところ結果は変わらない

portfolio_id |                product                
-------------+---------------------------------------
001          | 6.17760000000000000000000000000000000
(1 row)

適当にPerformanceテーブルからレコードを抜くとnullになった

portfolio_id | product
-------------+---------
001          |        
(1 row)

もちろん対策前のクエリでは結果は0になります

portfolio_id |                product                
-------------+---------------------------------------
001          | 0.00000000000000000000000000000000000
(1 row)

結局P1.rate_of_returnに-1のものが入ればNULLが伝搬して結果がNULLになってしまうのでは?

上記の自分の疑問が検証できそうなデータを作成しました

DELETE FROM Performance;
INSERT INTO Performance
VALUES ('001', '2006-01-06', 0.5),
       ('001', '2006-01-07', 0.3),
       ('001', '2006-01-08', 0.6),
       ('001', '2006-01-09', -1),--これ!
       ('001', '2006-01-10', 0.1);

するとやはり結果はNULLになりました

 portfolio_id | product
--------------+---------
 001          |        
(1 row)

ただし利率が-1になるような金融というか口座は存在し得ないと思うのでこちらの不具合は多分起こらないかも。 というか普通はこの辺の処理はホスト側の言語でやると思います。

感想

特に本書中で触れられていませんが、BigPiの中身のデータのような感じで 単位行列のようなデータを利用した変態なクエリが多かった印象です。

セルコの他の本でグラフ理論?を扱っているものがありましたが、 線形代数(行列)を使ったクエリ、テーブル設計、RDBMSの実装などもあればさらに変な使い方ができて面白いかも知れません。

*1:おそらく自分が出たことのある会で一番進捗が悪かったと思います

第20回横浜Go読書会に参加

初参加です。

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング

Goを学び始めた動機などは

yuyubu.hatenablog.com

にも書いていますが、インタプリタの本でGoのコードをちょっとみたのが大きかったかなと思います。*1

結局先日からは章末の連取問題を解くのに必死で全然読み進められていません。(p146で止まっている) 会場に向かう電車の中で軽く流し読みをして挑みました。

今回は13章のGo言語と並列処理の240ページからでした

Go1.11

Go1.11が近日発表された、ということで、リリースノートを軽く読みました。

https://golang.org/doc/go1.11

Webassemblyサポート

サンプルを試したところ、ChromeのコンソールにHello Worldが出せたらしい

Goのデバッガ

最適化したコードでもデバッグ情報がかなり出るようになった

Go fmtが少々変わる

古いバージョンのGoで静的解析が通っていたソースが 今回のバージョンでフォーマットの違いで警告が出る恐れがある

  • CIのバージョンが違うと怒られる可能性がある

Go2

まだ仕様は出ていないが、軽くディスカッションされている

  • generics
  • エラー処理に関して重要な変更が2件

Go 1.12は半年後くらい。 いつ出るかわからない

mutexはgorutin間で通信する時に重要

書き込みが1つだけであっても、 他のスレッド(gorutin)から「書いた」値を読めるように書き込みの完了までをロックする必要がある。 *2*3

メモリモデルは

https://golang.org/ref/mem#tmp_5

が詳しい

重い処理をマルチスレッドで実行する場合はCPUの物理的な個数分のプロセスに分解するとパフォーマンスが良い

Goではruntime.NumCPU()でCPUの個数を取得できるが、こちらで確認できるのは物理CPUの数ではなく、 論理CPU の数であるため注いが必要。 本書に記載されている通り、SMT(Simultaneous Multi-Threading)機能などにより、CPUのコア数が多く評価されることがある。 自分の環境で試したところ、デュアルコアであるにも関わらず、4が帰ってきた。

Goでは並列を保証しない。

平行だけ。これは各CPUに割り当てるプロセス「プロセス」は作れても 空いてる新規にプロセスを割り当てるCPUを増やすなどの動的な制御をGo内だけで処理できない。

実行じにGOMAXPROCの環境変数で実行時に指定するか、OSのスケジューラによる割り当てに任せる。

p242で配列が1000万件あったら,という話題になった

package main

import (
    "fmt"
    "time"
)

func main() {
    tasks := []string{
        "cmake ..",
        "cmake. --build Release",
        "cpack",
    }
    for _, task := range tasks {
        go func() {
            fmt.Println(task)
        }()
    }
    time.Sleep(time.Second)
}
  • 上記のコードは動機をとっていないので最初のgoroutineの実行時にfor文が周りきった状態になり最後の要素が3回出力される、という悪い見本コードです.
    • ちなみに動機をSleepを使って取るのもあまり良い手本ではありません
  • 流石に1万もあれば最後ではなく、最初の要素が出るでしょう、という話になった
  • 結局はスケジューラーによるのでは?

実験してみました。まずはサンプルコードのまま3件でそのまま動かしました。

cpack
cpack
cpack

本書の記載通り、最後の要素であるcpackが帰ってきました

ではsliceの中身を100個に増やしてみましょう

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup

    wg.Add(100)

    tasks := []string{

        "1",
        "2",
        "3",
        "4",
        "5",
        "6",
    ...(略)
        "99",
        "100",
    }
    for _, task := range tasks {
        go func() {
            fmt.Println(task)
            wg.Done()
        }()
    }
    wg.Wait()
}

このコードで実験してみました

注:前のコードでmain終了を待たせる同期を取っている箇所、time.Sleep(time.Second)の期限で処理が完了しなかったのでWaitGroupを使っています*4

結果

13
6
30
31
41
10
54
54
54
54
54
54
54
54
46
54
54
54
54
62
68
68
68
68
68
68
68
68
78
78
60
62
68
68
68
54
68
70
78
68
78
78
78
78
78
62
78
78
78
78
78
100
100
100
100
78
100
100
78
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
73
100

という出力が得られました。

一番最初/最後の要素が必ず出るという訳ではなさそうです。 結局は内部でどのようにスケジューリングしているか、という話なのでしょうか。

sync/atomicパッケージはgoで書かれていない

このあたりのコードは全てアセンブリで描かれている。

CPUごとに処理をさせたいときはCounting semaphoreを使う

Wikipediaによると、

任意個の資源を扱うセマフォをカウンティングセマフォ、値が0と1に制限されている(ロック/アンロック、使用可能/使用不可の意味がある)セマフォをバイナリセマフォと呼ぶ。後者はミューテックスと同等の機能を持つ。

とのことです

https://ja.wikipedia.org/wiki/セマフォ

セマフォは大学の時にOSの単元で軽く学習しましたが、 あまり覚えておらず、他の分散系の読書会でも出てきたので、どこかで復習しておきたいと思います。

interface{}はVariant型

読んでいるコードのところでわからない箇所があって助け舟を出してもらったりしました。

var count int
pool := sync.Pool{
  New: func() interface{} {
    count++
    return fmt.Sprintf("created:%d", count)
  },
}
pool.Put("manualy added: 1")
pool.Put("manualy added: 2")
fmt.Println(pool.Get())//manualy added: 1
fmt.Println(pool.Get())//manualy added: 2
fmt.Println(pool.Get())//created:1

poolに中身がないとNew:~以降が実行されてcountがインクリメントされた後string型が返されます。

interface{}の部分をStringに変えたり色々試してみましたが、まだこの部分の文法が何をやっているのかよくわかりませんでした。

この辺は時間をかけてGoを学んで吸収したいと思います

感想

Javaの有名な本を沢山訳されていたり、大きな講演会の基調講演をされている柴田さんがやられている勉強会なので、 前から気になっていましたが、敷居が高いかなと思い参加せずにいましたが、この読書会はそこまでハードコアではないと耳にしたので*5、思い切って参加してみました。

今回の範囲ですが、GoはランタイムがOSのような資源管理をしているのが面白いと思いました。

柴田さんが懇親会で「若い人はまともなAPI設計ができない、経験が積めていない」という趣旨のことをなんども仰られていました。

柴田さんが訳されたAPI設計の極意、10%ほど読んで放置になっていたので、こちらも早いうちに読まないといけないと思います。

パタヘネとコンピュータネットワーク(セスぺの試験までには読み終わりたいがこちらもペース的に若干きつい)も平行して読みたいので、勉強することが多くて大変だなと思います。

*1:言語処理系に興味があったので直近で発売された本を片っ端から眺めています。

*2:この本には出てこないらしい。

*3:Goのメモリモデルに関係している

*4:今回の読書会で得た知見を早速使ってみました

*5:柴田さんのJava/Go研修はかなり厳しいようです