• 基于 nacos/灰度发布 实现减少本地启动微服务数量的实践


    一、背景

    后台框架是基于 spring cloud 的微服务体系, 当开发同学在自己电脑上进行开发工作时, 比如开发订单模块, 除了需要启动订单模块外, 还需要启动网关模块、权限校验模块、公共服务模块等依赖模块, 非常消耗开发同学的本地电脑的资源, 也及其浪费时间.

    二、解决方案

    2.1 目标和关键问题

    能不能开发同学本地只需要启动需要开发的模块:订单模块, 其他模块均适用测试环境中正在运行的服务.

    既然要实现的目标有了, 我们就开始研究可行性和关键问题

    1. 开发环境和测试环境要在同一个 nacos 的 namespace 中, 这样才有可能让开发环境调用到测试环境的服务.
    2. 测试环境只能调用测试环境的微服务, 实现和开发环境的服务隔离
    3. 开发同学之间的微服务也要实现服务隔离

    2.2 思路

    既要在同一个 namespace 下, 又要能够实现不同人访问不同的副本, 很容易想到可以利用灰度发布来实现:

    1. 测试环境设置 metadata lemes-env=product 来标识测试环境副本, 用于区分开发环境的微服务测测试环境的微服务
    2. 开发同学本地启动注册开发环境副本, 都会携带唯一IP, 则我们可以通过IP来区分不同开发同学的副本

    假设我们需要开发的 API 的后台服务调用链条如下:

    我们需要开发的 API 为 /addMo, 打算写在 Order 这个微服务里面, 并且他会调用 common 这个微服务的 /getDict 获取一个字典数据, /getDict 是现成的, 不需要开发, 如果是之前的情况, 开发本地至少需要启动5个微服务才能进行调试.

    三、具体实现

    3.1 测试环境设置 metadata

    由于测试环境都是通过容器部署的, 那么启动方式就是下面容器中的 CMD, 我们在其中加入 -Dspring.cloud.nacos.discovery.metadata.lemes-env=product, 用于区分开发环境的微服务测测试环境的微服务

    1. # 说明:Dockerfile 过程分为两部分。第一次用来解压 jar 包,并不会在目标镜像内产生 history/layer。第二部分将解压内容分 layer 拷贝到目标镜像内
    2. # 目的:更新镜像时,只需要传输代码部分,依赖没有变动则不更新,节省发包时的网络传输量
    3. # 原理:在第二部分中,每次 copy 就会在目标镜像内产生一层 layer,将依赖和代码分开,
    4. # 绝大部分更新都不会动到依赖,所以只需更新代码几十k左右的代码层即可
    5. FROM 10.176.66.20:5000/library/amazoncorretto:11.0.11 as builder
    6. WORKDIR /build
    7. ARG ARTIFACT_ID
    8. COPY target/${ARTIFACT_ID}.jar app.jar
    9. RUN java -Djarmode=layertools -jar app.jar extract && rm app.jar
    10. FROM 10.176.66.20:5000/library/amazoncorretto:11.0.11
    11. LABEL maintainer="yangyj13@lenovo.com"
    12. WORKDIR /data
    13. ARG ARTIFACT_ID
    14. ENV ARTIFACT_ID ${ARTIFACT_ID}
    15. # 依赖
    16. COPY --from=builder /build/dependencies/ ./
    17. COPY --from=builder /build/snapshot-dependencies/ ./
    18. COPY --from=builder /build/spring-boot-loader/ ./
    19. # 应用代码
    20. COPY --from=builder /build/application/ ./
    21. # 容器运行时启动命令
    22. CMD echo "NACOS_ADDR: ${NACOS_ADDR}"; \
    23. echo "JAVA_OPTS: ${JAVA_OPTS}"; \
    24. echo "TZ: ${TZ}"; \
    25. echo "ARTIFACT_ID: ${ARTIFACT_ID}"; \
    26. # 去除了 server 的应用名
    27. REAL_APP_NAME=${ARTIFACT_ID//-server/}; \
    28. echo "REAL_APP_NAME: ${REAL_APP_NAME}"; \
    29. # 获取当前时间
    30. now=`date +%F+%T+%Z`; \
    31. # java 启动命令
    32. java $JAVA_OPTS \
    33. -Dtingyun.app_name=${REAL_APP_NAME}-${TINGYUN_SUFFIX} \
    34. -Dspring.cloud.nacos.discovery.metadata.lemes-env=product \
    35. -Dspring.cloud.nacos.discovery.metadata.startup-time=${now} \
    36. -Dspring.cloud.nacos.discovery.server-addr=${NACOS_ADDR} \
    37. -Dspring.cloud.nacos.discovery.group=${NACOS_GROUP} \
    38. -Dspring.cloud.nacos.config.namespace=${NACOS_NAMESPACE} \
    39. -Dspring.cloud.nacos.discovery.namespace=${NACOS_NAMESPACE} \
    40. -Dspring.cloud.nacos.discovery.ip=${HOST_IP} \
    41. org.springframework.boot.loader.JarLauncher

    3.2 开发前端传递开启智能连接

    1. const devIp = getLocalIP('10.')
    2. module.exports = {
    3. devServer: {
    4. proxy: {
    5. '/lemes-api': {
    6. target: 'http://10.176.66.58/lemes-api',
    7. ws: true,
    8. pathRewrite: {
    9. '^/lemes-api': '/'
    10. },
    11. headers: {
    12. 'dev-ip': devIp,
    13. 'dev-sc': 'true'
    14. }
    15. }
    16. }
    17. },
    18. }
    19. // 获取本机 IP
    20. function getLocalIP(prefix) {
    21. const excludeNets = ['docker', 'cni', 'flannel', 'vi', 've']
    22. const os = require('os')
    23. const osType = os.type() // 系统类型
    24. const netInfo = os.networkInterfaces() // 网络信息
    25. const ipList = []
    26. if (prefix) {
    27. for (const netInfoKey in netInfo) {
    28. if (excludeNets.filter(item => netInfoKey.startsWith(item)).length === 0) {
    29. for (let i = 0; i < netInfo[netInfoKey].length; i++) {
    30. const net = netInfo[netInfoKey][i]
    31. if (net.family === 'IPv4' && net.address.startsWith(prefix)) {
    32. ipList.push(net.address)
    33. }
    34. }
    35. }
    36. }
    37. }
    38. if (ipList.length === 0) {
    39. if (osType === 'Windows_NT') {
    40. for (const dev in netInfo) {
    41. // win7的网络信息中显示为本地连接,win10显示为以太网
    42. if (dev === '本地连接' || dev === '以太网') {
    43. for (let j = 0; j < netInfo[dev].length; j++) {
    44. if (netInfo[dev][j].family === 'IPv4') {
    45. ipList.push(netInfo[dev][j].address)
    46. }
    47. }
    48. }
    49. }
    50. } else if (osType === 'Linux') {
    51. ipList.push(netInfo.eth0[0].address)
    52. } else if (osType === 'Darwin') {
    53. ipList.push(netInfo.en0[0].address)
    54. }
    55. }
    56. console.log('识别到的网卡信息', JSON.stringify(ipList))
    57. return ipList.length > 0 ? ipList[0] : ''
    58. }

    3.3 后端灰度处理

    不论是 gateway 还是 openfeign 都是通过 spring 的 loadbalancer 进行应用选择的, 那我们通过实现或者继承 ReactorServiceInstanceLoadBalancer 来重写选择的过程.

    1. @Log4j2
    2. public class LemesLoadBalancer implements ReactorServiceInstanceLoadBalancer{
    3. @Autowired
    4. private NacosDiscoveryProperties nacosDiscoveryProperties;
    5. final AtomicInteger position;
    6. // loadbalancer 提供的访问当前服务的名称
    7. final String serviceId;
    8. // loadbalancer 提供的访问的服务列表
    9. ObjectProvider serviceInstanceListSupplierProvider;
    10. public LemesLoadBalancer(ObjectProvider serviceInstanceListSupplierProvider, String serviceId) {
    11. this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(1000));
    12. }
    13. public LemesLoadBalancer(ObjectProvider serviceInstanceListSupplierProvider,
    14. String serviceId, int seedPosition) {
    15. this.serviceId = serviceId;
    16. this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    17. this.position = new AtomicInteger(seedPosition);
    18. }
    19. @Override
    20. public Mono> choose(Request request) {
    21. ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
    22. .getIfAvailable(NoopServiceInstanceListSupplier::new);
    23. RequestDataContext context = (RequestDataContext) request.getContext();
    24. RequestData clientRequest = context.getClientRequest();
    25. return supplier.get(request).next()
    26. .map(serviceInstances -> processInstanceResponse(clientRequest,supplier, serviceInstances));
    27. }
    28. private Response processInstanceResponse(RequestData clientRequest,ServiceInstanceListSupplier supplier,
    29. List serviceInstances) {
    30. Response serviceInstanceResponse = getInstanceResponse(clientRequest,serviceInstances);
    31. if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
    32. ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
    33. }
    34. return serviceInstanceResponse;
    35. }
    36. private Response getInstanceResponse(RequestData clientRequest, List instances) {
    37. if (instances.isEmpty()) {
    38. if (log.isWarnEnabled()) {
    39. log.warn("No servers available for service: " + serviceId);
    40. }
    41. return new EmptyResponse();
    42. }
    43. int pos = Math.abs(this.position.incrementAndGet());
    44. // 筛选后的服务列表
    45. List filteredInstances;
    46. String devSmartConnect = clientRequest.getHeaders().getFirst(CommonConstants.DEV_SMART_CONNECT);
    47. if (StrUtil.equals(devSmartConnect, "true")) {
    48. String devIp = clientRequest.getHeaders().getFirst(CommonConstants.DEV_IP);
    49. // devIp 为空,为异常情况不处理,返回空实例集合
    50. if (StrUtil.isBlank(devIp)) {
    51. log.warn("devIp is NULL,No servers available for service: " + serviceId);
    52. return new EmptyResponse();
    53. }
    54. // 智能连接: 如果本地启动了服务,则优先访问本地服务,如果本地没有启动,则访问测试环境服务
    55. // 优先调用本地自有服务
    56. filteredInstances = instances.stream().filter(item -> StrUtil.equals(devIp, item.getHost())).collect(Collectors.toList());
    57. // 如果本地服务没有开启,则调用生产/测试服务
    58. if (CollUtil.isEmpty(filteredInstances)) {
    59. filteredInstances = instances.stream()
    60. .filter(item -> StrUtil.equals(CommonConstants.LEMES_ENV_PRODUCT, item.getMetadata().get("lemes-env")))
    61. .collect(Collectors.toList());
    62. // 解决开发环境无法访问 k8s 集群内 ip 的问题
    63. String oneNacosIp = nacosDiscoveryProperties.getServerAddr().split(",")[0].replaceAll(":[\\s\\S]*", "");
    64. filteredInstances.forEach(item -> {
    65. NacosServiceInstance instance = (NacosServiceInstance) item;
    66. // cloud 以 80 端口启动,认为是 k8s 内的应用
    67. if (instance.getPort() == 80) {
    68. instance.setHost(oneNacosIp);
    69. instance.setPort(Integer.parseInt(item.getMetadata().get("port")));
    70. }
    71. });
    72. }
    73. } else {
    74. // 不是智能访问,则只访问一个环境
    75. // 当前服务 ip
    76. String currentIp = nacosDiscoveryProperties.getIp();
    77. String lemesEnv = nacosDiscoveryProperties.getMetadata().get("lemes-env");
    78. filteredInstances = instances.stream()
    79. .filter(item -> StrUtil.equals(lemesEnv, CommonConstants.LEMES_ENV_PRODUCT)
    80. // 访问测试环境
    81. ? StrUtil.equals(CommonConstants.LEMES_ENV_PRODUCT, item.getMetadata().get("lemes-env"))
    82. // 访问开发环境
    83. : StrUtil.equals(currentIp, item.getHost()))
    84. .collect(Collectors.toList());
    85. }
    86. if (filteredInstances.isEmpty()) {
    87. log.warn("No oneself servers and beta servers available for service: " + serviceId + ", use other instances");
    88. // 找不到自己注册IP对应的服务和测试服务,则用nacos中其它的服务
    89. filteredInstances = instances;
    90. }
    91. //最终的返回的 serviceInstance
    92. ServiceInstance instance = filteredInstances.get(pos % filteredInstances.size());
    93. return new DefaultResponse(instance);
    94. }
    95. }
  • 相关阅读:
    令人拍手叫绝的运维小技巧
    ARM---CAN2.0B读取 汽车BMS报文
    UGUI学习笔记(十一)自制优化版滚动视图
    Nginx
    Netty 系列之编解码器和 handler 的调用机制
    知识分享|分段函数线性化及matlab测试
    CesiumJS 2022^ 原理[3] 渲染原理之从 Entity 看 DataSource 架构 - 生成 Primitive 的过程
    Studio One 6 for Mac v6.5.1激活破解版(音乐制作工具)
    kubesphere中间件部署
    Redis Java整合
  • 原文地址:https://blog.csdn.net/LBWNB_Java/article/details/126189799