20191224のGoに関する記事は17件です。

うちのGoogle Homeをサンタさんが来るのが待ち遠しい子供のようにしてみました

Googleが毎年やっているSanta Trackerを使ってサンタさんが今どこでプレゼントを配っているかを逐次報告してくれるようにしてみました。

https://github.com/usk81/til/tree/master/go/santa-stalker

Santa TrackerのAPI

色々諦めたこと

  • 緯度経度と距離から近くにいるよとかしたかった
    • 近くの定義が難しかったのでやめました
  • 住んでいるエリアにサンタさんが来たら、santa claus is comin to townをSpotifyでながす
    • Google Assistant APIの実装が必要そうなので1日では無理だったのでやめました

コード

main.go
package main

import (
    "context"
    "fmt"
    "time"

    "github.com/ikasamah/homecast"
)

const (
    // 自分の住んでいる場所に一番近いエリア
    myID     = "tokyo"
    santaURL = "https://firebasestorage.googleapis.com/v0/b/santa-tracker-firebase.appspot.com/o/route%2Fsanta_en.json?alt=media&2019b"
)

var jst *time.Location

func init() {
    var err error
    jst, err = time.LoadLocation("Asia/Tokyo")
    if err != nil {
        jst = time.FixedZone("Asia/Tokyo", 9*60*60)
    }
}

func main() {
    tk, err := GetTracker(santaURL)
    if err != nil {
        panic(err)
    }

    fmt.Println("Start stalking")
    for {
        r := tk.Current(time.Now())
        // なんかも同じメッセージを喋り続けられるとうざいので、ステータスが変わるまでキャッシュする 
        // キャッシュされてない場合のみメッセージを話させる
        if !r.IsCache {
            if r.Destination.ID == myID {
                // サンタさん キタ━━━(゚∀゚)━━━!! 
                googlehome("Santa Claus is coming to town!! Santa Claus is coming to town!! Santa Claus is coming to town!!")
                // 自分の街についたら通知をやめる
                break
            } else {
                if r.Status == statusDeliver {
                    // 今サンタさんがプレゼントを配っている場所を通知
                    googlehome(fmt.Sprintf("Santa Claus is in %s, %s\n", r.Destination.City, r.Destination.Region))
                }
            }
        }
        // ループを繰り返す感覚が短すぎるのでちょっとスリープ
        time.Sleep(5 * time.Second)
    }
    fmt.Println("finish")
}

// Google Homeデバイスに任意のメッセージ話させる
func googlehome(msg string) {
    fmt.Println(msg)
    ctx := context.Background()
    devices := homecast.LookupAndConnect(ctx)
    for _, device := range devices {
        device.Speak(ctx, msg, "en")
    }
}
tracker.go
package main

import (
    "encoding/json"
    "net/http"
    "time"
)

const (
    statusTakeOff = "Takeoff"
    statusMove    = "Move"
    statusDeliver = "Deliver"
    statusFinsish = "Finsish"
)

type APIResponse struct {
    Status       string        `json:"status"`
    Language     string        `json:"language"`
    TimeOffset   int           `json:"timeOffset"`
    Fingerprint  string        `json:"fingerprint"`
    Destinations []Destination `json:"destinations"`
}

type Destination struct {
    ID                string   `json:"id"`
    Arrival           int64    `json:"arrival"`
    Departure         int64    `json:"departure"`
    Population        int      `json:"population"`
    PresentsDelivered int      `json:"presentsDelivered"`
    City              string   `json:"city"`
    Region            string   `json:"region"`
    Location          Location `json:"location"`
}

type Location struct {
    Lat float64 `json:"lat"`
    Lng float64 `json:"lng"`
}

type Tracker struct {
    Destinations []Destination
    Cache        Cache
}

type Cache struct {
    ExpiredAt   int64
    Status      string
    Destination Destination
}

type Result struct {
    IsCache     bool
    Status      string
    Destination Destination
}

func GetTracker(uri string) (result *Tracker, err error) {
    resp, err := http.Get(uri)
    if err != nil {
        return
    }
    defer resp.Body.Close()

    var r APIResponse
    if err = json.NewDecoder(resp.Body).Decode(&r); err != nil {
        return
    }

    return &Tracker{
        Destinations: r.Destinations,
    }, nil
}

func (tk *Tracker) SetCache(dest Destination, status string, expiredAt int64) {
    tk.Cache = Cache{
        Destination: dest,
        Status:      status,
        ExpiredAt:   expiredAt,
    }
}

func (tk *Tracker) Current(tt time.Time) Result {
    ts := tt.Unix() * 1000

    c := tk.Cache
    if c.Status != "" && c.ExpiredAt >= ts {
        return Result{
            Status:      c.Status,
            Destination: c.Destination,
            IsCache:     true,
        }
    }

    home := tk.Destinations[0]
    if home.Departure > ts {
        return Result{
            Status:      statusTakeOff,
            Destination: home,
        }
    }

    landing := tk.Destinations[len(tk.Destinations)-1]
    if landing.Arrival < ts {
        return Result{
            Status:      statusFinsish,
            Destination: landing,
        }
    }

    ds := tk.Destinations[1:]
    for _, dest := range ds {
        if dest.Arrival > ts {
            tk.SetCache(dest, statusMove, dest.Arrival-1)
            return Result{
                Status:      statusMove,
                Destination: dest,
            }
        } else if dest.Arrival <= ts && dest.Departure >= ts {
            tk.SetCache(home, statusDeliver, dest.Departure)
            return Result{
                Status:      statusDeliver,
                Destination: dest,
            }
        }
    }
    return Result{
        Status:      statusFinsish,
        Destination: landing,
    }
}

テストをしている時点でサンタさんが日本に来てしまったので、やってみたい人は来年以降楽しんでください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Linux Network NamespaceをGoで操作する

TL;DR

  1. Go言語のgoroutineはdefaultではpreemptiveに動作するOS Threadが切り替わるのでOS Threadに強く紐づくlinuxのnamespace関連の操作を行うときはruntime.LockOSThread()しておく必要がある。1
  2. Go言語でLinuxのnetwork namespaceを操作したい場合はCNIのライブラリを使うのが便利

なんでこんな事してるの?

テナント(200~)毎にVMを用意してると管理やコストが大きいため、アドレス空間が衝突してるテナントに対してHTTP(S)リバースプロキシを提供する仕組みを作ってみようと思った。

Proof of Concept

試しに下記のコードを実行してみる。

package main

import (
        "log"
        "net"
        "net/http"
        "os"
        "runtime"

        "github.com/containernetworking/plugins/pkg/ns"
)

func main() {
        nspath := os.Args[1]
        addr := os.Args[2]
        var err error
        var l net.Listener
        ns.WithNetNSPath(nspath, func(_ ns.NetNS) error {
                l, err = net.Listen("tcp", addr)
                return nil
        })
        runtime.UnlockOSThread()
        if err != nil {
                log.Fatal(err)
        }
        if err := http.Serve(l, nil); err != nil {
                log.Fatal(err)
        }
}

このコード動かすには下記の様にネットワーク的に隔離されたコンテナを用意しておくとよい。

# build binary
go build -o nsproxy nsproxy.go
# setup environment
docker run -d --net none --name pause k8s.gcr.io/pause:3.1
ns=$(docker inspect --format '{{ .NetworkSettings.SandboxKey }}' pause)
# run program
sudo ./nsproxy "$ns" 127.0.0.1:8080 &

このバイナリを動かした場合、HTTPサーバーとして動作しているタイミングではコンテナのnetwork namaspace(以後netnsと表記)には存在していない。

# ls -l /proc/1/ns/net # hostの初期netnsの情報
lrwxrwxrwx 1 root root 0 Dec 24 21:42 /proc/1/ns/net -> 'net:[4026531984]'
# ls -l /proc/$(pgrep nsproxy)/task/*/ns/net # nsproxyプロセスはホストのnetnsに居る
lrwxrwxrwx 1 root root 0 Dec 24 21:42 /proc/4377/task/4377/ns/net -> 'net:[4026531984]'
lrwxrwxrwx 1 root root 0 Dec 24 21:47 /proc/4377/task/4378/ns/net -> 'net:[4026531984]'
lrwxrwxrwx 1 root root 0 Dec 24 21:47 /proc/4377/task/4379/ns/net -> 'net:[4026531984]'
lrwxrwxrwx 1 root root 0 Dec 24 21:47 /proc/4377/task/4380/ns/net -> 'net:[4026531984]'
lrwxrwxrwx 1 root root 0 Dec 24 21:47 /proc/4377/task/4381/ns/net -> 'net:[4026531984]'
lrwxrwxrwx 1 root root 0 Dec 24 21:47 /proc/4377/task/4382/ns/net -> 'net:[4026531984]'
lrwxrwxrwx 1 root root 0 Dec 24 21:47 /proc/4377/task/4393/ns/net -> 'net:[4026531984]'
# ls -l /proc/$(docker inspect --format '{{.State.Pid}}' pause)/task/*/ns/net # containerのnetnsの情報
lrwxrwxrwx 1 root root 0 Dec 24 21:50 /proc/3867/task/3867/ns/net -> 'net:[4026532117]'

しかしながらnsenterを用いてコンテナのnetnsの中に入ると127.0.0.1:8080でhttpサーバーが動作していることが分かる。

# nsenter --net=$(docker inspect --format '{{ .NetworkSettings.SandboxKey }}' pause) bash
# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
# ss -ltn
State           Recv-Q          Send-Q                   Local Address:Port                     Peer Address:Port
LISTEN          0               128                          127.0.0.1:8080                          0.0.0.0:*
# curl http://127.0.0.1:8080 -v
* Expire in 0 ms for 6 (transfer 0x5627619e7f50)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x5627619e7f50)
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.64.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Content-Type: text/plain; charset=utf-8
< X-Content-Type-Options: nosniff
< Date: Tue, 24 Dec 2019 12:58:10 GMT
< Content-Length: 19
<
404 page not found
* Connection #0 to host 127.0.0.1 left intact

たくさんのコンテナからアクセスできるようにしてみる

この方法がどれだけスケールすのか試してみる。
Listenするポートを複数になるように拡張する。

package main

import (
        "log"
        "net"
        "net/http"
        "os"
        "runtime"
        "sync"

        "github.com/containernetworking/plugins/pkg/ns"
)

func main() {
        addr := os.Args[1]
        var ls []net.Listener
        for _, nspath := range os.Args[2:] {
                ns.WithNetNSPath(nspath, func(_ ns.NetNS) error {
                        l, err := net.Listen("tcp", addr)
                        if err != nil {
                                log.Fatal(err)
                        }
                        ls = append(ls, l)
                        return nil
                })
        }
        runtime.UnlockOSThread()
        var wg sync.WaitGroup
        for _, l := range ls {
                wg.Add(1)
                go func(l net.Listener){
                        err := http.Serve(l, nil)
                        if err != nil {
                                log.Print(err)
                        }
                        wg.Done()
                }(l)
        }
        wg.Wait()
}

下記の様に100個ほどコンテナを用意する

# 100個のコンテナを作成する
seq 1000 1999 | xargs -I '{}' -exec docker run -d --net none --name 'pause{}' k8s.gcr.io/pause:3.1
# 100個のコンテナに対してListenする
sudo ./nsproxy 127.0.0.1:8080 $(docker inspect --format '{{.NetworkSettings.SandboxKey}}' pause{100..199} ) &

プロセスの稼働開始直後の状態

$ sudo cat /proc/$(pgrep nsproxy)/status
Name:   nsproxy
Umask:  0022
State:  S (sleeping)
Tgid:   17082
Ngid:   0
Pid:    17082
PPid:   17068
TracerPid:      0
Uid:    0       0       0       0
Gid:    0       0       0       0
FDSize: 128
Groups: 0
NStgid: 17082
NSpid:  17082
NSpgid: 17068
NSsid:  3567
VmPeak:   618548 kB
VmSize:   561720 kB
VmLck:         0 kB
VmPin:         0 kB
VmHWM:     10980 kB
VmRSS:     10980 kB
RssAnon:            6608 kB
RssFile:            4372 kB
RssShmem:              0 kB
VmData:   161968 kB
VmStk:       140 kB
VmExe:      2444 kB
VmLib:      1500 kB
VmPTE:       140 kB
VmSwap:        0 kB
HugetlbPages:          0 kB
CoreDumping:    0
Threads:        7
SigQ:   0/15453
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: ffffffffffc1feff
CapInh: 0000000000000000
CapPrm: 0000003fffffffff
CapEff: 0000003fffffffff
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
NoNewPrivs:     0
Seccomp:        0
Speculation_Store_Bypass:       thread vulnerable
Cpus_allowed:   ffff,ffffffff,ffffffff,ffffffff,ffffffff,ffffffff,ffffffff,ffffffff
Cpus_allowed_list:      0-239
Mems_allowed:   00000000,00000001
Mems_allowed_list:      0
voluntary_ctxt_switches:        6
nonvoluntary_ctxt_switches:     0

開始直後ではRSSが10980 kB程度とかなり軽量であることが分かる。

まとめ

network namespaceを触るのは怖くないので皆さんも触ってみてください。CNIのライブラリ自体は軽量なのでぜひとも実装自体を覗いてみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go言語で始めるgRPC】簡易CRUD+αアプリケーション

はじめに

こちらは、Go4 Advent Calendar 2019 の24日目の記事です。

こんにちは、最近Go言語にはまっているエンジニアです。

この前は別のアドベントカレンダーでGobotを使ってドローンを飛ばす記事を作成しました。
Go言語のフレームワークGobotでドローンを制御してみた。

今回は、最近入門したgRPCについて書いていきたいと思います。

gRPCとは

Googleが開発したRPC呼出プロトコルで、Protocol Buffersを使うことで、REST APIより高速で堅牢な通信を実現できる点が特長です。メッセージはProtocolBuffersを用いて通信を行い、HTTP/2を用いて並列呼出、双方向呼出、ストリーミングなどが可能となっています。

.protoファイルでサーバー側、クライアント側の雛形コードを作成し、それを元に様々な言語のコードを自動生成することができます(今回はGo言語)。この雛形からの自動生成によってAPIの仕様を半ば強制に明文化することが可能になっています。

image.png
https://grpc.io/

さらにgRPCでは、クライアントアプリケーションは別のマシン上のサーバーアプリケーションのメソッドをローカルオブジェクトのように直接呼び出すことができるため、分散型のアプリケーションやサービスを簡単に作成できます。

また、別々の言語を持ったシステム同士をつなぐことも容易です。

gRPCのRPC方式

gRPCは通信方法にHTTP/2を使用いるので、一般的なRPCにおける1Request-1Responseだけでなく、1つのTCPコネクションの中で複数のRequest/Responseをやり取りすることが可能となっており、下記の四つの方式に分かれます

  • Unary(Simple)
 1 request-1 response
  • ServerStreaming
 1 request- N response
  • ClientStreaming
 N request-1 response
  • BidirectionalStreaming
 N request-N response

では今回は基本的な1req-1res方式と、1req-Nresを用いてCRUD+α機能を実装していきます。

環境構築

Go言語の環境が整っている前提でいきます。
gRPCの環境自体はこの2つをインストールするのみです。

gRPCの環境構築

#grpcのインストール
$ go get -u google.golang.org/grpc

#Protocol Bufferのインストール
$ go get -u github.com/golang/protobuf/protoc-gen-go

また、今回はDBとしてMongoDBを利用するので、MongoDBをインストールします。

MongoDBのインストール

https://www.mongodb.com/jp
こちらのサイトからダウンロードとインストールをしてください。

また下記のgithubよりmogo-go-driverをインストールし、パッケージを取得します。(depを使います。)
https://github.com/mongodb/mongo-go-driver

下記コマンドを実行すルコとでインストールが可能です。

dep ensure -add "go.mongodb.org/mongo-driver/mongo"

まずprotoファイルを作成し、APIの基礎となるコードを実装していきます。
今回は簡易ツイッターのようなid,user_id,contentを持ったGweetをCreate,Read,Delete,Updateとgweet全取得の機能を作成していきます。

protoファイル

基本的には型を設定してそれを用いて、リクエストとレスポンスの方式を決めていくため、シンプルなコードになっています。

syntax = "proto3";

package gwitter;

option go_package = "gwitterpb";

//Gweetの型を決めます。各フィールドは型と名称を持ちます。
message Gweet {
    string id = 1;
    string user_id = 2;
    string content = 3;
}

message PostGwitterRequest{
    Gweet gweet = 1;
}

message PostGwitterResponse{
    Gweet gweet = 1;
}

message ReadGwitterRequest{
    string gweet_id = 1;
}

message ReadGwitterResponse {
    Gweet gweet = 1;
}

message UpdateGwitterRequest{
    Gweet gweet = 1;
}

message UpdateGwitterResponse {
    Gweet gweet = 1;
}

message DeleteGwitterRequest {
    string gweet_id = 1;
}

message DeleteGwitterResponse {
    string gweet_id = 1;
}

//全取得の時は特に何も指定しない
message ListGwitterRequest {

}

message ListGwitterResponse {
    Gweet gweet = 1;
}


service GweetService{
    rpc PostGwitter (PostGwitterRequest) returns (PostGwitterResponse);
    rpc ReadGwitter (ReadGwitterRequest) returns (ReadGwitterResponse);
    rpc UpdateGwitter (UpdateGwitterRequest) returns (UpdateGwitterResponse);
    rpc DeleteGwitter (DeleteGwitterRequest) returns (DeleteGwitterResponse); // return NOT_FOUND if not found
    rpc ListGwitter (ListGwitterRequest) returns (stream ListGwitterResponse);
}

protoファイルを作成後、下記コマンドを叩くだけで、各メッセージ型に対する型を含んだ .pb.go ファイルが作成されます。

protoファイルから雛形コードの生成

次に上記のprotoファイルからコードを生成していきます。
以下のコマンドを叩いてください。

protoc calculator/proto/calculator.proto --go_out=plugins=grpc:.

長いのでここでは割愛しますが、ファイルが作成されます。文頭にコメントで「DO NOT EDIT」と書かれているように、このファイルには絶対に手を加えてはいけません。これを基本として、Client&Server側を実装していきます。

コードを見たい方はこちら
https://github.com/waytkheming/gwitter-proto/blob/master/gwitterpb/gwitter.pb.go

土台となるServer,Clientのコードを実装

  • Server側
package main

import (
    "context"
    "fmt"
    "log"
    "net"
    "os"

    "os/signal"

    "github.com/waytkheming/grpc-go-course/gwitter/gwitterpb"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

var collection *mongo.Collection

type server struct {
}

func main() {
    //if crash the code, get the file name and line number
    log.SetFlags(log.LstdFlags | log.Lshortfile)
    // client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://localhost:27017"))

    // connect to database
    client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://localhost:27017"))
    if err != nil {
        log.Fatal(err)
    }
    err = client.Connect(context.TODO())
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Blog Service Started")
    collection = client.Database("mydb").Collection("gwitter")

    lis, err := net.Listen("tcp", "0.0.0.0:50051")
    if err != nil {
        log.Fatalf("Failed to listen: %v", err)
    }

    opts := []grpc.ServerOption{}
    s := grpc.NewServer(opts...)

    gwitterpb.RegisterGweetServiceServer(s, &server{})
    go func() {
        fmt.Println("Starting server ....")
        if err := s.Serve(lis); err != nil {
            log.Fatalf("Failed to serve: %v", err)
        }
    }()
    //Wait for Control C to Exit
    ch := make(chan os.Signal, 1)
    signal.Notify(ch, os.Interrupt)

    <-ch
    fmt.Println("Stopping the server")
    s.Stop()
    fmt.Println("Close the listener")
    lis.Close()
    fmt.Println("Closeing connection")
    client.Disconnect(context.TODO())
    fmt.Println("end of program")
}



  • Client側
package main
import (
    "fmt"
    "io"
    "log"
    "context"
    "github.com/waytkheming/grpc-go-course/gwitter/gwitterpb"
    "google.golang.org/grpc"
)

func main() {
    fmt.Println("Hello from Gwitter client")
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("could not connect:%v", err)
    }

    defer conn.Close()
}

Create

まずはCreate機能から作成して行きます。

  • Server側
//型を定義(bsonはMongoDB特有のフォーマットです。)
type gwitterItem struct {
    ID      primitive.ObjectID `bson:"_id,omitempty"`
    UserID  string             `bson:"user_id"`
    Content string             `bson:"content"`
}

//Post(Create)メソッドです。
func (*server) PostGwitter(ctx context.Context, req *gwitterpb.PostGwitterRequest) (*gwitterpb.PostGwitterResponse, error) {
    fmt.Println("Post Gweet invoked")

    gweet := req.GetGweet()

    data := gwitterItem{
        UserID:  gweet.GetUserId(),
        Content: gweet.GetContent(),
    }
        //MongoDBへ保存
    res, err := collection.InsertOne(context.Background(), data)
    if err != nil {
        return nil, status.Errorf(
            codes.Internal,
            fmt.Sprintf("Internal error: %v", err),
        )
    }

    oid, ok := res.InsertedID.(primitive.ObjectID)
    if !ok {
        return nil, status.Errorf(
            codes.Internal,
            fmt.Sprintf("Cannnot convert to OID "),
        )
    }

    return &gwitterpb.PostGwitterResponse{
        Gweet: &gwitterpb.Gweet{
            Id:      oid.Hex(),
            UserId:  gweet.GetUserId(),
            Content: gweet.GetContent(),
        },
    }, nil
}


  • Client側
    c := gwitterpb.NewGweetServiceClient(conn)

    gweet := &gwitterpb.Gweet{
        UserId:  "waytkheming",
        Content: "First Gweet",
    }

    createGweetRes, err := c.PostGwitter(context.Background(), &gwitterpb.PostGwitterRequest{Gweet: gweet})
    if err != nil {
        log.Fatalf("Unexpected Error %v: \n", err)
    }

    fmt.Printf("Gweet has been gweeted: %v \n", createGweetRes)

Read

  • Server側
func (*server) ReadGwitter(ctx context.Context, req *gwitterpb.ReadGwitterRequest) (*gwitterpb.ReadGwitterResponse, error) {
    fmt.Println("Read Gweet invoked")
    gweetID := req.GetGweetId()

    oid, err := primitive.ObjectIDFromHex(gweetID)
    if err != nil {
        return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("Cannot parse ID"))
    }

    // create empty struct
    data := &gwitterItem{}

    filter := bson.M{"_id": oid}
    res := collection.FindOne(context.Background(), filter)
    if err := res.Decode(data); err != nil {
        return nil, status.Errorf(
            codes.NotFound,
            fmt.Sprintf("cannnot fing gweet with this id: %v", err),
        )
    }

    return &gwitterpb.ReadGwitterResponse{
        Gweet: dataToGweetPb(data),
    }, nil
}

func dataToGweetPb(data *gwitterItem) *gwitterpb.Gweet {
    return &gwitterpb.Gweet{
        Id:      data.ID.Hex(),
        UserId:  data.UserID,
        Content: data.Content,
    }
}

  • Client側
    gweetID := createGweetRes.GetGweet().GetId()
    // read gwitter
    fmt.Println("Reading the gwitter")
    _, err2 := c.ReadGwitter(context.Background(), &gwitterpb.ReadGwitterRequest{GweetId: "waytkheming"})
    if err2 != nil {
        fmt.Printf("Error happened WHILE READING: %v \n", err2)
    }

    readGweetReq := &gwitterpb.ReadGwitterRequest{GweetId: gweetID}
    readGweetRes, readGweetError := c.ReadGwitter(context.Background(), readGweetReq)
    if readGweetError != nil {
        fmt.Printf("Error happened WHILE READING: %v \n", readGweetError)
    }
    fmt.Printf("Gweet was read: %v \n", readGweetRes)

Update

  • Server側
func (*server) UpdateGwitter(ctx context.Context, req *gwitterpb.UpdateGwitterRequest) (*gwitterpb.UpdateGwitterResponse, error) {
    fmt.Println("Update Gweet invoked")
    gweet := req.GetGweet()
    oid, err := primitive.ObjectIDFromHex(gweet.GetId())
    if err != nil {
        return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("Cannot parse ID"))
    }
    // create empty struct
    data := &gwitterItem{}
    filter := bson.M{"_id": oid}

    res := collection.FindOne(context.Background(), filter)
    if err := res.Decode(data); err != nil {
        return nil, status.Errorf(
            codes.NotFound,
            fmt.Sprintf("cannnot fing gweet with this id: %v", err),
        )
    }

    data.UserID = gweet.GetUserId()
    data.Content = gweet.GetContent()

    _, updateErr := collection.ReplaceOne(context.Background(), filter, data)
    if updateErr != nil {
        return nil, status.Errorf(
            codes.Internal,
            fmt.Sprintf("Cannot update object in MongoDB: %v", updateErr),
        )
    }

    return &gwitterpb.UpdateGwitterResponse{
        Gweet: dataToGweetPb(data),
    }, nil
}

  • Client側
    newGweet := &gwitterpb.Gweet{
        Id:      gweetID,
        UserId:  "changeMan",
        Content: "Editted content",
    }
    updateRes, updateErr := c.UpdateGwitter(context.Background(), &gwitterpb.UpdateGwitterRequest{Gweet: newGweet})
    if updateErr != nil {
        fmt.Printf("Error happened WHILE updateting: %v \n", readGweetError)
    }
    fmt.Printf("Gweet was updated: %v \n", updateRes)

Delete

  • Server側
func (*server) DeleteGwitter(ctx context.Context, req *gwitterpb.DeleteGwitterRequest) (*gwitterpb.DeleteGwitterResponse, error) {
    fmt.Println("Delete Gweet invoked")
    oid, err := primitive.ObjectIDFromHex(req.GetGweetId())
    if err != nil {
        return nil,
            status.Error(
                codes.InvalidArgument,
                fmt.Sprintf("Cannnot parse your gweet id"))
    }

    filter := bson.M{"_id": oid}
    res, err := collection.DeleteOne(context.Background(), filter)
    if err != nil {
        return nil, status.Errorf(
            codes.Internal,
            fmt.Sprintf("Internal error: %v", err),
        )
    }

    if res.DeletedCount == 0 {
        return nil, status.Errorf(
            codes.Internal,
            fmt.Sprintf("Internal error: %v", err),
        )
    }

    return &gwitterpb.DeleteGwitterResponse{GweetId: req.GetGweetId()}, nil
}

  • Client側
    fmt.Println("Deleting the gwitter")
    deleteGweetRes, deleteGweetError := c.DeleteGwitter(context.Background(), &gwitterpb.DeleteGwitterRequest{GweetId: gweetID})
    if deleteGweetError != nil {
        fmt.Printf("Error happened WHILE READING: %v \n", deleteGweetError)
    }
    fmt.Printf("Gweet was deleted: %v \n", deleteGweetRes)

List

ここではServer Streaming方式を用いることになります。

  • Server側
//引数であるstreamがNつのレスポンスを返す役割を担います。
func (*server) ListGwitter(req *gwitterpb.ListGwitterRequest, stream gwitterpb.GweetService_ListGwitterServer) error {
    fmt.Println("List gwitter request")

    list, err := collection.Find(context.Background(), primitive.D{{}})
    if err != nil {
        return status.Errorf(
            codes.Internal,
            fmt.Sprintf("Unknown internal error: %v", err),
        )
    }
    defer list.Close(context.Background())
        //for文を回して一つ一つ項目を取得して行きます。
    for list.Next(context.Background()) {
        data := &gwitterItem{}
        err := list.Decode(data)
        if err != nil {
            return status.Errorf(codes.Internal,
                fmt.Sprintf("Error while decoding data from MongoDB: %v", err),
            )
        }

        stream.Send(&gwitterpb.ListGwitterResponse{Gweet: dataToGweetPb(data)})
        if err := list.Err(); err != nil {
            return status.Errorf(codes.Internal,
                fmt.Sprintf("Error while decoding data from MongoDB: %v", err),
            )
        }
    }
    return nil

}

  • Client側
    stream, err := c.ListGwitter(context.Background(), &gwitterpb.ListGwitterRequest{})
    for {
        res, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatalf("somethig wrong things happened: %v", err)
        }
        fmt.Println(res.GetGweet())
    }

終わりに

ここで実装は終わりです。実際に動作を行うのは、Server側、Client側それぞれでrunさせてみてください。
MongoDBへデータのやり取りが行われるはずです。

長い記事でしたが最後まで読んでくださりありがとうございました。

想定よりもコード量が多くなってしまいましたので、今回記載したコードはまとめてこちらに掲載します。
https://github.com/waytkheming/gwitter-proto

よろしければ参考にしてみてください。

参考サイト

https://grpc.io/
https://qiita.com/muroon/items/1c9ad59653c00d8d5e3d
https://www.udemy.com/course/grpc-golang/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

goでたくさんの画像のサイズを小さくしたい!!!!

はじめに

こんにちはRIN1208です。
本日はクリスマスイブですね!!自分は特に予定がなく部屋で過ごしてチキンを食べました!悲しい....
この記事は画像のサイズ圧縮をかなりの量をしなくてはいけなくなったのでその制作の途中経過です(深刻なアドベントカレンダーのネタ不足....)

またこの記事はITRCアドベントカレンダーの24日目の記事になります。

開発環境

  • mac
  • goの実行環境

今回使用したパッケージ

  • github.com/google/uuid
    uuidを使うために使用しました
    GitHub
  • github.com/nfnt/resize
    画像のサイズを変更するのに使用しました
    GitHub

インストール

$ go get github.com/google/uuid
$ go get github.com/nfnt/resize

今回のコード(下で順々に解説します)

package main

import (
    "image"
    "image/jpeg"
    "image/png"
    "io/ioutil"
    "log"
    "os"

    "github.com/google/uuid"
    "github.com/nfnt/resize"
)

func main() {

    files, _ := ioutil.ReadDir("./image")
    for _, f := range files {
        save_image("./image/" + f.Name())
    }

}

func save_image(filename string) {

    path := "./datas/"  //保存先のディレクトリ名になります。現時点のコードでは既に作成していないとエラーが出ます


    uuid := create_uuid() //こちらの部分は後ほど解説します

    file, err := os.Open(filename)
    if err != nil {
        log.Fatal(err)
    }
    img, data, err := image.Decode(file)
    if err != nil {
        log.Fatal(err)
    }
    file.Close()
    m := resize.Resize(1000, 0, img, resize.Lanczos3)

    if data == "png" {
        out, err := os.Create(path + uuid + ".png")
        if err != nil {
            log.Fatal(err)
        }
        defer out.Close()
        png.Encode(out, m)
    } else if data == "jpeg" {
        out, err := os.Create(path + uuid + ".jpeg")
        if err != nil {
            log.Fatal(err)
        }
        defer out.Close()
        jpeg.Encode(out, m, nil)
    }

}

func create_uuid() string {
    u, _ := uuid.NewRandom()
    uuid := u.String()
    return uuid
}

特に難しいことはしてないでです

コードの解説

main()の部分

    files, _ := ioutil.ReadDir("./image")
    for _, f := range files {
        save_image("./image/" + f.Name())
    }

ここではimageディレクト下の画像のファイル名を取得しsave_image()関数に画像ファイルのパスを渡しています。

save_image()の部分は下で説明します。

save_image()の部分

画像のリサイズは以下の部分でしています。数値を変更するとサイズも変更されます。

m := resize.Resize(1000, 0, img, resize.Lanczos3)

こちらの部分は取得した画像がpngならpng、jpegならjpegで保存するようにしています

    if data == "png" {
        out, err := os.Create(path + uuid + ".png")
        if err != nil {
            log.Fatal(err)
        }
        defer out.Close()
        png.Encode(out, m)
    } else if data == "jpeg" {
        out, err := os.Create(path + uuid + ".jpeg")
        if err != nil {
            log.Fatal(err)
        }
        defer out.Close()
        jpeg.Encode(out, m, nil)
    }

create_uuid()の部分

ここに関しては特に説明するとがないですが今回使用した理由としては大量の画像のためファイル名がかぶらないようにしたかったので使用しました。(名前は適当でよかったので)

終わりに

ここまで読んでいただきありがとうございます。今回ここに書いてあるのは制作途中にの物でありこれから並列処理を使用して大量の画像を処理していく予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

mackerel-container-agentから学ぶGoにおけるリトライ実装

この記事は、Go Advent Calendarに、代打で出そうとして、クリスマスイブの夜から書き始めて、無事埋まったので野良記事として公開するものです。

TL;DR

  • mackerel-container-agent とは、Mackerel社がコンテナ監視のために用意している監視エージェントである
  • mackerel-container-agent は、Go言語で実装されており、監視ツールの性質上、リトライ処理が用意されている
  • Exponential backoff というリトライのアルゴリズムを実装している

背景

筆者は、業務でAmazon ECS(Elastic Container Service)というAWSのコンテナオーケストレーションサービスを利用したサービスを運用しています。そのサービスで稼働しているコンテナの監視のため、監視SaaSであるMackerelを使用しています。メインのコンテナのサイドカーとして、mackerel-container-agentというコンテナ用の監視エージェントを置くことで、監視を実現する仕組みです。普段、ユーザーとして使っている中でふとその中の実装がどうなっているのだろうと気になったので調べたことをここに記します。

特に、気になった点が、「いかにリトライを実現しているか」というポイントだったので、失敗時のリトライ処理にフォーカスして書きます。

mackerel-container-agentとは

上記で紹介したこちらは、GitHubにOSSとして公開されています。

https://github.com/mackerelio/mackerel-container-agent

調査の起点は、こちらのエラーログからたどっていきます(2019年12月19日当時、メンテナンス中でその際どういう動きをしてるんだろうと気になって調べたのがきっかけのきっかけです)。

2019/12/19 06:04:31 INFO <agent> retry to find host: failed to create a new host: API request failed: Site is under maintenance.

※ 内部実装がGoだと、おそらく errors.Wrap()でエラーがくるまれた結果、このような階層的なエラーメッセージが生成されているのだろう、という推測からです。

エラーメッセージの階層をたどる

まずは、エラーの文字列 retry to find host から、ここでログ出力されているのがわかります。

https://github.com/mackerelio/mackerel-container-agent/blob/697ce4a13eea5e614d4b72fa7ef7fa5374add839/agent/run.go#L78

            case <-time.After(duration):
                host, retryHostID, err := hostResolver.getHost(hostParam)
                if retryHostID {
                    logger.Infof("retry to find host: %s", err)
                    if duration *= 2; duration > 10*time.Minute {
                        duration = 10 * time.Minute
                    }
                    continue
                }       

hostResolver.getHostのなかをたどると、更に続きの階層のエラー failed to create a new host を見つけることが出来ます。

https://github.com/mackerelio/mackerel-container-agent/blob/69372758402f6388019adff2b5662a8e1a21ce31/agent/host_resolver.go#L61

            // create a new host
            hostID, err := r.client.CreateHost(hostParam)
            if err != nil {
                return nil, retryFromError(err), errors.Wrap(err, "failed to create a new host")
            }

r.client.CreateHostを続けて見つけると、Interfaceにたどり着きました。

https://github.com/mackerelio/mackerel-container-agent/blob/36cf0bb103d1645fb733c214fe142207b1318838/api/client.go#L9

package api

import mackerel "github.com/mackerelio/mackerel-client-go"

// Client represents a client of Mackerel API
type Client interface {
    FindHost(id string) (*mackerel.Host, error)
    FindHosts(param *mackerel.FindHostsParam) ([]*mackerel.Host, error)
    CreateHost(param *mackerel.CreateHostParam) (string, error)
    UpdateHost(hostID string, param *mackerel.UpdateHostParam) (string, error)
    UpdateHostStatus(hostID string, status string) error
    RetireHost(id string) error
    PostHostMetricValuesByHostID(hostID string, metricValues []*mackerel.MetricValue) error
    CreateGraphDefs([]*mackerel.GraphDefsParam) error
    PostCheckReports(reports *mackerel.CheckReports) error
}

このInterfaceの実装先は依存関係を解決している上位をたどるとわかりますが、github.com/mackerelio/mackerel-client-goが具象として使用されています。

https://github.com/mackerelio/mackerel-container-agent/blob/90e89ad9946df6dc89dad90f4b7f09528444054f/agent/agent.go#L97

    mackerel "github.com/mackerelio/mackerel-client-go"

mackerel-container-agent内の責務として具体的なAPI Clientとしての役割は持たせず別ライブラリを利用する戦略をとっていることがわかりました。

retry処理がどうなっているか眺める

表題にあげた、リトライ処理はどうなってるんでしょうか。その中身は、以下のコードから読み解くことが出来ます。

https://github.com/mackerelio/mackerel-container-agent/blob/697ce4a13eea5e614d4b72fa7ef7fa5374add839/agent/run.go#L73

        for {
            select {
            case <-time.After(duration):
                host, retryHostID, err := hostResolver.getHost(hostParam)
                if retryHostID {
                    logger.Infof("retry to find host: %s", err)
                    if duration *= 2; duration > 10*time.Minute {
                        duration = 10 * time.Minute
                    }
                    continue
                }

実装にfor-selectパターンという並行処理の実装パターンが使われています。
この中でtime.Afterを用いて一定期間時間が経過するのを待ち受けています。
引数で渡されているdurationですが、現在のリトライ時間を2倍ずつしていって、10分以上であれば10分にする実装となっています。

この手法は、Exponetial backoffという有名なリトライのアルゴリズムと知られています。

https://en.wikipedia.org/wiki/Exponential_backoff

Exponential backoff is an algorithm that uses feedback to multiplicatively decrease the rate of some process, in order to gradually find an acceptable rate.

Microsoftの「アプリケーション回復性パターン」というドキュメントにも、再思考パターンとして紹介されています。

https://docs.microsoft.com/ja-jp/dotnet/architecture/cloud-native/application-resiliency-patterns#retry-pattern

まとめ

簡単でしたが、コードリーディングの一端をご紹介させていただきました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoでJSONを扱う

はじめに

この時期はネタに走った面白い記事が多くあるとは思うのですが、私は自分の知識の確認のためにgoでのjsonの扱い方について書いていこうと思います!

https://golang.org/pkg/encoding/json/

jsonを構造体(struct)にする

まずはjson -> structにする方法です。これをするにはgoの標準パッケージであるjson(encoding/json)のjson.Unmarshalを使います。

json-->struct
type Person struct {
    Name       string
    Age        int
    Belongings []string
}

func main() {
    b := []byte(`{"name":"mike","age":20,"belongings":["apple","banana","orange"]}`)

    var p Person
    if err := json.Unmarshal(b, &p); err != nil {
        fmt.Println(err)
    }
    fmt.Println(p)
        // => {mike 20 [apple banana orange]}
}

まずは予め構造体を作っておきます。次にjsonのデータbyteに突っ込みます。
最後にそれをjson.Unmarshalで処理すればめでたくjsonが構造体になります!

また、structで宣言されていてもjson側に対応するものがない時は無視されます!

json-->struct
// Hogeを追加
type Person struct {
    Name       string
    Age        int
    Hoge       string
    Belongings []string
}

func main() {
    b := []byte(`{"name":"mike","age":20,"belongings":["apple","banana","orange"]}`)
    var p Person
    if err := json.Unmarshal(b, &p); err != nil {
        fmt.Println(err)
    }
    fmt.Println(p)
        // => {mike 20 [apple banana orange]}
}

構造体(struct)をjsonにする

今度は構造体(struct)をjsonに変換するやり方です。これも先ほどとやり方はほとんど同じでjson.Marshalを使用する事で変換できます!

struct-->json
type Person struct {
    Name       string   
    Age        int      
    Belongings []string 
}

func main() {
    b := []byte(`{"name":"mike","age":20,"belongings":["apple","banana","orange"]}`)

    var p Person
    if err := json.Unmarshal(b, &p); err != nil {
        fmt.Println(err)
    }

/* ~~~~~~~~~~ここまでさっきと同じ~~~~~~~~~~ */ 

    v, _ := json.Marshal(p)
    fmt.Println(string(v))
        // => {"Name":"mike","Age":20,"Belongings":["apple","banana","orange"]}
}

指定した名前で構造体(struct)をjsonにする

先ほどの実行結果をみてもらえばお分かりになると思うのですが、最初はnameだったのにもかかわらずstructからjsonにしたあとではNameとなっています。では、structからjsonにした後もnameにしたい場合はどのようにすれば良いでしょか?

名前を指定
type Person struct {
    Name       string   `json:"name"`
    Age        int      `json:"age"`
    Belongings []string `json:"belongings"`
}

func main() {
    b := []byte(`{"name":"mike","age":20,"belongings":["apple","banana","orange"]}`)
    var p Person
    if err := json.Unmarshal(b, &p); err != nil {
        fmt.Println(err)
    }

    v, _ := json.Marshal(p)
    fmt.Println(string(v))
        // => {"name":"mike","age":20,"belongings":["apple","banana","orange"]}
}

上のコードでは構造体の型の後ろでjson:"~~"のように宣言していますこうする事でjsonに変換したあとの名前を指定できます!

いい感じに名前を付けてくれる: https://mholt.github.io/json-to-go/

いろいろ指定してみる

json:"~~"は名前だけでなく他にも指定できます。

  • json:"-": structからjsonに変換した時に表示したくないものにはこれをしていする
  • json:"name,omitempty": nameが0や文字がない時や、nilの時にstructからjsonに変換した際、表示させないようにする

最後に

jsonをカスタマイズする方法もあったのですが、クリスマスに悲しく記事を書くのに疲れたので今回はここまでにしておきます(妥協)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

macOSでDockerを使ったGoのアプリケーション開発を爆速にするホットリローダを作った

はじめに

メリークリスマス!!

みなさんは Go のアプリケーション開発をどのような環境で行っていますか?
弊社ではゲームのアプリケーションサーバに Go を採用しており、開発は macOS で Docker for Mac を利用しています。開発当初はこの構成による不満は特に感じていませんでしたが、1年半ほど経ってプロジェクトの規模が大きくなったことで、無視できないレベルで開発スピードを低下させる要因となってしまいました。
弊社ではアプリケーション開発にソースコードの自動生成を多用しており、その影響もあってかコードベースの Go のコードは 150万行を超える規模になっています。 加えて、ビルドする際は cgo 経由で利用している C++ のコードもそれなりの量絡んでくることもあり、 Docker for Mac を使った Docker コンテナ上でのビルドに要する時間は、 メモリ8GB, 6CPUを割り当てたコンテナにも関わらず 5分を超える時間がかかっていました。 ( それでもまだ良い方で、他の方のマシンスペックでは 10分程かかる場合もあったようです )

実際はビルドキャッシュが効くので毎回 5 ~ 10 分かかるわけではありませんが、パッケージの依存関係によっては数珠つなぎ的に再ビルドが必要になってしまうケースもあるので、一文字編集したら 10 分待つという状況も起こり得ます。

このままではとても開発していられないということで、ビルドを爆速にするツールを開発してみました。
この記事では、開発したツールの紹介と、ちょっとトリッキーな実装をしているので、どうやって実現したかという話にも触れたいと思います。

ホットリローディング

ウェブアプリケーションなどを開発する際、ファイルの追加・更新・削除といったイベントを契機に自動で再ビルド・実行する仕組みを利用することが多いと思います。これらはホットリローディングやライブリローディングなどと呼ばれたりしますが、もちろん Go にも存在します。
有名どころだと https://github.com/gravityblast/freshhttps://github.com/oxequa/realize が挙げられますが、どちらも現在メンテされてはおりません...。

弊社では、上で挙げた https://github.com/gravityblast/fresh を利用していました。
また、ホットリローディングを Dockerコンテナ 上で動作するアプリケーションで行うため、以下のような設定を行っていました。

docker-compose.yml

version: '2'
services:
  app:
    image: golang:1.13.5
    container_name: app
    volumes:
      - '.:/go/src/app'
    working_dir: /go/src/app
    environment:
      GO111MODULE: "on"
    command: |
      go get -u github.com/pilu/fresh && fresh

つまり、 volumes でビルドに必要なソースコードが置かれているディレクトリをまるっとコンテナにマウントし、ホットリローダ ( fresh ) をコンテナ上にインストールしてファイル監視を始めます。

これによって、ローカル上のファイルを編集した場合でも、その変更がコンテナにも伝わり、
コンテナ上で動作しているホットリローダがそれを検知してアプリケーションを再ビルドし、無事ビルドできたら現在動いているアプリケーションと入れ替えます(リスタート)。

仕組み自体はとてもシンプルなものなので、再実装も難しくはありません。
ただ今回改善したいのは ビルド時間 なので、コンテナの上でビルドしているうちは改善できません。
そこで次のようなツールを開発しました

rebirth

rebirth という Go のための ホットリローダを開発しました。
既存のホットリローダと大きく異なるのは、 Docker コンテナ上でのビルドを避けるために
ホスト上でクロスコンパイルしつつDockerコンテナで動くアプリケーションをホットリロードできる 機能を持っている点です。
これによって、 Docker for Mac に依存せずにホストマシンの力を使い切ってビルドできるようになります。
( ホスト上でビルドするようにした結果 5分かかっていたビルド時間が 30 秒ほどに減り、目に見えて高速化しました )

どのように使うかというと

.
├── docker-compose.yml
├── main.go
└── rebirth.yml

このような構成のワークスペースがあったとして、
docker-compose.yml が以下のような内容だとします。 ( 先に挙げた docker-compose.yml 中の fresh の部分が tail -f /dev/null になっているだけです )

docker-compose.yml

version: '2'
services:
  app:
    image: golang:1.13.5
    container_name: app
    volumes:
      - '.:/go/src/app'
    working_dir: /go/src/app
    environment:
      GO111MODULE: "on"
    command: tail -f /dev/null

ここで docker-compose up -d とすると、 app という名前のコンテナが立ち上がると思います。
ここで、 rebirth.yml を記述します。

rebirth.yml

host:
  docker: app

host.docker にホットリロードしたいアプリケーションのあるコンテナの名前を書きます。

次に、以下を実行して rebirth という CLI をインストールします。

$ GO111MODULE=on go get -u github.com/goccy/rebirth/cmd/rebirth

これで準備完了です。 macOS上で rebirth を実行します

$ rebirth
# ホットリローダが立ち上がる。
# ファイルを編集すると、 app コンテナ上のアプリケーションがビルド後のものに入れ替わる

以上になります。

...ここで

!?

と思っていただけたら嬉しいのですが

コンテナ上に何もインストールしていないのに、 macOSにインストールしたバイナリのみでコンテナ上のアプリケーションのホットリロードを実現する というのがこのツールを作った時のこだわりポイントでした。これによって、Docker を使わない場合と使い方を変えることなく利用することができるようになっています ( rebirth.yml の書き方を変えるだけ )

続いて、これをどう実現しているかについて触れていきます

実装

流れが少し複雑なので、図を使って説明していきます。
はじめに、下の図を見てください。

一番外の大きい枠が macOS 上だということを表現しています。
その上にあるグレーの linux と書かれている部分は、
Docker for Mac を使って動作している linux コンテナを表しています。

破線で囲われている中は、 volumes でマウントされていることを表しています。
( つまり、 workspace が ~/work/app という状況で docker-compose.ymlvolumes.:/go/src/app と書かれている状態になります )

1. rebirth をインストールする

まず、 GO111MODULE=on go get -u github.com/goccy/rebirth/cmd/rebirthrebirth CLI をインストールします

2. rebirth 自身のクロスコンパイル

rebirth を実行した際にはじめに行うのは、ターゲットとなる Dockerコンテナのアーキテクチャ向けに自分自身をクロスコンパイルし、 __rebirth という名前で ~/work/app 直下の .rebirth ディレクトリ配下に置きます

コンパイル対象のアーキテクチャを知るため、 https://godoc.org/github.com/docker/docker/client を利用して docker remote API 経由で go env GOOSgo env GOARCH を実行しています。

3. コンテナ上にクロスコンパイルした rebirth バイナリを配置する

.rebirth があるディレクトリは ~/work/app 直下なので、コンテナ上にマウントされています。
このため、自動でコンテナ上の /go/src/app/.rebirth 配下にコンテナ上で実行可能な __rebirth バイナリが配置されます。

( このあたりは、マウントを利用せずともバイナリを直接コンテナ上にコピーすれば同じことができますが、大抵の場合はマウントを前提としても問題ないと思っているので、処理を簡単にするためにこのようにしています )

4. アプリケーションのファイルを監視し始める

図では main.go のファイルイベントを監視し始めることを表しています
( 実装には fsnotify を使っています )

5. アプリケーションコードをクロスコンパイルする

rebirth 自身をクロスコンパイルしたときと同じ要領で、コンテナのアーキテクチャ向けにクロスコンパイルします。
少し違うのは、アプリケーションコードが cgo を利用したものであったとしてもコンパイルできるようにしなければらない点です。

このため GOOSGOARCH の指定に加え、 CGO_ENABLED=1 を有効にします。
さらに、 C/C++ コードを macOS 上で linux 向けにコンパイルできるよう、クロスコンパイラを作らなければいけません。 https://github.com/FiloSottile/homebrew-musl-cross にあるように

$ brew install FiloSottile/musl-cross/musl-cross

でインストールをお願いします。 ( ビルドに30分程度かかります )

( 参考 : https://qiita.com/keijidosha/items/5f4a68a3341a44a25ab9 )

また、今までコンテナ上のパスとして設定されていた GOPATH が、ホスト上で指定されたものに変わるため、
振る舞いを揃えるために、ワークスペースに作った .rebirth ディレクトリを GOPATH の起点として扱い、 go.mod のモジュール名を見ながら、アプリケーションコードを .rebirth 配下に配置し直してビルドを行っています。

例えば、 go.modmodule github.com/company/webapp と書かれていたとすると、

  1. .rebirth/src/github.com/company というディレクトリを掘る
  2. ワークスペース ( webapp ディレクトリ ) への symlink を 1 で作ったディレクトリの配下に作る
  3. .rebirth/src/github.com/company/webapp へ移動する
  4. GOPATH=/path/to/.rebirth go build ... のように GOPATH を変更してビルドする

というようなことを行います。これによって、依存モジュールなどをアプリケーションワイドにインストールすることが可能なので、
ローカルの GOPATH と混ざったりすることはなくなります。

上記をまとめると、以下のような環境変数やオプションをつけて go build を実行しています

$ GOPATH=.rebirth GOOS=linux GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-linux-musl-cc CXX=x86_64-linux-musl-c++ go build --ldflags '-linkmode external -extldflags "-static"'

( ※ 実際には クロスコンパイラへのパスを通すために PATH に追加したり、 GOPATH も絶対パスで表現したりしています )

6. コンテナ上にクロスコンパイルしたアプリケーションバイナリを配置する

ビルドした結果は、 .rebirth 配下に program という名前で配置しています。
__rebirth のときと同様に、マウント先のコンテナ上に自動的に配置されます。

7. __rebirth の実行

コンテナ上で __rebirth バイナリを実行します。このとき、動作しているのが Dockerコンテナ上であり、
かつ rebirth.ymlhost.docker の指定がある場合には、ファイル監視を行わない専用のモードで起動します。
起動時に、自身の PID を記録したファイルを .rebirth 配下に書き出します。

( 自身がDockerコンテナ上で起動しているかどうかは、 /.dockerenv が存在するかで判定することができます )

8. コンテナ上でアプリケーションを起動する

__rebirth から .rebirth/program を実行します

9. ファイルの変更

main.go をホスト上で編集します

10. ファイルの変更を検知

ホスト上の rebirth プロセスが main.go の更新を検知します

11. アプリケーションの再ビルド

5 で説明したことをもう一度行います

12. アプリケーションの配置

6 で説明したことをもう一度行います

13. アプリケーションの再起動要請

ホスト上の rebirth から コンテナ上の __rebirth へアプリケーションの再起動要請を行います。
実装には、事前に書き出しておいた __rebirth プロセスの PID をもとに、 SIGHUP を送ることで実現しています。

本当は PID をファイルを経由せずに取得したかったのですが、 docker remote API を経由して知ろうとすると、ホスト上のPID名前空間で表現された値しかとれないため実現できませんでした ( コンテナ上で ps したときとは別の PID が返ってくる )

14. アプリケーションの再起動

SIGHUP を受け取った __rebirth プロセスが、起動中のプロセスを停止して新しく配置された program を実行すれば再起動の完了です

おまけ

cgo を利用しているコードから、他の C ライブラリを参照している場合

例えば弊社では、 cgo で記述されたコードから、 zlib を利用していました。
こういった場合は、別途 libz.a をクロスコンパイルする必要があります。

ビルドした libz.azlib のヘッダファイルを参照可能な場所に移して ( たとえば ワークスペース配下 )
rebirth.yml に以下のように書けばそれを用いてシンボル解決してくれるようになります

host:
  docker: app
build:
  env:
    CGO_LDFLAGS: ./lib/libz.a # lib に置いたクロスコンパイル済みの libz.a を参照する
    CGO_CXXFLAGS: -I./include # include に置いた `zlib.h` などを参照する

( 相対パスは、適宜ツール内部で絶対パスに置き換えて参照します )

ホットリロード以外の機能

ホットリロードをクロスコンパイルで行うようになると、今までコンテナ上で行っていたテストやスクリプトの実行などなどを同じ手段で行いたくなると思います。

そこで rebirth では go build , go test, go run をクロスコンパイルしつつ実行してくれるコマンドを用意しています。それぞれ rebirth build , rebirth test , rebirth run をホスト上で実行していただければ、クロスコンパイルしつつ、必要であればその結果をコンテナ上で走らせてくれます。ぜひご活用ください

おわりに

macOS 上で Docker を利用しているときのビルドを高速化したい!というニーズから実装したツールですが、
普通にホットリローダとして使う場合においても fresh より使いやすくなっていると思いますので、ぜひお試しいただければ幸いです。

引き続き改善しながら弊社で使っていこうと思っていますので、
なにか要望やバグを見つけた場合も気軽に報告いただければと思います。

それではよいクリスマスをお過ごしください!!

https://github.com/goccy/rebirth

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

11. タブをスペースに置換

11. タブをスペースに置換

タブ1文字につきスペース1文字に置換せよ.確認にはsedコマンド,trコマンド,もしくはexpandコマンドを用いよ.

Go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    //  読み込みファイルを指定
    name := "../hightemp.txt"

    //  読み込むファイルを開く
    f, err := os.Open(name)
    if err != nil {
        fmt.Printf("os.Open: %#v\n",err)
        return
    }
    defer f.Close() //  終了時にクリーズ

    //  スキャナライブラリを作成
    scanner := bufio.NewScanner(f)

    //  データを1行読み込み
    for scanner.Scan() {
        //  TAB を 空白へ置換
        fmt.Println(strings.Replace(scanner.Text(),"\t"," ",-1))
    }

    //  エラーが有ったかチェック
    if err = scanner.Err(); err != nil {
        fmt.Printf("scanner.Err: %#v\n",err)
        return
    }
}

python

# ファイルを開く
with open("../hightemp.txt", "r") as f:
    # 一行ずつ読み込む
    for data in f:
        # TAB を 空白へ置換(strip で white space を除去)
        print(data.strip().replace("\t"," "))

Javascript

// モジュールの読み込み
var fs = require("fs");
var readline = require("readline");

//  ストリームを作成
var stream = fs.createReadStream("../hightemp.txt", "utf8");

//  readlineにStreamを渡す
var reader = readline.createInterface({ input: stream });

//  行読み込みコールバック
reader.on("line", (data) => {
    //  TAB を 空白へ変換(文字列 "\t" 指定ではうまく動作しないため正規表現で指定)
    console.log(data.replace(/\t/g," "))
});

まとめ

Javascirpt で置換元文字列に "\t" が指定出来ないのか?。
Python コード数の少なさに改めて驚く。

トップ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PythonとGoの基礎文法をわかりやすく比較する

この記事は DeNA Advent Calendar 2019 の12/24(火)の記事です。

はじめに

普段は主にPerlを使っていますが、Goを覚えたいのとPythonを復習したいという目的でまとめました。
Pythonの文法はとてもシンプルなので、その差分でGoを覚えるのが早いのではないかと思いました。
なるべく多く書いたつもりですが、いろいろと不足はあると思います。ご留意ください。

コメント

まずはコメントの書き方です。
Pythonにはもともと複数行をコメントアウトする機能はありませんが、プログラム中に文字列を置いても影響がないため、それを利用して複数行コメントを記述することができます。
また、複数行コメントはドキュメンテーション文字列として残すことができます。

Python
# 1行コメント

'''
複数行コメント
'''
"""
複数行コメント
"""

def func():
   """
   プログラムの解説など
   """

help(func)
print(func.__doc__)
Go
// 1行コメント

/*
複数行コメント
*/

変数の定義

Pythonは動的型付け言語のため、変数の型を宣言する必要はありません。

Python
n = 10
name = "hoge"

# まとめて定義する
x, y, z = 1, 2, 3
a = b = c = 1

Goの場合は、まず変数名の1文字目に注意すべきです。

  • 1文字目が大文字の場合はパッケージ外からアクセスできる
  • 1文字目が小文字の場合はパッケージ外からアクセスできない

これは定数・関数にも言える事です。
Goは静的型付け言語だが、明示的な定義と暗黙的な定義が存在します。

明示的な定義

var [変数の名前] [変数の型]のように定義します。

Go
var n int
n = 1

// まとめて定義する
var x, y, z int
x, y, z = 1, 2, 3

var (
    x, y int
    name string
)
x, y, name = 1, 2, "hoge"

// 型の宣言と値の代入を同時に行う
var n int = 1
暗黙的な定義

[変数の名前] := [値]もしくはvar [変数の名前] = [値]のように定義します。値を代入すると変数の型は暗黙的に推論されます。

Go
n := 1

// varを用いた定義でも型が省略できる
var n = 1

// まとめて定義する
x, y, name := 1, 2, "hoge"

var (
    x = 1
    y = 2
    name = "hoge"
)

定数

Pythonには、定数を定義するためのキーワードはありません。慣例として大文字とアンダーバーのみで定数を表しています。

Python
PI = 3.14
MAX_NUM = 100

Goではconstを使用して定数を定義します。iotaという識別子を使えば、整数の連番を生成できます。
定数の値を変更しようとするとエラーになります。

Go
const Pi = 3.14
const MaxNum = 100

// ()でまとめて定義する
const (
    Pi = 3.14
    MaxNum = 100
)

const (
    X = iota  // 0
    Y         // 1
    Z         // 2
)

// 開始番号を指定する場合
const (
    X = iota + 10  // 10
    Y              // 11
    Z              // 12
)

配列

Pythonの配列(list)は非常にシンプルに書けます。以下は基本的な使い方です。

Python
# 定義
numbers = [1, 2, 3]

# 要素の追加
numbers.append(6)
numbers.insert(3, 5)  # numbers: [1, 2, 3, 5, 6]

# 要素数
print(len(numbers))

# 要素の削除
numbers.remove(3)  # numbers: [1, 2, 5, 6]
numbers.pop(1)  # numbers: [1, 5, 6]
del numbers[0]  # numbers: [5, 6]

# リストを結合
numbers += [3, 4]  # numbers: [5, 6, 3, 4]
numbers.extend([1, 2])  # numbers: [5, 6, 3, 4, 1, 2]

# 要素の検索
print(6 in numbers)  # True
print(numbers.index(6))  # 1

# リストをソート
numbers.sort()  # numbers: [1, 2, 3, 4, 5, 6]
numbers.sort(reverse=True)  # numbers: [6, 5, 4, 3, 2, 1]

Goの配列型(array)はサイズの拡張や縮小ができません。Pythonの配列(list)のようなデータ構造は、Goではスライス(slice)に相当します。
スライスの操作でappendがよく使われます。

Go
// 配列はサイズが変更できない
array := [3]int{1, 2, 3}
fmt.Println(array[0])  // 1
fmt.Println(array[1:3])  // [2 3]

// スライス
n1 := []int{}  // n1: []
n2 := make([]int, 0)  // n2: []
numbers := []int{1, 2, 3}

// 要素の追加
numbers = append(numbers, 6)  // numbers: [1 2 3 6]
numbers = append(numbers[0:3], append([]int{5}, numbers[3:]...)...)  // numbers: [1 2 3 5 6]

// 要素数
fmt.Println(len(numbers)) 

// 要素の削除
numbers = append(numbers[0:2], numbers[3:]...)  // numbers: [1 2 5 6]
numbers = numbers[2:]  // numbers: [5 6]

// 配列を結合
numbers = append(numbers, []int{3, 4, 1, 2}...)  // numbers: [5 6 3 4 1 2]

// 要素の検索
// Pythonのindexに相当するものはないので自分で書く
fmt.Println(IndexOf(numbers, 6))  // 1

func IndexOf(s []int, n int) int { 
    for i, v := range s { 
        if n == v { 
            return i 
        } 
    } 
    return -1
}

// 配列をソート
// sortパッケージを使う
sort.Ints(numbers)
fmt.Println(numbers)  // [1 2 3 4 5 6]
sort.Sort(sort.Reverse(sort.IntSlice(numbers)))
fmt.Println(numbers)  // [6 5 4 3 2 1]

連想配列

Pythonでは辞書(dictionary)と呼ばれるデータ構造を使います。

Python
# 定義
dic = {'hoge': 1, 'fuga': 2, 'piyo': 3}
list1 = [('hoge', 1), ('fuga', 2), ('piyo', 3)]
dic2 = dict(list1)  # dicの値と同じ
dic['hoge']
dic.get('hoge')

# 要素の追加と削除
dic['foo'] = 4
dic.setdefault('bar', 5)
dic.pop('hoge')  # {'fuga': 2, 'piyo': 3, 'foo': 4, 'bar': 5}
del dic['fuga'], dic['piyo']  # {'foo': 4, 'bar': 5}

# 要素数
len(dic)

# キーの存在確認
'foo' in dic

# キーと値の取り出し
list(dic.keys())  # ['foo', 'bar']
list(dic.values())  # [4, 5]
for k, v in dic.items():
    print(k, v)

Goのマップ(map)はPythonの辞書(dictionary)に相当します。以下の書式で定義します。
map[キーの型]要素の型

Go
// 定義
dic := map[string]int{"hoge": 1, "fuga": 2, "piyo": 3}
dic2 := make(map[string]int)
fmt.Println(dic)  // map[fuga:2 hoge:1 piyo:3]
fmt.Println(dic2)  // map[]

// 要素の追加と削除
dic["foo"] = 4
delete(dic, "hoge")
fmt.Println(dic)  // map[foo:4 fuga:2 piyo:3]

// 要素数
len(dic)

// キーの存在確認
_, exist := dic["foo"]
fmt.Println(exist)  // true
if value, exist := dic["foo"]; exist {
    fmt.Println(value)  // 4
}

// キーと値の取り出し
for k, v := range dic {
    fmt.Println(k, v)
}

条件分岐

Pythonには、条件式(三項演算子)と呼ばれる書き方があります。

6.12. 条件式 (Conditional Expressions)
https://docs.python.org/ja/3/reference/expressions.html#conditional-expressions

論理演算子はand,or,notを使用します。

Python
x, y = 1, 2
if x > y:
    print('x > y')
else:
    print('x <= y')

n = 10
# 条件式
result = "positive" if n > 0 else "negative or zero"

Goでは、簡易文付きifという書き方で、そのブロックだけで有効な変数が定義できます。
Goには三項演算子が存在しないが、mapでそれっぽい書き方ができます。
論理演算子は&&,||,!を使用します。

Go
x, y := 1, 2
if x > y {
    fmt.Println("x > y")
} else {
    fmt.Println("x <= y")
}

# 簡易文付きif
if x, y := 1, 2; x > y {
    fmt.Println("x > y")
} else {
    fmt.Println("x <= y")
}

n := 10
# 三項演算子っぽい書き方
result := map[bool]string{true: "positive", false: "negative"}[n > 0]

ループ

Python
sum = 0
for num in range(1, 11):
    sum += num

num, sum = 1, 0
while num <= 10:
    sum += num
    num += 1

# 無限ループ
num, sum = 1, 0
while True:
    sum += num
    num += 1
    if num > 10:
        break

Goのループはforしかないが、whileのような制御もできます。

Go
sum := 0
for num := 0 ; num <= 10 ; num++ {
    sum += num
}

// while
num, sum := 1, 0
for num <= 10 {
    sum += num
    num++
}

// 無限ループ
num, sum := 1, 0
for {
    sum += num
    num++
    if num > 10 {
        break
    }
}

関数

Pythonの関数はdefで定義します。関数の定義が関数呼び出しの実行よりも前に書かなければなりません。
以下のような使い方があります。

  • 引数にデフォルト値を持たせることができる
  • キーワード引数を使って関数を呼び出すことができる
  • 可変長引数を指定することができる
  • 複数の戻り値(タプル型)を返すことができる
Python
def greet(name="World"):
    print("Hello, " + name)

greet()
greet("Alice")
greet(name="Alice")

# 可変長変数
def greet(*names):
    for name in names:
        print("Hello, " + name)

greet("Alice", "Bob", "Carol")

# 複数の戻り値
def cal(a, b):
    add = a + b
    mul = a * b
    return add, mul

add, mul = cal(10, 5)

Goの関数はfuncで定義します。
func [関数名]( [引数の定義] ) [戻り値の型] { [関数の本体] }
デフォルト引数もキーワード引数も存在しませんが、以下のような特徴があります。

  • Pythonと同様に複数の戻り値や可変長引数をサポートしている
  • 戻り値に名前を予め付けることができる。その場合、returnの後ろに返す値を記述する必要がない
  • deferキーワードを付けた文は、関数が終了する際に実行される。複数定義した場合は最後の方から呼ばれる
Go
func main() {
    add, mul := cal(10, 5)
    fmt.Println(add, mul) // 15 50
    add, mul = calc(10, 5)
    fmt.Println(add, mul) // 15 50
    greet("Alice", "Bob", "Carol")
    testDefer() // BDCA
}

// 基本形
func cal(a int, b int) (int, int) {
    add := a + b
    mul := a * b
    return add, mul
}

// 名前付き戻り値
// 引数の型が同じ場合はまとめて書ける
func calc(a, b int) (add int, mul int) {
    add = a + b
    mul = a * b
    return 
}

// 戻り値を持たない関数
// 可変長引数
func greet(names ...string) {
    for _, name := range names {
        fmt.Println("Hello,", name)
    }
}

// defer遅延実行
func testDefer() {
    defer fmt.Print("A")
    fmt.Print("B")
    defer fmt.Print("C")  // Aより先にCが出力される
    fmt.Print("D")
}

例外処理

Pythonは例外をキャッチして処理するにはtry-except構文を使用します。

Python
def doDivision(x, y):
    try:
        result = x / y
    except Exception as e:
        result = None
        print("except:" + e.args[0])
    else:  # 正常終了時に実行する
        print("else")
    finally:  # 終了時に常に実行する
        print("finally")
    return result

doDivision(10, 2)
# else
# finally
doDivision(10, 0)
# except:test exception
# finally

Goにはtry-exceptのような例外機構が存在しません。代わりに、関数の戻り値を複数返せる特性を利用して、エラーが発生したかどうか(errorインターフェース)を戻り値の一部として返却することによってエラーの検知を実現しています。
errorインターフェースは以下のように定義されています。
https://golang.org/pkg/builtin/#error

Goのerrorインターフェース
type error interface {
    Error() string
}

以下の例ではerrorsパッケージのNew関数を使ってerror型を生成しています。
また、deferを使うことでPythonのfinallyと同じような動きが実現できます。

Go
package main
import (
    "fmt"
    "errors"
)

func main() {
    _, err := doDivision(10, 2)
    if (err != nil) {
        // エラー処理
    }
    // defer

    _, err = doDivision(10, 0)
    if (err != nil) {
        // エラー処理
    }
    // error
    // defer
}

func doDivision(i, j int) (result int, err error) {
    defer fmt.Println("defer")  // 終了時に常に実行する

    if j == 0 {
        fmt.Println("error")
        err = errors.New("Divided by Zero")
        return
    }
    result = i / j
    return
}

その他、Goにはpanic/recoverというエラー処理の仕組みもありますが、ここでは割愛します。

クラス

以下は、簡単なPythonクラスの例です。

Python
class Player:
    def __init__(self, id, name):
        self.id = id
        self.name = name
        self.__hp = 100

    @property
    def hp(self):
        return self.__hp

    def consume_hp(self, num):
        self.__hp -= num

player = Player(10001, "Alice")
print(player.hp) # 100
player.consume_hp(10)
print(player.hp) # 90

GoにはPythonで言うところのclassに相当する構文は存在しませんが、同じような役割として関連のある変数をひとまとめに扱う構造体(struct)が使用されます。
構造体に対してメソッドを定義することができます。メソッドは関数と違ってレシーバーの型とその変数名が必要になります。
以下の例では*Playerというポインタ型に対してconsumeHpというメソッドを定義しています。

Go
// Player型の構造体
type Player struct{
    ID int
    Name string
    Hp int
}

// コンストラクタ
func newPlayer(id int, name string) Player {
    return Player{ID: id, Name: name, Hp: 100}
}

// *Player型のメソッド
func (p *Player) consumeHp(num int) {
    p.Hp -= num
}

func main() {
    p := newPlayer(10001, "Alice")
    fmt.Println(p.Hp) // 100
    p.consumeHp(10)
    fmt.Println(p.Hp) // 90
}

マルチスレッド

最後は少しだけマルチスレッドについて書きます。
以下はthreadingモジュールを使ってスレッドを生成し、キューでデータを受け渡す簡単な例です。

Python
import threading
import time
from queue import Queue

def worker(a, b, q):
    time.sleep(1)
    result = a + b
    q.put(result)  # キューに要素を入れる
    print("result:", result)

q = Queue()
thread = threading.Thread(target=worker, args=(2, 3, q))
thread.start()
thread.join()

print("main thread")
result = q.get()  # キューから要素を取り出す
q.task_done()
print("received:", result)  # received: 5

同じことをGoで実現してみます。
Goでは軽量スレッドであるゴルーチン(goroutine)が並行して動作するように実装されています。go f(x)と書くと、新たなゴルーチンを起動してその関数を実行します。
ゴルーチンとゴルーチンの間でデータを受け渡しを行うためにチャネル(channel)と呼ばれるデータ構造を使用します。チャネルの型名はchan [データ型]のように書きます。

Go
package main

import (
    "fmt"
    "time"
)

func newThread(a, b int, ch chan int) {
    time.Sleep(1000)
    result := a + b
    ch <- result  // チャネルにデータを送信
    fmt.Println("result:", result)
}

func main() {
    ch := make(chan int)  // チャネルを生成する
    go newThread(2, 3, ch)  // 新たなゴルーチンでnewThreadを実行する

    fmt.Println("main thread")
    result := <-ch  // チャネルからデータを受信
    close(ch)
    fmt.Println("received:", result)  // received: 5
}

最後に

Pythonと比較しながらGo言語の文法を見てきました。
Goは静的型付け言語でありながら、Pythonなど動的型付け言語のような書きやすさもあります。
Goは様々な言語から影響を受けていると言われているように、C言語が分かる人ならばGoにおけるポインタや構造体がすぐ理解できるのではないかと思います。
並行処理に重要な goroutine と channel について詳しく書けませんでしたが、また今度書いてみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go で 円や線の画像を生成する

この記事はtomowarkar ひとりAdvent Calendar 2019の23日目の記事です。

はじめに

中学で習う点と直線の距離を駆使して円や直線の描画を実装していこうという記事になります。

以下のような画像をフレームワークなどは使わずに生成していきましょう!!
name.png

コード

メイン関数(抜粋)

type canvas struct {
    height int
    width  int
    data   []int
}

type point struct {
    x, y, r float64
}

type line struct {
    begin, end point
}

func main() {
    width, height := 1200, 800
    cnv := canvas{height, width, make([]int, width*height)}
    dot1 := point{600, 400, 200}
    dot2 := point{200, 400, 100}
    dot3 := point{400, 200, 100}
    cnv.dot(dot1, blue)
    cnv.dot(dot2, green)
    cnv.dot(dot3, red)

    line1 := line{point{300, 300, 3}, point{550, 730, 1}}
    line2 := line{point{50, 730, 3}, point{550, 730, 1}}
    line3 := line{point{50, 730, 3}, point{300, 300, 1}}
    cnv.line(line1, black)
    cnv.line(line2, black)
    cnv.line(line3, black)
    toPng("name", width, height, 1, palette, cnv.data)
}

やっていることといえば描画範囲であるcanvasと点pointと直線lineをそれぞれ定義して、
任意のポイントにプロットするという作業です。

現状レイヤー分けはしないので、後から描画したものがどんどん上書きされていく形になります。

コード全文
package main

import (
    "image"
    "image/color"
    "image/png"
    "math"
    "os"
)

type canvas struct {
    height int
    width  int
    data   []int
}

type point struct {
    x, y, r float64
}

type line struct {
    begin, end point
}

const (
    white = 0
    black = 1
    blue  = 2
    green = 3
    red   = 4
)

var palette = []color.Color{
    color.RGBA{255, 255, 255, 255},
    color.RGBA{0, 0, 0, 255},
    color.RGBA{100, 100, 225, 255},
    color.RGBA{100, 225, 100, 255},
    color.RGBA{225, 100, 100, 255},
}

func main() {
    width, height := 1200, 800
    cnv := canvas{height, width, make([]int, width*height)}
    dot1 := point{600, 400, 200}
    dot2 := point{200, 400, 100}
    dot3 := point{400, 200, 100}
    cnv.dot(dot1, blue)
    cnv.dot(dot2, green)
    cnv.dot(dot3, red)

    line1 := line{point{300, 300, 3}, point{550, 730, 1}}
    line2 := line{point{50, 730, 3}, point{550, 730, 1}}
    line3 := line{point{50, 730, 3}, point{300, 300, 1}}
    cnv.line(line1, black)
    cnv.line(line2, black)
    cnv.line(line3, black)
    toPng("name", width, height, 1, palette, cnv.data)
}

func max(a, b float64) float64 {
    if a < b {
        return b
    }
    return a
}

func distL(x, y int, l line) bool {
    var xx, yy = float64(x), float64(y)
    var mx, my = (l.begin.x + l.end.x) / 2, (l.begin.y + l.end.y) / 2
    var dx, dy = l.end.x - l.begin.x, l.end.y - l.begin.y
    var b1, b2 = -dy / dx, dx / dy
    var c1, c2 = -(b1*mx + my), -(b2*mx + my)
    var r1, r2 = max(l.begin.r, l.end.r), math.Sqrt(dx*dx+dy*dy) / 2
    var d1, d2 float64
    if dx == 0 || dy == 0 {
        d1 = math.Abs(yy - my)
        d2 = math.Abs(xx - mx)
    } else {
        d1 = math.Abs(yy+b1*xx+c1) / math.Sqrt(1+b1*b1)
        d2 = math.Abs(yy+b2*xx+c2) / math.Sqrt(1+b2*b2)
    }
    if d1 < r1 && d2 < r2 {
        return true
    }
    return false
}

func (c canvas) line(l line, obj int) {
    for y := 0; y < c.height; y++ {
        for x := 0; x < c.width; x++ {
            if distL(x, y, l) {
                c.data[y*c.width+x] = obj
            }
        }
    }
}

func distP(x, y int, p point) bool {
    var dx, dy = p.x - float64(x), p.y - float64(y)
    d := math.Sqrt(dx*dx+dy*dy) / p.r
    if d < 1 {
        return true
    }
    return false
}

func (c canvas) dot(p point, obj int) {
    for y := 0; y < c.height; y++ {
        for x := 0; x < c.width; x++ {
            if distP(x, y, p) {
                c.data[y*c.width+x] = obj
            }
        }
    }
}

func toPng(filename string, width, height, scale int, palette []color.Color, data []int) {
    img := image.NewRGBA(image.Rect(0, 0, width*scale, height*scale))
    for x := 0; x < width*scale; x++ {
        for y := 0; y < height*scale; y++ {
            img.Set(x, y, palette[data[y/scale*width+x/scale]%len(palette)])
        }
    }
    encodePng(img, filename)
}

func encodePng(img *image.RGBA, path string) {
    f, err := os.Create(path + ".png")
    if err != nil {
        panic("encode failed")
    }
    defer f.Close()
    png.Encode(f, img)
}

所感

一番のハマりポイントはやはり線の描画です。

二点から接戦と法線を導出し、その距離によって描画定義をしたのですが、見ての通りに計算が細かくケアレスミスで小一時間詰まってしまいました。

もう少し粒度を細かく実装すればハマりにくくなるので詰まったら粒度を下げることを意識していきたいです。
(今回でいえば法線や接戦を導出する関数を作るなど)

終わりに

まだまだ甘い部分もありますが、もう少しGoでの画像生成で遊んでいけたらと思います。

以上明日も頑張ります!!
tomowarkar ひとりAdvent Calendar Advent Calendar 2019

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSの利用料金をChatworkに通知してくれるバッチをGoで作った

はじめに

AWSの料金を毎日確認しにログインしてコンソールを開くのは面倒…。でも、気がついたら設定が間違っててすごい料金を請求されるなーんてことも。

そんなことが起こる前に、毎日使用料金を通知して把握しておこう!

ということで、Cost ExplorerのSDKを使ってGoでバッチを作成してみました。

AWSの構成

CostExplorer.png

当記事はLambdaにアップするバッチにフォーカスしてます。

実装

通知したいこと

  • 昨日の利用料金
  • 今月の利用料金(今月1日から昨日までの合計金額) or 今日が1日のときは先月の利用料金

必要なもの

  • Chatworkのルームid(トークルームURL:chatwork.com/#!rid********* ←9桁の数字)
  • ChatworkのAPIトークン(画面右上の自分の名前 → API設定で発行できます)

気をつけるところ

料金を知るのにもお金がかかります。
Cost Explorerに対して1リクエストあたり 0.01USD かかります。

AWSユーザーガイド

ソースコード

package main

import (
    "bytes"
    "fmt"
    "github.com/pkg/errors"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"
    "os"
    "time"

    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/costexplorer"
    "github.com/aws/aws-sdk-go/service/costexplorer/costexploreriface"
)

var chatworkClient = ChatworkClient{
    APIURL:   "https://api.chatwork.com/",
    Resource: "/v2/rooms/<ルームid>/messages",
}

// ChatworkClient はチャットワークへ通知するためのクライアントに相当する構造体
type ChatworkClient struct {
    APIURL   string
    Resource string
}

// メッセージの通知
func (chatwork ChatworkClient) postMessage(msg string) error {
    u, _ := url.ParseRequestURI(chatwork.APIURL)
    u.Path = chatwork.Resource
    urlStr := fmt.Sprintf("%v", u)

    data := url.Values{}
    data.Set("body", msg)
    fmt.Printf(data.Encode())

    client := &http.Client{}
    r, err := http.NewRequest("POST", urlStr, bytes.NewBufferString(data.Encode()))
    if err != nil {
        fmt.Println("HTTPリクエストの生成に失敗しました。date:" + fmt.Sprint(data) + ", urlStr:" + urlStr + ", err:" + fmt.Sprint(err))
        return errors.WithStack(err)
    }

    r.Header.Add("X-ChatWorkToken", "<APIトークン>")
    r.Header.Add("Content-Type", "application/x-www-form-urlencoded")

    resp, err := client.Do(r)
    if err != nil {
        fmt.Println("HTTPリクエストに失敗しました。date:" + fmt.Sprint(data) + ", urlStr:" + urlStr + ", err:" + fmt.Sprint(err))
        return errors.WithStack(err)
    }

    defer resp.Body.Close()
    contents, _ := ioutil.ReadAll(resp.Body)
    fmt.Printf("Http Status:%s, result: %s\n", resp.Status, contents)

    return nil
}

// コストの取得
func GetCost(svc costexploreriface.CostExplorerAPI, period string) (result *costexplorer.GetCostAndUsageOutput) {
    // 現在時刻の取得
    jst, _ := time.LoadLocation("Asia/Tokyo")
    now := time.Now().UTC().In(jst)
    dayBefore := now.AddDate(0, 0, -1)
    first := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, jst)
    if now.Day() == 1 { // 月初のときは先月分
        first = first.AddDate(0, -1, 0)
    }
    nowDate := now.Format("2006-01-02")
    nowDateP := &nowDate
    dateBefore := dayBefore.Format("2006-01-02")
    dateBeforeP := &dateBefore
    firstDate := first.Format("2006-01-02")
    firstDateP := &firstDate

    start := dateBeforeP
    if period == "Monthly" {
        start = firstDateP
    }

    granularity := aws.String("DAILY")
    if period == "Monthly" {
        granularity = aws.String("MONTHLY")
    }

    metric := "NetUnblendedCost" // 非ブレンド純コスト
    metrics := []*string{&metric}

    timePeriod := costexplorer.DateInterval{
        Start: start,
        End:   nowDateP,
    }

    // Inputの作成
    input := &costexplorer.GetCostAndUsageInput{
        Granularity: granularity,
        Metrics:     metrics,
        TimePeriod:  &timePeriod,
    }

    // 処理実行
    result, err := svc.GetCostAndUsage(input)
    if err != nil {
        log.Println(err.Error())
    }

    // 処理結果を出力
    log.Println(result)

    return result
}

// 処理実行
func run() error {
    log.Println("--- コスト取得バッチ 開始")
    log.Println("----- セッション作成")
    svc := costexplorer.New(session.Must(session.NewSession()))

    log.Println("----- コスト取得 実行")
    costDaily := GetCost(svc, "Daily")
    costMonthly := GetCost(svc, "Monthly")
    log.Println("----- コスト取得 完了")

    log.Printf("----- メッセージの通知 実行")
    err := chatworkClient.postMessage(costDaily.String())
    if err != nil{
        fmt.Printf("%+v\n", err)
        return err
    }
    err = chatworkClient.postMessage(costMonthly.String())
    if err != nil{
        fmt.Printf("%+v\n", err)
        return err
    }

    log.Println("--- コスト取得バッチ 完了")
    return nil
}

// メイン
func main() {
    lambda.Start(run)
}

実行結果

2019/12/16に実行してみました。

こちらが1日前(12/15)の利用料金を取得した結果です。期間(TimePeriod)を2019/12/15から2019/12/16で指定しています。料金はTotalの中にあるAmountで、今回だと約0.06USDとなっています。
cost_explorer_json_daily.png

こちらは当月(2019/12/01~2019/12/15)の合計金額です。指定した期間は2019/12/01から2019/12/16で、費用は約1.35USDという結果が得られました。
cost_explorer_json_monthly.png

解説

コストの取得

今回は、API「GetCostAndUsage」のInputを構成する要素として以下の3つを使用しています。

  • Granularity
  • Metrics
  • TimePeriod

Granularity

Granularityは「粒度」という意味で、どの粒度の期間で取得するのか指定できます。選択肢は以下の3つがあります。

  • MONTHLY
  • DAILY
  • HOURLY

今回はMONTHLYとDAILYを使いました。

Metrics

Metricsは「指標」という意味で、コストをどう計算するかを指定できます。選択肢は以下の7つがあります。

  • AmortizedCost
  • BlendedCost
  • NetAmortizedCost
  • NetUnblendedCost
  • NormalizedUsageAmount
  • UnblendedCost
  • UsageQuantity

それぞれのメトリクスの内容はこの記事を参考にしました。
AWS Cost Explorerに渡す、Metricsの値の意味

今回は「NetUnblendedCost = ディスカウント適用後(EDP割引等)のコスト」を使っています。
ちなみに、コンソール上で見る場合は、Cost Explorer: コストと使用状況のページの右下、詳細オプション → コストの表示方法 → 非ブレンド純コスト と同額になります。
cost-explorer-console.png

TimePeriod

Start(集計開始日時)とEnd(集計終了日時)を指定します。
フォーマットは " YYYY-MM-DD "です。

なお、Endに指定した日の利用料金は加算されません。
例えば、
Start : 2019-12-01
End : 2019-12-16
とした場合は1日から15日までの料金を取得することになります。

通知するベスト時間

最初、朝9時に前日の利用料金を通知するようにしていましたが、ある日、コスト通知用のトークルームを見ると「$ 0」。

昨日は無料キャンペーンだったのかなあ(わくわく)という話になりましたが、そんなことは…なかったです。

明確なコストエクスプローラーの更新時間は書かれていませんが、公式ページでは、「24 時間ごとに少なくとも一度コストデータを更新します。」とのことだったので、夜に通知するほうが正確な金額になると思われます。

ちなみに、私は朝9時に一昨日の利用料金を取得するようにしています。今のところ、それ以降の料金更新はないっぽいです。

おまけ

かわいく通知してみた

上記のままだと取得してきたJSONのまま表示されるので、お花をつけてかわいく整えてみました?

func MakeMassage(costMonthly, costDaily *costexplorer.GetCostAndUsageOutput) string {
    var msg bytes.Buffer
    msg.WriteString("(F)" + "AWS使用料金" + "(F)" + "\n\n")
    msg.WriteString("◆MONTHLY")
    msg.WriteString(" (" + *costMonthly.ResultsByTime[0].TimePeriod.Start + "~" +
        *costDaily.ResultsByTime[0].TimePeriod.Start + ")\n")
    msg.WriteString(" $ " + *costMonthly.ResultsByTime[0].Total["NetUnblendedCost"].Amount + "\n\n")
    msg.WriteString("◆DAILY")
    msg.WriteString(" (" + *costDaily.ResultsByTime[0].TimePeriod.Start + ")\n")
    msg.WriteString(" $ " + *costDaily.ResultsByTime[0].Total["NetUnblendedCost"].Amount + "\n")

    return msg.String()
}

処理実行箇所にメッセージ作成を追加し、実行コードを少し変更

log.Println("----- メッセージの作成 実行")
    msg := MakeMassage(costMonthly, costDaily)
log.Println("----- メッセージの作成 完了")

log.Printf("----- メッセージの通知 実行")
    err := chatworkClient.postMessage(msg)

実行結果

かわいくなりました?
cost_explorer_msg.png

おわりに

コンソール上だと下2桁しか表示されないですけど、Cost Explorer APIで取得すると下10桁まで表示されるので、コンソール上では0.00USD。でも実際は0.0012345678USDってことも分かるのでいいですよね。

大変だったとこととしては、はじめましてのGo。C言語を昔授業でちょっとやっていたけどポインタから逃げたので、今となってポインタに苦しめられました。

あとは、AWSのSDKを使う時に公式ドキュメントを読んでコードを書くことが大変でした。あまりGoでCostExplorerを使っている記事がなくて、自力では完成させることができず…。SDKの使い方、Goの書き方、とても勉強になりました。

次は藤原さんのQiitaの記事を見ながら、slackに通知できるようにしてみようと思っています!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Buffer を二回読む必要が生じたら?

自分が作っているコマンドで、Buffer を二回読む必要が生じた。Bufferって2回読めたっけ?
と思って、ソースを見てみた。

// A Buffer is a variable-sized buffer of bytes with Read and Write methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
    buf      []byte // contents are the bytes buf[off : len(buf)]
    off      int    // read at &buf[off], write at &buf[len(buf)]
    lastRead readOp // last read operation, so that Unread* can work correctly.
}

プライベートになっていて、アクセス出来ないプロパティがあって、オフセットがあるので、無理っぽい。
オフセットが進んでも、[]byteが変わらなければ、オフセットを元に戻せば良さげだけど、実際は、オフセット
を操作するメソッドは無さげ。

ソースコード

func main() {
    body := &bytes.Buffer{}
    body.WriteString("some string")
    b, err := ioutil.ReadAll(body)
    if err != nil {
        panic(err)
    }

    fmt.Println("1st:" + string(b))

    b, err = ioutil.ReadAll(body)
    if err != nil {
        panic(err)
    }
    fmt.Println("2nd:" + string(b))
}

実行結果

やはり無理でした。

$ go run main.go
1st:some string
2nd:

解決策

Buffer は二回以上読むものではないので、Buffer を2回読む必要があったら、一旦byte に変換すれば簡単でしょう。(追記)下記でコメントをいただきましたが、io.TeeReaderを使う方法でも行けそうです!

func main() {
    body := &bytes.Buffer{}
    body.WriteString("some string")
    b, err := ioutil.ReadAll(body)
    if err != nil {
        panic(err)
    }

    fmt.Println("1st:" + string(b))

    fmt.Println("2nd:" + string(b))

}
$ go run main.go
1st:some string
2nd:some string
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

tarの中身に対してチェックサムを打ちたい

LinuxやUnixに設定ファイルをリリースするときに、
1. 開発環境で設定ファイルいじる
2. tarで固める
3. ステージングor本番でtarを展開する

という流れはよくあると思いますが、tarの中身は開発環境と同じか?とか言われることあります。

また、クラスタを組んでいるサーバ同士で設定が同じかチェックするために、
sosreportなりで情報を取得して比較する、というタスクがあります。

そんなときに全ファイルをdiffしていると見づらい。手っ取り早く差分があるか見たい、しかし展開はしたくない、
というニーズが(自分には)あったので、作ってみました。
https://github.com/kuritayu/infra-tools

仕様

以下で取得できます。
go get github.com/kuritayu/infra-tools
go install "${GOPATH}"/src/infra-tools/cmd/lstar

引数なし

引数なしで実行するとhelpがでます。

NAME:
   lstar - print tar information

USAGE:
   lstar [global options] command [command options] [arguments...]

VERSION:
   1.0

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help (default: false)
   --version, -v  print the version (default: false)

引数あり

引数としてtarファイルを指定すると、こんな感じで出力されます。tartar.gzに対応してます。

Permission Owner    Group Size Date                Path             Checksum
-rw-r--r-- kuritayu staff 3584 2019-12-14 23:53:20 test/test.tar    b441b2f9a3e8a6154f60a1ef6509e9bf
drwxr-xr-x kuritayu staff 0    2019-12-09 23:13:54 ./test/          d41d8cd98f00b204e9800998ecf8427e
-rw-r--r-- kuritayu staff 2    2019-12-09 23:14:00 ./test/test2.txt 30cf3d7d133b08543cb6c8933c29dfd7
urw-r--r-- kuritayu staff 16   2019-12-10 08:39:10 ./test.txt       697f3de8175d739661ce5d0f9009eec4

チェックサム値はMD5で計算してます。

基本的な処理は、
1. 引数で与えられたファイルに対するvalidate
2. 該当ファイル(tar or tar.gz)を読み込み、tar.NewReaderに読ませる
3. ヘッダーデータや実データを構造体にセット
4. 実データからチェックサム計算

をしています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go runtimeの内部実装を覗きながら、チャンネルのことを知る

Intro

Goは平行処理の利便性と簡単さで選ばれることが多い言語です。

その基礎にある主なコンポーネントはGoRoutinesとチャンネルの二つで、一緒い使われることが非常に多いです。Goのチュートリアルをやったことあるなら大体触ったことある身近なものでしょうが、チャンネルとは実際どういったデータ構造なのか?内部のロジックや処理順番はどうなっているのか?と意外と分かりづらいところも多いので、今回はチャンネルのソースコードを覗きながらその構造について少し説明して行きます。

まずチャンネルのソースコードが見れる場所です。
コンピュータにGoがインストールされている場合は、Goがインストールされているroot dirからchan.goというファイルを探します。(GoのパッケージやGOPATHが指している場所ではなくruntimeの場所なので、例えばMacのHomebrewによってGoがインストールしてある場合は/usr/local/Cellar/go/[version]/libexec/src/runtime/chan.goみたいな場所にあります。

また、golangの公式Githubからも見れます:
https://github.com/golang/go/blob/master/src/runtime/chan.go

ここで参照するソースコードは主にchan.goと、その他関連のsrc/runtime/[].goのページからの抜粋です。

まず、Goのチャンネルは何をするものなのか?

goroutine同士でデータやメッセージの引き渡し、双方向のread/writeの処理を可能にするデータ構造です。データや処理のパイプラインという表現でよく説明されます。

特徴

  • 読み込み順番はFIFO
  • goroutine-safe (goroutine同士でメモリー扱いの安全性を担保する)
  • goroutineのスケジューリング(block/unblock処理)をコントロールできる

一つのデータ型にしては責務が多いですね。

作成例

// nilチャンネルを宣言。nilチャンネルのままだと書き込み、読み込み不可能。
var myChan chan int 

// チャンネルを作成&初期化。書き込み・読み込み可能。
myChan := make(chan int) 

*チャンネルの作成時に指定したチャンネルのデータ型(上記はint)以外のデータ型を扱えないので要注意。

channelは主に3種類ある

  • synchronous (バッファなし, mutexあり)
  • asynchronous (バッファあり)
  • asynchronous, 0-size (chan struct{}) (バッファなし)

バッファ無しチャンネルは容量がないため、receiver, senderの両方が必要。
バッファを指定すると、バッファがいっぱいになるまで書き込みができ、値を読み込むことでバッファの容量が開くのでasync処理が可能。
大きさゼロ、バッファ無しのchan struct{} = semaphoreです。

チャンネルの中身

まず作成してみる:

awesomeChan := make(chan string) 
fmt.Printf("awesomeChan = %v\n", awesomeChan)

channelのvalueを出力してみると、メモリーアドレスが帰ってきます。
なぜかと言うと、チャンネルの実態は、hchanというデータ構造へのポインターです。

makechanの実装を見てみましょう。

( go/src/runtime/chan.go )

func makechan(t *chantype, size int) *hchan {
    elem := t.elem

    // compiler checks this but be safe.
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
    if hchanSize%maxAlign != 0 || elem.align > maxAlign {
        throw("makechan: bad alignment")
    }

    mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
        panic(plainError("makechan: size out of range"))
    }

    // Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
    // buf points into the same allocation, elemtype is persistent.
    // SudoG's are referenced from their owning thread so they can't be collected.
    // TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
    var c *hchan
    switch {
    case mem == 0:
        // Queue or element size is zero.
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        // Race detector uses this location for synchronization.
        c.buf = c.raceaddr()
    case elem.ptrdata == 0:
        // Elements do not contain pointers.
        // Allocate hchan and buf in one call.
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
        // Elements contain pointers.
        c = new(hchan)
        c.buf = mallocgc(mem, elem, true)
    }

    c.elemsize = uint16(elem.size)
    c.elemtype = elem
    c.dataqsiz = uint(size)

    if debugChan {
        print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
    }
    return c
}

解読していきます。

func makechan(t *chantype, size int) *hchan {
    elem := t.elem
...

作成時にチャンネルが扱うデータ型chantypeオブジェクトへのポインター、そしてバッファの大きさを指定します。

chantypeの実装はsrc/runtime/type.goの中にあります。

// compiler checks this but be safe.
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
...

データ型の大きさやoverflowなどのチェックです。compilerが良しなにやってくれるので省略します。

var c *hchan
...

return c

ここはチャンネルの本性、hchanオブジェクトです。

makechanの戻り値を見ていると、チャンネルと言うものはhchanへのポインターだと分かります。

make(chan ...) でチャンネルを作成すると、heap上hchanというデータ構造用にメモリーがallocateされ、このhchanへのポインターを返します。

hchanの値の設定を見ていると、polymorphicなバッファを持っているものだとすぐ分かります:

( go/src/runtime/chan.go )

switch {
    case mem == 0:
        // Queue or element size is zero.
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        // Race detector uses this location for synchronization.
        c.buf = c.raceaddr()
    case elem.ptrdata == 0:
        // Elements do not contain pointers.
        // Allocate hchan and buf in one call.
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
        // Elements contain pointers.
        c = new(hchan)
        c.buf = mallocgc(mem, elem, true)
    }

c.elemsize = uint16(elem.size)
    c.elemtype = elem
    c.dataqsiz = uint(size)

hchanは何かと言いますと、ロックの持っているqueueを拡張したイメージです。

中身を見ていきます。

( go/src/runtime/chan.go )

type hchan struct {
    qcount   uint           // total data in the queue
    dataqsiz uint           // size of the circular queue
    buf      unsafe.Pointer // points to an array of dataqsiz elements
    elemsize uint16
    closed   uint32
    elemtype *_type // element type
    sendx    uint   // send index
    recvx    uint   // receive index
    recvq    waitq  // list of recv waiters
    sendq    waitq  // list of send waiters

...
    lock mutex
}

hchanは環状queuebuf、 バッファ内の書き込み位置のindexsendxと読み込み位置のindexrecvx、ロックを取得するためのmutexlock、チャンネルにread/write待ちのgoroutine情報のリストrecvqsendq、チャンネルがclosedかどうかのフラグなどのフィールドを持っています。

(struct{}チャンネルで使っているHchan(hchanの仲間)は、環状バッファもsend/recvもなく、チャンネルに書き込まれている値の数を記録している唯一のカウンターだけで状態を管理しています。full/emptyのコールもこのカウンターの値だけを参照します)。

それぞれのフィールドの意味を掴めるために、ここでgoroutinesが(バッファ有り)チャンネルに読み込み・書き込みする時の大まかな処理フローを説明します。

goroutinesによるチャンネルへの読み込み・書き込みの処理例

// 1つ目のGo routine
func main(){
...
  awesomeChan := make(chan int, 11);  // bufferサイズ11のチャンネルを作成
  for _, awesomeNumber := range awesomeNumber {
    awesomeChan <- task
  }


...
}

// 2つ目のGo routine
func processNumbers(channelToGetFrom <-chan string) {
  for {
    awesomeNumber := <- channelToGetFrom
    doStuffWithNumber(awesomeNumber)
  }
}

処理の概要
Case 1) synchronousで1対1の読み込み・書き込みの時の処理

はじめに一つ目のgoroutinemain関数が我がチャンネルに何かを送る。
1. mainがawesomeChanの中のhchanのロックを取得
2. hchanのバッファに値を追加してくれる(実際にバッファーのメモリー中にのコピーを行う)
3. 追加後、ロックを解除する

今度、受け取り側のgoroutineprocessNumbers関数がmainから送られてきた値を受け取る。
1. processNumbersがawesomeChanの中のhchanのロックを取得
2. hchanのバッファから値を取り出す(awesomeNumber変数の指しているメモリー位置の中に値をコピーする)
3. ロックを解除する

*チャンネルから読み込み、チャンネルへの書き込み時ともにメモリーのコピー(上記の2.)が行われるので、hchanのmutexによって保護されているバッファ以外、二つのgoroutineが共有するメモリーがない。これはgoroutine同士のメモリー安全性を担保できる一つのメカニズムます。

Case 2) bufferの容量がいっぱいになった時の処理

受け取り側の関数の処理時間が長かったり、チャンネルのバッファへの値の書き込み速度に追いつかなかったりする状況があったとします。

  1. バッファの容量がいっぱいになりバッファに書き込みできなかった
  2. main関数の実行が止まって待つ(blocking処理)
  3. hchanバッファの容量に開きが出たらmain関数の処理を再開

このblocking処理はどう行われるかと言うと、

→ ここでsudogとruntime schedulerくんの出番です。

  • sudog型のstructをallocateする→goroutineの待ち状況・再開条件をsudog型を使ってチャンネルの中に書き込み→実行をブロックする旨をスケジューラーくんに伝えます。

  • 次にチャンネルから読み込むgoroutineが現れたら、処理をresumeする。

sudogを見てみる:

( go/src/runtime/runtime2.go )

type sudog struct {
    // The following fields are protected by the hchan.lock of the
    // channel this sudog is blocking on. shrinkstack depends on
    // this for sudogs involved in channel ops.

    g *g

    // isSelect indicates g is participating in a select, so
    // g.selectDone must be CAS'd to win the wake-up race.
    isSelect bool
    next     *sudog
    prev     *sudog
    elem     unsafe.Pointer // data element (may point to stack)

    // The following fields are never accessed concurrently.
    // For channels, waitlink is only accessed by g.
    // For semaphores, all fields (including the ones above)
    // are only accessed when holding a semaRoot lock.

    acquiretime int64
    releasetime int64
    ticket      uint32
    parent      *sudog // semaRoot binary tree
    waitlink    *sudog // g.waiting list or semaRoot
    waittail    *sudog // semaRoot
    c           *hchan // channel
}

sudogは待っているgoroutineへのポインタg、そして待たれているelem(書き込み・読み取り待ち値)へのポインターelemを持っています。

sudogを作成しチャンネルのsendq(送り待ちのqueue)のリストに追加することで、チャンネルに対してrecvを行うGoRoutineが現れた時に、gの処理を再開できます。

recv関数を見てみる:

( go/src/runtime/chan.go )

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    if c.dataqsiz == 0 {
        if raceenabled {
            racesync(c, sg)
        }
        if ep != nil {
            // copy data from sender
            recvDirect(c.elemtype, sg, ep)
        }
    } else {
        // Queue is full. Take the item at the
        // head of the queue. Make the sender enqueue
        // its item at the tail of the queue. Since the
        // queue is full, those are both the same slot.
        qp := chanbuf(c, c.recvx)
        if raceenabled {
            raceacquire(qp)
            racerelease(qp)
            raceacquireg(sg.g, qp)
            racereleaseg(sg.g, qp)
        }
        // copy data from queue to receiver
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }
        // copy data from sender to queue
        typedmemmove(c.elemtype, qp, sg.elem)
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
    }
    sg.elem = nil
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    goready(gp, skip+1)
}

sendx, recvxでチャンネルにデータの挿入・取り出し位置を管理する。バッファーのあるhchanのqueueの中に値を入れていくと、sendxとrecvxが一緒にincrementされ、値を取り出すと受け取りのindexがincrementされる。

チャンネルのバッファがいっぱいで更にsendqにも値が入っている場合、sendqにあるsudogsgを外し、待っている値をバッファに追加する:

// copy data from sender to queue
        typedmemmove(c.elemtype, qp, sg.elem)
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz

その後、待っていたgoroutineが実行可能になったことをgoreadyコールでスケジューラーに知らせます:

    sg.elem = nil   // sudogで処理待ちの値を外す
    gp := sg.g   // sudogで処理待ちgoroutine
    unlockf()
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    goready(gp, skip+1)  // 処理待ちgoroutineを実行可能にする

スケジューラーがgoroutineをrunqueueに追加することで、今度は処理が再開されます。

*Goのスレッドについて

GoRoutineはGoランタイムによって作成されているユーザー空間の軽量スレッドで、GoのランタイムスケジューラーによってOSスレッドの上にマルチプレックスされる。GoRoutineを走らせるために、OSのスレッドがスケジューリングcontext(実行可能なgoroutineのrunqueue)を持ち、それぞれがそこから随時実行するGoRoutineを選ぶ。

そのため、上記のgoready, goparkコールで、スケジューラーにスケジューリングコンテキストのrunqueueにそれぞれのgoroutineを追加することができます。goparkシグナルでgoroutineが実行されていたOSのスレッドも解放され、スケジューラーのrunqueueから他のgoroutineを実行できます。

*これによってgoroutinesはblockされることがありながらも、作成も実行ものリソースがかかるOSのスレッドは中断されずにすむ

Case 3) bufferの中に値がないのに受け取りを待っている関数がある
  1. processNumbersがawesomeChanから読もうとするができない
  2. processNumbersがsudogを作り、resume状態を登録し、そのsudogをチャンネルのrecvqの中に入れる
  3. processNumbersがスケジューラーに(gopark自分)シグナルを送り、チャンネルにsendがあるまでブロックされる
  4. mainが値をバッファー内に書き込む→スケジューラーがprocessNumberをresume可能と判断するか、受け取り変数のメモリー位置に直接値を書き込む(mainがprocessNumberのスタックに直接書き込む)

読み込み待ちの場合は自分の情報をチャンネルのrecvq(読み取り待ちのqueue)に追加する。

これでsudogを利用し、blockされている読み込み待ちのgoroutineを解放するsend関数を見てみる:

( go/src/runtime/chan.go )

func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    if raceenabled {
        if c.dataqsiz == 0 {
            racesync(c, sg)
        } else {
            // Pretend we go through the buffer, even though
            // we copy directly. Note that we need to increment
            // the head/tail locations only when raceenabled.
            qp := chanbuf(c, c.recvx)
            raceacquire(qp)
            racerelease(qp)
            raceacquireg(sg.g, qp)
            racereleaseg(sg.g, qp)
            c.recvx++
            if c.recvx == c.dataqsiz {
                c.recvx = 0
            }
            c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
        }
    }
    if sg.elem != nil {
        sendDirect(c.elemtype, sg, ep)
        sg.elem = nil
    }
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    goready(gp, skip+1)
}

まず、send関数はhchan, sudogへのポインターを引数として受け取る。

if sg.elem != nil {
        sendDirect(c.elemtype, sg, ep)
        sg.elem = nil
    }

sudogにsend待ちの値がある時(hchanのバッファがいっぱいの時)、sendDirectでstackに送られます。

( go/src/runtime/chan.go )

func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
    // src is on our stack, dst is a slot on another stack.

    // Once we read sg.elem out of sg, it will no longer
    // be updated if the destination's stack gets copied (shrunk).
    // So make sure that no preemption points can happen between read & use.
    dst := sg.elem
    typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
    // No need for cgo write barrier checks because dst is always
    // Go memory.
    memmove(dst, src, t.size)
}

次は、

gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    ...
    goready(gp, skip+1)

stackロックが解除され、スケジューラーに対してsendを待っていたgp関数をgoreadyシグナルでスケジューリング可能とさせます。

チャンネルのgoroutine・メモリー安全性をなぜ担保できているか、実装から見える

  • チャンネルはただのポインターであるため、Fxn同士でそのまま渡せる(チャンネルへのぽいんたーを渡す必要ない)
  • そのままを渡しても、二つのFxnでも同じバッファーを参照します(deepcopyなど行われない)
  • goroutineは全員自分自身のスタックを持っている
  • チャンネルでの処理以外、goroutineのスタック同士で読み込み・書き込みをすることはない (↑受け取り待ちの変数がある時に、ロックを取得する必要なく、受け取り関数のメモリーに直接コピーをすることが可能なので、メモリーコピーとロック処理を一個減らすためのものだそうです。賢い。)
  • 一つのg(goroutineへのポインター)は複数の待ち行列(sudog)に登録できる

チャンネルが行える他のオペレーション

  • close (channelを閉めるとmutexをロック→closedフラグを設定し→待っている全てのgoroutineを解放させるの一連の作業が行われる)
  • select (関連チャンネルをシャッフル→メッセージ受け取り・読み込み可能なものがあるか確認→全てをブロックする。runtime/select.goの中で実装されている)

こちらを含めて実装の他の多くのことは省略しましたので、興味ある方は是非runtimeのソースコードを眺めてみてください。

runtime/chan.goの中身

chan.goの中で定義されている主な内容。
他にも関連している関数はruntime/runtime2.go, runtime/type.goなどにも登場されます。

const:
- maxAlign
- hchansize
- debugChan

struct:
- hchan
- waitq

func:
- reflect_makechan
- makechan64
- makechan
- chanbuf
- full
- chansend1
- chansend
- send
- sendDirect
- recvDirect
- closechan
- empty
- chanrecv, chanrecv1, chanrecv2
- recv
- chanparkcommit
- selectnbsend
- selectnbrecv, 2
- reflect_chansend, reflect_chanrecv, reflect_chanlenなど
- enqueue, dequeue
- raceaddr
- racesync
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

設定ファイル統一戦争勃発(もしくはHOCONのススメ

こんにちは、Wano株式会社でVeleTという動画広告配信サービスのエンジニアをやっております。stk0724です。

この記事はWanoグループ Advent Calendar 2019の24日目となります。

仰々しいタイトルですが、なんのことはありません。設定ファイル管理をどのようにいい感じにしたかについてお話します。

顛末

結論から先に言うと、統一してないです。

大本となるjsonファイルをパラメータファイルとして、用途別のテンプレートファイルから各種設定ファイルをレンダリングする方式をとりました。

前提: 設定ファイル、いっぱいありますよね?

webアプリケーションフレームワークやら、各種ミドルウェアやらで最近のサービス開発では複数種類の設定ファイルを扱うことになると思います。

VeleTの場合ですと、

  • 管理画面の設定ファイル
  • nginxの設定ファイル
  • fluentdの設定ファイル

などがあります。

設定ファイルがバラけると、どうなる?

設定ファイル毎に全く異なる値を管理しているのであれば問題ないと思うのですが、同じ意味合いの項目が複数の設定ファイルに存在していると、変更が入った場合の反映漏れが怖いです。

これに本番用、開発環境用、ローカル用など、環境別に必要なことを考えると、

設定ファイル種別 * 環境数の数の設定ファイル管理が必要となります。

これを手動で管理するのはちょっと厳しいです。

ソリューション: 単一パラメータファイルから生成したらよくね?

単一の設定ファイル読み込ませるようにしたら、反映漏れとか発生しないですよね。

じゃあ設定ファイル統一しよう!!

ってのはムリですよね。設定ファイルのフォーマットそれぞれ異なりますし。

なので、環境別に単一パラメータファイルを用意して、なんらかのテンプレートライブラリで設定ファイルテンプレートを書き、パラメータファイルを読み込ませてからレンダリングして作ればいいのではと考えました。

以下のような流れになります。

  1. HOCONによるパラメータファイルをjsonに変換する
  2. pongo2テンプレートファイルのレンダリングコマンドにパラメータファイルとパラメータファイルを入力して設定ファイル生成

単一パラメータファイルにHOCONフォーマットを利用する

現在VeleTでは管理画面をScala + Playframeworkでリニューアル中でして、新管理画面ではHOCONフォーマットで設定ファイルを書いています。

HOCONフォーマットはlightbend社が開発したフォーマットで、Playframweorkの他、lightbend社が開発したOSSなどで利用されています。

HOCONの利点

Playframeworkを使っている関係から半分成り行きでHOCONを利用することにしたのですが、HOCONにはjson, yamlなどと比較して大きな利点があります。

  1. 他のHOCONファイルをincludeできる
  2. 既存設定値を上書きできる

1はそのままの意味です。2の利点がとても大きく、ローカル環境用の設定ファイルをベースとして、環境別に上書きが必要な項目のみincludeするといったような書き方ができます。

例えば以下のような感じです。

base.conf
domain = "localhost"
...その他いろいろ...
dev_domain.conf
domain = "dev.velet.jp"
prod_domain.conf
domain = "velet.jp"
local.conf
include "base.conf"
dev.conf
include "base.conf"
include "dev_domain.conf"
prod.conf
include "base.conf"
include "prod_domain.conf"

上記の例ですと、local.confはbase.confそのまま、dev.confはdomainがdev_domain.confの値に上書き、prod.confはprod_domain.confの値に上書きされます。

jsonやyamlの場合ですと、環境別に同じ項目のファイルを用意する必要があるので、差分が発生する項目だけ分けて、includeして上書きできるのは大きな利点と考えています。

HOCONをjsonに変換する

後述しますが、レンダリング処理でpongo2テンプレートを利用する場合、jsonにしたほうが取り回しが楽なので、HOCONのパラメータファイルをjsonに変換します。

lightbend社純正のHOCONパーサを利用するのが確実なのですが、jvm起動が重たくて嫌なので、pyhoconというPython用HOCONパーサを利用します。

pyhoconをpipでインストールすると、CLIも入るので、それを利用して以下のように実行します。

pyhocon < /path/to/parameterfile > parameter.json

(本当はpyhoconが入ったdockerイメージを作って、docker run時に標準入力からパラメータファイルを食わせる方式にしたかったのですが、その場合だと別ファイルのincludeができないので、妥協しています。)

テンプレートファイルから設定ファイルをレンダリングする

次にjsonをパラメータファイルとして、テンプレートファイルから設定ファイルをレンダリングします。

レンダリングはGoによるCLIコマンドでやりたかったので、テンプレートライブラリにはpongo2を選択しました。
(他のGo用のテンプレートライブラリも検討しましたが、自分が慣れている and 別の箇所で利用している and どっかでansible使うはずなので、jinja2ライクな記法に慣れておいたほうがよい といった理由でpongo2にしました)

レンダリング用のCLIコマンドですが、以下で公開しています。

konfu

やってることはとても単純で、jsonをmap[string]interface{}としてUnmarshalし、それをpongo2のContextとしてテンプレートファイルのレンダリング処理を実行しているだけです。
(pongo2.Contextはmap[string]interface{}のエイリアス)

main.go
...省略...
var parser ParameterParser
if *mode == "yaml" {
  parser = NewYamlParser()
} else {
  parser = NewJsonParser()
}

f, err := os.Open(*config)
if err != nil {
  fmt.Fprintln(os.Stderr, err)
  os.Exit(1)
}
defer f.Close()

var context pongo2.Context
context, err = parser.Parse(f)
if err != nil {
  fmt.Fprint(os.Stderr, err)
  os.Exit(1)
}

pt, err := pongo2.FromFile(*templatePath)
if err != nil {
  fmt.Fprintln(os.Stderr, err)
  os.Exit(1)
}

output, err := pt.ExecuteBytes(context)
if err != nil {
  fmt.Fprintln(os.Stderr, err)
  os.Exit(1)
}
...省略...

これであとは目的別のテンプレートを用意すれば、パラメータファイルと一緒にkonfuに入力して設定ファイルをレンダリングすることができます。

最後に

どうでしょうか?いい感じではないでしょうか?

まあ、単一パラメータファイル方式にしても、パラメータに変更が入った後にレンダリングするの忘れたら意味ないんですけどね

(HOCONファイルにwatchかけて勝手にjson変換、設定ファイル再生成するようにしようか検討中です)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go getしてから、プログラムが実行できなくなり、解決するまでの話

経緯

実行環境はMacです。

Go環境を構築時、$GOPATHを明示的に指定せず構築してからプログラムを書き、実行していた。
外部パッケージをgo getで取得してプログラム内でインポートする必要が出てきた。
go get自体は実行だけなら問題なく出来たものの、その後go run foo.goがエラーで通らなくなった。

エラー内容で案内された通りのコマンド叩いても、反応無し
warningについては後述

エラー内容
human$ go run foo.go 
warning: GOPATH set to GOROOT (/usr/local/opt/go/libexec) has no effect
go: inconsistent vendoring in /usr/local/opt/go/libexec/src:
        go.mod requires github.com/stretchr/testify v1.4.0 but vendor/modules.txt does not include it.
        run 'go mod tidy; go mod vendor' to sync

$GOPATH$GOROOTを設定して、再度go run → 失敗
go getを再実行しても、最新だよと怒られる。

困った

結論

$GOPATH$GOROOTを適当に理解し、途中でイジイジしたせいでディレクトリ構造がイカれた。

修正の対応として、以下を実施
1. $GOPATHを設定&反映
2. エラー内容で案内されたコマンドを再度叩く
3. 100回目のgo run xxx.go再実行 → 成功

もう少し詳しく

そもそも、筆者は$GOPATH$GOROOTをあまり理解していなかった。
簡単な違いを以下に示します。

$GOPATH:
Goのワーキングディレクトリを指す。
自分で好きな場所を指定して良い。
そこでGoのプログラムを書き、実行する。
go getgo installを実行した時のインストール先となる。

$GOROOT:
Goのルートディレクトリを指す。
$GOROOTのPATHは改めて設定すべきではない
理由は、既に然るべき場所を参照しているから。
複数のGoバージョンを使うのであれば、設定が必要かも。

詳細は、以下参照

実践

筆者は$GOPATHと$GOROOTについて完全に理解したので、早速bash_profileを修正&反映する。

$ vi ~/.bash_profile

export GOPATH="$(brew --prefix golang)/libexec"
export PATH=$PATH:$HOME/go/bin
~
~
~
:wq

$ source ~/.bash_profile

go getで欲しかった外部パッケージを再度取得

$ go get xxxxxxx
go: downloading xxxx
go: downloading xxxx
go: extracting xxxx
...
...

エラーで案内されたコマンドを再度叩く
うんともすんとも言わなかったこのコマンドが、何やら喋り始めたが、無視。
※この時のログ取れてませんでした...

$ go mod tidy

$ go mod vendor

最後は、go run foo.goで実行確認をして、無事実行できることを確認。

経緯で書いていたwarningですが、あれは$GOPATH$GOROOTが同じディレクトリを指している場合に表示されるようです。
そうです、筆者はまだ完全に理解していませんでした

$GOPATH$GOROOTが同じディレクトリを指しているということは、Goのルートディレクトリで作業をしていることになるので、気持ち悪いです。

なので、しっかり分けてあげたところwarningも出なくなりました。

最後に

この問題で2時間くらい潰してしまったので、同じ状況に陥ってしまった方々の救いになれば...と。
これで明日からまたGoをイジイジ出来ます。

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goで祝日を判定するライブラリまとめ

はじめに

Yahoo!ファイナンスの更新が停止 12月23日が「天皇誕生日」のまま、運営元が設定ミス

関係者の皆様、対応おつかれさまでした。記事を見ているだけで胃が痛いです。
祝日や営業日を扱うサービスでは誰にでも起こりうるインシデントではないでしょうか。
というわけで、Goで祝日を扱うライブラリをまとめました。

祝日が判定できるライブラリ

najeira/jpholiday

https://github.com/najeira/jpholiday
2019年4月1日に天皇誕生日の修正がされていました。

holiday-jp/holiday_jp-go

https://github.com/holiday-jp/holiday_jp-go

kokardy/jpholiday

https://github.com/kokardy/jpholiday

pinzolo/flagday

https://github.com/pinzolo/flagday

yut-kt/goholiday

https://github.com/yut-kt/goholiday

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む