学习笔记W2

笔记汇总

1. K8S垃圾收集器

详解Kubernetes垃圾收集器的实现原理

垃圾收集器在K8S中以控制器的形式设计实现,删除以前有所有者但是现在所有者不存在的对象。引入垃圾收集器之前,所有的级联删除逻辑都由客户端完成,垃圾收集器的引入使得级联删除实现移动到了服务端。

级联删除: 对象的API中加入了metadata.ownerReferences字段,包含当前对象的所有依赖者。当所有依赖者都被删除,默认情况下该对象也会被删除。

1
2
3
4
5
6
7
8
9
10
type ObjectMeta struct {
...
OwnerReferences []OwnerReference
}
type OwnerReference struct {
APIVersion string
Kind string
Name string
UID types.UID
}

实现原理

GarbageCollector负责处理对象之间的联系并在所有依赖者不存在时将对象删除。其中包含了一个GraphBuilder结构体,该结构体会以Goroutine的形式运行使用Informer监听集群中几乎全部资源变动,一但发生变更事件,就将该事件交给主循环体处理。主循环体根据事件的不同选择将处理对象加入不同的队列,GarbageCollector持有的另外两组队列会负责删除或孤立目标对象。

  • attemptToDeleteItem : 首先获取待处理对象及其ownerReferences列表,使用classifyReferences将所有者进行分类处理:

    • 所有者还有存在于集群中的,当前对象不会被删除。将已经被删除、等待删除的所有者从对象ownerReferences删除;
    • 当正在被删除的所有者不存在任何依赖且ownerReference.blockOwnerDeletion=true时会阻止依赖方的删除,当前对象会等待ownerReference.blockOwnerDeletion=true的所有对象删除后才会被删除;
    • 当前对象不包含任何依赖,会选择三种不同的策略处理依赖:

      • 当前对象有FinalizerOrphanDependents终结器,DeletePropagationOrphan将对象的所有依赖者变成孤立的;
      • 当前对象有FinalizerDeleteDependents终接器,deletePropagationBackground策略在前台等待所有依赖被删除后才会删除,整个过程是同步的;
      • 默认情况下使用DeletePropagationDefault策略在后台删除当前对象的所有依赖;
  • attemptToOrphanItem

2. Go Select实现原理

Go 语言select实现原理

Go语言中select能够让Goroutine同时等待多个Channel的可读/可写,在Channel发生状态改变之前会一直阻塞当前Goroutine。其与switch不同的是,case中的表达式必须是Channel的收发操作。

  • select 能在 Channel 上进行非阻塞的收发操作
  • select 在遇到多个 Channel 同时响应时会随机挑选 case 执行;(随机的引入是为了避免饥饿问题)

通常情况下select语句会阻塞当前Goroutine并等待多个Channel中一个达到可以收发的状态,但是如果存在default:

  • 当存在可以收发的Channel时,直接处理该Channel对应的case;
  • 当不存在可以收发的Channel时,执行default中语句;
    非阻塞的Channel发送和接收操作还是很有必要的,在很多场景下我们不希望向Channel发送消息或者从Channel中接收消息会阻塞当前Goroutine,只是想看看 Channel的可读或者可写状态。

编译器在中间代码生成期间会根据select中case的不同对控制语句进行优化,这一过程都发生在 cmd/compile/internal/gc.walkselectcases 函数中:

  • select 不存在任何的 case; 将select{}转换为调用runtime.block,通过gopark让出当前Goroutine对处理器的使用权,导致当前G进入无法被唤醒的永久休眠状态
  • select 只存在一个 case; 将select改写成if条件预计,当case中Channel是空指针时,挂起当前G并永久休眠
  • select 存在两个 case,其中一个 case 是 default;

    • 非阻塞发送:编译器使用if else改写代码,selectnbsend向Channel非阻塞发送数据,即向chansend函数传入false,哪怕不存在接收方或者缓冲区空间不足都不会阻塞当前G,直接返回。
    • 非阻塞接收:根据接收数据返回值数量的不同使用selectnbrecv或者selectnbrecv2对返回值进行处理
  • select 存在多个 case;

    将所有case转换成包含Channel以及类型信息的runtime.scase结构体,调用runtime.selectgo选择一个可执行的scase。runtime.selectgo 函数首先会进行执行必要的初始化操作并决定处理 case 的两个顺序 — 轮询顺序 pollOrder 和加锁顺序 lockOrder:

    • 轮询顺序:通过 runtime.fastrandn 函数引入随机性; (避免Channel饥饿问题保证公平)
    • 加锁顺序:按照 Channel 的地址排序后确定加锁顺序;(避免死锁)

selectgo 处理流程

  • 随机生成一个遍历的轮询顺序pollOrder 并根据Channel地址生成锁定顺序lockOrder;
  • 根据pollOrder遍历所有的 case 查看是否有可以立刻处理的 Channel;
  • 如果存在就直接获取 case 对应的索引并返回;
  • 如果不存在就会创建 runtime.sudog 结构体,将当前 Goroutine 加入到所有相关 Channel 的收发队列,并调用 runtime.gopark 挂起当前 Goroutine 等待调度器的唤醒;
  • 当调度器唤醒当前 Goroutine 时就会再次按照 lockOrder 遍历所有的 case,从中查找需要被处理的 runtime.sudog 结构对应的索引;

### 3. ETCD九连问
ETCD九连问

ETCD是一个可信的分布式键值对存储服务,为分布式集群存储一些关键数据,协助分布式集群的正常运转。

  • 什么是键值数据库? 答:一种非关系型数据库,它使用简单的键值方法来存储数据,键作为唯一标识符。
  • KV存储服务能干啥? 答: 用存储服务为服务中介提供服务。
  • 什么是服务中介? 答:包含k-v键值对的字典,key是服务名称,value是服务提供者的地址列表。为服务提供者和服务消费者之间建立联系。
  • 服务发现是什么? 答:服务提供者、消费者和服务中介三者,实现了服务发现机制,同时etcd还提供服务注册中心的功能。通过服务发现让消费者找到服务提供者。
  • 服务注册和服务发现的目的是?答:在微服务和容器化环境中,应用的增加和弹性伸缩发生频率很高,服务地址可能经常发生变化,因此需要服务注册和发现机制。
    • 服务注册:服务提供者的实例(如pod)在启动时或者位置信息发生变化时,向etcd注册自身,在停止时注销自身。如果该实例发生故障,一段时间没有发送心跳之后会被服务注册表注销;
    • 服务发现:服务消费者请求服务,首先被发往一个中央路由器或负载均衡器(Service、Router等),查询服务注册表获取服务者的位置信息将请求转发给提供者。
  • etcd作为分布式键值存储服务,必须要保证分布式系统数据的可用性和一致性。一致性算法有Paxos、Raft,其中Paxos比较复杂很多KV数据库都采用后者。ETCD也不例外。
    • HTTP Server:用于处理用户发送的API请求以及其他ETCD节点的同步与心跳信息请求
    • Store: 用于处理ETCD支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、时间处理与执行等
    • Raft:Raft强一致行算法的具体实现
    • WAL:Write Ahead Log,预写式日志,除了内存中存有所有数据状态以及节点索引之外,ETCD通过WAL进行持久化存储,所有数据提交前都会时间记录日志。Snapshot是为了防止数据过多而进行的状态快照,Entry表示存储的具体日志内容。

4. GO STRUCT对齐

合理重排字段可以减少填充,使STRUCT字段排列紧密减少空间浪费。零大小字段指struct{}通常不需要对齐,但是当作为结构体最后一个字段时需要对齐。因为如果有指针指向该字段,返回的地址将在结构体之外。如果此指针一直存活不释放对应的内存会出现内存泄露。(该内存不因结构体释放而释放)因此零大小字段要避免作为 struct 最后一个字段,防止内存浪费。

32 位系统上对 64 位字的原子访问要保证其是 8bytes 对齐的;当然如果不必要的话,还是用加锁(mutex)的方式更清晰简单

一周算法汇总

面试问题汇总:

  1. 为什么redis选择单线程模型

答: Redis从一开始选择使用单线程模型处理客户端的绝大多数网络请求,原因是:

  • 使用单线程模型可维护性高,方便开发和调试;
  • 单线程模型也能并发处理客户端请求; (IO多路复用,通过select函数)
  • Redis服务中运行的绝大多数操作的性能瓶颈都不是CPU;

多线程技术能够帮助我们重复利用CPU的计算资源来并发执行不同的任务,但是CPU资源往往不是Redis服务器的瓶颈。Redis如果不开启AOF备份,所有的Redis操作都会在内存中完成不涉及IO,处理非常快。

普通的Linux服务器上启动Redis也能处理1s 1,000,000个请求。如果这种吞吐量不满足需求,应考虑使用分片的方式将请求发送给不同的Redis来处理。

Redis 4.0之后的版本加入了多线程的支持,是因为新版本中加入了一些可以被其他线程异步处理的删除操作。UNLINK,FLUSHALL ASYNC,FLUSHDB ASYNC。 删除超大键值对时,几十M,几百M的数据不能在几ms时间内处理完,Redis可能会需要在释放内存空间上消耗过多时间,这些操作会阻塞待处理任务,影响Redis的PCT99和可用性。释放内存的工作由后台线程异步进行处理来提升性能。

  1. Kafka如何保证数据不丢?

答: 需要分别从生产者、服务端、消费者三方面处理来保证数据不丢失。

  • 生产者:可以通过配置ACK策略或者retries策略保证数据不丢失:

    • ACK=all或-1,生产者发送消息后,需要等待ISR中所有的副本都成功写入消息之后才能接收来自服务端的成功响应。即发送消息时需要leader向follow同步完数据,ISR队列中所有broker全部保存这条消息后,才向ACK发送消息。
    • 对于可恢复错误(leader选举、网络抖动),配置retries>0进行重试并设置重试时间间隔确保重试时可恢复错误都已恢复;对于不可恢复错误,发生异常时把消息写入DB或者本地缓存文件中,等错误修复后再把数据发送给broker端
  • broker端:设置unclean.leader.election.enable=false,默认值为true,表示当存有最新一条记录的replication宕机时,kafka自己选举一个主节点,true表示允许未同步最新数据的replication所在节点作为主节点,数据会丢失。
  • 消费端:处理好offset保证exactly-once & at-least-once数据。设置enable.auto.commit=false手动提交offset,并保证offset的正确性。