軟件架構——分層、SOA、微內核


一、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/go-example/blob/main/patterns/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的有效插件。下面是一個典型的插件(源碼傳送門):

// 一個符合標准的服務
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/微服務


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM