golang 并发查库

back_data starting ........
catch_data starting ........
catch_service_data starting ........
channel starting ........
control_config starting ........
channel  is right
catch_service_data  is right
control_config  is right
edit_data starting ........
keyword_pic starting ........
back_data  is right
catch_data  is right
keyword_pic  is right
log starting ........
missed_news_data starting ........
open_course_attent starting ........
publish_log_v2 starting ........
push_billiards starting ........
push_chinese_basketball starting ........
push_chinese_football starting ........
push_international_football starting ........
push_list starting ........
push_nba starting ........
push_news starting ........
push_other starting ........
push_table_tennis starting ........
push_tennis starting ........
push_volleyball starting ........
roll_news starting ........
service_catch_data_v2 starting ........
service_config_v2 starting ........
sports_ad starting ........
sports_client_living starting ........
service_config_v2  is right
service_catch_data_v2  is right
sports_ad  is right
push_list  is right
sports_client_living  is right

上面的列表,也是挺壮观的,反应了goroutine之间的异步与结果的反馈。
实际证明,就算是goroutine的数量不受限制,也不意味着系统资源不受限制,系统描述符,mysql,redis的连接数量,却不是不受限制的,需要谨慎的控制最大的连接打开数量,及时关闭释放系统资源。

需求是这样的,要迁库,dba迁了之后,要求我们开发验证一下数据是不是全对,我就用go跑脚本,从数据库里面读取数据,通过md5获取指纹的方式验证有无缺失,之所以使用go,就是考虑到多进程,php单进程很慢,还有想尝试一下新的语言特性。

实际跑的时候,遇到的问题是第一个文件描述符未关闭,导致后来的后来无法继续添加查询,因为系统的可以打开的文件描述符是有上限的,一般来说是1024个,并发的时候,忘记关闭了,后面的就没有位置了。db.Query的rows千万要及时关闭。

还有一个,就是下面的报错

err:dial tcp 10.55.28.33:3727: cannot assign requested address 

从网上搜了下,说是数据库压力过大的时候报错,看了下DBA的监控平台,的确,当时的查询压力很大,就限制了下goroutine的数量上限,一旦达到这个阀值,就让后来的请求先sleep,等待前面的完之后,再开始查询,这个时候看了下监控平台,压力小了很多,考虑到go开启的时候,本身就有了7个goroutine处理自身,我将goroutine的上限设置在了21个。看了下因数据库压力过大的时候的报错,大约是达到35个goroutine的时候。

使用go开启goroutine的时候,尽可能的不要使用闭包的方式,因为闭包内部是可以访问父域内的变量的,也是可以修改父域的变量的,很容易造成不必要的干扰,在尽可能的情况下,将闭包独立差费成函数,很大程度增加了可读性,减少了耦合,降低了复杂度,如果和父域之间需要通信,可以使用信道的方式。

在不限制协程总量的情况下,单个goroutine处理mysql 请求的速度大约是160/s,通过top命令查看,发现 %CPU 一直在130上下波动,%MEM 一直是 11.2 ,此时协程数量在35个左右,可见多协程对 %CPU的使用率是挺高的,如果限制全部goroutine的数量,比如最大达到25个协程,发现单协程请求处理速度达到400/s,全部2747019个查询,花了1030s,协程15个的时候,但协程处理请求速度达到420/s,增加了一点,但是不多,可见不同的机器,不同的处理能力,线程数量和请求速度并非是负相关的。577s 内处理db请求 1800466,平均3120/s,并发的效率还是很高的。

如果在main函数里面开启go,一定要注意main函数退出之后,其他的goroutine也会自动退出,所以必须有一种机制,让主进程等所有的goroutine完成之后,再退出。

在协程数量达到15个的时候,服务器CPU负载其实不高,但是达到25的时候,负载还是比较高的,由此可见,goroutine虽然简单优雅,但是协程间的切换还是比较花时间的。

发现database/sql里,没有设置Query超时的地方,也就是如果一个query发出去,很久很久没有响应,程序会一直处于等待阶段,这个会很影响效率

信道这东西,不仅读取的时候会阻塞,插入的时候,也会阻塞,向一个信道中输入值,如果那边迟迟不读,这边也会一直阻塞。这个会影响调用的先后顺序。

time.NewTimer 可以返回一个定时器对象timer,在设定时间之后, timer中的信道属性C会被赋予一个值,但是如果提前调用了timer.Stop(),time.C会一直处于阻塞状态,例如

 ticker := time.NewTimer(time.Second * 400)
     go func() {
         //防止超时
      <-ticker.C
      timeOut = 1
      defer ticker.Stop()
     }()
     defer func() {
        // 如果已经被关闭,会返回false,如果没有,提前触发
       Println("closing ticker.C : ", ticker.Reset(time.Second*0))
  }()

注意,如果在第二个defer 里面调用 ticker.Stop()的话,则 上面的协程会一直阻塞在 <-ticker.C,因为ticker已经Stop了,永远都不会有数据写入ticker.C,但是信道本身并没有被关闭,还在阻塞等待,而且,因为是 receive-only channel ,所以并没有办法手动close,结果就是该协程就一直卡死在那里,像一个僵尸进程。唯一的办法只能是任由时间到,然后自己触发ticker.C然后 defer ticker.Stop(),如果无法忍受资源浪费,可以像我那样,提前触发,通过Reset的方式,当然,如果已经Close了,Reset会返回false,无伤大雅。

谨慎的对待阻塞,认真思考触发的条件,真的,不然程序就会卡死在某个你不小心触发的条件下

Leave a comment

Your email address will not be published.

*