本文会详细介绍主流的daemon进程的实现方案,以及网络编程中如何实现优雅重启,这些都是多进程的一些编程技巧!
如何创建daemon进程
- 为什么我们需要daemon进程?
我们平时做服务器开发都是启动一个程序,这个程序是一个前台程序,但是前台程序它一直在那开着,我想让他后台运行,例如mysql的server,那么怎么解决呢,我自己怎么实现一个daemon进程呢?
- 常见的手段
- systemctl 是linux最常见的手段,它需要软件定义一个 .service 文件,来定义和管理 软件的等 https://www.freedesktop.org/software/systemd/man/systemd.unit.html
- 可以用
systemctl status
查看所有 systemctl 的进程,但是貌似需要root权限用起来不太方便….
1 | ~ cat /lib/systemd/system/docker.service |
其实大概描述了,当前软件详情信息,如何启动当前软件等,具体可以参考这个文章 https://segmentfault.com/a/1190000023029058
nohup 就更简单了,只需要 nohup 命令一下即可,其实它所做的就更简单了,就是一个后台运行,并不会涉及到重启等操作
supervisor 我个人感觉就和 systemctl差不多
如何实现一个daemon进程
首先需要了解一个进程的机制,例如我们在shell里执行了一个命令(非nohup),那么整体流程是,shell进程启动了我们的进程,那么我们当前进程的父亲进程就是 shell 进程,当我们把shell进程关了,那么我们的进程也没了!
下图是我写了一个 Go代码,其实就是解释了上面说的!整个进程的关系!
根据上面描述基本无解了,那么怎么办呢,实际上这里就需要用到孤儿进程,孤儿进程的父进程ID是1,他的回收权就转移给了init进程(进程ID为1),那么如何创建一个孤儿进程了!
其实很简单,就是当父进程退出,子进程还在运行,此时子进程就是孤儿进程了,孤儿进程的父亲进程为init进程(进程ID=1)!
僵尸进程(zomibe)进程产生的原因是因为子进程退出后,父进程没有退出但是也没有及时清理子进程的资源,当父进程退出了僵尸进程会自动回收,僵尸进程造成的问题就是占用系统资源(pid资源)!
1 |
|
具体文章可以看:
- https://blog.csdn.net/a745233700/article/details/120715371
- https://segmentfault.com/a/1190000038820321
简单实现一个daemon进程
这个例子是实现一个 http 服务的daemon进程,直接运行后会后台启动一个http服务!
1 | package main |
我们成功实现了一个 孤儿进程, 如何结束孤儿进程了, 直接 kill 孤儿进程的进程ID即可
1 | ➜ test git:(master) ✗ ps -ef | grep './main' |
注意:Go的StartProcess底层是fork + exec , fork函数是创建一个子进程,exec函数是加载一个程序覆盖当前程序(替换整个程序). 不懂得可以百度下..
代码地址: https://coliru.stacked-crooked.com/a/caed3826f0be8391
1 |
|
封装 daemon 进程
这里我就不造轮子了,大概可以看一下 https://github.com/sevlyar/go-daemon 这个项目,我大概介绍一些几个方法的核心原理
- ctx
1 | func (s *DaemonService) newCtx() *daemon.Context { |
- DaemonStart
1 | func (s *DaemonService) DaemonStart() error { |
实现优雅重启tcp服务
- 现在已经有了 k8s / 自研的发布平台都支持滚动重启了,滚动重启阶段会新建一个新的服务,然后等待旧服务结束。但是吧他比较消耗资源,因为假如你服务1w台,滚动粒度时10%,那么需要冗余1000台服务器的资源!
- 原地重启吧,需要实现优雅重启或者暴力重启了,暴力重启可能会短暂影响sla,所以优雅重启也非常重要!
- 优雅重启的大概原理就是:多进程的文件共享,这里共享的是tcp socket的文件,当需要重启时候会创建一个新进程,然后通知旧进程关闭监听socket文件,两个进程共享socket文件,新进程启动后会重新监听共享的socket,那么新的连接会打向新进程,旧进程依然处理旧的连接,最后处理完后旧进程会退出,最终实现优雅重启,它很好的解决了新连接/旧连接的处理!
造个轮子
这个例子,我是主进程正常创建和监听TCPListener
, 当需要重启的时候此时需要关闭 TCPListener 然后创建子进程继续监听,当再监听到重启时同样的需要主进程关闭子进程!
1 | package main |
开源实现
- https://github.com/jpillora/overseer 父进程不负责监听端口(负责创建端口/管理子进程),子进程负责监听端口,当父进程监听到重启的时候会重启 子进程 (区别于我这个例子) 【比较推荐,sdk也比较成熟】
- https://github.com/facebookarchive/grace 没怎么细看
- https://github.com/fvbock/endless 这个做法更暴力了,相当于当监听到 SIGHUP 信号时,直接启动个孤儿进程(子进程),当前进程因为被close,自己主动退出!不太适用于!
总结
- 上面例子的缺陷就是 子进程/父进程 直接调用 close 方法去关闭连接,然后子进程时直接退出进程了,此时会存在部分已经建立连接的请求失败了,需要优雅关闭,但是实际上优雅关闭也会存在问题,就是wait的时间过长,导致后续新建的连接失败(连接超时),所以可以间接过度,也就先 close 关闭新建连接,然后创建子进程继续监听,然后等待前面这个子进程优雅退出即可!
- 优雅重启都强依赖于sdk,假如你父进程的sdk有BUG还是得强制升级的!
linux 小技巧
日常中我们也不需要上面那些复杂的东西,比如我就是想后台挂起几个进程是不是,还需要我自己造个轮子,太麻烦了!
后台运行
1 | package main |
- 后台运行 (当终端关闭的时候他也会关闭)
1 | ➜ test git:(master) ✗ ./main 10086 & |
- nohub (not hang up) 当终端关闭它也不会关闭
1 | ➜ test git:(master) ✗ nohup ./main 10086 >nohub.log 2>&1 & |
后台运行多个进程
信号:
1 | HUP 1 终端断线(你把终端关了,就会收到这个) |
这里我们运行多个后台进程,当脚本结束的时候杀死后台进程
1 | !/usr/bin/env bash |
总结
方案没有绝对的好与坏,取决于具体场景,掌握了各种方案的底层实现,会方便我们针对于各个场景做出支持!