腾讯云后端15连问!(一)

tony_ybliu
发布于 2022-5-30 17:31
浏览
0收藏

 

前言 
大家好,我是捡田螺的小男孩,最近一位朋友(6年工作经验)面了腾讯云,以下是面试题和答案。加油,一起卷。

  1. 聊聊项目,好的设计,好的代码
  2. 谈谈什么是零拷贝?
  3. 一共有几种 IO 模型?NIO 和多路复用的区别?
  4. Future 实现阻塞等待获取结果的原理?
  5. ReentrantLock和 Synchronized 的区别?Synchronized 的原理?
  6. 聊聊AOS?ReentrantLock的实现原理?
  7. 乐观锁和悲观锁, 让你来写你怎么实现?
  8. Paxos 协议了解?工作流程是怎么样的?
  9. B+树聊一下?B+树是不是有序?B+树和B-树的主要区别?
  10. TCP的拥塞机制
  11. 工作中有过JVM实践嘛
  12. 数据库分库分表的缺点是啥?
  13. 分布式事务如何解决?TCC 了解?
  14. RocketMQ 如何保证消息的准确性和安全性?
  15. 算法题:三个数求和

 

1.聊聊项目,好的设计,好的代码 
项目的话,你可以聊聊你平时做的项目,尤其有亮点的项目。如果没有什么特别亮点的项目,也可以说说一些好的设计,或者你优化了什么接口,性能提升了多少,优化了什么慢SQL都可以。甚至是一些好的代码写法都可以。

 

如果是讲优化接口的话,你可以看下我这篇文章哈:

 

记一次接口性能优化实践总结:优化接口性能的八个建议

 

如果是代码优化细节,可以看我这篇:

 

工作四年,分享50个让你代码更好的小建议

 

如果是慢SQL优化,可以看下我之前MySQL专栏系列文章哈:

  • 看一遍就理解:order by详解
  • 看一遍就理解:group by详解
  • 实战!聊聊如何解决MySQL深分页问题
  • 后端程序员必备:书写高质量SQL的30条建议
  • 阿里一面,给了几条SQL,问需要执行几次树搜索操作?
  • 生产问题分析!delete in子查询不走索引?!

 

2. 谈谈什么是零拷贝? 
零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及CPU的拷贝时间。它是一种I/O操作优化技术。

传统 IO 的执行流程

传统的IO流程,包括read和write的过程。

  • read:把数据从磁盘读取到内核缓冲区,再拷贝到用户缓冲区
  • write:先把数据写入到socket缓冲区,最后写入网卡设备。
     腾讯云后端15连问!(一)-鸿蒙开发者社区
  1. 用户应用进程调用read函数,向操作系统发起IO调用,上下文从用户态转为内核态(切换1)
  2. DMA控制器把数据从磁盘中,读取到内核缓冲区。
  3. CPU把内核缓冲区数据,拷贝到用户应用缓冲区,上下文从内核态转为用户态(切换2),read函数返回
  4. 用户应用进程通过write函数,发起IO调用,上下文从用户态转为内核态(切换3)
  5. CPU将用户缓冲区中的数据,拷贝到socket缓冲区
  6. DMA控制器把数据从socket缓冲区,拷贝到网卡设备,上下文从内核态切换回用户态(切换4),write函数返回
    传统IO的读写流程,包括了4次上下文切换(4次用户态和内核态的切换),4次数据拷贝(两次CPU拷贝以及两次的DMA拷贝)。

零拷贝实现方式:

零拷贝并不是没有拷贝数据,而是减少用户态/内核态的切换次数以及CPU拷贝的次数。零拷贝一般有这三种实现方式:

  • mmap+write
  • sendfile
  • 带有DMA收集拷贝功能的sendfile
    mmap+write

mmap就是用了虚拟内存这个特点,它将内核中的读缓冲区与用户空间的缓冲区进行映射,以减少数据拷贝次数!

 腾讯云后端15连问!(一)-鸿蒙开发者社区

  1. 用户进程通过mmap方法向操作系统内核发起IO调用,上下文从用户态切换为内核态。
  2. CPU利用DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
  3. 上下文从内核态切换回用户态,mmap方法返回。
  4. 用户进程通过write方法向操作系统内核发起IO调用,上下文从用户态切换为内核态。
  5. CPU将内核缓冲区的数据拷贝到的socket缓冲区。
  6. CPU利用DMA控制器,把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write调用返回。
    mmap+write实现的零拷贝,I/O发生了4次用户空间与内核空间的上下文切换,以及3次数据拷贝(包括了2次DMA拷贝和1次CPU拷贝)。

sendfile

sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作

 腾讯云后端15连问!(一)-鸿蒙开发者社区

  1. 用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态
  2. DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
  3. CPU将读缓冲区中数据拷贝到socket缓冲区
  4. DMA控制器,异步把数据从socket缓冲区拷贝到网卡,
  5. 上下文(切换2)从内核态切换回用户态,sendfile调用返回。
    sendfile实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中,包括了2次DMA拷贝和1次CPU拷贝。

带有DMA收集拷贝功能的sendfile

linux 2.4版本之后,对sendfile做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点搞零拷贝,即还可以多省去一次CPU拷贝。

 腾讯云后端15连问!(一)-鸿蒙开发者社区

  1. 用户进程发起sendfile系统调用,上下文(切换1)从用户态转向内核态
  2. DMA控制器,把数据从硬盘中拷贝到内核缓冲区。
  3. CPU把内核缓冲区中的文件描述符信息(包括内核缓冲区的内存地址和偏移量)发送到socket缓冲区
  4. DMA控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
  5. 上下文(切换2)从内核态切换回用户态,sendfile调用返回。
    可以发现,sendfile+DMA scatter/gather实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及2次数据拷贝。其中2次数据拷贝都是包DMA拷贝。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过CPU来搬运数据,所有的数据都是通过DMA来进行传输的。

看一遍就理解:零拷贝详解

 

3. 一共有几种 IO 模型?NIO 和多路复用的区别? 
一共有五种IO模型

  • 阻塞IO模型
  • 非阻塞IO模型
  • IO多路复用模型
  • IO模型之信号驱动模型
  • IO 模型之异步IO(AIO)
    NIO(非阻塞IO模型)

NIO,即Non-Blocking IO,是非阻塞IO模型。非阻塞IO的流程如下:

 腾讯云后端15连问!(一)-鸿蒙开发者社区

  1. 应用进程向操作系统内核,发起recvfrom读取数据。
  2. 操作系统内核数据没有准备好,立即返回EWOULDBLOCK错误码。
  3. 应用程序进程轮询调用,继续向操作系统内核发起recvfrom读取数据。
  4. 操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间。
  5. 完成调用,返回成功提示。
    NIO(非阻塞IO模型)存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的CPU资源。可以考虑IO复用模型去解决这个问题。

IO多路复用模型

IO多路复用就是,等到内核数据准备好了,主动通知应用进程再去进行系统调用。

IO复用模型核心思路:系统给我们提供一类函数(如我们耳濡目染的select、poll、epoll函数),它们可以同时监控多个fd的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom系统调用。

IO多路复用之select

应用进程通过调用select函数,可以同时监控多个fd,在select函数监控的fd中,只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时应用进程再发起recvfrom请求去读取数据。

 腾讯云后端15连问!(一)-鸿蒙开发者社区
非阻塞IO模型(NIO)中,需要N(N>=1)次轮询系统调用,然而借助select的IO多路复用模型,只需要发起一次询问就够了,大大优化了性能。

但是呢,select有几个缺点:

  • 监听的IO最大连接数有限,在Linux系统上一般为1024。
  • select函数返回后,是通过遍历fdset,找到就绪的描述符fd。(仅知道有I/O事件发生,却不知是哪几个流,所以遍历所有流)
    因为存在连接数限制,所以后来又提出了poll。与select相比,poll解决了连接数限制问题。但是呢,select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的socket。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降。

IO多路复用之epoll

为了解决select/poll存在的问题,多路复用模型epoll诞生,它采用事件驱动来实现,流程图如下:

 腾讯云后端15连问!(一)-鸿蒙开发者社区
epoll先通过epoll_ctl()来注册一个fd(文件描述符),一旦基于某个fd就绪时,内核会采用回调机制,迅速激活这个fd,当进程调用epoll_wait()时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的机制。这就是epoll的亮点。

 

4. Future 实现阻塞等待获取结果的原理? 
Future.get()用于异步结果的获取。它是阻塞的,背后原理是什么呢?

我们可以看下FutureTask的类结构图:

 腾讯云后端15连问!(一)-鸿蒙开发者社区
FutureTask实现了RunnableFuture接口,RunnableFuture继承了RunnableFuture这两个接口, 对于Runnable,我们太熟悉了, 那么Future呢?

Future 表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);
    //Future 是否被取消
    boolean isCancelled();
    //当前 Future 是否已结束
    boolean isDone();
    //或取Future的结果值。如果当前 Future 还没有结束,当前线程阻塞等待,
    V get() throws InterruptedException, ExecutionException;
    //获取 Future 的结果值。与 get()一样,不过多了超时时间设置
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

 

FutureTask 就是RunnableFuture的结合体,我们可以把Runnable看作生产者, Future 看作消费者。而FutureTask 是被这两者共享的,生产者运行run方法计算结果,消费者通过get方法获取结果。

生产者消费者模式,如果生产者数据还没准备的时候,消费者会被阻塞。当生产者数据准备好了以后会唤醒消费者继续执行。我们来看下FutureTask内部是如何实现的。

FutureTask内部维护了任务状态state

//NEW 新建状态,表示FutureTask新建还没开始执行
private static final int NEW          = 0;
//完成状态,表示FutureTask
private static final int COMPLETING   = 1;
//任务正常完成,没有发生异常
private static final int NORMAL       = 2;
//发生异常
private static final int EXCEPTIONAL  = 3;
//取消任务
private static final int CANCELLED    = 4;
//发起中断请求
private static final int INTERRUPTING = 5;
//中断请求完成
private static final int INTERRUPTED  = 6;

生产者run方法:

 public void run() {
        // 如果状态state不是 NEW,或者设置 runner 值失败,直接返回
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //调用callable的call方法,获取结果
                    result = c.call();
                    //运行成功
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    //运行不成功
                    ran = false;
                    //设置异常
                    setException(ex);
                }
                //运行成功设置返回结果
                if (ran)
                    set(result);
            }
        } finally {
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }


消费者的get方法

 public V get() throws InterruptedException, ExecutionException {
     int s = state;
     //如果状态小于等于 COMPLETING,表示 FutureTask 任务还没有完成, 则调用awaitDone让当前线程等待。
     if (s <= COMPLETING)
         s = awaitDone(false, 0L);
     return report(s);
 }

 

awaitDone做了什么事情呢?

 private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        WaitNode q = null;
        boolean queued = false;
        for (;;) {
            // 如果当前线程是中断标记,则  
            if (Thread.interrupted()) {
                //那么从列表中移除节点 q,并抛出 InterruptedException 异常
                removeWaiter(q);
                throw new InterruptedException();
            }

            int s = state;
            //如果状态已经完成,表示FutureTask任务已结束
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                //返回
                return s;
            }
            // 表示还有一些后序操作没有完成,那么当前线程让出执行权
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            //将当前线程阻塞等待
            else if (q == null)
                q = new WaitNode();
            else if (!queued)
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            //timed 为 true 表示需要设置超时                                        
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                //让当前线程等待 nanos 时间
                LockSupport.parkNanos(this, nanos);
            }
            else
                LockSupport.park(this);
        }
    }

 

当然,面试的时候,不一定要讲到源码这么细,只需要讲个大概思路就好啦。

 

5. ReentrantLock和Synchronized的区别?Synchronized 原理? 
ReentrantLock和 Synchronized 的区别?

  • Synchronized是依赖于JVM实现的,而ReenTrantLock是API实现的。
  • 在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者性能就差不多了。
  • Synchronized的使用比较方便简洁,它由编译器去保证锁的加锁和释放。而ReenTrantLock需要手工声明来加锁和释放锁,最好在finally中声明释放锁。
  • ReentrantLock可以指定是公平锁还是⾮公平锁。⽽synchronized只能是⾮公平锁。
  • ReentrantLock可响应中断、可轮回,而Synchronized是不可以响应中断的,
    至于Synchronized的原理,大家可以看我这篇文章哈

Synchronized解析——如果你愿意一层一层剥开我的心

 

6. 聊聊AOS?ReentrantLock的实现原理? 
AQS(抽象同步队列)的核心回答要点就是:

  • state 状态的维护。
  • CLH队列
  • ConditionObject通知
  • 模板方法设计模式
  • 独占与共享模式。
  • 自定义同步器。
    大家可以看下我之前这篇文章哈:AQS解析与实战

大家综合ReentrantLock的功能,比如可重入,公平锁,非公平锁等,与AQS结合一起讲就好啦。

 

7. 乐观锁和悲观锁, 让你来写你怎么实现? 
悲观锁:

悲观锁她专一且缺乏安全感了,她的心只属于当前线程,每时每刻都担心着它心爱的数据可能被别的线程修改。因此一个线程拥有(获得)悲观锁后,其他任何线程都不能对数据进行修改啦,只能等待锁被释放才可以执行。

 腾讯云后端15连问!(一)-鸿蒙开发者社区

  • SQL语句select ...for update就是悲观锁的一种实现
  • 还有Java的synchronized关键字也是悲观锁的一种体现
    乐观锁:

乐观锁的很乐观,它认为数据的变动不会太频繁,操作时一般都不会产生并发问题。因此,它不会上锁,只是在更新数据时,再去判断其他线程在这之前有没有对数据进行过修改。实现方式:乐观锁一般会使用版本号机制或CAS算法实现。

 腾讯云后端15连问!(一)-鸿蒙开发者社区
之前业务上使用过CAS解决并发问题,大家有兴趣可以看一下哈:

  • CAS乐观锁解决并发问题的一次实践

 

8. Paxos 协议了解?工作流程是怎么样的? 
8.1 为什么需要Paxos算法?
当前我们应用都是集群部署的,要求所有机器状态一致。假设当前有两台机器A和B,A要把状态修改为a,B要把状态修改为b,那么应该听谁的呢?这时候可以像2PC一样,引入一个协调者,谁最先到就听谁的。

 腾讯云后端15连问!(一)-鸿蒙开发者社区
这里有个问题,就是协调者是单节点,如果它挂了呢。因为可以引入多个协调者

 腾讯云后端15连问!(一)-鸿蒙开发者社区
但是这么多协调者,应该听谁的呢?

引入Paxos算法解决这个问题,Paxos算法是一种基于消息传递的分布式一致性算法。

 

文章转自公众号:小白debug

分类
标签
已于2022-5-30 17:31:01修改
收藏
回复
举报
回复