关于web服务器架构的思考
笔者最近一年都在从事企业私有云存储的开发,主导并推动了服务器架构的重构。在架构演化的过程中,有了很多的心得体会,这里记录一下,算是对自己架构成长的一个总结。
原则
对于笔者来说,设计一个web服务器架构方案,最先考虑的就是简单以及可扩展性。而这两个也是笔者设计架构的首要原则。
简单
对于一个企业级web产品来说,它其实是由非常多的基础服务来组合起来的。以私有云产品来说,如果想实现一个简单的文件共享功能,至少需要共享服务,文件服务,账号服务三个服务来共同实现。
- 共享服务,用来管理文件共享关系,如用户A给用户B共享了一个文件abc.txt
- 文件服务,用来提供共享文件下载,如用户B需要在哪里以及如何下载abc.txt这个文件
- 账号服务,用来提供共享人员的相关信息,如用户A和B的账号,姓名等信息
上面三个服务,缺少了任何一个都不能实现共享功能。但是为了实现一个功能就要与3个服务进行交互,有些童鞋就觉得非常麻烦,简单起见,他们将其糅合在一起,这样就能很好的进行代码编写了。但是这样做的弊端也非常明显,可维护性非常的差,因为功能都是耦合在一起了,如果这时候我们想用另一套企业自己的账号数据,那么完全无法实现。
上面说的只是笔者列举的一个例子,实际项目中,共享功能还是很好的进行了切分,但是仍然有很多功能过于耦合,以至于笔者的团队在很长的一段时间里面都在为以前的某些童鞋的错误设计买单。
鉴于有了上面的经验教训,笔者在考虑架构方案的时候最先想的就是简单。
所谓简单,其实很好理解,就是一个服务就干一件事情,不同的功能逻辑别糅在一个服务里面实现。更上层的服务是通过集成底层的服务来实现。其实这个就跟程序设计里面模块化的思想一样,只不过这里的模块就是单个服务。
一个服务一个模块,好处是很多的,但也不可能100%的完美,仍然很多问题需要考虑,譬如:
- 服务的可用性问题,如何判断一个服务是否可用,以及当机服务的恢复。
- 服务的运维管理问题,系统可能随着功能的增多而有了太多的服务,对这些服务的监控管理就是一个很难的问题,毕竟每个人都不希望凌晨因为服务当掉了这些问题被电话叫醒。
上面的这些问题,笔者认为已经涉及到服务的高可用问题了,与是否采用简单服务方案无关。而对于服务的高可用,分布式这些问题,笔者反而认为在简单这个原则下面,反而能更好的处理解决。
可扩展性
对于大规模web系统来说,随时可能面临着突然大并发量访问而造成系统负载撑不住的问题。对于这种情况,我们就需要扩展我们的系统使其能够处理过载的情况。
对于web系统的扩展,通常采用横向扩展的方式,当某一个服务出现性能瓶颈,我们只需要动态增加该服务就能减轻过载问题。因为服务是可以动态进行横向扩展的,所以服务提供的功能都应该是状态无关的。所谓无状态性,就是每一次服务器的请求都应该是独立的,如果服务是有状态的,为了维护调用的状态,我们会做非常多的事情,这非常不利于扩展,同时也增加了系统的复杂性。
stars
stars是私有云项目开始的时候葱头写的一套web服务器框架。
在项目开始的时候,大部分的开发同学都没有web服务器开发的经验,为了解决这个问题,葱头设计了stars,使得大家能够非常的使用python进行web服务器的开发。stars有很多设计巧妙以及值得学习的地方。虽然现在看来有些设计无法满足现有的需求。但是笔者一直认为没有最好的架构,只有合适的架构,作为一个架构师,即使你考虑了很多后续扩展的问题,但是仍然有一些需求变化是你考虑不到的。
rpc
stars最大的特点,就在于封装了复杂的http调用,使得开发的同学不需要关注底层http知识。而做到这一点,就是将http的调用封装变成了大家熟悉的函数调用RPC。譬如我们有如下的一个http请求:
http://domain.com/file/getFileInfo?fileId=1
该HTTP请求获取一个fileId为1的文件相关信息,在stars里面,我们可以这样写:
remote = RPC("domain.com")
remote.file.getFileInfo(fileId = 1)
而stars不光封装了http调用,同时为了方便大家的开发,也定义了一套API规范,只要大家写的API满足一定规则,就自动能够被注册到stars里面,这样外面就能使用RPC来调用。
对于web服务API的模式,笔者认为,通常有RPC以及Restful等几种方式:
RPC Pattern:
GET http://domain.com/file/getFileInfo?fileId=1
GET http://domain.com/file/deleteFile?fileId=1
Restful Pattern:
GET http://domain.com/file/1
DELETE http://domain.com/file/1
stars采用的是RPC Pattern,对于这种模式,它对于开发同学很好理解,因为它就跟普通的函数调用一样,使用起来则比较自然。反而Restful Pattern则对于开发同学不怎么好理解。只是随着现今系统规模的扩大,笔者越发觉得stars遇到了问题:
- 每实现一个功能,就新增一个API,导致API越来越多,管理越来越复杂。
- 第三方开发者难于对接。API过多,开发者不知道如何使用哪些API。
对于这种情况,可能Restful是一个很好的解决方案,如果有机会,笔者可能会在后续的新的项目中考虑实施。
stub
对于web服务来说,如何信任客户端的HTTP请求?如何高效的与客户端进行交互?这些都是需要考虑的问题。stars采用了stub方式,在高效交互的情况下也能保证不错的安全性。
- 客户端首先通过username + password的方式登陆系统,使用https协议保证其安全性。
- 登陆成功之后,服务器会下发一个stub作为后续客户端与服务器交互的凭证。
- 客户端的任何HTTP请求都需要带上stub。
- stub不会一直有效,一段时间之后过期,客户端需要重新请求申请新的stub。
因为使用了stub,客户端与服务器能高效的交互,但是stub有如下问题:
- 因为每次请求都会带上stub,只要该stub被外部截获,那么就可能伪装成该用户进行访问了。所以stub在很短的一段时间之后就会过期,但即使过期也仍然有风险。
- stub是放到url的请求参数上面传递给服务器的,即使采用HTTPS方式,该stub也能在浏览器上面看到,无法保证安全。
- stub放置在web服务一个总控服务的内存中,无持久化策略,只要该服务重启,先前所有的stub都会失效。
- 每个服务一个stub,随着服务的增多,客户需要关注过多的stub管理。
- 对于服务的任何HTTP请求,都需要在该服务对应的总控服务中进行stub验证,造成单点问题。
可以看到,stub虽然提供了很好的与web服务交互的方式,但是在web系统规模扩大之后,不利于后续的扩展,安全性也有漏洞。
信任链
stars另一个设计巧妙的地方在于其信任链机制。实际会遇到如下情况,假设现在客户端已经生成了一个服务的stub,可以与该服务交互,但这时候客户端需要访问另一个服务的某个API,可是这时候没有该服务的stub,那如何处理呢?
信任链机制其实就是这样一种情况,服务相互之间是可信任的,因为这些服务都是我们部署的,同时我们也可以通过很多其他方法保证可信任。
当客户端与服务A建立信任之后,如果客户端想访问服务B,客户端可以通过A向B申请一个stub,这样就可以通过该stub访问服务B。
resty
通过上面可以看到,stars架构是项目初期为了解决服务器快速开发等问题而提出的一套实现方案,当web系统越来越复杂的时候,stars就有了一些局限性,这时候就需要有另一套架构来满足现有的情况。
对于新架构的选择,笔者决定从如下几个方面考虑:
- 吸收stars精华,以其为基础演化,而不是完全重来。架构可能跟进化论一样,是不断进化的。而推倒重来,就如同突然基因突变一样,你自认为变好了,没准可能更差。
- 平滑升级,系统已经部署到很多企业里面,如何保证升级的平稳,以及升级数据的完整性,都是我们需要面临的问题。
- 简单,参考业界最通用的解决方案,不需要引入复杂的自造轮子。
签名
为了解决stars stub相关问题,笔者认为可以采用最通用的HTTP签名机制。
- 客户端使用username + password登陆成功之后,服务器下发一对id和key,因为登陆采用HTTPS方式,所以id和key不会被外部截获。
- 客户端发送HTTP请求,使用key对其签名,并带上id一起发送给服务器。
- 任何外部的HTTP请求,首先会通过验证服务器对其验证,如果验证通过,则证明是一个合法的url。
- 验证通过之后,系统将会把HTTP请求路由到实际处理的服务上处理。
url签名可以算是一种非常通过的安全交互模式,amazon s3,阿里云等云存储服务都使用了该方式。虽然引入验证服务器,增加了处理HTTP请求的时间,但是将验证与逻辑分离解耦,笔者认为更好,同时,后续我们可以通过很多方法优化验证服务。
原子api
前面说了,stars的RPC模式会导致API越来越多,以至于开发者为了实现一个功能,可能会与多个API进行交互,这明显提升了第三方开发的复杂性。所以笔者提出了原子API的概念,其实就是在服务器级别整合API,使得开发者只需要通过调用简单的API就能实现相应的功能。
对于如何提供原子级别的API,笔者觉得可以使用如下方法:
- 如果某一个功能需要多个API顺序执行,每个API的执行结果都可能影响该功能的最终结果,这样的功能我们就需要提供一个原子API来整合。
- 如果某一个功能只是需要调用多个API查询相关数据,那么我们只需要提供批量查询的API即可,不需要提供原子API整合。
既然引入了原子级别的API,那么在哪里进行整合呢?譬如有一个功能,需要服务A和服务B的API,我们不可能写一个服务C来特意的整合这个功能,这样会导致服务越来越多。所以笔者决定在nginx这一层提供原子API功能的整合。使用nginx的好处在于只需要增多一个location,该location里面进行API的整合。
不过,为nginx开发一个module去处理这一个location其实也是一件非常困难的事情,虽然nginx的module开发看起来很简单,但是实际做起来会发现非常的复杂。幸运的是,我们有openresty,直接可以使用lua进行nginx的开发。现在我们已经大量在使用openresty,这也是为什么笔者将这次的架构命名为resty的原因。
统一入口
stars有一个比较严重的问题,在于各个服务之间都是互相知道地址的,这样就有几个问题:
- 对外暴漏的入口地址太多,客户端知道太多的服务地址。
- 不利于动态部署,服务之间地址固定,重启某一个服务,相关服务都需要重启。
为了解决这个问题,笔者决定在nginx这一层做统一入口,所有的服务地址由nginx这边进行管理,这样有如下几个好处:
- 地址解耦,配置统一,只有nginx知道所有服务的地址,便于集中管理
- 隔离,服务与服务之间,服务与客户端之间完全隔离
- 动态部署,任何服务的动态变更,只需要nginx处理,该服务的相关服务完全不用重启。
总结
以上只是简单的记录进来服务的架构演化,虽然现有的架构能满足系统的需求,但是没准一段时间之后就又会出现局限性。笔者一直认为,没有最好的架构,只有合适的架构。但是无论怎样,保持简单就可以了。
版权声明:自由转载-非商用-非衍生-保持署名 Creative Commons BY-NC-ND 3.0