Golang微服务笔记

Golang微服务相关技能点整理,从protobuf到gRpc,再到golang的各个微服务框架实践与探索笔记。

Part1: 什么是微服务

依然记得刚毕业那会,大家还不会像今天这样热衷于微服务架构。那时候更多的是类似于企业ERP的风格,即一个项目所有的功能全部部署在一起,当然,代码也全部在一个项目下维护和修改,这种大杂烩的风格确实是有一些好处的,比如容易部署,所有功能都在同一个项目内,这意味着不同模块之间的功能调用更为简单,因为可以直接调用相应的方法即可。但是由于功能太大太杂,会导致模块与模块之间的耦合性太大,当不同项目需要用到同样或者相似的功能时,就会出现无法复用而只能重新写一个之前写过的功能的情况。不仅如此,在运维上,比如负载均衡,可伸缩等等,一个杂糅在一起的项目也增大了运维的难度。

所以,服务的重构就开始了。

同样是一个很庞大的项目,但是会进行功能拆分,将相对比较独立的功能单独封装成一个服务。这样,当后续有其他项目要使用到该功能时,直接由该服务提供。这样的好处是,服务复用。因为基于单一职责(23中重构方法的一种),所以对一个单独的服务维护比较简单,在部署上,也可以借助负载均衡技术对服务进行可伸缩的动态部署,以及更新过程中不会影响到其他服务,从而保证其服务的性能以及稳定性,这在当今热火朝天的云服务技术的大背景下,这种重构方式已经获得了世界开发者的青睐。

很明显,相比传统方式,这种重构方式还是有弊端的。

比如,服务于服务之间的通讯只能通过接口交互来实现了(当然也可以通过Socket,或者TCP交互等等),这就无形地耗费了更多的资源和性能。在部署上,部署的步骤也更加繁琐,因为设计到很多服务的创建与更新。

Part2: 微服务的设计

在人月神话中,有一个非常经典的案例,那就是康威定律(Conway’s Law)。一个好的微服务架构取决于一个好的架构设计。一个好的设计又取决于一个好的团队沟通方式。一个团队的沟通成本,类似于完全图的边长数量,即:

沟通成本 = (n*n-1)/2

当然,这也不是我说的,是在人月神话中非常经典的一个案例。Dunbar Number中指出,一个人最多只能维持150人的照面朋友联系,而亲密朋友,一般只有5个人左右。这很有意思,是一个叫做邓巴的生物学家提出的,但是体现在沟通上,好像正符合我们微服务的沟通方式。

总而言之,微服务的设计对团队沟通有很大的要求。

Part3: 微服务通讯方式

微服务之间一般通过HTTP或者RPC来通讯,一般来说,更为高效的是使用RPC协议来实现服务之间的内部通讯,使用HTTP或者socket来实现服务整体与前端或者外部系统的通讯。

Part4: Golang对RPC的支持

RPC是远程过程调用的意思,直观上理解就是一台机器上的服务调用一台机器上的另外一个服务内部的方法。Golang原生已经提供了rpc包来实现RPC服务。

首先我们定义通讯的方法,即通讯实体,这里logic提供了添加参数的方法Add

package logic

type Params struct {
    A int
    B int
}

type Result struct {
    C int
}

type Logic struct {
}

func (self *Logic) Add(params *Params, result *Result) error {
    result.C = params.A + params.B
    return nil
}

接下来我们构建服务端,服务端就是注册这个实体,并通过RPC协议暴露出来。

package main

import (
    "demo_test/rpc_demo/logic"
    "fmt"
    "net/http"
    "net/rpc"
)

func main() {
    rpc.Register(new(logic.Logic))
    rpc.HandleHTTP()
    err := http.ListenAndServe(":1234", nil)
    if err != nil {
        fmt.Println(err.Error())
    }
}

接下来我们构建客户端的代码,客户端直接调用服务端的logic.Add方法,并将需要的参数一次传递。

package main

import (
    "demo_test/rpc_demo/logic"
    "fmt"
    "net/rpc"
)

func main() {
    client, err := rpc.DialHTTP("tcp", "127.0.0.1"+":1234")
    if err != nil {
        panic(err)
    }
    params := &logic.Params{A: 1, B: 2}
    result := &logic.Result{}

    err = client.Call("Logic.Add", params, &result)
    if err != nil {
        panic(err)
    }

    fmt.Println(result.C)
}

所以,是不是So easy,其实本质上RPC就是这样的一个过程,和HTTP相比,RPC需要客户端和服务端共用一个协议通讯实体,即例子中的logic.Logic。这个在gRPC中则体现为*.pb.go

Part5: Protobuf

Protobuf(Google Protocol Buffers)是Google提供一个具有高效的协议数据交换格式工具库(类似Json),但相比于Json,Protobuf有更高的转化效率,时间效率和空间效率都是JSON的3-5倍。Protobuf提供了多语言的支持,协议代码是语言无关的,但也不类似于DSL(个人觉得DSL是非常难用的)。proto文件定义了协议数据中的实体结构(message ,field)。具体的规则,这里不再描述,可以阅读protobuf的官方文档。Protobuf序列化后直接形成二进制流,所以在空间上比普通的JSON和XML直接序列化为字节的空间占用要小的多,这也是为什么proto的效率要比普通JSON高的原因。

Protobuf的安装很简单,包含安装protocprotoc-gen-go,完整安装脚本如下:

# 安装protoc
# 所有版本都在https://github.com/protocolbuffers/protobuf/releases/中可以找到
$ wget https://github.com/protocolbuffers/protobuf/releases/download/v3.7.0rc2/protobuf-all-3.7.0-rc-2.tar.gz
$ tar zxvf protobuf-all-3.7.0-rc-2.tar.gz
$ cd  protobuf-all-3.7.0-rc-2
$ ./configure

# 如果报错,请先执行sudo apt-get install build-essential
$ make
$ make install
$ protoc -h

# 如果报以下错误:
#
#    protoc: error while loading shared libraries: libprotoc.so.18: 
#    cannot open shared object file: No such file or directory
#
# 执行 export LD_LIBRARY_PATH=/usr/local/lib 
# 或者 sudo ldconfig

# 安装golang版本protobuf插件protoc-gen-go
# 用于将.proto文件编译为.go文件
$ git clone git@github.com:golang/protobuf.git && (cd protobuf && git checkout v1.2.0 && go build -o $BIN_PATH/protoc-gen-go ./protoc-gen-go) && rm -r protobuf

既然安装成功了,我们来编写一下测试程序,将*.proto编译为*.go

syntax = "proto3";
package proto_buf;

message Header{
    string messageId = 1;
    string topic = 2;
}
message BytesMessage{
    Header  header= 1;
    bytes body = 2;    
}
message StringMessage{
    Header header = 1;
    string body = 2;
}

执行编译命令

$ anderson@anderson:~/workspace/src/demo_test/proto_buf$ protoc --go_out=. *.proto

此时,生成demo.pb.go。这个文件提供了一些序列化和反序列化的方法给我们使用。在RPC中我们将数据使用proto来进行序列化和反序列化,从而提升传输效率。

Part4: 常见的RPC框架之gRPC

gRPC是google开发的一款RPC框架,一般也用Protobuf来优化数据序列化的性能(对比普通的JSON有着性能上的优势)。并这普通RPC的基础上提供了流式RPC(普通RPC,客户端流式PRC,服务端流式RPC以及双向流式PRC),gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。

除此之外,还有thrift等。

在gRPC中,我们可以定义四种类型的服务格式:

//普通RPC格式,这和普通的接口调用没有区别,接收Header类型的参数并返回Header类型的数据
service Demo {
    rpc Get(Header) returns (Header) {}
}

//服务端流式RPC格式,服务端返回以二进制流的形式返回给客户端,用stream关键字标识即可
service StreamDemoServer {
    rpc GetSteam(BytesMessage) returns (stream BytesMessage) {}
}

//客户端流式RPC格式,客户端以二进制流的形式发送参数给服务端
service StreamDemoClient {
    rpc GetSteam(stream BytesMessage) returns (BytesMessage) {}
}

//客户端和服务端双向流式RPC格式
service StreamDemoBoth {
    rpc GetSteam(stream BytesMessage) returns (stream BytesMessage) {}
}

同样编写*.proto文件,只是从生成命令上,我们选择grpc插件模式:

$ anderson@anderson:~/workspace/src/demo_test/proto_buf$ protoc --go_out=plugins=grpc:. demo.proto

生成的代码包含Server端和Client端的代码,实际上是两个接口,默认有客户端的实现,而服务端只能我们自己写。现在我们来写下服务端的代码,实现Server接口:

package proto_buf

import context "golang.org/x/net/context"

type MyServer struct{}

func (self *MyServer) Get(ctx context.Context, header *Header) (*Header, error) {
    return header, nil
}

接下来我们来提供grpc服务:

package main

import (
    "demo_test/proto_buf"
    "log"
    "net"

    grpc "google.golang.org/grpc/grpc-go"
    "google.golang.org/grpc/grpc-go/reflection"
)

func main() {
    lis, err := net.Listen("tcp", ":6000")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &proto_buf.MyServer{})
    reflection.Register(s)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

接下来是客户端调用:

package main

import (
    "log"
    "os"

    "golang.org/x/net/context"
    "google.golang.org/grpc"
    pb "demo_test/proto_buf"
)

func main() {

    conn, err := grpc.Dial("localhost:6000", grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)
    r, err := c.Get(context.Background(), &pb.Header{})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.Message)
}

Part5: 服务发现与治理策略

目前比较多的是用Etcd或者Consul来进行服务的发现与注册。

Part6: 微服务的维护与部署

部署一般用docker和k8s。

赞赏我吗