写了一个简单的服务器端程序,用于校验用户及下发一些重要数据,用在自己的小软件上运行了好几年一直都很稳定。以前同事写的一个小软件也要用到账号校验及下发数据,给他搭了一套,最初还比较稳定,没怎么出问题,后来时不时的就会崩溃,然后增加了守护进程,当崩溃时自动重启,增加了崩溃时写dump文件。从其中一次崩溃的dump文件中分析是传给数据加密函数的秘钥无效了,此参数从上到下都是引用传递,那唯一可能的是保存此秘钥的用户数据被释放了,结合实际的使用场景,存在大量短时间顶掉已登录账号再次登录的情况,可以确定问题就在这里,前面一次登录完成,正在准备下发数据,结果被后面一次登录给顶掉了,把用户数据释放掉了,导致准备下发数据时的加密函数使用的秘钥参数被释放了,然后程序崩溃。

这个问题的直接原因是因为业务处理逻辑放在了多个线程中执行,自己的软件使用场景单一,几乎不存在顶掉已登录账号的使用情况,所以一直很稳定没有暴露问题。底层使用的是自己封装的基于完成端口的一套网络库,直接把回调函数放到了多个数据收发线程中执行,导致业务处理逻辑部分各种加锁,结果还没锁住崩溃了。服务器处理大并发的瓶颈在IO方面,不在数据处理方面,这种直接把回调函数放在网络层数据收发线程中进行处理,实际上是有很大问题的,影响了网络层IO的处理效率,而且业务逻辑在多个线程中处理也很难维护数据。

直接在网络层加了一个数据处理线程,处理业务逻辑的回调函数直接在数据处理线程中执行,所有网络IO处理线程中把事件及数据送给数据处理线程的队列,这样上层业务逻辑代码不用修改,直接更新网络库,就使得业务逻辑在单线程中处理。修改后的程序运行一直很稳定,没有出现过崩溃。

总结:
1、处理大并发的网络IO线程里不要执行任何业务逻辑代码,只处理IO收发,把数据抛给专门的线程进行处理;
2、位于网络层之上的第一层业务处理逻辑解包使用单线程处理,业务简单可直接在此线程处理,业务复杂的可以根据实际业务情况把包分到其他线程处理;

 

2020年4月5日 程序崩溃后续:

把数据处理修改为单线程后,稳定运行了一段时间,又发生了程序崩溃,从dump文件分析仍然是崩溃在同一个地方,前面还显示数据有效,作为参数传入函数内部时,显示参数无效,反复把这里的代码看了多遍,仍然无法找到代码里的问题,无论怎么看都不可能在这个地方崩溃,十分的费解。又回头把网络库的代码反复看了几遍,确认所有网络数据都是转发到单独的数据处理线程中进行处理的。实在是没有招了,只能按有可能是存在多个线程在处理网络数据的猜测来分析了,针对数据处理增加了线程ID、数据地址、SOCKET地址等日志,便于后续崩溃分析定位问题。把新加日志版本的服务器端程序更新到服务器上进行运行,期待它再次崩溃。

大概稳定运行了15天左右,崩溃再次发生了,通过dump文件分析,仍然是和以前崩溃在同一个地方,不过这次不同的是之前显示无效认为可能导致崩溃的参数,这次在函数内部显示是有效的,反而另外一个肯定不会被释放的参数显示为无效了。继续分析日志文件,数据处理确实都是在同一个线程中处理的,不存在其他线程清除参数的问题,而且崩溃时此玩家也是首次登录,也不存在顶掉已登录帐号的问题。查看此玩家数据,竟然和上次崩溃时登录的用户名字惊人的相似,前面名字相同,后面数字后缀不同,可以推断出是同一个用户的不同的账号,接着分析此用户所有帐号登录日志,没有发现任何异常。回头看这次崩溃的位置,解析加密密钥异常导致的崩溃,而且密钥参数显示有效,那么只能是密钥数据本身出了问题。参数是密钥文件路径,这里可以确定是由文件解析密钥失败的崩溃。

密钥在客户端生成,先写入临时文件,使用时从文件中读取数据上传给服务器,服务器把密钥写入临时文件,数据加密时从临时文件读取密钥使用。前后两次都崩溃在同一个用户的账号登录上,可以猜测是这个用户的机器上写密钥到临时文件或从临时文件读取密钥数据这两个步骤中出了问题,上传给服务器的密钥数据错误,由于服务器使用密钥时未做异常处理,导致了程序崩溃。这里大概率应该是这个用户的临时文件夹所在的磁盘分区出了问题,写文件后再读文件,数据不一致。

把以文件方式使用密钥修改成以内存数据方式使用密钥,在加密解密相关函数内部增加异常处理,确保即使密钥数据错误也不会发生程序崩溃,由于密钥数据来自客户端不可控。

总结:
1、dump文件分析函数参数显示无效,应该是因为崩溃时把栈破坏了,导致显示的不准确;
2、对于使用开源库相关代码,尽量加上异常处理,防止异常崩溃;
3、客户端使用文件方式存放重要数据时,最好做一个数据完整校验,确保读入的与写入的一致;