go web开发 - 微服务

 


微服务的定义

微服务: 将一个单体应用拆分成一组微小的服务组件,每个微小的服务组件运行在自己的进程上,组件之间通过RESTful API这样的轻量级机制进行交互,这些服务以业务能力为核心,用自动化部署机制独立部署。

微服务是一种编程架构思想,有不同的语言实现。

微服务要解决的问题

假设将业务商户服务A,订单服务B,产品服务C分别拆分为一个服务应用,单独部署,要解决一下问题。

1. 客户端如何访问这些服务?

在单体应用开发中,所有的服务都是本地的,前端,移动端可以直接访问后端服务器。

现在按照功能拆分成独立的服务,跑爱独立的进程中,如下:

1655881833192.png

此时后端有n个服务,前端就需要记录n个ip地址,一旦服务更新升级,前端和移动端非常麻烦。

通过网管(API Gateway)技术来解决这个问题,网关的作用主要包括:

  • 提供统一的服务入口,让微服务对前端透明
  • 聚合后端服务,节省流量,提高性能。
  • 提供安全,过滤,流量控制等API管理功能。

最终,添加了网关的业务结构图如下:

1655882220673.png

2. 每个服务之间如何通信?

所有的微服务都是独立部署,运行在自己的进程容器中,所以微服务与微服务之间的通信就是进程间通信即IPC(inter Process communication),常见的有两大类: 同步调用、异步消息调用。

1)同步调用

同步调用比较简单,一致性强,但容易出调用问题。同步调用有两种实现方式:RESTRPC

  • REST: 基于HTTP,实现更容易,各种语言都支持,同时能够跨客户端,对客户端没有特殊的要求,只要具备HTTP的网络请求库功能就能使用。
  • RPC: 传输效率高,安全性可控,在系统内部调用实现时使用的较多。

基于REST和RPC的特点,通常的原则是:向系统外部暴露采用REST,系统内部暴露调用采用RPC方式。

2)异步消息调用

常见的异步消息框架:kafka,notify等。

3. 多个微服务,应如何实现?

在微服务结构中,一般每个服务都是由多个拷贝,来做负载均衡,一个服务随时可能下线,也可能增加新的节点。会出现如下问题:

  • 服务之间如何感知?
  • 服务如何管理?

这就是服务发现、识别与管理问题。解决多个服务之间的识别,发现的问题一般是通过注册方式来进行。

常见的服务管理框架: zookeeper等

上面的问题解决方案的实现有两种,分别是:基于客户端的服务注册与发现、基于服务端的服务注册与发现。

1) 基于客户端的服务注册与发现

优点是架构简单,拓展灵活,只对服务注册器依赖。缺点是客户端需要维护所有调用服务的地址,有技术难度。

例如:订单服务先去服务注册器查询产品服务的IP地址,然后订单服务在根据返回结果再调用产品服务。

1655883518248.png

2)基于服务端的服务注册与发现

优点是简单,所有服务对于前台调用方透明,一般小公司在云服务上部署采用的比较多。

例如:订单服务直接访问负载均衡器,通过负载均衡器查询ip地址。

1655883682443.png

4. 如果服务出现宕机,该如何解决?

当系统由一系列的服务调用链组成的时候,我们必须确保任一环节问题不至于影响整体链路。相应的手段有很多,比如:

  • 重试机制
  • 限流
  • 熔断机制
  • 负载均衡
  • 降级(本地缓存)

protobuf协议

google开发的protocol buffers,主要应用在分布式和微服务架构中的一种数据传输协议。跟xmljson一样的,都是用来储存和传输数据的。通常各服务之间使用jsonxml进行通信,但在高性能和大数据通信的系统中,如果能够压缩数据量,提高传输效率,则会给用户带来更流畅的体验。

即现在的传输方式:

  • JSON
  • XML
  • Protocol Buffers

例如:一条消息数据,用protobuf序列化后的大小是json的10分之一,是xml格式的20分之一,但性能确实它们的5-100倍。

安装

编译器下载地址: https://github.com/protocolbuffers/protobuf 并 设置环境变量。

vscode安装插件: vscode-proto3

安装go protocol buffers插件:生成Go语言代码的工具

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest  // 下载并build到$GOPATH/bin目录下, 然后可以将该目录添加到path。

使用流程

定义消息文件: *.proto

编译生成制定语言文件后使用。

第一个protobuf示例

// person.proto
syntax = "proto3";

option go_package = "./;hello";

message Person{
    string name = 1;
    int32 age = 2;
    string email =3;
}

注意:

  • option go_package = "path;name"; 其中path表示生成go文件存放的地址,会自动生成目录。name表生成go文件所属的包。

编译生成go文件

protoc --go_out=./go ./proto/*

生成的go文件

// 生成person.pb.go

消息类型的定义

文件拓展名以.proto结尾。

示例:

syntax = "proto3";   // 指定使用proto3的语法,不指定的话默认为proto2

message SearchRequest{  // message用于定于消息,该消息名为SearchRequest
    string query = 1;        // 1,2,3 为编号,不是赋值
    int32 page_number = 2;   // 
    int32 result_per_page = 3; 
}

指定字段类型

proto协议中,字段类型包括字符串(string),整形(int32、int64),枚举(enum)等类型。

分配字段编号

消息定义中每个字段都有一个唯一的编号,这些标号用于在消息二进制格式中标识你的字段,并且在使用你的消息类型后不应修改。

注意:

  • 可以指定的最小字段编号为1, 最大是2^29 - 1。也不可以使用1900019999的编号,因为他们是为protocol buffers实现内部保留的。
  • 编号1-15范围占用1个字节,16-2047编号的字段占用2个字节。

指定字段规则

  • singular:表示消息格式中该字段可以有0个或1个值(不超过1个)。默认字段。
  • repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)会保留定义的顺序,类似slice。

注释

//  行注释

/* 块注释 */ 

默认值

string        // 默认值为""
bytes         // 默认值为空bytes
bools         // 默认值为false
numberic type // 默认值为0
enums         // 默认值为第一个定义的enum值,必须为0

枚举类型

syntax = "proto3";

message Order{
  int32  order_id = 1;
  int32 num = 2;
  string timestamp = 3;

  enum Category{  // 定义enum类型
    BOOK = 0;     // 第一个元素赋值为常量0。这里是赋值,不是编号
    CAR = 1;
    MOBILE=2;
    COMPUTER = 3;
    NOTEBOOK=4;
    TEA = 5;
  }
  Category category = 4;
}

注意:

  • 枚举类型第一个元素必须赋值为0

枚举字段设置别名

通过 option allow_alias = true;字段设置为允许别名。

message Order{
  int32  order_id = 1;
  int32 num = 2;
  string timestamp = 3;

  enum Category{  // 定义enum类型
    option allow_alias = true;
    BOOK = 0;
    CAR = 1;
    MOBILE=2;
    COMPUTER = 3;
    NOTEBOOK=4;
    TEA = 5;
    TABLE = 5;   // 设置别名
  }
  Category category = 4;
}

定义service

可以在.proto文件中定义RPC服务接口,protocol buffer的编译器将自动产生接口代码和存根。

// 定义格式
service SearchService{
  rpc Search(SearchRequest) returns (SearchResponse);  // 定义一个rpc服务,且具有一个方法,该方法接收SearchRequest,并返回一个SearchResponse。
}

RPC远程过程调用

一个完整的RPC架构里面包含了四个核心的组件,分别是Client ,Server, Client Stub以及Server Stub

客户端(Client): 服务的调用方。

服务端(Server):真正的服务提供者。

客户端存根:存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。

服务端存根:接收客户端发送过来的消息,将消息解包,并调用本地的方法。

rpc原理

① 服务调用方(client)以本地调用方式调用服务;

② client stub(客户端存根)接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;

③ client stub找到服务地址,并将消息发送到服务端;

④ server 端接收到消息;

⑤ server stub(服务端存根)收到消息后进行解码;

⑥ server stub根据解码结果调用本地的服务;

⑦ 本地服务执行并将结果返回给server stub;

⑧ server stub将返回结果打包成能够进行网络传输的消息体;

⑨ 按地址将消息发送至调用方;

⑩ client 端接收到消息;

⑪ client stub收到消息并进行解码;

⑫ 调用方得到最终结果。

需要解决的问题

本地过程调用发生在同一进程中,共享同一个内存空间,但RPC通信需要跨不同的机器和不同的进程。因此需要解决下面问题:

  1. 如何确定函数? 在本地调用中,函数主体通过函数指针函数指定,然后调用 add 函数,编译器通过函数指针函数自动确定 add 函数在内存中的位置。但是在 RPC 中,调用不能通过函数指针完成,因为它们的内存地址可能完全不同。因此,调用方和被调用方都需要维护一个{ function <-> ID }的映射表,以确保调用正确的函数。
  2. 如何表达参数? 本地过程调用中传递的参数是通过堆栈内存结构实现的,但 RPC 不能直接使用内存传递参数,因此参数或返回值需要在传输期间序列化并转换成字节流,反之亦然。
  3. 如何进行网络传输? 函数的调用方和被调用方通常是通过网络连接的,也就是说,function ID 和序列化字节流需要通过网络传输,因此,只要能够完成传输,调用方和被调用方就不受某个网络协议的限制。.例如,一些 RPC 框架使用 TCP 协议,一些使用 HTTP。

net/rpc包

在go中官方提供了net/rpc包提供对rpc的支持。

在编程实现过程中,服务器端需要注册结构体对象,然后通过对象所属的方法暴露给调用者,从而提供服务,该方法为输出方法。输出的方法可以被远程调用。在定义输出方法的时候,需要注意格式:

// 定义输出方法
func (t *T) MethodName(request T1, response *T2) error   // T, T1,T2 为定义的类型

注意:

  • 该输出方法,有且只能有两个参数,却第二个参数为指针类型,返回error

示例:

// 给出一个float类型的变量,作为圆形半径,要求通过RPC调用,返回对应的圆形面积。

// server.go
package main

import (
	"fmt"
	"log"
	"math"
	"net"
	"net/http"
	"net/rpc"
)

type MathUtils struct {
}

// 该方法对外暴露
func (mu *MathUtils) ComputeCircleArea(req float64, resp *float64) error {
	*resp = math.Pow(req, 2) * math.Pi
	return nil
}

func main() {
	// 1. 初始化指针类型数据
	mathUtils := new(MathUtils)
	// 2. 调用rpc包对服务对象进行注册
	err := rpc.Register(mathUtils)
	if err != nil {
		fmt.Println("register err:", err)
		return
	}
	// 3. 服务注册到http协议上,调用者可以通过http协议进行调用
	rpc.HandleHTTP()
	// 4. 监听端口
	listen, err := net.Listen("tcp", ":8081")
	if err != nil {
		log.Fatal(err)
	}
	http.Serve(listen, nil)
}


// client.go
package main

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

func main() {
	// 连接到基于http的rpc服务
	http, err := rpc.DialHTTP("tcp", "127.0.0.1:8081")
	if err != nil {
		log.Fatalln("connect failed", err)
	}
	var req float64 = 10
	var circleArea float64
	err = http.Call("MathUtils.ComputeCircleArea", req, &circleArea)
	if err != nil {
		log.Fatalln("call failed", err)
	}
	fmt.Printf("面积为:%f\n", circleArea)
}

多参数的调用

请求变量为结构体

// model.go
type Rectangle struct {
	Long  float64
	Width float64
}

// server_multi.go
package main

import (
	"log"
	"net"
	"net/http"
	"net/rpc"
)

type MathUtils struct {
}

func (mu *MathUtils) ComputeArea(req Rectangle, resp *float64) error {
	*resp = req.Long * req.Width
	return nil
}

func main() {
	// 1.初始化数据
	rect := new(MathUtils)

	// 2. 对服务对象进行注册
	err := rpc.Register(rect)
	if err != nil {
		log.Fatal(err)
	}

	// 3.服务注册到http协议
	rpc.HandleHTTP()

	// 4.监听端口
	listen, err := net.Listen("tcp", ":8082")
	if err != nil {
		log.Fatal(err)
	}
	http.Serve(listen, nil)
}


// client_multi.go
package main

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

func main() {
	http, err := rpc.DialHTTP("tcp", "127.0.0.1:8082")
	if err != nil {
		log.Fatal(err)
	}
	var rect = Rectangle{10, 20}
	var area float64
	err = http.Call("MathUtils.ComputeArea", rect, &area)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("area: ", area)
}

rpc和protobuf结合使用

需求:假设在一个系统中,有订单模块,其他模块要通过rpc,根据订单ID和时间戳获取订单信息,如果获取成功就返回相应的订单信息;如果查询不到返回失败信息。

1)传输数据格式.proto文件定义

syntax = "proto3";

package message;

option go_package="./;message";

// 订单请求参数
message OrderRequest{
  string OrderId = 1;
  int64 Timestamp = 2;
}

// 订单信息
message OrderInfo{
  string OrderId = 1;
  string OrderName = 2;
  string OrderStatus = 3;
}

2) 服务端

package main

import (
	"errors"
	"learn_rpc/message"
	"log"
	"net"
	"net/http"
	"net/rpc"
	"time"
)

type OrderService struct {
}

func (orderService *OrderService) GetOrderInfo(req message.OrderRequest, resp *message.OrderInfo) error {
    // 通过orderMap模拟数据库数据
	orderMap := make(map[string]message.OrderInfo, 3)
	orderMap["19216800"] = message.OrderInfo{
		OrderId: "19216800", OrderName: "衣服", OrderStatus: "已付款",
	}
	orderMap["19216801"] = message.OrderInfo{OrderId: "19216801", OrderName: "零食", OrderStatus: "已付款"}
	orderMap["19216802"] = message.OrderInfo{OrderId: "19216802", OrderName: "水果", OrderStatus: "未付款"}

	current := time.Now().Unix()
	if req.GetTimestamp() > current {
		// 传入的时间戳大于当前时间
		*resp = message.OrderInfo{OrderId: "0", OrderName: "", OrderStatus: "订单异常"}
		err := errors.New("参数错误")
		return err
	}
	result, ok := orderMap[req.OrderId]
	if !ok {
		*resp = message.OrderInfo{OrderId: "0", OrderName: "", OrderStatus: "订单不存在"}
		return errors.New("订单不存在")
	}
	*resp = result
	return nil
}

func main() {
	// 1. 初始化指针变量
	o := new(OrderService)
	// 2. 注册服务对象
	err := rpc.Register(o)
	if err != nil {
		log.Fatal(err)
	}
	// 3. 将服务对象注册到http协议
	rpc.HandleHTTP()
	// 4. 监听网络
	listen, err := net.Listen("tcp", ":8083")
	if err != nil {
		log.Fatal(err)
	}
	http.Serve(listen, nil)
}

3)客户端

package main

import (
	"fmt"
	"learn_rpc/message"
	"log"
	"net/rpc"
	"time"
)

func main() {
	client, err := rpc.DialHTTP("tcp", "127.0.0.1:8083")
	if err != nil {
		log.Fatal(err)
	}
	var reqMessage = message.OrderRequest{OrderId: "19216802", Timestamp: time.Now().Unix()}
	var resq = new(message.OrderInfo)
	err = client.Call("OrderService.GetOrderInfo", reqMessage, resq)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resq)
}

gRPC

gRPC是google开源的高性能远程过程调用RPC框架,该框架实现了负载均衡,跟踪,智能监控,身份验证等功能,可以实现系统间的高效连接。

网站: https://grpc.io

gRPC调用执行过程:

gRPC中默认采用的数据格式为protocol buffers

grpc-go的使用

grpc-go是grpc库的golang版本实现。

安装

// 安装grpc-go
go get -u google.golang.org/grpc

// 安装go的protobuf编译插件,注意添加到环境变量
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

// 使用protoc进行编译
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative helloworld/helloworld.proto

使用示例:

// hello/search.proto
syntax="proto3";

package hello;

option go_package="./;hello";

message UserRequest{
  string user_name = 1;
}

message UserResponse{
  string user_id = 1;
  string user_name = 2;
  string user_age = 3;
  string user_gender = 4;
  repeated string hobby = 5;  // 修饰符为可变数组,go转为切片
}

service Search{
  rpc GetUserInfo(UserRequest)returns(UserResponse){};
}

// 服务端 server/main.go
package main

import (
	"context"
	"errors"
	"google.golang.org/grpc"
	pb "learn_grpc/hello"
	"log"
	"net"
)

type UserInfoService struct {
	pb.UnimplementedSearchServer      // 注意:需要添加结构体嵌套
}

func (s *UserInfoService) GetUserInfo(context context.Context, req *pb.UserRequest) (resp *pb.UserResponse, err error) {
	// 通过用户名查询用户信息
	name := req.GetUserName()
	// 查询用户信息
	if name == "zs" {
		resp = &pb.UserResponse{
			UserId: "1", UserName: "zs", UserAge: "12", UserGender: "男",
			Hobby: []string{"唱歌", "跳舞"},
		}
		return
	} else {
		resp = &pb.UserResponse{}
		err = errors.New("查询内容为空")
		return
	}
}

func main() {
	// 1. 监听
	addr := "127.0.0.1:8084"
	listen, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatal("监听错误")
	}
	// 2. 实例化grpc微服务
	server := grpc.NewServer()

	// 3. 在grpc注册微服务
	var userInfoService UserInfoService
	pb.RegisterSearchServer(server, &userInfoService)
	// 4. 启动服务端
	err = server.Serve(listen)
	if err != nil {
		panic(err)
	}
}

// 客户端 client/main.go
package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	pb "learn_grpc/hello"
	"log"
)

func main() {
	// 1. 连接服务端
	conn, err := grpc.Dial("127.0.0.1:8084", grpc.WithInsecure())
	if err != nil {
		log.Fatalln("dial err", err)
	}
	defer conn.Close()

	// 2.实例化grpc客户端
	client := pb.NewSearchClient(conn)

	var req = pb.UserRequest{UserName: "zs"}
	// 3. 访问服务端
	info, err := client.GetUserInfo(context.Background(), &req)
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(info)
}

go-micro

posted @   学习记录13  阅读(190)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示