@zwh8800
2018-02-01T09:28:12.000000Z
字数 2188
阅读 297261
blog
并发
思考
最近在项目里做了一个数据同步的功能。需要把两个数据库里的数据做同步,在业务不繁忙的时候跑一次,核对库里每一条数据,主要是为了保证数据最终一致,做的一个保底工作。之前项目里同步的代码是单线程的,我改成了基于线程池的,但是在上线前的一些问题却引起了我的思考。
程序的代码还是比较简单的,大概类似下面:
// 两个参数分别为线程数量和总任务数
p := pool.NewPool(goroutineCount, len(userIds))
for _, userId := range userIds {
p.Queue(func(job *pool.Job) {
userId := job.Params()[0].(int64)
tx, err := sess.Begin()
if err != nil {
panic(err)
}
defer tx.RollbackUnlessCommitted()
if err := dao.SyncSingleUser(tx, userId); err != nil {
panic(err)
}
if err := tx.Commit(); err != nil {
panic(err)
}
}, userId)
}
errOccurs := false
for result := range p.Results() {
if err, ok := result.(*pool.ErrRecovery); ok {
glog.Errorln(err)
errOccurs = true
}
}
if errOccurs {
return
}
为了确定线程的数量,我逐步增加线程数记录系统各项数值。下面是在一台 RMBP 上做的测试。
T | QPS | CPU | DBCPU |
---|---|---|---|
1 | 9000 | 37 | 81 |
2 | 14000 | 60 | 150 |
3 | 15500 | 70 | 195 |
4 | 16500 | 90 | 260 |
可以发现随着线程数增加,性能会逐步提高,但是当线程数提高到一定程度之后,性能不增反降(这部分数据没放)。如果继续增加线程数进行测试,会发现系统的某些性能指标会被耗尽,程序开始报错。
recovering from panic: dial tcp 127.0.0.1:3306: getsockopt: operation timed out
为什么会出现这种情况呢?在讨论这些问题之前,先回顾一下 goroutine 的设计与实现。
事实上,在 golang 中的 goroutine 已经不是传统意义上的线程了。传统的操作系统提供的线程在使用时有一些限制,超过一定数量之后会对性能造成影响。而 golang 提供的 goroutine 是一种 green thread。
Green thread 和 NodeJS 的异步回调方案有类似的地方,都在底层调用的系统的异步 IO 系统调用。
由此可见,Green thread 在语言设计上比异步回调方案要略胜一筹,不会出现“冲击波”的现象(具体在 NodeJS 中如何避免“冲击波”代码可以看我这篇文章await & async 深度剖析),但在性能上实际是差不多的,都避免了大量使用操作系统级的线程带来的性能问题,同时又能充分的利用 CPU。
但是,今天我想谈的问题并不是 CPU 利用率/线程数量的问题,这个问题已经被上述两种设计方案比较完美的解决了。
在实际中,更多遇到的是 cpu /线程数量之外的资源瓶颈。比如锁、数据库链接、tcp链接。举个例子,假如做个爬虫应用,每个 goroutine 爬一个网页,golang 虽然号称百万级别的 goroutine ,但是你每个 goroutine 里面创建一个 tcp 链接,不到 5 万个 goroutine 就会把系统的 tcp 资源耗尽。
回到文章最上面的情况,在 goroutine 里调用了 dao.SyncSingleUser
函数,这个是一个数据库操作,不可避免的会有 socket 操作、硬盘操作。如果将所有数据库操作都无脑的放到 goroutine 中执行,当资源出现瓶颈之后,大量 goroutine 会阻塞或报错。
因此,我在项目里使用了一个 goroutine 池,来确保不会过多使用系统资源导致崩溃。那么问题来了,池里究竟该放多少线程?
又回到了最初使用系统线程时遇到的问题,不过这次导致问题的不再是线程资源,而是其他资源瓶颈(如 tcp、数据库链接 等)。这些资源比线程资源更加复杂,更加难以把控。更加难做 benchmark,也就更难找出一个通用的方法来解决实际场景的问题。
思考过后,我在网上开始搜索解决方案。发现这篇文章的作者和我做了类似的思考:《并发之痛 Thread,Goroutine,Actor》。作者最后得出 Actor 模型能解决一些问题(不过我才疏学浅至今不怎么理解 Actor 模型),但并发带来的问题还远远没到解决的程度。
革命尚未成功 同志任需努力
所以说,软件工程没有银弹,路漫漫其修远兮,吾将上下而求索。