一 : 浅谈数据库的结构优化
对数据库的优化一直是很多大型网站运营过程中必须应对的重要问题。比如,在2012 年3 月底本人有参与开发了一个A省的省级的政务信息公开发布系统,经过了4个月的功能开发和测试之后,该系统正式上线了,由于该系统采用的是地区分站模式,按照“省-市-县-镇-村”的站点模式,由政府主导,该系统在A省的所 有地区很快就开始推广使用,随着每日发布的政务公开信息逐步增加,结果2个月之后,发现系统曾出现过慢查询的问题。按照初步计算,A省共有15000多个 行政村,1200个镇,100个县与9个市,假设每个村、镇、县、市、省每日发布信息各10条,那么每日的信息发布数量将达到 (15000+1200+100+9+1)*10=163100条数据,那么1个月的数据量将达到163100*30=4893000︾5000000, 那么两个月将达到10000000条,一年将达到上亿条的数据量。面对着这么大的数据量,查询效率也会成为系统的一个性能的瓶颈。
在两个月的时间内,数据库达到千万级的信息条数是一个较大的数据量。特别是在刚开始的1-2个月的过程中,我们还能够在5秒左右的时间范围内对信息进行查询,可是随着信息 量的逐步增加,现在每次的信息汇总和报表都要进行较长的时间等待,最多的达到20多秒。核对了下当初选择的服务器性能配置是没有问题的,那么这个时候我们只有通过优化数据库的结构来提升数据的查询效率了。
数据库结构的优化有多种方法,主要的有两种,一是利用存储过程来代替常用的SQL查询语句;另一种是使用数据库管理系统中的分区表方法进。使用存储过程的优化方法有执行速度快的优点,但是其本身不利于调试、没有办法使用数据库缓存机制等缺点,所以在系统安全性和性能要求更高的情况下,建议使用分区表的方法。但要注意:并不是只要数据量就够多就需要通过数据库分区表来提高查询效率,而是要在数据是分段的前提下,我们才需要考虑到是否需要使用分区表。很显然,结合上述的例子,政务信息公开发布系统,发布的信息是按时间段进行查询的,所以我们可以进行数据库表分区。
数据库的表分区有两种方式,水平表分区和垂直表分区。
水平分区的目的是将一个表分为多个表。每个表包含的列数(表字段)都是相同的,但是记录数(数据行)会减少。比如,我们可以将一个包含1亿行记录的数据库表,按照水平分区的方式,分成12个小表,每个小表分别表示这一年份内从1月到12月的数据。这样,任何需要查询特定月份数据的查询只需查询相应月份的表,而避免从存储在1个大表中的所有月份的数据进行查询。根据SQL语句的执行效率,毫无疑问,从小表中的查询效率会远远高于从大表中查询的效率。垂直分区方式则与水平分区方式相反,从纵向进行分区,是将一个原始表分成多个只包含较少列的表。在日常的应用中,水平分区可以说是最常用的分区方式,所有下面我们以水平分区方式来介绍。
用最简单话说,数据库分区表就是将一个大表分成许多个小表。这里,我们以政务信息公开表为例,该表记录着所有时间段内发布的政务信息,那么我们就可以进行水平分区,把这个大的信息表按时间分成几个小表,假设这里分成10个小表。2012年、2013年、2014年…2021年。那么,如果你想查询哪个年份的记录,就可以去相对应的表里进行检索,由于每个表中的记录数,会比一个大表记录数少了很多,其查询效率也会得到明显的提高。
但我们还应该注意到,如果按照一个大表分成几个小表的处理方式,会给软件开发人员增加代码编程上的负担和难道。以记录增加或修改为例,以上10个表是独立的10个表,在不同时间进行记录增加或修改操作的时候,编程人员需要使用不同的SQL语句。例如在2012年添加记录时,程序员要将记录添加到2012年那个表里; 在2013年添加记录时,程序员要将记录添加到2013年的那个表里。这样,编程人员的工作量会增加,出现程序错误的可能性也会增加。那么这种情况下我们该如何进行水平分区呢?
使用分区表就可以很好的解决上面问题。分区表可以从物理上将一个大表分成 几个小表,但是从逻辑上来看,还是一个大表。分区表可以将一个信息记录表分成若干个物理上的小表,但是对于编程人员而言,他们所面对的依然是一个大表,无论是2012年记录添加还是2013年记录添加,对于编程人员而言是不需要考虑的,他只要将记录插入到信息记录表(逻辑意义上的大表)里就行了。
对应数据库分区表的操作,不同的数据库都有各自创建数据库分区的方法,比如微软的数据库SQL Server就可以实现数据库分区表的操作,通过相关的设置后就可以实现数据库的分区操作。使用SQL Server创建分区表的具体操作步骤可以使用SQL命令,也可以使用数据库本身自带的向导来完整数据库分区操作。
本文由心理学之家http://www.psybook.com 供稿,转载请标明出处!
二 : 浅谈数据库中的存储过程
一、存储过程与函数的区别:
1.一般来说,存储过程实现的功能要复杂一点,而函数的实现的功能针对性比较强。[www.61k.com]
2.对于存储过程来说可以返回参数(output),而函数只能返回值或者表对象。
3.存储过程一般是作为一个独立的部分来执行,而函数可以作为查询语句的一个部分来调用,由于函数可以返回一个表对象,因此它可以在查询语句中位于FROM关键字的后面。
二、存储过程的优点:
1.执行速度更快 – 在数据库中保存的存储过程语句都是编译过的
2.允许模块化程序设计 – 类似方法的复用
3.提高系统安全性 – 防止SQL注入
4.减少网络流通量 – 只要传输存储过程的名称
系统存储过程一般以sp开头,用户自定义的存储过程一般以usp开头
三、定义存储过程语法,"[" 里面的内容表示可选项
create proc 存储过程名
@参数1 数据类型 [=默认值] [output],
@参数2 数据类型 [=默认值] [output],
...
as
SQL语句
四、简单的一个例子
定义存储过程:
create proc usp_StudentByGenderAge
@gender nvarchar(10) [='男'],
@age int [=30]
as
select * from MyStudent where FGender=@gender and FAge=@age
执行存储过程:
Situation One(调用默认的参数):
exec usp_StudentByGenderAge
Situation Two(调用自己指定的参数):
exec usp_StudentByGenderAge '女',50
或者指定变量名 exec usp_StudentByGenderAge @age=50,@gender='女'
对存储过程进行修改
alter proc usp_StudentByGenderAge
@gender nvarchar(10) [='男'],
@age int [=30],
--加output表示该参数是需要在存储过程中赋值并返回的
@recorderCount int output
as
select * from MyStudent where FGender=@gender and FAge=@age
set @recorderCount=(select count(*) from MyStudent where FGender=@gender and FAge=@age)
--output参数的目的,就是调用者需要传递一个变量进来,然后在存储过程中为该变量完成赋值工作,存储过程执行完成以后,将执行的对应结果返回给传递进来的变量。(与C#中的out原理一模一样)
调用(记住这里的语法!)因为该存储过程前面还有其他参数,所以要把 @recorderCount写上,该存储过程执行后,相当与完成了以上的查询工作,同时将查询结果得到的条数赋值给了@count变量。(@count是当做参数传给usp_StudentByGenderAge,当存储过程执行完毕以后,将得到的条数返回给@count)
declare @count int
exec usp_StudentByGenderAge @recorderCount=@count output
print @count
五、使用存储过程完成分页
1、存储过程代码
create proc usp_page
@page int, ---一页显示多少条记录
@number int, ---用户选择了第几页数据
as
begin
select * from
--小括号里面内容是专门得到排列好的序号
(
select ROW_NUMBER() over(order by(Fid)) as number
from MyStudent
) as t
where t.number>= (@number-1)*@page+1 and t.number<=@number*@page
end
2、实现分页效果对应的ADO.NET代码:
1 private void button1_Click(object sender, EventArgs e)
{
2 string connStr = @"server=.\sqlexpress;database=MyDB;integrated security=true";
3 using (SqlConnection conn = new SqlConnection(connStr))
4 {
5 //打开数据库连接
6 conn.Open();
7 //用存储过程名作为Command处理的对象
8 string usp = "usp_page";
9 using (SqlCommand cmd = new SqlCommand(usp, conn))
10 {
11 //执行的是存储过程语句
12 cmd.CommandType = CommandType.StoredProcedure;
//textBox1.Text是指显示多少条记录
13 cmd.Parameters.AddWithValue("@page", textBox1.Text.Trim());
14 //textBox.Text是指用户选择了第几页
15 cmd.Parameters.AddWithValue("@number", textBox2.Text.Trim());
16 //用list作为数据源来实现
17 List<Person> p = new List<Person>();
18 using (SqlDataReader reader = cmd.ExecuteReader())
19 {
20 if (reader.HasRows)
21 {
22 while (reader.Read())
24 {
25 Person p1 = new Person();
26 p1.FName = reader.GetString(1);
27 p1.FAge = reader.GetInt32(2);
28 p1.FGender = reader.GetString(3);
29 p1.FMath = reader.GetInt32(4);
30 p1.FEnglish = reader.GetInt32(5);
31 p.Add(p1);
32 }
33 }
34 }
35 dataGridView1.DataSource = p;
36 }
37 }
38 }
下面是自定义的Person类
View Code
1 class Person
2 {
3 public string FName { get; set; }
4 public int FAge { get; set; }
5 public string FGender { get; set; }
6 public int FMath { get; set; }
7 public int FEnglish { get; set; }
8 }
三 : 谈谈陌陌争霸在数据库方面踩过的坑(Redis篇)
注:陌陌争霸的数据库部分我没有参与具体设计,只是参与了一些讨论和提出一些意见。 在出现问题的时候,也都是由肥龙、晓靖、Aply 同学判断研究解决的。所以我对 Redis 的判断大多也从他们的讨论中听来,加上自己的一些猜测,并没有去仔细阅读 Redis 文档和阅读 Redis 代码。
虽然我们最终都解决了问题,但本文中说描述的技术细节还是很有可能与事实相悖,请阅读的同学自行甄别。
在陌陌争霸之前,我们并没有大规模使用过 Redis 。只是直觉上感觉 Redis 很适合我们的架构:我们这个游戏不依赖数据库帮我们处理任何数据,总的数据量虽然较大,但增长速度有限。由于单台服务机处理能力有限,而游戏又不能分服, 玩家在任何时间地点登陆,都只会看到一个世界。
所以我们需要有一个数据中心独立于游戏系统。而这个数据中心只负责数据中转和数据落地就可以了。Redis 看起来就是最佳选择,游戏系统对它只有按玩家 ID 索引出玩家的数据这一个需求。
我们将数据中心分为 32 个库,按玩家 ID 分开。不同的玩家之间数据是完全独立的。在设计时,我坚决反对了从一个单点访问数据中心的做法,坚持每个游戏服务器节点都要多每个数据仓库直接连接。因为在这里制造一个单点毫无必要。
根据我们事前对游戏数据量的估算,前期我们只需要把 32 个数据仓库部署到 4 台物理机上即可,每台机器上启动 8 个 Redis 进程。一开始我们使用 64G 内存的机器,后来增加到了 96G 内存。实测每个 Redis 服务会占到 4~5 G 内存,看起来是绰绰有余的。
由于我们仅仅是从文档上了解的 Redis 数据落地机制,不清楚会踏上什么坑,为了保险起见,还配备了 4 台物理机做为从机,对主机进行数据同步备份。
Redis 支持两种 BGSAVE 的策略,一种是快照方式,在发起落地指令时,fork 出一个进程把整个内存 dump 到硬盘上;另一种唤作 AOF 方式,把所有对数据库的写操作记录下来。我们的游戏不适合用 AOF 方式,因为我们的写入操作实在的太频繁了,且数据量巨大。
第一次事故出在 2 月 3 日,新年假期还没有过去。由于整个假期都相安无事,运维也相对懈怠。
中午的时候,有一台数据服务主机无法被游戏服务器访问到,影响了部分用户登陆。在线尝试修复连接无果,只好开始了长达 2 个小时的停机维护。
在维护期间,初步确定了问题。是由于上午一台从机的内存耗尽,导致了从机的数据库服务重启。在从机重新对主机连接,8 个 Redis 同时发送 SYNC 的冲击下,把主机击毁了。
这里存在两个问题,我们需要分别讨论:
问题一:从机的硬件配置和主机是相同的,为什么从机会先出现内存不足。
问题二:为何重新进行 SYNC 操作会导致主机过载。
问题一当时我们没有深究,因为我们没有估算准确过年期间用户增长的速度,而正确部署数据库。数据库的内存需求增加到了一个临界点,所以感觉内存不足 的意外发生在主机还是从机都是很有可能的。从机先挂掉或许只是碰巧而已(现在反思恐怕不是这样, 冷备脚本很可能是罪魁祸首)。早期我们是定时轮流 BGSAVE 的,当数据量增长时,应该适当调大 BGSAVE 间隔,避免同一台物理机上的 redis 服务同时做 BGSAVE ,而导致 fork 多个进程需要消耗太多内存。由于过年期间都回家过年去了,这件事情也被忽略了。
问题二是因为我们对主从同步的机制了解不足:
仔细想想,如果你来实现同步会怎么做?由于达到同步状态需要一定的时间。同步最好不要干涉正常服务,那么保证同步的一致性用锁肯定是不好的。所以 Redis 在同步时也触发了 fork 来保证从机连上来发出 SYNC 后,能够顺利到达一个正确的同步点。当我们的从机重启后,8 个 slave redis 同时开启同步,等于瞬间在主机上 fork 出 8 个 redis 进程,这使得主机 redis 进程进入交换分区的概率大大提高了。
在这次事故后,我们取消了 slave 机。因为这使系统部署更复杂了,增加了许多不稳定因素,且未必提高了数据安全性。同时,我们改进了 bgsave 的机制,不再用定时器触发,而是由一个脚本去保证同一台物理机上的多个 redis 的 bgsave 可以轮流进行。另外,以前在从机上做冷备的机制也移到了主机上。好在我们可以用脚本控制冷备的时间,以及错开 BGSAVE 的 IO 高峰期。
第二次事故最出现在最近( 2 月 27 日)。
我们已经多次调整了 Redis 数据库的部署,保证数据服务器有足够的内存。但还是出了次事故。事故最终的发生还是因为内存不足而导致某个 Redis 进程使用了交换分区而处理能力大大下降。在大量数据拥入的情况下,发生了雪崩效应:晓靖在原来控制 BGSAVE 的脚本中加了行保底规则,如果 30 分钟没有收到 BGSAVE 指令,就强制执行一次保障数据最终可以落地(对这条规则我个人是有异议的)。结果数据服务器在对外部失去响应之后的半小时,多个 redis 服务同时进入 BGSAVE 状态,吃光了内存。
花了一天时间追查事故的元凶。我们发现是冷备机制惹的祸。我们会定期把 redis 数据库文件复制一份打包备份。而操作系统在拷贝文件时,似乎利用了大量的内存做文件 cache 而没有及时释放。这导致在一次 BGSAVE 发生的时候,系统内存使用量大大超过了我们原先预期的上限。
这次我们调整了操作系统的内核参数,关掉了 cache ,暂时解决了问题。
经过这次事故之后,我反思了数据落地策略。我觉得定期做 BGSAVE 似乎并不是好的方案。至少它是浪费的。因为每次 BGSAVE 都会把所有的数据存盘,而实际上,内存数据库中大量的数据是没有变更过的。一目前 10 到 20 分钟的保存周期,数据变更的只有这个时间段内上线的玩家以及他们攻击过的玩家(每 20 分钟大约发生 1 到 2 次攻击),这个数字远远少于全部玩家数量。
我希望可以只备份变更的数据,但又不希望用内建的 AOF 机制,因为 AOF 会不断追加同一份数据,导致硬盘空间太快增长。
我们也不希望给游戏服务和数据库服务之间增加一个中间层,这白白牺牲了读性能,而读性能是整个系统中至关重要的。仅仅对写指令做转发也是不可靠的。因为失去和读指令的时序,有可能使数据版本错乱。
如果在游戏服务器要写数据时同时向 Redis 和另一个数据落地服务同时各发一份数据怎样?首先,我们需要增加版本机制,保证能识别出不同位置收到的写操作的先后(我记得在狂刃中,就发生过数据版本错 乱的 Bug );其次,这会使游戏服务器和数据服务器间的写带宽加倍。
最后我想了一个简单的方法:在数据服务器的物理机上启动一个监护服务。当游戏服务器向数据服务推送数据并确认成功后,再把这组数据的 ID 同时发送给这个监护服务。它再从 Redis 中把数据读回来,并保存在本地。
因为这个监护服务和 Redis 1 比 1 配置在同一台机器上,而硬盘写速度是大于网络带宽的,它一定不会过载。至于 Redis ,就成了一个纯粹的内存数据库,不再运行 BGSAVE 。
这个监护进程同时也做数据落地。对于数据落地,我选择的是unqlite,几行代码就可以做好它的 Lua 封装。它的数据库文件只有一个,更方便做冷备。当然levelDB也是个不错的选择,如果它是用 C 而不是 C++ 实现的话,我会考虑后者的。
和游戏服务器的对接,我在数据库机器上启动了一个独立的 skynet 进程,监听同步 ID 的请求。因为它只需要处理很简单几个 Redis 操作,我特地手写了 Redis 指令。最终这个服务只有一个 lua 脚本,其实它是由三个 skynet 服务构成的,一个监听外部端口,一个处理连接上的 Redis 同步指令,一个单点写入数据到 unqlite 。为了使得数据恢复高效,我特地在保存玩家数据的时候,把恢复用的 Redis 指令拼好。这样一旦需要恢复,只用从 unqlite 中读出玩家数据,直接发送给 Redis 即可。
有了这个东西,就一并把 Redis 中的冷热数据解决了。长期不登陆的玩家,我们可以定期从 Redis 中清掉,万一这个玩家登陆回来,只需要让它帮忙恢复。
晓靖不喜欢我依赖 skynet 的实现。他一开始想用 python 实现一个同样的东西,后来他又对 Go 语言产生了兴趣,想借这个需求玩一下 Go 语言。所以到今天,我们还没有把这套新机制部署到生产环境。
原文地址。51CTO获作者授权转载。
四 : 谈谈陌陌争霸在数据库方面踩过的坑(mongoDB篇)
我们公司开始用 mongoDB 并不是因为开始的技术选型,而是我们代理的第一款游戏《狂刃》的开发商选择了它。
这款游戏在我们代理协议签订后,就进入了接近一年的共同开发期。期间发现了很多和数据库相关的问题,迫使我们熟悉了 mongoDB 。在那个期间,我们搭建的运营平台自然也选择了 mongoDB 作为数据库,这样维护人员就可以专心一种数据库了。
经过一些简单的了解,我发现国内很多游戏开发者都不约而同的采用了 mongoDB ,这是为什么呢?我的看法是这样的:
游戏的需求多变,很难在一开始就把数据结构设计清楚。而游戏领域的许多程序员的技术背景又和其他领域不同。
在设计游戏服务器前,他们更多的是在设计 游戏的客户端:画面、键盘鼠标交互、UI 才是他们花精力最多的地方。对该怎么使用数据库没有太多了解。这个时候,出现了 mongoDB 这样的 NOSQL 数据库。mongoDB 是基于文档的,不需要你设计数据表,和动态语言更容易结合。看起来很美好,你只需要把随便一个结构的数据对象往数据库里一塞,然后就祈祷数据库系统会为你搞定其它的事情。如果数据库干的不错,性能不够,那是数据库的责任,和我无关。看到那些评测数据又表明 mongoDB 的性能非常棒,似乎没有什么可担心的了。
其实无论什么系统,在对性能有要求的环境下,完全当黑盒用都是不行的。
游戏更是如此。上篇我就谈过,我们绝对不可能把游戏里数据的变化全部扔到数据库中去做。传统数据库并非为游戏设计的。
比如,你把一群玩家的坐标同步到数据库,能够把具体某个玩家附近玩家列表查询出来么?mongoDB 倒是提供了 geo 类型,可以用 near 或 within 指令查询得到附近用户。可他能满足 10Hz 的更新频率么?
我们可以把玩家的 buf 公式一一送入数据库,然后修改一些属性值,就可以查询到通过 buf 运算得到的结果么?
这类问题有很多,即使你能找到方法让数据库为你工作,那么性能也是堪忧的。当我们能在特定的数据库服务内一一去解决她们,最终数据库就是一个游戏服务器了。
狂刃这个项目在我们公司是负责平台建设的蜗牛同学跟的。我从他那里听来了许多错误使用 mongoDB 的趣闻。
一开始,整个数据库完全没有为查询建索引。在没什么数据的情况下,即使所有的查询都是 O(N) 的,遍历整个数据库,也不会有问题。可想而知,用户量一上来,性能会下降的多快。
然后,数据库又被建立了大量的无用的索引,和一些错误的复合索引,同样恶化了系统。感觉就是哪里似乎有点性能问题,那就是少了个索引的缘故。这种病 急乱投医的现象,在项目开发后期很容易出现。其实解决方法很简单:主导设计的人只要静下心来好好想一想,数据库系统其实也就是一个管理数据的封闭模块。如 果你来管理这些数据,怎样的数据结构更利于满足特定的检索,需要哪些索引数据辅助。
最终的问题依旧是算法和数据结构,不同的是,不需要你实现它,而需要你理解它。
另外,数据库是被设计成可以并发访问的,而并发永远是复杂的东西。mongoDB 缺乏事务操作,需要用文档操作的原子性来模拟。这很容易被没经验的人用错(这是个怪圈,越是没数据库经验的人越喜欢 mongoDB ,因为限制少,看起来更自然。)。
狂刃出过这样一个 bug :想让用户注册的时候用户名唯一,所以在用户注册的时候先查一下数据库看用户名是否存在,如果不存在就允许创建一个这个名字的用户。可想而之,上线运营不出一天,同名用户就会出现了。
因为公司项目需要,我给 skynet 增加了 mongo driver 。老实说,实现这个 driver 的时候,我对 mongo 就兴趣寥寥。最后只实现了最底层的通讯协议,光这个部分,它的协议设计就已经是很难看的了。但是即使这样,我也耐着性子把这部分做完,而不想使用现成的 driver 。
mongo 的官方 driver 都是内置 socket 通讯模块的。这种做法很难单独把协议解析部分提取出来,附加到自己项目的 IO 模型中去。(btw, redis 这方面就好的多,因为它的协议足够简单,你可以用几十行代码就实现它的通讯协议,而不需要依赖 driver 模块。)
狂刃服务器的 IO 采用的 boost.asio ,我很好奇他是怎样把 mongoDB 官方 C++ driver 整合进去的。不出所料,他们开了一个独立线程处理 mongo 的数据,然后把数据对象跨线程发出来。细究这个实现就能看出问题来。程序员很容易误解 mongoDB client api 的内在含义。
一开始,狂刃的开发同学以为从 mongo 中取到一组查询结果后,调用 cursor 的 findnext 只在对象内存中迭代,所有结果都是一开始一次性返回的。以为把一开始的 bson 对象从 mongo 线程转移到主线程中就好了。可事实并不是这样,mongo 一次只会返回一组查询结果,当结果迭代完时,findnext 还会自动提交新的查询请求。这时,对象已经不在原有的 mongo 线程中了。
学过 C++ 的同学可以想像一下,让你去 code review 不是你参于的 C++ 项目去找到 bug 需要多少功夫?对了,你还要在想像中要加上被各种 boost.asio 回调函数拆得支离破碎的业务流程。所以去年有那么一段日子,我们需要完全停下手头其他的工作,认真的从头阅读那数以万行计的 C++ 代码。
老八卦别人似乎不太厚道,下面来谈谈我们自己犯的错误。
陌陌争霸出的第一起服务器事故是在 2014 年一月中旬的一个周末。准确说,这次算不上重大运营事故,因为没有玩家数据受损,也没有意外停服。但却是我们第一次发现早先设计中有考虑不足的地方。
1 月 12 日周日。下午 17 点左右,我们的 SA Aply 发现我们运营用的 log 延迟了 3 个小时才到运营平台。但数据还是源源不断的进入,系统也很稳定,就没有特别深究。
到了晚上 20 点半,平台组的刘阳报告说运营数据已经延迟了 5 个小时了,这才引起了大家的警觉。由于是周末,开发人员都回家休息了,晓靖 21 点上线检查,这时发现游戏服务器内存占用比平常同期高了 10G 之多,并在持续上升。
我大约是在 21 点接到电话的,在电话中讨论分析了一下,觉得是 log 数据从 skynet 的 log 服务发走,可能被积压在 socket server 的一个链表上。这段代码并不复杂,插入新的写入数据是 O(1) 操作,所以没有阻塞玩家游戏的风险。而输出 log 的频率还不至于短期把所有内存吃光。游戏服务器暂时是安全的。
晚21点40分,虽然没能分析出事故的源头,但我们立刻采取了应急方案。重新启动了一套游戏服务器,在线将旧服务器上的 80% 玩家导到新的备用服务器上。并同时启动了新的 log 数据库集群。打算挺到周一再在固定维护时间处理。
晚 23 点,新启动的游戏服务器也出现了 log 输出延迟。因为运营 log 是输出到一个 mongos 管理的集群中的,我们尝试在旧的集群(已无新数据写入,但依旧没有消化完滞留的旧数据)做了删除部分索引的尝试,没有什么效果。
凌晨 0:45 ,开启了新的备机群,取消了 mongos ,让每台机器独立连接一个单独的 mongoDB ,情况终于好转了。
以上,是当时事故记录的节选。
彻底搞明白事故起源是周二的事情了。
表面上看起来是在 mongos 服务上堆积了大量的数据库插入操作。让这个单点过载了。我们起初的运营 log 输出是有点偏多,比如每个士兵的训练都有一条单独的 log ,而陌陌争霸游戏中这种 log 是巨量的。我们裁减并精简了一部分 log 但似乎并不能从根本上解释这起事故。
问题出在 mongos 的 shard key 的选择上。mongo 可以指定 document 的若干字段为 shard key ,mongos 把这个 key 当成一个整数,按整数区间把 document 分成若干个桶。再把桶均匀分配到背后的从机上。
如果你的 key 是有规律的数字,而你又需要这种规律不至于破坏桶分配的公平性,你还可以将一个 hash 算法应用于原始选择的 key 上,让 key 足够散列开。我们一开始就是按自增 id 的散列结果做 key 的。
错误的 shard key 选择就是这起事故的罪魁祸首。
因为我们是大量的顺序写操作,应该优先保证写入的流畅。如果用随机散列的方式去看待这些 document 的话,新旧 log 就很大几率被分配到一起。而 mongo 并不是一条一个单位将数据落地的,而是一块块的进行。这种冷热数据的交织会导致写盘 IO 量远远大于 log 实际的输出量。
最后我们调整了 shard key ,按 log 时间和自增 id 分开,就把 mongo 数据落地的 IO 量下降了几个数量级。
看吧,理解系统如何工作的很重要。
ps, 这起事故后,我给 skynet 加了更多的监控,方便预警单个模块的过载。这帮助我们更快的定位后面出现的问题。那些关于 redis 的故事,且听下回分解。
3 月 5 日补充:
根据下面的留言讨论,总结一下:
关于 shard key 的选择在 mongoDB 文档中被讨论过。但和我们遇到的情况有所不同。
有同学提到,这篇文章里描述在批量写入的时候,数字做 key 要比 hash 过的有更高的效率。
我们没有使用批量插入,而我们是单条逐条插入的。所以性能低下并不在于逐条调用 getLastError ,我们为了保证写入性能,都是单向推送,不获取 getLastError 的(最低 Write concern 级别)。我认为在我们的业务情况下,按时间片让一台机器接受一组数据是更好的利用方式。
原文地址。51CTO获作者授权转载。
本文标题:浅谈数据库连接-浅谈数据库的结构优化61阅读| 精彩专题| 最新文章| 热门文章| 苏ICP备13036349号-1