Tallate

该吃吃该喝喝 啥事别往心里搁

为什么需要Scrum

传统方法(主要是瀑布法)效率不足且经常引起项目最终无法交付。

Scrum团队

团队效率的提升比个人能力的提升对项目完成的影响更大
卓越团队的特点:

  1. 明确的目标
    他们具有希望超越寻常的目标。实现这种目标的动力促使他们超越寻常,达到卓越。他们下决心拒绝平庸,出类拔萃,这种决定改变了他们看待自己的方式,扩大了他们的能力范围。
  2. 自主决策能力
    这种团队自我组织,自我管理,有能力决定如何开展工作,并获得了根据自己决定做事的授权。
  3. 多功能
    这些团队具备完成项目的所有技能:计划、设计、生产、销售、分销。具有这些技能的成员相互学习,相互提高。一位设计革命性新款相机的团队成员说:“当所有团队成员都在一间大房子里办公时,某个人的信息就是你的信息,并且得来全不费功夫。你会开始思考对整个团体而言,最优选择或次优选择是什么,而不再仅仅从你个人的角度考虑问题。”
    每个团队成员都具备完成工作所需的各种技能,而不严格划分每个成员的角色,如果严格划分角色,不同角色间交流情报时就会容易产生误差。
  4. 透明度高、注重信息分享
  5. 团队规模
    如果团队规模过大,沟通渠道增多会带来更大的沟通成本。
    Scrum团队倾向于让每个成员都知道其他人在做什么,每个人正在做什么工作,正面临哪些挑战,取得了哪些进步等等,都是透明的。团队规模过大会影响沟通效果,进而导致团队分裂成小团体,以至于多功能的团队就不复存在了。
  6. Scrum Master
    制订工作框架:冲刺、每日立会、检查与回顾。
    Scrum Master负责确保整个工作流程的顺利推进,他的工作职责是召集会议,确保团队运作过程的透明度,为团队发现障碍,并持续改进。
  7. 工作制度
    不要把问题归咎于某一个人,而是寻求制度上的解决方案。

周期

  1. 冲刺
    冲刺指的是项目中的一个小的迭代周期,叫冲刺会比较有紧张感。
    每个周期固定时间审视一下这个周期内团队成员做的成果
    冲刺周期是固定的,有固定期限可以让团队成员方便评估自己的节奏在每个周期能做多少事情,并且不允许团队之外的任何人给团队内成员增加任务。
  2. 每日立会
    了解这个冲刺周期内各项任务的进展状况,有没有障碍,需不需要其他人来帮忙;团队的一切任务都是自主决定、自主完成的,不需要汇报。
    时间不宜过长,不应给团队带来负担。
    Scrum Master每日问3个问题示例:
  • 你昨天做了什么去帮助团队完成冲刺;
  • 今天你打算做什么来帮助团队完成冲刺;
  • 什么因素阻碍了团队的前进之路?
    每日立会规则:
    一、每天会议召开的时间是固定的,每个成员都要出席;
    二、开会时间不能超过15分钟,每次开会都直击重点,如果有事情需要进一步讨论,可以先记录下来,在每日立会结束之后再做进一步的讨论;
    三、每个人都要积极参与,尽量让每个人都站起来开会。

避免浪费(人力)

  1. 一次只做一件事
    研究报告指出,哪些最喜欢同时执行多项任务的人自制能力相对较弱,没有办法让自己长时间集中精力。
    同时执行多项任务一般不会加快速度,反而会拖累。
  2. 一次把事情做完
    Scrum开发过程中的工作是有节奏的。每一个迭代期,或者说每一个冲刺期,团队都试图完成很多事情。但是“完成”意味着一款能被消费者使用的、完整的、可交付的产品。如果在迭代期的最后,事情只做了一半,你将比一点都没开始做更糟糕。你花费了资源,付出了努力和时间,最后没有得到任何成果。

一次把事情做对

如果你出了一个错误(我们都会出错),那就在发现错误时立即修改,不然就会付出代价。
生产流程应该是持续改进的,而处理问题的最佳时机是你发现问题的时候,而不是发现问题之后很久再着手解决。
出现问题不立即修复,而是放到很久之后,会浪费更多时间:当你在做一个项目的时候,大脑专注于它,你很清楚做某件事的所有原因,这时你的脑子里存在一个与之相关的复杂架构,想在几个星期后重新建立这样的架构十分困难,会花费更多时间。

工时越长,效率越低

马克思韦尔曲线
工作时间太长的人会开始犯错,而改正错误可能会比创造新成绩花费更多的时间。工作超出负荷的员工比较不容易集中注意力,而且会影响别人也跟着分心,不久之后他们就会开始做出错误决策。
这意味着我们更应该关注完成任务的时长和质量而不是工时长短。

确保工作的合理性

有几种不合理现象:
1、目标荒谬、不切实际
2、期待过高
3、负担过重,比如繁琐的规定、没必要的汇报、会议等过多
4、情绪浪费,比如团队中某个人倾向于激怒别人、削弱团队斗志。

周期规划会

给项目划分任务
确立优先级:由产品负责人决定优先级
确定需要花费多少精力、时间和资金。怎么知道自己的速度?每个成员冲刺周期结束时复盘可以知道自己的效率如何,并且可以通过消除障碍来变得更快
让执行团队评估自己的工作
用用户故事描绘任务:角色、活动、商业价值
用户故事必须完整:独立性、可协商性(不要带太多细节)、有价值、可评估、规模小、可测试
用户故事来源:需要产品负责人拿出一半时间与购买产品的客户开展对话(了解客户对于最新产品的看法,看看自己的产品是否为客户创造了价值),还要拿出一半时间拟定待办事项清单(让团队成员知道客户重视什么、不重视什么)。

产品负责人的特点

  • 第一,产品负责人需要在相关领域内掌握丰富的专业知识。
  • 第二,产品负责人必须获得自主决策权。
  • 第三,产品负责人必须有足够的时间与团队成员接触,向团队成员解释清楚需要做什么以及为什么要这么做。
  • 第四,产品负责人必须为价值负责。

信息瀑布:人们倾向于遵从之前众人的行为 - 别人的评估意见只能拿来作为参考,以便改进自己的判断,而不是完全取代自己的判断。
光环效应:是指当认知者对一个人的某种特征形成好或坏的印象后,还倾向于据此推论该人其他方面的特征,本质上属于以偏概全的认知错误 - 评估时保证匿名性,减少互相影响
拥抱变化:一般最初的计划评估不会完美,关键在于,要在项目执行过程中及时加以调整和改进,而不是刻板地遵循计划。详细规划出能够创造出新价值的部分,至于项目中的剩余部分,计划粗略一些也无所谓。每个冲刺周期结束之际,都必须为顾客展示一些看得见、摸得着的新价值。你可以问顾客一些问题,比如,这是你要的吗?这能帮你解决一些问题吗?我们的方向是对的吗?如果答案是否定的,那么你就要修改你的计划。

二八原则

往往20%的功能可以覆盖80%的价值
1、根据产品负责人对需求优先级的排列可以确定最重要的那部分需求
2、先做最重要的MVP部分,以期快速得到市场反馈,在此基础上检查和调整产品

风险

Scrum的目的之一是降低项目研发过程中的风险,常见的风险是市场风险、技术风险和财务风险。
市场风险:Scrum注重渐进式迭代,每个周期通过show case将产品展示给客户,收集用户意见,因此可以实时对产品做出小改动,不必等到投入大量成本后再被迫做出改动。
技术风险:先做几种原型,实际加以比较。
财务风险:怎么发现自己做的产品能挣到钱?渐进式迭代+showcase

性能分析-内存

内存原理

内存映射

Linux为每个进程提供了一个独立的虚拟地址空间,分为内核空间用户空间,且32位和64位系统中空间范围也不同。
Linux虚拟内存

用户态和内核态

  • 用户态:进程在用户态时,只能访问用户空间内存;
  • 内核态:进入内核态后,才可以访问内核空间内存

内存映射虚拟内存地址映射到物理内存地址,这个映射是通过每个进程的页表来实现的:
Linux内存映射

建模

图数据结构

属性图结构
X

  • 包含节点和边,同时节点和边上又有属性property和标签label
  • 边有名字和方向

图数据库查询语言

  • Cypher
    应用于Neo4j、GDB等
  • Gremlin
    应用于GDB

跨域模型

在节点和关联关系不是很复杂的情况下,我们建的图通常只有一个领域的内容,比如人与人之间的关联图谱。但是随着业务的发展,我们会觉得单一的人物关系不能满足我们的使用需求,我还希望加入企业注册数据,来进行企业与人之间的管理分析。随后,我们陆续加入了法律诉讼信息、新闻资讯信息等,这样我们的图从一张小图变成了大图,也从只有一个领域(人)的模型变成了多个领域的跨域模型。
跨域模型有助于我们理解复杂的价值链背后的关联,不仅可以联合多个领域,而且每个领域的内容又能单独区分开来。这里主要借助了图数据里的2个概念:属性图和标签。属性图模型让不同的领域很容易联系起来,这样每个域都是可达的;标签既能表示不同节点在域中扮演的角色,又可以让我们将它归属的节点和元数据结合起来

面向查询设计

建模其实就是利用图结构来描述问题的过程,为了使我们的模型更接近业务需求,有一个方法,叫做面向查询设计:

  • 将需求转化为领域问题
  • 明确领域内出现的节点和关系
  • 把这些节点和联系翻译成查询语言
  • 使用路径表达式,描述需要解决的问题

使用图查询语言构建数据模型

1
2
3
4
5
6
7
8
# 创建带有属性name和age的People节点
create(p:People{name:"Alex", age:20});
# 匹配 People节点,并返回其 name 和 age 属性
match (p:People) return p.name, p.age
# 匹配所有 age 为20的 People 节点
match (p:People{age:20}) RETURN p
# 创建单向的Friend关系
create(:People{name:"Alex", age:20})-[r:Friends]->(:People{name:"Tom", age:22})

参考

  1. 《一起学图数据库》之四:理论与实践齐飞,聊聊图数据库的建模
  2. 《一起学图数据库》之五:再谈建模
  3. 一文教你用 Neo4j 快速构建明星关系图谱
  4. Neo4j - CQL简介
  5. Neo4j Developer Manual
  6. Gremlin

架构

RocketMQStreams架构

  • 采用 shared-nothing 的分布式架构设计,依赖消息队列做负载均衡和容错机制,单实例可启动,增加实例实现能力扩展,并发能力取决于分片数;
    其实就是stateless
  • 利用消息队列的分片做 shuffle,利用消息队列负载均衡实现容错
  • 利用存储实现状态备份,实现 Exactly-ONCE 的语义。用结构化远程存储实现快速启动,不等本地存储恢复;
  • 重力打造过滤优化器,通过前置指纹过滤,同源规则自动归并,hyperscan 加速,表达式指纹提高过滤性能

RocketMQ Streams消费一条消息的过程

github - rocketmq-streams

ISource消费数据源

触发条件:

分类:

  • RocketMQSource读取来自RocketMQ的消息

流程(RocketMQSource):

1.

IStreamOperator算子处理来自Source的数据

触发条件:

  • AbstractSource处理消息AbstractSource#executeMessage

ISink存储数据

触发条件:

  • SinkAction算子触发

流程(SinkAction):

  1. 默认写到cache IMessageCache#addCache
    触发时机:接收消息时 SinkAction#doMessage
  2. 启动自动flush ISink#openAutoFlush
    触发时机:刷新配置后的处理 SinkAction#doProcessAfterRefreshConfigurable
    触发逻辑:把队列排空,并写入到存储中 MessageCache#flush()
    触发间隔:100ms ScheduleManager#start

shared-nothing

shared-nothing体现在RocketMQ的以下属性:

  • 消息的存储采用Topic+Queue的模式;
  • Consumer端消费采取pull模式而不是push;
  • Consumer端负载均衡Rebalance,同时Rebalance也可以实现容错

RocketMQ 消息的存储和查询原理
RocketMQ 如何发送一条消息

负载均衡

RocketMQ Streams在启动时会对上游的ISource拆分分片,
AbstractPullSource#startSource

  • 获取所有分片 IPullSource#fetchAllSplits
  • 拆分分片 ISourceBalance#doBalance
  • 定时监听分片的变更 AbstractPullSource#doSplitChanged

Exactly-ONCE 语义

shuffle

容器的原理:Namespace做隔离,Cgroups做限制,rootfs做文件系统。
k8s的主要作用是调度容器,管理容器的生命周期,在k8s中调度的最小单位是pod,除此之外还有service、cluster这些概念。

阅读全文 »

性能分析-CPU

性能指标

平均负载

平均负载是指单位时间内,处于可运行状态不可中断状态的平均进程数,也就是平均活跃进程数。所以,它不仅包括了正在使用 CPU 的进程,还包括等待 CPU 和等待 I/O 的进程,因此和CPU使用率也并没有直接关系。

  • 所谓可运行状态的进程,是指正在使用 CPU 或者正在等待 CPU 的进程,也就是我们常用 ps 命令看到的,处于 R 状态(Running 或 Runnable)的进程。
  • 不可中断状态的进程则是正处于内核态关键流程中的进程,并且这些流程是不可打断的,比如最常见的是等待硬件设备的 I/O 响应,也就是我们在 ps 命令中看到的 D 状态(Uninterruptible Sleep,也称为 Disk Sleep)的进程。

当平均负载为2时,意味着:

  • 在只有2个CPU的系统上,意味着所有CPU都刚好被完全占用;
  • 在4个CPU的系统上,意味着CPU有50%的空闲;
  • 在只有一个CPU的系统中,意味着有一半的进程竞争不到CPU。

平均负载为多少比较合适?

  • 先看系统有几个CPU,可以通过top命令或文件/proc/cpuinfo读取
  • 经验讲当平均负载高于CPU数量70%时,需要分析排查负载过高的问题
  • uptime查看负载变化趋势,把系统的平均负载监控起来,根据更多的历史数据来判断负载变化趋势,如果有明显升高趋势再做分析。

CPU使用率

CPU使用率的定义和计算

CPU 使用率,是单位时间内 CPU 繁忙情况的统计,跟平均负载并不一定完全对应。
比如:
CPU 密集型进程,使用大量 CPU 会导致平均负载升高,此时这两者是一致的;
I/O 密集型进程,等待 I/O 也会导致平均负载升高,但 CPU 使用率不一定很高;
大量等待 CPU 的进程调度也会导致平均负载升高,此时的 CPU 使用率也会比较高。

Linux 通过 /proc 虚拟文件系统,向用户空间提供了系统内部状态的信息,而 /proc/stat 提供的就是系统的 CPU 和任务统计信息:

1
2
3
4
$ cat /proc/stat | grep ^cpu
cpu 280580 7407 286084 172900810 83602 0 583 0 0 0
cpu0 144745 4181 176701 86423902 52076 0 301 0 0 0
cpu1 135834 3226 109383 86476907 31525 0 282 0 0 0
  • 第一行cpu指所有cpu的累加,后面的cpu0, cpu1表示各个cpu的统计
  • user(通常缩写为 us),代表用户态 CPU 时间。注意,它不包括下面的 nice 时间,但包括了 guest 时间。
  • nice(通常缩写为 ni),代表低优先级用户态 CPU 时间,也就是进程的 nice 值被调整为 1-19 之间时的 CPU 时间。这里注意,nice 可取值范围是 -20 到 19,数值越
  • 大,优先级反而越低。
  • system(通常缩写为sys),代表内核态 CPU 时间
  • idle(通常缩写为id),代表空闲时间。注意,它不包括等待 I/O 的时间(iowait)。
  • iowait(通常缩写为 wa),代表等待 I/O 的 CPU 时间。
  • irq(通常缩写为 hi),代表处理硬中断的 CPU 时间。
  • softirq(通常缩写为 si),代表处理软中断的 CPU 时间。
  • steal(通常缩写为 st),代表当系统运行在虚拟机中的时候,被其他虚拟机占用的 CPU 时间。
  • guest(通常缩写为 guest),代表通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的 CPU 时间。
  • guest_nice(通常缩写为 gnice),代表以低优先级运行虚拟机的时间。

CPU使用率,就是除了空闲时间外的其他时间占总CPU时间的百分比,用公式来表示就是:

1
CPU使用率 = 1 - 空闲时间 / 总CPU时间

但是这个值是开机以来的CPU使用率,没什么参考价值,重要的是计算单位时间内的CPU使用率或简称平均CPU使用率

1
平均CPU使用率 = 1 - (空闲时间new - 空闲时间old) / (总CPU时间new - 总CPU时间old)

平均CPU使用率可以通过top、ps命令来查看。

造成CPU使用率过高的原因可能是:

  • 用户CPU过高
  • 系统CPU过高(如上下文切换)
  • 等待IO的CPU(如等待磁盘的响应)
  • 中断CPU(包括软中断和硬中断)

CPU上下文切换

CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
根据任务的不同,CPU 的上下文切换可以分为几个不同的场景,也就是进程上下文切换线程上下文切换以及中断上下文切换

进程上下文切换

根据Linux的特权等级分级:

  • 内核空间(Ring 0)具有最高权限,可以直接访问所有资源;
  • 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过 系统调用 陷入到内核中,才能访问这些特权资源。
    需要注意的是,系统调用陷入内核态执行完毕后,还需要切换回用户态,此时其实又发生了一次上下文切换,所以一次系统调用伴随了2次的CPU上下文切换。

系统调用过程的CPU上下文切换:

  1. CPU 寄存器里原来用户态的指令位置,需要先保存起来。
  2. 接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。
  3. 最后才是跳转到内核态运行内核任务。
  4. 系统调用结束后,CPU寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。

    需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。

进程上下文切换:
进程上下文切换
进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。

  • 进程上下文切换,是指从一个进程切换到另一个进程运行。而系统调用过程中一直是同一个进程在运行。
  • 进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。

进程上下文潜在的性能问题:

  1. 频繁的上下文切换容易导致平均负载升高:每次上下文切换都需要几十纳秒到数微秒的 CPU 时间。这个时间还是相当可观的,特别是在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间。这也正是上一节中我们所讲的,导致平均负载升高的一个重要因素。
  2. Linux 通过 TLB(Translation Lookaside Buffer)来管理虚拟内存到物理内存的映射关系。当虚拟内存更新后,TLB 也需要刷新,内存的访问也会随之变慢。特别是在多处理器系统上,缓存是被多个处理器共享的,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程。

切换进程上下文的时机:
只有在进程调度的时候,才需要切换上下文。Linux 为每个 CPU 都维护了一个就绪队列,将活跃进程(即正在运行和正在等待 CPU 的进程)按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是优先级最高和等待 CPU 时间最长的进程来运行。

  1. 进程执行完终止了,它之前使用的 CPU 会释放出来,这个时候再从就绪队列里,拿一个新的进程过来运行。
  2. 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行。
  3. 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。
  4. 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度。
  5. 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。
  6. 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。

线程上下文切换

线程与进程最大的区别在于,线程是调度的基本单位,而进程则是资源拥有的基本单位。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。

当进程只有一个线程时,可以认为进程就等于线程。
当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

因此,根据切换的多个线程所属的进程不同,有2种情况:

  1. 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
  2. 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。

    同进程内的线程切换消耗的资源更少。

中断上下文切换

为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。
跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。

CPU瓶颈定位

1、平均负载
系统平均活跃进程数,反映了系统的整体负载情况
2、CPU使用率
根据CPU上运行任务的不同,CPU使用率可以分为:

  • 用户 CPU 使用率,包括用户态 CPU 使用率(user)和低优先级用户态 CPU 使用率(nice),表示 CPU 在用户态运行的时间百分比。用户 CPU 使用率高,通常说明有应用程序比较繁忙。
  • 系统 CPU 使用率,表示 CPU 在内核态运行的时间百分比(不包括中断)。系统 CPU 使用率高,说明内核比较繁忙。
  • 等待 I/O 的CPU使用率,通常也称为iowait,表示等待 I/O 的时间百分比。iowait 高,通常说明系统与硬件设备的 I/O 交互时间比较长。
  • 软中断和硬中断的 CPU 使用率,分别表示内核调用软中断处理程序、硬中断处理程序的时间百分比。它们的使用率高,通常说明系统发生了大量的中断。
  • 除了上面这些,还有在虚拟化环境中会用到的窃取 CPU 使用率(steal)和客户 CPU 使用率(guest),分别表示被其他虚拟机占用的 CPU 时间百分比,和运行客户虚拟机的 CPU 时间百分比。

3、进程上下文切换
过多的上下文切换会将原本运行进程的CPU时间,消耗在寄存器、内核栈以及虚拟内存等数据的保存和回复上,缩短进程真正运行的时间,成为性能瓶颈。包括:

  • 无法获取资源而导致的自愿上下文切换;
  • 被系统强制调度导致的非自愿上下文切换。

4、CPU缓存的命中率
包括L1、L2、L3等三级缓存。

性能剖析

uptime - 查看平均负载
平均负载最理想情况下等于CPU个数,超过时表示发生了过载,达到70%时就应该分析排查负载高的问题,。

watch - 可用于观察负载变化情况

top 或 读取/proc/cpuinfo - 查看系统有几个CPU

top - 查看CPU使用率

mpstat - 处理器统计

pidstat - 进程统计

-w 查看进程上下文切换情况

  • cswch 每秒自愿上下文切换(voluntary context switches)的次数
    是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。
  • nvcswch 每秒非自愿上下文切换(non voluntary context switches)的次数。
    是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。

stress - 系统压测

sysstat - Linux常用性能工具

sysstat包含mpstat、pidstat
mpstat 是一个常用的多核 CPU 性能分析工具,用来实时查看每个 CPU 的性能指标,以及所有 CPU 的平均指标。
pidstat 是一个常用的进程性能分析工具,用来实时查看进程的 CPU、内存、I/O 以及上下文切换等性能指标。

vmstat - 查看系统上下文切换情况

主要用来分析系统的内存使用情况,也常用来分析CPU上下文切换和中断的次数

  • cs(context switch)是每秒上下文切换的次数。
  • in(interrupt)则是每秒中断的次数。
  • r(Running or Runnable)是就绪队列的长度,也就是正在运行和等待CPU的进程数。
  • b(Blocked)则是处于不可中断睡眠状态的进程数。

vmstat主要用于看系统整体的上下文切换情况,如果想看每个进程的详细情况,可以用pidstat

top - 默认以3秒时间间隔统计CPU使用率

ps - 统计进程整个生命周期的CPU使用率

pidstat - 分析每个进程CPU使用情况

perf 分析性能问题 比如CPU使用率过高

perf top 实时显示占用CPU时钟最多的函数或指令,因此可以用来查找热点函数
perf record、perf report 保存性能分析结果及展示结果

Future - 好莱坞原则的应用

Future

FutureTask

并行化

刚开始接触并发编程,容易写出下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
long startTime = System.currentTimeMillis();
ExecutorService threadPool = Executors.newCachedThreadPool();
List<Future> futures = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Future<?> future = threadPool.submit(() -> {
try {
// 假设这里是一个一直阻塞着的调用
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
futures.add(future);
}
futures.forEach(future -> {
try {
future.get(1000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println((System.currentTimeMillis() - startTime));

此时开发很有可能会误以为结果是 1s 左右,毕竟 3 个FutureTask同时执行、每个只等待 1s,结果确实应该是 1s,但是这里并没有实现并行化,因为future.get是轮询调用的,第一个执行完毕后,第二个仍然需要等待 1s,因此结果是 3s。
虽然每个 FutureTask 是同时开始执行的,但是future.get并不是同时开始等待的,如果想要达到并行执行的效果,一定是在上一个执行完毕的时候,下一个就已经执行完毕了,此时就可以直接获取结果了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
long startTime = System.currentTimeMillis();
ExecutorService threadPool = Executors.newCachedThreadPool();
ExecutorService innerThreadPool = Executors.newCachedThreadPool();
List<Future> futures = new ArrayList<>();
for (int i = 0; i < 3; i++) {
Future<?> future = threadPool.submit(() -> {
// 假设这里是一个一直阻塞着的调用
Future<?> innerFuture = innerThreadPool.submit(() -> {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
try {
innerFuture.get(800, TimeUnit.MILLISECONDS);
} catch (Exception e) {
e.printStackTrace();
}
});
futures.add(future);
}
futures.forEach(future -> {
try {
future.get(1000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
e.printStackTrace();
}
});
System.out.println((System.currentTimeMillis() - startTime));

阻塞还是轮询

阻塞虽然看起来很廉价,似乎线程阻塞后就不占用资源了,实际上很多阻塞都是通过轮询标志位实现的,比如 FutureTask 内部实现了这样的一种状态机:
FutureTask状态机
每次状态的变更都是通过CASUNSAFE)实现线程安全的,比如set方法:

1
2
3
4
5
6
7
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}

Guava - ListenableFuture

ListenableFuture 是在 JDK1.8 之前出现的,现在 CompletableFuture 一般是更好的选择。

CompletionService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
long start = System.currentTimeMillis();
CompletionService<String> cs = new ExecutorCompletionService<>(Executors.newFixedThreadPool(3));
cs.submit(() -> {
Thread.sleep(1000);
return "x";
});
cs.submit(() -> {
Thread.sleep(1000);
return "x";
});
for (int i = 0; i < 2; i++) {
String s = cs.take().get();
System.out.println(s);
}
System.out.println(System.currentTimeMillis() - start);
  • 将线程任务提交到线程池执行,并将 Future 提交到阻塞队列供客户端获取,阻塞队列默认是LinkedBlockingQueue
  • CompletionService 内部只有在任务完成的时候才会把 Future 丢到队列中,因此如果任务特别耗时,会导致take调用一直不返回,注意下面源码中的done方法:
    1
    2
    3
    4
    5
    6
    7
    8
    private class QueueingFuture extends FutureTask<Void> {
    QueueingFuture(RunnableFuture<V> task) {
    super(task, null);
    this.task = task;
    }
    protected void done() { completionQueue.add(task); }
    private final Future<V> task;
    }

CompletableFuture

CompletableFuture是对Future的加强,
CompletableFuture实现了 Future 接口,这意味着它本身也提供了通过阻塞或轮询获取结果的方式,相对 Future 来说,它的相对优势在于任务的编排,相对 CompletionService 来说,CompletableFuture又有接口更灵活的优势。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
long start = System.currentTimeMillis();
List<String> items = Lists.newArrayList("a", "b");
List<CompletableFuture<String>> futures = items.stream().map(
item -> CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return item + "x";
}))
.collect(Collectors.toList());
List<String> strs = futures.stream().map(CompletableFuture::join)
.collect(Collectors.toList());
strs.forEach(System.out::println);
System.out.println(System.currentTimeMillis() - start);

注意上面的CompletableFuture.supplyAsyncCompletableFuture.join

  1. 提交异步执行任务 CompletableFuture.supplyAsync,有2个方法,其中有个需要用户指定Executor,如果没有指定则使用JDK内置的线程池ForkJoinPool.commonPool()
    1. 提交任务到CompletableFuture和直接提交到ExecutorService的区别是线程任务提交前会用AsyncSupply封装
  2. 线程池执行任务
    1. 任务经过AsyncSupply封装,会调用重写的AsyncSupply.exec()
    2. 执行完毕后,回调CompletableFuture.internalComplete
  3. 任务结果聚合CompletableFuture::join
    1. 如果结果已经计算出来,则直接取result
    2. 否则CompletableFuture.waitingGet
      创建等待队列q = new WaitNode(interruptible, 0L, 0L);
      排队queued = UNSAFE.compareAndSwapObject(this, WAITERS, q.next = waiters, q);
      ForkJoinPool.managedBlock(q);阻塞,会调WaitNode.block()来判断是否还阻塞着
    3. 回头看CompletableFuture.internalComplete里的代码,会调LockSupport.unpark(t);唤醒等待的线程

参考

  1. [译]20 个使用 Java CompletableFuture 的例子
  2. Java CompletableFuture 详解

桐生枝梨子(高显秘书、相貌平平、认真、火灾毁容)
里中二郎(死者、高显亲生骨肉)
克子
一原高显(社长、癌症)
一原苍介(高显弟弟、大学教授)
一原直之(苍介弟弟、精明)
一原纪代美(苍介另一哥哥的太太)
由香(纪代美女儿)
一原曜子(苍介妹妹)
加奈江(曜子女儿)
本间重太郎(高显朋友)
本间菊代(重太郎太太、现假扮)
一原健彦(苍介儿子、喜欢由香)
小林真穗(店长、高显情人)
矢崎警部
古木律师
鲹泽弘美(古木助理)

尹之壹
火灾
车祸逃逸
高显遗嘱
七七法事
八泽温泉(墓地)
枝梨子遗书
由香的死:拿走信封、之后死了、倒着的N(俄语字母)、刀伤、勒脖
红酒
安眠药
审讯
直之的珍珠领带夹
私生子
脚印
纪代美的一对珍珠
头发
茶道
碎冰锥

0%