2024年1月

原创文章,转载请标注。https:https://www.cnblogs.com/boycelee/p/17967590

一、什么是配置中心?

配置中心是集中管理和动态更新应用配置信息的服务,服务能够在不停机的情况下新增或修改配置信息,具有以下关键特点:

(1)
集中管理
。配置中心集中存储服务所需要的各类配置信息;

(2)
动态变更
。应用服务不需要重启就可以从配置中心动态获取到最新数据;

(3)
通知机制
。当服务配置发生变化时,配置中心可以提供通知机制,通知应用程序关心的配置发生变化。

能够提高系统的可维护性、灵活性和实时性。

二、传统配置有什么问题?

传统配置会使用本地静态文件作为存储介质。就存在这几个问题:

(1)
动态修改
。本地静态文件修改时必须重启应用,无法做到动态修改;

(2)
统一管理
。存储格式、存储地点都杂乱无章,无法对配置进行统一规范和约束;

(3)
即时生效
。配置完成后,需要多机器部署完成,修改配置才能够生效。无法做到及时通知、及时生效。

三、配置中心的场景

大体场景有如下这几种:

(1)
系统相关
。如线程池配置信息、缓存大小、连接池大小、熔断/限流阈值等;

(2)
业务相关
。如活动文案、推广活动、积分规则、价格策略等;

(3)
开关相关
。A/B Test、特性开关、推送开关等;

(4)
安全相关
。数据库连接信息、加密秘钥、账号密码等。

四、架构设计

(1)基础模型

(2)详细架构

架构图分为三层,分别是客户端层、网络层以及服务层。其中客户端层包括client模块、portal模块,网络层包括Load Balancer(Nginx)和Mata Server以及Eureka,服务层包括Config Service模块和Admin Service模块。

六、模块介绍

客户端层

Client

  • 客户端负责从Config Service获取应用的配置信息;
  • 监听配置变化。当配置发生更新时,Config Service会通知Client,并出发其进行配置刷新;
  • 通过ip + port的方式远程调用Config Service,以获取配置数据。

Portal

  • 管理平台,提供配置中心的管理功能,包括应用创建、查看、修改、发布以及回滚等功能

网络层

NginxLB

  • Client、Portal通过域名的方式访问MetaServer,Nginx作为负载均衡器;
  • Nginx将请求分发到每个Meta Server服务实例,结合Eureka可以动态地获取到注册中心注册的服务实例(Config Service、Admin Service)列表。

Meta Server

  • Meta Server封装Eureka Client,通过Eureka Client获取Config Service和Admin Service的服务信息,Client与Portal不需要关心注册中心的服务发现问题;
  • Client和Portal通过ip+port的方式访问Client Service 与 Admin Service
  • Meta Server是逻辑概念与Config Service模块一起部署在同一实例中;
  • Meta Service还提供其他注册中心的封装类,其中包括Consul、Nacos、Kubernetes等;

Eureka

  • Eureka是用于服务注册与服务发现的注册中心,Config Sevice与Admin Service会定期向注册中心上报心跳;
  • Eureka与Config Service部署在一起,简化部署和管理。
  • 相对于Zookeeper其部署方式更便捷。

服务端层

Config Service

  • 服务于Client模块;
  • 提供获取配置的接口;
  • 基于长轮询,提供配置更新接口;

Admin Service

  • 服务于Admin模块;
  • 提供配置管理接口;
  • 提供修改、发布配置等接口。

七、思考

1、为什么NginxLB与Eureka一起使用?不使用Eureka是否可行?

(1)
负载均衡(Nginx LB)
。具有高可用和容错的特性,当Apollo配置中心节点出现故障时,负载均衡器可以将流量重新路由到其他可用的节点上,从而实现系统的稳定性。

(2)
服务注册与发现(Eureka)
。Eureka可以帮助配置中心实现动态的服务注册和发现。Config Service动态注册到Eureka中,而Client通过Eureka获取可用节点列表。从而实现动态获取配置中心节点变化。

(3)
综合使用Nginx负载均衡和Eureka注册中心,可以提高配置中心的可用性和容错性
。这种架构允许系统在动态环境中灵活地处理配置中心(Config Service)节点的变化,并且确保客户端(Client)始终能够访问到可用的的配置中心(Config Service)节点。

(4)
不使用Eureka,只使用Nginx负载均衡是可行的
。这种情况下配置中心节点(Config Service)由Nginx进行负载均衡和请求分发,不需要Eureka进行服务注册与服务发现。优点是架构简单,缺点是Nginx虽然可以感知到节点不可用,但其并不具备动态节点管理的能力,当新的节点加入时,Eureka能够及时发现且自动地处理,而Nginx则需要人工干预。

2、Confg Service 、Admin Service以及Portal为什么作为独立应用单独部署?

(1)Confg Service和Admin Service独立部署,是从
功能解耦
上考虑,Confg Service服务于Client端负责处理配置相关逻辑,而Admin Service服务于Portal管理平台,提供接口给Portal进行使用。从产品迭代角度来分析Admin Service因为服务于Protal管理平台其迭代频率会高于Confg Service,分开独立部署能够
提升开发灵活性以及降低发布风险

(2)Admin Service和Portal独立部署,是为了环境隔离时,Protal能够调用不同Service提供的API接口,进行
不同环境配置的统一管理

最后

懂得不多,做得太少。欢迎批评、指正。

背景

最近QA测试一个我开发的一个Web API时,我意识到之前对C#的
default
的理解一直是
想当然
的。具体情况是这样,这个API在某些条件下要返回模型的默认值,写法类似于下面这样

[HttpGet(Name = "GetWeatherForecast")]
public WeatherForecast Get()
{
   return default;
}

实际上,这个API会返回
204 No Content
,而不是想象中的一个空的WeatherForecast。API返回204,说明
default
得到值是
null
,为什么会这样?

正确理解default

查看C#语言规范里的说明,
default
表达式是产生一个
类型
的默认值(
A default value expression produces the default value of a type
),而不是类的默认值(Type和Class都被翻译成类真是不太友好)。 我们知道,C#里引用类型的默认值就是
null
,对此通过查看IL,可以发现给一个引用类型赋默认值,就是通过
ldnull
指令将一个空引用推送到计算堆栈上。

IL_0001: ldnull
IL_0002: stloc.0      // V_0

对于值类型,比如decimal,则是通过
initobj
指令将位于指定地址的基元类型字段初始化0。

IL_0001: ldloca.s     'value'
IL_0003: initobj      [System.Runtime]System.Decimal


newobj
不同,
initobj
不调用构造函数,只用于初始化值类型。引用类型和值类型的默认值都可以认为是常量。

真相大白,现在我们知道为什么上面那种情况API会返回204,还是要多看文档,不能想当然。

应用

在linq里,我们常用的
FirstOrDefault
方法,如果没有找到返回的默认值,内部其实就是返回的default

 private static TSource? TryGetFirst<TSource>(this IEnumerable<TSource> source, out bool found)
 {
     if (source == null)
     {
         ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
     }

     if (source is IPartition<TSource> partition)
     {
         return partition.TryGetFirst(out found);
     }

     if (source is IList<TSource> list)
     {
         if (list.Count > 0)
         {
             found = true;
             return list[0];
         }
     }
     else
     {
         using (IEnumerator<TSource> e = source.GetEnumerator())
         {
             if (e.MoveNext())
             {
                 found = true;
                 return e.Current;
             }
         }
     }

     found = false;
     return default;
 }

如果你已经厌倦用
null
判断是否为空,现在多了一个
default
选项。

if (_settings == default){

}

参考

前言

下单的过程包括订单创建,还有库存的扣减,为提高系统的性能,将库存放在redis扣减,则会涉及到Mysql和redis之间的数据同步,其中,这个过程还涉及到,必须是订单创建成功才进行库存的扣减操作。其次,还涉及到库存的同步,需要保证订单创建成功和redis里的库存都扣减成功,再将库存数据同步到Mysql,为了实现上述这里情况,可以借助RocketMQ的事务型消息来实现。

流程图

流程图如下,这里引入了stocklog,即订单流水表,通过判断stocklog的状态来决定是否commite消息去同步mysql,这里stocklog状态为成功的前提是订单入库和redis库存扣减成功。

image-20240116094823532

对于RocketMQ的事务消息的进一步解释:

在第五步执行成功返回可能因为网络状况卡住,但是stocklog状态已经得到修改

如果返回成功 MQ事务就会commite这条消息

如果没有返回成功 MQ事务会去轮询stocklog有没有被修改

一直轮询发现没有被修改就会回滚这条消息

这条消息Commit后,就会被MQ的消费者消费,对MySQL的实际库存进行更新

需要的SQL表

这里简化一下下单的流程,不涉及用户表,只涉及到库存表,库存流水表,订单表。

order表

CREATE TABLE `order` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `product_id` int(11) DEFAULT NULL COMMENT '产品id',
  `product_num` int(11) DEFAULT NULL COMMENT '产品数量',
  PRIMARY KEY (`id`),
  KEY `product_id_index` (`product_id`) USING BTREE COMMENT '产品id索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

stock表

CREATE TABLE `stock` (
  `id` int(10) NOT NULL AUTO_INCREMENT COMMENT '库存id',
  `product_id` int(11) DEFAULT NULL COMMENT '产品id',
  `product_name` varchar(255) DEFAULT NULL COMMENT '产品名字',
  `stock_num` int(11) DEFAULT NULL COMMENT '产品库存',
  PRIMARY KEY (`id`),
  UNIQUE KEY `product_id_index` (`product_id`) USING BTREE COMMENT '产品Id唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

stock_log表

CREATE TABLE `stock_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '库存id',
  `product_id` int(11) DEFAULT NULL COMMENT '产品id',
  `amount` int(11) DEFAULT NULL COMMENT '库存变化数量',
  `status` int(11) DEFAULT NULL COMMENT '状态0->初始化,1->成功,2->回滚',
  PRIMARY KEY (`id`),
  KEY `product_id_index` (`product_id`) USING BTREE COMMENT '产品id索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

关键代码

OrderController类

@Controller
@RequestMapping("/order")
@RequiredArgsConstructor
@Slf4j
public class OrderController {

    private final OrderService orderService;

    private final StockLogService stockLogService;

    private final DecreaseStockProducer decreaseStockProducer;

    private final StockService stockService;

    private final RedisTemplate redisTemplate;

    @PostMapping(value = "/create/{id}")
    public ResponseEntity<Object> create(@PathVariable("id") Integer productId) {
        // 检查redis是否有库存0的标识
        if (redisTemplate.hasKey("product_stock_invalid_" + productId)) {
            return new ResponseEntity<>("库存不足", HttpStatus.OK);
        }

        // 先创建库存流水 这里默认一次只能扣减数量1的库存
        StockLog stockLog = StockLog.builder()
                .amount(1)
                .productId(productId)
                .status(0)
                .build();
        stockLogService.save(stockLog);

        // 发送事务消息
        try {
            DecreaseStockEvent decreaseStockEvent = DecreaseStockEvent.builder()
                    .productId(productId)
                    .stockLogId(stockLog.getId())
                    .build();
            SendResult sendResult = decreaseStockProducer.sendMessageInTransaction(decreaseStockEvent);
            if (!Objects.equals(sendResult.getSendStatus(), SendStatus.SEND_OK)) {
                log.error("事务消息发送错误,请求参数productId:{}", productId);
            }
        } catch (Exception e) {
            log.error("消息发送错误,请求参数:{}", productId, e);
        }

        return new ResponseEntity<>("created successfully", HttpStatus.OK);
    }

StockStatusCheckerListener类,执行本地事务和检查事务

@Slf4j
@RocketMQTransactionListener
@RequiredArgsConstructor
public class StockStatusCheckerListener implements RocketMQLocalTransactionListener {

    private final OrderService orderService;

    private final StockLogService stockLogService;

    private final TransactionTemplate transactionTemplate;

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object arg) {
        log.info("message: {}, args: {}", message, arg);
        TypeReference<MessageWrapper<DecreaseStockEvent>> typeReference = new TypeReference<MessageWrapper<DecreaseStockEvent>>() {};
        MessageWrapper<DecreaseStockEvent> messageWrapper = JSON.parseObject(new String((byte[]) message.getPayload()), typeReference);
        DecreaseStockEvent decreaseStockEvent = messageWrapper.getMessage();
        log.info("decreaseStockEvent info : {}", decreaseStockEvent);
        try {
            orderService.createOrder(decreaseStockEvent.getProductId(), decreaseStockEvent.getStockLogId());
        } catch (Exception e) {
            log.error("插入订单失败, decreaseStockEvent info : {}", decreaseStockEvent, e);
            // 触发回查
            //设置对应的stockLog为回滚状态
            StockLog stockLog = stockLogService.getOne(new QueryWrapper<StockLog>().eq("id", decreaseStockEvent.getStockLogId()));
            stockLog.setStatus(2);
            stockLogService.updateById(stockLog);
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        return RocketMQLocalTransactionState.COMMIT;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        log.info("message: {}, args: {}", message);
        MessageWrapper<DecreaseStockEvent> messageWrapper = (MessageWrapper) message.getPayload();
        DecreaseStockEvent decreaseStockEvent = messageWrapper.getMessage();
        StockLog stockLog = stockLogService.getOne(new QueryWrapper<StockLog>().eq("id", decreaseStockEvent.getStockLogId()));
        if (stockLog == null) {
            return RocketMQLocalTransactionState.UNKNOWN;
        }
        // 已经被扣减了库存
        if (stockLog.getStatus().intValue() == 1) {
            return RocketMQLocalTransactionState.COMMIT;
            // 初始化状态
        } else if (stockLog.getStatus().intValue() == 0) {
            return RocketMQLocalTransactionState.UNKNOWN;
        }
        return RocketMQLocalTransactionState.ROLLBACK;
    }

}

MQ相关代码,使用模板方法

DecreaseStockProducer,消息生产者,实现了一些指定方法

@Slf4j
@Component
public class DecreaseStockProducer extends AbstractCommonSendProduceTemplate<DecreaseStockEvent> {

    private final ConfigurableEnvironment environment;

    public DecreaseStockProducer(@Autowired RocketMQTemplate rocketMQTemplate, @Autowired ConfigurableEnvironment environment) {
        super(rocketMQTemplate);
        this.environment = environment;
    }

    @Override
    protected BaseSendExtendDTO buildBaseSendExtendParam(DecreaseStockEvent messageSendEvent) {
        return BaseSendExtendDTO.builder()
                .eventName("库存同步到mysql")
                .keys(String.valueOf(messageSendEvent.getProductId()))
                .topic(environment.resolvePlaceholders(StockMQConstant.STOCK_TOPIC_KEY))
                .tag(environment.resolvePlaceholders(StockMQConstant.STOCK_DEREASE_STOCK_TAG_KEY))
                .sentTimeout(2000L)
                .build();
    }

    @Override
    protected Message<?> buildMessage(DecreaseStockEvent messageSendEvent, BaseSendExtendDTO requestParam) {
        String keys = StrUtil.isEmpty(requestParam.getKeys()) ? UUID.randomUUID().toString() : requestParam.getKeys();
        return MessageBuilder
                .withPayload(new MessageWrapper(requestParam.getKeys(), messageSendEvent))
                .setHeader(MessageConst.PROPERTY_KEYS, keys)
                .setHeader(MessageConst.PROPERTY_TAGS, requestParam.getTag())
                .build();
    }
}

AbstractCommonSendProduceTemplate,发送消息的类

@Slf4j
@RequiredArgsConstructor
public abstract class AbstractCommonSendProduceTemplate<T> {

    private final RocketMQTemplate rocketMQTemplate;

    /**
     * 构建消息发送事件基础扩充属性实体
     *
     * @param messageSendEvent 消息发送事件
     * @return 扩充属性实体
     */
    protected abstract BaseSendExtendDTO buildBaseSendExtendParam(T messageSendEvent);

    /**
     * 构建消息基本参数,请求头、Keys...
     *
     * @param messageSendEvent 消息发送事件
     * @param requestParam     扩充属性实体
     * @return 消息基本参数
     */
    protected abstract Message<?> buildMessage(T messageSendEvent, BaseSendExtendDTO requestParam);

   

    /**
     * 事务消息事件通用发送
     *
     * @param messageSendEvent 事务消息发送事件
     * @return 消息发送返回结果
     */
    public SendResult sendMessageInTransaction(T messageSendEvent) {
        BaseSendExtendDTO baseSendExtendDTO = buildBaseSendExtendParam(messageSendEvent);
        SendResult sendResult;
        try {
            StringBuilder destinationBuilder = StrUtil.builder().append(baseSendExtendDTO.getTopic());
            if (StrUtil.isNotBlank(baseSendExtendDTO.getTag())) {
                destinationBuilder.append(":").append(baseSendExtendDTO.getTag());
            }
            sendResult = rocketMQTemplate.sendMessageInTransaction(
                    destinationBuilder.toString(),
                    buildMessage(messageSendEvent, baseSendExtendDTO),
                    null
            );
            log.info("[{}] 消息发送结果:{},消息ID:{},消息Keys:{}", baseSendExtendDTO.getEventName(), sendResult.getSendStatus(), sendResult.getMsgId(), baseSendExtendDTO.getKeys());
        } catch (Throwable ex) {
            log.error("[{}] 消息发送失败,消息体:{}", baseSendExtendDTO.getEventName(), JSON.toJSONString(messageSendEvent), ex);
            throw ex;
        }
        return sendResult;
    }

OrderService的createOrder方法:

@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {

    private final OrderMapper orderMapper;

    private final StockLogMapper stockLogMapper;

    private final RedisTemplate redisTemplate;

    private final TransactionTemplate transactionTemplate;

    private static final String LUA_DECRESE_STOCK_PATH = "lua/decreseStock.lua";

    @Override
    public void createOrder(Integer productId, Integer stockLogId) {

        // 减少Redis里面的库存
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LUA_DECRESE_STOCK_PATH)));
        redisScript.setResultType(Long.class);


        // 执行Lua脚本
        Long redisResult = (Long) redisTemplate.execute(redisScript, Collections.singletonList(String.valueOf(productId)));

        if (redisResult < 1L) {
            throw new RuntimeException("库存售罄");
        }

        // 编程式事务
        transactionTemplate.executeWithoutResult(status -> {
            try {
                // 事务性操作
                Order order = Order.builder()
                        .productId(productId)
                        .productNum(1)
                        .build();
                orderMapper.insert(order);

                // 改stockLog
                StockLog stockLog = stockLogMapper.selectOne(new QueryWrapper<StockLog>().eq("id", stockLogId));
                if (stockLog == null) {
                    throw new RuntimeException("该库存流水不存在");
                }
                stockLog.setStatus(1);
                stockLogMapper.updateById(stockLog);
                // 如果操作成功,不抛出异常,事务将提交
            } catch (Exception e) {
                // 如果操作失败,抛出异常,事务将回滚 并且需要补偿redis的库存
                redisTemplate.opsForValue().increment(String.valueOf(productId));
                status.setRollbackOnly();
            }
        });

    }
}

redis的lua脚本代码如下,这里只会在库存大于0的时候进行扣减,先检查库存,再扣减。如果库存为0,在redis里面setIfAbsent该商品售罄的标识,这样子在controller查询到售罄就直接return

local key = KEYS[1]

-- 检查键是否存在
local exists = redis.call('EXISTS', key)
if exists == 1 then
    -- 键存在,获取值
    local value = redis.call('GET', key)
    if tonumber(value) > 0 then
        -- 如果值大于0,则递减
        redis.call('DECR', key)
        return 1  -- 表示递减成功
    else
        local prefix = "product_stock_invalid_"
        local stock_invalid_tag = prefix .. KEYS[1]
        local exists_tag = redis.call('EXISTS', stock_invalid_tag)
        if exists_tag == 0 then
            -- 键不存在,设置键的值
            redis.call('SET', stock_invalid_tag, "true")
        return 0  -- 表示递减失败,值不大于0
        end
    end
else
    return -1  -- 表示递减失败,键不存在
end

MQ的consumer:

@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(
        topic = StockMQConstant.STOCK_TOPIC_KEY,
        selectorExpression = StockMQConstant.STOCK_DEREASE_STOCK_TAG_KEY,
        consumerGroup = StockMQConstant.STOCK_DEREASE_STOCK_CG_KEY
)
public class DecreaseStockConsumer implements RocketMQListener<MessageWrapper<DecreaseStockEvent>> {

    private final StockService stockService;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void onMessage(MessageWrapper<DecreaseStockEvent> message) {
        DecreaseStockEvent decreaseStockEvent = message.getMessage();
        Integer productId = decreaseStockEvent.getProductId();
        try {
            stockService.decreaseStock(productId);
        } catch (Exception e) {
            log.error("库存同步到mysql失败,productId:{}", productId, e);
            throw e;
        }
    }
}

stockService.decreaseStock()方法如下

    public int decreaseStock(Integer productId) {
        return stockMapper.decreaseStock(productId);
    }

相关的SQL语句

    <update id="decreaseStock">
        UPDATE stock
        SET stock_num = stock_num - 1
        WHERE id = #{id} AND stock_num >= 1
    </update>

消息重复消费问题

我们知道,MQ可能会存在重复消费的问题,包括我在压测的时候,就存在了重复消费,导致MySQL的库存最终比redis库存要少,重复扣减了MySQL的库存,针对这种情况,应该解决幂等性问题。

在前面我们用MessageWrapper来包装消息体的时候,每次new一个MessageWrapper都会生成新的UUID,我们将这UUID存到Redis里面来保证幂等性

/**
 * 消息体包装器
 */
@Data
@Builder
@NoArgsConstructor(force = true)
@AllArgsConstructor
@RequiredArgsConstructor
public final class MessageWrapper<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 消息发送 Keys
     */
    @NonNull
    private String keys;

    /**
     * 消息体
     */
    @NonNull
    private T message;

    /**
     * 唯一标识,用于客户端幂等验证
     */
    private String uuid = UUID.randomUUID().toString();

    /**
     * 消息发送时间
     */
    private Long timestamp = System.currentTimeMillis();
}

修改后的扣减库存方法,先判断redis里面有没有存在已经扣除了库存的标识,有就直接返回

@Service
@RequiredArgsConstructor
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> implements StockService {

    private final StockMapper stockMapper;

    private final RedisTemplate redisTemplate;

    @Override
    public int decreaseStock(Integer productId, String UUID) {
        if(redisTemplate.hasKey("decrease_mark_" + UUID)) {
            return 0;
        }
        redisTemplate.opsForValue().set("decrease_mark_" + UUID, "true", 24, TimeUnit.HOURS);
        return stockMapper.decreaseStock(productId);
    }
}

下面是上述demo的代码地址,修改数据库和mysql地址即可使用

scottyzh/stock-demo: RocketMQ事务消息在订单生成和扣减库存的应用 (github.com)

现象

生产上频繁出现调用异常的信息

查询生产服务器日志

初步评估:查看了这个方法没有性能瓶颈,应该是服务本身出现问题、后来找运维要了一下线上dumpfile文件:内存溢出、猜测可能存在内存泄漏

MAT初步排查

从第一个页面来看,内存到达1.7G出现内存溢出

问题一:为什么1.7G就溢出了,问过刘罕生产环境配置的大小是8G

查看项目启动脚本

查看项目的ymal文件中的配置

查询一些资料表面如果没有配置Xmx和Xms会出现最大使用内存是机器内存的1/4,配置8G实际虚拟机最大使用内存不到2G、结论不谋而合

问题二:内存溢出的根源

查看直方图

从图中可以看出byte数组占比高达1.6G左右、但是看不出来被谁引用

从这里看、短时间出现大量查询费用单的请求每次请求的大小为10M

每个请求里面都有一个近10M的数组

这里出现大量Http11OutputBuffer对象、并且大小10M、查看一下源码

每次请求创建一个Http11OutputBuffer

JDK8默认8K

查看了一下线上配置

结论:

1.由于没有配置Xmx和Xms

2.server.max-http-header-size配置10M过大导致所有服务每次请求最低10M传输

上述两点导致内存溢出

建议:

1.配置Xmx和Xms

2.server.max-http-header-size减小到合理的范围、尽量减小请求头中的数据

完整代码:https://gitee.com/mom925/django-system

之前写的Django配置swagger(https://www.cnblogs.com/moon3496694/p/17657283.html)其实更多还是自己手动的写代码去书写接口文档,我希望它能更加的自动化生成出接口文档,所以我需要自己重写一些函数。
安装所需的包,注册app,注册路由参考之前的即可(https://www.cnblogs.com/moon3496694/p/17657283.html),下面是在之前的基础上做的改进

自定义swagger自动生成的类需要在配置里指定自定义的类

SWAGGER_SETTINGS ={'USE_SESSION_AUTH': False,'SECURITY_DEFINITIONS': {'身份验证': {'type': 'apiKey','in': 'header','name': 'Authorization'}
},
"DEFAULT_AUTO_SCHEMA_CLASS": "utils.swagger.CustomSwaggerAutoSchema",
}
我的swagger.py文件
from django.utils.encoding importsmart_strfrom drf_yasg.errors importSwaggerGenerationErrorfrom drf_yasg.inspectors importSwaggerAutoSchemafrom drf_yasg.utils importmerge_params, get_object_classesfrom rest_framework.parsers importFileUploadParserfrom rest_framework.request importis_form_media_typefrom rest_framework.schemas importAutoSchemafrom rest_framework.utils importformattingfrom Wchime.settings importSWAGGER_SETTINGSdefget_consumes(parser_classes):

parser_classes
=get_object_classes(parser_classes)
parser_classes
= [pc for pc in parser_classes if notissubclass(pc, FileUploadParser)]
media_types
= [parser.media_type for parser in parser_classes or[]]returnmedia_typesdefget_summary(string):if string is notNone:
result
= string.strip().replace(" ", "").split("\n")returnresult[0]classCustomAutoSchema(AutoSchema):defget_description(self, path, method):
view
=self.viewreturn self._get_description_section(view, 'tags', view.get_view_description())classCustomSwaggerAutoSchema(SwaggerAutoSchema):def get_tags(self, operation_keys=None):
tags
=super().get_tags(operation_keys)#print(tags) if "api" in tags andoperation_keys:#`operation_keys` 内容像这样 ['v1', 'prize_join_log', 'create'] tags[0] = operation_keys[SWAGGER_SETTINGS.get('AUTO_SCHEMA_TYPE', 2)]
ca
=CustomAutoSchema()
ca.view
=self.view
tag
= ca.get_description(self.path, 'get') orNoneiftag:#tags.append(tag) tags[0] =tag#print('===', tags) returntagsdefget_summary_and_description(self):
description
= self.overrides.get('operation_description', None)
summary
= self.overrides.get('operation_summary', None)#print(description, summary) if description isNone:
description
= self._sch.get_description(self.path, self.method) or ''description= description.strip().replace('\r', '')if description and (summary isNone):#description from docstring... do summary magic summary, description =self.split_summary_from_description(description)#print('====', summary, description) if summary isNone:
summary
=descriptionreturnsummary, descriptiondefget_consumes_form(self):returnget_consumes(self.get_parser_classes())defadd_manual_parameters(self, parameters):"""重写这个函数,让他能解析json,也可以解析表单"""manual_parameters= self.overrides.get('manual_parameters', None) or[]ifmanual_parameters:
parameters
=[]if any(param.in_ == openapi.IN_BODY for param in manual_parameters): #pragma: no cover raise SwaggerGenerationError("specify the body parameter as a Schema or Serializer in request_body")if any(param.in_ == openapi.IN_FORM for param in manual_parameters): #pragma: no cover has_body_parameter = any(param.in_ == openapi.IN_BODY for param inparameters)if has_body_parameter or not any(is_form_media_type(encoding) for encoding inself.get_consumes_form()):raise SwaggerGenerationError("cannot add form parameters when the request has a request body;" "did you forget to set an appropriate parser class on the view?")if self.method not inself.body_methods:raise SwaggerGenerationError("form parameters can only be applied to" "(" + ','.join(self.body_methods) + ") HTTP methods")returnmerge_params(parameters, manual_parameters)#-------------------------------------------------------------------------------------------------------------- from rest_framework importserializersfrom drf_yasg importopenapifrom rest_framework.relations importPrimaryKeyRelatedFieldfrom rest_framework.fields importChoiceFielddef serializer_to_swagger(ser_model, get_req=False):'''序列化转成openapi的形式''' if ser_model is None and get_req isTrue:return{}, []elif ser_model is None and get_req isFalse:return{}
dit
={}
serializer_field_mapping
={
ChoiceField: openapi.TYPE_INTEGER,
PrimaryKeyRelatedField: openapi.TYPE_INTEGER,
serializers.IntegerField: openapi.TYPE_INTEGER,
serializers.BooleanField: openapi.TYPE_BOOLEAN,
serializers.CharField: openapi.TYPE_STRING,
serializers.DateField: openapi.TYPE_STRING,
serializers.DateTimeField: openapi.TYPE_STRING,
serializers.DecimalField: openapi.TYPE_NUMBER,
serializers.DurationField: openapi.TYPE_STRING,
serializers.EmailField: openapi.TYPE_STRING,
serializers.ModelField: openapi.TYPE_OBJECT,
serializers.FileField: openapi.TYPE_STRING,
serializers.FloatField: openapi.TYPE_NUMBER,
serializers.ImageField: openapi.TYPE_STRING,
serializers.SlugField: openapi.TYPE_STRING,
serializers.TimeField: openapi.TYPE_STRING,
serializers.URLField: openapi.TYPE_STRING,
serializers.UUIDField: openapi.TYPE_STRING,
serializers.IPAddressField: openapi.TYPE_STRING,
serializers.FilePathField: openapi.TYPE_STRING,
}
fields
=ser_model().get_fields()ifget_req:
required
=[]for k, v infields.items():
description
= getattr(v, 'label', '')if isinstance(v, serializers.SerializerMethodField) or getattr(v, 'source'):continue elifisinstance(v, ChoiceField):
description
+= str(dict(getattr(v, 'choices', {})))if getattr(v, 'required', True) is notFalse:
required.append(k)
typ
=serializer_field_mapping.get(type(v), openapi.TYPE_STRING)
dit[k]
= openapi.Schema(description=description, type=typ)returndit, requiredelse:for k, v infields.items():
description
= getattr(v, 'label', '')ifisinstance(v, ChoiceField):
description
+= str(dict(getattr(v, 'choices', {})))elifisinstance(v, serializers.SerializerMethodField):continuetyp=serializer_field_mapping.get(type(v), openapi.TYPE_STRING)
dit[k]
= openapi.Schema(description=description, type=typ)returnditdefserializer_to_req_form_swagger(ser_model, filter_fields):
li
=list()
serializer_field_mapping
={
ChoiceField: openapi.TYPE_INTEGER,
PrimaryKeyRelatedField: openapi.TYPE_INTEGER,
serializers.IntegerField: openapi.TYPE_INTEGER,
serializers.BooleanField: openapi.TYPE_BOOLEAN,
serializers.CharField: openapi.TYPE_STRING,
serializers.DateField: openapi.TYPE_STRING,
serializers.DateTimeField: openapi.TYPE_STRING,
serializers.DecimalField: openapi.TYPE_NUMBER,
serializers.DurationField: openapi.TYPE_STRING,
serializers.EmailField: openapi.TYPE_STRING,
serializers.ModelField: openapi.TYPE_OBJECT,
serializers.FileField: openapi.TYPE_FILE,
serializers.FloatField: openapi.TYPE_NUMBER,
serializers.ImageField: openapi.TYPE_FILE,
serializers.SlugField: openapi.TYPE_STRING,
serializers.TimeField: openapi.TYPE_STRING,
serializers.URLField: openapi.TYPE_STRING,
serializers.UUIDField: openapi.TYPE_STRING,
serializers.IPAddressField: openapi.TYPE_STRING,
serializers.FilePathField: openapi.TYPE_STRING,
}
fields
=ser_model().get_fields()for k, v infields.items():if k infilter_fields:continuedescription= getattr(v, 'label', '')if isinstance(v, serializers.SerializerMethodField) or getattr(v, 'source'):continue elifisinstance(v, ChoiceField):
description
+= str(dict(getattr(v, 'choices', {})))
req
= getattr(v, 'required', True)
typ
=serializer_field_mapping.get(type(v), openapi.TYPE_STRING)
li.append(openapi.Parameter(name
=k, description=description, type=typ, required=req, in_=openapi.IN_FORM))returnliclassViewSwagger(object):

get_req_params
=[]
get_req_body
=None
get_res_data
=None
get_res_examples
= {'json': {}}
get_res_description
= ' 'get_res_code= 200get_tags=None
get_operation_description
=None

post_req_params
=[]
post_req_body
=None
post_res_data
=None
post_res_examples
= {'json': {}}
post_res_description
= ' 'post_res_code= 200post_tags=None
post_operation_description
=None

put_req_params
=[]
put_req_body
=None
put_res_data
=None
put_res_examples
= {'json': {}}
put_res_description
= ' 'put_res_code= 200put_tags=None
put_operation_description
=None

delete_req_params
=[]
delete_req_body
=None
delete_res_data
=None
delete_res_examples
= {'json': {}}
delete_res_description
= ' 'delete_res_code= 200delete_tags=None
delete_operation_description
=None

@classmethod
defreq_serialize_schema(cls, serializer):return serializer_to_swagger(serializer, get_req=True)

@classmethod
defres_serializer_schema(cls, serializer):return serializer_to_swagger(serializer, get_req=False)
@classmethod
def req_serializer_form_schema(cls, serializer, filter_fields=[]):returnserializer_to_req_form_swagger(serializer, filter_fields)
@classmethod
defget(cls):

ret
={'manual_parameters': cls.get_req_params,'request_body': cls.get_req_body,'responses': {cls.get_res_code: openapi.Response(description=cls.get_res_description, schema=cls.get_res_data, examples=cls.get_res_examples)} if cls.get_res_data elseNone
}
returnret

@classmethod
defpost(cls):
ret
={'manual_parameters': cls.post_req_params,'request_body': cls.post_req_body,'responses': {
cls.post_res_code: openapi.Response(description
=cls.post_res_description, schema=cls.post_res_data,
examples
=cls.post_res_examples)} if cls.post_res_data elseNone
}
returnret

@classmethod
defput(cls):
ret
={'manual_parameters': cls.put_req_params,'request_body': cls.put_req_body,'responses': {
cls.put_res_code: openapi.Response(description
=cls.put_res_description, schema=cls.put_res_data,
examples
=cls.put_res_examples)} if cls.put_res_data elseNone
}
returnret

@classmethod
defdelete(cls):
ret
={'manual_parameters': cls.delete_req_params,'request_body': cls.delete_req_body,'responses': {
cls.delete_res_code: openapi.Response(description
=cls.delete_res_description, schema=cls.delete_res_data,
examples
=cls.delete_res_examples)} if cls.delete_res_data elseNone
}
return ret

首先重写了get_tags方法,我希望只要在视图类下面注释里写上tags:"xxxx"即可自动的读取到。
上面写的CustomAutoSchema类就是读取了视图类的注释,然后获取出里面的tags值
只需要这样写:


然后即可生成:

得到了都在 测试图片标签下




重写get_summary_and_description方法,原来的这个方法获取到summary是有可能为空的,所以改成当summary为None时summary=description
如果需要在视图类注释中写这两个描述,则像下面一样:


也可以在方法注释中写,则像下面一样:


得到的结果一样:


注意如果两个地方都写则里面的注释会覆盖外层的,也就是方法中的注释会去覆盖视图类下面的注释



重写add_manual_parameters方法,原来的自动生成时只能解析一种数据类型,当传入多种解析类型时会默认的是JSON类型(因为rest_framework就是默认解析JSON)
因为在rest_framework中我们不管是表单还是json格式都可以request.data获取,像新增时是提交表单,批量删除时提交json格式,但是一般又写在同一个视图类下
所以给视图类指定解析数据类型 parser_classes = [MultiPartParser, JSONParser]
重写以后,存在两种都有的会返回表单格式先
视图类像下面一样:

得到的post和delete:

得到了post的表达数据和delete的JSON数据