内部类 |
作用 |
Call |
存储客户端发来的请求 |
Listener |
监听类: 监听客户端发来的请求,内部静态类Listener.Reader: 当监听器监听到用户请求,便让Reader读取用户请求 |
Responder |
响应RPC请求类,请求处理完毕,由Responder发送给请求客户端 |
Connection |
连接类,真正的客户端请求读取逻辑在这个类中 |
Handler |
请求处理类,会循环阻塞读取callQueue中的call对象,并对其进行操作 |
Server是个抽象类, 唯一抽象的方法. Server提供了一个架子, Server的具体功能, 需要具体类来完成。而具体类, 当然就是实现call方法。
/** Called for each call. */
public abstract Writable call(Class<?> protocol, Writable param, long receiveTime) throws IOException;
Server.Call 和Client.Call类似, Server.Call包含了一次请求, 其中id和param的含义和Client.Call是一致的。不同点:
connection是该Call来自的连接, 当请求处理结束时, 相应的结果会通过相同的connection, 发送给客户端。
timestamp是请求到达的时间戳, 如果请求很长时间没被处理, 对应的连接会被关闭, 客户端也就知道出错了。
response是请求处理的结果, 可能是一个Writable的串行化结果, 也可能一个异常的串行化结果。
Server.Connection 维护了一个来自客户端的socket连接。
它处理版本校验, 读取请求并把请求发送到请求处理线程, 接收处理结果并把结果发送给客户端。
Hadoop的Server采用了Java的NIO, 这样的话就不需要为每一个socket连接建立一个线程, 读取socket上的数据。
在Server中, 只需要一个线程, 就可以accept新的连接请求和读取socket上的数据, 这个线程, 就是Listener。
Server.Handler 请求处理线程一般有多个.
Handler的run方法循环地取出一个Server.Call, 调用Server.call方法, 搜集结果并串行化, 然后将结果放入Responder队列中。
对于处理完的请求, 需要将结果写回去, 同样, 利用NIO, 只需要一个线程, 相关的逻辑在Responder里。
Call
/** A call queued for handling. */ private static class Call { private int id; // the client's call id 客户端的RPC调用对象Call的id private Writable param; // the parameter passed 客户端的PRC调用对象Call的参数 private Connection connection;// connection to client 客户端连接对象,在服务端持有这个对象, 就能知道是哪个客户端连接 private long timestamp; // the time received when response is null; the time served when response is not null private ByteBuffer response; // the response for this call 当前RPC调用的响应 public Call(int id, Writable param, Connection connection) { this.id = id; this.param = param; this.connection = connection; this.timestamp = System.currentTimeMillis(); this.response = null; } public void setResponse(ByteBuffer response) { this.response = response; } }
Server initialize and start
ipc.Server是抽象类, 抽象类不能实例化, 那么系统启动的时候, 实例化的是ipc.Server抽象类的实现类, 即ipc.RPC.Server. 即启动RPC.Server
public class NameNode implements ClientProtocol, DatanodeProtocol, NamenodeProtocol, FSConstants, RefreshAuthorizationPolicyProtocol, RefreshUserMappingsProtocol { /** RPC server */ private Server server; /** RPC server for HDFS Services communication. * BackupNode, Datanodes and all other services should be connecting to this server if it is configured. Clients should only go to NameNode#server */ private Server serviceRpcServer; /** RPC server address */ private InetSocketAddress serverAddress = null; /** RPC server for DN address */ protected InetSocketAddress serviceRPCAddress = null; /** Initialize name-node. NameNode初始化 */ private void initialize(Configuration conf) throws IOException { InetSocketAddress socAddr = NameNode.getAddress(conf); // create rpc server InetSocketAddress dnSocketAddr = getServiceRpcServerAddress(conf); if (dnSocketAddr != null) { this.serviceRpcServer = RPC.getServer(this, dnSocketAddr.getHostName(), dnSocketAddr.getPort(), serviceHandlerCount, false, conf, namesystem.getDelegationTokenSecretManager()); this.serviceRPCAddress = this.serviceRpcServer.getListenerAddress(); setRpcServiceServerAddress(conf); } this.server = RPC.getServer(this, socAddr.getHostName(), socAddr.getPort(), handlerCount, false, conf, namesystem.getDelegationTokenSecretManager()); // The rpc-server port can be ephemeral... ensure we have the correct info this.serverAddress = this.server.getListenerAddress(); startHttpServer(conf); this.server.start(); //start RPC server if (serviceRpcServer != null) { serviceRpcServer.start(); } startTrashEmptier(conf); } }
RPC.newServer()
/** Construct a server for a protocol implementation instance listening on a port and address, with a secret manager. * 构造协议实现类的服务器, 该方法的调用者NameNode实现了一系列的协议接口. * 以读取HDFS文件为例, 客户端发送一个RPC调用getBlockLocations()到NameNode服务端, 由NameNode进行实际的方法调用 */ public static Server getServer(final Object instance, final String bindAddress, final int port, final int numHandlers, final boolean verbose, Configuration conf, SecretManager<? extends TokenIdentifier> secretManager) { return new Server(instance, conf, bindAddress, port, numHandlers, verbose, secretManager); } /** An RPC Server. */ public static class Server extends org.apache.hadoop.ipc.Server { /** Construct an RPC server. * @param instance the instance whose methods will be called 被调用的方法的实例对象 * @param conf the configuration to use * @param bindAddress the address to bind on to listen for connection * @param port the port to listen for connections on * @param numHandlers the number of method handler threads to run * @param verbose whether each call should be logged */ public Server(Object instance, Configuration conf, String bindAddress, int port, int numHandlers, boolean verbose, SecretManager<? extends TokenIdentifier> secretManager) { super(bindAddress, port, Invocation.class, numHandlers, conf, classNameBase(instance.getClass().getName()), secretManager); // 调用父类ipc.Server抽象类的构造方法 this.instance = instance; this.verbose = verbose; } }
ipc.Server构造方法
/** An abstract IPC service. IPC calls take a single Writable as a parameter, and return a Writable as their value. * A service runs on a port and is defined by a parameter class and a value class. */ public abstract class Server { private String bindAddress; private int port; // port we listen on private int handlerCount; // number of handler threads private int readThreads; // number of read threads private Class<? extends Writable> paramClass; // class of call parameters private int maxIdleTime; // the maximum idle time after which a client may be disconnected private int thresholdIdleConnections; // the number of idle connections after which we will start cleaning up idle connections int maxConnectionsToNuke; // the max number of connections to nuke during a cleanup protected RpcInstrumentation rpcMetrics; private Configuration conf; private SecretManager<TokenIdentifier> secretManager; private int maxQueueSize; private final int maxRespSize; private int socketSendBufferSize; private final boolean tcpNoDelay; // if T then disable Nagle's Algorithm volatile private boolean running = true;// true while server runs private BlockingQueue<Call> callQueue;// queued calls private List<Connection> connectionList = Collections.synchronizedList(new LinkedList<Connection>()); //maintain a list of client connections private Listener listener = null;//服务端监听器 private Responder responder = null;//服务端写回客户端的响应 private int numConnections = 0;//连接的客户端个数 private Handler[] handlers = null;//处理类 /** Constructs a server listening on the named port and address. * Parameters passed must be of the named class. * The handlerCount determines the number of handler threads that will be used to process calls. */ protected Server(String bindAddress, int port, Class<? extends Writable> paramClass, int handlerCount, Configuration conf, String serverName, SecretManager<? extends TokenIdentifier> secretManager) { this.bindAddress = bindAddress; this.conf = conf; this.port = port; this.paramClass = paramClass; this.handlerCount = handlerCount; this.socketSendBufferSize = 0; this.maxQueueSize = handlerCount * conf.getInt(IPC_SERVER_HANDLER_QUEUE_SIZE_KEY, IPC_SERVER_HANDLER_QUEUE_SIZE_DEFAULT); this.maxRespSize = conf.getInt(IPC_SERVER_RPC_MAX_RESPONSE_SIZE_KEY, IPC_SERVER_RPC_MAX_RESPONSE_SIZE_DEFAULT); this.readThreads = conf.getInt(IPC_SERVER_RPC_READ_THREADS_KEY, IPC_SERVER_RPC_READ_THREADS_DEFAULT); this.callQueue = new LinkedBlockingQueue<Call>(maxQueueSize); this.maxIdleTime = 2*conf.getInt("ipc.client.connection.maxidletime", 1000); this.maxConnectionsToNuke = conf.getInt("ipc.client.kill.max", 10); this.thresholdIdleConnections = conf.getInt("ipc.client.idlethreshold", 4000); this.secretManager = (SecretManager<TokenIdentifier>) secretManager; this.authorize = conf.getBoolean(HADOOP_SECURITY_AUTHORIZATION, false); this.isSecurityEnabled = UserGroupInformation.isSecurityEnabled(); // Start the listener here and let it bind to the port listener = new Listener(); this.port = listener.getAddress().getPort(); this.rpcMetrics = RpcInstrumentation.create(serverName, this.port); this.tcpNoDelay = conf.getBoolean("ipc.server.tcpnodelay", false); responder = new Responder(); // Create the responder here if (isSecurityEnabled) { SaslRpcServer.init(conf); } } private void closeConnection(Connection connection) { synchronized (connectionList) { if (connectionList.remove(connection)) numConnections--; } try { connection.close(); } catch (IOException e) { } } // NameNode在获得Server后, 会调用server.start()启动服务端. 三个对象responder,listener,handlers都是线程类, 都调用start() /** Starts the service. Must be called before any calls will be handled. */ public synchronized void start() { responder.start(); listener.start(); handlers = new Handler[handlerCount]; for (int i = 0; i < handlerCount; i++) { handlers[i] = new Handler(i); handlers[i].start(); } } }
RPC.Server.Listener
Client端的底层通信直接采用了阻塞式IO编程,Server端采用Listener监听客户端的连接
private static final ThreadLocal<Server> SERVER = new ThreadLocal<Server>(); /** Returns the server instance called under or null. May be called under #call(Writable, long)implementations, * and under Writable methods of paramters and return values. Permits applications to access the server context.*/ public static Server get() { return SERVER.get(); } //maintain a list of client connections 维护客户端的连接列表, 这里的Connection是Server.Connection private List<Connection> connectionList = Collections.synchronizedList(new LinkedList<Connection>()); /** Listens on the socket. Creates jobs for the handler threads 监听客户端Socket连接, 为handler线程创建任务 */ private class Listener extends Thread { private ServerSocketChannel acceptChannel = null; //the accept channel 服务端通道 private Selector selector = null; //the selector that we use for the server 选择器(NIO) private Reader[] readers = null; private int currentReader = 0; private InetSocketAddress address; //the address we bind at 服务端地址 private Random rand = new Random(); private long lastCleanupRunTime = 0; //the last time when a cleanup connection (for idle connections) ran private long cleanupInterval = 10000; //the minimum interval between two cleanup runs private int backlogLength = conf.getInt("ipc.server.listen.queue.size", 128); private ExecutorService readPool; //读取池, 任务执行服务(并发) public Listener() throws IOException { address = new InetSocketAddress(bindAddress, port); acceptChannel = ServerSocketChannel.open(); // Create a new server socket 创建服务端Socket连接, acceptChannel.configureBlocking(false); // and set to non blocking mode设置为非阻塞模式 bind(acceptChannel.socket(), address, backlogLength); // Bind the server socket to the local host and port将ServerSocket绑定到本地端口 port = acceptChannel.socket().getLocalPort(); // Could be an ephemeral port selector= Selector.open(); // create a selector 创建一个监听器的Selector readers = new Reader[readThreads];//读取线程数组 readPool = Executors.newFixedThreadPool(readThreads);//启动多个reader线程,为了防止请求多时服务端响应延时的问题 for (int i = 0; i < readThreads; i++) { Selector readSelector = Selector.open();//每个读取线程都创建一个Selector Reader reader = new Reader(readSelector); readers[i] = reader; readPool.execute(reader); } acceptChannel.register(selector, SelectionKey.OP_ACCEPT); // ①Register accepts on the server socket with the selector. 注册连接事件 this.setName("IPC Server listener on " + port); this.setDaemon(true); } // 在启动Listener线程时listener.start(), 服务端会一直等待客户端的连接 public void run() { SERVER.set(Server.this); //使用ThreadLocal本地线程,设置当前Server为当前的ThreadLocal对象 while (running) { SelectionKey key = null; try { selector.select(); Iterator<SelectionKey> iter = selector.selectedKeys().iterator(); while (iter.hasNext()) { key = iter.next(); iter.remove(); if (key.isValid()) { if (key.isAcceptable()) doAccept(key); // ②建立连接,服务端接受客户端连接 } key = null; } } catch (Exception e) { closeCurrentConnection(key, e); } cleanupConnections(false); } // 监听器不再监听客户端的连接,关闭通道和选择器和所有的连接对象 synchronized (this) { acceptChannel.close(); selector.close(); selector= null; acceptChannel= null; while (!connectionList.isEmpty()) { // clean up all connections closeConnection(connectionList.remove(0)); } } } void doAccept(SelectionKey key) throws IOException, OutOfMemoryError { //② Connection c = null; ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel channel; while ((channel = server.accept()) != null) {//建立连接server.accept() channel.configureBlocking(false); channel.socket().setTcpNoDelay(tcpNoDelay); Reader reader = getReader(); //从readers池中获得一个reader try { reader.startAdd();//激活readSelector,设置adding为true SelectionKey readKey = reader.registerChannel(channel); //③将读事件设置成兴趣事件 c = new Connection(readKey, channel, System.currentTimeMillis());//创建一个连接对象 readKey.attach(c);//将connection对象注入readKey synchronized (connectionList) { connectionList.add(numConnections, c); numConnections++; } } finally { reader.finishAdd(); //设置adding为false,采用notify()唤醒一个reader, 初始化Listener时启动的每个reader都使用了wait()方法等待 } // 当reader被唤醒, reader会执行doRead() } } void doRead(SelectionKey key) throws InterruptedException { //④ int count = 0; Connection c = (Connection)key.attachment(); if (c == null) { return; } c.setLastContact(System.currentTimeMillis()); try { count = c.readAndProcess(); } catch (InterruptedException ieo) { throw ieo; } catch (Exception e) { count = -1; //so that the (count < 0) block is executed } if (count < 0) { closeConnection(c); c = null; } else { c.setLastContact(System.currentTimeMillis()); } } synchronized void doStop() { if (selector != null) { selector.wakeup(); Thread.yield(); } if (acceptChannel != null) { try { acceptChannel.socket().close(); } catch (IOException e) { LOG.info(getName() + ":Exception in closing listener socket. " + e); } } readPool.shutdown(); } // The method that will return the next reader to work with // Simplistic implementation of round robin for now Reader getReader() { currentReader = (currentReader + 1) % readers.length; return readers[currentReader]; } }
Listener.Reader
private class Reader implements Runnable { private volatile boolean adding = false; //读取线程是否正在添加中,如果是,等待一秒钟 private Selector readSelector = null; //读取线程的Selector选择器 Reader(Selector readSelector) { this.readSelector = readSelector; } public void run() { synchronized (this) { while (running) { SelectionKey key = null; readSelector.select(); while (adding) { this.wait(1000); } Iterator<SelectionKey> iter = readSelector.selectedKeys().iterator(); while (iter.hasNext()) { key = iter.next(); iter.remove(); if (key.isValid()) { if (key.isReadable()) { doRead(key); //④ } } key = null; } } } } /** * This gets reader into the state that waits for the new channel to be registered with readSelector. * If it was waiting in select() the thread will be woken up, otherwise whenever select() is called * it will return even if there is nothing to read and wait in while(adding) for finishAdd call */ public void startAdd() { adding = true; readSelector.wakeup(); } public synchronized SelectionKey registerChannel(SocketChannel channel) { return channel.register(readSelector, SelectionKey.OP_READ); //③ } public synchronized void finishAdd() { adding = false; this.notify(); } }
NIO通信流程
①初始化服务器时,创建监听器, 在监听器的构造方法里会创建ServerSocketChannel, 选择器, 以及多个读取线程. 并在服务端通道上注册OP_ACCEPT操作
acceptChannel.register(selector, SelectionKey.OP_ACCEPT);
启动服务器会调用listener.start(), listener是个线程类, 会调用run().
监听器会一直监听客户端的请求, 通过监听器的Selector选择器进行轮询是否有感兴趣的事件发生(服务器感兴趣的是上面注册的接受连接事件)
②当客户端连接服务端, 被Selector捕获到该事件, 因为在ServerSocketChannle对OP_ACCEPT操作感兴趣,所以服务端接受了客户端的连接请求.
doAccept(SelectionKey key)
客户端连接到服务器, 服务端接受连接, 监听器会从读取线程池中选择一个读取线程, 委托给读取线程处理, 而不是监听器自己来处理.
建立SocketChannel连接, 注意不是ServerSocketChannel. (ServerSocketChannle在整个通信过程中只建立一次即服务端启动的时候)
SocketChannel channel = server.accept();
③往建立的SocketChannel通道注册感兴趣的OP_READ操作. 此时接收读取事件的选择器不再是监听器的, 而是读取线程的选择器
SelectionKey readKey = reader.registerChannel(channel);
channel.register(readSelector, SelectionKey.OP_READ); //往readSelector注册感兴趣的OP_READ操作.读取线程的选择器负责轮询监听客户端的数据写入
同时根据(readKey和SocketChannel,当前时间)建立一个Connection注入到readKey中.
这个Connection对象是客户端和服务器的连接对象, 客户端和服务器建立连接后, 在后续的客户端写入数据过程也应该使用同一个Connection
Connection c = new Connection(readKey, channel, System.currentTimeMillis());
readKey.attach(c); //在通信过程中如果想要保存某个对象,附加在selectionKey中
注册读操作后,服务端的监听器的读取线程就能读取客户端传入的数据
④客户端开始向服务端写入数据, 读取线程Reader的选择器捕获到客户端的写入事件,
因为读取线程注册了感兴趣的OP_READ操作,所以能够读取客户端的写入数据.
doRead(SelectionKey key)
读取事件的操作会根据selectionKey获得Connection, 这个Connection对象正是客户端和服务器建立连接时注入到readKey中的Connection对象
具体的读取客户端的数据的操作就在该Connection的readAndProcess方法里
connection.readAndProcess();
Listener.Reader的线程模型
Listener的doAccept()接受连接过程
从readers池中获得一个reader线程
reader.startAdd(); 激活readSelector,设置adding为true --> 读线程监听客户端的数据写入,如果adding=true,表示Reader正在添加,再等待一秒钟
将读事件设置成兴趣事件
创建一个连接对象
reader.finishAdd(); 设置adding为false,采用notify()唤醒一个reader, 初始化Listener时启动的每个reader都使用了wait()方法等待
将要设置读事件为兴趣事件包装在设置Reader的adding属性以及使用notify()两者之间. 是为了确保读取线程发生在设置读事件为感兴趣事件之后.
基于NIO的事件模型采用选择器来轮询感兴趣的事件.只要有感兴趣的操作, 选择器就会捕获进行处理. 如果没有感兴趣的事件发生则没有操作.
所以服务端接受客户端的连接和读取客户端的数据这两个操作过程发生的时刻完全是随机的.
也就是说监听客户端连接的选择器和多个读取客户端数据的读取线程的选择器捕获事件也都是随机的.
但是读取客户端的数据必须保证发生在客户端连接服务器之后.
因为如果客户端没有连接服务器, 也就不会注册读取事件OP_READ到读取线程上. 因为注册OP_READ发生在在doAccept()客户端连接服务器操作中.
初始化Listener时启动的每个Reader, 都会新建对应的选择器. Reader的默认字段adding=false
Selector readSelector = Selector.open(); //每个读取线程都创建一个Selector
Reader reader = new Reader(readSelector);
初始化时尽管adding=false在run()中不会执行this.wait(1000)的等待操作, 但是因为还没有客户端连接注册OP_READ事件所以选择器不会捕获该事件.
客户端连接服务器,服务器接受连接,在doAccept()中, 注册OP_READ到读取线程的感兴趣事件
1. 之前: 设置adding=true并激活读取线程的选择器, 注意此时读取线程的选择器进行轮询操作是不会捕获到读取事件的,因为还没注册OP_READ事件
所以读取线程的run()如果判断adding=true, 就知道选择器关注的SocketChannel上的OP_READ事件还没注册好,需要每隔一秒钟再判断
2. 往建立的SocketChannel注册好OP_READ事件
3. 之后: 设置adding=false并通知Reader读取线程不需要再等待下去, run()方法判断adding=false, 选择器开始轮询等待客户端的写入
Listener和Reader的选择器
Listener的选择器只有一个, Listener有多个Reader, 每个Reader都有自己的选择器.
Listener的选择器来监听客户端的连接, 当监听到有一个客户端连接服务器, 就会选取一个Reader, 并往Reader的选择器注册读取操作.
这样具体的读取操作就交给了Reader进行处理. 因为Reader有多个, 所以如果有多个客户端连接并写入数据给服务器, 就可以开多个Reader同时读取.
相关推荐
本文将深入探讨Hadoop的RPC机制,解析其工作原理,并结合源码分析其内部实现。 一、RPC简介 RPC是一种让程序能够调用运行在其他地址空间(通常在另一台机器上)的程序的方法。在Hadoop中,RPC被广泛用于NameNode、...
### Hadoop源码阅读总结:IPC/RPC 通信机制详解 #### 一、概述 Hadoop作为分布式计算框架,其内部各个组件之间的通信主要通过RPC(Remote Procedure Call)实现。本文将详细介绍Hadoop中RPC机制的工作原理,特别是...
4. RPC机制:理解HBase如何通过HBaseRpcController和RpcServer实现客户端与服务器之间的通信。 5. 并发控制:学习RegionSplitPolicy、RegionSplitter等类,理解HBase如何处理并发请求和Region分裂。 6. 客户端API:...
### Hadoop RPC 深入理解 #### 一、引言 ...通过以上分析可以看出,Hadoop RPC 不仅提供了一个高效的通信框架,还通过对细节的精确控制确保了高性能和服务的可靠性。这对于构建大型分布式系统来说至关重要。
Hadoop中的通信主要通过RPC实现,这是一种远程调用协议,使得客户端可以像调用本地方法一样调用远程服务器上的方法。Hadoop的RPC基于Java的Protocol Buffers,提供高效、灵活的序列化和反序列化能力,确保跨网络的...
这个框架由多个组件构成,包括RPCServer和RPCClient,它们是HBase内部类,负责处理客户端请求并转发给相应的服务器。RPC.Server是Hadoop中的基础RPC服务实现,而HBase在其基础上进行了定制,以满足其特定需求,如...
《深入解析Hadoop之HBase 0.99.2源码分析》 在当今的信息化社会,大数据处理已经成为企业核心竞争力的关键要素。Hadoop作为开源大数据处理框架的领头羊,其生态中的HBase更是备受关注。HBase是基于Google Bigtable...
在Hadoop的RPC实现方法中,Client类、Server类、RPC类以及HDFS通信协议组是核心。这些组件共同协作实现远程过程调用,使得HDFS中的各个组件能够相互交流和协作。通过这些组件,HDFS能够处理客户端请求,并在集群内部...
### HBase源码分析 #### 一、HBase性能测试要点与分析 ##### 1.1 测试环境 - **硬件配置**: - 客户端:1台 - RegionServer:5台 - Master:1台 - ZooKeeper:3台 - **软件配置**: - CPU:每台服务器配备8...
4. **RPC通信**:`org.apache.hadoop.hbase.ipc`包下的RpcServer实现了客户端与服务器之间的远程过程调用,处理客户端请求。 5. **版本控制与并发控制**:每个Cell都有时间戳,用于版本控制;`org.apache.hadoop....
源码分析有助于提升对空间和时间复杂度的理解。 8. **Test-HBase**: 测试模块,包含了HBase的单元测试和集成测试,这对于理解HBase的正确性验证和测试策略非常重要。 9. **HBase Hadoop Compat**: 类似于HBase ...
下面我们将深入源码,详细分析这一过程。 首先,当用户编写好MapReduce程序并调用Job的submit()方法时,客户端会进行一系列的初始化工作。这包括将程序的JAR包、配置信息以及输入输出路径等打包成一个JobConf对象,...
如果"owl-server"使用了Spring Boot,那么"owl-server-main"可能是Spring Boot的`Application`类,它通过`@SpringBootApplication`注解标记,启动Spring容器并运行应用。 服务器应用通常需要与数据库交互,可能使用...
通过阅读源码,可以了解其内部的Region分配策略、数据存储格式、RPC通信机制等细节,这对于理解和优化HBase系统非常有价值。 9. **扩展性** HBase支持多种插件和扩展,如Coprocessor机制,允许用户在RegionServer...
HDFS (Hadoop Distributed File System) 是Hadoop项目的两大核心之一,是分布式文件系统的实现。 #### 分布式协调 - **ZooKeeper**: 框架。Apache ZooKeeper 是一个集中式的、开源的协调服务,用于管理和协调分布式...
DskipTests2、配置CAT的环境mvn cat:installNote:Linux\Mac 需要对/data/appdatas/cat和/data/applogs/cat有读写权限Windows 则是对系统运行盘下的/...,需到/data/appdatas/cat/server.xml中配置对应hadoop信息...