转转测试环境治理的高效能实践

news2024/12/27 13:12:27

文章目录

    • 1. 背景及需求
      • 1.1 系统架构的发展
      • 1.2 测试环境的需求
    • 2. 传统的测试环境解决方案-物理隔离
    • 3. 转转测试环境V1-改进的物理隔离
      • 3.1 稳定环境
      • 3.2 动态环境
      • 3.3 优缺点
        • 3.3.1 优点
        • 3.3.2 缺点
    • 4. 转转测试环境V2-基于自动IP标签的流量路由
    • 5. 转转测试环境V3-基于手动标签的流量路由
      • 5.1 docker化
      • 5.2 服务及流量打标
      • 5.3 目标形态
      • 5.4 RPC调用实现
        • 5.4.1 服务注册、发现及调用
        • 5.4.2 标签的传递
      • 5.5 MQ消息实现
        • 5.5.1 消费原理
        • 5.5.2 存在的问题
        • 5.5.3 标签的传递
      • 5.6 进程内标签的传递
        • 5.6.1 通过方法参数传递
        • 5.6.2 通过ThreadLocal传递
        • 5.5.3 通过TransmittableThreadLocal传递
      • 5.6 上线收益
      • 5.7 辅助设施
        • 5.7.1 泛域名解析
        • 5.7.2 web shell
        • 5.7.3 debug插件
      • 5.8 优缺点
        • 5.8.1 优点
        • 5.8.2 缺点
    • 6. 分布式调用跟踪系统
      • 6.1 原理
      • 6.2 架构
      • 6.3 TraceId的获取
      • 6.4 在路由关键节点采集流量标签和当前节点标签
    • 7. 总结

转转测试环境治理历经3个版本的迭代,环境搭建耗时及资源占用大幅度下降,在此过程中积累了丰富的实践经验。本文将从测试环境的需求及背景出发,介绍转转测试环境治理各个版本的原理、技术、优缺点,毫无保留地将转转的实践经验分享给各位读者。

1. 背景及需求

1.1 系统架构的发展

很久很久以前,在并发量较低时系统大多采用单体架构,由单个web服务直接连接数据库,由nginx在多个web服务节点间做负载均衡。

单体架构

随着系统并发量的提高,单体架构无法满足性能需求,需要对单体服务进行拆分,于是来到了微服务架构。微服务架构与单体架构显著的不同在于链路更长更复杂了。

微服务架构

1.2 测试环境的需求

测试环境与线上环境的典型不同在于线上环境各节点一般情况下代码是一致的,各节点是对等的,请求到达任意节点业务逻辑都是一致的;而测试环境一般是多分支并行开发,每个节点的逻辑是不一致的,既然要测试本次所开发的功能,那就要求请求能精准地到达所部署的节点。

在单体架构下,该需求是很容易实现的,通过采用修改nginx配置,在upstream中仅包含目标测试节点可以很容易实现对目标节点的精准测试,或者不使用nginx直接通过IP+port的形式进行调用也同样可以实现请求精准到达目标节点。如下图所示的A’及A’'属于两个不同的需求,分别通过固定nginx upstream及IP+port的形式实现了请求的精准控制。

单体架构测试

而在微服务架构下,该需求实现起来就没那么简单了。如下图所示,链路上包括服务A、B、C,图中A’、B’、C’属于同一个需求,而A’‘、B’‘、C’‘属于另一个需求。采用在单体架构下的解决方案只能控制请求精准地到达A’或者A’‘,无法实现请求精准地到达B’、C’及B’‘、C’'。

微服务架构测试

2. 传统的测试环境解决方案-物理隔离

为了实现请求精准地到达被测服务节点,传统的做法是提供多套完全互相隔离的测试环境,每套测试环境包含全量的服务及注册中心、MQ broker等。每个需求分配一套测试环境,只需将被测服务部署至该环境内即可实现请求精准地到达被测节点,这种做法也称作物理隔离。

物理隔离

物理隔离在公司服务数量较少时(20个服务以下)称得上是完美的解决方案,非常简单可靠。但是随着服务数量的增长,物理隔离的缺点逐渐显露——资源浪费太严重。假设整个系统有100个服务,而每次需求仅修改其中的1-3个服务,资源浪费率高达97%-99%。而且服务数量的增长,往往也意味着公司业务的发展壮大,同时进行中的需求数量也在增长,需要提供更多的测试环境,资源消耗巨大。

3. 转转测试环境V1-改进的物理隔离

转转公司传统的测试环境解决方案属于改进型的物理隔离。

3.1 稳定环境

在转转有一套稳定环境包含全量的服务并且与线上代码一致。在测试环境不使用注册中心,而是为每一个服务分配唯一的域名并通过修改机器host映射的方式手动控制服务发现。默认情况下,如果服务A部署在稳定环境的192.168.1.1上,那么所有测试环境机器的host文件中都存在一项配置192.168.1.1 A.zhuaninc.com

3.2 动态环境

每次需求所申请的环境称为动态环境,为一台kvm虚拟机。假设某动态环境的IP为192.168.2.1,在该动态环境部署服务A时,同时将该机器的host文件中192.168.1.1 A.zhuaninc.com(假设稳定环境服务A的IP为192.168.1.1)映射,修改为127.0.0.1 A.zhuaninc.com

如下图所示的动态环境192.168.4.1,其中A’及E’即为本次需求所修改的服务。

动态环境

初始化过程如下:

  1. 申请动态环境192.168.4.1,申请完成后该机器的host文件中包含全量的服务域名映射,默认映射到对应的稳定环境所在的IP,该例中仅展示了F服务的域名映射为192.168.5.1 F.zhuaninc.com(假设稳定环境服务F的IP为192.168.5.1)。
  2. 部署服务E’,同时将127.0.0.1写入host文件。
  3. 部署服务D,同时将127.0.0.1写入host文件。
  4. 部署服务C,同时将127.0.0.1写入host文件。
  5. 部署服务B,同时将127.0.0.1写入host文件。
  6. 部署服务A’,同时将127.0.0.1写入host文件。
  7. 部署Entry。
  8. 部署nginx,同时将服务A的upstream修改为仅包含127.0.0.1
    动态环境搭建过程

如此以来,就像焊接管道一样,从服务E至nginx倒序焊接出一条没有分叉的管道。测试时只需要通过host映射将被测域名映射到该IP,即可实现请求精准地到达A’及E’,E’以后的链路(从F开始的链路)则使用稳定环境。

对于MQ,则通过在动态环境topic添加IP前缀的方式实现与稳定环境的物理隔离,如下图所示不同的topic在broker上存在着不同的队列,稳定环境的消息和动态环境的消息不能互通。

MQ消息物理隔离

如在测试过程中发现需要修改更多的服务,例如需要修改服务F,则在将F服务部署在该动态环境的同时,需要重启服务E’,因为E’已经与稳定环境的F建了tcp连接,修改F服务的域名映射并不会导致E’与F之间重新建立tcp连接,所以需要重启E’。如F服务的调用方不仅有E’,则都需要重启。

3.3 优缺点

该解决方案近似于物理隔离,但较物理隔离有所改进,改进的地方在于动态环境并没有包含全量的服务,动态环境仅包含了从nginx开始至最后一个被测的服务,而使用稳定环境充当被测链路的尾巴。

3.3.1 优点

  • 隔离性强,近似于物理隔离。
  • 链路简单,流量封闭在同一台机器上。

3.3.2 缺点

  • 需要部署从nginx开始至最后一个被测的服务,存在未修改服务部署在动态环境中的情况,造成资源浪费。
  • 由于部署过程依赖服务的调用关系,导致部署效率低下,极端情况下需要数天调试测试环境。
  • host管理复杂。
  • topic添加IP前缀易出错,代码与环境相关。
  • 单台机器内存有限,链路过长无法满足。

4. 转转测试环境V2-基于自动IP标签的流量路由

随着转转公司业务的飞速发展,服务数量迅速增加,每个动态环境部署的服务数量增至30-60个,搭建测试环境的成本越来越高。为了解决该问题,转转架构部、运维部、工程效率部推出了流量路由解决方案。该解决方案在此前的公众号文章转转测试环境的服务治理实践中已有详细讲解,本文仅做简要介绍。

该版本的流量路由称为基于自动IP标签的流量路由,之所以选择IP为标签是因为基于现有使用习惯发现所有被测服务部署在同一台虚拟机上,IP地址一样,并且IP易获取,用户无感知;“自动”则体现在无需用户对服务及流量进行打标,打标是完全自动化进行的。

基于自动IP标签的流量路由将测试环境的搭建时间从数小时-数天减少至30分钟-1小时,每环境部署的服务数量从30-60个服务下降至个位数,并且完全兼容没有流量路由时的使用习惯。但是仍然存在申请虚拟机耗时长,kvm内存无法扩容的问题。

5. 转转测试环境V3-基于手动标签的流量路由

基于自动IP标签的流量路由解决了转转测试环境存在的大部分问题,使测试环境的搭建时间从数小时-数天下降至30分钟-1小时,但是仍然存在申请环境耗时长,kvm内存无法扩容的问题,本着精益求精,用户至上的原则,我们开发了基于手动标签的流量路由。

5.1 docker化

为了解决申请环境耗时长,kvm内存无法扩容的问题,我们决定将服务部署在docker容器内,无需提前申请资源,理论上无内存限制。但是docker化以后,基于IP为标签的流量路由显然无法工作了,因为不同的服务部署在不同的docker pod内,IP也是不一样的。

5.2 服务及流量打标

此时就需要手动为服务及流量指定标签,我们称为基于手动打标的流量路由。为服务打标是自动化完成的,在环境平台申请环境时指定标签,在该环境内部署服务时,由环境平台自动添加jvm参数-Dtag=xxx。而为流量打标则通过http header进行,在发起请求时添加header tag=xxx。

申请环境

自动添加jvm参数

请求添加http header

并非所有请求都是通过http发起的,有些由进程内部(如定时任务)直接发起的调用则自动携带当前节点的标签。

5.3 目标形态

基于手动打标的流量路由目标形态如下图所示,标签为yyy的动态环境,本次需求修改了服务B及服务D,只只需要在该动态环境内部署B和D,真正实现修改什么就部署什么。

标签路由使用目标

5.4 RPC调用实现

5.4.1 服务注册、发现及调用

以下图服务A调用服务B为例,服务B有3个节点,稳定环境的B,动态环境的B’(标签为yyy)及B’‘(标签为xxx)。B’和B’‘在启动时会将标签参数注册到注册中心,服务A在启动时会从注册中心发现B、B’、B’',同时获取它们的标签参数。

RPC标签路由

以红色的链路为例,流量标签为zzz,A在调用时发现并没有动态环境的服务B拥有标签zzz,于是调用稳定环境的B节点。

橙色链路的标签为xxx,A在调用时发现B’‘的标签为xxx,且为动态环境,则调用B’'。

5.4.2 标签的传递

转转使用的RPC为自研RPC框架,在调用时除了传递请求方法及参数等信息之外,还可以通过attachement功能传递额外的参数,而路由标签就是通过该功能在RPC调用时实现传递。

5.5 MQ消息实现

5.5.1 消费原理

若想实现消息可以在动态环境与稳定环境之间路由,通过不同的的topic前缀实现动态环境和稳定环境的物理隔离显然已经行不通。此时的解决方案为动态环境和稳定环境拥有相同的topic,但是不同的消费group,不同的消费group就对应不同的消费offset。动态环境的group添加${tag}前缀,而稳定环境的group添加test_前缀,添加前缀的过程由MQ客户端自动完成,对用户透明。

如下图所示服务B有稳定环境及动态环境(B’)节点各一个,B’的标签为xxx。图中通过不同的颜色表示不同的标签,其中绿色表示没有标签。

MQ标签路由

B节点在消费时,首先判断是否有和消息中标签对应的消费组注册到broker上,如果有则过滤掉,否则消费。其中消息1、3、5、7的标签和B’匹配,消息2没有标签,而消息4、6的标签所对应的消费者没有注册到broker上,所以B节点消费了消息2、4、6。

B’节点在消费时,只消费消息中标签和自身标签前缀一致的消息,即消息1、3、5。

5.5.2 存在的问题

假如此时B’下线了,我们发现消息7没有被消费者消费,该消息丢了。这是因为稳定环境消费组和动态环境消费组offset不一致导致的,稳定环境消费组offset大于动态环境消费组offset,解决方案就是B’下线时将其与稳定环境offset之间的消息重新投递。重新投递后假如B’又上线了呢,就带来了消息的重复消费,但是我们认为重复消费是没有问题的,因为mq本身的消费语义就是至少一次,而不是仅仅一次重复消费幂等性应该由业务逻辑来保证

另一个问题是批量消费如何解决,mq客户端有批量消费功能,一批消息所携带的标签可能是不一样的。对于这个问题,只需要将消息按标签分组后再执行消费逻辑即可。

5.5.3 标签的传递

转转使用的是RocketMQ,并进行了二次开发,RocketMQ提供了可扩展的header可以用来传递路由标签。

5.6 进程内标签的传递

跨进程的标签传递相对来讲比较容易解决,而进程内标签的传递难度更高。

5.6.1 通过方法参数传递

通过为每一个方法调用添加tag参数可以实现标签的的进程内传递,但是显然这不现实,需要全公司所有的代码配合改动。

5.6.2 通过ThreadLocal传递

jdk内置有ThreadLocalInheritableThreadLocal可以实现标签的隐式传递,但是ThreadLocal无法实现new Thread及跨线程池传递,而InheritableThreadLocal可以实现new Thread传递却仍然无法实现跨线程池传递。虽然可以通过在跨线程池时对CallableRunnable进行包装实现跨线程池传递,但这仍然要求修改现有的业务代码,成本较高。

5.5.3 通过TransmittableThreadLocal传递

TransmittableThreadLocal是阿里巴巴开源的,通过java agent技术实现的可以跨线程/跨线程池传递的ThreadLocal,对用户透明,只需要在jvm启动参数中加入对应的java agent参数即可,最终我们采用了该方案。

TransmittableThreadLocal Agent

5.6 上线收益

下图为转转公司动态环境平均部署服务数量曲线,在2022年5月之前平均每环境约部署7-8个服务,在2022年5月之后标签路由开始推广,至2022年7月每环境约部署3-4个服务。虽然从原理上看基于手动标签的流量路由每环境仅比基于自动IP标签的流量路由仅少部署一个服务(Entry),但是由于kvm无法扩容,所以在自动IP路由时代环境初始化时用户总是会预防性地多选择一些服务,以达到申请更大内存虚拟机的目的,而docker化之后,此种担忧不再存在,所以每环境部署的服务数量下降了约4个,动态环境总内存节省了约65%。

测试环境搭建时间从30分钟-1小时下降至2分钟-5分钟,时间的节省主要来自无需提前申请kvmdocker资源隔离服务启动快无内存扩容担忧初始化服务数量少

每环境部署服数量曲线

5.7 辅助设施

docker化虽然带来了效率的提升,资源占用的下降,但是也引入了一些问题。每次部署IP地址都会变化,导致远程登录及debug的成本增加。在Http Header中添加路由标签增加了测试同学的工作量。为了解决这些问题,我们又开发了对应的辅助设施来提高工作效率。

5.7.1 泛域名解析

使用Http Header传递标签,仍然需要配置host映射将域名映射至测试环境的nginx,如192.168.1.1 app.zhuanzhuan.com,否则dns解析会将app.zhuanzhuan.com解析至线上nginx。

是否可以免去host配置呢,答案是可以的。通过直接使用域名传递标签的形式来实现免去host配置,如app.zhuanzhuan.com直接使用域名传递标签写作app-${tag}.test.zhuanzhuan.com,该域名会直接解析至测试环境nginx。在开发版app中内置了该功能,在app启动时输入环境标签即可实现域名的切换,无需配置host即可开始测试。

泛域名解析

5.7.2 web shell

docker化以后每次部署都是一个新的docker pod,IP地址也随之变化,如果需要登录查看日志,通过xshell等工具则需要在每次部署后使用新的IP重新登录。引入web shell功能只需要在环境平台页面中点击按钮即可通过web shell的方式直接登录,并且登录后的工作目录默认为该服务的日志目录。

web shell

5.7.3 debug插件

同样因为docker化以后IP地址总是变化,测试环境的远程debug也变得更加不方便,每次需要更换新的IP进行连接。为了解决此问题,我们开发了debug插件,该插件会自动获取项目名作为服务名,需要debug时输入环境标签,插件会自动向环境平台发起请求,而环境平台则通过解析jvm参数的方式获取debug端口并向插件返回IP和debug端口,插件在收到IP和debug端口后自动连接。

debug插件

5.8 优缺点

5.8.1 优点

  • 更加节省资源,仅部署X(修改的服务),不需要部署nginx+Entry。
  • 申请环境速度快,秒级完成。
  • 搭建环境速度快,约2-5分钟。
  • 无内存限制。

5.8.2 缺点

  • QA有感知,需要在HTTP Header中添加标签。

6. 分布式调用跟踪系统

以下图为例,在D调用E’时出现问题,E’中未打出相应的业务日志,到底是D没有调用呢,还是流量路由存在问题没有路由到E’呢。此时就需要分布式调用跟踪系统的辅助来排查问题。

6.1 原理

分布式调用跟踪系统的原理就是在链路中每个模块的入口和出口处进行埋点,并将埋点采集起来进行可视化展示。如下左图为调用链路,右图为采集到的埋点。

分布式调用跟踪系统原理

每一条链路有唯一的Id称为TraceId,而每一个埋点称之为span,每个span有唯一的Id称为SpanId。

6.2 架构

转转分布式调用跟踪系统采用自研与开源结合的方式,如下图所示。其中Radar为转转自研分布式调用跟踪系统客户端,可与MQ、SCF(转转RPC框架)、Servlet进行整合,异步批量地将埋点上传到Collector服务,Collector服务再将埋点写入kafka。开源部分则使用zipkin实现,zipkin具备自动从kafka消费埋点并存入DB的能力,而且自带UI界面可供查询。

分布式调用跟踪系统架构图

6.3 TraceId的获取

转转使用统一日志门面slf4j,Radar客户端自动将TraceId、SpanId存入MDCContext中,只需在日志配置文件中加入相应的占位符就可以将TraceId、SpanId打印至日志中,如下图所示。

TraceId打印到日志中

在Entry层每次请求结束后将TraceId以Http Header的形式返回至前端,前端收到响应后可立即获取TraceId进行查询。

网关层将TraceId返回

6.4 在路由关键节点采集流量标签和当前节点标签

如下图所示global.route.context.tag为流量标签,而global.route.instance.tag为当前节点标签,通过对比这两个标签是否匹配即可验证流量路由是否正确,本节开头所提到的问题也就迎刃而解。

流量标签及节点标签

7. 总结

转转测试环境治理共经历3个版本,物理隔离基于自动IP标签的流量路由基于手动标签的流量路由

  • 物理隔离:随着转转业务的发展,服务数量的增多,搭建测试环境极端情况下需要数天的时间,每环境平均部署服务数量高达30-60个
  • 基于自动IP标签的流量路由:每环境平均部署服务数量下降至7-8个,而环境搭建时间也下降至30分钟-1小时
  • 基于手动标签的流量路由:每环境平均部署服务数量进一步下降至3-4个,搭建时间降至2分钟-5分钟

流量路由带来效率提升及资源占用下降的同时,也引入了一些问题,如链路复杂性高,ip地址变化等。为了更好地利用流量路由带来的便利,消除负面影响,就需要各种配套设施的辅助,如分布式调用跟踪、泛域名解析、web shell、自动连接debug插件等。

回头来看,流量路由减少了部署时间,降低了资源消耗,得到了业务线的一致好评。架构、运维与工程效率部门的同学排查问题的数量也大大减少。真真正正做到了降本增效,实实在在好项目。两个版本的流量路由分别获得转转公司优秀项目奖


关于作者

王建新,转转架构部服务治理负责人,主要负责服务治理、RPC框架、分布式调用跟踪、监控系统等。爱技术、爱学习,欢迎联系交流。

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。
关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/140324.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大数据挖掘-伤寒论和金匮要略(COVID-19用药启示录,1.4万字收藏)

来自Toby老师,大数据挖掘-伤寒论和金匮要略 大家好,我是Toby老师,三年来新冠病毒肆虐全球,带来一些列症状,例如发热,恶寒,咳嗽,咽喉痛,腹泻,心脑血管疾病等…

C语言_动态内存管理

目录 1. 为什么存在动态内存管理 2. 动态内存函数介绍 2.1 开辟内存块函数_malloc 2.2 动态内存释放和回收函数_free 2.3 开辟空间初始化元素为0的函数_calloc 2.4 调整动态内存开辟大小的函数_realloc 3. 常见的动态内存错误 3.1 对NULL进行解引用操作 3.2 对动态开辟…

aloam学习笔记(二)

学习aloam框架中前端对于点云部分的预处理和点面特征提取。 这些功能在scanRegistration.cpp部分实现,所以也是对于这个源码的学习。 一、main函数 从main函数开始分析。 首先整个完整的main函数内容: int main(int argc, char **argv) {ros::init(…

RSA、MD5加密解密算法全套解析安装教程

第一部分介绍加密解密算法, 第二部分介绍我小组成功应用的RSA、MD5两种加密解密算法,以及心得体会。 1、加密解密算法介绍 应用的开发中安全很重要,所以信息加密技术显得尤为重要。我们需要对应用中的多项数据进行加密处理,从而来…

(人工智能的数学基础)第一章特征向量与矩阵分析——第三节:特征向量与特征值

参考 3Blue1Brown系列:特征向量和特征值第十章 线性代数之 特征向量与特征值】3Blue1Brown知乎:线性代数的本质10 特征向量和特征值 文章目录一:特征向量与特征值概念引入二:特征向量与特征值概念求解三:特征向量与特…

谷粒学院——第七章、课程分类管理

EasyExcel 介绍 简介 Excel导入导出的应用场景 1、数据导入:减轻录入工作量 2、数据导出:统计信息归档 3、数据传输:异构系统之间数据传输 EasyExcel的特点 Java 领域解析、生成 Excel 比较有名的框架有 Apache poi、jxl 等。但他们都存在一个严重的问题就是非常的耗内存。…

Python数据分析三剑客之Pandas

写在前面的话: 开始之前请确保已经配置好python环境,并安装好第三方库pandas和numpy。 1. pandas库介绍 什么是pandas?pandas是提供高性能易用数据类型和数据分析工具的第三方库。简单讲,pandas主要作用有两个:提供了…

电子学会2020年6月青少年软件编程(图形化)等级考试试卷(二级)答案解析

目录 一、单选题(共25题,每题2分,共50分) 二、判断题(共10题,每题2分,共20分) 三、编程题(共3题,共30分) 青少年软件编程(Scratch&…

谷粒学院——第八章、课程管理

一、课程添加功能 概览 课程添加的步骤 课程相关表的关系 后端实现 1、代码生成器 只修改表名即可,依次填入:“edu_course”, “edu_course_description”, “edu_chapter”, “edu_video” 生成完成后, 删除EduCourseDescriptionContr…

力扣 1801. 积压订单中的订单总数

题目 给你一个二维整数数组 orders ,其中每个 orders[i] [pricei, amounti, orderTypei] 表示有 amounti 笔类型为 orderTypei 、价格为 pricei 的订单。 订单类型 orderTypei 可以分为两种: 0 表示这是一批采购订单 buy 1 表示这是一批销售订单 sel…

学习疑惑:用什么方法进行产品原型设计

对于在互联网行业的各位来讲,应该很清楚原型设计在应用开发中的重要性。它所起到的不仅是沟通的作用,更有体现之效。通过内容和结构展示,以及粗略布局,能够说明用户将如何与产品进行交互,体现开发者及UI设计师的idea&a…

pytorch拼接函数:torch.stack()和torch.cat()详解

在pytorch中,常见的拼接函数主要是两个,分别是:stack()和cat()。 torch.stack()函数的意义:使用stack可以保留两个信息:[1. 序列] 和 [2. 张量矩阵] 信息,属于【扩张再拼接】的函数。 形象的理解&#xff…

谷粒学院——第十二章、Banner轮播图

Banner微服务 配置 Nginx 修改文件:nginx.conf 修改完后,重启 nginx nginx -s reload创建项目和初始化 1、新建模块 service_cms 2、配置文件和启动类 创建配置文件:application.properties # 服务端口 server.port8004# 服务名 spr…

电子招标采购系统源码之从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。

统一供应商门户 便捷动态、呈现丰富 供应商门户具备内外协同的能力,为外部供应商集中推送展示与其相关的所有采购业务信息(历史合作、考察整改,绩效评价等),支持供应商信息的自助维护,实时风险自动提示。…

互联网还会回暖吗?蛮三刀酱的2022年终技术总结

靠近年底的月份,内心深处总会不断提醒你,该写年终总结了。无论是工作上,生活上,亦或是互联网、APP上,无数的年终报告提醒我,一年了也该总结一下自己了,不然这之前的1年是不是白过了呢&#xff1…

Vue组件之间的通信(二)

一、兄弟组件之间的通信 (1)使用Vue的状态管理器Vue:建议在大型项目中使用 (2)使用第三方的模块:mitt(中央数据总线方式),创建一个事件中心,由它来进行事件的监听、触发&#xff0…

第一章算法简介

二分查找引入大O表示法 仅知道算法需要多长时间运行完毕还不够,还需要知道运行时间如何随列表长度增加而增加,这正是大O表示法的用武之地。 大O表示法 大O表示法指出了算法有多块,之所以称作大O,单纯因为前面有个O(funny!)大O表…

Allegro174版本新功能介绍之导入导出菜单栏设置

Allegro174版本新功能介绍之导入导出菜单栏设置 Allegro174版本除了支持锁定菜单栏设置之外,同样还支持导出和导入菜单栏的设置 具体操作如下 首先设置并且排列好菜单栏,类似下图选

基于链表的通信录管理

意义:对于一个通信录来说,要管理联系人的信息,包括编号,姓名,性别,电话。开发其系统主要为了帮助用户提高通讯录有管理效率,节约资源,提高信息的精确度模块:一级菜单内容…

【Go基础】数据类型

文章目录1. 数据类型1.1 基本数据类型1.2 复合数据类型1.3 自定义数据类型2. 数组3. 切片4. 字符串5. 数据类型转换6. Map7. Channel1. 数据类型 1.1 基本数据类型 类型长度(字节)默认值说明bool1falsebyte10uint8,取值范围[0,255]rune40Unicode Code Point, int3…