Fork me on GitHub

版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://vearne.cc

前言:前段时间在公司内部做了一个分享总结了部分我在使用各种数据库方面的遇到的问题。也在这里分享给大家。强调一下,这里的坑,我是打了引号的,有些坑,不过是某种数据库的特点,或者因为我们错误的事情而引出了问题,并不一定完全就是这种数据库有问题。

1. 业务篇

1)业务场景
不合理的业务设计,永远是对程序员最大的伤痛
在我维护的系统中有这样一种场景,用户要一次性下载全年或者半年的舆情数据,数据量会很大,单个任务就会达到数百万条数据。任何一个系统要在短时间内吞吐数据数百万条记录,也不是件很轻松的事情,尤其当这样的任务很多的时候。
目前这个时间跨度已经被调整成了3个月。说到这里不经让我想到12306错开时间发售火车票。
任何时候从业务角度的优化,总能带来立竿见影的效果
2) 字段设计
在我维护的某个系统中,同一种指标,在不同的表中,被存成了不同的字段名,这给我们带来了巨大的痛苦。所以建议对于同一种指标,或者事物使用同样的字段名(名称)进行表达、存储,否则后期光转换都要人命
3)表结构的反范式设计
大数据场景下,不要受到关系数据库范式设计的太多影响
数据机构能够立体的,尽量立体,不要扁平化
以新浪微博的一条转发举例
一条转发会包含有
1. 这条微博的作者
2. 这条微博的内容 text
3. 原创微博retweeted_status
4. 原创微博的内容 retweeted_status.text
5. 原创微博的作者 retweeted_status.user
...
一条记录就包含了这条转发,以及与这条转发相关的大部分内容,在实际使用时,无需连表查询可以方便的用NoSQL 数据库进行存储

 {
            "created_at": "Tue May 31 17:46:55 +0800 2011",
            "id": 11488058246,
            "text": "求关注。",
            "source": "<a href="http://weibo.com" rel="nofollow">新浪微博</a>",
            "favorited": false,
            "truncated": false,
            "in_reply_to_status_id": "",
            "in_reply_to_user_id": "",
            "in_reply_to_screen_name": "",
            "geo": null,
            "mid": "5612814510546515491",
            "reposts_count": 8,
            "comments_count": 9,
            "annotations": [],
            "user": {
                "id": 1404376560,
                "screen_name": "zaku",
                "name": "zaku",
                "province": "11",
                "city": "5",
                "location": "北京 朝阳区",
                "description": "人生五十年,乃如梦如幻;有生斯有死,壮士复何憾。",
                "url": "http://blog.sina.com.cn/zaku",
                "profile_image_url": "http://tp1.sinaimg.cn/1404376560/50/0/1",
                "domain": "zaku",
                "gender": "m",
                "followers_count": 1204,
                "friends_count": 447,
                "statuses_count": 2908,
                "favourites_count": 0,
                "created_at": "Fri Aug 28 00:00:00 +0800 2009",
                "following": false,
                "allow_all_act_msg": false,
                "remark": "",
                "geo_enabled": true,
                "verified": false,
                "allow_all_comment": true,
                "avatar_large": "http://tp1.sinaimg.cn/1404376560/180/0/1",
                "verified_reason": "",
                "follow_me": false,
                "online_status": 0,
                "bi_followers_count": 215
            },
            "retweeted_status": {
                "created_at": "Tue May 24 18:04:53 +0800 2011",
                "id": 11142488790,
                "text": "我的相机到了。",
                "source": "<a href="http://weibo.com" rel="nofollow">新浪微博</a>",
                "favorited": false,
                "truncated": false,
                "in_reply_to_status_id": "",
                "in_reply_to_user_id": "",
                "in_reply_to_screen_name": "",
                "geo": null,
                "mid": "5610221544300749636",
                "annotations": [],
                "reposts_count": 5,
                "comments_count": 8,
                "user": {
                    "id": 1073880650,
                    "screen_name": "檀木幻想",
                    "name": "檀木幻想",
                    "province": "11",
                    "city": "5",
                    "location": "北京 朝阳区",
                    "description": "请访问微博分析家。",
                    "url": "http://www.weibo007.com/",
                    "profile_image_url": "http://tp3.sinaimg.cn/1073880650/50/1285051202/1",
                    "domain": "woodfantasy",
                    "gender": "m",
                    "followers_count": 723,
                    "friends_count": 415,
                    "statuses_count": 587,
                    "favourites_count": 107,
                    "created_at": "Sat Nov 14 00:00:00 +0800 2009",
                    "following": true,
                    "allow_all_act_msg": true,
                    "remark": "",
                    "geo_enabled": true,
                    "verified": false,
                    "allow_all_comment": true,
                    "avatar_large": "http://tp3.sinaimg.cn/1073880650/180/1285051202/1",
                    "verified_reason": "",
                    "follow_me": true,
                    "online_status": 0,
                    "bi_followers_count": 199
                }
            }
        }

2. hbase 篇

1)无法建立索引
hbase 最大的问题是无法建立索引
两个变象建立索引的办法
1. 利用主键
2. 另外建立一张表提供索引信息
但这两种方法的效率都不是特别高,且需要额外维护数据,很痛苦
当需要数据的时候只好提MR作业,即使真正能够命中的数据只有几条
因此我的结论是,hbase 只适合离线作业,不适合任何在线作业

2)NoSQL 数据库被当成关系数据库使用
在维护的系统中发现了如下的案例,
业务场景是作者A 发布了一篇文章
在hbase中
作者A 的信息被存储在了 table author中
文章信息被存储在 table article 中
这就导致了当我想使用这篇文章的全部信息的时候,我要先从文章表中得到这篇文章,然后再从author表中get出作者信息,整合之后才能完整的吐出来
然后正确的做法应该是将作者信息作为article的一个列族单独存储,这样就避免了跨表访问,当然也有人提出不同的列族存储在不同的文件中,也对性能有影响,但是我个人倾向于对于结构比较复杂的记录,宁肯损失一点性能,也要保证结构的清晰。

3)离散查询
在hbase 内部所有get 操作都会被转换为scan 操作,因此单个get的操作性能很差,如果get操作的量过大(离散查询)hbase的性能会非常差,操作容易出现超时和异常。

事实上hbase的优势在于大批量的scan操作(MR),目前我对hbase的定位和做法是把它当做冷备来使用,我们把冷数据以条为单位转成json字符串保存在hbase上。当需要加载特定时间区间的数据时,再以MR作业的形式导出到热数据库中

3. mongo 篇

1)不加索引查询可能失败
对于mysql数据库,执行一条查询语句,如果没有命中索引,它可能会扫全表,只是速度会非常慢,对于mongo如果执行一条查询语句,并且需要对查询结果进行排序,如果没有命中一条索引,会直接导致程序发生异常,导致程序退出

2)类型过分灵活
mongo 没有对同一个字段进行类型检查,你可以刚开始为某个字段插入整型数据,过会儿再插入float 型数据,完全不会报错。这种事情,你可能能完全不会觉察,除非你使用强类型语言从库中取数。非常大的一个坑。

3)删除数据硬盘空间不释放
在mongo如果删除了某些数据,硬盘空间并不会释放,除非server重新启动
其实不光是mongo,mysql也不会主动释放硬盘空间,除非你用alter操作

4)dump库不带索引信息
使用mongodump dump 整个库时,dump出的文件不包含索引信息,因此如果你在执行数据库迁移时,要特别小心

5)库锁引起并发能力差
mongo 数据库的锁是库粒度的(据说高版本已经有了表粒度的),因此如果有多个任务同时对数据库进行读写操作,性能非常差,据我的观察,普通SATA硬盘只能达到 1k ~ 2k条/s 的写入速度,SSD可以达到1w ~ 2w条/s
使用 mongostat 可以看到库锁情况

mongo库没有固定的表结构,非常灵活,这是它的一个优点,但太灵活也不一定是好事,并且mongo的性能不大好,集群搭建和维护又过于繁琐。不是一个特别好的选择。

4. redis 篇

redis在我心目中一直算是比较稳定的,也不存在特别大的问题
1)Cannot allocate memory
redis 如果开启了持久化,rdb 或者 aopf
rdb 在执行bgsave, aopf 在执行rewrite 时都会导致fork操作。
而fork出的子进程会是父进程的一个copy(包含内存空间),虽然实际的内存消耗是一个copy-on-write,并不真的消耗2倍的内存,但是由于内核无法评估时间的内存消耗,可能在内存申请阶段就直接失败了,具体可以看我的文章http://blog.csdn.net/woshiaotian/article/details/48391387

2) 注意内存打满
redis 的所有数据都保存在内存中,所以使用和监控时要特别注意内存的消耗量
redis内存打满以后,会停止服务,但程序并不崩溃。

对于1)中描述的情况,如果不想修改内核参数,也可以使用网上推荐的一种做法,比如某个机器为24核CPU,64G内存,则启动12个redis实例,每个实例最大分配5GB内存,关闭rdb和aopf,改为由程序定时轮流触发bgsave,这样由于多个实例申请内存的时间不重叠,申请总是能够被满足。

4. elasticsearch 篇

1)mapping 不可修改
某个type的mapping一旦生成就无法修改(新增字段不算),出发删除这个type,重新导入数据

2)float类型和long型数据不能严格检查
在2.0之前的版本,即使mapping中设定了某个字段的类型是long型,仍然可以往这个字段中再插入一个float型的数据,如果只是读,这并没有什么问题,但如果你需要再此字段上做某些聚合操作,可能会引起类型错误。

3)网络抖动引起集群shard allocation
es集群对网络抖动很敏感,如果某个节点暂时不可达,集群会马上认为它已经退出,而进行shard allocation。如果你已经可以预期网络抖动,或者你打算重启某些节点,可以暂时关闭shard allocation

'
curl -XPOST 'http://localhost:9200/_cluster/settings' -d
'
{
    "transient": {
        "cluster.routing.allocation.enable": "none"
    }
}
'

4)cluster master 选举脑裂问题
es 集群的自发现,默认有两种方式一种是多播,一种是单播,多播会导致集群内
无用的探测包增加,且有可能被交换机拦截,因此在生产环境我们建议使用单播模式来实现集群的自发现。

# Elasticsearch, by default, binds itself to the 0.0.0.0 address, and listens
# on port [9200-9300] for HTTP traffic and on port [9300-9400] for node-to-node
# communication. (the range means that if the port is busy, it will automatically
# try the next port).

discovery.zen.minimum_master_nodes。这个参数决定了要选举一个Master需要多少个节点(最少候选节点数)。默认值是1。根据一般经验这个一般设置成 N/2 + 1,即只有保证自己被多数人支持才能申明自己是master
另外我的理解是只有

node.master: true

只有master node才会参与投票,其它节点对master的选举没有帮助。(待证实)

5)并发能力弱
elasticsearch的shard是基于库划分的,对于查询请求,每个请求会被分发给库中的每一个shard,等每一个shard都完成以后,再将结果进行merge,因此,高并发的查询请求会导致性能急剧下降
写入操作由于要进行分词,索引,速度也并不快
我们的测试表明es单个实例(SATA硬盘)写入速度大概为1w 条/min ,所以es的优势在于集群,不在于单个实例,也不建议用它做高频查询,它的最适宜场景还是OLAP

6)单个批次数据量过大
在elasticsearch中,写入操作可以使用index和bulk 2种。bulk 限定的理论大小是100MB,但是在实际中最好别这么干,因为批次过大会导致elasticsearch处理时间过长,引起超时,python的client还会自动进行重试(每次重试自动将超时时间翻倍)。我建议每个批次的条数不超过500条,字节大小不超过10MB,超时时间设置为120秒

7)硬件不一致,性能指标偏差
前面已经讲过对于查询请求,每个请求会被分发给库中的每一个shard,等每一个shard都完成以后,再将结果进行merge。所以如果es集群中机器硬件性能不一致,就观察到有些机器的负载很高,有些则很空闲,另外由于查询结果是每个子查询结果的merge,因此查询速度取决于最慢的机器。

附加我的一点总结

数据库 优缺点 使用场景
hbase 存储量大,无索引 off line
mongo 表结构灵活,集群搭建复杂,并发能力弱 on line
mysql 支持事物,服务稳定 OLTP OLAP
postgreSQL 支持JOSN OLTP OLAP
Elasticsearch 支持JOSN,分布式环境搭建容易 OLTP

PS:虽然这些年诞生了大量的Nosql数据库,但是传统关系型数据库依然有其存在的价值,我们这边使用mysql + SSD 单表就达到了20亿条记录,而看到相关资料sina微博使用mysql + 每日分库分表的办法实现了微博数据的存储。
PS: 最近有个叫kingshard 的mysql中间件,据说不错,大家也看看吧。

推荐几篇好文
http://blog.codingnow.com/2014/03/mmzb_redis.html
http://blog.codingnow.com/2014/03/mmzb_mongodb.html

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注