零、背景
近一段时间在学习和实践用go来实现微服务架构的开发,本文来记录下什么情况下要使用微服务架构,分析下利弊。并且用grpc+etcd初步实现带服务发现功能的微服务模型。
一、服务端架构的演进
1、单体架构
在 Web 应用程序发展的早期,大部分工程是将所有的服务端功能模块打包成单个巨石型应用,最终会形成如下图所示的架构。
优点:
- 开发简单
- 技术单一
- 部署方便
缺点:
- 随着业务的发展,应用会越来越庞大
- 技术栈单一,不易扩展
- 牵一发而动全身
2、垂直分层架构
随着单体应用越来越庞大,单体架构中不同业务模块的差异就会显现,将大应用拆分成一个个单体结构的应用。垂直分层是一个典型的对复杂系统进行结构化思考和抽象聚合的通用性方法。
优点:
- 分担部分流量
- 服务间相互独立,可以针对单个服务模块进行优化
- 易于水平扩展
缺点:
- 集群搭建变得复杂
- 可能存在大量耦合代码,调用关系错综复杂
- 难以维护
3、微服务架构
微服务是一种小型的SOA架构(面向服务的架构),其理念是将业务系统彻底地组件化和服务化,形成多个可以独立开发、部署和维护的服务或者应用的集合,以应对更快的需求变更和更短的开发迭代周期。
优点:
- 服务模块解耦
- 团队分工更容易,更明确
- 独立部署,可针对独立模块进行发布
- 扩展能力强
缺点:
- 服务划分标准多样
- 增加系统复杂度
- 部署更复杂
- 对整个团队的要求更高
微服务特点:
- 在分布式环境中,将单体应用拆分为一系列服务,共同组成整个系统。
- 每个服务都是轻量级,单独部署。
- 每个微服务注重自己的核心能力的开发,微服务组件之间采用RPC轻量级通信方式进行通信。
- 按照业务边界进行划分。
- 微服务是一种编程架构思想,有不同的语言实现。
二、微服务实践
1、语言选择
近几年,随着微服务架构的火热,也诞生了很多微服务框架,如 Java 语言的 Spring Cloud、Go 语言的 Go Kit 和 Go Micro 以及 Node.js 的 Seneca。充分说明了微服务架构的火热态势。虽然微服务架构的实践落地独立于编程语言,但是 Go 语言在微服务架构的落地中仍有其独特的优势。因此,Go 语言的微服务框架也相继涌现,各方面都较为优秀的有 Go Kit 和 Go Micro 等。
Go 语言十分轻量,运行效率极高,原生支持并发编程,相较其他语言主要有以下优势:
- 语法简单,上手快。Go 提供的语法十分简洁,关键字只有 25 个,几乎支持大多数现代编程语言常见的特性。
- 原生支持并发,协程模型是非常优秀的服务端模型。
- 丰富的标准库。Go 目前内置了大量的库,开箱即用,非常方便。
- 部署方便,编译包小,可直接编译成机器码,不依赖其他库。
- 简言之,用 Go 语言实现微服务,在性能、易用性和生态等方面都拥有优势。
2、服务间的通信
1)RPC
远程过程调用(Remote Procedure Call)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序。简单地说就是能使应用像调用本地方法一样的调用远程的过程或服务。
2)Protobuf
Protobuf 是由 Google 开源的消息传输协议,用于将结构化的数据序列化、反序列化通过网络进行传输。Protobuf 首先解决的是如何在不同语言之间传递数据的问题,目前支持的编程语言有C++、Java、Python、Objective-C、C#、JavaScript、Ruby、Go、PHP 等。
对比XML、JSON
优点:XML、JSON 也可以用来存储此类结构化数据,但是使用ProtoBuf表示的数据能更加高效,并且将数据压缩得更小。(比XML小3-10倍,快20-100倍)
缺点:由于是二进制,无法直接阅读
3)grpc
gRPC是由Google公司开源的一款高性能的远程过程调用(RPC)框架。
grpc结合protobuf实现远程服务调用的使用步骤展示:
01、定义.proto文件
syntax = "proto3";
package services;
//订单请求参数
message OrderRequest {
string orderId = 1;
int64 timeStamp = 2;
}
//订单信息
message OrderInfo {
string OrderId = 1;
string OrderName = 2;
string OrderStatus = 3;
}
//订单服务service定义
service OrderService{
rpc GetOrderInfo(OrderRequest) returns (OrderInfo);
}
02、工具生成go代码
protoc --go_out=plugins=grpc:. *.proto
03、编写方法体的业务实现
func (os \*OrderServiceImpl) GetOrderInfo(ctx context.Context, request \*OrderRequest) (\*OrderInfo, error) {
fmt.Println("收到请求订单号:", request.OrderId)
return &OrderInfo{OrderId: request.OrderId, OrderName: "订单名称", OrderStatus: "已经支付"}, nil
}
04、service端启动服务
addr :\= "127.0.0.1:8972"
rpcServer :\= grpc.NewServer()
services.RegisterOrderServiceServer(rpcServer, new(services.OrderServiceImpl))
listen, err :\= net.Listen("tcp", addr)
if err != nil {
log.Fatalf("启动网络监听失败 %v\\n", err)
}
//启动服务
if err :\= rpcServer.Serve(listen); err != nil {
fmt.Println("启动错误", err)
} else {
fmt.Println("服务开启")
}
05、client调用
addr :\= "127.0.0.1:8972"
conn, err :\= grpc.Dial(addr, grpc.WithInsecure())
if err != nil {
log.Fatalf("连接GRPC服务端失败 %v\\n", err)
}
defer conn.Close()
//实例客户端
orderServiceClient :\= sOrder.NewOrderServiceClient(conn)
//模拟订单号
orderId :\= "order\_" + strconv.Itoa(time.Now().Second())
//组织请求体
orderRequest :\= &sOrder.OrderRequest{OrderId: orderId, TimeStamp: time.Now().Unix()}
//rpc调用GetOrderInfo
orderInfo, err :\= orderServiceClient.GetOrderInfo(context.Background(), orderRequest)
if orderInfo != nil {
fmt.Println(orderInfo.GetOrderId(), orderInfo.GetOrderName(), orderInfo.GetOrderStatus())
return orderInfo.GetOrderStatus()
} else {
return "订单服务读取失败"
}
3、服务注册中心
在微服务架构中,一般每一个服务都是有多个拷贝,来保证高可用。一个服务随时可能下线,也可能应对临时访问压力增加新的服务节点。这就出现了新的问题:
- 服务之间如何相互感知?例如有新的服务实例上线,已上线的实例如何知道并与之通信。
- 服务如何管理?服务实例数量多了,也面临着如何管理的问题。
服务注册
service启动时向注册中心注册
健康检查
注册中心和service之间会保持心跳检查,来维护注册表里的service是否存活
服务发现
client向注册中心获取可用的service
- register 在每个服务启动时会向服务注册中心上报自己的网络位置。这样,在服务发现中心内部会形成一个服务注册表,服务注册表是服务发现的核心部分,是包含所有服务实例的网络地址的数据库。
- healthy check 注册中心与各微服务节点间保持心跳检测,来保证服务注册表的服务都是可用状态。
- discover 当需要对某服务进行请求时,服务实例通过该注册表,定位目标服务网络地址。若目标服务存在多个网络地址,则使用负载均衡算法从多个服务实例中选择出一个,然后发出请求。
注册中心中间件的比较
Consul
Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。Consul 使用 Go 语言编写,因此具有天然可移植性(支持Linux、windows和Mac OS X)。Consul 内置了服务注册与发现框架、分布一致性协议实现、健康检查、Key/Value 存储、多数据中心方案,不再需要依赖其他工具,使用起来也较为简单。
Etcd
etcd 是用 go 开发的,出现的时间并不长,不像 zookeeper 那么悠久和有名,但是前景非常好。etcd 是因为 kubernetes 而被人熟知的,kubernetes 的 kube master 使用 etcd 作为分布式存储获取分布式锁,这为 etcd 的强大做了背书。etcd 使用 RAFT 算法实现的一致性,比 zookeeper 的 ZAB 算法更简单
Zookeeper
zookeeper 起源于 Hadoop,后来进化为 Apache 的顶级项目。现在已经被广泛使用在 Apache 的项目中,例如 Hadoop,kafka,solr 等等。是用java 开发的,部署的时候要装JAVA环境。历史悠久,功能丰富,所以比较重,易用性不如以上2个。
代码展示
我暂时选用了etcd来做注册中心,以下展示加入注册中心后的grpc调用代码。
service端
package main
import (
"fmt"
"google.golang.org/grpc"
"goshare/etcd"
"grpc-service/services"
"log"
"net"
)
func main() {
addr := "127.0.0.1:8972"
rpcServer := grpc.NewServer()
services.RegisterOrderServiceServer(rpcServer, new(services.OrderServiceImpl))
listen, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("启动网络监听失败 %v\n", err)
}
//etcd服务注册
reg, err := etcd.NewService(etcd.ServiceInfo{
Name: "mirco.service.order",
IP: addr, //grpc服务节点ip
}, []string{"172.24.132.232:2379"}) // etcd的节点ip
if err != nil {
log.Fatal(err)
}
go reg.Start()
//启动服务
if err := rpcServer.Serve(listen); err != nil {
fmt.Println("启动错误", err)
} else {
fmt.Println("服务开启")
}
}
client端
r := etcd.NewResolver([]string{"172.24.132.232:2379"}, "mirco.service.order")
resolver.Register(r)
addr := fmt.Sprintf("%s:///%s", r.Scheme(), "")
//addr := "127.0.0.1:8972"
fmt.Println("addr", addr)
conn, err := grpc.Dial(addr, grpc.WithInsecure())
if err != nil {
log.Fatalf("连接GRPC服务端失败 %v\n", err)
}
defer conn.Close()
orderServiceClient := sOrder.NewOrderServiceClient(conn)
orderId := "order_" + strconv.Itoa(time.Now().Second())
orderRequest := &sOrder.OrderRequest{OrderId: orderId, TimeStamp: time.Now().Unix()}
orderInfo, err := orderServiceClient.GetOrderInfo(context.Background(), orderRequest)
if orderInfo != nil {
fmt.Println(orderInfo.GetOrderId(), orderInfo.GetOrderName(), orderInfo.GetOrderStatus())
return orderInfo.GetOrderStatus()
} else {
return "订单服务读取失败"
}
总结
以上就实现了注册中心来做服务注册和发现,我们测试效果的时候可以启用2个service,一个监听8972,一个监听8973。client通过注册中心(172.24.132.232:2379)来发现服务,当2个service其中任何一个关闭,我们的服务依然能正常提供服务,实现了高可用。
tips:另外推荐大家本人参与学习并且觉得不错的学习渠道
一个是拉钩教育上的《Go微服务实战38讲》98元。