软件设计~常用架构(SOA、微内核、分层)

目录一、Why Architecture?二、分层架构(layered architecture)2 1 分层架构介绍2 2 分层模式样例2 3 分层模

一、Why Architecture?

系统的架构设计相当于造房子的设计图纸,规定了房子的形状、地基的深度、各种排水系统等等问题。当设计图纸完成,正式交付给施工队后,要么这是一次可靠的设计,要么完全无法满足,最终需要减配设计乃至砍掉项目。

软件架构

图1 软件架构作用

软件架构设计就和这个过程极其相似,软件架构是一个系统的蓝图(见图1),它描述了系统内存在的各个模块,各个模块各自的职责领域,模块之间如何相互配合提供服务。通过这个蓝图,设计人员可以控制复杂度、保证可靠性,开发人员则能够快速理解系统的整体性,模块的职责,帮助开发人员设计出能够满足技术、运营需求、性能目标和安全性要求的软件。

架构设计常常会和软件设计相混淆,工程师往往在区分两个词的时候会感到困扰。实际上两者描述的维度完全不一样,打个比方,架构相当于人的骨骼、经络和脏器,它勾绘出系统的基本设施。软件则相当于人体免疫系统、循环系统,它有一套具体的标准,通过几个固有的脏器,依托于骨骼经络,提供抗病毒、氧气输送等功能。

总结下,软件设计是代码层得设计,对代码负责;架构设计是系统宏观层面得组件设计,对系统负责。

软件架构设计需要深入理解软件的产品需求和技术需求,在架构设计阶段任何一个决定都会对软件发展面临的稳定性、可维护性和性能等各方面产生深远的影响。Ralph Johnson, co-author of Design Patterns: Elements of Reusable Object-Oriented Software,说过一句话:架构设计是那些你希望在开发项目中早早做出的正确决定。

软件架构设计的最终的目的在于在可控的时间内,开发出满足产品需求和技术需求的软件,避免延期乃至项目失败。

二、分层架构(layered architecture)

2.1. 分层架构介绍

分层架构是最常见得软件架构,几乎所有优秀的软件设计都离不开它。工程师在设计软件架构的时候几乎可以照方抓药,根据软件需求来切分软件逻辑层次,进而提高软件的性能、鲁棒性、可维护性和可复用性。分层架构几乎可以和任意的软件架构合作,比如在微服务架构中,可以通过分层架构设计每个服务的逻辑结构。

这种架构将软件分成若干个水平层,每一层都有清晰的角色和分工,不需要知道其他层的细节,层与层之间通过接口通信。本文在图2中展示了一个web应用是如何进行分层设计的:

分层架构

图2 分层架构
  • 表现层(presentation):用户界面,负责视觉和用户互动
  • 业务层(business):实现业务逻辑
  • 持久层(persistence):提供数据,SQL 语句就放在这一层
  • 数据库(database) :保存数据

分层架构最大的好处在于对软件垂直层次做模块化,高层到底层仅提供通用接口,高层不再关心低层的实现,从而保证了软件的可扩展和可复用。

需要注意的是,分层架构是一种针对单体(相对于分布式服务)的模块化架构,层的概念是一种代码块上的概念,需要和分布式服务区分开。

2.2. 分层模式样例

分层模式的设计样例非常直观,主要每层的接口定义(源码)以及相应的层次组织用例(源码),本节将使用go来进行demo的开发:

……
type LayerExecutor interface {
    Do(ctx context.Context) error
}

type MiddleExecutor interface {
    DoMiddle(ctx context.Context) error
}

type LowExecutor interface {
    DoLow(ctx context.Context) error
}
……
    low := patterns.NewLowLayer(dev)
    mid := patterns.NewMiddleLayer(low)
    top := patterns.NewTopLayer(mid)
    err := top.Do(context.WithValue(context.Background(), patterns.GLabelContent, msg))
……

2.3. 分层模式总结

  • 优点:
    1. 结构简单,容易理解和开发
    2. 不同技能的程序员可以分工,负责不同的层,天然适合大多数软件公司的组织架构
    3. 每一层都可以独立测试,其他层的接口通过模拟解决
  • 缺点:
    1. 分层模式是一种单体设计架构,任意功能点的更改将导致整个服务的升级。
    2. 水平扩展性差,用户请求大量增加时,必须依次扩展每一层,由于每一层内部是耦合的,扩展会很困难。

三、事件驱动架构

3.1. 事件驱动架构和SOA(service oriented architecture)[4]

事件驱动架构是一种异步架构,也被称为响应式架构或者非阻塞架构,是一种非常流行的现代低耦合架构。抽象层面看,其实是一个复杂化的生产者-消费者模型[3],所有的业务逻辑都通过消息队列分发到最终的执行块,提升业务响应速度,如图三:

Event Driver

图3 事件驱动架构

从上图可知,事件驱动架构需要包含:

  • 事件队列(event queue):接收事件的入口
  • 分发器(event mediator):将不同的事件分发到不同的业务逻辑单元
  • 事件通道(event channel):分发器与处理器之间的联系渠道
  • 事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作

这个架构最核心的就是通过消息队列解偶,这里笔者引入了另一个架构SOA(如图4)。

SOA

图4 SOA

SOA是一个分布式组件模型,它规定不同的功能单元提供定义良好的接口和协议,形成单独的服务,平台提供商提供一个统一的企业服务总线,将不同协议的服务通过这个总线统一暴露给客户和其他服务。

通过SOA,服务调用方不再需要知道服务的具体协议和接口,它只关注于服务本身,这是Amazon在云服务时代能够脱颖而出的关键因素。

笔者为何引入SOA呢?因为笔者认为,SOA本身正是事件驱动架构在分布式领域的一次伟大实践:

  1. 同步调用企业总线起到了事件队列的作用。
  2. 企业服务总线起到了分发器和事件通道的作用。
  3. 服务起到了事件处理器的作用。

3.2. SOA架构样例

SOA架构最主要的是两个事情,定义良好的服务接口和企业事务总线(源码传送门),本节将使用go进行开发:

……
// 企业服务总线接口
type EnterpriseServiceBus interface {
    // 同步调用服务,相当于事件队列的缓冲容量为0
    PostEventAsBroken(srcName, dstName, event string, content ServiceContent) (ServiceContent, error)
    // 异步调用服务,相当于事件队列有一定的缓冲容量
    PostEventAsMQ(srcName, onEvent string, dstName, event string, content ServiceContent) error
    // 服务心跳
    Ping(srcName string, k ServiceProtocolKind) error
}

// 服务定义的接口
type EnterpriseService interface {
    // 接收心跳
    Pong() error
    // 服务名
    Name() string
    // 服务协议类型
    Kind() ServiceProtocolKind
    // 服务提供给企业服务总线的统一接口
    RecvEvent(name string, content ServiceContent) (ServiceContent, error)
}
……

// 内存中实现的企业事务总线
type MemBus struct {
    ……
    srvName2Srv       map[string]EnterpriseService
    ……
    mq                chan func() error
    ……
}
……
// 阻塞调用服务,并返回结果给调用方
func (m *MemBus) PostEventAsBroken(srcName, dstName, event string, content ServiceContent) (ServiceContent, error) {
    ……
    // 消息分发
    p, ok := m.srvName2Srv[dstName]
    ……
    return p.RecvEvent(event, content)
}
// 异步调用服务,通过消息队列异步调用服务
func (m *MemBus) PostEventAsMQ(srcName, onEvent string, dstName, event string, content ServiceContent) error {
    ……
    // 将调用函数塞入消息队列
    m.mq <- func() error {
        ……
        // 同步调用服务
        content, err := m.PostEventAsBroken(srcName, dstName, event, content)
        if err != nil {
            content = make(ServiceContent)
            content["Error"] = err.Error()
        }
        // 通过回调函数通知调用方结果
        _, err = m.PostEventAsBroken(dstName, srcName, onEvent, content)
        return err
    }
    return nil
}

看到上面的SOA实现源码,读者应该可以明白笔者为何将SOA作为事件驱动架构的样例。SOA本身就是通过企业事务总线作为中间人消息队列完成服务的事件触发。下面是SOA的实际调用样例(https://github.com/birdhkl/code-for-blog/tree/main/2022/software-arch/arch/soa_test.go):

……
    jsonSrv := &JsonEnterpriseService{name: "json", srv: NewServiceImpl()}
    rpcSrv := &RpcEnterpriseService{name: "rpc", srv: NewServiceImpl()}
    ……
    // prepare service bus,g是服务构造函数
    bus := patterns.NewMemEnterpriseServiceBus(g)
    ……
    if err := bus.Ping("json", patterns.ServiceJSON); err != nil {
        t.Error(err)
        return
    }
    if err := bus.Ping("rpc", patterns.ServiceRPC); err != nil {
        t.Error(err)
        return
    }
    // post json->rpc
    c1 := patterns.ServiceContent{"123": "456"}
    resp, err := bus.PostEventAsBroken("json", "rpc", "hello", c1)
    ……
    // post rpc->rpc
    c2 := patterns.ServiceContent{"456": "789"}
    resp, err = bus.PostEventAsBroken("rpc", "json", "world", c2)
    ……
    c3 := patterns.ServiceContent{"789": "91011"}
    if err := bus.PostEventAsMQ("json", "onTony", "rpc", "tony", c3); err != nil {
        t.Error(err)
        return
    }
……

3.3. 事件驱动架构总结

  • 优点
    1. 分布式的异步架构,事件处理器之间高度解耦,软件的扩展性好。
    2. 适用性广,各种类型的项目都可以用。
    3. 性能较好,因为事件的异步本质,软件不易产生堵塞。
    4. 事件处理器可以独立地加载和卸载,容易部署。
  • 缺点
    1. 涉及异步编程(要考虑远程通信、失去响应等情况),开发相对复杂。
    2. 难以支持原子性操作,因为事件通过会涉及多个处理器,很难回滚。
    3. 分布式和异步特性导致这个架构较难测试。

四、微服务[6] vs SOA vs 微内核[5]

4.1. 介绍

微服务架构(microservices architecture)是SOA的升级,每一个服务就是一个独立的部署分布式单元,服务间的调用不再引入企业事务总线这一单独的中间人,服务间互相解耦,通过远程通信协议(比如REST、SOAP)联系,因此,服务相互间需要知道通信协议。

微服务

图5 微服务

微核架构(microkernel architecture)又称为"插件架构"(plug-in architecture),它最小化了系统内核的功能,内核仅提供插件管理功能和插件间的消息通信能力,具体逻辑通过插件提供。插件则是互相独立的,插件之间的通信,应该减少到最低,避免出现互相依赖的问题。

微内核

图6 微内核

微内核和微服务核心思想本质上并无区别,通过拆分单独的模块,降低系统本身复杂度和容量,进而提供强大的扩展能力、维护性和可复用性。

区别在于,微内核作用于单体服务的架构设计,微服务则是一种分布式服务的架构风格。现代风格的微服务架构,往往需要具备服务发现和服务治理能力(这相当于微内核的插件管理能力),通过负载均衡的网络通信进行服务调用(微内核的消息机制)。微服务通过服务发现组件和网络通信,将微内核的插件管理和通信能力分布式化,融入到了架构中。

4.2. 微内核样例

上一节提到,微内核和微服务的核心指导思想没有本质差别,本文将以微内核的实现来表明这种解偶思路。

微内核的核心是组件管理、插件化服务和消息通知,本文将使用本节将使用go来开发demo:

  1. 插件化服务,笔者将使用go提供的动态库工具链将服务组织成动态库。
  2. 标准化插件,插件需要提供初始化接口,服务对象以及消息处理接口。
  3. 消息通知,笔者将使用go的协程作为服务提供方,并使用go的chan机制进行消息通知。

4.2.1. 插件化服务

插件化服务,必须要对服务进行限制,对插件做出描述,这些代码必须被核心层逻辑和插件的逻辑共享。耦合性要低,稳定性要高(基本不再修改),笔者基于此,设计了如下的逻辑(源码传送门):

……
// 插件在微内核里的实体,每一个插件可以定义若干个服务,每个Bundle对应一个声明式的插件描述json文件
type Bundle struct {
    URL             string `json:"url"`         // 插件对应的so文件路径
    Version         string `json:"version"`     // 插件版本
    Name            string `json:"name"`        // 插件名称
    Desc            string `json:"description"` // 插件描述
    SymbolActivator string `json:"activator"`   // 插件初始化对象的符号名称
    services        map[string]BundleService    // 插件在微内核服务中产生作用的服务集合
    ……
}
……

// 通过符号获取插件激活器
func (b *Bundle) GetBundleActivator() (BundleActivator, error) {
    symbol, err := b.lookUp(b.SymbolActivator)
    ……
    activator, ok := symbol.(BundleActivator)
    ……
    return activator, nil
}

// 通过go提供的plugin包进行插件符号提取
func (b *Bundle) lookUp(symbol string) (plugin.Symbol, error) {
    url := b.GetBundleUrl()
    plug, err := plugin.Open(url)
    ……
    return plug.Lookup(b.SymbolActivator)
}
……
// 插件激活器,在其中注册销毁服务等能力
type BundleActivator interface {
    Start(ctx BundleContext)    // 启动插件,开始提供服务
    Stop(ctx BundleContext)     // 停止插件
}

// 插件提供服务的上下文
type BundleContext interface {
    GetServiceReference(serviceName string) (BundleServiceReference, error) // 插件服务引用
    GetBundles() ([]*Bundle, error)                                         // 上下文中所有插件
    GetBundle(bundleName string) (*Bundle, error)                           // 获取插件
    InstallBundle(bundleName string) error                                  // 在上下文中安装插件
    UninstallBundle(bundleName string) error                                // 在上下文中拆卸插件
    RegisterService(serviceName string, srv BundleService) error            // 在上下文中注册插件提供的服务
    UnregisterService(serviceName string) error                             // 在上下文中剔除拆建提供的服务
    Stop() error
}
……
// 插件提供的服务
type BundleService interface {
    Recv(BundleServiceMessage) Message
}
……

4.2.2. 插件管理和消息通知

微内核的核心层逻辑仅仅包含了插件管理和消息通知,在本文的demo中,笔者在上节提到了一个BundleContext的概念,这是一个插件在运行态提供上下文的核心服务,需要具备安装插件,启动服务,停止服务和拆卸插件的能力,简而言之,这其实就是微内核的核的作用,核心代码传送门

……
// 插件上下文
type DefaultBundleContext struct {
    ……
    bundles      map[string]*bundle.Bundle
    services     map[string]*BundleServiceProxy
    ……
}
……
// 安装插件
func (ctx *DefaultBundleContext) InstallBundle(bundleName string) error {
    ……
    activator, err := b.GetBundleActivator()
    ……
    ctx.bundles[b.GetBundleName()] = b
    ……
    activator.Start(NewSpecificBundleContextMiddleware(ctx, b))
    return nil
}
……
// 注册启动服务
func (ctx *DefaultBundleContext) RegisterService(serviceName string, srv bundle.BundleService) error {
    ……
    go func() {
        ……
        for {
            select {
            case e, ok := <-p.Queue():
                ……
                msg := p.Recv(e)
                if resE, ok := e.(*messageWithResult); ok {
                    resE.GetResultChan() <- msg
                    close(resE.GetResultChan())
                }
                ……
            case <-c.Done():
                fmt.Printf("service %s done\n", serviceName)
                return
            }
        }
    }()
    return nil
}

4.2.3. 标准化插件

笔者在本文中定义的demo,每个被内核识别的插件必须包含显示声明的插件描述文件和一个包含BundleServiceBundleActivator的有效插件。下面是一个典型的插件(源码[传送门]https://github.com/birdhkl/code-for-blog/tree/main/2022/software-arch/arch/mka/plugins/src/hello_world.go)):

// 一个符合标准的服务
type HelloWorldService struct {
}

func (s *HelloWorldService) Recv(msg bundle.BundleServiceMessage) bundle.Message {
    ……
}
// 一个符合标准的激活器
type ServiceActivator struct {
}

func (activator *ServiceActivator) Start(ctx bundle.BundleContext) {
    if err := ctx.RegisterService("Service", &HelloWorldService{}); err != nil {
       ……
    }
}
func (activator *ServiceActivator) Stop(ctx bundle.BundleContext) {
    if err := ctx.UnregisterService("Service"); err != nil {
        ……
    }
}
// 激活器对象,必须
var Activator ServiceActivator

通过这样的定义,内核就能够将插件载入到插件上下文中,并提供插件具备的服务。

4.2.4. 微内核使用样例

具备上述的实现内容后,可得到一个使用微内核的例子(源码传送门):

    ……
    bundleContext := kernel.NewDefaultBundleContext(bundlePath, context.TODO())
    if err := bundleContext.InstallBundle(bundleName); err != nil {
       ……
    }
    installBundle, err := bundleContext.GetBundle(bundleName)
    ……
    defer func() {
        if err := bundleContext.Stop(); err != nil {
            t.Error(err)
        }
        ……
    }()

    ……
    srv, ok := installBundle.GetBundleServices()[serviceName]
    ……
    srvRef, err := bundleContext.GetServiceReference(serviceName)
    ……
    msg := kernel.NewDefaultMessage(funcName, sendmsg)
    res := <-srvRef.Send(msg)
    ……
    res = srv.Recv(msg)
    ……
}

4.3.1. 微内核总结

  • 优点;
    1. 良好的功能延伸性,需要什么功能,开发一个插件即可。
    2. 模块化好,插件相互隔离,可独立加载、升级和拆卸。
    3. 每个插件可以各自进行测试。
    4. 可以渐进式地开发,逐步增加功能
  • 缺点:
    1. 水平扩展性差,内核通常是一个独立单元,不容易做成分布式。
    2. 开发难度相对较高,因为涉及到插件与内核的通信,以及内部的插件登记机制。

4.3.2. 微服务总结

  • 优点:
    1. 各个服务之间以网络协议通信,耦合性低。
    2. 易于对热点服务水平扩容。
    3. 易于服务单独部署/升级。
    4. 服务可以进行持续集成式的开发,可以做到实时部署,不间断地升级。
    5. 易于测试,可以单独测试每一个服务。
  • 缺点:
    1. 运维成本高。
    2. 调用链长,架构复杂。
    3. 分布式的服务的原子性难以保证。

五、参考文献

[1] 架构:https://www.educative.io/blog/how-to-design-a-web-application-software-architecture-101

[2] Software Architecture Patterns:https://github.com/gg-daddy/ebooks/blob/master/software-architecture-patterns.pdf

[3]https://zhuanlan.zhihu.com/p/73442055

[4] SOA:https://www.zhihu.com/question/42061683

[5] 微内核:https://zh.wikipedia.org/zh/微內核

[6] 微服务:https://zh.wikipedia.org/zh/微服務