前言
难点非常难写,这个是可以预见的,我不想去编一些莫须有的东西,意味着纯靠我个人的见识来写这一篇项目难点。我个人的见识有限,之前也提到过我的工作环境,相对于可创造可发掘的亮点来说,难点更难遇到。问题肯定是遇到不少,但能被称作难点,并且有一定的说头,那肯定是少之又少。本文我承诺会根据我个人的工作经历持续更新,现在的话,我先按照我浅薄的见识来为大家提供一些参考案例和我个人对难点的想法。本文不会像上篇亮点一样会有清晰的思路,基本是想到哪写到哪,望读者见谅,当然我不会摆烂,会尽力去完善本篇内容。
破局思维
难点肯定不是凭空而来,和项目亮点的思路一致,难点也是绑定项目的,所以这肯定是个千人千面的问题。如果读者大大本身接触过高大上的项目,那么我相信项目难点就不是问题,反之就相当困难。没有环境如何创造环境,学习前人的优秀经验肯定比自己去开拓更加简单。说起来这难点吧,有时候真的难以判断什么是难点,我其实觉得大部分人和我的困惑是一样的,什么才叫难点?有时候我遇到一个麻烦的问题,和同事探讨、向技术群群友求助,甚至是知识付费,但是解决的时候会发现问题其实很小或者很细节。这算难点吗?确实在那个时间点难住我了,但是真的值得一说吧,也不尽然,因此项目难点最难的地方往往是最开始的地方,发现什么才是真正的项目难点。
就在写这篇文章的时候,不信邪的我再次开始搜索项目难点相关的文章,博客知乎公众号B站等都看了,除了一两篇还凑合,其他就是复制粘贴加口水话,放弃!学习前人经验这条路走的不太通顺,还是得靠自己,我来说说我的看法吧。难点肯定必须是技术含量的,有点东西的,别是一个一听就简单的把你难住了,那得是多菜,这样还不如说没有难点。
说笑的,当然不能说没有难点,那多牛啊,Java带师。说难点也代表你有正常工作过,而且有自己的思考,不是搬砖党。所以这个难点一定要是比较牛的,能一定程度体现你技术水平的问题。业务上面的问题就别说了,大多是人的问题,业务定制化代码无论多么深刻,终究是不能成为难点,倒是可以成为亮点,所以还是往技术和设计上靠靠。接下来还是按照上篇项目亮点如何挖掘项目中的亮点(多方向带案例)的格式,举例说明,用真实的案例征服大家。
当然也是有套路的,在具体描述中,我会按照这样的架子来阐述,总共五步,记牢了兄弟们。
- 难点是怎么产生的,如何发现,具体的技术环境是什么样的?
- 遇到难点时,我是如何思考,发现真正的问题?
- 剖析完问题后,我是如何解决问题的?
- 解决过程中,我走过的弯路,遇到的困难?
- 回顾问题,总结与思考
项目难点
接下来开始举例,有简单有困难,有沙雕有噩梦,款式多多,任君挑选。对了,SQL优化这个我就不说了,之前亮点聊过了,照着套就行了,再说就水文了。
日志丢失
Filebeat+Kafka+数据处理服务+Elasticsearch+Kibana+Skywalking日志收集系统
之前我不是做了一个日志收集系统嘛,其实最后做下来,感觉还挺顺利的。基本上没有遇到特别大的阻碍,不过我觉得也很正常,这种优秀的中间件,就是不会频繁出现一些恶性问题。我在开发的时候,基本上是一个节点一个节点的突破,所以别看我这设计的链路这么长,实际上我在解决问题的时候,能第一时间定位到是哪里出的问题。设计完成后,其实大部分都是配置的问题,然后数据处理服务比较特殊,这个我花费的时间长了点,因为相当于我去做了一个自定义的LogStash,有问题是肯定的,大部分的问题也是出在这个上面。小问题我基本都不用说了,配置上的这个属于经验问题,没有太大难度,就说说数据丢失问题吧。
这是在我日志收集系统上线一两周小规模试用的时候出现的问题,当时是有同事在早上使用的时候反馈的,发现没有当天的日志。我整个日志收集的技术栈是Filebeat+Kafka+数据处理服务+Elasticsearch+Kibana+Skywalking,出现日志丢失每个环节可能都有问题。
我第一时间上Kibana看了ES三个节点是否挂的多了,导致集群不可用,因为我之前有一次出问题就是因为OOM导致ES多个节点挂掉,从而整个集群不可用,那会儿解决方案很粗暴,加配置。检查了下ES三个节点都是正常的,然后我去服务器上看了Filebeat的采集日志,正常输出没有问题。紧接着就看了下Kafka的监控,有没有消息堆积,因为之前有一次就是数据处理服务挂了无法消费,导致了消息堆积,也是临时扩容消费者组来快速消费解决问题。发现中间件都没啥问题,自然问题就定位到了数据处理服务这块。
数据处理服务我首先检查了日志,发现没有报错,一切正常。但问题往往是看起来越正常越麻烦,所以看不出来问题,就只能debug开始测试问题。本地打开项目,然后修改测试一个服务的Filebeat的配置,指定发送开发环境的Topic,然后我本地消费这个Topic。一测试就发现问题了,消费日志有,但是并没有往ES插入数据的日志,也就是说真的是丢数据了。然后我就开始debug,一步步推进,然后就发现确实有异常, 我是用ES的批量异步API进行写入的,但是我并没有处理异步后的响应结果,所以批量插入失败但是同时提交了偏移量造成了数据丢失。处理的方法也很简单,改成同步插入,同时手动提交Kafka的偏移量即可。
除此之外还需要对链路上的中间件进行兜底,FlieBeat会记录当前读取的位置并且可以设置断档读取策略,保证了一定的高可用。Kafka的高可用,考虑三部分,生产者、节点、消费者。生产者有重试和重试间隔两个参数来控制消息发送的重试,ack参数来控制副本数据写入。Kafka节点,主要是从topic入手,多节点集群的Kafka,我们可以多副本保证高可用。消费者连着Elasticsearch的写入就一块说,Elasticsearch无论是宕机还是写入失败,都将写入改成同步,Kafka消费者手动提交偏移量,即使造成消息积压也不能产生数据丢失的问题。当然,产生的消息堆积问题可以采用临时扩容消费者组来解决。
其实这个要说难点还是挺麻烦的,跟中间件相关的东西,本身就很优秀了,毕竟不优秀人家也不会开源拿出来用。你要说难点其实就是跟场景还有应用实践相关的,说不到很高深的原理上面。举个栗子,我现在写Flink相关的代码,经常卡在一些煞笔问题上面,解决完了之后再看,好像也没啥,就是当时卡壳了,或者确实不知道这块知识点。你说这种就是纯纯知识点或者应用的,怎么写成难点,这都是难点,人家只会觉得黔驴技穷,丢大人了。所以还是得要有难度的,大家不常见的,有自己完善独立见解的,哪怕是稍微简单点的东西,能说出来一套体系,也是难度的体现。
线上问题排查
线上问题很多,这里主要是引入相对来说比较简单的,也是常见的JVM问题排查,例如OOM、CPU百分百和GC问题。大部分的线上问题其实最麻烦的在于定位,当然解决也挺麻烦,最好有完善的服务监控。没有的话,熟练应用JDK工具或者Arthas工具也行。单纯说OOM和CPU百分百的,现在来看确实有点简单了,后面会追加GC和堆外内存泄露的案例。
线上问题有很多种,我遇到的比较多的是OOM和CPU百分百。针对JVM的优化,一般设置堆内存最大最小值为同一个固定值,防止GC后堆内存抖动。如果遇到GC问题,首选也是直接替换别的垃圾回收器,比如G1,如果还是有问题,就根据实际情况,调整新生代老年代占比、年龄等参数。常规的Java启动参数,除了以上提到的优化,最好加上OOM时自动生成内存快照(Java Heap Dump)的参数(-XX:+HeapDumpOnOutOfMemoryError),该参数不影响程序运行,且运行时没有任何开销。
拿到内存快照(hprof)文件后,可以选用jvisualvm(Jdk8之后不自带,需要到Github上下载)、JProfiler和IDEA的Profiler(旗舰版才有)打开文件,三者的操作逻辑都是类似的。如果是常规的堆内存溢出(java.lang.OutOfMemoryError: Java heap space)问题,可以在快照文件解析后,看最大对象,查询其传入引用得到其堆栈信息。从堆栈信息中获取问题代码位置,从而定位解决问题。
CPU百分百则是另一套解决思路,首先善用jstack、监控和Arthas等工具。通过Prometheus+Grafana一类的监控软件可以实时观察CPU占用情况,配合告警和二开快速打印堆栈信息,能帮助开发人员快速定位问题。一般常规的定位思路是使用JDK自带的jstack工具,在服务器比如Linux,通过Top -o %CPU获取占用最多的Java应用PID。接着使用命令jstack pid > thread.txt,打印当前Java应用的堆栈信息。然后命令top -Hp pid,观察此pid进程中所有线程的CPU占用。找到CPU占用最多的线程pid,通过命令printf '%x\n' pid得到转换为16进制的nid。最后在jstack获得的文件thread.txt中,找到nid对应的线程堆栈信息,找到对应代码块即可。
当然除了笨拙的jstack,还有arthas可以直接用,使用thread系列命令,可以通过thread n得到top n的线程堆栈信息,更快也更简单。
这个难点还是有点欠说服力,后续追加有难度的吧,这次先这样,等更新吧。我觉得大部分的线上问题主要还是难在定位,其次才是解决。我现在写Flink真是觉得寸步难行,因为太黑箱了,稍微出点问题,就要面临搜不到相关资料,也没有同类问题的困境。难一点的问题还好,可能有别人遇到过,简单的一些配置问题真是难以排查,很让人头大。
新技术的学习和应用
其实我觉得难点也可以是新技术的学习和应用,比如一个大型的中间件,让你独立去做一套监控,一套日志收集或者一个新的技术方向。很多人都没做过的事情,那肯定就会有别人想不到的难点,你觉得很简单,那就分享你学习和快速应用的方法论或者解决问题的思路。
我之前有自己私下搭建过一套日志收集系统,后来被架构组收编纳入了基础架构服务。今年配合团队的技术升级,让我和leader去搭建一套大数据基础设施,短期目标是做一个小数仓。参考了市面上的大数据架构,选用了Flink作为基础数据处理方案,Tidb作为数据存储方案,Hadoop平台作为分布式计算的底座。对我来说,难点是什么,难点有很多!首先是如何从头学起Flink,我个人的一个方法论是通过教学视频培养兴趣,在学习过程中辅以优秀博客及技术文档查漏补缺,最后快速上手实践,边实践边学习。
我接到的第一个需求是实时数仓,目标是将Oracle和MySQL这些数据库的原始数据传输到TiDB分布式数据库中,MySQL是通过Flink-cdc传输到TiDB,基本按照官网配置即可。Oracle是通过OGG推送行变更数据到Kafka,Flink监听Kafka数据,然后将OGG数据转换成Sql语句,发送到TiDB执行。整个开发过程中最难的有三个问题,数据顺序、数据倾斜和实时速度。
数据顺序问题是通过对比原表和TiDB结果数据发现很明显的数据覆盖和丢失问题,定位后发现是监听得来的数据混合很多表的数据。Flink多并行度Sink的情况下,会根据slot数和负载情况动态均匀的分配任务。正常情况下这肯定是一个提高并发的好方法,但是因为我业务的特殊性,需要数据顺序执行,这就成了多线程开发的经典有序性问题。为了解决这个问题,首先对流数据进行keyBy处理,按照表名进行分组,从而确保同一张表的数据都输出到同一个slot里,因为OGG的数据顺序推送,Kafka的Topic是单分区,所以保证了整个链路的顺序性。
解决完了顺序性的问题后,紧接着通过FlinkWebUI能明显观测到Sink端出现了严重的数据倾斜,也就是用过对表HASH后,高频次的表扎堆到某些taskSlot中,造成了严重的背压。因此我需要对Flink的keyBy做一点小小的改造,找到Flink源码中计算slot位置的源码,粘贴下来对我指定的参数进行替换,再根据替换后的数据去keyBy就能将数据流向指定的Slot了。
以上问题处理后还会有背压情况发生,Sink端的压力很大,原因是OGG推送数据是ROW记录模式,也就是简单的一条批量语句会拆分为全量数据行推送过来。我这原本是单行转SQL,但遇到这样的批量语句会造成严重的性能问题。因此如何转换为批量SQL成了重点,我最后是选择了固定时间窗口开窗来分批处理数据。这里需要注意两个问题。一是批量的顺序问题,我的解决方案是相同类型(增删改)的SQL进行攒批,遇到不同类型立刻转换之前的批数据成为SQL,发送到TIDB执行,最后将新的数据保留重新开始攒批。二是如何转换为高效的批量操作语句,更新语句没办法,拿不到原始条件,只能根据唯一值修改,攒批后推送到TIDB执行。新增语句转换为批量新增语句,删除语句则根据主键批量删除语句。
其实以上几个难题相对来说并没有特别困难,但是我们谁又能保证学习新技术的时候没有问题呢。每个人都是小白过来的,我们需要展现出解决问题的能力,这也是项目难点真正想要考察的重点。
写在最后
最近每天加班到十一点,现在是十点半,博主的精神状态很糟糕,本篇落笔于一两个月前。第一个案例和前半段是当时文思泉涌之下写的,非常顺,后半段是加班时间断断续续写的,不知道有没有很好地表达出我的本意。我之前有写过项目亮点,发布时间是2022-11-01,后续追加了四次更新,最后一次更新是在2022-03-05。当时说下一篇是项目难点,哈哈,一晃快一年了,真是岁月如梭啊。现在想想,博主当时的一些目标也没有达成,比较遗憾,最近在找补。本篇文章并不算完美,质量也比不过亮点那篇,不过总算是开了个头,全网这样的应该和亮点一样是独一份,哈哈。本篇后续也会持续追加案例,希望看到文章的读者快快乐乐,身体健康!
重申一遍我的人生信条,我要让这痛苦压抑的世界绽放幸福快乐之花,向美好的世界献上祝福!!!最后的最后,我想告诉自己,要改掉随意点评的坏毛病,多一些鼓励和夸奖,让世界更美好!