2022版Netty面试题小总结

共 11461字,需浏览 23分钟

 ·

2022-06-09 12:43


点击上方蓝色字体,选择“设为星标”
回复"面试"获取更多惊喜

轻戳有惊喜:全网最全大数据面试提升手册!

一. 基本概念

1、什么是 Netty?

Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序,是目前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的 Elasticsearch 、Dubbo 框架内部都采用了 Netty。

2、Netty 的优势?
  1. 使用简单:封闭了 Java 原生 NIO 类库繁琐的 API,使用起来更加高效;

  2. 功能强大:预置多种编码能力,支持多种主流协议。同时通过 ChannelHandler 可以进行灵活的拓展,支持很强的定制能力;

  3. 高性能:与其它业界主流 NIO 框架相比,Netty 综合更优。主要体现在吞吐量更高、延迟更低、减少资源消耗以及最小化不必要的内存复制;

  4. 社区活跃与稳定:版本更新周期短,BUG 修复速度快,让开发者可以专注业务本身。

3、Netty 有什么特点?
  1. 高并发:Netty 是一款基于 NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架。

  2. 传输快:Netty 使用零拷贝特性,尽量减少不必要的内存拷贝,实现更快的传输效率。

  3. 封装好:Netty 封装了 NIO 操作的很多细节,提供易于使用的 API。

4、Netty 有哪些应用场景?

理论上来说,NIO 可以做的事情,Netty 都可以做并且更好。Netty 主要用来做网络通信:

  • RPC 框架的网络通信工具;
  • 实现一个 HTTP 服务器;
  • 实现一个即时通讯系统;
  • 实现消息推送系统。
5、Netty 的高性能体现在?
  • IO 线程模型:同步非阻塞;
  • 零拷贝:尽量做到不必要的内存拷贝:
  • 内存池设计:使用直接内存,并且可重复利用;
  • 串行化处理读写:避免使用锁带来的额外开销;
  • 高性能序列化协议:支持 protobuf 等高性能序列化协议。
6、相比原生 NIO 的优势?
  1. 易用性:Netty 在 NIO 基础上封装了更加人性化的 API,大大降低开发人员的学习成本,同时还提供了很多开箱即用的工具。

  2. 稳定性:Netty 修复了 Java NIO 较多已知问题,如 select 空转导致 CPU 100%,TCP 断线重连,Keep-alive 检测等问题。

  3. 高性能:对象池复用(通过对象复用避免频繁创建和销毁带来的开销)和零拷贝技术。

7、Netty 和 Tomcat 的区别?

Netty 和 Tomcat 最大的区别在于对通信协议的支持:

  • Tomcat 是基于 Http 协议的,本质是一个基于 http 协议的 web 容器,而 Netty 不仅支持 HTTP,还能通过编程自定义各种协议,通过 codec 自定义编码/解码字节流,完成数据传输。

  • Tomcat 需要遵循 Servlet 规范(HTTP 协议的请求-响应模型),而 Netty 不需要受到 Servlet 规范约束,可以发挥 NIO 最大特性。

8、BIO. NIO. AIO 分别是什么?
  • BIO(同步阻塞 IO)

服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK 1.4 以前的唯一选择,但程序直观简单易理解。

  • NIO(同步非阻塞 IO)

服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 IO 请求时才启动一个线程进行处理。NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK 1.4 开始支持。

  • AIO(异步非阻塞 IO)

服务器实现模式为一个有效请求一个线程,客户端的 IO 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK 1.7 开始支持。

9、Select、Poll、Epoll 的区别?
10、什么是 Reactor 模型?

二. 架构组件

1、Netty 有哪些核心组件?
  • Channel

基础的 IO 操作,如绑定、连接、读写等都依赖于底层网络传输所提供的原语,在 Java 的网络编程中,基础核心类是 Socket,而 Netty 的 Channel 提供了一组 API,极大地简化了直接与 Socket 进行操作的复杂性,并且 Channel 是很多类的父类,如 EmbeddedChannel、LocalServerChannel、NioDatagramChannel、NioSctpChannel、NioSocketChannel 等。

  • EventLoop

EventLoop 定义了处理在连接过程中发生的事件的核心抽象。

说白了,EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 IO 操作的处理。

那 Channel 和 EventLoop 直接有啥联系呢?

Channel 为 Netty 网络操作(读写等操作)抽象类,EventLoop 负责处理注册到其上的 Channel 处理 IO 操作,两者配合参与 IO 操作。

  • ChannelFuture

由于 Netty 是异步非阻塞的,所有的 IO 操作也都为异步的,我们不能立刻得到操作是否执行成功,因此 Netty 提供 ChannelFuture 接口,使用其 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。

并且,我们还可以通过 ChannelFuture 的 channel() 方法获取关联的Channel,甚至使用 sync() 方法让异步的操作变成同步的。

  • ChannelHandler 和 ChannelPipeline

从应用开发者看来,ChannelHandler 是最重要的组件,其中存放用来处理进站和出站数据的用户逻辑。ChannelHandler 的方法被网络事件触发,可以用于几乎任何类型的操作,如将数据从一种格式转换为另一种格式或处理抛出的异常。如其子接口ChannelInboundHandler,接受进站的事件和数据以便被用户定义的逻辑处理,或者当响应所连接的客户端时刷新ChannelInboundHandler的数据。

ChannelPipeline为ChannelHandler 链提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API。当创建 Channel 时,会自动创建一个附属的 ChannelPipeline。

  • Bootstrap 和 ServerBootstrap

Netty 的引导类应用程序网络层配置提供容器,其涉及将进程绑定到给定端口或连接一个进程到在指定主机上指定端口上运行的另一进程。引导类分为客户端引导 Bootstrap 和服务端引导 ServerBootstrap。

2、什么是 EventLoop 和 EventLoopGroup?

EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程),上面我们已经说了 EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。

并且 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,即 Thread 和 EventLoop 属于 1 : 1 的关系,从而保证线程安全。

上图是一个服务端对 EventLoopGroup 使用的大致模块图,其中 Boss EventloopGroup 用于接收连接,Worker EventloopGroup 用于具体的处理(消息的读写以及其他逻辑处理)。

从上图可以看出:当客户端通过 connect 方法连接服务端时,bossGroup 处理客户端连接请求。当客户端处理完成后,会将这个连接提交给 workerGroup 来处理,然后 workerGroup 负责处理其 IO 相关操作。

3、说说 Netty 的线程模型?

Netty 通过 Reactor 模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss 线程池和 work 线程池,其中 boss 线程池的线程负责处理请求的 accept 事件,当接收到 accept 事件的请求时,把对应的 socket 封装到一个 NioSocketChannel 中,并交给 work 线程池,其中 work 线程池负责请求的 read 和 write 事件,由对应的 Handler 处理。

  • 单线程模型

所有 IO 操作都由一个线程完成,即多路复用、事件分发和处理都是在一个 Reactor 线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送、读取请求或应答、响应消息。一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,速度慢,若线程进入死循环,整个程序不可用,对于高负载、高并发的应用场景不合适。

//1.eventGroup既用于处理客户端连接,又负责具体的处理。 
EventLoopGroup eventGroup = new NioEventLoopGroup(1); 
//2.创建服务端启动引导/辅助类:
ServerBootstrap ServerBootstrap b = new ServerBootstrap(); 
boobtstrap.group(eventGroup, eventGroup) 
//......
  • 多线程模型

有一个 NIO 线程(Acceptor) 只负责监听服务端,接收客户端的 TCP 连接请求;NIO 线程池负责网络 IO 的操作,即消息的读取、解码、编码和发送;1 个 NIO 线程可以同时处理 N 条链路,但是 1 个链路只对应 1 个 NIO 线程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时,一个 Acceptor 线程可能会存在性能不足问题。

对于代码:

// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try { 
    //2.创建服务端启动引导/辅助类:
    ServerBootstrap ServerBootstrap b = new ServerBootstrap(); 
    //3.给引导类配置两大线程组,确定了线程模型 
    b.group(bossGroup, workerGroup) 
    //......
}
  • 主从多线程模型

Acceptor 线程用于绑定监听端口,接收客户端连接,将 SocketChannel 从主线程池的 Reactor 线程的多路复用器上移除,重新注册到 Sub 线程池的线程上,用于处理 IO 的读写等操作,从而保证主 Reactor 只负责接入认证、握手等操作。如果多线程模型无法满足你的需求的时候,可以考虑使用主从多线程模型 。

// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try { 
    //2.创建服务端启动引导/辅助类:
    ServerBootstrap ServerBootstrap b = new ServerBootstrap(); 
    //3.给引导类配置两大线程组,确定了线程模型 
    b.group(bossGroup, workerGroup) 
    //......
}
4、Netty 服务端的启动过程?

先来看一段代码实现:

// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 
EventLoopGroup bossGroup = new NioEventLoopGroup(1); 
EventLoopGroup workerGroup = new NioEventLoopGroup(); 
try { 
    //2.创建服务端启动引导/辅助类:ServerBootstrap
    ServerBootstrap b = new ServerBootstrap(); 
    //3.给引导类配置两大线程组,确定了线程模型 
    b.group(bossGroup, workerGroup) // (非必备)打印日志 
    .handler(new LoggingHandler(LogLevel.INFO)) // 4.指定 IO 模型 
    .channel(NioServerSocketChannel.class) 
    .childHandler(new ChannelInitializer() {
         @Override 
         public void initChannel(SocketChannel ch) {
             ChannelPipeline p = ch.pipeline(); 
             //5.可以自定义客户端消息的业务处理逻辑 
             p.addLast(new HelloServerHandler()); 
        } 
    }); 
    // 6.绑定端口,调用 sync 方法阻塞知道绑定完成 
    ChannelFuture f = b.bind(port).sync();
    // 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法) 
    f.channel().closeFuture().sync(); 
} finally { 
    //8.优雅关闭相关线程组资源 
    bossGroup.shutdownGracefully(); 
    workerGroup.shutdownGracefully(); 
}

三. 具体实现

1、Netty 的无锁化体现在哪里?

Netty 采用了串行无锁化设计,在 IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。

Netty 的 NioEventLoop 读取到消息后,直接调用 ChannelPipeline 的 fireChannelRead(Object msg),只要用户不主动切换线程,一直会由 NioEventLoop 调用到用户的 handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁竞争,从性能角度看是最优的。

2、如何解决 JDK epoll 空轮询问题?

这个 BUG 是指 Java 的 NIO 在 Linux 下进行 selector.select() 时,本来如果轮询的结果为空并且不调用 wakeup 方法的话,这个 selector.select() 应该是一直阻塞的,但是 Java 却会打破阻塞,继续执行,导致程序无限空转,造成 CPU 使用率 100%。(这个问题只存在 Linux 是因为 Linux 的 NIO 是基于 epoll 实现的,而 Java 实现的 epoll 存在 BUG,windows 下 NIO 基于 poll 就不存在此问题)

Netty 的解决方案:

为 Selector 的 select 操作设置超时时间,同时定义可以跳出阻塞的四种情况

  • 有事件发生
  • wakeup
  • 超时
  • 空轮询 BUG

而前两种返回值不为 0,可以跳出循环,超时有时间戳记录,所以每次空轮询,有专门的计数器进行 +1,如果空轮询的次数超过了 512 次(默认),就认为其触发了空轮询 BUG。

当触发 BUG 后,Netty 直接重建一个 Selector,将原来的 Channel 重新注册到新的 Selector 上,并将旧的 Selector 关掉。

3、什么是拆包和粘包?

TCP 是一个面向流的传输协议,所谓流,就是没有界限的一串数据。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。

粘包和拆包是 TCP 网络编程中不可避免的,无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑 TCP 底层的粘包/拆包机制。

数据从发送方到接收方需要经过操作系统的缓冲区,而造成粘包和拆包的主要原因就在这个缓冲区上。粘包可以理解为缓冲区数据堆积,导致多个请求数据粘在一起,而拆包可以理解为发送的数据大于缓冲区,进行拆分处理。

详细来说,造成粘包和拆包的原因主要有以下三个:

  1. 应用程序 write 写入的字节大小大于套接口发送缓冲区大小;

  2. 进行 MSS 大小的 TCP 分段;

  3. 以太网帧的 payload 大于 MTU 进行 IP 分片。

4、拆包粘包的解决方案?

由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议可以归纳出以下解决方案:

  1. 消息长度固定,累计读取到长度和为定长LEN的报文后,就认为读取到了一个完整的信息。

  2. 将特殊的分隔符作为消息的结束标志,如回车换行符。

  3. 通过在消息头中定义长度字段来标识消息的总长度。

5、Netty 如何解决拆包粘包?

相比粘包,拆包问题比较简单,用户可以自己定义自己的编码器进行处理,Netty 并没有提供相应的组件。对于粘包的问题,代码比较繁琐,Netty 提供了 4 种解码器来解决,分别如下:

  • 固定长度的拆包器(FixedLengthFrameDecoder),每个应用层数据包的都拆分成都是固定长度的大小;

  • 行拆包器(LineBasedFrameDecoder),每个应用层数据包都以换行符作为分隔符,进行分割拆分;

  • 分隔符拆包器(DelimiterBasedFrameDecoder),每个应用层数据包,都通过自定义的分隔符,进行分割拆分;

  • 基于数据包长度的拆包器(LengthFieldBasedFrameDecoder),将应用层数据包的长度,作为接收端应用层数据包的拆分依据。按照应用层数据包的大小,拆包。这个拆包器,有一个要求,就是应用层协议中包含数据包的长度。

6、Netty 零拷贝体现在哪里?

Zero-copy 就是在操作数据时, 不需要将数据 buffer从 一个内存区域拷贝到另一个内存区域。少了一次内存的拷贝,CPU 效率就得到的提升。

接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝;

提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作;

文件传输采用了 transferTo 方法,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。

和操作系统上的零拷贝的区别?

Netty 的 Zero-copy 完全是在用户态(Java 应用层)的, 更多的偏向于优化数据操作。而在 OS 层面上的 Zero-copy 通常指避免在用户态(User-space)与内核态(Kernel-space)之间来回拷贝数据。

7、TCP 的长连接和短连接?

我们知道 TCP 在进行读写之前,server 与 client 之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。

所谓,短连接说的就是 server 端 与 client 端建立连接之后,读写完成之后就关闭掉连接,如果下一次再要互相发送消息,就要重新连接。短连接的有点很明显,就是管理和实现都比较简单,缺点也很明显,每一次的读写都要建立连接必然会带来大量网络资源的消耗,并且连接的建立也需要耗费时间。

长连接说的就是 client 向 server 双方建立连接之后,即使 client 与 server 完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。长连接的可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的依赖,节约时间。对于频繁请求资源的客户来说,非常适用长连接。

8、Netty 长连接、心跳机制了解么?

在 TCP 保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,它们是无法发现对方已经掉线的。为了解决这个问题, 我们就需要引入心跳机制。

心跳机制的工作原理是: 在 client 与 server 之间在一定时间内没有数据交互(即处于 idle 状态)时, 客户端或服务器就会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。所以, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性。

TCP 实际上自带的就有长连接选项,本身是也有心跳包机制,也就是 TCP 的选项:SO_KEEPALIVE。但 TCP 协议层面的长连接灵活性不够,所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。通过 Netty 实现心跳机制的话,核心类是 IdleStateHandler 。

9、说说 Netty 的对象池技术?

对象池其实就是缓存一些对象从而避免大量创建同一个类型的对象,类似线程池的概念。对象池缓存了一些已经创建好的对象,避免需要时才创建对象,同时限制了实例的个数。池化技术最终要的就是重复的使用池内已经创建的对象。从上面的内容就可以看出对象池适用于以下几个场景:

  • 创建对象的开销大;
  • 会创建大量的实例;
  • 限制一些资源的使用。

Netty 自己实现了一套轻量级的对象池。在 Netty 中,通常会有多个 IO 线程独立工作(基于 NioEventLoop 实现)。每个 IO 线程轮询单独的 Selector 实例来检索 IO 事件,并在 IO 来临时开始处理。最常见的 IO 操作就是读写,具体到 NIO 就是从内核缓冲区拷贝数据到用户缓冲区或者从用户缓冲区拷贝数据到内核缓冲区。这里会涉及到大量的创建和回收 Buffer, Netty 对 Buffer 进行了池化从而降低系统开销。

10、有哪些序列化协议?

序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等;而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象的解码,以便完成远程调用。

影响序列化性能的关键因素:序列化后的码流大小(网络带宽的占用)、序列化的性能(CPU资源占用);是否支持跨语言(异构系统的对接和开发语言切换)。

目前几种主流协议:

  • Java 默认提供的序列化

无法跨语言、序列化后的码流太大、序列化的性能差。

  • XML

优点是人机可读性好,可指定元素或特性的名称。缺点:序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。适用场景:当做配置文件存储数据,实时数据转换。

  • JSON

是一种轻量级的数据交换格式,优点:兼容性高、数据格式比较简单,易于读写、序列化后数据较小,可扩展性好,兼容性好、与XML相比,其协议比较简单,解析速度比较快。缺点:数据的描述性比XML差、不适合性能要求为ms级别的情况、额外空间开销比较大。适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于Web browser的Ajax请求、传输数据量相对小,实时性要求相对低(例如秒级别)的服务。

  • Fastjson

采用一种“假定有序快速匹配”的算法。优点:接口简单易用、目前java语言中最快的json库。缺点:过于注重快,而偏离了“标准”及功能性、代码质量不高,文档不全。适用场景:协议交互、Web输出、Android客户端。

  • Thrift

不仅是序列化协议,还是一个RPC框架。优点:序列化后的体积小, 速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编码。缺点:使用者较少、跨防火墙访问时,不安全、不具有可读性,调试代码时相对困难、不能与其他传输层协议共同使用(例如HTTP)、无法支持向持久层直接读写数据,即不适合做数据持久化序列化协议。适用场景:分布式系统的RPC解决方案。

  • Avro

Hadoop的一个子项目,解决了JSON的冗长和没有IDL的问题。优点:支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用RPC、支持跨编程语言实现。缺点:对于习惯于静态类型语言的用户不直观。适用场景:在Hadoop中做Hive、Pig和MapReduce的持久化数据格式。

  • Protobuf

将数据结构以.proto文件进行描述,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。优点:序列化后码流小,性能高、结构化数据存储格式(XML JSON等)、通过标识字段的顺序,可以实现协议的前向兼容、结构化的文档更容易管理和维护。缺点:需要依赖于工具生成代码、支持的语言相对较少,官方只支持Java 、C++ 、python。适用场景:对性能要求高的RPC调用、具有良好的跨防火墙的访问属性、适合应用层对象的持久化。

如果这个文章对你有帮助,不要忘记 「在看」 「点赞」 「收藏」 三连啊喂!


2022年全网首发|大数据专家级技能模型与学习指南(胜天半子篇)
互联网最坏的时代可能真的来了
我在B站读大学,大数据专业
我们在学习Flink的时候,到底在学习什么?
193篇文章暴揍Flink,这个合集你需要关注一下
Flink生产环境TOP难题与优化,阿里巴巴藏经阁YYDS
Flink CDC我吃定了耶稣也留不住他!| Flink CDC线上问题小盘点
我们在学习Spark的时候,到底在学习什么?
在所有Spark模块中,我愿称SparkSQL为最强!
硬刚Hive | 4万字基础调优面试小总结
数据治理方法论和实践小百科全书
标签体系下的用户画像建设小指南
4万字长文 | ClickHouse基础&实践&调优全视角解析
【面试&个人成长】2021年过半,社招和校招的经验之谈
大数据方向另一个十年开启 |《硬刚系列》第一版完结
我写过的关于成长/面试/职场进阶的文章
当我们在学习Hive的时候在学习什么?「硬刚Hive续集」
浏览 33
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报