Blog

任务分解的三明治模型

Content #

我们需要对要完成的任务进行有效的剖析,区分出“容忍中断”的部分和“无法容忍中断”的部分,然后用可保证的相对完整的时间去进行那些“无法容忍中断”的部分。为此我提出了一般任务分解的“三明治模型”。该模型具体内容是什么?

想象它是一个金枪鱼三明治,它的中心部分是金枪鱼肉泥,吃下这个部分的努力我称为“核心思考区间”。事实上大多数任务都有一个至关重要、通常也是最棘手的部分,这个部分需要我们集中精力、非常专注地进行思考,然后将其破解。一旦这个部分被我们“吃下”,那么这个任务就已经完成了大半,余下的就是一些支持性的、补充性的工作了(即“支持性思考区间”和“操作性动作区间”)。

举个简单的例子,现在领导让你做一个PPT,你第一步准备做什么,是先挑一个漂亮的主题模板吗?不是。是马上去百度谷歌查资料吗?也不是。正确的答案是:设计PPT的架构。即你要分析你的受众,他们的知识水平、理解水平以及兴趣点、关注点,在此基础上设计你的内容以及展现内容的顺序,先讲什么,占比多少,再讲什么,占比多少,以及讲的时候采取什么风格、策略,然后PPT的架构就出来了。这个过程就是该任务的“核心思考区间”。在这个过程中,你只需思考,非常专注地思考,你需要的工具,仅仅是一张纸和一支笔(你需要把你的灵感快速地记下来)。等你完成了这个过程,你可以选择继续填充具体的内容(“支持性思考区间”),也可以暂停一下去做别的工作。之后,等你再为这个PPT选择模板、寻找配图或者调整字体的时候(“操作性动作区间”),你并不会太介意被打断,因为你知道,在某种意义上,这个任务你已经完成了。

这就是多线程工作的秘诀。很多时候,你只是需要一个专注不受干扰的、能纯粹跳脱出来思考的、能达至“心流”状态的、能把最关键的“硬核”搞定的——半小时。

Viewpoint #

From #

no-appendfsync-on-rewrite

Redis中下面的配置项的含义是什么? #

no-appendfsync-on-rewrite yes

这个配置项设置为 yes 时,表示在 AOF 重写时,不进行 fsync 操作。也就是说,Redis 实例把写命令写到内存后,不调用后台线程进行 fsync 操作,就可以直接返回了。当然,如果此时实例发生宕机,就会导致数据丢失。反之,如果这个配置项设置为 no(也是默认配置),在 AOF 重写时,Redis 实例仍然会调用后台线程进行 fsync 操作,这就会给实例带来阻塞。

Viewpoint #

From #

休眠的关系

与当前的关系相比,休眠的关系可以提供更加新异的信息。为什么? #

虽然这些人在过去几年里与这些经理们失去了联系,但是他们同时也在接收新的想法和视角。现在的关系所拥有的知识和视角,则更有可能是经理们已经拥有的。有一位经理评价说:“在与他们联络之前,我以为他们不会提供多少我不知道的东西,但事实证明我错了。他们给出的新点子让我感到非常惊讶。”休眠的关系可以像弱关系一样带来新异信息,但与此同时不会让我们感到不适。正如莱文和同事所解释的:“重新激活一段休眠的关系,不同于从头建立新关系。当人们重新建立联系时,他们仍然保持着信任感。”一位经理透露:“我感觉很自在……我不需要猜测他的意图是什么……许多年前我们就建立了相互的信任,这让我们今天的交流非常顺畅。”重新激活休眠关系所需的交流时间实际上更短了,因为已经有了一些共同的基础。对于休眠的关系,经理们不需要像对待弱关系那样,花费心力从头开始。

Viewpoint #

From #

使用vendor

使用vendor #

vendor 机制虽然诞生于 GOPATH 构建模式主导的年代,但在 Go Module 构建模式下,它依旧被保留了下来,并且成为了 Go Module 构建机制的一个很好的补充。特别是在一些不方便访问外部网络,并且对 Go 应用构建性能敏感的环境,比如在一些内部的持续集成或持续交付环境(CI/CD)中,使用 vendor 机制可以实现与 Go Module 等价的构建。

和 GOPATH 构建模式不同,Go Module 构建模式下,我们再也无需手动维护 vendor 目录下的依赖包了,Go 提供了可以快速建立和更新 vendor 的命令,我们还是以前面的 module-mode 项目为例,通过下面命令为该项目建立 vendor:

$go mod vendor
$tree -LF 2 vendor
vendor
├── github.com/
│   ├── google/
│   ├── magefile/
│   └── sirupsen/
├── golang.org/
│   └── x/
└── modules.txt

我们看到,go mod vendor 命令在 vendor 目录下,创建了一份这个项目的依赖包的副本,并且通过 vendor/modules.txt 记录了 vendor 下的 module 以及版本。

...

升级依赖版本到一个不兼容版本

升级依赖版本到一个不兼容版本 #

按照语义导入版本的原则,不同主版本的包的导入路径是不同的。所以,同样地,我们这里也需要先将代码中 redis 包导入路径中的版本号改为 v8:

import (
  _ "github.com/go-redis/redis/v8"
  "github.com/google/uuid"
  "github.com/sirupsen/logrus"
)

接下来,我们再通过 go get 来获取 v8 版本的依赖包:

$go get github.com/go-redis/redis/v8
go: downloading github.com/go-redis/redis/v8 v8.11.1
go: downloading github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
go: downloading github.com/cespare/xxhash/v2 v2.1.1
go get: added github.com/go-redis/redis/v8 v8.11.1

这样,我们就完成了向一个不兼容依赖版本的升级。

Viewpoint #

From #

升降级依赖的版本

升降级依赖的版本 #

Go Module 的版本号采用了语义版本规范,也就是版本号使用 vX.Y.Z 的格式。其中 X 是主版本号,Y 为次版本号 (minor),Z 为补丁版本号 (patch)。主版本号相同的两个版本,较新的版本是兼容旧版本的。如果主版本号不同,那么两个版本是不兼容的。

Go 命令也可以根据版本兼容性,自动选择出合适的依赖版本了。我们以 logrus 为例,logrus 现在就存在着多个发布版本,我们可以通过下面命令来进行查询:

$go list -m -versions github.com/sirupsen/logrus
github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4 v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1 v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3 v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0 v1.4.0 v1.4.1 v1.4.2 v1.5.0 v1.6.0 v1.7.0 v1.7.1 v1.8.0 v1.8.1

假设基于初始状态执行的 go mod tidy 命令,帮我们选择了 logrus 的最新发布版本 v1.8.1。如果你觉得这个版本存在某些问题,想将 logrus 版本降至某个之前发布的兼容版本,比如 v1.7.0,那么我们可以在项目的 module 根目录下,执行带有版本号的 go get 命令:

...

MESI协议

MESI协议 #

我们先来了解 MESI 协议中,处理器对缓存的请求:

  1. PrRd:处理器请求从缓存块中读出;
  2. PrWr:处理器请求向缓存块写入。

而总线对缓存的请求分别是:

  1. BusRd:总线侦听到一个来自另一个处理器的读出缓存请求;
  2. BusRdX:总线侦听到来自另一个尚未取得该缓存块所有权的处理器读独占(或者写)缓存的请求;
  3. BusUpgr:侦听到一个其他处理器要写入本地缓存块上的数据的请求;
  4. Flush:总线侦听到一个缓存块被另一个处理器写回到主存的请求;
  5. FlushOpt:侦听到一个缓存块被放置在总线以提供给另一个处理器的请求,和 Flush 类似,但只不过是从缓存到缓存的传输请求。

缓存块的状态分为 4 种,也是 MESI 协议名字的由来:

  1. Modified(M):缓存块有效,但是是“脏”的,其数据与主存中的原始数据不同,同时还表示处理器对于该缓存块的唯一所有权,表示数据只在这个处理器的缓存上是有效的;
  2. Exclusive(E):缓存块是干净有效且唯一的;
  3. Shared(S):缓存块是有效且干净的,有多个处理器持有相同的缓存副本;
  4. Invalid(I):缓存块无效。

同样,我们用状态机来表示缓存块状态的变化,如下图所示:

在上图中,“/”前表示的是请求,这个请求可能来自 CPU 自己,也可能来自总线,“/”后表示的是当前请求所引起的总线事件,“-”表示不产生总线事件。

这个状态机看起来比较复杂,首先,图中的黑色箭头表示是由当前处理器发起的,红色箭头表示,这个事件是从总线来的,也就是由其他处理器发起的。

我们先看由处理器发起的请求(黑线部分):

  1. M 状态:读写操作都不会改变状态,并且因为能够确定不会有其他副本,因此不会产生任何总线事务;
  2. E 状态:任何对该缓存块的读操作都会缓存命中,且不触发任何总线事务。一个对 E 状态的写操作,也不会产生总线事务,只需将缓存块状态改为 M;
  3. S 状态:当处理器读时,缓存命中,不产生总线事务。当处理器写时,需要产生 BusUpgr 事件,通知其他处理器我要写这个缓存块,并将缓存块状态置为 M;
  4. I 状态:当处理器发出读请求时,遇到缓存块缺失,要把数据加载进缓存,产生一个 BusRd 总线请求。内存控制器响应 BusRd 请求,将所需要的缓存块从内存中取出,同时会检查有没有其他处理器也有该缓存块拷贝,如果发现拷贝则将状态置为 S, 并且把其他有拷贝的处理器的状态也相应地置为 S;如果没有发现其他拷贝,则将状态置为 E。

接下来,我们看下由总线发起的请求(红色部分):

  1. M 状态:该缓存块是整个系统里唯一有效的,并且内存的数据也是过时的。因此当侦听到 BusRd 时,缓存块必须被清空以保证写传播,所以会产生 Flush 事件。并且将状态置为 S。当侦听到 BusRdX 时,也必须产生 Flush 事件,因为有其他处理器要写,所以当前缓存块置为 I;
  2. E 状态:当侦听到 BusRd 请求时,说明另一个处理器遇到了缓存缺失,并试图获取该缓存块,因为最终的结果是要将这个缓存块,放在不止一个处理器缓存上,所以状态必须被置为 S。这样就会产生 FlushOpt 事件,来完成缓存到缓存的传输。当 BusRdX 被侦听到时,说明有其他处理器想要独占这个缓存块上的数据,这种情况下,本地缓存块将会被清空并且状态需要置为 I,同时也会产生 FlushOpt 事件,完成缓存到缓存的传输,将当前数据的最新值同步给需要进行写操作的其他处理器。而当侦听到 BusUpgr 时,说明其他处理器要写当前处理器持有的缓存副本,所以要将状态置为 I,但是不必产生总线事务;
  3. S 状态:当侦听到 BusRd 时,也就是另一个处理器遇到缓存缺失而试图获取该缓存块,因为 S 状态本身是共享的,所以状态保持 S 不变;
  4. I 状态:侦听到的 BusRd、BusRdX、BusUpgr 都不会影响它,所以忽略该情况,状态保持不变。

总体来讲,MESI 协议通过引入了 Modified 和 Exclusive 两种状态,并且引入了处理器缓存之间可以相互同步的机制,非常有效地降低了 CPU 核间带宽。它是当前设计中进行 CPU 核间通讯的主流协议,被广泛地使用在各种 CPU 中。

...

排查Redis所用的swap的大小

swap 的大小是排查 Redis 性能变慢是否由 swap 引起的重要指标,应如何排查Redis所用的swap的大小? #

  1. 先通过下面的命令查看 Redis 的进程号,这里是 5332。

$ redis-cli info | grep process_id process_id: 5332

  1. 查看该 Redis 进程的swap使用情况。

$ cd /proc/5332 $cat smaps | egrep ‘^(Swap|Size)’ Size: 584 kB Swap: 0 kB Size: 4 kB Swap: 4 kB Size: 4 kB Swap: 0 kB Size: 462044 kB Swap: 462008 kB Size: 21392 kB Swap: 0 kB 每一行 Size 表示的是 Redis 实例所用的一块内存大小,而 Size 下方的 Swap 和它相对应,表示这块 Size 大小的内存区域有多少已经被换出到磁盘上了。如果这两个值相等,就表示这块内存区域已经完全被换出到磁盘了。

...

cache与buffer这两个术语的区别

cache与buffer这两个术语的区别 #

cache 这个词,往往意味着它所存储的信息是副本。cache 中的数据即使丢失了,也可以从内存中找到原始数据(不考虑脏数据的情况),cache 存在的意义是加速查找。

但是 buffer 更像是蓄水池,你可以理解成它是一个收作业的课代表,课代表会把所有同学的作业都收集齐以后再一次性地交到老师那里。buffer 中的数据没有副本,一旦丢失就彻底丢失了。

Viewpoint #

From #

最小版本选择原则

Go Module 的最小版本选择原则 #

依赖关系一旦复杂起来,比如像下图中展示的这样,Go 又是如何确定使用依赖包 C 的哪个版本的呢?

在这张图中,myproject 有两个直接依赖 A 和 B,A 和 B 有一个共同的依赖包 C,但 A 依赖 C 的 v1.1.0 版本,而 B 依赖的是 C 的 v1.3.0 版本,并且此时 C 包的最新发布版为 C v1.7.0。这个时候,Go 命令是如何为 myproject 选出间接依赖包 C 的版本呢?选出的究竟是 v1.7.0、v1.1.0 还是 v1.3.0 呢?

其实,当前存在的主流编程语言,以及 Go Module 出现之前的很多 Go 包依赖管理工具都会选择依赖项的“最新最大 (Latest Greatest) 版本”,对应到图中的例子,这个版本就是 v1.7.0。

当然了,理想状态下,如果语义版本控制被正确应用,并且这种“社会契约”也得到了很好的遵守,那么这种选择算法是有道理的,而且也可以正常工作。在这样的情况下,依赖项的“最新最大版本”应该是最稳定和安全的版本,并且应该有向后兼容性。至少在相同的主版本 (Major Verion) 依赖树中是这样的。

但我们这个问题的答案并不是这样的。Go 设计者另辟蹊径,在诸多兼容性版本间,他们不光要考虑最新最大的稳定与安全,还要尊重各个 module 的述求:A 明明说只要求 C v1.1.0,B 明明说只要求 C v1.3.0。所以 Go 会在该项目依赖项的所有版本中,选出符合项目整体要求的“最小版本”。

...