Perl语言再学习(9): FCGI进程的内存共享
FCGI进程模型
跨请求的进程生命周期
- 最古老的CGI处理请求的方式一般是fork出一个cgi进程,在处理完成返回结果之后并结束进程。
- FCGI的进程生命期较长,在处理完一个请求之后不会结束,而是继续处理下一个请求。
长寿的进程和残存的变量
因为FCGI进程独立且持续。 所以上一次处理请求时用到过的数据结构和一些变量没有被清空,仍然驻留在这个进程的当前使用的内存空间里。 其实也是挺显然的一件事情:既然进程没有结束,其中在全局数据区的变量自然会维持上一次执行过后的状态。
但是注意
- 这里子函数内的局部变量的值,在子函数执行完成之后会被回收,因此是不会被保存的。
- 被保存的只有模块内的全局变量(perl的情况),它们充当的角色有些类似于C++里类的静态变量。
在上边的例子中$result的值会被缓存,而$a的值则不会。 这样会导致每次执行get_result得到的结果都不一样。
内存膨胀问题
FCGI这种进程模型当然有其好处:
- 可以免掉每次fork进程/线程的系统开销。 一般的做法是维护一个进程/线程池而不是在接到请求之后再去创建进程/线程。
- 因为可以保存上一次操作的结果。甚至可以把一些进程直接当做Memcached用。 效率上可能会差一点,然而易用性和自由度上会有很大的提升。
至于坏处:
- 这些进程占用的内存会一点点的增大。 引用的库及相关数据结构占用的内存越多,这个现象就越明显。
- 因为各个进程的内存空间相互独立,20个相同的进程把一份变量缓存了20次造成很大浪费。
定期重启
比较普遍的解决方案一般是定期结束进程并且重启。 这个方案最简单,且没有什么额外的操作负担。 缺点是如果把使用了进程内变量Cache,那么定期重启会导致Cache的Compulsory Miss以及重新加载。 虽然一个进程的生命周期中只有一次Compulsory Miss,但是无谓的损失应该尽量避免。 而且这个方法并不能够解决过度使用进程Cache的问题。
COW与预加载
对于过度使用Cache的问题,既然一份数据被缓存了20份,那么能不能通过共享内存来减少浪费呢? 通过一些库(比如IPC::SharedMem)当然是选择之一,但是这种方法引入了额外的操作成本,且对于应用程序编写者而言不够透明。
什么是Copy-on-write(COW)?
简而言之就是:
- 对于一份数据(内存中的数据或者文件),默认拷贝的时候不创建新的副本。
- 只有在这些“拷贝”被写入的时候才“真正的”创建原数据的副本并写入。
参考资料:
Linux内存管理是基于COW的。所以也就是说:
- 在创建子进程时,如果父进程已经包含了一些数据,并且子进程又不会修改这些数据。
- 那么在创建子进程之后,子进程所“拥有”的这些数据实际上仍然在父进程的内存空间里。
所以对于上边问题的解决方案也就呼之欲出了:
- 首先创立一个父进程,在父进程完成所有想要Cache内容的预加载。
- 由这个父进程负责创建所有的子进程
实际上运行的效果而言,20个fcgi进程可以节省1G以上的内存空间。
- 考虑到服务大小不同,全局静态数据比例不同,实际节省的内存大小会有变动。
- 一般而言可以节省所有子进程1/4~1/2的内存。
缺陷:
- 因为是通过父进程启动FCGI的worker进程,所以实际上对于worker的管理会比原来复杂。
总结
- 基于COW的预加载机制可以为在FCGI进程池模型下运行的服务节省大量内存。
- 实现利用COW机制的预加载会对FCGI进程管理带来额外的代价。
- 关于进程内部内存Cache和进程外部内存Cache(Memcached/Redis)的选用问题以后在别的文章中再作讨论。