微服务之 PHP 浅尝 Thrift

微服务是用来解决海量请求的一种分布式架构方案。

一、Monolithic 架构

在讲 Thrift 之前,我们先来认识一下传统系统构架。

monolitich.png

我们把上图这种传统架构称为:Monolithic 架构。

Monolithic 架构是目前中小型系统常用的构架方案。

1.1)Monolithic 架构优缺点:

它的优点是:

  • 开发简单直接,集中式管理,基本不会重复开发。
  • 功能都在本地(即所有业务都由当前服务器完成,区别于微服务),没有分布式的管理开销和调用开销。
  • 开发效率低:所有的开发在一个项目改代码,提交代码相互等待,代码冲突不断(简单的业务分工明确这种情况比较少)。
  • 代码维护难:代码功能耦合在一起,任何修改都可能会影响到其他功能。
  • 部署不灵活:因为代码耦合一起,任何小修改都要整个系统重新发布。
  • 扩展性不好:高并发场景下,我们想遇到瓶颈的业务进行扩展不得不整个系统横向扩展。

1.2)Monolithic 构架现状:

我们再扩展讲一下当前中小型项目系统架构的现状:

1.2.1)前后台整合一起

这种常见于企业站、个人博客。如:PHPCMS V9、typecho、workpress 等。它们就是把面向用户的前台系统与面向管理员的后台系统代码整合到一个项目当中进行开发部署。这种模式的最大优点是开发部署简单。毕竟,系统整个业务都比较简单。所以,Monolithic 框架的缺点就并不明显,可以忽略不计。

1.2.2)前后台分离

这种常见于中小型的公司系统。如电商系统。博主寒冰之前在多家公司就是采用这种模式。面向用户端的前台单独一套系统,面向管理员的后台单独一套系统。这样前后台的系统之间互不影响。这种模式在项目稍大且业务稍大的情况下,非常适用。通常这种系统流量也不会很大。百万级左右。当然,也可以通过横向扩展服务器来加大业务吞吐性能。

1.2.3)模块化拆分

这个模块化拆分跟微服务的模块化拆分有异曲同工之妙。它是把一个系统按照功能模块进行拆分。比如我们的系统有用户中心、订单中心、广告中心、活动中心等模块。我们就把这四个模块拆分成单独的系统来开发部署。

这个模式是解决了 1.2.2 当中前后台分离系统的带来的大多数问题。

比如,在 1.2.2 的前后台分离的模式中,订单模块现在吞吐性能已经不满足当前的业务了。由于它是与其他模块一起在同一个项目中,我们要对这个模块进行优化的时候就会遇到如下几个问题:

  • 服务扩容。必须会把其它模块也进行了扩容。
  • 代码优化或重构势必会影响关联模块代码的稳定性。增加开发成本、测试成本、和部署的风险。

通过系统模块化拆分之后,我们只需要对订单模块的系统优化重构以及水平扩容。不需要考虑其他模块了。同时还有一个很大的优点:服务降级时,假如订单系统压力过大,我们可以在订单系统做限流降级等操作。

通常,使用这种模式已经足够满足大多数业务场景了。毕竟,在整个互联网流量能上千万过亿的公司毕竟还是少数。

但是,为了能让自己更进一步何乐而不为呢?于是,就有了今天这篇博文。

二、微服务特性

在上面,我们讲到了 Monolithic 这种独立式单体构架的优缺点。现在我们来说说微服务。网上有很多对微服务的定义。我用一句话来概括:

微服务就是把系统拆分成若干小功能,每个小功能单独开发部署。我们把每个小功能称之为微服务。微服务互相调用采用 RPC 通信,外部采用 RESTFUL API 与微服务通信。

我这各大概括可能不够百分百准确。但是,大体也差不多是这个意思。

这里面有一个很重要的点:服务折分。

2.1)微服务拆分原则

服务拆分并不是盲目地拆分,而应该是有一套对应的准则。能不能拆分成微服务,取决于四个要求:

  • 小:微服务体积小,2 pizza 团队。
  • 独:能够独立的部署和运行。
  • 轻:使用轻量级的通信机制和架构。
  • 松:为服务之间是松耦合的。

注:2 pizza 团队最早是亚马逊 CEO Bezos提出来的,意思是说单个服务的设计,所有参与人从设计、开发、测试、运维所有人加起来 只需要2个披萨就够了

服务拆分得过小,则集成的时候工作量就上升,折分过大,则耦合度很高。所以,服务的拆分是非常重要的。

2.2)微服务设计原则

单一职责原则

  • 意思是每个微服务只需要实现自己的业务逻辑就可以了,比如订单管理模块,它只需要处理订单的业务逻辑就可以了,其它的不必考虑。

服务自治原则

  • 意思是每个微服务从开发、测试、运维等都是独立的,包括存储的数据库也都是独立的,自己就有一套完整的流程,我们完全可以把它当成一个项目来对待。不必依赖于其它模块。

轻量级通信原则

  • 首先是通信的语言非常的轻量,第二,该通信方式需要是跨语言、跨平台的,之所以要跨平台、跨语言就是为了让每个微服务都有足够的独立性,可以不受技术的钳制。

接口明确原则

  • 由于微服务之间可能存在着调用关系,为了尽量避免以后由于某个微服务的接口变化而导致其它微服务都做调整,在设计之初就要考虑到所有情况,让接口尽量做的更通用,更灵活,从而尽量避免其它模块也做调整。

2.3)微服务优势与缺点

2.3.1)微服务特点

  • 每个微服务可独立运行在自己的进程里;
  • 一系列独立运行的微服务共同构建起了整个系统;
  • 每个服务为独立的业务开发,一个微服务一般完成某个特定的功能,比如:订单管理,用户管理等;
  • 微服务之间通过一些轻量级的通信机制进行通信,例如通过 REST API 或者 RPC 的方式进行调用。

2.3.2)微服务优点

易于开发和维护

  • 由于微服务单个模块就相当于一个项目,开发这个模块我们就只需关心这个模块的逻辑即可,代码量和逻辑复杂度都会降低,从而易于开发和维护。

启动较快

  • 这是相对单个微服务来讲的,相比于启动单体架构的整个项目,启动某个模块的服务速度明显是要快很多的。

局部修改容易部署

  • 在开发中发现了一个问题:如果是单体架构的话,我们就需要重新发布并启动整个项目,非常耗时间。但是微服务则不同,哪个模块出现了 BUG 我们只需要解决那个模块的 BUG 就可以了。解决完 BUG 之后,我们只需要重启这个模块的服务即可。部署相对简单,不必重启整个项目从而大大节约时间。

技术栈不受限

  • 比如订单微服务和电影微服务原来都是用 Java 写的,现在我们想把电影微服务改成 Go 技术,这是完全可以的。而且由于所关注的只是电影的逻辑而已,因此技术更换的成本也就会少很多。

按需伸缩

  • 我们上面说了单体架构在想扩展某个模块的性能时不得不考虑到其它模块的性能会不会受影响。对于我们微服务来讲,完全不是问题,电影模块通过什么方式来提升性能不必考虑其它模块的情况。

2.3.3)微服务缺点

运维要求较高

  • 对于单体架构来讲,我们只需要维护好这一个项目就可以了。但是对于微服务架构来讲,由于项目是由多个微服务构成的,每个模块出现问题都会造成整个项目运行出现异常。想要知道是哪个模块造成的问题往往是不容易的,因为我们无法一步一步通过 debug 的方式来跟踪,这就对运维人员提出了很高的要求。

分布式的复杂性

  • 对于单体架构来讲,我们可以不使用分布式。但是对于微服务架构来说,分布式几乎是必会用的技术。由于分布式本身的复杂性,导致微服务架构也变得复杂起来。

接口调整成本高

  • 比如,用户微服务是要被订单微服务和电影微服务所调用的,一旦用户微服务的接口发生大的变动,那么所有依赖它的微服务都要做相应的调整。由于微服务可能非常多,那么调整接口所造成的成本将会明显提高。

重复劳动

  • 对于单体架构来讲,如果某段业务被多个模块所共同使用,我们便可以抽象成一个工具类,被所有模块直接调用。 但是微服务却无法这样做,因为这个微服务的工具类是不能被其它微服务所直接调用的,从而我们便不得不在每个微服务上都建这么一个工具类,从而导致代码的重复。

三、微服务实践理论

3.1)客户端如何访问这些服务?(API Gateway)

传统的开发方式,所有的服务都是本地的,UI可以直接调用,现在按功能拆分成独立的服务,跑在独立的一般都在独立的虚拟机上的 Java进程了。客户端UI如何访问他的?后台有N个服务,前台就需要记住管理N个服务,一个服务下线/更新/升级,前台就要重新部署,这明显不服务我们 拆分的理念,特别当前台是移动应用的时候,通常业务变化的节奏更快。另外,N个小服务的调用也是一个不小的网络开销。还有一般微服务在系统内部,通常是无状态的,用户登录信息和权限管理最好有一个统一的地方维护管理(OAuth)。

所以,一般在后台N个服务和UI之间一般会一个代理或者叫API Gateway,他的作用包括

  • 提供统一服务入口,让微服务对前台透明
  • 聚合后台的服务,节省流量,提升性能
  • 提供安全,过滤,流控等API管理功能
  • 我的理解其实这个API Gateway可以有很多广义的实现办法,可以是一个软硬一体的盒子,也可以是一个简单的MVC框架,甚至是一个 Node.js 的服务端。他们最重要的作用是为前台(通常是移动应用)提供后台服务的聚合,提供一个统一的服务出口,解除他们之间的耦合,不过 API Gateway 也有可能成为单点故障点或者性能的瓶颈。

3.2)服务之间如何通信?(服务调用)

因为所有的微服务都是独立的Java进程跑在独立的虚拟机上,所以服务间的通行就是IPC(inter process communication),已经有很多成熟的方案。现在基本最通用的有两种方式。这几种方式,展开来讲都可以写本书,而且大家一般都比较熟悉细节了, 就不展开讲了。

  • REST(JAX-RS,Spring Boot)
  • RPC(Thrift, Dubbo)
  • 异步消息调用(Kafka, Notify)

一般同步调用比较简单,一致性强,但是容易出调用问题,性能体验上也会差些,特别是调用层次多的时候。RESTful和RPC的比较也是一个很有意 思的话题。一般REST基于HTTP,更容易实现,更容易被接受,服务端实现技术也更灵活些,各个语言都能支持,同时能跨客户端,对客户端没有特殊的要 求,只要封装了HTTP的SDK就能调用,所以相对使用的广一些。RPC也有自己的优点,传输协议更高效,安全更可控,特别在一个公司内部,如果有统一个的开发规范和统一的服务框架时,他的开发效率优势更明显些。就看各自的技术积累实际条件,自己的选择了。

异步消息的方式在分布式系统中有特别广泛的应用,他既能减低调用服务之间的耦合,又能成为调用之间的缓冲,确保消息积压不会冲垮被调用方,同时能 保证调用方的服务体验,继续干自己该干的活,不至于被后台性能拖慢。不过需要付出的代价是一致性的减弱,需要接受数据最终一致性;还有就是后台服务一般要 实现幂等性,因为消息发送出于性能的考虑一般会有重复(保证消息的被收到且仅收到一次对性能是很大的考验);最后就是必须引入一个独立的broker,如 果公司内部没有技术积累,对broker分布式管理也是一个很大的挑战。

3.3)这么多服务怎么查找?(服务发现)

在微服务架构中,一般每一个服务都是有多个拷贝,来做负载均衡。一个服务随时可能下线,也可能应对临时访问压力增加新的服务节点。服务之间如何相互 感知?服务如何管理?这就是服务发现的问题了。一般有两类做法,也各有优缺点。基本都是通过zookeeper等类似技术做服务注册信息的分布式管理。当 服务上线时,服务提供者将自己的服务信息注册到ZK(或类似框架),并通过心跳维持长链接,实时更新链接信息。服务调用者通过ZK寻址,根据可定制算法,找到一个服务,还可以将服务信息缓存在本地以提高性能。当服务下线时,ZK会发通知给服务客户端。

客户端做:优点是架构简单,扩展灵活,只对服务注册器依赖。缺点是客户端要维护所有调用服务的地址,有技术难度,一般大公司都有成熟的内部框架支持,比如 Dubbo。

服务端做:优点是简单,所有服务对于前台调用方透明,一般在小公司在云服务上部署的应用采用的比较多。

3.4)服务挂了怎么办?

分布式最大的特性就是网络是不可靠 的。通过微服务拆分能降低这个风险,不过如果没有特别的保障,结局肯定是噩梦。我们刚遇到一个线上故障就是一个很不起眼的SQL计数功能,在访问量上升 时,导致数据库load彪高,影响了所在应用的性能,从而影响所有调用这个应用服务的前台应用。所以当我们的系统是由一系列的服务调用链组成的时候,我们必须确保任一环节出问题都不至于影响整体链路。相应的手段有很多:

  • 重试机制
  • 限流
  • 熔断机制
  • 负载均衡
  • 降级(本地缓存) 这些方法基本上都很明确通用,就不详细说明了。比如Netflix的Hystrix:https://github.com/Netflix/Hystrix

最后,我们给出一幅微服务大致的架构图:

microservice.png

这幅架构图只是画了一个大概。还有很多细节并未列出。旨在让大家明白微服务的架构形态。

四、PHP 微服务实战

今天我们选用 thrift 构架来实现微服务之间的通信。单独的微服务是没有任何意义的。微服务将一个整体拆分成多个小块。但是,最终都会为上去完整的业务而服务。所以,微服务之间一定会存在调用与被调用的关系。

调用就会存在通信。通信就需要一套规范,不然就会乱套。我们今天用 Thrift 来通信。当然类似的产品还有 Google 出品的 gRPC。还有阿里出品的 Dubbo。

接下来,我们以 CentOS 7 64 bit 系列为例对 Thrift 框架进行说明讲解。

4.1)安装 thrift 依赖库

开发工具集

$ sudo yum -y groupinstall "Development Tools"

安装 autoconf

$ sudo yum -y install autoconf

安装 automake

$ sudo yum -y install automake

安装 bison

$ sudo yum -y install bison

安装 C++ 依赖库

$ wget http://iweb.dl.sourceforge.net/project/boost/boost/1.60.0/boost_1_60_0.tar.gz
$ tar zxvf boost_1_60_0.tar.gz
$ ./bootstrap.sh --prefix=/usr
./b2 install 
cd ..

安装 libevent 库

$ sudo yum -y install libevent

4.2)安装 thrift

$ wget https://github.com/apache/thrift/archive/master.zip
$ unzip master.zip
$ cd thrift-master
$ ./bootstrap.sh
$ ./configure --with-boost=/usr/local
$ make && make install

这样我们的 thrift 安装成功了。

5)官方示例

官方提供了当前 Thrift 支持的所有语言的示例。

官网提供的一些示例链接几乎都失效了。博主花了好久时间才发现在安装包里面提供了示例。可见国外做的开源东西有时候也并不那么可靠。

示例的目录在我们 4.2 步骤当中解压出来的 tutorial 目录下。比如常见的 C、C++、C#、Go、Java、PHP 等。

其中 shared.thrifttutorial.thrift 这两个 IDL 文件。

我们在实际的开发中,会单独在项目下创建一个目录来存放这些 IDL 文件。然后,进入该目录执行如下命令,会自动给我们生成文件。

$ thrift -r --gen php tutorial.thrift

由于在 tutorial.thrift 文件当中使用 include 加载了 shared.thrift 文件。所以,我们不需要单独再对该文件进行操作。

然后,在目录下会生成一个 gen-php 。下面就有一堆的 PHP 文件。

6)库文件

很多人在初学 Thrift 的时候,经常会由于加载不到指定的库而报错。网上很多资料并未交代这些类库所在位置。这当中也包括官方网站的文档。至少截止目前(2018-11-06)是没有交代清楚的。要不是在 Thrift 的安装包当中发现了示例代码,再由示例代码找到了库。我怀疑我都没办法深入学习 Thrift 了。

所有的库都在 Thrift 安装目录下的 lib 。下面提供了当前 Thrift 所支持的所有语言的。

7)实际项目示例

因为,我是用 PHP 语言入手了解并学习 Thrift。所以,我提供的示例也是 PHP 的。其他语言的,根据 Thrift 包的示例,就能很好的入门 Thrift。

[root@localhost thrift]# tree -d
.
├── Thrift
│   ├── Base
│   ├── ClassLoader
│   ├── Exception
│   ├── Factory
│   ├── Protocol
│   │   ├── JSON
│   │   └── SimpleJSON
│   ├── Serializer
│   ├── Server
│   ├── StringFunc
│   ├── Transport
│   └── Type
└── ThriftGen
    └── gen-php
        ├── shared
        └── tutorial

总结:PHP 不太适合用来做服务端。至少原生 PHP 提供的机制并不能提供高效的 RPC 机制。通常都只能通过 PHP 的第三方扩展来实现。比如 workerman、swoole。通常我们都是用 Java、Go 等语言做微服务开发,然后用 PHP 等语言来做接口的调用。当然,这并不是绝对的。

微服务当前主要是解决海量并发请求的场景。如果,你的项目并没有这方面的需求。其实,微服务并不是那么有用。反而没有传统的单体应用靠谱。

资料参考链接:http://thrift.apache.org/tutorial/php

博主 2011 年创建了一个《PHP 初学者官方群》,目前群成员 500 人左右。群号:168159147。为了防止广告,设置为付费入群。欢迎大家加入讨论技术!

标签: 无

发表评论: