❏ 站外平台:

腾讯与阅文技术合作,微服务框架 Tars 再添PHP

作者: 腾讯开源

| 2017-11-11 07:59   收藏: 1    

梁晨 (ted),任职阅文集团技术中心,负责起点的 WEB 后台开发工作。曾负责腾讯上海企业产品部营销 QQWeb 后台开发、QQ 公众号 Web 后台开发,对大型网站技术架构,有自己的经验和见解。腾讯开源项目 TSF2.0 框架开发者,腾讯开源组件 Tars-PHP 开发者,也曾是腾讯公司多个 PHP 扩展组件的开发者与维护者。

引言

TARS 作为由腾讯公司开源的优秀 RPC 框架与服务部署运维解决方案,被阅文集团引入了实际实践中,同时阅文集团对 TARS 在 PHP 语言层面进行了能力的补全,令 TARS 如虎添翼。TARS-PHP 的解决方案兼具简单高效、接口维护方便容易扩展、代码自动生成,以及集成寻址、服务发现、监控、上报等功能。经历了阅文集团线上业务的考验与洗礼,充分证明了该解决方案的优势。
项目地址: https://github.com/Tencent/Tars/tree/master/php 

“PHP 是世界上最好的语言”

众所周知,在 PHP 诞生之初,就是 WEB 站点的开发而生。但是一直以来,都无法摆脱弱类型、脚本语言的性能之殇的帽子。随着互联网行业的不断发展,以及用户需求和基础架构的不断变化,PHP 语言本身也一直在发展。无论是 SWOOLE 的出现,还是 PHP7 对性能的提升,都丰富和助力了 PHP 本身的应用。

相信大家在开发中也会发现,作为经常处在 WEB 中间层的 PHP,其实有很多的痛点。既要接收前端的 HTTP 请求,又要调用各式各样的后台服务与存储服务,常常成为一个站点的性能瓶颈。其中 HTTP 协议的过分冗余以及上层封装带来的损耗,就是一个比较突出的问题。

开发者不但要应对使用同步的 HTTP 的调用库所带来的吞吐量的下降,还要忍受 HTTP 协议本身,以及 JSON、XML 协议在信息传输上的低效率。为了解决这一问题,一套在 TCP 协议层的,使用简单的二进制协议。才能保证业务用更少的传输带宽,承载更多的传输内容,从而提高吞吐量和 WEB 服务伺服能力。 

同时,在实际开发的层面上,PHP 逻辑层与后台服务之间通信协议的维护成本较高。同时,后台服务侧新增或修改接口字段,往往调用侧也要配合修改,很多时候无法保证接口的完全兼容而引发线上的运营问题。因此,这种二进制协议又要做到接口方便维护,同时又容易扩展。

除此之外,从开发效率上而言,原本的开发中总是包含大量的重复的,但又不得不去做的工作内容。因为每一次新协议的开发,代码很难复用,JSON 和 XML 也并不允许你共用部分数据。同时一个很现实的问题是,不同 HTTP 接口的提供方,往往会视自己的心情和习惯来定义接口。

一个常见的例子就是对返回码的定义,有些人叫 ret,有些人叫 code,还有些人就叫 r,简直是无所不包。因此这类重复无趣的开发工作,给调用方的开发同学带来了极大的生理和心理负担。基于这种需求,一种服务端和客户端都能够根据协议和接口自动生成调用代码,保证联调通畅的解决方案必不可少。

再者,调用方对后端服务的发现和调用的上报与监控,也是一个老生常谈的问题。后端服务如何被发现,后端的接口如何被发现,这都是调用方真真切切想知道的。同时,调用方非常有必要对后端服务的调用情况进行上报到中央服务器,中央服务器再根据收集上来的信息,对后端服务的负载进行动态的调整,保证服务的高可用。要实现这样的需求,必须引入一种集成了监控、主控寻址、上报通道、负载均衡功能的解决方案。

Tars 作为腾讯公司的优秀 RPC 框架与服务部署运维解决方案,可以满足上述的所有需求。通过引入 Tars-PHP 的全套解决方案,开发者既可以使用二进制的 Tars 协议,大大压缩了服务请求的流量。同时也能够借助 Tars 协议解析的 PHP 扩展,提高了打包解包的性能进而提升了单进程的任务处理能力。再次,自动生成代码的工具也能够提升开发者的效率。

Tars-PHP 解决方案

Tars-PHP 的开源方案,首先从二进制的协议说起:

二进制协议

HTTP 协议可能是在应用层上使用最为广泛的协议了。现有 HTTP 的版本主要是 1.0 和 1.1 版本。它在 TCP 协议的基础上做了十分简洁的应用层协议封装,纯文本的内容,以及 Header 和 Body 的区分。都使得这种协议的使用和理解十分的方便。但是不可避免的,使用和阅读的简单意味着信息的冗余,为了传输少量的内容,往往需要耗费大量的流量。

另外两个比较熟知的协议,就是 JSON 和 XML 了,这两位在 API 交互常用的协议中不分上下,可读性强、容易理解、语言客户端支持丰富、协议表述能力突出,都是两者的优势所在。先看看同样一段信息,两者需要的数据量。

假定有一所学校,一个学生,如果用 JSON 标识的话,如下所示:

{
    "school":
    {
        "student":{
            "name":"ted",
            "age":18,
            "degree":"master"
        }
    }
}

很简单的结构,共需要 65 个字符来表述。

而如果换成 XML:

<school>
        <student>
            <name>ted</name>
            <age>18</age>
            <degree>master</degree>
        </student>
</school>

则一共需要 92 个字符。从信息学的角度而言,信息熵明显就是太低了。所以为了实现通信的更高性能和更少带宽的使用,二进制协议的引入势在必行。

Tars 协议作为一个二进制的协议,相比于上述两个协议的优势不言自明。从上文中的 JSON 和 XML 中发现其灵活性,也就是没有指定字段的类型。但是不可避免的,这种灵活带来了性能的大损失。因此 Tars 定义了八种基本的数据类型,通过对不同的数据类型进行编码优化:

bool、byte、short、int、long、float、double 、string

而同时为了满足业务需求,扩展出了 struct(包含任意字段)、vector(数组)、map(key-value 结构)这三种可以嵌套数据,丰富协议表现力的复杂类型。

按照上文的表现结构,几个 struct 就可以完成。

首先是 student 结构体:

struct student {
    0 required string name; // tag为0,type为string,实际数据为ted,共5个字节
    1 required byte age; // tag为1,type为short,实际数据为18, 共2个字节
}

从注释中可以看到,三个字段需要的字节数为 14,再加上结构体的开始和结构体结束的标识共 2 个字节,一共只需要 16 个字节而已。相比之下,这仅仅是 JSON 的 1/4,是 XML 协议标识同样信息的 1/5,高下立判。巧妙地用协议强约定换传输可读性,这就是高信息熵的二进制协议的诀窍。 

为了使得 PHP 能够充分与 Tars 结合,必须使其具备作为客户端和作为服务端两个方面的能力。

Tars-PHP 的客户端

作为客户端而言,要能够满足快速开发的需求,也要能够与PHP现有的常见使用方式相结合,同时还要给出远程调用的实例。基于这些需求,客户端方案中实现了如下的特性:

  • 实现了用 TUP 协议进行打包解包、编码解码的PHP扩展及相应的测试用;
  • 实现了从 Tars 协议文件生成对应 PHP 类文件的 tars2php 工具;
  • 实现了包含网络库的二次封装,以及远程调用的代码示例;

作为客户端实现的最核心一步,就是对 TUP 协议的支持。TUP 协议是在 Tars 协议的上层,通过固定的数据结构封装一些收发包必须的信息,如返回值、输入输出参数、包本身的状态、包计数等,来给非 Tars 原生客户端与 Tars 服务端进行通信的协议。Tars-PHP 在支持 TUP 协议的方案中,选择了使用 PHP 扩展作为实现方式。

PHP 语言本身被诟病最多的,就是针对 CPU 密集型的运算的低效率。由于并不十分高效的 ZEND 虚拟机、松散的数据结构和弱类型的存在,使得打包、解包这类CPU密集型的效率低下。因此,PHP 扩展应运而生。通过引入高性能的 C/C++ 类库和一些原生的 C/C++ 实现,使得 PHP 在性能处理方面迎头赶上。这也就是以扩展的方式实现打包解包主逻辑的初衷。

首先来看看 PHP5x 语言的结构:

最底层的 Server API 用来 PHP 与 Webserver 通信,这个主要是之前与 APACHE 配合需要使用的。在其左上的 PHPCORE 层,是为了提供最基本的文件和网络操作的能力。而右上的 ZEND,则是用来把 PHP 的脚本语言编译成机器码的工具。最上面就是扩展层了,这层会充分利用 ZEND 的 API 和 PHPCORE 的能力,直接写出 ZEND 能够高效执行和理解的代码,省去了 PHP 脚本编译为机器码的过程,从而大大的提高执行的效率。

如果要设计这个扩展,必须要将上文中 Tars 的数据结构通过 C 语言的方式加以表达,同时设计出基于这套数据结构的编码器与解码器。另一个需要考虑的方面是,必须要使得在 PHP 层面尽可能的简单、易用,这就对扩展的设计提出了比较高的挑战。一方面要兼顾性能,另一方面,要将 Tars 协议中的 struct,进行了 PHP 中的 Class 的表达:

从图中可以清晰的看到,结构体 SimpleStruct 被分解成了三个部分:

  • TAG 部分;
  • 成员变量部分;
  • 变量描述的 fields

TAG 部分至关重要,这部分用来代表 struct 中每个元素的 TAG 值。这也是实际进行 TUP 编码和解码的时候,二进制包里面最终包含的内容。为什么要有 TAG?这是因为相比于 JSON 里面对字段的文本性质的描述,TAG 本身更节省空间。

第二部分则是类的成员变量,这部分成员变量和 Tars 协议的 struct 中的变量一一对应。这是为了承载对应变量的实际值而存在的。借此才能对真正的数据进行打包和解包。

为了在 TAG 和变量之间搭起一座桥梁,就有了第三部分:Fields 部分。这部分是 TAG 与其对应的变量属性的一个映射。包含了变量的名称、变量是否必填以及变量的类型。通过这些信息,一方面实现了对 Tars 协议的二进制编码,也实现了解码时候的映射。可谓一举两得。

那么经过复杂的扩展设计与实现,有必要将扩展实现的打包解包性能和原生PHP实现的打包解包性能进行比对。从下面的表格中可以非常明显的看出扩展实现拥有性能上面的绝对优势:

方式/100次 tars复杂度 打包时间(ms) 打包耗时倍数 解包时间(ms) 解包耗时倍数
扩展 简单 0.69 1 1.18 1
php原生 简单 11.25 16 16.28 13
扩展 复杂 1.17 1 1.55 1
php原生 复杂 14.5 12 15.1 10

从这个表格中可以非常清晰的看到,无论是简单的 Tars 协议,还是复杂的 Tars 协议,使用扩展进行打包解包都比原生 PHP 的性能提高十倍以上。当遇到复杂的业务逻辑,需要调用大量的使用 Tars 协议的后台服务的时候,这种效率的提升会让服务的吞吐量上一个数量级。

开发者在完成扩展的编译工作之后,就可以非常方便的使用 TUP 协议进行打包,解包与编码解码的工作了。 

// 针对基本类型的打包和解包的方法,输出二进制buf
$buf = \TASAPI::put*($name, $value);
$value = \TUPAPI::get*($name, $buf);

// 针对Struct,传输对象,返回结果的时候,以数组的方式返回,其元素与类的成员变量一一对应
$buf = \TUPAPI::putStruct($name, $clazz);
$result = \TUPAPI::getStruct($name, $clazz, $buf);

// 针对Vector,传入完成pushBack的Vector
$buf = \TUPAPI::putVector($name, TARS_Vector $clazz);
$value = \TUPAPI::getVector($name, TARS_Vector $clazz, $buf);

// 针对Map,传入完成pushBack的Map
$buf = \TUPAPI::putMap($name, TARS_Map $clazz);
$value = \TUPAPI::getMap($name, TARS_Map $clazz, $buf);

// 需要将上述打好包的数据放在一起用来编码
$inbuf_arr[$name] = $buf;
// 进行tup协议的编码,返回结果可以用来传输、持久化
$reqBuffer = \TUPAPI::encode(
                         $iVersion=3,
                         $iRequestId,
                         $servantName,
                         $funcName,
                         $cPacketType=0,
                         $iMessageType=0,
                         $iTimeout,
                         $context=[],
                         $statuses=[],
                         $inbuf_arr);
// 进行tup协议的解码
$ret = \TUPAPI::decode($respBuffer);
$code = $ret['code'];
$msg = $ret['msg'];
$buf = $ret['sBuffer'];

为了方便开发者扩展使用中经常遇到的无法找到具体函数和参数的问题,同时在 Github 上提供了 tars-ide-helper:

以 PHPSTORM 为例,只需要导入到相应的 INCLUDE 路径中,就可以实现自动提示了:

除了打包解包的能力,Tars-PHP 同时也提供了网络收发的能力,网络收发主要实现了以下几个点:

  • TarsAssistant.php 文件:通过 COMPOSER 加载,底层内置 SOCKET 原生网络层收发包实现;
  • 根据 Interface 自动生成 PHP 的 Class,与 TarsAssistant 无缝结合;
  • 提供 Exception 等容错处理;

一旦完成了代码的自动生成之后,使用者即可通过如下代码,方便的进行远程 Tars 服务调用:

    require_once "./vendor/autoload.php";

    $ip = "";// taf服务ip
    $port = 0;// taf服务端口
    $servant = new App\Server\Servant\servant($ip,$port);

    $in1 = "test";
    $ss1 = new SimpleStruct();
    $ss1->id = 1;
    $ss1->count = 2;
    $ss1->page = 3;

    try {
        $intVal = $servant->singleParam($in1,$ss1,$out1);
    }
    catch(phptars\TarsException $e) {
        // 错误处理
    }

Tars-PHP 的服务端

除了建设 Tars-PHP 作为客户端的能力之外,服务端的能力同样是必不可少的。为了能够满足不同业务场景下的需求,Tars-PHP 在服务端主要会关注两类服务。

第一类是 HTTP 的服务,会以 SWOOLE 2.0 为网络收发的基础,实现一套高性能、简洁好用的面向 WEB 服务的框架。这套框架会支持基本的 路由、中间件、MVC 架构等常见的 WEB 框架特性。同时也会集成 Redis、Mysql、Http、Multicall、Tars 等常见的客户端,方便 WEB 服务再去调用后台服务。更重要的是,接入到 Tars 平台中,使得服务可监控,可重启,享受 Tars 运维平台带来的一站式便利。现在框架的第一个版本已经实现,并在阅文集团内部上线使用,测试成熟后,会及时进行开源。

第二类则是 TCP 的服务,同样底层依赖于 SWOOLE 2.0,但是协议从 HTTP 换成了对 TUP 和 Tars 的支持。框架实现上而言,会与 JAVA、C++ 的服务端保持一致,底层集成网络能力,使用者只需关心服务名称以及接口参数和自己的业务处理逻辑而已。当然,这个服务肯定也是要与 Tars 运维平台相结合的。现在框架对 TUP 协议支持的第一个版本已经完成,后续也会在完成 Tars 协议的底层支持之后,在业务上进行使用和验证。 

业务实践

阅文集团在进行后台服务治理与改造的过程中,使用了 Tars-PHP 的解决方案。一方面,所有 WEB 后台与后台服务的接口,全部从原有的 HTTP 接口,切换为了基于 Tars 协议的 TCP 网络传输。依赖于 Tars-PHP 的自动代码生成,开发效率提升巨大,保证了项目的顺利按时上线。同时,这套基于 PHP 扩展的方案,也保证了代码执行效率的高效,单个请求的处理时间,相比于原有的 HTTP 接口调用,得到了显著的缩短。

另一方面,由于使用的 WEB 后台服务是常驻内存的,基于 SWOOLE 的实现。所以在发布、启动、监控等方面与原有 PHP 中固有的 Apache 和 PHP-FPM 的方式都不相同。因此,正如上文中所说,服务接入 Tars 平台,享受其监控、保活、日志等一系列的功能,会大大提高服务本身的运维和扩容的便利性。如今在其线上服务中,超过十个服务已经切入并稳定运行了接入到 Tars 平台的 HTTP 服务。这些服务的发布、扩容和运维完全依赖 Tars 平台,十分便利。

除去对 Tars 平台运维的使用,阅文 WEB 后台侧同样在服务发现上,有一套方案。

对于远程服务的地址管理,最差的方案就是将其写入本地文件中。这种方案无法应对快速缩扩容以及服务器下线的需求,会给后续的运维带来很大的工作量。

稍微好一些的方案是本地存储虚拟 IP,那么每次只需要调整虚拟 IP,就可以实现服务地址的自动映射和变化。但是这意味着对要调用的每一个后台服务,都需要存储其对应的虚拟 IP、HOST 信息、接口信息等一系列的信息,同样维护成本很高。

而更加通用的方案,则是提供服务的统一配置中心,每次需要调用后台服务的时候,就从配置中心根据唯一的标识拉取出服务最新的地址。这样一方面能够做到缩扩容对业务的无感知,另一方面配置中心也能够通过服务的寻址情况,给每个客户端分配最适合它的服务机器地址,比如机房或者 SET 就近分配等等。本地的服务只需要提供两个能力,第一个是能够调用定期的寻址服务,并存入本机的存储中,保证寻址的速度。第二个则是能够接收配置中心下发的命令,更新特定服务的地址。能做到这两点,就能够实现高效的寻址和可靠的寻址了。

在实际使用中,结合实际业务情况,一方面每分钟向主控请求一次服务的地址,通过轮询的方式获取一个可用的服务地址,再放入本地的高速共享内存,方便在这一分钟之内重复的读取。另一方面在每次服务调用的时候,都自动在底层集成对服务调用情况的耗时、成功率的上报。在双管齐下的作用之下,对远程服务的调用不再像过去那样难以维护、难以开发、难以监控,而是清晰可见高效的被管理。 

结语

从开发效率上而言,使用 Tars-PHP 摆脱了过分冗余的业务代码,以自动生成的方式提高代码开发自动化程度。

从性能方面而言,Tars-PHP 方案通过引入扩展,做到了性能的大幅度提升,让性能不再成为 PHP “之殇”。

从易用性而言,通过提供 TarsAssistant 的网络收发组件,使得收发包无需单独实现。后面也会引入更高性能的 Swoole 作为 socket 收发的利器,进一步提高网络性能。

后续,Tars-PHP 的 SERVER 侧方案也会尽快开源,从而能够提供一套包含客户端与服务端的完整解决方案。这一整套的 WEB 后台的 Tars-PHP 开发体系,能够真正做到了高性能、高效率与高可用。而阅文集团也会继续与腾讯在 Tars-PHP 技术方案上深度合作与实践。欢迎开发者试用!



最新评论


返回顶部

分享到微信

打开微信,点击顶部的“╋”,
使用“扫一扫”将网页分享至微信。