如何在不中断服务的情况下更新C#服务器应用程序?
无中断更新C#服务器应用的实践方案
核心思路:实现版本平滑过渡
本质是让新版本上线时,旧版本能继续处理现有客户端请求,直到所有连接自然结束或超时,同时新版本接管新请求,避免直接杀掉旧进程导致服务中断。
具体实现技巧
- 反向代理流量切换:用Nginx、IIS ARR这类反向代理工具,更新时先启动新版本实例,通过健康检查确认服务可用后,逐步将流量切到新版本。旧版本保持运行,直到所有已连接客户端处理完请求再关停。C#应用要确保支持多实例运行,用户数据、会话不要存在进程内存里,改用分布式缓存或数据库共享。
- 优雅关闭机制:在C#应用中监听进程关闭信号,停止接受新连接,等待现有请求处理完成再退出。比如ASP.NET Core可以用
IHostApplicationLifetime实现:
var host = Host.CreateDefaultBuilder(args) .ConfigureServices(services => services.AddHostedService<GracefulShutdownService>()) .Build(); host.Run(); public class GracefulShutdownService : IHostedService { private readonly IHostApplicationLifetime _appLifetime; private readonly ILogger<GracefulShutdownService> _logger; public GracefulShutdownService(IHostApplicationLifetime appLifetime, ILogger<GracefulShutdownService> logger) { _appLifetime = appLifetime; _logger = logger; } public Task StartAsync(CancellationToken cancellationToken) { _appLifetime.ApplicationStopping.Register(() => { _logger.LogInformation("触发优雅关闭,停止接受新连接"); // 这里添加停止监听新连接的逻辑,比如关闭Socket或Kestrel的监听 // 等待现有连接处理完成,设置合理超时(比如30秒) Task.Delay(TimeSpan.FromSeconds(30)).Wait(); _logger.LogInformation("所有连接处理完毕,准备退出"); }); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }
- 解耦进程内状态:如果应用把用户会话、临时数据存在内存,更新时会丢失状态。改用Redis、SQL Server等外部存储保存这些数据,让新旧实例都能访问,客户端切换到新版本后状态不会中断。
- 滚动更新(集群场景):如果是多实例集群部署,每次只更新一个实例。等该实例启动并通过健康检查后,再更新下一个。这样始终有可用实例处理请求,完全避免服务中断。
关于UNIX Sockets传递更新数据的意见
UNIX Sockets是本地进程间通信的高效方案,适合在新旧进程间传递临时数据,但要注意以下几点:
- 适用场景:单实例部署时,用来传递未处理完的请求上下文、临时状态,帮助新版本快速承接旧版本的工作。如果是多实例集群,更推荐用共享存储传递状态,比进程间通信更可靠。
- 关键注意事项:
- 新版本启动后要先监听UNIX Socket,再通知旧进程发送数据,避免旧进程发送时无接收方导致数据丢失。
- 数据序列化用稳定格式,比如Protobuf、MessagePack,不要用JSON(性能差且易出现兼容性问题)。
- 做降级处理:如果旧进程崩溃没发送数据,新版本要能从持久化存储恢复状态,不能完全依赖Socket传递的内容。
- 跨平台兼容:Windows 10 1809+或Server 2019+才支持UNIX Sockets,若服务器可能跑Windows,建议用命名管道作为替代方案。
- 简单实现示例:
// 旧进程发送数据 using var senderSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); senderSocket.Connect(new UnixDomainSocketEndPoint("/tmp/app_update.sock")); var payload = Encoding.UTF8.GetBytes("待传递的用户数据"); senderSocket.Send(payload); senderSocket.Shutdown(SocketShutdown.Send); // 新进程接收数据 using var listenerSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); var endpoint = new UnixDomainSocketEndPoint("/tmp/app_update.sock"); listenerSocket.Bind(endpoint); listenerSocket.Listen(1); using var clientSocket = listenerSocket.Accept(); var buffer = new byte[1024]; var receivedBytes = clientSocket.Receive(buffer); var receivedData = Encoding.UTF8.GetString(buffer, 0, receivedBytes); // 处理接收的数据
内容的提问来源于stack exchange,提问作者Mohammed Sitni
相关产品推荐
相关产品推荐

