规模化敏捷思考

敏捷 XP和scrum作为敏捷开发里面最重要的两种思想,相辅相成的发展了20多个年头。要了解这两种方式的本质,要先了解发明人。XP的发明人Kent Beck来自美国的软件工程师,TDD(测试驱动开发)的推崇者。在软件领域最出名的是他的Junit单元测试框架。Jeff Sutherland和Ken Schwaber共同发展了scrum的敏捷理念。Jeff Sutherland毕业于西点,11年的军旅生涯里面当了医生,随后涉及到IT领域. Ken Schwaber软件工程师,产品和工业的咨询。 在这里我们可以了解到Kect Beck更偏向于技术,所以在XP里面除了价值,原则外,更看重各种开发的实践。而scrum则更偏向于人,团队,流程。所以我们采用的敏捷一般是XP和scrum的混合方式。既有XP里面的开发实践,也有scrum的关于人,团队,流程的框架。这两种理念相辅相成,共同发展,形成了今天的敏捷。 小规模的敏捷 在90年代末期,没有现在的云计算,没有大数据,没有docker,k8s。受限于当时的软件技术和规模。XP和scrum在当时的环境下不管从价值观,原则和可以落地的工程实践,都比当时普遍采用的瀑布开发方式好上太多。在2000年后敏捷开发方式开始一步一步成为主流的开发方式。后来持续集成,持续交付,精益开发,用户故事地图,实例化需求的发展进一步加强了敏捷的各个部分。 在当时的背景下,采用组织架构的重新划分,以及架构的持续演进,就可以从component team,变化为feature team. 在feature team里面,团队之间依赖少。所以一个个独立的团队自己采用XP和scrum方式进行持续改进。整个组织的交付效率和质量得益于不断提升的单个团队。 唯一的不变就是变化,这句话时刻提醒着我们。随着时间推移,软件的规模越来越大,SaaS,PaaS,大数据,中台化等技术和组织架构的变化。使得小而美的敏捷团队遇到了前所未有的挑战。 规模化的敏捷 软件规模变得越来越大后,遇到的首要问题是团队间需求的依赖问题。一个完整的具有用户价值的功能现在无法由一个团队完成。有时需求会横跨3,4个团队,甚至7,8个团队。团队间就像以前的component team一样依赖起来。整个开发的过程变得臃肿,反应变慢,反馈周期变长。这样敏捷团队的价值观和原则被现实打破。怎么才能短,快的交付有价值的需求,得到反馈,使敏捷的价值重现。所以后来出现了规模化敏捷的思考,就是基于现状找到可以解决的方法。不管从safe,还是less,或者Scrum@Scale,其实本质就是解决两个核心问题。 第一个问题就是:需求怎么对齐,其实也就是依赖团队间目标怎么对齐,怎么协同开发? 第二个问题就是:随着软件规模扩大,团队,团队人数怎么扩张? 针对第一个问题的本质是,如果无法解决团队间开发依赖问题,那么通过在依赖团队间建立统一的product backlog和统一迭代起止时间的方式进行缓解。统一的product backlog可以解决团队之间需求排序优先级的问题,再加上团队之间迭代的起止时间变得一致性,提升沟通,联调的效率,降低协作的成本。 针对第二个问题,还是考虑到团队人数开始变多,沟通渠道变宽,团队整体的透明性,沟通效率变差,团队变得迟缓。所以通过保持小而美的团队,通过一层层把小团队聚合起来,形成更大规模的团队群。上层团队之间的协作依靠团队代表成员(PO代表,scrum master代表,技术代表,测试代表等)进行沟通协作,来提升沟通效率。 当软件规模更大后,所有的规模化方法都是解决协作的问题,沟通效率问题,解决这类问题本身会产生更多的成本(相较于以前的小规模敏捷团队) 所以当组织达到一定规模化,进行敏捷的方式要进行相应的调整,根据每个组织的独特性一般有以下几点: 优先解决团队开发的依赖 通过统一product backlog 统一团队迭代进行依赖缓解(比如集中多个团队的sprint plan) scrum of scrums, scrum of scrums of scrums(或者其他方法用于同步团队间的进度) 集体团队回顾(持续改进团队依赖问题) 持续交付基础设施的完善 说了那么多,其实规模化敏捷也属于一直在探索的道路上,在这条遍布荆棘的道路上,一定要坚持敏捷的价值观和原则,找寻到适合自己组织和团队的方法。

April 27, 2020

golang-NSQ讲明白

版本 golang – 1.12.4 nsq-1.1.0.linux-amd64.go1.10.3.tar.gz 什么是NSQ 一句话讲NSQ是一个简单队列,类似于java经常使用的activeMQ或者RocketMQ,一般在同步分离成异步,发送消息和接受消息解耦的地方使用到。 NSQ有以下特性: 支持拓扑的高可用性和避免单点故障(SPOFs)。 更强的消息递交保证 为单次处理绑定着内存的足迹(通过把一些持久话的消息放入磁盘) 对生产者和消费者的配置进行极大的简化 提供直接的升级路径 提升效率 NSQ组成 NSQ由三个组件组成: nsqd 用于接收消息,排队消息,投递消息,我们的客户端(生产者,消费者)主要和它打交道 nsqlookupd 管理nsqd,nsqadmin拓扑信息。 我们的客户端(消费者)询问此组件来发现nsqd等 nsqadmin web UI 查询各种NSQ组件的信息,消息信息 NSQ使用步骤 启动nsqlookupd组件 启动nsqd并向nsqlookupd注册 启动nsqadmin并向nsqlookupd注册 生产者推送一个message到其中一个nsqd,并将此消息设置到一个topic里面 消费者向nsqlookupd询问指定topic的消息,nsqlookupd把有此topic的nsqd地址给到消费者 消费者建立channel和topic之间的订阅关系,通过channel向nsqd获取指定topic里面的消息 nsqd向所有订阅该topic的channel推送message, 然后其中一个消费者可以通过其中一个channel获取该topic的message 注意第4点,生产者为什么没有从nsqlookupd注册中心去寻找可以推送消息的nsqd呢?因为nsq的设计理念是将nsqd本地化,也就是说生产者直接推送消息到local-nsqd。这点和RocketMQ的设计理念不一样,RocketMQ的NameServer和nsqlookupd类似,但是设计上RocketMQ生产者会访问NameServer去寻找可用的MQ推送消息。 启动,注册过程: 生产者,消费者: 这就是nsq一个完整的使用流程,下面分别从客户端和代码两个方面介绍详细怎么使用 客户端使用 启动nsqlookup $ nsqlookupd 在另一个shell启动一个nsqd,并在lookupd注册,注意-broadcast-address一定是消费者可以访问的地址 $ nsqd --lookupd-tcp-address=127.0.0.1:4160 -broadcast-address="x.x.x.x" -tcp-address="0.0.0.0:4150" 启动nsqadmin,并在lookupd注册: $ nsqadmin --lookupd-http-address=127.0.0.1:4161 生产者生产一个message,并创建该消息的topic $ curl -d 'hello world 1' 'http://127.0.0.1:4151/pub?topic=test' 消费者通过lookupd查找对应的topic的nsq并绑定topic和channel,通过channel接受该topic的message $ nsq_to_file --topic=test --output-dir=/tmp --lookupd-http-address=127.0.0.1:4161 生产者生产更多消息 $ curl -d 'hello world 2' 'http://127.0.0.1:4151/pub?topic=test' $ curl -d 'hello world 3' 'http://127.0.0.1:4151/pub?topic=test' 可以打开nsaadmin查看所有详情http://127.0.0.1:4171/ ,同时也可以查看/tmp下面接收并写入的message (test.*.log) ...

June 24, 2019

golang实现协程安全的几种方式

版本 golang – 1.12.4 golang协程同步 1.channel - monitor goroutine var deposits = make(chan int) // send amount to deposit var balances = make(chan int) // receive balance func Deposit(amount int) { deposits <- amount } func Balance() int { return <-balances } func teller() { var balance int // balance is confined to teller goroutine for { select { case amount := <-deposits: balance += amount case balances <- balance: } } } func init() { go teller() // start the monitor goroutine } 2.channel - serial confinement ...

May 13, 2019

用golang实现mongodb数据库连接池-高级篇-协程安全

版本 golang – 1.12.4 mongodb – 4.0 go driver – 1.0.0 简介 在上一篇《用golang实现mongodb数据库连接池-基本篇》我们实现了mongodb的golang driver按序使用的基本版,但还需要进一步提升效率和高并发安全。本篇张实现高效率协程安全版。 data race 什么是data race,考虑如下代码: var balance int func Deposit(amount int){ balance = balance + amount} func Balance() int { return balance} //Alice: go func(){ bank.Deposit(200) // A1 fmt.Println("=", bank.Balance()) // A2 }() //Bob go bank.Deposit(100) // B 当alice和bob同时执行如上的操作,最后的存款有几种可能性? 根据直觉会有3种可能: alice first bob first alice/bob/alice 0 0 0 A1 200 B 100 A1 200 A2 “=200” A1 300 B 300 B 300 A2 “=300” A2 “=300” 这个结果最后存款都是剩余300似乎也没什么问题,但是这里还有第4种可能,那就是bob的存款操作发生在A1的balance + amount之后,但是在A1的balance =之前,那么会出现什么? ...

May 11, 2019

用golang实现mongodb数据库连接池-基本篇

版本 golang – 1.12.4 mongodb – 4.0 go driver – 1.0.0 简介 mongodb的数据库driver在官方文档里面明确写明所有的数据库连接需要自己建立和释放,而且建议尽量复用已有的建立,那么也就是说driver里面并未实现连接池的功能。在我们实际应用中就需要自己实现这套数据库连接池提升程序和数据库之间的执行效率。 设计思路 用一个数组来存放数据库连接的指针,并记录每一个指针两个状态: a.是否申请了数据库连接 b.这个连接是否已经给系统在使用中。举个例子就比较好理解了: 申请一个用于存放数据连接的数组,一开始空的什么都没有 程序需要一个数据库连接,连接池把数组第一个位置建立一个数据库连接,并把这个连接的状态置为:a.已申请 b.已给系统 程序使用完释放数据库连接,现在数据库指针状态为:a.已申请 b.未使用 程序需要新申请一个数据库连接,那么就回到了第2的状态。 核心代码 const( MAX_CONNECTION = 10 INITIAL_CONNECTION = 4 AVAILABLE = false USED = true ) /* 代码取了一个巧,用实际存放数据库指针的大小ClientPool.size和mongodata.flag来表示上述a,b两个状态 如果mongodata.flag都为USED,那么需要新申请个数据库连接: size++ clientList: the client pool clientAvailable: the available flag, means the location and available flag in the client pool size: the size of allocated client pool <= MAX_CONNECTION */ type mongodata struct{ client *mongo.Client pos int flag bool } type ClientPool struct{ clientList [MAX_CONNECTION]mongodata size int } //create a new database connection to the pool func (cp *ClientPool) allocateCToPool(pos int) (err error){ cp.clientList[pos].client, err = Dbconnect() if err != nil { utils.Logger.SetPrefix("WARNING ") utils.Logger.Println("allocateCToPool - allocateCToPool failed,position: ", pos, err) return err } cp.clientList[pos].flag = USED cp.clientList[pos].pos = pos return nil } //apply a connection from the pool func (cp *ClientPool) getCToPool(pos int){ cp.clientList[pos].flag = USED } //free a connection back to the pool func (cp *ClientPool) putCBackPool(pos int){ cp.clientList[pos].flag = AVAILABLE } //program apply a database connection func GetClient() (mongoclient *mongodata, err error) { for i:=1; i<cp.size; i++ { if cp.clientList[i].flag == AVAILABLE{ return &cp.clientList[i], nil } } if cp.size < MAX_CONNECTION{ err = cp.allocateCToPool(cp.size) if err != nil { utils.Logger.SetPrefix("WARNING ") utils.Logger.Println("GetClient - DB pooling allocate failed", err) return nil, err } pos := cp.size cp.size++ return &cp.clientList[pos], nil } else { utils.Logger.SetPrefix("WARNING ") utils.Logger.Println("GetClient - DB pooling is fulled") return nil, errors.New("DB pooling is fulled") } } //program release a connection func ReleaseClient(mongoclient *mongodata){ cp.putCBackPool(mongoclient.pos) } 以上就是核心代码实现,但是这个代码有一个问题,就是在高并发下并非协程安全,这个留在下一篇《用golang实现mongodb数据库连接池-高级篇-协程安全》来优化。

May 10, 2019