前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >简明入门讲义——如何实现可扩展的 Web 服务

简明入门讲义——如何实现可扩展的 Web 服务

作者头像
benny
发布2021-05-13 15:29:10
8340
发布2021-05-13 15:29:10
举报

一. 服务器

可扩展的应用服务器(Application Server)集群藏身于负载均衡器(Load balance,LB)背后,LB 将负载(即用户请求)平均地分配到各个组或集群的应用服务器上,此时负载均衡器可能运行在 TCP 层(Layer 4),分配请求的方式默认是简单的轮询(Round-Robin),即假设有服务器 A-D,请求依次从 A 分配到 D,列表循环。

现在,小明向你的 Web 服务发起请求,第一个请求可能被分配到服务器 A,第二个请求可能被分配到服务器 C,要求小明每次请求总能获得相同的返回结果,无论请求最终落到哪个服务器上

不改变设计,可能出现下面的情况,小明发起第一次请求,填完密码登录,提示 3 天内直接进入无需填写密码。结果再次登录时,路由到另一个服务器,又一次提示小明登录!原因很简单,这台服务器上面没有小明的 Session。

这个示例引出了扩展性的第一个黄金法则:每个服务器都包含完全相同的代码库,不在本地磁盘或内存上存储任何与用户相关的数据,例如会话(Session)或个人资料。

怎么实现会话保持(Sticky Session) 是水平扩展服务器中的常见问题。

假设请求随机到任一服务器,则必须有一个中心化的存储服务用来保存 Session,并且所有应用服务器都可以访问。这项存储服务独立于应用服务器之外,可以是持久化的数据库或者缓存

如果没有额外的存储服务怎么办,假设现在只有负载均衡器和应用服务器?

我们可以让负载均衡器监听 HTTP 层(Layer 7,应用层) 的请求,当小明第一次请求分配到服务器 A 时,产生一个随机数 r,将它写到 Cookie 中随请求返回。当小明再次请求时,负载均衡器层通过一个哈希函数,计算 Cookie 中的随机数 r,请求即可再次路由到服务器 A。

这个方案节省了存储空间,但引入一个问题,服务器 A 挂掉之后,小明还需重新登录,所幸这不是非常关键的数据,还可以接受。但独立存储也存在自己的问题,最明显的,怎么解决单点问题(Single Point Of Failure)?这个后文再谈。

现在你的关键问题是,如何使多个应用服务器发布时都存有同一份代码?可以借助 capistrano 这个开源项目。

将用户数据移出应用服务器,并解决完全相同代码库问题后,就可以打包为服务器镜像进行统一部署了。

二. 数据库

完成第一步用户数据中心化隔离、代码库同步后,不费多大力气就可以添加多个应用服务器,使你的 Web 服务处理大量并发请求。但 Web 服务还是会变慢甚至挂掉,原因就在中心化的数据库上!

最好从一开始就走反范式的数据设计方式,数据库只做简单的写入和查询操作,其他复杂的操作、约束都通过代码解决。这样你的数据库会更容易进行水平扩展,更方便做迁移,单个数据库实例也不需要很大。

否则,数据量一大,迁移、修改等操作,还是会由于数据库外键约束,导致长时间锁表等问题,或者比反范式设计的数据库消耗更多的内存和 CPU,成本与日俱增。

在进行数据库复制(Replication)时,一般使用主从模式(Master-Slave)。这里的主从采用读写分离,主库负责写,从库定时同步主库的数据,接收读请求。当主库挂掉的时候,从库也不能提升为主库。

为了解决这个问题,在主库上引入双主(Master-Master)或者待命(Standby)模式双主即两个主库(或者两个集群)都可以接收写请求,无论哪一方收到写请求,另一方会立刻同步。待命模式则同一时刻只有一个接收写请求,另一方只做同步。当主库宕机,待命方将自己升为主库,继续提供服务

当你引入了多个数据库(集群)时,最好不要通过硬编码(Hard-code)来解决故障重连问题,开发同学没必要了解你的架构拓扑,而且在你扩展或者收缩集群的时候,开发同学可不想跟着你加班发布。

这时同样可以引入负载均衡器来解决扩展问题。如果你还需要根据用户名分区操作,比如小明分到了新手区 Z,小红分配到新手区 X,那么负载均衡器可能解决不了,因为 MySQL 请求内容是二进制的,对 LB 是透明的

你可以引入分库分表的中间件,在代码层面解决。对于业务开发团队而言,这个中间件的处理过程同样是透明的。

但这个时候请求也只是“可用”,还不够快,是时候考虑引入缓存了。

三. 缓存

相同配置下,以 Redis 为例,缓存在读取和写入上要远胜于 MySQL 这样的关系型数据库。建议只使用 Redis 或者 Memcached 这类基于内存的缓存服务,不要使用基于文件的缓存,这会使数据迁移和复制(水平扩展)变得复杂。

保存缓存数据一般有两种方式

请注意,这里不是在讨论缓存更新的模式,如果感兴趣,可以阅读 缓存更新的套路。

其一是基于数据库查询(SQL-Based)来缓存,不难理解,就是把数据库的查询结果保存到缓存中,键名(Key)可以是查询的 SQL 语句哈希,简单粗暴。但这会存在问题,例如前面我们已经用了反范式的设计,尽量避免使用 JOIN 查询,一个语句有时候解决不了查询,怎么办?

这就有第二种方式,直接缓存对象(Object-Based)。一个请求(多次)查询后的数据在代码中“组装”(Assemble)完毕后。例如一个嵌套的数据结构,查询一个小明的个人信息和他的订单,其中订单数组中是一个个独立的订单对象。可以在代码中将数据组装完毕后,直接缓存整个对象。

想想看如果是第一种,你还需要分开缓存多个查询,下次读缓存还要读两次,再组装数据返回给用户,太麻烦了,用户可等不及!

四. 异步

做完了上面的三个步骤,用户可能还在抱怨我不想等!Web 服务的设计可不能像排队买所谓的网红奶茶一样,让一排用户在原地死等。

想象一下你到一个面包店买蛋糕,有这样的情况:

1.你要的蛋糕已经提前做好了,店员直接给你,交易完成2.你要的蛋糕卖完了,新一批晚上才出炉3.你要的蛋糕有,但你是给小明祝寿的,上面要有小明寿比南山的字。

情形一对应 Web 服务中的第一种异步模式,提前把内容生产好,等用户消费。典型的场景是个人博客、新闻网站等,提前将 HTML 渲染完毕,通过自动的定时任务或者手动执行脚本将内容上传到服务器,必要的时候配合 CDN(Content Delivery Network)进行加速,用户就可以快速的访问你的网页内容了。

情形二、三你肯定不想在蛋糕店干等,而是希望制作完成的时候,蛋糕店通知你来取就可以了。面包店有自己的订单队列,有些订单可能是加急(加钱)订单,面包店还需要优先处理。这对应 Web 服务中第二种异步方式,将用户的请求转化为异步任务在后台排队处理,用户注册一个监听器,处理完成后就会收到通知了。

相关的服务例如 RabbitMQ、Celery 等等。一旦你发现 Web 服务中有需要等的动作,务必将它异步处理。

参考文献

1.Lecture 9 Scalability Harvard Web Development David Malan(视频) (https://www.youtube.com/watch?v=-W9F__D3oY4)2.Scalability for Dummies(https://www.lecloud.net/tagged/scalability/chrono)

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-05-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员的碎碎念 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一. 服务器
  • 二. 数据库
  • 三. 缓存
  • 四. 异步
    • 参考文献
    相关产品与服务
    负载均衡
    负载均衡(Cloud Load Balancer,CLB)提供安全快捷的流量分发服务,访问流量经由 CLB 可以自动分配到云中的多台后端服务器上,扩展系统的服务能力并消除单点故障。负载均衡支持亿级连接和千万级并发,可轻松应对大流量访问,满足业务需求。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档