Web服务

218 阅读6分钟

1. 路由

1.1 什么是路由:

路由就是URL到函数的映射

/users        ->  getAllUsers()
/users/count  ->  getUsersCount()

这是两条路由,当访问 /users 的时候,会执行 getAllUsers() 函数;当访问 /users/count 的时候,会执行 getUsersCount() 函数。
而 router 可以理解为一个容器,或者说一种机制,它管理了一组 route。简单来说,route 只是进行了URL和函数的映射,而在当接收到一个URL之后,去路由映射表中查找相应的函数,这个过程是由 router 来处理的。一句话概括就是 "The router routes you to a route"。

1.2 服务器端路由

对于服务器来说,当接收到客户端发来的HTTP请求,会根据请求的URL,找到相应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。对于静态资源服务器,可以认为,所有URL的映射函数就是一个文件的读取操作。对于动态资源,映射函数可能是一个数据库的读取操作,也可能是进行一些数据处理等等。
例如:

app.get('/', (req, res) => {
  res.sendFile('index')
})

app.get('/users', (req, res) => {
  db.queryAllUsers()
    .then(data => res.send(data))
})
  • 当访问/的时候,会返回index页面
  • 当访问/users的时候,会从数据库中取出所有用户数据并返回

不仅仅是URL

在 router 匹配 route 的过程中,不仅会根据URL来匹配,还会根据请求的方法来看是否匹配。例如上面的例子,如果通过 POST 方法来访问 /users,就会找不到正确的路由。

1.3 客户端路由

客户端(通常是浏览器),路由的映射函数通常是进行一些DOM的显示和隐藏操作。这样,当访问不同的路径的时候,会显示不同的页面组件。客户端路由最常见的有以下两种实现方案:

  • 基于Hash
  • 基于History API

Hash
URL中 # 及其后面的部分为 hash。例如:

const url = require('url')
var a = url.parse('http://example.com/#/foo/bar')
console.log(a.hash)
// => #/foo/bar

hash仅仅是客户端的一个状态,当向服务器发请求的时候,hash部分并不会发过去。

通过监听 window 对象的 hashChange 事件,可以实现简单的路由。例如:

window.onhashchange = function() {
  var hash = window.location.hash
  var path = hash.substring(1)

  switch (path) {
    case '/':
      showHome()
      break
    case '/users':
      showUsersList()
      break
    default:
      show404NotFound()
  }
}

History API
通过HTML5 History API可以在不刷新页面的情况下,直接改变当前URL
可以通过监听 window 对象的 popstate 事件,来实现简单的路由:

window.onpopstate = function() {
  var path = window.location.pathname

  switch (path) {
    case '/':
      showHome()
      break
    case '/users':
      showUsersList()
      break
    default:
      show404NotFound()
  }
}

但是这种方法只能捕获前进或后退事件,无法捕获 pushState 和 replaceState

1.4 动态路由

上面提到的例子都是静态路由。但是有时候我们需要在路径中传入参数,例如获取某个用户的信息,我们不可能为每个用户创建一条路由,而是在通过捕获路径中的参数(例如用户id)来实现。

2. 高并发Web系统(单机到分布式集群)

在Web系统的访问量越来越高的过程中,系统承受的压力越来越大,需要再Web系统架构层面搭建多个层次的缓存机制。在不同的压力阶段,通过搭建不同的服务和架构来解决。

2.1 Web负载均衡

Web负载均衡(load balancing),简单地说就是给我们的服务器集群分配“工作任务”,采取恰当的分配方式,对于保护处于后端的Web服务器来说,非常重要。

负载均衡的策略包括:HTTP重定向、反向代理负载均衡、IP负载均衡、DNS负载均衡等。

1.HTTP重定向

当用户发来请求的时候,Web服务器通过修改HTTP响应头中的Location标记来返回一个新的url,然后浏览器再继续请求这个新url,实际上就是页面重定向。通过重定向,来达到“负载均衡”的目标。例如,我们在下载PHP源码包的时候,点击下载链接时,为了解决不同国家和地域下载速度的问题,它会返回一个离我们近的下载地址。重定向的HTTP返回码是302,如下图:

这个重定向非常容易实现,并且可以自定义各种策略。但是,它在大规模访问量下,性能不佳。而且,给用户的体验也不好,实际请求发生重定向,增加了网络延时。

2. 反向代理负载均衡

反向代理服务的核心工作主要是转发HTTP请求,扮演了浏览器端和后台web服务器中转的角色。因为它工作在HTTP层(应用层),常见的反向代理软件如Ngnix。
Ngnix是一种非常灵活的反向代理软件,可以自由定制化转发策略,分配服务器流量的权重等。反向代理中,常见的一个问题,就是web服务器存储的session数据,因为一般负载均衡的策略都是随机分配请求的。同一个登录用户的请求,无法保证一定分配到相同的web机器上,会导致无法找到session的问题。

解决的方案主要有两种:

  • 配置反向代理的转发规则,让同一个用户的请求一定落在同一台机器上(通过分析cookie),复杂的转发规则将会消耗更过的CPU,也增加了代理服务器的负担。
  • 将session这类的信息,专门用某个独立的服务来存储,例如redis/memcache,这个方案是比较推荐的。

反向代理服务,也是可以开启缓存的,如果开启了,会增加反向代理的负担,需要谨慎使用。这种负载均衡策略实现和部署非常简单,而且性能表现也比较好。但是,它有“单点故障”的问题,如果挂了,会带来很多的麻烦。而且,到了后期Web服务器继续增加,它本身可能成为系统的瓶颈。

3. IP负载均衡

IP负载均衡服务是工作在网络层(修改IP)和传输层(修改端口),比起工作在应用层性能要高出非常多。原理是,它是对IP层的数据包的IP地址和端口信息进行修改,达到负载均衡的目的。常见的负载均衡方式,是LVS(Linux Virtual Server,Linux虚拟服务),通过IPVS(IP Virtual Server,IP虚拟服务)来实现。

在负载均衡服务器收到客户端的IP包的时候,会修改IP包的目标IP地址或端口,然后原封不动地投递到内部网络中,数据包会流入到实际Web服务器。实际服务器处理完成后,又会将数据包投递回给负载均衡服务器,它再修改目标IP地址为用户IP地址,最终回到客户端。

4. DNS负载均衡

DNS(Domain Name System)负责域名解析的服务,域名url实际上是服务器的别名,实际映射是一个IP地址,解析过程,就是DNS完成域名到IP的映射。而一个域名是可以配置成对应多个IP的。因此,DNS也就可以作为负载均衡服务。