其他分享
首页 > 其他分享> > Go-micro微服务

Go-micro微服务

作者:互联网

Go微服务

服务拆分原则 :高内聚低耦合

简而言之,微服务架构风格是将单个应用程序作为一组小型服务开发的方法,每个服务程序都在自己的进程中运行,并与轻量级机制(通常是HTTP资源API)进行通信。这些服务是围绕业务功能构建的。可以通过全自动部署机器独立部署。这些服务器可以用不同的编程语言编写,使用不同的数据存储技术,并尽量不用集中式方式进行管理

微服务架构是将复杂的系统使用组件化的方式进行拆分,并使用轻量级通讯方式进行整合的一种设计方法。

微服务是通过这种架构设计方法拆分出来的一个独立的组件化的小应用。

1. 单体式服务与微服务对比

​ 和微服务架构相反的就是单体式架构,我们来看看单体式架构设计的缺点,就更能体会微服务的好处了。单体架构在规模比较小的情况下工作情况良好,但是随着系统规模的扩大,它暴露出来的问题也越来越多,主要有以下几点:

1.1 单体式服务

即以往大家熟悉的服务器

特性:

1.2 微服务

优点:

缺点:

功能 传统单体架构 分布式微服务化架构
部署 不经常而且容易部署 经常发布,部署复杂
隔离性 故障影响范围大 故障影响范围小
架构设计 初期技术选型难度大 设计逻辑难度大
系统性能 相对时间快,吞吐量小 相对时间慢,吞吐量大
系统运维 运维难度简单 运维难度复杂
新人上手 学习曲线大(应用逻辑) 学习曲线大(架构逻辑)
技术 技术单一而且封闭 技术多样而且容易开发
测试和差错 简单 复杂(每个服务都要进行单独测试,还需要集群测试)
系统扩展性 扩展性差 扩展性好
系统管理 重点在于开发成本 重点在于服务治理和调度

2. RPC协议

RPC(Remote Procedure Call Protocol),是远程过程调用的缩写,通俗的说就是调用远处的一个函数,属于应用层协议。底层使用TCP实现

1.本地函数调用:

result := Add(1,2)

​ 我们知道,我们传入了1,2两个参数,调用了本地代码中的一个Add函数,得到result这个返回值。这时参数,返回值,代码段都在一个进程空间内,这是本地函数调用。

2.RPC远程调用

通过RPC协议,传递:函数名 函数参数,达到在本地,调用远端函数,得到返回值到本地的目的。

我们调用一个跨进程(所以叫"远程",典型的事例,这个进程部署在另一台服务器上),来得到对应返回值。

像调用本地函数一样,去调用远程函数

我们使用微服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现公司跨团队的技术解耦,如下图:

​ 这样的话,如果没有统一的服务框架,RPC框架,各个团队的服务提供方就需要各自实现一套序列化、反序列化、网络框架、连接池、收发线程、超时处理、状态机等“业务之外”的重复技术劳动,造成整体的低效。

为什么微服务需要使用RPC?

2.1 RPC入门使用

socket通信:

RPC通信步骤

2.2 RPC相关函数

2.3 RPC测试demo

server

package main

import (
	"fmt"
	"net"
	"net/rpc"
)

type World struct{
}

//给结构体对象绑定方法 该方法必须满足前述4个条件
func (this *World)HelloWorld(name string,resp *string)error{
	*resp=name+" 你好...."
	return nil
}

func main() {
	//1. 注册 RPC 服务对象,给对象绑定对应方法
	err:=rpc.RegisterName("world",new(World))
	if err != nil {
		fmt.Println("rpc registername err:",err)
		return
	}

	//2.设置监听器
	listener,err:=net.Listen("tcp","127.0.0.1:8080")
	if err != nil {
		fmt.Println("net listen err:",err)
		return
	}
	defer listener.Close()
	fmt.Println("设置监听器成功")
    
	//3.建立连接
	conn,err:=listener.Accept()
	if err != nil {
		fmt.Println("listen accpet err:",err)
		return
	}
	fmt.Println("连接建立成功")

	//4.将连接绑定 RPC 服务
	rpc.ServeConn(conn)
}

client

package main

import (
	"fmt"
	"net/rpc"
)

func main() {
	//1. 用RPC 连接服务器
	conn,err:=rpc.Dial("tcp","127.0.0.1:8080")
	if err != nil {
		fmt.Println("rpc dial err:",err)
		return
	}
	defer conn.Close()

	//2.调用远程函数
	var ans string
    //第二个参数是传入参数,即传参给HelloWorld的name,然后HelloWorld的resp是返回值,用ans来承接
	err=conn.Call("world.HelloWorld","李白",&ans)
	if err != nil {
		fmt.Println("conn call err:",err)
		return
	}
	fmt.Println(ans)
}

2.4 json版RPC

使用不同方法会收到乱码数据

产生原因:

​ 在数据通信过程中,会伴随本机字节序和网络字节序相互转化,因此需要进行大端小端序列化操作。乱码即是可能一端进行序列化操作,而另一端没有进行序列化操作。此处RPC使用go语言特有的数据序列化gob,其他编程语言无法解析,因此会产生乱码现象。

因此要避免乱码现象,即客户端和服务端需要使用通用的序列化方法 --- json、protobuf

2.4.1 修改client端

client端修改

//conn,err:=rpc.Dial("tcp","127.0.0.1:8080")
conn,err:=jsonrpc.Dial("tcp","127.0.0.1:8080")	//即将rpc修改jsonrpc 其他一样

2.4.2 修改服务端

server端修改

//rpc.ServerConn(conn)
jsonrpc.ServerConn(conn)

2.5 RPC的封装

​ 不进行封装的话,这种编译只有在运行阶段才会报错。因此为了在编译阶段就能够成功排查错误,对客户端和服务端分别进行封装。

MyClient实现了MyInterface的全部方法,因此实现了继承

即实现了空接口类型的全部方法,才能对传参为空接口类型的方法进行传参

//定义一个空接口类型
type IFace interface {
	add(int,int)int
}

//传参为空接口类型的方法
func sum(i IFace){
	fmt.Println("这里传参是空接口类型")
}

type test1 struct {
}

//定义了test1类实现了空接口类型的方法
func (t1 *test1) add(a int,b int)int  {
	return a+b
}
//定义了test2类没有实现了空接口类型的方法
type test2 struct {
}

func main() {
	//继承了空接口类型的可以给 传参为空接口类型的方法 传参
	sum(&test1{})
	sum(new(test1))
	
	//没有继承了空接口类型的可以给 传参为空接口类型的方法 传参
	sum(&test2{})	//报错
	sum(new(test2))	//报错
}

3. Protobuf

Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做序列化和反序列化数据存储RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

Protobuf刚开源时的定位类似于XML、JSON等数据描述语言,通过附带工具生成代码并实现将结构化数据序列化的功能。这里我们更关注的是Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言RPC接口的基础
工具。

需要了解两点

  1. protobuf是类似与json一样的数据描述语言(数据格式)
  2. protobuf非常适合于RPC数据交换格式

接着我们来看一下protobuf的优势和劣势:

**优势: **

1:序列化后体积相比Json和XML很小,适合网络传输

2:支持跨平台多语言

3:消息格式升级和兼容性还不错

4:序列化反序列化速度很快,快于Json的处理速度

劣势:

1:应用不够广(相比xml和json)

2:二进制格式导致可读性差

3:缺乏自描述

windows下安装

需要将protoc.exeprotoc-gen-go.exe添加到path

3.1 protobuf简单语法

参考文档(需FQ):https://developers.google.com/protocol-buffers/docs/proto3

例子:

syntax = "proto3";

//指定所在包名
option go_package="../pb"; //在当前pb文件下生成.go文件   //不加会报错

//定义枚举类型
enum Week{
    Monday=0; //枚举值必须从0开始
    Tuesday=2;
}

//定义消息体
message Student{
    int32 age = 1;  //可以不从1开始  但是不能重复
    string name= 2;
    People p=3;
    repeated int32 score=4; //定义数组

    //枚举值
    Week w=5;

    //联合体
    oneof data{
      string teacher =6;
      string class=7;
    }
}

//消息体可以嵌套
message People{
    int32 weight=1;
}

执行命令:

protoc --go_out=./ *.proto

eg:即可根据add.proto文件生成对应的add.pb.proto 文件名中间嵌入包名

3.2 添加RPC服务

利用proto生成服务的go文件

/////客户端
type AddClient interface {
	Sum(ctx context.Context, in *Numb, opts ...grpc.CallOption) (*Ans, error)
}

type addClient struct {
	cc grpc.ClientConnInterface
}

func NewAddClient(cc grpc.ClientConnInterface) AddClient {
	return &addClient{cc}
}

func (c *addClient) Sum(ctx context.Context, in *Numb, opts ...grpc.CallOption) (*Ans, error) {
	out := new(Ans)
	err := c.cc.Invoke(ctx, "/add/sum", in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

//////服务端
type AddServer interface {
	Sum(context.Context, *Numb) (*Ans, error)
}

// UnimplementedAddServer can be embedded to have forward compatible implementations.
type UnimplementedAddServer struct {
}

func (*UnimplementedAddServer) Sum(context.Context, *Numb) (*Ans, error) {
	return nil, status.Errorf(codes.Unimplemented, "method Sum not implemented")
}

func RegisterAddServer(s *grpc.Server, srv AddServer) {
	s.RegisterService(&_Add_serviceDesc, srv)
}

4. GRPC

gRPC 官方文档中文版:http://doc.oschina.net/grpc?t=60133

gRPC官网:https://grpc.io

实现两个数相加

示例:

服务端:

package main

import(
	"context"
	"fmt"
	"google.golang.org/grpc"
	"awustjq/rpclearn/pb"
	"net"
)
//定义类
type Server struct {
}

//按接口绑定类方法
func (this *Server)Do(ctx context.Context, p *pb.Person) (*pb.Person, error){
	p.Name+="is sleeping"
	p.Age+=666
	return p,nil
}

func main(){
	//初始化一个grpc对象
	grpcServer:=grpc.NewServer()

	//注册服务
	pb.RegisterDelPersonServer(grpcServer,new(Server))

	//设置监听 指定IP Port
	listener,err:=net.Listen("tcp","127.0.0.1:8080")
	if err != nil {
		fmt.Println("net listen err:",err)
		return
	}
	defer listener.Close()

	//启动服务
	grpcServer.Serve(listener)
}

客户端:

package main

import (
	"awustjq/rpclearn/pb"
	"context"
	"fmt"
	"google.golang.org/grpc"
)


func main() {
	//连接grpc
	grpcConn,err:=grpc.Dial("127.0.0.1:8080",grpc.WithInsecure())
	if err != nil {
		fmt.Println("grpc dial err:",err)
		return
	}
	defer grpcConn.Close()

	//初始化grpc客户端  完成初始化
	grpcClient:=pb.NewDelPersonClient(grpcConn)

	//func (c *delPersonClient) Do(ctx context.Context, in *Person, opts ...grpc.CallOption) (*Person, error)
	//调用远程服务
	newPerson,err:=grpcClient.Do(context.TODO(),&pb.Person{Name: "李白",Age: 333})
	if err != nil {
		fmt.Println("client do err:",err)
		return
	}
	fmt.Println(newPerson)
}

5. Go-micro框架

Micro是一个专注于简化分布式系统开发的微服务生态系统。由开源库和工具组成。主要包含以下几种库:

其他各种库和服务可以在github.com/micro找到。

5.1 服务发现

我们在做微服务开发的时候,客户端的一个接口可能需要调用N个服务,客户端必须知道所有服务的网络位置(ip+port)以往的做法是把服务的地址放在配置文件活数据库中,这样就有以下几个问题:

总结起来一句话:服务多了,配置很麻烦,问题一大堆

所以现在就选择服务发现来解决这些问题。我们来看一下,服务发现如何解决这个问题,具体设计如下:

​ 与之前解决方法不同的是,加了个服务发现模块。服务端把当前自己的网络位置注册到服务发现模块(这里注册的意思就是告诉),服务发现就以K-V的方式记录下,K一般是服务名,V就是IP:PORT。服务发现模块定时的轮询查看这些服务能不能访问的了(这就是健康检查,类似心跳包机制)。客户端在调用服务时候,就跑去服务发现模块问下它们的网络位置,然后再调用它们的服务。这样的方式是不是就可以解决上面的问题了呢?客户端完全不需要记录这些服务的网络位置,客户端和服务端完全解耦!

即:

每个服务端运行前先将自己的网络信息(服务名,IP,Port等)在服务发现模块进行注册,客户端后续访问服务端前会先去服务发现模块获取一个健康的服务端网络信息,然后再建立连接,而服务发现模块可通过健康检查来获取服务端健康状态(类似心跳包的机制)。

常见的服务发现框架有:Etcd、Eureka、Consul、Zookeeper

常见服务发现的种类

Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。包含多个组件,但是作为一个整体,为你的基础设施提供服务发现和服务配置的工具.他提供以下关键特性:

服务发现consul提供服务,服务端主动向consul发起注册

健康检查:健康检测使consul可以快速的告警在集群中的操作。和服务发现的集成,可以防止服务转发到故障的服务上面。(心跳机制)

键/值存储:一个用来存储动态配置的系统。提供简单的HTTP接口,可以在任何地方操作。

多数据中心:无需复杂的配置,即可支持任意数量的区域。

官方建议:最好是三台或者三台以上的consul在运行,同名服务最好是三台或三台以上,默认可以搭建集群

5.2 consul

consul不同版本网址:https://releases.hashicorp.com/consul/1.10.1 后面对应版本号可修改

consul常用命令:

5.2.1 consul使用

首先我们要运行consul,运行有两种模式,分别是server和client,通过下面的命令开启:

consul agent -server

consul agent 

每个数据中心至少必须拥有一个server。一个client是一个非常轻量级的进程.用于注册服务,运行健康检查和转发对server的查询.agent必须在集群中的每个主机上运行.

接着我们以server的模式启动一个consul:

命令:consul members 查看集群中成员

5.2.2 向consul注册服务 (linux)

这里我们使用定义服务文件来注册一个服务:

{"service": {
    "name": "Faceid",
    "tags": ["rails"],
    "port": 9000
	}
}

服务定义文件在我们的配置目录下面(需要sudo),/etc/consul.d/,文件都是以.json结尾。

注册完服务之后,我们重启consul,

健康检查

健康检查是服务发现的关键组件.预防使用到不健康的服务.和服务注册类似,一个检查可以通过检查定义或HTTP API请求来注册.我们将使用和检查定义来注册检查.和服务类似,因为这是建立检查最常用的方式.

在/etc/consul.d/目录下面创建文件web2.json,内容如下:

{"service": {
    "name": "web",
    "tags": ["extract", "verify", "compare", "idcard"],
    "address": "192.168.137.130",
    "port": 9000,
    "check": {
        "id": "api",
        "name": "HTTP API on port 9000",
        "http": "http://localhost:9000",
        "interval": "10s",
        "timeout": "1s"
        }
   }
}

这时候我们没有开启这个服务,所以这个时候健康检查会出错。打开web界面,如下

consul做健康检查的必须是Script、HTTP、TCP、TTL中的一种。

5.3 consul结合grpc使用

我们操作consul使用的是github.com/hashicorp/consul/包,我们先来下载一下,命令如下:

$ go get -u -v github.com/hashicorp/consul

然后我们先注册一个服务到consul上:

使用的具体流程

代码:

server

type Server struct {
}

func (this *Server)Sum(ctx context.Context, resp *pb.Resp) (*pb.Reply, error){
	return &pb.Reply{Ans: resp.Num1+resp.Num2},nil
}

func main() {
	//1.初始化consul配置 客户端与服务端需要一致
	consulConfig := api.DefaultConfig()

	//2.创建consul操作对象
	client, err := api.NewClient(consulConfig)
	if err != nil {
		fmt.Println("api newclient err:", err)
		return
	}

	//3.告诉consul 即将注册的服务配置信息
	service := api.AgentServiceRegistration{
		ID:      "jq",
		Tags:    []string{"grpc-consul", "grpcconsul"},
		Name:    "grpc And consul",
		Address: "127.0.0.1",
		Port:    8999,
        
		Check: &api.AgentServiceCheck{
			TCP:      "127.0.0.1:8999",
			Timeout:  "2s", //超时时长
			Interval: "5s", //时间间隔
		},
	}

	//4.注册服务到consul上
	client.Agent().ServiceRegister(&service)

	////////////////////////////////////////////////////
	//1.创建grpc对象
	grpcServer:=grpc.NewServer()

	//2.注册grpc服务
	pb.RegisterAddServer(grpcServer,new(Server))

	//3.创建监听
	listener, err := net.Listen("tcp", "127.0.0.1:8999")
	if err != nil {
		fmt.Println("net listen err:",err)
		return
	}
	defer listener.Close()

	fmt.Println("服务启动成功.....")
	//4. 启动服务
	grpcServer.Serve(listener)
}

配置信息里面的address和port要与tcp的IP和端口一致

client

func main() {
	//初始化consul配置, 客户端服务器需要一致
	consulConfig := api.DefaultConfig()

	//2.创建consul操作对象
	registerClient, err := api.NewClient(consulConfig)
	if err != nil {
		fmt.Println("api newclient err:", err)
		return
	}

	//3.找寻一个健康的服务
	serviceslice,_,err:=registerClient.Health().Service("grpc And consul","grpcconsul",true,nil)
	if err != nil {
		fmt.Println("get health service err:",err)
		return
	}

	//4.
	//serviceslice[0].Service
	realIPPort:=serviceslice[0].Service.Address + ":" +strconv.Itoa(serviceslice[0].Service.Port)
	/////////////////////////////////////////////////////////
	//1. 连接grpc服务
    //clientConn, err := grpc.Dial("127.0.0.1:8999", grpc.WithInsecure())
    clientConn, err := grpc.Dial(realIPPort, grpc.WithInsecure())
	if err != nil {
		fmt.Println("grpc dial err:",err)
		return
	}

	//2.初始化客户端
	client := pb.NewAddClient(clientConn)

	//3.调用服务
	reply, err := client.Sum(context.TODO(), &pb.Resp{Num1: 33, Num2: 66})
	if err != nil {
		fmt.Println("调用远程服务错误")
		return
	}
	fmt.Println(reply.Ans)
}

服务显示

通过调用远程服务的结果

服务注销

func main(){
	 //初始化consul配置,客户端服务器需要一致
	consulConfig := api.DefaultConfig()
    
    //获取consul操作对象
    registerClient,_ := api.NewClient(consulConfig)


	//注销服务
     client.Agent().ServiceDeregister("1")
}

5.4 Go-micro

5.4.1 Linux下安装

首先我们先来安装一下go-micro开发环境。安装步骤如下:

#安装go-micro
go get -u -v github.com/micro/go-micro
#安装工具集
go get -u -v github.com/micro/micro
#安装protobuf插件
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
go get -u github.com/micro/protoc-gen-micro

或者通过docker镜像安装:

$ docker pull microhq/micro

安装之后输入micro命令,显示如下就证明安装成功

5.4.2 windows下安装

地址:https://github.com/go-micro/cli

这样在$GOPATH/bin会有4个exe文件,将go-micro.exe改为micro.exe ,这样在cmd终端可以输入micro即可看到

5.4.3 go-micro使用

5.4.3.1 主要命令
micro new
	参数:
		--namespace    命名空间=包名
		--type         微服务类型
		  * srv:微服务
		  * web:基于微服务的web网站

5.4.3.2 创建服务
micro new --type srv 微服务名称     ----linux下
micro new service 微服务名称        -----windows下

在对应项目列表会被创建对应项目及文件

5.4.3.3 查看相应文件
5.4.4.4 解析部分

1. 服务端:

xxx.pb.micro.go中: 【223行】

// 定义一个抽象类,抽象类有一些成员方法
type JqHandler interface {
	Call(context.Context, *CallRequest, *CallResponse) error
	ClientStream(context.Context, Jq_ClientStreamStream) error
	ServerStream(context.Context, *ServerStreamRequest, Jq_ServerStreamStream) error
	BidiStream(context.Context, Jq_BidiStreamStream) error
}

//抽象类传参的注册方法,也即是你必须实现了抽象类全部方法,才能传参该注册方法
func RegisterJqHandler(s server.Server, hdlr JqHandler, opts ...server.HandlerOption) error {
	type jq interface {
		Call(ctx context.Context, in *CallRequest, out *CallResponse) error
		ClientStream(ctx context.Context, stream server.Stream) error
		ServerStream(ctx context.Context, stream server.Stream) error
		BidiStream(ctx context.Context, stream server.Stream) error
	}
	type Jq struct {
		jq
	}
	h := &jqHandler{hdlr}
	return s.Handle(s.NewHandler(&Jq{h}, opts...))
}

handler/xx,go文件中

//定义的实例结构体Jq全部实现了空接口JqHandler的全部方法,因此该结构体就可以调用该注册方法进行传参
type Jq struct{}

func (e *Jq) Call(ctx context.Context, req *pb.CallRequest, rsp *pb.CallResponse) error {
	log.Infof("Received Jq.Call request: %v", req)
	rsp.Msg = "Hello " + req.Name
	return nil
}

func (e *Jq) ClientStream(ctx context.Context, stream pb.Jq_ClientStreamStream) error {
}

func (e *Jq) ServerStream(ctx context.Context, req *pb.ServerStreamRequest, stream pb.Jq_ServerStreamStream) error {
}

func (e *Jq) BidiStream(ctx context.Context, stream pb.Jq_BidiStreamStream) error {
}

main.go中:

//调用该注册方法完成注册
pb.RegisterJqHandler(srv.Server(), new(handler.Jq))

2. 客户端

**在xxx.pb.micro.go中: ** 【46行】

//定义一个客户端抽象类
type JqService interface {
	Call(ctx context.Context, in *CallRequest, opts ...client.CallOption) (*CallResponse, error)
	ClientStream(ctx context.Context, opts ...client.CallOption) (Jq_ClientStreamService, error)
	ServerStream(ctx context.Context, in *ServerStreamRequest, opts ...client.CallOption) (Jq_ServerStreamService, error)
	BidiStream(ctx context.Context, opts ...client.CallOption) (Jq_BidiStreamService, error)
}

type jqService struct {
	c    client.Client
	name string
}

//初始化客户端的方法
func NewJqService(name string, c client.Client) JqService {
	return &jqService{
		c:    c,
		name: name,
	}
}

//后续客户端进行调用回调方法需要我们自己进行调用

所以总体实现跟grpc一致

5.4.4.5 go-micro添加consul服务发现
HTTP相关知识
1xx    100    请求资源已经被接受,需要继续发送
2xx    200    请求成功
3xx    302    请求资源被转移,请求被转接 
4xx    404    请求资源失败
5xx    500    服务器错误,代理错误

路由器:资源分发

路由:请求分发

URL:

6. 实例应用

gin框架作为web客户端,微服务作为服务端提供远程调用服务

6.1 具体流程

标签:服务,err,grpc,consul,micro,go,Go
来源: https://www.cnblogs.com/wustjq/p/16439676.html