0%

Go微服务与云原生一

基本概念

架构演变

用架构历史

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

2.分层架构 典型的有MVC和MSC架构 当访问量逐渐增大,单体架构扛不住了,把单体项目进行垂直划分,耦合还是很大,项目之间的接口多为数据同步,比如不同项目之间的数据库同步。 架构简单,成本低开发周期短,经过垂直拆分之后原来的单体项目不至于太大,每一层可以用不同的技术,但还是不易拓展和维护

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

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

img

优点:

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

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

4.微服务架构:

img

特点

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.第一代

img

2.第二代

img

把那些服务监控、服务管理作为基础服务提供给我们的业务

架构分层

img

核心组件

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

net/rpc

RPC出现的原因

RPC需要解决三个问题

1.如何要确定要执行的函数?在本地调用中,函数主体通过函数指针函数指定,然后调用add函数,编译器你通过函数指针函数确定add函数在内存中的位置。但是在RPC中,调用不能通过函数指针完成,因为他们的内存地址可能完全不同。因此,调用方和被调用方都需要维护一个{fuction<->ID}映射表,以确保调用正确的函数

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
server
//定义参数和响应
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))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
client
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调用则不需要如此

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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原理

img

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