梁晨(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诞生之初,就是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的开源方案,首先从二进制的协议说起:
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>tedname>
<age>18age>
<degree>masterdegree>
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个字节
2 required string degree; // tag为2,type为string,实际数据为master,共7个字节
}
从注释中可以看到,三个字段需要的字节数为14,再加上结构体的开始和结构体结束的标识共2个字节,一共只需要16个字节而已。相比之下,这仅仅是JSON的1/4,是XML协议标识同样信息的1/5,高下立判. 巧妙地用协议强约定换传输可读性,这就是高信息熵的二进制协议的诀窍。
为了使得PHP能够充分与Tars结合,必须使其具备作为客户端和作为服务端两个方面的能力。
作为客户端而言,要能够满足快速开发的需求,也要能够与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'];
为了方便开发者扩展使用中经常遇到的无法找到具体函数和参数的问题,同时提供了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在服务端主要会关注两类服务。
第一类是HTTP的服务,会以SWOOLE2.0为网络收发的基础,实现一套高性能、简洁好用的面向WEB服务的框架。这套框架会支持基本的 路由、中间件、MVC架构等常见的WEB框架特性。同时也会集成Redis、Mysql、Http、Multicall、Tars等常见的客户端,方便WEB服务再去调用后台服务。更重要的是,接入到Tars平台中,使得服务可监控,可重启,享受Tars运维平台带来的一站式便利。现在框架的第一个版本已经实现,并在阅文集团内部上线使用,测试成熟后,会及时进行开源。
第二类则是TCP的服务,同样底层依赖于SWOOLE2.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技术方案上深度合作与实践。欢迎开发者试用!