宁静·致远


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • RSS

设计一个容错的微服务架构(转)

发表于 2017-08-23 | 阅读次数

摘要:转载请保留出处:https://github.com/jasonGeng88/blog

原文地址

https://blog.risingstack.com/designing-microservices-architecture-for-failure/


微服务架构使得可以通过明确定义的服务边界来隔离故障。但是像在每个分布式系统中一样,发生网络、硬件、应用级别的错误都是很常见的。由于服务依赖关系,任何组件可能暂时无法提供服务。为了尽量减少部分中断的影响,我们需要构建容错服务,来优雅地处理这些中断的响应结果。

本文介绍了基于RisingStack 的 Node.js 咨询和开发经验构建和操作高可用性微服务系统的最常见技术和架构模式。

如果你不熟悉本文中的模式,那并不一定意味着你做错了。建立可靠的系统总是会带来额外的成本。

微服务架构的风险

微服务架构将应用程序逻辑移动到服务,并使用网络层在它们之间进行通信。这种通过网络间通信代替单应用程序内调用的做法,会带来额外的延迟,以及需要协调多个物理和逻辑组件的系统复杂度。分布式系统的复杂性增加也将导致更高的网络故障率。

微服务体系结构的最大优势之一是,团队可以独立设计,开发和部署他们的服务。他们对服务的生命周期拥有完全的所有权。这也意味着团队无法控制他们依赖的服务,因为它更有可能由不同的团队管理。使用微服务架构,我们需要记住,提供者服务可能会临时不可用,由于其他人员发行的错误版本,配置以及其他更改等。

优雅的服务降级

微服务架构的最大优点之一是您可以隔离故障,并在当组件单独故障时,进行优雅的服务降级。 例如,在中断期间,照片共享应用程序中的客户可能无法上传新图片,但仍可以浏览,编辑和共享其现有照片。

微服务容错隔离

在大多数情况下,由于分布式系统中的应用程序相互依赖,因此很难实现这种优雅的服务降级,您需要应用几种故障转移的逻辑(其中一些将在本文后面介绍),以为暂时的故障和中断做准备。

服务间彼此依赖,再没有故障转移逻辑下,服务全部失败。

变更管理

Google的网站可靠性小组发现,大约70%的中断是由现有系统的变化引起的。当您更改服务中的某些内容时,您将部署新版本的代码或更改某些配置 - 这总有可能会造成故障,或者引入新的bug。

在微服务架构中,服务依赖于彼此。这就是为什么你应该尽量减少故障并限制它的负面影响。要处理变更中的问题,您可以实施变更管理策略和自动回滚机制。

例如,当您部署新代码或更改某些配置时,您应该先小范围的进行部分的替换,以渐进式的方式替换服务的全部实例。在这期间,需要监视它们,如果您发现它们对您的关键指标有负面影响,应立即进行服务回滚,这称为“金丝雀部署”。

变更管理 - 回滚部署

另一个解决方案可能是您运行两个生产环境。您始终只能部署其中一个,并且在验证新版本是否符合预期之后才,将负载均衡器指向新的。这称为蓝绿或红黑部署。

回滚代码不是坏事。你不应该在生产中遗留错误的代码,然后考虑出了什么问题。如果必要,越早回滚你的代码越好。

健康检查与负载均衡

实例由于出现故障、部署或自动缩放的情况,会进行持续启动、重新启动或停止操作。它可能导致它们暂时或永久不可用。为避免问题,您的负载均衡器应该从路由中跳过不健康的实例,因为它们当前无法为客户或子系统提供服务。

应用实例健康状况可以通过外部观察来确定。您可以通过重复调用GET /health端点或通过自我报告来实现。现在主流的服务发现解决方案,会持续从实例中收集健康信息,并配置负载均衡器,将流量仅路由到健康的组件上。

自我修复

自我修复可以帮助应用程序从错误中恢复过来。当应用程序可以采取必要步骤从故障状态恢复时,我们就可以说它是可以实现自我修复的。在大多数情况下,它由外部系统实现,该系统会监视实例运行状况,并在较长时间内处于故障状态时重新启动它们。自我修复在大多数情况下是非常有用的。但是在某些情况下,持续地重启应用程序可能会导致麻烦。 当您的应用程序由于超负荷或其数据库连接超时而无法给出健康的运行状况时,这种情况下的频繁的重启就可能就不太合适了。

对于这种特殊的场景(如丢失的数据库连接),要实现满足它的高级自我修复的解决方案可能很棘手。在这种情况下,您需要为应用程序添加额外的逻辑来处理边缘情况,并让外部系统知道实例不需要立即重新启动。

故障转移缓存

由于网络问题和我们系统的变化,服务经常会失败。然而,由于自我修复和负载均衡的保障,它们中的大多数中断是临时的,我们应该找到一个解决方案,使我们的服务在这些故障时服务仍就可以工作。这就是故障转移缓存的作用,它可以帮助并为我们的应用程序在服务故障时提供必要的数据。

故障转移缓存通常使用两个不同的到期日期; 较短的时间告诉您在正常情况下缓存可以使用的过期时间,而较长的时间可以在服务故障时缓存依旧可用的过期时间。

故障转移缓存

请务必提及,只有当服务使用过时的数据比没有数据更好时,才能使用故障转移缓存。

要设置缓存和故障转移缓存,可以在 HTTP 中使用标准响应头。

例如,使用 max-age 属性可以指定资源被视为有效的最大时间。使用 stale-if-error 属性,您可以明确在出现故障的情况下,依旧可以从缓存中获取资源的最大时间。

现代的 CDN 和负载均衡器都提供各种缓存和故障转移行为,但您也可以为拥有标准可靠性解决方案的公司创建一个共享库。

重试逻辑

在某些情况下,我们无法缓存数据,或者我们想对其进行更改,但是我们的操作最终都失败了。对于此,我们可以重试我们的操作,因为我们可以预期资源将在一段时间后恢复,或者我们的负载均衡器将请求发送到了健康的实例上。

您应该小心地为您的应用程序和客户端添加重试逻辑,因为大量的重试可能会使事情更糟,甚至阻止应用程序恢复,如当服务超载时,大量的重试只能使状况更糟。

在分布式系统中,微服务系统重试可以触发多个其他请求或重试,并启动级联效应。为了最小化重试的影响,您应该限制它们的数量,并使用指数退避算法来持续增加重试之间的延迟,直到达到最大限制。

当客户端(浏览器,其他微服务等)发起重试,并且客户端不知道在处理请求之前或之后操作失败时,您应该为你的应用程序做好幂等处理的准备。例如,当您重试购买操作时,您不应该再次向客户收取费用。为每个交易使用唯一的幂等值键可以帮助处理重试。

限流器和负载降级

流量限制是在一段时间内定义特定客户或应用程序可以接收或处理多少个请求的技术。例如,通过流量限制,您可以过滤掉造成流量峰值的客户和服务,或者您可以确保您的应用程序在自动缩放无法满足时,依然不会超载。

您还可以阻止较低优先级的流量,为关键事务提供足够的资源。

限流器可以阻止流量峰值产生

有一个不同类型的限流器,叫做并发请求限制器。当您有重要的端点,您不应该被调用超过指定的次数,而您仍然想要能提供服务时,这将是有用的。

负载降级的一系列使用,可以确保总是有足够的资源来提供关键交易。它为高优先级请求保留一些资源,不允许低优先级的事务使用它们。负载降级开关是根据系统的整体状态做出决定,而不是基于单个用户的请求量大小。负载降级有助于您的系统恢复,因为当你有一个偶发事件时(可能是一个热点事件),您仍能保持核心功能的正常工作。

要了解有关限流器和负载降级的更多信息,我建议查看这篇Stripe的文章。

快速失败原则与独立性

在微服务架构中,我们想要做到让我们的服务具备快速失败与相互独立的能力。为了在服务级别上进行故障隔离,我们可以使用舱壁模式。你可以在本文的后面阅读更多有关舱壁的内容。

我们也希望我们的组件能够快速失败,因为我们不希望对于有故障的服务,在请求超时后才断开。没有什么比挂起的请求和无响应的 UI 更令人失望。这不仅浪费资源,而且还会影响用户体验。我们的服务在调用链中是相互调用的,所以在这些延迟累加之前,我们应该特别注意防止挂起操作。

你想到的第一个想法是对每个服务调用都设置明确的超时等级。这种方法的问题是,您不能知道真正合理的超时值是多少,因为网络故障和其他问题发生的某些情况只会影响一两次操作。在这种情况下,如果只有其中一些超时,您可能不想拒绝这些请求。

我们可以说,在微服务种通过使用超时来达到快速失败的效果是一种反模式的,你应该避免使用它。取而代之,您可以应用断路器模式,依据操作的成功与失败统计数据决定。

舱壁模式

工业中使用舱壁将船舶划分为几个部分,以便在船体破坏的情况下,可以将船舶各个部件密封起来。

舱壁的概念在软件开发中可以被应用在隔离资源上。

通过应用舱壁模式,我们可以保护有限的资源不被耗尽。例如,对于一个有连接数限制的数据库实例来说,如果我们有两种连接它的操作,我们采用可以采用两个连接池的方式进行连接,来代替仅采用一个共享连接池的方式。由于这种客户端与资源进行了隔离,超时或过度使用池的操作页不会使其他操作失败。

泰坦尼克号沉没的主要原因之一是其舱壁设计失败,水可以通过上面的甲板倒在舱壁的顶部,导致整个船体淹没。

泰坦尼克号舱壁设计(无效的设计)

断路器

为了限制操作的持续时间,我们可以使用超时。超时可以防止挂起操作并保持系统响应。然而,在微服务中使用静态、精细的超时是一种反模式,因为我们处于高度动态的环境中,几乎不可能提出在每种情况下都能正常工作的正确的时间限制。

替代这种静态超时的手段是,我们可以使用断路器来处理错误。断路器以现实世界的电子元件命名,因为它们的作用是相同的。您可以保护资源,并帮助他们使用断路器进行恢复。它们在分布式系统中非常有用,因为在分布式系统中,重复故障可能导致雪球效应并使整个系统瘫痪。

当特定类型的错误在短时间内多次发生时,断路器会被断开。开路的断路器可以防止进一步的请求 - 就像我们平时所说的电路跳闸一样。断路器通常在一定时间后关闭,在这期间可以为底层服务提供足够的空间来恢复。

请记住,并不是所有的错误都应该触发断路器。例如,您可能希望跳过客户端问题,例如具有4xx响应代码的请求,但不包括5xx服务器端故障。一些断路器也具有半开状态。在这种状态下,服务发送第一个请求以检查系统可用性,同时让其他请求失败。如果这个第一个请求成功,它将使断路器恢复到关闭状态并使流量流动。否则,它保持打开。

断路器

测试故障

您应该不断测试您系统的常见问题,以确保您的服务可以抵抗各种故障。您应经常测试故障,让您的团队具备故障处理的能力。

对于测试,您可以使用外部服务来标识实例组,并随机终止此组中的一个实例。这样,您可以准备单个实例故障,但您甚至可以关闭整个区域来模拟云提供商的故障。

最流行的测试解决方案之一是 Netflix 的 ChaosMonkey 弹性工具。

结尾

实施和运行可靠的服务并不容易。 您需要付出很多努力,同时公司也要有相应的财力投入。

可靠性有很多层次和方面,因此找到最适合您团队的解决方案很重要。您应该使可靠性成为您的业务决策流程中的一个因素,并为其分配足够的预算和时间。

主要收获

  • 动态环境和分布式系统(如微服务)会导致更高的故障机率;
  • 服务应该做到故障隔离,到达优雅降级,来提升用户体验;
  • 70%的中断是由变化引起的,代码回滚不是一件坏事;
  • 做到服务快速失败与独立性。团队是无法控制他们所依赖的服务情况;
  • 缓存、舱壁、断路器和限流器等架构模式与技术有助于构建可靠的微服务架构。

语义化版本2.0.0

发表于 2017-07-27 | 阅读次数

本文翻译自semver.org ,如需看原版请点击前面的链接。

摘要

版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

主版本号:当你做了不兼容的 API 修改,
次版本号:当你做了向下兼容的功能性新增,
修订号:当你做了向下兼容的问题修正。
先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

简介

在软件管理的领域里存在着被称作“依赖地狱”的死亡之谷,系统规模越大,加入的套件越多,你就越有可能在未来的某一天发现自己已深陷绝望之中。

在依赖高的系统中发布新版本套件可能很快会成为恶梦。如果依赖关系过高,可能面临版本控制被锁死的风险(必须对每一个相依套件改版才能完成某次升级)。而如果依赖关系过于松散,又将无法避免版本的混乱(假设兼容于未来的多个版本已超出了合理数量)。当你专案的进展因为版本相依被锁死或版本混乱变得不够简便和可靠,就意味着你正处于依赖地狱之中。

作为这个问题的解决方案之一,我提议用一组简单的规则及条件来约束版本号的配置和增长。这些规则是根据(但不局限于)已经被各种封闭、开放源码软件所广泛使用的惯例所设计。为了让这套理论运作,你必须先有定义好的公共 API 。这可以透过文件定义或代码强制要求来实现。无论如何,这套 API 的清楚明了是十分重要的。一旦你定义了公共 API,你就可以透过修改相应的版本号来向大家说明你的修改。考虑使用这样的版本号格式:XYZ (主版本号.次版本号.修订号)修复问题但不影响API 时,递增修订号;API 保持向下兼容的新增及修改时,递增次版本号;进行不向下兼容的修改时,递增主版本号。

我称这套系统为“语义化的版本控制”,在这套约定下,版本号及其更新方式包含了相邻版本间的底层代码和修改内容的信息。

为什么要使用语义化的版本控制?

这并不是一个新的或者革命性的想法。实际上,你可能已经在做一些近似的事情了。问题在于只是“近似”还不够。如果没有某个正式的规范可循,版本号对于依赖的管理并无实质意义。将上述的想法命名并给予清楚的定义,让你对软件使用者传达意向变得容易。一旦这些意向变得清楚,弹性(但又不会太弹性)的依赖规范就能达成。

举个简单的例子就可以展示语义化的版本控制如何让依赖地狱成为过去。假设有个名为“救火车”的函式库,它需要另一个名为“梯子”并已经有使用语义化版本控制的套件。当救火车创建时,梯子的版本号为 3.1.0。因为救火车使用了一些版本 3.1.0 所新增的功能, 你可以放心地指定相依于梯子的版本号大等于 3.1.0 但小于 4.0.0。这样,当梯子版本 3.1.1 和 3.2.0 发布时,你可以将直接它们纳入你的套件管理系统,因为它们能与原有相依的软件兼容。

作为一位负责任的开发者,你理当确保每次套件升级的运作与版本号的表述一致。现实世界是复杂的,我们除了提高警觉外能做的不多。你所能做的就是让语义化的版本控制为你提供一个健全的方式来发行以及升级套件,而无需推出新的相依套件,节省你的时间及烦恼。

如果你对此认同,希望立即开始使用语义化版本控制,你只需声明你的函式库正在使用它并遵循这些规则就可以了。请在你的 README 文件中保留此页连结,让别人也知道这些规则并从中受益。

语义化版本控制规范(SemVer)

以下关键词 MUST、MUST NOT、REQUIRED、SHALL、SHALL NOT、SHOULD、SHOULD NOT、 RECOMMENDED、MAY、OPTIONAL 依照 RFC 2119 的叙述解读。(译注:为了保持语句顺畅, 以下文件遇到的关键词将依照整句语义进行翻译,在此先不进行个别翻译。)

1.使用语义化版本控制的软件“必须 MUST ”定义公共 API。该 API 可以在代码中被定义或出现于严谨的文件内。无论何种形式都应该力求精确且完整。

2.标准的版本号“必须 MUST ”采用 XYZ 的格式,其中 X、Y 和 Z 为非负的整数,且“禁止 MUST NOT”在数字前方补零。X 是主版本号、Y 是次版本号、而 Z 为修订号。每个元素“必须 MUST ”以数值来递增。例如:1.9.1 -> 1.10.0 -> 1.11.0。

3.标记版本号的软件发行后,“禁止 MUST NOT ”改变该版本软件的内容。任何修改都“必须 MUST ”以新版本发行。

4.主版本号为零(0.y.z)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共 API 不应该被视为稳定版。

1.0.0 的版本号用于界定公共 API 的形成。这一版本之后所有的版本号更新都基于公共 API 及其修改内容。

5.修订号 Z(x.y.Z | x > 0)“必须 MUST ”在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改。

6.次版本号 Y(x.Y.z | x > 0)“必须 MUST ”在有向下兼容的新功能出现时递增。在任何公共 API 的功能被标记为弃用时也“必须 MUST ”递增。也“可以 MAY ”在内部程序有大量新功能或改进被加入时递增,其中“可以 MAY ”包括修订级别的改变。每当次版本号递增时,修订号“必须 MUST ”归零。

7.主版本号 X(X.y.z | X > 0)“必须 MUST ”在有任何不兼容的修改被加入公共 API 时递增。其中“可以 MAY ”包括次版本号及修订级别的改变。每当主版本号递增时,次版本号和修订号“必须 MUST ”归零。

8.先行版本号“可以 MAY ”被标注在修订版之后,先加上一个连接号再加上一连串以句点分隔的标识符号来修饰。标识符号“必须 MUST ”由 ASCII 码的英数字和连接号 [0-9A-Za-z-] 组成,且“禁止 MUST NOT ”留白。数字型的标识符号“禁止 MUST NOT ”在前方补零。先行版的优先级低于相关联的标准版本。被标上先行版本号则表示这个版本并非稳定而且可能无法达到兼容的需求。范例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。

9.版本编译信息“可以 MAY ”被标注在修订版或先行版本号之后,先加上一个加号再加上一连串以句点分隔的标识符号来修饰。标识符号“必须 MUST ”由 ASCII 的英数字和连接号 [0-9A-Za-z-] 组成,且“禁止 MUST NOT ”留白。当判断版本的优先层级时,版本编译信息“可 SHOULD ”被忽略。因此当两个版本只有在版本编译信息有差别时,属于相同的优先层级。范例:1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。

10.版本的优先层级指的是不同版本在排序时如何比较。判断优先层级时,“必须 MUST ”把版本依序拆分为主版本号、次版本号、修订号及先行版本号后进行比较(版本编译信息不在这份比较的列表中)。由左到右依序比较每个标识符号,第一个差异值用来决定优先层级:主版本号、次版本号及修订号以数值比较,例如:1.0.0 < 2.0.0 < 2.1.0 < 2.1.1。当主版本号、次版本号及修订号都相同时,改以优先层级比较低的先行版本号决定。例如:1.0.0-alpha < 1.0.0。有相同主版本号、次版本号及修订号的两个先行版本号,其优先层级“必须 MUST ”透过由左到右的每个被句点分隔的标识符号来比较,直到找到一个差异值后决定:只有数字的标识符号以数值高低比较,有字母或连接号时则逐字以 ASCII 的排序来比较。数字的标识符号比非数字的标识符号优先层级低。若开头的标识符号都相同时,栏位比较多的先行版本号优先层级比较高。范例:1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0- rc.1 < 1.0.0。

FAQ

1.在0.y.z初始开发阶段,我该如何进行版本控制?

最简单的做法是以 0.1.0 作为你的初始化开发版本,并在后续的每次发行时递增次版本号。

2.如何判断发布 1.0.0 版本的时机?

当你的软件被用于正式环境,它应该已经达到了 1.0.0 版。如果你已经有个稳定的 API 被使用者依赖,也会是 1.0.0 版。如果你很担心向下兼容的问题,也应该算是 1.0.0 版了。

3.这不会阻碍快速开发和迭代吗?

主版本号为零的时候就是为了做快速开发。如果你每天都在改变 API,那么你应该仍在主版本号为零的阶段(0.y.z),或是正在下个主版本的独立开发分支中。

4.对于公共 API,若即使是最小但不向下兼容的改变都需要产生新的主版本号,岂不是很快就达到 42.0.0 版?

这是开发的责任感和前瞻性的问题。不兼容的改变不应该轻易被加入到有许多依赖代码的软件中。升级所付出的代价可能是巨大的。要递增主版本号来发行不兼容的改版,意味着你必须为这些改变所带来的影响深思熟虑,并且评估所涉及的成本及效益比。

5.为整个公共 API 写文件太费事了!

为供他人使用的软件编写适当的文件,是你作为一名专业开发者应尽的职责。保持专案高效一个非常重要的部份是掌控软件的复杂度,如果没有人知道如何使用你的软件或不知道哪些函数的调用是可靠的,要掌控复杂度会是困难的。长远来看,使用语义化版本控制以及对于公共 API 有良好规范的坚持,可以让每个人及每件事都运行顺畅。

6.万一不小心把一个不兼容的改版当成了次版本号发行了该怎么办?

一旦发现自己破坏了语义化版本控制的规范,就要修正这个问题,并发行一个新的次版本号来更正这个问题并且恢复向下兼容。即使是这种情况,也不能去修改已发行的版本。可以的话,将有问题的版本号记录到文件中,告诉使用者问题所在,让他们能够意识到这是有问题的版本。

7.如果我更新了自己的依赖但没有改变公共 API 该怎么办?

由于没有影响到公共 API,这可以被认定是兼容的。若某个软件和你的套件有共同依赖,则它会有自己的依赖规范,作者也会告知可能的冲突。要判断改版是属于修订等级或是次版等级,是依据你更新的依赖关系是为了修复问题或是加入新功能。对于后者,我经常会预期伴随着更多的代码,这显然会是一个次版本号级别的递增。

8.如果我变更了公共 API 但无意中未遵循版本号的改动怎么办呢?(意即在修订等级的发布中,误将重大且不兼容的改变加到代码之中)

自行做最佳的判断。如果你有庞大的使用者群在依照公共 API 的意图而变更行为后会大受影响,那么最好做一次主版本的发布,即使严格来说这个修复仅是修订等级的发布。记住, 语义化的版本控制就是透过版本号的改变来传达意义。若这些改变对你的使用者是重要的,那就透过版本号来向他们说明。

9.我该如何处理即将弃用的功能?

弃用现存的功能是软件开发中的家常便饭,也通常是向前发展所必须的。当你弃用部份公共 API 时,你应该做两件事:(1)更新你的文件让使用者知道这个改变,(2)在适当的时机将弃用的功能透过新的次版本号发布。在新的主版本完全移除弃用功能前,至少要有一个次版本包含这个弃用信息,这样使用者才能平顺地转移到新版 API。

10.语义化版本对于版本的字串长度是否有限制呢?

没有,请自行做适当的判断。举例来说,长到 255 个字元的版本已过度夸张。再者,特定的系统对于字串长度可能会有他们自己的限制。

一个Guava HashBasedTable引发的故障

发表于 2017-07-25 | 阅读次数

前言

这篇故障从原因来看比较简单,但分享出来的原因是因为在分析故障原因的过程中忽略了一些关键因素,导致走了一些弯路,所以写出来供大家借鉴。

故障现象

上周负责的系统出现了一次故障,故障的表象是部分搜索超时严重。在对监控进行排查的时候,发现有一台服务器CPU使用率有比较大的飙升,为了不影响业务,先将此服务器重启。重启后,CPU使用率恢复正常,故障恢复(见下图)。
CPU使用率

故障原因分析过程

剩下的事情就是对故障原因进行分析了,是什么原因导致了请求的超时呢?
于是查看其它的监控,发现CPU使用率异常的服务器在相似的时间点,出现了大量的CLOSE_WAIT,这时立马警觉起来。我们都知道,TCPIP断开连接要经历4次挥手的过程,如下图:

TCPIP4次挥手过程

从上面这个图我们可以看出,CLOSE_WAIT状态是由于对端主动断开了连接导致的。在我们的这个case中,我们的系统是client端,那也就意味着,是server端主动断开了与我们的连接,而正常情况client端应该继续发送ACK和FIN指令完成4次挥手过程的,但很显然我们的系统并没有继续后续的挥手,开始查找原因。

CLOSE_WAIT监控

由于系统中使用了AsyncHttpClient组件(以下简称AHC),开始时怀疑是AHC使用不当造成的。比如如果在AHC的时候使用流处理,而在处理流的时候出现异常,很有可能导致连接不能关闭而导致CLOSE_WAIT出现。但排查代码确认我们的系统中没有使用流,此原因排除。

考虑到我们的异步请求是通过继承AsyncCompletionHandler抽象类来实现异步结果的处理,于是开始确认onCompleted和onThrowable方法是不是有一些特殊处理导致连接没有关闭,但看代码也没发现有什么问题。于是“排除”了自己代码的原因。这里用双引号是因为其实并没有真正排除代码的问题,而是忽略了一处有问题的代码。这里先卖个关子。

后面开始怀疑是否是AHC的bug导致,于是放狗(google)查资料,在AHC的issue讨论区也看到有人反馈CLOSE_WAIT的情况,但对方是在大访问量的情况下会导致CLOSE_WAIT出现(issue链接),但这个issue也没有得到AHC官方确认。考虑到我们的系统QPS不是很高,所以也排除了bug的可能性。

继续放狗,看到一篇博客(链接)讲netty里面的autoread特性会导致CLOSE_WAIT出现,但autoread是netty4里面的特性,我们使用的AHC依赖的是netty 3.10,这个原因也排除了。

至此,线索中断,于是求助余老师。PS:上面的那博客其实就是余老师写的,😆

余老师了解了情况之后,首先询问AHC是否使用的非单例(如果创建多个AsyncHttpClient实例是有可能导致大量CLOSE_WAIT的),答案是没有。
继续询问使用AHC的线程中是否使用了阻塞,答案是没有。
到这里,余老师也没给出更进一步的建议,所以继续排查。

第二天已经是周末,余老师继续帮忙定位问题,在看代码时发现有个地方的缓存使用了HashBasedTable,而HashBasedTable是非线程安全的,因为它使用了HashMap实现的,于是怀疑是这个地方可能会有问题。通过查看监控,发现故障时CPU、Load是同时增长的,而且CPU使用率在25%左右,基本可以确定在当时某个线程CPU使用率达到100%(服务器是4核)。

通过代码和监控的关联,可以确定这次故障的原因是因为使用了非线程安全的HashBasedTable,导致竞态条件下踩中了HashMap的坑导致当前线程CPU使用率100%,这样当前线程也就没有时间来处理后续的网络事件了,比如断开连接,最终导致大量CLOSE_WAIT以及超时出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private final Cache<String, Table<String,String,PackageFlightBean>> packageListBeanCache = CacheBuilder.from(packageListSpec).build();//<key,<code,PackageFlightBean>>

@Resource
private DomesticSearchService domesticSearchService;
@Resource
private InterSearchService interSearchService;
@Resource
private PackageSearchService packageSearchService;

public void putPackageFlightBean(MultiQuery query,String code,PackageFlightBean packageFlightBean){
List<String> codeList = splitter.splitToList(code);
if(codeList.size()!=2 || packageFlightBean==null){
return;
}
String key = query.genPackageKey();
Table<String,String,PackageFlightBean> beanTable = packageListBeanCache.getIfPresent(key);
if(beanTable==null){
beanTable = HashBasedTable.create();//这里使用了HashBasedTable,内部使用HashMap实现,非线程安全
beanTable.put(codeList.get(0),codeList.get(1),packageFlightBean);
packageListBeanCache.put(key,beanTable);
}else{
beanTable.put(codeList.get(0),codeList.get(1),packageFlightBean);
}

}

总结

这次故障原因分析的过程走了一些弯路,原因有这么几点:

1.没有及时发现某个线程CPU使用率达到100%的情况:

如果能及时发现这一点,那么很可能就能在第一时间反应到是死循环导致的,那么定位问题会更快一些。当然如果我们的监控系统能够发现某个线程CPU使用率100%并告警就更好了,这个建议已经反馈给OPS同学,他们在想办法做了。那么在现有的情况下,如果我们发现CPU使用率平滑持续在25%左右,那么我们也需要额外注意,因为在4核的系统中,这可能代表某个线程CPU使用率已经在100%了;

2.没有保留现场如线程栈之类的信息:
如果在重启服务之前保留了线程栈,那么定位问题会更方便一些,可惜当时的同学急于修复故障,而未做到这一点;

3.没有发现HashBasedTable的坑:

因为故障系统是个低频应用而且是从别的团队接手过来的,所以对代码不够熟悉,更不知道HashBasedTable是非线程安全的,最终导致故障的出现。这里也提醒一下,Guava里面的HashBasedTable、ArrayTable、TreeBasedTable、StandardTable、StandardRowSortedTable都是非线程安全的,使用的时候务必注意。另外,我们在使用在使用第三方工具的时候,务必要仔细查看相关说明,比如一些限制因素,避免踩坑。

Linux性能分析工具

发表于 2017-06-13 | 阅读次数

集群限流实现

发表于 2017-04-26 | 阅读次数

在上一篇文章中,提到了限流的方式分为单机限流和集群限流,在这篇文章中,就来看一下集群限流如何实现。

对于集群限流,我们一般会考虑以下三方面:

  1. 分布式:能够进行全局限流,尤其在访问受限制的资源的情况下。比如某个第三方接口只允许每秒100次的访问量,但我们的应用是部署在多台服务器上,在这种情况下,就需要进行全局限流了;
  2. 滑动窗口:能够避免流量突变(比如洪峰)对系统的影响。比如我们限制了1秒的访问量不能超过100,但很有可能出现一种情况就是前900毫秒没有任何请求,但900毫秒的时候突然来了100个请求;在第2秒的前100毫秒又来了100个请求。这也就意味着在200毫秒的时间涌入了200个请求。这种情况也很有可能把系统打挂掉。
  3. 最小请求间隔:为了避免请求太频繁,还需要限制调用端的请求时间间隔,如果时间间隔太短,同样也需要限制;

对于分布式的限流,需要使用分布式存储来存放请求的状态数据,在这里,我们使用Redis作为存储引擎。

第一版实现

限流的标准实现一般是使用令牌桶算法,实现思路如下:

  1. 每个接口都有一个对应的令牌桶,桶中含有若干令牌;
  2. 当用户发起请求的时候,首先从桶中获取一个令牌。在获取令牌的时候,首先会检查桶中剩余的令牌;
  3. 如果桶是空的,则表明已经达到上限,请求将被阻塞住;
  4. 反之,从桶中移除一个令牌,执行用户请求;
  5. 随着时间推移,按照一定的频率往桶里放入令牌,直到达到容量上限。

这种算法的好处在于占用存储空间小,每个接口只需要一个整形计数器即可,不足之处在于:需要有额外的处理程序往桶里放令牌。如果系统有很多的接口,那也就意味着系统需要有很多的桶,而且每个桶都需要按照固定的速率放入令牌,那么这不仅会增加系统的压力,也会增加系统的复杂性。于是我们可以在此基础上进行优化:

改进实现

  1. 每个接口有2个对应的key:令牌桶和桶被填充满时的时间戳;
  2. 当接口被访问的时候,读取对应的时间戳;
  3. 计算从上次访问到现在该请求可以获取的令牌数量;
  4. 如果满足要求,请求继续执行。

不幸的是,这个算法也存在缺陷:我们需要同步令牌桶和时间戳的状态,如果同步失败,这种方案将会失效。Redis可以通过批量操作来保证原子操作,但计算用户可以获取多少令牌的时候,我们需要与Redis有两次交互:第一次获取上次访问的时间戳,第二次设置令牌的数量。如果你不想钻研Redis Lua脚本,恐怕我们无法在一个原子操作中实现上述需求。鉴于此,如果有2个客户端在同一时间来校验用户请求的时候,我们可能会得到下面的执行顺序:

  1. 用户有足够的令牌来执行一个请求;
  2. 前后请求的间隔内,并没有新的令牌放入桶中;
  3. 客户端1获得存储的时间戳和令牌数量;
  4. 客户端2获得存储的时间戳和令牌数量;
  5. 客户端1通过计算断定令牌足够,放行请求,通知redis清空令牌桶;
  6. 客户端2通过计算也断定令牌足够,放行请求,通知redis清空令牌桶;

无需多言,上述方案完全失效了,显然这情况并不是我们想要的。

最佳实践

幸运的是,Redis有另外一种数据结构可以避免上述竞态条件-Sorted Set。下面是实现思路:

  1. 每个接口都有一个对应的Sorted Set,set中key和value是唯一的,key和value都为请求提交时的时间戳;
  2. 当客户端提交请求时,首先将某个时间间隔前的元素全部清除,这个操作可以使用Redis的ZREMRANGEBYSCORE命令实现;
  3. 使用ZRANGE(0, -1)命令获取set中剩余的所有元素;
  4. 使用ZADD命令将当前请求的时间戳添加到set中;
  5. 设置TTL给set;TTL=限制请求的时间间隔。

进行完上述所有操作后,计算获取到的元素数量,如果超出限制,则拒绝请求;

同时,获取获取的元素中最大的时间戳,与当前请求时间做比较。如果间隔太小,则拒绝请求;

这种方案的好处是所有的redis请求都可以使用MULTI命令作为一个原子操作。这意味着如果两个客户端请求相同的接口,是能够避免前面提到的竞态条件出现的。

服务化架构-服务降级

发表于 2017-04-23 | 阅读次数

1. 什么是降级

高可用系统为了保证自身的高可用性,会在异常情况下限制自身的一些能力,来保证核心功能的可用性。这有点类似武侠小说里面的壮士断腕,也有点类似于象棋里面的弃车保帅。

2. 为什么需要降级?

在系统复杂度越来越高的今天,我们可能会经常遇到这样的困扰:一个非核心的功能异常最终导致了整个系统的不可用。比如一个获取非核心数据接口的超时最终导致了整个线程池全部阻塞,影响了核心功能线程的运行;业务链条中某个环节的接口不可用导致整个业务链的失败。这样的例子比比皆是,造成的损失往往也非常大,所以为了避免这种小功能搞垮大系统的情况发生,降级的概念就应运而生了。

3. 降级预案

在要对系统制定降级策略之前,我们先对系统中的功能、服务进行梳理,识别出系统中的核心、非核心功能,这样能够梳理出哪些失败需要阻断主流程,哪些失败可以不阻断主流程。这样我们也就能确定哪些功能和服务必须死保,哪些功能和服务能够降级。
举个栗子:在机票的搜索流程中,报价、航班详情(如起降时间、航站楼)是关键信息,这些信息是无论如何不能缺失的;但餐食信息对于用户来讲就是可有可无的,当系统压力比较大的时候,我们就可以将餐食信息的获取给关掉,来优先保证核心数据的正常获取;
再举个栗子:对于机票的信息中,报价信息是经常变化的,但航班详情信息变化频率就没那么大。那么在系统压力比较大的情况下,可以将获取航班详情的方式由实时获取改为读取缓存,这样既不会导致数据准确性有太大的变化,也保证了系统核心功能的可用。

4. 降级策略

4.1 按功能降级

按功能降级是指在不影响核心功能的情况下,减少对非核心功能的使用,来保护系统的基本使用。
比如前面提到的例子,在机票搜索过程中,如果系统负载比较大出现超时,可以先将中转拼接报价计算功能(非核心功能)关闭,减少计算量,来降低系统负载保证用户搜索不至于失败;
再比如,在双11或者618的时候,系统的负载往往都比较大,这时候为了降低系统的负载,系统往往会把诸如支付后推荐、买了又买之类的非核心功能关闭。这些case我们可以参考淘宝、京东同学写的技术文章,例子不胜枚举。

4.2 按来源降级

在某些时候,系统的QPS比较高,服务器扛不住压力的时候,可以针对请求进行限流。当然对于限流我们不能一刀切,如果能先限制某些不重要业务的访问,那是最好的。如果所有请求的重要程度是一样的,那么只能按照统一的策略进行限流了,毕竟牺牲掉部分用户请求来保证服务的可用性是第一位的。

4.3 按质量降级

比如利用缓存。在机票行业,飞机上的舱位就相当于电商行业的库存,不同的是,电商的库存基本是自己管理,但机票的库存(舱位)是航司管理的,查询可用舱位花费的成本又很高。为了解决成本,我们可以针对某些冷门航线的舱位使用缓存,而且缓存时间设置得比较久,毕竟这些航线出票量比较少,没必要去占用过多的查询指令。

5. 降级介入方式

在系统遇到异常情况时,我们需要执行事先准备的降级预案,这时候就需要额外的分支介入系统逻辑,来执行系统降级策略。降级的介入方式分为两种:人工介入和自动介入。当然有的时候在系统所处的局面比较复杂的时候,往往需要两者方式都需要使用,比如自动降级失效的情况下,那么必须进行人工介入了。

5.1 人工介入

人工介入的方式是指当系统维护人员在发现系统异常之后,通过人工修改参数、关闭服务等方式进行降级的方法。这种方式的好处是比较灵活,能够根据异常情况灵活应对;但弊端是对人的要求比较高,一来需要维护人员对系统有足够的了解,另外要求维护人员在系统异常时能够在第一时间进行处置。

5.2 自动介入

自动介入的方式一般是当系统达到某些设定的条件之后,自动执行一些策略。比如当系统以来的某个第三方数据接口的失败率达到设定的阈值,那么就使用缓存数据;再比如购买机票的时候,如果遇到航司接口维护,那么我们可以先收单,保证主流程不阻断,等航司接口恢复之后再执行出票处理。

服务化架构-服务的限流

发表于 2017-04-22 | 阅读次数

1. 什么是限流

顾名思义,限流就是限制请求量,当用户请求量达到设定的阈值之后,对超出部分的请求进行拒绝或者延时处理。

2.为什么要限流

限流是服务提供方进行自我保护的一种手段。如果对用户请求量不做限制,那么过多的请求很有可能会将服务器有限的资源消耗殆尽,最终导致服务器宕机,进而引发雪崩。所以对于高可用的系统来讲,限流是一种必不可少的手段。

3. 限流算法

3.1 令牌桶(Token bucket)

令牌桶算法的基本过程如下:

  1. 每秒会有 r 个令牌放入桶中,或者说,每过 1/r 秒桶中增加一个令牌;
  2. 桶中最多存放 b 个令牌,如果桶满了,新放入的令牌会被丢弃;
  3. 当一个 n 字节的数据包到达时,消耗 n 个令牌,然后发送该数据包;
  4. 如果桶中可用令牌小于 n,则该数据包将被缓存或丢弃;

令牌桶算法的实现,可以借助于Google guava的RateLimiter实现,使用SmoothBursty模式。具体实现将在后面的blog中进行详细说明。

3.2 漏桶(Leaky bucket)

漏桶算法强制一个常量的输出速率而不管输入数据流的突发性,当输入空闲时,该算法不执行任何动作.就像用一个底部开了个洞的漏桶接水一样,水进入到漏桶里,桶里的水通过下面的孔以固定的速率流出,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率.如下图所示:

漏桶的实现,可以使用Guava RateLimiter的SmoothWarmingUp模式实现。

3.3 令牌桶与漏桶算法的比较

令牌桶算法能够允许一定程度的流量突增,要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的上限,因此它适合于具有突发特性的流量。
漏桶算法能够强行限制数据的传输速率,因此更适合进行平滑限流的应用场景,比如限制下载速率。

4. 限流方式

4.1 单机限流

单机限流一般使用在对硬件资源进行保护的场景中,避免服务器过载而宕机,引发进一步的雪崩。比如对访问量进行限流。有同学可能会问了,我评估一个集群总体的阈值,用集群限流不也可以吗?这里面存在的隐患是,如果负载均衡策略不够均衡,可能会导致部分服务器存在热点,那么那些流量仍然有可能将这台热点服务器打挂掉,从而引发雪崩。

4.2 集群限流

集群限流的方式一般会使用在某些外部资源存在访问限制的情况下,比如依赖的第三方系统限制了请求的访问量,那么上游的系统就需要在请求第三方系统前进行全局的限流,避免对方因过载而拒绝服务。比如说抓取。

使用 jrebel 远程部署开发环境的 dubbo 服务

发表于 2017-04-11 | 阅读次数

本文转自http://wulfric.me/2017/04/jrebel/ ,如果转载请尊重原作者版权。

声明: 本文采用 CC BY-NC-ND 4.0 授权。

jrebel

在开发 java 程序的时候,如果改动非常频繁,每次改动都要重新打包、部署,对于很重的应用(比如本文所说的 dubbo provider 服务),这样反复的流程十分消耗精力和热情。我们需要一个工具,当代码作改动之后能够立即看到效果。Java 在 1.4 的时候引入了 HotSwap 技术,允许调试者使用同一个类标识来更新类的字节码。这意味着所有对象都可以引用一个更新后的类,并在它们的方法被调用的时候执行新的代码,这就避免了无论何时只要有类的字节码被修改就要重载容器的这种要求1。不幸的是,这种重定义仅限于修改方法体—除了方法体之外,它既不能添加方法或域,也不能修改其他任何东西。

和 PHP 这样的脚本语言不一样,Java 部署是一个令人心烦的问题。一个运行中的 Java 程序相当于一辆在公路上奔跑的汽车,而 HotSwap 技术只能让你更换坐垫,但是让你想要更换轮胎的时候,HotSwap 就帮不上忙了,这个时候你只能停下来,换掉轮胎,再重新启动。而对于 PHP 来说,每个请求都会新开一个进程加载所有的 .php 文件编译和执行,所以不存在这个问题。运行 PHP 程序相当于给静止的汽车拍照,你大可以换完轮胎之后再拍照,这没有任何不良影响。(这个比喻可能不太恰当,却很有趣:在比喻中,静态的语言看起来是运动的,而动态语言看起来是静止的)

JRebel 是 java 的热部署插件,它监控已编译的 .class 文件,只要有变动,就立即更新在部署好的应用上,能够实现实时查看代码变化的功能。

skip build, package and deploy

skip build, package and deploy

HotSwap 是工作在虚拟机层面上,且依赖于 JVM 的内部运作,JRebel 用到了 JVM 的两个显著的功能特征—抽象的字节码和类加载器。类加载器允许 JRebel 辨别出类被加载的时刻,然后实时地翻译字节码,用以在虚拟机和可执行代码之间创建另一个抽象层1。这种技术也应用在 zeroturnaround 家其他的工具,比如 xrebel。

如果继续使用上面的比喻,JRebel 相当于在汽车运行过程中,先额外生成一个轮胎,然后以迅雷之势替换掉目标轮胎—汽车不需要停止装载和再启动。

安装和使用

JRebel 的安装参照官网即可,我是直接在 Intellij 的插件中心安装的,速度比较慢,插件也比较大,挂了代理才下载完成。下载完毕重启 IDE 即可使用。

JRebel 是收费插件,第一次打开的时候需要激活,网上有各种神奇教程可以参考。其实这个插件官方提供了免费的激活码,只要需要你注册一个帐号,并关联一个 twitter 或者 facebook 帐号即可。jrebel 需要这个关联帐号的写权限,会定期发送使用统计到你的 timeline,你可以使用一个小号来关联。

安装和激活完毕之后,就可以在 IDE 的配置页看到 JRebel 选项了。

jrebel preference

jrebel preference

运行

为了能够比较熟悉该插件的使用,建议按照官方教程,先通过 IDE 本地启动,再通过命令行启动,然后尝试部署到远端服务器。

当在 console 窗口观察到这些输出之后,说明应用正确启用了 JRebel 插件。

2017-04-01 16:27:31 JRebel:  #############################################################
2017-04-01 16:27:31 JRebel:
2017-04-01 16:27:31 JRebel:  JRebel Agent 7.0.6 (201703201213)
2017-04-01 16:27:31 JRebel:  (c) Copyright ZeroTurnaround AS, Estonia, Tartu.
2017-04-01 16:27:31 JRebel:
2017-04-01 16:27:31 JRebel:  Over the last 2 days JRebel prevented
2017-04-01 16:27:31 JRebel:  at least 0 redeploys/restarts saving you about 0 hours.
2017-04-01 16:27:31 JRebel:
2017-04-01 16:27:31 JRebel:  JRebel started in remote server mode.
2017-04-01 16:27:31 JRebel:
2017-04-01 16:27:31 JRebel:
2017-04-01 16:27:31 JRebel:  #############################################################

IDE 运行

IDE 运行本地应用最简单。参见官方教程。

  1. 执行mvn clean清空 target 目录;
  2. 打开 「View > Tool Windows > JRebel」调出 JRebel Panel 编辑窗,点击「generate rebel.xml」按钮选中需要监控和热部署的模块。因为是本地执行,所以另外一个「generate rebel-remote.xml」 不需要选中;
  3. 将以前通过 「Run ‘MyApp’」或者「Debug ‘MyApp’」来运行的命令改成「Run with JRebel ‘MyApp’」或者「Debug with JRebel ‘MyApp’」即可;
  4. 修改代码,然后在「Build」菜单下选择合适的构建选项(只构建单个文件/构建模块/构建项目);
  5. 访问接口,查看结果是否实时改变。

命令行运行

在 JRebel 设置页的「Startup」标签中选择「Run locally from command line」, 选择合适的 jvm 和目标环境。我的 dubbo 使用了 netty 部署,所以选择了「Standalone application」,给出的启动参数如下。

# agentpath 是必需的,foo.bar.MyApp 是你要运行的程序
java -agentpath:/Users/xxxx/Library/Application Support/IntelliJIdea2017.1/jr-ide-idea/lib/jrebel6/lib/libjrebel64.dylib foo.bar.MyApp

P.S.: 注意上面的「Application Support」需要换成「Application\ Support」。

dubbo 一般推荐使用 start.sh 脚本来启动应用,我们编辑 start.sh 文件,在启动的具体位置那里,将「-agentpath:…」添加在 java 后面,大致如下。

java -agentpath:... $JAVA_OPTS $JAVA_MEM_OPTS $JAVA_DEBUG_OPTS -classpath ...

各种目标环境的配置方法有着或大或小的区别,请按照自己的目标环境跟着 JRebel 给的配置方案来设置。

  1. 配置启动参数,如上所述;
  2. 选择需要热部署的模块;
  3. 执行start.sh部署应用;
  4. Run with JRebel ‘MyApp’;
  5. 修改代码,构建改动的文件;
  6. 访问接口,查看结果是否实时改变。

从输出结果可以看到,JRebel 接管了 console。

远程服务器运行

官方教程1,教程2更好2。

同上,在 JRebel 配置页选择「Run on a remote server」,根据给出的建议配置好启动项。

java -agentpath:... -Drebel.remoting_plugin=true ... -jar ...

在 JRebel 配置页添加远程服务器地址,注意要是能够 http 访问的 url 才可以。

添加远程服务器

添加远程服务器

  1. 配置启动参数和添加远程服务器,如上所述;
  2. 选择需要热部署的模块,注意这个时候「generate rebel.xml」和「generate rebel-remote.xml」 两个都要选中;
  3. 将包含 rebel.xml 和 rebel-remote.xml 的代码部署到远程服务器。本地打包并上传,或者代码上传并在远程服务器打包都可以;
  4. 在远程服务器上start.sh启动应用;
  5. 修改代码,构建改动的文件;
  6. 访问接口,查看结果是否实时改变。

P.S.: 教程1似乎有些问题,远端启动应用后,本地不需要再「Run with JRebel ‘MyApp’」了。

P.S.: 在官方教程里,「Run with JRebel ‘MyApp’」和「generate rebel.xml」checkbox 的图标一模一样,注意区分:有的是让你启动,有的是选中。

P.S.: JRebel 对 Spring 的 bean 注入默认就有很好的支持,参见 JRebel Spring。它可以根据 xml 文件或者注解自动重新装载 bean。还支持动态实时地在 Spring 中添加 bean 和依赖。

  1. HotSwap 和 JRebel 原理原文,译文 ↩ ↩2

  2. 参照官方的详细使用文档是更好的选择 ↩

Spring xml扩展机制

发表于 2017-03-06 | 阅读次数

介绍

Spring框架从2.0版本开始,提供了基于Schema风格的XML扩展机制,允许开发者扩展最基本的spring配置文件,这样我们就可以编写自定义的xml bean解析器然后集成到Spring IoC容器中。很多支持Spring的框架,比如Dubbo、mybatis都提供了对Spring xml扩展的支持。

Xml扩展大概有以下几个步骤:

  • 编写xml schema来描述自定义元素
  • 编写自定义类
  • 编写NamespaceHandler的实现类
  • 编写BeanDefinitionParser实现类
  • 把上述组件注册到Spring

编写自定义schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.mycompany.com/schema/product"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
targetNamespace="http://www.mycompany.com/schema/product"
elementFormDefault="qualified"
attributeFormDefault="unqualified">

<xsd:import namespace="http://www.springframework.org/schema/beans"/>

<xsd:element name="robot">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="beans:identifiedType">
<xsd:attribute name="brand" type="xsd:string" use="required"/>
<xsd:attribute name="type" type="xsd:string" use="required"/>
<xsd:attribute name="height" type="xsd:int"/>
<xsd:attribute name="weight" type="xsd:float"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
</xsd:element>
</xsd:schema>

编写自定义类

1
2
3
4
5
6
7
public class Robot {

private String brand;
private String type;
private int height;
private float weight;
}

然后我们就可以在xml中定义如下的robot元素:

1
<product:robot id="bb8" brand="StarWar" type="bb8" height="100" weight="20"/>

编写NamespaceHandler

NamespaceHandler用于解析我们自定义名字空间下的所有元素,目前我们要解析上面的product:robot元素。
NamespaceHandler里面只有3个方法:

  • init()会在NamespaceHandler初始化的时候被调用。
  • BeanDefinition parse(Element, ParserContext) - 当Spring遇到一个顶层元素的时候被调用。
  • BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext) - 当Spring遇到一个属性或嵌套元素的时候调用.

Spring提供了默认实现类NamespaceHandlerSupport,我们只需在init的时候注册每个元素的解析器即可。

1
2
3
4
5
6
7
8
public class ProductNamespaceHandler extends NamespaceHandlerSupport {

@Override
public void init() {
//遇到robot元素的时候交给RobotBeanDefinitionParser来解析
registerBeanDefinitionParser("robot", new RobotBeanDefinitionParser());
}
}

编写BeanDefinitionParser

这样在解析xml过程中遇到robot元素时,Spring会交给RobotBeanDefinitionParser来解析。RobotBeanDefinitionParser取出相应的属性然后设置到bean中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class RobotBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

@Override
protected Class<?> getBeanClass(Element element) {
//robot元素对应Robot对象类型
return Robot.class;
}

@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {

String brand = element.getAttribute("brand");
String type = element.getAttribute("type");
String height = element.getAttribute("height");
String weight = element.getAttribute("weight");

//把对应的属性设置到bean中
if(StringUtils.hasText(brand))
builder.addPropertyValue("brand", brand);

if(StringUtils.hasText(type))
builder.addPropertyValue("type", type);

if(StringUtils.hasText(height))
builder.addPropertyValue("height", height);

if(StringUtils.hasText(weight))
builder.addPropertyValue("weight", weight);
}
}

注册handler和schema

为了让Spring在解析xml的时候能够感知到我们的自定义元素,我们需要把namespaceHandler和xsd文件放到2个指定的配置文件中,这2个文件都位于META-INF目录中。

META-INF/spring.handlers

`spring.handlers`文件包含了xml schema uri和handler类的映射关系,比如:

http\://www.mycompany.com/schema/product=spring.xml.ext.schema.ProductNamespaceHandler

这表示遇到http://www.mycompany.com/schema/product命名空间的时候会交给ProductNamespaceHandler来处理。

注意上面的冒号转义。
key部分必须和xsd文件中的targetNamespace值保持一致。

META-INF/spring.schemas

http\://www.mycompany.com/schema/product.xsd=META-INF/product.xsd

最后测试下

写个Spring配置文件products.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:my="http://www.mycompany.com/schema/product"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.mycompany.com/schema/product
http://www.mycompany.com/schema/product.xsd">

<product:robot id="bb8" brand="StarWar" type="bb8" height="100" weight="20"/>

</beans>

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:products.xml" })
public class SchemaTest {

@Autowired
@Qualifier("bb8")
private Robot robot;

@Test
public void propertyTest() {
assertNotNull(robot);

String brand = robot.getBrand();
String type = robot.getType();
int height = robot.getHeight();
float weight = robot.getWeight();

assertEquals("Brand should be bb8.", "bb8", brand);
assertEquals("Type should be bb8.", "bb8", type);
assertEquals("Height should be 100.", 100, height);
assertEquals("Weight should be 20.", 20, weight);
}
}

参考资料

  • 基于Spring可扩展Schema提供自定义配置支持

jcmd命令详解

发表于 2016-12-14 | 阅读次数

简介

在JDK 1.7及之后的版本,新增了一个命令行工具jcmd。它是一个多功能工具,可以做哪些事情呢?这里先卖个关子,先来看看Oracle官方对jcmd的介绍:

The jcmd utility is used to send diagnostic command requests to the JVM, where these requests are useful for controlling Java Flight Recordings, troubleshoot, and diagnose JVM and Java Applications. It must be used on the same machine where the JVM is running, and have the same effective user and group identifiers that were used to launch the JVM.

翻译成人话就是:
jcmd一个用来发送诊断命令请求到JVM的工具,这些请求对于控制Java飞行记录器、故障排除、JVM和Java应用诊断来说是比较有用的。jcmd必须与正在运行的JVM在同一台机器上使用,并且使用启动该JVM时的用户权限。

用法

jcmd的用法是:
jcmd <process id/main class> <command> [options]

其中command的说明如下:

命令 说明
help 打印帮助信息,示例:jcmd help []
ManagementAgent.stop 停止JMX Agent
ManagementAgent.start_local 开启本地JMX Agent
ManagementAgent.start 开启JMX Agent
Thread.print 参数-l打印java.util.concurrent锁信息,相当于:jstack
PerfCounter.print 相当于:jstat -J-Djstat.showUnsupported=true -snap
GC.class_histogram 相当于:jmap -histo
GC.heap_dump 相当于:jmap -dump:format=b,file=xxx.bin
GC.run_finalization 相当于:System.runFinalization()
GC.run 相当于:System.gc()
VM.uptime 参数-date打印当前时间,VM启动到现在的时候,以秒为单位显示
VM.flags 参数-all输出全部,相当于:jinfo -flags , jinfo -flag
VM.system_properties 相当于:jinfo -sysprops
VM.command_line 相当于:jinfo -sysprops grep command
VM.version 相当于:jinfo -sysprops grep version
1234…6
宁静·致远

宁静·致远

60 日志
9 标签
友情链接
  • 卡拉搜索
© 2020 宁静·致远
由 Hexo 强力驱动
主题 - NexT.Gemini