視角 | 微服務的數據一致性解決方案

衆所周知,微服務架構解決了不少問題,經過分解複雜的單體式應用,在功能不變的狀況下,使應用被分解爲多個可管理的服務,爲採用單體式編碼方式很難實現的功能提供了模塊化的解決方案。同時,每一個微服務獨立部署、獨立擴展,使得持續化集成成爲可能。由此,單個服務很容易開發、理解和維護。git

微服務架構爲開發帶來了諸多好處的同時,也引起了不少問題。好比服務運維變得更復雜,服務之間的依賴關係更復雜,數據一致性難以保證。github

本篇文章將討論和介紹Choerodon豬齒魚是如何保障微服務架構的數據一致性的。spring

主要內容包括 :數據庫

  • 傳統應用使用本地事務保持一致性
  • 多數據源下的分佈式事務
  • 微服務架構中應知足數據最終一致性原則
  • 使用Event Sourcing保證微服務的最終一致性
  • 使用可靠事件模式保證微服務的最終一致性
  • 使用Saga保證微服務的最終一致性

下面將經過一個實例來分別介紹這幾種模式。bash

在Choerodon 豬齒魚的 DevOps流程中,有這樣一個步驟。markdown

(1)用戶在Choerodon 平臺上建立一個項目;網絡

(2)DevOps 服務對應建立一個項目;架構

(3)DevOps 爲該項目 在 Gitlab 上建立對應的group。併發

傳統應用使用本地事務保持一致性

在講微服務架構的數據一致性以前,先介紹一下傳統關係型數據庫是如何保證一致性的,從關係型數據庫中的ACID理論講起。app

ACID 即數據庫事務正確執行的四個基本要素。分別是:

  • 原子性(Atomicity):要麼所有完成,要麼所有不完成,不存在中間狀態
  • 一致性(Consistency):事務必須始終保持系統處於一致的狀態
  • 隔離性(Isolation):事務之間相互隔離,同一時間僅有一個請求用於同一數據
  • 持久性(Durability):事務一旦提交,該事務對數據庫所做的更改便持久的保存在數據庫之中,並不會被回滾

能夠經過使用數據庫自身的ACID Transactions,將上述步驟簡化爲以下僞代碼:

... ... 
transaction.strat();
createProject(); 
devopsCreateProject(); 
gitlabCreateGroup(); 
transaction.commit(); 
... ...
複製代碼

這個過程能夠說是十分簡單,若是在這一過程當中發生失敗,例如DevOps建立項目失敗,那麼該事務作回滾操做,使得最終平臺建立項目失敗。因爲傳統應用通常都會使用一個關係型數據庫,因此能夠直接使用 ACID transactions。保證了數據自己不會出現不一致。爲保證一致性只須要:開始一個事務,改變(插入,刪除,更新)不少行,而後提交事務(若是有異常時回滾事務)。

隨着業務量的不斷增加,單數據庫已經不足以支撐龐大的業務數據,此時就須要對應用和數據庫進行拆分,於此同時,也就出現了一個應用須要同時訪問兩個或者兩個以上的數據庫或多個應用分別訪問不一樣的數據庫的狀況,數據庫的本地事務則再也不適用。

爲了解決這一問題,分佈式事務應運而生。

多數據源下的分佈式事務

想象一下,若是不少用戶同時對Choerodon 平臺進行建立項目的操做,應用接收的流量和業務數據劇增。一個數據庫並不足以存儲全部的業務數據,那麼咱們能夠將應用拆分紅IAM服務和DevOps服務。其中兩個服務分別使用各自的數據庫,這樣的狀況下,咱們就減輕了請求的壓力和數據庫訪問的壓力,兩個分別能夠很明確的知道本身執行的事務是成功仍是失敗。可是同時在這種狀況下,每一個服務都不知道另外一個服務的狀態。所以,在上面的例子中,若是當DevOps建立項目失敗時,就沒法直接使用數據庫的事務。

那麼若是當一個事務要跨越多個分佈式服務的時候,咱們應該如何保證事務呢?

爲了保證該事務能夠知足ACID,通常採用2PC或者3PC。 2PC(Two Phase Commitment Protocol),實現分佈式事務的經典表明就是兩階段提交協議。2PC包括準備階段和提交階段。在此協議中,一個或多個資源管理器的活動均由一個稱爲事務協調器的單獨軟件組件來控制。

咱們爲DevOps服務分配一個事務管理器。那麼上面的過程能夠整理爲以下兩個階段:

準備階段:

提交/回滾階段:

2PC 提供了一套完整的分佈式事務的解決方案,遵循事務嚴格的 ACID 特性。

可是,當在準備階段的時候,對應的業務數據會被鎖定,直到整個過程結束纔會釋放鎖。若是在高併發和涉及業務模塊較多的狀況下,會對數據庫的性能影響較大。並且隨着規模的增大,系統的可伸縮性越差。同時因爲 2PC引入了事務管理器,若是事務管理器和執行的服務同時宕機,則會致使數據產生不一致。雖然又提出了3PC 將2PC中的準備階段再次一分爲二的來解決這一問題,可是一樣可能會產生數據不一致的結果。

微服務架構中應知足數據最終一致性原則

不能否認,2PC 和3PC 提供瞭解決分佈式系統下事務一致性問題的思路,可是2PC同時又是一個很是耗時的複雜過程,會嚴重影響系統效率,在實踐中咱們儘可能避免使用它。因此在分佈式系統下沒法直接使用此方案來保證事務。

對於分佈式的微服務架構而言,傳統數據庫的ACID原則可能並不適用。首先微服務架構自身的全部數據都是通 過API 進行訪問。這種數據訪問方式使得微服務之間鬆耦合,而且彼此之間獨立很是容易進行性能擴展。其次 不一樣服務一般使用不一樣的數據庫,甚至並不必定會使用同一類數據庫,反而使用非關係型數據庫,而大部分的 非關係型數據庫都不支持2PC。

**在這種狀況下,又如何解決事務一致性問題呢? **

一個最直接的辦法就是考慮數據的強一致性。根據Eric Brewer提出的CAP理論,只能在數據強一致性(C)和可用性(A)之間作平衡。

CAP 是指在一個分佈式系統下,包含三個要素:Consistency(一致性)、Availability(可用性)、Partition tolerance(分區容錯性),而且三者不可得兼。

  • 一致性(Consistency),是指對於每一次讀操做,要麼都可以讀到最新寫入的數據,要麼錯誤,全部數據變更都是同步的。
  • 可用性(Availability),是指對於每一次請求,都可以獲得一個及時的、非錯的響應,可是不保證請求的結果是基於最新寫入的數據。即在能夠接受的時間範圍內正確地響應用戶請求。
  • 分區容錯性(Partition tolerance),是指因爲節點之間的網絡問題,即便一些消息丟包或者延遲,整個系統仍可以提供知足一致性和可用性的服務。

關係型數據庫單節點保證了數據強一致性(C)和可用性(A),可是卻沒法保證分區容錯性(P)。

然而在分佈式系統下,爲了保證模塊的分區容錯性(P),只能在數據強一致性(C)和可用性(A)之間作平衡。具體表現爲在必定時間內,可能模塊之間數據是不一致的,可是經過自動或手動補償後可以達到最終的一致。

可用性通常是更好的選擇,可是在服務和數據庫之間維護事務一致性是很是根本的需求,微服務架構中應該選擇知足最終一致性。

那麼咱們應該如何實現數據的最終一致性呢?

使用Event Sourcing保證微服務的最終一致性

什麼是Event Sourcing(事件溯源)?

一個對象從建立開始到消亡會經歷不少事件,傳統的方式是保存這個業務對象當前的狀態。但更多的時候,咱們也許更關心這個業務對象是怎樣達到這一狀態的。Event Sourcing從根本上和傳統的數據存儲不一樣,它存儲的不是業務對象的狀態,而是有關該業務對象一系列的狀態變化的事件。只要一個對象的狀態發生變化,服務就須要自動發佈事件來附加到事件的序列中。這個操做本質上是原子的。

如今將上面的訂單過程用Event Sourcing 進行改造,將訂單變更的一個個事件存儲起來,服務監聽事件,對訂單的狀態進行修改。

能夠看到 Event Sourcing 完整的描述了對象的整個生命週期過程當中所經歷的全部事件。因爲事件是隻會增長不會修改,這種特性使得領域模型十分的穩定。

Event sourcing 爲總體架構提供了一些可能性,可是將應用程序的每一個變更都封裝到事件保存下來,並非每一個人都能接受的風格,並且大多數人都認爲這樣很彆扭。同時這一架構在實際應用實踐中也不是特別的成熟。

使用可靠事件模式保證微服務的最終一致性

可靠事件模式屬於事件驅動架構,微服務完成操做後向消息代理髮布事件,關聯的微服務從消息代理訂閱到該 事件從而完成相應的業務操做,關鍵在於可靠事件投遞和避免事件重複消費。

可靠事件投遞有兩個特性:

  1. 每一個服務原子性的完成業務操做和發佈事件;
  2. 消息代理確保事件投遞至少一次 (at least once)。避免重複消費要求消費事件的服務實現冪等性。

有兩種實現方式:

1. 本地事件表

本地事件表方法將事件和業務數據保存在同一個數據庫中,使用一個額外的「事件恢復」服務來恢 復事件,由本地事務保證更新業務和發佈事件的原子性。考慮到事件恢復可能會有必定的延時,服務在完成本 地事務後可當即向消息代理髮佈一個事件。

使用本地事件表將事件和業務數據保存在同一個數據庫中,會在每一個服務存儲一份數據,在必定程度上會形成代碼的重複冗餘。同時,這種模式下的業務系統和事件系統耦合比較緊密,額外增長的事件數據庫操做也會給數據庫帶來額外的壓力,可能成爲瓶頸。

2. 外部事件表

針對本地事件表出現的問題,提出外部事件表方法,將事件持久化到外部的事件系統,事件系統 需提供實時事件服務以接收微服務發佈的事件,同時事件系統還須要提供事件恢復服務來確認和恢復事件。

藉助Kafka和可靠事件,Choerodon經過以下代碼實現項目建立流程。

// IAM ProjectService

@Service
@RefreshScope
public class ProjectServiceImpl implements ProjectService {

    private ProjectRepository projectRepository;

    private UserRepository userRepository;

    private OrganizationRepository organizationRepository;

    @Value("${choerodon.devops.message:false}")
    private boolean devopsMessage;

    @Value("${spring.application.name:default}")
    private String serviceName;

    private EventProducerTemplate eventProducerTemplate;

    public ProjectServiceImpl(ProjectRepository projectRepository,
                              UserRepository userRepository,
                              OrganizationRepository organizationRepository,
                              EventProducerTemplate eventProducerTemplate) {
        this.projectRepository = projectRepository;
        this.userRepository = userRepository;
        this.organizationRepository = organizationRepository;
        this.eventProducerTemplate = eventProducerTemplate;
    }

    @Transactional(rollbackFor = CommonException.class)
    @Override
    public ProjectDTO update(ProjectDTO projectDTO) {
        ProjectDO project = ConvertHelper.convert(projectDTO, ProjectDO.class);
        if (devopsMessage) {
            ProjectDTO dto = new ProjectDTO();
            CustomUserDetails details = DetailsHelper.getUserDetails();
            UserE user = userRepository.selectByLoginName(details.getUsername());
            ProjectDO projectDO = projectRepository.selectByPrimaryKey(projectDTO.getId());
            OrganizationDO organizationDO = organizationRepository.selectByPrimaryKey(projectDO.getOrganizationId());
            ProjectEventPayload projectEventMsg = new ProjectEventPayload();
            projectEventMsg.setUserName(details.getUsername());
            projectEventMsg.setUserId(user.getId());
            if (organizationDO != null) {
                projectEventMsg.setOrganizationCode(organizationDO.getCode());
                projectEventMsg.setOrganizationName(organizationDO.getName());
            }
            projectEventMsg.setProjectId(projectDO.getId());
            projectEventMsg.setProjectCode(projectDO.getCode());
            Exception exception = eventProducerTemplate.execute("project", EVENT_TYPE_UPDATE_PROJECT,
                    serviceName, projectEventMsg, (String uuid) -> {
                        ProjectE projectE = projectRepository.updateSelective(project);
                        projectEventMsg.setProjectName(project.getName());
                        BeanUtils.copyProperties(projectE, dto);
                    });
            if (exception != null) {
                throw new CommonException(exception.getMessage());
            }
            return dto;
        } else {
            return ConvertHelper.convert(
                    projectRepository.updateSelective(project), ProjectDTO.class);
        }
    }
}

複製代碼
// DEVOPS DevopsEventHandler
@Component
public class DevopsEventHandler {

    private static final String DEVOPS_SERVICE = "devops-service";
    private static final String IAM_SERVICE = "iam-service";

    private static final Logger LOGGER = LoggerFactory.getLogger(DevopsEventHandler.class);

    @Autowired
    private ProjectService projectService;
    @Autowired
    private GitlabGroupService gitlabGroupService;

    private void loggerInfo(Object o) {
        LOGGER.info("data: {}", o);
    }

    /**
     * 建立項目事件
     */
    @EventListener(topic = IAM_SERVICE, businessType = "createProject")
    public void handleProjectCreateEvent(EventPayload<ProjectEvent> payload) {
        ProjectEvent projectEvent = payload.getData();
        loggerInfo(projectEvent);
        projectService.createProject(projectEvent);
    }

    /**
     * 建立組事件
     */
    @EventListener(topic = DEVOPS_SERVICE, businessType = "GitlabGroup")
    public void handleGitlabGroupEvent(EventPayload<GitlabGroupPayload> payload) {
        GitlabGroupPayload gitlabGroupPayload = payload.getData();
        loggerInfo(gitlabGroupPayload);
        gitlabGroupService.createGroup(gitlabGroupPayload);
    }
}

複製代碼

使用Saga保證微服務的最終一致性 - Choerodon的解決方案

Saga是來自於1987年Hector GM和Kenneth Salem論文。在他們的論文中提到,一個長活事務Long lived transactions (LLTs) 會相對較長的佔用數據庫資源。若是將它分解成多個事務,只要保證這些事務都執行成功, 或者經過補償的機制,來保證事務的正常執行。這一個個的事務被他們稱之爲Saga。

Saga將一個跨服務的事務拆分紅多個事務,每一個子事務都須要定義一個對應的補償操做。經過異步的模式來完 成整個Saga流程。

在Choerodon中,將項目建立流程拆分紅多個Saga。

// ProjectService

    @Transactional
    @Override
    @Saga(code = PROJECT_CREATE, description = "iam建立項目", inputSchemaClass = ProjectEventPayload.class)
    public ProjectDTO createProject(ProjectDTO projectDTO) {

        if (projectDTO.getEnabled() == null) {
            projectDTO.setEnabled(true);
        }
        final ProjectE projectE = ConvertHelper.convert(projectDTO, ProjectE.class);
        ProjectDTO dto;
        if (devopsMessage) {
            dto = createProjectBySaga(projectE);
        } else {
            ProjectE newProjectE = projectRepository.create(projectE);
            initMemberRole(newProjectE);
            dto = ConvertHelper.convert(newProjectE, ProjectDTO.class);
        }
        return dto;
    }

    private ProjectDTO createProjectBySaga(final ProjectE projectE) {
        ProjectEventPayload projectEventMsg = new ProjectEventPayload();
        CustomUserDetails details = DetailsHelper.getUserDetails();
        projectEventMsg.setUserName(details.getUsername());
        projectEventMsg.setUserId(details.getUserId());
        ProjectE newProjectE = projectRepository.create(projectE);
        projectEventMsg.setRoleLabels(initMemberRole(newProjectE));
        projectEventMsg.setProjectId(newProjectE.getId());
        projectEventMsg.setProjectCode(newProjectE.getCode());
        projectEventMsg.setProjectName(newProjectE.getName());
        OrganizationDO organizationDO =
                organizationRepository.selectByPrimaryKey(newProjectE.getOrganizationId());
        projectEventMsg.setOrganizationCode(organizationDO.getCode());
        projectEventMsg.setOrganizationName(organizationDO.getName());
        try {
            String input = mapper.writeValueAsString(projectEventMsg);
            sagaClient.startSaga(PROJECT_CREATE, new StartInstanceDTO(input, "project", newProjectE.getId() + ""));
        } catch (Exception e) {
            throw new CommonException("error.organizationProjectService.createProject.event", e);
        }
        return ConvertHelper.convert(newProjectE, ProjectDTO.class);
    }
複製代碼
// DevopsSagaHandler
@Component
public class DevopsSagaHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(DevopsSagaHandler.class);
    private final Gson gson = new Gson();

    @Autowired
    private ProjectService projectService;
    @Autowired
    private GitlabGroupService gitlabGroupService;

    private void loggerInfo(Object o) {
        LOGGER.info("data: {}", o);
    }

    /**
     * 建立項目saga
     */
    @SagaTask(code = "devopsCreateProject",
            description = "devops建立項目",
            sagaCode = "iam-create-project",
            seq = 1)
    public String handleProjectCreateEvent(String msg) {
        ProjectEvent projectEvent = gson.fromJson(msg, ProjectEvent.class);
        loggerInfo(projectEvent);
        projectService.createProject(projectEvent);
        return msg;
    }

    /**
     * 建立組事件
     */
    @SagaTask(code = "devopsCreateGitLabGroup",
            description = "devops 建立 GitLab Group",
            sagaCode = "iam-create-project",
            seq = 2)
    public String handleGitlabGroupEvent(String msg) {
        ProjectEvent projectEvent = gson.fromJson(msg, ProjectEvent.class);
        GitlabGroupPayload gitlabGroupPayload = new GitlabGroupPayload();
        BeanUtils.copyProperties(projectEvent, gitlabGroupPayload);
        loggerInfo(gitlabGroupPayload);
        gitlabGroupService.createGroup(gitlabGroupPayload, "");
        return msg;
    }
}

複製代碼

能夠發現,Saga和可靠事件模式很類似,都是將微服務下的事務做爲一個個體,而後經過有序序列來執行。可是在實現上,有很大的區別。

可靠事件依賴於Kafka,消費者屬於被動監聽Kafka的消息,鑑於Kafka自身的緣由,若是對消費者進行橫向擴展,效果並不理想。

而在 Saga 中,咱們爲 Saga 分配了一個orchestrator做爲事務管理器,當服務啓動時,將服務中全部的 SagaTask 註冊到管理器中。當一個 Saga 實例經過sagaClient.startSaga啓動時,服務消費者就能夠經過輪詢的方式主動拉取到該實例對應的Saga數據,並執行對應的業務邏輯。執行的狀態能夠經過事務管理器進行查看,展示在界面上。

經過Choerodon的事務定義界面,將不一樣服務的SagaTask 收集展現,能夠看到系統中的全部Saga 定義以及所屬的微服務。同時,在每個Saga 定義的詳情中,能夠詳細的瞭解到該Saga的詳細信息:

在這種狀況下,當併發量增多或者 SagaTask 的數量不少的時候,能夠很便捷的對消費者進行擴展。

▌Saga的補償機制

Saga支持向前和向後恢復:

  • 向後恢復:若是任意一個子事務失敗,則補償全部已完成的事務

  • 向前恢復:若是子事務失敗,則重試失敗的事務

Choerodon 採用的是向前恢復,經過界面能夠很方便的對事務的信息進行檢索,當Saga發生失敗時,也能夠看到失敗的緣由,而且手動進行重試。

經過Choerodon的事務實例界面,能夠查詢到系統中運行的全部Saga實例,掌握實例的運行狀態,並對失敗的實例進行手動的重試:

對於向前恢復而言,理論上咱們的子事務最終老是會成功的。可是在實際的應用中,可能由於一些其餘的因素,形成失敗,那麼就須要有對應的故障恢復回滾的機制。

▌使用Saga的要求

Saga是一個簡單易行的方案,使用Saga的兩個要求:

  • 冪等:冪等是每一個Saga 屢次執行所產生的影響應該和一次執行的影響相同。一個很簡單的例子,上述流程中,若是在建立項目的時候由於網絡問題致使超時,這時若是進行重試,請求恢復。若是沒有冪等,就可能建立了兩個項目。
  • 可交換:可交換是指在同一層級中,不管先執行那個Saga最終的結果都應該是同樣的。

綜合比較

2PC是一個阻塞式,嚴格知足ACID原則的方案,可是由於性能上的緣由在微服務架構下並非最佳 的方案。

Event sourcing 爲總體架構提供了一些可能性。可是若是隨着業務的變動,事件結構自身發生必定的變化時,須要經過額外的方式來進行補償,並且當下並無一個成熟完善的框架。

基於事件驅動的可靠事件是創建在消息隊列基礎上,每個服務,除了本身的業務邏輯以外還須要額外的事件 表來保存當前的事件的狀態,因此至關因而把集中式的事務狀態分佈到了每個服務當中。雖然服務之間去中 心化,可是當服務增多,服務之間的分佈式事務帶來的應用複雜度也再提升,當事件發生問題時,難以定位。

而Saga下降了數據一致性的複雜度,簡單易行,將全部的事務統一可視化管理,讓運維更加簡單,同時每個 消費者能夠進行快速的擴展,實現了事務的高可用。

關於豬齒魚

Choerodon豬齒魚是一個開源企業服務平臺,是基於Kubernetes的容器編排和管理能力,整合DevOps工具鏈、微服務和移動應用框架,來幫助企業實現敏捷化的應用交付和自動化的運營管理的開源平臺,同時提供IoT、支付、數據、智能洞察、企業應用市場等業務組件,致力幫助企業聚焦於業務,加速數字化轉型。

Choerodon的源代碼放在github上,你們有興趣能夠來關注:github.com/choerodon/c…

相關文章
相關標籤/搜索