写点什么

Web Farm 中异步、高效的用户登录解决方案

2008 年 1 月 22 日

在我的咨询工作中,常常会碰到一些持如下观点的人:“有些东西并不适合使用异步模式”——尽管他们自己也认可异步通讯 模式与生俱来的稳定性。一个常常被引用的例子就是用户验证——将用户名和密码对提交给后端系统验证。为了讨论方便,我假设后端系统使用了用户数据库。

问题假设

为保证基本的安全性,我们假设密码在被存储前以某种散列算法编码。同时假设网络结构设计合理,Web 服务器在 DMZ 区域中得到隔离,它与应用服务器交互,应用服务器再与数据库服务器通讯。当然,再在 Web 服务器间(尤其是对用户登录等功能)应用轮循式(round- robin)负载均衡,也是一个很好的想法。

在深入讨论这个问题前,先来段开场白。我发现人们不感冒异步模式,大多是因为没有考虑应用的实际发布环境,或者解决方案不需要以多服务器、Web Farm 或多数据中心的分布式模式部署。

同步解决方案

在同步解决方案中,每个 Web 服务器对于每个用户的登录请求,都必须与应用服务器通讯。换句话说,应用服务器上的负载(数据库服务器也是类似的)将随用户登录数正比上升。

我不想在这里对同步解决方案多加纠缠,因为以前已经分析得太多了。在这种系统中,数据库最终都会成为瓶颈。为解决这个问题,一般会采用数据库分割方法 。很多大型站点配备有多个只读数据库——主数据库负责数据更新,并将这些数据复制到只读数据库。如果在LAMP 架构下使用廉价的MySQL,这是一个很好的解决方案;但如果运行Oracle 或MS SQL Server,就不是那么回事了。

无论你在数据层动什么手脚,都回避不了这个问题。将数据访问操作限制在Web 服务器内部不是很好吗?即便使用廉价的Apache,系统也会运行得更为流畅。异步解决方案的本质,就是以较小的内存代价,换取对其他资源很大的节省。

异步解决方案

在异步方案中,我们可将用户名/ 散列密码对缓存在Web 服务器的内存中,并用缓存中的数据实现对用户的验证。首先,我们分析一下这种方法对内存的消耗量。

用户名一般不超过12 个字符,我们在这里大方些,假设平均为32 个字符。使用Unicode 编码后,每用户名占用64 字节。散列加密的密码因算法而异,会占用256 到512 位,即最长64 字节。因此总的是128 字节。也就是说,使用Web 服务器上1GB 内存,我们可以安全缓存8 百万用户名/ 密码对。如果你有1 百万用户(也不错了,行啊你),则只需要消耗128MB 内存——这对于不需要花多少钱就可以配备2GB 内存的服务器来说,小意思了。

新用户注册时,我们可以在Web 服务器缓存中检查是否存在该用户名。当然,若考虑到并发问题,还需要通过数据库再次检查,但毫无疑问,数据库的负载已经大大降低。另外值得一提的是,在这种方案中,已经不存在只读数据库副本和数据复制操作。换个角度看,其实是我们的Web 服务器充当了“数据库副本”。

验证服务

整个系统的核心模块是应用服务器上的“验证服务”,用于处理来自Web 服务器的所有登录请求,当然包括新用户及其信息的注册。以前我们总是使用同步模式,新方案的不同之处在于新用户注册时,它会发出一个消息。此服务保证所有Web 服务器都能收到各自订阅的全部用户名/ 散列密码对信息。

在这里,我使用开源通讯框架 nServiceBus 说明此方案的实现过程,当然你也可以使用其他任何消息处理或 ESB 方案。在 nServiceBus 中,一条物理消息可包括多条逻辑消息,这样我们可以模拟单个更新消息发布,用相同类型的逻辑消息返回整个结果清单。我们定义如下消息:

复制代码
[Serializable]<br></br> public class UsernameInUseMessage : IMessage<br></br>{<br></br> private string username;<br></br> public string Username<br></br> {<br></br> get { return username; }<br></br> set { username = value; }<br></br> }<br></br>
private byte[] hashedPassword;<br></br> public byte[] HashedPassword<br></br> {<br></br> get { return hashedPassword; }<br></br> set { hashedPassword = value; }<br></br> }<br></br>}

定义需要整个清单时,Web 服务器发出的消息:

[Serializable]<br></br> public class GetAllUsernamesMessage : IMessage<br></br> {<br></br>}Web 服务器启动时执行的代码大致如下(可在构造函数中注入依赖对象):

public class UserAuthenticationServiceAgent<br></br>{<br></br> public UserAuthenticationServiceAgent(IBus bus)<br></br> {<br></br> this.bus = bus;<br></br> bus.Subscribe(typeof(UsernameInUseMessage)); // 订阅更新类消息 <br></br> bus.Send(new GetAllUsernamesMessages()); // 请求整个清单的信息 <br></br> }<br></br>}当验证服务收到消息GetAllUsernamesMessage时,它的消息处理器将首先访问用户名 / 散列密码缓存,然后构造一个新的消息,并返回给请求者,代码如下:

public class GetAllUsernamesMessageHandler : BaseMessageHandler<getallusernamesmessage></getallusernamesmessage><br></br>{<br></br> public override void Handle(GetAllUsernamesMessage message)<br></br> {<br></br> this.Bus.Reply(Cache.GetAll<usernameinusemessage></usernameinusemessage>());<br></br> }<br></br>}消息UsernameInUseMessage到达 Web 服务器时,负责处理的类定义如下:

public class UsernameInUseMessageHandler : BaseMessageHandler<usernameinusemessage></usernameinusemessage><br></br> {<br></br> public override void Handle(UsernameInUseMessage message)<br></br> {<br></br> WebCache.SaveOrUpdate(message.Username, message.HashedPassword);<br></br> }<br></br>}应用服务器向 Web 服务器发送整个清单时,UsernameInUseMessage类的多个实例会被包含在单独一条物理消息中。而 Web 服务器上的 bus 对象则每次只会向如上的消息处理器发出一条逻辑消息。

这样,实际验证一个用户时,Web 页(如果你使用 MVC,也可叫做控制器)将调用:

public class UserAuthenticationServiceAgent<br></br>{<br></br> public bool Authenticate(string username, string password)<br></br> {<br></br> byte[]existingHashedPassword = WebCache[username];<br></br> if (existingHashedPassword != null)<br></br> return existingHashedPassword == this.Hash(password);<br></br> return false;<br></br> }<br></br>}注册新用户时,Web 服务器当然会首先检查缓存,然后发出一条包含了用户名和散列密码的消息RegisterUserMessage

[Serializable]<br></br>[StartsWorkflow]<br></br>public class RegisterUserMessage : IMessage<br></br> {<br></br> private string username;<br></br> public string Username<br></br> {<br></br> get { return username; }<br></br> set { username = value; }<br></br> }<br></br> private string email;<br></br> public string Email<br></br> {<br></br> get { return email; }<br></br> set { email = value; }<br></br> }<br></br> private byte[] hashedPassword;<br></br> public byte[] HashedPassword<br></br> {<br></br> get { return hashedPassword; }<br></br> set { hashedPassword = value; }<br></br> }<br></br>}消息RegisterUserMessage到达应用服务器时,如下流程的新实例负责处理:

public class RegisterUserWorkflow :<br></br> BaseWorkflow<registerusermessage></registerusermessage>,IMessageHandler<uservalidatedmessage></uservalidatedmessage><br></br> {<br></br> public void Handle(RegisterUserMessage message)<br></br> {<br></br> // 通过 message.Email 发出包含了 this.Id(一个 guid 号,是 URL 的组成部分) 的确认请求 <br></br> }<br></br> /// <summary></summary><br></br> /// 用户点击 email 中的确认链接后,Web 服务器发出包含了流程 Id 的消息 UserualidatedMessage<br></br> /// <br></br> public void Handle(UserValidatedMessage message)<br></br> {<br></br> // 将用户存入数据库 <br></br> this.Bus.Publish(new UsernameInUseMessage(<br></br> message.Username, message.HashedPassword));<br></br> }<br></br>}消息UsernameInUseMessage最终将到达所有订阅了它的 Web 服务器。

性能 / 安全性的权衡

更深入考察整个流程,我们发现实际可实现为两个独立的消息处理器,并可用 email 地址代替流程 Id。不过,在这个改进的替代方案中必须考虑安全问题。删除对流程 Id 的依赖,即表示我们可在未收到消息RegisterUserMessage前收到UserValidatedMessage

因为UserValidatedMessage的处理过程消耗的资源相对较多——写入数据库,并向所有 Web 服务器发布消息,恶意用户用不多的消息就可以发起拒绝服务攻击( DOS ),同时也能躲开多数探测系统的眼睛。而要想依靠 GUID 欺骗则困难得多。不过,因为注册流程的处理实例可以缓存于内存中,这将大大降低相关数据搜索带来的资源消耗——甚至可以小到在探测系统察觉前,DOS 攻击不能发生作用。

对带宽和服务器资源的要求降低

这个解决方案,使我们可以通过扩展 Web 层,规避对数据层的巨大压力和扩展需求。同时,它也能大大节省带宽消耗。对于用户名和密码而言,这看起来不是大问题,但在其他一些情况下,需处理的数据量可能大很多。当然,在此方案中处理用户信息所需的时间也会大大缩短,因为我们不需要在 Web 服务器(位于 DMZ)、应用服务器和数据库服务器间来回奔走。

在这个解决方案中,我们应该谨记的部分是消息发布 / 订阅。nServiceBus 提供的在消息发布 / 订阅基础上设计系统的 API 十分简单。消息发布,是实现系统扩展性的核心部分。随着用户的增长,你只需要增添 Web 服务器,而不是数据库服务器。在整个解决方案,每请求所消耗的平均要小很多,因为所有的工作都是在接收请求的服务器本地完成。

锦上添花:ETags

为方便高级用户,我们还将此解决方案封装成了 ETags 。Web 服务器停止运行时,缓存会被清空,我们能做的就是将缓存内容记录到磁盘上去(可用后台线程),并用服务器随 UsernameInUseMessage 消息一起传给我们的某种数据作为标记。这样,Web 服务器重新启动后,它请求 GetAllUsernamesMessage 时可同时发出 ETag,应用服务器就只需要发送有变动的数据。使用“If-Modified-Since”头 HTTP GET 的 REST (译者注:可参看 深入浅出REST), 也能很好解决这个问题。所有这些措施,都可以依靠Web 服务器上磁盘空间的较小消耗,大大降低对网络带宽的需求。

结束语

即便你只有一台机子,同时充当Web 和数据库服务器,在这个解决方案基础上构建的系统的运行效率也会很高。如果服务器更多,性能自然会更好。不仅如此,此方案还极具可扩展性——即使你得到了8 百万Facebook 用户,也不会因为遭受重大冲击而必须修改整个系统架构。

更多信息

http://www.nservicebus.com/

nServiceBus 是一个用于构建企业级.NET 系统的开源通讯框架。它在消息发布 / 订阅支持、工作流集成和高度可扩展性等方面表现优异,因此是很多分布式系统基础平台的理想选择。

Podcast on Autonomous Servers and Publish/Subscribe

我们在这里主要研究服务自治、消息发布 / 订阅、异常、数据复制、系统复用和监管等领域的问题。

作者简介

Udi Dahan:以主张简化软件闻名,Microsoft Solutions Architect MVP,公认的.NET 专家,Microsoft Architects 和 Technologists Councils 会员。

Udi 为遍布世界各地的客户提供培训、指导和高端架构咨询服务,特别是想在 SOA、.NET 架构扩展和安全性设计和 Web Service 领域。

他也是 INETA(International Speakers Bureau of the International .NET Association)的会员、IASA(International Association of Software Architects)准会员,经常出席各种技术会议;Dr. Dobb’s 期刊 Web Service、SOA 和 XML 专栏作家。他的网址是 http://www.UdiDahan.com。

查看英文原文: Asynchronous, High-Performance Login for Web Farms

2008 年 1 月 22 日 23:341679
用户头像

发布了 26 篇内容, 共 66294 次阅读, 收获喜欢 1 次。

关注

评论

发布
暂无评论
发现更多内容

第八周心得

方堃

week 08作业

Safufu

架构师训练营week8作业

小高

数据结构与算法、网络模型总结

2流程序员

第八周课程总结

考尔菲德

找出两个链表交点(golang版)

2流程序员

判断两单链表是否相交

石印掌纹

架构师训练营Week8作业总结

小高

week08总结

Safufu

架构师训练营第八周总结

邵帅

作业-第八周

superman

一个文学青年的至暗时刻

半亩房顶

反思 就业

哪些资源容易造成性能瓶颈

彭阿三

IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效

YourBatman

IDEA 新特性 IntelliJ IDEA

HTML5+CSS3前端入门教程---从0开始通过一个商城实例手把手教你学习PC端和移动端页面开发第4章CSS文本样式

Geek_8dbdc1

CSS

HTML5+CSS3前端入门教程---从0开始通过一个商城实例手把手教你学习PC端和移动端页面开发第5章CSS盒子模型

Geek_8dbdc1

CSS

领域驱动设计 学习笔记

半亩房顶

DDD

HTML5+CSS3前端入门教程---从0开始通过一个商城实例手把手教你学习PC端和移动端页面开发第3章初识CSS

Geek_8dbdc1

CSS

Week08总结

熊威

week8 总结

a晖

架构师训练营0期-8周作业

WW

Week 08 作业

鱼_XueTr

EasyDL全新升级,文心(ERNIE)3项能力助力快速定制企业级NLP模型

百度大脑

人工智能 nlp 百度大脑

Week08作业

熊威

架构师训练营——第8周学习总结

jiangnanage

week8 作业

a晖

架构师训练营作业

邵帅

week8 作业

雪涛公子

初识 - DDD-CQRS

半亩房顶

DDD CQRS

Week8作业

丿淡忘

数据结构&网络通讯原理

石印掌纹

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

Web Farm中异步、高效的用户登录解决方案-InfoQ