zoco

php性能优化之并行与异步

2019-05-05


PHP性能优化之并行与异步

在我们的一个核心接口中会请求大量的RPC服务,用来获取各种数据,比如一个接口一次请求将会产生平均7~8次RPC,调用虽然每个接口都非常的快(ms级),但8次累加起来的消耗还是相当的可观,所以我最近的优化工作主要是:

通过某种方式并行(异步)调用各RPC请求,以缩短执行时间。

当我开始接手这项工作的时候,脑海中想到的第一个对应思想就是Lazy evaluation( 缓式求值 ),维基百科上对于缓式求值的定义是:

In programming language theory, lazy evaluation, or call-by-need[1] is an evaluation strategy which delays the evaluation of an expression until its value is needed (non-strict evaluation) and which also avoids repeated evaluations (sharing).

Lazy evaluation是编程语言设计领域中的一个表达式求值策略,它延缓对表达式的求值直到你需要它的时候。看上去lazy evaluation好像和我们的问题挨不上边,而且php也不支持 lazy evaluation,不过仔细想一下,如果我们能把对RPC请求的后续操作延缓到对返回结果的使用时,就可以用一种优雅的实现来使框架支持并行执行,而且对于业务层的改动也非常的小。

具体点说就是在进行RPC调用的时候,不再返回结果,而是返回一个句柄,这个句柄标识了一个被提交到后台的请求,它被加入到一个变量中,你不再关心它,由其他服务替你完成,你的代码可以继续往下执行,去完成其他的业务逻辑。而当我们需要这个结果时,检查这个句柄是否已经完成,如果已经完成则执行接受结果之后的所有操作,返回结果。

解决这样的问题大概有这样几种方式:多线程、协程、如果是HTTP请求的话可以用CURL提供的 multi*方法。

但是,在我们这里统统不适用,PHP语言本身对并行的处理能力有限,需要借助其他的服务。

方案

我们会在调用的时候,将多个RPC请求进行合并,统一发送给一个新的RPC服务(暂时称作Multi RPC),Multi RPC将以代理的身份,并行获取多个数据。处理完成或者整体超时后,会将所有数据返回,由API进行下一步的处理。

关键问题

  1. 超时问题,使用最大超时,调用方比指定超时多设置10ms
  2. 服务拆分逻辑,对互不依赖代码进行拆分
  3. 提前打包后缓存再取,还是当次请求直接返回

协议方案

service uri, method, protocal, params, timeout, retry

RPC服务端

由于PHP等语言不能很好地支持多线程并行调用,在一些复杂业务逻辑的场景下,需要依次串行调用多个远程服务,极大地拖延了整个处理流程消耗的时间。此时如果将可以并行处理的请求合并交给并行RPC服务执行,利用Java的多线程并行执行多个调用、将所有响应统一返回,可以有效解决上述串行执行带来的问题。

设计方案

NnYnht.png

执行方式

在并行RPC的接口实现中,所有的请求被submit到Java的ThreadPoolExecutor执行,在分别独立的线程中通过跨语言的Redis协议发起调用。

当所有线程执行完毕或到达parallelTimeout上限时,从每个线程获取远程调用的Response,或者为没有执行完成的线程创建一个超时的Response,合并之后作为整个并行调用的返回值。

应用案例

如设计方案图所示,假如PHP API需要执行4个查询请求,它们消耗的时间分别为60ms、110ms、150ms(超时)、80ms,完成这些调用总时间将是300ms。如果通过并行MOA的方式,去除远程调用开销的时间缩短至150ms

对于没有出现超时异常、可并行调用请求数更多、单个请求消耗时间更长的场景,响应时间的提升会更加明显。

问题与挑战

异常处理

当并行的请求中有部分超时失败时,将其标记为超时并与其他执行成功的响应一同返回。每个独立的调用都以一个公共的超时时间作为限制。

并行MOA的调用方需要根据具体业务场景决定如何处理部分失败的请求,如将整个请求归结为失败、单独重试失败的请求、重新执行一次并行请求等。

内部重试

当某一个远程调用出现失败、当前时间并未达到超时上限时,线程内部会执行指定次数的重试调用。

增加黑名单和开关机制,避免连续重试已经崩溃的服务而耗费过多资源。

日志与监控

对于执行失败的请求,Response中会添加ErrorMsg信息,调用方可将该信息记录到系统日志中。

并行MOA内部会同时记录一份更详细的错误日志,并对整体的成功、失败、线程资源等数据进行监控。

资源分配与隔离

并行RPC处于代理层的位置,因此会承载较大的系统压力。网络IO、序列化运算、线程资源消耗等因素可能成为系统瓶颈。具体资源配置需要进一步测试后得出结论。

资源隔离方面按业务场景进行拆分,如/service/parallel/profile、/service/parallel/feed等。

风险控制

  1. 系统资源不足:基于RPC的水平扩展性增加Server端实例
  2. 目标服务异常导致线程资源耗尽:通过手动或自动的方式将异常服务加入黑名单,跳过实际调用直接返回异常Response。