Write a Microservice via Go-Micro

I’ve learning Go-Micro around a month, that I wanna write an article to record what I have learnt. In case I forgot it, I can still pick it up by reading this article. What I have wrote about might be inaccurate, I’m very glad that you can correct it.

Architecture

Write a Microservice via Go-Micro - 1

my design about Microservices architecture

What is go-micro

Go-Micro is a pluggable GRPC frameworks, for building Microservices it’s wisely choice, because it’s really easy to working with it, also you can pick up others, but this time I choice Go-Micro to write demonstration code.

Example to microservices

Project structure

├── main.go #  srv program
├── proto # proto file
│   ├── basis.micro.go
│   ├── basis.pb.go
│   └── basis.proto
├── tracer #  racer lib
│   └── tracer.go
└── client #  client
    └── main.go

Build a simple GRPC service

To write a grpc service with Go-Micro, start with proto file, you should design what interface / datastruct this service provides

syntax = "proto3";
package proto;
service Basis {
    rpc Hello (Request) returns (Response);
}
message Request{
    string name = 1;
}
message Response {
    string msg = 1;
}

In this case I wrote a service named basis, and provide Hello function, and also defined two data structure Request && Response

Next, generate Go Code with protoc --proto_path=$GOPATH/src:. --micro_out=. --go_out=. basis.proto.

Now, we can write a service with those Go Code

package main

import (
    "time"
    micro "github.com/micro/go-micro"
    proto "github.com/[private information]/gomicroexample/basis/proto"
)
var (
    servicename = "go.micro.srv.basis"
)
// basis grpc handler
type basis struct {
}
// Hello process function for basis.hello
func (b *basis) Hello(ctx context.Context, req *proto.Request, rsp *proto.Response) error {
    rsp.msg := "Hello " + req.Name
    return nil
}
func main() {
    srv := micro.NewService(
        micro.Name(servicename), // service name
        micro.RegisterTTL(time.Second*30), // srv ttl in services discovery system
        micro.RegisterInterval(time.Second*10), // interval for re-reigster
    )
    srv.Init() // read some args (like reigstory / listen address etc...) from cli
    proto.RegisterSayHandler(service.Server(), new(basis)) // reigster your GRPC handler into srv
    if err = service.Run();err != nil { // run service
        log.Fatal(err)
    }
}

Download consul, run it with ./consul agent -dev, and run your srv with go run main.go, also you can add --registry_address=host:port to specify registry addres and port

# ./consul catalog services # now run consul catalog to look what we got
consul
go.micro.srv.basis

Build a GRPC client

Now to communicate with basis Srv, we need a client, below you can see my code

package main

import (
    "context"
    "fmt"
    "time"

    micro "github.com/micro/go-micro"
    "github.com/micro/go-micro/selector/cache"
    proto "github.com/[private information]/gomicroexample/basis/proto"
)
func main() {
    service := micro.NewService() // create new service
    service.Init() // read args from cli
    h := proto.SayServiceClient("go.micro.srv.basis", service.Client()) // get basis handler through service name
    result, err := h.Hello(context.Background(), &proto.Request{Name: "John"}) //  handler handler Hello interface with `Request` structure
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(result.Msg) // get result
}

Now run it with go run main.go, also you can add --registry_address=host:port too

# go run main.go
Hello John

Integrate roundrobin plugin into GRPC client

I said Go-Micro is a pluggable before, and roundrobin is a features in Go-Plugins, so you can easyly add it into your client, before we do it, for make sure roundrobin works, add some log into your Srv process function

  1. Srv
import (
    "github.com/micro/go-log"
)
// Hello process function for basis.hello
func (b *basis) Hello(ctx context.Context, req *proto.Request, rsp *proto.Response) error {
    msg := "Hello " + req.Name
    log.Infoln("Yoooo! I received it")
    return nil
}
  1. Client
import (
    "github.com/micro/go-plugins/wrapper/select/roundrobin"
)
func main() {
    service := micro.NewService(
        micro.WrapClient(roundrobin.NewClientWrapper()), // round robin plugin for client
    )
......

Then run another srv, then run ./consul monitor, it will show services reigster logs in your cli, below you can see two Srv reigsted in my consul, got different uuid means they are listenning different service port

2018/11/00 00:00:00 [INFO] agent: Synced service "go.micro.srv.basis-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"
2018/11/00 00:00:00 [INFO] agent: Synced service "go.micro.srv.basis-xxxxxxx1-xxxx-xxxx-xxxx-xxxxxxxx"

Now execute your client twice, I can sure you both Srv will print Yoooo! I received it in your command line

Integrate ratelimiter / circuitbreaker into GRPC client

Before we start write code, I want to talk about what exactly Ratelimter and Circuitbreaker is in Microservices

  • Ratelimiter

Ratelimiter is used to limit the speed of request number increase, to make sure Srv can handler specify number of requests, and slow down the new requests come in speed.

About principle you can take a look here Token bucket && leaky bucket

  • Circuitbreaker

Circuit breaker is use to blocking request when Srv is overload or unavailable, circuit breaker got a counter inside, it will record Srv response, after those response over a specific percentage, it will directly return service unavailable message to client, and after a few second, request can access circuit breaker to Srv again, if Srv still not recovering, then do it again until Srv ready to receive request.

Now let’s add ratelimiter into our client, for tests we can add time.Sleep into our Srv
* srv

import (
    "time"
)
// Hello process function for basis.hello
func (b *basis) Hello(ctx context.Context, req *proto.Request, rsp *proto.Response) error {
    time.Sleep(time.Second * 1)
    rsp.msg := "Hello " + req.Name
    return nil
}
  • client
import (
    rateplugin "github.com/micro/go-plugins/wrapper/ratelimiter/uber"
)
func main() {
    service := micro.NewService(
        // middlewares executes in reverse order
        micro.WrapClient(rateplugin.NewClientWrapper(1)),
    )
...

After this, start your client with go run main.go & make it running on background, after you start over 3 client in background, the first client will return in 1s approximately, but the remaining two clients will take more than time to get the reponse

Alright, then Circuitbreaker, I’ll use hystrix to achieve Circuitbreaker feature, Go-Micro already packaged hystrix in Go-Plugins, all we have to do is add it into our service’s Init function, set some properties of hystrix or you can use default properties.

import (
    hystrixplugin "github.com/micro/go-plugins/wrapper/breaker/hystrix"
    "github.com/afex/hystrix-go/hystrix"
)
func main() {
    // time for waiting command response
    hystrix.DefaultTimeout = 1200
    // how long to open circuit breaker again
    hystrix.DefaultSleepWindow = 2000
    // percent of bad response
    hystrix.DefaultErrorPercentThreshold = 10
    // how much request can be accessed in persecond
    hystrix.DefaultMaxConcurrent = 2
    how much requests to enable circuit breaker
    hystrix.DefaultVolumeThreshold = 1

    service := micro.NewService(
        // middlewares executes in reverse order
        micro.WrapClient(hystrixplugin.NewClientWrapper()),
    )
...

Still, use go run main.go & run your client twice quickly, the client might return hystrix: timeout, after 2 seconds, run your client again it will truen normal response in one second approximately

Tracing your code between client and server

I’m writing demonstration here, but in real project you might have many function calls, some of them are RPC calls, once something wrong, you might hard to deal with it, because in Microservice architecture every service are individual part, code tracing helps to pinpoint failure occur and find what cause poor performance.

before we start into tracing code, we need a Jaeger to collect all of our tracing infos, run jaeger I recommend you start it with docker

docker run -d --name jaeger -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p 5775:5775/udp -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 14268:14268 -p 9411:9411 jaegertracing/all-in-one:1.8

Port 6831/UDP is used to received tracing data, and 16686 is Jaeger Web UI, all tracer infos will displayed graphically in it.

Now we have to write a function to init a Tracer, because Srv and Client both depend it, so I wrote a NewTracer function in this case.

  1. tracer.go
package tracer

import (
    "io"
    "time"
    opentracing "github.com/opentracing/opentracing-go"
    jaeger "github.com/uber/jaeger-client-go"
    jaegercfg "github.com/uber/jaeger-client-go/config"
)

func NewTracer(servicename string, addr string) (opentracing.Tracer, io.Closer, error) {
    cfg := jaegercfg.Configuration{
        ServiceName: servicename, // tracer name
        Sampler: &jaegercfg.SamplerConfig{
            Type:  jaeger.SamplerTypeConst,
            Param: 1,
        },
        Reporter: &jaegercfg.ReporterConfig{
            LogSpans:            true,
            BufferFlushInterval: 1 * time.Second,
        },
    }
    sender, err := jaeger.NewUDPTransport(addr, 0) // set Jaeger report revice address
    if err != nil {
        return nil, nil, err
    }
    reporter := jaeger.NewRemoteReporter(sender) // create Jaeger reporter
    // Initialize Opentracing tracer with Jaeger Reporter
    tracer, closer, err := cfg.NewTracer(
        jaegercfg.Reporter(reporter),
    )
    return tracer, closer, err
}
  1. Srv
import (
    "context"
    "log"
    "time"

    ocplugin "github.com/micro/go-plugins/wrapper/trace/opentracing"
    "github.com/micro/go-micro/metadata"
    opentracing "github.com/opentracing/opentracing-go"
    proto "github.com/[private information]/gomicroexample/basis/proto"
    "github.com/[private information]/gomicroexample/basis/tracer"
    micro "github.com/micro/go-micro"
)

// Hello ...
func (s *Say) Hello(ctx context.Context, req *proto.Request, rsp *proto.Response) error {
    // get tracing info from context
    md, ok := metadata.FromContext(ctx)
    if !ok {
        md = make(map[string]string)
    }
    var sp opentracing.Span
    wireContext, _ := opentracing.GlobalTracer().Extract(opentracing.TextMap, opentracing.TextMapCarrier(md))
    // create new span and bind with context
    sp = opentracing.StartSpan("Hello", opentracing.ChildOf(wireContext))
    // record request
    sp.SetTag("req", req)
    defer func() {
        // record response
        sp.SetTag("res", rsp)
        // before function return stop span, cuz span will counted how much time of this function spent
        sp.Finish()
    }()
    rsp.Msg = "Hello " + req.Name
    return nil
}

func main() {
    t, io, err := tracer.NewTracer(servicename, "localhost:6831")
    if err != nil {
        log.Fatal(err)
    }
    defer io.Close()
    // set var t to Global Tracer (opentracing single instance mode)
    opentracing.SetGlobalTracer(t)

    service := micro.NewService(
        micro.Name(servicename),
        micro.RegisterTTL(time.Second*30),
        micro.RegisterInterval(time.Second*10),
        micro.WrapHandler(ocplugin.NewHandlerWrapper(opentracing.GlobalTracer())), // add tracing plugin in to middleware
    )

    service.Init()
    proto.RegisterSayHandler(service.Server(), new(Say))
    err = service.Run()
    if err != nil {
        log.Fatal(err)
    }
}
  1. Client
package main

import (
    "context"
    "fmt"
    "log"
    micro "github.com/micro/go-micro"
    "github.com/micro/go-micro/metadata"
    ocplugin "github.com/micro/go-plugins/wrapper/trace/opentracing"
    opentracing "github.com/opentracing/opentracing-go"
    proto "github.com/[private information]/gomicroexample/basis/proto"
    "github.com/[private information]/gomicroexample/basis/tracer"
)

func main() {
    // init tracer
    t, io, err := tracer.NewTracer("go.micro.client.basis", "localhost:6831")
    if err != nil {
        log.Fatal(err)
    }
    defer io.Close()
    // create service handler by go-micro client
    service := micro.NewService(
        micro.WrapClient(ocplugin.NewClientWrapper(t)),
    )
    c := proto.SayServiceClient("go.micro.srv.basis", service.Client())
    // create a empty context, generate tracer span
    span, ctx := opentracing.StartSpanFromContext(context.Background(), "call")
    md, ok := metadata.FromContext(ctx)
    if !ok {
        md = make(map[string]string)
    }
    defer span.Finish()
    // inject opentracing textmap into empty context, for tracking
    opentracing.GlobalTracer().Inject(span.Context(), opentracing.TextMap, opentracing.TextMapCarrier(md))
    ctx = opentracing.ContextWithSpan(ctx, span)
    ctx = metadata.NewContext(ctx, md)
    // record req && resp && err
    req := &proto.Request{Name: "bob"}
    span.SetTag("req", req)
    resp, err := c.Hello(ctx, req)
    if err != nil {
        span.SetTag("err", err)
        return
    }
    span.SetTag("resp", resp)
}

Yep, Your Jaeger , if there’s nothing wrong with your code, you might see the calling function infos in that link.

Leave a Reply

Your email address will not be published. Required fields are marked *