为什么设计系列文章学习总结

之前也有订阅draveness的博客跟着学习,这次抽时间把为什么设计系列文章再温习总结一遍,知识点常读常新吶!

为什么TCP需要三次握手

RFC793-Trannsmission Control Protocol文档中定义的TCP连接:用于保证可靠性和流控制机制的信息,包括socket、序列号及窗口大小。

The raliability and flow control mechanisms described above require that TCPs initialize and maintain certain status information for each data stream. The combination of this information, including sockets,sequence numbers, and windows sizes, is called a connection.

所以建立TCP连接就是要对上述的三种信息达成共识,连接中的socket是由互联网地址标志符和端口号组成,窗口大小主要用来做流量控制,最后的序列号是用来追踪通信发送的数据包序号,接收方可以通过序列号向发送方确认某个数据包成功接收。

因此问题转换成,为什么需要通过三次握手才可以初始化socket、sequenceNum、windowSize?

  • 通过三次握手才能阻止重复历史连接的初始化;
  • 通过三次握手才能对通信双方的初始序列号进行初始化;

历史连接

假设只有两次握手,发送方一旦发出建立连接的请求后就没法撤回请求,如果网络较差时,发送方多次连续发出建立连接请求。如果只通信两次那么接收方只能选择接受或拒绝,它并不清楚这一次请求是由于网络拥堵而早就过期的连接。

因此TCP选择三次握手来建立连接,并在连接中引入RST这一控制信息,接收方收到请求会将发送方发送的SEQ+1发送给对方,这时由发送方判断当前连接是否是历史连接:

  • 如果是历史连接,即SEQ过期超时,那么发送方会直接发送RST控制消息终止这一次连接;
  • 反之,发送方就会发送ACK控制消息,通信双方成功建立连接;

初始序列号

TCP需要在不稳定的网络构建一个可靠的传输层,在不稳定网络下可能存在:1)数据包被发送方多次重复发送;2)数据包在传输中被路由或者节点丢失;3)数据包到达接收方的顺序不正确。

为了解决这些问题,TCP协议在数据包中加入了序列号字段,支持:1)接收端去重;2)发送方在对应数据包未被ACK时重发;3)接收方根据序列号对数据包重排序。

TCP连接双方要获取初始序列号,需要向对方发送SYN控制消息并携带自己期望的初始化SEQ,对方收到后通过ACK控制消息一次SEQ+1来确认。

A-> SYN,SEQ=n->B ; B->ACK,SEQ=n+1->A; B->SYN,SEQ=m->A; A->ACK,SEQ=m+1->B。 其中中间两次可以将ACK+SYN合并发送,将四次减少为三次通信。

使用更多的通信次数交换相同的信息是可以实现的,三次是满足建立连接所需的最小次数。

为什么DNS使用UDP

实际上DNS不仅使用了UDP还使用了TCP,DNS查询类型不仅包含A记录、CNAME记录等常见查询,还有AXFR类型的特殊查询,这种特殊查询需要用于DNS区域传输,其作用是在多个命名服务器之间快速迁移记录,对数据准确性由有强烈的要求,由于查询返回的响应比较大,所以会使用TCP传递数据包。

UDP协议在过去的几十年都是DNS主要使用的协议,每次DNS查询都会直接向命名服务器发送UDP数据包。常见的DNS查询数据包都非常小,如果使用TCP的话建立连接需要三次通信,传输~130字节的数据,销毁连接需要4次通信,传输~160字节的数据。再考虑到实际场景中DNS解析可能会递归地与多个命名服务器通信,加倍放大了TCP协议在额外开销上的劣势。

现在网络设计中,我们不仅遇到了IPV4即将无法分配的情况,还需要引入DNSSEC等机制保证DNS查询和请求的完整性以及传输安全。导致DNS需要处理的数据包越来越大,理论上一个UDP数据包大小最多可达64KB,实际生产环境下数据包大于MTU(1500字节)时,数据包就会被分片传输、丢弃,部分网络甚至会拒绝处理包含EDNS选项的请求。导致UDP协议的DNS不稳定。

此时TCP可以非常好的解决这个问题,通过序列号和重传机制保证消息不重不漏,且建立连接和销毁连接的额外开销在大数据包面前可忽略不计。

为什么TCP协议有性能问题

TCP在设计之初没有考虑到现今复杂的网络环境,在地铁或火车上信号断断续续其实本质就是TCP协议造成的。

底层数据传输协议在设计时必须要对带宽利用率和通信延迟进行权衡和取舍,TCP选择了充分利用带宽,为流量而设计,期望在尽可能短的时间内传输更多的数据。

网络通信中,从发送方发出数据到收到来自接收方的确认,总时长称为往返时延。 RTT,Round-Trip Time。

弱网环境下影响TCP性能有以下几个因素:

  • TCP拥塞控制算法会在丢包时主动降低吞吐量;(首要原因)
  • TCP三次握手增加了数据传输的延迟和额外开销;
  • TCP的累计应答机制导致了数据段的传输;

每个TCP连接都会维护一个拥塞控制窗口CongestionWindow,决定了一次并发发送多少数据包。除了cwnd之外TCP连接双方都有一个rwnd接收窗口。在TCP连接开始时双方都不知道对方的rwnd大小,需要动态的估算机制改变传输的速度。

客户端能同时最大传输的数量时min(swnd,rwnd),初始大小时TCP_INIT_CWND。通过慢启动阈值slow start threshold(ssthresh)来决定:

  • < ssthresh, 使用慢启动,发送方每收到一个响应ACK,拥塞窗口大小就+1(即每个RTT时间内加倍)
  • ssthresh, 使用拥塞避免算法,每收到一个ACK,拥塞窗口大小就+1,当丢包时ssthresh设置为拥塞窗口的一半

重传机制: TCP传输可靠性时通过序列号和接收方的ACK来保证的,当TCP传输一个数据段时,它会将该数据段的副本放到重传队列并开启计时器。1)如果发送方收到ACK响应,从重传队列中删除;2)如果在计时器到期之间都没收到ACK则会重发该数据段。

TCP ACK的意思是指当前数据段之前的所有数据段都已经被接收和处理。一旦之前有一个数据包没有接收,之后的所有数据段即使接收到了也不会响应,超时时都会重传,在弱网环境下会导致大量的带宽浪费。

为了缓解上述问题,快重传技术当接收方收到乱序的数据段后,就会立刻连续发送三个ACK触发发送方的重传。

为什么UDP头只有8个字节

UDP协议头中只包含四个字段:源端口、目的端口、长度、校验码,其中每个字段16bit,所以总共8字节。其中长度是协议头和数据长度的总和,校验码使用IP首部、UDP首部和数据报中数据进行计算,接收方可以通过校验码进行验证数据的准确性。

UDP协议底层IP网际协议负责数据包在主机间的传输;UDP协议的首部端口号用于定位处理数据的具体进行并转发数据。UDP本质只起到定位具体进程的作用。

虽然UDP和TCP都有端口号概念,但两者不在一个命名空间,因为可以同时使用相同的端口号。

为什么TCP/IP协议会拆分数据

  • IP协议分片传输过大的数据包Packet,避免物理设备限制
  • TCP协议分段传输过大的数据段Segment,保证传输性能

在IP协议层不同的设备传输数据前会先确定一个IP数据包大小上限,即最大传输单元MTU Maximum transmission Unit。由整个链路上MTU最小的物理设备决定。(一般1500字节)

Path MTU Discovery算法:

  • 向目的主机发送IP头带DF控制位=1的数据包,Don’t Fragment不分片
  • 路径上的网络设备根据数据包大小和自己的MTU做不同的决定:
    • 数据包 > MTU,就会丢弃数据包,并返回一个包含该设备MTU的ICMP消息
    • 数据包 < MTU,就会继续向目的主机传递数据
  • 源主机收到ICMP消息后,不断使用新的MTU发送IP数据,直到数据包到达目的主机。

数据包分片时第一个数据包中包含UDP协议的相关信息,后续的都不包含,因此如果丢包整个UDP数据都无法组装。

TCP协议中引入了最大分段大小Max Segment Size,MSS,即TCP数据段能够携带的数据上限。正常情况下MSS=MTU-40,是操作系统内核层面的限制,通信双方会在三次握手时确定。

TCP协议之所以能够不重不漏按序传输,因为它会通过IP协议的MTU计算出MSS,并根据MSS分段避免IP协议对数据包进行分片。

为什么流媒体直播延迟高

流媒体直播从音视频的采集和编码到解码和播放设计了非常长的链路,需要途径主播端、流媒体服务器、观众端。在冗长的采集和分发流程中,不同过程都会通过一系列的技术保证直播的质量,为了保证可靠性、降低系统带宽而使用的手段共同造成了直播高延迟的问题。

  • 音视频使用的编码格式决定了客户端只能从特定帧开始解码;
  • 音视频传输使用的网络协议切片大小决定了客户端接收数据的间隔
  • 服务器和客户端为了保证用户体验和直播质量预留缓存

为什么HTTPS需要7次握手+9倍延迟

Netscape在1994年设计了HTTPS协议,使用安全套接字层SocketLayer,SSL,保证数据传输的安全。随着传输层安全协议TransportLayerSecurity TLS的发展,目前已经使用TLS取代了废弃的SSL协议。

HTTPS从发送方发出到接收响应需要经过4.5 RTT:

  • TCP协议三次握手建立TCP连接;(TCP快启策略使用存储在客户端的TFO cookie快速建立连接,可以减少通信次数)
  • TLS协议四次握手建立TLS连接;
  • HTTP协议,客户端发出请求,服务端发回响应。

其中TLS1.2建立过程如下:(1.3通过优化协议降低到1RTT)

  • 客户端向服务器发送client hello消息,携带客户端支持的协议版本、加密算法、压缩算法和一个客户端生成的随机数;
  • 服务端收到客户端的信息后;
    • 向客户端发送server hello消息,携带选择特定的协议版本,加密方法,会话ID,服务端生成的随机数;
    • 向客户端发送certificate消息,包含证书支持的域名、发送方和有效期信息;
    • 向客户端发送server key Exchange消息,传递公钥及签名信息;
    • 向客户端发送可选的certificateRequest,验证客户端证书;
    • 向客户端发送server hello done消息,通知服务器已经发送了全部消息;
  • 客户端收到消息后,验证证书
    • 向服务器端发送client key exchange消息,包含使用服务端公钥加密后的随机字符串
    • 向服务器发送change cipher spec消息,通知服务器后面的数据段要加密;
    • 向服务器发送finish消息,包含加密后的握手信息;
  • 服务器端收到消息后
    • 向客户端发送change cipher spec消息,通知客户端后面的数据段会加密传输
    • 向客户端发送finish消息,验证客户端的finish消息并完成TLS握手
      HTTP3使用基于UDP和QUIC协议进行握手,将TCP和TLS握手过程结合起来,把7次握手减少到了3次。

为什么TCP有粘包问题

粘包并不是TCP协议造成的,而是因为应用层协议设计者对TCP协议的错误理解,忽略TCP协议的定义并且缺乏设计应用层协议的经验。

  • TCP协议是面向字节流的协议,它可能会组合或者拆分应用层协议的数据
  • 应用层协议没有定义消息的边界,导致数据的接收方无法拼接

Nagle算法:通过减少数据包的方式提高TCP传输性能的算法,因为网络带宽有限,它不会降小的数据块直接发送到目的主机,而是会在本地缓冲区中等待更多待发送的数据,这种批量发送数据的策略虽然会影响实时性和网络延迟,但是能降低网络拥堵并减少额外开销。

当应用层使用TCP协议传输数据时,实际待发送的数据先写到了TCP协议的缓冲区,如果用户开启了Nagle算法,那么协议会等待缓冲区数据超过MSS最大数据段,或者上一个数据段被ACK时才会发送。

这样导致应用层多次写入的协议被合并或拆分发送,当接收方接收后应用层无法拆分和重组。 为了解决此问题,可以通过在应用层协议中基于长度或者终结符进行处理,或者基于特定的规则,如JSON数据。

UDP消息是有边界的,上限的65536,但一半都会被路径MTU限制。所以不会出现粘包问题。

为什么TCP协议有TIME_WAIT状态

TCP常见的关闭连接过程:

  • 当客户端没有待发送数据时,会想服务器端发送FIN消息,之后自己进入FIN_WAIT_1状态;
  • 服务器端收到客户端的FIN消息后,会进入到CLOSE_WAIT状态,并向客户端发送ACK消息。客户端收到ACK消息后会进入到FIN_WAIT_2状态;
  • 当服务器端没有待发送的消息后,服务器端会向客户端发送FIN消息;
  • 客户端接收到FIN消息后,进入TIME_WAIT状态并向服务器端发送ACK消息,服务器收到后进入CLOSED状态;
  • 客户端等待两个最大数据段生命周期的时间后也进入CLOSED状态。 MaxSegmentLifetime,MSL

TIME_WAIT仅在主动断开连接的一方出现,被动断开连接的一方会主动进入CLOSED状态。TCP需要TIME_WAIT状态和需要等待2MSL再CLOSED的原因一样:

  • 防止延迟的数据段被其他使用相同源地址、源端口号、目的地址、目的端口号的TCP连接收到;
  • 保证TCP连接的远程被正确关闭,即等待被动关闭的连接的一方收到FIN对应的ACK消息

阻止延迟数据段 : 每个TCP数据都包含唯一的序列号,这个序列号保证TCP协议的可靠性和顺序性。为了保证新TCP连接的数据段不会和还在网络中传输的重复,TCP在分配新数据号之前需要至少静默1MSL。

TIME_WAIT等待2MSL是考虑到网络中可能存在来自发起方的数据段,这些数据段被服务器处理后又要返回响应给客户端。

保持连接关闭 : 如果客户端等待时间不长,当服务器端还没有接收到ACK消息时,客户端就与服务器重新建立连接,客户端重新发送的SYN请求会收到服务器端RST消息,连接建立被终止。

如果客户端等待足够时间:1)服务器正常收到了ACK消息并关闭当前连接;2)服务器没收到ACK消息,重新发送FIN关闭连接并等待新的ACK。 所以只要客户端等2MSL时间,连接就会正常关闭,保证了数据传输的可靠性。

在某些场景下60s的等待时间难以接受,高并发场景下可能会产生大量的TIME_WAIT状态的TCP连接。可以通过使用net.ipv4.tcp_tw_reuse选项,通过TCP的时间戳选项允许内核重用处于TIME_WAIT状态的TCP连接。

为什么MAC地址不需要全球唯一

MAC:Media Access Control address,是分配给网络接口控制器的唯一标志符,它会在网络段中充当网络地址使用。 48位一般前半部分是组织标志符,后面是地址,需要在IEEE官方购买。

机构分发地址段并由设备商保证地址唯一的方式就是为了保证全世界所有硬件的网络地址唯一,但实际操作中是无法严格保证的,而且我们也不需要:

  • 不同操作系统上,我们都可以通过软件直接修改网卡的MAC地址;
  • 只需要保证一个局域网内的MAC地址不重复,网络就可以正常工作。 (因为交换机可以从数据帧的源地址学次到设备的MAC地址并插入本地缓存,跨局域网需要网络层的IP协议所以链路层地址重复影响不大)

为什么IPV6难以取代IPV4

  • NAT技术很大程度上缓解IPV4地址短缺问题;
  • IPV6协议并没有在设计时考虑和IPV4兼容问题;
  • 更细粒度的管控IPV4地址并回收闲置资源;

为什么集群需要Overlay网络

  • 云计算集群内、跨集群或者数据中心间的虚拟机和实例的迁移比较常见;
  • 单个集群中的虚拟机规模可能非常大,大量MAC地址和ARP请求会为网络设备带来巨大的压力;
  • 传统的网络隔离技术VLAN只能建立4096个虚拟网络,公有云以及大规模的虚拟化集群需要更多的虚拟网络才能满足网络隔离的需求。

目前K8S官方支持最大集群为5000节点,如果每个节点都只包含一个容器,这对于内部的网络设备其实没有太大的压力。但是在实际情况下集群中可能包含几万甚至几十万个容器,当某个容器向集群中发送ARP请求时,所有容器都会收到带来极高的网络负载。

使用VxLan搭建的Overlay网络中,网络会将虚拟机发送的数据重新封装成IP数据包,这样网络之需要知道不同VTEP的MAC地址,可以将MAC地址表项中的几十万条数据降低到几千条,ARP请求也只会在集群的VTEP之间扩散,远端的VTEP将数据拆包后也仅会在本地广播,不影响其他VTEP。大大降低了核心网络设备的压力。

网络隔离:同一个物理集群可能会被拆分成多个小块分配给不同的租户tenant,因为二层网络的数据帧会进行广播,出于安全考虑需要将这些不同的租户进行隔离。传统的网络隔离会使用虚拟局域网技术VLAN,其使用12位bit表示虚拟网络ID,上限是4096个。对于大规模集群来说远远不够,VxLAN会使用24bit的VNI表示虚拟网络个数,1677w+个虚拟网络能满足数据中心多租户网络隔离的需求。

为什么Redis选择单线程模型

Redis作为一个内存服务器,需要处理很多来自外部的网络请求,它使用IO多路复用机制同时监听多个文件描述符的可读和可写状态。Redis4.0之后的版本,在执行一些命令时会使用主处理线程之外的其他线程,如UNLINK、FLUSHALL、ASYNC、FLUSHDB ASYNC等非阻塞的删除操作。

单线程模型: Redis从一开始选择单线程模型主要考虑:1)使用单线程模型可维护性高,方便开发和测试;2)使用单线程模型也能并发处理客户端的请求;3)Redis中绝大多数操作的性能瓶颈都不是CPU。

Redis在删除超大键值对时并不能在几ms时间内处理完,可能需要在释放内存空间上消耗更多的时间。这样会阻塞待处理的任务影响redis可用性。释放内存空间的操作可以在后台线程异步执行,即UNLINK命令的实现原理,将键从元数据中删除,真正的删除操作会在后台异步执行。

为什么mysql使用B+树

首先MySQL和B+树没有直接关系,主要是因为mysql的默认存储引擎innodb。除了innodb之外mysql还支持多种存储引擎,如MyISAM等。

对于innodb来说,无论表中的数据主键索引还是辅助索引最终都会使用B+树来存储数据,其中前者在表中会以的方式存储,后者会以 的方式存储。所以后者查询时往往需要回表操作。

  • Innodb需要支持的场景和功能需要在特定查询上拥有较强的性能;
  • CPU将磁盘上的数据加载到内存需要花费大量的时间,B+树称为非常好的选择。

和hash对比:使用hash可以达到O(1)时间的查询,但是在处理范围查询或者排序时性能会非常差,只能进行全表扫描。使用B+树能够保证数据按照键的顺序进行存储,查询性能O(logN)也可接受。
和B树对比: 计算机以页为单位加载内存,页大小都是4KB。当我们需要在数据库中查询数据时,CPU会发现当前数据在磁盘而不在内存中,这时就会触发IO操作将数据加载到内存中进行访问,数据的加载都是以页为单位进行加载的,将数据从磁盘读取到内存中成本非常大,普通的非SSD磁盘需要经过队列、寻道、旋转以及传输等过程,大概需要花费10ms左右时间。B树会在非叶子结点中存储数据,B+树所有数据都是存储在叶子节点中。当一个表底层是B树时,我们总是要从根节点向下遍历子树查找满足条件的数据行,这引入了大量随机IO。而B+树所有数据都存储在叶子节点,且叶子节点通过指针链接,因此可以大量节省磁盘IO时间。

为什么Redis快照需要子进程

RDB和AOF是Redis为我们提供的持久化工具,其中RDB就是Redis的数据快照。RDB每隔一段时间都会对Redis服务中当下的数据集进行快照,除了Redis的配置文件设置间隔之外,还可以使用SAVE、BGSAVE命令。

  • SAVE在执行时会直接阻塞当前的线程,Redis是单线程的,所以SAVE命令会直接阻塞来自客户端的所有其他请求。
  • BGSAVE在后台生成快照,即首先fork子进程,子进程会执行将内存数据保存到RDB文件这一过程,不影响Redis主线程响应客户端请求。

通过fork生成的父子进程会共享包括内存空间在内的资源。fork函数并不会带来明显的性能开销,尤其是内存大量拷贝,是通过写时复制来完成,将真正的拷贝推迟到需要的时候。

fork函数返回0时,意味当前进程是子进程;反之表明是父进程,返回的是子进程的pid。fork后的父子进程会运行在不同的内存空间中,当fork发生时两者的内存空间有着完全相同的内容,对内存的写入和修改、文件映射都是独立的,两个进程不会相互影响。

为什么数据库会丢失数据

人为错误: 造成数据丢失的首要原因,如腾讯云数据丢失事件,虽然起因是硬件故障,但最终导致数据完成性受损的还是运维人员的不当操作。1)正常数据搬迁流程默认开启数据校验,开启后可以有效发现并规避源数据异常,但运维人员为了加速搬迁任务,违规关闭了数据校验;2)正常数据搬迁后源仓库要保留24h用于异常恢复,但运维人员为了尽快降低仓库使用率违规对源仓库进行了数据回收。因此尽量将操作标准化自动化流程处理,降低人为干预带来的风险,敬畏生产环境,谨慎的在生产环境执行一切操作。

硬件故障: 磁盘硬件使用时间足够长很有可能发生损坏,因此我们需要数据冗余的方式保证磁盘在发生不可修复读错误时能够恢复磁盘数据。Redundant Array of Independent Disk, RAID独立冗余磁盘阵列时一种能够将多个物理磁盘组合成一个逻辑磁盘的数据存储虚拟化技术,增加冗余并提高性能。

实现复杂: 除了持久化特性之外,数据库可能还需要提供ACID、BASE的保证,有些数据库还提供分片、副本、以及分布式事务等复杂功能,这些功能的引入也增加了数据库系统的复杂性。随着程序复杂性的增加,出现问题的可能性也随之增长。

为什么MySQL的自增主键不单调也不连续

MYSQL中默认的AUTO_INCREMENT属性在多数情况下可以保证主键的连续性,但是在实际使用中会遇到两个问题:1)记录的主键并不连续;2)可能会创建多个主键相同的记录。

  • 较早版本的MySQL将AUTO_INCREMENT存储在内存中,实例重启后会根据表中数据重置该值。(在8.0之后每次计数器的变化都会写入redo log,并在检查点存储在引擎私有的系统表中)
  • 获取AUTO_INCREMENT时不会使用事务锁,并发的插入事务可能会出现字段冲突导致插入失败。要想保证主键的连续性需要串行地执行插入语句。 (事务回滚会导致之前的id失效)

为什么Linux需要虚拟内存

利用中间层对资源进行更合理地调度充分提高资源的利用率并提供统一的抽象。

主存是比较稀缺的资源,虽然顺序读取只比磁盘快1个量级,但是随机读取确实磁盘的100,000倍,充分利用内存的随机访问速度是改善程序执行效率的有效方式。

操作系统以页为单位访问内存,当进程发现需要的数据不在内存中,MMU(内存管理单元)会将数据以页的形式加载到内存。

  • 虚拟内存可以利用内存起到缓存的作用,提高进程访问磁盘的速度;
  • 虚拟内存可以为进程提供独立的内存空间,并引入多层的页表结构将虚拟内存翻译成物理内存。简化程序链接、加载过程并通过动态库共享内存;
  • 虚拟内存可以控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统安全性;

因为主内存空间有限,当主内存不包含可使用的空间时,OS会从选择合适的物理内存页驱逐回磁盘,为新的内存页让出位置。缺页中断和页面替换技术都是页面调度算法,充分利用内存资源作为磁盘的缓存以提高程序的运行效率。

四层页表结构 :实现虚拟地址到物理地址的转换。OS使用低12位(正好4K)作为页面的偏移量,剩下36位分为四组分别表示当前层级在上一层级的索引,所有虚拟地址都可以用上述的多层页表查找对应的物理地址。因为有多层的页表结构可以用来转换虚拟地址,所以多个进程可以通过虚拟内存共享物理内存。Redis的fork子进程时的写时复制就是利用了虚拟内存这一特性,fork时实际上只复制了父进程的页表。

为什么每层页表结构只能负责9位虚拟地址的寻址? 64位虚拟地址占8B的大小,所以一个页表可以有4K/8B=2^9项。

64位虚拟内存在操作系统中需要多少层的页表结构才能寻址? 12+9*5=57,因此需要5层。

为什么系统调用会消耗较多资源

为什么Linux默认页是4KB

4KB时一个历史遗留问题,在上世纪80年代确定的4KB一直保持到今天,是过去在特定场景下做出的权衡:

  • 过小的页面大小会带来较大的页表项增加寻址时TLB的查找时间和额外开销;
  • 过大的页面会浪费内存空间,造成内存碎片,降低内存的利用率。

除了内存利用率之外,较大的内存页也会增加内存拷贝时的额外开销,因为Linux写时复制,在多个进程共享同一块内存时,当其中一个进程修改了共享的虚拟内存会触发内存页的拷贝。这时操作系统内存页越小,写时拷贝带来的额外开销也会越小。

为什么CPU访问硬盘很慢

机械硬盘HDD和固态硬盘SSD是最常见的硬盘,SSD中随机访问4KB数据所需要的时间是访问主存的1500倍,机械硬盘的寻道时间是访问主存的100,000倍。

  • CPU需要通过I/O操作访问外部存储的数据,编程IO、中断驱动IO和DMA几种方式都会带来额外开销并占用较多的CPU时间;
    • 编程IO:CPU会负责全部的工作,向IO设备中写入数据后轮询设备状态等待完成后继续写入。占用全部CPU资源,造成计算资源的严重浪费。
    • 中断驱动IO:将一部分操作交给IO设备更高效,设备会在闲置时主动发起中断暂停当前进程并保存上下文,OS执行IO设备的中断处理程序。
    • DMA: CPU会一次将缓冲区的数据全部堵到DMA控制器中,DMA控制器负责将数据写入IO。解放CPU并减少中断次数,执行速度比CPU更慢。
  • 机械硬盘会通过机械结构访问其中存储的数据,每一次硬盘的随机IO都需要执行队列、寻道、旋转和转移数据几个过程,大约需要消耗10ms时间。

为什么NUMA会影响程序的延迟

  • NUMA引入本地内存和远程内存,CPU访问本地内存的延迟会小于访问远程内存;
  • NUMA的内存分配和内存回收策略结合时会可能导致Linux的频繁交换分区SWAP,影响系统的稳定性。

为什么HugePages可以提升数据库性能

2MB一般时HugePages的默认大小,在arm64和X86_64的架构上甚至支持1GB的大页面。

虽然HugePages的申请方式和默认的内存相差不多,但是它实际上是操作系统单独管理的特殊资源,K8S页会认为大页是不同于内存的独立资源。

  • HugePages可以降低内存页面的管理开销;
    • 减少内存中的页表层级,降低页表占用;
    • 更高的缓存命中率,CPU有更高的几率直接在TLB中获取对应的物理地址;
    • 减少获取大内存的次数
  • HugePages可以锁定内存,禁止操作系统的内存交换和释放(预先分配的资源,哪怕是内存不足时也不会被swap到磁盘);

为什么Linux需要swapping

  • Swapping可以直接将进程中使用较少的页面换出内存,立刻给正在执行的进程分配内存;
  • Swapping可以将进程中的闲置页面换出内存,为其他进程未来使用内存做好准备;

系统内存不足时可以立即触发OOM杀掉进程,但是Swapping为系统管理者提供了另一种选择,利用磁盘交换空间避免程序直接退出。以降低服务质量的代价换取服务的部分可用性。

K8S中要求禁用Swapping,该OOM的时候就kill掉,迁移到其他机器上运行。