Golang优雅关闭gRPC实践
本文主要讨论了在 Go 语言中实现gRPC服务优雅关闭的技术和方法,从而确保所有连接都得到正确处理,防止数据丢失或损坏。原文: Go Concurrency — Graceful Shutdown

问题
我在上次做技术支持的时候,遇到了一个有趣的错误。我们的服务在 Kubernetes 上运行,有一个容器在重启时不断出现以下错误信息--"Error bind: address already in use"。对于大多数程序员来说,这是一个非常熟悉的错误信息,表明一个进程正试图绑定到另一个进程正在使用的端口上。
背景
我的团队维护一个 Go 服务,启动时会在各自的 goroutine 中生成大量不同的 gRPC 服务。
Goroutine - Go 运行时管理的轻量级线程,运行时只需要几 KB 内存,是 Go 并发性的基础。
以下是我们服务架构的简化版本,以及以前启动和停止服务器时所执行的任务。
package main
type GrpcServerInterface interface{
Run(stopChan chan <-struct{})
}
type Server struct {
ServerA GrpcServerIface
ServerB GrpcServerIface
}
func NewServer() *Server {
return &NewServer{
ServerA: NewServerA,
ServerB: NewServerB,
}
}
// Start runs each of the grpc servers
func (s *Server) Start(stopChan <-chan struct{}){
go ServerA.Run(stopChan)
go ServerB.Run(stopChan)
<- stopChan
}
func main() {
stopChan := make(chan struct{})
server := NewServer()
server.Start(stopChan)
// Wait for program to terminate and then signal servers to stop
ch := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-ch
close(stopChan)
}
package internal
type ServerA struct {
stopChan <-chan struct{}
}
// Start runs each of the grpc servers
func (s *ServerA) Run(stopChan <-chan struct{}){
grpcServer := grpc.NewServer()
var listener net.Listener
ln, err := net.Listen("tcp", ":8080")
if err != nil {
// handle error
}
for {
err := grpcServer.Serve(listener)
if err != nil {
return
}
}
<- stopChan
grpcServer.Stop() // Gracefully terminate connections and close listener
}
我首先想到这可能是 Docker 或 Kubernetes 运行时的某种偶发性错误。这个错误让我觉得很奇怪,原因如下:1.)查看代码,我们似乎确实在主程序退出时关闭了所有监听,端口怎么可能在重启时仍在使用?2.)错误信息持续出现了几个小时,以至于需要人工干预。我原以为在最坏情况下,操作系统会在尝试重启容器之前为我们清理资源。或许是清理速度不够快?
团队成员建议我们再深入调查一下。
解决方案
经过仔细研究,发现我们的代码实际上存在一些问题...
通道(Channel)与上下文(Context)
通道用于在程序之间发送信号,通常以一对一的方式使用,当一个值被发送到某个通道时,只能从该通道读取一次。在我们的代码中,使用的是一对多模式。我们将在 main
中创建的通道传递给多个不同的 goroutine,每个 goroutine 都在等待 main
关闭通道,以便知道何时运行清理函数。
从 Go 1.7 开始,上下文被认为是向多个 goroutine 广播信号的标准方式。虽然这可能不是我们遇到问题的根本原因(我们是在等待通道关闭,而不是试图让每个 goroutine 从通道中读取相同的值),但考虑到这是最佳实践,还是希望采用这种模式。
以下是从通道切换到上下文后更新的代码。
package internal
type ServerA struct {}
func (s *ServerA) Run(ctx context.Context){
grpcServer := grpc.NewServer()
var listener net.Listener
ln, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal("ServerA - Failed to create listener")
}
for {
err := grpcServer.Serve(listener)
if err != nil {
log.Fatal("ServerA - Failed to start server")
}
}
<- ctx.Done()
// Clean up logic
grpcServer.Stop() // Gracefully terminate connections and close listener
}
package main
type GrpcServerInterface interface{
Run(stopChan chan <-struct{})
}
type Server struct {
ServerA GrpcServerIface
ServerB GrpcServerIface
stopServer context.CancelFunc
serverCtx context.Context
}
func NewServer() *Server {
return &NewServer{
ServerA: NewServerA,
ServerB: NewServerB,
}
}
// Start runs each of the grpc servers
func (s *Server) Start(ctx context.Context){
// create new context from parent context
s.serverCtx, stopServer := context.WithCancel(ctx)
go ServerA.Run(s.serverCtx)
go ServerB.Run(s.serverCtx)
}
func (s *Server) Stop() {
s.stopServer() // close server context to signal spawned goroutines to stop
}
func main() {
ctx, cancel := context.withCancel()
server := NewServer()
server.Start(ctx)
// Wait for program to terminate and then signal servers to stop
ch := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-ch
cancel() // close main context on terminate signal
server.Stop() // clean up server resources
}
基于等待组(WaitGroup)的优雅停机
虽然我们通过取消主上下文向 goroutine 发出了退出信号,但并没有等待它们完成工作。当主程序收到退出信号时,即使我们发送了取消信号,也不能保证它会等待生成的 goroutine 完成工作。因此我们必须明确等待每个 goroutine 完成工作,以避免任何泄漏,为此我们使用了 WaitGroup。
WaitGroup 是一种计数器,用于阻止函数(或者说是 goroutine)的执行,直到其内部计数器变为 0。
package internal
type ServerA struct {}
func (s *ServerA) Run(ctx context.Context, wg *sync.WaitGroup){
wg.Add(1) // Add the current function to the parent's wait group
defer wg.Done() // Send "done" signal upon function exit
grpcServer := grpc.NewServer()
var listener net.Listener
ln, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal("ServerA - Failed to create listener")
}
for {
err := grpcServer.Serve(listener)
if err != nil {
log.Fatal("ServerA - Failed to start server")
}
}
<- ctx.Done()
// Clean up logic
grpcServer.Stop() // Gracefully terminate connections and close listener
fmt.Println("ServerA has stopped")
}
package main
type GrpcServerInterface interface{
Run(stopChan chan <-struct{})
}
type Server struct {
ServerA GrpcServerIface
ServerB GrpcServerIface
wg sync.WaitGroup
stopServer context.CancelFunc
serverCtx context.Context
}
func NewServer() *Server {
return &NewServer{
ServerA: NewServerA,
ServerB: NewServerB,
}
}
// Start runs each of the grpc servers
func (s *Server) Start(ctx context.Context){
s.serverCtx, stopServer := context.WithCancel(ctx)
go ServerA.Run(s.serverCtx, &s.wg)
go ServerB.Run(s.serverCtx, &s.wg)
}
func (s *Server) Stop() {
s.stopServer() // close server context to signal spawned goroutines to stop
s.wg.Wait() // wait for all goroutines to exit before returning
fmt.Println("Main Server has stopped")
}
func main() {
ctx, cancel := context.withCancel()
server := NewServer()
server.Start(ctx)
// Wait for program to terminate and then signal servers to stop
ch := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-ch
cancel() // close main context on terminate signal
server.Stop() // clean up server resources
}
基于通道的启动信号
在测试过程中,又发现了一个隐藏错误。我们未能在接受流量之前等待所有服务端启动,而这在测试中造成了一些误报,即流量被发送到服务端,但没有实际工作。为了向主服务发送所有附属服务都已准备就绪的信号,我们使用了通道。
package internal
type ServerA struct {
startChan
}
func (s *ServerA) Run(ctx context.Context, wg *sync.WaitGroup){
wg.Add(1) // Add the current function to the parent's wait group
defer wg.Done() // Send "done" signal upon function exit
go func(){
grpcServer := grpc.NewServer()
var listener net.Listener
ln, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal("ServerA - Failed to create listener")
}
for {
err := grpcServer.Serve(listener)
if err != nil {
log.Fatal("ServerA - Failed to start server")
}
}
close(s.startChan) // Signal that we are done starting server to exit function
// Wait in the background for mina program to exit
<- ctx.Done()
// Clean up logic
grpcServer.Stop() // Gracefully terminate connections and close listener
fmt.Println("ServerA has stopped")
}()
<- s.StartChan // Wait for signal before exiting function
fmt.Println("ServerA has started")
}
package main
type GrpcServerInterface interface{
Run(stopChan chan <-struct{})
}
type Server struct {
ServerA GrpcServerIface
ServerB GrpcServerIface
wg sync.WaitGroup
stopServer context.CancelFunc
serverCtx context.Context
startChan chan <-struct{}
}
func NewServer() *Server {
return &NewServer{
ServerA: NewServerA,
ServerB: NewServerB,
startChan: make(chan <-struct{}),
}
}
// Start runs each of the grpc servers
func (s *Server) Start(ctx context.Context){
s.serverCtx, stopServer := context.WithCancel(ctx)
ServerA.Run(s.serverCtx, &s.wg)
ServerB.Run(s.serverCtx, &s.wg)
close(s.startChan)
<- s.startChan // wait for each server to Start before returning
fmt.Println("Main Server has started")
}
func (s *Server) Stop() {
s.stopServer() // close server context to signal spawned goroutines to stop
s.wg.Wait() // wait for all goroutines to exit before returning
fmt.Println("Main Server has stopped")
}
func main() {
ctx, cancel := context.withCancel()
server := NewServer()
server.Start(ctx)
// Wait for program to terminate and then signal servers to stop
ch := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-ch
cancel() // close main context on terminate signal
server.Stop() // clean up server resources
}
结论
不瞒你说,刚开始学习 Go 时,并发会让你头疼不已。调试这个问题让我有机会看到这些概念的实际用途,并强化了之前不确定的主题,建议你自己尝试简单的示例!
你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!
本文由 mdnice 多平台发布
相关文章:

Golang优雅关闭gRPC实践
本文主要讨论了在 Go 语言中实现gRPC服务优雅关闭的技术和方法,从而确保所有连接都得到正确处理,防止数据丢失或损坏。原文: Go Concurrency — Graceful Shutdown 问题 我在上次做技术支持的时候,遇到了一个有趣的错误。我们的服务在 Kubern…...

Maven笔记(一):基础使用【记录】
Maven笔记(一)-基础使用 Maven是专门用于管理和构建Java项目的工具,它的主要功能有: 提供了一套标准化的项目结构 Maven提供了一套标准化的项目结构,所有IDE(eclipse、myeclipse、IntelliJ IDEA 等 项目开发工具) 使…...

[vulnhub] Jarbas-Jenkins
靶机链接 https://www.vulnhub.com/entry/jarbas-1,232/ 主机发现端口扫描 扫描网段存活主机,因为主机是我最后添加的,所以靶机地址是135的 nmap -sP 192.168.75.0/24 // Starting Nmap 7.93 ( https://nmap.org ) at 2024-09-21 14:03 CST Nmap scan…...
js设计模式(26)
js设计模式(26) JavaScript 中常用的设计模式可以分为几大类,包括创建型模式、结构型模式和行为型模式。以下是它们的分类和常见设计模式的完整列表: 一、创建型模式 这些模式主要关注对象的创建方式,目的是降低系统耦合和复杂性。 工厂模…...
数据库中, drop、delete与truncate的区别?
在数据库中,drop、delete和truncate都可以用于删除数据,但它们之间有以下区别: 一、作用对象 drop:可以删除数据库对象,如表、视图、索引、存储过程等。例如:DROP TABLE table_name;可以删除名为 table_na…...

2024年项目经理不能错过的开源项目管理系统大盘点:全面指南
在2024年,随着项目管理领域的不断发展,开源项目管理系统成为了项目经理们提升工作效率的利器。本文将全面盘点几款备受推荐的开源项目管理系统,帮助项目经理们找到最佳选择,不容错过。 在项目管理日益复杂的今天,开源项…...

MATLAB基本语句
MATLAB语言为解释型程序设计语言。在程序中可以出现顺序、选择、循环三种基本控制结构,也可以出现对M-文件的调用(相当于对外部过程的调用)。 由于 MATLAB开始是用FORTRAN语言编写、后来用 C语言重写的,故其既有FORTRAN的特征,又在许多语言规…...

委托的注册及注销+观察者模式
事件 委托变量如果公开出去,很不安全,外部可以随意调用 所以取消public,封闭它,我们可以自己书写两个方法,供外部注册与注销,委托调用在子方法里调用,这样封装委托变量可以使它更安全,这个就叫…...

Jetpack02-LiveData 数据驱动UI更新(类似EventBus)
前提 LiveData使用了Lifecycle的生命周期,阅读本文前,请先了解Lifecycle源码。 简介 LiveData本质是数据类型,当改变数据的时候,会通知观察者,且只在界面可见的时候才会通知观察者。只能在主线程注册观察者…...

Redis 的 Java 客户端有哪些?官方推荐哪个?
Redis 官网展示的 Java 客户端如下图所示,其中官方推荐的是标星的3个:Jedis、Redisson 和 lettuce。 Redis 的 Java 客户端中,Jedis、Lettuce 和 Redisson 是最常用的三种。以下是它们的详细比较: Jedis: 线程安全&…...
工作笔记20240927——vscode + jlink调试
launch.json的配置,可以用的 {"name": "Debug","type": "cppdbg","request": "launch","miDebuggerPath": "./arm-gnu-toolchain-12.2.rel1-x86_64-arm-none-eabi/bin/arm-none-eabi-g…...

Python | Leetcode Python题解之第433题最小基因变化
题目: 题解: class Solution:def minMutation(self, start: str, end: str, bank: List[str]) -> int:if start end:return 0def diffOne(s: str, t: str) -> bool:return sum(x ! y for x, y in zip(s, t)) 1m len(bank)adj [[] for _ in ra…...
opengauss使用遇到的问题,随时更新
一、查看数据库状态的方式 1、gs_ctl -D /opt/huawei/install/data/dn/ status 2、gs_om -t status --detail 3、cm_ctl query -Cv二、opengauss打印WDR性能报告 1、开启WDR性能参数开关 gs_guc reload -N all -D /opt/huawei/install/data/dn -c "enable_wdr_snap…...

从环境部署到开发实战:消息队列 RocketMQ
文章目录 一、消息队列简介1.1 什么是消息队列1.2 常见消息队列对比1.3 RockectMQ 核心概念1.4 RockectMQ 工作机制 (★) 二、RocketMQ 部署相关2.1 服务器单机部署2.2 管控台页面 三、RocketMQ 的基本使用3.1 入门案例3.2 消息发送方式3.2.1 同步消息3.…...

【机器学习(九)】分类和回归任务-多层感知机(Multilayer Perceptron,MLP)算法-Sentosa_DSML社区版
文章目录 一、算法概念二、算法原理(一)感知机(二)多层感知机1、隐藏层2、激活函数sigma函数tanh函数ReLU函数 3、反向传播算法 三、算法优缺点(一)优点(二)缺点 四、MLP分类任务实现…...

渗透测试-文件上传绕过思路
文件上传绕过思路 引言 分享一些文件上传绕过的思路,下文内容多包含实战图片,所以打码会非常严重,可多看文字表达;本文仅用于交流学习, 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#x…...
等保测评中的密码学应用分析
等保测评中密码学应用的分析 等保测评(信息安全等级保护测评)是中国信息安全领域的一项重要活动,旨在评估信息系统的安全性,并根据评估结果给予相应的安全等级。在等保测评中,密码学应用分析是评估信息系统安全性的关键…...
LCR 007. 三数之和
文章目录 1.题目2.思路3.代码 1.题目 LCR 007. 三数之和 给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a ,b ,c *,*使得 a b c 0 ?请找出所有和为 0 且 不重复 的三元组。 示例 1:…...

【入门01】arcgis api 4.x 创建地图、添加图层、添加指北针、比例尺、图例、卷帘、图层控制、家控件(附完整源码)
1.效果 2.代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title></title><link rel"s…...
STL迭代器标签
STL(标准模板库)迭代器标签是用来标识迭代器类型的分类机制。这些标签有助于确定迭代器的特性,比如它是否可以进行随机访问、是否支持修改元素等。主要的迭代器标签包括: Input Iterator:只读迭代器,可以顺…...

SpringBoot-17-MyBatis动态SQL标签之常用标签
文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...

python/java环境配置
环境变量放一起 python: 1.首先下载Python Python下载地址:Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个,然后自定义,全选 可以把前4个选上 3.环境配置 1)搜高级系统设置 2…...
FastAPI 教程:从入门到实践
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,支持 Python 3.6。它基于标准 Python 类型提示,易于学习且功能强大。以下是一个完整的 FastAPI 入门教程,涵盖从环境搭建到创建并运行一个简单的…...

聊聊 Pulsar:Producer 源码解析
一、前言 Apache Pulsar 是一个企业级的开源分布式消息传递平台,以其高性能、可扩展性和存储计算分离架构在消息队列和流处理领域独树一帜。在 Pulsar 的核心架构中,Producer(生产者) 是连接客户端应用与消息队列的第一步。生产者…...

ardupilot 开发环境eclipse 中import 缺少C++
目录 文章目录 目录摘要1.修复过程摘要 本节主要解决ardupilot 开发环境eclipse 中import 缺少C++,无法导入ardupilot代码,会引起查看不方便的问题。如下图所示 1.修复过程 0.安装ubuntu 软件中自带的eclipse 1.打开eclipse—Help—install new software 2.在 Work with中…...

基于IDIG-GAN的小样本电机轴承故障诊断
目录 🔍 核心问题 一、IDIG-GAN模型原理 1. 整体架构 2. 核心创新点 (1) 梯度归一化(Gradient Normalization) (2) 判别器梯度间隙正则化(Discriminator Gradient Gap Regularization) (3) 自注意力机制(Self-Attention) 3. 完整损失函数 二…...

力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...
SQL Server 触发器调用存储过程实现发送 HTTP 请求
文章目录 需求分析解决第 1 步:前置条件,启用 OLE 自动化方式 1:使用 SQL 实现启用 OLE 自动化方式 2:Sql Server 2005启动OLE自动化方式 3:Sql Server 2008启动OLE自动化第 2 步:创建存储过程第 3 步:创建触发器扩展 - 如何调试?第 1 步:登录 SQL Server 2008第 2 步…...
二维FDTD算法仿真
二维FDTD算法仿真,并带完全匹配层,输入波形为高斯波、平面波 FDTD_二维/FDTD.zip , 6075 FDTD_二维/FDTD_31.m , 1029 FDTD_二维/FDTD_32.m , 2806 FDTD_二维/FDTD_33.m , 3782 FDTD_二维/FDTD_34.m , 4182 FDTD_二维/FDTD_35.m , 4793...
多元隐函数 偏导公式
我们来推导隐函数 z z ( x , y ) z z(x, y) zz(x,y) 的偏导公式,给定一个隐函数关系: F ( x , y , z ( x , y ) ) 0 F(x, y, z(x, y)) 0 F(x,y,z(x,y))0 🧠 目标: 求 ∂ z ∂ x \frac{\partial z}{\partial x} ∂x∂z、 …...