跳到主要内容

2 篇博文 含有标签「naison」

查看所有标签

排查公有云 Cloud IDE websocket 链接异常断开问题

· 阅读需 7 分钟
wencaiwulue
Senior DevOps / System Engineer @ bytedance

背景

之前做了一个 Cloud IDE,可以实现多人同时编辑同一个文件。在私有云是好好地,但是上线到公有云后,出现了 WebSocket 异常断开的问题。 img.png

现象

同时打开两个工作空间,一个工作空间打开多个标签页,开启 network 查看网络流量。然后放置不理,过一段时间后,WebSocket 断开。通过 WebSocket 中的 pingpong 观察到多个网页的 WebSocket 基本上都是在同一时刻断开的。

  • WebSocket 断开链接都是同时发生,不是一个个顺序发生的
  • 相同工作空间多个标签页 WebSocket 断开的时刻相同
  • 不同工作空间标签页 WebSocket 断开的时刻不同

初步尝试

起初的排查是链接超时时间。但是修改了超时时间后,依旧有问题。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/proxy-connect-timeout: "120"
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
nginx.ingress.kubernetes.io/proxy-send-timeout: "120"
nginx.ingress.kubernetes.io/proxy-request-buffering: "off"

再次尝试

没有办法了,祭出大招 tcpdump

root@iv-yd6qd3t0qo5i3z3c3v05:~# ps -ef | grep ingress
systemd+ 2410639 2410439 0 Jun25 ? 00:00:00 /usr/bin/dumb-init -- /nginx-ingress-controller --publish-service=kube-system/ingress-nginx-controller --election-id=ingress-controller-leader-nginx --controller-class=k8s.io/ingress-nginx --ingress-class=nginx --configmap=kube-system/ingress-nginx-controller --validating-webhook=:8443 --validating-webhook-certificate=/usr/local/certificates/cert --validating-webhook-key=/usr/local/certificates/key --watch-ingress-without-class=true -v=10
systemd+ 2410653 2410639 0 Jun25 ? 00:08:37 /nginx-ingress-controller --publish-service=kube-system/ingress-nginx-controller --election-id=ingress-controller-leader-nginx --controller-class=k8s.io/ingress-nginx --ingress-class=nginx --configmap=kube-system/ingress-nginx-controller --validating-webhook=:8443 --validating-webhook-certificate=/usr/local/certificates/cert --validating-webhook-key=/usr/local/certificates/key --watch-ingress-without-class=true -v=10
root 2602901 2602885 0 10:24 pts/0 00:00:00 grep ingress
root@iv-yd6qd3t0qo5i3z3c3v05:~# nsenter -n -t 2410653
root@iv-yd6qd3t0qo5i3z3c3v05:~# tcpdump -i eth0 -vvv -w tcpdump.pcap

在流量经过的每个节点抓包。从而绘制出网络拓扑图。 cloudide-websocket-arch.svg 常用的 wireshark 过滤语句

  • ip.addr == 192.0.2.1 and tcp.port not in 25
  • ip.dst == 192.168.0.65 and tcp.flags.fin

使用 tcp.flags.fin 过滤出 FIN 包的链接

观察

从图中可以看出,ingress-nginx-controlleride-server 和 控制面的 proxy,都发送了 FIN 包,从而导致链接断开。因此定位是 ingress-nginx-controller 的问题。

表层原因

虽然找到了是 ingress-nginx-controller 的问题,但是不知道为什么会断开,于是先去检查 ingress 的配置。

➜  ~ kubectl get ingress -n shared-webapp-01
NAME CLASS HOSTS ADDRESS PORTS AGE
ide-wco5mu3vihefepmq77ap0 nginx ide.volcengine.com 192.168.1.7 80 62s
ide-wcptaca7ihefdlmf7ci4g nginx ide.volcengine.com 192.168.1.7 80 77s
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 1G
nginx.ingress.kubernetes.io/proxy-connect-timeout: "120"
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
nginx.ingress.kubernetes.io/proxy-request-buffering: "off"
nginx.ingress.kubernetes.io/proxy-send-timeout: "120"
nginx.ingress.kubernetes.io/rewrite-target: /$2
name: ide-wco5mu3vihefepmq77ap0
namespace: shared-webapp-01
spec:
ingressClassName: nginx
rules:
- host: ide.volcengine.com
http:
paths:
- backend:
service:
name: ide-wco5mu3vihefepmq77ap0
port:
name: http
path: /webapp-01/ide-wco5mu3vihefepmq77ap0(/|$)(.*)
pathType: Prefix
status:
loadBalancer:
ingress:
- ip: 192.168.1.7

一个工作空间,会创建一个 ingress。起初并没有觉着有什么问题。然后又对比了数据面和控制面的 /etc/nginx/nginx.conf ,并没有发现很明显的差异。

架构入手

后来看了一下设计文档,发现有个组件 agent 用来控制和管理 ide 生命周期的。想着会不会是因为这个组件出 bug 了,导致 websocket 被异常关闭。因此将数据面的 agent 的副本数设置为 0,然后 websocket 就稳定了。翻看了一下 agent 的代码,发现有创建/删除 ingress的操作。于是去搜了一下 ingress-nginx-controller 的 issue,发现有个 #2461,当 nginx reload 的时候,会把已经存在的 websocket 链接关闭。找到个选项 worker-shutdown-timeout,可以设置 nginx reload 的超时时间。试了之后,发现的确有效果。至此,问题的真正原因找到了。

真正原因

当 ingress 发生变化后,ingress-nginx-controller 会收集 ingress 的配置,然后转换为 nginx 的配置,然后 nginx -s reload。reload 的时候。先发送 HUP 信号给 master process,然后 master process 会发送消息给 worker processes,通知 worker processes 关闭。但如果此时 worker processes 上还有链接正在处理,worker processes 会等待链接处理完成后再关闭。如果处理时间过长,worker processes 会超时关闭。而超时时间就是 worker-shutdown-timeout。默认值是 240s,也就是 4 分钟。

https://nginx.org/en/docs/ngx_core_module.html#worker_shutdown_timeout

Configures a timeout for a graceful shutdown of worker processes. When the time expires, nginx will try to close all the connections currently open to facilitate shutdown.

https://nginx.org/en/docs/control.html

In order for nginx to re-read the configuration file, a HUP signal should be sent to the master process. The master process first checks the syntax validity, then tries to apply new configuration, that is, to open log files and new listen sockets. If this fails, it rolls back changes and continues to work with old configuration. If this succeeds, it starts new worker processes, and sends messages to old worker processes requesting them to shut down gracefully. Old worker processes close listen sockets and continue to service old clients. After all clients are serviced, old worker processes are shut down.

临时解决方法

增加 worker-shutdown-timeout 的值,比如 10 小时。测试后发现,可以解决问题。但是,这个值不能太大,否则会导致 worker processes 无法及时关闭,导致资源无法回收。从而导致文件描述符耗尽。无法创建出新的 worker processes

https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#worker-shutdown-timeout

解决方向

  • 改架构。使用动态代理,而不是静态代理。这样就不需要 ingress 了。
events {
worker_connections 1024;
}

http {
# 定义服务列表
map $uri $service_name {
~/service/(?<service>\w+)/* $service;
default "";
}

server {
listen 80;

# 根据 URL 中的 path 转发到对应的服务
location / {
proxy_pass http://$service_name;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
  • 不改架构。使用 higress ,阿里 MSE 的开源项目,基于 envoy 实现的动态代理,没有 reload。

使用 kubevpn dev 模式在本地开发云原生工程

· 阅读需 6 分钟
wencaiwulue
Senior DevOps / System Engineer @ bytedance

使用 KubeVPN dev 模式在本地开发项目

前言

最近在开发 WebIDE 功能,开发其中的聊天功能,原理不是很复杂,但是需要存储用户的聊天信息。因此需要使用到 MySQL 数据库,但是我们的集群现在都关闭了公网访问,开发环境的集群需要通过其他集群的网络做跳板。并且这个 MySQL 是个外置的数据库,也就是没有运行在集群中,而是在同一个 VPC 下。

诉求

能够在本地启动项目,快速开发。

简单的解决之道

在本地直接使用 docker 启动一个 MySQL,修改一下配置文件,就可以在本地启动项目了。但需要手动改配置,也不是很方便。

使用 kubevpn 在本地开发

kubevpn dev deployment/ws-co98bl9mhb1pisov4tc0 -n feiyan-1000000001 --rm --dev-image naison/kubevpn:v2.2.4 --entrypoint bash -v ~/GolandProjects/ide-server:/app -p 2345:2345 --extra-domain mysql1a1bb5fc80a6.rds.ivolces.com -it --connect-mode container --no-proxy

参数解释:

  • -n 指定工作负载所在的工作空间。
  • --rm 自动删除本地 container。
  • --dev-image 指定在本地启动 container 的镜像,如果不指定的话,默认使用工作负载的镜像。
  • --entrypoint 指定启动命令,如果不指定,默认使用工作负载的启动命令。
  • -v 挂载磁盘。将本地目录挂载在 docker 容器内。
  • -p 指定暴露端口,将 docker 容器端口暴露在主机上。
  • --extra-domain 额外的域名。将本域名解析的 ip 加入到本地路由表。
  • -it 交互模式,并启动 tty。
  • --connect-mode,指定链接模式,可以直接在容器中链接集群网络。
  • --no-proxy 指定是否将工作负载的流量拦截到本地。

熟悉 kubectl 和 docker 的同学可以很明显的看出,这些参数都是 kubectl 以及 docker 的选项,没错,就是这样的。

启动完成后,就会自动进入到 terminal 中,然后这个 terminal 的网路就是和 k8s 集群的网络是打通的。并且环境变量,磁盘挂载,也都是集群中工作负载的配置一模一样。这样就可以在这里启动项目啦~

➜  ~ kubevpn dev deployment/ws-co98bl9mhb1pisov4tc0 -n feiyan-1000000001 --rm --dev-image naison/kubevpn:v2.2.4 --entrypoint bash -v ~/GolandProjects/ide-server:/app -p 2345:2345 --extra-domain mysql1a1bb5fc80a6.rds.ivolces.com -it --connect-mode container --no-proxy
starting container connect to cluster
Created container: kubevpn_local_96e04
Wait container kubevpn_local_96e04 to be running...
Container kubevpn_local_96e04 is running now
start to connect
got cidr from cache
get cidr successfully
update ref count successfully
traffic manager already exist, reuse it
port forward ready
tunnel connected
adding route...
dns service ok
container connect to cluster successfully
tar: Removing leading `/' from member names
tar: Removing leading `/' from hard link targets
/var/folders/30/cmv9c_5j3mq_kthx63sb1t5c0000gn/T/522107804466811630:/code
tar: Removing leading `/' from member names
tar: Removing leading `/' from hard link targets
/var/folders/30/cmv9c_5j3mq_kthx63sb1t5c0000gn/T/6989754788069797291:/etc/feiyan
tar: Removing leading `/' from member names
tar: Removing leading `/' from hard link targets
/var/folders/30/cmv9c_5j3mq_kthx63sb1t5c0000gn/T/7236198221188709503:/var/run/secrets/kubernetes.io/serviceaccount
network mode is container:4edc150aceaa685763e7d380d667eb5a02eb1e69df00768f8b8825c23e93acdd
root@4edc150aceaa:/app#
root@4edc150aceaa:/app# ping -c 4 mysql1a1bb5fc80a6.rds.ivolces.com
PING mysql1a1bb5fc80a6.rds.ivolces.com (10.0.0.32) 56(84) bytes of data.
64 bytes from mysql1a1bb5fc80a6.rds.ivolces.com (10.0.0.32): icmp_seq=1 ttl=62 time=120 ms
64 bytes from mysql1a1bb5fc80a6.rds.ivolces.com (10.0.0.32): icmp_seq=2 ttl=62 time=127 ms
64 bytes from mysql1a1bb5fc80a6.rds.ivolces.com (10.0.0.32): icmp_seq=3 ttl=62 time=50.4 ms
64 bytes from mysql1a1bb5fc80a6.rds.ivolces.com (10.0.0.32): icmp_seq=4 ttl=62 time=54.9 ms

--- mysql1a1bb5fc80a6.rds.ivolces.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3017ms
rtt min/avg/max/mdev = 50.389/88.098/126.673/35.539 ms

这样就可以在此容器中启动自己的项目啦~

root@4edc150aceaa:/app/cmd# alias start='./cmd server --config /etc/feiyan/config.yaml --webide-workspace-id co98bl9mhb1pisov4tc0'
root@4edc150aceaa:/app/cmd# start
Using config file: /etc/feiyan/config.yaml
2024/04/08 19:09:41.483649 cmd.go:59: [Info] FLAG: --config="/etc/feiyan/config.yaml"
2024/04/08 19:09:41.488000 cmd.go:59: [Info] FLAG: --help="false"
2024/04/08 19:09:41.488006 cmd.go:59: [Info] FLAG: --hertz-max-request-body-size="4194304"
2024/04/08 19:09:41.488009 cmd.go:59: [Info] FLAG: --hertz-port="6789"
2024/04/08 19:09:41.488012 cmd.go:59: [Info] FLAG: --hertz-tls="false"
2024/04/08 19:09:41.488017 cmd.go:59: [Info] FLAG: --kitex-port="8888"
2024/04/08 19:09:41.488019 cmd.go:59: [Info] FLAG: --kitex-tls-enable="false"
2024/04/08 19:09:41.488021 cmd.go:59: [Info] FLAG: --log-caller-key="caller"
2024/04/08 19:09:41.488023 cmd.go:59: [Info] FLAG: --log-compress="true"
2024/04/08 19:09:41.488025 cmd.go:59: [Info] FLAG: --log-level="debug"
2024/04/08 19:09:41.488027 cmd.go:59: [Info] FLAG: --log-level-key="level"
2024/04/08 19:09:41.488029 cmd.go:59: [Info] FLAG: --log-max-age="1"
2024/04/08 19:09:41.488033 cmd.go:59: [Info] FLAG: --log-max-backups="3"
2024/04/08 19:09:41.488035 cmd.go:59: [Info] FLAG: --log-max-size="100"
2024/04/08 19:09:41.488037 cmd.go:59: [Info] FLAG: --log-message-key="msg"
2024/04/08 19:09:41.488039 cmd.go:59: [Info] FLAG: --log-path="app.log"
2024/04/08 19:09:41.488041 cmd.go:59: [Info] FLAG: --log-time-key=""
2024/04/08 19:09:41.488054 cmd.go:59: [Info] FLAG: --mysql-conn-max-idle-time="30s"
2024/04/08 19:09:41.488059 cmd.go:59: [Info] FLAG: --mysql-conn-max-life-time="1h0m0s"
2024/04/08 19:09:41.488061 cmd.go:59: [Info] FLAG: --mysql-create-batch-size="1000"
2024/04/08 19:09:41.488063 cmd.go:59: [Info] FLAG: --mysql-database="MYSQL_DB"
2024/04/08 19:09:41.488065 cmd.go:59: [Info] FLAG: --mysql-host="MYSQL_HOST"
2024/04/08 19:09:41.488067 cmd.go:59: [Info] FLAG: --mysql-max-idle-conns="1"
2024/04/08 19:09:41.488068 cmd.go:59: [Info] FLAG: --mysql-max-open-conns="100"
2024/04/08 19:09:41.488070 cmd.go:59: [Info] FLAG: --mysql-password="MYSQL_PASSWORD"
2024/04/08 19:09:41.488072 cmd.go:59: [Info] FLAG: --mysql-port="MYSQL_PORT"
2024/04/08 19:09:41.488074 cmd.go:59: [Info] FLAG: --mysql-username="MYSQL_USERNAME"
2024/04/08 19:09:41.488076 cmd.go:59: [Info] FLAG: --server-ca-file=""
2024/04/08 19:09:41.488078 cmd.go:59: [Info] FLAG: --server-cert-file=""
2024/04/08 19:09:41.488088 cmd.go:59: [Info] FLAG: --server-key-file=""
2024/04/08 19:09:41.488099 cmd.go:59: [Info] FLAG: --webide-git-ignore="[*~,*.swo,*.swp,*.swpx,*.swx,.ccls-cache]"
2024/04/08 19:09:41.488143 cmd.go:59: [Info] FLAG: --webide-log-level="debug"
2024/04/08 19:09:41.488167 cmd.go:59: [Info] FLAG: --webide-max-file-size="10Mi"
2024/04/08 19:09:41.488170 cmd.go:59: [Info] FLAG: --webide-port="8910"
2024/04/08 19:09:41.488198 cmd.go:59: [Info] FLAG: --webide-revision-period="2s"
2024/04/08 19:09:41.488203 cmd.go:59: [Info] FLAG: --webide-root-dir="/code"
2024/04/08 19:09:41.488205 cmd.go:59: [Info] FLAG: --webide-sync-period="2s"
2024/04/08 19:09:41.488207 cmd.go:59: [Info] FLAG: --webide-workspace-id="co98bl9mhb1pisov4tc0"
init db witch config &{0x400032a100}init mysql feiyanadmin:Mirrors79Bio@tcp(mysql1a1bb5fc80a6.rds.ivolces.com:3306)/feiyan?charset=utf8mb4&parseTime=True&loc=Local