- 投稿日:2019-12-24T23:04:53+09:00
うちの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.gopackage 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.gopackage 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, } }テストをしている時点でサンタさんが日本に来てしまったので、やってみたい人は来年以降楽しんでください。
- 投稿日:2019-12-24T22:56:48+09:00
Linux Network NamespaceをGoで操作する
TL;DR
- Go言語のgoroutineはdefaultではpreemptiveに動作するOS Threadが切り替わるのでOS Threadに強く紐づくlinuxのnamespace関連の操作を行うときは
runtime.LockOSThread()
しておく必要がある。1- 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のライブラリ自体は軽量なのでぜひとも実装自体を覗いてみてください。
- 投稿日:2019-12-24T21:45:53+09:00
【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の仕様を半ば強制に明文化することが可能になっています。
さらに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/
- 投稿日:2019-12-24T21:35:46+09:00
goでたくさんの画像のサイズを小さくしたい!!!!
はじめに
こんにちはRIN1208です。
本日はクリスマスイブですね!!自分は特に予定がなく部屋で過ごしてチキンを食べました!悲しい....
この記事は画像のサイズ圧縮をかなりの量をしなくてはいけなくなったのでその制作の途中経過です(深刻なアドベントカレンダーのネタ不足....)またこの記事はITRCアドベントカレンダーの24日目の記事になります。
開発環境
- mac
- goの実行環境
今回使用したパッケージ
インストール
$ 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()の部分
ここに関しては特に説明するとがないですが今回使用した理由としては大量の画像のためファイル名がかぶらないようにしたかったので使用しました。(名前は適当でよかったので)
終わりに
ここまで読んでいただきありがとうございます。今回ここに書いてあるのは制作途中にの物でありこれから並列処理を使用して大量の画像を処理していく予定です。
- 投稿日:2019-12-24T20:43:02+09:00
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
から、ここでログ出力されているのがわかります。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
を見つけることが出来ます。// 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にたどり着きました。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が具象として使用されています。
mackerel "github.com/mackerelio/mackerel-client-go"mackerel-container-agent内の責務として具体的なAPI Clientとしての役割は持たせず別ライブラリを利用する戦略をとっていることがわかりました。
retry処理がどうなっているか眺める
表題にあげた、リトライ処理はどうなってるんでしょうか。その中身は、以下のコードから読み解くことが出来ます。
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の「アプリケーション回復性パターン」というドキュメントにも、再思考パターンとして紹介されています。
まとめ
簡単でしたが、コードリーディングの一端をご紹介させていただきました。
- 投稿日:2019-12-24T16:00:46+09:00
GoでJSONを扱う
はじめに
この時期はネタに走った面白い記事が多くあるとは思うのですが、私は自分の知識の確認のためにgoでのjsonの扱い方について書いていこうと思います!
jsonを構造体(struct)にする
まずはjson -> structにする方法です。これをするにはgoの標準パッケージであるjson(encoding/json)の
json.Unmarshal
を使います。json-->structtype 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-->jsontype 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をカスタマイズする方法もあったのですが、クリスマスに悲しく記事を書くのに疲れたので今回はここまでにしておきます(妥協)
- 投稿日:2019-12-24T15:18:38+09:00
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/fresh や https://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.yml
のvolumes
に.:/go/src/app
と書かれている状態になります )1.
rebirth
をインストールするまず、
GO111MODULE=on go get -u github.com/goccy/rebirth/cmd/rebirth
でrebirth
CLI をインストールします2.
rebirth
自身のクロスコンパイル
rebirth
を実行した際にはじめに行うのは、ターゲットとなる Dockerコンテナのアーキテクチャ向けに自分自身をクロスコンパイルし、__rebirth
という名前で~/work/app
直下の.rebirth
ディレクトリ配下に置きますコンパイル対象のアーキテクチャを知るため、 https://godoc.org/github.com/docker/docker/client を利用して docker remote API 経由で
go env GOOS
とgo env GOARCH
を実行しています。3. コンテナ上にクロスコンパイルした rebirth バイナリを配置する
.rebirth
があるディレクトリは~/work/app
直下なので、コンテナ上にマウントされています。
このため、自動でコンテナ上の/go/src/app/.rebirth
配下にコンテナ上で実行可能な__rebirth
バイナリが配置されます。( このあたりは、マウントを利用せずともバイナリを直接コンテナ上にコピーすれば同じことができますが、大抵の場合はマウントを前提としても問題ないと思っているので、処理を簡単にするためにこのようにしています )
4. アプリケーションのファイルを監視し始める
図では
main.go
のファイルイベントを監視し始めることを表しています
( 実装には fsnotify を使っています )5. アプリケーションコードをクロスコンパイルする
rebirth
自身をクロスコンパイルしたときと同じ要領で、コンテナのアーキテクチャ向けにクロスコンパイルします。
少し違うのは、アプリケーションコードがcgo
を利用したものであったとしてもコンパイルできるようにしなければらない点です。このため
GOOS
やGOARCH
の指定に加え、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.mod
にmodule github.com/company/webapp
と書かれていたとすると、
.rebirth/src/github.com/company
というディレクトリを掘る- ワークスペース (
webapp
ディレクトリ ) への symlink を 1 で作ったディレクトリの配下に作る.rebirth/src/github.com/company/webapp
へ移動する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.yml
にhost.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.a
やzlib
のヘッダファイルを参照可能な場所に移して ( たとえば ワークスペース配下 )
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
より使いやすくなっていると思いますので、ぜひお試しいただければ幸いです。引き続き改善しながら弊社で使っていこうと思っていますので、
なにか要望やバグを見つけた場合も気軽に報告いただければと思います。それではよいクリスマスをお過ごしください!!
- 投稿日:2019-12-24T13:59:32+09:00
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 コード数の少なさに改めて驚く。
- 投稿日:2019-12-24T13:19:28+09:00
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は動的型付け言語のため、変数の型を宣言する必要はありません。
Pythonn = 10 name = "hoge" # まとめて定義する x, y, z = 1, 2, 3 a = b = c = 1Goの場合は、まず変数名の1文字目に注意すべきです。
- 1文字目が大文字の場合はパッケージ外からアクセスできる
- 1文字目が小文字の場合はパッケージ外からアクセスできない
これは定数・関数にも言える事です。
Goは静的型付け言語だが、明示的な定義と暗黙的な定義が存在します。明示的な定義
var [変数の名前] [変数の型]
のように定義します。Govar 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 [変数の名前] = [値]
のように定義します。値を代入すると変数の型は暗黙的に推論されます。Gon := 1 // varを用いた定義でも型が省略できる var n = 1 // まとめて定義する x, y, name := 1, 2, "hoge" var ( x = 1 y = 2 name = "hoge" )定数
Pythonには、定数を定義するためのキーワードはありません。慣例として大文字とアンダーバーのみで定数を表しています。
PythonPI = 3.14 MAX_NUM = 100Goでは
const
を使用して定数を定義します。iota
という識別子を使えば、整数の連番を生成できます。
定数の値を変更しようとするとエラーになります。Goconst 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
を使用します。Pythonx, 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でそれっぽい書き方ができます。
論理演算子は&&
,||
,!
を使用します。Gox, 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]ループ
Pythonsum = 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: breakGoのループはforしかないが、whileのような制御もできます。
Gosum := 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
で定義します。関数の定義が関数呼び出しの実行よりも前に書かなければなりません。
以下のような使い方があります。
- 引数にデフォルト値を持たせることができる
- キーワード引数を使って関数を呼び出すことができる
- 可変長引数を指定することができる
- 複数の戻り値(タプル型)を返すことができる
Pythondef 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
キーワードを付けた文は、関数が終了する際に実行される。複数定義した場合は最後の方から呼ばれるGofunc 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
構文を使用します。Pythondef 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 # finallyGoには
try-except
のような例外機構が存在しません。代わりに、関数の戻り値を複数返せる特性を利用して、エラーが発生したかどうか(errorインターフェース
)を戻り値の一部として返却することによってエラーの検知を実現しています。
errorインターフェース
は以下のように定義されています。
https://golang.org/pkg/builtin/#errorGoのerrorインターフェースtype error interface { Error() string }以下の例では
errors
パッケージのNew
関数を使ってerror型
を生成しています。
また、defer
を使うことでPythonのfinally
と同じような動きが実現できます。Gopackage 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クラスの例です。
Pythonclass 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) # 90Goには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
モジュールを使ってスレッドを生成し、キューでデータを受け渡す簡単な例です。Pythonimport 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 [データ型]
のように書きます。Gopackage 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 について詳しく書けませんでしたが、また今度書いてみたいと思います。
- 投稿日:2019-12-24T12:43:57+09:00
Go で 円や線の画像を生成する
この記事はtomowarkar ひとりAdvent Calendar 2019の23日目の記事です。
はじめに
中学で習う点と直線の距離を駆使して円や直線の描画を実装していこうという記事になります。
以下のような画像をフレームワークなどは使わずに生成していきましょう!!
コード
メイン関数(抜粋)
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
- 投稿日:2019-12-24T11:48:20+09:00
AWSの利用料金をChatworkに通知してくれるバッチをGoで作った
はじめに
AWSの料金を毎日確認しにログインしてコンソールを開くのは面倒…。でも、気がついたら設定が間違っててすごい料金を請求されるなーんてことも。
そんなことが起こる前に、毎日使用料金を通知して把握しておこう!
ということで、Cost ExplorerのSDKを使ってGoでバッチを作成してみました。
AWSの構成
当記事はLambdaにアップするバッチにフォーカスしてます。
実装
通知したいこと
- 昨日の利用料金
- 今月の利用料金(今月1日から昨日までの合計金額) or 今日が1日のときは先月の利用料金
必要なもの
- Chatworkのルームid(トークルームURL:chatwork.com/#!rid********* ←9桁の数字)
- ChatworkのAPIトークン(画面右上の自分の名前 → API設定で発行できます)
気をつけるところ
料金を知るのにもお金がかかります。
Cost Explorerに対して1リクエストあたり 0.01USD かかります。ソースコード
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となっています。
こちらは当月(2019/12/01~2019/12/15)の合計金額です。指定した期間は2019/12/01から2019/12/16で、費用は約1.35USDという結果が得られました。
解説
コストの取得
今回は、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: コストと使用状況のページの右下、詳細オプション → コストの表示方法 → 非ブレンド純コスト と同額になります。
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)実行結果
おわりに
コンソール上だと下2桁しか表示されないですけど、Cost Explorer APIで取得すると下10桁まで表示されるので、コンソール上では0.00USD。でも実際は0.0012345678USDってことも分かるのでいいですよね。
大変だったとこととしては、はじめましてのGo。C言語を昔授業でちょっとやっていたけどポインタから逃げたので、今となってポインタに苦しめられました。
あとは、AWSのSDKを使う時に公式ドキュメントを読んでコードを書くことが大変でした。あまりGoでCostExplorerを使っている記事がなくて、自力では完成させることができず…。SDKの使い方、Goの書き方、とても勉強になりました。
次は藤原さんのQiitaの記事を見ながら、slackに通知できるようにしてみようと思っています!
- 投稿日:2019-12-24T10:30:59+09:00
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
- 投稿日:2019-12-24T08:40:52+09:00
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ファイルを指定すると、こんな感じで出力されます。
tar
とtar.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. 実データからチェックサム計算をしています。
- 投稿日:2019-12-24T04:26:09+09:00
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は環状queue
buf
、 バッファ内の書き込み位置のindexsendx
と読み込み位置のindexrecvx
、ロックを取得するためのmutexlock
、チャンネルにread/write待ちのgoroutine情報のリストrecvq
とsendq
、チャンネルが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の読み込み・書き込みの時の処理
はじめに一つ目のgoroutine
main
関数が我がチャンネルに何かを送る。
1. mainがawesomeChanの中のhchanのロックを取得
2. hchanのバッファに値を追加してくれる(実際にバッファーのメモリー中にのコピーを行う)
3. 追加後、ロックを解除する今度、受け取り側のgoroutine
processNumbers
関数がmainから送られてきた値を受け取る。
1. processNumbersがawesomeChanの中のhchanのロックを取得
2. hchanのバッファから値を取り出す(awesomeNumber変数の指しているメモリー位置の中に値をコピーする)
3. ロックを解除する*チャンネルから読み込み、チャンネルへの書き込み時ともにメモリーのコピー(上記の
2.
)が行われるので、hchanのmutexによって保護されているバッファ以外、二つのgoroutineが共有するメモリーがない。これはgoroutine同士のメモリー安全性を担保できる一つのメカニズムます。Case 2) bufferの容量がいっぱいになった時の処理
受け取り側の関数の処理時間が長かったり、チャンネルのバッファへの値の書き込み速度に追いつかなかったりする状況があったとします。
- バッファの容量がいっぱいになりバッファに書き込みできなかった
- main関数の実行が止まって待つ(blocking処理)
- 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にあるsudog
sg
を外し、待っている値をバッファに追加する:// 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の中に値がないのに受け取りを待っている関数がある
- processNumbersがawesomeChanから読もうとするができない
- processNumbersがsudogを作り、resume状態を登録し、そのsudogをチャンネルのrecvqの中に入れる
- processNumbersがスケジューラーに(gopark自分)シグナルを送り、チャンネルにsendがあるまでブロックされる
- 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
- 投稿日:2019-12-24T01:26:01+09:00
設定ファイル統一戦争勃発(もしくはHOCONのススメ
こんにちは、Wano株式会社でVeleTという動画広告配信サービスのエンジニアをやっております。stk0724です。
この記事はWanoグループ Advent Calendar 2019の24日目となります。
仰々しいタイトルですが、なんのことはありません。設定ファイル管理をどのようにいい感じにしたかについてお話します。
顛末
結論から先に言うと、統一してないです。
大本となるjsonファイルをパラメータファイルとして、用途別のテンプレートファイルから各種設定ファイルをレンダリングする方式をとりました。
前提: 設定ファイル、いっぱいありますよね?
webアプリケーションフレームワークやら、各種ミドルウェアやらで最近のサービス開発では複数種類の設定ファイルを扱うことになると思います。
VeleTの場合ですと、
- 管理画面の設定ファイル
- nginxの設定ファイル
- fluentdの設定ファイル
などがあります。
設定ファイルがバラけると、どうなる?
設定ファイル毎に全く異なる値を管理しているのであれば問題ないと思うのですが、同じ意味合いの項目が複数の設定ファイルに存在していると、変更が入った場合の反映漏れが怖いです。
これに本番用、開発環境用、ローカル用など、環境別に必要なことを考えると、
設定ファイル種別 * 環境数の数の設定ファイル管理が必要となります。
これを手動で管理するのはちょっと厳しいです。
ソリューション: 単一パラメータファイルから生成したらよくね?
単一の設定ファイル読み込ませるようにしたら、反映漏れとか発生しないですよね。
じゃあ設定ファイル統一しよう!!
ってのはムリですよね。設定ファイルのフォーマットそれぞれ異なりますし。
なので、環境別に単一パラメータファイルを用意して、なんらかのテンプレートライブラリで設定ファイルテンプレートを書き、パラメータファイルを読み込ませてからレンダリングして作ればいいのではと考えました。
以下のような流れになります。
- HOCONによるパラメータファイルをjsonに変換する
- pongo2テンプレートファイルのレンダリングコマンドにパラメータファイルとパラメータファイルを入力して設定ファイル生成
単一パラメータファイルにHOCONフォーマットを利用する
現在VeleTでは管理画面をScala + Playframeworkでリニューアル中でして、新管理画面ではHOCONフォーマットで設定ファイルを書いています。
HOCONフォーマットはlightbend社が開発したフォーマットで、Playframweorkの他、lightbend社が開発したOSSなどで利用されています。
HOCONの利点
Playframeworkを使っている関係から半分成り行きでHOCONを利用することにしたのですが、HOCONにはjson, yamlなどと比較して大きな利点があります。
- 他のHOCONファイルをincludeできる
- 既存設定値を上書きできる
1はそのままの意味です。2の利点がとても大きく、ローカル環境用の設定ファイルをベースとして、環境別に上書きが必要な項目のみincludeするといったような書き方ができます。
例えば以下のような感じです。
base.confdomain = "localhost" ...その他いろいろ...dev_domain.confdomain = "dev.velet.jp"prod_domain.confdomain = "velet.jp"local.confinclude "base.conf"dev.confinclude "base.conf" include "dev_domain.conf"prod.confinclude "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コマンドですが、以下で公開しています。
やってることはとても単純で、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変換、設定ファイル再生成するようにしようか検討中です)
- 投稿日:2019-12-24T01:00:33+09:00
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 get
やgo 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をイジイジ出来ます。以上です。
- 投稿日:2019-12-24T00:02:05+09:00
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