最近接到这样一个应用部署需求有点特别,他说他的程序中用到了webscoket,这让没有部署过webscoket应用的小白心里一惊,这咋办?我该说多久可以部署好?部署上线出了问题让我帮助排查我不是只能胡乱抓瞎吗?抱着惶恐的心情开始分析了一下他应用的部署需求,服务是在一个容器中的,但是暴露了3个端口分别用来处理不同的服务:
- 前端 Web 站点 3000 端口
- 后端 API 服务 3001 端口
- 后端 Websocket 聊天服务 3002 端口
让我们尝试部署一下
先写一个部署文件,将这个程序的镜像运行起来。
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 有什么区别呢?
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。
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 个服务通过一个域名 “串” 起来如图
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 连接试试。
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 不对!程序的锅! 这连协议头都没带好吗? MDN 中指出第一个参数必须带着协议的 ws://
或者 wss://
。
自己做了一下实验
错误的:
和业务方的报错一模一样!
正确的:
我去看看他代码和打包文件,收集好证据,知己知彼!
很快就在程序的 .env
发现了问题:
api_endpoint='/api'
chat_endpoint=/wss'
这里的 chat_endpoint
地址不符合规范,应该加上协议,是一个完整的地址,websocket API 才能创建连接成功,否则连校验都过不去,都不会发起连接。
于是修改为 chat_endpoint=wss://chatapp.com/wss
后再重新打包部署前端后,聊天页面不报错了可以正常运行了:
但是这么部署怎么看觉得怎么有问题,因为在部署文件中写死了域名,很多情况是我们其实并不知道最终的实际域名是什么再或者以后如果域名更换了还需要重新打包前端代码,这都是不合理的,所以怎么能将这个 websocket 连接也写成动态的呢?
先把地址改回去 chat_endpoint=/wss
, 然后将程序中连接 websocket 的 js 代码做个灵活的修改!
const getWSURI = (): string => {
const loc = window.location;
let new_uri: string;
if (loc.protocol === 'https:') {
new_uri = 'wss:';
} else {
new_uri = 'ws:';
}
new_uri += '//' + loc.host + process.env.COLLABORATION_API_URL;
return new_uri;
};
new WebSocket(getWSURI())
好了,完美!写到这里不得不感叹啊, Web 生态太大了,导致标准和规范的统一真心难, webkit 环境中其实 Websocket 是可以使用相对域名的,但是浏览器则必须带着协议头。
看起来这次的部署和 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 连接的可靠性和稳定性。可以使用故障检测和容错机制来监测服务器的状态,并在服务器故障时自动切换到其他可用的服务器。此外,可以实现断线重连机制,以便客户端在连接中断后能够重新建立连接。
如果本文对你有帮助,请点个赞和关注吧❤️,我会持续更新有价值的技术和视野,谢谢!