Hi,我是阿昌
,今天学习记录关于开发避免踩坑注意点
的学习记录。
在并发工具这提到的 CopyOnWriteArrayList
。如果你仅仅认为 CopyOnWriteArrayList 是 ArrayList 的线程安全版本,在不知晓原理之前把它用于大量写操作
的场景,那么很可能会遇到性能问题。
JDK 或各种框架随着时间的推移会不断推出各种特殊类,用于极致化各种细化场景下的程序性能。
在使用这些类之前,我们需要认清楚这些类的由来,以及要解决的问题,在确认自己的场景符合的情况下再去使用。而且,越普适的工具类通常用起来越简单,越高级的类用起来越复杂,也更容易踩坑。
比如,代码加锁提到的,锁工具类 StampedLock 就比 ReentrantLock 或者 synchronized 的用法复杂得多,很容易踩坑。
通常情况下,偏底层的框架趋向于提供更多细节的配置,尽可能让使用者根据自己的需求来进行不同的配置,而较少考虑最佳实践的问题;而高层次的框架,则会更多地考虑怎么方便开发者开箱即用。
比如,在HTTP 请求,我们谈到 Apache HttpClient
的并发数限制问题。如果你使用 Spring Cloud Feign 搭配 HttpClient,就不会遇到单域名默认 2 个并发连接的问题。因为,Spring Cloud Feign 已经把这个参数设置为了 50,足够应对一般场景了。
比如,我们使用的 Tomcat 服务器、序列化框架等,就是黑客关注的安全突破口。
我们需要及时关注这些组件和框架的稳定大版本和补丁,并及时更新升级,以避免组件和框架本身的性能问题或安全问题带来的大坑。
流行框架最大的好处是成熟,在经过大量用户的使用打磨后,你能想到、能遇到的所有问题几乎别人都遇到了,框架中也有了解决方案。
很多时候我们会以“轻量级”为由来造轮子,但其实很多复杂的框架,一开始也是轻量的。
只不过是,这些框架经过各种迭代解决了各种问题,做了很多可扩展性预留之后,才变得越来越复杂,而并不一定是框架本身的设计臃肿。
如果我们自己去开发框架的话,很可能会踩一些别人已经踩过的坑。
比如,直接使用 JDK NIO
来开发网络程序或网络框架的话,我们可能会遇到 epoll 的 selector 空轮询 Bug,最终导致 CPU 100%。
而 Netty
规避了这些问题,因此使用 Netty 开发 NIO 网络程序,不但简单而且可以少踩很多坑。
比如,在OOM,我提到的配置超大 server.max-http-header-size
参数导致的 OOM 问题,可能就是来自网络的解决方案。
网络上别人给出的解决方案,可能只是适合“自己”,不一定适合所有人。
并且,各种框架迭代很频繁,今天有效的解决方案,明天可能就无效了;
今天有效的参数配置,新版本可能就不再建议使用甚至失效了。因此,只有知其所以然,才能从根本上避免踩坑。
比如,搜索 Java 8 的一些介绍,你可以看到有些资料提到了在 Java 8 中 Files.lines
方法进行文件读取更高效,但是 Demo 代码并没使用 try-with-resources 来释放资源。
在文件 IO中,这么做会导致文件句柄无法释放。
其实,网上的各种资料,本来就是大家自己学习分享的经验和心得,不一定都是对的。
另外,这些资料给出的都是 Demo,演示的是某个类在某方面的功能,不一定会面面俱到地考虑到资源释放、并发等问题。
因此,对于系统学习某个组件或框架,我最推荐的还是 JDK 或者三方库的官方文档
。
这些文档基本不会出现错误的示例,一般也会提到使用的最佳实践,以及最需要注意的点。
如果你开发的是一个偏底层的服务或框架,有非常多的受众和分支流程,那么单元测试(或者是自动化测试)就是必须的。
人工测试一般针对主流程和改动点,只有单元测试才可以确保任何一次改动不会影响现有服务的每一个细节点。
此外,许多坑都涉及线程安全、资源使用,这些问题只有在高并发的情况下才会产生。
没有经过性能测试的代码,只能认为是完成了功能,还不能确保健壮性、可扩展性和可靠性。
人都会犯错,而且任何一个人的知识都有盲区。
因此,项目的设计如果能提前有专家组进行评审
,每一段代码都能有至少三个人进行代码审核,就可以极大地减少犯错的可能性。
比如,对于熟悉 IO 的开发者来说,他肯定知道文件的读写需要基于缓冲区。
如果他看到另一个同事提交的代码,是以单字节的方式来读写文件,就可以提前发现代码的性能问题。
又比如,一些比较老的资料仍然提倡使用MD5 摘要来保存密码。
但是,现在 MD5 已经不安全了。如果项目设计已经由公司内安全经验丰富的架构师和安全专家评审过,就可以提前避免安全疏漏。
其实,我们犯很多低级错误时,并不是自己不知道,而是因为疏忽。
就好像是,即使我们知道可能存在这 100 个坑,但如果让我们一条一条地确认所有代码是否有这些坑,我们也很难办到。
但是,如果我们可以把规则明确的坑使用工具来检测
,就可以避免大量的低级错误。
比如,使用 YYYY 进行日期格式化的坑、使用 == 进行判等的坑、List.subList原 List 和子 List 相互影响的坑等,都可以通过阿里 P3C 代码规约扫描插件发现。
我也建议你为 IDE 安装这个插件。此外,我还建议在 CI 流程中集成Sonarqube代码静态扫描平台,对需要构建发布的代码进行全面的代码质量扫描。
诸如内存泄露、文件句柄不释放、线程泄露等消耗型问题,往往都是量变积累成为质变,最后才会造成进程崩溃。
如果一开始我们就可以对应用程序的内存使用、文件句柄使用、IO 使用量、网络带宽、TCP 连接、线程数等各种指标进行监控,并且基于合理阈值设置报警,那么可能就能在事故的婴儿阶段及时发现问题、解决问题。
此外,在遇到报警的时候,我们不能凭经验想当然地认为这些问题都是已知的,对报警置之不理。我们要牢记,所有报警都需要处理和记录。