面经

C++ 八股

  • 继承和多态这种面向对象的设计?(把共同点抽出来复用,把不同点留给子类自己实现)
  • 多态解决了什么问题,C++ 为什么要使用多态?
    • 多态:主要用于对同一个接口,进行不同的实现;
    • C++ 使用多态主要是解决代码复用;
  • 不用继承,多态的这种设计应该怎么做呢?
    • 静态多态:模板、函数重载、工厂函数
  • 多态的虚函数可以是内联函数嘛?
    • 虚函数可以是内联函数,类内定义的成员函数默认就有 inline 属性,在语法上完全没问题;
    • “能声明成 inline“调用时真的被内联展开” 不是一回事。

      inline 本质上只是给编译器一个建议

    • 虚函数在通过 父类指针/引用调用时,是 运行时动态绑定,编译器在 编译阶段 通常不知道最终会调用哪个版本,所以一般不能直接内联;
  • 纯虚函数是干嘛的?
    • 这个行为必须有,但父类没法给出通用实现;
    • 纯虚函数不可以被实例化;
    • 强制子类实现接口,子类没有实现父类的纯虚函数,那子类自己也还是抽象类。
  • STL 了解吧,一起讲 mapunordered_map
    • map
      • 底层使用 红黑树
      • key 是有序的;
      • 插入和查找的复杂度 O(N)
    • unordered_map
      • 底层使用 哈希表
      • key 是无序的;
      • 插入和查找的平均复杂度 O(1)
  • 是线程安全的嘛?(STL 中的实现不是线程安全的,需要自己进行加锁)
  • 有哪些手段实现访问 STL 是线程安全的?
    • 外部加锁
    • 通过原子变量实现无锁 STL
  • 分别讲解一下 自旋锁读写锁 的使用场景?
    • 自旋锁:
      • 使用场景:临界区非常短、锁持有时间很短、竞争不重、线程很快就能拿到锁
    • 读写锁:
      • 使用场景:读多写少,多个线程可以同时拿共享读锁,但写锁必须独占。

项目

  • 进程、线程、协程的区别?
    • 进程:资源分配的基本单位
      • 拥有独立的资源:文件描述符、虚拟内存地址空间、页表;
      • 切换开销最大;
      • 适合强隔离任务,防止以后模块影响另一个模块;
    • 线程:CPU 调度的基本单位
      • 共享进程的资源:地址空间、堆、全局变量、打开的文件
      • 自己的资源:程序计数器、线程栈、寄存器
      • 切换开销小于进程;
      • 适合利用多核并行执行任务;
    • 协程:用户态线程,程序员自己进行保存协程的上下文
      • 共享线程的资源:一个线程里可以跑很多协程;
      • 自己的资源:协程栈、程序计数器、寄存器;
      • 切换开销最小;
      • 适合大量 I/O 并发任务;
  • 进程、线程、协程的上下文是什么呢?
    • 进程:
      • CPU 执行现场:
        • 程序计数器
        • 通用寄存器
        • 栈指针 sp
        • 内核栈信息
      • 进程运行环境:
        • 虚拟内存空间
        • 页表项
        • 管理的文件描述符
        • 进程控制块 PCB
    • 线程:
      • 共享进程的资源
      • 私有资源:
        • 程序计数器、线程栈、寄存器、线程控制块 pcb
    • 协程:
      • 共享进程/进程的资源
      • 私有资源:
        • 协程栈、程序计数器、寄存器
  • 有栈协程和无栈协程的区别?
    • 有栈协程:
      • 独立的栈空间
      • 栈指针
      • 调用链
      • 内存开销更大、切换不灵活
    • 无栈协程:
      • 没有独立调用栈
      • 必要状态保存到一个对象里,通常像状态机
      • 一个可暂停、可恢复的函数状态对象
      • 内存开销更小、切换灵活、但是只能在特定挂起点挂起
  • 你有没有对这个协程服务器进行调研选型?这方面的轮子不少,为什么想自己做一个?
    • 调研过一些网络库的实现,比如 muduo 网络库,它主要用于 主从多 Reaactor 模式;
    • 我做这个项目不是为了替代成熟框架,而是为了在一个可控的小系统里吃透协程调度和网络模型,并验证关键设计;
    • 如果是生产优先场景,我会更倾向直接用成熟框架。
  • 你有没有针对性各个模块设计一些测试用例去自测功能,看看是不是完备的,或者说对你这服务器有没有验证性能是符合你自己要求的?
    • 有做过
      • 针对于每个模块,我都会去写测试样例,去验证这个模块是否是符合我的预期的,比如协程,我会去调用多个协程,去查看调用链是否是正确的;
      • 针对这个服务器的性能,我主要是做了 Apach Benchmark,去查看了一下相关的指标,主要是看看在不同并发程度下系统什么时候出现瓶颈,瓶颈是在 I/O调度器调度不过来了 还是 锁竞争
  • 协程被饿死或者线程上的协程负载不均,现在交给你这个任务,你要去覆盖这种用例以及针对性的去评估这方面的性能,你觉得你会怎么去做?
    • 问题描述:
      • 针对我这个项目,协程确实会发生 协程饿死
        • 我的协程是非抢占式的,只要某个协程长期不 yield,它所在的线程上其他协程就可能被拖死。
      • 针对我这个项目,同样会发生 线程上的协程负载不均
        • 因为我使用的是 全局任务队列,可能会发生某个线程刚好比较快,连续抢到很多任务以及某个线程刚好在忙,抢不到新任务。
    • 方案评估
      • 首先会做的是 补观测指标
        • A. 比如说给 协程 添加一个时间戳:
          • 创建时间:create_time
          • 单次调度等待时延:enqueue_time
          • 首次运行时间:first_run_ts
          • 终止运行时间:finish_ts
        • B. 协程的调度行为:
          • 被调度次数
          • 主动 yield 次数
          • IO 挂起次数
          • 恢复次数
          • 连续运行时长最大值

            如果某个协程连续运行很久几乎不 yield 那它就是 “疑似霸占线程” 的元凶。

      • C. 每个线程的负载指标
        • 执行的协程数
        • 累计运行时间
        • idle 时间
        • epoll 唤醒次数
        • 平均队列等待时间
      • D. 调度器的指标
        • 全局队列长度
        • 队列长度峰值
        • ready 协程数
        • pending IO
        • tickle 次数
        • 空唤醒次数
        • 每秒调度次数
    • 覆盖测试样例
      • 样例一:单个 “坏协程” 霸占线程
        • 验证:没有时间片时,单协程是否会饿死同线程其他协程
        • 做法:
          • 1CPU 密集协程:死循环计算 200ms,不主动 yield
          • 100 个轻协程:每个只做很少计算,然后 yield
          • 100IO 协程:定时器/pipe/loopback/socket 周期唤醒
        • 查看:
          • 坏协程 所在线程的其他协程等待时间是否暴涨;
          • 是否出现 ready 很久但一直没运行” 的协程;
          • 线程间是否出现明显失衡;
        • 样例二:热点连接集中到同一个线程
          • 验证:某一批高频活跃连接是否会长期压在同一个线程上,导致该线程过热、其他线程空闲
          • 做法:
            • 4 个工作线程;
            • 1000 个连接,其中:
              • 800 个高频连接:持续小包收发,周期性触发可读/可写事件;
              • 200 个低频连接:低频发送或基本空闲;
            • 故意让这 800 个高频连接优先由同一个线程 accept 或恢复;
            • 或者在 schedule 时,把大部分高频协程指定到同一个 threadid
  • 讲一下 IO 多路复用以及 epoll 的底层机制?
    • IO 多路复用:使用一个线程来管理多个 socket 连接
    • epoll:
      • 红黑树: 管理 socket
      • 就绪链表: 放入已经就绪的连接;
      • 等待队列: 放入 socket 的进程 id以及对应的回调函数用于唤醒该进程
  • 你刚才讲了一下从网卡这个入口,讲了一下 epoll IO 多路复用的处理的逻辑链条,但是 epoll 只能通过网卡触发嘛,还是可以通过别的入口进到这个处理流程?
    • epoll 监听的对象不是 “网卡中断”,而是 “文件描述符上的就绪事件”
    • epoll(7) 的定义就是监视多个 file descriptor,看它们是否可进行 I/O
    • 网卡只是让某些 socket fd 进入 ready 状态的一个来源;
    • 监听 socket 上有新连接可 accept、已连接 socket 上有数据可读、发送缓冲区腾出了空间可写,这些都会表现成 socket fd 的就绪事件;
  • epoll 的监听集合使用红黑树,是出于什么考虑?
    • 红黑树是平衡搜索树,查找、插入、删除的复杂度都是稳定的 O(log n)
    • 红黑树天然适合这种 内核里长期维护的一组有序唯一键对象
  • 对比 epollpoll 又是用什么进行监听的呢?
    • poll 是让用户态每次传一个 struct pollfd 数组,pollfd 中的每个元素包含 fd/events/revents
    • 用户态会拷贝 pollfd内核态,内核收到后会遍历这些 fd,当 fd 就绪后,poll 会逐个调用 fdpoll 回调判断是否就绪;
    • 内核通过 poll_table + wait_queue 把当前线程挂到这些 fd 的等待队列上。如果没有事件就睡眠,被唤醒后再重新扫描,并把结果写回各个 pollfd.revents
  • 不同的进程可以监听同一个端口吗?
    • Linux 上显式使用 SO_REUSEPORT,可以让多个进程/线程绑定完全相同的地址和端口。
    • SO_REUSEPORT 指的是多个 socket 可以同时 bind 到同一个 IP:Port,数据包一路到传输层后,内核根据四元组找到目标 socket;如果目标端口对应的不是单个 socket,而是一组开启了 SO_REUSEPORTsocket,那么内核会在这组 socket 之间选一个来接收这个连接或数据包。
  • 结合你讲的从网卡、DMA、硬中断、软中断的方式通知,那么去匹配不同的进程是哪个环节进行的呢?
    • 完整链路: 网卡 → DMA 到内存 → 硬中断 → 通知 CPU → 软中断里跑收包 → 进入 IP/TCP/UDP 协议栈 → 查 socket → 把数据/连接挂到这个 socket → 唤醒等这个 socket 的进程/线程/epoll
    • 如果启用了 SO_REUSEPORT
      • 多个进程都在 listen
      • 选中 socket 之后,内核再通过 socket 的等待队列和 epoll 的回调机制,唤醒阻塞在 recv/accept/epoll_wait 上的线程。
      • 也就是说,内核先匹配 socket,再间接匹配到进程。
  • 两个线程可以监听同一个端口嘛?(这需要分情况)
    • 两个线程共享同一个监听 socket,这个是可以的。
      • 一个线程先 socket -> bind -> listen,然后把同一个监听 fd 交给两个线程,它们都可以在这个 fdaccept(),这是常见的多线程服务器模型之一。
      • 传统方式就是 “多个线程竞争同一个 listening socket 上的 accept()”。
      • accept(2) 也说明了,它只是从这个监听 socket 的等待队列里取出一个连接请求,并返回一个新的已连接 socket,原来的监听 socket 不受影响。
    • 两个线程各自创建 “不同的监听 socket,再去绑定同一个 ip:port。默认不行。
      • 同一个本地 (ip, port) 一般只能绑定一个 IP socket
      • 如果已经有一个 active listening socket 绑定在这个地址上,再绑定通常会失败。
      • 但如果两个线程各自的 socket 都提前设置了 SO_REUSEPORT,那就可以。

计网

  • tcp 三次握手?
  • tcp 三次握手可以缩短成两步嘛?
  • 恶意客户端不发送第三次握手,这种情况会造成什么影响呢?
  • 对已经建立好的连接有影响吗?
  • 如何解决 SYN Flood 攻击?
    • SYN-Cookie:在三次握手完成前尽量不为半连接分配状态,把必要状态编码进 SYN-ACK 的序列号里,等客户端真正回 ACK 再重建连接状态。
    • 这样即使 SYN 很多,也不容易把半连接状态耗尽。
  • tcp 解决什么问题?有哪些技术手段?
  • 关于网络阻塞这里,我了解网络的不同层其实都有这些手段,你有了解过吗?想看你这方面是否有了解?
    • 数据链路层: 数据包缓冲队列
    • 网络层:
      • 路由器和交换机在转发时排队
      • 主动队列管理 AQM
      • 在包头打标记,让接收方通知发送方降速;
    • 传输层: 流量控制和拥塞控制
    • 应用层: 限流、背压、超时/重试/退避、消息队列
  • HttpsHttp 的区别?
  • 引用数字证书为了什么呢?
    • 引入数字证书主要是为了解决 公钥分发中的信任问题
    • 客户端在建立安全连接时,不仅需要拿到服务器的公钥,还需要确认这个公钥确实属于目标服务器。
    • 数字证书通过 CA 签名把服务器身份和公钥绑定起来,从而实现身份认证,并防止中间人伪造公钥。

MySQL

  • 介绍 MySQL 有几种 log,对比的进行去描述?
    • 回滚日志 undo log Innodb 存储引擎层生成的日志,实现了事务中的 原子性,主要用于 事务回滚MVCC;
    • 重做日志 redo log Innodb 存储引擎层生成的日志,实现了事务中的 持久性,主要用于 故障恢复;
    • 归档日志 bin log Server 层生成的日志,主要用于 数据备份主从复制,事务执行完成后、真正提交前先写 binlog;
  • undo logbin log 的刷盘时机?
    • bin log
      • MySQL 提供一个 sync_binlog 参数来控制数据库的 binlog 刷到磁盘上的频率:
        • sync_binlog = 0 的时候,表示每次提交事务都只 write,不 fsync,后续交由操作系统决定何时将数据持久化到磁盘;
        • sync_binlog = 1 的时候,表示每次提交事务都会 write,然后马上执行 fsync
        • sync_binlog = N 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync
    • undo log
      • undo 先写入内存中的 undo 页;
      • undo 页修改生成 redo
      • 提交时 redo 按规则决定是否刷盘;
    • redo log
      • 通过参数判断 innodb_flush_log_at_trx_commit
        • 0:每次事务提交,将 redo log 留在 redo log buffer 中,该模式下在事务提交时不会主动触发写入磁盘的操作。
        • 1:每次事务提交,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。
        • 2:表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,相当于写入到了 PageCache
  • 索引的原理?联合索引也是通过 B+ 树来承载的嘛,有什么特点嘛?
  • 联合索引(a,b)a > 1 and b > 1,很好的利用联合索引嘛?
  • 别的中间件有一些了解不?比如 RedisMQ?
  • 讲一下你对 kafka 的理解?
  • kafka 怎么保证不丢消息的呢?