golang插件化方案

1. 背景

业务线的活动,每一次新活动都做独立项目开发,有大量重复代码,并且浪费数据服务的连接资源;排序服务也许要经常添加业务代码,目前是停服务发布……这些场景为了开发维护效率、稳定性、安全性和性能都使用了Go语言。Go是静态编译语言,在具体的动态场景该如何实现应用级别的持续交付呢?

基于k8s,nginx网关,队列回溯消费等工具的实现也可以实现不同程度的持续交付,但是持续交付的要求越高,搭建平台和维护的成本也越高。

从应用开发本身出发,可以考虑插件化

插件使用场景特点

  1. 可以热更新式扩展应用程序的功能列表
  2. 应对多变的业务需求,方便功能上下线
  3. 对于任意的go应用,能进行增量架构、代码分发以及代码上下线

插件设计标准

  • 性能:调用插件要尽可能的快;对于任务插件,使用单独的工作空间(协程、线程、进程的池子化处理),大的、慢的、长期运行的插件,要少调用
  • 稳定性:插件依赖的发布平台要少发布,交互API的设计要做好抽象,上下文的环境变量非必须不添加,减少升级需求,甚至能支持多个实例互备热升级
  • 可靠性:如果有失效、崩溃的可能,必须有快速、简单、完整的恢复机制;业务插件的执行不能影响依赖的发布平台的守护进程或者线程的稳定
  • 安全性:应该通过代码签名之类的手段防篡改
  • 扩展性:支持插件热更新和上下线,下线需要健康检查,公共库插件至少能热加载
  • 复用性:业务插件不要太多一次性的上下线
  • 易用性:提供使用简单、功能正交的API,业务插件能够获取依赖的发布平台的上下文和调用公共库

2. Go的插件方式

动态链接库plugin,官方文档

语言本身支持,插件和主程序原生语法交互

  • 进程隔离:无,单进程
  • 主程序调用插件:一切预协定object(包括function、channel)
  • 插件感知主程序上下文:主程序预定义类型参数object(包括function、channel)
  • stream支持:单向,基于channel
  • 插件发现:主程序循环扫描插件目录并维护状态;通过第三方文件diff工具维护,例如git
  • 上线:能
  • 下线:不能
  • 更新:不能
  • 通信:进程内
  • 序列化:不需要
  • 性能:高

Go plugin判断两个插件是否相同是通过比较pluginpath实现的,如果没有指定pluginpath,则由内部的算法生成, 生成的格式为plugin/unnamed-“ + root.Package.Internal.BuildID 。这种情况下,如果两个插件的文件名不同,引用包不同,或者引用的cgo不同,则会生成不同的插件,同时加载不会有问题。但是如果两个插件的文件名相同,相关的引用包也相同,则可能生成相同的插件,即使插件内包含的方法和变量不同,实现也不同。判断插件相同,热加载不会成功,也就意味着老插件不支持覆盖更新。

最好在编译的指定pluginpath,同时方便版本跟踪。目前生产环境建议一些公共库无服务依赖的函数,例如算法库之类的。

go build -ldflags "-pluginpath=plugin/hot-$(date +%s)" -buildmode=plugin -o so/Eng.so eng/greeter.go

通信+序列化

natefinch/pie,github仓库

  • 进程隔离:有,多进程,provider+comsumer
  • 主程序调用插件:provider模式调用插件进程中预协定method;consumer模式消费插件进程中的预协定参数object(包括function、除了channel)
  • 插件感知主程序上下文:provider模式消费主程序的预定义参数object(包括function、除了channel);consumer模式调用主程序中预定义method
  • stream支持:不支持
  • 插件发现:主程序循环扫描插件目录并维护状态;通过第三方文件diff工具维护,例如git
  • 上线:能
  • 下线:能
  • 更新:能
  • 通信:支持stdin/stdout、pipe、unix socket、tcp、http、jsonrpc
  • 序列化:gob,protobuf, json, xml
  • 性能:中/偏高

基于Go的net/rpc库,无法支持主程序和插件之间的streaming数据交互,有golang的官方包[issue1]和[issue2]直接建议。另外,每一个插件都要开一个进程,因此要注意通信序列化的性能消耗和进程管理,默认使用stdin/stdout建立连接,如下图,一个plugin和主程序之间有两条单向连接。

可以上成产环境,要做好资源管理。

plugin

hashicorp/go-plugin,github仓库

  • 进程隔离:有,多进程,server+client
  • 主程序调用插件:一切协议预协定object
  • 插件感知主程序上下文:一切协议预协定object
  • stream支持:单向和双向,基于http/2
  • 插件发现:主程序循环扫描插件目录并维护状态;通过第三方文件diff工具维护,例如git
  • 上线:能
  • 下线:能
  • 更新:能
  • 通信:支持grpc
  • 序列化:protobuf
  • 性能:中/偏高

基于Google的grpc库,按照微服务的流程定义proto文件,能通信就能互相调用。知名团队出品。可以上成产环境,要做好资源管理。

grpc

go-mangos/mangos,github仓库

  • 进程隔离:有,多进程,provider+comsumer
  • 主程序调用插件:一切预协定object
  • 插件感知主程序上下文:一切预协定object
  • stream支持:单向,基于mq
  • 插件发现:主程序循环扫描插件目录并维护状态;通过第三方文件diff工具维护,例如git
  • 上线:能
  • 下线:能
  • 更新:能
  • 通信:支持mq
  • 序列化:未知
  • 性能:中/偏高

基于消息队列协议通信,nanomsg和ZeroMQ一类的规范包含一组预定义的通信拓扑(称为“可扩展性协议”),涵盖许多不同的场景:Pair,PubSub,Bus,Survey,Pipeline和ReqRep。Mangos是该协议的golang实现,能够灵活方便支地持两个插件交流。

可以上成产环境,要走大量的基础建设开发。

mq

嵌入式脚本语言

一般都是进程内内嵌第三方语言的解释器,需要考虑解释器的工作线程资源的重复利用。

embedscript

  • 进程隔离:无,单进程,解释器有goroutine开销
  • 主程序调用插件:一切语言协定object
  • 插件感知主程序上下文:一切语言协定object
  • stream支持:看语言是否支持channel互通
  • 插件发现:主程序循环扫描插件目录并维护状态;通过第三方文件diff工具维护,例如git
  • 上线:能
  • 下线:能
  • 更新:能
  • 通信:无
  • 序列化:无
  • 性能:中

go-like脚本语言,agora七牛qlang

agora和qlang都是go语法的动态脚本语言,都好几年没维护了,建议不要用在生产环境,其中Qlang还有用户提[issue]觉得不稳定。

其他脚本语言,js-ottogo-lua5.1go-lua5.2

otta支持目前受欢迎的js语法,star比较多,协定了大部分go原生支持的类型,不包括channel和goroutine,没有提供解释器的工作空间池子化管理,需要开发者使用goroutine和解释器的interrupt接口自行实现,但是从issue和TODO来看,也不适合生产环境。

gopher-lua支持lua5.1语法,和go交互的object类型比较完备,协定了大部分go原生支持的类型,包括channel和goroutine,有提供解释器的工作空间池子化管理,可以上生产环境。

go-lua支持lua5.2语法,目前不建议上生产环境。

3. 思考

  1. 主程序需要怎样设计才能给业务插件预定义完美的上下文呢?例如线程池、redis连接池、mysql连接池、rocketmq、外部服务依赖等等
  2. 公共库插件和业务插件是否适合不同的插件方式?公共库插件方便为业务插件增加提供上下文吗?

人生半坡 想上就努力上

忽然朋友圈刷满了那些年,18岁的天空,很蓝,云朵,很闲。我知道2017的最后一天,大家在虚拟的空间里集合,是要也是该缅怀点什么了,所以一批人生半坡的中年男在朋友圈里回到了18岁,其中一个就是我,但是我已经不认识18的他们了,甚至不认识18岁的自己。

一恍惚,18岁过去了,大学毕业已经5年了。再回头看看,我只有2013年毕业写过一篇一周年总结2013一周年总结,4年“嗖”的一下就过去了。

2014年,我在一家做酒店在线旅游的公司上班,叫快捷酒店管家,那是我呆的第一家创业公司,十几个小伙伴让我觉得很新鲜,leader也很提携后生,在这里有很多事做,我一直忙着写业务,才发现自己的技术基础很薄弱,就算看了很多文章,做具体东西时,还是有点茫然,加上学习贪多又没有形成舒适的节奏,其实做的蛮辛苦,但是当时团队有很多牛逼的资源,所以我还是想留下努力继续试试。无奈后来因为工作环境和人事调整各种原因,我还是选择了离开。啊,这里还有一段失败的不能称之为恋情的感情,还有,这里的同事都是生活的高手,叫我理财,帮我找女朋友,挺有意思的,反而,我觉得我是个没意思的人,而且对生活常识有点失去了信心。这里一方面因为个人的素质偏差,一方面因为工作环境的变化,导致我应该及早抽身离开,却贪恋一些东西而没有及早离开,是一种小遗憾。我在很多时候,还是会怀念一下那一段不一样的日子。

2015年,我离开了颇为眷恋又不想呆的第一家创业公司,在同学ZYQ和XJQ配合准备下,来到了北京,又是一家小的创业公司,叫光点图灵,人不多,不过我已经没有了当年初进创业公司的新鲜感了,同事也都很好,老板一张娃娃脸,人也很nice,最爽的是酒喝得少了。平时我就忙着做事,而且我跟俩同学是有自己的小的创业计划的,虽然最后没成,那又怎样,我们付出了。在和同学的创业过程中,我发现自己的节奏也不是很好,原因还是整体素质和心态不到位。

2016年,我离开了前一家创业公司,因为这家创业公司搬到武汉去了,不过就算不搬,我也要离开了。我选了一家叫赤子城的做海外直播的公司,去之前,拿过果壳和小米的offer,但是这里给很多期权,薪水也不少,我也在一些统计数据网站调查了,公司的数据不错,都D轮了。来干了一年半载,只能说有我喜欢的,也有我不喜欢的地方,而且我现在的想法是做好工作,拿到想要的待遇,没有其他任何想法。

然而2017年底,因为种种原因,我又离开了,我离开了北京,回到武汉斗鱼,有时候默默念叨着“Don’t cry for me, Beijing, I will be back, maybe in a winter.”算是勉励自己吧。在武汉也挺好的,今晚我见到了小时候一起在河里摸鱼儿的玩伴,他也是个成熟稳重的小伙子了,感觉很温暖。

这几年的工作,我还是在逐步上升,越来越相信自己了,总结在以下几点:

产品素质提升,曾经工作的时候,同事们聊业务我很被动,现在我能主导控制了。之所以有这种变化,是因为第一份工作的leader灌输了很多提高产品嗅觉的方法论,不知不觉用上了;而且俩同学的产品素质都很好,经常聊,慢慢长进了;还有一个重要原因是,生活消费慢慢打开买买买,会让人了解主动分析更多的生活逻辑,这是产品逻辑的根本来源。

专业技能提升,这个我觉得,一方面是很多以前不太注意的基础理论,例如操作系统、数据库、网络原理、算法数据结构、分布式,都在恶补,一遍看不懂,再看一遍,再看不懂,看三遍…..直到有自己的理解为止,理论懂的多了,更容易把握项目整体,而且能控制变化;另一方面经历的项目变多变丰富,做事情效率提高很多,也就有更多时间去思考和学习。

生活能力提升,还是要提的,毕竟自己是一个山里娃,曾经不敢消费,节约的观念和家境的忧虑根深蒂固,导致做事畏首畏尾,自己也过得不舒坦,别人也不乐意交往。压抑一些正常的消费行为,也让人对社会生活缺少信心,总之状态差,精神面貌也不好。现在随着朋友和同学的消费理财引导下,我也开始建立正常的消费观念,让生活舒适一些,这一点,很感激他们,虽然现在还没达到理想状态,但是在不断变好。

虽然生活工作爱情还没有满意,而人生已经半坡,但是我一定会握紧泡满枸杞的保温杯,想上就努力上。

具体的执行包括以下几点:

  • 创业,是我永远不会停下的想法,就算最后创业不成功。技术创业,俩同学是我最好的搭档;如果是农业养殖的话,不知道找谁好;如果是搞文化,也有很适合的同学…..总之,不愿做工作的人们,前进,前进,进

  • 产品素质,通过多观察生活,理解人生,使用各种新技术产品,和不同的人多聊聊,来提升自己的产品理解能力和构建能力

  • 专业技能,一是要通过看技术文章和书籍巩固基础理论知识,多动手写代码实践理解;二是要抓热点,吸纳新的知识,紧跟潮流,把控未来

  • 生活能力,多找朋友聊天,多找姑娘聊天,多找亲人聊天,强力刷出自己的存在感,早点定个姑娘,安顿生活

  • 我的文字爱好,照常继续

  • 该做减法了,不擅长或者不喜欢的东西不碰了,要时常自我反省,自娱自乐

我觉得和同学技术创业三人组很像海贼王里的一些角色,哈哈哈。
three

今天是28岁的最后一天,我只想过好28岁,18岁的一切,记得就记得,忘了就忘了,没有啥好强求的,毕竟脑容量有限,遗忘、辞旧才能迎新,至于记得的那部分就是一部分你。

29岁,我告诉你,我不是一个油腻的中年男,也不是一个成功的中年男,但我是一个努力的男人。

go的context

context

包(context)类型是包装了channel通信来关联goroutine服务之间的控制机制,支持树状的上级控制一个或者多个下级,不支持反向控制和平级控制,同理参数的共享是树状的传递流动方向,也可以用来管理有超时依赖的函数,以下代码实例里面,存在forever for循环的才算服务。

参数共享特性

  • context的参数存储不是map,是struct的key和val两个属性,对外暴露操作接口
  • context的参数由父goroutine初始化,子goroutine调用
  • 父context会嵌入子context,同名参数相当于有多个版本
  • 子context获取参数会按照embed的向上查找机制获得多层父级context的参数,同名参数优先使用子context的参数版本
  • 子context初始化非同名参数,相当于增加新参数
  • 子context初始化同名参数,相当于增加一个参数版本
  • context传递的参数注意选择必要的,简单的,size小的
  • context因为只读所以是gorountine安全的,可以多个子gorountine服务共享同一个context的父控制

控制特性

  • 父gorountine服务通过context控制子gorountine服务,由struct里面的空channel实现,父gorountine服务close空channel,子gorountine服务的select监听并可能触发关闭事件,结束生命周期
  • 父gorountine服务达到某种状态,传递信号,结束子gorountine服务
  • 父gorountine服务初始化启动子gorountine服务时,传入截止时刻或者超时时间
  • 父gorountine结束,则所有的子gorountine服务结束
  • 以上三条规则共同决定一个gorountine服务的生命周期

使用

1
2
3
4
5
6
type Context interface {
Done() <-chan struct{} //控制生命周期的空channel
Err() error //获取错误码
Deadline() (deadline time.Time, ok bool) //获取截止时间
Value(key interface{}) interface{} //传递参数
}

子goroutine服务的关联的context是通过WithCancel, WithDeadline, WithTimeout, WithValue复制父goroutine服务的内容,派生出新的context。CancelFunc可以取消所有的后代,移除父goroutine服务在子goroutine服务的关联,并停止所有关联timer。

1
2
3
4
5
6
7
8
9
10
11
package main
import "context"
func tree() {
ctx1 := context.Background()
ctx2, _ := context.WithCancel(ctx1)
ctx3, _ := context.WithTimeout(ctx2, time.Second * 5)
ctx4, _ := context.WithTimeout(ctx3, time.Second * 3)
ctx5, _ := context.WithTimeout(ctx3, time.Second * 6)
ctx6 := context.WithValue(ctx5, "userID", 12)
}

以上代码的Context链下图:
ctx
3秒超时:ctx4超时退出
ctx
5秒超时:ctx3超时退出,导致子节点ctx5和ctx6都退出
ctx
这里只是形象的表示了多级context之间形成的控制关系,实际业务里面ctx5,ctx6所在的goroutine如果在执行业务是不会立即退出的,只有当goroutine在等待IO事件等待数据的时候才会由select响应结束的case。

Background和TODO
都是返回一样的无控制条件无参数的empty Context,background在主服务里面通常作为匿名参数生成可控的context变量;todo可能会用于程序兼容处理,当空桩,做空调用

WithCancel
传入父Context,返回可控子Context,并且返回结束调用子context的goroutine的控制函数

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
31
//func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
package main
import "context"
func main() {
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for { //for循环是goroutine 函数作为服务的标志
select {
case <-ctx.Done():
return
case dst <- n: //这里堵塞有可能会成为函数的超时依赖
n++
}
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() //一定要手动加取消操作
for n := range gen(ctx) {
println(n)
if n == 5 {
break
}
}
}

WithDeadline
传入父Context和截止期限,返回包含截止期限的可控子Context,并且返回结束调用子context的goroutine的控制函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
package main
import "context"
import "time"
func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
select {
case <-time.After(1 * time.Second):
println("overslept")
case <-ctx.Done():
println(ctx.Err())
}
}

WithTimeout
传入父Context和超时时间,返回包含超时时间的可控子Context,并且返回结束调用子context的goroutine的控制函数

1
//func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue
传入父Context和参数key-val,返回包含参数的子Context,如果父context是background,那么子context不可控,否则可控

  • 在服务里面context.Background(),不适合作为有变量接收的WithValue的参数,可以是ctx := context.WithCancel(context.WithValue(context.Background(), k, “Go”))
  • context的key是包内的全局变量,不可导出,避免冲突
  • context的key的数据类型必须支持==和!=,类似于map的key,包括bool,number,string,pointer,channel,interface和包含上述类型的array,struct。不支持slice,map,function或者包含它们的类型。
  • 存储的数据最好是type-safe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//func WithValue(parent Context, key, val interface{}) Context
package main
import "context"
func main() {
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
println("found value:", v)
return
}
println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
}

go vet tool可以检查CancelFuncs的使用情况。context参数不要包裹在struct等一些复杂的数据结构里面,context参数位置放在函数的第一个变量,context参数不要传递nil。

问题

如何实现平级控制,一个子goroutine出现问题同时取消共享父控制的其他子goroutine,参见golang.org/x/sync/errgroup

To avoid allocating when assigning to an interface{}, context keys often have concrete type struct{}. Alternatively, exported context key variables’ static type should be a pointer or interface. 这是什么意思?

pilosa分布式位图数据库(1)

Pilosa

  • 能够轻松横向扩展,并且保证速度的位图数据库,支持数据版本的粗粒度的时间序列(最小粒度到小时)
  • 适合高频次的流数据处理,使用场景例如,如果你的业务主体数据达到10亿量级,并且该数据的附加属性数量达到百万级别,当你需要实时的筛选符合各种属性组合条件的业务主体数据,Pilosa会是个好帮手
  • Pilosa的数据存储不像现有的关系型数据mysql、oracle的行式存储(一行是一个业务实体,一列是同一种数据实体同一个属性),包括index,column、frame、row、cell,是列式存储
  • 一个index类似一张表,数据查询不能跨index
  • 一个column表示一个业务实体,有业务意义,包括多个frame一行row的组合,从平面形象上看是纵向的
  • 一个frame,表示一个业务实体的属性,有业务意义,是row的集合,从平面形象上看是横向的
  • 一个row表示一个属性的一行数据存储,是cell的集合,从平面形象上看是横向的
  • 一个cell就是0/1,是数据存储的逻辑最小单元

安装

MacOS

1
(workspace) ➜ brew install pilosa

二进制包

1
2
3
(workspace) ➜ curl -L -O https://github.com/pilosa/pilosa/releases/download/v0.4.0/pilosa-v0.4.0-darwin-amd64.tar.gz
(workspace) ➜ tar xfz pilosa-v0.4.0-darwin-amd64.tar.gz
(workspace) ➜ cp -i pilosa-v0.4.0-darwin-amd64/pilosa /usr/local/bin

go源码

1
2
3
(workspace) ➜ go get -d github.com/pilosa/pilosa
(workspace) ➜ cd $GOPATH/src/github.com/pilosa/pilosa
(workspace) ➜ make install

docker

1
2
(workspace) ➜ docker pull pilosa/pilosa:latest
(workspace) ➜ docker run --rm pilosa/pilosa:latest help

使用

业务场景

Star Trace是一个跟踪github开源项目的关注情况的业务,有1000条最近有更新并且名称包含go的项目数据,数据字段包括编程语言,标签,项目的关注者。

启server

1
2
(workspace) ➜ pilosa --help
(workspace) ➜ pilosa server

client操作

  • 建表,column是repository,frame是stargazer和language,构成了业务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(workspace) ➜ curl localhost:10101/status
(workspace) ➜ curl localhost:10101/schema
(workspace) ➜ curl localhost:10101/index/repository \
-X POST \
-d '{"options": {"columnLabel": "repo_id"}}'
(workspace) ➜ curl localhost:10101/index/repository/frame/stargazer \
-X POST \
-d '{"options": {"rowLabel": "stargazer_id",
"timeQuantum": "YMD",
"inverseEnabled": true}}'
(workspace) ➜ curl localhost:10101/index/repository/frame/language \
-X POST \
-d '{"options": {"rowLabel": "language_id",
"inverseEnabled": true}}'
  • 数据导入
1
2
3
4
(workspace) ➜ curl -O https://raw.githubusercontent.com/pilosa/getting-started/master/stargazer.csv
(workspace) ➜ curl -O https://raw.githubusercontent.com/pilosa/getting-started/master/language.csv
(workspace) ➜ pilosa import -i repository -f stargazer stargazer.csv
(workspace) ➜ pilosa import -i repository -f language language.csv
  • 查询操作,查询的结果都是列id

查14号用户关注的repository

1
2
3
(workspace) ➜ curl localhost:10101/index/repository/query \
-X POST \
-d 'Bitmap(frame="stargazer", stargazer_id=14)'

查编程语言是5的repository

1
2
3
(workspace) ➜ curl localhost:10101/index/repository/query \
-X POST \
-d 'TopN(frame="language", n=5)'

查14号用户和19号用户的关注的repository交集

1
2
3
(workspace) ➜ curl localhost:10101/index/repository/query \
-X POST \
-d 'Intersect(Bitmap(frame="stargazer", stargazer_id=14), Bitmap(frame="stargazer", stargazer_id=19))'

查14号用户和19号用户的关注的repository并集

1
2
3
(workspace) ➜ curl localhost:10101/index/repository/query \
-X POST \
-d 'Union(Bitmap(frame="stargazer", stargazer_id=14), Bitmap(frame="stargazer", stargazer_id=19))'

查14号用户和19号用户的共同关注的并且语言是1的repository

1
2
3
(workspace) ➜ curl localhost:10101/index/repository/query \
-X POST \
-d 'Intersect(Bitmap(frame="stargazer", stargazer_id=14), Bitmap(frame="stargazer", stargazer_id=19), Bitmap(frame="language", language_id=1))'

在frame为stargazer里面,加一行关注者为99999数据,列上repository为77777的cell为1

1
2
3
(workspace) ➜ curl localhost:10101/index/repository/query \
-X POST \
-d 'SetBit(frame="stargazer", repo_id=77777, stargazer_id=99999)'

经过基本的使用,我觉得pilosa比传统的关系型数据库更侧重于关系,而通过列式存储的架构,方便了大数据的实时聚合计算,所以pilosa是为了在某些场景替代传统关系型数据库,对于文档数据库mongo和嵌入式数据库没有影响。在一些业务庞大的公司里面应该是可以考虑引入的。如果要继续了解pilosa,请阅读pilosa分布式位图数据库(2)

pilosa分布式位图数据库(2)

几种数据结构

Pilosa的元逻辑存储结构是一个bool矩阵,每一个cell的0/1表示行列是否存在关系。行和列可以表示任何东西,甚至可以表示相同的事物(这一点对基于pilosa思维优化查询和存储非常重要)。

基本的逻辑数据结构决定了业务的架构和封装模式。Pilosa的数据结构比文档数据库和传统关系数据库都多,是否能满足各种业务设计呢?是否满足就适合用呢?

Index

类似于mysql的table,mongo的collection,表示一个业务数据的命名空间,无法对两个以上的index做联合查询。

Column

column用ID表示,是顺序递增的整数,并且要赋予业务意义,是同一个index下面的所有frame共用,在index的作用域里是唯一的。

Row

row用ID表示,是顺序递增的整数,并且要赋予业务意义,在frame的作用域里是唯一的。

Frame

pilosa
frame其实是对应传统关系型数据库中的每一列的属性,是pilosa的row的高维结构,由多row组成,从平面形象上看,将数据表格上多行row的整体看做一个frame,这样划分可以对0/1的二元表示意义进行扩展,满足业务多状态的需求

  • ranked cache of a frame
    pilosa
    有序frame是通过行ID维护列计数的高速缓存,该缓存便于TopN查询,缓存大小默认为50,000

  • LRU cache of a frame
    pilosa
    LRU缓存维护frame最近访问的行

Slice 与分布式有关

切片是列组,是pilosa的column的高维结构,包含多个column,每个切片包含固定数量的列,有SliceWidth。和column的关系,从平面形象上看类似于frame和row的关系,但是frame是有业务意义的,slice是为了分布式设计,通过预设宽度划分column成切片,用一致性哈希算法将切片分布到各cluster

Attribute

在传统关系数据库里面关系表的数据,分为两种,一种是纯粹的关系,只存储ID,通过增删或者update status来表示关系的状态;另外一种关系同时已经是一种新的业务实体了,除了ID关系,还有业务属性。
第二种情况的附加业务属性就需要Attribute来支撑。

Time Quantums

time quantums在frame上设置支持时间序列,会产生额外的数据冗余,这允许范围查询缩小到指定的时间间隔。 例如 - 如果时间量被设置为YMD,则支持范围查询到一天的粒度。

如果帧具有时间量化,则为每个定义的时间段生成视图。 例如,对于时间量级为YMD的帧,以下SetBit()查询将导致下图中描述的数据:

View

视图表示frame的数据物理布局存在的可能情况

  • Standard view of frame

标准视图的行列是输入的行列对应的,也是index中一定存在的一个布局

  • Inverse view of frame
    pilosa
    反向视图的行列是输入的行列颠倒的,即输入的行对应frame的列,输入的列对应frame的行,是index中可选的一个布局,会和标准视图及其他视图并存,在行列的业务意义是相同的时候,比较有意义,例如行列都是user_id

  • Time Quantums of frame
    pilosa
    时间序列数据产生的多个布局,时间单位最小到小时,小时的布局包括年、月、日、时四个布局

几种业务案例

(未完待续……)

go 无数据抽象设计

interface是golang的抽象设计的根基,是方法集合的接口,是一个非常强大的并且规范的指针,可以引用任意实现了该接口的方法集合的struct,不能定义属性,意味着在抽象设计里是不允许有数据的,使语言的编译运行管理更纯粹方便。

一切属性都是setter/getter

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main
type Surface struct{
skin string
}
func (s *Surface) Skin() string{
return "My skin is " + s.skin
}
type Men interface {
GetSurface() Surface
}
type European struct {
surface Surface
}
func (p *European) GetSurface() Surface{
return p.surface
}
type African struct {
surface Surface
}
func (p *African) GetSurface() Surface{
return p.surface
}
func Introduce(men Men){
s := men.GetSurface()
println(s.Skin())
}
func main() {
var s Surface
s = Surface{"white"}
e := European{s}
Introduce(&e)
s = Surface{"black"}
a := African{s}
Introduce(&a)
}

上例中的多态,通过setter/getter变相完成了property在interface定义,用方法多态来实现属性多态。

父类方法A调用子类方法B

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main
type Men interface {
Age() int
}
type Parent struct {
Men //隐式
m Men //显式
}
func (p *Parent) Age() int{
return 80
}
func (p *Parent) Display1() {
println(p.Men.Age())
}
func (p *Parent) Display2() {
println(p.m.Age())
}
type Child struct {
Parent
}
func (c *Child) Age() int{
return 40
}
func main() {
var child *Child
var parent *Parent
parent = &Parent{parent, nil}
child = &Child{Parent: *parent}
println("====parent")
child.Display1()
parent = &Parent{child, nil}
child = &Child{Parent: *parent}
println("====child")
child.Display1()
child.m = parent
println("====parent")
child.Display2()
child.m = child
println("====child")
child.Display2()
}

上例中的实现了在父类的A方法中调用父类或者子类的B方法,类似于抽象方法。因为go是静态语言,没有动态语言强大的查找机制,也没有虚函数(virtural method)和抽象方法(abstract method)之类的辅助高级别的多态。

幸好,golang有指针,即interface,又名接口,通过在父类定义接口类型的属性,并在实例化的时候动态绑定该属性的真正类型,来完成父类方法中一些多态方法的灵活调用,增加了公共部分的复用性。

Airflow的基本搭建

airflow是一个基于celery任务DAG管理工具,包括

  • manage system
  • worker
  • scheduler

支持redis、rabbitmq、mongo、mysql、SQLite、postgresql作为queue,支持同时使用flower作为可视化任务的管理后台。
管理平台的数据落地支持mongo、mysql、SQLite、postgresql.

安装依赖

1
2
3
4
5
6
[lavenderuni@~]# pip install airflow
pip install "airflow[crypto, password]"
pip install "airflow[mysql]"
pip install "airflow[hive]"
pip install "airflow[celery]"
pip install "airflow[rabbitmq]"

初始化管理平台的数据结构到mysql

1
2
3
4
5
6
7
8
create database airflow;
grant all privileges on airflow.* to 'ct'@'localhost' identified by '152108';
flush privileges;
select * from mysql.user;
airflow initdb # 任意目录下执行
cd ~/airflow/
vi airflow.cfg # 替换sql_alchemy_conn = mysql://ct:152108@localhost/airflow
airflow initdb # 再次执行

初始化数据

1
2
3
4
5
6
cd ~/airflow/
vi airflow.cfg # 替换broker_url = redis://127.0.0.1:6379/15
和 celery_result_backend = redis://127.0.0.1:6379/16
airflow webserver --debug &
airflow scheduler

设置执行任务的broker即queue

1
2
3
4
5
6
cd ~/airflow/
vi airflow.cfg # 替换broker_url = redis://127.0.0.1:6379/15
和 celery_result_backend = redis://127.0.0.1:6379/16
airflow webserver --debug &
airflow scheduler

启动三大组件

1
2
3
airflow webserver --debug & #启动管理平台,访问http://127.0.0.1:8080/
airflow worker #启动工作者
airflow scheduler #启动任务调度分发器

管理平台如下图所示
airflow

airflow的dag文件例子参照testlab的airflow/dags

猿联

猿联

coder


| | 连年coding | |
| :——:| :——: | :——: |
| 猴 |                  | 鸡 |
| 子 |                  | 群 |
| 补 |                  | 算 |
| 丁 |                  | 法 |
| 扫 |                  | 翻 |
| 除 |                  | 新 |
| 昔 |                  | 炼 |
| bug |                  | need |

multi process from shell script in parallel in background

Failed test.

1
2
3
4
5
6
7
8
(workspace) ➜ cat test2.sh
#!/bin/bash
for((k=0;k<5;k++));
do sleep 1 &;
done
(workspace) ➜ sh test2.sh
test2.sh: line 3: syntax error near unexpected token `;'
test2.sh: line 3: ` do sleep 1 &;'

Here’s an easy command to fork off a bunch of jobs in parallel.

1
2
3
4
5
6
7
8
9
10
11
(workspace) ➜ cat test1.sh
#!/bin/bash
seq 10 20 | xargs -n 1 -P 5 sleep &
(workspace) ➜ sh test1.sh
(workspace) ➜ ps -eff|grep sleep
501 75381 1 0 9:33下午 ttys001 0:00.00 xargs -n 1 -P 5 sleep
501 75383 75381 0 9:33下午 ttys001 0:00.00 sleep 10
501 75384 75381 0 9:33下午 ttys001 0:00.00 sleep 11
501 75385 75381 0 9:33下午 ttys001 0:00.00 sleep 12
501 75386 75381 0 9:33下午 ttys001 0:00.00 sleep 13
501 75387 75381 0 9:33下午 ttys001 0:00.00 sleep 14