对自己所参与过的事物做个复盘是个好习惯,能让自己对事物有个更深的了解。比如说,读了一本书,那么这个时候你需要回去思考:这本书讲了些啥,对你有何影响;又或者,你参与了一个活动,你完了后,你也可以对其进行复盘,思考下这个活动的意义,这个活动是让你增加见识了还是让你放松心情了等;再或者,你参与了一个项目,这个时候还是可以复盘,考虑项目的进展中的难点等。其实复盘,就是对自己经历过的事物进行思考总结。是一个很好的自我矫正升华的方式。

下面我以一个程序员参与的项目的复盘来简要说明下,复盘过程中可能思考的问题和方向。

抛出问题

  1. 为什么要做这个项目?
  2. 项目是怎么选型的?
  3. 项目的规模(数据量、并发量、增量)?
  4. 项目中遇到了什么难点?是如何解决这些问题的?
  5. 项目还有什么不足?有什么解决方案?

问题解答

这里以之前我做过的一个出单项目为例子简单说一下可能的问题思考总结。


为什么要做这个项目

面对日益增加的互联网合作伙伴对接平安的出单系统,原有的系统已经无法完全满足与日俱增出单压力。对此,针对保险单较为单一的投保,分离业务,做成一个互联网出单系统。快速响应合作伙伴的出单需求.

项目是怎么选型的

SpringCloud: 成熟的为服务体系,JAVA生态,成员已有springboot基础. 公司内部已有基于springcloud的成功案例. MongoDB: 简单保单类型适合嵌套文档形式存储. 分布式(支持分片,可扩展), 高可用(可配置副本) RockMq: 在保证了可靠性的前提下,队列特性足够丰富,可运维程度比较高。其快速横向扩展的能力,也能保证未来几年我们对其性能的要求。RocketMQ基于Java语言开发

RabbitMQ由于采用同步刷盘,其可靠性非常强(采用ACK机制),但是性能却是这三款开源产品中最差的,在实际的测试中也能很轻易的得到验证。所以RabbitMQ更适合使用在对消息可靠性要求很高,但对消息吞吐量不太敏感的场景中。这里额外提一下一个个人经验,我们在对开源产品选型时,建议大家需要了解该产品是基于什么协议/规范实现的,这样大致就清楚了这款开源产品可以解决什么问题,适用于哪些场景。例如消息中间件领域的AMQP、JMS、STOMP协议,工作流领域的bpmn2.0标准。 Kafka是由LinkedIn开发的一个分布式的消息系统,使用Scala编写,它以可水平扩展和高吞吐率而被广泛使用。目前越来越多的开源分布式处理系统如Cloudera、Apache Storm、Spark都支持与Kafka集成。Kafka的高吞吐是其最重要的一个特点,这也带来了一个潜在隐患在异常情况下,其消息丢失率较高。所以它比较适用于将高吞吐作为首要考虑,能容忍少量消息丢失的场景。例如作为大数据平台数据传输的流式管道,其在日志采集场景中也被广泛应用。 RocketMQ的核心架构设计参考了Kafka,有网友甚至提出它是Kafka Java版本的实现。不可否认的是RocketMQ的设计实现的确参考了很多Kafka的设计思想,比如说均采用了基于文件的存储方案、Topic的并行化操作、底层网络协议均基于TCP (RocketMQ使用netty成熟的解决方案,Kafka使用一套自行设计的基于TCP层的协议),但两者之间相对特点还是比较明显,RocketMQ在牺牲了部分性能的前提下,提供了更多的机制来保证消息的可靠性,同时也提供了更为丰富和实用的消息队列特性。比如说消息的查询、消息的回溯。这些接口的提供,极大的便利了消息中间件的运维,提高了在一些异常场景中的故障恢复速度。 最终选择了RocketMQ,理由是RocketMQ在保证了可靠性的前提下,队列特性足够丰富,可运维程度比较高。其快速横向扩展的能力,也能保证未来几年我们对其性能的要求。另外RocketMQ基于Java语言开发,也降低了我们后续对其进行扩展和二次开发的难度。

项目的规模(数据量、并发量、增量等)

1wQPS, 每日单量60w、目前已有6000w单量. 还有比如说项目部署的规模等情况(eg. 服务器多少台,缓存服务,消息中间件等是如何部署的)

项目中遇到了什么难点?是如何解决这些问题的

压测监控查看各方面的系统资源并未达到瓶颈,但是并发量达不到预期

问题解决和排查思路: 老一套方案,首先定位性能瓶颈: CPU/内存/IO等

  1. ps aux | grep appname 查看进程ID
  2. top -H pid查看cpu和内存的使用情况(未发现应用服务器的cpu和内存有较高的使用率) cpu繁忙一般可能的情况如下:
    • 线程中的不合理循环
    • 发生了频繁的full gc
    • 多线程的上下文频繁切换
  3. 通过jmap/jstack命令查看内存/线程信息(可能是线程出现等待导致,所以也需要查看线程信息)
  4. 通过Arthas工具查看接口执行过程中每个方法的耗时情况

    $trace xxx #cost 方法内部调用路径,并输出方法路径上的每个节点上耗时, trace 命令能主动搜索 class-pattern/method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。trace 能方便的帮助你定位和发现因 RT 高而导致的性能问题缺陷,但其每次只能跟踪一级方法的调用链路

    结果如下(包名经过处理):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    +---[0.001162ms] com.xxx.service.dto.ApplyResultDTO:setApplyPolicyNo()
    +---[0.511972ms] com.xxx.service.apply.save.ApplySaveService:fillApplyPolicyNo()
    +---[19.709352ms] com.xxx.service.apply.save.ApplySaveService:saveContractDTOInfo()
    +---[1.313593ms] com.xxx.service.apply.save.ApplySaveService:saveApplyParameterInfo()
    +---[min=4.35E-4ms,max=9.48E-4ms,total=0.002326ms,count=3] java.lang.StringBuilder:<init>()
    +---[min=5.1E-4ms,max=0.00146ms,total=0.004705ms,count=6] java.lang.StringBuilder:append()
    +---[0.001361ms] com.xxx.base.dto.BaseInfoDTO:getTransactionNo()
    +---[min=4.86E-4ms,max=6.61E-4ms,total=0.001777ms,count=3] java.lang.StringBuilder:toString()
    +---[523.544075ms] com.xxx.common.service.redis.RedisService:stringSet()
    +---[9.91E-4ms] com.xxx.common.util.StringUtils:isNotBlank()
    +---[min=6.07E-4ms,max=7.75E-4ms,total=0.001382ms,count=2] java.lang.String:equals()
    +---[min=5.03E-4ms,max=6.95E-4ms,total=0.001198ms,count=2] java.lang.Integer:intValue()
    +---[min=592.763104ms,max=868.853641ms,total=1461.616745ms,count=2] com.xxx.common.service.redis.RedisService:ObjectSet()
  5. 通过trace 定位到是redis耗时后,定位redis问题
    1. 查看redis服务器状态,发现压测时有台服务器的 查看redis服务器状态,发现压测时有台服务器的CPU达到70%左右
    2. slowlog get 命令获取redis慢查询定位到是某个key的读取导致, 到此发现是redis数据的热点问题
  6. 解决方案
    1. 出现该问题是有工具类滥用redis的问题,每次获取数据都从redis获取。首先,调整代码,增加内存缓存。redis缓存刷新时发布事件动态刷新内存缓存; 然后,优化存储的dto,只缓存必要的数据。减小数据量;最后,替换序列化工具,将jackson序列化替换为fastjson.
    2. 流程中部分耗时的IO操作线程异步后台处理

项目还有什么不足?有什么解决方案?

  1. 缺少网关限流

    spring-cloud-zuul-ratelimit、 针对不同合作伙伴分别限流

  2. 使用了较多的内存缓存,这些缓存通常无需改变

    调整jvm参数,如增加老年代比例。减少gc耗时停顿时间

  3. 使用缓存时没考虑并发问题导致的数据不一致性

    目前使用缓存的时候,策略是先删除缓存后更新数据库的,在并发情况下,可能会导致缓存和数据库的数据不一致,该问题需要解决。目前考虑的解决方案有如下几种:

    • 在写数据库操作的时候。利用Redis加锁,使得数据更新完成之后才能读缓存
    • 写数据库后,启动一个延迟定时任务,定时任务负责读取数据库的最新数据更新Redis中的缓存
    • Redis读写请求入队列,根据请求类型决定是否需要阻塞等待,以保证数据的一致性

总结

上面只是给了一个例子,还有更多的思考点等着自己去发掘。再者每个人的经历也不一样,思考方向也许也会有更多的不同,但是最重要的是一个保持思考总结的习惯,该习惯并不是一定要等事情完结后才去考虑,在过程中我们也需要不断的思考总结。 人会在思考总结中进步,如果只是走马观花一般,那就真的像网上传的一样(十年工作经验,其实就是一年的工作经验重复可十年)。希望大家不是活成了段子,而是在不断的进步,看到越来越好的自己。