最近在写一个用于存储protobuf配置的配置管理服务,业务逻辑不难,2天就搞定,但是后续bug不少,也踩了很多坑,在这里记录下。

首先,一直以为golang内的sql模块是单链接的,所以一开始在每个goroutine内都open了一个DB,并写了一个连接池进行管理。后续发现是多此一举,白白写了好多代码。golang的sql模块自带连接池功能,在执行sql语句的时候才会分配连接,执行完毕后归还给连接池,所以假设用golang的sql模块,一个程序一个DB就行了。

既然有连接池的支持,那么也要注意千万不要泄露连接池的连接。假设你采用Query来执行查询语句,那么会返回一个sql.Rows结构,这个结构会占用一个连接,只有在遍历完才会自动关闭,所以最好是获得了Rows后执行一次Rows的Close方法,多次Close是没事的。

然后,因为被上级否定了使用transaction的想法,只能在程序内进行事务控制。一开始整个sql执行model共享一个读写锁,在执行性能测试的情况下,读的tps在3k左右,性能还行,可是写却只是200。这是无法接受的,后来仔细分析了下代码,在写前面锁了写,那么并行的几个routine会等待占有锁的那个routine写入完毕才会有第二个routine进行写操作,就等于白白的排队了,而测试用例是insert新的记录而已,不会有冲突的问题。现在想想一个锁虽然写起来方便,但是性能影响很大,于是今天写了一个新的读写锁管理器,绑定特定的key,每一个key在当前key的锁被占用的情况下,会返回被占用的锁,并且将引用计数值+1,假设没有对应的锁,则返回新的锁。释放锁的时候,判定当前key的锁的引用值,假设已经为0了,说明没有被其它routine进行锁wait,则销毁这个锁,否则引用计数值-1。

这样的话,将不同的sql生成一个key,采用这个key来进行写冲突管理,当两个sql有相同的key的时候,则会进行锁竞争,假设key不相同,则不会有竞争。同时采用引用计数来避免读写锁的泄露,对长期稳定运行服务器有好处。

读操作的话,则当前没有写锁的情况下,则直接进行读取,所以读的性能不会有很大的影响。

在这个新的锁的设计下,tps从200提升到了1200,算是可接受的范围了。这个方案的关键点在于key的生成,在于提取每个sql操作影响的行,只要能得到这个key,则生成读写锁将十分方便。

共 0 条回复
暂时没有人回复哦,赶紧抢沙发
发表新回复

作者

sryan
today is a good day