网站制作,vue 实现网站开发,企业网站建设规划,企业建站工具Yupoo!#xff08;又拍网#xff09; 是目前国内最大的图片服务提供商#xff0c;整个网站构建于大量的开源软件之上。以下为其使用到的开源软件信息#xff1a; 操作系统#xff1a;CentOS、MacOSX、Ubuntu 服务器#xff1a;Apache、Nginx、Squid 数据库#xff1a;… Yupoo!又拍网 是目前国内最大的图片服务提供商整个网站构建于大量的开源软件之上。以下为其使用到的开源软件信息 操作系统CentOS、MacOSX、Ubuntu 服务器Apache、Nginx、Squid 数据库MySQLmochiweb、MySQLdb 服务器监控Cacti、Nagios、 开发语言PHP、Python、Erlang、Java、Lua 分布式计算Hadoop、Mogilefs、 日志分析AWStats 任务管理Redmine 消息系统RabbitMQ、php-amqp 前端框架Mootools 缓存系统Memcached、php-memcached、libmemcached、pylibmc、XCache、RedisRiak、Predis 图片处理GraphicsMagick、gmagick FTP工具vsftpd 开发工具VIM、Readline 调试工具Firebug、Xdebug 版本控制Mercurial 搜索服务Solr 邮件服务Postfix 网络编程Twisted、cURL、libevent、Net-SNMP、NTP 可用性测试ibrowse 集群系统Heartbeat 并行开发gevent 负载均衡IPVS Python框架bottle 虚拟通道OpenVPN 信息来源http://www.yupoo.com/info/about/。 一、Yupoo的整体架构 二、程序语言的选择 Yupoo 的服务器端开发语言主要是PHP和Python其中PHP用于编写Web逻辑通过HTTP和用户直接打交道 而Python则主要用于开发内部服务和后台任务。在客户端则使用了大量基于MooTools框架的Javascript。 另外Yupoo把图片处理过程从PHP进程里独立出来变成一个服务。这个服务基于nginx作为nginx的一个模块开放REST API。 三、服务器的选用 选用Squid的原因是“目前暂时还没找到效率比 Squid 高的缓存系统原来命中率的确很差后来在 Squid 前又装了层 Lighttpd, 基于 url 做 hash, 同一个图片始终会到同一台 squid 去所以命中率彻底提高了。” 同时Yupoo也使用Python开发了YPWS/YPFS YPWS-Yupoo Web Server 是用 Python开发的一个小型 Web 服务器提供基本的 Web 服务外可以增加针对用户、图片、外链网站显示的逻辑判断可以安装于任何有空闲资源的服务器中遇到性能瓶颈时方便横向扩展。 YPFS-Yupoo File System 与 YPWS 类似也是基于这个 Web 服务器上开发的图片上传服务器。 有网友留言质疑 Python 的效率Yupoo 老大刘平阳在 del.icio.us 上写到 “YPWS用Python自己写的每台机器每秒可以处理294个请求, 现在压力几乎都在10以下” 四、Yupoo的消息系统 由 于PHP的单线程模型Yupoo把耗时较久的运算和I/O操作从HTTP请求周期中分离出来 交给由Python实现的任务进程来完成以保证请求响应速度。这些任务主要包括邮件发送、数据索引、数据聚合和好友动态推送等等。PHP通过消息队列 Yupoo用的是RabbitMQ来触发任务执行。这些任务的主要特点为 由用户或者定时触发的 耗时比较长的 需要异步执行的 整个任务系统主要分为以消息分发、进程管理和工作进程组成。 五、数据库的设计 数 据库一向是网站架构中最具挑战性的瓶颈通常出现在这里。又拍网的照片数据量很大数据库也几度出现严重的压力问题。和很多使用MySQL的2.0站点一 样又拍网的MySQL集群经历了从最初的一个主库一个从库、到一个主库多个从库、 然后到多个主库多个从库的一个发展过程。 最 初是由一台主库和一台从库组成当时从库只用作备份和容灾当主库出现故障时从库就手动变成主库一般情况下从库不作读写操作同步除外。随着压力 的增加加上了memcached当时只用其缓存单行数据。 但是单行数据的缓存并不能很好地解决压力问题因为单行数据的查询通常很快。所以把一些实时性要求不高的Query放到从库去执行。后面又通过添加多个 从库来分流查询压力不过随着数据量的增加主库的写压力也越来越大。在参考了一些相关产品和其它网站的做法后进了行数据库拆分。也就是将数据存放到不 同的数据库服务器中。 如何进行数据库拆分 垂直拆分是指按功能模块拆分比如可以将群组相关表和照片相关表存放在不同的数据库中这种方式多个数据库之间的表结构不同。 水平拆分而水平拆分是将同一个表的数据进行分块保存到不同的数据库中这些数据库中的表结构完全相同。 一 般都会先进行垂直拆分因为这种方式拆分方式实现起来比较简单根据表名访问不同的数据库就可以了。但是垂直拆分方式并不能彻底解决所有压力问题另外 也要看应用类型是否合适这种拆分方式。如果合适的话也能很好的起到分散数据库压力的作用。比如对于豆瓣我比较适合采用垂直拆分 因为豆瓣的各核心业务/模块书籍、电影、音乐相对独立数据的增加速度也比较平稳。不同的是又拍网的核心业务对象是用户上传的照片而照片数据的增 加速度随着用户量的增加越来越快。压力基本上都在照片表上显然垂直拆分并不能从根本上解决我们的问题所以Yupoo采用水平拆分的方式。 水 平拆分实现起来相对复杂我们要先确定一个拆分规则也就是按什么条件将数据进行切分。 一般2.0网站都以用户为中心数据基本都跟随用户比如用户的照片、朋友和评论等等。因此一个比较自然的选择是根据用户来切分。每个用户都对应一个数据 库访问某个用户的数据时 要先确定他/她所对应的数据库然后连接到该数据库进行实际的数据读写。那么怎么样对应用户和数据库呢Yupoo有这些选择 1、按算法对应 最 简单的算法是按用户ID的奇偶性来对应将奇数ID的用户对应到数据库A而偶数ID的用户则对应到数据库B。这个方法的最大问题是只能分成两个库。另 一个算法是按用户ID所在区间对应比如ID在0-10000之间的用户对应到数据库A ID在10000-20000这个范围的对应到数据库B以此类推。按算法分实现起来比较方便也比较高效但是不能满足后续的伸缩性要求如果需要增加 数据库节点必需调整算法或移动很大的数据集 比较难做到在不停止服务的前提下进行扩充数据库节点。 2、按索引/映射表对应 这 种方法是指建立一个索引表保存每个用户的ID和数据库ID的对应关系每次读写用户数据时先从这个表获取对应数据库。新用户注册后在所有可用的数据库 中随机挑选一个为其建立索引。这种方法比较灵活有很好的伸缩性。一个缺点是增加了一次数据库访问所以性能上没有按算法对应好。 比较之后Yupoo采用的是索引表的方式我们愿意为其灵活性损失一些性能更何况我们还有memcached 因为索引数据基本不会改变的缘故缓存命中率非常高。所以能很大程度上减少了性能损失。 索 引表的方式能够比较方便地添加数据库节点在增加节点时只要将其添加到可用数据库列表里即可。 当然如果需要平衡各个节点的压力的话还是需要进行数据的迁移但是这个时候的迁移是少量的可以逐步进行。要迁移用户A的数据首先要将其状态置为迁移 数据中这个状态的用户不能进行写操作并在页面上进行提示。 然后将用户A的数据全部复制到新增加的节点上后更新映射表然后将用户A的状态置为正常最后将原来对应的数据库上的数据删除。这个过程通常会在临晨进 行所以所以很少会有用户碰到迁移数据中的情况。当然有些数据是不属于某个用户的比如系统消息、配置等等把这些数据保存在一个全局库中。 分库带来的问题如何解决 分库会给在应用的开发和部署上都带来很多麻烦。 1、不能执行跨库的关联查询 如 果我们需要查询的数据分布于不同的数据库没办法通过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来提供全站标签检索和照片搜索服务。 2、不能保证数据的一致/完整性 跨 库的数据没有外键约束也没有事务保证。比如上面的评论照片的例子 很可能出现成功插入photo_comments表但是插入user_comments表时却出错了。一个办法是在两个库上都开启事务然后先插入 photo_comments再插入user_comments 然后提交两个事务。这个办法也不能完全保证这个操作的原子性。 3、所有查询必须提供数据库线索 比 如要查看一张照片仅凭一个照片ID是不够的还必须提供上传这张照片的用户的ID也就是数据库线索才能找到它实际的存放位置。因此必须重新设计 很多URL地址而有些老的地址我们又必须保证其仍然有效。Yupoo把照片地址改成/photos/{username}/{photo_id}/的形 式然后对于系统升级前上传的照片ID 又增加一张映射表保存photo_id和user_id的对应关系。当访问老的照片地址时通过查询这张表获得用户信息, 然后再重定向到新的地址。 4、自增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了 但是这个结果又让我们实在有点无法接受太复杂的组合键在写入时会带来一定的性能影响这样的自然键看起来也很不自然。所以Yupoo放弃了在节点上使 用自增字段想办法让这些ID变成全局唯一。为此增加了一个专门用来生成ID的数据库这个库中的表结构都很简单只有一个自增字段id。 当我们要插入新的评论时我们先在ID库的photo_comments表里插入一条空的记录以获得一个唯一的评论ID。 当然这些逻辑都已经封装在我们的框架里了对于开发人员是透明的。 为什么不用其它方案呢比如一些支持incr操作的Key-Value数据库。Yupoo还是比较放心把数据放在MySQL里。 另外Yupoo会定期清理ID库的数据以保证获取新ID的效率。 数据库优化的实现 前 面提到的一个数据库节点为Shard一个Shard由两个台物理服务器组成 可以理解为Node-A和Node-BNode-A和Node-B之间是配置成Master-Master相互复制的。 虽然是Master-Master的部署方式但是同一时间还是只使用其中一个原因是复制的延迟问题 当然在Web应用里可以在用户会话里放置一个A或B来保证同一用户一次会话里只访问一个数据库 这样可以避免一些延迟问题。但是Python任务是没有任何状态的不能保证和PHP应用读写相同的数据库。那么为什么不配置成Master-Slave 呢Yupoo觉得只用一台太浪费了所以在每台服务器上都创建多个逻辑数据库。 如下图所示在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状态然后升级它的表结构最后恢复复制。 当然这个步骤不一定适合所有升级过程如果表结构的更改会导致数据复制失败那么还是需要停止服务再升级的。 前 面提到过添加服务器时为了保证负载的平衡需要迁移一部分数据到新的服务器上。为了避免短期内迁移的必要在实际部署的时候每台机器上部署了8个逻辑 数据库 添加服务器后只要将这些逻辑数据库迁移到新服务器就可以了。最好是每次添加一倍的服务器 然后将每台的1/2逻辑数据迁移到一台新服务器上这样能很好的平衡负载。当然最后到了每台上只有一个逻辑库时迁移就无法避免了不过那应该是比较久 远的事情了。 Yupoo把分库逻辑都封装在我们的PHP框架里了开发人员基本上不需要被这些繁琐的事情困扰。下面是使用框架进行照片数据的读写的一些例子 ?php$Photos newShardedDBTable(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 newDBTable(Users, yp_users, array(user_id array(type long, primary true, auto_increment true),username array(type string),));? DBTable是ShardedDBTable的父类除了定义时参数有些不同DBTable不需要指定数据库线索字段它们提供一样的API。 六、缓存方案的选择 Yupoo使用的框架自带缓存功能对开发人员是透明的。 ?php$photo $Photos-load(1, 10001);? 比如上面的方法调用框架先尝试以Photos-1-10001为Key在缓存中查找未找到的话再执行数据库查询并放入缓存。当更改照片属性或删除照片时框架负责从缓存中删除该照片。这种单个对象的缓存实现起来比较简单。稍微麻烦的是像下面这样的列表查询结果的缓存。 ?php$photos $Photos-fetch(array(user_id 1, posted_date__gt 2010-06-01));? Yupoo 把这个查询分成两步第一步先查出符合条件的照片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] newDBTable(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的网络请求。另外Yupoo的框架也会尽可能的发送memcached的gets命令来获取数据 从而减少网络请求。 参考文章http://www.infoq.com/cn/articles/yupoo-partition-database 另外推荐学习Facebook图片存储架构Yupoo又拍网的系统架构 转载于:https://www.cnblogs.com/dasn/articles/3209639.html