GTD实践再分享

引子

上回 总结反思自己的gtd实践还是19年的3月份,转眼已过去一年半,来到了2020年的下半年。

在之前的反思中,我回顾了自己对于时间这个概念在不同时期的理解及应对方式,然后介绍了自己对任务类型的理解,及在org-mode中的简单应用。

org-mode很强大,但是在现实场景中,需要自己去做一些额外工作,比如手机端和电脑端的同步、手机端的使用体验等,都是比较大的问题。

在使用一段时间后,我也自己写过一个兼容org的gtd工具,但是后面还是因为使用成本过高而终止了很长一段时间。

再一次用起gtd工具是几个月前,契机是有一些事情需要跟踪处理。时间很多和事情很多两种极端情况都是gtd大展身手的好时候。

虽然我也买了嘀嗒清单的vip,但是我相信工具应该是脱离实现的。所以在这里借一篇文章再次对gtd作一番梳理和思考。

解决的问题域

首先,工具的存在是为了解决问题的,我们当然要明确gtd的存在是为了解决什么问题。

解放大脑几乎是所有工具都会尝试解决的问题,这也是把制造工具作为人类与其他动物有别的一个原因。

当然,大脑功能很多,我们要明确的是,gtd要解放的,是大脑的记忆和注意力。

记忆是很好理解的,gtd首先是具备任务记录功能的,包括一些其它的电脑存储设备也可以很好地解决记忆容量的问题,比如磁盘文件等。

解放注意力是gtd有别于存储的一个重要特性,这里说的其实就是gtd的规划和调度的能力,我们需要一种工具让我们日益复杂的任务管理变得简单。

这里,我再来一句个人对gtd解决问题的理解,gtd是一种解放大脑存储负担,并方便我们安排调度任务的工具

工具手段

解放大脑负担是很简单的,大部分计算机存储部件都可以解决。协助我们安排调度任务则是一个需要一系列工具来协助的过程。

首先,我们的第一个强有力的工具就是流程化。当把复杂的任务抽象成一个流程框架后,整体过程就变得有章法可寻,一切新事物也变得似曾相识。

我说一下我个人的实践情况,可能跟标准gtd有些微差别,工具嘛,进行个人定制才是达到最佳效果。

gtd会定义一个收件箱的概念,它是一个暂存区或者缓冲区,有一些临时性的任务会被临时放置其中。为什么需要暂存?因为这样可以最大限度得减少上下文切换的开销。

比较常见的暂存区可以由一些工具来充当,比如记事本或者邮箱的草稿箱等。

暂存区的任务只用作暂存,是需要定期做处理的,具体的时间因人而易。比如我每天晚上都会清理下当天的暂存区。

清理就是为这些暂存任务添加属性的过程,比如这个任务是个人任务还是工作相关的任务,它的紧急程度是怎么样的,是否要加入近期的待办中,等等一类的属性。

其中比较重要的属性是排期,因为我们说gtd很重要的一个作用就是协助我们进行任务的排期调度。

排期可能有周期属性,比如连续某几天都要处理某件事;或者只是在某个时间点花较少的时间经历做个确认;或者压根就没有排期,这一类一般是不重要的任务,大多实践者会选择在空闲时选取不重要但想做的任务来做。

有了收件箱和任务清理,还有一个阶段就是定期对所有任务进行复盘,比如每周检查一下是否有遗漏的任务或者随着时间推移有的任务优化程度下降等。

这实际上是一个闭环的过程,而且整个过程的步骤都比较有章法,可以大大减缓个人的心智负担。

一些实践

现实是复杂的,所以工具需要反复打磨以符合现实中个人的要求。当然,一个子工具的概念和使用上的思考也可以帮助我们做出更好的优化改进。

下面是我对一些子工具的思考,它们是gtd这个系统中的一部分,使用熟练能够让整个系统运转更流畅更高效。

优先级

我个人有尝试过优先级方面的设定,但是一来优先级的变化频率比较高,二来优化级的设定比较耗费精力(任务属性往往比较多,且优化级不好量化),所以后面就省掉了优化级的维护。

但是优先级的存在可以帮助我们对未排期的任务做排序,如果不对优先级做排序,我们就需要替代的手段。

个人经验是通过标签+排期来区分优先级程度,因为这两个工具往往更贴近实际,标签有语义,排期有实际的操作时间。

比如个人成长标签一般是重要不紧急的,而某些重要项目当前比较紧急,那它的关联任务一般也会较为紧急。

如果一些任务比较紧急,我们就要提前为它们确认排期,未确认排期的任务再经过下个循环评估紧急程度。

标签

标签其实也是一个很有意思的设计模式,它可以提供强大的灵活性,让用户可以根据自己的情况灵活定制。

从数据结构上来说,标签是一个多对多的关系,一个标签可以贴到多个任务上,一个任务也可以贴多个标签。这样我们就可以很方便地使用标签对任务做过滤。

我的个人经验是,对标签做一个层级管理,这样方便对整个层级做统计和任务间的快速区分,任务标签只用二级。按照优先级里讲的方法,也是间接地设置了优化级及查看方式。

通过标签颜色,就可以在日历视图中很方便地看到自己一个月内的任务分布情况,如个人任务和工作任务的占比、时间分布等。

排期

上面说到用排期来指标任务的紧急程度,其实排期也可以有更为灵活的处理。

比如我们最近要做某个任务,但是并没有确定具体时间。其实就是一个deadline任务,那我们可以把它安排到明天或者后天,来提醒自己到明天或后天再来评估排期。

对排期分类的思考也是比较有用的,我在上篇gtd文章中也做过一些分类。现在看法更简单些了,要思考的无非两点,要不要排期,排在哪一天,当然也可能是排在哪一段时间。

我之前还有具体时间段的划分,org-mode也有这类工具,比如上午10点-11点看书。但是目前来看,至少对我个人来说,粒度过细是不可取的,因为没有足够的掌控空间。这个还是需要结合自己的时间特点,场景很重要。

我该用什么工具?

还是要选择自己感觉顺眼的工具,心情好,什么都好。

当然,它可能需要有一些比较明显的特点,对我来说,下面几点比较重要:

  1. 界面美观:上面已经说过,使用工具时工具反馈给我们的心情变化比较重要
  2. 功能覆盖:不一定全,但是一定要直接或间接满足自己的需要,且整个过程顺畅
  3. 足够灵活:工具是要贴合人的,而现实是复杂的,这就需要工具有一定的定制能力可以随场景发生一定程度的变化
  4. 全平台支持:这个应该算是目前软件的标配了,手机电脑需要互通是一项基本且重要的能力

如果只有一天,如何学习vuejs?

1. 树靶:从入门到精通?

伴随着计算机技术的发展,我们总是能听到或看到《n天从入门到精通》系列丛书,精通一词总能给人带来一阵兴奋,好像找到了武功秘籍,练后就登峰造极。

但这多半只是换来脑中一热,只是精通招式,还是不能为我所用创造出令自己满意的效果。

这篇文章必然不属于该系列,”一天”这个限制并不想蒙蔽读者,而只是为了创造一个极限条件,时间有限不能全部照顾到,逼迫我们去思考什么才是最重要的。

打蛇打七寸,当真屡试不爽。但是每个人都有自己的学习方式,并不一定对所有人都有效。不以为然者尽可以把这当作另一种思路借鉴。

下面我们就来看看,一天时间,能把哪些重要的东西学到手。

2. 探路:我眼中的vuejs是怎样的?

首先,我要做些铺垫,来说明一下我眼中的vuejs是个什么工具,用以解决什么问题。

计算机语言发展史,在我看来就是一个抽象层次不断升级的历史。

最开始的面向过程编程,比如c语言,实际上是将功能逻辑封闭到了函数,是对过程的抽象。这是第一层。

但是过程虽然做了抽象,数据却还是游离在代码中,面向对象编程即是再将数据跟逻辑一起封装为一个整体,即对象。这是第二层。

这两者都是代码层面,复用程度也还是有限。于是出现了一些第三方模块,复用程度较面向对象又高了一个层次。这是第三层。

再上一层就涉及到具体功能设计了,比如django的contrib.admin,即是django将整个功能以插件方式使用的极好例子,这些代码已可以完整提供功能模块了。这是第四层。

当然,上面还有更高明的设计,IaaS、SaaS、PaaS更是提供了整个运行时的能力,直接申请运行时即可使用,简单方便,程度又高了一层。这是第五层。

vuejs的设计介于第三层和第四层之间,虽说提供了某种层次的抽象,类似第三方模块,但是它更提供了抽象的解决方案。这正是它跟jQuery这类 js库 的差别所在。

更具体得说,vuejs实现了一个叫做component的前端复用粒度,并提供了一整套框架来使component足够灵活以满足绝大多数需求。

我们下面便来看一下vuejs中的component是如何达到抽象复用的。

3. 观详:vuejs中的component设计

这里的标题是component设计,而不是”component如何编写”,如何编写并不十分重要,我们想要知道的是,它是怎么设计来满足我们目标的。

为什么是它满足我们的目标而不是我们去学习它的使用?因为我们的思考是第一位的,否则很容易在这纷繁的世界中迷失,人云易云。

那我们就先设计一个框架,让vuejs实现这个框架(奇怪,竟然是vuejs来贴合我们的思考,而不是我们去学习了)。

那我们现在就来思考,如何设计一个可以复用的component,满足我们复用的需求。

。。。

经过了几分钟的思考,我认为一个可复用的组件,要满足以下几个条件:

  • 属性:我们可以利用属性来定义一个组件的外观,或者交互方式
  • 输出:component不光好看(或不好看),可能还能输出某种状态或数据,比如一个获取输入的组件,我们需要从组件中获取输入的内容
  • 控制:component需要提供一些对外接口,以便我们对它进行控制
  • 感知:有时component需要主动将自己的内部状态通知出来。跟被动控制不同的是,感知是component主动通知外部的行为

现在,我们只需要拿着这份药方去vuejs抓药就可以了。下面是我们找到的药品的位置:

  • 属性:vuejs中明显地提供了属性的定义及访问方式,component中使用 props 来指定其支持哪些属性,使用方可以通过 :prop="config.prop" 传入属性
  • 输出:输出的实现有些技巧,后面我们专列一个小节来说明
  • 控制:想要控制某个component,必先获取它对应的句柄(与之对应的某个变量)。vuejs为组件的使用提供了一个 ref 属性,可通过 this.refs 变量引用到,直接调用其函数
  • 感知:主动通知的实现方式,一般为信号(linux进程)和事件(js),但本质是相同的,都是底层提供一种通信方式。vuejs提供了 $emit() 函数,用以向component使用方发送通知

4. 试剑:不跑起来不算验收通过

上面的实现描述不够直观,还是要看到代码才能更清楚明白。下面我们就创建一个项目来验证上面的设计。

这里我们先编写一个简单的用例,一个用以显示文本的component,同时可通过一个api来修改字体大小。

我们先安装vue命令行工具,并用其创建一个脚手架项目:

script
1
2
npm install -g @vue/cli
vue create component-demo

然后我们创建一个component,该component可定义显示一行文本,可通过一个属性来设置文本颜色。同时,我们提供两个接口以供使用方调整字体大小。

先看使用方的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- src/App.vue -->
<Label color="green" text="hello" ref="label1" />
<button @click="enlarge">enlarge</button>
<button @click="diminish">diminish</button>

<script>
export default {
name: 'App',
components: {
Label
},
methods: {
enlarge() {
this.$refs.label1.enlarge()
},
diminish() {
this.$refs.label1.diminish()
}
}
}
</script>

下面是component的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- src/components/Label.vue -->
<template>
<span :style="{color: color, 'font-size': fontSize + 'px'}">{{ text }}</span>
</template>

<script>
export default {
name: 'Label',
props: ['color', 'text'],
data() {
return {
fontSize: 18
}
},
methods: {
enlarge() {
this.fontSize += 1
},
diminish() {
this.fontSize -= 1
}
}
}
</script>

5. 转锋:同步数据给父component

上面的组件已可以很好的工作了,但是它并没有覆盖到我们上面给出的四个条件,我对比了一下,缺少 输出感知 两个条件,这两个条件实现的技术实际是同一个。

vuejs提供了一种通信方式,可以向父component发送数据,原型为 $emit( eventName, […args] ),eventName为事件名,是一个字符串,可为该事件提供参数 args。

如果我们是直接看vuejs的官方教程,官方指出v-model可以实现数据的双向绑定(数据显示<=>数据变量这两个方向)。官方也给出了v-model背后的诀窍:

你可以用 v-model 指令在表单 input、textarea 及 select 元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 v-model 本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。

text 和 textarea 元素使用 value property 和 input 事件;

下面的代码更直白一些:

1
2
3
4
<input type="text" v-model="data">

<!--等同于下面的代码-->
<input type="text" v-bind="data" v-on:input="data = $event.target.value" />

即是如此,我们便可以通过包装一个input,并给上层上送input事件来通知上层更新数据了。下面是一个片段:

1
2
3
4
5
6
7
<!--父component-->
<InputComponent v-model="data" />

<!--子component-->
<input>
<input v-bind="data" v-on:input="$emit('input', data)">
</template>

当然,框架里还提供了一个model属性,可以修改v-model绑定的属性(这里默认为value)和事件(这里默认为input)。这算是个彩蛋了。

如果要更细致的使用,还需要了解一下生命周期的概念,不过这对于使用来说并不是最核心最要紧的。

6. 回首:再来看n天精通系列

我相信n天精通是存在的,但是这不是必然的。比起精通,能够产生有实际价值的产品,才是更令人兴奋的。

所以,n天能精通也罢,不能精通也罢,并不是那么重要。

网上看到一个朋友的说法,我觉得说得挺好:工具是帮助我们解决问题的,而不是让人成为大牛的。

精通和有效,必然有先后。我觉得大可先让学习有效,真正到了一定程度,自然便精通了。

有了上面的学习,我们就可以触类旁通,继续学习相关知识,一边学习、一边享受学习的成果,不也是一件很快乐的事吗?

zabbix监控方案

概况

zabbix是一个老牌的开源分布式监控方案,它是由Alexei Vladishev在2000年左右创建(PS: 那时我刚好升初中 😀),最早主要用来监控网络和服务器的健康度和完整性。

如果按照中国的法律,到现在的2020年,zabbix已经成年。

经过近20年的发展,zabbix已由最初的网络和服务器监控扩展到应用监控和web监控等更丰富的监控目标,数据库支持已从mysql覆盖到了elasticsearch、oracle等更多的第三方数据库,告警函数也从最初的6个增加到了5.0版本的29个。

架构

zabbix的架构比较典型,分为采集端、展示端、告警端。采集端主要通过agent采集信息,并上报到server,数据存储在mysql等数据库中。

下面是zabbix数据采集端接入zabbix的架构图:

zabbix-proxy

为了性能的考虑,zabbix有类似elk等架构的典型特点,agent和server间支持加入一个proxy来均衡负载。

2019年的4.4版本中,官方开始提供基于golang的agent2作为上报端,性能和维护性上也会好很多。未来有望取代c版本的agent。

agent2源码解读

这里以5.2.0alpha1为准,来分析一下agent2的核心实现。代码对应哈希86c3d82,agent2对应的代码在src/go下。

zabbix同时支持主动上报和被动请求两种,主动上报是agent主动发起的上报,而被动请求是由server端发起。

agent2代码量比较庞大,但是目录结构和处理逻辑比较清晰。核心代码主要集中在internal/agent中。

从agent2的总体设计来看,整个执行逻辑分为scheduler.Manager、server.Connector、ServerListener三个主要逻辑(goroutine启动),主程序对三个goroutine进行监控。

其中,SchedulerManager负责数据采集、配置更新的调度;ServerConnecter用于与server的连接,负责更新监控项配置;ServerListener主要监控server端发起的被动请求。

这里以三个小条目来分别讨论agent2的实现,分别是监控项同步、plugin机制以及一个简单的plugin实现。

监控项同步

server端启动时会监听一个端口(默认10051),agent2会定期从该端口拉取监控项,默认每两分钟拉取一次。

前面说过,拉取是由server.Connector来做的,它从server端获取配置项后,做一些简单处理,并将其包装为updateRequest,然后将配置作为input输入到scheduler.Manager中。

scheduler.Manager的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
type Manager struct {
input chan interface{}
plugins map[string]*pluginAgent
pluginQueue pluginHeap
clients map[uint64]*client
aliases *alias.Manager
// number of active tasks (running in their own goroutines)
activeTasksNum int
// number of seconds left on shutdown timer
shutdownSeconds int
}

input是一个输入源,上面server.Connector即是将updateRequest发送到Manager.input。

Manager会一直监听Manager.input,检测到*updateRequest后,会将updateRequests中包含的配置更新到plugins字段中,后面会根据plugins做收集和上报。

额外补充个,input还有两类输入,performer*queryRequestperformer 对应监控采集事件,而 *queryRequest 对应被动请求的事件。

需要注意的是,虽然agent2默认支持很多监控项,但是它们不会主动采集。只有server端配置了监控项的指标才会被采集上报。

plugin机制

agent2中,plugin机制为plugin预留了多种接口可供实现不同阶段和功能的逻辑,如下:

  • Collector:定期收集指标。一般与Exporter结合,以便将收集的指标上报给server
  • Exporter:上报指标数据到server。Exporter是这几个接口中唯一一个可以并行处理的接口
  • Runner:可在该接口实现初始化和清理操作
  • Watcher:可实现自定义的指标采集调度方式
  • Configurator:处理配置内容,如配置的有效性判断

plugin可以选择实现其中的一组或多组接口,来提供不同功能的处理。

agent2提供了一个指标注册函数,来注册plugin及其支持的指标,接口定义如下:

1
func RegisterMetrics(impl Accessor, name string, params ...string)

其中,impl即为实现了plugin接口的结构体,下面将使用一个例子来展示如何注册。

scheduler.Manager在初始化时,会将支持的plugins信息写入Manager.plugins。上面说的配置项也会更新到这里。plugins字段是一个核心字段。

插件示例

这里给出一个利用插件机制实现的简单插件,该插件提供了两个metric分别生成一个随机整数和一个随机浮点数。

该插件只实现了Exporter接口,并在init()中注册了该插件及两个监控项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Plugin struct {
plugin.Base
}

func (Plugin) Export(key string, params []string, context plugin.ContextProvider) (interface{}, error) {
switch key {
case "test.randint":
return rand.Int(), nil
case "test.randfloat":
return rand.Float32(), nil
default:
return nil, errors.New(fmt.Sprintf("Unknown metric %s!", key))
}
return rand.Int(), nil
}

var impl Plugin

func init() {
plugin.RegisterMetrics(&impl, "test",
"test.randint", "Export random int.",
"test.randfloat", "Export random float.")
}

plugins下有plugins_darwin.go、plugins_linux.go、plugins_windows,里面分别配置了不同操作系统下支持的监控项,把test插件记录进去即可支持该监控了。

zabbix优缺点

作为一个老牌监控方案,zabbix提供的默认监控比较全面,而且技术上有一定积累,运行比较稳定。zabbix的部署相对比较简单。

zabbix的数据库多为mysql等结构型数据库或者elasticsearch等文档型数据库,缺少对时序数据库的支持。

虽然官网也有集成influxdb的方案(链接见参考部分),但都不是官方集成,且需要与其它平台结合使用。最受欢迎的方案3年也只有60个star,可见这种方式并不十分招人待见。

网上也有人提出使用postgresql的tsdb扩展,但是如果没有使用历史,而性能又比较关键的前提下,大家还是会比较关注数据落到何种db。

也有一些方案使用了混搭的方式来寻找出路,比如使用zabbix的采集端,使用grafana来作展示等,方案也是多种多样。这也可见监控相关产品的思路都已比较成熟,各有亮点,解耦也比较彻底。

参考

open-falcon 告警判定

引子

上一篇文章 《open-falcon 监控上报》中讲了open-falcon的设计以及监控上报的逻辑,这篇文章呈接上篇,继续探讨告警判定的逻辑。

监控数据是一个写多读少的数据,几乎每时每刻都在写,但是查看的时间一般很短。用户不可能一直盯着它,告警即是做为另一个入口补充进来的。告警如何配置、如何触发,如何优雅的通知到接收人,是告警要解决的问题。

在设计告警时,可能会存在几个坑,一是告警过多,如阈值设置太低,而没有做告警收敛就会遇到这种情况,既耗费了用户的精力,还有可能把告警通道阻塞;二是告警失效,如果后端发送如短信超载被禁就会出现该种情况。

第一种情况,告警过多一般会设置告警收敛,如一条告警告了几次就不要再告了,或者过一段时间再告,防止接收人被告警信息淹没;同时,一些不重要的告警可以设置告警屏蔽,先屏蔽它一小时,一小时后再来处理。

第二种情况,在确保告警通道稳定的情况下,最好提供多种方案,短信、微信、邮箱、钉钉,一个挂了还有另外的补全。同时,可发送多个相关人员,A没注意到B也可以处理。

下面主要从以下几个方面说一下open-falcon的告警逻辑:

  • 数据链路:agent 采集的数据是如何流转到告警判定模块的
  • 判定规则:judge 如何判定一份监控数据是否异常
  • 告警配置:open-falcon 中是如何管理告警配置的

数据链路

上篇文章中讲到,agent 组件会以多种方式(builtins、plugin、push)采集监控数据。open-falcon组件间多以rpc协议通信,agent通过rpc把采集到的数据上传到transfer,再由transfer分发给judge。

这里judge可以部署多个节点来分担负载,transfer会通过一致性哈希来决定将信息分发到哪个judge节点。

judge即是告警判定组件,它会从hbs中获取portal的监控规则配置,然后判断推送过来的数据是否应该告警,并将告警信息写入redis。

后端的alarm从redis中取告警信息,并通过不同的告警通道转发出去。

判定规则

judge 的判定条件有两种,一种是 strategy,它通过绑定告警模板到某个主机组生效;另一种是 expression,它可以监控非主机的情况。

这两种方式的实现核心是一样的,这里以机器监控为例。

假设我们希望定义一个告警规则,某个主机组中的主机,如果负载连续3次上报均大于4,则告警。我们会在该主机组绑定的告警模板下,添加一条判定规则:load.1min all(#3) > 4。

其中all为聚合函数,all(#3) > 4 意思是最近三次的值均大于4。open-falcon 提供了多种聚合函数,目前可以支持的聚合函数有:max, min, all, sum, avg, diff, pdiff, lookup, stddev。

该规则下发到judge后,被 ParseFuncFromString() 函数解析为一个包含判定逻辑的结构体:

1
2
3
type Function interface {
Compute(L *SafeLinkedList) (vs []*model.HistoryData, leftValue float64, isTriggered bool, isEnough bool)
}

all聚合函数对应的逻辑结构体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type AllFunction struct {
Function
Limit int
Operator string
RightValue float64
}

func (this AllFunction) Compute(L *SafeLinkedList) (vs []*model.HistoryData, leftValue float64, isTriggered bool, isEnough bool) {
vs, isEnough = L.HistoryData(this.Limit)
if !isEnough {
return
}

isTriggered = true
for i := 0; i < this.Limit; i++ {
isTriggered = checkIsTriggered(vs[i].Value, this.Operator, this.RightValue)
if !isTriggered {
break
}
}

leftValue = vs[0].Value
return
}

该函数可以判定历史数据是否触发告警规则,触发则生成一条 event,event 结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 机器监控和实例监控都会产生Event,共用这么一个struct
type Event struct {
Id string `json:"id"`
Strategy *Strategy `json:"strategy"`
Expression *Expression `json:"expression"`
Status string `json:"status"` // OK or PROBLEM
Endpoint string `json:"endpoint"`
LeftValue float64 `json:"leftValue"`
CurrentStep int `json:"currentStep"`
EventTime int64 `json:"eventTime"`
PushedTags map[string]string `json:"pushedTags"`
}

告警管理

前面提到过,open-falcon的告警规则是绑定到主机组的,这也是为了简化告警的配置。数台机器怎么配都不是问题,但是当机器增长到上千上万台,不分组管理是不现实的。

上面还提到一个 expression 的告警配置,这个配置比较适合于业务告警的场景,摆脱了主机组等实体的限制,更为灵活机动。

一些补充

值得一提的是,transfer 的后端支持比较丰富,目前还支持 tsdb 和 influxdb 数据库,可灵活使用后端来完成定制化的功能。

judge 的判定规则是基于阈值的,难以做一些同比环比类的告警,不过可以通过其它方式来自主实现,应该不是很大的问题。

open-falcon 监控上报

监控平台应用背景

监控平台被形象地称为运维之眼,没有它软件产品的上线及后期维护将像摸虾一样。

从监控的对象来划分,监控又可分为基础监控和业务监控。

基础监控偏重基础组件和基础资源的监控,一般指标比较固定,很大程度上可默认采集。

业务监控偏重具体服务的指标监控,较为灵活,一般为具体业务自己定义。

监控平台在整个上线的各个过程中都是有帮助的,它使用的场合有以下几项:

  1. 提供服务上线的参考,如可观察新特性上线后的各项指标是否正常,作为稳定运行的依据
  2. 出现事故后,可作为定位问题的参考,让排查问题更方便
  3. 监控服务及资源的使用情况,为优化或扩容做准备

其中,12基础监控和业务监控兼有,一般方式为基础监控缩小范围,业务监控更准确地查找。业务监控一般会配合日志平台一起提供帮助。

3涉及到具体IaaS层资源,基础监控可以提供更多帮助。

常见架构设计

市面监控平台多以agent上报的方式采集数据,由于监控平台写多读少的缘故,上报链路中间多可插入一个消息队列或代理。

由于采集端已有监控数据,告警平台多与监控平台整合,这样一个采集,一个消费,就会方便很多。

open-falcon

open-falcon是小米的监控平台,open-falcon之前,小米使用的是zabbix。

open-falcon的架构图如下所示:

open-falcon架构图

乍一看挺复杂,捊一捊就比较清晰了。

数据上报链路:绿色粗线即为数据上报链路,数据从App产生或由agent采集,然后发送到transfer,transfer再将数据分发给judge和graph。judge即为告警判定模块,graph存储监控数据。

配置下发链路:橙色虚线为配置下发链接,portal为配置存储中心。为了加速配置的下发,同时减小portal的压力,hbs会将portal缓存一份到自己的内存,在需要的时候会下发给agent(监控配置)和judge(告警配置)。

监控查看链路:蓝色虚线部分为监控查看链路,graph是监控数据的存储组件,出于负载均衡的考虑,graph可能有多个实例,query组件负责整合多个实例的结果,供dashboard读取。dashboard为通往用户的web界面。

open-falcon中的大部分组件都是可以水平扩展的,transfer、judge和graph都是可能会出现瓶颈的组件。官网有一个机器量与瓶颈及应对的对应关系,很有参考价值。

agent 采集上报

agent 是数据采集和上报端,更接近数据,比较重要。

agent 默认已提供机器的大部分基础监控,modules/funcs/ 下可以看到监控的列表,cpu、内存、磁盘、负载、网络io等,涵盖了绝大部分机器相关监控。

agent 每隔一段会将 metrics 上报到 transfer,metrics 是一个列表,下面是一个比较典型的 metric 结构:

1
2
3
4
5
6
7
8
9
type MetricValue struct {
Endpoint string `json:"endpoint"`
Metric string `json:"metric"`
Value interface{} `json:"value"`
Step int64 `json:"step"`
Type string `json:"counterType"`
Tags string `json:"tags"`
Timestamp int64 `json:"timestamp"`
}

这里说一下 Type,主要有 GAUGE 和 COUNTER 两种。GAUGE 为实际的测量值,如内存使用率;COUNTER 为计数值,上报时会计算差值,比较典型的如磁盘读请求次数(linux内核提供的值为总计,我们希望了解的是上一分钟及这一分钟的读请求次数)。

在扩展性方面,agent 还提供了另外两种采集方式,http push 上报的方式和 plugin 采集。这两种方式都是以 MetricValue 为上报的基础数据结构。

push 方式,agent 会在本机提供一个 http 服务,应用可将数据以 http 请求的方式上报给 agent。http 提供了很大的灵活性,应用可整合上报功能来集成业务监控。

plugin 方式,需要提供一个包含采集脚本的仓库。所谓采集脚本,即是指本地可运行的脚本,该脚本会输出 MetricValue 结构的 json 数据,agent 将定期执行并将输出上报给 transfer。

下面是官方提供的一个 python 采集脚本:

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
# https://github.com/open-falcon/plugin/blob/master/demo/60_plugin.py
#! /usr/bin/env python
#-*- coding:utf-8 -*-

import json
import time

data = [
{
'metric': 'plugins.xx',
'endpoint': 'host-001.bj',
'timestamp': int(time.time()),
'step': 60,
'value': 1,
'counterType': 'GAUGE',
'tags': 'idc=aa'
},
{
'metric': 'plugins.yy',
'endpoint': 'host-002.bj',
'timestamp': int(time.time()),
'step': 60,
'value': 0,
'counterType': 'GAUGE',
'tags': 'idc=bb'
}
]

print(json.dumps(data))

NOTE: 需要注意的是,为了避免 plugin 全量上报造成干扰,plugin 上报需要在 dashboard/portal 中指定启用哪些上报脚本。

第三方监控多以 push 方式上报,有了上面两种扩展的上报方式,即可以实现其它基础监控或者业务监控。

总结

open-falcon 算是国内比较成熟的监控产品,虽说多组件的方式部署可能较为庞杂,但 falcon-plus 对 open-falcon 的组件做了整合,部署难度已经下降了好几个数量级,安装已经不再是大问题。

open-falcon 以 golang 和 python 为主要开发语言。提供水平扩展的组件多为 golang 开发,可单独打包,降低了部署的难度;web 端以 python 为主,开发效率较高。各取所长。组件粒度较细,解耦也较彻底,二次开发难度较小。

push 上报方式是许多监控产品采用的方式,较为灵活通用,可满足扩展的需要。其它监控如 mysql 或者具体业务的监控等也可基于此方式实现。

另外,open-falcon 组件内部多使用 rpc 协议沟通,效率也是比较高的。有时间再展开写一写。

除了监控数据,另一块是告警判定逻辑。后面来补充。

macosx上的平铺式窗口管理器

背景

在提供图形界面的操作系统里,都会存在一个问题,就是当我们需要查找大量资源时,窗口会变得较多不方便管理。

比如我现在打开的窗口,就包含了编辑博客的编辑器,一个命令终端,一个查找资料的浏览器,一个临时记录信息的备忘录,还有一些其它的软件。

这时会存在两个问题:

  1. 应用过多,切换应用时会有很多精力浪费在找目标应用的过程中
  2. 窗口布局过乱,重新规划放多个桌面又耗费太多精力,得不尝失

对于窗口的管理,现在已经存在一些方案,主要有以下几大类:

  1. 提供窗口管理工具,这类工具一般以快捷键为入口,方便对目标窗口调整大小,设置位置
  2. 加速操作,这类工具一般是牺牲精确度,将移动和修改窗口大小合并为一个操作,如将窗口拉到屏幕边缘自动贴合并调整位置和大小,或直接选择提前准备好的位置,magnet和divvy就属于此类
  3. 平铺式窗口设计,将所有窗口平铺到屏幕

其中,1和2虽能解决布局问题,但是耗费精力也不小,而且当应用过多时,仍然会有很大一部分精力耗费在切换应用上。目前来看,3似乎是一种比较理想的方式。

平铺式窗口

平铺式窗口管理器的设计存在很长一段时间了,最早应该是linux平台先行做的实践。

原因猜测应该是linux分层外包的协作方式,内核、核心工具、窗口系统、UI系统等都是由不同的开源团队分别完成,这样大家也有更多的精力和空间去做尝试。

我最早使用的linux上的平铺式窗口管理器是awesome,它将所有的窗口都平铺在电脑的屏幕上,不需要自己去调整窗口的位置和大小或窗口间做切换时的视觉适应,而且可以一眼看到其它应用,可以节省很多的精力。

不过,平铺式窗口也有一些问题,比如窗口过多而电脑屏幕过小时就会很尴尬,再比如使用一些gui软件如chrome时也会很别扭,所以它较适合用于多个终端的情况。一些软件如tmux、vim split window的设计也有异曲同工之妙。

mac os x 的情况稍有不同,它的开发模型是大一统,整个系统从内核到上层用户软件的设计都是由苹果公司包揽,所以做窗口管理只能提供有限的能力。但这是足够的,我们极少需要完全从底层自定义整个设计,这是一种偏执。

该设计的主要目标还是解放人的精力,让我们能够投入到更有价值的事情中。所以下面我以此为基础,探索一下mac os x上的相关实现。

Amethyst

这是我在寻找平铺式窗口管理器时找到的一个开源方案,仓库的地址在 https://github.com/ianyh/Amethyst

Amethyst是一个基于xmonad的软件,xmonad运行在linux平台上,类似awesome。

Amethyst默认提供了十多个布局供用户使用,用户可以使用快捷键来管理窗口布局或窗口大小。

缺点是,Amethyst提供的用户界面只有快捷键,所以上手难度较大,用户需要自己去寻找符合自己的操作方式。

比如,我现在的方式是,使用 Widescreen tall 布局,左半屏即是一个主要的工作窗口,其余窗口都竖排在右侧窗口。可以鼠标点击需要切换的窗口,然后快捷键切换到左侧使用。

默认布局和默认快捷键在官方文档都有,这里就不赘述了。

总结

这篇文章对操作讲解地较少,对于高度可配的软件来说,具体的使用是见仁见智的事情。

第一种方案,大多工具只是提供了一个快捷键操作的方式而已,对我的问题没有太大帮助。

我之前也在第二种方案上试用过很长一段时间,总体来说如果打开的应用较为稳定且数量较少,也是一种不错的方案。使用的工具类似magnet,是一个开源方案,具体工具名称倒记不得了。

当然工具是一方面,良好的使用习惯也是很重要的,甚至它完全可以取代大部分效率提升工具。真正的效率提升还是在人,工具只是在为人打补丁而已。

golang并行编程

关于本文

本文主要阐述作者对golang并行编程设计的理解,重在讲解思路而非实际使用。读者可以通过阅读本文,来了解golang中并行设计的关键技术及其作用,知其所以然。

本文并不追求完整和严谨,重点是提供一个学习思路,理清基本概念。使用的细节仍需要参考官方文档。

由于作者没有专门研究过golang的核心实现,所以文中可能存在理解或表述不当之处,欢迎批评指正。

并行需求的背景

在计算机诞生之初,系统还是批处理的方式。彼时,碍于性能和基础工具的不完善,一台计算机同时只能跑一个任务。

后来出现了能在一台计算机上同时跑多个任务的多任务操作系统,*nix系统是典型代表。

但是多任务同时也只能有一个任务在运行,只是硬件性能足够高时,多任务切换快速,让人类看上去像是多个任务在并行处理。

再后来,硬件进一步升级,出现了多核CPU的技术,这时计算机真正具有了并行处理的能力。

伴随多核技术的出现的,还有相关的计算机编程语言。如何能便利地开发利用多核技术的程序,是这类编程语言要解决的核心问题之一。

golang标榜的即是该优势,它在语言层面提供了并行机制,并提供了相关语言工具,来简化多核心编程的过程。

golang中的并行实现:goroutine

golang实现了一种轻量级的线程,即goroutine,来为用户提供并行编程的支持。

goroutine由golang的运行时管理,它使用go关键字创建,使用较为简单。

下面是一个例子:

1
2
3
4
5
6
go func() {
h1 := hash("hello")
fmt.Printf("hello => %s\n", h1)
}()

doSomeThing()

如果CPU有多个核心,golang会将耗时的hash计算自动分配到单独的核心,以充分利用多核心的优势。

但是这里有一个很明显的问题,上例中我们只是把计算结果打印出来,大多时候这不是我们想要的。我们想把计算的结果返回到主进程,供后面使用。

这就涉及到多个逻辑单元间的通信问题,有这么几个类似的总是,函数间参数的传递,goroutine间的数据通信,及进程间的数据通信,其实说的是同一个问题。

当然实现手段是多样的,golang提供了一种channel技术来解决goroutine间通信的问题(关键字CSP)。

goroutine间的通信:channel

我们先来考查一下编程语言函数中的数据通信,来更好的理解数据通信的方式。

  1. 一个比较简单易得的方式是使用全局变量,在语言层面来说,这是一个可行的设计。但是当把它放到并行的场景下时,它的缺点就非常明显了,为了保证数据的一致性,需要引入锁机制或者一些其它的同步机制,这就使问题变得更为复杂。
  2. 另一个可行的方式是把数据作为参数传递给具体的变量,类比于逻辑单元间的消息传递(实际上在object-c中即是把普遍意义上的函数调用定义为信号传递,本质类似,只是不同的看待方式)。这种方式的问题是,需要底层来提供支持,比如linux的管道。

下面我们再来看一下上面两种方式在golang多个goroutine间的实现:

全局变量必然是行得通的。我们使用锁机制来保证数据的一致性,下面是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"sync"
)

var mutex sync.RWMutex
var y int

func main() {
mutex.Lock()
go func() {
y = 4
mutex.Unlock()
}()

mutex.RLock()
fmt.Println(y)
mutex.RUnlock()
}

第二种方式即是golang中提供的channel工具,它的操作方式有点类似linux中的管道,数据从一端流入,另一端流出。它是一个内置的数据结构,基本使用如下:

1
2
3
ch := make(chan int)
ch <- 3
i := <-ch

上面定义了具有1个元素的channel,写入元素时会阻塞当前goroutine,并等待其它goroutine读取该元素(还可以指定channel元素个数来为其添加缓存,这里不展开)。

使用channel工具来重写上面的代码会更为简洁,可读性更好。下面是重构后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
)

func main() {
y := make(chan int)
go func() {
y <- 4
}()

fmt.Println(<-y)
}

代码确实简洁了许多,需要注意的是,go func()中的 y <- 4 语句会阻塞该goroutine,直到最后 <-y 将数据读出后,go func()才会继续执行。

golang中有一个原则性的表述,可以看出其对使用channel来进行数据通信的倾向:

Do not communicate by sharing memory; instead, share memory by communicating.

goroutine协同:select

现在我们已经有了多个goroutine间通信的工具,但是这个工具是阻塞的,有时我们希望能更灵活地处理channel的状态,如指定3秒的超时时间。

goroutine提供了一个select工具来解决这类问题,这个select跟linux中的select有些类似,监控多个目标,如果监控到某个目标达到了我们期望的状态,就做某类操作。

golang中的select跟switch有着一样的结构,而且都使用case关键字来处理多分支,不同的是select处理的是channel的输入输出。

假如我们定义了两个channel,并由两个goroutine来分别写channel数据,select的处理结构类似下面这种:

1
2
3
4
5
6
7
8
select {
case <-ch1:
doSomeThing()
case <-ch2:
doOtherThing()
default:
allIsBlocking()
}

程序会检测ch1和ch2中是否有写入数据,并随机选择一个有写入数据的分支执行,注意这里是随机。如果没有,执行default分支。具体执行过程更复杂些,具体可点击文章底部的链接查看。

可以使用下面的例子来检测超时:

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
package main

import (
"fmt"
"time"
)

func main() {
blockCh := make(chan int)
timeoutCh := make(chan int)

go func() {
time.Sleep(time.Second*3) // 1
blockCh <- 1
}()

go func() {
time.Sleep(time.Second*1) // 2
timeoutCh <- 1
}()

select {
case <-blockCh:
fmt.Println("block routine is finished!")
case <-timeoutCh:
fmt.Println("block routine is timeout!")
}
}

由于1处的goroutine执行太慢(3秒),程序将在等待一秒后,打印”block routine is timeout!”并退出。

case语句同时支持写入和读出,具体可以参考官方文档。

辅助工具

golang还为并行编程提供了更为丰富的工具,使并行编程的过程更为便利,下面列举一些比较常用的工具。这里省去了示例代码。

goroutine会在主进程退出时自动退出,如果我们想让主进程等待goroutine的运行,可以使用sync.WaitGroup()工具。

超时检测的情况比较多,time包借助运行时,实现了一个更为高效的超时检测,time.After(),另外,time.Tick()可以定期写channel配置for实现周期性任务。

参考资料

https://golang.org/ref/spec
https://golang.org/doc/effective_go.html