分佈式系統中間件整理

整理多種中間件,橫向對比分佈式相關的特性,關注高可用、負載均衡、故障恢復等不同中間件的實現方式。主要包含Redis、Kafka、RabbitMQ、RocketMQ、Elasticsearch、MongoDB。

Redis

當前版本6.0.5(2020-6-28)
集羣架構中,Redis支持主從,哨兵,以及最新的無主集羣模式(Redis-Cluster)

集羣架構(主從模式+哨兵)

Redis主節點正常啓動,從節點使用slaveof命令設定爲從節點。
哨兵節點最好設置爲集羣,這樣能夠在單個哨兵節點網絡故障時不會出現誤判,多數哨兵節點共同判斷節點失效可信度更高。當主服務器失效的時候,見識這個主服務器的所有Sentinel就會基於彼此共有的信息選出一個Sentinel,並從現有的從服務器當中選出一個新的主服務器。當被選中的從服務器轉換爲主服務器之後,那個被選中的Sentinel就會讓剩餘的其他服務器去複製這個新的主服務器(默認Sentinel會一個一個遷移從服務器,可以通過配置選項進行修改)
S_DOWN:subjectively down,直接翻譯的爲"主觀"失效,即當前sentinel實例認爲某個redis服務爲"不可用"狀態.
O_DOWN:objectively down,直接翻譯爲"客觀"失效,即多個sentinel實例都認爲master處於"SDOWN"狀態,那麼此時master將處於ODOWN,ODOWN可以簡單理解爲master已經被集羣確定爲"不可用",將會開啓failover.

選主算法

一、使用如下條件篩選備選node:
1、slave節點狀態處於S_DOWN,O_DOWN,DISCONNECTED的除外
2、最近一次ping應答時間不超過5倍ping的間隔(假如ping的間隔爲1秒,則最近一次應答延遲不應超過5秒,redis sentinel默認爲1秒)
3、info_refresh應答不超過3倍info_refresh的間隔(原理同2,redis sentinel默認爲10秒)
4、slave節點與master節點失去聯繫的時間不能超過( (now - master->s_down_since_time) + (master->down_after_period * 10))。總體意思是說,slave節點與master同步太不及時的(比如新啓動的節點),不應該參與被選舉。
5、Slave priority不等於0(這個是在配置文件中指定,默認配置爲100)。

二、從備選node中,按照如下順序選擇新的master
1、較低的slave_priority(這個是在配置文件中指定,默認配置爲100)
2、較大的replication offset(每個slave在與master同步後offset自動增加)
3、較小的runid(每個redis實例,都會有一個runid,通常是一個40位的隨機字符串,在redis啓動時設置,重複概率非常小)
4、如果以上條件都不足以區別出唯一的節點,則會看哪個slave節點處理之前master發送的command多,就選誰。

集羣架構(Redis-Cluster)

在3.x提出cluster集羣模式。Redis-Cluster採用無中心結構,每個節點保存數據和整個集羣狀態,每個節點都和其他所有節點連接。
key 分佈模式,key空間分佈被劃分爲16384個slot,所以一個集羣,主節點的個數最大爲16384(一般建議master最大節點數爲1000)
Cluster bus,每個節點有一個額外的TCP端口,這個端口用來和其他節點交換信息。這個端口一般是在與客戶端鏈接端口上面加10000,比如客戶端端口爲6379,那麼cluster bus的端口爲16379.
cluster 拓撲,Redis cluster 是一個網狀的,每一個節點通過tcp與其他每個節點連接。假如n個節點的集羣,每個節點有n-1個出的鏈接,n-1個進的鏈接。這些鏈接會一直存活。假如一個節點發送了一個ping,很就沒收到pong,但還沒到時間把這個節點設爲 unreachable,就會通過重連刷新鏈接。
Nodes handshake,如果一個節點發送MEET信息(METT 類似ping,但是強迫接受者,把它作爲集羣一員)。一個節點發送MEET信息,只有管理員通過命令行,運行如下命令CLUSTER MEET ip port。如果這個節點已經被一個節點信任,那麼也會被其他節點信任。比如A 知道B,B知道C,B會發送gossip信息給A關於C的信息。A就會認爲C是集羣一員,並與其建立連接。
失敗檢測,集羣失效檢測就是,當某個master或者slave不能被大多數nodes可達時,用於故障遷移並將合適的slave提升爲master。當slave提升未能有效實施時,集羣將處於error狀態且停止接收Client端查詢。
集羣中的每個節點都會定期地向集羣中的其他節點發送PING消息,以此交換各個節點狀態信息,檢測各個節點狀態:在線狀態、疑似下線狀態PFAIL、已下線狀態FAIL。當主節點A通過消息得知主節點B認爲主節點D進入了疑似下線(PFAIL)狀態時,主節點A會在自己的clusterState.nodes字典中找到主節點D所對應的clusterNode結構,並將主節點B的下線報告(failure report)添加到clusterNode結構的fail_reports鏈表中
如果集羣裏面,半數以上的主節點都將主節點D報告爲疑似下線,那麼主節點D將被標記爲已下線(FAIL)狀態,將主節點D標記爲已下線的節點會向集羣廣播主節點D的FAIL消息,
所有收到FAIL消息的節點都會立即更新nodes裏面主節點D狀態標記爲已下線。
將node標記爲FAIL需要滿足以下兩個條件:
1.有半數以上的主節點將node標記爲PFAIL狀態。
2.當前節點也將node標記爲PFAIL狀態。

選主算法

選新主的過程基於Raft協議選舉方式來實現的
1)當從節點發現自己的主節點進行已下線狀態時,從節點會廣播一條
CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到這條消息,並且具有投票權的主節點向這個從節點投票
2)如果一個主節點具有投票權,並且這個主節點尚未投票給其他從節點,那麼主節點將向要求投票的從節點返回一條,CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示這個主節點支持從節點成爲新的主節點
3)每個參與選舉的從節點都會接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,並根據自己收到了多少條這種消息來統計自己獲得了多少主節點的支持
4)如果集羣裏有N個具有投票權的主節點,那麼當一個從節點收集到大於等於集羣N/2+1張支持票時,這個從節點就成爲新的主節點
5)如果在一個配置紀元沒有從能夠收集到足夠的支持票數,那麼集羣進入一個新的配置紀元,並再次進行選主,直到選出新的主節點爲止

負載均衡

客戶端請求獲得指定鍵的值時只需要向任意集羣中的節點發送命令,該節點會判斷這個鍵是否在本節點中,在的話直接獲取返回,如果不在則返回給客戶端一個重定向命令,並附帶存儲此鍵對應的節點的ip、端口、槽位。客戶端重新發送請求即可。也可以配置令節點自動轉發請求到指定節點處理再返回給客戶端。當然這樣在一定程度上會影響性能,可以由客戶端配置緩存插槽和節點的信息來解決。

其他

Codis是豌豆莢團隊開源的使用Go語言編寫的Redis分佈式解決方案,它是作爲中間件,以代理的身份接收請求,底層再將請求轉發到指定的節點中,Codis的優勢在於可以不停機動態增加或者刪除節點,並提供了圖像化的管理界面!

Kafka

當前版本2.5.0(2020-6-28)
KafkaController作爲Kafka集羣控制管理模塊。由於Zookeeper上保存了Kafka機器的元數據信息,因爲KafkaController通過3在不同目錄註冊不同的回調函數來達到監測集羣狀態的目的,以及響應集羣狀態的變化

集羣架構

集羣中主要的模式是Partition-Replica,Kafka的核心概念在於 每個Topic下分爲多個Partition,每個Partition都有一個或多個Replica。其中一個Replica爲Leader,其他都爲Follower,Leader處理Partition的所有讀寫請求,Follower定期同步Leader上的數據。

選主算法

Leader和Follower的選舉是基於Zookeeper實現的,嘗試在Zookeeper的相同路徑上創建瞬時節點(Ephemeral Node),只有一個KafkaController會創建成功。其中負責狀態管理的類爲ZookeeperLeaderElector。

Topic分區的Leader Replica在不同場景下的選舉策略是不一樣的,不同選舉策略都基礎PartitionLeaderSelector。其根據Topic、Partition、當前Leader、當前的ISR選舉出新的Leader,新的ISR和新的AR(在線狀態),共有5種不同的策略:

  • NoOpLeaderSelector:默認的選舉策略
  • ReassignedPartitionLeaderSelector:當分區AR重新分配時使用的策略
  • PreferredReplicaPartitionLeaderSelector:集羣內部自動平衡負載或者用戶觸發手動平衡負載時使用的策略
    隨着Topic的新建刪除以及Broker Server的上下線,原本Topic分區的Leader Replica在集羣中的分佈越來越不均勻。 auto.leader.rebalance.enable爲true,則會自動觸發分區的Leader Replica選舉,或者管理員下發分區Leader Replica選舉指令。這會在Zookeeper的 /admin/preferred_replica_election指定具體的Topic和分區,此時Leader狀態的KafkaController監測到這個路徑的數據變化就會觸發相應的回調函數,促使對應的Topic分區發生Leader Replica的選舉。
  • OfflinePartitionLeaderSelector:分區狀態從OfflinePartition或者NewPartition切換爲OnlinePartition時使用的策略
  1. 篩選出在線的ISR和AR
  2. 優先從在線的ISR中選擇,如果列表不爲空則選擇列表中的第一個,選舉結束
  3. 在線ISR爲空,根據 unclean.leader.election.enable 決定是否從在線的AR中選舉Leader,如果允許,則選擇AR列表中的第一個,結束選舉,如果AR列表爲空選舉失敗。
  • ControllerShutdownLeaderSelector:Leader狀態的KafkaController處理其他Broker Server下線導致分區的Leader Replica發生切換時使用的策略。
  1. 篩選出在線的ISR
  2. 剔除離線的ISR形成新的ISR列表
  3. 如果新的ISR列表不爲空,則選舉第一個Replica作爲新的Leader,否則選舉失敗

負載均衡

消費的時候,只會從leader去讀,但是隻有當一個消息已經被所有follower都同步成功並返回ack的時候,這個消息才能夠被消費者讀到。所以可以說讀取是沒有負載均衡地。複製集僅爲了高可用。
Partition的AR列表的第一個Replica稱爲“Preferred Replica”,並均勻分佈在整個Kafka集羣中。由於每個Partition只有Leader Replica對外提供讀寫服務,並且Partition創建的時候默認的Leader Replica位於Preferred Replica之上,此時Kafka集羣的負載是均衡的,如果Kafka集羣長時間運行,Broker Server中途由於異常而發生重啓,此時Partition的Leader Replica會發生遷移,這樣會導致其Partition的Leader Replica在集羣中不再均衡了。
kafka-reassign-partitions.sh提供來重新分配分區副本的能力。該工具可以促進Kafka集羣的負載均衡。因爲Follower Replica需要從Leader Replica Fetch數據以保持與與Leader Replica同步,僅保持Leader Replica分佈的平衡對整個集羣的負載均衡時不夠的。另外當Kafka集羣擴容後,該工具可以將已有Topic的Partition遷移到新加入的Broker上。

RabbitMQ

當前版本3.8.5(2020-6-28)
RabbitMQ客戶端中與事務機制相關的方法有三個:channel.txSelect(設置爲事務模式)、channel.txCommit(提交事務)和channel.txRollback(回滾事務)。事務會很大影響RabbitMQ的消息吞吐量。
輕量級的方式:發送方確認機制。生產者將信道設置成confirm(確認)模式,一旦信道進入confirm模式,所有該信道上面發佈的消息都會被指派一個唯一的ID(從1開始),一旦消息被投遞到所有匹配隊列之後,RabbitMQ就發送一個確認(Basic.Ack)給生產者(包含消息的唯一ID),這就使得生產者知曉消息已經正確的到達了目的地。如果消息和隊列是可持久化的,那麼確認消息就會在消息寫入磁盤後發出。RabbitMQ回傳給生產者的確認消息中的deliveryTag包含了待確認消息的序號,此外RabbitMQ也可以設置channel.basicAck方法中的multiple參數,表示這個序號之前的所有消息都已經得到了處理。

集羣架構

RabbitMQ集羣中的所有節點都會備份所有的元數據信息,包括:

  • 隊列元數據:隊列的名稱以及屬性
  • 交換器:交換器的名稱以及屬性
  • 綁定關係元數據:交換器與隊列或者交換器與交換器之間的綁定關係
  • vhost元數據:爲vhost內的隊列、交換器和綁定命名空間及安全屬性
    每個virtual host本質上都是一個RabbitMQ Server,擁有它自己的queue,exchagne,和bings rule等等。這保證了你可以在多個不同的application中使用RabbitMQ。

在RabbitMQ集羣中創建隊列,集羣只會在單個節點而不是在所有節點上創建隊列的進程幷包含完整的隊列(元數據、狀態、內容)。這樣只有隊列的宿主節點(所有者)節點知道隊列的所有信息,所有其他非所有者只知道隊列的元數據和指向該隊列存在的那個節點的指針。節點崩潰時,該節點的隊列進程和關聯的綁定都會消失,附加在那些隊列上的消費者也會丟失其所訂閱的信息,並且任何匹配該隊列綁定信息的新消息也會丟失。
不同於隊列那樣有自己的進程,交換器實際上只是一個名稱和綁定列表。當消息發佈到交換器時,實際上是由所連接的信道將消息上的路由鍵同交換器的綁定列表進行比較,然後再路由消息。當創建一個新的交換器時,RabbitMQ所要做的就是將綁定列表添加到集羣中的所有節點上。這樣每個節點上的每個信道都可以訪問到新的交換器。

多機多節點是指每臺物理機器都安裝了RabbitMQ,應當只在局域網內使用,廣域網應當使用Federation或者Shovel。
命令行主要使用 rabbitmqctl join_cluster {nodename} 加入集羣節點
rabbitmqctl forget_cluester_node {nodename}
RabbitMQ要求集羣中至少有一個磁盤節點,其他節點可以是內存節點。當節點加入或者離開集羣的時候,它們必須將變更通知到至少一個磁盤節點。如果唯一一個磁盤節點崩潰,集羣可以繼續收發消息,但是不能執行創建隊列、交換器、綁定關係、用戶,以及更改權限、添加和刪除集羣節點的操作。所以集羣應該保障有兩個或者多個磁盤節點的存在。

Federation(聯邦交換器)

Federation插件可以讓多個交換器或者多個隊列進行聯邦,一個聯邦交換器(federated exchange)或者一個聯邦隊列(federated queue)接收上游(upstream)的消息,這裏的上游時指位於其他Broker上的交換器或者隊列聯邦交換器能夠將原本發送給上游交換器(upstream exchange)的消息路由到本地的某個隊列中;聯邦隊列則允許一個本地消費者接收到來自上游隊列(upstream queue)的消息。

Shovel

與Federation具備的數據轉發功能類似,Shovel能夠可靠、持續的從一個Broker的隊列拉取數據並轉發只另一個Broker的交換器。實際上是基於AMQP協議的轉發器。
Shovel可以部署在源端也可以部署在目的端。有兩種方式可以部署Shovel:靜態方式(static)和動態方式(dynamic)。靜態方式是指在RabbitMQ.config配置文件中設置,動態方式只指通過Runtime Parameter設置。

當集羣消息堆積嚴重時,可以通過Shovel將隊列中的消息移交給另一個集羣,這是一備一的情況。如果需要一備多,可以採用鏡像隊列或者引入Federation。

Shovel工作在Federation的更低一層。監獄Federation從一個交換器中轉發消息到另一個交換器(如果有必要可以確認消息是否被轉發),Shovel只是簡單地從某個Broker上的隊列消費消息,然後轉發消息到另一個Broker上的交換器而已。Shovel也可以再一臺單獨的服務器上去轉發消息,例如將一個隊列中的數據移動到另一個隊列中。

鏡像隊列

引入鏡像隊列(Mirror Queue)的機制,可以將隊列鏡像到集羣中的其他Broker節點之上,如果集羣中的一個節點失效了,隊列能自動地切換到鏡像的另一個節點上以保證服務的可用性。每一個配置鏡像的隊列都包含一個主節點(master)和若干個從節點(slave),如果master失效,slave加入時間最長的會提升爲master。發送到鏡像隊列的所有消息會被同時發往master和其他所有的slave。除了發送消息(Basic.Publish)外所有動作都只會想master發送,然後由master將命令執行的結果廣播給各個slave。
消費者與slave建立連接消費時實質上都是從master上獲取消息,只不過看似從slave上消費而已。例如消費者與slave建立了TCP連接後執行Basic.Get操作,由slave轉發給master,再由master準備好數據返回給slave,投遞給消費者。這裏的master和slave針對隊列而言,隊列可以均勻地散落在集羣的各個Broker節點中以達到負載均衡地目的,真正的負載還是針對實際的物理機器而言,而不是內存中駐留的隊列進程。

網絡分區

網絡分區的恢復
首先選一個最信任的partition,Mnesia使用該partition中的狀態,其他partitions中發生的變化都將丟失。
停止其他partitions中的所有nodes,之後重啓這些nodes。當這些nodes重新加入cluster後將從信任的partition恢復狀態。
最後還需重啓信任的partition中的所有nodes以清除network partition的警告信息

Rabbitmq自動處理網絡分區的3種模式
RabbitMQ提供了3種自動處理network partitions的方式:默認爲ignore模式,也即需要手工處理
pause-minority mode:暫停少數模式;
pause-if-all-down mode:暫停-如果全部停止模式
autoheal mode:自動癒合模式
pause-minority mode:暫停少數模式
在pause-minority模式下,察覺其他nodes down掉後,RabbitMQ將自動暫停認爲自己是少數派的 nodes(例如小於或等於總nodes數的一半),network partition一旦發生,“少數派”的nodes將立刻暫停,直至partition結束後重新恢復。這可以保證在network partition發生時,至多隻有一個partition中的nodes繼續運行。(犧牲可用性保證一致性)
若所有分區的nodes個數都小於總nodes個數一半,則意味着所有分區的nodes都會認爲自己是少數派,即所有nodes都將暫停
pause-if-all-down mode:暫停-如果全部停止模式
http://www.rabbitmq.com/partitions.html
autoheal模式
在autoheal模式下一旦發生了partition,RabbitMQ將自動確定一個優勝partition,然後重啓所有不在優勝partition中的nodes。
獲勝的partition爲擁有最多客戶端連接的partition(若連接相同則爲節點最多的partition)。
關於自動處理partitions的設置在配置文件的cluster_partition_handling參數中進行。
各自的適用場景
network partitions自動處理並不能保證cluster不出任何問題。
一般來說可作如下選擇:
ignore:若網絡非常可靠。所有nodes在同一機架,通過交換機連接,該交換機也是通往外部網絡的出口。在cluster的某一部分故障時不希望其餘部分受影響。或者cluster只有兩個node。
pause_minority:網絡較不可靠。cluster處於EC2的3個AZ中,假定每次至多隻有其中一個AZ故障,想要剩餘的AZ繼續提供服務而故障的AZ中的nodes在AZ恢復後重新自動加入到cluster。
autoheal:網絡很不可靠。與數據完整性相比更關注服務的持續性。cluster只有兩個node。

RocketMQ

當前版本4.7.0 release(2020-6-28)
RocketMQ是一個消息與流處理平臺,具有低延時、高性能、高可靠、萬億級消息儲存以及可擴展性靈活等特性。主要由4個核心部分組成:name servers, brokers, producers and consumers,每一部分都能集羣部署避免單點故障。

集羣架構

Name Server

Name Server是一個幾乎無狀態節點,可集羣部署,節點之間無任何信息同步。提供輕量級的服務發現和路由。 每個 NameServer 記錄完整的路由信息,提供等效的讀寫服務,並支持快速存儲擴展。

Broker

Broker 通過提供輕量級的 Topic 和 Queue 機制來處理消息存儲,同時支持推(push)和拉(pull)模式以及主從結構的容錯機制。
Broker部署相對複雜,Broker分爲Master與Slave,一個Master可以對應多個Slave,但是一個Slave只能對應一個Master,Master與Slave的對應關係通過指定相同的Broker Name,不同的Broker Id來定義,BrokerId爲0表示Master,非0表示Slave。Master也可以部署多個。
每個Broker與Name Server集羣中的所有節點建立長連接,定時(每隔30s)註冊Topic信息到所有Name Server。Name Server定時(每隔10s)掃描所有存活broker的連接,如果Name Server超過2分鐘沒有收到心跳,則Name Server斷開與Broker的連接。

Producer

Producer生產者,產生消息的實例,擁有相同 Producer Group 的 Producer 組成一個集羣。
Producer與Name Server集羣中的其中一個節點(隨機選擇)建立長連接,定期從Name Server取Topic路由信息,並向提供Topic服務的Master建立長連接,且定時向Master發送心跳。Producer完全無狀態,可集羣部署。
Producer每隔30s(由ClientConfig的pollNameServerInterval)從Name server獲取所有topic隊列的最新情況,這意味着如果Broker不可用,Producer最多30s能夠感知,在此期間內發往Broker的所有消息都會失敗。
Producer每隔30s(由ClientConfig中heartbeatBrokerInterval決定)向所有關聯的broker發送心跳,Broker每隔10s中掃描所有存活的連接,如果Broker在2分鐘內沒有收到心跳數據,則關閉與Producer的連接。

Consumer

Consumer消費者,接收消息進行消費的實例,擁有相同 Consumer Group 的
Consumer 組成一個集羣。
Consumer與Name Server集羣中的其中一個節點(隨機選擇)建立長連接,定期從Name Server取Topic路由信息,並向提供Topic服務的Master、Slave建立長連接,且定時向Master、Slave發送心跳。Consumer既可以從Master訂閱消息,也可以從Slave訂閱消息,訂閱規則由Broker配置決定。
Consumer每隔30s從Name server獲取topic的最新隊列情況,這意味着Broker不可用時,Consumer最多最需要30s才能感知。
Consumer每隔30s(由ClientConfig中heartbeatBrokerInterval決定)向所有關聯的broker發送心跳,Broker每隔10s掃描所有存活的連接,若某個連接2分鐘內沒有發送心跳數據,則關閉連接;並向該Consumer Group的所有Consumer發出通知,Group內的Consumer重新分配隊列,然後繼續消費。
當Consumer得到master宕機通知後,轉向slave消費,slave不能保證master的消息100%都同步過來了,因此會有少量的消息丟失。但是一旦master恢復,未同步過去的消息會被最終消費掉。

Elasticsearch

當前版本7.8.0(2020-6-28)

集羣架構

綠色——最健康的狀態,代表所有的主分片和副本分片都可用;
黃色——所有的主分片可用,但是部分副本分片不可用;
紅色——部分主分片不可用。此時執行查詢部分數據仍然可以查到

  1. 啓動過程:
    es使用發現(discovery)模塊(根據配置文件中的集羣名稱)對網絡進行多播,找到擁有相同集羣名稱的其他節點,由一個節點被選爲主節點,負責集羣狀態以及集羣拓撲變化時做出反應,分發索引分片至集羣相應節點。對等架構決定了主(管理)節點並不重要。
    管理節點讀取集羣狀態信息,有必要會進行恢復(recovery)處理。會簡稱由哪些索引分片,決定哪些分片作爲主分片,之後集羣進入黃色狀態。集羣可以查詢,但是吞吐量以及其他情況未知(主分片就緒副本未分配)。接下來會尋找冗餘的分片作爲副本,如果某個主分片副本數量過少,管理節點將決定基於某個主分片創建分片和副本,一切順利則進入綠色狀態。
  2. 故障檢測
    管理節點會監控所有可用節點,如果任何節點再預定義的超時時間內不響應,則認爲該節點已經斷開並啓動錯誤處理過程。意味着可能要在集羣 - 分片直接重新平衡,選擇在新的主節點等。
  3. 與Elasticsearch通信
    每個es功能模塊都有一個API,主API是基於Restful。也可以使用Java API進行節點間通信。
    索引數據API:可以通過REST API,也可以通過bulk API或者 UDP bulk API一次發送多個文檔至集羣。建索引操作只會發生在主分片上,而不是副本。如果索引請求發送到副本節點,會被轉發到爭取的主分片節點,然後該節點會把索引請求羣發給所有副本,等他們響應(比如達到規定數目的副本都完成了更新時)結束索引過程。
    查詢數據API:使用查詢DSL可以做下面這些事情
  • 使用各種查詢類型,包括簡單詞項查詢、短語查詢、布爾查詢、模糊查詢、區間查詢、通配符查詢、空間查詢、以及具備人類可讀的打分控制功能的函數查詢等
  • 組合簡單查詢構建複雜查詢
  • 文檔過濾,在不影響評分的前提下拋棄那些不滿足特定查詢條件的文檔
  • 查找與特定文檔相似的文檔
  • 查找特定短語的查詢建議和拼寫檢查
  • 使用切面構建動態導航和計算各種統計量
  • 使用預搜索(prospective search)和查找與指定文檔匹配的query集合

查詢不是簡單單步驟的操作,一般分爲兩個階段:分散階段(scatter phase)和合並階段(gather phase)。在分散階段將查詢分發到包含相關文檔的多個分片中執行查詢,合併階段從衆多分片中收集返回結果,對它們進行合併、排序、進行後續處理並返回客戶端。

節點和分片(物理設計)

默認情況下每個索引由5個主要分片組成,每個主要分片有一個副本。一個分片是一個目錄中的文件,Lucene用這些文件存儲索引數據,分片也是Elasticsearch將數據從一個節點遷移到另一個節點的最小單位。
多個節點使用同樣的集羣名稱(cluster.name)啓動,可以連接集羣中的任一節點訪問完整數據集。

默認情況下,索引一篇文檔時系統首先根據文檔ID的散列值選擇一個主分片,並將文檔發送到該主分片,然後發送給該主分片的所有副本分片進行索引。搜索時就可以在主副分片之間進行負載均衡。
索引的每篇文檔都有一個ID,ID經過了散列處理。索引的每個分片有一個散列的取值範圍。索引的文檔會分發到散列範圍包含該文檔ID散列值的分片。散列的ID被稱爲路由值(routing value),將文檔分配到某個分配的過程稱爲路由(routing)。

一個分片是Lucene的索引:一個包含倒排索引的文件目錄。

選主算法

Elasticsearch可以使用廣播(broadcast)或者單播(unicast)來發現另一個節點。發現節點後會確認主節點,負責管理集羣的狀態(當前的設置和集羣中分片、索引以及節點的狀態)。會建立內部ping機制來確保每個節點在集羣中保持活躍和健康,被成爲錯誤識別。discovery.zen.minimum_master_nodes最好是節點數除以2加1,能夠防止腦裂。
Bully算法
Leader選舉的基本算法之一。 它假定所有節點都有一個惟一的ID,該ID對節點進行排序。 任何時候的當前Leader都是參與集羣的最高id節點。 該算法的優點是易於實現,但是,當擁有最大 id 的節點處於不穩定狀態的場景下會有問題,例如 Master 負載過重而假死,集羣擁有第二大id 的節點被選爲 新主,這時原來的 Master 恢復,再次被選爲新主,然後又假死…
elasticsearch 通過推遲選舉直到當前的 Master 失效來解決上述問題,但是容易產生腦裂,再通過 法定得票人數過半 解決腦裂

選主流程
只有一個 Leader將當前版本的全局集羣狀態推送到每個節點。 ZenDiscovery(默認)過程就是這樣的:
每個節點計算最低的已知節點ID,並向該節點發送領導投票
如果一個節點收到足夠多的票數,並且該節點也爲自己投票,那麼它將扮演領導者的角色,開始發佈集羣狀態。
所有節點都會參數選舉,並參與投票,但是,只有有資格成爲 master 的節點的投票纔有效.
有多少選票贏得選舉的定義就是所謂的法定人數。 在彈性搜索中,法定大小是一個可配置的參數。 (一般配置成:可以成爲master節點數n/2+1)

MongoDB

當前版本4.2.8(2020-6-28)

集羣架構

默認數據目錄是/data/db,它負責存儲所有的MongoDB的數據文件。在MongoDB內部每個數據庫都包含一個.ns文件(保存了每張表和每個索引的命名空間元數據)和一些數據文件(foo.0 foo.1 foo.2 遞增)。
MongoDB內部有預分配空間的機制,每個預分配的文件都用0填充,這樣就能始終保持額外的空間和空餘的數據文件。隨着表中數據增加,數據文件每新分配一次,大小就會是上一個數據文件的2倍,最大2G。

Replica Sets 複製集

MongoDB支持在多個機器中通過異步複製達到故障轉移和實現冗餘。多機器中同一時刻只有一臺用於寫操作。擔當Primary角色的機器能把讀操作分發給slave。
高可用分爲兩種:

  • Master-Slave主從複製
    只需要在某一個服務啓動時加上 -master,另一個服務加上 -slave與-source參數。不推薦
  • Replica Sets 複製集
    1.6版本推出了新功能複製集,比之前的replication強大,增加來故障自動切換和自動修復成員節點,各個DB之間數據完全一致。

需要創建主從key文件,用於標識集羣的私鑰完整路徑,各個實例的key file內部不一致程序不能正常使用。

Replica Set通過一個日誌來存儲寫操作,這個日誌就是oplog。oplog.rs時一個固定長度的capped collection,存在local數據庫中。Oplog 其實就像 MySQL 的 Binlog 一樣,記錄着主節點上執行的每一個操作,而 Secondary 通過複製 Oplog 並應用的方式來進行數據同步。
選擇同步源節點
Replica Sets中的節點從距離它“最近”的節點同步數據,這個“最近”是通過ping的時間來判斷的。在節點之間的心跳檢測中,會記錄ping某個節點和收到響應的時間,通過這個時間的長短,來確定距離的遠近,時間越長視爲距離越遠。知道了和節點之間的距離健康度來確定同步的源節點。
當我們在MongoDB中執行一個寫操作時,默認情況下,寫操作指令發送後,就認爲寫操作執行成功了。爲了保證系統可用性和數據安全性,我們可以更改配置,當寫操作在n個節點(n包括primary,如果n=1,那就是在primary執行成功後返回)都執行成功後,才返回成功。
我們可以通過運行db.adminCommand({replSetGetStatus:1})命令來查看當前的節點狀況,在secondary上運行這個命令的時候,能夠看到syncingTo這個字段,這個字段的值就表示secondary節點同步數據的源節點。有2個secondary或者更多,鏈式同步

Replica Sets角色

  • Primary:主服務器,只有一組,處理客戶端的請求,一般是讀寫
  • Secondary:從服務器,有多組,保存主服務器的數據副本,主服務器出問題時其中一個從服務器可提升爲新主服務器,可提供只讀服務,可設置爲允許客戶端讀,但默認情況下Secondary不允許讀,需要設置slaveok參數。
  • Hidden:一般只用於備份節點,不處理客戶端的讀請求
  • Secondary-Only:不能成爲 primary 節點,只能作爲 secondary 副本節點,防止一些性能不高的節點成爲主節點
  • Delayed:slaveDelay 來設置,爲不處理客戶端請求,一般需要隱藏
  • Non-Voting:沒有選舉權的 secondary 節點,純粹的備份數據節點。
  • Arbiter:仲裁節點,不存數據,只參與選舉,可用可不用。通常在擁有偶數個節點的複製集中添加(且僅能添加)一個Arbiter,這樣可以使一次選舉中達到大多數(majority)而避免選舉分裂(split vote)。

選主算法

pv0: 基於priority 和 optime 選舉新主,依賴clock synchronization。
有選舉權的節點,每一輪選舉最多投一票,在30s內,不能重複投票。

pv1:基於Raft協議,每個成員都有 對候選主列表成員投贊成或者反對票,不是單方面否決選舉,沒有節點投反對票,且獲得贊成票數超過有權投票節點總數的1/2,則能成爲Primary。否則進入下一輪選舉。
因使用了Raft協議,加快 back-to-back選主,減少整個選舉新主所需花費的總時間,相應的會增加WriteConcern(w:1)rollback的可能性。
Raft將時間分爲多個term,term以連續的整數來標識,每個term以一次election開始,如果有server被選爲leader,則該term的剩餘時間該server都是leader。

3.2之前(只支持pv0 協議),>= 3.2開始:支持pv0 協議、pv1協議,默認是pv1協議。

Sharding 分片

將海量的數據水平擴展的數據庫集羣系統,數據分表存儲在sharding的各個節點上。
MongoDB的數據分塊稱爲chunk,每個chunk都是collection中一段連續的數據記錄,通常最大尺寸是200MB,超出則生成新的數據塊
一個MongoDB Sharding Cluster,需要三種角色:

  • Shard Server
    存儲實際數據的分片,每個Shard可以是一個mongod實例,也可以是一組mongd實例構成的Replica Set。爲了實現每個Shard內部的auto-failover,MongoDB官方建議每個Shard爲一組Replica Set。
  • Config Server
    爲了將一個特定的collection存儲在多個shard中,需要爲該collection指定一個shard key。shard key可以決定記錄歸屬於那個chunk。Config Server就是用來存儲所有shard節點的配置信息,每個chunk的shard key範圍、chunk在各shard的分佈情況、該集羣中所有的DB和collection的sharding配置信息。
  • Route Process
    這是一個前端路由,客戶端由此接入,詢問Config Servers需要到哪個Shard上查詢或保存記錄,再連接相應的Shard操作,最後將結果返回給客戶端。客戶端值需要將原本發給mongod的查詢更新發給RoutingKey Process,而不關心操作記錄存儲在哪個Shard上。

整合 Replica Sets+Sharding

MongoDB Auto-Sharding 解決來海量存儲和動態擴容的問題,“Replica Sets+Sharding”的方案:

  • Shard:使用Replica Sets,確保每個數據節點都具有備份、自動容錯轉移、自動恢復能力
  • Config:使用3個配置服務器,確保元數據完整性
  • Route:使用3個路由進程,實現負載平衡,提高客戶端接入性能。

選主算法

Consensus一致性這個概念,它是指多個服務器在狀態達成一致,但是在一個分佈式系統中,因爲各種意外可能,有的服務器可能會崩潰或變得不可靠,它就不能和其他服務器達成一致狀態。這樣就需要一種Consensus協議,一致性協議是爲了確保容錯性,也就是即使系統中有一兩個服務器當機,也不會影響其處理過程。
  爲了以容錯方式達成一致,我們不可能要求所有服務器100%都達成一致狀態,只要超過半數的大多數服務器達成一致就可以了,假設有N臺服務器,N/2 +1 就超過半數,代表大多數了。
  Paxos和Raft都是爲了實現Consensus一致性這個目標,這個過程如同選舉一樣,參選者需要說服大多數選民(服務器)投票給他,一旦選定後就跟隨其操作。Paxos和Raft的區別在於選舉的具體過程不同。

Paxos算法原理

Paxos算法是什麼?
Paxos算法運行在允許宕機故障的異步系統中,不要求可靠的消息傳遞,可容忍消息丟失、延遲、亂序以及重複。它利用大多數 (Majority) 機制保證了2F+1的容錯能力,即2F+1個節點的系統最多允許F個節點同時出現故障。
一個或多個提議進程 (Proposer) 可以發起提案 (Proposal),Paxos算法使所有提案中的某一個提案,在所有進程中達成一致。系統中的多數派同時認可該提案,即達成了一致。最多隻針對一個確定的提案達成一致。

Paxos算法角色

Proposer: 提出提案 (Proposal)。Proposal信息包括提案編號 (Proposal ID) 和提議的值 (Value)。
Acceptor:參與決策,迴應Proposers的提案。收到Proposal後可以接受提案,若Proposal獲得多數Acceptors的接受,則稱該Proposal被批准。
Learner:不參與決策,從Proposers/Acceptors學習最新達成一致的提案(Value)

Raft算法

Raft將系統中的角色分爲領導者(Leader)、跟從者(Follower)和候選人(Candidate):
Leader:接受客戶端請求,並向Follower同步請求日誌,當日志同步到大多數節點上後告訴Follower提交日誌。
Follower:接受並持久化Leader同步的日誌,在Leader告之日誌可以提交之後,提交日誌。
Candidate:Leader選舉過程中的臨時角色。

Raft算法原理

Raft 使用心跳(heartbeat)觸發Leader選舉。當服務器啓動時,初始化爲Follower。Leader向所有Followers週期性發送heartbeat。如果Follower在選舉超時時間內沒有收到Leader的heartbeat,就會等待一段隨機的時間後發起一次Leader選舉。
Follower將其當前term加一然後轉換爲Candidate。它首先給自己投票並且給集羣中的其他服務器發送 RequestVote RPC (RPC細節參見八、Raft算法總結)。結果有以下三種情況:

  • 贏得了多數的選票,成功選舉爲Leader;
  • 收到了Leader的消息,表示有其它服務器已經搶先當選了Leader;
  • 沒有服務器贏得多數的選票,Leader選舉失敗,等待選舉時間超時後發起下一次選舉。

Nocos

Nacos中的服務註冊數據被設計爲五層結構,包括Namespace、Group、Service、Cluster、Instance。
NACOS選舉機制的底層原理是RAFT共識算法,NACOS沒有依賴諸如zookeeper之類的第三方庫,而是自實現了一套RAFT算法。
相較於大名鼎鼎的Paxos算法,RAFT算法最突出的優勢就是易於理解,學習起來很輕鬆。
在RAFT算法領域中,有三種基本的狀態(角色):follower、candidate、leader。
處於follower狀態的server不會發起任何的request,只是被動的響應leader和candidate。
處於leader狀態的server會主動的發送心跳包給各個follower,並且接收client所有的request。
而candidate是一種過渡狀態,只有整個cluster在進行新的選舉的時候,纔會出現此種狀態的server。

註冊中心選型

C是所有節點在同一時間看到的數據是一致的;
A的定義是所有請求都會收到響應。

1 eureka AP
eureka 保證了可用性,實現最終一致性。
Eureka各個節點都是平等的,幾個節點掛掉不會影響正常節點的工作,剩餘的節點依然可以提供註冊和查詢服務。而Eureka的客戶端在向某個Eureka註冊或時如果發現連接失敗,則會自動切換至其它節點,只要有一臺Eureka還在,就能保證註冊服務可用(保證可用性),只不過查到的信息可能不是最新的(不保證強一致性),其中說明了,eureka是不滿足強一致性,但還是會保證最終一致性
2 zookeeper CP
zookeeper在選舉leader時,會停止服務,直到選舉成功之後纔會再次對外提供服務,這個時候就說明了服務不可用,但是在選舉成功之後,因爲一主多從的結構,zookeeper在這時還是一個高可用註冊中心,只是在優先保證一致性的前提下,zookeeper纔會顧及到可用性
選型依據:
在粗粒度分佈式鎖,分佈式選主,主備高可用切換等不需要高 TPS 支持的場景下有不可替代的作用,而這些需求往往多集中在大數據、離線任務等相關的業務領域,因爲大數據領域,講究分割數據集,並且大部分時間分任務多進程 / 線程並行處理這些數據集,但是總是有一些點上需要將這些任務和進程統一協調,這時候就是 ZooKeeper 發揮巨大作用的用武之地。
但是在交易場景交易鏈路上,在主業務數據存取,大規模服務發現、大規模健康監測等方面有天然的短板,應該竭力避免在這些場景下引入 ZooKeeper,在阿里巴巴的生產實踐中,應用對 ZooKeeper 申請使用的時候要進行嚴格的場景、容量、SLA 需求的評估。
所以可以使用 ZooKeeper,但是大數據請向左,而交易則向右,分佈式協調向左,服務發現向右。

配置中心過程

簡單總結一下剛剛分析的整個過程。

  • 客戶端發起長輪訓請求,
  • 服務端收到請求以後,先比較服務端緩存中的數據是否相同,如果不通,則直接返回
  • 如果相同,則通過schedule延遲29.5s之後再執行比較
  • 爲了保證當服務端在29.5s之內發生數據變化能夠及時通知給客戶端,服務端採用事件訂閱的方式來監聽服務端本地數據變化的事件,一旦收到事件,則觸發DataChangeTask的通知,並且遍歷allStubs隊列中的ClientLongPolling,把結果寫回到客戶端,就完成了一次數據的推送
  • 如果 DataChangeTask 任務完成了數據的 “推送” 之後,ClientLongPolling 中的調度任務又開始執行了怎麼辦呢?很簡單,只要在進行 “推送” 操作之前,先將原來等待執行的調度任務取消掉就可以了,這樣就防止了推送操作寫完響應數據之後,調度任務又去寫響應數據,這時肯定會報錯的。所以,在ClientLongPolling方法中,最開始的一個步驟就是刪除訂閱事件

所以總的來說,Nacos採用推+拉的形式,來解決最開始關於長輪訓時間間隔的問題。當然,30s這個時間是可以設置的,而之所以定30s,應該是一個經驗值。

服務如何發現

服務註冊到註冊中心後,服務的消費者就可以進行服務發現的流程了,消費者可以直接向註冊中心發送獲取某個服務實例的請求,這種情況下注冊中心將返回所有可用的服務實例給消費者,但是一般不推薦這種情況。另一種方法就是服務的消費者向註冊中心訂閱某個服務,並提交一個監聽器,當註冊中心中服務發生變更時,監聽器會收到通知,這時消費者更新本地的服務實例列表,以保證所有的服務均是可用的。
Nacos 客戶端進行服務註冊有兩個部分組成,一個是將服務信息註冊到服務端,另一個是像服務端發送心跳包,這兩個操作都是通過 NamingProxy 和服務端進行數據交互的。
Nacos 客戶端進行服務訂閱時也有兩部分組成,一個是不斷從服務端查詢可用服務實例的定時任務,另一個是不斷從已變服務隊列中取出服務並通知 EventListener 持有者的定時任務。

HostReactor#getServiceInfo
維護了一個serviceInfoMap,顧名思義,維護了serverList的信息,key值是serverName,value是ServiceInfo;類中還有一個定時任務ScheduledExecutorService
getServiceInfo()方法主要邏輯是:
1、先從已經存在的serviceInfoMap中通過serverName獲取一個ServiceInfo,如果已經有了,需要再判斷,另一個updatingMap是否存在這個key,如果存在,在wait 5秒,這個時間是寫死的。在返回ServiceInfo前,調用scheduleUpdateIfAbsent()方法更新。
2、如果上面第一步serviceInfoMap不存在,則將傳來的參數(erviceName, clusters, env)構建一個ServiceInfo,同時維護到serviceInfoMap和updatingMap中,同時根據allIPs這個參數的不同(我斷點時爲false)調用不同的接口去Nacos服務端拉取數據,通過方法updateService4AllIPNow 和 updateServiceNow,最後與上一步一樣,調用scheduleUpdateIfAbsent方法。
3、scheduleUpdateIfAbsent方法,維護另一個map --futureMap

1.客戶端組裝自己的服務信息,然後向服務端發起註冊請求
2.服務端接收到註冊請求後,將服務信息加入到本地緩存,並且加入定時,不斷的檢測服務是否健康,更新或刪除註冊的服務
3.客戶端會在啓動時,spring創建feign bean的時候會去從Nacos服務端獲取service信息,並且加入定時,不斷的拉取feign對應模塊的服務信息
4.客戶端在通過feign調用的時候,會通過負載均衡算法,從本地緩存中選擇一個服務進行調用
5.服務端接收到發現請求時,會根據條件從服務端本地緩存中獲取對應的實例,封裝好返回給客戶端

分佈式系統之Quorum機制

  1. Quorum介紹
    1.1 Write all read one(WARO)
    WARO的意思是:在更新時寫所有副本,只有在所有副本中更新成功纔算成功 ,保證了所有副本中數據的一致性,讀取時可以讀任意副本數據。
    1.2 Quorum機制
    對WARO條件進行鬆弛,對讀寫服務可用性做折中。
    1.3 增強Quorum機制(讀取最新成功提交數據)
    爲了能夠保證系統的強一致性,系統應該返回最新成功提交數據,需要對Quorum機制進行條件增強。
  2. 基於Quorum機制選擇primary
    當Quorum機制與Primary-Secondary協議結合時,可以通過讀取primary的方式讀取最新成功提交的數據。
    在primary-secondary協議中,primary負責進行更新同步操作。在primary-secondary協議中引入Quorum機制,primary成功更新W個節點(含本身)後向用戶返回成功。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章