【系统设计】从服务到架构

新的系列,参考了九章算法的系统设计讲解

文章目录

  • 系统设计面试的规范
    • 4S分析法
  • 系统设计——推特设计之信息流 newsfeed
    • storage存储-PUll model(下拉)
    • storage存储-PUSH model(发送)
    • 模型对比
      • 升级
      • 明星效应
    • 关注与取关
    • 存储点赞
  • 设计用户系统——理解数据库和缓存
    • 不同的QPS对存储系统的影响
    • 用户系统的特点
    • 好友关系存储
    • 防止单点失效(切片,备份)
    • 一致性哈希算法
    • 备份与复制
  • 系统设计————设计短网址系统
    • 短网址算法
    • 优化(如何加速)
      • 自定义URL
      • 是否需要保证长短网址一一对应?
      • 301还是302跳转
  • 系统设计——地理位置信息服务
    • 如何存储查询地理位置信息
    • 实现GeoHash算法
  • 系统设计——实时聊天系统
    • 用户发送与接受消息的流程
  • 系统设计————如何进行限流
    • google的Ratelimiter源码
  • 系统设计————datadog,网站访问数据统计
  • 系统设计——网页爬虫系统
    • 具体实现
    • 升级策略
  • 系统设计——拼写联想
  • 分布式文件系统(GFS)
    • 真题
  • Map Reduce(大数据场景下的计数)
    • Map Reduce的系统设计
  • 分布式数据库Bigtable
    • 完整系统的读写过程
    • 集群的Bigtable+GFS

系统设计面试的规范

可行解25%,特定场景下的特殊问题20%,分析能力25%,权衡15%,知识储备15%。

按照我对校招生的要求的理解,首先在这类问题中考察我们的交流能力,能不能理解面试的想法,能不能清楚的表达自己的想法。其次是看个人的思考问题的能力,包括理解问题的场景,抽象一个问题,提出一个可行解。尽量快速的抓住问题的核心。进一步升级的话,还应该尽可能表现出来自己在思考问题时候的全面性,可行解不是完美的,我们可以进一步分析这个方法的优缺点。并且给出自己的升级策略。这里就是权衡和知识储备的作用了。

4S分析法

  • Scenario场景:分析清楚需要在什么场景下,需要什么服务。QPS、DAU、Interface
  • Service服务:大的系统拆分出来核心模块。
  • Storage存储:数据的存储。这里设计到面向对象的设计,如何设计表,采用什么样的结构存储数据。Data、SQL、Nosql、File System
  • scale升级:可拓展性,当前可行解有什么问题,如何升级。(升级、维护-鲁棒性,单点故障问题;拓展性,应对流量暴增)

场景分析:给出一些参考,twitter的日活用户大概是150M+。选择一些核心的服务,post a tweet, TimeLine, News Feed等。这里自己可以进行一些合理的估计,包括并发用户数量(日活*每个用户平均请求数量/一天秒数),峰值(均值的三倍),读写频率(300k,5k)

在这里插入图片描述

服务的核心是replay和merger,为每个需求设计服务,并且合并相同的服务。

存储:关系型数据库:适合用户信息等;非关系型:适合推文,社交图谱等;文件系统:适合图片视频等。这里也需要合理设计表。

系统设计——推特设计之信息流 newsfeed

类似的需求还存在与微博,朋友圈等。新鲜事系统的核心是关注和被关注的关系,以及每个人看到的都是不一样的。

storage存储-PUll model(下拉)

  • 思路:采用了K路合并的思路,在用户要查看的时候,获取用户全部关注的前100条,然后进行合并,得到topK。类似K路归并。

  • 复杂度分析:在用户获取信息流时候很慢,需要进行N次数据库的读取;发送时候很快,直接一次DB写

  • 缺点:每次看新的事件流时候都是比较慢的。需要从数据库中读取排序。用户体验稍差

storage存储-PUSH model(发送)

  • 思路:每个用户都建立一个list存储每个人的feed news。用户发送推文以后,发送到每个用户的News feed中。(关键词,fanout,扇出)。这样只需要读取前100个消息就可以。
  • 复杂度分析:读消息很快,一次DB读取。发送推文比较麻烦,N个粉丝就需要进行N次DB write。但是这一步可以异步进行,无需等待。
  • 缺陷:不及时,因为followers可能很多。存在延迟,对于明星的社交网络可能是个问题。

模型对比

Facebook-pull; Twitter-pull; Ins-Push+pull 可见pull方法是主流。因为对于应用来说,用户体验是核心,要考虑延迟和僵尸粉问题等问题

push适合双向关注,朋友圈这种。粉丝不大,实时性不高,占用资源很少。pull适合实时性高,单向关注有明星问题,微博。

升级

  • PULL方法:在DB之前加入缓存,对于一些用户的推文进行cache。缓存维护timeline某个用户的消息队列。这样N次BD就变成了N次cache请求。并且可以进行权衡,采用LRU等淘汰过期的缓存。缓存每个用户的News Feed,直接维护用户的new feed。
  • PUSH方法:push模型将NewsFeed模型放在内存中。这种的成本其实并不高。

明星效应

对于热点明星,扇出压力很大。可以短时间内加服务器解决。这里不要上来就改模型,这样成本很大。

  • push+pull:普通用户依然push,明星用户采用pull的方法。明星用户维护自己的timeline,粉丝需要自己来曲。

关注与取关

Follow 一个用户之后,异步地将他的 Timeline 合并到你的 News Feed 中。Unfollow 一个用户之后,异步地将他发的 Tweets 从你的 News Feed 中移除。异步可以让用户快速得到反馈,缺点是可能延迟效果。

存储点赞

在这里插入图片描述

设计用户系统——理解数据库和缓存

设计用户系统,包括实现注册,登录、用户信息查询等。难点在于数据的存储,包括了好友关系的存储

4S分析

  • 场景:注册、登录、查询、用户信息修改。其中查询是最大的需求,这要支持的QPS最高。
  • 服务:注册登录模块,查询用户信息的模块,负责好友关系的存储

不同的QPS对存储系统的影响

  • MySQ等SQL数据库:大概可以支持到1K的qps
  • MongoDB等硬盘型Nosql可以支持到10K的级别
  • Redis/Memcached 内存型数据库 100k的性能

用户系统的特点

读非常多,写非常少。对于一个读多写少的系统,一定要考虑使用Cache优化。给人用的系统都是都多写少,给机器用大概率是写多读少

对于缓存的使用:一定要注意先在db中get,然后从cache中set。删除时候先从cache删除,再去DB中更新。

保证用户登录:使用cookie或者session。用户向服务器发送访问都会带上自己的cookie。

在这里插入图片描述

这个Session Table可以存放在,cache中也可以DB中。如果用户非常多,可以使用Cache优化。但是需要警惕全部放在cache中可能导致掉电以后大规模重新登录,数据库压力陡增。

好友关系存储

可以是不重复存储,给用户编号,小号在前大号在后;也可以是重复存储,A的好友有B,B也有A。

如果需要事务,一般不用cache。
在这里插入图片描述

防止单点失效(切片,备份)

数据拆分是保证某个库挂了,也不会导致网站百分百不可用。可以采用分库分表。纵向拆分其实就是按照业务进行拆分,把大表拆开成为小表,对于一些热点数据尽量使用小表。防止锁问题。横向切分是考虑一些巨大的表的拆分,这里的关键在于分表键的选择和分表的方法。分表键一般需要与业务的应用场景相关,分表方法包括简单取模等,粗暴的方法存在热点数据、不容易拓展等问题,比较好的方法是采用一致性哈希。这样可以最大程度降低在拓展之后需要重新分表的麻烦。

数据备份就是考虑主从备份,集群。还可以实现主库写,从库读,降低压力。

一致性哈希算法

将整个hash区间看成一个环,环的大小为 [ 0 , 2 64 − 1 ] [0, 2^{64}-1] [0,2641]。并且引入了虚拟节点的概念(Virtual nodes)的概念。每个实体机器对应1000个环上的点。当我们添加一个数据时候,我们计算key的hash值对应到环上一个点。顺时针找到第一个虚拟节点存储。当新加入一台机器进行数据迁移时候,1000个virtual node都向各自的顺时针的第一个虚拟节点要数据。

  • 为什么是1000个点?

因为如果是一个点,很容易不均匀,比如我们只有三台服务器,撒3000个点肯定比3个点更均匀。为什么不更多呢?这里存在一个trade of,我们是需要存储下来环上全部虚拟节点的位置的,存储的形式是使用TreeMap,本质上就是使用红黑树,可以在 O ( l o n g n ) O(longn) O(longn)的时间内快速的找到大于当前数据hash值的第一个虚拟节点。因此太多的节点数量会增加存储的成本。

备份与复制

备份一般是周期性的,复制一般是实时的。我们以Mysql的master-slave模型为例子。原理是Write Ahead Log也就是sql的主库的任何操作都会在log中记录,然后会将这个log通过一个专用的io线程发送给slave节点。当然这里需要考虑主从延迟的问题

系统设计————设计短网址系统

短网址系统就是将一个比较长的网址比如www.baidu.com映射成为http://goo.gl/2KEEaJ

  • 场景:实现长url到短url的相互转换。并且可以输入短url以后正确的跳转到长url去。
  • 需求:QPS+Storage 同样根据DAU可以估测出来QPS,并且考虑峰值的情况,选择合适的存储系统。可以根据URL需要的存储来计算需要的硬盘空间。
  • 服务:只有一个简单的URL转换
  • 存储:sql和nosql都可以。

短网址算法

  1. 直接使用hash函数的后6位(不可行)。虽然非常快,但是会存在碰撞无法解决。
  2. 随机生成一个短URL,使用数据库去重。实现非常简单,但是后期随着短网址的增加会变慢严重。
  3. 进制转换BASE62。可以使用的URL包括(0-9, a-z, A-Z),一共62个。可以变为62进制。每个URL对应一个整数,整数就是数据库表的主键,是自增的。这样6位数可以存放570亿个内容。优点是效率高,缺点是依赖于自增主键,只能使用sql数据库。

如果采用随机生成的方法。如果使用SQL数据库,我们需要分别对两个url建立索引,便于快速查找。或者建立两张nosql数据库的表,分别存放对应的键值对。

如果采用进制转换的方法。必须使用有自增主键的sql数据库。只需要有主键和long URL就可以,短URL可以从主键转换得到。

优化(如何加速)

首先可以使用缓存层加速读效率,存放常用的转换的长短URL这里可以使用使用一些淘汰算法。

其次可以使用地理位置信息提速,优化服务器的访问速度,通过DNS解析不同的地区用户到不同服务器,更进一步的,可以将中国网址存放在中国数据库中,美国网址存放在美国数据库中。

我们还需要解决如何扩容的问题,这个系统设计瓶颈在于忙不过来,而不是存不开。因此我们可以采用水平分表的方法,但是这就设计到分表键的选取。存在矛盾的因素,选取ID作为分表键就无法很快的查询long URL在那个分数据库上,只能采用广播的方法了。比较好的方法就是引入一个标识位,比如AB123->0AB123这个0是根据(Hash(long_URL)%62)得到的,这样可以很快的找到了。同时,对于分库的情况,我们还需要用到一致性哈希

对于分表以后的全局自增ID,可以专门使用一个数据库实现自增ID的操作,或者使用ZooKeeper。其实这也未必是单点的,可以拆分为分单号的,分双号的等。

自定义URL

我们需要新建一个CustomURLTable,存放我们希望自己定义的简写,而不是在原来的表中加一列,这会导致大量的空洞。

是否需要保证长短网址一一对应?

短网址对应的长网址肯定是需要对应的,但是长网址对应的短网址完全没必要。我们可以使用简单的kv对,设置过期时间是24h。长网址过来时候都先去缓存中检查,如果有就返回,否则给一个新的短网址就可以了。

301还是302跳转

这也是一个有意思的话题。首先当然考察一个候选人对301和302的理解。浏览器缓存机制的理解。然后是考察他的业务经验。301是永久重定向,302是临时重定向。短地址一经生成就不会变化,所以用301是符合http语义的。同时对服务器压力也会有一定减少。

但是如果使用了301,我们就无法统计到短地址被点击的次数了。而这个点击次数是一个非常有意思的大数据分析数据源。能够分析出的东西非常非常多。所以选择302虽然会增加服务器压力,但是我想是一个更好的选择。

当然我们一般是不让短网址过期的。

系统设计——地理位置信息服务

很多的app都是需要使用到位置信息,然后统计距离你当前位置最近的一些信息反馈给用户。典型的类型就是滴滴,大众点评这类本地生活类app。

  • 场景:系统需要实时了解到司机的位置,也就是需要由司机位置的上报;在用户请求打车的时候,需要根据用户当前的位置去匹配附近最近的司机。从中可以分析出来,uber应该是一个写密集的需求,因为我们需要每4s同步一次司机的位置。假设由200k司机,每秒的写请求就是50k,峰值可以达到150k。对于存储,如果每条location都保存,200k86400/4100bytes = 0.5T每天。如果只记录当前的位置,大概也就200k*100bytes=20M;
  • 服务:需要包括两个大的模块,一个是位置的记录,一个是位置的匹配。
  • 存储:显然对于位置的存储时高写的,对于订单的查询是高读的。并且对于地理信息的存储比较简单,可以用kv键值对即可,这里可以使用高速的redis实现。对于订单信息的查询因为比较表会比较复杂,这里使用sql数据库比较好。

在这里插入图片描述

对于Sql中订单表和司机表的OOD设计可以如下图。
在这里插入图片描述

如何存储查询地理位置信息

两个比较常用的算法,一个是google S2算法,采用了希尔伯特曲线,将地址空间映射为了 2 64 2^{64} 264个整数。空间上相近的两个点,一般情况下整数也比较相近。这个方法的库函数相对丰富,且更为精准。另外一个方法是使用GeoHash,算法的思路将地理位置转换为字符串,两个字符串匹配的越多,两个点一般越接近。算法是二分地图,具体代码实现下面有实现。

对于地理位置存储可以使用sql数据库,因为我们经常查找,因此需要对geohash建立索引,并且使用LIKE进行查询。但是这样是不怎么符合sql的使用军规的,首先建立索引的列不应该经常进行插入操作,会导致索引空洞的出现,浪费空间。其次,LIKE查询是相对比较慢的,一般不建议使用。

既然我们是写频繁的操作,我们可以使用redis建立简单的键值对,key是位置,value是司机编号的set。比如 Driver 的位置如果是 9q9hvt,则存储在 9q9hvt, 9q9hv, 9q9h 这 3 个 key 中就可以,因为四个位置重复的精度是20km了已经,你不会打车20km以上的车辆。对于6位一致,已经是一公里以内了,也足够精确了。如果匹配成功(距离够近,司机空闲,司机统一接单),系统返回司机的编号。然后再去数据库或者另外一个redis表中,查询司机的具体经纬度信息即可。也就是我们既需要从地理位置得到附近的司机的位置,也需要知道司机的id得到位置

在这里插入图片描述

  • 拓展:解决单点故障问题,通过多台redis可以解决的性能的瓶颈。当然也需要合理的分摊流量,一个比较好的思路是根据城市进行划分。其实就是判断一个点在不在多边形内的一个数学问题。如何判断用户在不在机场区域打车,可以先判断城市,然后再耳机内容中查找用户是否位于促销区域。

实现GeoHash算法

Base32,0-9,a-z去除(a,i,l,o)。这里采用的其实是皮亚诺曲线进行覆盖。在转换为字符串的时候,一般是交叉经纬度信息。之所以选择32位因为这个数字正好是2的5次方。在保证精度的情况下也利于计算。

由于GeoHash是将区域划分为一个个规则矩形,并对每个矩形进行编码,这样在查询附近POI信息时会导致以下问题,比如红色的点是我们的位置,绿色的两个点分别是附近的两个餐馆,但是在查询的时候会发现距离较远餐馆的GeoHash编码与我们一样(因为在同一个GeoHash区域块上),而较近餐馆的GeoHash编码与我们不一致。距离相近但是编码不一样的情况往往产生在边界处。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ygcfmm9z-1622815449688)(https://images0.cnblogs.com/blog/522490/201309/09190137-edd3b1fe3d754c5d836e2812ac298674.png)]

解决这个问题,我们就需要所其周边八个方格也考虑上,将自身方格和周边八个方格内的点都遍历一次,再返回符合要求的点。那么如何知道周边方格的前缀呢?仔细观察相邻方格,我们会发现两个小方格会在 经度或纬度的二进制码上相差1;我们通过 GeoHash 码反向解析出二进制码后,将其经度或纬度(或两者)的二进制码加一,再次组合为 GeoHash 码。

我们已经知道现有的GeoHash算法使用的是Peano空间填充曲线,这种曲线会产生突变,造成了编码虽然相似但距离可能相差很大的问题,因此在查询附近餐馆时候,首先筛选GeoHash编码相似的POI点,然后进行实际距离计算。

public class GeoHash {
    /*
     * @param latitude: one of a location coordinate pair 
     * @param longitude: one of a location coordinate pair 
     * @param precision: an integer between 1 to 12
     * @return: a base32 string
     */
    private String _base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; // base32算法
    public String encode(double latitude, double longitude, int precision) {
        // write your code here
        // 实现GeoHash算法 涉及到的函数包括地理位置的转换为二进制,二进制转换为字符串
        // 然后根据字符串的匹配程度可以知道位置的误差信息
        //String _base32 = "0123456789bcdefghjkmnpqrstuvwxyz"; // base32算法
        String x = getBin(latitude, -90.0, 90.0);
        String y = getBin(longitude,-180.0, 180.0);
        StringBuffer sb = new StringBuffer();
        for (int i = 0;i<30 ;i++){
            sb.append(y.charAt(i));
            sb.append(x.charAt(i));
        }
        StringBuffer ans = new StringBuffer();
        String res = sb.toString();
        for (int i = 0;i<60;i+=5){
            ans.append(_base32.charAt(Integer.parseInt(sb.substring(i,i+5),2)));
        }
        return ans.toString().substring(0,precision);
    }

    private String getBin(double position, double left, double right){
        StringBuffer sb = new StringBuffer();
        // 这里为什么是30呢?因为我们最终的字符串的长度是12,因此二进制需要有12*5=60 
        // 因为是由经纬度拼接成的,所以各自都需要时30
        for(int i = 0;i<30;i++){
            double mid = (left+right)/2.0;
            if (position<=mid){
                sb.append("0");
                right = mid;
            }else{
                sb.append("1");
                left = mid;
            }
        }
        return sb.toString();
    }
    public double[] decode(String geohash) {
        // write your code here
        //String _base32 = "0123456789bcdefghjkmnpqrstuvwxyz";
        int[] mask = {16, 8, 4, 2, 1};
        double[] x = {-180, 180};
        double[] y = {-90, 90};
        boolean is_even = true;
        for (int i = 0; i < geohash.length(); i++){
            int index = _base32.indexOf(geohash.charAt(i));
            for(int j = 0; j<5;j++){ // j之所以是5,因为我们还是32个base
                if (is_even){
                    refine_interval(x, index, mask[j]);
                }else{
                    refine_interval(y, index, mask[j]);
                }
                is_even = !is_even;
            }
        }
        double[] location = {(y[0]+y[1])/2.0, (x[0]+x[1])/2.0};
        return location;
    }

    private void refine_interval(double[] pos, int val, int mask){
      // 判断当前位是向左还是向右
      if((val & mask) != 0){
          pos[0] = (pos[0] + pos[1])/2.0;
      }else{
          pos[1] = (pos[0] + pos[1])/2.0;
      }
    }
}

系统设计——实时聊天系统

基本问题很明显,就是设计一个微信的类型设计。

  • 场景和主要功能:用户之间互相发送消息,群聊,用户状态设置。Facebook的数据是10亿月活用户,日活数据大概是75%,因此可以有7500万日活。因此我们需要设计百万级别的系统,假设一个用户每天发20条信息,QPS=100M*20/86400~20K。存储,每个记录大约30bytes的话,一天2B条消息,大概60G存储。
  • 服务:message service 信息管理,负责信息的基础发送和存储;RealTime service,主要负责信息的读取操作。
  • 存储:

对于一个聊天软件,信息的存储是关键。正常的思路是表中只保留,from_user_id 发送方id, to_user_id 接收方id, content 内容, created_time 创建时间。但是这个设计存在明显的缺点,比如我们查询两个人的对话,需要SELECT * FROM message_table WHERE from_user_id=A and to_user_id=B OR to_user_id=B and from_user_id=A ORDER BY created_at DESC;

这个查询显然是非常低效的,并且如果是多人群聊,这个方法就很难了。对于微信这类应用,任何交互都是有收有发的双向,因此比较好的思路定义一下对话,引入sessionId,标识每个对话框。我们可以设计一个Message table 这个表格是针对整个app的全部用户的消息的存储。额外的对于每个人都有一个Thread table。两个表通过thread id进行连接,这样设计的好处,对于message table 我们可以很容易的查询到用户的全部发言信息。对于用户进行检索某个对话的信息时候,肯定是会指定Thread id这个参数的,因此我们查询的时候也是可以很容易的查询到某个对话的信息。并且对于用户的对话设置,其实包括了很多私人设置,比如对于聊天的备注和静音设置等。也就是说,所有用户发送的信息是共用的一张表,而所有用户的对话是另外一张表。

在这里插入图片描述

对于message 表分表键最好是thread id,正如我们前面说到的,每次的查询的时候一般都会指定对话id。这是可以作为分表的(分表键的设定一定要从业务侧考虑,绝大多数进行查询的时候,我们会携带哪些参数进行,这个参数就适合作为分表键)。对于thread table, owner_id更适合作为分表键,因为这个一般提供给用户的,用户只需要查询“我有哪些对话”。对于这个表的主键应该是[owner_id, thread_id],对这个建立索引可能比较好。同时我们还需要对Owner ID+update time建立索引,因为我们一般提供给用户的都是按照更新时间排序的。

对于message 表,数据量比较大且比较简单,不需要自增id,每条消息就是一个log,可以使用可持久化的nosql,将thread id作为key,value可以是{user_id, content, created_time}的json。对于Thread 表,因为需要使用索引,且是经常进行查找的筛选的,使用sql比较好。

用户发送与接受消息的流程

  • 用户发送消息message service:

首先client把消息和消息接收者发送给server,这里是对于1vs1的对话。如果是多人聊天,一般会现在本机中检索是不是存在相应的session id,如果存在,直接发送session id给server,否则委托服务端建立一个(让服务端查询多个人之间是否存在一个session id是很麻烦的,需要对thread table 中的participants也建立索引,这个比较麻烦)。之后服务端创建一个message,包含了{user_id, content, created_time}。这里有一些比较巧妙的优化,ThreadId在创建时候可以有一些小技巧。比如对于两个人的对话,我们可以定义ID是两个人的ID拼合一起,这样更好查找。群聊ID可以是创建者+创建时间等。

  • 用户接受信息RealTime service:

比较简单方法是5s中用户问服务器查询一下有无信息。这样最简单,但是存在明显的延迟。这里可以引入socket这个概念,并且server提供push service,可以与client保持长连接。当用户打开app的时候,就连接到push service中一个属于自己的socket,有人发送消息的时候,message service收到消息并且通过push service发送出去。如果一个用户长时间不打开app,可以切断连接,释放端口。这个Android GCM和ios APNS都可以维护。socket和http的核心区别是,http只能客户端向服务器要数据,但是socket下服务器可以主动推送数据给客户端。从架构的角度,完全可以分离开信息发送与信息推送两个模块。

在这里插入图片描述

  • 用户之间的群聊

群聊的人数非常的多,每条消息都要发送给很多人。但是很多用户都是其实不在线的,只有push service模块知道用户是否在线。因此我们中间加一层,channel service,对于较大的群聊,在线用户需要先订阅到相应的群聊channel上。用户上线时候,找到用户所属的群聊进行标记,channel是知道哪些频道里的用户还存活的,用户下线的时候push service通知channel。因此发送消息的时候,message发送给channel,channel给在线用户进行push。这部分使用内存存储就可,挂了就重启。

在这里插入图片描述

  • 用户在线状态的查询
    服务器需要知道每个时刻,哪些用户在线;用户也需要知道哪些好友在线。同样可以分为push和pull两个思路。

push是用户告诉服务器自己的上线和下线。但是这个存在缺点是网络突然错误以后,用户没法给服务器告知。服务器每隔一段时间告诉全部用户,自己的哪些好友在线。

pull方法更好。还是采用心跳的策略,用户上线以后每几秒给服务发送一个心跳,告诉服务器自己存活。在线好友每过一段时间请求服务器查询自己的好友在线情况。因此这个方法也很容易知道用户下线多久了。

系统设计————如何进行限流

限流常出现在程序中,限制短时间内刷新网页的次数等。对于一般的业务,限流可以通过简单的维护数据库实现。但是对于秒杀业务,数据库的方法效率非常的底下,我们需要设计一个更好的策略。首先从工程实现的角度来说,google有开源的RateLimiter工具,采用了令牌桶的算法,可以有效限制输入的流量。当然这个模块其实也可以认为是一个小型的系统设计问题了,我们一样可以采用4S方法进行分析。

  • 场景:限制用户的行为,比如限制最近30s内的行为,超过了这个数量就进行限制。
  • 服务:本身就已经是一个最小的模块了,无法进一步细化。
  • 存储:这部分数据可以被用于分析网站的流量等。并且不要求非常强的一致性和准确性,但是对于读取写入的速率要求非常高。因此使用redis这种高效的存取结构就可以。

设计思路:

  1. 首先是一个比较简单的设计方法,我们使用redis的key的过期策略。比如我们要求限制某个操作在一分钟内不能超过10次操作,那我就使用当前系统时间的分钟作为key,设置过期时间为2分钟,保证完整的计算这一分钟内的次数。这个方法比较简单,我们需要什么样粒度的限制,就使用什么粒度作为key。但是并不完全精确,比如说我们可以想到,最极端的情况下可能在净时间出现两倍的流量。但是一般对于限流的要求并不要求准确。
  2. 另外一个策略是,使用最细粒度的时间作为key,比如秒级,过期时间是分钟级别,然后在统计分钟流量的时候,可以循环累计求和。这样可以很准确的得到最近一分钟内的流量。思路类似于不断的写一个循环数组。一般来说,我们要统计分钟级别的流量,那我们key就是秒级,过期时间是分钟级别。如果是天级别,key是小时级别,过期时间就是天级别。这样可以保证查询的时候,循环的次数不会太多。

当然这样会带来一些的误差。如果还需要在精确得到,我们可以采用多级查询的策略,比如我们希望得到最近一天的访问次数,当前时间是23:30:33,加和包括秒级23:30:00 ~ 23:30:33一共34次查询,在分的查询中23:00 ~ 23:29一共30次查询,在时的查询00 ~ 22,一共23次。并且加上昨日的秒级26次和分的29次,一共42次查询。

google的Ratelimiter源码

采用了令牌桶的设计思路,可以实现限制提交的线程数量,限制执行的数据量等。最有特色的地方是延迟等待的策略,也就是说请求的许可数量并不会影响对请求本身的控制。比如当前请求100个令牌,系统也会进行放行,但是后面的请求将会补偿等待。

详解RateLimiter

系统设计————datadog,网站访问数据统计

需要统计网站的流量,可以作为系统分析的数据。对于某个网页的访问,每访问一次加一,我们需要知道最后统计的次数。

  • 存储:这个是一个计数问题,也就是说系统几乎全部都是写,几乎没有读。并且我们是需要进行持久化的。并且应该功能非常简单,我们可以将操作的“名称+时间戳”作为key,每有一个访问就+1;但是需要注意的是,存储的粒度是多大。对于最近一周的数据,我们可能需要分钟级别的,本月内的需要十分钟级别的就可以,本年的需要小时级别就可以,去年的数据或许按照天就可以。因此我们可以借鉴多bucket的思路,每次进行整理汇总就可以。定期进行重新整理。

考虑到每次都进行检测,如果需要监控非常多的业务,每秒一次更新是qps非常大的,因此可以委托服务器每15s缓存一次数据,然后集中给datadog反馈一下。同时需要定期对数据进行Retention,进行瘦身。

系统设计——网页爬虫系统

爬虫系统可以帮助公司搜集信息,会使用到多线程,系统设计等知识。爬虫可以通过爬取网站的url地址的网页源代码得到相应的文字信息。对于爬虫系统,需要定期爬取全部的网页进行更新,并且需要的存储空间还比较多。

  • scenario:一万亿网页、每秒抓160w网页。每周都需要更新一次。存储空间每个网页10K,大概也需要10petabyte才能存得开。
  • service:包括爬取系统,taskservice,storageservice
  • storage:对于爬取任务可以使用db存储,但是爬取的数据可能只能使用BigTable才能存放的开。

在这里插入图片描述

具体实现

我们直接考虑多线程的设计,首先我们给出多个起始的门户网站作为起始,然后爬虫去这个url进行爬取的时候,读取到网页源代码,然后可以使用正则表达式等完成对于特定内容的爬取。比如我们希望爬取某个被

标签包围的内存,<h3[^>]*><a[^>]*>(.*?)<\/a><\/h3>。为了使用多线程,bfs的搜索方法更为方便。但是这里我们就不能使用queue进行分配任务了,因为queue是存储在内存中的,但是因为url太多这个是无法加载到内存的;并且url的队列不是很好的控制抓取的优先级等比如一些新闻网页可能我们希望更频繁的抓取。因此我们可以使用db存放一个task table这样可以更自定义的爬取。

在这里插入图片描述

升级策略

  • 如何控制抓取频率?可以每次抓取时候进行比对,如果网页内容发生了变化,就将抓取的频率提高一倍。如果没有发生任何变化,可以降低频率。从而动态调整更新的频率。
  • 如何解决爬取死链的问题? 门户网站的内容可能都是内部跳转,我们需要有一个限额,比如qq.com的限流需要10%。
  • 多地区爬虫分布。对于美国的网站从美国爬取,对于中国的网站从国内爬取。

系统设计——拼写联想

我们在输入发打字,或者搜索引擎的时候经常都会有联想功能。其实这个就是都某个特定前缀的topK查询。

  • 场景:一个DAU为500m的应用,每天的搜索量可以认为使用6次,每次大概4个字符。因此qps大概是4 * 6 * 500m / 86400 = 138K。预计峰值大概是2~3倍。
  • 服务:查询服务,数据收集服务。查询服务是根据用户的当前前缀去查询得到topK,数据收集需要考虑统计最近一段的时间的查询热词,然后排序存储。
  • 存储:对于查询服务,我们肯定是在内存中的,比较高效的可以使用缓存进行加速。然后一般的数据可以使用字典树的形式。完整数据的存储肯定是通过log data然后存储到了硬盘中。
    在这里插入图片描述

使用字典树可以很容易的查找当前的高频词是什么,这是一个空间换时间的思路。当然这个结构是存储在内容中的,可以认为是这部分应该分配给query service。最后通过序列化的方法放入硬盘中。

在这里插入图片描述

用户的搜索记录以log的形式进行了记录,可以使用dataCollectionService进行定期的整理。比如每一小时更新一次,然后将这些数据重新整合成一个新的trie树,然后进行热切换。

  • 拓展
  1. 性能评判:可以使用输入前缀以后,给出推荐词的时间作为一个指标;点击命中率也是一个指标。
  2. 如何提高相应时间?首先可以使用浏览器进行缓存,其次还有一个pre-fetch的思路。比如我输入ab时候,去查找ab的top10,以及这些词的top10,也就是100个数据。如此递进,这样我们大幅度的提高反应速度。
  3. 如何处理tire太大的情况。我们可以使用一致性哈希的方法,比如我们有多个queryService的机器,输入a的时候,一致性哈希指向机器1进行查找,返回答案。然后再输入ab时候,又到机器2进行查找。

在这里插入图片描述

  1. log文件占用的空间太大?可以使用概率的方法进行压缩log,每当有人查询了amazon,我就从1-1000之间进行一次随机。因此我们可以理解为我们使用了这种概率的方法进行记录。这样做的前提是只想要查询量很大的数据。

分布式文件系统(GFS)

google三剑客,GFS(google file system)文件系统,Map Reduce快速处理数据,Bigtable如何连接底层存储和上层数据。

常用的分布式文件系统是HDFS,是开源的一个分布式文件系统。但是主要是来自于GFS的演化。

  • 场景:用户需要写入和读取文件。支持写入的文件可以很大。同时我们需要可以使用多台机器存储这些文件,因此需要考虑到机器之间的协同。
  • 服务:是client+server的模型,我们客户端需要切分文件等。服务端需要完成存储。同时这里涉及到如何存储。

常用的模型包括peer2peer模型,没有中心节点,这样对造成一致性同步比较困难。好处是不存在瓶颈,节点挂了可以继续工作,难点是同步一致性。

Master-slave模型,有一个中心节点,其他的都是从属的。master节点只负责控制,不进行任何的存储操作;存储的操作全部交给子节点处理,非常类似于哨兵模式。优点在于容易保持数据一致性,缺点在于master节点成为了一个性能瓶颈。如果master挂了,立即重启就可以。

  • 存储:大的文件是存储在文件系统的。数据一般存放的只是一些比较小的信息文件。对于非常的大的文件可能需要切片等,最好是放在专门的文件系统。

一个大文件可能包括了,metadata-文件名,创建修改时间,大小等文件基础信息,和文件的具体内容。最好将两者分开存,metadata文件可以直接加载到内存中,因此这部分文件比较小,且经常被访问。而文件的主题在硬盘中存储,对于window是采用的连续存储,linux使用的更优的分开存储。

分开存储的优势在于可以解决空间碎片化的问题。大文件被切分为小的chunk,每一块的大小大约是64M。好处是可以减少metadata文件的大小,因为metadata中需要存放所有的chunk的相对位置,缺点会稍微有点碎片化问题。在使用master-slave模型以后,salve中存放的是真实数据,master只存放metadata,会标明每一个chunk存放的位置。并且offset也不用存放在metadata上,存放在对应的chunk就可以。

存放64M的文件大概metadata只有64B,这样对于一个非常大的文件,其实是可以把metadata读入内存的。

在这里插入图片描述

写入:是拆分写入的,拆分的过程在客户端完成。这样保证在传输的过程中出错重传的成本也比较小。在写入过程中,client首先于master交互,然后master可以根据磁盘的空闲情况,节点的繁忙情况等告诉client应该存放到那个slave上。client在与slave进行联系。
在这里插入图片描述

修改:GFS本身是不支持文件的修改的,真要是进行修改,其实就是重新在新的空间写一份。

读入:client与master进行交互,master告诉客户端应该从哪里进行读入那个片段,然后client在于slave进行读入。

也就是master的任务包括:存放全部文件的metadata,存储一个map(file name + chunk index -> chunk server),在读入和写入进行分配。

  • 拓展:

Q:单master是不是瓶颈:单master是工业届90%使用的策略,因为更简单的设计。如果需要也可以升级为多个master,paxos算法来协调多个master

Q:如何进行校验? 使用校验和,简单的包括MD5等。在每个chunk的片段尾部加上一个校验和。校验和很小,4bytes=32bit,对于一个1P的文件也就1P/64MB*32bit=62.5MB。在读入这个文件时候进行检查校验和,如果不一致,可以从其他的备份点上进行修正。

Q:如何检测一个slave节点在不在正常工作? slave节点定期进行心跳检测机制。

Q:写入过程如何首先备份?比较好的一个方法是,每次传输时候client只对一个slave节点进行写操作,然后这个slave 节点成为一个队长节点,负责给其他节点进行同步。这个思想就非常类似于哨兵+集群的设计思路了。队长节点的选择可以是地理位置最近的或者空闲的,这里的队长可以不是固定的,根据每次的client请求可以有不一样的队长。

真题

Q:设计一个只读的lookup service. 后台的数据是10 billion个key-value pair, 服务形式是接受用户输入的key,返回对应的value。已知每个key的size是0.1kB,每个value的size是1kB。要求系统qps >= 5000,latency < 200ms.

server参数:commodity server,8X CPU cores on each server,32G memory,6T disk。使用任意数量的server,设计这个service。

A:

total key size ~ 10 billion * 0.1kB = 1T;
total value size ~ 10 billion * 1kB = 10T。
所以每台服务器用两块硬盘,共12T。数据结构用SSTable(Sorted String Table)就好了。

充分利用内存,本来我想用binary search tree做index,但是仔细想想这个服务是只读的,而且硬盘存储键值对用的是SSTable是有序的,key和value长度又是固定的,所以直接把key以有序的方式存在内存就好了,查询的时候对key进行binary search,然后用key在内存中的offset来计算键值对在硬盘中的offset。1T/32G = 31.25. 所以一共需要32台服务器的内存分担key index。前面加一个master负责管理consistent hasing。lg(32G) = 35, 平均查询一个key就算18次内存访问,大约才1800ns,在ms这个量级上可以忽略。

每一次request,在硬盘上读取1kB value的时间:10ms(disk seek) + 4ms(rotation delay for 7200rpm) + 1kB/1MB * 30ms(reading 1kB sequentially from disk) = 14ms. 目前一台server能处理的的QPS: 1000ms/14ms = 71, 总的QPS: 71 * 32 = 2272。距离要求还有两倍多的差距。所以我们可以给每台server装上6个6T硬盘,组成3套数据硬盘,3套硬盘可以并行处理3个请求,这样也算是稍微利用了一下8X的多核CPU。这时QPS即为2272 * 3=6816.

延迟:

  • master内存查找consistent hashing map的时间:忽略
  • master与slave的round trip delay:1 round trip in the same data - center is 1ms.
  • slave内存index查询时间:忽略
  • slave硬盘读取时间:14ms

so total latency is 15ms。

Map Reduce(大数据场景下的计数)

谷歌大数据的三家马车 Map reduce,GFS,。

问题背景:统计所有网站中的词频。单一机器进行统计的话存在性能瓶颈,因此选择大数据的处理方式。多台机器负责map,reduce这两个过程。map负责统计一篇文章中的单词个数,reduce负责对特定的单词次数进行合并。

全过程可以划分为6个过程,input设定好输入文件,split系统完成文件的平均分配到机器,map实现文章切分为单词,传输整理,reduce实现单词统一,output设定输出文件。大数据的框架已经帮我们完成了基本的搭建,我们只需要实现map和reduce即可。注意两个函数的输入输出。map输入:key文章存储地址,value文章内容。reduce输入:key,map输出的key,value:map输出的value。

/**
 * Definition of OutputCollector:
 * class OutputCollector<K, V> {
 *     public void collect(K key, V value);
 *         // Adds a key/value pair to the output buffer
 * }
 */
public class WordCount {
    public static class Map {
        public void map(String key, String value, OutputCollector<String, Integer> output) {
            // key 对应这个文章的地址
            // value 对应文章的内容
            String[] tokens = value.split(" ");
            for(String word:tokens){
                output.collect(word,1);
            }
            // Output the results into output buffer.
            // Ps. output.collect(String key, int value);
        }
    }

    public static class Reduce {
        public void reduce(String key, Iterator<Integer> values,
                           OutputCollector<String, Integer> output) {
            int sum = 0;
            while(values.hasNext()){
                sum += values.next();
            }
            output.collect(key,sum);
            // Output the results into output buffer.
            // Ps. output.collect(String key, int value);
        }
    }
}

map和reduce的机器数量完成由自己决定。但是也未必是机器越多越好,机器增加可以提高数据处理的并行速度,但是需要考虑启动机器的成本,并且对于reduce是存在上限的(单词的种类的),一味的增加机器会导致master的压力也变大。

在词频统计的时候,是由一个stopwords列表的,这个列表的内容需要对一些高频无意义词的过滤,包括I,you,the等。在统计到这些词的时候直接进行跳过。

在这里插入图片描述

这里的排序只能使用外排序,因为数据量非常的大,因此无法加载到内存中直接进行排序。外排序的基本思路是首先对大的文件进行拆分到内存可以正常读入,然后再内存中对小文件完成排序,然后采用K路归并的策略进行合并。

典型的题目

  1. 倒排列表:统计某个单词都出现在了哪些文章中。这里就需要统计出来每篇文章中出现了哪些单词(map),然后按照单词进行分配(reduce)统计最后输出一个倒排表。
  2. 同类词:对众多的单词进行拆分,然后统计某个排序出现了哪些单词。

Map Reduce的系统设计

输入的文件和输出的文件都是需要放在GFS上的。中间的过程量只需要放在本地硬盘上就可,不用特殊处理。

在这里插入图片描述

  • Q:mapper和reducer的工作顺序?mapper需要完成了工作reducer才可以开始工作。
  • Q: 运行中如果mapper或者reducer挂了怎么办?我们再启动时候,其实由一个机器池,然后由master操控支配机器进行不同的工作。如果挂了就重新分配一台机器执行。
  • Q: 有一个key的统计数值非常大会导致什么问题?该如何解决? 会导致不平衡,有一个reducer的运行时间非常长。可以对这个key后面随机添加一个数字,相当于对这个key进行了划分,分为了若干块。

分布式数据库Bigtable

解决大数据场景下的Nosql数据库问题。文件系统一般是给一个地址,然后返回一个文件。而数据库系统是从按照某种规范的填写的文件(表)中得到某数据。核心在于查询数据。一般情况下,数据库系统建立在文件系统上,负责组织把一些数据存到文件系统,对外的接口比较方便操作数据。

  • 场景(需求):查询一个key,能够返回对应的value。相对来说场景很简单。数据的存储也并不会按照表的形式,而是可能会写为json格式等进行了序列化以后存放到文件里。

如何从硬盘的文件中进行查找?由于数据量巨大,因此无法全部读入内存中进行查找。一个可行的思路是有序的保存数据到硬盘上,然后在每个硬盘的文件上进行二分就可,这里设计到部分外排序的思路,硬盘二分的思路也是部分读入然后查找。

如何解决数据修改问题?直接从文件中修改时不可以的,因为无法保证修改以后的文件大小;读取整个文件进行复写也是不合适的,效率太低。可行的做法是,直接append在文件的最后面。但是这会导致一些新的问题,首先这部分数据因为是后期修改的,肯定是无序的,以及会有多个数据,不容易进行查找到正确的数据。只能过一段时间统一把文件进行有序整理,比如我们每次把添加的信息都先放在内存中,到了一定的大小以后再内存中进行一次排序,然后写入硬盘进行持久化。这样做的好处是每一块文件都是小块了,保证数据在每一块内部都是有序的,只有最后添加的信息在内存中是无序的。因此我们在查找数据的时候,for循环每一块硬盘然后再硬盘内进行二分。对于修改的数据,我们可以加上一个时间戳,保证争取的数据是最新的数据就可以。由于这样可能导致一些冗余的信息的存储,我们可以每隔一段时间进行一次K路归并整理数据。

完整系统的读写过程

数据写入过程中,内存中保存了每个文件块的起始的便宜地址,写入过程首先在内存中进行,这里使用了跳表的数据结构,然后序列化到硬盘上。保证硬盘是有序的。为了保证万一内存掉电,数据丢失,我们需要额外采用WAL策略。当然wal是需要写硬盘的,但是这个非常的快速。因此写入过程需要内存排序+一次硬盘统一写入+一次硬盘写log。

在这里插入图片描述

数据读出过程中,首先去内存中读,如果读到了直接返回最新的;否则去每个硬盘文件中进行二分。为了加快我们可以建立索引,每个文件都会自己建立一份索引。因为每个文件的内部都是有序的,但是文件之间未必是有序的,这索引是保存在内存中(如果太大也可以放到硬盘中,分批读入),借助索引可以快速定位到应该去某个文件块的某个位置读取。Sstable = Sorted String Table

在这里插入图片描述

如何快速检查某个数据在不在file中?如果某个文件不在file中其实就不需要去读取了,使用hashmap是可以的,但是对于大量的数据会占用更多的空间,采用布隆过滤器的方法更好,布隆过滤器是由多个hash函数,位数组构成的。对于一个key,每个hash函数都进行hash得到相应的数值,并且在位数组中对相应的位置置一。这样我们在判断的时候,判断相应的位置是不是全部为1就可以。因此可以看到布隆过滤器的特点,他说没有的一定没有,说有的可能存在误判。误判发生的概率与hash函数的个数,位数组的长度,加入的字符串的个数都有关系。

集群的Bigtable+GFS

如何实现集群?同样需要有一个master进行一致性哈希等控制操作。master指挥client去哪里进行读取写入数据。如果数据量继续增加,单机硬盘无法写开,可以在结合使用GFS。将bigtable的sstable拆分为一个个小的chunk然后写入GFS中。

在这里插入图片描述

我们一般将Bigtable的salve成为Tablet server,其实就是存储的tablet的从服务器。对于bigtable,因为存在修改操作我们需考虑竞争问题,因此我们需要引入分布式锁。比如谷歌使用的chubby或者hadoop使用的zookeeper。当用户进行读取的时候,请求会发送给lock,lock进行一致性哈希后会锁住相应的节点,然后返回节点信息给客户端去读写,在完成以后在放开锁资源。也就是说lock接管了部分原来master的服务,master现在的主要任务是检测slave的健康和启动时候进行sharding。其余的metadata都交给lock操作就可以。

因此整个系统需要Client + Master + Tablet Server + Distributed Lock。其中分布式锁需要更新metadata(index信息等),以及对key加锁解锁。

整个设计思路中,我们为了加快写入:采用添加式写入,使用sstable;为加快读取:硬盘进行二分,metadata维护了index,布隆过滤器。

热门文章

暂无图片
编程学习 ·

exe4j详细使用教程(附下载安装链接)

一、exe4j介绍 ​ exe4j是一个帮助你集成Java应用程序到Windows操作环境的java可执行文件生成工具&#xff0c;无论这些应用是用于服务器&#xff0c;还是图形用户界面&#xff08;GUI&#xff09;或命令行的应用程序。如果你想在任务管理器中及Windows XP分组的用户友好任务栏…
暂无图片
编程学习 ·

AUTOSAR从入门到精通100讲(126)-浅谈车载充电系统通信方案

01 引言 本文深入研究车载充电系统策略,设计出一套基于电动汽车电池管理系统与车载充电机的CAN通信协议,可供电动汽车设计人员参考借鉴。 02 电动汽车充电系统通讯网络 电动汽车整车控制系统中采用的是CAN总线通信方式,由一个整车内部高速CAN网络、内部低速CAN网络和一个充电…
暂无图片
编程学习 ·

CMake(九):生成器表达式

当运行CMake时&#xff0c;开发人员倾向于认为它是一个简单的步骤&#xff0c;需要读取项目的CMakeLists.txt文件&#xff0c;并生成相关的特定于生成器的项目文件集(例如Visual Studio解决方案和项目文件&#xff0c;Xcode项目&#xff0c;Unix Makefiles或Ninja输入文件)。然…
暂无图片
编程学习 ·

47.第十章 网络协议和管理配置 -- 网络配置(八)

4.3.3 route 命令 路由表管理命令 路由表主要构成: Destination: 目标网络ID,表示可以到达的目标网络ID,0.0.0.0/0 表示所有未知网络,又称为默认路由,优先级最低Genmask:目标网络对应的netmaskIface: 到达对应网络,应该从当前主机哪个网卡发送出来Gateway: 到达非直连的网络,…
暂无图片
编程学习 ·

元宇宙技术基础

请看图&#xff1a; 1、通过AR、VR等交互技术提升游戏的沉浸感 回顾游戏的发展历程&#xff0c;沉浸感的提升一直是技术突破的主要方向。从《愤怒的小鸟》到CSGO,游戏建模方式从2D到3D的提升使游戏中的物体呈现立体感。玩家在游戏中可以只有切换视角&#xff0c;进而提升沉浸…
暂无图片
编程学习 ·

flink的伪分布式搭建

一 flink的伪分布式搭建 1.1 执行架构图 1.Flink程序需要提交给 Job Client2.Job Client将作业提交给 Job Manager3.Job Manager负责协调资源分配和作业执行。 资源分配完成后&#xff0c;任务将提交给相应的 Task Manage。4.Task Manager启动一个线程以开始执行。Task Manage…
暂无图片
编程学习 ·

十进制正整数与二进制字符串的转换(C++)

Function one&#xff1a; //十进制数字转成二进制字符串 string Binary(int x) {string s "";while(x){if(x % 2 0) s 0 s;else s 1 s;x / 2;}return s; } Function two&#xff1a; //二进制字符串变为十进制数字 int Decimal(string s) {int num 0, …
暂无图片
编程学习 ·

[含lw+源码等]微信小程序校园辩论管理平台+后台管理系统[包运行成功]Java毕业设计计算机毕设

项目功能简介: 《微信小程序校园辩论管理平台后台管理系统》该项目含有源码、论文等资料、配套开发软件、软件安装教程、项目发布教程等 本系统包含微信小程序做的辩论管理前台和Java做的后台管理系统&#xff1a; 微信小程序——辩论管理前台涉及技术&#xff1a;WXML 和 WXS…
暂无图片
编程学习 ·

树莓派驱动DHT11温湿度传感器

1&#xff0c;直接使用python库 代码如下 import RPi.GPIO as GPIO import dht11 import time import datetimeGPIO.setwarnings(True) GPIO.setmode(GPIO.BCM)instance dht11.DHT11(pin14)try:while True:result instance.read()if result.is_valid():print(ok)print(&quo…
暂无图片
编程学习 ·

ELK简介

ELK简介 ELK是三个开源软件的缩写&#xff0c;Elasticsearch、Logstash、Kibana。它们都是开源软件。不过现在还新增了一个 Beats&#xff0c;它是一个轻量级的日志收集处理工具(Agent)&#xff0c;Beats 占用资源少&#xff0c;适合于在各个服务器上搜集日志后传输给 Logstas…
暂无图片
编程学习 ·

Linux 基础

通常大数据框架都部署在 Linux 服务器上&#xff0c;所以需要具备一定的 Linux 知识。Linux 书籍当中比较著名的是 《鸟哥私房菜》系列&#xff0c;这个系列很全面也很经典。但如果你希望能够快速地入门&#xff0c;这里推荐《Linux 就该这么学》&#xff0c;其网站上有免费的电…
暂无图片
编程学习 ·

Windows2022 无线网卡装不上驱动

想来 Windows2022 和 windows10/11 的驱动应该差不多通用的&#xff0c;但是死活装不上呢&#xff1f; 搜一下&#xff0c;有人提到 “默认安装时‘无线LAN服务’是关闭的&#xff0c;如果需要开启&#xff0c;只需要在“添加角色和功能”中&#xff0c;选择开启“无线LAN服务…
暂无图片
编程学习 ·

【嵌入式面试宝典】版本控制工具Git常用命令总结

目录 创建仓库 查看信息 版本回退 版本检出 远程库 Git 创建仓库 git initgit add <file> 可反复多次使用&#xff0c;添加多个文件git commit -m <message> 查看信息 git status 仓库当前的状态git diff 差异对比git log 历史记录&#xff0c;提交日志--pret…
暂无图片
编程学习 ·

用Postman生成测试报告

newman newman是一款基于nodejs开发的可以运行postman脚本的工具&#xff0c;使用Newman&#xff0c;可以直接从命令运行和测试postman集合。 安装nodejs 下载地址&#xff1a;https://nodejs.org/en/download/ 选择自己系统相对应的版本内容进行下载&#xff0c;然后傻瓜式安…
暂无图片
编程学习 ·

Java面向对象之多态、向上转型和向下转型

文章目录前言一、多态二、引用类型之间的转换Ⅰ.向上转型Ⅱ.向下转型总结前言 今天继续Java面向对象的学习&#xff0c;学习面向对象的第三大特征&#xff1a;多态&#xff0c;了解多态的意义&#xff0c;以及两种引用类型之间的转换&#xff1a;向上转型、向下转型。  希望能…