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
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
- 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
}
- 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.
- 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
}
- 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)
}
}
- 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.