Go教程

Go-micro微服务

本文主要是介绍Go-micro微服务,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Go微服务

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

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

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

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

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

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

1.1 单体式服务

即以往大家熟悉的服务器

特性:

  • 复杂性随着开发越来越高,遇到问题解决困难
  • 技术债务逐渐上升
  • 耦合度较高,维护成本大
    • 出现bug,不易排查
    • 解决旧bug,会引起新bug
  • 持续交付时间较长
  • 技术选型成本高,风险大
  • 扩展性较差
    • 垂直扩展:通过增加单个系统的负荷来实现扩展
    • 水平扩展:通过增加更多的系统成员来实现扩展

1.2 微服务

优点:

  • 职责单一:原来需求通过各个模块实现,现在微服务通过将每个模块改为每个服务,即每个服务就是一个进程,所以相较于更加独立
  • 轻量级通信,各个语言之间可以通信
  • 独立性:每个服务之间单独开发 单独测试
  • 迭代开发

缺点:

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

2. RPC协议

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

1.本地函数调用:

result := Add(1,2)

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

2.RPC远程调用

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

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

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

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

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

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

  • 每个服务都被封装成为进程,彼此独立
  • 进程和进程之间可以使用不同语言实现,但是借助RPC进行通信。

2.1 RPC入门使用

socket通信:

  • server

    listener,err:=net.Listen()   创建监听
    conn:=listener.Accept()     启动监听
    conn.Read()
    conn.Write()
    defer conn.Close()/listener.Close()
    
  • client

    conn,err:=net.Dial()
    conn.Write()
    conn.Read()
    defer conn.Close()
    

RPC通信步骤

  • server

    • 注册RPC服务对象,给对象绑定方法 (定义类,绑定类方法)

      rpc.Register("服务名",回调对象)
      
    • 创建监听器

      listener,err:=net.Listen()
      
    • 建立连接

      conn,err:=listener.Accept()
      
    • 将连接绑定RPC服务

      rpc.ServerConn(conn)
      
  • client

    • 用RPC连接服务器

      conn,err:=rpc.Dial()
      
    • 调用远程函数

      conn.Call("服务名.方法名",传入参数,传出参数)
      

2.2 RPC相关函数

  • 注册RPC服务

    func (server *Server) RegisterName(name string, rcvr interface{}) error
        name:服务名,字符串类型
        rcvr:对应rpc服务,该对象绑定的方法必须满足相应条件
    		1)方法必须是导出的--包外可见,即首字母大写
    		2)方法必须有两个参数,都是导出类型或内建类型
    		3)方法的第二个参数必须是“指针” 传出参数
    		4)方法只有一个 error 接口类型的返回值
    
            如:
            type World struct{
            }
            func (this *World) HelloWorld (name string,resp *string)error{
            }
            rpc.RegisterName("服务名",new(World))
    
  • 绑定RPC服务

    func (server *Server) ServeConn(conn io.ReadWriteCloser)
    	conn:成功建立连接的socket
    
  • 调用远程函数

    func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error
    
    	serviceMethod:"服务名.方法名"
    	args:传入参数,即上述方法需要的数据
    	reply:传出参数,常用变量的地址去接收
    

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 其他一样
  • 使用nc -l 127.0.0.1 8080充当服务器
  • 使用client.go充当客户端,发起通信

2.4.2 修改服务端

server端修改

//rpc.ServerConn(conn)
jsonrpc.ServerConn(conn)
  • 使用终端充当客户端,将数据发送给服务端echo -e '{"method":"hello.HelloWorld","paras":["李白"],"id":0}' |nc 127.0.0.1 8080
  • 使用server.go进行充当服务端,进行通信

2.5 RPC的封装

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

  • 对服务端进行封装

    design.go文件中

    package main
    
    import "net/rpc"
    
    //要求 服务端在 注册RPC 对象时,能够在编译阶段检测出注册对象是否合法
    //创建接口,在接口中定义方法的原型
    type MyInterface interface{
    	HelloWorld( string, *string)error
    }
    
    //不能给 interface类型 绑定方法
    //调用该方法时候,需要给该方法传参  该参数实现了 HelloWorld方法的类对象
    func RegisterService(i MyInterface){
    	rpc.RegisterName("hello",i)
    }
    

    server.go

    type World struct{
    }
    
    func (this *World)HelloWorld(name string,resp *string)error{
    	*resp=name+" 你好...."
    	return nil
    }
    
    func main() {
    	//1. 注册 RPC 服务对象,给对象绑定对应方法
    	//err:=rpc.RegisterName("hello",new(World))
    	//if err != nil {
    	//	fmt.Println("rpc registername err:",err)
    	//	return
    	//}
        //RegisterService(&World{}) //两种方法都可以  主要是要父类指向子类的指针对象
    	RegisterService(new(World))
        
        ...
    }
    

    ​ 在server.go中,World结构体实现了MyInterface的全部方法(即HelloWorld方法),且参数完全一致,因此此处实现继承,即MyInterface为 父类,World为子类。

    ​ 而在调用RegisterService方法时,server.go传参为new(World)对应interface的指针,即父类指向子类的指针对象,此处即为多态现象。(此处World结构体调用,即interface对象表现为World)

  • 对客户端进行封装

    design.go文件中

    //像调用本地函数一样,调用远程函数
    //定义类
    type MyClient struct{
    	client *rpc.Client
    }
    
    //由于使用client调用call() 因此需要进行初始化
    func Init(addr string)MyClient{
    	conn,_:=rpc.Dial("tcp",addr)
    	//defer conn.Close()  因为后续需要用conn 所以这里千万不能关闭
    	return MyClient{client: conn}
    }
    
    //实现函数原型参照上面interface
    func (this *MyClient)HelloWorld(a string ,b *string)error{
    	return this.client.Call("hello.HelloWorld",a,b)
        
    }
    

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:https://github.com/protocolbuffers/protobuf/releases/tag/v3.14.0

  • 安装protoc-gen-go

    • go get -u github.com/golang/protobuf/protoc-gen-go
      
  • 安装grpc

    • go get google.golang.org/grpc
      

需要将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服务

  • 语法

    service 服务名{
    	rpc 函数名(参数:消息体message) returns (返回值:消息体message)
    }
    
    
    //例如:
    message People{
    	string name =1;
    }
    message Student{
    	int32 age=2;
    }
    
    service hello{
    	rpc HelloWorld(People) returns (Student);
    }
    
  • 默认在编译protobuf,步编译service,因此当添加了RPC服务,需要使用gRPC

    执行命令:

    protoc --go_out=plugins=grpc:./ *.proto
    
  • 我们封装的client与通过protobuf生成的client进行对比

    //我们自己封装的客户端 2.4.2
    type MyInterface interface{
    	HelloWorld( string, *string)error
    }
    
    type MyClient struct{
    	client *rpc.Client
    }
    
    //由于使用client调用call() 因此需要进行初始化
    func Init(addr string)MyClient{
    	conn,_:=rpc.Dial("tcp",addr)
    	//defer conn.Close()  因为后续需要用conn 所以这里千万不能关闭
    	return MyClient{client: conn}
    }
    
    //实现函数原型参照上面interface
    func (this *MyClient)HelloWorld(a string ,b *string)error{
    	return this.client.Call("hello.HelloWorld",a,b)
    }
    
    ////////////////////////////////////////////////////
    
    //protobuf生成的client进行
    type AddNameClient interface {
    	AddFunc(ctx context.Context, in *Student, opts ...grpc.CallOption) (*Student, error)
    }
    
    type addNameClient struct {
    	cc grpc.ClientConnInterface
    }
    
    //类比于Init() 完成初始化
    func NewAddNameClient(cc grpc.ClientConnInterface) AddNameClient {
    	return &addNameClient{cc}
    }
    
    //同样子类去继承空接口父类的方法,同样实现多态
    func (c *addNameClient) AddFunc(ctx context.Context, in *Student, opts ...grpc.CallOption) (*Student, error) {
    	out := new(Student)
    	err := c.cc.Invoke(ctx, "/pb.AddName/AddFunc", in, out, opts...)
    	if err != nil {
    		return nil, err
    	}
    	return out, nil
    }
    
    
  • 服务端同样去实现父类的方法到达继承,然后去实现多态

利用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

实现两个数相加

  • 定义protobuf文件

    syntax="proto3";
    
    option go_package="../proto";
    
    message req{
      int32 num1=1;
      int32 num2=2;
    }
    
    message reply{
        int32 res=3;
    }
    
    service sum{
      rpc add(req) returns(reply);
    }
    
    
    protoc --go_out=plugins=grpc:./ *.proto
    
    

    生成相应的.go文件

  • 实现server端代码

    package main
    import (
    	"awustjq/rpclearn2/proto"
    	"context"
    	"fmt"
    	"google.golang.org/grpc"
    	"net"
    )
    
    type Server struct{
    }
    
    func (this *Server)Add(ctx context.Context, req *proto.Req) (*proto.Reply, error){
    	res:=sum(req.Num1,req.Num2)
    	return &proto.Reply{Res: res},nil
    }
    
    //封装一个加法
    func sum(a,b int32)int32{
    	return a+b
    }
    
    func main() {
    	//1.创建rpc服务
    	grpcserver:=grpc.NewServer()
    
    	//2.注册服务
    	//s.RegisterService(&_Sum_serviceDesc, srv)
    	proto.RegisterSumServer(grpcserver,new(Server))
    
    	//3.设置监听  指定IPPort
    	listener,err:=net.Listen("tcp","127.0.0.1:9999")
    	if err != nil {
    		fmt.Println("net listen err:",err)
    		return
    	}
    	defer listener.Close()
    	fmt.Println("服务端开始运行")
    	//4.启动服务
    	grpcserver.Serve(listener)
    }
    
    
    • 定义子类去实现父类接口的方法时:

      func (this *Server)Add(ctx context.Context, req *proto.Req) (*proto.Reply, error){
      	res:=sum(req.Num1,req.Num2)
      	return &proto.Reply{Res: res},nil
      }
      
      

      起初定义message只有一两个变量,但是生成对应struct时,会添加其他成员变量,因此要注意是将值赋值给对应成员变量(Res:res)

  • 实现client端

    package main
    
    import (
    	"awustjq/rpclearn2/proto"
    	"context"
    	"fmt"
    	"google.golang.org/grpc"
    )
    
    func main() {
    	//1.连接grpc
    	grpcConn,err:=grpc.Dial("127.0.0.1:9999",grpc.WithInsecure())
    	if err != nil {
    		fmt.Println("grpc dial err:",err)
    		return
    	}
    
    	//2.初始化客户端
    	grpcClient:=proto.NewSumClient(grpcConn)
    
    	//3.调用远程服务
    	//Add(ctx context.Context, in *Resq, opts ...grpc.CallOption) (*Reply, error)
    	resq:= proto.Req{
    		Num2: 666,
    		Num1: 333,
    	}
    
    	reply,err:=grpcClient.Add(context.TODO(),&resq)
    	if err != nil {
    		fmt.Println("grpcclient add err:",err)
    		return
    	}
    	fmt.Println("客户端连接成功")
    	fmt.Println(resq.Num1," + ",resq.Num2," = ",reply.Res)
    }
    
    
    • grpcConn,err:=grpc.Dial("127.0.0.1:9999",grpc.WithInsecure())  后面一定跟grpc.WithInsecure(),不然编译会报错
      
      

示例:

服务端:

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

  • go-micro:用于编写微服务的可插入Go-RPC框架; 服务发现,客户端/服务器rpc,pub/sub等,是整个Micro的核心。

    默认使用mdns做服务发现,可以在插件中替换成consul,etcd,k8s等

    组播 广播

  • go-plugins:go-micro的插件,包括etcd,kubernetes(k8s),nats,rabbitmq,grpc等

  • micro:一个包含传统入口点的微服务工具包; API网关,CLI,Slack Bot,Sidecar和Web UI。

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

5.1 服务发现

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

  • 需要配置N个服务的网络位置,加大配置的复杂性
  • 服务的网络位置变化(ip port发生改变),需要改变每个调用者的配置
  • 集群的情况下,难以做负载(反向代理的方式除外)

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

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

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

即:

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

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

常见服务发现的种类

  • concul:常应用于go-micro
  • mdnsgo-micro中默认自带的服务发现
  • etcdk8s内嵌的服务发现
  • zookeeperjava中常用

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

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

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

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

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

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

5.2 consul

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

consul常用命令:

  • consul agent
    • -dev 以consul开发者模式运行一个consul,以默认配置启动consul
    • -bind=0.0.0.0 指定consul所在机器的IP地址,默认值0.0.0.0 表任意有效IP
    • -http-port=8500 consul自带的web访问端口
    • -client=127.0.0.1 表明哪些机器可以访问consul,默认本机 ;0.0.0.0 表所有机器均可访问
    • -config-dir=foo 所有主动注册服务的描述信息
    • -data-dir=path 储存所有注册过来的srv机器的详细信息
    • -node=hostname 服务发现的名字
    • -rejoin consul启动时候,可加入到consul集群
    • -server 以服务方式开启consul,允许其他的consul连接到开启consul上(形成集群), 如果不加-server,表示以“客户端”方式开启,不能被连接。
    • -ui 可以使用web页面来查看服务发现的详情

5.2.1 consul使用

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

consul agent -server

consul agent 

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

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

  • server模式启动

    $ consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -node=n1 -bind=192.168.8.180 -ui -rejoin -config-dir=/etc/consul.d/ -client 0.0.0.0   //案例
    
    

    需要先在/etc/下面创建consul.d目录

    • -server : 定义agent运行在server模式
    • -bootstrap-expect :在一个datacenter中期望提供的server节点数目,当该值提供的时候,consul一直等到达到指定sever数目的时候才会引导整个集群,该标记不能和bootstrap共用
    • -bind:该地址用来在集群内部的通讯,集群内的所有节点到地址都必须是可达的,默认是0.0.0.0
    • -node:节点在集群中的名称,在一个集群中必须是唯一的,默认是该节点的主机名
    • -ui: 启动web界面 :8500
    • -rejoin:使consul忽略先前的离开,在再次启动后仍旧尝试加入集群中。
    • -config-dir:配置文件目录,里面所有以.json结尾的文件都会被加载
    • -client:consul服务侦听地址,这个地址提供HTTP、DNS、RPC等服务,默认是127.0.0.1所以不对外提供服务,如果你要对外提供服务改成0.0.0.0
    • data-dir:提供一个目录用来存放agent的状态,所有的agent允许都需要该目录,该目录必须是稳定的,系统重启后都继续存在
    $ consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -node=n1 -bind=192.168.8.180 -ui -rejoin -client 0.0.0.0     //我的windows机器上
    
    

    在windows下浏览器键入192.168.8.180:8500/127.0.0.1:8500

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

  • client模式启动

    $ consul agent -data-dir /tmp/consul -node=n2 -bind=192.168.137.82 -config-dir /etc/consul.d -rejoin -join 192.168.137.81
    
    

    运行cosnul agent以client模式,-join 加入到已有的集群中去。

    优雅的停止consul

    ​ 在退出中,Consul提醒其他集群成员,这个节点离开了.如果你强行杀掉进程.集群的其他成员应该能检测到这个节点失效了.当一个成员离开,他的服务和检测也会从目录中移除.当一个成员失效了,他的健康状况被简单的标记为危险,但是不会从目录中移除.Consul会自动尝试对失效的节点进行重连.允许他从某些网络条件下恢复过来.离开的节点则不会再继续联系.

    ​ 此外,如果一个agent作为一个服务器,一个优雅的离开是很重要的,可以避免引起潜在的可用性故障影响达成一致性协议.
    consul优雅的退出:不优雅--ctrl+c

    $ consul leave
    
    

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上:

使用的具体流程

    1. 创建proto文件,指定rpc服务
    protoc --go_out=plugins=grpc:./ *.proto
    
    
    1. 启动consul服务发现
    在linux终端:consul agent -dev
    
    
    1. 启动server
    • 3.1 初始化consul配置

      consulConfig := api.DefaultConfig()
      
      
    • 3.2 创建consul对象

      client, err := api.NewClient(consulConfig)
      
      
    • 3.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", //时间间隔
      		},
      	}
      
      
    • 3.4 注册服务到consul

      client.Agent().ServiceRegister(&service)
      
      
    1. 启动client
    • 4.1 初始化consul配置

      consulConfig := api.DefaultConfig()
      
      
    • 4.2 创建consul对象

      client, err := api.NewClient(consulConfig)
      
      
    • 4.3 使用consul对象,从consul服务发现上获取健康服务

      registerClient.Health().Service()
      
      

      参数说明:

      func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error)
      		//参数
                  service:服务名-----注册服务时,指定该string  对应3.3 中Name字段
                  tag:别名 如果有多个,任选一个
                  passingOnly:是否通过健康检查 true表示必须通过
                  q:表示一些查询参数,一般传nil
      		//返回值:
                  []*ServiceEntry:储存健康的服务
                  *QueryMeta:额外查询返回值,与参数q对应,传nil
      
      
    • 4.4 和rpc建立连接,调用远程服务

代码:

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

  • 安装go-micro

    go install github.com/go-micro/cli/cmd/go-micro@v1.1.1
    

    安装完成后会在$GOPATH/bin下生成go-micro.exe

  • 安装protoc.exeprotoc-gen-go.exe 参照前面3.1

  • 安装protoc-gen-micro.exe

    go install go-micro.dev/v4/cmd/protoc-gen-micro@v4
    

    .

这样在$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下

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

  • main.go : 项目的入口文件。
  • handler/: 处理 grpc 实现的接口。对应实现接口的子类,都放置在 handler 中。
  • proto/: 预生成的 protobuf 文件。
  • Dockerfile:部署微服务使用的 Dockerfile
  • Makefile:编译文件。—— 快速编译 protobuf 文件。
5.4.3.3 查看相应文件
  • proto文件夹下,利用make命令进行编译,主要原因是有makefile文件

    make proto
    
    生成:
    
    xxx.pb.go------主要是除微服务定义以外的
    xxx.pb.micro.go----是有关微服务相关的,包含客户端和服务端需要调用的函数
    
    

  • main.go

    func main() {
    	// Create service	   初始化服务器对象
    	srv := micro.NewService(
    		micro.Name(service),	//服务器名
    		micro.Version(version),	//服务器版本信息
    	)
    
    	//与micro.NewService作用一致,但优先级高,后续代码运行期才有使用的必要
    	//srv.Init()
    
    	// Register handler  注册服务
    	pb.RegisterJqHandler(srv.Server(), new(handler.Jq))
    
    	// Run service       启动服务
    	if err := srv.Run(); err != nil {
    		log.Fatal(err)
    	}
    }
    
    
  • handler/xx.go中 就是实现了服务端空接口类型下的全部方法

    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 {
    }
    
    
    
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服务发现
  • github.com/go-micro/plugins/v4/registry/consul
    
HTTP相关知识
1xx    100    请求资源已经被接受,需要继续发送
2xx    200    请求成功
3xx    302    请求资源被转移,请求被转接 
4xx    404    请求资源失败
5xx    500    服务器错误,代理错误

路由器:资源分发

路由:请求分发

URL:

  • 组成:https://ip+port/资源路径
    • https://ip+port:通过IP找到网络中对应的PC机,通过PORT确定唯一一个进程
    • 资源路径:在代码中,即为路由
  • "/":代表主机上进程对应的默认资源
    • http协议,会自动找当前目录下的index.html文件,作为默认页面 (前提是你没有指定页面)

6. 实例应用

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

6.1 具体流程

  • 首先利用micro new service srv(微服务名称) (windows下命令) 创建微服务

  • srv端利用makefile文件,键入make proto生成protobuf文件

  • srv端的proto文件全部拷贝至gin框架下 (因为远程调用就是借助protobuf文件,使服务端和客户端通信)

  • 分别运行客户端和服务端,在浏览器下进行测试

    服务端:

    var (
    	service = "srv"
    	version = "latest"
    )
    
    func main() {
    	// Create service	初始化服务器对象
    	srv := micro.NewService(
    		micro.Name(service),	//服务器名
    		micro.Version(version),	//服务器版本信息
    	)
    	srv.Init()
    
    	//注册服务
    	// Register handler  handler/xx.go下Srv结构体实现了空接口下全部方法,因此可以传参
    	pb.RegisterSrvHandler(srv.Server(), new(handler.Srv))
    
    	// Run service	运行服务
    	if err := srv.Run(); err != nil {
    		log.Fatal(err)
    	}
    }
    

    handler/srv.go:

    结构体Srv绑定的回调方法,即是给传入参数前面拼hello

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

    客户端:

    func Handler(c *gin.Context){
    	//初始化客户端   其中pb包是我们从srv复制来的proto文件夹
    	srvClient:=pb.NewSrvService("srv",client.DefaultClient)
    	
    	//调用远程方法    我们结构体对应字段传入姜庆 传出字段应该是hello 姜庆
    	resp,err:=srvClient.Call(context.TODO(),&pb.CallRequest{Name: "姜庆"})
    	if err != nil {
    		log.Fatal("调用远程方法失败")
    		return 
    	}
    	//fmt.Println(resp.Msg) 给浏览器显示
    	c.Writer.WriteString(resp.Msg)
    }
    
    
    func main() {
    	//创建一个默认的路由引擎
    	r:=gin.Default()
    
    	r.GET("/",Handler)
    
    	err := r.Run(":8999")
    	if err != nil {
    		log.Fatal("router run err:", err)
    		return
    	}
    }
    
    
    

    主要函数:

    func NewSrvService(name string, c client.Client) SrvService
        参数:
            name :服务名名称
            c: 初始客户端信息
        返回值:
            SrvService: 初始化后的客户端,类比于Grpc中的NewClient方法类似
    
    

    结果显示:

    可以发现默认使用的mdns服务注册发现

    6.2 引入consul作服务注册发现

    go-micro默认使用的是mdns服务发现

    mdns服务发现:仅支持本地服务,即局域网内的服务

    consul服务发现:支持网际服务,即双方可以不在同一个地方

    6.2.1 go-micro引入consul

    • 初始化consul服务发现 :consulReg:=consul.NewRegistry()

    • 添加服务

      • srv := micro.NewService(
          		micro.Name(service),	//服务器名
              micro.Registry(consulReg),
          		micro.Version(version),	//服务器版本信息
          	)
        
    • 在命令行运行consulconsul agent -dev

    6.2.2 gin 客户端引入consul

    • 初始化consul服务发现 :consulReg:=consul.NewRegistry()

    • 初始化服务对象。指定consul服务发现

      • service := micro.NewService(
               micro.Registry(consulReg),
          	)
        
    • 修改初始化客户端

      • newClient:=pb.NewSrvService("srv",service.Client())
        //得到新的客户端去远程调用
        newClient.Call()
        
这篇关于Go-micro微服务的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!