架构-联调中心怎么建设

2,398 阅读5分钟

前言


我们这季度针对开发态出现的问题进行优化,开发的时候我们都遇到哪些比较棘手的问题呢?

  1. 联调异常的时候,代码排查比较麻烦,部署时间比较长
  2. 前端需要等待后端开发完成,然后部署后才能进行调试数据,依赖性比较强

那么今天我们讲讲联调中心怎么建设的

组成


有两块组成,一块是流量负载到本地,可以参考 啃老本-开发环境多泳道建设,这个解决第一个问题就是调试上效率比较低的问题,另一块是mock service,我们重点来讲讲第二块。

mock service

它分为前端、后端mock,前端mock为了减少依赖后端服务,提高开发阶段的效率,后端mock也是为了减少其他中间件或者服务依赖。

怎么联动

这个是很重要的问题,如果是单纯看某一端的mock是很方便的,但是整体来看是需要去琢磨的。

比如说后端的接口文档统一在哪里管理,可能某个新人想调一下商品接口查某个sku,他又不认识人,这个接口文档的统一就至关重要。

还有就是后端导出接口文档之后,怎么让前端去mock数据?这就是所谓的联动效果。

实战场景

我们以yapi产品来实践,后端在写完外部请求接口的时候,导出到yapi文档,这时后端同学继续开发里面的功能,然后前端同学根据yapi的高级mock调试里面数据,跟前端内容调试。

这里我们就实现了接口文档统一管理,然后也实现了前后端mock功能,达到前端联调对后端服务依赖减少的效果。

mock代码误提交

image.png

我有个单测群,里面腾讯开源的APIJSON框架负责人提到了这一点,就是当后端在做mock的时候,很容易将调试代码误提交。

这也是我们做架构这些基础功能的时候需要考虑的风险点。

说下我的思路:就是实现一个公共后端mock功能,如果本地打开调试开关,或者说环境是调试环境,自动我们拦截rpc、mq自动将数据mock回去,至于返回的数据格式,可以有个地方让你配置,比如说本地搞个文件,你不要提交到远程即可,即使提交了,默认调试开关关闭,也不会有影响,这样我们就解决了误提交的问题。

当然了对于大厂也有其他的问题,比如

image.png

这就是为什么很多中间件需要做兼容。

值得注意不是mock怎么搞,而是里面细节需要我们思考。

mock实现原理


Mock是一种在开发过程中模拟数据和行为的技术。其实现原理是通过创建虚拟接口来模拟真实的接口请求和响应,通过预先定义返回结果的数据结构和内容,从而实现在不依赖实际数据源的情况下进行开发和测试。

在具体实现中,可以使用一些工具或框架,比如json-server、YApi等,通过自定义路由、数据模板等方式实现Mock数据的生成和返回。Mock的实现可以帮助开发人员在开发过程中快速迭代和测试,提高开发效率。

后端mock实现原理是通过拦截后端接口的请求,并返回预设的模拟数据,以达到模拟接口数据的目的,从而在开发过程中减少对真实接口的依赖性,提高开发效率。常用的后端mock工具如MockServer、WireMock等,它们可以通过配置预设的规则和响应数据来实现后端接口的模拟。

后端mock框架 TestableMock

原理

通过asm技术改写字节码,将需要改的类、方法重写,我们在调用对应mock类的mock方法的时候,会调到我们改写的方法,从而实现mock功能。

具体源码

SourceClassHandler

image.png

com.alibaba.testable.agent.handler.SourceClassHandler#transformMethod

private void transformMethod(MethodNode mn, Set<MethodInfo> memberInjectMethods,
                             Set<MethodInfo> newOperatorInjectMethods) {
    LogUtil.verbose("   Found method %s", mn.name);
    if (mn.name.startsWith(DOLLAR)) {
        // skip methods e.g. "$jacocoInit"
        return;
    }
    AbstractInsnNode[] instructions = mn.instructions.toArray();
    if (instructions.length == 0) {
        // native method (issue-52)
        return;
    }
    int i = 0;
    do {
        if (invokeOps.contains(instructions[i].getOpcode())) {
            MethodInsnNode node = (MethodInsnNode) instructions[i];
            if (CONSTRUCTOR.equals(node.name)) {
                if (LogUtil.isVerboseEnabled()) {
                    LogUtil.verbose("     Line %d, constructing "%s"", getLineNum(instructions, i),
                            MethodUtil.toJavaMethodDesc(node.owner, node.desc));
                }
                MethodInfo newOperatorInjectMethod = getNewOperatorInjectMethod(newOperatorInjectMethods, node);
                if (newOperatorInjectMethod != null) {
                    // it's a new operation and an inject method for it exist
                    int rangeStart = getConstructorStart(instructions, node.owner, i);
                    if (rangeStart >= 0) {
                        if (rangeStart < i) {
                            handleFrameStackChange(mn, newOperatorInjectMethod, rangeStart, i);
                        }
                        instructions = replaceNewOps(mn, newOperatorInjectMethod, instructions, rangeStart, i);
                        i = rangeStart;
                    }
                }
            } else {
                if (LogUtil.isVerboseEnabled()) {
                    LogUtil.verbose("     Line %d, invoking "%s"", getLineNum(instructions, i),
                            MethodUtil.toJavaMethodDesc(node.owner, node.name, node.desc));
                }
                MethodInfo mockMethod = getMemberInjectMethodName(memberInjectMethods, node);
                if (mockMethod != null) {
                    // it's a member or static method and an inject method for it exist
                    int rangeStart = getMemberMethodStart(instructions, i);
                    if (rangeStart >= 0) {
                        if (rangeStart < i) {
                            handleFrameStackChange(mn, mockMethod, rangeStart, i);
                        }
                        instructions = replaceMemberCallOps(mn, mockMethod,
                                instructions, node.owner, node.getOpcode(), rangeStart, i);
                        i = rangeStart;
                    } else {
                        LogUtil.warn("Potential missed mocking at %s:%s", mn.name, getLineNum(instructions, i));
                    }
                }
            }
        }

        i++;
    } while (i < instructions.length);
}

将新方法替换到旧的方法,当然这里我们就不解读了,读完asm技术可能博主的头发又要掉几根了,哈哈~

怎么避免mock代码提交到远程

目前我公司基本没有写单测,所以存在直接在代码里面mock数据,我记得阿里之前也有要求说写单测,但是实际上业务团队在追求效率情况下,也会采用类似的粗暴的直接mock,我的想法希望能够兼容这种场景。

其实mock不止会写在单元测试,还可能会出现在正式代码,由于业务团队追求效率导致,testable-mock采用agent方式去改写,所以生产环境没有挂载的话不会影响生产环境。

总结


在联调阶段,我们遇到了一些比较棘手的问题:代码排查比较麻烦,部署时间比较长,前端需要等待后端开发完成才能进行调试数据,依赖性比较强等等。

为了解决这些问题,我们建设了联调中心,其中重点介绍了mock service。我们使用yapi实践了这一技术,并且实现了接口文档统一管理和前后端mock功能,达到了前端联调对后端服务依赖减少的效果。同时,我们也讨论了mock代码误提交的问题,提出了拦截rpc、mq自动将数据mock回去的解决方案。这些优化对于提升开发效率、减少依赖性以及保证代码质量都有很大的帮助。