微服务
认识微服务
概念网址
https://www.topgoer.com/
行业背景
不同行业IT系统更新频率

IT系统存在的问题

微服务架构在企业中应用情况

docker在企业中的使用情况

什么是微服务
- 使用一套小服务来开发单个应用的方式,每个服务运行在独立的进程里,一般采用轻量级的通讯机制互联,并且它们可以通过自动化的方式部署
- 什么叫微?
- 单一功能
- 代码少,不是,而且代码多
- 架构变的复杂了
- 微服务是设计思想,不是量的体现
微服务的特点
- 单一职责,此时项目专注于登录和注册
- 轻量级的通信,通信与平台和语言无关,http是轻量的,例如java的RMI属于重量的
- 隔离性,数据隔离
- 有自己的数据
- 技术多样性
微服务诞生背景
- 互联网行业的快速发展,需求变化快,用户数量变化快
- 敏捷开发深入人心,用最小的代价,做最快的迭代,频繁修改、测试、上线
- 容器技术的成熟,是微服务的技术基础
互联网架构演进之路

单体架构
- 所有功能放一个项目里
- 应用和数据库服务器可能部在一起,分开部
- 优点:
- 简单,高效,小型项目
- 缺点:
- 扛不住
- 技术栈受限

垂直架构
- 将大项目架构拆分成一个一个单体架构
- 优点:
- 不至于像单体无限扩大
- 缺点:
- 有瓶颈
- 成本高

SOA架构,面向服务的编程
- ESB,比较传统的中间件技术
- 优点:
- 代码提高重用性,ESB接口解耦
- 针对不同服务,做不同数据层和部署
- 缺点:
- ESB比较重量级
- 对于开发人员来说,系统层和服务层界限模糊

微服务架构
- 每个功能抽取成一个一个的服务
- 微服务之间访问是轻量级的,RPC

微服务架构的优势
- 独立性
- 使用者容易理解
- 技术栈灵活
- 高效团队
微服务架构的不足
- 额外的工作,服务的拆分
- 保证数据一致性
- 增加了沟通成本
微服务生态
硬件层
- 用docker+k8s去解决

通信层
- 网络传输,用RPC(远程过程调用)
- HTTP传输,GET POST PUT DELETE
- 基于TCP,更靠底层,RPC基于TCP,Dubbo(18年底改成支持各种语言),Grpc,Thrift
- 需要知道调用谁,用服务注册和发现
- 需要分布式数据同步:etcd,consul,zk

- 数据传递这里面可能是各种语言,各种技术,各种传递
- 效率对比:https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html
应用平台层
- 云管理平台、监控平台、日志管理平台,需要他们支持
- 服务管理平台,测试发布平台
- 服务治理平台
微服务层
- 用微服务框架实现业务逻辑
微服务详解
微服务架构

- 从程序架构来看如下


服务注册和发现
- 客户端做,需要实现一套注册中心,记录服务地址,知道具体访问哪个,轮询算法去做,加权轮询
- 服务端做,比较简单,服务端启动,自动注册即可,AWS的ELB去访问

- 微服务一般不用LVS负载,扩展实例需要改配置,不符合微服务弹性扩展思想
- 更多公司倾向于客户端做注册发现
- etcd解决分布式一致性,raft
- etcd使用场景:
- 注册发现
- 共享配置
- 分布式锁
- leader选举
rpc调用和服务监控
- RPC相关内容
- 数据传输:JSON Protobuf thrift
- 负载:随机算法 轮询 一致性hash 加权
- 异常容错:健康检测 熔断 限流
- 服务监控
- 日志收集
- 打点采样
RPC
RPC简介
- 远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议
- 该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程
- 如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用
流行RPC框架的对比

golang中如何实现RPC
- golang中实现RPC非常简单,官方提供了封装好的库,还有一些第三方的库
- golang官方的net/rpc库使用encoding/gob进行编解码,支持tcp和http数据传输方式,由于其他语言不支持gob编解码方式,所以golang的RPC只支持golang开发的服务器与客户端之间的交互
- 官方还提供了net/rpc/jsonrpc库实现RPC方法,jsonrpc采用JSON进行数据编解码,因而支持跨语言调用,目前jsonrpc库是基于tcp协议实现的,暂不支持http传输方式
- 例题:golang实现RPC程序,实现求矩形面积和周长
服务端
package main
import (
"log"
"net/http"
"net/rpc"
)
// 例题:golang实现RPC程序,实现求矩形面积和周长
type Params struct {
Width, Height int
}
type Rect struct{}
// RPC服务端方法,求矩形面积
func (r *Rect) Area(p Params, ret *int) error {
*ret = p.Height * p.Width
return nil
}
// 周长
func (r *Rect) Perimeter(p Params, ret *int) error {
*ret = (p.Height + p.Width) * 2
return nil
}
// 主函数
func main() {
// 1.注册服务
rect := new(Rect)
// 注册一个rect的服务
rpc.Register(rect)
// 2.服务处理绑定到http协议上
rpc.HandleHTTP()
// 3.监听服务
err := http.ListenAndServe(":8000", nil)
if err != nil {
log.Panicln(err)
}
}
客户端
package main
import (
"fmt"
"log"
"net/rpc"
)
// 传的参数
type Params struct {
Width, Height int
}
// 主函数
func main() {
// 1.连接远程rpc服务
conn, err := rpc.DialHTTP("tcp", ":8000")
if err != nil {
log.Fatal(err)
}
// 2.调用方法
// 面积
ret := 0
err2 := conn.Call("Rect.Area", Params{50, 100}, &ret)
if err2 != nil {
log.Fatal(err2)
}
fmt.Println("面积:", ret)
// 周长
err3 := conn.Call("Rect.Perimeter", Params{50, 100}, &ret)
if err3 != nil {
log.Fatal(err3)
}
fmt.Println("周长:", ret)
}
- golang写RPC程序,必须符合4个基本条件,不然RPC用不了
- 结构体字段首字母要大写,可以别人调用
- 函数名必须首字母大写
- 函数第一参数是接收参数,第二个参数是返回给客户端的参数,必须是指针类型
- 函数还必须有一个返回值error
- 练习:模仿前面例题,自己实现RPC程序,服务端接收2个参数,可以做乘法运算,也可以做商和余数的运算,客户端进行传参和访问,得到结果如下:
服务端代码:
package main
import (
"errors"
"log"
"net/http"
"net/rpc"
)
// 结构体,用于注册的
type Arith struct{}
// 声明参数结构体
type ArithRequest struct {
A, B int
}
// 返回给客户端的结果
type ArithResponse struct {
// 乘积
Pro int
// 商
Quo int
// 余数
Rem int
}
// 乘法
func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
res.Pro = req.A * req.B
return nil
}
// 商和余数
func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
if req.B == 0 {
return errors.New("除数不能为0")
}
// 除
res.Quo = req.A / req.B
// 取模
res.Rem = req.A % req.B
return nil
}
// 主函数
func main() {
// 1.注册服务
rect := new(Arith)
// 注册一个rect的服务
rpc.Register(rect)
// 2.服务处理绑定到http协议上
rpc.HandleHTTP()
// 3.监听服务
err := http.ListenAndServe(":8000", nil)
if err != nil {
log.Fatal(err)
}
}
客户端代码:
package main
import (
"fmt"
"log"
"net/rpc"
)
type ArithRequest struct {
A, B int
}
// 返回给客户端的结果
type ArithResponse struct {
// 乘积
Pro int
// 商
Quo int
// 余数
Rem int
}
func main() {
conn, err := rpc.DialHTTP("tcp", ":8000")
if err != nil {
log.Fatal(err)
}
req := ArithRequest{9, 2}
var res ArithResponse
err2 := conn.Call("Arith.Multiply", req, &res)
if err2 != nil {
log.Fatal(err2)
}
fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)
err3 := conn.Call("Arith.Divide", req, &res)
if err3 != nil {
log.Fatal(err3)
}
fmt.Printf("%d / %d 商 %d,余数 = %d\n", req.A, req.B, res.Quo, res.Rem)
}
另外,net/rpc/jsonrpc库通过json格式编解码,支持跨语言调用
服务端代码:
package main
import (
"fmt"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
type Params struct {
Width, Height int
}
type Rect struct {
}
func (r *Rect) Area(p Params, ret *int) error {
*ret = p.Width * p.Height
return nil
}
func (r *Rect) Perimeter(p Params, ret *int) error {
*ret = (p.Height + p.Width) * 2
return nil
}
func main() {
rpc.Register(new(Rect))
lis, err := net.Listen("tcp", ":8080")
if err != nil {
log.Panicln(err)
}
for {
conn, err := lis.Accept()
if err != nil {
continue
}
go func(conn net.Conn) {
fmt.Println("new client")
jsonrpc.ServeConn(conn)
}(conn)
}
}
客户端代码:
package main
import (
"fmt"
"log"
"net/rpc/jsonrpc"
)
type Params struct {
Width, Height int
}
func main() {
conn, err := jsonrpc.Dial("tcp", ":8080")
if err != nil {
log.Panicln(err)
}
ret := 0
err2 := conn.Call("Rect.Area", Params{50, 100}, &ret)
if err2 != nil {
log.Panicln(err2)
}
fmt.Println("面积:", ret)
err3 := conn.Call("Rect.Perimeter", Params{50, 100}, &ret)
if err3 != nil {
log.Panicln(err3)
}
fmt.Println("周长:", ret)
}
1.1.4. RPC调用流程
- 微服务架构下数据交互一般是对内 RPC,对外 REST
- 将业务按功能模块拆分到各个微服务,具有提高项目协作效率、降低模块耦合度、提高系统可用性等优点,但是开发门槛比较高,比如 RPC 框架的使用、后期的服务监控等工作
- 一般情况下,我们会将功能代码在本地直接调用,微服务架构下,我们需要将这个函数作为单独的服务运行,客户端通过网络调用
1.1.5. 网络传输数据格式
- 两端要约定好数据包的格式
- 成熟的RPC框架会有自定义传输协议,这里网络传输格式定义如下,前面是固定长度消息头,后面是变长消息体

- 自己定义数据格式的读写
package rpc
import (
"encoding/binary"
"io"
"net"
)
// 测试网络中读写数据的情况
// 会话连接的结构体
type Session struct {
conn net.Conn
}
// 构造方法
func NewSession(conn net.Conn) *Session {
return &Session{conn: conn}
}
// 向连接中去写数据
func (s *Session) Write(data []byte) error {
// 定义写数据的格式
// 4字节头部 + 可变体的长度
buf := make([]byte, 4+len(data))
// 写入头部,记录数据长度
binary.BigEndian.PutUint32(buf[:4], uint32(len(data)))
// 将整个数据,放到4后边
copy(buf[4:], data)
_, err := s.conn.Write(buf)
if err != nil {
return err
}
return nil
}
// 从连接读数据
func (s *Session) Read() ([]byte, error) {
// 读取头部记录的长度
header := make([]byte, 4)
// 按长度读取消息
_, err := io.ReadFull(s.conn, header)
if err != nil {
return nil, err
}
// 读取数据
dataLen := binary.BigEndian.Uint32(header)
data := make([]byte, dataLen)
_, err = io.ReadFull(s.conn, data)
if err != nil {
return nil, err
}
return data, nil
}
测试类
package rpc
import (
"fmt"
"net"
"sync"
"testing"
)
func TestSession_ReadWriter(t *testing.T) {
// 定义地址
addr := "127.0.0.1:8000"
my_data := "hello"
// 等待组定义
wg := sync.WaitGroup{}
wg.Add(2)
// 写数据的协程
go func() {
defer wg.Done()
lis, err := net.Listen("tcp", addr)
if err != nil {
t.Fatal(err)
}
conn, _ := lis.Accept()
s := Session{conn: conn}
err = s.Write([]byte(my_data))
if err != nil {
t.Fatal(err)
}
}()
// 读数据的协程
go func() {
defer wg.Done()
conn, err := net.Dial("tcp", addr)
if err != nil {
t.Fatal(err)
}
s := Session{conn: conn}
data, err := s.Read()
if err != nil {
t.Fatal(err)
}
// 最后一层校验
if string(data) != my_data {
t.Fatal(err)
}
fmt.Println(string(data))
}()
wg.Wait()
}
编码解码
package rpc
import (
"bytes"
"encoding/gob"
)
// 定义RPC交互的数据结构
type RPCData struct {
// 访问的函数
Name string
// 访问时的参数
Args []interface{}
}
// 编码
func encode(data RPCData) ([]byte, error) {
//得到字节数组的编码器
var buf bytes.Buffer
bufEnc := gob.NewEncoder(&buf)
// 编码器对数据编码
if err := bufEnc.Encode(data); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// 解码
func decode(b []byte) (RPCData, error) {
buf := bytes.NewBuffer(b)
// 得到字节数组解码器
bufDec := gob.NewDecoder(buf)
// 解码器对数据节码
var data RPCData
if err := bufDec.Decode(&data); err != nil {
return data, err
}
return data, nil
}
1.1.6. 实现RPC服务端
- 服务端接收到的数据需要包括什么?
- 调用的函数名、参数列表,还有一个返回值error类型
- 服务端需要解决的问题是什么?
- Map维护客户端传来调用函数,服务端知道去调谁
- 服务端的核心功能有哪些?
- 维护函数map
- 客户端传来的东西进行解析
- 函数的返回值打包,传给客户端
package rpc
import (
"fmt"
"net"
"reflect"
)
// 声明服务端
type Server struct {
// 地址
addr string
// map 用于维护关系的
funcs map[string]reflect.Value
}
// 构造方法
func NewServer(addr string) *Server {
return &Server{addr: addr, funcs: make(map[string]reflect.Value)}
}
// 服务端需要一个注册Register
// 第一个参数函数名,第二个传入真正的函数
func (s *Server) Register(rpcName string, f interface{}) {
// 维护一个map
// 若map已经有键了
if _, ok := s.funcs[rpcName]; ok {
return
}
// 若map中没值,则将映射加入map,用于调用
fVal := reflect.ValueOf(f)
s.funcs[rpcName] = fVal
}
// 服务端等待调用的方法
func (s *Server) Run() {
// 监听
lis, err := net.Listen("tcp", s.addr)
if err != nil {
fmt.Printf("监听 %s err :%v", s.addr, err)
return
}
for {
// 服务端循环等待调用
conn, err := lis.Accept()
if err != nil {
return
}
serSession := NewSession(conn)
// 使用RPC方式读取数据
b, err := serSession.Read()
if err != nil {
return
}
// 数据解码
rpcData, err := decode(b)
if err != nil {
return
}
// 根据读到的name,得到要调用的函数
f, ok := s.funcs[rpcData.Name]
if !ok {
fmt.Println("函数 %s 不存在", rpcData.Name)
return
}
// 遍历解析客户端传来的参数,放切片里
inArgs := make([]reflect.Value, 0, len(rpcData.Args))
for _, arg := range rpcData.Args {
inArgs = append(inArgs, reflect.ValueOf(arg))
}
// 反射调用方法
// 返回Value类型,用于给客户端传递返回结果,out是所有的返回结果
out := f.Call(inArgs)
// 遍历out ,用于返回给客户端,存到一个切片里
outArgs := make([]interface{}, 0, len(out))
for _, o := range out {
outArgs = append(outArgs, o.Interface())
}
// 数据编码,返回给客户端
respRPCData := RPCData{rpcData.Name, outArgs}
bytes, err := encode(respRPCData)
if err != nil {
return
}
// 将服务端编码后的数据,写出到客户端
err = serSession.Write(bytes)
if err != nil {
return
}
}
}
1.1.7. 实现RPC客户端
- 客户端只有函数原型,使用reflect.MakeFunc() 可以完成原型到函数的调用
- reflect.MakeFunc()是Client从函数原型到网络调用的关键
package rpc
import (
"net"
"reflect"
)
// 声明服务端
type Client struct {
conn net.Conn
}
// 构造方法
func NewClient(conn net.Conn) *Client {
return &Client{conn: conn}
}
// 实现通用的RPC客户端
// 传入访问的函数名
// fPtr指向的是函数原型
//var select fun xx(User)
//cli.callRPC("selectUser",&select)
func (c *Client) callRPC(rpcName string, fPtr interface{}) {
// 通过反射,获取fPtr未初始化的函数原型
fn := reflect.ValueOf(fPtr).Elem()
// 需要另一个函数,作用是对第一个函数参数操作
f := func(args []reflect.Value) []reflect.Value {
// 处理参数
inArgs := make([]interface{}, 0, len(args))
for _, arg := range args {
inArgs = append(inArgs, arg.Interface())
}
// 连接
cliSession := NewSession(c.conn)
// 编码数据
reqRPC := RPCData{Name: rpcName, Args: inArgs}
b, err := encode(reqRPC)
if err != nil {
panic(err)
}
// 写数据
err = cliSession.Write(b)
if err != nil {
panic(err)
}
// 服务端发过来返回值,此时应该读取和解析
respBytes, err := cliSession.Read()
if err != nil {
panic(err)
}
// 解码
respRPC, err := decode(respBytes)
if err != nil {
panic(err)
}
// 处理服务端返回的数据
outArgs := make([]reflect.Value, 0, len(respRPC.Args))
for i, arg := range respRPC.Args {
// 必须进行nil转换
if arg == nil {
// reflect.Zero()会返回类型的零值的value
// .out()会返回函数输出的参数类型
outArgs = append(outArgs, reflect.Zero(fn.Type().Out(i)))
continue
}
outArgs = append(outArgs, reflect.ValueOf(arg))
}
return outArgs
}
// 完成原型到函数调用的内部转换
// 参数1是reflect.Type
// 参数2 f是函数类型,是对于参数1 fn函数的操作
// fn是定义,f是具体操作
v := reflect.MakeFunc(fn.Type(), f)
// 为函数fPtr赋值,过程
fn.Set(v)
}
1.1.8. 实现RPC通信测试
- 给服务端注册一个查询用户的方法,客户端使用RPC方式调用
package rpc
import (
"encoding/gob"
"fmt"
"net"
"testing"
)
// 给服务端注册一个查询用户的方法,客户端使用RPC方式调用
// 定义用户对象
type User struct {
Name string
Age int
}
// 用于测试用户查询的方法
func queryUser(uid int) (User, error) {
user := make(map[int]User)
// 假数据
user[0] = User{"zs", 20}
user[1] = User{"ls", 21}
user[2] = User{"ww", 22}
// 模拟查询用户
if u, ok := user[uid]; ok {
return u, nil
}
return User{}, fmt.Errorf("%d err", uid)
}
func TestRPC(t *testing.T) {
// 编码中有一个字段是interface{}时,要注册一下
gob.Register(User{})
addr := "127.0.0.1:8000"
// 创建服务端
srv := NewServer(addr)
// 将服务端方法,注册一下
srv.Register("queryUser", queryUser)
// 服务端等待调用
go srv.Run()
// 客户端获取连接
conn, err := net.Dial("tcp", addr)
if err != nil {
fmt.Println("err")
}
// 创建客户端对象
cli := NewClient(conn)
// 需要声明函数原型
var query func(int) (User, error)
cli.callRPC("queryUser", &query)
// 得到查询结果
u, err := query(1)
if err != nil {
fmt.Println("err")
}
fmt.Println(u)
}
Raft
Raft是consoul和etcd的核心算法
Raft介绍
- Raft提供了一种在计算系统集群中分布状态机的通用方法,确保集群中的每个节点都同意一系列相同的状态转换
- 它有许多开源参考实现,具有Go,C ++,Java和Scala中的完整规范实现
- 一个Raft集群包含若干个服务器节点,通常是5个,这允许整个系统容忍2个节点的失效,每个节点处于以下三种状态之一
- follower(跟随者) :所有节点都以 follower 的状态开始。如果没收到 leader消息则会变成 candidate状态
- candidate(候选人):会向其他节点“拉选票”,如果得到大部分的票则成为leader,这个过程就叫做Leader选举(Leader Election)
- leader(领导者):所有对系统的修改都会先经过leader
Raft一致性算法
- Raft通过选出一个leader来简化日志副本的管理,例如,日志项(log entry)只允许从leader流向follower
- 基于leader的方法,Raft算法可以分解成三个子问题
- Leader election (领导选举):原来的leader挂掉后,必须选出一个新的leader
- Log replication (日志复制):leader从客户端接收日志,并复制到整个集群中
- Safety (安全性):如果有任意的server将日志项回放到状态机中了,那么其他的server只会回放相同的日志项
Raft动画演示
- 地址:http://thesecretlivesofdata.com/raft/
- 动画主要包含三部分:
- 第一部分介绍简单版的领导者选举和日志复制的过程
- 第二部分介绍详细版的领导者选举和日志复制的过程
- 第三部分介绍如果遇到网络分区(脑裂),raft算法是如何恢复网络一致的
Leader election (领导选举)
- Raft 使用一种心跳机制来触发领导人选举
- 当服务器程序启动时,节点都是 follower(跟随者) 身份
- 如果一个跟随者在一段时间里没有接收到任何消息,也就是选举超时,然后他就会认为系统中没有可用的领导者然后开始进行选举以选出新的领导者
- 要开始一次选举过程,follower 会给当前term加1并且转换成candidate状态,然后它会并行的向集群中的其他服务器节点发送请求投票的 RPCs 来给自己投票。
- 候选人的状态维持直到发生以下任何一个条件发生的时候
- 他自己赢得了这次的选举
- 其他的服务器成为领导者
- 一段时间之后没有任何一个获胜的人
Log replication (日志复制)
- 当选出 leader 后,它会开始接收客户端请求,每个请求会带有一个指令,可以被回放到状态机中
- leader 把指令追加成一个log entry,然后通过AppendEntries RPC并行地发送给其他的server,当该entry被多数server复制后,leader 会把该entry回放到状态机中,然后把结果返回给客户端
- 当 follower 宕机或者运行较慢时,leader 会无限地重发AppendEntries给这些follower,直到所有的follower都复制了该log entry
- raft的log replication要保证如果两个log entry有相同的index和term,那么它们存储相同的指令
- leader在一个特定的term和index下,只会创建一个log entry
gRPC
gRPC简介
- gRPC由google开发,是一款语言中立、平台中立、开源的远程过程调用系统
- gRPC客户端和服务端可以在多种环境中运行和交互,例如用java写一个服务端,可以用go语言写客户端调用
gRPC与Protobuf介绍
- 微服务架构中,由于每个服务对应的代码库是独立运行的,无法直接调用,彼此间的通信就是个大问题
- gRPC可以实现微服务,将大的项目拆分为多个小且独立的业务模块,也就是服务,各服务间使用高效的protobuf协议进行RPC调用,gRPC默认使用protocol buffers,这是google开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式如JSON)
- 可以用proto files创建gRPC服务,用message类型来定义方法参数和返回类型
安装gRPC和Protobuf
- go get github.com/golang/protobuf/proto
- go get google.golang.org/grpc(无法使用,用如下命令代替)
- git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc
- git clone https://github.com/golang/net.git $GOPATH/src/golang.org/x/net
- git clone https://github.com/golang/text.git $GOPATH/src/golang.org/x/text
- go get -u github.com/golang/protobuf/
- git clone https://github.com/google/go-genproto.git $GOPATH/src/google.golang.org/genproto
- cd $GOPATH/src/
- go install google.golang.org/grpc
- go get github.com/golang/protobuf/protoc-gen-go
- 上面安装好后,会在GOPATH/bin下生成protoc-gen-go.exe
- 但还需要一个protoc.exe,windows平台编译受限,很难自己手动编译,直接去网站下载一个,地址:https://github.com/protocolbuffers/protobuf/releases/tag/v3.9.0 ,同样放在GOPATH/bin下
注意:这里面好多都是需要vpn才能下载好的!分享一个下载好的https://pan.baidu.com/s/1T8eJkHib2uPL3gNMCRdZmQ 提取码 s42z
Protobuf语法
基本规范
-
文件以.proto做为文件后缀,除结构定义外的语句以分号结尾
-
结构定义可以包含:message、service、enum
-
rpc方法定义结尾的分号可有可无
-
Message命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式
message SongServerRequest { required string song_name = 1; } -
Enums类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式
enum Foo { FIRST_VALUE = 1; SECOND_VALUE = 2; } -
Service与rpc方法名统一采用驼峰式命名
字段规则
- 字段格式:
限定修饰符 | 数据类型 | 字段名称 | = | 字段编码值 | [字段默认值] - 限定修饰符包含 required\optional\repeated
- Required: 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃
- Optional:表示是一个可选字段,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。---因为optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡
- Repeated:表示该字段可以包含0~N个元素。其特性和optional一样,但是每一次可以包含多个值。可以看作是在传递一个数组的值
- 数据类型
- Protobuf定义了一套基本数据类型。几乎都可以映射到C++\Java等语言的基础数据类型

- N 表示打包的字节并不是固定。而是根据数据的大小或者长度
- 关于 fixed32 和int32的区别。fixed32的打包效率比int32的效率高,但是使用的空间一般比int32多。因此一个属于时间效率高,一个属于空间效率高
- Protobuf定义了一套基本数据类型。几乎都可以映射到C++\Java等语言的基础数据类型
- 字段名称
- 字段名称的命名与C、C++、Java等语言的变量命名方式几乎是相同的
- protobuf建议字段的命名采用以下划线分割的驼峰式。例如 first_name 而不是firstName
- 字段编码值
- 有了该值,通信双方才能互相识别对方的字段,相同的编码值,其限定修饰符和数据类型必须相同,编码值的取值范围为
1~2^32(4294967296) - 其中 1~15的编码时间和空间效率都是最高的,编码值越大,其编码的时间和空间效率就越低,所以建议把经常要传递的值把其字段编码设置为1-15之间的值
- 1900~2000编码值为Google protobuf 系统内部保留值,建议不要在自己的项目中使用
- 有了该值,通信双方才能互相识别对方的字段,相同的编码值,其限定修饰符和数据类型必须相同,编码值的取值范围为
- 字段默认值
- 当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端
service如何定义
- 如果想要将消息类型用在RPC系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器会根据所选择的不同语言生成服务接口代码
- 例如,想要定义一个RPC服务并具有一个方法,该方法接收SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse) {}
}
- 生成的接口代码作为客户端与服务端的约定,服务端必须实现定义的所有接口方法,客户端直接调用同名方法向服务端发起请求,比较麻烦的是,即便业务上不需要参数也必须指定一个请求消息,一般会定义一个空message
Message如何定义
- 一个message类型定义描述了一个请求或响应的消息格式,可以包含多种类型字段
- 例如定义一个搜索请求的消息格式,每个请求包含查询字符串、页码、每页数目
- 字段名用小写,转为go文件后自动变为大写,message就相当于结构体
syntax = "proto3";
message SearchRequest {
string query = 1; // 查询字符串
int32 page_number = 2; // 页码
int32 result_per_page = 3; // 每页条数
}
- 首行声明使用的protobuf版本为proto3
- SearchRequest 定义了三个字段,每个字段声明以分号结尾,.proto文件支持双斜线 // 添加单行注释
添加更多Message类型
- 一个.proto文件中可以定义多个消息类型,一般用于同时定义多个相关的消息,例如在同一个.proto文件中同时定义搜索请求和响应消息
syntax = "proto3";
// SearchRequest 搜索请求
message SearchRequest {
string query = 1; // 查询字符串
int32 page_number = 2; // 页码
int32 result_per_page = 3; // 每页条数
}
// SearchResponse 搜索响应
message SearchResponse {
...
}
如何使用其他Message
- message支持嵌套使用,作为另一message中的字段类型
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
1.1.7. Message嵌套的使用
- 支持嵌套消息,消息可以包含另一个消息作为其字段。也可以在消息内定义一个新的消息
- 内部声明的message类型名称只可在内部直接使用
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
- 另外,还可以多层嵌套
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
1.1.8. proto3的Map类型
- proto3支持map类型声明
map<key_type, value_type> map_field = N;
message Project {...}
map<string, Project> projects = 1;
- 键、值类型可以是内置的类型,也可以是自定义message类型
- 字段不支持repeated属性
1.1.9. .proto文件编译
- 通过定义好的.proto文件生成Java, Python, C++, Go, Ruby, JavaNano, Objective-C, or C# 代码,需要安装编译器protoc
- 当使用protocol buffer编译器运行.proto文件时,编译器将生成所选语言的代码,用于使用在.proto文件中定义的消息类型、服务接口约定等。不同语言生成的代码格式不同:
- C++: 每个.proto文件生成一个.h文件和一个.cc文件,每个消息类型对应一个类
- Java: 生成一个.java文件,同样每个消息对应一个类,同时还有一个特殊的Builder类用于创建消息接口
- Python: 姿势不太一样,每个.proto文件中的消息类型生成一个含有静态描述符的模块,该模块与一个元类metaclass在运行时创建需要的Python数据访问类
- Go: 生成一个.pb.go文件,每个消息类型对应一个结构体
- Ruby: 生成一个.rb文件的Ruby模块,包含所有消息类型
- JavaNano: 类似Java,但不包含Builder类
- Objective-C: 每个.proto文件生成一个pbobjc.h和一个pbobjc.m文件
- C#: 生成.cs文件包含,每个消息类型对应一个类
1.1.10. import导入定义
- 可以使用import语句导入使用其它描述文件中声明的类型
- protobuf 接口文件可以像C语言的h文件一个,分离为多个,在需要的时候通过 import导入需要对文件。其行为和C语言的#include或者java的import的行为大致相同,例如import "others.proto";
- protocol buffer编译器会在 -I / --proto_path参数指定的目录中查找导入的文件,如果没有指定该参数,默认在当前目录中查找
1.1.11. 包的使用
- 在.proto文件中使用package声明包名,避免命名冲突
syntax = "proto3";
package foo.bar;
message Open {...}
- 在其他的消息格式定义中可以使用包名+消息名的方式来使用类型,如
message Foo {
...
foo.bar.Open open = 1;
...
}
- 在不同的语言中,包名定义对编译后生成的代码的影响不同
- C++ 中:对应C++命名空间,例如Open会在命名空间foo::bar中
- Java 中:package会作为Java包名,除非指定了option jave_package选项
- Python 中:package被忽略
- Go 中:默认使用package名作为包名,除非指定了option go_package选项
- JavaNano 中:同Java
- C# 中:package会转换为驼峰式命名空间,如Foo.Bar,除非指定了option csharp_namespace选项
gRPC构建微服务
代码实现查询,客户端向服务端查询用户的信息
编写proto文件
// 版本号
syntax = "proto3";
// 指定包名
package proto;
// 定义结构体
message UserRequest {
// 定义用户名
string name = 1;
}
// 响应结构体
message UserResponse {
int32 id = 1;
string name = 2;
int32 age = 3;
// repeated修饰符是可变数组,go转切片
repeated string hobby = 4;
}
// service定义方法
service UserInfoService {
rpc GetUserInfo (UserRequest) returns (UserResponse) {
}
}
生成.go文件
- goland中打开命令行,输入命令生成接口文件:protoc -I . --go_out=plugins=grpc:. ./user.proto
编写服务端
package main
// 1.需要监听
// 2.需要实例化gRPC服务端
// 3.在gRPC商注册微服务
// 4.启动服务端
import (
"context"
"fmt"
pb "/gRPC/proto" //注意这个路径
"google.golang.org/grpc"
"net"
)
// 定义空接口
type UserInfoService struct{}
var u = UserInfoService{}
// 实现方法
func (s *UserInfoService) GetUserInfo(ctx context.Context, req *pb.UserRequest) (resp *pb.UserResponse, err error) {
// 通过用户名查询用户信息
name := req.Name
// 数据里查用户信息
if name == "zs" {
resp = &pb.UserResponse{
Id: 1,
Name: name,
Age: 22,
Hobby: []string{"Sing", "Run"},
}
}
return
}
func main() {
// 地址
addr := "127.0.0.1:8080"
// 1.监听
listener, err := net.Listen("tcp", addr)
if err != nil {
fmt.Printf("监听异常:%s\n", err)
}
fmt.Printf("监听端口:%s\n", addr)
// 2.实例化gRPC
s := grpc.NewServer()
// 3.在gRPC上注册微服务
pb.RegisterUserInfoServiceServer(s, &u)
// 4.启动服务端
s.Serve(listener)
}
编写客户端
package main
import (
"context"
"fmt"
pb "/gRPC/proto" //注意这个路径
"google.golang.org/grpc"
)
// 1.连接服务端
// 2.实例gRPC客户端
// 3.调用
func main() {
// 1.连接
conn, err := grpc.Dial("127.0.0.1:8080", grpc.WithInsecure())
if err != nil {
fmt.Printf("连接异常: %s\n", err)
}
defer conn.Close()
// 2. 实例化gRPC客户端
client := pb.NewUserInfoServiceClient(conn)
// 3.组装请求参数
req := new(pb.UserRequest)
req.Name = "zs"
// 4. 调用接口
response, err := client.GetUserInfo(context.Background(), req)
if err != nil {
fmt.Println("响应异常 %s\n", err)
}
fmt.Printf("响应结果: %v\n", response)
}
Go Micro入门
go-micro简介
- Go Micro是一个插件化的基础框架,基于此可以构建微服务,Micro的设计哲学是可插拔的插件化架构
- 在架构之外,它默认实现了consul作为服务发现(2019年源码修改了默认使用mdns),通过http进行通信,通过protobuf和json进行编解码
go-micro的主要功能
- 服务发现:自动服务注册和名称解析。服务发现是微服务开发的核心。当服务A需要与服务B通话时,它需要该服务的位置。默认发现机制是多播DNS(mdns),一种零配置系统。您可以选择使用SWIM协议为p2p网络设置八卦,或者为弹性云原生设置设置consul
- 负载均衡:基于服务发现构建的客户端负载均衡。一旦我们获得了服务的任意数量实例的地址,我们现在需要一种方法来决定要路由到哪个节点。我们使用随机散列负载均衡来提供跨服务的均匀分布,并在出现问题时重试不同的节点
- 消息编码:基于内容类型的动态消息编码。客户端和服务器将使用编解码器和内容类型为您无缝编码和解码Go类型。可以编码任何种类的消息并从不同的客户端发送。客户端和服务器默认处理此问题。这包括默认的protobuf和json
- 请求/响应:基于RPC的请求/响应,支持双向流。我们提供了同步通信的抽象。对服务的请求将自动解决,负载平衡,拨号和流式传输。启用tls时,默认传输为http / 1.1或http2
- Async Messaging:PubSub是异步通信和事件驱动架构的一流公民。事件通知是微服务开发的核心模式。启用tls时,默认消息传递是点对点http / 1.1或http2
- 可插拔接口:Go Micro为每个分布式系统抽象使用Go接口,因此,这些接口是可插拔的,并允许Go Micro与运行时无关,可以插入任何基础技术
go-micro通信流程
- Server监听客户端的调用,和Brocker推送过来的信息进行处理。并且Server端需要向Register注册自己的存在或消亡,这样Client才能知道自己的状态
- Register服务的注册的发现,Client端从Register中得到Server的信息,然后每次调用都根据算法选择一个的Server进行通信,当然通信是要经过编码/解码,选择传输协议等一系列过程的
- 如果有需要通知所有的Server端可以使用Brocker进行信息的推送,Brocker 信息队列进行信息的接收和发布
go-micro核心接口
- go-micro之所以可以高度订制和他的框架结构是分不开的,go-micro由8个关键的interface组成,每一个interface都可以根据自己的需求重新实现,这8个主要的inteface也构成了go-micro的框架结构

Go Micro接口详解
Transort通信接口
通信相关接口
type Socket interface {
Recv(*Message) error
Send(*Message) error
Close() error
}
type Client interface {
Socket
}
type Listener interface {
Addr() string
Close() error
Accept(func(Socket)) error
}
type Transport interface {
Dial(addr string, opts ...DialOption) (Client, error)
Listen(addr string, opts ...ListenOption) (Listener, error)
String() string
}
Codec编码接口
编解码,底层也是protobuf
type Codec interface {
ReadHeader(*Message, MessageType) error
ReadBody(interface{}) error
Write(*Message, interface{}) error
Close() error
String() string
}
Registry注册接口
服务注册发现的实现:etcd、consul、mdns、kube-DNS、zk
type Registry interface {
Register(*Service, ...RegisterOption) error
Deregister(*Service) error
GetService(string) ([]*Service, error)
ListServices() ([]*Service, error)
Watch(...WatchOption) (Watcher, error)
String() string
Options() Options
}
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
}
Broker发布订阅接口
pull push watch
type Broker interface {
Options() Options
Address() string
Connect() error
Disconnect() error
Init(...Option) error
Publish(string, *Message, ...PublishOption) error
Subscribe(string, Handler, ...SubscribeOption) (Subscriber, error)
String() string
}
Client客户端接口
type Client interface {
Init(...Option) error
Options() Options
NewMessage(topic string, msg interface{}, opts ...MessageOption) Message
NewRequest(service, method string, req interface{}, reqOpts ...RequestOption) Request
Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
Publish(ctx context.Context, msg Message, opts ...PublishOption) error
String() string
}
Server服务端接口
type Server interface {
Options() Options
Init(...Option) error
Handle(Handler) error
NewHandler(interface{}, ...HandlerOption) Handler
NewSubscriber(string, interface{}, ...SubscriberOption) Subscriber
Subscribe(Subscriber) error
Register() error
Deregister() error
Start() error
Stop() error
String() string
}
Serveice接口
type Service interface {
Init(...Option)
Options() Options
Client() client.Client
Server() server.Server
Run() error
String() string
}
python防脱发技巧

浙公网安备 33010602011771号