- 可用性概念
- 服务器可用性
- 进程容错
- 进程容灾
- 系统容灾
- 数据容灾
可用性概念
什么是可用性
可用性指系统在面对异常时可以提供正常服务的能力,异常包括手写代码带入的bug、硬件故障、天灾人祸等, 正常服务要跟生存系统业务的概念有所区分。
![4933701-49e3b2ff5097ddd3.png](https://i-blog.csdnimg.cn/blog_migrate/40d3aad6cd78f6671b80d365ce2d6d28.png)
可用性并不等于易用性,易用性更多指代的是用户UI、用户习惯上的设计 。可用性指的是你所用的服务在你想用的时候它是不是照样可用。
可用性度量
计算机系统的可靠性用平均无故障时间(MTFF)来度量,也就是说计算机平均能够正常运行多长时间,才发生一次故障。系统的可靠性越高,平均无故障时间越长。
可维护性可以用平均维修时间(MTTR)来度量,即系统发生故障后维修和重新恢复正常运行平均花费的时间。系统的可维护性越好,平均修复时间越短。
计算机系统的可用性定义可以定义为
MTTF / (MTTF + MTTR) * 100%
互联网服务的服务定义为
服务总时长 / (服务总时长 + 服务总中断时长) * 100%
可用性级别
![4933701-c780a3c194c0ab1e.png](https://i-blog.csdnimg.cn/blog_migrate/ba4c1a91655651d5475047fc7f84beed.png)
可用性的重要性
经济损失、运营风险、信任建立
服务器可用性
游戏服务器需要什么样的可用性目标呢?你在玩一款新推出的网游,忽然中断了1分钟,第一直觉会认为“刚才网络把我卡掉线了”...
- 中断1分钟:“刚才网络把我卡掉了”
- 中断5分钟:“刚才服务器卡了,我多试几次又好了”
- 中断30分钟:“刚才游戏里恰好是xxx活动,我错过了,要求补偿。”
- 中断数小时:玩家愤怒,打爆客服电话,全服公告补偿,影响收入。
影响业务可用性的因素
![4933701-cffff7b7a455c959.png](https://i-blog.csdnimg.cn/blog_migrate/b4a1e387dc24614c5f7ddf0758fc3a50.png)
灾难问题一般由运维人员关注,但需要软件开发人员提供解决方案。
典型错误类型和影响范围
![4933701-3ed1f745280cbe4b.png](https://i-blog.csdnimg.cn/blog_migrate/c3895327d1433a531752ca544a9d47a6.png)
功能异常通过几轮测试还是蛮容易发现的,但服务器宕机一般都是非常边界的问题引起的,比如空指针的访问、数组的越界、内存的OOM、死循环...
OOM指Out Of Memory,Linux系统自己在发现内存不足的时候,会挑选一些内存占用比较高的进程给杀掉。系统里跑太多耗内存的进程的时候,会产生的一个现象。
软件开发人员为什么要关注硬件错误呢?
硬件故障率
![4933701-48dded3848bf4afe.png](https://i-blog.csdnimg.cn/blog_migrate/58f23064cd1c085ccd2829761fad6283.png)
单台机器的小概率事件随着规模的扩大变成必然事件,要把机器故障当作软件容灾设计中的常规问题。
可用性建设
错误监控、错误容忍、错误恢复
![4933701-ca76ce54d155e097.png](https://i-blog.csdnimg.cn/blog_migrate/db1dd93627c52b4e9fa6ec65673be52c.png)
容灾容错
容灾容错分级
- 进程容错:关注进程内部功能运行的稳定性,主要机制包括功能屏蔽和错误隔离等。
- 进程容灾:关注单个进程或服务整体运行的稳定性,主要机制包括重启、resume、冷热备等。
- 系统容灾:关注多个进程不可用情况下的可用性,主要机制包括冷热备、去单点设计等。
- 数据容灾:关注数据的安全性和可用性,主要机制包括备份、流水日志等。
进程容错
进程容错思路
![4933701-0c5251c9404a8ae6.png](https://i-blog.csdnimg.cn/blog_migrate/aa496af6529edad83c56ba53d577f066.png)
在系统拓扑图中,绿点表示一个功能模块或一个进程, 它在不同层级其实概念上是一样的。此时,如果其中的一个进程出现了故障 (红点)。简单的解决思路就是能不能把它先给屏蔽掉先不提供服务,让别的模块提供正常的服务。
错误隔离原理
# server 主循环
server_proc()
{
while(1)
{
# 处理外界发来的各种请求
while(have_request(request))
hanle_request(request);
# 服务器上的定时器
while(have_timer(timer))
handle_timer(timer);
}
}
服务器的两类驱动源:请求和定时器,对逻辑错误的隔离可转化为对引起的错误的请求和定时器的隔离。
错误隔离系统设计
- 逻辑错误:基于动态开关框架对模块添加的动态开关,可随时关闭开启相关逻辑。
- 数据错误:由于游戏服务器大多将对象数据保存于共享内存中,可见导致一场的驱动源实例或者内存对象通过接口进行隔离。
错误隔离框架
![4933701-b6afbf77a52dbd33.png](https://i-blog.csdnimg.cn/blog_migrate/04cd16ed2b34a951e9c29571760149a9.png)
错误隔离实现示例
# 对用户请求的隔离
int OnPlayerRequest(int player_id, Request &request)
{
if(!request_allowed(request.op_type))
return ERR_OPERATION_NOT_ALLOWED;
else
return request_handler(player_id, request.data);
}
# 对定时器的隔离
void OnTimeout(Timer &timer)
{
if(!timer_allowed(timer.type))
return;
else
timer_handler(timer);
}
# 内存池的访问接口
void *object_get(Object_Handler *handler)
{
if(!object_allowed(handle))
return NULL;
return _obj_get_impl(hanle);
}
记录列表
OP List:
- OP_TYPE_TRADE (item_id)
- OP_TYPE_AUCTION
Timer List:
- TIMER_TYPE_A
- TIMER_TYPE_B
Object List:
- object_handler_1
- object_handler_2
进程容灾
引起进程宕机的原因
- 代码bug:空指针、越界...
- 系统限制:堆栈溢出、内存溢出...
- 人为因素:运维误操作、死循环强杀...
是否杀掉进程后,将进程重启下就OK呢?这个就涉及到进程的有状态和无状态。
例如:有一个叫做GET_SEQ_SRV的服务器,它的作用就是每次调用GET_SEQ_REQ请求的时候返回SEQ_NUMBER,然后这个SEQ_NUMBER不断加一。另外一个服务器叫做ADD_ONE_SVR,它的功能就是给它一个数字COUNT然后返回N+1的结果。这两类服务器的区别在哪里呢?区别在于GET_SEQ_SRV服务器上的SEQ_NUMBER是要始终保持在服务器端的。如果服务器挂掉了,这个SEQ_NUMBER就没有了。而ADD_ONE_SVR服务器,它所有计算的上下文信息都是客户端自己带来的,服务器只是起到了一个计算的作用并吐回一个结果,它是不需要保存 任何的上下文。
![4933701-7157109da846ac7f.png](https://i-blog.csdnimg.cn/blog_migrate/f12409185df12e119c24519c00e960b2.png)
- 有状态的服务器:是需要服务器保存请求处理的上下文信息的
- 无状态的服务器:是请求自己携带处理所需的上下文信息
那么,游戏服务器适合用那种模型呢?其实两类都有。
一种朴素的状态维护方式
![4933701-92480aa422ee885d.png](https://i-blog.csdnimg.cn/blog_migrate/d04bd821a6b8dd565e63c1c35878029f.png)
手游使用较多,它的数据完全是放在可靠存储里面,服务器相当于每次收到客户端请求的时候,它先去可靠存储中把数据读出来,计算完毕返回结果,然后再把数据给存回去。
其优点是健壮性、维护和扩容更加容易。因为把可靠性完全托管给后边的存储模块,服务器自己也就没有所谓的可靠或不可靠了。
其缺点是所有操作需要和存储进行异步交互,编码复杂访 问存储需要额外通信时间,降低了响应速度设计多个数据对象时需要加锁,影响吞吐。
此种方式不适合应用在强交互类的游戏上,只适用于简单的手游上。交互类的游戏不适合这种把可靠性完全往后端存储下沉的方案。
使用共享内存维护状态
共享内存技术是利用Linux的共享内存,Linux系统中有一个叫做shmget的系统调用,它会在kernal内核中分配一块共享内存。然后通过shmat接口可以把这块内存映射到进程的内存空间里。如果有多个进程的话,它是可以同时采取这块内存,也就是说,它的物理存在只是在kernal中有一份,但它在多个进程的地址空间中会各有一份。 更多的情况下,它是拿来做进程间通信用的。
![4933701-1ea422e3157a5e87.png](https://i-blog.csdnimg.cn/blog_migrate/c165bd1f6e798f17ac76cbfbafb912ce.png)
如果不调用shmdt,进程退出后shm仍然保存在内核中。进程重新启动后重新shmat这块内存的技术我们称之为resume。
![4933701-dc78e3dc8be3fe23.png](https://i-blog.csdnimg.cn/blog_migrate/080465a411c493bf0249883020490f07.png)
共享内存数据的组织形式
- 游戏服务器内常见的状态数据:角色信息、队伍信息、帮盟信息、活动信息...
- 数据特点:大小固定、数量固定
共享内存内部数据能怎么组织呢?是使用内存池还是对象池呢?内存池相当于Linux里面的一个堆的管理 ,因为事先不知道所需内存块的大小。要去管理不同大小的内存块的分配,还要去管理分配过程中产生的碎片。对象池的概念事先已经有了大小固定的对象,数量也是固定的,一开始其实就可以分配好。
![4933701-a1f0d3f0bc1b825f.png](https://i-blog.csdnimg.cn/blog_migrate/a8b058d427f0eb93d4b8db66544e67f5.png)
Mempool就是整个共享内存块的一个映射,它里面会根据内存块的类型会分成不同的池子unitpool,每个unitpool里面会记录一些内存块的信息,包括未分配的空闲列表、unit使用数量、单个unit的size...,剩下的空闲内存块以链表的形式,以freelistnode链表的形式给串联起来。
是不是已经万无一失呢?如果resume失败?如果进程所在硬件故障呢?如果进程还是单点呢?
系统容灾
单点
单点是指系统中只存在唯一实例的节点,是获取或维护某些值(决议)的权威方,单点可分为关键路径单点和非关键路径单点。关键路径单点是说少了这个节点的话,整个业务就无法正常地运行了。非关键路径单点是 说少了这个节点无所谓,大不了这部分功能不用了。
![4933701-5fe8054d517394da.png](https://i-blog.csdnimg.cn/blog_migrate/26ecf355ad32aa6a3e2222946b05263f.png)
单点是只存在唯一实例的节点,世界之上的都是属于单点。 世界属于关键路径节点,是必不可少的节点。
针对单点问题的应对策略
- 硬件策略:可用性层级分类、同级部署、冗余部署...
- 软件策略:服务失效(隔离)、主从或冷热备...
![4933701-45770c5b2070049c.png](https://i-blog.csdnimg.cn/blog_migrate/39879ab7882f6e464402f18024487ee2.png)