系统设计

引子

事情还要从大三说起,当时不知道哪里来的想法,可能是知乎刷多了,又联合了一个“六度分隔理论”,就想爬取知乎上所有人的关注和被关注数据,看是否能够验证一下这个理论。当时也就脑子一热,只是想完成这件事情,但没有考虑过具体怎样完成。

然后等到毕业设计的时候,这个想法又在脑子里闪过,不过想着由于工程量太大,就换了另外一个设计,是结合实习期间看到,学到的一些东西,做了一个能够运行的系统,现在也考虑要不要放出来,或者等以后有时间再打磨一下API再放出来。这都是后话了。

真正的开展,还要到三个月前,我找工作的那两周。那个周末被CMGS帮忙内推了几家公司,等面试的时间也怪无聊的,就把这个想法捡了起来,毕竟一个月的工作,还是跟着CMGS学了一些工程上的东西的(咱就像是海绵,到哪里都能吸两口)。

那这个系统是什么呢?

简单的说,是一个爬虫。复杂的说,是一个可以自我复制的爬虫。

我其实并没有深入了解过一般爬虫的实现方法,但大体推测,也无外乎是抓取页面,分析链接,然后顺藤摸瓜,如此反复。这样就会有一个问题,我要放出多少只爬虫,才能够满足我的需求?像搜索引擎这种级别的,他们资源多,放出去的虫子也多,基本可以无限制的爬来爬去。但如果是个人性质的,针对某一个站点,有筛选过滤策略条件的,而且还要充分利用资源,那就得好好琢磨一下了。

一个大胆的想法。

为什么不能让爬虫学习一下鸣人,可以自我复制呢?

我放出去一个蜘蛛,抓取 kfd.me 这个站的所有页面,母蜘蛛发现了 “kfd.me/a/",然后分裂一个小蜘蛛去爬 “kfd.me/a/*",自己再去寻找,子蜘蛛此时如果发现了”*kfd.me/a/a1/*” 那么这个蜘蛛再次分裂,产生一个新的蜘蛛去抓 “kfd.me/a/a1/*",自己去寻找 “kfd.me/a/*” 其他的URI。

这个分裂过程,可以是fork,等系统资源差不多了,再去远程调用创建一个新的蜘蛛,远程调用的方式可以是对进程的调用,也可以直接创建一个容器,利用kubernetes,docker swarm或者CMGS做的eru-lambda,总之能够在集群资源中创建新的容器的调度系统就可以。

这样一来,只要我们有充足的资源,那这个蜘蛛分裂的过程就可以一直下去,也不必考虑循环问题,因为每个蜘蛛都有自己的作用域,或者说抓取的范围,不是他的地盘,他不会操心,而等到蜘蛛抓取完自己地盘里的信息了,就自杀,释放集群资源。

子子孙孙无穷尽也。

系统设计

前面罗嗦了那么多(真不愧是话唠),终于到正规了。

那如何设计一个这样的系统呢?

我认为,世界上大部分的事情,都是相通的。自然界中的事物,现象,人类可以学习并应用到自己的生活中。而人类社会中,经过历史验证过的流程,设计,理念也同样可以应用在系统设计方面。

比如说流水线作业,比如说我们的等级制度,比如说我们的通信流程。

流水线作业可以对应微服务架构;社会等级制度可以对应系统中的等级划分(主从结构,或者其他的结构);通信流程可以对应系统中的网络划分(子网掩码对应不同省份的区号,集团内部网络可以理解为自制域)。等等等等。

所以,在设计系统的时候,我感觉,应该跟建一座工厂,或者建立一支军队差不多。都是利用一些资源,去完成我们的目的,或者提供某种服务。

工厂里有机器,有工人,有宿舍,有老板,还有洗手间,工作车间,秘书,经理,各种各种;军队又分军种,分武器,分等级,分战区,分连队,分部门,组织精密,等级森严。

那么一个系统呢,系统中也要考虑网络架构,存储,通信,日志,跨区域服务,分不同的组件,资源调度,资源监控,还要考虑机器的成本,维护的成本(相当于后勤部门),还有项目管理,配置管理,问题追踪,告警处理等等等等。乍一听很庞大,是的没错,因为我说了一大坨,但如果我们条理化呢,每种角色都分门别类呢。一口吃不成个胖子,一天也设计不出一套系统。

其实冯诺依曼的体系已经经过了历史的验证,至少目前还没有更好的架构出现。那么我们就可以参照着来啊,比如说一台计算机,要有CPU,内存,网卡,硬盘,显卡,主板,声卡,电源,那么系统中就要有,调度器,运行时环境,网络,存储,监控,日志,整体构架,告警系统,宿主机系统。

调度器可以理解为对系统的整体掌控,对业务应用的调度和计算;运行时环境可以理解为Python,JVM,Docker;网络可以分为区域网络,服务网络;存储又有分布式存储,文件存储,数据库,消息队列,文件共享;监控就是对系统中各个组件的监控(包括负载,性能,各种指标);日志则是收集各个组件产生的日志文件;整体架构,则是对于系统而言,是不是方便扩展的,是不是方便调试的,有没有容灾,可不可以拆分,解耦做得好不好,备份恢复方案做的怎么样;告警系统则是在某个环节中出现错误时,能不能即使有效准确的通知到上级(也就是我们),能不能让人迅速定位问题,解决问题;宿主机系统则是这个“系统”运行在哪里,是Windows还是Linux,是Ubuntu还是CentOS,是Andriod还是iOS。

理清楚了,就可以开始干活了。

拿我的spider来说,单就角色划分而言,可以有四种角色:

  • discover:发现新大陆,只负责找人
  • worker:抓取分析找到的人的信息,苦力活
  • monitor:监控worker的工作,万一有占着茅坑不拉屎的,就给干掉;也会监控discover和watcher,万一有哪个gg了,重启一下他们
  • watcher:检测某一个用户的动态,更新数据库中内容
  • boss:monitor向其汇报,它再汇报给我

当然这中间还要考虑其他的很多问题,比如说网络怎么弄(现在看来,可以用gus-proxy作为代理),角色之间信息传递通过什么(消息队列,etcd,DB选型),监控策略是什么,如何将数据展示出来等等。discover和worker如何分身,watcher如何分身,他们之间怎么互相通信(可以参考蚂蚁,两两通信,然后慢慢扩散,但是又有时效性的问题)。

所以真的是一件很庞大的事情。

组件设计

个人感觉,设计这个东西就是一个大事化小,小事化了的过程,一个系统提供复杂的服务,满足复杂的需求,完成复杂的任务,而系统中的组件就是将复杂的任务拆分,并一点点完成的小蚂蚁。虽然说是蚂蚁,但是也有基础的功能,最简单的,输入和输出。

其实任何东西都可以理解为输入和输出,典型的就是函数,要有参数和返回值,当然也可以没有返回值,没有那就是空,返回值为空,当然,参数也可以为空。许多函数累加,就会变成一个程序,这个时候,输入和输出就变得复杂了起来,但拆开来看,还是有输入和输出的,只是很细了而已。

既然一个组建是有输入和输出组成的(大体上),那么这样看起来就简单多了。比如说上面提到的 discover,就是一个发现新用户的组建或者成为程序(但程序的话,也是一些程序,毕竟discover不会只有一个,而是以集群的形式存在,对外展现是输入和输出,对内而言是一个组件),他的输入是知乎的网址,输出是新发现的用户。

怎样让这个程序变成一个集群,从而成为系统的一个组件呢。比如,某个discover发现了一个用户,他怎么知道是新用户,而不是已经被别人发现过的呢。当然可以将每个discover发现的用户保存到一个共享的数据库中,然后每次查询的时候,如果发现已经被人发现过了,那就不查了。但能不能有这样一种思路,就是,每个discover的发现,对自身而言都是新大陆,重复发现并不会对现有的数据产成影响,派出去的discover越多,重复发现的概率虽然也会增大,但是我们发现的新用户也会越多,而且,还可以定义discover回家的规则,即如何让一个discover停下(关于停止,可以采用命令发布的模式,每个discover接受中央发出的指令,也可以采用discover主动领取圣旨的模式,衡量自身是否满足停止的条件,然后自行判断:这两种模式没有本质的区别,都是中央集权)。

(忽然想到一句话,从知乎某个大V那里看来的,说,“如果你在行动之前想清楚要怎样做,已经超过80%的程序员了”,很有道理。)

把组建抽象为输入和输出,但每个组件不可能只有一组输入输出,他可能会有别的功能,这个时候就需要把每一对输入输出抽出来,在脑子里形成一个清单,然后一点一点实现。当然,最好能够一次性把需求列完成,这样可以避免以后的大改动,抽象程度也会更高一点。但,这好像是不可能的,我们的需求不可能一成不变,组件的功能也不可能不会增加,这就需要我们组件内部的耦合性要低。当我们把系统拆开的时候,其实就是在解耦了,现在我们要把组件拆开,也是对组件的解藕。

系统的解藕对于系统功能的扩展和规模的扩展都有好处;组件内部的解藕,对于程序员而言,则是很关键和必要的事情,能省下以后不少的麻烦,开发人员节省出来的时间,对于公司而言就是节省金钱。

解藕意味着抽象,拆分,但也不能把程序拆的七零八散,把系统弄得超级复杂。Simple is good。

后语

这篇文章跨了两天的时间,思路有点断。

原本这里想说一下对于程序设计的看法,然而鄙人有点懒,等想起来再说好了。

就酱。

comments powered by Disqus