博客:cbb777.fun

全平台账号:安妮的心动录

github: https://github.com/anneheartrecord

下文中我说的可能对,也可能不对,鉴于笔者水平有限,请君自辨。有问题欢迎大家找我讨论

基本概念

架构演变

用架构历史

1.单体架构 堆机子 高耦合 一改动就需要重新部署 而且编译时间很长,不容易拓展,不支持多语言技术栈

2.分层架构 典型的有MVC和MSC架构 当访问量逐渐增大,单体架构扛不住了,把单体项目进行垂直划分,耦合还是很大,项目之间的接口多为数据同步,比如不同项目之间的数据库同步。

架构简单,成本低开发周期短,经过垂直拆分之后原来的单体项目不至于太大,每一层可以用不同的技术,但还是不易拓展和维护

3.SOA面向服务架构 :当垂直架构的应用越来越多,就会出现多个应用都依赖的业务组件,比如数据库,而且各个应用交互越来越频繁,此时就需要把部分通用的组件拆分独立处理,于是SOA面向服务架构诞生了,它带来了模块化开发、分布式拓展部署和服务接口定义等概念

实时SOA需要建立企业服务总线,外部应用通过总线调用服务,有以下特征:可从企业外部访问、随时可用、标准化的服务接口等
image.png

优点:

  • 已经具有微服务的影子了,将重复的功能抽离出来,提高开发效率
  • 减少接口耦合

SOA架构适用于大型软件服务企业对外提供服务的场景,并不适合一般的业务场景,其服务的定义、注册和调用都需要繁琐的配置,业务总线的吞吐量决定了整个系统的上限,因为整个系统都是通过总线进行任务分配的。并且业务总线也容易导致系统崩掉、影响性能。

4.微服务架构:

image.png
特点

1.服务层完全独立出来 并将服务层抽取为一个一个的微服务

2.微服务遵循单一原则

3.微服务之间采用RESTful等轻量协议通信

4.微服务一般用容器技术部署 运行在自己的独立进程中

微服务架构下服务的拆分粒度更细,有利于资源重复利用,提高开发效率,采用去中心化思想,更轻量级

缺点:如果服务实例过多,治理成本就会很大,不利于维护;服务之间相互依赖,可能形成复杂的依赖链条,往往单个服务异常,其他服务也会受到影响,出现服务雪崩效应。

微服务与SOA的区别:

微服务继承了SOA的众多优点和理念

SOA更适合与许多其他应用程序继承的大型复杂企业应用程序环境,小型的应用并不适合SOA,微服务则更适合于较小和良好的分割式web业务系统

微服务不再强调SOA架构中比较重要的ESB企业服务总线,而是通过轻量级通信机制相互沟通

SOA注重的是系统继承,而微服务关注的则是完全分离,SOA尝试采用中心化管理来确保各个应用能够协同运作,微服务则尝试部署新功能,快速有效地拓展开发团队,它着重于分散管理、代码再利用和自动化执行。

微服务的优势和劣势

微服务的优势

1.快:更注重CI/CD 敏捷开发、持续交付

2.准:服务粒度小、服务质量精准可控

3.狠:适用于互联网时代、产品迭代周期更短

微服务的劣势

1.系统的复杂性

2.服务依赖管理

3.数据的一致性保障

4.测试更加艰难

5.对于DevOps等基础设施的高要求

如何划分微服务界限

如何进行服务划分?

1.按照业务职能进行划分

由公司内部不同部门提供的只能。例如客户服务部门提供客户服
务的职能,财务部门提供财务相关的职能

2.按照DDD的限界上下文划分

限界上下文是DDD中用来划分不同业务边界的元素

这里业务边界的含义是“解决不同业务问题”的问题域和对应的解决方案域

为了解决某种类型的业务问题,贴近领域,也就是业务

CQRS将系统中的操作划分为两类,即【命令】Command和【查询】Query

命令则是对会引起数据发生变化操作的总称,即我们常说的新增、更新、删除的这些操作,都是命令。

而查询则和字面意思一样,即不会对数据产生变化的操作,只是按照某些条件查询数据。

CQRS的核心思想是将两类不同的操作进行分离,然后在两个独立的【服务】中实现。这里的服务一般指的是两个独立部署的应用,在某些特殊情况下,也可以部署在同一个应用内的不同接口上。

微服务的迭代

1.第一代
image.png
2.第二代
image.png
把那些服务监控、服务管理作为基础服务提供给我们的业务
架构分层
image.png

核心组件

  • API网关
  • 服务注册中心
  • 配置中心
  • 服务通信
  • 服务治理
  • 服务监控

net/rpc

RPC出现的原因

RPC需要解决三个问题

1.如何要确定要执行的函数?

在本地调用中,函数主体通过函数指针函数指定,然后调用add函数,编译器通过函数指针函数确定add函数在内存中的位置。

但是在RPC中,调用不能通过函数指针完成,因为他们的内存地址可能完全不同。

因此,调用方和被调用方都需要维护一个{fuction<->ID}映射表,以确保调用正确的函数

2.如何表达参数?

本地过程调用中传递的参数是通过堆栈结构实现的,但是RPC不能直接使用内存传递参数,因此参数或返回值需要在传输期间转换成字节流,反之亦然

3.如何通过网络传输?

函数的调用方和被调用方通常是通过网络连接的,也就是说 function ID和序列化字节流需要通过网络传输,因此,只要能够完成传输,调用方和被调用方就不受某个网络协议的限制。例如,一些RPC框架使用TCP协议,一些使用HTTP。

也就是说,RPC是一种软性的规定,而不是硬性的协议,只要能解决这三个问题的远程调用,我们都称之为"RPC"

以往实现跨服务调用的时候,我们会采用restful api的方式,被调用方会对外提供一个HTTP接口,调用方按要求发起HTTP请求并接收API接口返回的响应数据。

下面是通过HTTP API实现本地调用的一个栗子

本地调用,通过HTTP的API的方式

server.go

//定义参数和响应  
type addParam struct {
    X int `json:"x"`
    Y int `json:"y"`
}
type addResult struct {
    Code int `json:"code"`
    Data int `json:"data"`
}

func add(x, y int) int {
    return x + y
}
// addHandler 解析参数+调用add+响应写回
func addHandler(w http.ResponseWriter, r *http.Request) {
    // parse parameters
    b, _ := ioutil.ReadAll(r.Body)
    var param addParam
    json.Unmarshal(b, &param)
    // use the add func
    ret := add(param.X, param.Y)
    // return the response
    respBytes, _ := json.Marshal(addResult{Code: 0, Data: ret})
    w.Write(respBytes)
}

func main() {
    http.HandleFunc("/add", addHandler)
    log.Fatal(http.ListenAndServe(":9090", nil))
}

client.go

type addParam struct {
    X int `json:"x"`
    Y int `json:"y"`
}
type addResult struct {
    Code int `json:"code"`
    Data int `json:"data"`
}

func main() {
    url := "http://127.0.0.1:9090/add"
    param := addParam{
        X: 10,
        Y: 20,
    }
    // marshal to json

    paramBytes, _ := json.Marshal(param)
    // call
    resp, _ := http.Post(url, "application/json", bytes.NewReader(paramBytes))
    defer resp.Body.Close()
    respBytes, _ := ioutil.ReadAll(resp.Body)
    var respData addResult
    json.Unmarshal(respBytes, &respData)
    fmt.Println(respData.Data)
}

而RPC调用则不需要如此,下面是一个使用go原生net/rpc库的栗子

service.go

type Args struct {
    X, Y int
}

type ServiceA struct{}

// Add is an out method
// has two args and a return 
// two params must be out 
// and the return value must be error type 
func (s *ServiceA) Add(args *Args, reply *int) error {
    *reply = args.X + args.Y
    return nil
}

server.go

func main() {
  //new service instance   
    service := new(yunyuansheng.ServiceA)
  //register rpc service   
    rpc.Register(service) 
  //botton on http  
    //rpc.HandleHTTP()      
  //botton on tcp   
    l, e := net.Listen("tcp", ":9091")
    if e != nil {
        log.Fatal("listen error:", e)
    }
    //http.Serve(l, nil)
    for {
      // accpet the request and serve   
        conn, _ := l.Accept()
        rpc.ServeConn(conn)
    }
}

client.go

func main() {
    //因为服务端是HTTP请求 所以要建立HTTP连接
    client, err := rpc.Dial("tcp", "127.0.0.1:9091")
    if err != nil {
        fmt.Println(err)
    }
    // 同步调用 Call
    args := &yunyuansheng.Args{10, 20}
    reply := new(int)
    err = client.Call("ServiceA.Add", args, reply)
    if err != nil {
        log.Fatal("ServiceA.Add error:", err)
    }
    fmt.Printf("ServiceA.Add %d+%d=%d\n", args.X, args.Y, *reply)

    //异步调用 Go
    var reply2 int
    divCall := client.Go("ServiceA.Add", args, &reply2, nil)
    replyCall := <-divCall.Done //Done是一个调用结果的通知 有值了就说明调用完成了
    fmt.Println(replyCall.Error)
    fmt.Println(reply2)
}

RPC的最终目的:让调用远程方法更加简单,并且速度更快

Go原生net/rpc库需要注意的几点

1.可以支持很多种协议,包括但不限于HTTP和TCP,如果使用HTTP的话,那么客户端就使用DialHTTP,服务端通过HandleHTTP进行HTTP连接的处理,使用TCP的话,客户端使用Dial,服务端就应该for循环监听连接,一旦有就处理连接

2.客户端支持同步调用和异步调用两种方式,对应的分别是Call和Go

3.暴露出的服务必须满足两个条件,两个参数,一个返回值,返回值必须要是error类型,第二个参数必须是指针

RPC原理

image.png

  1. client以本地调用方式调用服务
  2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体
  3. client stub找到服务地址,并将服务发送到服务端
  4. server 接收到消息之后,通过server stub对消息进行解码
  5. server stub根据解码的结果调用本地服务
  6. 本地服务执行并把消息返回给server stub
  7. server stub将结果打包成能够进行网络传输的结构体,发送到消息方
  8. client 收到消息并进行解码,得到最终结果

安妮的心动录
10 声望0 粉丝