记录一下人生第一次高并发实战。

前情回顾 链接到标题

停办了两年的精弘毅行在疫情结束后的第一个秋天如约而至,而大家的热情似乎没有被这暂停的两年冲淡,宣传推送以极快的速度突破了一万阅读量,那几天朋友圈里连续被这消息刷屏了好几天,好不热闹。

但这样高热度的背后是我作为技术总监的担忧,我们作为一个本科生技术社团,自主开发的报名系统真的可以顶得住那么大的流量吗?如果顶不住那么备选方案是什么,我们还配得上全工大顶尖技术的代名词吗?

但无论如何,作为技术的掌门人,我得办好这件事。

前期准备 链接到标题

高并发的情况下会遇到如下问题:

  • 系统资源不足:高并发会占用系统的CPU、内存、磁盘IO等资源,当请求量超过系统资源的极限时,系统会出现响应变慢、甚至崩溃的情况。
  • 系统性能下降:高并发时,系统需要同时处理大量的请求,这可能会导致系统的性能下降,响应时间变长,甚至出现死锁、卡顿等问题。
  • 数据不一致:在高并发的情况下,多个请求同时访问同一份数据,如果没有进行有效的并发控制,可能会导致数据的不一致,比如出现重复提交、脏数据等问题。
  • 安全性问题:高并发时,系统容易受到恶意攻击,比如DDoS攻击、SQL注入等,这可能会导致系统瘫痪、数据泄露等问题。

因此为了本次报名,我们专门准备了一台 4c8g20m 的服务器来部署报名系统的前后端,而因为这台机器在政企机房内,我错误估计了安全风险,导致了后面出现了一个巨大的问题,但现在这里卖个关子。

我们总体要解决的问题就是 系统资源不足数据不一致,下面是我的解决方案:

系统资源不足 链接到标题

系统资源总是由一些阈值来控制上限的,而这一阈值是需要一些经验来调节的,因为这些阈值过小或过大都会导致服务效率的降低,甚至是拒绝服务或大量资源泄漏浪费,因此选择一个恰当的阈值变得十分重要了。

好在毅行并不是第一年进行,我在查阅了历届的报名数据与前辈留下的总结经验后作出了选择:将 MySQL 的最大连接数改为 900,将系统句柄上限改为 65535。

数据不一致 链接到标题

这一问题我们选择通过减少对数据库的访问来避免。

正式报名抢限量名额前的注册与组队的业务流量我们预期是分散的,我们也有意在各大宣传平台上发布这样的消息,让大家避免在同一时间涌入系统。

因此这些业务直接操作数据库是几乎没有风险的(但是最后打脸打的很痛),甚至每个功能都只需要用到一条 SQL 语句,连事物都不需要使用到。

正式报名是重中之重,是必定会出现短时间海量流量涌入的,直接操作数据库变得十分危险了。所以我们的选择是将报名的数据全部存储在 Redis 上,利用其内存存储的特性,更快更稳定的处理这些对于数据库来说海量,但对内存来说可以拿捏的流量。然后在我们规划的后半夜系统休眠时期将内存中的数据同步到磁盘中实现持久化存储,事实证明这十分有效。

正式实战 链接到标题

前面分析了一大串,以为手拿把攥了,等真的到了实战阶段,发现自己真是个二百五。

系统正式对外开放的第一个小时就挂了,当时的我还在上课,看到进程留下的最后一句话是 Too many open files in system,就决定调大一些句柄阈值再试试,没想到这一试就是一节课隔三四分钟重启一次服务的痛苦人肉运维。

此时的流量已经来到了惊人的瞬时 900k。 img1

等到我第一节下课,转移阵地去勤工办公室之后,发现服务器的 ssh 密码被人改了,我只能看着进程因为同样的原因挂了而做不了任何事。最终只得通过控制面板重装系统,一切环境重新来过。

重装系统后的第一件事就是关闭密码登陆,只允许我自己的机器通过密钥登陆,这才让我有了些许安全感,接着我就想办法通过 nginx 的设置进行限流,试图降低瞬时的句柄占用量,但可惜这一举措只能延后句柄耗尽,且会导致用户的使用感受十分恶劣(限流会导致一部分用户只能看到 nginx 的白色限流页,无法正确进入系统获取前端页面)。

深夜 debug 链接到标题

在经历了半天的人肉运维后,我决定发动部门内广大技术宅的力量,一起 debug。

在一顿头脑风暴后我们发现业务进程会随着处理的请求数量上升同步产生大量的 inotify 句柄,且数量只增不减。

img3

在查阅资料后我们得知,之前的句柄耗尽并非是进程打开的所有文件数量太多导致的,而是进程占用的 inotify 句柄数量达到阈值导致的,那么接下来的问题就是找到产生这么多 inotify 句柄的原因,然后解决就好。

顺藤摸瓜我们发现在配置文件解析库 viper 中每次 new 一个 watcher 对配置文件的改动进行监控时,会调用 fsnotify 库生成一个 watcher,而在 fsnotify 库中会根据运行环境调用系统 api 生成获取一个对应的监控子进程,而在 Ubuntu 下该监控子进程正是 inotify。

viper 库下对应的代码: img4

GitHub库

fsnotify库下对应的代码: img5

GitHub库

而我们的开发者在开发时为了实现配置文件的热更改,在每次进行数据库操作之前都要初始化一次配置信息,而初始化会调用上文的 WatchConfig,于是每次对数据库的操作都会产生一个新的 inotify 句柄,长此以往就会导致句柄池耗尽。

尾声 链接到标题

这个 bug 被解决后我们迎来了被推后的第一天报名,热度甚至因为之前的服务挂了被宣传而更加高了,不过这次服务就安安稳稳的完成了,大家两秒内抢空了最热门的屏峰全程名额。

但哪怕这样,瞬间流量也没有再追平前一次的结果,纠结有没有人恶意攻击我们的服务器,又是怎样攻击的,他到底是谁?这一切谜题的答案可能就只有当事人才知晓了。

img2