端口永远就绪,而进程只在有人敲门时才起来,这正是少用却沉重的服务该有的妙招。每个闲置守护进程都在花钱:常驻内存、周期性唤醒、补丁面、日志噪音。一台机器上几十个不常用的服务加起来,税交了不少,事一件没干。套接字激活把交易反过来——内核拿着监听套接字,守护进程出生之前它就归系统服务管理器所有,第一个带着真活的连接到了,服务进程才出生。

交接的顺滑让一切透明。客户端照常连端口,不知道家里没人;管理器拉起服务,把已经接受的连接递进去,第一个客户端都不必等两遍。做对了,按需启动和常开除了账单看不出区别——除了首击延迟,这是唯一要诚实预算的成本。

让监听器先等着

systemd 在服务之前先把 socket 打开,第一个连接到来时才启动单元,并把收到的文件描述符交过去。把 ListenStream 配好一次,让 socket 在任何守护进程起来之前就先诞生并绑定好。套接字单元和服务单元按名配对,一个管监听、一个管干活;默认模式把整个监听套接字交给单个服务实例,按连接派生的模式则给简单请求处理配每连接一个实例。

分离还顺手解决了开机排序。不用写脆弱的先后依赖:套接字开机早期就在,客户端随便什么顺序起,连不上就等一等、重试一次,而不是直接报错。排队的连接在内核积压里等着,开机竞态变成了队列深度而不是错误。当然积压长度也要调:启动慢的服务遇上开机早期的连接洪峰,溢出起来和普通监听队列一样。

服务按需唤醒

空闲的守护进程可以一直停着,用每实例的内存与启动周期,换一次交接的开销。所以套接字激活的服务可以一直睡,直到第一个请求值得它跑起来。这套最适合管理工具、老协议网关、定期但罕见的任务——闲忙比按天算、不按分钟算的服务。

闲时退出补上闭环:服务空闲若干秒自己停,系统在零成本和满能力之间振荡,不用人管。套接字从不关闭,下一个客户端透明触发下一次启动。没有闲时退出,激活只省了开机内存,进程还是越积越多——按需启动加闲时停止成对出现,妙招才变成稳态节省。

用速度换内存

代价是第一次访问有一段短暂热身,这适合很少跑的任务,而对始终热呼的路径很浪费。在首次调用的延迟可接受、而内存节省真的有意义时才开启它。动手前两边都量化:从连接到首个响应的冷启动耗时,常开会永远占住的常驻内存。启动三百毫秒换八百内存,大胆开;预热二十秒换五十内存,算了。

有些负载无论如何都受不了这个间隙。靠健康检查编排的系统会在启动期判端点死亡,负载均衡会摘掉首个慢后端,超时卡得紧的客户端没等服务热完就失败了。这些就让守护进程常开,内存该花就花——或者开机时预热一次,激活只覆盖之后漫长的闲置。

坏掉的样子

标志性故障是惊群式启动:监控扫一轮,五十个睡着的服务同时被连醒,同时启动,主机内存告急、负载冲顶,服务大面积变慢。每个激活单独看都合理,合在一起就是踩踏。用错峰检查间隔、按服务的启动限制、足够短的闲时退出,把稳态重叠压小。测试激活要在监控负载下测,不只拿一条手工请求测。

第二种故障是半吊子适配:服务认定套接字归自己,启动时自己绑端口,和管理器已绑的套接字撞车。被激活的那一刻恰恰启动失败,连接挂住,日志怪地址已被占用。服务必须接受递进来的文件描述符而不是自己绑;上游代码改不动,就写个小垫片继承套接字再拉起守护进程过渡。

快速验证

端到端验证按需行为:

systemctl status foo.socket foo.service
ss -ltnp | grep ':8080'
curl -m 10 localhost:8080/health; systemctl show foo.service -p ActiveState,ExecMainStartTimestamp

请求之前服务未激活、套接字在监听;请求之后时间戳显示一次由连接触发的新启动。

延伸阅读

来源