关于云原生:为构建大型复杂系统而生的微服务框架-Erda-Infra

5次阅读

共计 7537 个字符,预计需要花费 19 分钟才能阅读完成。

作者|宋瑞国(尘醉)
起源|尔达 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/
正文完
 0