惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
Security @ Cisco Blogs
小众软件
小众软件
D
Docker
博客园_首页
A
About on SuperTechFans
P
Privacy International News Feed
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
A
Arctic Wolf
Spread Privacy
Spread Privacy
有赞技术团队
有赞技术团队
T
Tailwind CSS Blog
Latest news
Latest news
WordPress大学
WordPress大学
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
The Exploit Database - CXSecurity.com
C
Cybersecurity and Infrastructure Security Agency CISA
大猫的无限游戏
大猫的无限游戏
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
K
Kaspersky official blog
V2EX - 技术
V2EX - 技术
SecWiki News
SecWiki News
U
Unit 42
GbyAI
GbyAI
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LINUX DO - 热门话题
S
Security Affairs
Y
Y Combinator Blog
aimingoo的专栏
aimingoo的专栏
Blog — PlanetScale
Blog — PlanetScale
MongoDB | Blog
MongoDB | Blog
博客园 - 【当耐特】
The GitHub Blog
The GitHub Blog
T
Tenable Blog
W
WeLiveSecurity
Cloudbric
Cloudbric
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
G
Google Developers Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
N
Netflix TechBlog - Medium
F
Full Disclosure
N
News and Events Feed by Topic
D
DataBreaches.Net
P
Proofpoint News Feed
B
Blog RSS Feed
B
Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型 Kubernetes 核心概念 软件版本语义化
Kubernetes 应用最佳实践 - 优雅关闭长连接
2023-10-20 · via 蛮荆

2023-10-20 Cloud Native Kubernetes Kubernetes 应用最佳实践

概述

Kubernetes 中的网络连接由节点上面的 Service 代理进行管理,Service 会将流量转发到对应的 Pod 中,相比于传统网络编程中使用 IP 地址连接到单个服务器的方式, Kubernetes 可以提供更高的灵活性和扩展性。

Pod 是 Kubernetes 中应用运行的最小单位,虽然单个 Pod 拥有自己的 IP 和主机名称,但是 Pod 可能会因为伸缩、驱逐、容器内进程异常等情况被终止并重启, 重启后的 Pod 和之前之前的 Pod 拥有不同的 IP 地址,这样一来,也就产生了下面的问题:

网络编程长连接场景中,Pod 被终止后,容器内应用服务管理的所有连接都会被释放,如何在 Pod 终止前通知到所有长连接的客户端呢?

针对这个问题,Kubernetes 提供了成熟的 “优雅关闭应用” 方案。

优雅关闭

在 Kubernetes 中要优雅地关闭长连接,需要保证执行以下步骤:

  1. 确保应用程序可以快速启动和关闭,并且可以处理 SIGTERM 信号 (这个是前提)
  2. Pod 被终止时, kubelet 会向 API Server 发送 DELETE 请求
  3. API Server 处理完 DELETE 请求会通知监听对象 kubelet 和 EndPoint 控制器
  4. kubelet 执行 Pre-Stop 钩子函数并且向容器内应用程序发送 SIGTERM 信号
  5. Endpoints 控制器删除 Pod 对应的 EndPoint 对象, 然后发送事件到 API Server
  6. API Server 通知所有关注这个被删除的 Endpoint 对象的观察者,其中也包括了被删除 Pod 所在节点上面的 kube-proxy
  7. kube-proxy 更新转发规则,更新完成后就会停止向被终止的 Pod 转发流量
  8. 应用程序在接收到 SIGTERM 信号时,应该继续处理新的连接请求,同时开始进行收尾工作 (通知客户端并关闭连接,例如向所有客户端发送广播消息后关闭所有连接)
  9. 客户端在连接断开后,尝试发起新的连接,然后新的连接会被转发到新的 Pod 中

Pod 关闭后转发规则变化

如图所示,当 Pod-2 终止后,新的流量会被转发到 Pod-3。

潜在问题

kubelet 和 Endpoints 控制器接收到通知消息后,会进入并行执行阶段,因此可能出现的情况是: 关闭 Pod 应用进程的完成时间要早于 kube-proxy 更新转发规则的完成时间,此时 Pod 已经终止,但是转发规则还没有更新,又正好新的请求连接过来了, 于是 kube-proxy 将新的请求转发到了已终止的 Pod, 抛出客户端连接错误。

上面这个问题的解决方案,我们将在下文中的 “细节问题” 小节详细描述,接下来先来看看信号捕获和处理的问题。


信号捕获方法

主流编程语言标准库都提供了信号捕获和处理方法,直接使用即可,例如在 Go 语言中可以这样捕获 SIGTERM 信号 (伪代码):

func main() {
	// 创建一个 channel 接收信号
	signalChan := make(chan os.Signal, 1)

	// 捕获 SIGTERM 信号
	signal.Notify(signalChan, syscall.SIGTERM)

	// 无限循环
	for {
		// 收到 SIGTERM 信号之后退出程序
		<-signalChan
		
		fmt.Println("收到 SIGTERM 信号")
		return
	}
}

信号处理

在下面的代码中,我们创建了一个 TCP 服务并监听请求连接 (伪代码,不考虑并发读写等问题),并在接收到 SIGTERM 信号之后进行以下工作:

  1. goroutine 收到信号之后打印一条日志
  2. 遍历所有已建立的连接,逐个关闭
  3. 关闭 TCP 服务监听,不再接收新的连接
package main

// 全局连接池
var connections = make(map[net.Conn]struct{})

func main() {
    // 创建 TCP 服务
    listener, err := net.Listen("tcp", ":8080")
    if err != nil {
		log.Fatal(err)
        os.Exit(1)
    }
    defer listener.Close()

	// 创建一个 channel 接收信号
	signalChan := make(chan os.Signal, 1)

	// 捕获 SIGTERM 信号
	signal.Notify(signalChan, syscall.SIGTERM)

    // 启动一个 goroutine 用来监听信号
    go func() {
        <-signalChan
		fmt.Println("收到 SIGTERM 信号,开始关闭连接")

        // 遍历所有已建立的连接,依次关闭
		// 注意: 这里可能存在并发读写延迟,更好的实现方式为 channel
        for conn := range connections {
            conn.Close()
        }

		// 关闭 TCP 服务器,停止接受新连接
		listener.Close()

        // 退出程序
        os.Exit(0)
    }()

    // 无限循环,接受客户端新连接
    for {
        conn, err := listener.Accept()
        if err != nil {
			log.Fatal(err)
            continue
        }

        // 将新连接添加到连接池
		// 注意: 这里可能存在并发读写延迟,更好的实现方式为 channel
        connections[conn] = struct{}{}

        // 在新协程中处理客户端请求
        go handleConnection(conn)
    }
}

// 处理客户端请求
func handleConnection(conn net.Conn) {
    // ...
}

细节问题

为什么收到 SIGTERM 信号之后,对于新到的连接请求,应用程序还要继续处理,而不是直接拒绝呢?

因为 Kubernetes 处理 kube-proxy 的转发规则和负载均衡需要时间, 如果应用程序直接拒绝新的请求,会导致客户端错误。Pod 在删除期间虽然处于 Terminating 状态,但是依然可以正常接收和处理请求。

最佳实践是: 在收到 SIGTERM 信号之后,使 就绪探针 停止检查 (探针直接返回失败即可),这样 Pod 将不再接收任何流量,应用程序不需要再处理新的请求连接。

Pre-Stop 钩子函数

除了使用应用程序来处理 Pod 的终止到重启之间的过渡阶段,还可以使用 Pre-Stop 钩子函数来处理,常见的策略是在钩子函数内休眠一段时间, 这样可以为 Pod 的删除过程提供额外的缓冲时间。

Pre-Stop 钩子函数执行完成后向容器应用进程发送 SIGTERM 信号,默认情况下,Pod 删除的超时 (terminationGracePeriodSeconds) 时间为 30 秒, 超时后 Pod 会被 Kill, 这个时间指的就是 Pre-Stop 钩子函数的执行时间加应用程序处理 SIGTERM 信号的时间

例如 terminationGracePeriodSeconds 的值为 30 秒, Pre-Stop 钩子函数执行了 25 秒,应用程序处理 SIGTERM 信号需要 10 秒,那么应用程序等不到执行完成, 就会被 kill, 因为 25 + 10 > 30。

下面是是一个 Nginx Pod 使用 Pre-Stop 钩子函数的示例:

# 指定超时为 60 秒
terminationGracePeriodSeconds: 60
containers:
- image: nginx
  lifecycle:
    preStop:
      exec:
        command: [
          "sh", "-c",
          # 休眠 10 秒,然后优雅关闭 Nginx
          "sleep 10 && /usr/sbin/nginx -s quit",
        ]

注意: Kubernetes 集群自动伸缩的执行超时为 10 分钟,所以 terminationGracePeriodSeconds 参数的值应该小于 10 分钟,默认情况下的 30 秒其实是良好的实践。


小结

Pod 的删除首先是 逻辑删除过程,具体来说就是 etcd 中删除了对应的 Pod 记录,其次是 物理删除过程,也就是节点的 Pod 在执行完 Pre-Stop 钩子函数和 SIGTERM 信号捕获之后, 关闭 Pod 内的所有容器,最后 Pod 被节点删除。所有想实现 “优雅关闭、升级” 的应用程序,都要围绕着这两个过程展开具体的业务逻辑,最终实现服务端的高可用加客户端的优秀体验。

扩展阅读