HDFS 短路读取:mmap 与 Unix Domain Socket 铸就的零拷贝艺术
本文深入解析了HDFS短路读取技术如何通过mmap和Unix Domain Socket实现零拷贝优化。传统HDFS读取需要3次CPU拷贝,而短路读取将拷贝次数减少至1次甚至完全消除。mmap通过内存映射让客户端直接访问page cache,Unix Domain Socket则安全传递文件描述符完成权限交接。文章详细对比了不同读取方式的CPU开销,分析了技术原理和实现路径,并指出HDFS通过操作
HDFS 短路读取:mmap 与 Unix Domain Socket 铸就的零拷贝艺术
在大数据时代,每一次数据读取都像一场隐形的马拉松。Spark 扫描百亿行 Parquet 文件、HBase 承受百万 QPS 查询、Hive 执行复杂聚合时,底层 I/O 的每一毫秒延迟、每一次 CPU 拷贝,都会放大为整个系统的性能税。
HDFS 的短路本地读取(Short-Circuit Local Read)将传统 HDFS 路径中 3 次 CPU 拷贝(DataNode 读文件①、send 进内核 TCP②、Client recv 出内核③)缩减至仅 1 次(FileChannel.read:page cache → Client JVM 堆),甚至借助 mmap 映射彻底消除 CPU 拷贝,实现真正意义上的零拷贝。
这项技术的灵魂,是操作系统两大经典机制的优雅共舞:mmap(内存映射) 与 Unix Domain Socket(域套接字)。前者让文件直接映射进进程地址空间,后者则用文件描述符传递完成安全、高效的权限交接。HDFS 没有发明新轮子,而是把内核的智慧发挥到了极致。
本文将从操作系统原理出发,逐步深入 HDFS 短路读取的完整机制,再结合 Hadoop 3.x 主线源码进行逐层拆解,最后给出生产调优与故障排查实战。希望读完之后,你不仅能理解"为什么快",更能体会"为什么这么设计"——这才是分布式系统最迷人的地方。
一、mmap:把"搬书"变成"开窗"

传统 read()/write() 的本质,是反复的"数据搬家"。磁盘数据先通过 DMA 进入内核 page cache(0 次 CPU 拷贝),再被 read() 系统调用的 copy_to_user() 从 page cache 拷贝到用户态缓冲区(1 次 CPU 拷贝)。写回时又要从用户态拷贝回内核。每次搬家都伴随着用户态与内核态的切换、上下文保护、TLB 刷新,CPU 开销惊人。
mmap 的天才之处在于:它干脆取消了搬家。
当进程调用 mmap() 时,内核只在进程的页表中建立一个虚拟地址到文件偏移的映射关系,并不立即加载任何数据。直到程序真正访问某个虚拟地址,才触发缺页中断(page fault)。内核捕获中断后,按需将对应页面从磁盘加载到 page cache,并更新页表。此后,用户态指针与内核 page cache 共享同一块物理内存——程序通过指针直接读写 page cache 页面,无需任何额外的 read()/write() 系统调用和内存拷贝,这是 mmap 实现真正零拷贝的本质。
写操作直接修改 page cache 中的页面,内核后台通过 flusher 线程(writeback 机制)异步回写。整个过程,程序员只需像操作普通数组一样用指针读写,操作系统默默接管了分页、换出、脏页跟踪、预读等全部复杂工作。
用一个经典比喻:传统读取像每次借书都要管理员从书库取书、复印、再还回去;而 mmap 则是图书馆直接在你书桌上划一块区域,把整本书的位置告诉你。你想看哪页,管理员才把那一页悄然放上桌面。你所有的读写,都发生在桌面(page cache)上,无需反复复印。
在 HDFS 中,mmap 正是让客户端进程把 DataNode 上的块文件(blk_xxx)直接映射进自己虚拟地址空间的核心武器。后续所有读取,都变成了对 page cache 的直接指针访问——这正是零拷贝的本质。
CPU 拷贝次数速查表
| 场景 | CPU 拷贝 | 路径 |
|---|---|---|
普通 read() 读本地文件 |
1 | page cache → 用户缓冲区(copy_to_user) |
| mmap 映射读 | 0 | 页表直指 page cache,指针直接访问 |
| 传统 HDFS 读取(同机 DataNode TCP 中转) | 3 | ① read() → DN JVM 堆② send() → 内核 TCP send buffer③ recv() → Client JVM 堆 |
短路读取 + FileChannel.read() |
1 | page cache → Client JVM 堆(绕过 DN 和网络) |
短路读取 + MappedByteBuffer |
0 | page cache 直接映射到 Client 虚拟地址空间 |
二、Unix Domain Socket:同一主机最优雅的通信礼仪
跨进程通信时,即使在同一台机器上,TCP 也要走完整的协议栈:三次握手、序列号、窗口管理、校验和、拥塞控制。即使是 localhost,延迟和 CPU 开销也不容忽视。而 Unix Domain Socket(AF_UNIX)专为同一主机设计,完全绕过 TCP/IP 协议栈。
它有两种地址形式:
- 基于文件系统的路径(如
/var/lib/hadoop-hdfs/dn_socket),权限由文件系统天然控制。Hadoop 3.x 中dfs.domain.socket.path默认为空字符串(即不启用),生产环境需显式配置。 - Linux 特有的抽象命名空间(以
\0开头),无需实际文件,更安全。
最关键的是,Unix Domain Socket 原生支持 SCM_RIGHTS ancillary message 机制——文件描述符传递。发送方通过 sendmsg() 把一个已打开的 fd 嵌入控制消息,内核会在接收方进程的文件描述符表中复制一个新 fd,指向同一个底层文件对象(struct file)。即使发送方关闭原 fd,接收方仍可继续使用。
这正是 HDFS 短路读取的控制通道:客户端无法直接打开 DataNode 的块文件(权限受限),于是通过 domain socket 发起协商,DataNode 验证通过后,把 fd 优雅地递给客户端。内核完成授权,双方共享同一物理页面。

三、传统 HDFS 读取路径:三次 CPU 拷贝的马拉松
在未启用短路读取时,一次 HDFS 块读取要经历以下旅程:
什么是"CPU 拷贝"?每次
read()、write()、send()这类系统调用,内核需要把数据从一块内存搬运到另一块内存,这一步消耗 CPU 周期。而磁盘 → page cache 是 DMA(直接内存访问),由磁盘控制器硬件完成,不消耗 CPU。以下所有数字均排除 DMA 步骤。
可以看到,传统 HDFS 读取即便 DataNode 与 Client 在同一台机器上,也要经过 TCP 协议栈的 send/recv 中转——一次本地读被拆成了三次 CPU 拷贝,每次还伴随用户态/内核态切换。这就是为什么早期 Hadoop 在本地扫描时 CPU 利用率高企、GC 频繁。
四、短路本地读取:零拷贝的巅峰之作
当满足以下条件时,HDFS 切换到短路读取路径:
dfs.client.read.shortcircuit = true(默认false)- 客户端与 DataNode 共置同一节点
- native 库(
libhadoop.so)可用(Domain Socket 依赖 native 层) dfs.domain.socket.path已正确配置(默认空字符串意为禁用)
其中,dfs.client.use.legacy.blockreader.local(默认 false)不是开关,而是选择短路读实现方式:
false(默认):使用新版BlockReaderLocal,依赖 Domain Socket 的SCM_RIGHTSfd 传递 + 共享内存true:使用旧版BlockReaderLocalLegacy,通过 DataNode 提供的块文件路径直接打开(不需要 Domain Socket)
两种实现都是短路读取,只是底层机制不同。
完整流程分为四步:
-
客户端通过 Unix domain socket 向 DataNode 发送短路请求——Protobuf 编码的
OpRequestShortCircuitAccessProto,携带块 ID(BaseHeaderProto.block)、客户端 token(BaseHeaderProto.token)、最大支持版本号(maxVersion)、可选的共享内存槽位 ID(slotId)以及回执验证标志(supportsReceiptVerification)。偏移量和长度不在此请求中,它们由后续BlockReaderLocal.read()调用按需指定。 -
DataNode 验证 token 通过后,在本侧打开块文件,使用
sendmsg()+SCM_RIGHTS将块文件 fd(data + meta 共两个)传递给客户端。 -
客户端 JNI 层(
DomainSocket.recvFileInputStreams())接收 fd,封装为FileInputStream,传入ShortCircuitReplica构造函数。ShortCircuitReplica内部可选调用FileChannel.map()建立 mmap 映射。 -
BlockReaderLocal从ShortCircuitReplica提取FileChannel,后续读取通过FileChannel.read()直接操作本地文件描述符——数据流完全绕过网络协议栈,只剩一次从 page cache 到用户缓冲区的拷贝。
此时,传统 HDFS 路径中 3 次 CPU 拷贝被缩短为 1 次(FileChannel.read():page cache → Client JVM 堆)。短路读的价值在于省去了 DataNode 进程和 TCP 协议栈的两次额外 CPU 拷贝。
更进一步,如果读取路径跳过了校验和验证(createNoChecksumContext() 返回 true),BlockReaderLocal 走 readWithoutBounceBuffer() 分支直接接收数据,避免中间 Bounce Buffer 的额外拷贝;若使用 ShortCircuitReplica.loadMmapInternal() 将块文件映射为 MappedByteBuffer,则 page cache 与用户地址空间共享同一物理页,CPU 拷贝降为 0 次。
五、源码解读:从配置决策到文件描述符传递的内核之旅
HDFS 短路读取的源码实现,分布在 hadoop-hdfs-client、hadoop-common、hadoop-hdfs 三个模块,核心类超过十个。下面按执行流程逐层拆解 Hadoop 3.x 主线的关键代码逻辑。
5.1 客户端入口:DFSInputStream 与 BlockReaderFactory 的决策智慧
读取起点在 org.apache.hadoop.hdfs.DFSInputStream:
// DFSInputStream.java
protected BlockReader getBlockReader(LocatedBlock targetBlock,
long offsetInBlock, long length, InetSocketAddress targetAddr,
StorageType storageType, DatanodeInfo datanode) throws IOException {
ExtendedBlock blk = targetBlock.getBlock();
Token<BlockTokenIdentifier> accessToken = targetBlock.getBlockToken();
CachingStrategy curCachingStrategy;
boolean shortCircuitForbidden;
synchronized (infoLock) {
curCachingStrategy = cachingStrategy;
shortCircuitForbidden = shortCircuitForbidden();
}
return new BlockReaderFactory(dfsClient.getConf()).
setInetSocketAddress(targetAddr).
setRemotePeerFactory(dfsClient).
setDatanodeInfo(datanode).
setStorageType(storageType).
setFileName(src).
setBlock(blk).
setBlockToken(accessToken).
setStartOffset(offsetInBlock).
setVerifyChecksum(verifyChecksum).
setClientName(dfsClient.clientName).
setLength(length).
setCachingStrategy(curCachingStrategy).
setAllowShortCircuitLocalReads(!shortCircuitForbidden).
setClientCacheContext(dfsClient.getClientContext()).
setUserGroupInformation(dfsClient.ugi).
setConfiguration(dfsClient.getConfiguration()).
build(); // 进入决策链路
}
真正决定走哪条路径的是 BlockReaderFactory.build(),Hadoop 3.x 实现了 四级降级链路:
// BlockReaderFactory.java
public BlockReader build() throws IOException {
Preconditions.checkNotNull(configuration);
Preconditions.checkState(length >= 0,
"Length must be set to a non-negative value");
// 第一级:外部块读取器(如 HDFS 缓存零拷贝访问器)
BlockReader reader = tryToCreateExternalBlockReader();
if (reader != null) {
return reader;
}
final ShortCircuitConf scConf = conf.getShortCircuitConf();
try {
if (scConf.isShortCircuitLocalReads() && allowShortCircuitLocalReads) {
if (clientContext.getUseLegacyBlockReaderLocal()) {
// 第二级:旧版短路读取(BlockReaderLocalLegacy)
reader = getLegacyBlockReaderLocal();
if (reader != null) return reader;
} else {
// 第三级:新版短路读取(共享内存 + fd 传递)
reader = getBlockReaderLocal();
if (reader != null) return reader;
}
}
if (scConf.isDomainSocketDataTraffic()) {
// 第四级:Domain Socket 数据流(无短路,但 socket 可用)
reader = getRemoteBlockReaderFromDomain();
if (reader != null) return reader;
}
} catch (IOException e) {
LOG.debug("Block read failed. Getting remote block reader using TCP", e);
}
// 最终兜底:TCP 远程读取
return getRemoteBlockReaderFromTcp();
}
降级链(Legacy 与 New 为互斥分支):
这里体现了 HDFS 的设计哲学:配置驱动 + 优雅降级。即使 short-circuit 配置打开,如果 native 库不可用或 domain socket 路径缺失,也会安全回退,不会导致任务失败。
5.2 控制通道建立:DomainSocket 的连接与请求发送
短路路径的核心控制通道由 DomainSocket 负责:
// DomainSocket.java
public static DomainSocket connect(String path) throws IOException {
if (loadingFailureReason != null) {
throw new UnsupportedOperationException(loadingFailureReason);
}
int fd = connect0(path); // 调用 native connect0()
return new DomainSocket(path, fd);
}
连接成功后,客户端通过 DomainSocket.getOutputStream() 发送短路请求。Hadoop 3.x 使用 Protobuf 编码的 OpRequestShortCircuitAccessProto,携带块 ID、token 和 slot ID。
5.3 DataNode 侧处理:DataXceiver 与 ShortCircuitRegistry 的协作
DataNode 的短路请求入口是 DataXceiver.requestShortCircuitFds():
// DataXceiver.java
public void requestShortCircuitFds(final ExtendedBlock blk,
final Token<BlockTokenIdentifier> token,
SlotId slotId, int maxVersion, boolean supportsReceiptVerification)
throws IOException {
updateCurrentThreadName("Passing file descriptors for block " + blk);
DataOutputStream out = getBufferedOutputStream();
// 1. 权限校验
checkAccess(out, true, blk, token,
Op.REQUEST_SHORT_CIRCUIT_FDS, BlockTokenIdentifier.AccessMode.READ,
null, null);
BlockOpResponseProto.Builder bld = BlockOpResponseProto.newBuilder();
FileInputStream fis[] = null;
SlotId registeredSlotId = null;
boolean success = false;
try {
try {
if (peer.getDomainSocket() == null) {
throw new IOException("You cannot pass file descriptors over " +
"anything but a UNIX domain socket.");
}
// 2. 在共享内存中注册 slot(绑定块 ID 到 slot 位置)
if (slotId != null) {
boolean isCached = datanode.data.
isCached(blk.getBlockPoolId(), blk.getBlockId());
datanode.shortCircuitRegistry.registerSlot(
ExtendedBlockId.fromExtendedBlock(blk), slotId, isCached);
registeredSlotId = slotId;
}
// 3. 打开块文件(返回 FileInputStream 数组:data + meta)
fis = datanode.requestShortCircuitFdsForRead(blk, token, maxVersion);
Preconditions.checkState(fis != null);
bld.setStatus(SUCCESS);
bld.setShortCircuitAccessVersion(
DataNode.CURRENT_BLOCK_FORMAT_VERSION);
} catch (ShortCircuitFdsVersionException e) {
bld.setStatus(ERROR_UNSUPPORTED);
bld.setShortCircuitAccessVersion(
DataNode.CURRENT_BLOCK_FORMAT_VERSION);
bld.setMessage(e.getMessage());
} catch (ShortCircuitFdsUnsupportedException e) {
bld.setStatus(ERROR_UNSUPPORTED);
bld.setMessage(e.getMessage());
} catch (IOException e) {
bld.setStatus(ERROR);
bld.setMessage(e.getMessage());
}
bld.build().writeDelimitedTo(socketOut);
// 4. 通过 SCM_RIGHTS 传递文件描述符
if (fis != null) {
FileDescriptor fds[] = new FileDescriptor[fis.length];
for (int i = 0; i < fds.length; i++) {
fds[i] = fis[i].getFD();
}
byte buf[] = new byte[1];
buf[0] = (byte)(supportsReceiptVerification
? USE_RECEIPT_VERIFICATION.getNumber()
: DO_NOT_USE_RECEIPT_VERIFICATION.getNumber());
DomainSocket sock = peer.getDomainSocket();
sock.sendFileDescriptors(fds, buf, 0, buf.length); // 内核级 fd 传递
success = true;
}
} finally {
// 失败则清理已注册的 slot
if ((!success) && (registeredSlotId != null)) {
datanode.shortCircuitRegistry.unregisterSlot(registeredSlotId);
}
// 关闭块文件流
if (fis != null) {
IOUtils.cleanup(null, fis);
}
}
}
ShortCircuitRegistry 负责共享内存段管理,DataNode.requestShortCircuitFdsForRead() 负责打开块文件,fd 的传递最终由 sock.sendFileDescriptors() 完成——三者各司其职,协作紧密。
5.4 文件描述符传递的内核之旅:SCM_RIGHTS 机制详解
真正执行 fd 传递的是 DomainSocket.c。
发送侧(sendFileDescriptors0 JNI):
// DomainSocket.c — 精简核心逻辑
struct cmsghdr_with_fds {
struct cmsghdr hdr;
int fds[MAX_PASSED_FDS];
} __attribute__((packed, aligned(8)));
Java_org_apache_hadoop_net_unix_DomainSocket_sendFileDescriptors0(
JNIEnv *env, jclass clazz, jint fd, jobject jfds, jobject jbuf,
jint offset, jint length) {
struct iovec vec[1];
struct cmsghdr_with_fds aux;
struct msghdr socketMsg;
int jfdsLen = (*env)->GetArrayLength(env, jfds);
int auxLen = CMSG_LEN(jfdsLen * sizeof(int));
memset(&aux, 0, auxLen);
memset(&socketMsg, 0, sizeof(socketMsg));
socketMsg.msg_iov = vec;
socketMsg.msg_iovlen = 1;
socketMsg.msg_control = &aux;
socketMsg.msg_controllen = auxLen;
aux.hdr.cmsg_len = auxLen;
aux.hdr.cmsg_level = SOL_SOCKET;
aux.hdr.cmsg_type = SCM_RIGHTS;
for (i = 0; i < jfdsLen; i++) {
jobject jfd = (*env)->GetObjectArrayElement(env, jfds, i);
aux.fds[i] = fd_get(env, jfd);
}
RETRY_ON_EINTR(ret, sendmsg(fd, &socketMsg, PLATFORM_SEND_FLAGS));
}
接收侧(receiveFileDescriptors0 JNI):
// DomainSocket.c— 精简核心逻辑
Java_org_apache_hadoop_net_unix_DomainSocket_receiveFileDescriptors0(
JNIEnv *env, jclass clazz, jint fd, jarray jfds, jarray jbuf,
jint offset, jint length) {
struct iovec vec[1];
struct cmsghdr_with_fds aux;
struct msghdr socketMsg;
int auxLen = CMSG_LEN(jfdsLen * sizeof(int));
memset(&aux, 0, auxLen);
memset(&socketMsg, 0, auxLen);
socketMsg.msg_iov = vec;
socketMsg.msg_iovlen = 1;
socketMsg.msg_control = &aux;
socketMsg.msg_controllen = auxLen;
aux.hdr.cmsg_len = auxLen;
aux.hdr.cmsg_level = SOL_SOCKET;
aux.hdr.cmsg_type = SCM_RIGHTS;
RETRY_ON_EINTR(bytesRead, recvmsg(fd, &socketMsg, 0));
int jRecvFdsLen =
(aux.hdr.cmsg_len - sizeof(struct cmsghdr)) / sizeof(int);
for (i = 0; i < jRecvFdsLen; i++) {
fdObj = jniCreateFileDescriptor(env, aux.fds[i]);
(*env)->SetObjectArrayElement(env, jfds, i, fdObj);
}
}
内核在收到 SCM_RIGHTS 后:查找发送方传递的 struct file → 为接收进程分配新 fd 槽位 → 设置新 fd 指向同一 struct file(引用计数 +1) → 返回给用户态。整个过程完全由内核完成,无需额外拷贝,安全性由 domain socket 文件权限 + DataNode 的 checkAccess() 凭证校验双重保障。
5.5 mmap 映射与 BlockReaderLocal 读取
接收到 fd 后,ShortCircuitReplica 负责建立 mmap 映射:
// ShortCircuitReplica.java
MappedByteBuffer loadMmapInternal() {
try {
FileChannel channel = dataStream.getChannel();
MappedByteBuffer mmap = channel.map(MapMode.READ_ONLY, 0,
Math.min(Integer.MAX_VALUE, channel.size())); // Java NIO mmap
LOG.trace("{}: created mmap of size {}", this, channel.size());
return mmap;
} catch (IOException e) {
LOG.warn(this + ": mmap error", e);
return null;
} catch (RuntimeException e) {
LOG.warn(this + ": mmap error", e);
return null;
}
}
随后 BlockReaderLocal 接管读取,核心是两条分支——零拷贝路径 vs 校验和路径:
// BlockReaderLocal.java
@Override
public synchronized int read(ByteBuffer buf) throws IOException {
boolean canSkipChecksum = createNoChecksumContext();
try {
int nRead;
try {
if (canSkipChecksum && zeroReadaheadRequested) {
nRead = readWithoutBounceBuffer(buf); // 零拷贝路径
} else {
nRead = readWithBounceBuffer(buf, canSkipChecksum); // 校验和路径
}
} catch (IOException e) {
throw e;
}
return nRead;
} finally {
if (canSkipChecksum) releaseNoChecksumContext();
}
}
// 零拷贝读取——直接操作 FileChannel,无中间缓冲区
private synchronized int readWithoutBounceBuffer(ByteBuffer buf)
throws IOException {
freeDataBufIfExists();
freeChecksumBufIfExists();
int total = 0;
while (buf.hasRemaining()) {
int nRead = blockReaderIoProvider.read(dataIn, buf, dataPos);
if (nRead <= 0) break;
dataPos += nRead;
total += nRead;
}
return (total == 0 && (dataPos == dataIn.size())) ? -1 : total;
}
Hadoop 3.x 的 BlockReaderLocal 零拷贝路径直接通过 FileChannel.read() 操作底层文件描述符,配合 MappedByteBuffer 即可获得 mmap 的零拷贝收益。Hadoop 2.x 早期实验性的 sun.misc.Unsafe 在 3.x 主线中已被彻底移除。
5.6 缓存与生命周期管理:ShortCircuitCache 的艺术
为了避免高并发下频繁连接和 mmap,客户端维护 ShortCircuitCache:
// ShortCircuitCache.java
public ShortCircuitReplicaInfo fetchOrCreate(ExtendedBlockId key,
ShortCircuitReplicaCreator creator) {
Waitable<ShortCircuitReplicaInfo> newWaitable;
lock.lock();
try {
ShortCircuitReplicaInfo info = null;
for (int i = 0; i < FETCH_OR_CREATE_RETRY_TIMES; i++) {
if (closed) return null;
Waitable<ShortCircuitReplicaInfo> waitable = replicaInfoMap.get(key);
if (waitable != null) {
try {
info = fetch(key, waitable); // 已有副本,等待或直接返回
break;
} catch (RetriableException e) {
LOG.debug("{}: retrying {}", this, e.getMessage());
}
}
}
if (info != null) return info;
// 缓存未命中,需新建
newWaitable = new Waitable<>(lock.newCondition());
replicaInfoMap.put(key, newWaitable);
} finally {
lock.unlock();
}
return create(key, creator, newWaitable); // 真正创建 ShortCircuitReplica
}
replicaInfoMap 使用 HashMap<> + ReentrantLock 保护。Waitable 模式允许同一个 block 的并发请求共享等待同一个创建过程,避免重复创建。
缓存受以下配置控制:
dfs.client.read.shortcircuit.streams.cache.size:最大缓存副本数(默认 256)dfs.client.read.shortcircuit.streams.cache.expiry.ms:副本过期时间(默认 5 分钟)
当缓存满或过期时,自动执行 NativeIO.POSIX.munmap(mmap) + close(fd),避免 fd 泄漏和虚拟地址空间耗尽。
结语:内核智慧的分布式升华
HDFS 短路读取不是简单的功能叠加,而是对操作系统哲学的深刻致敬。它把最繁重的拷贝与调度工作,交还给内核的 page cache 和缺页中断机制;把最安全的授权,交给 Unix Domain Socket 的 SCM_RIGHTS;把极致的性能,交给 mmap 后的指针访问。
在云计算、AI 大模型、实时湖仓一体化的今天,当我们追求极致吞吐与最低延迟时,这项诞生于十余年前的技术依然闪耀。它提醒每一位工程师:真正的优化,往往不是堆砌新特性,而是让系统回归最本质、最优雅的设计——让内核做它最擅长的事,让代码保持简单而强大。
当你下次运行 Spark 任务,看到 CPU 曲线平稳、扫描速度飞跃时,请记得——背后,正是 mmap 与 Unix Domain Socket 在内核深处,悄然完成了一场零拷贝的华丽舞蹈。而 HDFS 的源码,则是这场舞蹈最精美的乐谱。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)