go-micro微服务框架

背景

  已经学习了微服务之间通信采用的通信协议,如何实现服务的注册和发现,搭建服务管理集群,以及服务与服务之间的RPC通信方式。具体的内容包括:protobuf协议,consul及docker部署consul集群,gRPC框架的使用等具体的实现方案。

  以上这些具体的方案都是为了解决微服务实践过程中具体的某个问题而提出的,实现微服务架构的项目开发。但是,在具体的项目开发过程中,开发者聚焦的是业务逻辑的开发和功能的实现,大量的环境配置,调试搭建等基础性工作会耗费相当一部分的精力,因此有必要将微服务架构中所涉及到的,相关的解决方案做集中管理和维护。这就是我们要学习的Micro。

概述

  Micro是一个简化分布式开发的微服务生态系统,该系统为开发分布式应用程序提供了高效,便捷的模块构建。主要目的是简化分布式系统的开发。

  学习完该框架以后,可以方便开发者们非常简单的开发出微服务架构的项目,并且随着业务模块的增加和功能的增加,Micro还能够提供管理微服务环境的工具和功能。

micro组成

  micro是一个微服务工具包,是由一系列的工具包组成的,如下图所示:

 

 

   Go Micro:用于在Go中编写微服务的插件式RPC框架。它提供了用于服务发现,客户端负载平衡,编码,同步和异步通信库。

  API: API主要负责提供将HTTP请求路由到相应微服务的API网关。它充当单个入口点,可以用作反向代理或将HTTP请求转换为RPC。

  Sidecar:一种对语言透明的RPC代理,具有go-micro作为HTTP端点的所有功能。虽然Go是构建微服务的伟大语言,但您也可能希望使用其他语言,因此Sidecar提供了一种将其他应用程序集成到Micro世界的方法。

  Web:用于Micro Web应用程序的仪表板和反向代理。我们认为应该基于微服务建立web应用,因此被视为微服务领域的一等公民。它的行为非常像API反向代理,但也包括对web sockets的支持。

  CLI:一个直接的命令行界面来与你的微服务进行交互。它还使您可以利用Sidecar作为代理,您可能不想直接连接到服务注册表。

  Bot:Hubot风格的bot,位于您的微服务平台中,可以通过Slack,HipChat,XMPP等进行交互。它通过消息传递提供CLI的功能。可以添加其他命令来自动执行常见的操作任务。

 

工具包介绍

  1.API:启用API作为一个网关或代理,来作为微服务访问的单一入口。它应该在您的基础架构的边缘运行。它将HTTP请求转换为RPC并转发给相应的服务。

 

 

   2.Web:UI是go-micro的web版本,允许通过UI交互访问环境。在未来,它也将是一种聚合Micro Web服务的方式。它包含一种Web应用程序的代理方式。将/[name]通过注册表路由到相应的服务。Web UI将前缀“go.micro.web。”(可以配置)添加到名称中,在注册表中查找它,然后将进行反向代理。

 

 

   3.Sidecar:该Sidecar是go-micro的HTTP接口版本。这是将非Go应用程序集成到Micro环境中的一种方式。

 

 

   4.Bot:Bot是一个Hubot风格的工具,位于您的微服务平台中,可以通过Slack,HipChat,XMPP等进行交互。它通过消息传递提供CLI的功能。可以添加其他命令来自动执行常用操作任务。

 

  5.CLI:Micro CLI是go-micro的命令行版本,它提供了一种观察和与运行环境交互的方式。

  6.Go-Micro:Go-micro是微服务的独立RPC框架。它是该工具包的核心,并受到上述所有组件的影响。在这里,我们将看看go-micro的每个特征。

Go-Micro特性

  Registry:主要负责服务注册和发现功能。我们之前学习过的consul,就可以和此处的Registry结合起来,实现服务的发现功能。

  Selector:selector主要的作用是实现服务的负载均衡功能。当某个客户端发起请求时,将首先查询服务注册表,返回当前系统中可用的服务列表,然后从中选择其中一个节点进行查询,保证节点可用。

  Broker:Broker是go-micro框架中事件发布和订阅的接口,主要是用消息队列的方式实现信息的接收和发布,用于处理系统间的异步功能。

  Codec:go-micro中数据传输过程中的编码和解码接口。go-micro中有多重编码方式,默认的实现方式是protobuf,除此之外,还有json等格式。

  Transport:go-micro框架中的通信接口,有很多的实现方案可以选择,默认使用的是http形式的通信方式,除此以外,还有grpc等通信方式。

  Client和Server:分别是go-micro中的客户端接口和服务端接口。client负责调用,server负责等待请求处理。

 

创建微服务

服务的定义

  在micro框架中,服务用接口来进行定义,服务被定义为Service,完整的接口定义如下:

type Service interface {
    Init(...Option)
    Options() Options
    Client() client.Client
    Server() server.Server
    Run() error
    String() string
}

初始化服务实例

  micro框架,除了提供Service的定义外,提供创建服务实例的方法供开发者调用:

service := micro.NewService()

  完整的实例化对象代码如下所示:

func main() {
    //创建一个新的服务对象实例
    service := micro.NewService(
        micro.Name("helloservice"),
        micro.Version("v1.0.0"),
    )
}

  开发者可以直接调用micro.Name为服务设置名称,设置版本号等信息。在对应的函数内部,调用了server.Server.Init函数对配置项进行初始化。

定义服务接口,实现服务业务逻辑

  我们依然通过案例来讲解相关的知识点:在学校的教务系统中,有学生信息管理的需求。学生信息包含学生姓名,学生班级,学习成绩组成;可以根据学生姓名查询学生的相关信息,我们通过rpc调用和学生服务来实现该案例。

1、定义.proto文件
syntax = 'proto3';
package message;

//学生数据体
message Student {
    string name = 1; //姓名
    string classes = 2; //班级
    int32 grade = 3; //分数
}

//请求数据体定义
message StudentRequest {
    string name = 1;
}

//学生服务
service StudentService {
    //查询学生信息服务
    rpc GetStudent (StudentRequest) returns (Student);
}
2、编译.proto文件

  在原来学习gRPC框架时,我们是将.proto文件按照grpc插件的标准来进行编译。而现在,我们学习的是go-micro,因此我们可以按照micro插件来进行编译。micro框架中的protobuf插件,我们需要单独安装。

  安装micro框架的protobuf插件

go get github.com/micro/protobuf/{proto,protoc-gen-go}

  指定micro插件进行编译

protoc --go_out=plugins=micro:. message.proto
3、编码实现服务功能

服务端

package main

import (
    "WXProjectDemo/goMicroDemo/message"
    "context"
    "errors"
    "fmt"
    "github.com/micro/go-micro"
    "github.com/micro/go-micro/registry"
    "github.com/micro/go-plugins/registry/consul"
    "log"
    "time"
)

// 学生服务管理实现
type StudentManager struct {
}

// 获取学生信息的服务接口实现
func (this *StudentManager) GetStudent(ctx context.Context, request *message.StudentRequest, response *message.Student) error {
    studentMap := map[string]message.Student{
        "davie":  message.Student{Name: "davie", Classes: "软件工程专业", Grade: 80},
        "steven": message.Student{Name: "steven", Classes: "计算机科学与技术", Grade: 90},
        "tony":   message.Student{Name: "tony", Classes: "计算机网络工程", Grade: 85},
        "jack":   message.Student{Name: "jack", Classes: "工商管理", Grade: 96},
    }
    if request.Name == "" {
        return errors.New("请求参数错误,请重新请求。")
    }
    // 获取对应的student
    student := studentMap[request.Name]
    if student.Name != "" {
        fmt.Println(student.Name, student.Classes, student.Grade)
        *response = student
        return nil
    }
    return errors.New("未查询到相关学生信息")
}

func main() {

    consulReg := consul.NewRegistry( //新建一个consul注册的地址,也就是我们consul服务启动的机器ip+端口
        registry.Addrs("127.0.0.1:8500"),
    )

    //reg := etcdv3.NewRegistry(func(op *registry.Options) {
    //    op.Addrs = []string{"http://127.0.0.1:2379"}
    //})

    // 创建一个新的服务对象实例
    service := micro.NewService(
        micro.Name("student_service"),
        micro.Version("v1.0.0"),
        micro.Registry(consulReg),
        micro.RegisterTTL(10*time.Second),
        micro.RegisterInterval(5*time.Second),
    )

    // 服务初始化
    service.Init()
    // 注册
    message.RegisterStudentServiceHandler(service.Server(), new(StudentManager))
    // 运行
    err := service.Run()
    if err != nil {
        log.Fatal(err)
    }
}

  其中将服务注册到consul中,指定consul时,需要先将consul进行启动

客户端

package main

import (
    "WXProjectDemo/goMicroDemo/message"
    "context"
    "fmt"
    "github.com/micro/go-micro"
    "github.com/micro/go-micro/registry"
    "github.com/micro/go-plugins/registry/consul"
    "time"
)

func main() {
    consulReg := consul.NewRegistry( //新建一个consul注册的地址,也就是我们consul服务启动的机器ip+端口
        registry.Addrs("127.0.0.1:8500"),
    )
    //reg := etcdv3.NewRegistry(func(op *registry.Options) {
    //    op.Addrs = []string{
    //        "http://127.0.0.1:2379",
    //    }
    //})

    service := micro.NewService(
        micro.Name("student.client"),
        micro.Registry(consulReg),
    )
    service.Init()

    studentService := message.NewStudentServiceClient("student_service", service.Client())
    res, err := studentService.GetStudent(context.TODO(), &message.StudentRequest{Name: "davie"})
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(res.Name)
    fmt.Println(res.Classes)
    fmt.Println(res.Grade)
    time.Sleep(50 * time.Second)
}

  同样客户端也需要指定从consul中去发现服务

插件化

  我们提到过go-micro是支持插件化的基础的微服务框架,不仅仅是go-micro,整个的micro都被设计成为“可插拔”的机制。这里的–registry就是是“可插拔”的插件化机制的体现。因为在2019年最新的代码中,go-micro中默认将服务注册到mdns中,同时支持开发者手动指定特定的服务发现组件。

  我们可以看到在这个过程中,我们的服务的程序没有发生任何变化,但是却轻松的实现了服务注册发现组件的更换,这就是插件化的优势,利用插件化能最大限度的解耦。

  在go-micro框架中,支持consul,etcd,zookeeper,dns等组件实现服务注册和发现的功能。如果有需要,开发者可以根据自己的需要进行服务发现组件的替换。

服务注册发现的原理

  让我们再回顾一下服务注册与发现的原理:服务注册发现是将所有的服务注册到注册组件中心,各服务在进行互相调用功能时,先通过查询方法获取到要调用的服务的状态信息和地址,然后向对应的微服务模块发起调用。我们学习过consul的工作原理和环境搭建,congsul的工作原理图如下所示:

 

弊端与解决方法

  服务实例与发现组件的工作机制是:当服务开启时,将自己的相关信息注册到发现组件中,当服务关闭时,发送卸载或者移除请求。在实际生产环境中,服务可能会出现很多异常情况,发生宕机或者其他等情况,往往服务进程会被销毁,或者网络出现故障也会导致通信链路发生问题,在这些情况下,服务实例会在服务发现组件中被移除。

TTL和间隔时间

  为了解决这个问题,go-micro框架提供了TTL机制和间隔时间注册机制。TTL是Time-To-Live的缩写,指定一次注册在注册组件中的有效期,过期后便会删除。而间隔时间注册则表示定时向注册组件中重新注册以确保服务在线。

  在微服务创建时通过配置来完成。具体代码如下:

...
service := micro.NewService(
micro.Name("student_service"),
micro.Version("v1.0.0"),
micro.RegisterTTL(10*time.Second),
micro.RegisterInterval(5*time.Second),
)
...

解耦利器–事件驱动机制

  之前我们已经学习了使用go-micro创建微服务,并实现了服务的调用。我们具体的实现是实例化了client对象,并调用了对应服务的相关方法。这种方式可以实现系统功能,但有比较大的缺点。

  我们通过举例来说明:在某个系统中存在用户服务(user service)、产品服务(product service)和消息服务(message service)。如果用户服务中要调用消息服务中的功能方法,则具体的实现方式可用下图所示方法表示:

 

  按照正常的实现是在user service模块的程序中实例化message service的一个client,然后进行RPC调用,调用sendMessage来实现发送消息。

缺点

  这种实现方式代码耦合度高,用户服务的模块中出现了消息服务模块的代码,不利于系统的扩展和功能的迭代开发。

发布/订阅机制

事件驱动

  依然是上述的案例,用户服务在用户操作的过程中,需要调用消息服务的某个方法,假设为发送验证码消息的一个方法。为了使系统代码能够实现解耦,用户服务并不直接调用消息服务的具体的方法,而是将用户信息等相关数据发送到一个中间组件,该组件负责存储消息,而消息服务会按照特定的频率访问中间的消息存储组件,并取出其中的消息,然后执行发送验证码等操作。具体的示意图如下所示:

 

  在上述的架构图中,我们可以看到,相较于之前的实现,多了一个中间的消息组件系统。

事件发布

  只有当用户服务中的某个功能执行时,才会触发相应的事件,并将对应的用户数据等消息发送到消息队列组件中,这个过程我们称之为事件发布。

事件订阅

  与事件发布对应的是事件订阅。我们增加消息队列组件的目的是实现模块程序的解耦,原来是程序调用端主动进行程序调用,现在需要由另外一方模块的程序到消息队列组件中主动获取需要相关数据并进行相关功能调用。这个主动获取的过程称之为订阅。

  基于消息发布/订阅的消息系统有很多种框架的实现,常见的有:Kafka、RabbitMQ、ActiveMQ、Kestrel、NSQ等。

Broker

  在我们介绍go-micro的时已经提到过,go-micro整个框架都是插件式的设计。没错,这里的发布/订阅也是通过接口设计来实现的。

定义
type Broker interface {
    Init(...Option) error
    Options() Options
    Address() string
    Connect() error
    Disconnect() error
    Publish(topic string, m *Message, opts ...PublishOption) error
    Subscribe(topic string, h Handler, opts ...SubscribeOption) (Subscriber, error)
    String() string
}

  如果我们要具体实现事件的发布和订阅功能,只需要安装对应支持的go-plugins插件实现就可以了。go-plugins里支持的消息队列方式有:kafka、nsq、rabbitmq、redis等。同时,go-micro本身支持三种broker,分别是http、nats、memory,默认的broker是http,在实际使用过程中往往使用第三方的插件来进行消息发布/订阅的实现。

  我们演示RabbitMQ插件实现的事件订阅和发布机制。

 

安装go-plugins

...

Broker实现

MQTT介绍及环境搭建

MQTT简介

  MQTT全称是Message Queuing Telemetry Transport,翻译为消息队列遥测传输协议,是一种基于发布/订阅模式的"轻量级"的通讯协议,该协议基于TCP/IP协议,由IBM在1999年发布。MQTT的最大优点在于,可以用极少的代码和有限的宽带,为连接远程设备提供提供实时可靠的消息服务。

MQTT安装

  windows系统上的mqtt的安装和启动,可以到https://activemq.apache.org/中下载最新的安装文件,然后进行安装和运行。

编程实现

  接下来进行订阅和发布机制的编程的实现。

消息组件初始化  

  如果要想使用消息组件完成消息的发布和订阅,首先应该让消息组件正常工作。因此,需要先对消息组件进行初始化。我们可以在服务创建时,对消息组件进行初始化,并进行可选项配置,设置使用mqtt作为消息组件。代码实现如下:

...
server := micro.NewService(
        micro.Name("go.micro.srv"),
        micro.Version("latest"),
        micro.Broker(mqtt.NewBroker()),
)
...

  可以使用micro.Broker来指定特定的消息组件,并通过mqtt.NewBroker初始化一个mqtt实例对象,作为broker参数。

消息订阅

  因为是时间驱动机制,消息的发送方随时可能发布相关事件。因此需要消息的接收方先进行订阅操作,避免遗漏消息。go-micro框架中可以通过broker.Subscribe实现消息订阅。编程代码如下所示:

...
pubSub := service.Server().Options().Broker
_, err := pubSub.Subscribe("go.micro.srv.message", func(event broker.Event) error {
        var req *message.StudentRequest
        if err := json.Unmarshal(event.Message().Body, &req); err != nil {
            return err
        }
        fmt.Println(" 接收到信息:", req)
        //去执行其他操作

        return nil
    })
...
消息发布

  完成了消息的订阅,我们再来实现消息的发布。在客户端实现消息的发布。在go-micro框架中,可以使用broker.Publish来进行消息的发布,具体的代码如下:

  

...

brok := service.Server().Options().Broker
if err := brok.Connect(); err != nil {
    log.Fatal(" broker connection failed, error : ", err.Error())
}

student := &message.Student{Name: "davie", Classes: "软件工程专业", Grade: 80, Phone: "12345678901"}
msgBody, err := json.Marshal(student)
if err != nil {
    log.Fatal(err.Error())
}
msg := &broker.Message{
    Header: map[string]string{
        "name": student.Name,
    },
    Body: msgBody,
}

err = brok.Publish("go.micro.srv.message", msg)
if err != nil {
    log.Fatal(" 消息发布失败:%s\n", err.Error())
} else {
    log.Print("消息发布成功")
}

...

弊端

  在服务端通过fmt.println日志,可以输出event.Message().Body)数据,其格式为:

{"name":"davie","classes":"软件工程专业","grade":80,"phone":"12345678901"}

  我们可以看到在服务实例之间传输的数据格式是json格式。根据之前学习proto知识可以知道,在进行消息通信时,采用JSON格式进行数据传输,其效率比较低。

  因此,这意味着,当我们在使用第三方消息组件进行消息发布/订阅时,会失去对protobuf的使用。这对追求高消息的开发者而言,是需要解决和改进的问题。因为使用protobuf可以直接在多个服务之间使用二进制流数据进行传输,要比json格式高效的多。

googlepubsub

  在go-micro框架中内置的Broker插件中,有google提供的googlepubsub插件实现,位于代理层之上,同时还省略了使用第三方代理消息组件(如mqtt)。

Micro负载均衡组件–Selector

   我们说过go-micro具备负载均衡功能。所谓负载均衡,英文为Load Balance,其意思是将负载进行平衡、分摊到多个操作单元上进行执行。例如Web服务器,应用服务器,微服务程序服务器等,以此来完成达到高并发的目的。

  当只有一台服务部署程序时,是不存在负载均衡问题的,此时所有的请求都由同一台服务器进行处理。随着业务复杂度的增加和功能迭代,单一的服务器无法满足业务增长需求,需要靠分布式来提高系统的扩展性,随着而来的就是负载均衡的问题。因此需要加入负载均衡组件或者功能,两者的区别和负载均衡的作用如下所示:

 

  从图中可以看到,用户先访问负载均衡器,再由负载均衡器对请求进行处理,进而分发到不同的服务器上的服务程序进行处理。

  负载均衡器主要处理四种请求,分别是:HTTP、HTTPS、TCP、UDP。

负载均衡算法

  负载均衡器的作用既然是负责接收请求,并实现请求的分发,因此需要按照一定的规则进行转发处理。负载均衡器可以按照不同的规则实现请求的转发,其遵循的转发规则称之为负载均衡算法。常用的负载均衡算法有以下几个:

  Round Robin(轮训算法):所谓轮训算法,其含义很简单,就是按照一定的顺序进行依次排队分发。当有请求队列需要转发时,为第一个请求选择可用服务列表中的第一个服务器,为下一个请求选择服务列表中的第二个服务器。按照此规则依次向下进行选择分发,直到选择到服务器列表的最后一个。当第一次列表转发完毕后,重新选择第一个服务器进行分发,此为轮训。

  Least Connections(最小连接):因为分布式系统中有多台服务器程序在运行,每台服务器在某一个时刻处理的连接请求数量是不一样的。因此,当有新的请求需要转发时,按照最小连接数原则,负载均衡器会有限选择当前连接数最小的服务器,以此来作为转发的规则。

  Source(源):还有一种常见的方式是将请求的IP进行hash计算,根据计算结果来匹配要转发的服务器,然后进行转发。这种方式可以一定程度上保证特定用户能够连接到相同的服务器。

Mico的Selector

  Selector的英文是选择器的意思,在Micro中实现了Selector组件,运行在客户端实现负载均衡功能。当客户端需要调用服务端方法时,客户端会根据其内部的selector组件中指定的负载均衡策略选择服务注册中的一个服务实例。Go-micro中的Selector是基于Register模块构建的,提供负载均衡策略,同时还提供过滤、缓存和黑名单等功能。

Selector定义

  首先,让我们来看一下Selector的定义:

type Selector interface {
    Init(opts ...Option) error
    Options() Options
    // Select returns a function which should return the next node
    Select(service string, opts ...SelectOption) (Next, error)
    // Mark sets the success/error against a node
    Mark(service string, node *registry.Node, err error)
    // Reset returns state back to zero for a service
    Reset(service string)
    // Close renders the selector unusable
    Close() error
    // Name of the selector
    String() string
}

  如上是go-micro框架中的Selector的定义,Selector接口定义中包含Init、Options、Mark、Reset、Close、String方法。其中Select是核心方法,可以实现自定义的负载均衡策略,Mark方法用于标记服务节点的状态,String方法返回自定义负载均衡器的名称。

DefaultSelector

  在selector包下,除Selector接口定义外,还包含DefaultSelector的定义,作为go-micro默认的负载均衡器而被使用。DefaultSelector是通过NewSelector函数创建生成的。NewSelector函数实现如下:

func NewSelector(opts ...Option) Selector {
    sopts := Options{
        Strategy: Random,
    }

    for _, opt := range opts {
        opt(&sopts)
    }

    if sopts.Registry == nil {
        sopts.Registry = registry.DefaultRegistry
    }

    s := ®istrySelector{
        so: sopts,
    }
    s.rc = s.newCache()

    return s
}

  在NewSelector中,实例化了registrySelector对象并进行了返回,在实例化的过程中,配置了Selector的Options选项,默认的配置是Random。我们进一步查看会发现Random是一个func,定义如下:

func Random(services []*registry.Service) Next {
    var nodes []*registry.Node

    for _, service := range services {
        nodes = append(nodes, service.Nodes...)
    }

    return func() (*registry.Node, error) {
        if len(nodes) == 0 {
            return nil, ErrNoneAvailable
        }

        i := rand.Int() % len(nodes)
        return nodes[i], nil
    }
}

  该算法是go-micro中默认的负载均衡器,会随机选择一个服务节点进行分发;除了Random算法外,还可以看到RoundRobin算法,如下所示:

func RoundRobin(services []*registry.Service) Next {
    var nodes []*registry.Node

    for _, service := range services {
        nodes = append(nodes, service.Nodes...)
    }

    var i = rand.Int()
    var mtx sync.Mutex

    return func() (*registry.Node, error) {
        if len(nodes) == 0 {
            return nil, ErrNoneAvailable
        }

        mtx.Lock()
        node := nodes[i%len(nodes)]
        i++
        mtx.Unlock()
        return node, nil
    }
}

registrySelector

  registrySelector是selector包下default.go文件中的结构体定义,具体定义如下:

type registrySelector struct {
    so Options
    rc cache.Cache
}

缓存Cache

  目前已经有了负载均衡器,我们可以看到在Selector的定义中,还包含一个cache.Cache结构体类型,这是什么作用呢?

  有了Selector以后,我们每次请求负载均衡器都要去Register组件中查询一次,这样无形之中就增加了成本,降低了效率,没有办法达到高可用。为了解决以上这种问题,在设计Selector的时候设计一个缓存,Selector将自己查询到的服务列表数据缓存到本地Cache中。当需要处理转发时,先到缓存中查找,如果能找到即分发;如果缓存当中没有,会执行请求服务发现注册组件,然后缓存到本地。
具体的实现机制如下所示:

type Cache interface {
    // embed the registry interface
    registry.Registry
    // stop the cache watcher
    Stop()
}

func (c *cache) watch(w registry.Watcher) error {
    // used to stop the watch
    stop := make(chan bool)

    // manage this loop
    go func() {
        defer w.Stop()

        select {
        // wait for exit
        case <-c.exit:
            return
        // we've been stopped
        case <-stop:
            return
        }
    }()

    for {
        res, err := w.Next()
        if err != nil {
            close(stop)
            return err
        }
        c.update(res)
    }
}

  通过watch实现缓存的更新、创建、移除等操作。

黑名单

  在了解完了缓存后,我们再看看Selector中其他的方法。在Selector接口的定义中,还可以看到有Mark和Resetf昂发的声明。具体声明如下:

// Mark sets the success/error against a node
Mark(service string, node *registry.Node, err error)
// Reset returns state back to zero for a service
Reset(service string)

  Mark方法可以用于标记服务注册和发现组件中的某一个节点的状态,这是因为在某些情况下,负载均衡器跟踪请求的执行情况。如果请求被转发到某天服务节点上,多次执行失败,就意味着该节点状态不正常,此时可以通过Mark方法设置节点变成黑名单,以过滤掉掉状态不正常的节点。

 

第006节:RESTful API设计标准和实践

划分调用范围

  在整体的系统架构中,我们会将系统分为前台和后台。前台负责与用户交互,展示数据,执行操作。后台负责业务逻辑处理,数据持久化等操作。在系统运行过程中,前台和后台,后台和后台都可能发生功能调用:

  内部调用:后台各个微服务之间的互相调用,属于系统后台内部的调用,称之为内部调用。

  外部调用:前台与后台的接口请求调用,通常被城之外外部调用。

技术选型

  在开发实践中,我们对于外部调用和内部调用所采用的技术方案会有所不同。

    RPC调用:后台各个服务之间内部的互相调用,为了实现高效率的服务的交互,通常采用RPC的方式进行实现。

    REST:对于前端客户端通过HTTP接口,与后台交互的场景。因为涉及到对不同资源的管理和操作,因此往往采用RESTful标准进行实现。

Go-Micro API网关

  Micro框架中有API网关的功能。API网关的作用是为微服务做代理,负责将微服务的RPC方法代理成支持HTTP协议的web请求,同时将用户端使用的URL进行暴露。

安装Micro工具

  要想使用go-micro 的api网关功能。需要下载Micro源码并安装Mico。

安装Micro

  可以直接通过go get命令下载并安装,具体命令为:

go get -u github.com/micro/micro
安装micro
go install github.com/micro/micro

  使用go install命令安装micro框架,等待命令执行结束。

检验Micro安装成功

micro --version
micro version 1.9.1

Micro API工作原理

  micro工具提供了构建api网关服务的功能,并基于go-micro框架进行编程实现,核心作用是把RPC形式的服务代理成为支持HTTP协议的WEB API请求。

运行Micro api服务

  可以通过如下命令启动micro api:

micro api

反向代理的API服务启动

  在Micro api功能中,支持多种处理请求路由的方式,我们称之为Handler。包括:API Handler、RPC Handler、反向代理、Event Handler,RPC等五种方式。在本案例中,我们使用反向代理来进行演示。

反向代理

  格式:/[service]

  请求/响应:HTTP方式

  micro api启动时通过–handler=proxy设置

  因此,反向代理形式的micro api网关服务启动命令为:

micro api --handler=http

  在本案例中,我们将micro api的反向代理和REST代表的HTTP WEB请求结合起来一起使用。

安装go-restful

  可以通过安装go-restful库来实现RESTful风格的路径映射,从而实现HTTP的WEB API服务。安装go-restful的命令如下:

go get github.com/emicklei/go-restful

服务定义和编译

  定义学生消息体proto文件:

syntax = 'proto3';

package proto;

message Student {
    string id = 1;
    string name = 2;
    int32 grade = 3;
    string classes = 4;
}

message Request {
    string name = 1;
}

service StudentService {
    rpc GetStudent (Request) returns (Student);
}

  在proto文件中定义了Student、Request消息体和rpc服务。使用micro api网关功能,编译proto文件,需要生成micro文件。编译生成该文件需要使用到一个新的protoc-gen-micro库,安装protoc-gen-micro库命令如下:

protoc --go_out=. --micro_out=. student.proto

  上述命令执行成功后,会自动生成两个go语言文件:student.pb.go和student.micro.go。

  micro.go文件中生成的内容包含服务的实例化,和相应的服务方法的底层实现。

服务端实现

  我们都知道正常的Web服务,是通过路由处理http的请求的。在此处也是一样的,我们可以通过路由处理来解析HTTP请求的接口,service对象中包含路由处理方法。详细代码如下所示:

...
type StudentServiceImpl struct {
}

//服务实现
func (ss *StudentServiceImpl) GetStudent(ctx context.Context, request *proto.Request, resp *proto.Student) error {

    //tom
    studentMap := map[string]proto.Student{
        "davie":  proto.Student{Name: "davie", Classes: "软件工程专业", Grade: 80},
        "steven": proto.Student{Name: "steven", Classes: "计算机科学与技术", Grade: 90},
        "tony":   proto.Student{Name: "tony", Classes: "计算机网络工程", Grade: 85},
        "jack":   proto.Student{Name: "jack", Classes: "工商管理", Grade: 96},
    }

    if request.Name == "" {
        return errors.New(" 请求参数错误,请重新请求。")
    }

    //获取对应的student
    student := studentMap[request.Name]
    if student.Name != "" {
        fmt.Println(student.Name, student.Classes, student.Grade)
        *resp = student
        return nil
    }
    return errors.New(" 未查询当相关学生信息 ")
}

func main() {
    service := micro.NewService(
        micro.Name("go.micro.srv.student"),
    )

    service.Init()
    proto.RegisterStudentServiceHandler(service.Server(), new(StudentServiceImpl))

    if err := service.Run(); err != nil {
        log.Fatal(err.Error())
    }
}
...

  server程序进行服务的实现和服务的运行。

REST 映射

  现在,RPC服务已经编写完成。我们需要编程实现API的代理功能,用于处理HTTP形式的请求。
在rest.go文件中,实现rest的映射,详细代码如下:

type Student struct {
}

var (
    cli proto.StudentService
)

func (s *Student) GetStudent(req *restful.Request, rsp *restful.Response) {

    name := req.PathParameter("name")
    fmt.Println(name)
    response, err := cli.GetStudent(context.TODO(), &proto.Request{
        Name: name,
    })

    if err != nil {
        fmt.Println(err.Error())
        rsp.WriteError(500, err)
    }

    rsp.WriteEntity(response)
}

func main() {

    service := web.NewService(
        web.Name("go.micro.api.student"),
    )

    service.Init()

    cli = proto.NewStudentService("go.micro.srv.student", client.DefaultClient)

    student := new(Student)
    ws := new(restful.WebService)
    ws.Path("/student")
    ws.Consumes(restful.MIME_XML, restful.MIME_JSON)
    ws.Produces(restful.MIME_JSON, restful.MIME_XML)

    ws.Route(ws.GET("/{name}").To(student.GetStudent))

    wc := restful.NewContainer()
    wc.Add(ws)

    service.Handle("/", wc)

    if err := service.Run(); err != nil {
        log.Fatal(err)
    }
}

 

posted @ 2020-05-03 09:30  顽强的allin  阅读(6001)  评论(1编辑  收藏  举报