天天看點

騰訊雲MongoDB多機房部署場景下就近通路原理詳解

背景介紹

為了保證服務可用性和資料可靠性,一些重要業務會将存儲系統部署在多地域多機房。比如在北京,上海,深圳 每個地域的機房各存儲一份資料副本,保證即使某個地域的機房無法提供通路,也不會影響業務的正常使用。

在多機房部署時,需要考慮多機房之間的網絡延遲問題。以作者的ping測試結果為例,上海<-->深圳的網絡延遲約為30ms,而在一個機房内部,網絡延遲僅在0.1ms左右。

騰訊雲MongoDB在架構上,結合L5就近接入以及内部的“nearest”通路模式,實作了業務對機房的就近通路,避免了多機房帶來的網絡延遲問題。整體架構如下圖所示,其中mongos為接入節點,可以了解為proxy;mongod為存儲節點,存儲使用者的實際資料,并通過 1 Primary 多 Secondary的模式形成多個副本,分散到多個機房中

騰訊雲MongoDB多機房部署場景下就近通路原理詳解

下面主要對騰訊雲MongoDB中nearest模式的實作和使用方式做詳細介紹。

2. 什麼是nearest通路模式

MongoDB中,副本集 是指儲存相同資料的多個副本節點的集合。使用者可以通過Driver直接通路副本集,或者通過mongos通路副本集。如下圖所示

騰訊雲MongoDB多機房部署場景下就近通路原理詳解

副本集内部通過raft算法來選主,通過oplog同步資料。

MongoDB預設讀寫都在Primary節點上執行,但是也提供了接口進行讀寫請求分離,充分發揮系統的整體性能。讀寫分離的關鍵在于設定請求的readPreference。這個參數辨別了使用者希望讀取哪種節點,目前可配置的類型共5種,如下所示

2.3 讀寫一緻性保證

有些讀者可能已經産生了疑問:如果Secondary節點從Primary同步資料可能存在延遲,如何保證在從節點能夠讀取到剛剛寫入的資料?解決方法是:設定寫入操作的WriteConcern,保證資料寫入到全部節點之後再傳回,此時再去從節點,肯定可以讀取到最新的資料。

寫操作是需要跨機房同步資料的,是以如果業務模型是寫多讀少,需要謹慎考慮。

3. nearest實作原了解析

如果業務通過mongos接入(騰訊雲MongoDB架構常用方式),則mongos側來完成到mongod的就近通路。如果業務直接接入副本集,則在driver層會完成到mongod的就近通路。

下面會結合mongos(騰訊雲MongoDB代碼),mgo-driver,以及官方最新釋出的go-driver,來分析如何實作nearest通路,并給出一些使用上的建議。

mongos <code>每隔5秒</code>會對叢集中的每個副本集啟動探測線程,執行 <code>isMaster指令</code>并采集自己到每個節點的網絡延遲情況,采集方式如下所示:

然後根據本次采集的延遲進行平滑更新, 核心如下所示:

節點選取的算法,可以參考SetState::getMatchingHost方法。大緻的選取流程為:按照每個節點的延遲升序排序 -&gt; 排除延遲太高的節點(比最近節點的延遲大15ms)-&gt; 随機傳回一個符合條件的節點。

<code>case ReadPreference::SecondaryOnly:</code>

<code>case ReadPreference::Nearest: {</code>

<code>   BSONForEach(tagElem, criteria.tags.getTagBSON()) {</code>

<code>       uassert(16358, "Tags should be a BSON object", tagElem.isABSONObj());</code>

<code>       BSONObj tag = tagElem.Obj();</code>

<code>       std::vector&lt;const Node*&gt; matchingNodes;</code>

<code>       for (size_t i = 0; i &lt; nodes.size(); i++) {</code>

<code>   // 如果是SecondaryOnly模式,需要進行過濾</code>

<code>           if (nodes[i].matches(criteria.pref) &amp;&amp; nodes[i].matches(tag)) {</code>

<code>               matchingNodes.push_back(&amp;nodes[i]);</code>

<code>           }</code>

<code>       }</code>

<code>       // don't do more complicated selection if not needed</code>

<code>       if (matchingNodes.empty())</code>

<code>           continue;</code>

<code>       if (matchingNodes.size() == 1)</code>

<code>           return matchingNodes.front()-&gt;host;</code>

<code>       // order by latency and don't consider hosts further than a threshold from the</code>

<code>       // closest.</code>

<code>       // 對候選節點按延遲進行排序</code>

<code>       std::sort(matchingNodes.begin(), matchingNodes.end(), compareLatencies);</code>

<code>       for (size_t i = 1; i &lt; matchingNodes.size(); i++) {</code>

<code>           int64_t distance =</code>

<code>               matchingNodes[i]-&gt;latencyMicros - matchingNodes[0]-&gt;latencyMicros;</code>

<code>           if (distance &gt;= latencyThresholdMicros) {</code>

<code>               // this node and all remaining ones are too far away</code>

<code>       // 剔除延遲超過門檻值(預設15ms,可配置)的節點</code>

<code>               matchingNodes.erase(matchingNodes.begin() + i, matchingNodes.end());</code>

<code>               break;</code>

<code>       // of the remaining nodes, pick one at random (or use round-robin)</code>

<code>       if (ReplicaSetMonitor::useDeterministicHostSelection) {</code>

<code>           // only in tests</code>

<code>           return matchingNodes[roundRobin++ % matchingNodes.size()]-&gt;host;</code>

<code>       } else {</code>

<code>           // normal case</code>

<code>           // 從剩餘的候選節點中,随機選取一個傳回</code>

<code>           return matchingNodes[rand.nextInt32(matchingNodes.size())]-&gt;host;</code>

<code>       };</code>

<code>   }</code>

<code>   return HostAndPort();</code>

<code>}</code>

可以注意到mongos代碼中有一個 <code>預設的15ms配置</code>,含義為:如果有一個節點的延遲比最近節點的延遲還要大15ms,則認為這個節點不應該被nearest政策選中。但是15ms并不是對每一個業務都合理。如果業務對延遲非常敏感,可以根據自己的需要來進行設定方法是在mongos配置檔案中添加下面配置選項:

mgo driver <code>每隔15秒</code>會通過 <code>ping指令</code>采集自己到mongod節點的網絡延遲狀況,并将最近6次采集結果的最大值作為目前網絡延遲的參考值。代碼如下所示:

<code>for {</code>

<code>   if loop {</code>

<code>       time.Sleep(delay)   // 每隔一段時間(預設15秒)采集一次</code>

<code>   op := op</code>

<code>   socket, _, err := server.AcquireSocket(0, delay)</code>

<code>   if err == nil {</code>

<code>       start := time.Now()</code>

<code>       _, _ = socket.SimpleQuery(&amp;op)  // 執行ping指令</code>

<code>       delay := time.Now().Sub(start)  // 并統計耗時</code>

<code>       server.pingWindow[server.pingIndex] = delay</code>

<code>       server.pingIndex = (server.pingIndex + 1) % len(server.pingWindow)</code>

<code>       server.pingCount++</code>

<code>       var max time.Duration</code>

<code>       for i := 0; i &lt; len(server.pingWindow) &amp;&amp; uint32(i) &lt; server.pingCount; i++ {</code>

<code>           if server.pingWindow[i] &gt; max {</code>

<code>               max = server.pingWindow[i]  // 統計最近6次(預設)采集的最大值</code>

<code>       socket.Release()</code>

<code>       server.Lock()</code>

<code>       if server.closed {</code>

<code>           loop = false</code>

<code>       server.pingValue = max  // 将最大值作為網絡延遲統計,作為後續選擇節點時的評估依據</code>

<code>       server.Unlock()</code>

<code>       logf("Ping for %s is %d ms", server.Addr, max/time.Millisecond)</code>

<code>   } else if err == errServerClosed {</code>

<code>       return</code>

<code>   if !loop {</code>

和mongos相同,會排除延遲太高(&gt;15ms)的節點。但是差別在于不是随機傳回一個滿足條件的節點,而是盡量傳回目前壓力比較小的節點(通過目前使用的連接配接數來判定),這樣可以盡量做到負載均衡。代碼如下所示:

官方go driver <code>每隔10秒</code>會通過 <code>isMaster</code>指令采集自己到mongod節點的網絡延遲狀況:

<code>now := time.Now()    // 開始統計耗時</code>

<code>// 去對應的節點上執行isMaster指令</code>

<code>isMasterCmd := &amp;command.IsMaster{Compressors: s.cfg.compressionOpts}</code>

<code>isMaster, err := isMasterCmd.RoundTrip(ctx, conn)</code>

<code>...</code>

<code>delay := time.Since(now)    // 得到耗時統計</code>

<code>desc = description.NewServer(s.address, isMaster).SetAverageRTT(s.updateAverageRTT(delay))    // 進行平滑統計</code>

采集完成後,會結合曆史資料進行平滑統計,如下:

以Find指令為例,go driver會生成一個 <code>複合選擇器</code>,複合選擇器會依次執行各項選擇算法,得到一個候選節點清單:

其中對于節點延遲的選擇主要依賴于 <code>LatencySelector</code>。大緻流程為:統計到所有節點的最小延遲min--&gt;計算延遲滿足标準:min+15ms(預設)--&gt;傳回所有滿足延遲标準的節點清單。核心代碼如下:

<code>func (ls *latencySelector) SelectServer(t Topology, candidates []Server) ([]Server, error) {</code>

<code>   if ls.latency &lt; 0 {</code>

<code>       return candidates, nil</code>

<code>   switch len(candidates) {</code>

<code>   case 0, 1:</code>

<code>   default:</code>

<code>       min := time.Duration(math.MaxInt64)</code>

<code>       for _, candidate := range candidates {</code>

<code>           if candidate.AverageRTTSet {    // 計算所有候選節點的最小延遲</code>

<code>               if candidate.AverageRTT &lt; min {</code>

<code>                   min = candidate.AverageRTT</code>

<code>               }</code>

<code>       if min == math.MaxInt64 {</code>

<code>           return candidates, nil</code>

<code>       // 用最小延遲加門檻值配置(預設15ms)作為最大容忍延遲</code>

<code>       max := min + ls.latency</code>

<code>       var result []Server</code>

<code>           if candidate.AverageRTTSet {</code>

<code>               if candidate.AverageRTT &lt;= max {</code>

<code>                   // 傳回所有符合延遲标準(最大容忍延遲)的節點</code>

<code>                   result = append(result, candidate)</code>

<code>       return result, nil</code>

最後根據選擇得到的候選清單,随機傳回一個正常節點作為目标節點。核心代碼如下:

<code>   // 根據前面介紹的“複合選擇器”,得到候選節點清單</code>

<code>   suitable, err := t.selectServer(ctx, sub.C, ss, ssTimeoutCh)</code>

<code>   if err != nil {</code>

<code>       return nil, err</code>

<code>   // 随機選擇一個作為目标節點</code>

<code>   selected := suitable[rand.Intn(len(suitable))]</code>

<code>   selectedS, err := t.FindServer(selected)</code>

<code>   switch {</code>

<code>   case err != nil:</code>

<code>   case selectedS != nil:</code>

<code>       return selectedS, nil</code>

<code>       // We don't have an actual server for the provided description.</code>

<code>       // This could happen for a number of reasons, including that the</code>

<code>       // server has since stopped being a part of this topology, or that</code>

<code>       // the server selector returned no suitable servers.</code>

關于上述15ms的預設配置,官方go driver也提供了設定接口。對于延遲敏感的業務,可以通過這個接口配置ClientOptions,降低門檻值。

4. 總結

MongoDB通過nearest模式支援多機房部署場景中用戶端driver-&gt;mongod以及mongos-&gt;mongod的就近讀。本文結合騰訊雲MongoDB核心代碼和常用的go driver代碼對nearest的原理進行分析,并給出了一些使用建議。