天天看點

微服務拆分之無鎖程式設計

介紹

如果你受夠了微服務系統中無休無止的痛苦,哪些資料庫事務,分布式鎖,永無止境的系統優化,莫名其妙的卡死,詭異的性能波動。來嘗試一下最新的無鎖程式設計技術吧。這個技術最酷的地方就是不需要資料庫事務和分布式鎖就能實作分布式系統的開發。衆所周知分布式鎖和資料庫事務的濫用導緻了分布式系統耦合的問題。

我在這個系列的第二篇文章中曾經對一個開源的電商軟體進行了分布式的系統分析。您可以點選下面連結找到這篇文章。

ITDSD - 1. Splitting in Microservice Architecture 在這裡我已經使用AP&RP理論将這個工程改造為分布式系統。在服務端軟體開發的過程中。随着使用者數量的增加我們都會遇到服務端性能的瓶頸。為了解決服務端性能的瓶頸需要拆分服務端到不同的硬體以提高叢集整體的承載能力。沒有AP&RP理論之前這種服務端的拆分非常低效。通常人們引入大量的資料庫事務和分布式鎖,這些資料庫事務錯綜複雜,并最終使人們迷失在系統耦合中。通過學習AP&RP理論可以讓你具備編寫無鎖分布式系統的能力。本文所使用的執行個體就是第一個使用AP&RP理論開發的無鎖分布式系統。為了說明AP&RP理論的通用性,我選擇了最常見的網上商場系統作為執行個體。因為它的複雜度适中,适合初學者學習。并公開其源代碼于github.com上。連結為 gantleman/shopd

。或者您可以通過下載下傳本文的附件得到它。

性能的提升

評價一個系統重構是否真的有效,通過性能對比就可以得到結論。本文是通過對Manphil/shop工程進行改造得到的。原工程是一個非常簡單而明确的網上商場系統。是由有一個服務端和一個資料庫共同組成。服務端又由62個任務組成。

假設其服務端和資料庫分别運作在兩個伺服器容器内。改造以前服務端的62個任務共享一個伺服器容器。我們将其改造為分布式系統後。根據AP&RP理論可以将62個任務分為3個類型。第一種類型是多個任務必須放在一個伺服器容器内。第二種類型是1個任務可以放在一個伺服器容器内。第三種類型是1個任務可以複制多份放入任意數量的伺服器容器内。

第一個類型的任務一共有30個分為8組,這8組任務隻能分别放入8個伺服器容器。這8組任務中最多的有7個任務,最少的有2個任務。在分布式系統改造前每個任務能配置設定的資源為一個伺服器器容器的1/62。在系統改造後運作任務最多的伺服器容器内每個任務所配置設定的資源為1/7。可知在進行分布式系統改造後單任務獲得的伺服器資源最少提升了8.8倍。同理可知第二種類型的任務可以獲得的資源提升了62倍。而對第三種類型的任務因為可以複制到任意數量的伺服器容器中,是以能夠獲得的性能提升沒有限制,随着硬體的增加而增加。因為第一種類型的任務占任務總數的48%。是以對于48%的系統性能提升了8.8到62倍。而對于剩下的62%的任務可以獲得無限的性能提升。

單機Berkeley DB寫入極限為10萬每秒。7個任務可以每個任務可以配置設定到1萬左右。因為這個7個任務包含了訂單完成的任務。是以這個網站的訂單完成功能的理論承載極限為每秒1萬。全球最大的電商網站的促銷活動中訂單數量的峰值為每秒8萬。是以本次分布式改造理論上可以使軟體性能達到世界頂級水準。當然這種大型分布式系統也需要大量的硬體作為支援。不能單獨依靠軟體系統性能的提升。

安裝說明

開發環境:Ubuntu 16.04.4, vscode 1.25.1, mysql Ver 14.14 Distrib 5.7.26, redis 3.0.6, maven 3.6.0, java 1.8.0_201, git 2.7.4

軟體架構:SpringBoot, JE.

安裝好上述環境之後在根目錄執行

>mvn install

然後将shop.sql導入到mysql資料庫

>mysql -h localhost -u root -p test < /shop.sql --default-character-set=utf8

如果使用VSCode編輯器需要添加launch.json檔案

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "java",
            "name": "Debug (Launch) - Current File",
            "request": "launch",
            "mainClass": "${file}"
        },
        {
            "type": "java",
            "name": "Debug (Launch)-DemoApplication<shopd>",
            "request": "launch",
            "mainClass": "com.github.gantleman.shopd.DemoApplication",
            "projectName": "shopd"
        }
    ]
}
           

編輯application.properties檔案配置資料庫位址和帳戶密碼,以及伺服器端口号。

spring.datasource.url=jdbc:mysql://localhost/shop?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driverClassName=com.mysql.jdbc.Driver
server.port=8081
           

點選F5啟動調試模式。

打開浏覽輸入

http://localhost:8081/main

即可打開首頁。

分布式系統的說明

這個分布式系統是由5個部分組成,分别是nginx反向代理,springboot伺服器,reids記憶體資料庫,mysql資料庫,以及還沒有開發完成的分布式管理器。還有一個非獨立運作的JE資料庫。分布式管理器在目前版本并沒有一個獨立的實體。其負責管理redis資料庫中的routeconfig字段的資料釋出。是以我們需要通過運作測試例程t7()函數,來實作routeconfig字段的資料釋出。

如果按純粹的AP&RP理論進行微服務化的拆分,那麼是不需要mysql伺服器的。在第一個版本實作中是遵循原AP&RP理論。首先需要将資料按讀寫功能進行分析。這部分工作在ITDSD2中以及完成并存儲在了shop.xlsx文檔,你在下載下傳區可以找到它。被分析過的資料按讀寫功能分别提前存儲到每個JE資料庫内。使用者通過nginx調用對應功能的springboot伺服器。再由springboot服務将資料釋出到redis記憶體資料庫上。

在最新的版本裡,為了能夠有效地管理分布式系統引入了緩存機制。緩存機制可以将Mysql資料庫一部分的資料讀取到JE資料庫。在不使用這些緩存資料後可以将資料再寫回Mysql資料庫。與其他分布式系統中的緩存機制不同的是。這套緩存機制使用頁緩存架構。避免了使用不存在資料導緻反複觸發讀取緩存。也使得緩存随機命中率得到了提高。其效果要好于在分布式系統逐條交換資料。

到這裡我們成功地建立了一個單機的調試環境。如何進行分布式系統的部署呢?隻要建立一個nginx反向代理伺服器。并依據shop.xlsx檔案所顯示的分析結果。将可以分布的任務單獨建立一個springboot伺服器并修改對應的routeconfig字段。假設我們把/admin/activity/show任務單獨放入一個伺服器。由shop.xlsx檔案可以知道/admin/activity/show任務屬于可以任意複制多個類型。那麼隻要單獨建立一個springboot伺服器,并設定端口為8082并修改redis中的routeconfig字段。以及修改反向代理的nginx,使得/admin/activity/show的調用指向8082。/admin/activity/show任務就被單獨的拆分出去了。

微服務拆分之無鎖程式設計

在叢集狀态下伺服器請求由nginx反向代理伺服器到springboot伺服器。Springboot伺服器檢查是否命中頁緩存。如果沒有命中就從mysql伺服器調入資料。目前Springboot伺服器調入資料後将資料釋出到redis伺服器中供其它Springboot伺服器讀取。其它Springboot伺服器讀取資料redis伺服器中的資料如果不存在。就觸發負責管理指定資料的Springboot伺服器更新緩存。注意這裡如果不是負責管理指定資料的Springboot伺服器是無權直接讀取mysql資料庫的指定資料。隻能通知負責管理指定資料的Springboot伺服器進行操作。

索引和緩存的代碼分析

使用AP&RP理論進行微服務化的拆分本身非常簡單。對于面向使用者的産品邏輯更改也非常的少。在重構過程中最大的問題是建構索引和緩存頁排程。Redis不支援建構索引,需要使用map和set結構自行建構。JE資料庫源于Berkeley DB,其支援索引但索引結構和Myslq完全不同。把資料從Mysql資料庫讀入JE資料庫時需要進行重新建立索引。 Mysql資料庫并沒有将索引資料作為單獨的資料提供給使用者。隻能間接的通過指令使用索引資料。這與索引資料嚴重依賴資料結構有關。索引資料會因為資料集合的改變而改變。是以在工程中可以看到大量建構索引和載入緩存的代碼。

以getAllAddressByUser函數為例。請看代碼注釋

@Override
public List<Address> getAllAddressByUser(Integer userID, String url) {
    List<Address> re = new ArrayList<Address>();
///這裡查詢指定userID的資料是否被載入到redis
    if(redisu.hHasKey(classname_extra+"pageid", cacheService.PageID(userID).toString())) {
        //讀取redis的索引資料,這個索引内包含有userID的所有addressID
        Set<Object> ro = redisu.sGet("address_u"+userID.toString());
        if(ro != null){
            for (Object id : ro) {
                Address r =  getAddressByKey((Integer)id, url);
                if (r != null)
                    re.add(r);
            }
///增加索引頁的引用次數,為了索引頁的排程               
 redisu.hincr(classname_extra+"pageid", cacheService.PageID(userID).toString(), 1);               
        }
    }else {
///如果指定頁沒有在緩存内就觸發載入頁面。
        if(!cacheService.IsLocal(url)){
            cacheService.RemoteRefresh("/addressuserpage", userID);
        }else{
            RefreshUserDBD(userID, true, true);
        }
///如果載入成功就再次讀取資料。
        if(redisu.hHasKey(classname_extra+"pageid", cacheService.PageID(userID).toString())) {
            //read redis
            Set<Object> ro = redisu.sGet("address_u"+userID.toString());
            if(ro != null){
                for (Object id : ro) {
                    Address r =  getAddressByKey((Integer)id, url);
                    if (r != null)
                        re.add(r);
                }
                redisu.hincr(classname_extra+"pageid", cacheService.PageID(userID).toString(), 1);               
            }
        }
    }
    return re;
}
           

緩存的載入和索引的建立通常的混合在一起。如果索引沒有命中需要載入相關的索引資料的緩存。因為存在分布式架構是以每個伺服器隻存儲了部分資料。是以要嚴格禁止對資料進行全局檢索。例如動态檢查使用者名下全部位址。是以我在資料庫内建立address_user表用于儲存使用者名下所有位址的id。如果沒有address_user就需要每次查詢使用者位址的時候進行一次資料庫address表的全局查詢。因為address_user的索引資料存在。我隻要獲得對應使用者的索引資料就知道他的全部位址id了。

緩存的管理是通過cacheService實作的。我使用了表的ID作為緩存的分割标緻。這樣可以通過ID友善的計算出目前所在頁面。當然這樣的簡化的寫法會導緻名字,類别,商品搜尋等純文字類的檢索功能無法實作頁緩存。商品搜尋以後會通過大資料的方式實作。使用名字的資料可以通過bit矩陣的方式。這些可能會在後續的改進中實作。

結論

這是首個在分布式中實作無鎖程式設計的示例,讓我遺憾的是他還不是一個架構。對于希望在分布式工程中實作無鎖程式設計的程式員。這個示例可以作為一個很好的開始。使用類似設計的分布式系統将不會再受到分布式鎖與資料庫事物帶來的耦合影響。因為遵循AP&RP理論所設計的系統不存在分布式鎖和資料庫事物。超大型的多人互動系統的軟體開發将不在高不可及。普通的程式員也可以輕易的上手。我想這将會很好地推動伺服器端軟體的普及工作。