从零打造 Instagram

Posted DeepNoMind

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零打造 Instagram相关的知识,希望对你有一定的参考价值。

Instagram是全球最大的照片、视频分享社区,如果让我们自己设计一个Instagram这样的服务,应该怎么做呢?这篇文章解析了Instagram的功能和架构,从中我们可以看到设计一个内容分享服务所需要关注的部分。原文:Instagram System Architecture[1]

Instagram是一个免费的照片和视频分享社交网络,有很多人每天在上面分享故事,记录生活中的点点滴滴。

功能性需求

  1. 用户可以上传照片和视频

  2. 用户可以查看照片和视频

  3. 用户可以根据照片标题进行搜索

  4. 用户可以关注/取消关注其他用户

  5. 用户可以通过搜索栏搜索用户id

  6. 为关注的每个用户创建信息流

  7. 可以把照片存档

  8. 可以通过聊天窗口分享故事

  9. 可以拉黑/限制其他用户

  10. 可以在其他用户的帖子下面点赞和评论

  11. 用户可以发帖

非功能性需求

  1. 高可扩展性

  2. 高一致性

  3. 高可用性

  4. 高可靠性

  5. 用户数据应该是持久化的(任何上传的照片都不应该丢失)

  6. 生成信息流的最大延迟是150毫秒


接下来我们做一下系统容量估算

  1. 假设注册用户 = 5亿

  2. 30%的活跃用户 = 1.5亿

  3. 注册名人人数 = 10k

  4. 读请求数 = 100 *上传(写)请求数

  5. 高峰时刻,假设平均流量 = X,目标处理上限是6X

活跃用户:

  1. 每周发帖3次,每个帖子包含1 MB的图片和文本

  2. 每个帖子至少收到10个赞和2-3条评论

  3. 关注100个用户,有50个粉丝

  4. 每天刷新2次信息流

名人:

  1. 每周发帖2次,每个帖子包含大于500K的图片和文本

  2. 每个帖子至少收到50K个赞和至少1K条评论

  3. 拥有500万粉丝

  4. 每天刷新2次信息流

每秒请求数(QPS):

  1. 发帖

  • Create_post_avg = (150 Million + 10 K) * 2 / (72460*60) = 496/s

  • Create_post_peak = 496/s*6 = 3k/s

    1. 点赞

  • like_post_avg = (150 million10 +10K50K) * 2 / (72460*60) = 6.6 k/s

  • like_post_peak = 6.6 k/s*6 = 40 k/s

    1. 评论

  • comment_post_avg = (150 million * 2 + 10K * 1K) = 1k/s

  • Comment_post_peak = 1k/s * 6 = 6k/s

    1. 关注信息流

  • get_follow_feed_avg = (150 million + 10K) * 2 / (246060) = 3.5k/s

  • get_follow_feed_peak = 3.5k/s * 6 = 21.8 k/s

    1. 数据量

  • 64base([‘a-z’,‘A-Z’,‘0–9’,‘-’,‘_’])编码的user_id,需要5 bits ~ 1Byte

  • 500 Million + 10K * 5 bits ~ 1 Byte = 1G user

  • 容量估计:

    1. 每天上传的活跃用户 = 100万

    2. 每天上传的照片 = 500万张

    3. 每天每秒上传的照片 = 57张照片

    4. 平均照片大小 = 150 KB

    5. 每天存储开销 = 500万* 150KB = 716GB

    6. 数据保存10年,所需存储容量为716 GB * 365 * 10年 = 2553 TB ≈ 2.6 PB

    7. 日活跃用户查看 = 1000万

    8. 每小时的信息流产生量为1000万,即2800 RPS(每秒请求数)。

    9. 如果用户每天搜索一次,那就是每天1000万次搜索,也就是115个RPS。


    系统组件设计

  • 上传照片和视频 = 写操作

  • 查看照片和视频 = 读操作

  • 读写比 = 20:80

  • Web服务器可以同时支持1000个活动连接

  • 200个连接会被写操作占用,写入(上传)会使连接长时间保持打开状态

  • 因此,更好的方法是用2个数据库分别处理读写操作。此外,分离照片的读写请求可以帮助我们独立的扩展和优化每个过程。下图显示了读写的过程。

    1. 信息流生成服务(News Feed Generation services)
  • 为用户更新所关注的用户的最新帖子

  • 每个用户的信息流都是独一无二的,组合非常复杂

  • 为了生成新的信息流,系统必须获取这些照片的元数据(喜欢、评论、时间、位置等),并将其传递给排名算法,以决定哪些照片应该根据元数据安排在信息流中

  • 后端需要同时查询大量的表,然后使用预定义的参数对它们进行排序,这种方法将导致更高的延迟,需要大量的时间来生成新的信息流

  • 因此,可以采用预生成的信息流。创建专门用于生成每个用户独有信息流的服务器,并将其结果存储在单独的信息流表中。当用户点击更新时,直接从数据库中读取信息流并显示给用户。

  • 2. 提供信息流(Serving the News Feed)
  • 推模式(Push) — 当用户上传了新的照片/视频,他/她的所有粉丝都会获得更新。如果用户关注了很多人或名人,服务器就必须非常频繁的向用户推送更新。

  • 拉模式(Pull) — 用户主动刷新他们的信息流(向服务器发出一个拉取请求)。在用户刷新之前,新帖子是不可见的。

  • 混合模式(Hybrid Approach) — 对拥有大量粉丝的名人用户应用拉模式,普通用户采用推模式。

  • 3. 负载均衡(Load Balancing)
  • 将流量分流到一组服务器中,从而提高网站或应用程序的响应和可用性

  • 使用最小带宽法

  • 该算法将选择流量最小的服务器(以每秒兆位(Mbps)计算)提供服务

  • 部署在客户端和服务器或服务器和数据库之间


  • 数据架构

    数据库设计

    1. 用户相关数据

  • User ID(主键):唯一的用户ID,便于全局区分用户

  • Name:用户名

  • Email:用户邮件地址

  • Password:用户密码,用于用户登录

  • Create Date:用户注册时间

  • 2. 照片相关数据(AWS S3)

  • photo id(主键):10字节长度的唯一照片id,用于标识每一张照片

  • UserId:上传照片的用户id

  • Path:存放照片的对象存储路径/URL

  • Latitude & Longitude(纬度和经度):存储这些信息来找到照片的位置

  • Date & time(日期和时间):照片上传的日期和时间戳

  • 3. 用户关注和粉丝相关数据

  • Following:该用户所关注的所有用户的UserId

  • Followers:关注该用户的所有用户的UserId

  • 因此,我们需要两种不同的数据库:
    1)关系型数据库(mysql
    2)NoSQL数据库(Cassandra)

    数据模型

    典型查询

  • 获取用户X关注的所有用户——为用户X发送信息流

  • 获取所有关注用户X的用户——将用户X的帖子推送到关注者的信息流中

  • 获取所有活跃用户(为活跃用户提供缓存的关注者信息流)


  • 接口/API

    1. create_post(user_id, image, text, timestamp) -> success/failure

    2. comment_post(user_id, post_id, comment, timestamp) -> success/failure

    3. like_post(user_id, post_id, timestamp) -> success/failure

    4. get_follow_feed(user_id, timestamp) -> list of newest posts from user follow list, ordered by time, limit 20

    5. get_profile_feed(user_id, user2_id, timestamp) -> list of newest posts from user2, ordered by time, limit 20


    系统架构

    发帖

    信息流

    进一步细化

    发帖

    信息流


    延伸阅读:

    1. Instagram Engineering: https://medium.com/@InstagramEng

    2. Instagram System Design: https://youtu.be/da7mdMz0g0g

    3. Designing Instagram: https://www.educative.io/courses/grokking-the-system-design-interview/m2yDVZnQ8lG

    4. Design Photo Sharing Platform - Instagram: https://techtakshila.com/system-design-interview/chapter-4/

    5. Designing Instagram: https://www.codercrunch.com/design/634265/designing-instagram

    6. Designing Instagram Architecture: https://nlogn.in/designing-instagram-architecture-system-design/

    7. System Design Analysis of Instagram: https://towardsdatascience.com/system-design-analysis-of-instagram-51cd25093971

    References:
    [1] Instagram System Architecture: https://medium.com/interviewnoodle/instagram-system-architecture-fdbec22e48ee

    你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。
    微信公众号:DeepNoMind

    秒杀系统从零打造秒杀系统:防止超卖

    前言

    大家好,好久不发文章了。(快一个月了- -)最近有很多学习的新知识想和大家分享,但无奈最近项目蛮忙的,很多文章写了一半搁置在了笔记里,待以后慢慢补充发布。

    本文主要是通过实际代码讲解,帮助你一步步搭建一个简易的秒杀系统。从而快速的了解秒杀系统的主要难点,并且迅速上手实际项目。

    我对秒杀系统文章的规划:

    • 从零开始打造简易秒杀系统:乐观锁防止超卖

    • 从零开始打造简易秒杀系统:令牌桶限流

    • 从零开始打造简易秒杀系统:Redis 缓存

    • 从零开始打造简易秒杀系统:消息队列异步处理订单

    秒杀系统

    秒杀系统介绍

    秒杀系统相信网上已经介绍了很多了,我也不想黏贴很多定义过来了。

    废话少说,秒杀系统主要应用在商品抢购的场景,比如:

    • 电商抢购限量商品

    • 卖周董演唱会的门票

    • 火车票抢座

    秒杀系统抽象来说就是以下几个步骤:

    • 用户选定商品下单

    • 校验库存

    • 扣库存

    • 创建用户订单

    • 用户支付等后续步骤…

    听起来就是个用户买商品的流程而已嘛,确实,所以我们为啥要说他是个专门的系统呢。。

    为什么要做所谓的“系统”

    如果你的项目流量非常小,完全不用担心有并发的购买请求,那么做这样一个系统意义不大。

    但如果你的系统要像12306那样,接受高并发访问和下单的考验,那么你就需要一套完整的流程保护措施,来保证你系统在用户流量高峰期不会被搞挂了。(就像12306刚开始网络售票那几年一样)

    这些措施有什么呢:

    • 严格防止超卖:库存100件你卖了120件,等着辞职吧

    • 防止黑产:防止不怀好意的人群通过各种技术手段把你本该下发给群众的利益全收入了囊中。

    我们先从“防止超卖”开始吧

    毕竟,你网页可以卡住,最多是大家没参与到活动,上网口吐芬芳,骂你一波。但是你要是卖多了,本该拿到商品的用户可就不乐意了,轻则投诉你,重则找漏洞起诉赔偿。让你吃不了兜着走。

    不能再说下去了,我这篇文章可是打着实战文章的名头,为什么我老是要讲废话啊啊啊啊啊啊。

    上代码。

    说好的做“简易”的秒杀系统,所以我们只用最简单的SpringBoot项目

    建立“简易”的数据库表结构

    一开始我们先来张最最最简易的结构表,参考了crossoverjie的秒杀系统文章。

    等未来我们需要解决更多的系统问题,再扩展表结构。

    一张库存表stock,一张订单表stock_order

    -- ----------------------------
    -- Table structure for stock
    -- ----------------------------
    DROP TABLE IF EXISTS `stock`;
    CREATE TABLE `stock` (
      `id` int(11unsigned NOT NULL AUTO_INCREMENT,
      `name` varchar(50NOT NULL DEFAULT '' COMMENT '名称',
      `count` int(11NOT NULL COMMENT '库存',
      `sale` int(11NOT NULL COMMENT '已售',
      `version` int(11NOT NULL COMMENT '乐观锁,版本号',
      PRIMARY KEY (`id`)
    ENGINE=InnoDB DEFAULT CHARSET=utf8;

    -- ----------------------------
    -- Table structure for stock_order
    -- ----------------------------
    DROP TABLE IF EXISTS `stock_order`;
    CREATE TABLE `stock_order` (
      `id` int(11unsigned NOT NULL AUTO_INCREMENT,
      `sid` int(11NOT NULL COMMENT '库存ID',
      `name` varchar(30NOT NULL DEFAULT '' COMMENT '商品名称',
      `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
      PRIMARY KEY (`id`)
    ENGINE=InnoDB DEFAULT CHARSET=utf8;

    通过HTTP接口发起一次购买请求

    代码中我们采用最传统的Spring MVC+Mybaits的结构

    结构如下图:

    【秒杀系统】从零打造秒杀系统(一):防止超卖

    Controller层代码

    提供一个HTTP接口: 参数为商品的Id

    @RequestMapping("/createWrongOrder/{sid}")
    @ResponseBody
    public String createWrongOrder(@PathVariable int sid) {
        LOGGER.info("购买物品编号sid=[{}]", sid);
        int id = 0;
        try {
            id = orderService.createWrongOrder(sid);
            LOGGER.info("创建订单id: [{}]", id);
        } catch (Exception e) {
            LOGGER.error("Exception", e);
        }
        return String.valueOf(id);
    }

    Service层代码

    @Override
    public int createWrongOrder(int sid) throws Exception {
        //校验库存
        Stock stock = checkStock(sid);
        //扣库存
        saleStock(stock);
        //创建订单
        int id = createOrder(stock);
        return id;
    }

    private Stock checkStock(int sid) {
        Stock stock = stockService.getStockById(sid);
        if (stock.getSale().equals(stock.getCount())) {
            throw new RuntimeException("库存不足");
        }
        return stock;
    }

    private int saleStock(Stock stock) {
        stock.setSale(stock.getSale() + 1);
        return stockService.updateStockById(stock);
    }

    private int createOrder(Stock stock) {
        StockOrder order = new StockOrder();
        order.setSid(stock.getId());
        order.setName(stock.getName());
        int id = orderMapper.insertSelective(order);
        return id;
    }

    发起并发购买请求

    我们通过JMeter(https://jmeter.apache.org/) 这个并发请求工具来模拟大量用户同时请求购买接口的场景。

    注意:POSTMAN并不支持并发请求,其请求是顺序的,而JMeter是多线程请求。希望以后PostMan能够支持吧,毕竟JMeter还在倔强的用Java UI框架。毕竟是亲儿子呢。

    如何通过JMeter进行压力测试,请参考下文,讲的非常入门但详细,包教包会:

    https://www.cnblogs.com/stulzq/p/8971531.html

    我们在表里添加一个Iphone,库存100。(请忽略订单表里的数据,开始前我清空了)

    【秒杀系统】从零打造秒杀系统(一):防止超卖

    在JMeter里启动1000个线程,无延迟同时访问接口。模拟1000个人,抢购100个产品的场景。点击启动:

    【秒杀系统】从零打造秒杀系统(一):防止超卖

    你猜会卖出多少个呢,先想一想。。。

    答案是:

    卖出了14个,库存减少了14个,但是每个请求Spring都处理了,创建了1000个订单。

    【秒杀系统】从零打造秒杀系统(一):防止超卖

    我这里该夸Spring强大的并发处理能力,还是该骂MySQL已经是个成熟的数据库,却都不会自己锁库存?

    避免超卖问题:更新商品库存的版本号

    为了解决上面的超卖问题,我们当然可以在Service层给更新表添加一个事务,这样每个线程更新请求的时候都会先去锁表的这一行(悲观锁),更新完库存后再释放锁。可这样就太慢了,1000个线程可等不及。

    我们需要乐观锁。

    一个最简单的办法就是,给每个商品库存一个版本号version字段

    我们修改代码:

    Controller层

    /**
     * 乐观锁更新库存
     * @param sid
     * @return
     */

    @RequestMapping("/createOptimisticOrder/{sid}")
    @ResponseBody
    public String createOptimisticOrder(@PathVariable int sid) {
        int id;
        try {
            id = orderService.createOptimisticOrder(sid);
            LOGGER.info("购买成功,剩余库存为: [{}]", id);
        } catch (Exception e) {
            LOGGER.error("购买失败:[{}]", e.getMessage());
            return "购买失败,库存不足";
        }
        return String.format("购买成功,剩余库存为:%d", id);
    }

    Service层

    @Override
    public int createOptimisticOrder(int sid) throws Exception {
        //校验库存
        Stock stock = checkStock(sid);
        //乐观锁更新库存
        saleStockOptimistic(stock);
        //创建订单
        int id = createOrder(stock);
        return stock.getCount() - (stock.getSale()+1);
    }

    private void saleStockOptimistic(Stock stock) {
        LOGGER.info("查询数据库,尝试更新库存");
        int count = stockService.updateStockByOptimistic(stock);
        if (count == 0){
            throw new RuntimeException("并发更新库存失败,version不匹配") ;
        }
    }

    Mapper

    <update id="updateByOptimistic" parameterType="cn.monitor4all.miaoshadao.dao.Stock">
        update stock
        <set>
          sale = sale + 1,
          version = version + 1,
        </set>
        WHERE id = #{id,jdbcType=INTEGER}
        AND version = #{version,jdbcType=INTEGER}
      </update>

    我们在实际减库存的SQL操作中,首先判断version是否是我们查询库存时候的version,如果是,扣减库存,成功抢购。如果发现version变了,则不更新数据库,返回抢购失败。

    发起并发购买请求

    这次,我们能成功吗?

    再次打开JMeter,把库存恢复为100,清空订单表,发起1000次请求。

    这次的结果是:

    卖出去了39个,version更新为了39,同时创建了39个订单。我们没有超卖,可喜可贺。

    【秒杀系统】从零打造秒杀系统(一):防止超卖

    由于并发访问的原因,很多线程更新库存失败了,所以在我们这种设计下,1000个人真要是同时发起购买,只有39个幸运儿能够买到东西,但是我们防止了超卖。

    手速快未必好,还得看运气呀!

    OK,今天先到这里,之后我们继续一步步完善这个简易的秒杀系统,它总有从树苗变成大树的那一天!

    源码

    我会随着文章的更新,一直同步更新项目代码,欢迎关注:

    https://github.com/qqxx6661/miaosha

    参考

    • https://cloud.tencent.com/developer/article/1488059

    • https://juejin.im/post/5dd09f5af265da0be72aacbd

    • https://crossoverjie.top/%2F2018%2F05%2F07%2Fssm%2FSSM18-seconds-kill%2F

    关注我

    我是一名后端开发工程师。

    主要关注后端开发,数据安全,物联网,边缘计算方向,欢迎交流。

    各大平台都可以找到我

    • Github:@qqxx6661

    • CSDN:@Rude3knife

    • 知乎:@后端技术漫谈

    • 简书:@蛮三刀把刀

    • 掘金:@蛮三刀把刀

    原创博客主要内容

    • 后端开发技术

    • Java面试知识点

    • 设计模式/数据结构

    • LeetCode/剑指offer 算法题解析

    • SpringBoot/SpringCloud入门实战系列

    • 数据分析/数据爬虫

    • 逸闻趣事/好书分享/个人生活

    个人公众号:后端技术漫谈

    公众号:后端技术漫谈.jpg

    如果文章对你有帮助,不妨收藏,转发,在看起来~


    以上是关于从零打造 Instagram的主要内容,如果未能解决你的问题,请参考以下文章

    从零打造一个Web地图引擎

    从零打造一个Web地图引擎

    从零开始分布式数据库中间件打造读写分离架构

    利用Instagram版权功能构造CSRF漏洞删除其他用户文件

    使用 Instagram/Facebook API 访问其他用户的公共数据

    从零打造树莓派智能助手——让树莓派说话