什么是架构
架构也叫做软件架构,是指软件系统的顶层结构。它需要明确系统包含的个体,以及个体运作和协作的规则。
几种典型的架构
单机架构
单机架构非常简单,就是所有功能和服务包含在一个进程中,并部署在一台机器上。
但简单往往意味着在别的方面会有损耗,比如并发处理的问题,维护时候无法保证运行
单体架构
Dan Kegel于1999年在其个人站点提出了个经典问题——C10K problem,即如何处理1000万个并发连接,由此延申除了后续的单体架构。
单体架构时单机架构的改进,它采用分布式部署的模式,设置多台单机,全部的请求会经过分流后,一一转发给这些单机,由此做到负载均衡,维护不需要停服的效果。
不过所有功能依旧结合在一起,耦合度高,无法专注于某功能的开发。
垂直应用架构
此架构将系统按照服务拆分任务,分配给不同的服务器,从而实现职责的简单划分,提高开发和维护的效率,降低了耦合度,但不够彻底,仍然存在功能琐碎的问题。
SOA、微服务架构
SOA架构具有两个特性
- 将不同功能单元抽象为服务,从而细分职责
- 定义服务之间的通信标准,确保服务之间的数据流通
微服务架构则是SOA架构去中心化后的结果,旨在介绍服务之间的沟通消耗,进一步减少耦合,让服务更加明确。
不过微服务架构同时也出现了新的问题,比如数据一致性问题,成本问题,高可用问题等。
Go-Zero架构
这里主要介绍微服务架构中的go-zero架构,go-zero
整体上做为一个稍重的微服务框架,提供了微服务框架需要具备的通用能力,同时也只带一部分的强约束,例如针对web
和rpc
服务需要按照其定义的DSL
的协议格式进行定义,日志配置、服务配置、apm
配置等都要按照框架定义的最佳实践来走。
go-zero安装
go-zero需要安装goctl
,是 go-zero 的内置脚手架,可以一键生成代码、文档等
# Go 1.16 及以后版本
go install github.com/zeromicro/go-zero/tools/goctl@latest
除此之外,还需要安装protoc
(protobuf
的编译器),步骤如下
- github.com/protocolbuf…下载对应的
protoc
- 下载之后解压就行,然后把bin文件夹里面的
protoc.exe
路径加入到环境变量 - 将
protoc.exe
,复制一份到gopath
目录下的bin
目录中 - 调用
cmd
,输入protoc
,查看是否能够使用
除此之外,记得开启Go Modules,设置GOPROXY=https://goproxy.cn,direct
http服务
创建一个目录,叫zero-order
并初始化go.mod
go mod init zero-order
在该目录下,新建一个文件order.api
info(
author: "技术小虫"
date: "2023-04-21"
desc: "订单api说明"
)
type (
OrderInfoReq {
OrderId int64 `json:"order_id"`
}
OrderInfoResp {
OrderId int64 `json:"order_id"` //订单id
GoodsName string `json:"goods_name"` //商品名称
}
)
//定义了一个服务叫order-api
service order-api {
//获取接口的名字叫获取用户信息
@doc "获取订单信息"
//对应的hanlder即controller是orderInfo
@handler orderInfo
//请求方法是post,路径是/order/order_id,参数是OrderInfoReq,返回值是OrderInfoResp
post /order/info (OrderInfoReq) returns (OrderInfoResp)
//可以继续定义多个api
}
goctl api go -api order.api -dir ./ --style=goZero
#目录结构
zero-order
│ go.mod
│ go.sum
│ order.api
│ order.go
├─etc
│ order-api.yaml
└─internal
├─config
│ config.go
├─handler
│ orderInfoHandler.go
│ routes.go
├─logic
│ orderInfoLogic.go
├─svc
│ serviceContext.go
└─types
types.go
目录构建好后,导一下包,比如goland
,ALT+Shift+Enter
即可
我们从internal/handler/routes.go
中的RegisterHandlers
方法
追踪到internal/handler/monsterInfoHandler.go
中的orderInfoHandler
方法
追踪到internal/logic/orderInfoLogic.go
中的OrderInfo
方法
重构OrderInfo
方法
func (l *OrderInfoLogic) OrderInfo(req *types.OrderInfoReq) (resp *types.OrderInfoResp, err error) {
order_id:=req.OrderId
resp=new(types.OrderInfoResp)
resp.GoodsName="雪茄"
resp.OrderId=order_id
return
}
其中order-api.yaml
文件中定义了启动的端口号和ip,internal/handler/routes.go
文件定义了路由。
使用go run order.go -f etc/order-api.yaml
启动服务,使用默认端口8888。请求为order/info
接口。
使用windows terminal
请求
Invoke-RestMethod -Uri "http://localhost:8888/order/info" -Method Post -Headers @{"Content-Type"="application/json";} -Body '{"order_id": 1}'
获得返回值
{
"order_id":34,
"goods_name":"雪茄"
}
rpc服务
通过goctl
生成服务,在此之前,请确定已经安装protoc
,否则后续会有乱码报错。
创建一个目录,叫two
并初始化go.mod
go mod init two
在该目录下,新建一个文件two.proto
,编写一个proto文件用于自定义微服务
syntax = "proto3";
package goods;
// protoc-gen-go 版本大于1.4.0, proto文件需要加上go_package,否则无法生成
option go_package = "./goods";
//定义请求体
message GoodsRequest {
int64 goods_id = 1;
}
//定义响应体
message GoodsResponse {
// 商品id
int64 goods_id = 1;
// 商品名称
string name = 2;
}
service Goods {
//rpc方法
rpc getGoods(GoodsRequest) returns(GoodsResponse);
//可以继续定义多个方法
}
goctl rpc protoc goods.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.
#目录结构
goods
│ go.mod
│ go.sum
│ goods.go
│ goods.proto
├─etc
│ goods.yaml
├─goodsclient
│ goods.go
├─internal
│ ├─config
│ │ config.go
│ ├─logic
│ │ getgoodslogic.go
│ ├─server
│ │ goodsserver.go
│ └─svc
│ servicecontext.go
└─types
└─goods
goods.pb.go
goods_grpc.pb.go
目录构建好后,导一下包,比如goland
,ALT+Shift+Enter
即可
重构internal/logic/getgoodslogic.go
中的GetGoods
方法
func (l *GetGoodsLogic) GetGoods(in *goods.GoodsRequest) (res *goods.GoodsResponse, err error) {
goodsId := in.GoodsId
res = new(goods.GoodsResponse)
res.GoodsId = goodsId
res.Name = "茅台" + l.svcCtx.Config.ListenOn
return
}
通过go run goods.go -f etc/goods.yaml
启动rpc
服务
安装etcd
,下载https://github.com/etcd-io/etcd/releases
合适的版本,解压,用控制台打开etcd.exe
,启动rpc
服务,再查询rpc
是否在etcd
中注册
get "goods" --prefix
two.rpc/7587872873255701764
192.168.1.25:8080
http调用rpc服务
需要暴露rpc
的proto
文件,有三种方法
- 通过go.mod引用
- 通过git托管,然后包的方式引入
- 直接把文件拷贝到对应目录
我们这里采取方法1,在上面两个go.mod文件(api一个,rpc一个)中加入下列语句,这里one与two两个目录同级
replace goods => ../zero-goods
require goods v0.0.0
配置api文件(zero-order/etc/order-api.yaml
)
Name: order-api
Host: 0.0.0.0
Port: 8888
TwoRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: goods.rpc
引入config (zero-order/internal/config/config.go
)
package config
import (
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/zrpc"
)
type Config struct {
rest.RestConf
GoodsRpc zrpc.RpcClientConf
}
加载svc文件(zero-order/internal/svc/serviceContext.go
)
package svc
import (
"github.com/zeromicro/go-zero/zrpc"
"goods/goodsclient"
"zero-order/internal/config"
)
type ServiceContext struct {
Config config.Config
//定义rpc类型
Goods goodsclient.Goods
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
//引入gprc服务
Goods: goodsclient.NewGoods(zrpc.MustNewClient(c.GoodsRpc)),
}
}
logic调用rpc(zero-order/internal/logic/orderInfoLogic.go
)
package logic
import (
"context"
"goods/types/goods"
"zero-order/internal/svc"
"zero-order/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type OrderInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewOrderInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OrderInfoLogic {
return &OrderInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *OrderInfoLogic) OrderInfo(req *types.OrderInfoReq) (resp *types.OrderInfoResp, err error) {
orderId := req.OrderId
goodRequest := new(goods.GoodsRequest)
goodRequest.GoodsId = 25
goodsInfo, err := l.svcCtx.Goods.GetGoods(l.ctx, goodRequest)
if err != nil {
return nil, err
}
resp = new(types.OrderInfoResp)
resp.GoodsName = goodsInfo.Name
resp.OrderId = orderId
return
}
修改zero-goods/goodsclient/goods.go
// Code generated by goctl. DO NOT EDIT.
// Source: goods.proto
package goodsclient
import (
"context"
"goods/types/goods"
"github.com/zeromicro/go-zero/zrpc"
"google.golang.org/grpc"
)
type (
GoodsRequest = goods.GoodsRequest
GoodsResponse = goods.GoodsResponse
Goods interface {
// rpc方法
GetGoods(ctx context.Context, in *GoodsRequest, opts ...grpc.CallOption) (*GoodsResponse, error)
}
defaultGoods struct {
cli zrpc.Client
}
)
func NewGoods(cli zrpc.Client) Goods {
return &defaultGoods{
cli: cli,
}
}
// rpc方法
func (m *defaultGoods) GetGoods(ctx context.Context, in *GoodsRequest, opts ...grpc.CallOption) (*GoodsResponse, error) {
client := goods.NewGoodsClient(m.cli.Conn())
return client.GetGoods(ctx, in, opts...)
}
启动
依次启动etcd ,rpc和api,并请求api
Invoke-RestMethod -Uri "http://localhost:8888/order/info" -Method Post -Headers @{"Content-Type"="application/json";} -Body '{"order_id": 1}'
得到结果
{
"order_id":34,
"goods_name":"茅台"
}