58集团_容器云插件化在房金融业务实践

背景

现状

在微服务架构中,如果应用多了就会形成一些共有的需求。特别是流量控制方面,包括限流、流量分发和监控、灰度等等。通常我们对一类需求可以实现一个抽象层,然后在这个抽象层上实现具体的业务逻辑。使用各种语言的 SDK 来集成到应用中。目前我们现在在用的fang-spring-boot-starter-auth-login 这个jar包,就是这种模式。

               

这过程中会有这样的一些问题需要考虑:

SDK 的维护成本是很高

SDK 集成到代码中,其中一个组件发生故障就可能会影响到其他组件,SDK 和应用程序之间是保持着相互依赖的关系的。

在应用层和基础服务没有解耦的情况下,我们对基础服务做改动会增加很多风险和复杂度。

为什么使用插件平台?

现在很多架构都跑在容器这样的环境了,统一的抽象层能大大减少架构上的复杂度。web插件在不改变主应用的情况下,会起来一个辅助应用,来辅助主应用做一些基础性的甚至是额外的工作。

这个辅助应用不一定属于应用程序的一部分,而只是与应用相连接。

               

这样的好处在于:

应用层和基础服务层解耦

基础服务统一维护,SDK 统一集成,减少复杂度,减少应用服务中的重复部分

可以在不改变原有应用的情况下,为应用扩展新的功能

准备工作

在 58云 上新建插件 ,目前该功能权限未放开,需要插件负责人操作。

               

下载 插件demo 工程  javademo ,插件是基于gRPC 框架,整体设计使用观察者模式,我们开发的插件,在其中扮演一个观察者,观察的内容就是绑定集群的请求。平台会维护观察者集合,可以按照顺序依次调用不同的观察者(插件)执行相应的代码逻辑,所以同一个集群可以使用多个插件,且插件的执行顺序可自定。

插件框架使用

入口函数callV2Execute

private void callV2Execute(MeshRequestV2 request, StreamObserver<MeshResponseV2> responseObserver) ;

我们基于插件开发功能,就是写在callV2Execute这个入口函数中 。核心需要掌握平台定义的两个入参MeshRequestV2 和MeshResponseV2

MeshRequestV2

public class MeshRequestV2{ long id; // 业务一次http请求的id,用于串联业务同一个请求的req和resp String tag;// 个性化配置中的标签,在此透传 int position;//0:req阶段;1:resp阶段 List<Header> headers ; long bizStartAt; //当前请求业务开始时间,毫秒时间戳}class Header{ private ByteString key; private List<ByteString> value;}

插件平台拦截web请求后,会将request的头信息传过来,同时会添加一些其他信息,这些信息以X-Mesh开头

X-Mesh-Method  请求方式GET 或 POST 等

X-Mesh-Host 请求中要访问的域名或IP加端口

X-Mesh-Remote-Ip 发出请求的客户端ip

X-Mesh-Querystring 请求携带的query参数,平台只能取query中的参数,body体的参数获取不到

X-Mesh-Path  请求的uri路径

除了插件平台加到头里的这这个参数外,还有一些http协议中的头信息

Cookie

这里需要注意的是,获取的Cookie的value 是个类似"foo=bar;test=abc"这样的字符串,需要解析使用,这里不能简单通过分号,等号拆分cookie的值,因为比如token信息的value中含有“=”,所以这里推荐使用netty 包中的

io.netty.handler.codec.http.cookie.ServerCookieDecoder#decodeAll方法,将Cookie的value解析成一个cookie集合,方便后续读取使用。

<dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.66.Final</version></dependency>

User-Agent 客户端请求的UA头

Referer  先前网页的地址,即当前请求的来路

Connection 表示是否需要持久连接。(HTTP 1.1默认进行持久连接)

Content-Type 请求的与实体对应的MIME信息

Host 指定请求的服务器的域名和端口号

Cache-Control 指定请求和响应遵循的缓存机制

除了以上请求头外,另有一些自定义请求头,以X-开头

X-Forwarded-For 用于记录代理信息的,每经过一级代理(匿名代理除外),代理服务器都会把这次请求的来源IP追加在X-Forwarded-For中

X-Real-Ip 请求端真实ip,这个值不一定有(生产环境有,测试环境就没有)

X-User-Ip 请求端真实ip,  这个生产不准,测试环境可以用这个替代 X-Real-Ip

X-Forwarded-Port  代理的目的端口,这个也不一定有,测试环境就没有

MeshResponseV2

public class MeshResponseV2{ int httpCode ;// 0:放行 非0:拦截 List<Header> headerSet;// headerSet 同一个key只存在于一个Header中,有值:覆盖此header,无值:删除此header String body;// 拦截时生效,返回给客户端的http body}

MeshResponseV2 是插件处理结果的载体。其中httpCode值为0放行,请求可以到后台真实服务上。非0,插件就会拦截请求,直接返回客户端。这里需要注意的是,这个httpCode值将直接赋值到http请求的状态码上,根据我们的wf项目,即使是异常返回HTTP状态码都统一是200,真正的错误码是通过body体内的code值体现的。所以这个拦截的httpCode 值应为200。

body值为字符串,需要将异常的标准输出,转换为字符串返回前端。

这里的核心是headerSet, 因为插件不能操作body内容,所有的信息都只能放在头里传递到后台服务中。

这里需要强调的是,header中自定义存储的信息,要以X-开头,为了区分其他自定义的header信息,后续插件开发放入的参数计划以X-WEB-开头,并且参数不要用驼峰命名,而是以- 间隔,因为web项目在获取头信息的时候,不区分大小写,当把头信息放到map中存储的时候,key都是小写。

不管是拦截还是正常放行,都是通过io.grpc.stub.StreamObserver 来实现的。

package io.grpc.stub;public interface StreamObserver<V> {void onNext(V var1);void onError(Throwable var1);void onCompleted();}

StreamObserver是定义了三个方法,其中这个onError(Throwable var1)的信息会被插件平台接收,并不会返回前端或后台服务,实际开发中用不到。

我们的插件是使用的gRPC中的steam模式, onNext(),会一直将信息以流的形式返回,直到执行onCompleted()后,流会关闭,此次响应结束。示例代码如下

//路由未匹配,什么也不做,放行。MeshResponseV2 defaultInstance = MeshResponseV2.getDefaultInstance();responseObserver.onNext(defaultInstance);responseObserver.onCompleted();

打包

插件开发完后,本地并不能启动运行。需要部署到测试环境中运行测试。

在插件平台上创建好的插件,名称就是项目名称,我们现在开发的这个叫mesh_plugin_aaa,以此为例,我们需要打一个叫mesh_plugin_aaa.zip的压缩包。包内容如下:

.├── mesh_plugin_aaa│ └── bin│ ├── mesh_plugin_aaa.jar│ ├── restart.sh│ ├── start.sh│ └── stop.sh

当我们开发完代码,执行mvn clean package 后,所需要的这些文件会落在不同的地方。需要我们新建一个名为mesh_plugin_tradesercie的文件夹,然后将target/classes下的bin/ 目录及目录下的脚本文件,复制到mesh_plugin_aaa中,再将编译出的mesh_plugin_aaa.jar 复制到mesh_plugin_aaa/bin 下,之后压缩打包成mesh_plugin_aaa.zip ,最后在云平台上传文件

     

每次发版都要经历这些步骤,这个过程繁琐费时,后续我自己写了个脚本,可以根据自己情况适当修改。

#!/bin/shROOT_DIR="/Users/CODING/mesh_plugin_aaa/target"DOWN_DIR=/Users/DownloadsJAR_NAME="aaa*.jar"MESH=mesh_plugin_aaacd $ROOT_DIRcp $JAR_NAME classes/bincd classescp scfconfig/* binif [ ! -d "$MESH" ]; thenmkdir $MESHficp -r bin $MESHzip -r "$MESH.zip" $MESHif [ -f $DOWN_DIR/$MESH.zip ]; thenrm -f $DOWN_DIR/$MESH.zipfimv $MESH.zip $DOWN_DIR

这样,每次执行完mvn package后,执行一下自定义的这个shell脚本,就可以直接组装好需要的mesh_plugin_aaa.zip文件,然后上传到云平台,然后构建。

构建完成后,在云平台插件版本页签下会看到构建的版本,云平台改版后,这里多了一个审批环节,构建的版本需要提交发布申请,并且审批通过后才可以发布。

               

在发布页签下,点击新建发布,只有审批通过的版本才会被看到,并可以选择。

               

这里,需要注意的是,尽量先选择灰度发布,即部分发布,因为选择灰度发布可以修改,可以删除,同时也可以生效。在测试及线上环境都发布成功后,可以修改为全量发布,只有有新的全量发布,并且老的版本没有被任何服务使用的情况下,老的全量发布版本才能够被删除。全量发布的版本不可修改,只能删除。

这样需要注意的是,如果老的全量发布不及时删除的话,并且新的稳定版本不改为全量发布,由于插件平台开关生产和测试共享,当开发新的版本的过程中,如果这时生产环境项目发版,就又可能将老的版本插件发到生产上,造成不必要的麻烦。

生效

发布版本的时候需要指定集群,在指定的集群重新部署的时候,插件也会重启,如果插件升级过,重启后新版本会生效。因为插件是使用的Sidecar 架构模式,就是说插件是在服务的相同机器上新起了一个容器,在服务的机器上并不能看到插件进程。这时候插件容器可以通过ssh 登陆。例如,我们的XXX的项目测试环境部署在172.0.0.1上,这时,这台机器上的插件需要如下命令登陆。

ssh -p 1050 [email protected] -t "cd /opt/mesh/plugins/mesh_plugin_xxx/log;bash --login"

插件开的1050端口,日志文件在/opt/mesh/plugins/mesh_plugin_xxx/log下,执行上面脚本,可以直接到达日志目录,可以在线查看日志,或者下载日志文件。

云平台升级改版后,也可以直接在云平台网页上直接登陆

       

       

一点思考

目前,公司的web插件整体上还存在很多使用上的不便。

首先就是不能本地运行,不能debug调试,这对前期开发的影响非常大。其次就是升级插件必须重启对应的应用服务,这点也很麻烦。

另外web插件只支持http的流量拦截,所以对于scf服务之间的通信场景,插件将无法工作。

目前service mesh 最成功的实践框架istio,系统会有一些基础的网关能力,对于定制的逻辑是通过写lua脚本实现的。另外一个比较知名的网关APISIX 也是通过写lua脚本实现定制需求。我最近一直研究,咱们的插件能否引入脚步执行引擎,也用lua脚本或者其他脚本写自有逻辑,这样就能热部署。

你如果想  学技术 | 囤干货 | 聊职场 

请长按识别二维码关注即可