作者|宋瑞国(尘醉)
起源|尔达 Erda 公众号
导读:Erda Infra 微服务框架是从 Erda 我的项目演进而来,并且齐全开源。Erda 基于 Erda Infra 框架实现了大型简单我的项目的构建。本文将全面、深刻地分析 Erda Infra 框架的架构设计以及如何应用。
- Erda Infra: https://github.com/erda-project/erda-infra
- Erda: https://github.com/erda-project/erda
背景
在互联网技术高速倒退的浪潮中,泛滥的大型零碎缓缓从单体利用演变为微服务化零碎。
单体利用
单体利用的劣势是开发疾速、部署简略,咱们不须要思考太多就能疾速构建出利用,很快地上线产品。
然而,随着业务的倒退,单体程序缓缓变得复杂凌乱,非常容易改出 bug,体积也变得越来越大,当业务量上来的时候,很容易解体。
微服务架构
大型零碎往往采纳微服务架构,这种架构把简单的零碎拆分成了多个服务,微服务之间松耦合、微服务外部高内聚。
同时,微服务架构也带来了一些挑战。服务变多,对整个零碎的稳定性是一种挑战,比方:该如何解决某个服务挂了的状况、服务之间如何通信、如何观测零碎整体的情况等。于是,各种各样的微服务框架诞生了,采纳各种技术来解决微服务架构带来的问题,Spring Cloud 就是一个 Java 畛域针对微服务架构的一个综合性的框架。
云平台
Spring Cloud 提供了许多技术解决方案,然而对于企业来说,运维老本还是很高。企业须要保护各种中间件和泛滥的微服务,于是呈现了各种各样的云服务、云平台。
Erda (https://github.com/erda-project/erda) 是一个针对企业软件系统在 开发阶段 和运维阶段 进行全生命周期治理、一站式的 PaaS 平台,在各个阶段都可能解决微服务带来的各种问题。
Erda 自身也是一个十分大的零碎,它采纳微服务架构来设计,同样面临着微服务架构带来的问题,同时对系统又提出了更多的需要,咱们心愿实现:
- 零碎高度模块化
- 零碎具备高扩展性
- 适宜多人参加的开发模式
- 同时反对 HTTP、gRPC 接口、能主动生成 API Client 等
另一方面,Erda 的开发语言是 golang,在云原生畛域,golang 是一个支流的开发语言,特地适宜开发根底的组件,Docker、Kubernetes、Etcd、Prometheus 等泛滥我的项目也都选用 golang 开发。不像 Spring Cloud 在 Java 中的位置,在 golang 的生态圈里,没有一个相对霸主位置的微服务框架,咱们能够找到许多 web 框架、grpc 框架等,他们提供了很多工具,但不会通知你应该怎么去设计零碎,不会帮你去解耦零碎中的模块。
基于这样的背景,咱们开发了 Erda Infra 框架。
Erda Infra 微服务框架
一个大的零碎,个别由多个应用程序组成,一个应用程序蕴含多个模块,个别的应用程序构造 如下图所示:
这样的构造存在一些问题:
- 代码耦合:个别会在程序最开始的中央,读取所有的配置,初始化所有模块,而后启动一些异步工作,而这个集中初始化的中央,就是代码比拟耦合的中央之一。
- 依赖传递:因为模块之间的依赖关系,必须得依照肯定的程序初始化,包含数据库 Client 等,必须得一层层往里传递。
- 可扩展性差:增删一个模块,并不那么不便,也很容易影响到其余模块。
- 不利于多人开发:如果一个应用程序里的模块是由多人负责开发的,那么也很容易相互影响,调试一个模块,也必须得启动整个应用程序里的所有模块。
接下来咱们通过几个步骤来解决这些问题。
构建以模块驱动的应用程序
咱们能够将整个零碎拆分为一个个 小的性能点 ,每一个 小的性能点 对应一个 微模块。整个零碎像拼图、搭积木一样,自由组合各种功能模块为一个大的模块作为独立的应用程序。
这也意味着咱们 无需放心整个零碎的服务过多、过于扩散 ,只须要 专一于性能自身的拆分。微服务不仅存在于跨节点的多过程之间,也同样存在于一个过程内。
咱们利用 Erda Infra 框架来定义一个模块:
package example
import (
"context"
"fmt"
"time"
"github.com/erda-project/erda-infra/base/logs"
"github.com/erda-project/erda-infra/base/servicehub"
)
// Interface 以接口的模式,对外提供本模块的性能
type Interface interface {Hello(name string) string
}
// config 申明式的配置定义
type config struct {Message string `file:"message" flag:"msg" default:"hi" desc:"message to print"`}
// provider 代表一个模块
type provider struct {
Cfg *config // 框架会主动注入
Log logs.Logger // 框架会主动注入
}
// Init 初始化模块。可选,如果存在,会被框架主动调用
func (p *provider) Init(ctx servicehub.Context) error {p.Log.Info("message:", p.Cfg.Message)
return nil
}
// Run 启动异步工作。可选,如果存在,会被框架主动调用
func (p *provider) Run(ctx context.Context) error {tick := time.NewTicker(3 * time.Second)
defer tick.Stop()
for {
select {
case <-tick.C:
p.Log.Info("do something...")
case <-ctx.Done():
return nil
}
}
}
// Hello 实现接口
func (p *provider) Hello(name string) string {return fmt.Sprintf("hello %s", p.Cfg.Message)
}
func init() {
// 注册模块
servicehub.Register("helloworld", &servicehub.Spec{Services: []string{"helloworld-service"}, // 代表模块的服务列表
Description: "here is description of helloworld",
ConfigFunc: func() interface{} {return &config{} }, // 配置的构造函数
Creator: func() servicehub.Provider { // 模块的构造函数
return &provider{}},
})
}
当咱们定义了很多个这样的功能模块后,能够通过一个 main 函数来启动模块:
package main
import (
_ ".../example" // your package import path
"github.com/erda-project/erda-infra/base/servicehub"
)
func main() {
servicehub.Run(&servicehub.RunOptions{ConfigFile: "example.yaml",})
}
package main
import (
"github.com/erda-project/erda-infra/base/servicehub"
_ ".../example" // your package import path
)
func main() {
servicehub.Run(&servicehub.RunOptions{ConfigFile: "example.yaml",})
}
而后,通过一份配置文件 example.yaml 来确定咱们启动哪些模块:
# example.yaml
helloworld:
message: "erda"
提醒:当然这里也能够内置配置,能够参考 servicehub.RunOptions 里的定义。
这种形式的长处有以下几点:
- 面向微模块编程,只须要关怀本身的性能,更容易做到高内聚、低耦合。
- 申明式的配置定义,无需关怀配置读取的步骤,框架实现多种形式的配置读取。
- 无需关怀其余模块如何初始化,也无需关怀整个利用的初始化程序,只须要专一本身的初始化步骤。
- 异步工作的治理,框架会解决 过程信号,优雅的敞开模块工作。
- 零碎高度可配置,任意模块都可独立配置,并且能够独自启动某个模块进行调试。
模块间的依赖
正如微服务所面临的问题之一,服务之间有着简单的调用,主观上存在着依赖关系。咱们将性能模块化之后,该如何解决模块之间的依赖关系。
Erda Infra 给咱们提供了 依赖注入 的形式,在介绍依赖注入之前,咱们先理解一些概念:
- Service,代表一种能够供其余模块或其余零碎应用的性能。
- Provider,代表服务的提供者,提供 0 个或多个服务,相当于一个模块,一组服务汇合。
- 一个 Provider 能够依赖 0 个或多个 Service。
咱们能够在 Provider 上定义所依赖的 Service 类型作为一个字段,框架会主动将依赖的 Service 实例注入。
例如,咱们定义一个 模块 2来援用上一节定义的 helloword 模块所提供的 helloworld-service 服务:
package example2
// 以下省略号非关键代码
import (
".../example" // your package import path
"github.com/erda-project/erda-infra/base/servicehub"
)
type provider struct {Example example.Interface `autowired:"helloworld-service"` // 框架会主动注入实例}
func (p *provider) Init(ctx servicehub.Context) error {p.Example.Hello("i am module 2")
return nil
}
func init() {// 注册模块 ...}
能够思考一下,为什么不是 Provider 之间间接依赖,而是通过 Service 依赖?因为雷同的 Service 能够由多个不同实现的 Provider 提供。
正如咱们依赖一个接口,而非具体实现类一样,咱们能够将依赖的 Service 接口类型定义在一个公共的中央,由 不同的 Provider 实现来接口,调用者无需关怀具体实现,咱们能够通过配置来切换不同的实现。这样能够做到模块之间的解耦。
框架能够通过 autowired 申明的 Service 名称来进行依赖注入,也能够通过接口类型来进行依赖注入,具体能够参考 Erda Infra 仓库里的例子。
框架会剖析模块之间的依赖关系,来确定每个模块的初始化程序,所以,当咱们编写一个模块的时候,也就无需关怀整个程序里所有模块的初始化程序了。
构建跨过程的 HTTP + gRPC 服务
当解决了模块之间的依赖关系后,咱们接下来思考如何跨过程通信的问题。
所谓天下大势,合久必分,分久必合,咱们常常会因为一些架构调整或其余起因,须要将局部功能模块迁徙到另外的应用程序里,又或者是将一个大的应用程序拆分为多个小的程序。另一方面,要实现整个零碎的所有小性能任意组合为一个大模块,必然也波及到跨过程的通信。
好在咱们能够进行面向接口的编程,模块之间的依赖是通过 Service 接口依赖的,那么这个接口就有可能是本地的模块,也有可能是近程的模块。
Erda Infra 能够做到模块之间的解偶,也能解决跨过程通信的问题,框架通过定义 ProtoBuf API 的形式,为模块提供同时反对 HTTP 和 gRPC 接口的能力:
框架也提供了 cli 工具,来帮忙咱们生成相干的代码:
上面咱们来看一个例子。
第一步,创立一个 greeter.proto 文件,定义一个 GreeterService 服务:
syntax = "proto3";
package erda.infra.example;
import "google/api/annotations.proto";
option go_package = "github.com/erda-project/erda-infra/examples/service/protocol/pb";
// the greeting service definition.
service GreeterService {
// say hello
rpc SayHello (HelloRequest) returns (HelloResponse) {option (google.api.http) = {get: "/api/greeter/{name}",
};
}
}
message HelloRequest {string name = 1;}
message HelloResponse {
bool success = 1;
string data = 2;
}
第二步,通过 Erda Infra 提供的 gohub 工具,能够编译出相干的协定代码和 Client 模块。
cd protocol
gohub protoc protocol *.proto
其中 protocol/pb 为协定代码,protocol/client 为客户端代码
第三步,有了协定代码,必然须要去实现对应的服务接口,通过 gohub 生成接口实现的代码模版:
cd server/helloworld
gohub protoc imp ../../protocol/*.proto
greeter.service.go 文件内容如下:
package example
import (
"context"
"github.com/erda-project/erda-infra/examples/service/protocol/pb"
)
type greeterService struct {p *provider}
func (s *greeterService) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
// TODO: 编写业务逻辑
return &pb.HelloResponse{
Success: true,
Data: "hello" + req.Name,
}, nil
}
如此一来,就能够开始在模块里编写咱们的业务逻辑了。
咱们编写好接口的实现后,就同时领有了 HTTP 和 gRPC 接口,当然这两种接口是能够选择性裸露的。
那么,刚编写好的模块如何被其余模块所援用呢?
来看上面的例子:
package caller
import (
"context"
"time"
"github.com/erda-project/erda-infra/base/logs"
"github.com/erda-project/erda-infra/base/servicehub"
"github.com/erda-project/erda-infra/examples/service/protocol/pb"
)
type config struct {Name string `file:"name" default:"recallsong"`}
type provider struct {
Cfg *config
Log logs.Logger
Greeter pb.GreeterServiceServer // 由 本地模块 或 近程模块 提供
}
// 调用 GreeterService 服务的例子
func (p *provider) Run(ctx context.Context) error {tick := time.NewTicker(3 * time.Second)
defer tick.Stop()
for {
select {
case <-tick.C:
resp, err := p.Greeter.SayHello(context.Background(), &pb.HelloRequest{Name: p.Cfg.Name,})
if err != nil {p.Log.Error(err)
}
p.Log.Info(resp)
case <-ctx.Done():
return nil
}
}
}
func init() {
servicehub.Register("caller", &servicehub.Spec{Services: []string{},
Description: "this is caller example",
Dependencies: []string{"erda.infra.example.GreeterService"},
ConfigFunc: func() interface{} {return &config{}
},
Creator: func() servicehub.Provider {return &provider{}
},
})
}
其中 pb.GreeterServiceServer 是一个由 ProtoBuf 文件生成的接口,调用者无需关怀该接口的实现是由本地模块提供还是近程模块提供,这能够通过配置文件来确定。
当它 由本地模块提供实现时 ,会通过接口调用到本地的实现函数;当它是 由近程模块提供时,会通过 gRPC 来调用。
例子残缺代码:https://github.com/erda-project/erda-infra/tree/master/examples/service。
模块通用化
Erda Infra 提供了许多现成的通用模块,开箱即用。
以上通用模块中,httpserver 这个模块提供了相似于 Spring MVC 中 Controller 的成果,能够写任意参数的处理函数,而不是固定的 http.HandlerFunc 模式。
每个程序可能都须要 health、pprof 等接口,咱们只需导入相应的模块,就能领有这些接口。
同样,开发者们也能开发更多的、散布在不同仓库里的通用业务模块,供其余业务零碎应用,能很大水平上进步功能模块的复用性。
总结
Erda Infra 是一个可能疾速构建以模块驱动的零碎框架、可能解决微服务带来的许多问题。未来,也会有更多的通用模块,来解决不同场景下的问题,可能更大程度地进步开发效率。
对于 Erda 如果你有更多想要理解的内容,欢送增加小助手微信(Erda202106)进入交换群探讨,或者间接点击下方链接理解更多!
- Erda Github 地址:https://github.com/erda-project/erda
- Erda Cloud 官网:https://www.erda.cloud/