作者|宋瑞国(尘醉)

来源|尔达 Erda 公众号

导读:Erda Infra 微服务框架是从 Erda 项目演进而来,并且完全开源。Erda 基于 Erda Infra 框架完成了大型复杂项目的构建。本文将全面、深入地剖析 Erda Infra 框架的架构设计以及如何使用。

背景

在互联网技术高速发展的浪潮中,众多的大型系统慢慢从单体应用演变为微服务化系统。

单体应用

单体应用的优势是开发快速、部署简单,我们不需要考虑太多就能快速构建出应用,很快地上线产品。



然而,随着业务的发展,单体程序慢慢变得复杂混乱,非常容易改出 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 Infra的更多相关文章

  1. 手把手0基础项目实战(一)——教你搭建一套可自动化构建的微服务框架(SpringBoot+Dubbo+Docker+Jenkins)...

    原文:手把手0基础项目实战(一)--教你搭建一套可自动化构建的微服务框架(SpringBoot+Dubbo+Docker+Jenkins)... 本文你将学到什么? 本文将以原理+实战的方式,首先对& ...

  2. 在微服务框架Demo.MicroServer中添加SkyWalking+SkyApm-dotnet分布式链路追踪系统

    1.APM工具的选取 Apm监测工具很多,这里选用网上比较火的一款Skywalking. Skywalking是一个应用性能监控(APM)系统,Skywalking分为服务端Oap.管理界面UI.以及 ...

  3. 大型互联网 b2b b2c o2o 电子商务微服务云平台

    鸿鹄云商大型企业分布式互联网电子商务平台,推出PC+微信+APP+云服务的云商平台系统,其中包括B2B.B2C.C2C.O2O.新零售.直播电商等子平台. 分布式.微服务.云架构电子商务平台 java ...

  4. 大型网站系统与Java中间件实践

    大型网站系统与Java中间件实践(贯通分布式高并发高数据高访问量网站架构与实现之权威著作,九大一线互联网公司CTO联合推荐) 曾宪杰 著   ISBN 978-7-121-22761-5 2014年4 ...

  5. Java面试题精选,大型网站系统架构你不得不懂的10个问题

    作者:JavaGuide(公众号) 下面这些问题都是一线大厂的真实面试问题,不论是对你面试还是说拓宽知识面都很有帮助.之前发过一篇8 张图读懂大型网站技术架构 可以作为不太了解大型网站系统技术架构朋友 ...

  6. Spring Cloud与微服务构建:Spring Cloud简介

    Spring Cloud简介 微服务因该具备的功能 微服务可以拆分为"微"和"服务"二字."微"即小的意思,那到底多小才算"微&q ...

  7. 关于Python构建微服务的思考(一)

    一:什么是微服务? 微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成. 系统中的各个微服务可被独立部署,各个微服务之间是松耦合的. 每个微服务仅关注于完成一件任务并很好地完成该任务. ...

  8. Java 18套JAVA企业级大型项目实战分布式架构高并发高可用微服务电商项目实战架构

    Java 开发环境:idea https://www.jianshu.com/p/7a824fea1ce7 从无到有构建大型电商微服务架构三个阶段SpringBoot+SpringCloud+Solr ...

  9. Aooms_微服务基础开发平台实战_002_工程构建

    一.关于框架更名的一点说明 最近在做年终总结.明年规划.还有几个项目需要了结.出解决方案,事情还比较多,死了不少脑细胞,距离上一篇文章发出已经过了3天,是不是有些人会认为我放弃了又不搞了,NONO,一 ...

随机推荐

  1. 访问单个结点的删除 牛客网 程序员面试金典 C++ Python

    访问单个结点的删除 牛客网 程序员面试金典 C++ Python 题目描述 实现一个算法,删除单向链表中间的某个结点,假定你只能访问该结点. 给定待删除的节点,请执行删除操作,若该节点为尾节点,返回f ...

  2. Canvas 放烟花合集 -- 用粉丝头像做成烟花绽放🧨

    "我对着烟花许愿,希望你永远在我身边" "凑不够满天星辰那就去看看烟花吧,人间烟火气,最抚凡人心" 小tips:喜欢的可以关注博主私信代码噢~ 也可以看看前面两 ...

  3. makefile编译子目录

    make子目录常用方法 一般是 SUB_DIR = lib_src service .PHONY: subdirs $(SUB_DIR) subdirs: $(SUB_DIR) $(SUB_DIR): ...

  4. dart系列之:dart类中的构造函数

    目录 简介 传统的构造函数 命名构造函数 构造函数的执行顺序 重定向构造函数 Constant构造函数 工厂构造函数 总结 简介 dart作为一种面向对象的语言,class是必不可少的.dart中所有 ...

  5. 逐浪CMSv8.2发布-集成Node与Vue脚手架和PowerShell支持的新一代网站管理系统

    极速下载:https://www.z01.com/down/3713.shtml 楼倚霜树外,镜天无一毫. 南山与秋色,气势两相高. -(唐)杜牧 北京时间2020年10月20日:领先的CMS与web ...

  6. R数据分析:跟随top期刊手把手教你做一个临床预测模型

    临床预测模型也是大家比较感兴趣的,今天就带着大家看一篇临床预测模型的文章,并且用一个例子给大家过一遍做法. 这篇文章来自护理领域顶级期刊的文章,文章名在下面 Ballesta-Castillejos ...

  7. [noi712]练级

    先考虑一个联通块,可以发现这个联通快内不会存在两个偶数的点证明:如果存在,那么这两个点的某一条路径上的边全部反过来,可以使答案+2,即答案为点数或点数-1同时,发现答案的奇数点数一定与边数同奇偶,那么 ...

  8. [bzoj1107]驾驶考试

    转化题意,如果一个点k符合条件,当且仅当k能到达1和n考虑如果l和r($l<r$)符合条件,容易证明那么[l,r]的所有点都将会符合条件,因此答案是一个区间枚举答案区间[l,r],考虑如何判定答 ...

  9. 和安卓对接老是ping不通?试试内网映射

    https://ngrok.cc/download.html

  10. 低代码开发Paas平台时代来了

    概述 **本人博客网站 **IT小神 www.itxiaoshen.com 低代码理论 概念 低代码开发基于可视化和模型驱动的概念,结合了云原生和多终端体验技术,它可以在大多数业务场景中,帮助企业显著 ...