前言:
对于代码的热更新我想大家都感兴趣,这也确实显得更加的高大上。 那么问题来了,你真的需求代码热更新么? 真的有必要设计这么复杂么? 强制更新代码会扣你工资? more ? 我想大家心里都有自己的答案。 作为一个技术及其时尚的coder,是有追求的,如果的kpi和激情的保证下,可以全力开火去实现热更新,如果你们不仅缺人又缺工时,那还b不如省点功夫不做代码级别的热更新,直接在架构上做AB灰度来更新服务代码。
该文章后续仍在不断更新中, 请移步到原文地址 http://xiaorui.cc/?p=4979
那么,听到这里估计你应该听出来热更新有两种方法了,一种是代码级别的,另一种是架构级别的。
代码级别热更新
在Python里面,我们可以在运行后动态的加载库,像这类动态语言基本都支持这类延迟加载模块的。既然是可以延迟加载那么就有了很多的想象的空间。 比如关于你这边增加了一组功能api, 在Python里你是可以直接饮用reload方法来进行重载的,重载的原理很简单,就是重新读取模块文件,解析成字节码,重新映射到python的模块映射就可以了。
在Golang里无法做到完美的延迟加载,但是1.5之后倒是支持so动态链接库和plugin插件模式,但, 然而这类库包里是需要定义main文件。go-nuts社区里也大多不推荐这么搞。其实我个人也不喜欢这类链接库的用法。
实现步骤:
其实代码热更新也没必要设计成signal lazy reload的方式。我们可以参照nginx master worker的思想来实现热更新,达到不停机最少影响连接的热更新。 这里我使用Python实现的master worker框架,当然Golang和其他语言差不多也是类似实现。 下面是实现的基本步骤,代码我会整理出来扔到github里。
1. 用新build的可执行文件替换老的可执行文件.
2. 给老服务发送kill -HUP信号,正在运行的老进程接收到信号后,以exec的方式启动新的可执行文件
3. 新执行文件会以新的master的角色创建子进程,并开始处理新请求.
4. 老进程不再监听listen fd, 接受新的请求,只会处理已经连接连接,但是还未断开的连接。另外通过setproctitle重置进程名,这样可以看出哪个是old.
5. 新进程在父进程退出后,会被init进程领养,并继续提供服务.
下面是master worker框架的设计图… 注意,一定要区分fork和exec的区别,fork是写实copy on write,子进程默认会复制继承父进程的所有进程空间,exec是新启动进程, 并替换进程空间里的数据段,代码段,堆栈段…
详细说,当我们调用这个fork函数时发生了什么呢?fork函数启动一个新的进程,这个子进程几乎是当前进程的一个拷贝:子进程和父进程使用相同的代码段;子进程复制父进程的堆栈段和数据段。这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了。
对于exec系列函数一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。不过exec类函数中有的还允许继承环境变量之类的信息,这个通过exec系列函数中的一部分函数的参数可以得到。
存在的问题
看起来很完美吧, 但如果客户端的长连接一直有活动,就是不断连接,那么老进程是否需要一直存在 ? 我们可以先分析下这个场景,现在http服务的客户端一般不是浏览器,就是app里的httpclient类库。app好办,app一般内置重试的。浏览器的长连接请求一般是短轮询和长轮询。 短轮询,再返回给客户端数据的时候,顺手把连接关闭,这样浏览器再次请求的时候会重新建立连接的。长轮询也是一样的….
那么对于tcp服务端就简单多了,主要别在传输数据的时候强制中断连接就可以了。另外不要太依赖操作系统的keepalive, 因interval周期太长,会导致一段时间的半连接,所以需要自己实现逻辑心跳,这样可以最快的速度来回收无用的内存。
其实也不要太悲观,浏览器会在一定时间后,干掉相关连接。用户也不可能一直开着你那个应用和页面。iphone会在十分钟后关闭后台的连接,并收敛到apple apn通道。移动端在弱网络环境下,会导致你时不时的产生新连接。咱们自家的移动端app也会帮你做好try catch,不至于断线就提示服务端有异常? 有不少移动端应用,比如聊天服务器依赖自定义的业务ack确认,我们知道socket send是个异步方法,不能确保另一端是否收到。再说,tcp协议里要终止一个连接的正常方式是发送FIN。在发送缓冲区中所有排队数据都已发送之后才发送FIN,正常情况下没有任何数据丢失。 这篇主要介绍代码级别的热更新,下一篇介绍 如何在架构层规避热更新相关的问题。
END.