随着 Web 服务变得日益复杂,其实践者将需要掌握涵盖事务处理、数据库管理、中间件集成和异步消息传递的技能。IBM Lightweight Services (LWS) 是一个实验性的托管环境,旨在支持复杂服务的快速原型设计,同时使开发人员免受多线程、事务和资源锁定等高级问题的困扰。为了实现这一目标,我们采用了一种高级的、事件驱动的、单线程脚本环境来托管服务器端应用程序。开发人员可以免费使用此环境来创建强大的 Web 服务,这些服务可以存储持久性数据、使用其他服务以及与现有中间件集成。轻量级服务由标准 HTTP SOAP 客户端调用,反过来也可以使用 WSDL 调用其他 Web 服务。
在此,原型设计意味着创建功能齐全的 Web 服务,但优先考虑开发的便捷性而不是运行时性能。LWS 服务是事务性的,跨服务器故障维护状态,最重要的是,可以与标准 Web 服务客户端和服务器互操作。用户友好的开发工具弥合了 SOAP 数据类型和松散类型脚本之间的差距,从而允许 Web 服务操作在调试会话过程中逐步演变。基于脚本的服务由标准 WSDL 描述,客户端无需区分 LWS 服务或其更传统的对应项。一旦程序逻辑、数据需求和外部接口稳定下来,开发人员可以选择重新实现原型,以更优化的运行时环境为目标。
我们将使用两个具体的示例,其中基于脚本的原型设计为开发人员提供了通往功能性 Web 服务的简化路径。这些脚本不仅以少量源代码表示,而且由于灵活的持久性存储机制,可以非常快速地安装、运行和重新安装。然后,我们将了解如何将事务性持久性的“重量级”概念应用于这种简化的脚本环境。
我们将 JavaScript 视为一种轻量级语言,因为其相对简单性使其易于学习和使用。它通常与 DHTML 相关联,在 DHTML 中,它在网页中用作事件驱动的脚本语言。LWS 使用 JavaScript 作为一种高级的动态语言,用于开发可以访问 Web 服务和中间件系统(如即时消息传递)的服务器端进程。托管环境提供针对脚本语言微调的持久性和事务管理,从而最大限度地减少这些鲁棒性功能对脚本表达性编程风格的影响。请注意,虽然我们以 JavaScript 为例,但可以为其他语言(如解释型 Python 和 Scheme)创建类似的工具。
我们将首先查看两种不同类型的 Web 服务场景。数据中心型 Web 服务可以定义为关系数据库的直接的、请求驱动的包装器。这种类型的服务通常在 J2EE 环境中实现为 servlet 和/或 Enterprise JavaBean (EJB)。例如,我们将原型设计一个简单的电话簿服务,该服务将名称映射到电话号码,并提供用于访问数据的 get 和 set 操作。面向流程型 Web 服务执行复杂的操作(例如,网络请求),这些操作跨越或存在于单个用户请求之外。在 Java 等语言中实现这些服务通常需要更高级的编程技术,例如多线程和资源锁定。为了说明这种情况,我们将使用一个众所周知的温度服务来跟踪可配置的一组 ZIP 码的平均温度。此应用程序同时且按计划调度多个 Web 服务请求,并在新结果可用时将其合并。
没有必要完全理解以下 JavaScript 代码和编程 API。我们将逐步了解(相对简洁的)服务实现,重点关注其高级操作。
在电话簿服务的生产实现中,我们将定义一个数据库表来存储名称及其对应的电话号码,最有可能还会添加一个额外的“所有者”列来支持多个电话簿。为了进行原型设计,我们可以通过将数据存储在 JavaScript 对象中来跳过耗时的数据库管理任务。LWS 托管环境自动将 JavaScript 程序的 state(包括其顶级变量)存储在现有的通用数据库中。在运行时,这肯定不如使用专用电话簿数据库效率高,但对于原型设计和 100-200 个电话簿条目的数据集来说,性能绰绰有余。
电话簿脚本将其所有数据存储在单个通用对象中。由于 JavaScript 的扩展属性,我们可以像使用哈希表一样使用此对象。numbers 对象在脚本初始化时创建,程序员使用开发工具将 setPhoneNumber 和 getPhoneNumber 函数指定为 Web 服务操作。当调用其中一个操作时,托管环境会创建和管理新的数据库事务,从而支持与显式数据库访问相同的鲁棒性级别。如果底层系统在操作 numbers 变量的过程中失败,则整个 JavaScript 程序将回滚,放弃当前操作所做的任何更改。
var numbers = new Object();
function setPhoneNumber(name, number) {
numbers[name] = number;
}
function getPhoneNumber(name) {
return numbers[name];
}
服务的每个实例都对应于存储在 LWS 数据库中的 JavaScript 程序状态,并代表其自身的 Web 服务端点。为了支持多个电话簿,例如,针对不同的用户,开发人员(或其他程序)将创建服务的多个实例。然后,将为每个客户端分配与其电话簿对应的 Web 服务端点。
平均温度服务配置有 ZIP 码列表,然后定期从简单的查找服务中获取相应的温度。平均温度按需计算,以服务传入的请求。我们使用此场景来演示 Web 服务提供商也将其调用另一个服务作为客户端。更复杂的是,这些 Web 服务客户端操作不是直接响应传入的请求而执行的。相反,该服务按计划查询温度数据,而无需考虑对平均值的传入请求。
LWS 提供异步 Web 服务客户端 API,这样就没有 JavaScript 调用会阻塞网络操作。非阻塞 API 使我们能够在同一个单线程程序中处理 Web 服务客户端操作和传入请求。脚本开发人员指定 JavaScript 函数作为事件处理程序,以在结果可用时处理结果。除了是单线程之外,传入请求和事件处理程序也是序列化的,在处理下一个事件之前完整执行。对于多线程和同步的熟练用户来说,强制执行这种编程风格可能看起来很奇怪,但对于许多脚本开发人员来说,这是一个熟悉的场景,尤其是那些在网页中嵌入脚本的开发人员。
由于此脚本比第一个示例更复杂,因此我们将将其分解为几个部分。首先,考虑用于调用温度 Web 服务和存储结果的代码。顶级变量 temperatures 使用通用对象初始化;这将充当将 ZIP 码映射到温度的哈希表。然后,我们创建一个对象,表示我们要调用的温度查找服务。使用 WSDL 文档初始化后,此便捷对象将有助于创建 Web 服务请求。我们可以为 WSDL 指定完整的 URL,但在这里我们假设它作为我们的项目文件之一包含在内。
var temperatures = new Object();
var service = WebServices.WSDL.createService( “TemperatureService.wsdl”);
Web 服务客户端 API 的异步性质意味着我们需要用于启动请求的代码和用于处理结果的事件处理程序。getTemperature 函数接受 ZIP 码,使用上面定义的温度服务创建请求,并使用 JavaScript “点”表示法指定单个参数以访问对象属性。在将事件处理程序设置为 onGetTemperatureComplete 函数后,Web 服务调用将启动,并且 getTemperature 立即返回。
onGetTemperatureComplete 函数首先检查标准 response 参数是否成功。然后,它使用响应对象来获取原始请求中使用的 ZIP 码(请注意,可能随时有多个 “getTemp” 调用处于挂起状态,并且它们的结果可能以任何顺序到达)。然后,使用所请求位置的最新值更新 temperatures 哈希表。
function getTemperature(zipCode) {
var request = service.createRequest(“getTemp”);
request.body.zipcode = zipCode;
request.onComplete = onGetTemperatureComplete;
request.call();
}
function onGetTemperatureComplete(response) {
if (!response.failed) {
var zipCode = response.request.body.zipcode;
temperatures[zipCode] = response.result.value;
}
}
现在,我们将实现一些顶级初始化代码,以计划温度请求。我们首先解析以逗号分隔的 ZIP 码列表,该列表作为平均温度服务的此实例的配置参数提供。对于每个指定的位置,我们立即进行第一次 getTemperature 调用,然后使用特殊的计时器对象以可配置的时间间隔计划对同一函数的定期调用。或者,开发人员可以选择错开这些调用或根据结果的传入情况即时调整计划。
var zipCodes = Host.configuration.zipCodes.split(“,”);
for (var idx = 0; idx < zipCodes.length; idx++) {
var zipCode = zipCodes[idx];
getTemperature(zipCode);
var timer = new Host.Timer(getTemperature, [zipCode]);
timer.start(Host.configuration.period, true);
}
最后,我们将实现此 Web 服务公开的单个操作 getMeanTemperature。此函数只是累积已添加到 temperatures 哈希表中的任何结果,并返回最新的平均温度。开发人员将指示托管环境将返回值标记为 SOAP 浮点数,从而允许使用无穷大来表示“无可用结果”的情况。
function getMeanTemperature() {
var sum = 0, count = 0;
for (var zipCode in temperatures) {
sum += temperatures[zipCode];
count++;
}
return sum / count;
}
虽然此服务的实现与网页中嵌入的短暂的内存脚本非常相似,但它受益于事务性数据库的鲁棒性功能。托管环境不仅管理 temperatures 哈希表的持久性和一致性,而且还在服务器故障后恢复 getTemperature 调用的计划。挂起的 SOAP HTTP 请求将在服务器重启时被丢弃,但下一个计划的调用仍将发生。
IBM Lightweight Services 在很大程度上是一项实验,旨在最大限度地减少开发人员必须学习的内容,才能从持久的、事务性的编程环境中受益。我们已经讨论了脚本程序如何映射到持久性存储:服务的每个实例的程序状态都自动存储在通用数据库中。将自己限制为具有序列化事件处理程序的单线程脚本简化了此持久性方案;托管环境只需要在事件需要处理时获取程序的状态。这种安排与异步 API 相结合,还可以保护开发人员免受数据库编程中常见的资源锁定问题(例如,死锁)的困扰。
由于事件处理程序现在代表事务边界,因此脚本开发人员必须认识到,当处理程序函数抛出异常或未能完成时,整个程序状态都会回滚。对于以数据为中心、请求驱动的服务,对事务的基本认识足以大大简化实现。开发人员不再需要在面对异常时确保程序变量之间的一致性。对于复杂的、面向流程的服务,我们选择解决另外两个问题:管理非事务性资源和服务器重启。
虽然回滚对 JavaScript 变量的更改相对简单,但服务也可能,例如,发出 SOAP HTTP 请求或发送即时消息。脚本程序状态的回滚应如何影响对这些非事务性资源进行的不可撤销的操作?我们简化此经典问题的方法是与程序员建立以下约定:
1. 对非事务性资源的操作(例如,通过即时消息连接发送消息)由托管环境排队,直到当前事件处理程序返回。
2. 如果当前事件处理程序抛出异常或服务器发生故障,则 JavaScript 程序的 state 将回滚,并且排队的操作将被丢弃。
3. 如果当前事件处理程序成功返回,则托管环境会在当前事务提交后尝试执行排队的操作。可以使用其他事件处理程序检测错误。
虽然上述规则对大多数脚本的影响相对较小,但服务器重启带来了更大的挑战。如果程序存储表示与即时消息服务器的连接的对象,则在服务器故障后,托管环境应执行什么操作?尝试恢复连接?触发错误条件?这两种方法都有缺点,我们制定了一种旨在最大限度地减少程序员工作量的折衷方案。默认情况下,不会恢复与非事务性资源的连接,并且不会通知脚本服务器重启。LWS 确实为开发人员提供了一个可选事件来处理服务器重启,从而允许在适当的时候立即重新打开连接。此外,我们的中间件 API 的设计方式是,只需一行脚本即可重新建立连接,并自动回忆起以前连接的规范。
// 创建即时消息会话。
var session = new Sametime.Session();
...
session.connect(“host.ibm.com”, “user”, “password”);
// 处理服务器重启。
Host.onResume = onResume;
function onResume() {
session.connect();
}
在这种环境中,简单服务的开发人员只需对事务和持久性有最基本的了解即可获得成功。只有在需要更复杂的操作时,开发人员才需要识别上述编程约定并显式处理服务器重启。
简化开发人员体验的一个重要部分是丰富的集成开发环境 (IDE)。服务是脚本代码和元数据的组合,使用 Eclipse 的 LWS 插件创建和打包。如上所述,导出 Web 服务操作和分配 SOAP 类型(JavaScript 默认不强制类型)等开发任务是通过用户界面而不是通过代码执行的。
我们的 IDE 最重要的功能是用于与远程 LWS 服务器交互的“一键式”部署和调试操作。只需单击一个键或鼠标,开发人员就可以删除服务的先前实例,使用最新的源代码重新打包,安装更新的包,并创建一个新的实例以进行调试。一旦服务被实例化,开发人员就可以立即访问命令行,以与脚本的程序状态交互,以及每个实例的日志,以查看调试输出和错误条件。命令行解释器对于直接测试操作特别有用,然后再创建相应的 Web 服务客户端。
我们预计,未来关于轻量级服务的大部分工作将与持久性数据的管理有关。从理论上讲,动态 JavaScript 可以创建任意数据结构,并且脚本的每个实例可能具有不同的存储需求。在实践中,我们希望我们的存储机制能够利用同一服务的多个实例之间的相似性,从而允许我们在适当的情况下执行关系查询。动态分配持久性变量的能力是一项有价值的开发功能,但在服务部署后很少发生。考虑电话簿示例,其中多个用户创建了他们自己的服务实例,并带有他们自己的哈希表。在基于关系数据库的实现中,我们可以查询包含特定名称或号码的所有电话簿。在灵活但结构松散的轻量级服务方案中,无法有效地执行相同的搜索。混合存储方案,尤其是程序员的提示,可以将这两个世界的优势结合起来。
通过专注于开发人员体验,我们已经能够将简单的脚本环境适应到强大的服务器端应用程序领域。我们为 Web 普及的事件驱动脚本模型量身定制了一种新的持久性存储机制,从而使程序员免受事务处理中一些更高级问题的困扰。
我们用五行源代码实现了一个简单的示例,真正的节省在于安装时间。在大多数环境中配置相同的电话簿服务将需要手动创建(和管理)关系数据库表。更复杂的示例,平均温度服务,通常需要多线程、同步以及对数据库锁定问题的理解。利用简化的托管环境,我们用大约二十行 JavaScript 创建了一个强大的原型。我们希望我们的方法可以帮助脚本程序员和经验丰富的软件工程师 alike 从想法快速过渡到实施。
本专栏介绍了一组由 IBM 开发的工具和技术。该软件可从 IBM 的 alphaWorks 新兴技术站点下载,并附带免费评估许可证。IBM Lightweight Services 站点包含许多编程示例和教程,说明了这种 Web 服务开发风格。
• IBM alphaWorks 上的 IBM Lightweight Services
• 示例中的温度服务
###
CHRISTOPHER VINCENT 是 IBM Systems Group 互联网技术团队的软件工程师。他的研究兴趣包括快速应用程序开发、动态编程语言、即时消息传递和发布/订阅基础设施。
最初发表于 Queue vol. 1, no. 1—
在 数字图书馆 中评论本文
Niklas Blum, Serge Lachapelle, Harald Alvestrand - WebRTC - 开放 Web 平台的实时通信
在这个疫情时期,世界比以往任何时候都更加转向基于互联网的 RTC(实时通信)。在过去十年中,RTC 产品的数量呈爆炸式增长,这在很大程度上是由于更便宜的高速网络访问和更强大的设备,但也归功于一个名为 WebRTC 的开放、免版税平台。WebRTC 正在从实现有用的体验发展到在疫情期间对于让数十亿人继续工作和教育,并保持重要的人际交往至关重要。WebRTC 未来的机遇和影响确实引人入胜。
Benjamin Treynor Sloss, Shylaja Nukala, Vivek Rau - 重要的指标
衡量您的站点可靠性指标,设置正确的目标,并努力准确地衡量这些指标。然后,您会发现您的服务运行得更好,停机时间更少,并且用户采用率更高。
Silvia Esparrachiari, Tanya Reilly, Ashleigh Rentz - 跟踪和控制微服务依赖项
如果您曾经将钥匙锁在房屋或汽车内,您就会熟悉依赖循环。没有钥匙您就无法打开锁,但是不打开锁您就无法拿到钥匙。有些循环很明显,但是更复杂的依赖循环在导致中断之前可能很难找到。跟踪和控制依赖项的策略对于维护可靠的系统是必要的。
Diptanu Gon Choudhury, Timothy Perrett - 为互联网规模的服务设计集群调度器
希望构建调度系统的工程师应考虑其使用的底层基础设施的所有故障模式,并考虑调度系统的操作员如何在租户系统所有者进行故障排除期间配置补救策略,同时帮助保持租户系统尽可能稳定。