前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >腾讯云MongoDB多机房部署场景下就近访问原理详解

腾讯云MongoDB多机房部署场景下就近访问原理详解

作者头像
腾讯数据库技术
发布2019-08-29 11:10:28
4.7K1
发布2019-08-29 11:10:28
举报

提示:公众号展示代码会自动折行,建议横屏阅读

多机房容灾是存储系统领域非常重要的课题。本文将从内核代码层面,介绍腾讯云MongoDB数据库系统(CMongo)在多机房部署场景下,如何实现业务到机房的就近访问,并保证数据一致性。

1. 背景介绍

为了保证服务可用性和数据可靠性,一些重要业务会将存储系统部署在多地域多机房。比如在北京,上海,深圳 每个地域的机房各存储一份数据副本,保证即使某个地域的机房无法提供访问,也不会影响业务的正常使用。

在多机房部署时,需要考虑多机房之间的网络延迟问题。以作者的ping测试结果为例,上海<-->深圳的网络延迟约为30ms,而在一个机房内部,网络延迟仅在0.1ms左右。

腾讯云MongoDB在架构上,结合L5就近接入以及内部的“nearest”访问模式,实现了业务对机房的就近访问,避免了多机房带来的网络延迟问题。整体架构如下图所示,其中mongos为接入节点,可以理解为proxy;mongod为存储节点,存储用户的实际数据,并通过 1 Primary 多 Secondary的模式形成多个副本,分散到多个机房中

下面主要对腾讯云MongoDB中nearest模式的实现和使用方式做详细介绍。

2. 什么是nearest访问模式

2.1 副本集概念

MongoDB中,副本集 是指保存相同数据的多个副本节点的集合。用户可以通过Driver直接访问副本集,或者通过mongos访问副本集。如下图所示

副本集内部通过raft算法来选主,通过oplog同步数据。

2.2 读写分离和readPreference

MongoDB默认读写都在Primary节点上执行,但是也提供了接口进行读写请求分离,充分发挥系统的整体性能。读写分离的关键在于设置请求的readPreference。这个参数标识了用户希望读取哪种节点,目前可配置的类型共5种,如下所示

2.3 读写一致性保证

有些读者可能已经产生了疑问:如果Secondary节点从Primary同步数据可能存在延迟,如何保证在从节点能够读取到刚刚写入的数据?解决方法是:设置写入操作的WriteConcern,保证数据写入到全部节点之后再返回,此时再去从节点,肯定可以读取到最新的数据。

写操作是需要跨机房同步数据的,所以如果业务模型是写多读少,需要谨慎考虑。

3. nearest实现原理解析

如果业务通过mongos接入(腾讯云MongoDB架构常用方式),则mongos侧来完成到mongod的就近访问。如果业务直接接入副本集,则在driver层会完成到mongod的就近访问。

下面会结合mongos(腾讯云MongoDB代码),mgo-driver,以及官方最新发布的go-driver,来分析如何实现nearest访问,并给出一些使用上的建议。

3.1 mongos代码分析

延迟信息采集

mongos 每隔5秒会对集群中的每个副本集启动探测线程,执行 isMaster命令并采集自己到每个节点的网络延迟情况,采集方式如下所示:

代码语言:javascript
复制
try {    ScopedDbConnection conn(ConnectionString(ns.host), socketTimeoutSecs);    bool ignoredOutParam = false;    Timer timer;    //开始计时    if (conn->isStillConnected()) {        conn->isMaster(ignoredOutParam, &reply);    //执行isMaster命令获取副本集状态    } else {        log() << "Connection to " << ns.host.toString() << " is closed";        reply = BSONObj();    }    pingMicros = timer.micros();    //统计本次耗时    conn.done();} catch (const DBException& ex) {    ...}

然后根据本次采集的延迟进行平滑更新, 核心如下所示:

代码语言:javascript
复制
if (reply.latencyMicros >= 0) {    if (latencyMicros == unknownLatency) {        latencyMicros = reply.latencyMicros;    //第一次更新,直接赋值    } else {        // update latency with smoothed moving average (1/4th the delta)        latencyMicros += (reply.latencyMicros - latencyMicros) / 4; //根据当前延迟和历史统计,进行平滑更新    }}

nearest节点选取

节点选取的算法,可以参考SetState::getMatchingHost方法。大致的选取流程为:按照每个节点的延迟升序排序 -> 排除延迟太高的节点(比最近节点的延迟大15ms)-> 随机返回一个符合条件的节点。

代码语言:javascript
复制
case ReadPreference::SecondaryOnly:case ReadPreference::Nearest: {    BSONForEach(tagElem, criteria.tags.getTagBSON()) {        uassert(16358, "Tags should be a BSON object", tagElem.isABSONObj());        BSONObj tag = tagElem.Obj();
        std::vector<const Node*> matchingNodes;        for (size_t i = 0; i < nodes.size(); i++) {    // 如果是SecondaryOnly模式,需要进行过滤            if (nodes[i].matches(criteria.pref) && nodes[i].matches(tag)) {                matchingNodes.push_back(&nodes[i]);            }        }
        // don't do more complicated selection if not needed        if (matchingNodes.empty())            continue;        if (matchingNodes.size() == 1)            return matchingNodes.front()->host;
        // order by latency and don't consider hosts further than a threshold from the        // closest.        // 对候选节点按延迟进行排序        std::sort(matchingNodes.begin(), matchingNodes.end(), compareLatencies);        for (size_t i = 1; i < matchingNodes.size(); i++) {            int64_t distance =                matchingNodes[i]->latencyMicros - matchingNodes[0]->latencyMicros;            if (distance >= latencyThresholdMicros) {                // this node and all remaining ones are too far away        // 剔除延迟超过阈值(默认15ms,可配置)的节点                matchingNodes.erase(matchingNodes.begin() + i, matchingNodes.end());                break;            }        }
        // of the remaining nodes, pick one at random (or use round-robin)        if (ReplicaSetMonitor::useDeterministicHostSelection) {            // only in tests            return matchingNodes[roundRobin++ % matchingNodes.size()]->host;        } else {            // normal case            // 从剩余的候选节点中,随机选取一个返回            return matchingNodes[rand.nextInt32(matchingNodes.size())]->host;        };    }
    return HostAndPort();}

使用建议

可以注意到mongos代码中有一个 默认的15ms配置,含义为:如果有一个节点的延迟比最近节点的延迟还要大15ms,则认为这个节点不应该被nearest策略选中。但是15ms并不是对每一个业务都合理。如果业务对延迟非常敏感,可以根据自己的需要来进行设置方法是在mongos配置文件中添加下面配置选项:

代码语言:javascript
复制
replication:   localPingThresholdMs: <int>

3.2 mgo driver代码分析

延迟信息采集

mgo driver 每隔15秒会通过 ping命令采集自己到mongod节点的网络延迟状况,并将最近6次采集结果的最大值作为当前网络延迟的参考值。代码如下所示:

代码语言:javascript
复制
for {    if loop {        time.Sleep(delay)   // 每隔一段时间(默认15秒)采集一次    }    op := op    socket, _, err := server.AcquireSocket(0, delay)    if err == nil {        start := time.Now()        _, _ = socket.SimpleQuery(&op)  // 执行ping命令        delay := time.Now().Sub(start)  // 并统计耗时
        server.pingWindow[server.pingIndex] = delay        server.pingIndex = (server.pingIndex + 1) % len(server.pingWindow)        server.pingCount++        var max time.Duration        for i := 0; i < len(server.pingWindow) && uint32(i) < server.pingCount; i++ {            if server.pingWindow[i] > max {                max = server.pingWindow[i]  // 统计最近6次(默认)采集的最大值            }        }        socket.Release()        server.Lock()        if server.closed {            loop = false        }        server.pingValue = max  // 将最大值作为网络延迟统计,作为后续选择节点时的评估依据        server.Unlock()        logf("Ping for %s is %d ms", server.Addr, max/time.Millisecond)    } else if err == errServerClosed {        return    }    if !loop {        return    }}

nearest节点选取

和mongos相同,会排除延迟太高(>15ms)的节点。但是区别在于不是随机返回一个满足条件的节点,而是尽量返回当前压力比较小的节点(通过当前使用的连接数来判定),这样可以尽量做到负载均衡。代码如下所示:

代码语言:javascript
复制
// BestFit returns the best guess of what would be the most interesting// server to perform operations on at this point in time.func (servers *mongoServers) BestFit(mode Mode, serverTags []bson.D) *mongoServer {    var best *mongoServer    for _, next := range servers.slice {    // 遍历每一个候选节点        if best == nil {            best = next            best.RLock()            if serverTags != nil && !next.info.Mongos && !best.hasTags(serverTags) {                best.RUnlock()                best = nil            }            continue        }        next.RLock()        swap := false        switch {        case serverTags != nil && !next.info.Mongos && !next.hasTags(serverTags):            // Must have requested tags.        case next.info.Master != best.info.Master && mode != Nearest:            // Prefer slaves, unless the mode is PrimaryPreferred.            swap = (mode == PrimaryPreferred) != best.info.Master        case absDuration(next.pingValue-best.pingValue) > 15*time.Millisecond:            // Prefer nearest server.            // 如果找到一个明显更近(默认15ms为判断依据)的节点,则用更近的            swap = next.pingValue < best.pingValue        case len(next.liveSockets)-len(next.unusedSockets) < len(best.liveSockets)-len(best.unusedSockets):            // Prefer servers with less connections.            // 如果延迟没有明显差距,则用当前连接数比较小的节点            swap = true        }        if swap {            best.RUnlock()            best = next        } else {            next.RUnlock()        }    }    if best != nil {        best.RUnlock()    }    return best}

3.3 官方go driver代码分析

延迟信息采集

官方go driver 每隔10秒会通过 isMaster命令采集自己到mongod节点的网络延迟状况:

代码语言:javascript
复制
now := time.Now()    // 开始统计耗时
// 去对应的节点上执行isMaster命令isMasterCmd := &command.IsMaster{Compressors: s.cfg.compressionOpts}isMaster, err := isMasterCmd.RoundTrip(ctx, conn)
...
delay := time.Since(now)    // 得到耗时统计desc = description.NewServer(s.address, isMaster).SetAverageRTT(s.updateAverageRTT(delay))    // 进行平滑统计

采集完成后,会结合历史数据进行平滑统计,如下:

代码语言:javascript
复制
func (s *Server) updateAverageRTT(delay time.Duration) time.Duration {    if !s.averageRTTSet {        s.averageRTT = delay    // 如果是第一次统计,则直接赋值    } else {        alpha := 0.2        // 进行平滑处理,新数据和历史数据的比重为 1 : 4        s.averageRTT = time.Duration(alpha*float64(delay) + (1-alpha)*float64(s.averageRTT))    }    return s.averageRTT}

nearest节点选取

以Find命令为例,go driver会生成一个 复合选择器,复合选择器会依次执行各项选择算法,得到一个候选节点列表:

代码语言:javascript
复制
readSelect := description.CompositeSelector([]description.ServerSelector{  // 复合选择器    description.ReadPrefSelector(rp),   // 1.根据readPreference设置,选择主从节点    description.LatencySelector(db.client.localThreshold),  //2.根据延迟选择最优节点})

其中对于节点延迟的选择主要依赖于 LatencySelector。大致流程为:统计到所有节点的最小延迟min-->计算延迟满足标准:min+15ms(默认)-->返回所有满足延迟标准的节点列表。核心代码如下:

代码语言:javascript
复制
func (ls *latencySelector) SelectServer(t Topology, candidates []Server) ([]Server, error) {    if ls.latency < 0 {        return candidates, nil    }
    switch len(candidates) {    case 0, 1:        return candidates, nil    default:        min := time.Duration(math.MaxInt64)        for _, candidate := range candidates {            if candidate.AverageRTTSet {    // 计算所有候选节点的最小延迟                if candidate.AverageRTT < min {                    min = candidate.AverageRTT                }            }        }
        if min == math.MaxInt64 {            return candidates, nil        }        // 用最小延迟加阈值配置(默认15ms)作为最大容忍延迟        max := min + ls.latency
        var result []Server        for _, candidate := range candidates {            if candidate.AverageRTTSet {                if candidate.AverageRTT <= max {                    // 返回所有符合延迟标准(最大容忍延迟)的节点                    result = append(result, candidate)                }            }        }
        return result, nil    }}

最后根据选择得到的候选列表,随机返回一个正常节点作为目标节点。核心代码如下:

代码语言:javascript
复制
for {    // 根据前面介绍的“复合选择器”,得到候选节点列表    suitable, err := t.selectServer(ctx, sub.C, ss, ssTimeoutCh)    if err != nil {        return nil, err    }
    // 随机选择一个作为目标节点    selected := suitable[rand.Intn(len(suitable))]    selectedS, err := t.FindServer(selected)    switch {    case err != nil:        return nil, err    case selectedS != nil:        return selectedS, nil    default:        // We don't have an actual server for the provided description.        // This could happen for a number of reasons, including that the        // server has since stopped being a part of this topology, or that        // the server selector returned no suitable servers.    }}

使用建议

关于上述15ms的默认配置,官方go driver也提供了设置接口。对于延迟敏感的业务,可以通过这个接口配置ClientOptions,降低阈值。

4. 总结

MongoDB通过nearest模式支持多机房部署场景中客户端driver->mongod以及mongos->mongod的就近读。本文结合腾讯云MongoDB内核代码和常用的go driver代码对nearest的原理进行分析,并给出了一些使用建议。


腾讯数据库技术团队对内支持QQ空间、微信红包、腾讯广告、腾讯音乐、腾讯新闻等公司自研业务,对外在腾讯云上支持TencentDB相关产品,如CynosDB、CDB、CTSDB、CMongo等。腾讯数据库技术团队专注于持续优化数据库内核和架构能力,提升数据库性能和稳定性,为腾讯自研业务和腾讯云客户提供“省心、放心”的数据库服务。此公众号和广大数据库技术爱好者一起,推广和分享数据库领域专业知识,希望对大家有所帮助。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-08-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯数据库技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 提示:公众号展示代码会自动折行,建议横屏阅读
  • 1. 背景介绍
  • 2. 什么是nearest访问模式
    • 2.1 副本集概念
      • 2.2 读写分离和readPreference
      • 3. nearest实现原理解析
        • 3.1 mongos代码分析
          • 延迟信息采集
          • nearest节点选取
          • 使用建议
        • 3.2 mgo driver代码分析
          • 延迟信息采集
          • nearest节点选取
        • 3.3 官方go driver代码分析
          • 延迟信息采集
          • nearest节点选取
          • 使用建议
      • 4. 总结
      相关产品与服务
      云数据库 MongoDB
      腾讯云数据库 MongoDB(TencentDB for MongoDB)是腾讯云基于全球广受欢迎的 MongoDB 打造的高性能 NoSQL 数据库,100%完全兼容 MongoDB 协议,支持跨文档事务,提供稳定丰富的监控管理,弹性可扩展、自动容灾,适用于文档型数据库场景,您无需自建灾备体系及控制管理系统。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档