我们都知道当今互联网发展特点就是快,我们作为研发所开发的任何产品,包括不限于APP、WEB端、WISE、H5等。本人经历过产品经理提出过要求研发team一个月开发一款新的APP上线,接下来就是避免重复造轮子似的“Ctrl+c&&Ctrl+v”,上线过后的代码运行阶段的稳定性结局可想而知。所以始终牢记一点,写常规代码的过程相对容易,但如何保证线上代码长期稳定的运行才是一个系统能否生存下去的关键,就好比开发一款产品是“0-1”的过程,类比于“婴儿”出生,成长的过程的稳定和恰到好处的高可用率是我们作为研发(“父母”)需要付出很多关心的地方。故而作为一名研发,当前系统在长期运行阶段,暴露许多数据资源不一致问题,这些问题有大有小,严重的影响波分快速扩容带宽需求的业务下发成功率,以及对Controller管控设备产生影响。并且对于整体波分系统的控制通道发生的设备托管问题较为频繁且严重,针对以上特点问题,天元平台项目启动。下文主要从项目概述、数据库、高并发架构、golang高级特性,以下都是我在开发过程中用到的一些经验和技术手段分享,没有最好的技术,只有合适的技术,因此也称不上是最佳实践,仅供参考。
架构设计
天元技术架构依托于go开发,微服务集群化部署,服务开发层面采取高并发方式,增加数据间的环形队列缓存用于存放Controller解析处理化后的diff数据,多handler获取队列数据后通过ral资源访问层服务获取南向网元设备数据进行并发diff;在服务治理方面增加接口层面超时控制,监控以及降级处理;数据库层面使用读写分离方式,mongodb这里存储针对网元Id唯一的Document建设并且只保留最新的数据比对结果,防止长期运行后产生的冗余数据占用空间;mysql层面主备分离方式,设计读写分离的方式,分业务的读写场景增加相对应的技术手段处理,Rcc_Server作为天元对TOOP资源一致性检查服务集成,以下介绍Rcc_Server所使用的主要技术手段,仅供参考:
关于读写性能主要有两点需要考虑,首先是写性能,影响写性能的主要因素是key/value的数据大小,比较简单的场景可以使用JSON
的序列化方式存储,但是在高并发场景下使用JSON不能很好的满足性能要求,而且也比较占存储空间,并且数据存入数据库也要考虑到接口“读多写少”or“读少写多”,不同的接口使用场景对应的需要设计好服务端到DB层的中间缓存问题并且要考虑到数据库的索引设计以及大小设计,主从数据库一致性的两种通用解决方案:
如果某一个key有写操作,在不一致的时间窗口内,中间件会将这个key的读操作也路由到主库上,缺点是数据库中间件的门槛较高了
”双主当主从用“的架构,不存在主从不一致的问题
如果db与缓存间的不一致:
常见的玩法:缓存+数据
常见的缓存架构上,此时读写操作顺序是:a) 淘汰cache;b) 写数据库;读操作的顺序是:a) 读cache,如果cache hit则返回;b)如果cache miss,则读从库;c) 读从库后,将数据放回cache
在一些异常时序情况下,有可能(从库读到旧数据(主从同步还没有完成),旧数据入cache后),数据会长期不一致,解决方案是”缓存双淘汰“,写操作时序升级为:a) 淘汰cache;b)写数据库;c)在经历”主从同步延时窗口时间后“,再次发起一个异步淘汰cache的请求;这样,即使有脏数据在cache,延迟时间窗口后,脏数据还是会被淘汰。带来的代价是,多引入一次读miss、除此之外,最佳实践之一是:将所有cache中的item设置一个超时时间。
在微服务架构下,会按各业务领域拆分不同的服务,服务与服务之前通过RPC请求或MQ消息的方式来交互,在分布式环境下必然会存在调用失败的情况,特别是在高并发的系统中,由于服务器负载更高,发生失败的概率会更大,因此补偿就更为重要。常用的补偿模式有两种:定时任务模式或者消息队列模式。
消息队列
在高并发系统的架构中,消息队列(MQ)是必不可少的,当大流量来临时,我们通过消息队列的异步处理和削峰填谷的特性来增加系统的伸缩性,防止大流量打垮系统,此外,使用消息队列还能使系统间达到充分解耦的目的。
消息队列的核心模型由生产者(Producer)、消费者(Consumer)和消息中间件(Broker)组成。使用消息队列后,可以将原本同步处理的请求,改为通过消费MQ消息异步消费,这样可以减少系统处理的压力,增加系统吞吐量
定时任务
定时任务补偿的模式一般是需要配合数据库的,补偿时会起一个定时任务,定时任务执行的时候会扫描数据库中是否有需要补偿的数据,如果有则执行补偿逻辑,这种方案的好处是由于数据都持久化在数据库中了,相对来说比较稳定,不容易出问题,不足的地方是因为依赖了数据库,在数据量较大的时候,会对数据库造成一定的压力,而且定时任务是周期性执行的,因此一般补偿会有一定的延迟。
这里介绍下Rcc_Server的数据流处理过程,类比与Flink思想,首先获取到数据流的addSink合流做下游多个子task(DataInit)处理入对应的CircleQueue缓存;增加算子服务下游加入多个Handler(用于pop队列元素做相应计算服务),算子可以开启实例绑定Handle节点进行计算服务,增加同步方法讲diff数据存入Db
//并发处理多喝init后继续执行主线程下游的diff处理
handlers := []func() error{t.xxx, t.xxx, t.xx, t.xx, t.xx}
var wg sync.WaitGroup
var once sync.Once
for _, f := range handlers {
wg.Add(1)
go func(handler func() error) {
defer func() {
if e := recover(); e != nil {
buf := make([]byte, 1024)
buf = buf[:runtime.Stack(buf, false)]
once.Do(func() {
err = errs.New(errs.RetServerSystemErr, "panic found in call handlers")
})
}
wg.Done()
}()
if e := handler(); e != nil {
once.Do(func() {
err = e
})
}
}(f)
}
wg.Wait()
sync.WaitGroup是Go团队发布的第一个 goroutines 的管理工具。sync.WaitGroup 用于阻塞等待一组 Go 程的结束,主 Go 程调用 Add() 来设置等待的 Go 程数,然后该组中的每个 Go 程都需要在运行结束时调用 Done(), 递减 WaitGroup 的 Go 程计数器 counter。当 counter 变为 0 时,主 Go 程被唤醒继续执行。业务使用场景:多个func无关联和通信,可以多个方法可以并发的执行,所有方法执行完后才会进行返回。
主要需要注意:
RWMutex(读写锁)
Mutex 是最简单的一种锁类型,同时也比较暴力,当一个 goroutine 获得了 Mutex 后,其他 goroutine 就只能乖乖等到这个 goroutine 释放该 Mutex,不管是读操作还是写操作都会阻塞,但其实我们知道为了提升性能,读操作往往是不需要阻塞的,因此 sync 包提供了 RWMutex 类型,即读/写互斥锁,简称读写锁,这是一个是单写多读模型。
sync.RWMutex
分读锁和写锁,会对读操作和写操作区分对待,在读锁占用的情况下,会阻止写,但不阻止读,也就是多个 goroutine 可同时获取读锁,读锁调用 RLock()
方法开启,通过 RUnlock
方法释放;而写锁会阻止任何其他 goroutine(无论读和写)进来,整个锁相当于由该 goroutine 独占,和 Mutex 一样,写锁通过 Lock
方法启用,通过 Unlock
方法释放,从 RWMutex 的底层实现看实际上是组合了 Mutex,同样,使用 RWMutex 时,任何一个 Lock()
或 RLock()
均需要保证有对应的 Unlock()
或 RUnlock()
调用与之对应,否则可能导致等待该锁的所有 goroutine 处于阻塞状态,甚至可能导致死锁。