0经验小白运维生产 k8s 翻车日记2 - 应用部署失败

最近接到这样一个应用部署需求有点特别,他说他的程序中用到了webscoket,这让没有部署过webscoket应用的小白心里一惊,这咋办?我该说多久可以部署好?部署上线出了问题让我帮助排查我不是只能胡乱抓瞎吗?抱着惶恐的心情开始分析了一下他应用的部署需求,服务是在一个容器中的,但是暴露了3个端口分别用来处理不同的服务:

  • 前端 Web 站点 3000 端口
  • 后端 API 服务 3001 端口
  • 后端 Websocket 聊天服务 3002 端口

让我们尝试部署一下

先写一个部署文件,将这个程序的镜像运行起来。

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
    name: chatapp-deployment
spec:
    replicas: 1
    selector:
        matchLabels:
            app: chatapp
    template:
        metadata:
            labels:
                app: chatapp
        spec:
            containers:
                - name: chatapp
                  image: chatapp

Line 1 指定了 K8S API 版本为 v1, Line 2 定义了一个类型为 Deployment 的资源,并且这个 Deployment 资源的名称为 chatapp-deployment

Line 5 的 spec 描述了这个 deployment 的规范,replicas 为 1,分布式部署的话,就该这个参数就可以,比如写 5,K8S 就会部署 5 个程序,默认情况下这 5 台机器不一定会部署在同一个机器上。

这里有个细节 这个 matchLabelsLine 12labels 有什么区别呢?

yaml 复制代码
  selector:
        matchLabels:
            app: chatapp
            
... ...

           labels:
               app:chatapp

matchLabels 和 labels 可以相同,并且不会产生副作用。将它们设置为相同的值可以确保选择器选择与指定标签匹配的 Pod,并将它们管理在同一个 Deployment 或 ReplicaSet 下。这么说太抽象,举个🌰,比如我们在 Deployment 中申明部署 5 个程序,那 K8S 就会保证只有 5 个 POD,它怎么知道这 1个 Deployment 对应的那几个 POD 呢? 就是通过这个 label!为了避免潜在的问题和混淆,通常建议在定义 Deployment 或 ReplicaSet 的 selector 时,与 Pod 的 metadata.labels 使用相同的标签。

Line 14 描述了 POD 的规范,比如指定了从哪里拉镜像。

在 K8S 上执行这个配置文件后,业务方给的应用就算在 K8S 上跑起来,但是他们没有 IP 还不能访问,下面我们通过 Service 给他们创建 IP。

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
    name: chatapp-svc
spec:
    selector:
        app: chatapp
    ports:
        - name: web	
          protocol: TCP
          port: 3001 
          targetPort: 5001 
        - name: api	
          protocol: TCP
          port: 3002
          targetPort: 5002 
        - name: ws
          protocol: TCP
          port: 3003
          targetPort: 5003 

我们将这个容器的 3 个服务端口都暴露出来,在Kubernetes中,targetPort和port是部署service时的两个重要参数。

  • targetPort 是指定Pod中容器的端口号,用于将流量转发到Pod中运行的应用程序。
  • port 是指定Service的端口号,用于将流量引入Service并将其转发到Pod。

简而言之,targetPort 就是开发程序中写的,他们写的多少,那就是多少,开发应该将端口写成配置文件,可以通过配置文件灵活改动端口。 port 就只是个 service 端口,没有含义,当有人请求这个 service,我们将他转发到 实际应用的端口就行(也就是targetPort) ,所以为了心智负担低,这里两个值写一样。

然后再去 K8S 上执行这个 Service 申明后,新建一个 Nginx ingress 做一个反代,将 3 个服务通过一个域名 "串" 起来如图

yaml 复制代码
spec:
  rules:
    - host: chatapp.com
      http:
        paths:
          - backend:
              service:
                name: chatapp-deployment
                port:
                  number: 3001
            path: /
            pathType: Prefix
          - backend:
              service:
                name: chatapp-deployment
                port:
                  number: 3002
            path: /api
            pathType: Prefix
          - backend:
              service:
                name: chatapp-deployment
                port:
                  number: 3003
            path: /wss
            pathType: Prefix

目前我们的部署架构图如下:

然而,当点到聊天页面时,终于,墨菲定律发生了,害怕什么来什么,聊天页面挂掉了!

然后大脑开始飞速运转:

  • 这个前端转发是 OK 的,不然看不到聊天页面
  • 这个后端 API 转发也是 Ok 的,API 接口访问不报错
  • 这个 Websocket 就报上面的错,难道?难道 Nginx ingress 在反代 websocket 需要特殊配置?

快速找到 Nginx ingress 官网看看相关介绍

从这个描述中看,Nginx ingress 反代应该没有什么东西需要配置的,就当它是一个 Nginx 配置就行。

先排查一下 websocket 服务是否正常提供服务, 创建一个 NodePort 将这个服务单独暴露出来,然后通过 Postman 连接试试。

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: 3003
      nodePort: 30000 
  selector:
    app: chatapp

用 Postman 单独连接一下它这个 websocket 服务试试呢

显示没问题,websocket 聊天服务器已经连接上了,那奇怪了? 那有没有可能是业务方程序自己的问题呢?报错页面打个断点看看。

看这个报错明显是前端 call 浏览器的 websocket API 在创建连接时候失败了。

这 URL 不对!程序的锅! 这连接连协议头都没带好吗? 自己做了一下实验

错误的:

和业务方的报错一模一样!

正确的:

我去看看他代码和打包文件,收集好证据,知己知彼!

很快就在程序的 .env 发现了问题:

.env 复制代码
api_endpoint='/api'
chat_endpoint=/wss'

这里的 chat_endpoint 地址没填,还是调试时候的,应该加上协议,是一个完整的地址,websocket API 才能创建连接成功,否则连校验都过不去,都不会发起连接。

于是修改为 chat_endpoint=ws://localhost/wss,但是看到这里突然发现之前的部署架构"太多余",我恍然大悟为什么他 3 个程序写在一个容器里了, 这样部署起来简单! 前端打包时配置文件写成 localhost + 对应端口号,部署时候只要将 ingress 的流量转发到前端即可!所以改一下这个配置文件和架构图然后重新打包前端程序。

.env 复制代码
api_endpoint='http://localhost:3002/api'
chat_endpoint=ws://localhost:3003'

同时把 ingress 的转发规则删掉,就是下面这两条

yaml 复制代码
          - backend:
              service:
                name: chatapp-deployment
                port:
                  number: 3002
            path: /api
            pathType: Prefix
          - backend:
              service:
                name: chatapp-deployment
                port:
                  number: 3003
            path: /wss
            pathType: Prefix

最终部署架构图为:

见证奇迹:

看起来这次的部署和 websocket 基本没有关系,但是为了未来更好的故障排查,这里还是巩固一下 websocket 的连接过程:

  1. 客户端发送一个 HTTP 请求到服务器,请求升级到 WebSocket 协议。

    • 也就是说浏览器会发送一个普通的 HTTP GET 请求到服务器,其中包含了一些特殊的头部信息,如 Upgrade 和 Connection,表明客户端希望升级到 WebSocket 协议。
    • 如果客户端没发这个请请求头,说明客户端代码的问题
  2. 服务器收到请求后,会检查是否支持 WebSocket 协议,如果支持,则返回一个 101 Switching Protocols 的响应,表示协议切换成功。

    • 服务器在收到客户端的请求后,会进行协议切换的验证。如果服务器支持 WebSocket 协议,会返回一个 101 Switching Protocols 的响应,表明协议切换成功,可以进行 WebSocket 通信。
    • 如果服务端没返回这个,客户端得到类似升级失败或者把请求当做一个普通 Http 请求处理,那说明是服务端代码问题,没有协商成功 将 http 升级为 ws
  3. 客户端收到响应后,连接成功建立,可以开始进行 WebSocket 通信。

    • 客户端接收到服务器返回的 101 Switching Protocols 的响应后,表示连接已经成功建立。此时,客户端和服务器可以开始进行 WebSocket 通信,并通过 WebSocket 连接发送和接收消息。
    • 这时候浏览器的 network 里我们可以通过过滤 ws标签来查看 message了

同时开发同学为了保障在分布式环境中 websocket 应用不出问题,还应该至少考虑下面 3 种情况:

  • 会话同步:如果需要在多个服务器之间共享会话状态,可以考虑使用会话同步机制。可以使用共享存储或数据库来存储会话状态,并确保多个服务器可以访问和更新该状态。这样可以确保在不同服务器上的 WebSocket 连接都可以访问到相同的会话状态。

  • 消息广播:在分布式环境中,确保 WebSocket 消息可以正确地广播到所有连接的客户端是很重要的。可以使用消息队列或其他分布式通信机制来实现消息广播。当一个服务器接收到消息时,它可以将消息发送到消息队列,然后其他服务器从队列中获取消息并发送到连接的客户端。

  • 容错处理:在分布式环境中,需要处理网络故障、服务器崩溃等异常情况,以确保 WebSocket 连接的可靠性和稳定性。可以使用故障检测和容错机制来监测服务器的状态,并在服务器故障时自动切换到其他可用的服务器。此外,可以实现断线重连机制,以便客户端在连接中断后能够重新建立连接。

如果本文对你有帮助,请点个赞和关注吧❤️,我会持续更新有价值的技术和视野,谢谢!

相关推荐
丁总学Java8 分钟前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
姜学迁8 分钟前
Rust-枚举
开发语言·后端·rust
懒羊羊大王呀19 分钟前
CSS——属性值计算
前端·css
无咎.lsy44 分钟前
vue之vuex的使用及举例
前端·javascript·vue.js
【D'accumulation】1 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
fishmemory7sec1 小时前
Electron 主进程与渲染进程、预加载preload.js
前端·javascript·electron
fishmemory7sec1 小时前
Electron 使⽤ electron-builder 打包应用
前端·javascript·electron
2401_854391081 小时前
高效开发:SpringBoot网上租赁系统实现细节
java·spring boot·后端
Cikiss1 小时前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
Cikiss1 小时前
微服务实战——平台属性
java·数据库·后端·微服务