Zookeeper 通知更新可靠吗? 解读源码找答案!

2023-11-08

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~

本文由特鲁门发表于云+社区专栏

导读:

遇到Keepper通知更新无法收到的问题,思考节点变更通知的可靠性,通过阅读源码解析了解到zk Watch的注册以及触发的机制,本地调试运行模拟zk更新的不可靠的场景以及得出相应的解决方案。

过程很曲折,但问题的根本原因也水落石出了,本文最后陈述了更新无法收到的根本原因,希望对其他人有所帮助。-----------------------------------------

通常Zookeeper是作为配置存储、分布式锁等功能被使用,配置读取如果每一次都是去Zookeeper server读取效率是非常低的,幸好Zookeeper提供节点更新的通知机制,只需要对节点设置Watch监听,节点的任何更新都会以通知的方式发送到Client端。

img

如上图所示:应用Client通常会连接上某个ZkServer,forPath不仅仅会读取Zk 节点zkNode的数据(通常存储读取到的数据会存储在应用内存中,例如图中Value),而且会设置一个Watch,当zkNode节点有任何更新时,ZkServer会发送notify,Client运行Watch来才走出相应的事件相应。这里假设操作为更新Client本地的数据。这样的模型使得配置异步更新到Client中,而无需Client每次都远程读取,大大提高了读的性能,(图中的re-regist重新注册是因为对节点的监听是一次性的,每一次通知完后,需要重新注册)。但这个Notify是可靠的吗?如果通知失败,那岂不是Client永远都读取的本地的未更新的值?

由于现网环境定位此类问题比较困难,因此本地下载源码并模拟运行ZkServer & ZkClient来看通知的发送情况。


1、git 下载源码 https://github.com/apache/zookeeper

2、cd 到路径下,运行ant eclipse 加载工程的依赖。

3、导入Idea中。

https://stackoverflow.com/questions/43964547/how-to-import-zookeeper-source-code-to-idea

查看相关问题和步骤。

首先运行ZkServer。QuorumPeerMain是Server的启动类。这个可以根据bin下ZkServer.sh找到入口。注意启动参数配置参数文件,指定例如启动端口等相关参数。

img

在此之前,需要设置相关的断点。

首先我们要看client设置监听后,server是如何处理的

ZkClient 是使用Nio的方式与ZkServer进行通信的,Zookeeper的线程模型中使用两个线程:

SendThread专门成立的请求的发送,请求会被封装为Packet(包含节点名称、Watch描述等信息)类发送给Sever。

EventThread则专门处理SendThread接收后解析出的Event。

ZkClient 的主要有两个Processor,一个是SycProcessor负责Cluster之间的数据同步(包括集群leader选取)。另一个是叫FinalRuestProcessor,专门处理对接受到的请求(Packet)进行处理。

    //ZookeeperServer 的processPacket方法专门对收到的请求进行处理。
    public void processPacket(ServerCnxn cnxn, ByteBuffer incomingBuffer) throws IOException {
        // We have the request, now process and setup for next
        InputStream bais = new ByteBufferInputStream(incomingBuffer);
        BinaryInputArchive bia = BinaryInputArchive.getArchive(bais);
        RequestHeader h = new RequestHeader();
        h.deserialize(bia, "header");
        // Through the magic of byte buffers, txn will not be
        // pointing
        // to the start of the txn
        incomingBuffer = incomingBuffer.slice();
        //鉴权请求处理
        if (h.getType() == OpCode.auth) {
            LOG.info("got auth packet " + cnxn.getRemoteSocketAddress());
            AuthPacket authPacket = new AuthPacket();
            ByteBufferInputStream.byteBuffer2Record(incomingBuffer, authPacket);
            String scheme = authPacket.getScheme();
            ServerAuthenticationProvider ap = ProviderRegistry.getServerProvider(scheme);
            Code authReturn = KeeperException.Code.AUTHFAILED;
            if(ap != null) {
                try {
                    authReturn = ap.handleAuthentication(new ServerAuthenticationProvider.ServerObjs(this, cnxn), authPacket.getAuth());
                } catch(RuntimeException e) {
                    LOG.warn("Caught runtime exception from AuthenticationProvider: " + scheme + " due to " + e);
                    authReturn = KeeperException.Code.AUTHFAILED;
                }
            }
            if (authReturn == KeeperException.Code.OK) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Authentication succeeded for scheme: " + scheme);
                }
                LOG.info("auth success " + cnxn.getRemoteSocketAddress());
                ReplyHeader rh = new ReplyHeader(h.getXid(), 0,
                        KeeperException.Code.OK.intValue());
                cnxn.sendResponse(rh, null, null);
            } else {
                if (ap == null) {
                    LOG.warn("No authentication provider for scheme: "
                            + scheme + " has "
                            + ProviderRegistry.listProviders());
                } else {
                    LOG.warn("Authentication failed for scheme: " + scheme);
                }
                // send a response...
                ReplyHeader rh = new ReplyHeader(h.getXid(), 0,
                        KeeperException.Code.AUTHFAILED.intValue());
                cnxn.sendResponse(rh, null, null);
                // ... and close connection
                cnxn.sendBuffer(ServerCnxnFactory.closeConn);
                cnxn.disableRecv();
            }
            return;
        } else {
            
            if (h.getType() == OpCode.sasl) {
                Record rsp = processSasl(incomingBuffer,cnxn);
                ReplyHeader rh = new ReplyHeader(h.getXid(), 0, KeeperException.Code.OK.intValue());
                cnxn.sendResponse(rh,rsp, "response"); // not sure about 3rd arg..what is it?
                return;
            }
            else {
                Request si = new Request(cnxn, cnxn.getSessionId(), h.getXid(),
                  h.getType(), incomingBuffer, cnxn.getAuthInfo());
                si.setOwner(ServerCnxn.me);
                // Always treat packet from the client as a possible
                // local request.
                setLocalSessionFlag(si);
                //交给finalRequestProcessor处理
                submitRequest(si);
            }
        }
        cnxn.incrOutstandingRequests(h);
    }

FinalRequestProcessor 对请求进行解析,Client连接成功后,发送的exist命令会落在这部分处理逻辑。

img

img

zkDataBase 由zkServer从disk持久化的数据建立而来,上图可以看到这里就是添加监听Watch的地方。

然后我们需要了解到,当Server收到节点更新事件后,是如何触发Watch的。

首先了解两个概念,FinalRequestProcessor处理的请求分为两种,一种是事务型的,一种非事务型,exist 的event-type是一个非事物型的操作,上面代码中是对其处理逻辑,对于事物的操作,例如SetData的操作。则在下面代码中处理。

    private ProcessTxnResult processTxn(Request request, TxnHeader hdr,
                                        Record txn) {
        ProcessTxnResult rc;
        int opCode = request != null ? request.type : hdr.getType();
        long sessionId = request != null ? request.sessionId : hdr.getClientId();
        if (hdr != null) {
            //hdr 为事物头描述,例如SetData的操作就会被ZkDataBase接管操作,
            //因为是对Zk的数据存储机型修改
            rc = getZKDatabase().processTxn(hdr, txn);
        } else {
            rc = new ProcessTxnResult();
        }
        if (opCode == OpCode.createSession) {
            if (hdr != null && txn instanceof CreateSessionTxn) {
                CreateSessionTxn cst = (CreateSessionTxn) txn;
                sessionTracker.addGlobalSession(sessionId, cst.getTimeOut());
            } else if (request != null && request.isLocalSession()) {
                request.request.rewind();
                int timeout = request.request.getInt();
                request.request.rewind();
                sessionTracker.addSession(request.sessionId, timeout);
            } else {
                LOG.warn("*****>>>>> Got "
                        + txn.getClass() + " "
                        + txn.toString());
            }
        } else if (opCode == OpCode.closeSession) {
            sessionTracker.removeSession(sessionId);
        }
        return rc;
    }

img

这里设置了断点,就可以拦截对节点的更新操作。

这两个设置了断点,就可以了解到Watch的设置过程。

接下来看如何启动Zookeeper的Client。ZookeeperMain为Client的入口,同样在bin/zkCli.sh中可以找到。注意设置参数,设置Server的连接地址。

img

修改ZookeeperMain方法,设置对节点的Watch监听。

    public ZooKeeperMain(String args[]) throws IOException, InterruptedException, KeeperException {
        cl.parseOptions(args);
        System.out.println("Connecting to " + cl.getOption("server"));
        connectToZK(cl.getOption("server"));
        while (true) {
            // 模拟注册对/zookeeper节点的watch监听
            zk.exists("/zookeeper", true);
            System.out.println("wait");
        }
    }

启动Client。

由于我们要观察节点变更的过程,上面这个Client设置了对节点的监听,那么我们需要另外一个cleint对节点进行更改,这个我们只需要在命令上进行就可以了。

img

此时命令行的zkClient更新了/zookeeper节点,Server此时会停在setData事件的处理代码段。

    public Stat setData(String path, byte data[], int version, long zxid,
            long time) throws KeeperException.NoNodeException {
        Stat s = new Stat();
        DataNode n = nodes.get(path);
        if (n == null) {
            throw new KeeperException.NoNodeException();
        }
        byte lastdata[] = null;
        synchronized (n) {
            lastdata = n.data;
            n.data = data;
            n.stat.setMtime(time);
            n.stat.setMzxid(zxid);
            n.stat.setVersion(version);
            n.copyStat(s);
        }
        // now update if the path is in a quota subtree.
        String lastPrefix = getMaxPrefixWithQuota(path);
        if(lastPrefix != null) {
          this.updateBytes(lastPrefix, (data == null ? 0 : data.length)
              - (lastdata == null ? 0 : lastdata.length));
        }
        //触发watch监听
        dataWatches.triggerWatch(path, EventType.NodeDataChanged);
        return s;
    }

此时,我们重点关注的类出现了。WatchManager

package org.apache.zookeeper.server;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class manages watches. It allows watches to be associated with a string
 * and removes watchers and their watches in addition to managing triggers.
 */
class WatchManager {
    private static final Logger LOG = LoggerFactory.getLogger(WatchManager.class);
    //存储path对watch的关系
    private final Map<String, Set<Watcher>> watchTable =
        new HashMap<String, Set<Watcher>>();
    //存储watch监听了哪些path节点
    private final Map<Watcher, Set<String>> watch2Paths =
        new HashMap<Watcher, Set<String>>();

    synchronized int size(){
        int result = 0;
        for(Set<Watcher> watches : watchTable.values()) {
            result += watches.size();
        }
        return result;
    }
    //添加监听
    synchronized void addWatch(String path, Watcher watcher) {
        Set<Watcher> list = watchTable.get(path);
        if (list == null) {
            // don't waste memory if there are few watches on a node
            // rehash when the 4th entry is added, doubling size thereafter
            // seems like a good compromise
            list = new HashSet<Watcher>(4);
            watchTable.put(path, list);
        }
        list.add(watcher);

        Set<String> paths = watch2Paths.get(watcher);
        if (paths == null) {
            // cnxns typically have many watches, so use default cap here
            paths = new HashSet<String>();
            watch2Paths.put(watcher, paths);
        }
        paths.add(path);
    }
    //移除
    synchronized void removeWatcher(Watcher watcher) {
        Set<String> paths = watch2Paths.remove(watcher);
        if (paths == null) {
            return;
        }
        for (String p : paths) {
            Set<Watcher> list = watchTable.get(p);
            if (list != null) {
                list.remove(watcher);
                if (list.size() == 0) {
                    watchTable.remove(p);
                }
            }
        }
    }

    Set<Watcher> triggerWatch(String path, EventType type) {
        return triggerWatch(path, type, null);
    }
    //触发watch
    Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
        WatchedEvent e = new WatchedEvent(type,
                KeeperState.SyncConnected, path);
        Set<Watcher> watchers;
        synchronized (this) {
            watchers = watchTable.remove(path);
            if (watchers == null || watchers.isEmpty()) {
                if (LOG.isTraceEnabled()) {
                    ZooTrace.logTraceMessage(LOG,
                            ZooTrace.EVENT_DELIVERY_TRACE_MASK,
                            "No watchers for " + path);
                }
                return null;
            }
            for (Watcher w : watchers) {
                Set<String> paths = watch2Paths.get(w);
                if (paths != null) {
                    paths.remove(path);
                }
            }
        }
        for (Watcher w : watchers) {
            if (supress != null && supress.contains(w)) {
                continue;
            }
            //通知发送
            w.process(e);
        }
        return watchers;
    }
}

重点关注triggerWatch的方法,可以发现watch被移除后,即往watch中存储的client信息进行通知发送。

    @Override
    public void process(WatchedEvent event) {
        ReplyHeader h = new ReplyHeader(-1, -1L, 0);
        if (LOG.isTraceEnabled()) {
            ZooTrace.logTraceMessage(LOG, ZooTrace.EVENT_DELIVERY_TRACE_MASK,
                                     "Deliver event " + event + " to 0x"
                                     + Long.toHexString(this.sessionId)
                                     + " through " + this);
        }

        // Convert WatchedEvent to a type that can be sent over the wire
        WatcherEvent e = event.getWrapper();

        sendResponse(h, e, "notification");
    }

没有任何确认机制,不会由于发送失败,而回写watch。

结论:

到这里,可以知道watch的通知机制是不可靠的,zkServer不会保证通知的可靠抵达。虽然zkclient与zkServer端是会有心跳机制保持链接,但是如果通知过程中断开,即时重新建立连接后,watch的状态是不会恢复。


现在已经知道了通知是不可靠的,会有丢失的情况,那ZkClient的使用需要进行修正。

img

本地的存储不再是一个静态的等待watch更新的状态,而是引入缓存机制,定期的去从Zk主动拉取并注册Watch(ZkServer会进行去重,对同一个Node节点的相同时间类型的Watch不会重复)。

另外一种方式是,Client端收到断开连接的通知,重新注册所有关注节点的Watch。但作者遇到的现网情况是client没有收到更新通知的同时,也没有查看到连接断开的错误信息。这块仍需进一步确认。水平有限,欢迎指正 ?


在StackOverFlow上的提问有了新进展:

https://stackoverflow.com/questions/49328151/is-zookeeper-node-change-notification-reliable-under-situations-of-connection-lo

原来官方文档已经解释了在连接断开的时候,client对watch的一些恢复操做,ps:原来上面我提到的客户端的策略已经官方实现。。。

img

客户端会通过心跳保活,如果发现断开了连接,会重新建立连接,并发送之前对节点设置的watch以及节点zxid,如果zxid与服务端的小则说明断开期间有更改,那么server会触发通知。

这么来看,Zookeeper的通知机制至少在官方的文档说明上是可靠的,至少是有相应机制去保证。ps:除Exist watch外。但是本人遇到的问题仍未解开。。后悔当初没有保留现场,深入发掘。计划先把实现改回原来的,后续进一步验证。找到原因再更新这里。


最终结论更新!

通过深入阅读apache的zk论坛以及源码,有一个重要的信息。

上面提到的连接断开分为recoverble以及unrecoverble两种场景,这两种的区别主要是基于Session的有效期,所有的client操作包括watch都是和Session关联的,当Session在超时过期时间内,重新成功建立连接,则watch会在连接建立后重新设置。但是当Session Timeout后仍然没有成功重新建立连接,那么Session则处于Expire的状态。下面连接讲述了这个过程

How should I handle SESSION_EXPIRED?

这种情况下,ZookeeperClient会重新连接,但是Session将会是全新的一个。同时之前的状态是不会保存的。

    private void conLossPacket(Packet p) {
        if (p.replyHeader == null) {
            return;
        }
        switch (state) {
        case AUTH_FAILED:
            p.replyHeader.setErr(KeeperException.Code.AUTHFAILED.intValue());
            break;
        case CLOSED:
            // session关闭状态,直接返回。
            p.replyHeader.setErr(KeeperException.Code.SESSIONEXPIRED.intValue());
            break;
        default:
            p.replyHeader.setErr(KeeperException.Code.CONNECTIONLOSS.intValue());
        }
        // 如果session未过期,这里进行session的状态(watches)会重新注册。
        finishPacket(p);
    }

*1、什么是zookeeper的会话过期?*

一般来说,我们使用zookeeper是集群形式,如下图,client和zookeeper集群(3个实例)建立一个会话session。

img

在这个会话session当中,client其实是随机与其中一个zk provider建立的链接,并且互发心跳heartbeat。zk集群负责管理这个session,并且在所有的provider上维护这个session的信息,包括这个session中定义的临时数据和监视点watcher。

如果再网络不佳或者zk集群中某一台provider挂掉的情况下,有可能出现connection loss的情况,例如client和zk provider1连接断开,这时候client不需要任何的操作(zookeeper api已经给我们做好了),只需要等待client与其他provider重新连接即可。这个过程可能导致两个结果:

1)在session timeout之内连接成功

这个时候client成功切换到连接另一个provider例如是provider2,由于zk在所有的provider上同步了session相关的数据,此时可以认为无缝迁移了。

2)在session timeout之内没有重新连接

这就是session expire的情况,这时候zookeeper集群会任务会话已经结束,并清除和这个session有关的所有数据,包括临时节点和注册的监视点Watcher。

在session超时之后,如果client重新连接上了zookeeper集群,很不幸,zookeeper会发出session expired异常,且不会重建session,也就是不会重建临时数据和watcher。

我们实现的ZookeeperProcessor是基于Apache Curator的Client封装实现的。

Apache Curator 错误处理机制

它对于Session Expire的处理是提供了处理的监听注册ConnectionStateListner,当遇到Session Expire时,执行使用者要做的逻辑。(例如:重新设置Watch)遗憾的是,我们没有对这个事件进行处理,因此连接是一致断开的,但是!我们应用仍然会读到老的数据!

在这里,我们又犯了另外一个错误,本地缓存了zookeeper的节点数据。。其实zookeeperClient已经做了本地缓存的机制,但是我们有加了一层(注:这里也有一个原因,是因为zk节点的数据时二进制的数组,业务要使用通常要反序列化,我们这里的缓存是为了减少反序列化带来的开销!),正式由于我们本地缓存了,因此即使zk断开了,仍然读取了老的值!

至此,谜团已经全部解开,看来之前的实现有许多姿势是错误的,导致后续出现了各种奇怪的BUG 。现在处理的方案,是监听Reconnect的通知,当收到这个通知后,主动让本地缓存失效(这里仍然做了缓存,是因为减少反序列化的开销,zkClient的缓存只是缓存了二进制,每次拿出来仍然需要反序列化)。代码:

       curatorFramework.getConnectionStateListenable().addListener(new ConnectionStateListener() {
            @Override
            public void stateChanged(CuratorFramework client, ConnectionState newState) {

                switch (newState) {
                    case CONNECTED:
                        break;
                    case RECONNECTED:
                        LOG.error("zookeeper connection reconnected");
                        System.out.println("zookeeper connection reconnected");
                        //本来使用invalidateAll,但是这个会使得cache所有缓存值同时失效
                        //如果关注节点比较多,导致同时请求zk读值,可能服务会瞬时阻塞在这一步
                        //因此使用guava cache refresh方法,异步更新,更新过程中,
                        //老值返回,知道更新完成
                        for (String key : classInfoMap.keySet()) {
                            zkDataCache.refresh(key);
                        }

                        break;
                    case LOST:
                       // session 超时,断开连接,这里不要做任何操作,缓存保持使用
                        LOG.error("zookeeper connection lost");
                        System.out.println("zookeeper connection lost");
                        break;
                    case SUSPENDED:
                        break;
                    default:
                        break;
                }

            }
        });

问答
如何阅读Zookeeper事务日志?
相关阅读
Zookeeper总览
ZooKeeper入门
zookeeper原理
【每日课程推荐】机器学习实战!快速入门在线广告业务及CTR相应知识

此文已由作者授权腾讯云+社区发布,更多原文请点击

搜索关注公众号「云加社区」,第一时间获取技术干货,关注后回复1024 送你一份技术课程大礼包!

海量技术实践经验,尽在云加社区

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Zookeeper 通知更新可靠吗? 解读源码找答案! 的相关文章

  • 如何在TheBrain中为Plex选择自定义图像并更改默认字体?

    TheBrain 点击下载 您的终极数字记忆和无限思维导图软件 我们从一个想法跳到另一个想法 构建越来越复杂的网络 直到新想法形成 TheBrain允许你以同样的方式组织你的信息 而不限制你预先确定的文件结构 事实上 你的数字大脑是没有限制

随机推荐

  • Unix/Linux编程:文件树遍历-----nftw()

    nftw 函数是对执行类似功能的老函数 ftw 的加强 由于提供了更多功能 对符号链接的处理也更易于把握 GNU C 语言函数库也提供了派生自 BSD 分支的 fts API fts open fts read fts children f
  • telnet配置及用法

    内容转载 http jingyan baidu com article ae97a646b22fb6bbfd461d19 html 搭建或配置网络环境时 经常会使用ping命令检查网络是否可达 有些时候Ping命令也不好使 比如因防火墙禁止
  • 中国半导体硅片行业未来发展趋势与销售规模预测报告2022版

    中国半导体硅片行业未来发展趋势与销售规模预测报告2022版 HS HS HS HS HS HS HS HS HS HS HS HS 修订日期 2021年11月 搜索鸿晟信合研究院查看官网更多内容 第一章 半导体硅片相关概述 1 1 半导体硅
  • C语言向上取整数简单写法

    C语言整数除法向上取整 define GET M N M 1 N 1 实例 一共100字节的数据 一次发送11字节 需要发送多少次 GET 100 16 7 次
  • 记录一道leetcode算法题的坑

    题目链接 这个题并不难 只是想让自己牢记一句老话 题目里面的每个条件都有用 int twoSum int nums int numsSize int target int returnSize int a 2 0 注意 注意 注意 retu
  • Mathematica 随机打乱列表顺序/列表随机重排列

    更新 找到相关函数了 没仔细看说明以为RandomSample只能随机取样来着 汗 RandomSample list 原内容 没必要看了 没有找到直接的相关函数 想到的方法是随机交换列表元素 例如以下程序为1 10的数字乱序 Permut
  • IP数据包长度问题总结

    首先要看TCP IP协议 涉及到四层 链路层 网络层 传输层 应用层 其中以太网 Ethernet 的数据帧在链路层 IP包在网络层 TCP或UDP包在传输层 TCP或UDP中的数据 Data 在应用层 它们的关系是 数据帧 IP包 TCP
  • TypeError: write() argument must be str, not bytes报错原因及Python3写入二进制文件方法

    Python2随机写入二进制文件 with open python2 random bin w as f f write os urandom 10 但使用Python3会报错 TypeError must be str not bytes
  • Java设计模式:深入解析与应用示例

    文章目录 引言 一 单例模式 二 工厂模式 三 抽象工厂模式 四 建造者模式 五 原型模式 六 适配器模式 七 装饰器模式 八 观察者模式 九 策略模式 十 命令模式 结语 引言 设计模式是一种在特定上下文中反复出现的可重用解决方案 用于处
  • Linux / ldd 命令的介绍与使用

    0 介绍 ldd 用来打印或者查看程序运行所需的共享库 访问共享对象依赖关系 常用来解决程序因缺少某个库文件而不能运行的一些问题 1 首先 ldd 不是一个可执行程序 而只是一个 shell 脚本 2 ldd 能够显示可执行模块的 depe
  • 学习笔记Flink(八)—— 基于Flink 在线交易反欺诈检测

    一 背景介绍 信用卡欺诈 信用卡欺诈是指故意使用伪造 作废的信用卡 冒用他人的信用卡骗取财物 或用本人信用卡进行恶意透支的行为 在当今数字时代 信用卡欺诈行为越来越被重视 罪犯可以通过诈骗或者入侵安全级别较低系统来盗窃信用卡卡号 用盗得的信
  • mipi和isp处理_ISP-摄像头的最强大脑- 图像质量及色彩科技知识分享平台 图像质量与色彩管理 - Powered by HDWiki!...

    做为拍照手机的核心模块之一 camera sensor效果的调整 涉及到众多的参数 如果对基本的光学原理及sensor软 硬件对图像处理的原理能有深入的理解和把握的话 对我们的工作将会起 到事半功倍的效果 否则 缺乏了理论的指导 只能是凭感
  • 【系统篇 / 文件】01. 文件服务安装与配置 ❀ Windows Server 2008 R2

    简介 文件服务提供帮助管理存储 启用文件复制 管理共享文件夹 确保快速搜索文件 以及启用对UNXI客户端计算机访问的技术 使用文件服务 组织可以将文件存储到中心位置 然后通过公司网络与用户共享 可以为这些共享文件创建索引 以帮助用户快速查找
  • K8S deployment可视化故障排查指南

    这是一个示意图 可帮助您调试Kubernetes中的deployemnt 当您希望在Kubernetes中部署应用程序时 通常定义三个组件 一个deployment 这是创建名为Pods的应用程序副本的秘诀 一个service 内部负载平衡
  • cocos creator 中读取Excel表格中的数据

    一 使用相应工具将Excel文件转化成JSON文件导入到cocos creator资源文件 二 在VS中对Excel文本中的数据进行转换 Excel文本中各项数据的名称对应代码中的data export default class Task
  • Apache-tomcat-8.5.82下载安装以及环境变量配置

    一 下载apache tomcat 8 5 82 1 进入apache官网 Apache Tomcat Welcome 选择Download gt Tomcat8 进入Apache Tomcat Apache Tomcat 8 Softwa
  • 分布式计算,泛在计算,BOINC

    BOINC平台简介 知乎 Download BOINC client software 开源源代码 https github com BOINC boinc 介绍 https www equn com wiki BOINC 使用指南 htt
  • 【Antdv】a-date-picker showTime带时间默认00:00:00

    show time 默认当前系统时间 设置默认 00 00 00
  • 预编码

    原则上说MIMO技术并不一定需要预编码 使用预编码的前提是发射端可以及时获取信道信息 也就是CSIT 在通常情况下 只有接收端可以知道信道信息CSIR 在这个情况下 接收端通过复杂的信号处理算法 如MMSE SIC 可以解调出多路的MIMO
  • Zookeeper 通知更新可靠吗? 解读源码找答案!

    欢迎大家前往腾讯云 社区 获取更多腾讯海量技术实践干货哦 本文由特鲁门发表于云 社区专栏 导读 遇到Keepper通知更新无法收到的问题 思考节点变更通知的可靠性 通过阅读源码解析了解到zk Watch的注册以及触发的机制 本地调试运行模拟