热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

支撑微博千亿调用的轻量级RPC框架:Motan

编者按:高可用架构分享及传播在架构领域具有典型意义的文章,本文由张雷在高可用架构群分享。转载请注明来自高可用架构公众号「ArchNotes」。

编者按:高可用架构分享及传播在架构领域具有典型意义的文章,本文由张雷在高可用架构群分享。转载请注明来自高可用架构公众号「ArchNotes」。


张雷,新浪微博技术专家,MotanRPC 框架技术负责人。2013 年加入新浪微博,作为核心技术成员参与微博 RPC 服务化、混合云等多个重点项目,当前负责 MotanRPC 框架的维护与架构改进。专注于高可用架构及服务中间件开发方向。


“2013 年微博 RPC 框架 Motan 在前辈大师们(福林、fishermen、小麦、王喆等)的精心设计和辛勤工作中诞生,向各位大师们致敬,也得到了微博各个技术团队的鼎力支持及不断完善,如今 Motan 在微博平台中已经广泛应用,每天为数百个服务完成近千亿次的调用。” —— 张雷


随着微博容器化部署以及混合云平台的高速发展,RPC 在微服务化的进程中越来越重要,对 RPC 的需求也产生了一些变化。今天主要介绍一下微博 RPC 框架 Motan,以及为了更好的适应混合云部署所做的一些改进。



RPC 框架的发展与现状

RPC(Remote Procedure Call)是一种远程调用协议,简单地说就是能使应用像调用本地方法一样的调用远程的过程或服务,可以应用在分布式服务、分布式计算、远程服务调用等许多场景。说起 RPC 大家并不陌生,业界有很多开源的优秀 RPC 框架,例如 Dubbo、Thrift、gRPC、Hprose 等等。下面先简单介绍一下 RPC 与常用远程调用方式的特点,以及一些优秀的开源 RPC 框架。


RPC 与其它远程调用方式比较

RPC 与 HTTP、RMI、Web Service 都能完成远程调用,但是实现方式和侧重点各有不同。


HTTP

HTTP(HyperText Transfer Protocol)是应用层通信协议,使用标准语义访问指定资源(图片、接口等),网络中的中转服务器能识别协议内容。HTTP 协议是一种资源访问协议,通过 HTTP 协议可以完成远程请求并返回请求结果。

    

HTTP 的优点是简单、易用、可理解性强且语言无关,在远程服务调用中包括微博有着广泛应用。HTTP 的缺点是协议头较重,一般请求到具体服务器的链路较长,可能会有 DNS 解析、Nginx 代理等。


RPC 是一种协议规范,可以把 HTTP 看作是一种 RPC 的实现,也可以把 HTTP 作为 RPC 的传输协议来应用。RPC 服务的自动化程度比较高,能够实现强大的服务治理功能,和语言结合更友好,性能也十分优秀。与 HTTP 相比,RPC 的缺点就是相对复杂,学习成本稍高。


RMI

RMI(Remote Method Invocation)是指 Java 语言中的远程方法调用,RMI 中的每个方法都具有方法签名,RMI 客户端和服务器端通过方法签名进行远程方法调用。RMI 只能在 Java 语言中使用,可以把 RMI 看作面向对象的 Java RPC。


Web Service

Web Service 是一种基于 Web 进行服务发布、查询、调用的架构方式,重点在于服务的管理与使用。Web Service 一般通过 WSDL 描述服务,使用 SOAP通过 HTTP 调用服务。


RPC 是一种远程访问协议,而 Web Service 是一种体系结构,Web Service 也可以通过 RPC 来进行服务调用,因此 Web Service 更适合同一个 RPC 框架进行比较。当 RPC 框架提供了服务的发现与管理,并使用 HTTP 作为传输协议时,其实就是 Web Service。


相对 Web Service,RPC 框架可以对服务进行更细粒度的治理,包括流量控制、SLA 管理等,在微服务化、分布式计算方面有更大的优势。



RPC 框架介绍

RPC 协议只规定了 Client 与 Server 之间的点对点调用流程,包括 stub、通信协议、RPC 消息解析等部分,在实际应用中,还需要考虑服务的高可用、负载均衡等问题,所以这里的 RPC 框架指的是能够完成 RPC 调用的解决方案,除了点对点的 RPC 协议的具体实现外,还可以包括服务的发现与注销、提供服务的多台 Server 的负载均衡、服务的高可用等更多的功能。目前的 RPC 框架大致有两种不同的侧重方向,一种偏重于服务治理,另一种偏重于跨语言调用。


服务治理型 RPC 框架

服务治理型的 RPC 框架有 Dubbo、DubboX 等,Dubbo 是阿里开源的分布式服务框架,能够实现高性能 RPC 调用,并且提供了丰富的管理功能,是十分优秀的 RPC 框架。DubboX 是基于 Dubbo 框架开发的 RPC 框架,支持 REST 风格远程调用,并增加了一些新的 feature。


这类的 RPC 框架的特点是功能丰富,提供高性能的远程调用以及服务发现及治理功能,适用于大型服务的微服务化拆分以及管理,对于特定语言(Java)的项目可以十分友好的透明化接入。但缺点是语言耦合度较高,跨语言支持难度较大。


跨语言调用型

跨语言调用型的 RPC 框架有 Thrift、gRPC、Hessian、Hprose 等,这一类的 RPC 框架重点关注于服务的跨语言调用,能够支持大部分的语言进行语言无关的调用,非常适合于为不同语言提供通用远程服务的场景。但这类框架没有服务发现相关机制,实际使用时一般需要代理层进行请求转发和负载均衡策略控制。


微博的 RPC 框架 Motan 属于服务治理类型,是一个基于 Java 开发的高性能的轻量级 RPC 框架,Motan 提供了实用的服务治理功能和优秀的 RPC 协议扩展能力。


Motan 提供的主要功能包括:


  • 服务发现:服务发布、订阅、通知

  • 高可用策略:失败重试(Failover)、快速失败(Failfast)、异常隔离(Server 连续失败超过指定次数置为不可用,然后定期进行心跳探测)

  • 负载均衡:支持低并发优先、一致性 Hash、随机请求、轮询等

  • 扩展性:支持 SPI 扩展(service provider interface)

  • 其他:调用统计、访问日志等


Motan 可以支持不同的 RPC 协议、传输协议。Motan 能够无缝支持 Spring 配置方式使用 RPC 服务,通过简单、灵活的配置就可以提供或使用 RPC 服务。通过使用 Motan 框架,可以十分方便的进行服务拆分、分布式服务部署。


与同类型的 Dubbo 相比,Motan 在功能方面并没有那么全面,也没有实现特别多的扩展,但 Motan 是一个小而精的 RPC 框架,它的特点是简单、易用,是一个不断向着实用、易用方向发展的 RPC 服务框架。



Motan RPC 框架介绍

Motan 的交互流程

Motan 中有服务提供方 RPC Server,服务调用方 RPC Client 和服务注册中心 Registry 三个角色。


  • Server 向 Registry 注册服务,并向注册中心发送心跳汇报状态。

  • Client 需要向注册中心订阅 RPC 服务,Client 根据 Registry 返回的服务列表,对具体的 Sever 进行 RPC 调用。

  • 当 Server 发生变更时,Registry 会同步变更,Client 感知后会对本地的服务列表作相应调整。


交互关系如下图:




Motan 可以支持不同的注册中心,如 ZK、Consul,目前使用的注册中心是平台开发的 Vintage,Vintage 是一个基于 Redis 的轻量级 KV 存储系统,能够提供命名空间服务、服务注册、服务订阅等功能。


Motan 框架

Motan 中主要有 register、transport、serialize、protocol 几个功能模块,各个功能模块都支持通过 SPI 进行扩展,各模块的交互如下图所示:



Register:用来和注册中心进行交互,包括注册服务、订阅服务、服务变更通知、服务心跳发送等功能。Server 端会在系统初始化时通过 register 模块注册服务,Client 端在系统初始化时会通过 register 模块订阅到具体提供服务的 Server 列表,当 Server 列表发生变更时也由 register 模块通知 Client。


Protocol:用来进行 RPC 服务的描述和 RPC 服务的配置管理,这一层还可以添加不同功能的 filter 用来完成统计、并发限制等功能。


Serialize:将 RPC 请求中的参数、结果等对象进行序列化与反序列化,即进行对象与字节流的互相转换;默认使用对 Java 更友好的 hessian2 进行序列化。


Transport:用来进行远程通信,默认使用 Netty nio 的 TCP 长链接方式。


Cluster: Client 端使用的模块,cluster 是一组可用的 Server 在逻辑上的封装,包含若干可以提供 RPC 服务的 Server,实际请求时会根据不同的高可用与负载均衡策略选择一个可用的 Server 发起远程调用。


在进行 RPC 请求时,Client 通过代理机制调用 cluster 模块,cluster 根据配置的 HA 和 LoadBalance 选出一个可用的 Server,通过 serialize 模块把 RPC 请求转换为字节流,然后通过 transport 模块发送到 Server 端。


Server 端的 transport 模块收到数据后,通过 serialize 模块还原成 RPC 请求,并通过 protocol 层配置的参数找到具体提供服务的实现类,通过反射进行调用。调用后的结果在通过类似的方式返回到 Client 端,完成一次 RPC 请求。


Motan RPC 在混合云中新的需求和挑战

在混合云平台建设过程中,RPC 服务需要适应云端部署,在实际部署当中对 RPC 提出了一些新的要求。混合云平台包括私有云与公有云两部分,其中私有云在本地机房,为了保证云与云之间通信的稳定,使用了专线的方式链接私有云与公有云。RPC 服务在云端扩容大致有如下三种情况:


  • 扩容 RPC Client


  • 扩容 RPC Server


  • 按比例同时扩容 Client 和 Server





在实际扩容中会我们会尽量使用第三种方式进行就近访问,但是有些情况下不可避免会产生第一二种情况,例如需要单独扩容 RPC 服务,或者某些 RPC 服务依赖的资源暂时无法在公有云部署等。 在第一、二种情况下,会产生跨专线调用(图中蓝色虚线),为了能够灵活控制 RPC 调用,需要 RPC 支持跨机房调用的能力,并且能够控制跨机房调用的比例。


另外,以前 RPC 都是通过 group 配置来实现同机房调用,实际使用当中并不需要特别关注带宽占用情况,但是在混合云环境中,跨机房调用会占用专线带宽,因此,RPC 占用的带宽也需要尽量节约。


我们做了如下改进:


流量压缩

在微博中有两种典型的 RPC 使用场景:一种是请求的参数和返回值都非常小,但是请求量大,QPS 高。例如微博的未读数 unread 服务。另一种场景是 QPS 不高,但每次返回值较大。


针第一种场景,我们对协议自身信息进行了压缩。Motan 的请求信息大致包括 4 个部分 header 信息、Service 及请求方法描述、参数值、附加信息。


  • header 信息包括 RPC 版本、消息类型、消息长度等固定内容,长度 16 字节。

  • Service 和方法描述包括请求服务的接口类全称、方法名、方法参数描述。

  • 参数值是通过 hessian2 序列化后的参数对象。

  • 附加信息包括调用方信息(application)、接口版本号、group、requestId 等。


一般情况下 Service 和方法描述、附加信息等就有 200 多个字节,这样在发送一个只有 long 型参数等请求时,协议的有效载荷就比较低。压缩的思路是使⽤方法签名代替方法描述信息,并缓存传输中固定的附加信息。


RPC Server 提供的接口和方法是有限的,只要能标示出想要调用的方法及版本,就不需要携带完整的方法信息。Server 端和 Client 端都把接口名、方法名、参数描述、版本信息生成 16 字节的签名并把对应关系进行缓存,这样在每次请求时只需要携带 16 字节的签名就能找到具体的调用方法。


方法签名需要在一个 Server 内唯一,所以不需要考虑全局碰撞,只要对应 Server 中的方法签名不产生碰撞冲突就可以。按单个 Server 中包含 10W 个方法估算,16 字节签名产生冲突的概率⼤约为 1 / 2 * 10W  * (10W - 1)  * 1 / (2 ^ 128) 约为 1 / 2 ^ 99,碰撞的概率可以忽略不计。当 Server 端新增方法出现碰撞时,可以通过修改⽅法名避免冲突。


请求中的附加信息用来统计调用情况,每个 Client 在运行中附加信息大部分是不会变动的,因此如果 Server 端能够缓存这些信息也就不需要每次请求都携带了。


在 Client 首次调用服务时,会把固定的附加信息和对应的附加信息签名一起发送给 Server 端,Server 端收到请求后会以 IP 为维度缓存附加信息和签名,然后返回值中会回传对应签名作为应答。Client 收到应答信息后再次请求时就只需传递签名了。当 Server 端因为重启等原因丢失签名对应信息后,如果收到未知的签名信息会要求 Client 下次请求携带完整附加信息,这样就能够再次缓存对应信息。


对协议信息进行压缩后,一次只有一个 long 参数的 RPC 请求从 280 字节减少到了 94 个字节。在实测场景下平均每个请求压缩了 60%,在 QPS 较高的场景中上行带宽明显减少。


针对另一种返回值较大的场景,我们尝试了不同的序列化协议,如使用 Protocol Buffers 代替 hessian2,实际测试约减小 10%~15%,效果不算理想,当使用 QuickLZ 或 GZIP 进行压缩后,根据原始大小不同压缩后大约可以减少 30%~70%,缺点就是 CPU 负载会有所增加。


最后我们增加了压缩功能,业务方可以根据业务特点配置是否启用压缩以及启用压缩的最小阈值。为了尽量减少第三方包依赖,默认使用 GZIP 进行压缩。实际测试在返回值 2KB~20KB 时压缩会有比较好的效果,大于 50KB 压缩平均耗时就会比较高。


压缩统计数据如下:



动态流量调整

Motan 中的 Service(即接口类)以 group 为单位划分,group 一般由机房+业务线组成,一个 group 下可以有多个 Service。例如 group 为 yf-user 表示永丰机房的 user RPC 服务,yf-user 的 Client 只会订阅 yf-user 的 Server,通过 group 实现同机房就近访问。


要想控制 Client 的跨机房调用,实际上就是允许 Client 以一定比例进行跨 group 调用。因此从注册中心或 Client 端自身都可以实现这个功能,为了 Motan 框架在使用不同注册中心时都能拥有流量控制的功能,我们选择了在 Client 端实现。


我们设计了一套指令系统,这样方便后续扩展更多的管理功能,指令使用 JSON 格式保存在注册中心,当 Client 在订阅 RPC 服务时同时也订阅对应的指令。指令以 group 为单位存储,即相同 group 的 Client 会接收到相同的指令。当从管理后台发布一条指令后,对应的 Client 会收到指令,并且解析执行指令。


单条指令的样例如下:

{

    " index " : 1,

    " version " :  "1.0",

    " dc " :  " yf ",

    " pattern " :  " * ",

    " mergeGroups " : [

        " openapi-tc-test-RPC : 12 ",

        " openapi-yf-test-RPC : 1 "

    ] ,

    " routeRules " :  [],

    " remark " : " 切换 50% 流量到另外一个机房 "

}


其中 mergeGroups 属性用来实现跨机房调用设置,可以设置多个机房以及各机房调用的权重。跨机房调用过程如下图所示:



group1 和 group2 分别属于两个不同的机房,提供相同的 Service,Client1 和 Server1 属于 group1,Client2 和 Server2 属于 group2。 正常情况下 Client 对Server 的调用是同 group 调用,如图中蓝色箭头所示。


当 group1 的 Server 压力突增时,通过管理后台 manager 在 registry 设置 group1 的 mergeGroup 指令为


" mergeGroups " :  [

    " group1 : 5 ",

    " group2 : 1 "

]


这时 group1 的 Client1 会收到指令并解析,根据指令 Client1 会同时订阅 group1 和 group2,并按 5 : 1 的比例同时访问 group1 的 Server1 和 group2 的 Server2,如图中红色虚线表示。


Client2 由于不属于 group1,所以并不会接收到这条指令,仍然只访问 Server2。


指令控制的粒度可以到接口类级别,通过 pattern 字段来进行设置。通过设置 group 的权重来控制流量切换的比例,最小比例可以到 1%。


指令中的 routeRules 字段可以实现路由功能,来精确控制调用,或者用来实现预览等功能。如:


{

    " index " : 3,

    " version " :  " 1.0 ",

    " dc " : " yf ",

    " pattern ": " com.weibo.xxxx.Preview ",

    " mergeGroups " :  [],

    " routeRules " : [

        " * to !10.75.0.1 "

    ],

    " remark " : " 预览某台机器,关闭其线上流量 "

}


routeRules 中的每条 rule 表示 from to 的关系, " * to !10.75.0.1 ” 这条 rule 表示禁止访问预览机 10.75.0.1。


rule 规则支持通配符,如 “ 10.75.0.* to 10.75.0.1 ” 表示 10.75.0 段的所有 Client 只能请求 10.75.0.1 这台 Server。


其他优化

在 RPC 调用中还有一些小的细节,例如,Server 端业务异常时会把异常栈序列化并传递到 Client,一个异常栈可能会有 1- 2k 的大小,如果 RPC 调用中异常大量增加,也会占用不小带宽。考虑到大部分场景 Client 端只关心异常原因,并不关心异常栈内容,我们对业务异常时的异常栈进行了替换,避免传输不必要的栈信息。


此外还对 RPC 服务的注册与注销机制进行了一些调整,并支持了使用 consul 作为注册中心。使 RPC 服务在混合云的部署能够更加灵活。



后记

Motan RPC 框架在微博平台的各个业务中发挥着重要作用,通过在微博将近三年的线上运行考验,正在变的越来越优秀,功能也更加完善。Motan 的设计理念是简单、易用,前辈大师们在设计 Motan 框架时就朝着这个方向努力,后续也将向这个方向继续发展。


如何打造更为易用的高性能 RPC 框架?我觉得除了与时俱进的实现新的需求,更需要从细节上考虑框架的易用性,例如新功能的无缝升级、尽量降低业务方的接入成本、增加必要的切换开关提高灵活性,后台指令预览防止误操作等,从每个细节完善 Motan 框架。


近期我们打算对 Motan 框架进行全面梳理,去掉特定的依赖,使用更通用的方式来实现,并完善配套的文档。开源,是 Motan 设计之初的方向之一,我们要把 Motan 打造成一个更易用的高性能 RPC 开源框架。欢迎大家一起参与。


Q & A

1、Motan 是否开源?现在能下载代码不?

我们正在进行梳理,近期准备开源核心部分。


2、支持 PHP 等跨语言调用吗?

目前 Motan 还不支持 PHP 等其他语言调用,我们正在进行这方面工作,已经在小范围测试。


3、图里的耗时是压缩耗时么?

图里的耗时是单次压缩耗时均值。


4、从零开始实现一个 RPC,一般需要考虑实现哪些东西,大致思路是什么?

简单的 RPC 调用只需要考虑协议(方法描述)、序列化(参数值传递)、传输(TCP、HTTP 等)


5、Motan 支持异步调用吗?如何实现?Load balance 支持哪些策略?是否支持自定义策略?

Motan 的请求在传输层面都是异步调用的,但是在使用本地引用时不能显示异步执行。Load balance 策略支持低并发优先、一致性 hash、随机、轮询。支持通过 SPI 机制扩展其他策略。


6、感觉和 Dubbo 好像好像,里面的序列化协议,通信协议,框架模块定义,架构分层,服务分组都一样一样的,命令行改配置好像也不方便,Dubbo 的管理界面很方便,相比 Dubbo 具体怎么轻量?Dubbo 用起来好像也只要一个注册中心就可以了。

与 Dubbo 的分层来对比,Motan 的模块层次要更简单一些,没有 exchange 和 directory 等,Motan 提供了一些 SLA 策略,例如并发限制等。


7、如果设计监控的话一般涉及到那些指标,如何快速发现服务出现问题?

一般通过监控统计日志中的耗时、框架异常、业务异常数量,来发现服务问题

RPC 的调用情况是在内存中通过 Metric 进行统计的,然后统一输出到统计日志中。


8、重新实现了一套 Dubbo 是出于什么样的考虑,或者说业务上 Dubbo 无法解决哪方面的需求?

Dubbo 功能上比较丰富,但当时我们想要一个比较轻量的 RPC 框架,方便我们做一些适合自己业务场景的改造和功能 feature,以达到内部业务平滑改造和迁移的目的。这种情况下,在 dubbo 上改的成本可能比重新写一套更高。最终我们决定开发 motan RPC。从另外角度,选择复用和选择自行开发需要看团队的研发能力与资源,我们当时刚好有合适的工程师有兴趣来做这个。


9、Motan 支持功能扩展吗?

支持通过 SPI 方式进行扩展


10、Registry 如何保证高可用?Server 与 Registry 是双向心跳还是单向?或者说如何保证 Server 上下线状态及时感知?

Registry 自身需要较强的容灾来保证高可用,例如 ZK、Consul 都支持很强的容灾。另外,当 Registry 不幸各个节点都挂掉的话,会影响服务的发布与注销,不会影响 Client 的正常调用。Server 与 Registry 是单向心跳。Server 上下线 Client感知有可能会有延迟,一般都是秒级。


12、一致性hash的负载策略能具体讲一下吗?

根据请求 Request 参数计算出一个 hashcode,按 hashcode 每次请求同一个 server。一致性 hash 的策略主要用于有状态的RPC服务场景,比如有session的IM服务等。 


推荐阅读
  • 本文将详细介绍如何在ThinkPHP6框架中实现多数据库的部署,包括读写分离的策略,以及如何通过负载均衡和MySQL同步技术优化数据库性能。 ... [详细]
  • 本文探讨了Web开发与游戏开发之间的主要区别,旨在帮助开发者更好地理解两种开发领域的特性和需求。文章基于作者的实际经验和网络资料整理而成。 ... [详细]
  • Spring Cloud因其强大的功能和灵活性,被誉为开发分布式系统的‘一站式’解决方案。它不仅简化了分布式系统中的常见模式实现,还被广泛应用于企业级生产环境中。本书内容详实,覆盖了从微服务基础到Spring Cloud的高级应用,适合各层次的开发者。 ... [详细]
  • 本文深入探讨了MySQL中常见的面试问题,包括事务隔离级别、存储引擎选择、索引结构及优化等关键知识点。通过详细解析,帮助读者在面对BAT等大厂面试时更加从容。 ... [详细]
  • 本文探讨了如何在日常工作中通过优化效率和深入研究核心技术,将技术和知识转化为实际收益。文章结合个人经验,分享了提高工作效率、掌握高价值技能以及选择合适工作环境的方法,帮助读者更好地实现技术变现。 ... [详细]
  • 远程过程调用(RPC)是一种允许客户端通过网络请求服务器执行特定功能的技术。它简化了分布式系统的交互,使开发者可以像调用本地函数一样调用远程服务,并获得返回结果。本文将深入探讨RPC的工作原理、发展历程及其在现代技术中的应用。 ... [详细]
  • 深入解析Spring Cloud微服务架构与分布式系统实战
    本文详细介绍了Spring Cloud在微服务架构和分布式系统中的应用,结合实际案例和最新技术,帮助读者全面掌握微服务的实现与优化。 ... [详细]
  • 本文详细介绍了Git分布式版本控制系统中远程仓库的概念和操作方法。通过具体案例,帮助读者更好地理解和掌握如何高效管理代码库。 ... [详细]
  • 2018年3月31日,CSDN、火星财经联合中关村区块链产业联盟等机构举办的2018区块链技术及应用峰会(BTA)核心分会场圆满举行。多位业内顶尖专家深入探讨了区块链的核心技术原理及其在实际业务中的应用。 ... [详细]
  • 阿里云ecs怎么配置php环境,阿里云ecs配置选择 ... [详细]
  • 本文探讨如何配置 Nginx 以将传入请求反向代理到运行在本地绑定端口上的 Docker 容器,并解决常见的路径重定向问题。 ... [详细]
  • 本文详细介绍了在 Windows 7 系统中配置 Nginx 1.10.3 和 PHP 7.1.1 NTS 的步骤,包括修改 PHP 配置文件、处理依赖项以及创建批处理脚本启动和停止服务。重点解释了如何解决常见的运行时错误。 ... [详细]
  • 本文详细介绍了 Kubernetes 集群管理工具 kubectl 的基本使用方法,涵盖了一系列常用的命令及其应用场景,旨在帮助初学者快速掌握 kubectl 的基本操作。 ... [详细]
  • 全能终端工具推荐:高效、免费、易用
    介绍一款备受好评的全能型终端工具——MobaXterm,它不仅功能强大,而且完全免费,适合各类用户使用。 ... [详细]
  • 深入理解Docker网络管理
    本文介绍了Docker网络管理的基本概念,包括为什么需要Docker网络管理以及Docker提供的多种网络驱动模式。同时,文章还详细解释了Docker网络相关的命令操作,帮助读者更好地理解和使用Docker网络功能。 ... [详细]
author-avatar
手机用户2702933671_440
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有