项目与科研
项目:图像质量评价系统V3
系统详细介绍

系统详细介绍(内容已脱密重写)

本节介绍系统的详细设计,包括系统架构、数据库设计、基础业务逻辑等。

同时还会记录一些系统的优化思路以及技术细节。

系统架构图

项目梳理

难点

事务处理速度严重不对等

web后端处理事务的速度起码可以达到1000TPS,但是单个python服务的TPS顶多0.2TPS(5秒1张图),对此采取三方面优化策略:

  1. 直接提高计算能力:分布式部署与分布式计算:将计算任务分散到多个服务器上

  2. 异步调用流量削峰:异步处理与消息队列:将这些任务放入队列中并在资源允许的情况下异步处理,可以优化资源使用,提高系统的吞吐量

  3. 计算能力弹性伸缩:微服务和自动化部署:使用容器化技术和自动化的资源管理,以快速适应变化的需求

跨语言调用以及项目重用

设计方案迭代:

  • Python服务调用方式

    • V1-将python模型也作为服务注册到nacos,由taskservice直接调用

      缺点:

      • 用户体验不佳:这种调用是同步调用,前端需要长时间等待,平均一个子任务的完成时间是5秒,而主任务一般包含4~5个子任务

      • 违反单一职责,存在重复开发:python后端的设计初衷是单纯提供深度学习评估能力,本身不关心并发、数据存储等情况。如果python模型直接作为一个服务,需要考虑以上情景,不仅破坏单一职责原则,而且增加python开发人员的开发难度。

      • 若使用python的sdk(如nacos-group的nacos-sdk-python)需要格外注意版本等问题,存在不稳定性,增加调试难度

    • V2-不将python注册为服务,而是将任务加入消息队列,由一对一的java消费者从队列取出任务,进而调用python

      优点:

      • 用户体验较好:任务提交后在消息队列中异步处理,期间用户可以进行其他操作

      • 遵守单一职责:python只负责美学评估本身,无需深入的web开发知识,只需使用flask简单地处理http请求即可

  • Feign接口抽取方案迭代

    • V1-为每种评估任务写一个Client

      缺点:

      • 违反开闭原则:增加任务类型后需要修改starter源码

      • 通用性差:client指定的url和endpoint是静态的,不支持在实际场景下更换参数

      • 代码冗余:每个client接口差不多,只是传入参数不同

      • 内存浪费:所有类型的接口都会自动加载到IOC容器中,无论服务有没有使用此接口的需求

    • V2-统一接口,继承后有多种实现,调用时使用工厂模式返回合适的Client

      缺点:

      • 只解决了内存浪费,其他问题依旧

      • 增加了调用难度

    • V3-统一接口,url和endpoint通过配置文件指定

      优点:

      • 遵守开闭原则:增加任务类型、修改client参数都不需要修改源码

      • 提高代码重用性

      • 增加灵活性:服务可以根据需要自行决定是否开启feignclient

任务状态更新策略

正常情况:

  1. 任务提交后初始状态是排队中,并会立即加入消息队列;

  2. 若被正常消费,状态会变为评估中;

  3. 若评估成功,状态会变为已完成;

  4. 若评估失败,状态会变为评估失败。

异常情况:若消息队列宕机,任务状态会一直停留在排队中。

解决方案:

  • 定期检查任务的状态

    如果发现有任务一直处于"排队中"的状态,而且超过了一定的时间(1小时), 那么可以认为这个任务可能是因为消息队列宕机而没有被正常处理。 这种情况下就将这个任务的状态设置为"评估失败"。 如果实际上这个任务还在消息队列中排队,当取到这个消息时, 即使发现任务状态是"评估失败"也没有关系,我们把它当作重试请求即可。

  • 用户手动重试

    用户可以手动重试任务,这样会重新将任务加入消息队列。

遇到的问题

测试环境下数据不一致

由于测试时直接操作数据库,忽略了redis缓存, 导致前端测试业务数据时拿到的是缓存的旧数据, 即数据不一致,排查了很久。

建议测试环境下也不要直接操作数据库,而是编写单独的测试控制台, 采用和生产环境相同的业务流程来测试。

SQL数据库

MySQL docker 启动命令:

docker run -d \
    --name mysql \
    -p 3306:3306 \
    -e MYSQL_ROOT_PASSWORD=1346aaaa \
    -v /home/xr/IAAsystemJava/mysql/data:/var/lib/mysql \
    -v /home/xr/IAAsystemJava/mysql/conf:/etc/mysql/conf.d \
    mysql:8.0

两个数据库:

  • iaa_database:存储本系统主要数据(用本系统建表语句创建)

  • nacos_databse:持久化nacos数据(需要用nacos官方sql语句初始化)

表设计

用户表(可使用系统的用户)、员工表(简单记录某个用户对应的员工信息)、美学评估任务表、美学评估子任务表、任务类型表

  • 用户表(可使用系统的用户)
DROP TABLE IF EXISTS user;
CREATE TABLE user (
  id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  name varchar(32) COLLATE utf8_bin NOT NULL COMMENT '昵称',
  username varchar(32) COLLATE utf8_bin NOT NULL COMMENT '用户名',
  password varchar(64) COLLATE utf8_bin NOT NULL COMMENT '密码',
  employee_number varchar(16) COLLATE utf8_bin NOT NULL COMMENT '此用户的员工工号',
  employee_name varchar(16) COLLATE utf8_bin NOT NULL COMMENT '此用户的员工名字',
  employee_department varchar(16) COLLATE utf8_bin NOT NULL COMMENT '此用户的员工部门',
  status int NOT NULL DEFAULT '0' COMMENT '状态 0:禁用,1:启用',
  permissions int NOT NULL DEFAULT '0' COMMENT '权限 0:普通用户,1:管理员',
  create_user bigint NOT NULL COMMENT '创建人',
  create_time datetime DEFAULT NULL COMMENT '创建时间',
  update_time datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (id)
  UNIQUE KEY uk_username (username)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='用户信息';
  • 员工表(仅用于测试)(简单记录某个用户对应的员工信息)
DROP TABLE IF EXISTS employee;
CREATE TABLE employee (
  id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  employee_number varchar(16) COLLATE utf8_bin NOT NULL COMMENT '工号',  
  employee_name varchar(16) COLLATE utf8_bin NOT NULL COMMENT '名字',
  employee_department varchar(16) COLLATE utf8_bin NOT NULL COMMENT '部门',
  sex varchar(2) COLLATE utf8_bin NOT NULL COMMENT '性别',
  id_number varchar(18) COLLATE utf8_bin NOT NULL COMMENT '身份证号',
  create_time datetime DEFAULT NULL COMMENT '创建时间',
  update_time datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (id),
  UNIQUE KEY uk_employee_number (employee_number)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='员工信息';
  • 美学评估任务表
DROP TABLE IF EXISTS iaa_task;
CREATE TABLE iaa_task (
  id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  number varchar(36) COLLATE utf8_bin NOT NULL COMMENT '任务号',
  name varchar(32) COLLATE utf8_bin NOT NULL COMMENT '任务名',
  description varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '任务描述',
  img_path varchar(255) COLLATE utf8_bin NOT NULL COMMENT '图片路径',
  status int NOT NULL DEFAULT '1' COMMENT '任务状态 1排队中 2评估中 3全部完成 4部分完成(包括评估全部失败)',
  user_id bigint NOT NULL COMMENT '任务提交用户',
  commit_time datetime NOT NULL COMMENT '任务提交时间',
  finish_time datetime DEFAULT NULL COMMENT '任务完成时间',
  create_time datetime DEFAULT NULL COMMENT '创建时间',
  update_time datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (id),
  INDEX idx_user_id (user_id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='美学评估任务表';
  • 美学评估子任务表:多对一:子任务(多)的主任务(一)
DROP TABLE IF EXISTS iaa_subtask;
CREATE TABLE iaa_subtask (
  id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  number varchar(36) COLLATE utf8_bin NOT NULL COMMENT '子任务号',
  main_task_id bigint NOT NULL COMMENT '子任务(多)的主任务(一),逻辑外键',
  status int NOT NULL DEFAULT '1' COMMENT '子任务状态 1排队中 2评估中 3已完成 4评估失败',
  type_id bigint NOT NULL COMMENT '子任务类型,逻辑外键',
  type_name varchar(32) COLLATE utf8_bin NOT NULL COMMENT '子任务类型名(如colorv1)',
  subtask_score DECIMAL(6, 4) NOT NULL DEFAULT 0.0000 COMMENT '子任务评分',
  subtask_result varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '子任务评估结果',
  commit_time datetime NOT NULL COMMENT '子任务提交时间',
  finish_time datetime DEFAULT NULL COMMENT '子任务完成时间',
  create_time datetime DEFAULT NULL COMMENT '创建时间',
  update_time datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (id)
  INDEX idx_main_task_id (main_task_id)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='美学评估子任务表';
  • 任务类型表
DROP TABLE IF EXISTS subtask_type;
CREATE TABLE subtask_type (
  id bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  type_name varchar(32) COLLATE utf8_bin NOT NULL COMMENT '子任务类型名(如光照评估V1)',
  model_name varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '子任务使用的模型(如exposure_release_1.0)',
  description varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '子任务描述',
  create_time datetime DEFAULT NULL COMMENT '创建时间',
  update_time datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='评估任务类型表';

基础业务逻辑

  • 查询每个user的所有task
// 用 user_id 查(覆盖索引)
SELECT id FROM iaa_task WHERE user_id = 1;
 
// 用 用户名 查
SELECT t.*
FROM user AS u
JOIN iaa_task AS t ON u.id = t.user_id
WHERE u.username = 'someusername';
  • 用user_id查询这个用户task的全部详细信息
SELECT t.*, s.*
FROM iaa_task AS t
JOIN iaa_subtask AS s ON t.id = s.main_task_id
WHERE t.user_id = 1;
// task一定有subtask,所以无需LEFT JOIN,效率更高
  • 用task的id查询所有子任务,以及主任务的信息
SELECT t.*, s.*
FROM iaa_task AS t
JOIN iaa_subtask AS s ON t.id = s.main_task_id
WHERE t.id = 1;

SQL优化

  • 表设计优化

    • 字段冗余:把subtask_type表中常用到的字段type_name varchar(32) COLLATE utf8_bin NOT NULL COMMENT '任务类型名(如光照评估V1)',加到iaa_subtask表中,减少多表查询数量
  • 索引优化

    • 通过user_id和main_task_id查询的场景较多,故为其添加索引。

    • 同时一般查询只需要获得task_id,故user_id的索引可以形成覆盖索引。

  • SQL语句优化

    • 不使用select*

    • 用JOIN代替子查询:

      而且我使用的是JOIN(inner join),不用left join right join,因为内连接会对两个表进行优化,以小表为驱动

      SELECT * FROM iaa_subtask WHERE main_task_id IN (
          SELECT id FROM iaa_task WHERE user_id = 1
      );

Redis缓存

Redis docker 启动命令:

docker run -d \
    --name redis \
    -p 6379:6379 \
    redis \
    --requirepass "choco"

缓存作用

评估任务状态变化较频繁,用户查询频率较高。 添加Redis缓存、使用事务注解和Cache Aside Pattern。 用于评估任务状态缓存维护与更新、保持缓存与SQL数据库更新(持久化)的一致性。 有效提高查询速度,减少数据库负载。

缓存维护

  1. 添加的缓存
  • 每个用户第一页的任务信息
  1. 缓存的维护(写更新)由调用python实例的各{subtask}service处理。
  • 处理流程:消息队列监听到消息receiveMessage -> 任务是否已完成,否则调用pyhon实例进行评估assessment -> 拿到评估结果,更新数据库和缓存update

  • 注:上述“判断任务是否已完成”是直接访问数据库的,不涉及缓存,因为{subtask}service是同步调用,而且其处理速度会受到python的限制,单实例约5s/次,对数据库的压力可忽略不计。

@Transactional // 事务注释,保证原子性
public void update(Integer id, String path, IAAResult iaaResult) {
    // 1.更新数据库
    updateById(id, iaaResult);
    // 2.删除缓存
    stringRedisTemplate.delete(CACHE_SUBTASK_KEY + id);
    return;
}
  1. 缓存的维护(读更新)由taskservice维护,如果未命中就查数据库并写入缓存。

Spring后端

Restful接口文档

【重要】前后端之间的所有请求以及返回不允许携带实际的数据库主键, 前端的请求不能试图通过主键id操作数据库,后端的响应不允许包含主键信息。

接口幂等

考虑任务提交接口:

  • 任务提交:[POST] /task

    数据库中任务号是UUID,具有唯一性,在业务层写数据库前先判断

  • 任务重试:[POST] /task/subtask

    子任务数据库中有任务状态标识,在业务层判断,如果不能重试说明此次尝试前可能已经有其他服务提前完成了重试操作,无需再次重试(任务评估具有一致性,同样的图每次评估结果是一样的)

自动配置与starter

  • 主要是Feign接口的抽取、OSS工具类自动配置以及pojo的定义

    • vrobotit-iaa-spring-boot-starter 模块

      只有pom.xml的模块,

      重点是需要引入spring-boot-starter和vrobotit-iaa-spring-boot-autoconfigure:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>cn.vrobotit</groupId>
    <artifactId>vrobotit-iaa-spring-boot-starter</artifactId>
    <version>0.0.5-SNAPSHOT</version>
    <name>vrobotit-iaa-spring-boot-starter</name>
    <description>vrobotit-iaa-spring-boot-starter</description>
    <properties>
        <java.version>11</java.version>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
        <spring-boot.version>2.3.9.RELEASE</spring-boot.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.vrobotit</groupId>
            <artifactId>vrobotit-iaa-spring-boot-autoconfigure</artifactId>
            <version>0.0.5-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>
  • vrobotit-iaa-spring-boot-autoconfigure 模块(包括IAAClient 接口、【废弃】其他Client接口、OSS相关AutoConfiguration+Properties+Utils、jwt和鉴权AutoConfiguration+Properties+Utils、MessageConverter的AutoConfiguration)

    IAAClient 接口以及实现(实际业务场景可能有变化,通过配置文件加载)

package cn.vrobotit.iaa.client;
 
...
 
@FeignClient(name = "${vrobotit.iaa.client.name:disabled}", url = "${vrobotit.iaa.client.url:disabled}")
public interface IAAClient {
    @GetMapping("${vrobotit.iaa.client.endpoint:disabled}")
    IAAResult assessment(@RequestParam("imagePath") String path);
}

OSS的AutoConfiguration以及Properties和实际Utils(可选阿里云或七牛云,通过配置文件vrobotit.iaa.oss.enable属性选择), 这里以阿里云为例:

package cn.vrobotit.iaa;
 
...
 
@Configuration
@EnableConfigurationProperties(AliOSSProperties.class)
public class AliOSSAutoConfiguration {
    @Bean
    @ConditionalOnProperty(name = "vrobotit.iaa.oss.enable",havingValue = "ali")
    public AliOSSUtils aliOSSUtils(AliOSSProperties aliOSSProperties){
        AliOSSUtils aliOSSUtils = new AliOSSUtils();
        aliOSSUtils.setAliOSSProperties(aliOSSProperties);
        return aliOSSUtils;
    }
}
package cn.vrobotit.iaa;
 
...
 
@Data
@ConfigurationProperties(prefix = "vrobotit.iaa.oss.access")
public class AliOSSProperties {
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
}
package cn.vrobotit.iaa;
 
...
 
public class AliOSSUtils {
 
    private AliOSSProperties aliOSSProperties;
 
    public AliOSSProperties getAliOSSProperties() {
        return aliOSSProperties;
    }
 
    public void setAliOSSProperties(AliOSSProperties aliOSSProperties) {
        this.aliOSSProperties = aliOSSProperties;
    }
    
    // 上传文件,返回url
    public String upload(MultipartFile file) throws IOException {
        ...
        return url;
    }
 
}

MQ的MessageConverter的AutoConfiguration

package cn.vrobotit.iaa;
 
...
 
@Configuration
public class MqConverterAutoConfiguration {
    @Bean
    @ConditionalOnProperty(name = "vrobotit.iaa.mqconverter",havingValue = "json")
    public MessageConverter jsonMessageConverter(){
        return new Jackson2JsonMessageConverter(); 
    }
}

spring.factories 文件,定义自动配置文件META-INF/spring.factories

#AutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.vrobotit.iaa.AliOSSAutoConfiguration,\
cn.vrobotit.iaa.MqConverterAutoConfiguration
  • 要使用starter的项目

    在pom中添加:

<dependency>
    <groupId>cn.vrobotit</groupId>
    <artifactId>vrobotit-iaa-spring-boot-starter</artifactId>
    <version>0.0.5-SNAPSHOT</version>
</dependency>

在配置文件中添加:

vrobotit:
  iaa:
    client: # 远程调用信息
      name: colorclient1 # 自定义名字
      url: http://localhost:8181 # 主机路径
      endpoint: /color-assessment # 端点
    mqconverter: json # 使用jsonconverter
    oss: 
      enable: ali # 开启阿里的oss
      access: # 访问信息
        endpoint: xxx
        accessKeyId: xxx
        accessKeySecret: xxx
        bucketName: xxx
      # enable: qiniu # 开启七牛的oss
      # access: # 访问信息
        # accessKey: xxx
        # secretKey: xxx
        # bucketName: xxx
    jwt: 
      signKey: xxx # 签名
      ttl: 30m # 有效期
    auth:
      excludePaths: # 无需鉴权的路径
        - /login      

IAAClient 接口:注入并调用:

// 启动类中:
@EnableFeignClients(clients = {IAAClient.class})
 
// 业务类中:
@Autowired
IAAClient iaaClient;
IAAResult iaaResult = iaaClient.assessment(img_url);

阿里OSS工具类:注入并调用:

@Autowired
AliOSSUtils aliOSSUtils;
String url = aliOSSUtils.upload(file)

微服务

网关-SpringCloudGateway

作用:提供统一的入口点、服务路由、鉴权、负载均衡

  • 路由配置:
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: 10.112.87.236:8848 # nacos地址
    gateway:
      routes: # 网关路由配置
        - id: user-service 
          uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言
            - Path=/user/**,/login/** # 按照路径匹配,只要以/user/开头就符合要求
        - id: upload-service
          uri: lb://uploadservice
          predicates:
            - Path=/upload/**
        - id: task-service 
          uri: lb://taskservice
          predicates:
            - Path=/task/**
  • 鉴权-GlobalFilter:
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
 
    @Autowired
    private JwtTool jwtTool;
    
    @Autowired    
    private AuthProperties authProperties;
    
    private AntPathMatcher antPathMatcher = new AntPathMatcher();
 
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        if(isExclude(request.getPath().toString())){
            return chain.filter(exchange);
        }
        String token = null;
        List<String> headers = request.getHeaders().get("authorization");
        if (headers.size() > 0) {
            token = headers.get(0);
        }
        
        // 校验jwt
        Long userId = null;
        try {
            jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            // 出错说明鉴权失败
            ServerHttpResponse response = exchange.getResponse();
            response.setRawStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
    }
 
    // 判断当前请求路径是否不需要鉴权
    private boolean isExclude(String antPath) {
        for (String pathPattern : authProperties.getExcludePaths()) {
            if(antPathMatcher.match(pathPattern, antPath)){
                return true;
            }
        }
        return false;
    }
 
    @Override
    public int getOrder() {
        return 0;
    }
}

服务注册与发现-Nacos

Nacos-server docker命令:

docker run -d \
    --name nacos \
    -p 8848:8848 \
    -e MODE=standalone \
    -e SPRING_DATASOURCE_PLATFORM=mysql \
    -e MYSQL_SERVICE_HOST=10.112.87.236 \
    -e MYSQL_SERVICE_PORT=3306 \
    -e MYSQL_SERVICE_USER=root \
    -e MYSQL_SERVICE_PASSWORD=1346aaaa \
    -e MYSQL_SERVICE_DB_NAME=nacos_database \
    nacos/nacos-server:1.4.1

作用:将网关、userservice、taskservice和uploadservice到注册中心,以便远程调用、负载均衡

# 网关
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 10.112.87.236:8848 # nacos地址
      
# userservice
spring:
  application:
    name: userservice
  cloud:
    nacos:
      server-addr: 10.112.87.236:8848
      
# taskservice
spring:
  application:
    name: taskservice
  cloud:
    nacos:
      server-addr: 10.112.87.236:8848
      
# uploadservice
# 见配置中心的说明

配置中心-Nacos

作用:存放多服务共享配置(数据库连接信息) 或 可能变动的配置(uploadservice中配置OSS密钥、网关的jwt密钥信息)

例:数据库连接信息

# taskservice的bootstrap.yml:
spring:
  application:
    name: taskservice # 服务名称
  profiles:
    active: dev # 开发环境
  cloud:
    nacos:
      server-addr: 10.112.87.236:8848
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: 
          - dataId: shared-jdbc.yaml

例:uploadservice中配置OSS密钥、上传文件限制

# uploadservice的bootstrap.yml:
spring:
  application:
    name: uploadservice # 服务名称
  profiles:
    active: dev # 开发环境
  cloud:
    nacos:
      server-addr: 10.112.87.236:8848
      config:
        file-extension: yaml # 文件后缀名

注意:这里的配置可以热更新是因为本项目starter中的AliOSSProperties类有 @ConfigurationProperties注解,其他配置想实现热更新可以考虑在@Value 注入的变量所在类上添加注解@RefreshScope

服务调用-Feign

作用:处理各微服务间的远程调用

各微服务调用关系

  • 网关会路由到userservice和uploadservice

  • userservice会调用taskservice

  • {subtask}service会调用相应的python实例

服务部署位置:

  • 目前系统压力最大的地方是python服务, 因此除了python进行了多机部署外,其他服务位于同一实体机

  • 为了保证python服务的高可用,python服务部署在三台机器上

消息队列-SpringAMQP-RabbitMQ

RabbitMQ docker 命令:

docker run \
    -e RABBITMQ_DEFAULT_USER=choco \
    -e RABBITMQ_DEFAULT_PASS=1346 \
    --name mq \
    --hostname mq1 \
    -p 15672:15672 \
    -p 5672:5672 \
    -d \
    rabbitmq:3-management

作用:异步调用python深度学习模型,降低服务之间的耦合度,分离耗时服务

使用路由模式:Direct Exchange

spring:
  rabbitmq:
    host: 10.112.87.236 # 主机名
    port: 5672 # 端口
    virtual-host: / # 虚拟主机 
    username: choco # 用户名
    password: 1346 # 密码
    listener:
      simple:
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
  • 生产者:taskservice
@Autowired
private RabbitTemplate rabbitTemplate;
String queueName = "color.queue";
 
rabbitTemplate.convertAndSend(queueName, message);
 
String exchangeName = "vrobotit.direct";
Map<String, Object> message = new HashMap<>();
message.put("subtaskId", id);
message.put("imagePath", path);
rabbitTemplate.convertAndSend(exchangeName, "color", message);
  • 消费者:{subtask}service,以color为例
@Component
public class MessageListener {
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.colorqueue"),
            exchange = @Exchange(name = "vrobotit.direct", type = ExchangeTypes.DIRECT),
            key = {"color"}
    ))
    public void receiveMessage(Map<String, Object> msg){
        Integer id = (Integer) msg.get("subtaskId");
        String path = (String) msg.get("imagePath");
    }
}

项目部署示例

  • 打包java的dockerfile及其运行命令,以colorservice为例
FROM openjdk:11-jdk
WORKDIR /usr/src/app
COPY ./app.jar /usr/src/app/app.jar
ENTRYPOINT ["java", "-jar", "/usr/src/app/app.jar"]
# 构建
docker build -t colorservice .
# 运行
docker run -d \
    -p 8080:8080 \
    colorservice \
    --vrobotit.iaa.client.name=colorclient1 \
    --vrobotit.iaa.client.url=http://localhost:8181 \
    --vrobotit.iaa.client.endpoint=/color-assessment
  • 打包python的dockerfile及其运行命令
# v1.2 主要是opengl安装
# 使用基础镜像
FROM ubuntu:20.04
# 避免在安装过程中出现交互式对话框
ARG DEBIAN_FRONTEND=noninteractive
# 设置工作目录
WORKDIR ./ImgEvalSys
# 时区设置,避免奇奇怪怪的问题
ENV TZ=Asia/Shanghai
# 复制项目文件到容器中
ADD . /ImgEvalSys
# Info
LABEL version="1.2"
LABEL manager="Shuai He <hs19951021@bupt.edu.cn>"
LABEL maintainer1="Rui Xie <chenke771@bupt.edu.cn>"
LABEL maintainer2="Yi Xiao <@bupt.edu.cn>"
# apt安装opengl和python3.9
RUN apt-get update && apt-get upgrade -y
RUN apt-get install -y libgl1-mesa-glx libgl1-mesa-dri
RUN apt-get install -y python3.9 python3-pip
RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.9 1 && \
update-alternatives --set python /usr/bin/python3.9
RUN apt-get install libglib2.0-0
# # 安装依赖(为避免问题,在docker创建完成后再安装)
# RUN python3.9 -m pip install -r requirements.txt -i http://pypi.douban.com/simple --trusted-host pypi.douban.com
# RUN python3.9 -m pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn
# RUN python3.9 -m pip install -r requirements.txt
# 替换opencv为headless版本
RUN python3.9 -m pip uninstall opencv-python -y
RUN python3.9 -m pip install opencv-python-headless -i http://pypi.douban.com/simple --trusted-host pypi.douban.com
# 补充安装有问题的依赖包
RUN python3.9 -m pip install opencv-contrib-python-headless
RUN python3.9 -m pip install colour-science
# Release版启动命令:直接启动系统
CMD ["python", "./run.py"]
# cd 项目文件夹,构建image
docker build -t imevsys-headless .
 
# 启动container
# 启动,设定挂载目录
sudo docker run --runtime=nvidia --gpus all -v /home/xr/Templates/ImgEvalSysss:/ImgEvalSys -p 9999:9999 -it imevsys-headless
  • 其余service的docker compose文件及其运行命令
version: '3'
services:
  mysql:
    image: mysql:8.0
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: 1346aaaa
    volumes:
      - /home/xr/IAAsystemJava/mysql/data:/var/lib/mysql
      - /home/xr/IAAsystemJava/mysql/conf:/etc/mysql/conf.d
    ports:
      - "3306:3306"
    restart: always
 
  redis:
    image: redis
    container_name: redis
    command: --requirepass "choco"
    ports:
      - "6379:6379"
    restart: always
 
  nacos:
    image: nacos/nacos-server:1.4.1
    container_name: nacos
    environment:
      MODE: standalone
      SPRING_DATASOURCE_PLATFORM: mysql
      MYSQL_SERVICE_HOST: 10.112.87.236
      MYSQL_SERVICE_PORT: 3306
      MYSQL_SERVICE_USER: root
      MYSQL_SERVICE_PASSWORD: 1346aaaa
      MYSQL_SERVICE_DB_NAME: nacos_database
    ports:
      - "8848:8848"
    restart: always
 
  mq:
    image: rabbitmq:3-management
    container_name: mq
    hostname: mq1
    environment:
      RABBITMQ_DEFAULT_USER: choco
      RABBITMQ_DEFAULT_PASS: 1346
    ports:
      - "15672:15672"
      - "5672:5672"
    restart: always
    
  gateway:
    image: gateway
    ports:
      - "10010:10010"
    restart: always
 
  uploadservice1:
    image: uploadservice
    ports:
      - "9090:9090"
    command: ["--server.port=9090"]
    restart: always
    
  uploadservice2:
    image: uploadservice
    ports:
      - "9091:9091"
    command: ["--server.port=9091"]
    restart: always
 
  userservice:
    image: userservice
    ports:
      - "8091:8091"
    restart: always
    
  taskservice:
    image: taskservice
    ports:
      - "8092:8092"
    restart: always
docker compose up -d