金庸经典《射雕英雄传》里,黄蓉为了让洪七公交自己和靖哥哥武功,天天对师傅美食相待,在做了“玉笛谁家听落梅”这样一些世间珍品之后,告诉师傅说今天要做的是"炒白菜"。洪七公露出非常欣赏的眼光,说:“好,我倒要看看你怎样化腐朽为神奇。”上周五听了一个我们内部的深度学习讲座,基本这方面处于初始探索阶段。上周六去3w咖啡听了百度的人工智能讲座,他们的深度学习也只限于对代码的训练。想一想代码这个东西分支相对来说还是有限的,所以现在的各种集成开发软件已经很简化程序员的工作了,所以看百度做的基于AI的效果还是有点杀鸡用牛刀。我们部门不涉及大数据,云计算,人工智能,深度学习这些听起来高大上的业务和技术,但是扎扎实实做好自己要比用这些包装好的TensorFlow啥的对底层的理解要深入的多。特别是在长期做一个业务的过程中,系统一些潜在的问题会在大脑中不断的旋转,一些新知识的摄取很快就能够产生对现有业务的改造想法。好了:Talking is cheap, show me the code.
我现在主要在做两件事,一件事是媒资接口的并发量上不去,我已经跟领导说好了:给我时间,我会搞定的。我脑子里的方案有A,B,C,D,E。但是这个业务相当重要,实际上做的时候虽然觉得这个架构太老太不合理,也只能先从JVM调优,dubbo参数调优,缓存参数调优这些做起,看看不改动架构的前提下能改善多少。然后再从一些局部的性能消耗点入手,从局部到整体一点点来。上周因为并发量上去之后,CPU使用率过高,jstat看到minor gc相当频繁,并发量上去之后竟然达到每秒4,5次。就先增加了新生代的容量,效果有,但是很小。用jmap看到频繁的对象是系统的本地缓存,这个是定时任务每次新数据覆盖的,会有相当多的垃圾回收。而且每次服务层启动,先运行大量本地缓存,很耗时,服务已经暴露,但是实际还不具备处理能力,结果经常会启动时出现dubbo线程池满,一段时间后自行恢复。为了解决本地缓存的问题,我想采用缓存数据存于redis,用canel订阅mysql的更新,启动时只是取一下redis的值,采用redis的哈希结构,可以直接反序列化成java的hashmap,很快。然后监听redis更新,不用定时跑。这样就涉及到一个问题:线上没有此业务的redis集群,本地缓存的字典值很多,究竟性能怎样,需要测试对比。
好在我还有另外一件事情要做:离线服务,之前做的时候也比较仓促,虽然细节处我做了很多的优化,处处体现java功底,但是跟人家讲,我讲不出来到底这个有什么闪光点,太零碎了,人家听了就一个反应:不就是一个后台服务嘛。确实从大的架构层做的就不像一个架构师,仅仅增量上做了一个负载分摊,全量只是简单的主备。像全量这种既消耗时间又消耗资源的,怎么能从一开始不做分布式计算呢。于是最近做了一版改造,解决分布式计算,横向扩展问题。采用redis作为中间通信工具和字典存储工具,正好和媒资接口的字典数据是一样的,这样就可以用这个项目来进行线上本地缓存性能的测试,而不影响最重要的媒资接口服务。本来也想用搜索中间件来存储数据,解耦数据库,因为我最终肯定是要做自己的搜索中间件的。但是确实,对于项目来说是可用可不用的东西,还增加维护成本,那就不应该用。想做自己找时间做去。
上面是整个系统的架构图。其中包括了对接搜索部门直到面向用户终端的整个流程,里面用到了自己做的离线数据框架epiphany。放在我的github:https://github.com/xiexiaojing/epiphany。可以通过maven管理下载,pom配置如下(如需引用请注意版本更新):
<dependency> <groupId>com.brmayi</groupId> <artifactId>epiphany</artifactId> <version>0.7</version> </dependency>
框架核心思想:
将离线数据服务划分为全量服务,增量服务和手动处理服务三部分。全量和增量采用redis作为作业调度和管理机制。在redis宕机时各个服务独立运行,产生相同的输出,结果集是在正常情况下的n倍,n为服务器单元。其中全量服务因为原型是在我们项目的离线服务基础上进行开发,数据量大,文件压缩后是几十G的数据量级,所以数据存于磁盘。每个服务通过redis获取将处理数据的区间,各自处理。服务器的磁盘采用async同步处理结果。为了高可用,采用的是分步计算,结果冗余。获取方可以将其中一个磁盘作为主磁盘作为hadoop的节点或者采用linux的async同步,或者ftp,nfs等手段拉取数据。增量服务可以采用消息队列等手段进行数据传递,如果消息多,消息体大,可以用消息传递更新的id,内容可存于磁盘,中间数据库,缓存等,让调用方来进行拉取。手动处理服务直接采用netty处理客户端的http请求。整个框架运行不需任何外部容器。直接用jvm运行main方法。容错可根据需要采用简单主备或者failover=roundrobin。
框架使用方法:
整个架构体系已经在框架内部处理,业务方只需实现DataService接口,将数据传入框架,然后按照自己的需求启动服务即可。DataService接口定义如下:
package com.brmayi.epiphany.service; import java.util.List; import com.brmayi.epiphany.exception.EpiphanyException; /** * * 通用文件处理类:这是业务代码的核心类 * * .==. .==. * //'^\ //^'\ * // ^^(\__/)/^ ^^\ * //^ ^^ ^/6 6 ^^^ \ * //^ ^^ ^/( .. )^ ^^ \ * // ^^ ^/|v""v|/^^ ^ \ * // ^^// / '~~' /^ ^\ * ---------------------------------------- * HERE BE DRAGONS WHICH CAN CREATE MIRACLE * * @author 静儿(987489055@qq.com) * */ public interface DataService { /** * 根据ID进行业务数据处理 * @param dealIds 处理ID * @param path 要保存到的磁盘路径,不需要保存磁盘,可以为null * @throws EpiphanyException 抛出通用异常 */ public void dealDataByIds(List<Long> dealIds, String path) throws EpiphanyException; /** * 根据时间区间获取id列表 * @param beginTime 开始时间 * @param endTime 结束时间 * @return id列表 * @throws EpiphanyException 抛出通用异常 */ public List<Long> getIds(String beginTime, String endTime) throws EpiphanyException; /** * 根据开始结束ID处理数据 * @param beginId 开始ID * @param endId 结束ID * @param path 要保存到的磁盘路径,不需要保存磁盘,可以为null * @throws EpiphanyException 抛出通用异常 */ public void dealDataByBeginEnd(long beginId, long endId, String path) throws EpiphanyException; /** * 取得最大ID * @return 最大ID * @throws EpiphanyException 抛出通用异常 */ public long getMaxId() throws EpiphanyException; /** * 取得最小ID * @return 最小ID * @throws EpiphanyException 抛出通用异常 */ public long getMinId() throws EpiphanyException; }
深入技术细节:
☆ 关于压缩:压缩是递归操作,如果java栈设置很大,压缩操作会非常消耗CPU。所以框架设计时,业务方可设置全量的线程数,但是压缩是异步用另外的线程池来管理,这个线程池的容量是全量线程数的一半。比如我们线上用的是24核高配物理机,现在上面有多个服务进行复用。我的离线服务是视频和专辑两个部分,有数据通用的逻辑,但是是独立的业务,所以我用一个工程来进行项目管理,但是用的是两个独立进程,采用两个脚本分开部署。千万级数据,每个业务全量都使用10个线程。在改造前的那一版采用的是专辑400个线程,视频660个线程,用50个线程的线程池来跑。测试发现改造后的10个线程速度并不比改造前差多少。原因是追加操作和文件大小关系不是很大,开销要小于新建文件的开销。线程少减少了资源开销和上下文切换。还有就是压缩操作,大文件的压缩效率要高很多。因为用的是哈夫曼系的gz压缩,减少了头文件的字符映射。
☆ Redis的哈希结构:这个结构看起来是对java的hashmap的很好的对应。但是实际使用的时候,如果map的key(对应于redis哈希中的field)大于1000,插入效率急剧下降。因为redis是单线程的IO,而一个map对应的redis的key是一个,所有这些写操作会被映射到一个redis节点,效率很低。我试图将一个3w7k的字典map放入redis。结果运行了近一个小时,插入了20402条后再也插不进去了,连接超时,运行几次都没能插入更多。
☆ 巧用对象池:我在框架中封装了有限制的对象和无限制的对象池来作为线程池进行一些异步调用。无限制的对象池是因为对象的总数在其他地方有限制。而有限制的对象池是为了防止对象在异常时过多资源占用。而异步有点地方是为了提高效率,有些地方又是必须的。比如我在程序中一个方法调用mysql取数据,而这个方法处理完数据后还要给MQ发消息,消息体特别大,发送时间特别长。长时间mysql不断开,就会连接超时异常。
一点感悟:
一个人的智商决定了学习的速度和领悟能力。而对情商决定了在一条路上能走多远。对一个项目的热爱可以深入到对用到的每条sql都对其性能做深入的研究。而对于整个项目的架构更可以深入到linux的内核方面。所以足够用心就会掌握更多的技术。而写一个自己的框架会对国内的框架有一个更好的理解和容忍度。比如我在写框架的时候用到的默认值和建议值都是基于我自己的项目。因为这个框架在我们内部很多的离线项目都可以用,我在考虑他们的具体环境怎样设置更加合适。但是再远一点,别人用的时候,怎么设置合理,性能曲线我还在研究中。像dubbo这种开源框架也没能在这方面给出一个特别好的文档。
周末轻松一下:
周末在家开电脑,儿子在旁边千万不要打开数据库。否则他的小手在键盘上划一下,你就会见识到什么叫真正的噩梦。
儿子特别黏我,我总想找借口把儿子推给他爸。昨晚他有粘着我的时候,我说:跟你爸下象棋去。儿子找了半天,蓝棋子少两个,所以他想在手机上玩。我说:将红棋子那两个也拿走就可以公平的玩了嘛。他爹平静的说:恩,没有車和將随便下。 当场笑的肚子疼。
我要写文,他爹带着儿子去外面玩。临走很温柔的说:你手机快没电了,记得充。我一下子就感动了,好细心,暖男。再一想,前面他还说过让我在家订好烤串,5点半送到他好回来吃!其实我要表达的意思是:人家之所以担心我手机没电,只是怕他的烤串送来联系不到我/哭笑