0/参考网址
juejin.cn/post/721625…
感谢大佬分享
1/前言
很多程序员对微服务开发感到无从下手,其实这并不是因为大家对微服务不懂,也不是因为开发微服务有多难,
而是不知道开发微服务的流程。
实际上,开发微服务并不复杂,主要还是一个流程的问题。
俗话说,熟能生巧,多练习几次就行了,因为它是一个比较固定的东西。
这篇文章会详细的带大家从零到一通过 CloudWeGo 的开源框架来学习如何进行微服务的开发。
2/什么cloudwego
CloudWeGo 是一套由字节跳动开源的、可快速构建企业级云原生微服务架构的中间件集合。
CloudWeGo 项目共同的特点是高性能、高扩展性、高可靠,专注于微服务通信与治理。
3/什么是cloudwego-kitex
Kitex[kaɪt’eks] 字节跳动内部的 Golang 微服务 RPC 框架,具有高性能、强可扩展的特点.
在字节内部已广泛使用。
如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。
4/CloudWeGo-Hertz
Hertz[həːts] 是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttp、gin、echo 的优势, 并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点.
目前在字节跳动内部已广泛使用。
如今越来越多的微服务选择使用 Golang,如果对微服务性能有要求,又希望框架能够充分满足内部的可定制化需求,Hertz 会是一个不错的选择。
5/如何开发微服务
<1>前置准备
为了使用到 Hertz 与 Kitex 的命令行工具,我们需要先进行下载。
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
go install github.com/cloudwego/hertz/cmd/hz@latest
<2>IDL和代码的生成
微服务的消费者(调用方)和提供者(开发者)之间总要有个约定。
不跨语言的话,这种语言本身的定义就可以在不同的组件之间直接共享。
一旦支持多语言,用一种公共的接口定义语言来定义他们之间的接口能力就是有必要的了。
所以我们在正式开发之前需要先把 IDL 文件定义好,在这里我们使用到的是 Thfift,他的性能会比 Protobuf 更出色一点。
由于我们是入门开发,所以服务方面就只开发开发一个 `user` 服务和 `article` 服务,`article` 服务通过 RPC 调用使用 `user` 的一些服务。
第一个服务的.thrift文件如下所示:
namespace go user
struct RegisterRequest {
1: string username
2: string password
}
struct ResgisterResponse {}
struct LoginRequest {
1: string username
2: string password
}
struct LoginResponse {}
struct GetArticleNumRequest {
1: i64 user_id
}
struct GetArticleNumResponse {
1: i64 num
}
struct AddArticleNumRequest {
1: i64 user_id
}
struct AddArticleNumResponse {}
service UserService {
LoginResponse Login(1: LoginRequest req)
ResgisterResponse Register(1: RegisterRequest req)
GetArticleNumResponse GetArticleNum(1: GetArticleNumRequest req)
AddArticleNumResponse AddArticleNum(1: AddArticleNumRequest req)
}
在 `idl/user.thrift` 中定义了四个服务接口,
分别是用户登录与注册、获取用户发布文章数量与增加用户发布文章的服务接口,与文章相关的两个服务会被 `article` 这部分来进行调用。
第二个服务的.thrift文件如下所示:
事实上一般开发过程中会将这两个服务放到 `article` 中,这里放到 `user` 中方便演示微服务之间的调用
namespace go article
struct PostArticleRequest {
1: string title
2: string content
}
struct PostArticleResponse {}
service ArticleService {
PostArticleResponse PostArticle(1: PostArticleRequest req)
}
业务逻辑
现在开始写业务逻辑,由于我们是面向 CloudWeGo 的基础进行学习,所以这里不会用的一些花里胡哨的技术栈,也没有配置中心等一系列的工具。
<1>用户
首先实现用户的一系列方法,我个人的开发习惯是先用接口再实现接口,因此我们先把需要用到的接口先定义出来,到后面再进行实现。
func (s *UserServiceImpl) Login(_ context.Context, req *user.LoginRequest) (resp *user.LoginResponse, err error) {
flag, err := s.MySqlManager.LoginCheck(req.Username, req.Password)
if err != nil {
klog.Error("login check error, err :", err)
return nil, status.Err(codes.Internal, "login error")
}
if !flag {
klog.Info("wrong password")
return nil, status.Err(codes.Internal, "login error")
}
return nil, nil
}
func (s *UserServiceImpl) Register(_ context.Context, req *user.RegisterRequest) (resp *user.ResgisterResponse, err error) {
err = s.MySqlManager.CreateUser(req.Username, req.Password)
if err != nil {
klog.Error("register error, err :", err)
return nil, status.Err(codes.Internal, "register error")
}
return nil, nil
}
func (s *UserServiceImpl) GetArticleNum(_ context.Context, req *user.GetArticleNumRequest) (resp *user.GetArticleNumResponse, err error) {
count, err := s.MySqlManager.GetArticleNum(req.UserId)
if err != nil {
klog.Error("get article num error, err :", err)
return nil, status.Err(codes.Internal, "get article num error")
}
return &user.GetArticleNumResponse{Num: count}, nil
}
func (s *UserServiceImpl) AddArticleNum(_ context.Context, req *user.AddArticleNumRequest) (resp *user.AddArticleNumResponse, err error) {
err = s.MySqlManager.ArticleNumPlusOne(req.UserId)
if err != nil {
klog.Error("article num plus one error, err :", err)
return nil, status.Err(codes.Internal, "add article num error")
}
return nil, nil
}
通过我们定义好的接口就可以将这几个方法实现好,`MySQLManager` 的实现并不是这次项目的重点,这些代码可以到 `user/pkg/mysql.go` 中进行查看。
接下来我们进行 `main` 函数的实现,在 `main` 函数中我们需要进行服务注册还要对数据库相关内容进行初始化。
数据库初始化
数据库层面我们用到的是 gorm。
func InitDB() *gorm.DB {
dsn := "root:123456@tcp(127.0.0.1:3306)/cloudwego?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
})
if err != nil {
klog.Fatalf("init gorm failed: %s", err.Error())
}
return db
}
服务的注册
这里我们使用的是 `consul` 并使用 Kitex 提供的 `consul` 中间件进行初始化。
func InitRegistry() (registry.Registry, *registry.Info) {
r, err := consul.NewConsulRegister("127.0.0.1:8500",
consul.WithCheck(&api.AgentServiceCheck{
Interval: "7s",
Timeout: "5s",
DeregisterCriticalServiceAfter: "15s",
}))
if err != nil {
klog.Fatalf("new consul register failed: %s", err.Error())
}
sf, err := snowflake.NewNode(4)
if err != nil {
klog.Fatalf("generate service name failed: %s", err.Error())
}
info := ®istry.Info{
ServiceName: "user_srv",
Addr: utils.NewNetAddr("tcp", "127.0.0.1:8881"),
Tags: map[string]string{
"ID": sf.Generate().Base36(),
},
}
return r, info
}
main 函数实现
需要注意的是我们需要将 `MySqlManager` 进行配置好。
func main() {
r, info := init.InitRegistry()
db := init.InitDB()
srv := user.NewServer(&UserServiceImpl{
MySqlManager: mysql.NewUserManager(db),
},
server.WithServiceAddr(utils.NewNetAddr("tcp", "127.0.0.1:8881")),
server.WithRegistry(r),
server.WithRegistryInfo(info),
server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: "user_srv"}),
)
err := srv.Run()
if err != nil {
klog.Fatal(err)
}
}
<2>文章
用和上面一样的方法我们可以实现 `article` 部分,但我们这里我们除了需要 `MySqlManager` 之外还需要一个 `UserManager` 用来进行用户文章数的加一。
type ArticleServiceImpl struct {
MySqlManager
UserManager
}
type UserManager interface {
ArticleNumPlusOne(uid int64) error
}
type MySqlManager interface {
Post(title, content string, uid int64) error
}
func (s *ArticleServiceImpl) PostArticle(_ context.Context, req *article.PostArticleRequest) (resp *article.PostArticleResponse, err error) {
err = s.MySqlManager.Post(req.Title, req.Content, req.Uid)
if err != nil {
klog.Error("post article error, err:", err)
return nil, status.Err(codes.Internal, "post error")
}
err = s.UserManager.ArticleNumPlusOne(req.Uid)
if err != nil {
klog.Error("article num plus one error, err:", err)
return nil, status.Err(codes.Internal, "post error")
}
return nil, nil
}
同样的,关于 `Manager` 的实现可以到项目源码 `pkg` 包下查看。
在 `article` 中有类似的数据库和服务发现初始化操作,除此之外大家别忘了我们在 `article` 服务中调用了 `user` 服务,因此还需要对 `user` 服务进行调用初始化也就是服务发现。
user 服务调用初始化
func InitUser() user.Client {
r, err := consul.NewConsulResolver("127.0.0.1:8500")
if err != nil {
klog.Fatalf("new nacos client failed: %s", err.Error())
}
c, err := user.NewClient(
"user_srv",
client.WithResolver(r),
client.WithClientBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: "user_srv"}),
)
if err != nil {
klog.Fatalf("ERROR: cannot init client: %v\n", err)
}
return c
}
main 函数实现
func main() {
r, info := init.InitRegistry()
db := init.InitDB()
srv := article.NewServer(&ArticleServiceImpl{
MySqlManager: pkg.NewArticleManager(db),
},
server.WithServiceAddr(utils.NewNetAddr("tcp", "127.0.0.1:8882")),
server.WithRegistry(r),
server.WithRegistryInfo(info),
server.WithServerBasicInfo(&rpcinfo.EndpointBasicInfo{ServiceName: "article_srv"}),
)
err := srv.Run()
if err != nil {
klog.Fatal(err)
}
}