BT

又拍网架构中的分库设计

作者 周兆兆 发布于 2010年6月11日 | 被首富的“一个亿”刷屏?不如定个小目标,先把握住QCon上海的优惠吧!

又拍网是一个照片分享社区,从2005年6月至今积累了260万用户,1.1亿张照片,目前的日访问量为200多万。5年的发展历程里经历过许多起伏,也积累了一些经验,在这篇文章里,我要介绍一些我们在技术上的积累。

又拍网和大多数Web2.0站点一样,构建于大量开源软件之上,包括MySQLPHPnginxPythonmemcachedredisSolrHadoopRabbitMQ等等。又拍网的服务器端开发语言主要是PHPPython,其中PHP用于编写Web逻辑(通过HTTP和用户直接打交道), 而Python则主要用于开发内部服务和后台任务。在客户端则使用了大量的Javascript, 这里要感谢一下MooTools这个JS框架,它使得我们很享受前端开发过程。 另外,我们把图片处理过程从PHP进程里独立出来变成一个服务。这个服务基于nginx,但是是作为nginx的一个模块而开放REST API。

开发语言

图1:开发语言

由于PHP的单线程模型,我们把耗时较久的运算和I/O操作从HTTP请求周期中分离出来, 交给由Python实现的任务进程来完成,以保证请求响应速度。这些任务主要包括:邮件发送、数据索引、数据聚合和好友动态推送(稍候会有介绍)等等。通常这些任务由用户触发,并且,用户的一个行为可能会触发多种任务的执行。 比如,用户上传了一张新的照片,我们需要更新索引,也需要向他的朋友推送一条新的动态。PHP通过消息队列(我们用的是RabbitMQ)来触发任务执行。

PHP和Python的协作

图2:PHP和Python的协作

数据库一向是网站架构中最具挑战性的,瓶颈通常出现在这里。又拍网的照片数据量很大,数据库也几度出现严重的压力问题。 因此,这里我主要介绍一下又拍网在分库设计这方面的一些尝试。

分库设计

和很多使用MySQL的2.0站点一样,又拍网的MySQL集群经历了从最初的一个主库一个从库、到一个主库多个从库、 然后到多个主库多个从库的一个发展过程。

数据库的进化过程

图3:数据库的进化过程

最初是由一台主库和一台从库组成,当时从库只用作备份和容灾,当主库出现故障时,从库就手动变成主库,一般情况下,从库不作读写操作(同步除外)。随着压力的增加,我们加上了memcached,当时只用其缓存单行数据。 但是,单行数据的缓存并不能很好地解决压力问题,因为单行数据的查询通常很快。所以我们把一些实时性要求不高的Query放到从库去执行。后面又通过添加多个从库来分流查询压力,不过随着数据量的增加,主库的写压力也越来越大。

在参考了一些相关产品和其它网站的做法后,我们决定进行数据库拆分。也就是将数据存放到不同的数据库服务器中,一般可以按两个纬度来拆分数据:

垂直拆分:是指按功能模块拆分,比如可以将群组相关表和照片相关表存放在不同的数据库中,这种方式多个数据库之间的表结构不同

水平拆分:而水平拆分是将同一个表的数据进行分块保存到不同的数据库中,这些数据库中的表结构完全相同

拆分方式

一般都会先进行垂直拆分,因为这种方式拆分方式实现起来比较简单,根据表名访问不同的数据库就可以了。但是垂直拆分方式并不能彻底解决所有压力问题,另外,也要看应用类型是否合适这种拆分方式。如果合适的话,也能很好的起到分散数据库压力的作用。比如对于豆瓣我觉得比较适合采用垂直拆分, 因为豆瓣的各核心业务/模块(书籍、电影、音乐)相对独立,数据的增加速度也比较平稳。不同的是,又拍网的核心业务对象是用户上传的照片,而照片数据的增加速度随着用户量的增加越来越快。压力基本上都在照片表上,显然垂直拆分并不能从根本上解决我们的问题,所以,我们采用水平拆分的方式。

拆分规则

水平拆分实现起来相对复杂,我们要先确定一个拆分规则,也就是按什么条件将数据进行切分。 一般2.0网站都以用户为中心,数据基本都跟随用户,比如用户的照片、朋友和评论等等。因此一个比较自然的选择是根据用户来切分。每个用户都对应一个数据库,访问某个用户的数据时, 我们要先确定他/她所对应的数据库,然后连接到该数据库进行实际的数据读写。

那么,怎么样对应用户和数据库呢?我们有这些选择:

按算法对应

最简单的算法是按用户ID的奇偶性来对应,将奇数ID的用户对应到数据库A,而偶数ID的用户则对应到数据库B。这个方法的最大问题是,只能分成两个库。另一个算法是按用户ID所在区间对应,比如ID在0-10000之间的用户对应到数据库A, ID在10000-20000这个范围的对应到数据库B,以此类推。按算法分实现起来比较方便,也比较高效,但是不能满足后续的伸缩性要求,如果需要增加数据库节点,必需调整算法或移动很大的数据集, 比较难做到在不停止服务的前提下进行扩充数据库节点。

按索引/映射表对应

这种方法是指建立一个索引表,保存每个用户的ID和数据库ID的对应关系,每次读写用户数据时先从这个表获取对应数据库。新用户注册后,在所有可用的数据库中随机挑选一个为其建立索引。这种方法比较灵活,有很好的伸缩性。一个缺点是增加了一次数据库访问,所以性能上没有按算法对应好。

比较之后,我们采用的是索引表的方式,我们愿意为其灵活性损失一些性能,更何况我们还有memcached, 因为索引数据基本不会改变的缘故,缓存命中率非常高。所以能很大程度上减少了性能损失。

数据访问过程

图4:数据访问过程

索引表的方式能够比较方便地添加数据库节点,在增加节点时,只要将其添加到可用数据库列表里即可。 当然如果需要平衡各个节点的压力的话,还是需要进行数据的迁移,但是这个时候的迁移是少量的,可以逐步进行。要迁移用户A的数据,首先要将其状态置为迁移数据中,这个状态的用户不能进行写操作,并在页面上进行提示。 然后将用户A的数据全部复制到新增加的节点上后,更新映射表,然后将用户A的状态置为正常,最后将原来对应的数据库上的数据删除。这个过程通常会在临晨进行,所以,所以很少会有用户碰到迁移数据中的情况。

当然,有些数据是不属于某个用户的,比如系统消息、配置等等,我们把这些数据保存在一个全局库中。

问题

分库会给你在应用的开发和部署上都带来很多麻烦。

不能执行跨库的关联查询

如果我们需要查询的数据分布于不同的数据库,我们没办法通过JOIN的方式查询获得。比如要获得好友的最新照片,你不能保证所有好友的数据都在同一个数据库里。一个解决办法是通过多次查询,再进行聚合的方式。我们需要尽量避免类似的需求。有些需求可以通过保存多份数据来解决,比如User-A和User-B的数据库分别是DB-1和DB-2, 当User-A评论了User-B的照片时,我们会同时在DB-1和DB-2中保存这条评论信息,我们首先在DB-2中的photo_comments表中插入一条新的记录,然后在DB-1中的user_comments表中插入一条新的记录。这两个表的结构如下图所示。这样我们可以通过查询photo_comments表得到User-B的某张照片的所有评论, 也可以通过查询user_comments表获得User-A的所有评论。另外可以考虑使用全文检索工具来解决某些需求, 我们使用Solr来提供全站标签检索和照片搜索服务。

评论表结构

图5:评论表结构

不能保证数据的一致/完整性

跨库的数据没有外键约束,也没有事务保证。比如上面的评论照片的例子, 很可能出现成功插入photo_comments表,但是插入user_comments表时却出错了。一个办法是在两个库上都开启事务,然后先插入photo_comments,再插入user_comments, 然后提交两个事务。这个办法也不能完全保证这个操作的原子性。

所有查询必须提供数据库线索

比如要查看一张照片,仅凭一个照片ID是不够的,还必须提供上传这张照片的用户的ID(也就是数据库线索),才能找到它实际的存放位置。因此,我们必须重新设计很多URL地址,而有些老的地址我们又必须保证其仍然有效。我们把照片地址改成/photos/{username}/{photo_id}/的形式,然后对于系统升级前上传的照片ID, 我们又增加一张映射表,保存photo_id和user_id的对应关系。当访问老的照片地址时,我们通过查询这张表获得用户信息, 然后再重定向到新的地址。

自增ID

如果要在节点数据库上使用自增字段,那么我们就不能保证全局唯一。这倒不是很严重的问题,但是当节点之间的数据发生关系时,就会使得问题变得比较麻烦。我们可以再来看看上面提到的评论的例子。如果photo_comments表中的comment_id的自增字段,当我们在DB-2.photo_comments表插入新的评论时, 得到一个新的comment_id,假如值为101,而User-A的ID为1,那么我们还需要在DB-1.user_comments表中插入(1, 101 ...)。 User-A是个很活跃的用户,他又评论了User-C的照片,而User-C的数据库是DB-3。 很巧的是这条新评论的ID也是101,这种情况很用可能发生。那么我们又在DB-1.user_comments表中插入一行像这样(1, 101 ...)的数据。 那么我们要怎么设置user_comments表的主键呢(标识一行数据)?可以不设啊,不幸的是有的时候(框架、缓存等原因)必需设置。那么可以以user_id、 comment_id和photo_id为组合主键,但是photo_id也有可能一样(的确很巧)。看来只能再加上photo_owner_id了, 但是这个结果又让我们实在有点无法接受,太复杂的组合键在写入时会带来一定的性能影响,这样的自然键看起来也很不自然。所以,我们放弃了在节点上使用自增字段,想办法让这些ID变成全局唯一。为此增加了一个专门用来生成ID的数据库,这个库中的表结构都很简单,只有一个自增字段id。 当我们要插入新的评论时,我们先在ID库的photo_comments表里插入一条空的记录,以获得一个唯一的评论ID。 当然这些逻辑都已经封装在我们的框架里了,对于开发人员是透明的。 为什么不用其它方案呢,比如一些支持incr操作的Key-Value数据库。我们还是比较放心把数据放在MySQL里。 另外,我们会定期清理ID库的数据,以保证获取新ID的效率。

实现

我们称前面提到的一个数据库节点为Shard,一个Shard由两个台物理服务器组成, 我们称它们为Node-A和Node-B,Node-A和Node-B之间是配置成Master-Master相互复制的。 虽然是Master-Master的部署方式,但是同一时间我们还是只使用其中一个,原因是复制的延迟问题, 当然在Web应用里,我们可以在用户会话里放置一个A或B来保证同一用户一次会话里只访问一个数据库, 这样可以避免一些延迟问题。但是我们的Python任务是没有任何状态的,不能保证和PHP应用读写相同的数据库。那么为什么不配置成Master-Slave呢?我们觉得只用一台太浪费了,所以我们在每台服务器上都创建多个逻辑数据库。 如下图所示,在Node-A和Node-B上我们都建立了shard_001和shard_002两个逻辑数据库, Node-A上的shard_001和Node-B上的shard_001组成一个Shard,而同一时间只有一个逻辑数据库处于Active状态。 这个时候如果需要访问Shard-001的数据时,我们连接的是Node-A上的shard_001, 而访问Shard-002的数据则是连接Node-B上的shard_002。以这种交叉的方式将压力分散到每台物理服务器上。 以Master-Master方式部署的另一个好处是,我们可以不停止服务的情况下进行表结构升级, 升级前先停止复制,升级Inactive的库,然后升级应用,再将已经升级好的数据库切换成Active状态, 原来的Active数据库切换成Inactive状态,然后升级它的表结构,最后恢复复制。 当然这个步骤不一定适合所有升级过程,如果表结构的更改会导致数据复制失败,那么还是需要停止服务再升级的。

Database Layout

图6:数据库布局

前面提到过添加服务器时,为了保证负载的平衡,我们需要迁移一部分数据到新的服务器上。为了避免短期内迁移的必要,我们在实际部署的时候,每台机器上部署了8个逻辑数据库, 添加服务器后,我们只要将这些逻辑数据库迁移到新服务器就可以了。最好是每次添加一倍的服务器, 然后将每台的1/2逻辑数据迁移到一台新服务器上,这样能很好的平衡负载。当然,最后到了每台上只有一个逻辑库时,迁移就无法避免了,不过那应该是比较久远的事情了。

我们把分库逻辑都封装在我们的PHP框架里了,开发人员基本上不需要被这些繁琐的事情困扰。下面是使用我们的框架进行照片数据的读写的一些例子:

<?php
    $Photos = new ShardedDBTable('Photos', 'yp_photos', 'user_id', array(
                'photo_id'    => array('type' => 'long', 'primary' => true, 'global_auto_increment' => true),
                'user_id'     => array('type' => 'long'),
                'title'       => array('type' => 'string'),
                'posted_date' => array('type' => 'date'),
            ));

    $photo = $Photos->new_object(array('user_id' => 1, 'title' => 'Workforme'));
    $photo->insert();

    // 加载ID为10001的照片,注意第一个参数为用户ID
    $photo = $Photos->load(1, 10001);

    // 更改照片属性
    $photo->title = 'Database Sharding';
    $photo->update();

    // 删除照片
    $photo->delete();

    // 获取ID为1的用户在2010-06-01之后上传的照片
    $photos = $Photos->fetch(array('user_id' => 1, 'posted_date__gt' => '2010-06-01'));
?>

首先要定义一个ShardedDBTable对象,所有的API都是通过这个对象开放。第一个参数是对象类型名称, 如果这个名称已经存在,那么将返回之前定义的对象。你也可以通过get_table('Photos')这个函数来获取之前定义的Table对象。 第二个参数是对应的数据库表名,而第三个参数是数据库线索字段,你会发现在后面的所有API中全部需要指定这个字段的值。 第四个参数是字段定义,其中photo_id字段的global_auto_increment属性被置为true,这就是前面所说的全局自增ID, 只要指定了这个属性,框架会处理好ID的事情。

如果我们要访问全局库中的数据,我们需要定义一个DBTable对象。

<?php
    $Users = new DBTable('Users', 'yp_users', array(
                'user_id'  => array('type' => 'long', 'primary' => true, 'auto_increment' => true),
                'username' => array('type' => 'string'),
            ));
?>

DBTable是ShardedDBTable的父类,除了定义时参数有些不同(DBTable不需要指定数据库线索字段),它们提供一样的API。

缓存

我们的框架提供了缓存功能,对开发人员是透明的。

<?php
    $photo = $Photos->load(1, 10001);
?>

比如上面的方法调用,框架先尝试以Photos-1-10001为Key在缓存中查找,未找到的话再执行数据库查询并放入缓存。当更改照片属性或删除照片时,框架负责从缓存中删除该照片。这种单个对象的缓存实现起来比较简单。稍微麻烦的是像下面这样的列表查询结果的缓存。

<?php
    $photos = $Photos->fetch(array('user_id' => 1, 'posted_date__gt' => '2010-06-01'));
?>

我们把这个查询分成两步,第一步先查出符合条件的照片ID,然后再根据照片ID分别查找具体的照片信息。 这么做可以更好的利用缓存。第一个查询的缓存Key为Photos-list-{shard_key}-{md5(查询条件SQL语句)}, Value是照片ID列表(逗号间隔)。其中shard_key为user_id的值1。目前来看,列表缓存也不麻烦。 但是如果用户修改了某张照片的上传时间呢,这个时候缓存中的数据就不一定符合条件了。所以,我们需要一个机制来保证我们不会从缓存中得到过期的列表数据。我们为每张表设置了一个revision,当该表的数据发生变化时(调用insert/update/delete方法), 我们就更新它的revision,所以我们把列表的缓存Key改为Photos-list-{shard_key}-{md5(查询条件SQL语句)}-{revision}, 这样我们就不会再得到过期列表了。

revision信息也是存放在缓存里的,Key为Photos-revision。这样做看起来不错,但是好像列表缓存的利用率不会太高。因为我们是以整个数据类型的revision为缓存Key的后缀,显然这个revision更新的非常频繁,任何一个用户修改或上传了照片都会导致它的更新,哪怕那个用户根本不在我们要查询的Shard里。要隔离用户的动作对其他用户的影响,我们可以通过缩小revision的作用范围来达到这个目的。 所以revision的缓存Key变成Photos-{shard_key}-revision,这样的话当ID为1的用户修改了他的照片信息时, 只会更新Photos-1-revision这个Key所对应的revision。

因为全局库没有shard_key,所以修改了全局库中的表的一行数据,还是会导致整个表的缓存失效。 但是大部分情况下,数据都是有区域范围的,比如我们的帮助论坛的主题帖子, 帖子属于主题。修改了其中一个主题的一个帖子,没必要使所有主题的帖子缓存都失效。 所以我们在DBTable上增加了一个叫isolate_key的属性。

<?php
$GLOBALS['Posts'] = new DBTable('Posts', 'yp_posts', array(
        'topic_id'    => array('type' => 'long', 'primary' => true),
        'post_id'     => array('type' => 'long', 'primary' => true, 'auto_increment' => true),
        'author_id'   => array('type' => 'long'),
        'content'     => array('type' => 'string'),
        'posted_at'   => array('type' => 'datetime'),
        'modified_at' => array('type' => 'datetime'),
        'modified_by' => array('type' => 'long'),
    ), 'topic_id');
?>

注意构造函数的最后一个参数topic_id就是指以字段topic_id作为isolate_key,它的作用和shard_key一样用于隔离revision的作用范围。

ShardedDBTable继承自DBTable,所以也可以指定isolate_key。 ShardedDBTable指定了isolate_key的话,能够更大幅度缩小revision的作用范围。 比如相册和照片的关联表yp_album_photos,当用户往他的其中一个相册里添加了新的照片时, 会导致其它相册的照片列表缓存也失效。如果我指定这张表的isolate_key为album_id的话, 我们就把这种影响限制在了本相册内。

我们的缓存分为两级,第一级只是一个PHP数组,有效范围是Request。而第二级是memcached。这么做的原因是,很多数据在一个Request周期内需要加载多次,这样可以减少memcached的网络请求。另外我们的框架也会尽可能的发送memcached的gets命令来获取数据, 从而减少网络请求。

总结

这个架构使得我们在很长一段时间内都不必再为数据库压力所困扰。我们的设计很多地方参考了netlogflickr的实现,因此非常感谢他们将一些实现细节发布出来。

关于作者:

周兆兆(Zola,不是你熟知的那个),又拍网架构师。6年IT从业经验,不太专注于某项技术,对很多技术都感兴趣。


给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家加入到InfoQ中文站用户讨论组中与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

关于自增ID by Byers Roger

自增ID的问题,如果在具体的节点上生成自增id时附带上节点本身的编号,能不能解决这个问题呢?

Re: 关于自增ID by Zhou Zola

解决的办法有问多,你说的是一种,也可以使用Surrogate Key(就是弄个无意义的主键),我们只是挑了一个我们认为比较方便的办法。
节点上自增还有一个麻烦的事情是Master-Master复制,需要保证不重复,当然这个也有办法解决。

数据一致性 by 柯 里

"不能保证数据的一致/完整性"这个是怎么解决的?

Re: 数据一致性 by Zhou Zola

涉及到两个库的事务,我们用文章里提到的方法,将两个事务嵌套起来。
有些地方的一致性我们是在读的时候进行修复。
另外,对于复杂的事务,我们使用任务进行一致性检查和修复。

事务问题,靠补救 by zheng spell

楼主说的不错,思路清晰,阐述明了,让我这个菜鸟一看就都懂了,顶下先,不过事务方面应该有其他的解决办法,不能保证一致性的话,我有个笨想法,第一个事务提交的时候,如果成功就提交第二个,如果第二个也成功,那当然最好了,不成功,就记录下,人工排查问题,最后做补救措施,事务提交不成功的问题毕竟比较少。

Re: 数据一致性 by 柯 里

按上面的说法,分开两个事务也无法保证一致性不是?

上面那个例子,如果插入photo_comments成功,而插入user_comments失败,然后两个都提交了。
是不是可以这样理解,后台任务没进行修复之前,用户是看不到User-A的那条丢失的评论

不错,了解中 by Yin Register

不错,了解中。。。

使用NO-SQL,MongoDB来解决id冲突呢? by xiao deshi

按你分享的思路,应用到MongoDB上,实现master/slaver模式,使用中心数据库分发id也可行。

Re: 数据一致性 by Zhou Zola

的确是不能完全保证,文章里我也有提到。
我们的系统有可能出现数据不一致的情况,但是几率非常小。

Re: 关于自增ID by Byers Roger

我觉得每台Master自增的起始种子设置成不一样的就可以了,比如long的最大值,可以除以100,分成100段给一百个Master用,相信一个long即使是除以了100,一台Master想用完估计也很困难吧。。

Re: 关于自增ID by Wang Richard

看了你的拆分规则想了一种方法(愚见):

一台物理数据库配置多个schma,比方按1:10(可以更多)来配置,如果yupoo目前是2台数据库服务总配置20个schma1...20。
具体算法可以按目前的用户id拆分纬度来做一致性hash(或者简单的hash取模)来路由到对应的schma。

当需要增加物理数据库时,只需要迁移其中的schma(数据)(如schma1...5,schma11...15迁移到新增加数据库)
前期可以考虑给每台数据库配置相对合理的schma节点,(当需要增加节点的话 还是要考虑更合理的算法如:一致性hash)

这种方案,对于数据迁移比较简单,影响范围比较集中,不会影响大规模的节点,不需要额外维护索引/映射表。

关于系统升级的处理 by 赵 进东

用两台物理机,每台建两个逻辑数据库,交叉复制,想法很不错啊!

Re: 关于自增ID by Zhou Zola

这个想法不错,
不过我觉得部署、维护太麻烦。而且只能预设Shard数,不够灵活。

Re: 关于自增ID by Zhou Zola

看了你的拆分规则想了一种方法(愚见):

一台物理数据库配置多个schma,比方按1:10(可以更多)来配置,如果yupoo目前是2台数据库服务总配置20个schma1...20。
具体算法可以按目前的用户id拆分纬度来做一致性hash(或者简单的hash取模)来路由到对应的schma。

当需要增加物理数据库时,只需要迁移其中的schma(数据)(如schma1...5,schma11...15迁移到新增加数据库)
前期可以考虑给每台数据库配置相对合理的schma节点,(当需要增加节点的话 还是要考虑更合理的算法如:一致性hash)

这种方案,对于数据迁移比较简单,影响范围比较集中,不会影响大规模的节点,不需要额外维护索引/映射表。

实际上,我们就是这么做的,可能我在文章里没有说清楚。

Re: 关于自增ID by ma prance

使用自增长数字型ID而不是使用字符串型的UUID是为了考虑索引的效率吗?可能我的项目涉及的数据量不够大,我发现用UUID作为主键速度没有明显下降,除了索引文件会比较大。
还有现在的id库解决方法是否会碰到锁定的问题,“当我们要插入新的评论时,我们先在ID库的photo_comments表里插入一条空的记录,以获得一个唯一的评论ID”,怎么把那个空记录的ID给取出来?

Re: 关于自增ID by Zhou Zola

使用自增长数字型ID而不是使用字符串型的UUID是为了考虑索引的效率吗?可能我的项目涉及的数据量不够大,我发现用UUID作为主键速度没有明显下降,除了索引文件会比较大。
还有现在的id库解决方法是否会碰到锁定的问题,“当我们要插入新的评论时,我们先在ID库的photo_comments表里插入一条空的记录,以获得一个唯一的评论ID”,怎么把那个空记录的ID给取出来?

是的,是考虑到索引大小的问题;
写入ID库很快,写入后可以获得本连接的最后一个insert_id,比如PHP有mysql_insert_id方法可以获取。这和单个库的应用开发是一样的。

楼主能不能讲讲对图片存储的经验? by liu emsdog

楼主能不能讲讲对图片存储的经验?

不如垂直分割 by Xu Sam

水平分割会造成很大的麻烦,开发上的成本会大大增加,如果两台服务器还不够,三台还不够,怎么办?个人觉得不如垂直分割,如果单模块的读写压力过于大,就单独用一台机器,可以在这台机器上投入更大的资金,搞台高配置的。另外,DAO层的设计、查询优化等也是很关键,应该多使用memcached,用好memcached,压力会减轻许多,因为对于DB来说,主要的作用就是条件查询与写入

Re: 不如垂直分割 by Zhao Jeffrey

blog.zhaojie.me/2010/03/sharding-by-id-characte...

以前我用过这样的做法。

询问 by 崔 corey

想知道文中的图是用什么软件做的。?

请教 by 崔 corey

另外,向楼主请教一下分布式的流水号如何生成(考虑效率),不是随机生成UUID。

Re: 楼主能不能讲讲对图片存储的经验? by Zhou Zola

楼主能不能讲讲对图片存储的经验?

我们用的是mogilefs, 由于mogilefs的一些性能和伸缩性问题,目前也在考虑其它方案

Re: 不如垂直分割 by Zhou Zola

blog.zhaojie.me/2010/03/sharding-by-id-characte...

以前我用过这样的做法。

很有参考价值,早点发布就好了:)

Re: 请教 by Zhou Zola

想知道文中的图是用什么软件做的。?

omnigraffle

另外,向楼主请教一下分布式的流水号如何生成(考虑效率),不是随机生成UUID。

不太明白你指的“分布式的流水号”是什么?ID吗,我们没用UUID。ID的生成方式在文中有比较详细的介绍。

Re: 请教 by 崔 corey

不好意思。文中的描述在我看来比较笼统:
您的意思是在数据库中建立一张ID表,然后ID表的主键设置为自增长,每当需要生成全局唯一ID的时候,则插入一条该记录以自增生成一个全局ID吗?
如果是这样的话,那是不是意味着每次需要这种ID的时候,都必须要对数据库该表进行一次插入操作,然后将该记录ID查询出来,这一插入一查询会不会影响性能。如何能保证这一次插入和查询的原子性?望请教。

不服高人有罪 by li ramon

以前看过livejurnal的架构介绍文章,也是说了很多他们如何做sharding,因为没有具体做过,自己看来都有点不得要领的感觉。 今天看了这篇,感觉理解更多了些,但是还是得有机会机会实践才行,总之,这篇文章不错,不服高人有罪啊

Re: 请教 by Zhou Zola

不好意思。文中的描述在我看来比较笼统:
您的意思是在数据库中建立一张ID表,然后ID表的主键设置为自增长,每当需要生成全局唯一ID的时候,则插入一条该记录以自增生成一个全局ID吗?
如果是这样的话,那是不是意味着每次需要这种ID的时候,都必须要对数据库该表进行一次插入操作,然后将该记录ID查询出来,这一插入一查询会不会影响性能。如何能保证这一次插入和查询的原子性?望请教。

每次需要ID都会有插入操作,不过不需要查询操作,MySQL会在插入操作的响应包中返回刚刚插入的ID,也就是说原子性是由数据库来保证的。
对于单个写入请求来说性能的影响当然会有一些(不过相比分库前性能还是提高了,因为大数据集会影响写入性能),但是从整体上来考虑这个损失是可以接受的。
这当然不是最好的方案,但是只要适合自己、方便实现,我认为就可以了。

Re: 请教 by 崔 corey

tx very much

跨库总是麻烦的 by zoe dollars

碰到跨库的事务问题总是麻烦的,可惜现在还没有成熟的分布式解决方案,不过如作者所说,够用能满足需求,错误在可控的范围内就好,感谢分享这篇有价值的文章。

关于绘图工具 by peter zyliu

想问一下图画的很漂亮用什么工具呀 :)

Re: 关于绘图工具 by Zhou Zola

想问一下图画的很漂亮用什么工具呀 :)

omnigraffle

Re: 楼主能不能讲讲对图片存储的经验? by zoe dollars

能说说目前mogilefs性能和伸缩性方面的具体问题吗?打算用什么方案来替换?

Re: 楼主能不能讲讲对图片存储的经验? by Zhou Zola

能说说目前mogilefs性能和伸缩性方面的具体问题吗?打算用什么方案来替换?

mogilefs的伸缩性问题主要是它将文件位置信息存放在单台MySQL上;性能问题主要是小文件的随机读取性能。
我们在开发一个自己的存储系统。

ID冲突 by 邱 锦森

这才是真正的《Head First》

看到ID冲突那段的时候也是想到了用No-SQL来解决,不知道为何不采取这种方式呢?

Re: 关于自增ID by 王 晓乐

好文章,学习了。
孕妇护肤品

Re: 关于自增ID by biao jiang

6年就到这个地步。我做了6年,现在还在对日外包,赚着狗屎钱,一事无成,不知道自己能干什么,去他妈的对日外包,去他妈比的南京,去他妈比的南京IT行业,我操他妈卖必

消息队列容错 by lei ren

Hi Zola,

你们有否遇到消息队列失败导致索引未能及时更新、未发出通知消息/邮件的情况?
是否有针对性的设计?

感谢分享。

Lei

Re: 关于自增ID by sun sqtds

哥们给力,速度换工资

不理解 by dl zhang

数据库升级保持一定的兼容性,数据库高可用+slave.分库分表需要水平分的话,一致性哈希啊.

Re: 关于系统升级的处理 by yong leo

请问,博文中提到的逻辑库指的是一台物理机上跑多个mysql实例么?

Re: 请教 by zscome zscome

这种实现方式只是保证了自增字段原子性(osflow也是采用这种方式),利用mysql自增字段的特点,但是并发性依赖于mysql数据库。

用户id生成的数据库,感觉可以像运营库 by 杨 亮

感谢分享,正好在考虑这里,用户的id要用一个单独的数据库来处理,如果还有其他的一些共享信息,将这些共享信息还有用户id统一生成的放在一起,这样没问题吧?

用户id生成的数据库,感觉可以像运营库 by 杨 亮

感谢分享,正好在考虑这里,用户的id要用一个单独的数据库来处理,如果还有其他的一些共享信息,将这些共享信息还有用户id统一生成的放在一起,这样没问题吧?

关于revision by 赵 石

大师好,
"所以,我们需要一个机制来保证我们不会从缓存中得到过期的列表数据。我们为每张表设置了一个revision,当该表的数据发生变化时(调用insert/update/delete方法), 我们就更新它的revision,所以我们把列表的缓存Key改为Photos-list-{shard_key}-{md5(查询条件SQL语句)}-{revision}, 这样我们就不会再得到过期列表了"

没有看明白是怎样保证不取到过期数据的,取列表时,取哪个key对应的列表呢,数据修改后,是怎样得到新的列表的呢

Re: 不如垂直分割 by zhong wang

水平分割会造成很大的麻烦,开发上的成本会大大增加,如果两台服务器还不够,三台还不够,怎么办?个人觉得不如垂直分割,如果单模块的读写压力过于大,就单独用一台机器,可以在这台机器上投入更大的资金,搞台高配置的。另外,DAO层的设计、查询优化等也是很关键,应该多使用memcached,用好memcached,压力会减轻许多,因为对于DB来说,主要的作用就是条件查询与写入


水平分割到一定程度, 感觉单台应用服务器的数据库连接压力会很大, 一台的连接数限制记得是8000, 不过貌似也很少有服务器压力达到这个程度, 可能是我杞人忧天

Re: 不如垂直分割 by zhong wang

水平分割会造成很大的麻烦,开发上的成本会大大增加,如果两台服务器还不够,三台还不够,怎么办?个人觉得不如垂直分割,如果单模块的读写压力过于大,就单独用一台机器,可以在这台机器上投入更大的资金,搞台高配置的。另外,DAO层的设计、查询优化等也是很关键,应该多使用memcached,用好memcached,压力会减轻许多,因为对于DB来说,主要的作用就是条件查询与写入


水平分割到一定程度, 感觉单台应用服务器的数据库连接压力会很大, 一台的连接数限制记得是8000, 不过貌似也很少有服务器压力达到这个程度, 可能是我杞人忧天

如何解决数据均匀分布的问题 和 热点数据的问题 by Jing Jeff

按索引/映射表对应 是一个不错的办法, 比较的灵活, 但是如何解决数据均匀分布的问题 和 热点数据的问题。 打个比方, 平均每个用户传了10张图片, 个别的狂热用户可能特别喜欢我们的这个产品, 传了上百万图片(举个极端的例子), 这些用户的粉丝少了倒还好, 如果是粉丝又特别多, 频繁地进行查询还能扛得住吗?

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

47 讨论
提供反馈
错误报告
商务合作
内容合作
Marketing
InfoQ.com及所有内容,版权所有 © 2006-2016 C4Media Inc. InfoQ.com 服务器由 Contegix提供, 我们最信赖的ISP伙伴。
北京创新网媒广告有限公司 京ICP备09022563号-7 隐私政策
BT

We notice you’re using an ad blocker

We understand why you use ad blockers. However to keep InfoQ free we need your support. InfoQ will not provide your data to third parties without individual opt-in consent. We only work with advertisers relevant to our readers. Please consider whitelisting us.