今年做的重要项目之一,就是对一个核心Web系统重构,使之达到了99.999% 的高可用性。在此过程中,积累了一些系统架构、自动故障发现与解决、代码健壮性等方面的经验,予以记录。

业务背景介绍

该web系统是一个大型互联网系统商业运营侧的核心web系统,PHP语言实现,之前的可用性方面存在一定问题,在历史上也出过不少事故。由于是商业系统,其PV仅是中等规模,但复杂度相对较高,体现在其所涉及的网络交互、DB调用、Cache交互较多。

需要解决的问题

  1. 所依赖的非核心上游服务不可用时,及时发现并自动降级
  2. 所依赖的上游服务部分节点不可用时,及时发现并自动摘除故障节点
  3. 通过Lib库,封装网络交互中的重试、连接和读写超时、异常日志和以上各种功能,使之对业务层透明

整体架构设计

该系统采用集中式+分布式相结合的异常发现和处理方案。由浅蓝色业务层和深蓝色基础服务设施一起,识别并处理异常攻击、网络、系统和上游服务问题。为了最大程度的解耦,它们交互的方式,就是规范化的文本日志和INI配置。

之所以采用集中+分布的混合方式,是由于所处系统环境的复杂度导致的:

  1. 对于异常流量,由于目前集群规模较大,单点不可能阈值配置过大(规则),也无法收集到全局信息(策略),只有集中式才可以综合判断;
  2. 对于上游服务的整体故障,同样的道理,单点也不可能阈值配置过小,否则很容易产生抖动,而集中式可以全局收集信息,全局联动;
  3. 但集中式的服务降级,无法很好解决以下两种问题,所以需要通过每个业务节点通过节点健康检查来处理。
    1. 不可降级的核心服务交互
    2. 点对点的非核心服务故障,例如上游服务部分机房故障,或某台服务器故障

全局故障监控与降级

故障采集:为了降低侵入性,采取了由业务模块打印warn、error log,各业务节点上的守护进程阻塞read日志文件,并发往多个(目前配置为2个)同构监控与调度服务的方式。之所以采用多写的方式,是由于一期实现时,复用了日志ETL流,走的是公司的消息队列和流式计算平台,而它们都是强一致性的,当网络出现抖动或拥塞时,故障日志甚至会延迟数小时之久!所以,最终我们决定采用最简单的方式,对于这种对实时性要求极高的消息交互,采用CS直接透传的方式。而交叉多写可以保证只要不是所有机房间链路都发生故障,就可以在秒级别完成消息传递。多个监控与调度服务完全同构,都会接收异常日志并予以计算和处理,由于重复降级不会产生副作用,所以没有做maser、slave角色的划分。

故障判别:出于扩展性和复用角度的考虑,故障判别的规则是基于Protobuf格式配置的,基本原理就是配置:某一种类型的异常日志在一段时间内到达指定阈值后,需要将什么配置项改为什么值。由于异常日志的数量一般不会太大,所以每个监控与调度服务都是独立运行的,不存在数据同步的问题,时间窗口、频率等信息都在内存中直接计算。当需要横向扩展时,可以与故障采集配合,根据业务模块、异常日志类型进行纵向划分,映射到不同的监控节点上。仅有一点需要注意的是,为了防止网络等问题导致消息延迟到达,在计算频率时,会过滤掉超时的消息。

故障处理:同样为了降低业务侵入性和耦合度,故障处理是通过修改业务模块的配置文件实现的。通过在PHP业务节点上部署我们的zk_agent,监听zk文件变化并修改业务模块的ini配置对应项,和复用C++进程的基于zk的热加载功能,监控与调度模块只需与zookeeper集群通信,修改指定配置项,而无需知晓业务模块的具体信息。上面提到的多实例方式,也可以保证只要有一个监控与调度模块与zk集群通信成功,就可以成功完成降级指令的发布。这里需要注意的是,由于机器规则总有其不完善处,引入了保险栓的方式,保证在开启保险栓的情况下,人工干预的优先级最高。

当然,这套流程也有其不完善处,例如为了避免网络抖动,我们是通过设置一个较大的阈值来实现的,而没有做更为精细化的处理。同时,其配置恢复是手工的,因为监控与调度模块为了降低耦合度和复杂度,没有主动去探测故障的恢复情况。

单点故障监控与摘除

作为自动降级的补充,该功能着眼于发现点对点的问题,并予以处理。其原理也是:该业务服务器上,在一段时间内,对某上游节点的调用若失败超过阈值,则屏蔽该上游节点。这里直接通过真实请求的失败进行计数,没有开启独立进程,也是为了降低复杂度,提升易用性。

采用策略模式,实现了APC和File两种存储介质的封装,并支持扩展。由于APC也是跨进程的,所以可以在单机所有PHP进程间共享失败次数、故障节点信息。经过测试,APC的性能优于File,所以一般推荐使用APC模式。但为了兼容那些未安装APC module的业务模块,所以也保留了File的方式。

由于大中型系统的点对点故障是频繁发生的,所以这里采用屏蔽超时自动恢复的方式,虽然可能会造成锯齿状的耗时波动,但无需人工干预,是综合而言最优的方式。

由于请求间可能有相互依赖关系,单点是无法handle降级的,所以一旦检查到某服务的上游节点全部不可用,则重新置位,设为全部可用。

PHP基础lib库

以上及其他未提及的稳定性保障,对于业务层而言略显复杂,且由每个业务开发者来保证这些也是不可靠的,所以必须有一套对业务透明的php Lib库,予以封装。我们实现了这样一套Lib,首先细致处理了Mysql、Memcache、Curl、Socket、Redis等网络交互时的重试、连接和读写超时。其次,以上网络组件又是基于代码层的负载均衡、节点健康检查、服务降级、配置组件,从而对上屏蔽稳定性细节。同时,该Lib充分考虑易用性,例如通过对多种Cache的封装,仅暴露一个简单的Call接口和规范化配置,就可以使业务层完成旁路cache的使用。

总结

通过以上方式,该系统在上线3个月内,自动处理了2000多次故障,实现了按请求成功率计算,99.999%以上的可用性。感触最深的是,很多做业务开发的PHPer,简单的认为稳定大多是运维同学的责任,但想要达到高稳定性,工作其实从架构设计就开始了,更深深渗透在每一行代码里!

Leave a Reply