探讨tcp服务端代码热更新的问题[上]

前言:

       对于代码的热更新我想大家都感兴趣,这也确实显得更加的高大上。 那么问题来了,你真的需求代码热更新么?  真的有必要设计这么复杂么?  强制更新代码会扣你工资?  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.


对Golang感兴趣的朋友可以加群: 278517979 !!!
另外如果大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码, 感谢!
另外再次标注博客原地址  xiaorui.cc