架构
Last updated
Was this helpful?
Last updated
Was this helpful?
冯·诺依曼体系架构:所有的电脑都可以统一看作由“中央处理器 + 存储 + 一系列的输入输出设备”构成。
直接用机器指令编写软件太累,而且这些机器指令像天书一样没人看得懂,没法维护。所以,编程语言 + 编译器就出现了。编译器负责把我们人类容易理解的语言,转换为机器可以理解的机器指令,这样一来就大大解放了编写软件的门槛。
多个软件大家往同一个存储地址写数据冲突怎么办?一起往打印机去发送打印指令怎么办?有的软件可能偷偷搞破坏怎么办?于是,操作系统就出现了。操作系统要解决软件治理的问题,建立软件之间的协作秩序,让大家按照期望的方式进行协作。比如存储你写到这里,那么我就要写到别处;使用打印机要排队,你打完了,我才能接着去打印。操作系统需要解决基础编程接口问题,这些编程接口一方面简化了软件开发,另一方面提供了多软件共存(多任务)的环境,实现了软件治理。
操作系统和编程语言是我们开发一个应用程序所依赖的基础架构。
客户端应用程序和服务端有非常大的差别。单就操作系统来说,PC 就有 Windows、Mac、Linux 等数十种,手机也有 Android、iOS,Windows Mobile 等等。而设备种类而言就更多了,不只有笔记本、平板电脑,还有手机、手表、汽车,未来只会更加多样化。想消除客户端的多样性,并且跨平台提供统一编程接口的,是浏览器。浏览器的地位非常特殊,可以看作是操作系统之上的操作系统。
客户端领域特征是强交互,以事件为输入,GDI 为输出; 服务端领域特征是大规模的用户请求,以及 24 小时不间断的服务。
客户端是为单个用户服务的,所以它关注点是用户交互体验的不断升级。
服务端程序是被所有用户所共享,所以它的关注点是如何长期稳定的为所有用户同时提供服务。
一个客户端实例运行崩溃,它只影响一个用户。但一个服务端程序实例崩溃,可能影响几十万甚至几百万的用户。这是不可接受的。
一个服务端程序的实例可以崩溃,但是它的工作必须立刻转交给其他的实例重新做,否则损失太大了。所以,服务端程序必须是多实例的。
当用户规模达到一定基数后,每一秒都会有用户在使用它,不存在关闭程序这样的概念。从用户视角看,服务端程序 7x24 小时持续服务,任何时刻都不应该崩溃。
一台物理的机器资源总归是有限的,能够服务的用户数必然存在上限,所以一个服务端程序在用户规模到达一定程度后,需要分布式化,跑在多台机器上以服务用户。
相比客户端而言,服务端程序依赖的基础软件不只是操作系统和编程语言,还多了两类:
负载均衡(Load Balance);
数据库或其他形式的存储(DB/Storage)。
一个域名通过 DNS 解析到多个 IP,每个 IP 对应不同的服务端程序实例,这样就完成了流量调度。那么这种做法有什么不足?
第一个问题,是升级不便。要想升级 IP1 对应的服务端程序实例,必须先把 IP1 从 DNS 解析中去除,等 IP1 这个实例没有流量了,然后升级该实例,最后把 IP1 加回 DNS 解析中。
看起来还好,但是还有 DNS 解析缓存问题。把 IP1 从 DNS 解析中去除,就算写明 TTL 是 15 分钟,但是过了一天可能都还稀稀拉拉有一些用户请求被发送到 IP1 这个实例。所以通过调整 DNS 解析来实现升级,有极大的不确定性。
第二个问题,是流量调度不均衡。DNS 服务器是有能力做一定的流量均衡的。比如第一次域名解析返回 IP1 优先,第二次域名解析让 IP2 优先,以此类推,它可以根据域名解析来均衡地返回 IP 列表。
但是域名解析均衡,并不代表真正的流量均衡。由于客户端会对 DNS 做缓存,不是每次用户请求都会对应一次 DNS 解析,另外,DNS 解析本身也有层层缓存,到 DNS 服务器的比例已经很少了。
那么,怎么让流量调度能够做到真正均衡?
LVS(Linux Virtual Server)在网络层(IP 层)做负载均衡。
LVS 这种在网络层底层来做负载均衡,相比其他负载均衡技术来说,其特点是通用性强、性能优势高。
但它也有一些缺点。假如某个业务服务器实例 RS(Real Server) 挂掉,但 LVS 调度器(Director Server)还没有感知到,在这个短周期内转发到该实例的请求都会失败。这样的失败只能依赖客户端重试来解决。
有办法避免出现这种请求失败的情况吗?可以。答案是:服务端重试。
Nginx 和 Apache 是常见的 HTTP 应用网关。HTTP 网关收到一个 HTTP 请求(Request)后,根据一定调度算法把请求转发给后端真实的业务服务器实例 RS,收到 RS 的应答(Response)后,再把它转发给客户端。
在发现某个 RS 实例挂了后,HTTP 网关可以将同一个 HTTP 请求(Request)重新发给其他 RS 实例。
负载均衡的价值并不只是做流量的均衡调度,它也让业务服务器优雅升级成为可能。对于 HTTP 应用网关这种负载均衡的场景,升级过程很简单:
升级系统通知升级的业务服务器(Real Server)实例退出。
要升级的 RS 实例进入退出状态,这时新请求进来直接拒绝(返回一个特殊的 Status Code);处理完所有处理中的请求后,RS 实例主动退出。
升级系统更新 RS 实例到新版本,并重启。
客户端与服务端最大的差别是业务状态的表示不同。
一个桌面程序基本上是由一系列的 “用户交互事件” 所驱动。可以把它理解为一个状态机:假设在 i 时刻,该桌面程序的状态为业务状态i ,它收到用户交互事件i 后,状态变化为业务状态i+1 。
服务端程序由 “网络 API 请求” 所驱动。假设在 i 时刻,该服务端程序的状态为业务状态i ,它收到网络 API 请求i 后,状态变化为业务状态i+1 。
桌面程序的业务状态是如何表示的?内存中的数据结构。桌面程序的 Model 层是一棵 DOM 树,根结点通常叫 Document,这棵 DOM 树其实就是桌面程序的业务状态。
服务端程序的业务状态如何表示?用内存中的数据结构可以吗?答案当然是不能。如果业务状态在内存中,服务端程序一挂,数据就丢了。
在没有存储中间件的情况下,服务端需要自己在响应完每一个网络 API 请求之后,对业务状态进行持久化。听起来这好像不复杂?其实不然。
桌面程序是单用户使用的,持久化的时候什么别的事情也不干,看起来用户体验也可以接受。
但是对服务端程序而言,如果我们在某个 API 请求完成并持久化的时候,其他 API 请求如果只能排队等着的话,服务端在用户眼里就停止服务了。所以持久化的时间必须要足够短,短到让人感知不到服务停顿。
服务端程序的业务状态并不简单。这是一个多租户的持久化状态。就算一个用户的业务状态数据只有 100K,有个 100 万用户,那么需要持久化的数据也有 100G。这显然不能用“常规桌面程序每次完全重新生成一个新文件”的持久化思路做到,它需要被设计为一种增量式的存储系统。
如果每一个做服务端程序的开发人员需要自己考虑如何持久化业务状态,这个代价显然过高了。于是,存储中间件就应运而生了。
数据库单机故障会导致服务临时不可访问,甚至会出现更严重的数据丢失。
单机存储量终归有上限,这样我们服务的用户数就有上限。在分布式数据库出现之前,人们的解决方案是手工的分库分表。总之,业务上我们需要做到规模可伸缩,不必担心单机物理存储容量的限制。
单机房的可靠性也是不够的,机房可能会出现网络中断,极端情况下还可能因为自然灾害,比如地震,导致整个机房的数据丢失。于是就出现了“两地三中心”,跨机房容灾的数据灾备方案。
对比桌面程序,服务端的系统状态也是存储于某些数据结构中,通过持久化这些数据结构来持久化服务的状态,这样服务重启或者扩容的时候可以利用这些数据来恢复服务状态,而存放持久化数据的存储系统即可被认为是内存外的数据结构。内存类型的数据结构有list map set 等,对应的内存外的数据结构类型有kv数据库,关系型数据库,对象储存,倒排索引等即“元数据结构”
键值存储(KV-Storage);
对象存储(Object Storage);
数据库(Database);
消息队列(MQ);
倒排索引(SearchEngine);
等等。
REST 的全称是 “Representational State Transfer”。它强调的是:
第一,客户端和服务器之间的交互在请求之间是 “无状态” 的。这里的无状态更严谨的说法是 “无会话(Session)” 的,从客户端到服务器的每个请求,都必须包含理解请求所必需的完整信息。服务器可以在请求之间的任何时间点重启,客户端不会得到通知。
第二,是统一的表现规范,也就是 Representational 一词传递的意思。它认为,所有网络 API 请求都应该统一抽象为对某种资源 URI 的 GET、PUT、POST、DELETE 操作。
protobuf 取代的不是 HTTP 协议,而是 json、xml 或 Web 表单(form)。
对于一个 Web 应用而言,授权的第一步是登录(login)。登录最经典的方式就是 “用户名 + 密码” 授权。“用户名 + 密码” 授权往往只发生在登录那一下,登录后就会生成一个会话(Session)用途的 Cookie。此后 Web 应用的授权都基于 Session,直到 Session 过期。假如在每一次 API 请求中都带上密码,那么显然密码泄漏的概率会更大。
RESTful API 层中的 Token 授权,和 Web 应用中的 Session 授权的地位是非常像的。
Session 授权会有过期时间,Token 授权也会有过期时间。Session 授权有自动顺延,Token 授权有 Refresh。Session 授权的典型入口是登录(login),Token 授权也一样有 “用户名 + 密码” 授权这个入口。
这样来看,Token 授权和 Session 授权的差别只是应用场景不同,一个用于 API 层,一个用于 Web。而这也导致承载它们的机制有些不同,Token 授权基于 HTTP 的 Authorization 头,而 Session 授权则基于 Cookie。
基于 Token 的授权,多数发生在面向终端用户的场景,也就是我要做一个 To C 的应用。当前推荐的 Token 授权标准是 OAuth 2.0,OAuth 2.0 的优势是对外提供 Open API,而不仅仅局限于自己的 App 用。OAuth 2.0 提供了一个很好的方式,能够让我们的客户不用向第三方应用去暴露自己的用户隐私(比如用户名和密码)的前提下,调用 API 来使用我们的服务。
基于 AK/SK 的授权(access key / secret key),多数发生在面向企业用户提供 API,也就是说提供的是一个 To B 的云服务。AK/SK 并不是公私钥,AK/SK 授权的背后是数字签名。AK 是密钥提示(keyHint),SK 是数字签名的密钥(key)。