keary 发布的文章

数据库集群:读写分离

  • 原理:将数据库读写操作分散到不同的节点上。

    • 数据库服务器搭建主从集群,一主一从、一主多从都可以
    • 数据库主机负责读写操作,从机只负责读操作
    • 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据
    • 业务服务器将写操作发给数据库主机,将读操作发给数据库从机
  • 存在问题:复制延迟

    • 解决方式1:写操作后的读操作指定发给数据库主服务器

      • 存在问题:和业务强绑定,对业务的侵入和影响较大
    • 解决方式2:读从机失败后再读一次主机

      • 优点:与业务无绑定,只需要在底层访问数据API接口做封装
      • 存在问题:要读两次,增加主机的访问压力
    • 解决方式3:关键业务读写操作全部指向主机,非关键业务采用读写分离

      • 存在问题:还是和业务强绑定,即要对业务做分级,关键业务不走读写分离
  • 具体实现:分配机制

    • 实现1:程序代码封装

      • 在代码里面抽出一个数据访问层,实现读写操作分离和数据库服务器连接的管理。
      • 优点:实现简单,且可以做定制。
      • 缺点:每个编程语言都要实现一次;如果要做主从切换,则需要改代码或者配置,然后重启
      • 已有产品:TDDL
    • 实现2:中间件封装

      • 独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。
      • 优点:支持多编程语言;主从切换等操作,业务服务器无感知
      • 缺点:实现复杂,容易出BUG;所有请求都走中间件系统,性能要求高;
      • 已有产品:MySQL Router(官方)、Atlas(360)

数据库集群:分库分表

  • 业务分库

    • 定义:指的是按照业务模块将数据分散到不同的数据库服务器
    • 存在问题

      • join操作问题——无法使用join,只能业务层面分别查询
      • 事务问题——无法支持本地事务,而分布式事务性能又太差
      • 成本问题——需增加数据库机器
  • 分表

    • 垂直分表

      • 定义:表记录数不变,但按照列切成多张表(例如原有A、B、C三列,切成A、B和A、C两张表)
      • 适用于将不常用的列单独切出去存储
      • 缺点:可以会涉及多次读写操作
    • 水平分表

      • 定义:列不变,将数据切成多份放在不同表里面
      • 路由算法

        • 范围路由:选取有序的数据列(例如整形)作为路由的条件,不同分段分散到不同的数据库表中

          • 优点:可以随着数据的增加平滑地扩充新的表
          • 不足:可能数据分布不均匀
        • Hash路由:将某一列或多列,按照hash分布在不同分表内

          • 优点:数据分布均匀
          • 不足:扩充新表麻烦,需要数据重新分布
        • 配置路由:新增一张路由表,记录原表每行数据对应的分表id

          • 优点:设计简单,需要扩充的时候只要迁移数据再修改路由表数据即可
          • 不足:需要多查询一次,切路由表本身会成为性能瓶颈
      • 分表后问题

        • join操作、count、order by等,都需要业务层面代码支持
    • 实现方式

      • 程序代码封装、中间件封装

高性能NoSQL

  • 关系数据库存在的不足

    • 存储的是行记录,无法存储数据结构
    • schema扩展不方便
    • 在大数据场景下I/O高
    • 全文搜索能力弱
  • K-V存储

    • 解决关系数据库无法存储数据结构的问题,以 Redis 为代表
  • 文档数据库

    • 解决关系数据库强 schema 约束的问题,以 MongoDB 为代表
    • 目前绝大部分文档数据库存储的数据格式是 JSON
    • 优点:新增字段简单、历史数据不会出错、很容易存储复杂数据
    • 不足:不支持事务、无法实现join操作
  • 列式数据库

    • 解决关系数据库大数据场景下的 I/O 问题,以 HBase 为代表
    • 按照列来存储数据的数据库,与之对应的传统关系数据库被称为“行式数据库”
    • 优点:节省I/O、存储压缩比更大
    • 不足:频率更新多个列性能较差
  • 全文搜索引擎

    • 解决关系数据库的全文搜索性能问题,以 Elasticsearch 为代表
    • 基本原理:倒排索引
    • 使用方式:关系型数据库的数据,转换成JSON,再输入全文搜索引擎做索引

高性能缓存

  • 适用场景

    • 需要经过复杂运算后得出的数据
    • 读多写少的数据
  • 缓存存在的问题

    • 缓存穿透

      • 定义:是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据
      • 出现该问题的两种情况

        • 存储数据确实不存在。解决方式是若不存在,也提供一个默认值存在缓存当中
        • 缓存数据生成耗费大量时间或者资源。当缓存失效时,由于生成缓存数据的时间较长,因此这段时间内的请求都会打到存储系统上。
    • 缓存雪崩

      • 定义:是指当缓存失效(过期)后引起系统性能急剧下降的情况。在生成新缓存的这段时间内,所有请求仍然会打到存储系统上。
      • 解决方式

        • 更新锁机制。对缓存更新操作进行加分布式锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
        • 后台更新机制。缓存本身的有效期设置为永久,后台线程定时更新缓存。

          • 注意存在内存不够,缓存数据被踢出的问题。此时两种解决方式:要么更新缓存的线程还要频繁读缓存,若发现被踢了则再写进去;或者业务线程发现缓存数据不存在,发消息通知,然后更新缓存线程收到消息来重新写缓存。
    • 缓存热点

      • 定义:对于一些特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大
      • 解决方式:复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。

单服务器高性能模式:PPC与TPC

  • PPC(Process Per Connection)

    • 定义:每次有新的连接就新建一个进程去专门处理这个连接的请求
    • 父进程流程:socket->bind->listen->accept->fork->close
    • 子进程流程:read->业务处理->write->close
    • 适用场景:服务器的访问量和并发量并没有那么大
    • 存在问题

      • fork代价高(要创建进程)、父子进程通信复杂(适用IPC等方案通信)、支持的并发连接数量有限(一般最大几百个)
  • prefork

    • 定义:prefork 就是提前创建进程(pre-fork)。系统在启动的时候就预先创建好进程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去 fork 进程的操作
    • 父进程流程:socket->bind->listen->fork
    • 子进程流程:accept->read->业务处理->write->close
    • 实现关键就是多个子进程都accept同一个socket,当有新连接进入时,保证只有一个进程能最后accept成功
  • TPC(Thread Per Connection)

    • 定义:每次有新的连接就新建一个线程去专门处理这个连接的请求
    • 优势:创建线程的消耗比进程要少得多;线程通信相比进程通信更简单
    • 主进程流程:socket->bind->listen->accept->pthread
    • 子线程流程:read->业务处理->write->close
    • 存在问题

      • 创建线程仍然有代价、可能出现死锁、多线程互相影响可能导致整个进程退出
  • prethread

    • 定义:预先创建线程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去创建线程的操作
    • 主进程流程:socket->bind->listen->pthread
    • 子线程流程:accept->read->业务处理->write->close

单服务器高性能模式:Reactor与Proactor

  • Reactor

    • 使用I/O多路复用技术

      • 当多条连接共用一个阻塞对象后,进程只需要在一个阻塞对象上等待,而无须再轮询所有连接
      • 当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理
    • 核心组成:Reactor(负责监听和分配事件)、处理资源池(进程池/线程池)(负责处理事件)
    • 具体实现方式

      • 单Reactor单进程/线程

        • Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行分发。
        • 如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。
        • 如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的 Handler)来进行响应。
        • Handler 会完成 read-> 业务处理 ->send 的完整业务流程。
      • 单Reactor多线程

        • 主线程中,Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行分发
        • 如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件
        • 如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的 Handler)来进行响应
        • Handler 只负责响应事件,不进行业务处理;Handler 通过 read 读取到数据后,会发给 Processor 进行业务处理
        • Processor 会在独立的子线程中完成真正的业务处理,然后将响应结果发给主进程的 Handler 处理;Handler 收到响应后通过 send 将响应结果返回给 client
      • 多Reactor多进程/线程

        • 父进程中 mainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 接收,将新的连接分配给某个子进程
        • 子进程的 subReactor 将 mainReactor 分配的连接加入连接队列进行监听,并创建一个 Handler 用于处理连接的各种事件
        • 当有新的事件发生时,subReactor 会调用连接对应的 Handler(即第 2 步中创建的 Handler)来进行响应
        • Handler 完成 read→业务处理→send 的完整业务流程
  • Proactor

    • Proactor Initiator 负责创建 Proactor 和 Handler,并将 Proactor 和 Handler 都通过 Asynchronous Operation Processor 注册到内核
    • Asynchronous Operation Processor 负责处理注册请求,并完成 I/O 操作
    • Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor
    • Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理
    • Handler 完成业务处理,Handler 也可以注册新的 Handler 到内核进程

高性能负载均衡:分类及架构

  • 高性能集群的复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法
  • 负载均衡不只是为了计算单元的负载达到均衡状态,可以基于性能、负载、业务来考虑
  • 负载均衡分类

    • DNS负载均衡

      • 优点:简单、成本低;就近访问,提升访问速度。
      • 不足:更新不及时,有DNS缓存;扩展性差,控制权在域名商;分配策略比较简单
    • 硬件负载均衡

      • 通过单独的硬件设备来实现负载均衡功能,这类设备和路由器、交换机类似
      • 典型的硬件产品:F5、A10
      • 优点:功能强大;性能强大;稳定性高;支持安全防护
      • 不足:贵;扩展性差
    • 软件负载均衡

      • 通过在普通服务器上运行负载均衡软件来实现负载均衡功能
      • 典型产品:Nginx(7层负载均衡)、LVS(4层负载均衡)
      • 优点:简单;便宜(用普通Linux服务器即可);灵活可扩展
      • 不足:性能一般;功能没有硬件负载均衡强大;一般不具备防火墙和防 DDoS 攻击等安全功能
  • 三种负载均衡可以同时使用

    • 基本原则:DNS 负载均衡用于实现地理级别的负载均衡;硬件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡

高性能负载均衡:算法

  • 算法分类

    • 任务平分类。将收到的任务平均分配给服务器进行处理(绝对平均或加权平均)
    • 负载均衡类。根据服务器的负载来进行分配(CPU load、连接数、I/O使用率等)
    • 性能最优类。根据服务器的响应时间来进行任务分配,优先将新任务分配给响应最快的服务器
    • Hash类。根据任务中的某些关键信息进行 Hash 运算,将相同 Hash 值的请求分配到同一台服务器上
  • 具体算法

    • 轮询

      • 优点:最简单
      • 不足:不关注服务器本身状态(只要没宕机或断线);不关心服务器之间性能差异(加权轮询解决了该问题)
    • 负载最低优先

      • 将任务分配给当前负载最低的服务器。具体负载指标可以有多个(CPU load、连接数等)
      • 优点:感知服务器本身状态
      • 不足:复杂
    • 性能最优

      • 性能最优优先类算法是站在客户端的角度来进行分配的,优先将任务分配给处理速度最快的服务器
      • 优点:同样感知服务器本身状态(通过响应时间)
      • 不足:复杂(收集和统计响应时间本身性能有损耗,采样率和统计周期的设置都有讲究)
    • Hash类

      • 常见两种场景:按源地址Hash,按请求ID Hash

架构是什么

  • 系统

    • 定义:泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。
    • 关键词:关联、规则、能力
  • 模块和组件

    • 模块定义:一套一致而互相有紧密关连的软件组织。它分别包含了程序和数据结构两部分。现代软件开发往往利用模块作为合成的单位。模块的接口表达了由该模块提供的功能和调用它时所需的元素。模块是可能分开被编写的单位。
    • 组件定义:自包含的、可编程的、可重用的、与语言无关的软件单元,软件组件可以很容易被用于组装应用程序中。
    • 两者区别:从逻辑的角度来拆分系统后,得到的单元就是“模块”;从物理的角度来拆分系统后,得到的单元就是“组件”。划分模块的主要目的是职责分离;划分组件的主要目的是单元复用。
  • 框架和架构

    • 框架定义:通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品
    • 架构定义:指软件系统的“基础结构”,创造这些基础结构的准则,以及对这些结构的描述。
    • 两者区别:框架关注的是“规范”,架构关注的是“结构”。
  • 重新定义架构

    • 定义:软件架构指软件系统的顶层结构。

架构设计的目的

  • 架构设计的主要目的是为了解决软件系统复杂度带来的问题

    • 通过熟悉和理解需求,识别系统复杂性所在的地方,然后针对这些复杂点进行架构设计。
    • 架构设计并不是要面面俱到,不需要每个架构都具备高性能、高可用、高扩展等特点,而是要识别出复杂点然后有针对性地解决问题。
    • 理解每个架构方案背后所需要解决的复杂点,然后才能对比自己的业务复杂点,参考复杂点相似的方案。

软件系统复杂度来源

  • 高性能

    • 高性能带来的复杂度主要有两方面

      • 单台计算机内部为了高性能带来的复杂度
      • 多台计算机集群为了高性能带来的复杂度
    • 单机复杂度

      • 批处理->进程->线程->CPU多核并行
    • 集群复杂度

      • 任务分配、任务分解
  • 高可用

    • 定义:系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。
    • 计算高可用

      • 增加任务分配器、任务分配器与业务服务器之间的连接与交互、分配算法
    • 存储高可用

      • 存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响
    • 高可用状态决策

      • 通过冗余来实现的高可用系统,状态决策本质上就不可能做到完全正确
      • 决策形式:独裁式、协商式、民主式
  • 可扩展性

    • 具备良好可扩展性的系统,有两个基本条件:正确预测变化完美封装变化
    • 预测变化

      • 不能每个设计点都考虑可扩展性
      • 不能完全不考虑可扩展性
      • 所有的预测都存在出错的可能性
    • 应对变化

      • 方案1:将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”

        • 复杂点:系统需要拆分出变化层和稳定层、需要设计变化层和稳定层之间的接口
      • 方案2:提炼出一个“抽象层”和一个“实现层”
  • 低成本

    • 往往只有“创新”才能达到低成本目标
  • 安全

    • 功能安全:代码层面防范XSS 攻击、CSRF 攻击、SQL 注入等
    • 架构安全:访问控制策略
  • 规模

    • 规模带来复杂度的主要原因就是“量变引起质变”

      • 功能越来越多、数据越来越多

架构设计三原则

  • 合适原则

    • 合适原则宣言:“合适优于业界领先
    • 不合适导致失败的几种原因:开发资源不足;技术积累不够;业务场景不存在
  • 简单原则

    • 简单原则宣言:“简单优于复杂
    • 软件领域复杂性体现在:结构的复杂性、逻辑的复杂性
  • 演化原则

    • 演化原则宣言:“演化优于一步到位
    • 软件架构设计其实更加类似于大自然“设计”一个生物,通过演化让生物适应环境,逐步变得更加强大

      • 首先,设计出来的架构要满足当时的业务需要。
      • 其次,架构要不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善。
      • 第三,当业务发生变化时,架构要扩展、重构,甚至重写。

架构设计流程

  • 识别复杂度

    • 将主要复杂度问题列出来(几个方面:高性能、高可用、可扩展、低沉本、安全、规模)
    • 根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题
  • 设计备选方案

    • 常见错误1:设计最优秀的方案

      • 根据架构设计原则中“合适原则”和“简单原则“的要求去设计
    • 常见错误2:只设计一个备选方案

      • 备选方案的数量以 3 ~ 5 个为最佳
      • 备选方案的差异要比较明显
      • 备选方案的技术不要只局限于已经熟悉的技术
    • 常见错误3:备选方案过于详细

      • 耗费了大量的时间和精力
      • 将注意力集中到细节中,忽略了整体的技术设计
      • 评审的时候其他人会被很多细节给绕进去,评审效果很差
  • 评估和选择备选方案

    • 360度环评:列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案。
    • 常见的质量属性点:性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性等
    • 按优先级选择
  • 详细方案设计

    • 详细设计方案阶段可能遇到的一种极端情况就是在详细设计阶段发现备选方案不可行,一般情况下主要的原因是备选方案设计时遗漏了某个关键技术点或者关键的质量属性。

      • 架构师不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解
      • 通过分步骤、分阶段、分系统等方式,尽量降低方案复杂度
      • 如果方案本身就很复杂,那就采取设计团队的方式来进行设计

关于 Linux 网络,你必须知道这些

  • 网络模型(TCP/IP)

    • 应用层,负责向用户提供一组应用程序,比如 HTTP、FTP、DNS 等
    • 传输层,负责端到端的通信,比如 TCP、UDP 等
    • 网络层,负责网络包的封装、寻址和路由,比如 IP、ICMP 等
    • 网络接口层,负责网络包在物理网络中的传输,比如 MAC 寻址、错误侦测以及通过网卡传输网络帧等。
  • Linux网络栈

    • 传输层在应用程序数据前面增加了 TCP 头
    • 网络层在 TCP 数据包前增加了 IP 头
    • 网络接口层,又在 IP 数据包前后分别增加了帧头和帧尾
    • (网络接口层定义最大传输单元MTU,若网络层包大小超过该值,则网络层要先做分片后往下交付)
    • 总体调用栈:应用程序->系统调用->套接字->TCP/UDP->IP->链路层->igb/bnx->网卡
  • Linux网络收发流程

    • 参见下图

311.png

  • 性能指标

    • 带宽,表示链路的最大传输速率,单位通常为 b/s
    • 吞吐量,表示单位时间内成功传输的数据量,单位通常为 b/s(比特 / 秒)或者 B/s(字节 / 秒)
    • 延时,表示从网络请求发出后,一直到收到远端响应,所需要的时间延迟
    • PPS,是 Packet Per Second(包 / 秒)的缩写,表示以网络包为单位的传输速率
    • 其它包括:网络的可用性(网络能否正常通信)、并发连接数(TCP连接数量)、丢包率、重传率
  • 网络配置

    • 查看工具:ifconfig、ip
    • 命令:ifconfig eth0、ip -s addr show dev eth0
    • 指标介绍

      • 网络接口的状态标志。ifconfig 输出中的 RUNNING ,或 ip 输出中的 LOWER_UP ,都表示物理网络是连通的
      • MTU 的大小。MTU 默认大小是 1500
      • 网络接口的 IP 地址、子网以及 MAC 地址
      • 网络收发的字节数、包数、错误数以及丢包情况

        • errors 表示发生错误的数据包数,比如校验错误、帧同步错误等
        • dropped 表示丢弃的数据包数,即数据包已经收到了 Ring Buffer,但因为内存不足等原因丢包;
        • overruns 表示超限数据包数,即网络 I/O 速度过快,导致 Ring Buffer 中的数据包来不及处理(队列满)而导致的丢包;
        • carrier 表示发生 carrirer 错误的数据包数,比如双工模式不匹配、物理电缆出现问题等;
        • collisions 表示碰撞数据包数。
  • 套接字信息

    • 查看工具:netstat、ss
    • 命令:netstat -nlp、ss -ltnp
    • 指标介绍

      • 接收队列(Recv-Q)和发送队列(Send-Q),通常应该是 0。若不为0说明有网络包的堆积发生
      • syn backlog 是 TCP 协议栈中的半连接队列长度;全连接队列(accept queue)
  • 协议栈统计信息

    • 命令:netstat -s、ss -s
  • 网络吞吐&PPS

    • 查看工具:sar
    • 命令:sar -n DEV 2 10
    • 指标介绍

      • rxpck/s 和 txpck/s 分别是接收和发送的 PPS,单位为包 / 秒
      • rxkB/s 和 txkB/s 分别是接收和发送的吞吐量,单位是 KB/ 秒
      • rxcmp/s 和 txcmp/s 分别是接收和发送的压缩数据包数,单位是包 / 秒
  • 连通性&延迟

    • 查看工具:ping

C10K 和 C1000K 回顾

  • 定义

    • C表示Client的意思,C10K就是单机支持1w的请求;C1000K即单机支持100w请求。
  • I/O模型优化

    • I/O事件通知方式

      • 水平触发:只要文件描述符可以非阻塞地执行 I/O ,就会触发通知。也就是说,应用程序可以随时检查文件描述符的状态,然后再根据状态,进行 I/O 操作
      • 边缘触发:只有在文件描述符的状态发生改变(也就是 I/O 请求达到)时,才发送一次通知。这时候,应用程序需要尽可能多地执行 I/O,直到无法继续读写,才可以停止。
    • 使用非阻塞 I/O 和水平触发通知(select和poll)

      • 实现方式:每个线程同时监控一批套接字的文件描述符,轮询遍历出哪些可以执行I/O,再执行真正的网络读写
      • 缺点:轮询耗时、select还有文件描述符数量限制、每次调用都涉及用户态和内核态的两次切换
    • 使用非阻塞 I/O 和边缘触发通知,比如 epoll

      • 实现方式

        • epoll 使用红黑树,在内核中管理文件描述符的集合,这样就不需要应用程序在每次操作时都传入、传出这个集合
        • epoll 使用事件驱动的机制,只关注有 I/O 事件发生的文件描述符,不需要轮询扫描整个集合
    • 使用异步 I/O(Asynchronous I/O,简称为 AIO)

      • 实现方式:异步 I/O 允许应用程序同时发起很多 I/O 操作,而不用等待这些操作完成。而在 I/O 完成后,系统会用事件通知(比如信号或者回调函数)的方式,告诉应用程序
  • 工作模型优化

    • 主进程 + 多个 worker 子进程

      • 实现方式

        • 主进程执行 bind() + listen() 后,创建多个子进程
        • 在每个子进程中,都通过 accept() 或 epoll_wait() ,来处理相同的套接字
      • 惊群问题

        • 定义:当网络 I/O 事件发生时,多个进程被同时唤醒,但实际上只有一个进程来响应这个事件,其他被唤醒的进程都会重新休眠
        • 解决方法:在每个 worker 进程中,都增加一个了全局锁(accept_mutex)。这些 worker 进程需要首先竞争到锁,只有竞争到锁的进程,才会加入到 epoll 中,这样就确保只有一个 worker 子进程被唤醒
    • 监听到相同端口的多进程模型

      • 实现方式

        • 所有的进程都监听相同的接口,并且开启 SO_REUSEPORT 选项,由内核负责将请求负载均衡到这些监听进程中去
  • 支撑C1000K的优化措施

    • 实现方式

      • 跳过内核协议栈的冗长路径,把网络包直接送到要处理的应用程序那里去(DPDK 和 XDP)
    • DPDK

      • 定义:用户态网络的标准,它跳过内核协议栈,直接由用户态进程通过轮询的方式,来处理网络接收。
    • XDP

      • 定义:Linux 内核提供的一种高性能网络数据路径。它允许网络包,在进入内核协议栈之前,就进行处理,也可以带来更高的性能

【案例】怎么评估系统的网络性能

  • Linux网络基于TCP/IP协议栈,因此协议栈每层都可以评估分析其性能
  • 各协议层性能测试

    • 网络接口层&网络层(转发能力)

      • 测试工具:pktgen、hping3
      • 使用命令

        • modprobe pktgen
        • ps -ef | grep pktgen | grep -v grep
        • ls /proc/net/pktgen/
    • TCP/UDP

      • 测试工具:iperf3
      • 使用方式

        • 在服务端执行:iperf3 -s -i 1 -p 8888

          • (-s表示启动服务端,-i表示汇报间隔,-p表示监听端口)
        • 在客户端执行:iperf3 -c 192.168.0.30 -b 1G -t 15 -P 2 -p 10000

          • (-c表示启动客户端,192.168.0.30为目标服务器的IP,-b表示目标带宽,-t表示测试时间,-P表示并发数,-p表示目标服务器监听端口)
        • 结果指标

          • SUM 行就是测试的汇总结果,包括测试时间、数据传输量以及带宽等
          • 按照发送和接收,这一部分又分为了 sender 和 receiver 两行
    • HTTP

      • 测试工具:ab
      • 使用方式

        • 安装命令:yum install -y httpd-tools
        • 执行命令:ab -c 1000 -n 10000 http://192.168.0.30/

          • (-c表示并发请求数为1000,-n表示总的请求数为10000)
      • 结果指标

        • 测试结果分为三个部分,分别是请求汇总、连接时间汇总还有请求延迟汇总
    • 应用负载

      • 测试工具:wrk
      • 使用方式

【案例】DNS 解析时快时慢,我该怎么办

  • 域名与DNS解析

    • 查看DNS服务器:cat /etc/resolv.conf
    • DNS记录类型

      • A 记录,用来把域名转换成 IP 地址
      • CNAME 记录,用来创建别名
      • NS 记录,表示该域名对应的域名服务器地址
    • 查询域名IP与使用的DNS服务器

      • 使用工具:nslookup
      • 执行命令:nslookup www.baidu.com
      • 返回结果:使用的域名服务器及端口信息、域名的非权威查询结果
    • DNS查询链路

      • 使用工具:dig
      • 执行命令:dig +trace +nodnssec www.baidu.com

        • (+trace表示开启跟踪查询,+nodnssec表示禁止DNS安全扩展)
      • 返回结果:DNS解析过程中,各级DNS服务器的信息与查询耗时
    • (上述两个工具的安装:yum install -y bind-utils)
    • 本地DNS解析

      • 执行命令:cat /etc/hosts
  • DNS解析失败案例分析

    • 执行命令:nslookup www.baidu.com,返回connection timed out; no servers could be reached
    • 但直接ping IP地址是通的,说明服务器本身没问题
    • 再次执行命令:nslookup -debug www.baidu.com,开启debug模式,发现提示127.0.0.1#53(127.0.0.1)连接失败,说明是本地访问DNS服务器有问题
    • 执行命令:cat /etc/resolv.conf,发现没有输出,说明是本地没有配置DNS服务器
  • DNS解析不稳定案例分析

    • 执行命令:time nslookup www.baidu.com,发现耗时非常长,且有时候直接超时
    • 怀疑可能的问题有

      • DNS 服务器本身有问题,响应慢并且不稳定
      • 客户端到 DNS 服务器的网络延迟比较大
      • DNS 请求或者响应包,在某些情况下被链路中的网络设备弄丢了
    • 直接ping DNS服务器,发现耗时确实比较长,且有丢包情况
    • 更换DNS服务器后情况好转,或者可以开启DNS缓存:/etc/init.d/dnsmasq start
  • DNS优化方法

    • 对 DNS 解析的结果进行缓存
    • 对 DNS 解析的结果进行预取(这是浏览器等 Web 应用中最常用的方法,预取域名的DNS解析结果)
    • 使用 HTTPDNS 取代常规的 DNS 解析
    • 基于 DNS 的全局负载均衡(GSLB)

【案例】使用 tcpdump 和 Wireshark 分析网络流量

  • tcpdump

    • 仅支持命令行格式使用,常用在服务器中抓取和分析网络包
    • 使用范例:tcpdump -nn udp port 53 or host 35.190.27.188

      • -nn ,表示不解析抓包中的域名(即不反向解析)、协议以及端口号
      • udp port 53 ,表示只显示 UDP 协议的端口号(包括源端口和目的端口)为 53 的包
      • host 35.190.27.188 ,表示只显示 IP 地址(包括源地址和目的地址)为 35.190.27.188 的包
      • 这两个过滤条件中间的“ or ”,表示或的关系,也就是说,只要满足上面两个条件中的任一个,就可以展示出来
  • wireshark

    • 除了可以抓包外,还提供了强大的图形界面和汇总分析工具,在分析复杂的网络情景时,尤为简单和实用
    • 可以先使用tcpdump抓包并将结果输出,然后用wireshark分析

      • 输出命令:tcpdump -nn udp port 53 or host 35.190.27.188 -w ping.pcap

【案例】怎么缓解 DDoS 攻击带来的性能下降问题

  • DDoS

    • DDoS 的前身是 DoS(Denail of Service),即拒绝服务攻击,指利用大量的合理请求,来占用过多的目标资源,从而使目标服务无法响应正常请求。
    • DDoS(Distributed Denial of Service) 则是在 DoS 的基础上,采用了分布式架构,利用多台主机同时攻击目标主机。
    • 攻击类型

      • 耗尽带宽
      • 耗尽操作系统的资源
      • 消耗应用程序的运行资源
  • 排查经过

    • 客户端curl页面发现响应非常慢
    • 服务端通过sar命令,查看网络情况

      • 执行命令:sar -n DEV 1
      • 结果发现rxpck/s非常大,而txpck/s则相对较小。说明服务端收到大量的小包请求
    • 通过tcpdump抓包排查是什么小包

      • 执行命令:tcpdump -i eth0 -n tcp port 80
      • 结果发现大量数据包是Flags [S],表示这是一个 SYN 包。大量的 SYN 包表明这是 SYN Flood 攻击
      • SYN Flood的原理,是通过大量TCP的半开连接状态,从而无法建立新的 TCP 连接
    • 查看服务端TCP半开连接

      • 执行命令:netstat -n -p | grep SYN_REC
      • 结果发现大量相同的IP占用TCP连接
    • 防止SYN Flood攻击手段

      • 禁止某个指定IP的连接

        • 执行命令:iptables -I INPUT -s 192.168.0.2 -p tcp -j REJECT
      • 限制syn并发数为每秒1次

        • 执行命令:iptables -A INPUT -p tcp --syn -m limit --limit 1/s -j ACCEPT
      • 限制单个IP在60秒新建立的连接数为10

        • 执行命令:iptables -I INPUT -p tcp --dport 80 --syn -m recent --name SYN_FLOOD --update --seconds 60 --hitcount 10 -j REJECT
      • 增大半连接的最大数量

        • 执行命令:sysctl -w net.ipv4.tcp_max_syn_backlog=1024
      • 设置SYN_RECV失败时的重试次数

        • 执行命令:sysctl -w net.ipv4.tcp_synack_retries=1
      • 使用TCP SYN Cookies

        • TCP SYN Cookies 也是一种专门防御 SYN Flood 攻击的方法。SYN Cookies 基于连接信息(包括源地址、源端口、目的地址、目的端口等)以及一个加密种子(如系统启动时间),计算出一个哈希值(SHA1),这个哈希值称为 cookie

Linux 文件系统是怎么工作的?

  • 索引节点&目录项

    • Linux文件系统为每个文件都分配两个数据结构:索引节点(index node)和目录项(directory entry)
    • 索引节点,简称为 inode,用来记录文件的元数据,比如 inode 编号、文件大小、访问权限、修改日期、数据的位置等。索引节点和文件一一对应,它跟文件内容一样,都会被持久化存储到磁盘中。索引节点同样占用磁盘空间。
    • 目录项,简称为 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。
    • 目录项本身就是一个内存缓存,而索引节点则是存储在磁盘中的数据,并且也会缓存到内存中加速访问
    • 磁盘被划分为三个存储区域:超级块、索引节点区和数据块区

      • 超级块,存储整个文件系统的状态
      • 索引节点区,用来存储索引节点
      • 数据块区,则用来存储文件数据
  • 虚拟文件系统(VFS)

    • 为了支持各种不同的文件系统,Linux 内核在用户进程和文件系统的中间,又引入了一个抽象层,也就是虚拟文件系统 VFS
    • VFS 定义了一组所有文件系统都支持的数据结构和标准接口。这样,用户进程和内核中的其他子系统,只需要跟 VFS 提供的统一接口进行交互,而不需要再关心底层各种文件系统的实现细节
    • 底层文件系统分为三类:基于磁盘、内存、网络的文件系统

      • 基于磁盘的文件系统,即数据直接存储在计算机本地挂载的磁盘中。常见的 Ext4、XFS、OverlayFS 等
      • 基于内存的文件系统,也就是常说的虚拟文件系统。不需要任何磁盘分配存储空间,但会占用内存
      • 网络文件系统,也就是用来访问其他计算机数据的文件系统,比如 NFS、SMB、iSCSI 等
  • 文件系统I/O

    • 根据是否利用标准库缓存,可以把文件 I/O 分为缓冲 I/O 与非缓冲 I/O

      • 缓冲 I/O,是指利用标准库缓存来加速文件的访问,而标准库内部再通过系统调度访问文件。
      • 非缓冲 I/O,是指直接通过系统调用来访问文件,不再经过标准库缓存。
    • 根据是否利用操作系统的页缓存,可以把文件 I/O 分为直接 I/O 与非直接 I/O

      • 直接 I/O,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件。
      • 非直接 I/O 正好相反,文件读写时先要经过系统的页缓存,然后再由内核或额外的系统调用,真正写入磁盘。
    • 根据应用程序是否阻塞自身运行,可以把文件 I/O 分为阻塞 I/O 和非阻塞 I/O

      • 阻塞 I/O,是指应用程序执行 I/O 操作后,如果没有获得响应,就会阻塞当前线程,自然就不能执行其他任务。
      • 非阻塞 I/O,是指应用程序执行 I/O 操作后,不会阻塞当前的线程,可以继续执行其他的任务,随后再通过轮询或者事件通知的形式,获取调用的结果。
    • 根据是否等待响应结果,可以把文件 I/O 分为同步和异步 I/O

      • 同步 I/O,是指应用程序执行 I/O 操作后,要一直等到整个 I/O 完成后,才能获得 I/O 响应。
      • 异步 I/O,是指应用程序执行 I/O 操作后,不用等待完成和完成后的响应,而是继续执行就可以。等到这次 I/O 完成后,响应会用事件通知的方式,告诉应用程序。
  • 性能观测工具

    • 容量

      • df。所用命令:df -h(-h用可读性更好的容量单位,-i表示展示索引节点的容量占用情况)
    • 缓存

      • cat /proc/meminfo | grep -E "SReclaimable|Cached"
      • cat /proc/slabinfo | grep -E '^#|dentry|inode'
      • slabtop

Linux 磁盘I/O是怎么工作的

  • 虚拟文件系统(VFS)

    • 目录项,记录了文件的名字,以及文件与其他目录项之间的目录关系(内存缓存)
    • 索引节点,记录了文件的元数据(持久化数据)
    • 逻辑块,是由连续磁盘扇区构成的最小读写单元,用来存储文件数据(持久化数据)
    • 超级块,用来记录文件系统整体的状态,如索引节点和逻辑块的使用情况等(持久化数据)
  • 磁盘

    • 按存储介质分类:机械磁盘(HDD)、固态磁盘(SSD)

      • 机械磁盘

        • 盘片和磁头组成;随机I/O性能慢(要移动磁头)
        • 最小读写单位:扇区(512byte)
      • 固态磁盘

        • 无需寻址,速度快;
        • 最小读写单位:页(通常4KB、8KB)
      • 文件系统会把连续的扇区或页,组成逻辑块,作为最小管理单元
    • 按接口分类:IDE、SCSI 、SAS 、SATA 、FC等

      • 不同接口的设备,会分配不同的前缀作为设备名(例如IDE设备以hd开头,SCSI以sd开头)
      • 相同接口的多块设备,再以字母a、b、c等编号(例如sda,sdb)
      • 同一块设备,又可以分为不同的逻辑分区,以数字区分(sda1,sda2)
    • 在 Linux 中,磁盘实际上是作为一个块设备来管理的,也就是以块为单位读写数据,并且支持随机读写。
    • 每个块设备都会被赋予两个设备号,分别是主、次设备号。主设备号用在驱动程序中,用来区分设备类型;而次设备号则是用来给多个同类设备编号。
  • I/O栈

    • Linux存储系统的I/O栈,从上至下整体分为三个层次:文件系统层、通用块层、设备层
    • 文件系统层

      • 包括虚拟文件系统和其他各种文件系统的具体实现。它为上层的应用程序,提供标准的文件访问接口;对下会通过通用块层,来存储和管理磁盘数据
    • 通用块层

      • 主要功能

        • 向上,为文件系统和应用程序提供访问块设备的标准接口;向下,把各种异构的磁盘设备抽象为统一的块设备,并提供统一框架来管理这些设备的驱动程序
        • 给上层发来的 I/O 请求排队,并通过重新排序、请求合并等方式,提高磁盘读写的效率
      • 包括块设备 I/O 队列和 I/O 调度器。它会对文件系统的 I/O 请求进行排队,再通过重新排序和请求合并,然后才要发送给下一级的设备层

        • I/O调度算法:NONE、NOOP、CFQ 以及 DeadLine

          • NONE,相当于没算法。它完全不使用任何 I/O 调度器,对上层的 I/O 不做任何处理
          • NOOP ,是最简单的一种 I/O 调度算法。它实际上是一个先入先出的队列,只做一些最基本的请求合并
          • CFQ(Completely Fair Scheduler),也被称为完全公平调度器,是现在很多发行版的默认 I/O 调度器,它为每个进程维护了一个 I/O 调度队列,并按照时间片来均匀分布每个进程的 I/O 请求(支持优先级调度)
          • DeadLine,分别为读、写请求创建了不同的 I/O 队列,可以提高机械磁盘的吞吐量,并确保达到最终期限(deadline)的请求被优先处理
    • 设备层

      • 包括存储设备和相应的驱动程序,负责最终物理设备的 I/O 操作
  • 磁盘性能指标

    • 使用率,是指磁盘处理 I/O 的时间百分比。过高的使用率(比如超过 80%),通常意味着磁盘 I/O 存在性能瓶颈。
    • 饱和度,是指磁盘处理 I/O 的繁忙程度。过高的饱和度,意味着磁盘存在严重的性能瓶颈。当饱和度为 100% 时,磁盘无法接受新的 I/O 请求。
    • IOPS(Input/Output Per Second),是指每秒的 I/O 请求数。
    • 吞吐量,是指每秒的 I/O 请求大小。
    • 响应时间,是指 I/O 请求从发出到收到响应的间隔时间。
  • 磁盘I/O观测工具

    • 系统维度:iostat

      • 命令范例:iostat -d -x 2 10(-d显示磁盘情况,-x显示详细信息,每隔2秒输出10次)
      • r/s,每秒发送给磁盘的读请求数量(合并后)
      • w/s,每秒发送给磁盘的写请求数量(合并后)
      • rkB/s,每秒从磁盘读取的数据量(单位kB)
      • wkB/s,每秒写入磁盘的数据量(单位kB)
      • rrqm/s,每秒合并的读请求数(%rrqm表示合并读请求的百分比)
      • wrqm/s,每秒合并的写请求数(%rrqm表示合并写请求的百分比)
      • avgrq-sz,请求队列中的平均大小
      • avgqu-sz,平均请求队列长度
      • r_await,读请求处理完成等待时间(包括队列中的等待时间和设备实际处理实际,单位ms)
      • w_await,写请求处理完成等待时间(包括队列中的等待时间和设备实际处理实际,单位ms)
      • await,即r_await和w_await的平均值
      • svctm,处理I/O请求所需的平均实际(不包括等待时间,单位ms,该时间只是推断)
      • %util,磁盘处理I/O的时间百分比
    • 进程维度:pidstat

      • 命令范例:pidstat -d 2 10
      • 每秒读取的数据大小(kB_rd/s) ,单位是 KB。
      • 每秒发出的写请求数据大小(kB_wr/s) ,单位是 KB。
      • 每秒取消的写请求数据大小(kB_ccwr/s) ,单位是 KB。

【案例】如何找出狂打日志的“内鬼”

  • 排查经过

    • 通过top,查看系统整体性能

      • (CPU iowait%占比高,内存buffer/cache容量大,说明是I/O问题)
    • 通过iostat,查看系统整体I/O性能

      • 命令:iostat -d -x 2 10
      • (sda磁盘的 I/O使用率%util大,wkB/s、w_await数值高,说明sda磁盘有写瓶颈)
    • 通过pidstat,查看可疑进程的I/O性能

      • 命令:pidstat -d
      • (某进程的kB_wr/s数值很高,说明该进程一直在写I/O)
    • 通过strace,查看某进程的系统调用

      • 命令:strace -p
      • (发现该进程确实在写某个文件)
    • 通过lsof,查看进程打开的文件

      • 命令:lsof -p
      • (发现确实在打开某个文件疯狂写入)
    • 查看该进程的源代码,定位写文件的代码

【总结】如何迅速分析出系统I/O的瓶颈在哪里

  • 文件系统I/O性能指标

    • 存储空间容量、使用量、剩余空间
    • 索引节点容量、使用量、剩余空间
    • 页缓存、目录项缓存、索引节点缓存、具体文件系统缓存
    • IOPS(包括 r/s 和 w/s)、响应时间(延迟)、吞吐量(B/s)
  • 磁盘I/O性能指标

    • 使用率,是指磁盘忙处理 I/O 请求的百分比
    • IOPS(Input/Output Per Second),是指每秒的 I/O 请求数
    • 吞吐量,是指每秒的 I/O 请求大小
    • 响应时间,是指从发出 I/O 请求到收到响应的间隔时间
  • 性能工具

    • 系统维度

      • df:容量、使用量、剩余空间(加-i表示看索引,不加是磁盘;-h展现更好的容量单位)
      • /proc/meminfo:普通文件系统占用的缓存页Cached、可回收的slab的大小SReclaimable
      • /proc/slabinfo:目录项、索引节点、文件系统的缓存
      • slabtop:同上,但更直观
      • iostat:磁盘I/O使用率、IOPS、吞吐量、响应时间、平均队列长度与大小等等(-d显示磁盘情况,-x显示详细信息)
      • vmstat(加-d展示磁盘状态信息)
    • 进程维度

      • pidstat:进程读写I/O大小与延迟(加-d)
      • strace:查看某进程的系统调用
      • lsof:查看进程打开的文件
  • I/O问题整体分析思路

    • 先用 iostat 发现磁盘 I/O 性能瓶颈;
    • 再借助 pidstat ,定位出导致瓶颈的进程;
    • 随后分析进程的 I/O 行为;
    • 最后,结合应用程序的原理,分析这些 I/O 的来源。

【总结】磁盘 I/O 性能优化的几个思路

  • I/O基准测试

    • 基准测试工具:fio
    • 命令范例:fio -name=randread -direct=1 -iodepth=64 -rw=randread -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=1000 -group_reporting -filename=/dev/sdb
    • 入参选项

      • direct,表示是否跳过系统缓存(1表示跳过)
      • iodepth,表示使用异步 I/O时,同时发出的 I/O 请求上限
      • rw,表示 I/O 模式( read/write 分别表示顺序读 / 写, randread/randwrite 则分别表示随机读 / 写)
      • ioengine,表示 I/O 引擎,支持同步(sync)、异步(libaio)、内存映射(mmap)、网络(net)等
      • bs,表示 I/O 的大小
      • filename,表示文件路径。它可以是磁盘路径(测试磁盘性能),或文件路径(测试文件系统性能)
    • 返回内容

      • slat ,是指从 I/O 提交到实际执行 I/O 的时长(Submission latency)
      • clat ,是指从 I/O 提交到 I/O 完成的时长(Completion latency)
      • lat ,指的是从 fio 创建 I/O 到 I/O 完成的总时长
  • 应用程序优化

    • 用追加写代替随机写,减少寻址开销,加快 I/O 写的速度
    • 借助缓存 I/O ,充分利用系统缓存,降低实际 I/O 的次数
    • 在应用程序内部构建自己的缓存,或者用 Redis 这类外部缓存系统
    • 在需要频繁读写同一块磁盘空间时,可以用 mmap 代替 read/write,减少内存的拷贝次数
    • 在需要同步写的场景中,尽量将写请求合并,而不是让每个请求都同步写入磁盘,即可以用 fsync() 取代 O_SYNC
    • 在多个应用程序共享相同磁盘时,为了保证 I/O 不被某个应用完全占用,推荐使用 cgroups 的 I/O 子系统,来限制进程 / 进程组的 IOPS 以及吞吐量
  • 文件系统优化

    • 根据实际负载场景的不同,选择最适合的文件系统
    • 优化文件系统的配置选项,包括文件系统的特性(如 ext_attr、dir_index)、日志模式(如 journal、ordered、writeback)、挂载选项(如 noatime)等

      • 调整文件系统的特性(tune2fs)
      • 调整文件系统的日志模式和挂载选项(/etc/fstab,mount)
    • 优化文件系统的缓存

      • 优化 pdflush 脏页的刷新频率(设置 dirty_expire_centisecs 和 dirty_writeback_centisecs)
      • 优化脏页的限额(调整 dirty_background_ratio 和 dirty_ratio 等)
    • 优化内核回收目录项缓存和索引节点缓存的倾向

      • 调整 /proc/sys/vm/vfs_cache_pressure(默认值 100),数值越大,就表示越容易回收
    • 在不需要持久化时,还可以用内存文件系统 tmpfs,以获得更好的 I/O 性能
  • 磁盘优化

    • 换用性能更好的磁盘,比如用 SSD 替代 HDD
    • 可以使用 RAID ,把多块磁盘组合成一个逻辑磁盘,构成冗余独立磁盘阵列
    • 针对磁盘和应用程序 I/O 模式的特征,我们可以选择最适合的 I/O 调度算法
    • 可以对应用程序的数据,进行磁盘级别的隔离
    • 在顺序读比较多的场景中,我们可以增大磁盘的预读数据

      • 调整内核选项 /sys/block/sdb/queue/read_ahead_kb,默认大小是 128 KB,单位为 KB。
      • 使用 blockdev 工具设置,比如 blockdev --setra 8192 /dev/sdb
    • 可以优化内核块设备 I/O 的选项

      • 调整磁盘队列的长度 /sys/block/sdb/queue/nr_requests,适当增大队列长度

Linux内存是怎么工作的?

  • 内存映射

    • 每个进程会有对应的虚拟地址空间
    • 虚拟地址空间包括:用户空间(128TB,64位系统)和内核空间(128TB,64位系统)
    • 内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系
    • 页表实际上存储在 CPU 的内存管理单元 MMU 中
    • 当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行
    • MMU 并不以字节为单位来管理内存,而是规定了一个内存映射的最小单位,也就是,通常是4KB大小
    • 解决页表过大问题:多级页表

      • 把内存分成区块来管理,将原来的映射关系改成区块索引区块内的偏移
      • Linux 用的是四级页表来管理内存页,虚拟地址被分为5个部分,前4个表项用于选择页,而最后一个索引表示页内偏移
  • 虚拟内存空间分布

    • 用户空间内存,地址空间从低到高分别是五种不同的内存段

      • 只读段:包括代码和常量等
      • 数据段:包括全局变量等
      • :包括动态分配的内存,从低地址开始向上增长
      • 文件映射段:包括动态库、共享内存等,从高地址开始向下增长
      • :包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB
    • 堆和文件映射段的内存是动态分配的
  • 内存分配与回收

    • malloc()是C标准库提供的内存分配函数,对应到系统调用上有两种实现方式,即brk()mmap()
    • 对小块内存(小于 128K),C 标准库使用 brk() 来分配

      • 通过移动堆顶的位置来分配内存
      • 这些内存释放后并不会立刻归还系统,而是被缓存起来重复使用

        • 可以减少缺页异常的发生,提高内存访问效率
        • 在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片
    • 而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配

      • 在文件映射段找一块空闲内存分配

        • 每次 mmap 都会发生缺页异常
        • 在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大
    • 当这两种调用发生后,其实并没有真正分配内存。这些内存,都只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存
    • 调用 free() 或 unmap() ,来释放不用的内存
    • 内存回收方式

      • 回收缓存:比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面
      • 回收不常访问的内存:把不常用的内存通过交换分区SWAP直接写到磁盘中

        • Swap 其实就是把一块磁盘空间当成内存来用
      • 杀死进程:内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程

        • 监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分

          • 一个进程消耗的内存越大,oom_score 就越大
          • 一个进程运行占用的 CPU 越多,oom_score 就越小
          • 进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死
          • 可以手动设置进程的 oom_adj ,从而调整进程的 oom_score

            • oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死
            • echo -16 > /proc/$(pidof sshd)/oom_adj
  • 查看内存使用情况

    • free

      • 第一列,total 是总内存大小
      • 第二列,used 是已使用内存的大小,包含了共享内存
      • 第三列,free 是未使用内存的大小
      • 第四列,shared 是共享内存的大小
      • 第五列,buff/cache 是缓存和缓冲区的大小
      • 最后一列,available 是新进程可用内存的大小

        • available 不仅包含未使用内存,还包括了可回收的缓存,所以一般会比未使用内存更大
    • top

      • VIRT 是进程虚拟内存的大小,只要是进程申请过的内存,即便还没有真正分配物理内存,也会计算在内
      • RES 是常驻内存的大小,也就是进程实际使用的物理内存大小,但不包括 Swap 和共享内存
      • SHR 是共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等
      • %MEM 是进程使用物理内存占系统总内存的百分比

怎么理解内存中的Buffer和Cache?

  • Buffers

    • Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)
  • Cached

    • Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据
  • SReclaimable

    • Slab 包括两部分, SReclaimable 是可回收部分; SUnreclaim是不可回收部分
  • Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中
  • Buffer/Cache用处

    • 从写的角度来说,不仅可以优化磁盘和文件的写入,对应用程序也有好处,应用程序可以在数据真正落盘前,就返回去做其他工作。
    • 从读的角度来说,既可以加速读取那些需要频繁访问的数据,也降低了频繁 I/O 对磁盘的压力。
  • 磁盘&文件

    • 磁盘是一个块设备,可以划分为不同的分区
    • 在分区之上再创建文件系统,挂载到某个目录,之后才可以在这个目录中读写文件
    • 在读写普通文件时,会经过文件系统,由文件系统负责与磁盘交互;而读写磁盘或者分区时,就会跳过文件系统,也就是所谓的“裸I/O“

【案例】如何利用系统缓存优化程序的运行效率?

  • 缓存命中率

    • 命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好。
    • 查看系统缓存命中情况的工具:cachestat,cachetop

      • 工具安装:yum install bcc-tools,且要求kernel版本在4.1以上
      • 安装完成后,手动设置PATH目录:export PATH=$PATH:/usr/share/bcc/tools
      • cachestat

        • 提供了整个操作系统缓存的读写命中情况
        • TOTAL ,表示总的 I/O 次数
        • MISSES ,表示缓存未命中的次数
        • HITS ,表示缓存命中的次数
        • DIRTIES, 表示新增到缓存中的脏页数
        • BUFFERS_MB 表示 Buffers 的大小,以 MB 为单位
        • CACHED_MB 表示 Cache 的大小,以 MB 为单位
      • cachetop

        • 提供了每个进程的缓存命中情况
        • READ_HIT 和 WRITE_HIT ,分别表示读和写的缓存命中率
  • 查看文件缓存

    • 使用pcstat工具(前提需要安装go语言)

      • 安装完go之后,执行以下命令:

        • export GOPATH=~/go
        • export PATH=~/go/bin:$PATH
        • go get golang.org/x/sys/unix
        • go get github.com/tobert/pcstat/pcstat
      • 命令:pcstat <file_url>

【案例】内存泄漏了,我该如何定位和处理?

  • 内存可能出现的问题

    • 内存泄漏。没正确回收分配后的内存,导致不仅应用程序自己不能访问,系统也不能把它们再次分配给其他应用。
    • 越界访问。访问的是已分配内存边界外的地址,导致程序异常退出
  • 用户空间内存是否会泄漏

    • 只读段,包括程序的代码和常量,由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏。
    • 数据段,包括全局变量和静态变量,这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏。
    • 内存映射段,包括动态链接库和共享内存,其中共享内存由程序动态分配和管理。所以,如果程序在分配后忘了回收,就会导致跟堆内存类似的泄漏问题。
  • 检测内存泄漏工具:memleak

    • 是bcc-tools内的工具之一
    • 命令:memleak -p -a

Swap概念

  • 系统内存紧张的处理方式:内存回收、OOM杀死进程
  • 可回收内存的类型:文件页(Buffer和Cache)、匿名页(应用程序动态分配的堆内存)

    • 文件页,可直接回收。若数据暂时还未写入磁盘(脏页),则回收前先写入磁盘再回收

      • 应用程序中,通过系统调用 fsync,把脏页同步到磁盘
      • 内核线程 pdflush 负责这些脏页的刷新
    • 匿名页,不能直接回收。需要使用swap,将数据写入磁盘中,然后释放内存给其它进程使用。当需要使用这些数据时,再从磁盘中读取即可
  • swap原理

    • Swap 说白了就是把一块磁盘空间或者一个本地文件(以下讲解以磁盘为例),当成内存来使用。

      • 换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存
      • 换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来
    • 系统回收内存的两种方式:直接回收、内核线程定期回收

      • 直接回收:当有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内存(Buffer、Cache),进而尽可能地满足新内存请求
      • 内核线程定期回收

        • 定期回收内存的内核线程:kswapd0
        • 定义了三个阈值:页最小阈值(pages_min)页低阈值(pages_low)页高阈值(pages_high)

          • 剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存
          • 剩余内存落在页最小阈值页低阈值中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止
          • 剩余内存落在页低阈值页高阈值中间,说明内存有一定压力,但还可以满足新内存请求
          • 剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力
        • 查看页最小阈值:cat /proc/sys/vm/min_free_kbytes
        • 其它阈值由最小阈值通过公式计算得出

          • pages_low = pages_min*5/4
          • pages_high = pages_min*3/2
  • NUMA和Swap

    • 在 NUMA 架构下,多个处理器被划分到不同 Node 上,且每个 Node 都拥有自己的本地内存空间
    • 而同一个 Node 内部的内存空间,实际上又可以进一步分为不同的内存域(Zone),比如直接内存访问区(DMA)、普通内存区(NORMAL)、伪内存区(MOVABLE)等
    • 使用numactl工具,查看每个node的内存使用情况

      • 命令:numactl --hardware
    • 查看每个node的各个swap阈值等信息

      • 命令:cat /proc/zoneinfo
  • swappiness

    • 直接回收和Swap两种回收方式,系统如何选择?
    • Linux 提供了一个 /proc/sys/vm/swappiness 选项,用来调整使用 Swap 的积极程度
    • swappiness 的范围是 0-100,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页

【总结】如何“快准狠”找到系统内存的问题

  • 内存性能指标

    • 系统维度

      • 已使用内存(Used),即已经使用的内存,包含了共享内存
      • 未使用内存(free),即未使用的内存
      • 可用内存(available),表示进程可以使用的最大内存,其包括未使用内存和可回收缓存
      • 共享内存(shared),其允许两个不相关的进程访问同一个逻辑内存。通过 tmpfs 实现,所以它的大小也就是 tmpfs 使用的内存大小
      • 缓存&缓冲区(Cache&Buffer)

        • 缓存包括两部分:一部分是磁盘读取文件的页缓存,另一部分,是 Slab 分配器中的可回收内存
        • 缓冲区是对原始磁盘块的临时存储,用来缓存将要写入磁盘的数据
    • 进程维度

      • 虚拟内存(VIRT),进程申请过的内存,即便还没有真正分配物理内存也会计算在内。包括:只读段、数据段、堆、文件映射段、栈等等。
      • 常驻内存(RES),进程实际使用的物理内存大小,不包括 Swap 和共享内存。
      • 共享内存(SHR),既包括与其他进程共同使用的真实的共享内存,还包括了加载的动态链接库以及程序的代码段等
      • 实际使用的物理内存(PSS),比例分配共享库占用的内存
      • 进程独自占用的物理内存(USS),不包含共享库占用的内存
    • 缺页异常

      • 系统调用内存分配请求后,不会立刻为其分配物理内存,而是在请求首次访问时通过缺页异常来分配

        • 可以直接从物理内存中分配时,被称为次缺页异常
        • 需要磁盘 I/O 介入(比如 Swap)时,被称为主缺页异常
    • Swap内存

      • 已用空间和剩余空间很好理解,即已经使用和没有使用的内存空间
      • 换入和换出速度,表示每秒钟换入和换出内存的大小
  • 内存性能工具

    • 系统整体维度

      • free
      • vmstat
      • cachestat
      • sar
    • 进程维度

      • top
      • ps
      • pidstat
      • cachetop
      • memleak
  • 分析思路

    • 先用 free 和 top,查看系统整体的内存使用情况
    • 再用 vmstat 和 pidstat,查看一段时间的趋势,从而判断出内存问题的类型
    • 最后进行详细分析,比如内存分配分析、缓存 / 缓冲区分析、具体进程的内存使用分析等
  • 优化思路

    • 最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时 Swap 的使用倾向
    • 减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等
    • 尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问
    • 使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽
    • 通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧张,核心应用也不会被 OOM 杀死

211.png
212.png
213.png