因为每个进程能够服务于多个请求,所以没有必要针对每个请求都重新连接数据库。数据库连接会进行重用,这样的话能够规避创建连接的成本,从而减少延迟。但是,每个进程本身还是需要创建和管理自己的数据库连接。因为进程之间无法共享数据库连接,所以进程间公用的数据库会打开更多的连接。打开过多的连接将会降低数据库的性能。这是因为数据库连接是有状态的,数据库应用必须要在自己的进程中为每个连接分配资源。
多线程单进程的线程模型
我们有一种保护数据库的更好方式,这就是在多线程、长期存活的单进程模型中通过可配置的连接数来使用连接池。尽管数据库连接无法跨多进程共享,但是在同一个进程中,它可以在多个线程间共享。
(点击放大图像)
如下是一个样例:如果有100个单线程的进程,每个进程有10台服务器,那么数据库将会有100 X 10 = 1000个连接。如果我们的每个进程有100个线程,共有10台服务器,每个进程在它的连接池中配置了10个连接,那么数据库只会有10 X 10 = 100个连接,它依然能够实现很高的吞吐量。对于服务和数据库来说,跨线程的连接池都是一种高效的方案。
这种连接池技术既能实现很高的吞吐量又能保护数据库,但是它会带来额外的代码复杂性。因为线程必须共享有状态的数据库连接,所以开发人员需要识别并修正并发相关的缺陷,比如死锁、活锁、线程饿死和竞态条件。解决这些缺陷的方式之一就是进行序列化地访问,但是太多的序列化访问会降低并行性。对于初级的开发人员来说,这些类型的缺陷很难识别和修正。
多线程、长期存活的单进程模型有两种实现风格:一种是为请求分派一个专属的线程,另一种是所有的请求共享一个线程。在前者的线程模型中,每个请求会有一个专门的线程与之关联,这要限制并行处理的请求数量。太多的连接可能会导致效率低下,这是因为在操作系统的CPU调度器中需要执行太多的任务切换。
在后者的线程模型中,我们没有必要为每个请求创建额外的线程,但是I/O相关的任务必须要在单独的线程池中运行,这样的话,能够防止系统因为遇到较慢的操作而hang住,请求处理器需要等待线程池的处理结果。
这种方式没有为每个请求创建专门的线程,对于异步操作我们可以期望有很高的吞吐量和较低的延迟,但是对于同步操作来说,相对于为每个请求创建专属的线程,这种方式不会带来性能方面的改善。
小结
(点击放大图像)
结论
在考虑采用哪种库和语言之前,软件架构师应该反思哪种线程模型能够最适合其工程文化和能力。在代码复杂性和效率之间取得一个很好的平衡将会有助于理清这些困惑,在各种可行的技术栈之间做出选择时,也能有一个正确的方向。因为微服务的范围要比单体应用更小,为了实现更高的效率,可以在代码复杂性方面做出更多的努力。
关于作者
Glenn Engstrand 是Zoosk架构师团队的技术领导。他的主要关注点在于服务端的应用架构,他所关注的架构需要运行在B2C Web可扩展的环境中,并且还要保证可控的运维和部署成本。在波士顿的2012 Lucene Revolution会议上,Glenn是一位知名的演讲者。他的专长在于将单体应用拆分为微服务并使其与实时通信设施进行深度集成。