最近接到这样一个应用部署需求有点特别,他说他的程序中用到了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 台机器不一定会部署在同一个机器上。
这里有个细节 这个 matchLabels 和 Line 12 的 labels 有什么区别呢?
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 的连接过程:
-
客户端发送一个 HTTP 请求到服务器,请求升级到 WebSocket 协议。
- 也就是说浏览器会发送一个普通的 HTTP GET 请求到服务器,其中包含了一些特殊的头部信息,如 Upgrade 和 Connection,表明客户端希望升级到 WebSocket 协议。
- 如果客户端没发这个请请求头,说明客户端代码的问题
-
服务器收到请求后,会检查是否支持 WebSocket 协议,如果支持,则返回一个 101 Switching Protocols 的响应,表示协议切换成功。
- 服务器在收到客户端的请求后,会进行协议切换的验证。如果服务器支持 WebSocket 协议,会返回一个 101 Switching Protocols 的响应,表明协议切换成功,可以进行 WebSocket 通信。
- 如果服务端没返回这个,客户端得到类似升级失败或者把请求当做一个普通 Http 请求处理,那说明是服务端代码问题,没有协商成功 将 http 升级为 ws
-
客户端收到响应后,连接成功建立,可以开始进行 WebSocket 通信。
- 客户端接收到服务器返回的 101 Switching Protocols 的响应后,表示连接已经成功建立。此时,客户端和服务器可以开始进行 WebSocket 通信,并通过 WebSocket 连接发送和接收消息。
- 这时候浏览器的 network 里我们可以通过过滤 ws标签来查看 message了
同时开发同学为了保障在分布式环境中 websocket 应用不出问题,还应该至少考虑下面 3 种情况:
-
会话同步:如果需要在多个服务器之间共享会话状态,可以考虑使用会话同步机制。可以使用共享存储或数据库来存储会话状态,并确保多个服务器可以访问和更新该状态。这样可以确保在不同服务器上的 WebSocket 连接都可以访问到相同的会话状态。
-
消息广播:在分布式环境中,确保 WebSocket 消息可以正确地广播到所有连接的客户端是很重要的。可以使用消息队列或其他分布式通信机制来实现消息广播。当一个服务器接收到消息时,它可以将消息发送到消息队列,然后其他服务器从队列中获取消息并发送到连接的客户端。
-
容错处理:在分布式环境中,需要处理网络故障、服务器崩溃等异常情况,以确保 WebSocket 连接的可靠性和稳定性。可以使用故障检测和容错机制来监测服务器的状态,并在服务器故障时自动切换到其他可用的服务器。此外,可以实现断线重连机制,以便客户端在连接中断后能够重新建立连接。
如果本文对你有帮助,请点个赞和关注吧❤️,我会持续更新有价值的技术和视野,谢谢!