记一次线上内存溢出

今天上班刚坐到位置上,准备开始摸鱼。呸,开始工作。产品就在钉钉上怼过来一张截图,图上显示dmp系统的几个在跑任务挂了,最后运行时间是昨天下午,状态还是运行中。

于是我放下手中还没啃完的烧麦,开始bug时间。从表象上来看,看不出是什么问题,因为这个业务流程相对来说比较长,每一个环节都有可能出现异常,也许是因为没有考虑到的异常未被捕获造成的。心里这样想着,cd到这个业务昨天的日志文件,是他是他就是他,我们的老朋友。看到这东西,莫名开始激动了起来呢,毕竟是线上为数不多可以看到的情况。

1
Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded

在此之前呢还有

1
nested exception is com.mongodb.MongoException$CursorNotFound: Cursor 2114193476805984430 not found on server

于是自然的开始Google一把这个错误,说是驱动版本的问题或者响应超时。快速排出了第一种情况,那么MongoDB怎么就超时了呢。细想一下,最近开发的功能。有一处读取Hive清洗的dmp原始数据到MongoDB,这个部分是写过程但是并没有出现Hive相关的报错信息。因此最大的可能就是落在读取MongoDB这部分数据打包成压缩文件的这个点了。

果然排查之后发现,在昨天的下午这个时间点。有业务人员操作了一个400W数据的包体,并且使用了其管理的5个账号。当时为了能够在同一时间处理多个任务,以及考虑到第一期清洗数据单个包体不会超过300W的情况下,开启线程池同时执行的最大线程数设置成了8。

因此在这种情况下,几乎同一时段加载到内存当中的数据为5x400W=2000W 而该Java进程设置的最大内存为4G,所以超出了内存上限导致溢出。

问题定位到了,就好解决了。与产品和运维沟通。该业务并不要求实时性,因此把并发线程数降到3,同时扩展内存到6G。对于大数据量的包体,进行切分,分批次查询数据append进文件中再打包。同时观察线上运行情况,及时调整,做好失败重试方案。

关于异常处理的一点思考

前言

关于异常处理,在网上的大部分博客大多关注编程语言本身的异常处理机制。而很少谈及在业务当中的实际应用情况。因而不是很能够get到其中的要点。这部分感觉只能自己多多思考和积累了。

最近在工作当中开发的一些基于Quartz任务调度实现的自动化功能,涉及到比较长的运行流程。包括hive数据的清洗导出,数据分包,打包成txt或者zip,多文件上传,自动重试。而每一个流程当中可能会出现诸多的异常,对应于多个运行的结果状态。

如何保证不管结果如何,到每一个流程运行完都能标记为正确的状态,并且可以通过结果来进行自动重试,最后能够将结果信息、错误信息返回到顶层?如何编码能够避免过多的if条件判断,拥有统一的编码风格?

查看了类似业务的编码,因为多状态的问题,代码判断很多导致可读性很差。对于多个渠道对应不同的上传预处理方式,直接判断处理导致代码更加臃肿。为此,我决定重构一下这块逻辑,以便更好的扩展和维护。

思路

首先,将所有可能出现的正常和异常情况列出来,然后归类,设计对应的枚举值。然后定义一个统一的处理结果类,如下:

1
public class ResultInfo implements Serializable {
2
	private static final long serialVersionUID = 1L;
3
	//处理结果标记
4
	private boolean success = true;
5
	//结果代码
6
	private String resultCode;
7
	//存储的结果对象
8
	private Object model;
9
	//返回信息,用于上层展示
10
	private String message;
11
	//调用链存储结果对象
12
	private Map<String, Object> map = new LinkedHashMap<String, Object>();
13
	//调用链存储错误信息
14
	private Map<String, String> errorMessages = new LinkedHashMap<String, String>();
15
}

接下来,我们所有的业务处理正常返回的数据都存储到这个结果对象当中。或者定义一个统一的业务异常类来集中处理。对于具体的异常处理,我们用try catch捕获,对于底层的就通过结果对象往上层调用传。在上层业务通过取结果对象当中的代码或者错误消息还有返回数据对象等等来统一处理当前业务的状态变更,而不在下层处理导致底层模块以来上层业务。

对于底层异常来说,因为比较独立。例如文件的打包等等,我们就没必要做统一异常的处理。这个时候用throws 往上层抛出,上层接受后在统一处理。另外对于并发锁的部分我们要在finally当中单独进行处理。

关于不同渠道的文件上传,因为有不同的数据预处理方式,走的接口也是完全不一样的,没有办法统一,但是它们在抽象上都属于上传这个领域。因此,我考虑使用工厂模式来组织代码。在服务层,依赖于工厂和上传接口。而工厂去生产具体的上传服务,各渠道服务实现抽象上传接口。从而实现这部分引起的业务逻辑判断过多的问题。

踩坑总结

具体的实现过程当中,还是发现因为判断多而思路不清晰的时候。不过当我把业务拆解到更细粒度的方法的时候就变的清晰很多了。很重要的经验:先处理异常情况,最后在处理正常情况
虽然是日常编码中很小的点,但是正是这些小地方让代码变得更优雅。争哥的设计模式课程的学习,给到我很大的看见,继续加油Storm。

记一次线上Redis资源优化

闲话

最近在开发一个数据仓库的小版本,因为自己的疏忽(误以为一个重要的流程可以复用,实际不可以)导致工期估计很少而坑了自己,一周的时间都在加班搞这个事情。幸好还是扛下来,少不了同事老毕的帮助,还是很感激有这样的同事的。版本进入测试阶段,终于有时间可以来开始写写博客。这个等版本上线之后可以在来复盘一下。

今天呢,主要是想仔细来梳理一下之前做的一个版本优化,主要是针对线上的一块大流量业务做的Redis资源优化,优化之后这块业务内存资源节约超过65%。在不影响性能的情况下,缓存时间从1天提高到7天。话不多说,下次开始吧。

项目背景

公司有广告数据上报的业务,峰值在1200w+每天,均值600w+。为了应对复杂的归因流程,以及归因回调,需要将这部分数据放入缓存,以免给数据库造成太大的压力。回调相关的参数数据也是放在缓存当中的,缓存保留时间是1天。

需求变更

为了提高归因率,现需要把缓存时间从1天提升到7天。这意味着缓存数据量是原来的7倍,并且增加两种匹配机制,数据量又增加30%。
(我们线上的Redis内存最大容量是16G,12G为告警阈值,按照现有的机制增加的话理论计算上面就已经超过阈值了,因此肯定是要优化的)

前期调研

为了清楚现有业务内存的占用情况,分别对涉及到的7种匹配机制进行调研。在本地分别测试10W数据的内存大小。间隔5天取近两个月的数据总量进行统计,取均值和峰值以及8倍峰值数据量,分别计算理论情况下,各个匹配机制所需要的Redis内存大小。

优化思路

业务层面来说,用户点击广告不一定会下载激活游戏来玩,这一层级的转化其实是有很大损失的,因此匹配回调的数据可以和点击数据剥离。考虑到不同渠道的数据格式和参数个数,检测数据类型不一样的原因,为了便于往后扩展,这里考虑使用MongoDB来存储这部分的数据。

为了不降低归因的效率,我们提供一个主键来允许其他系统业务定位到具体的点击数据。自然的Redis中我们就直接缓存这个主键,而不在缓存原始数据了。归因匹配上了,我们在直接通过原始数据来构造回调参数。这样即使增加了匹配机制类型,但是总的数据量还是减少了很大一部分。
(对于日均量来说,减少了78.3%,对于峰值来说减少了58.7%)

至此还有,最后一个问题没有解决。如何处理7天缓存的这个问题呢。从业务上面分析,对于超过一天的这部分数据,是指一个用户点击了广告,但是呢在一天之后才激活,当然这其中也存在异常激活的情况,但是比较少。

为了这个少部分的数据, 而且不是热点的数据,来缓存如此庞大的数据是非常不优雅的。为此,我们使用到MongoDB的另外一个特性来解决这个问题。

对于MongoDB来说,可以设置文档的自动过期时间,虽然它的精确度不是特别高,但是对于7天来说可以忽略。你是不是已经知道我要干什么了。对的,我们把超过一天缓存的数据都写到MongoDB当中,设置过期时间为7天,我们也只存储它的索引,用于定位,这个缓存库设置分片。

这样整个优化,在理论上就可行了,接下来就是动手实践的时候。具体实施的过程还是踩了一些坑,但都是可以实战解决的。

复盘总结

对于这次优化而言,其实没有特别大的技术点,要点还是在于业务的剖析,能够准确定位哪些是热点数据,哪些是半热数据,哪些是冷数据。从而针对不同类型的数据做文章,分割开来用不同的手段处理。当然对于业务规模的前期调研以及后期扩展的展望也是非常重要的,能够让我们清晰的知道现有系统的健康状况,能不能支撑我们做迭代,以及相对长期的迭代。

我相信这还只是一个开始,未来的路还很长。Storm,砥砺前行!

使用docker部署高可用的Eureka集群

写在前面的话

Hi,小伙伴们。继上期分享了如何用Docker部署自己的第一个SpringBoot应用之后,爱折腾的Storm还是耐不住性子,又折腾了一下SpringCloud应用的部署。在不使用Docker部署的时候呢,在本地模拟搭建一个高可用的Eureka集群是相当简单的。但是使用Docker部署的时候遇到了节点无法通信的问题,折腾半天终于完美解决这个问题,以此记录一下。

原因猜测

在项目中,我使用的是域名来区分不同的Eureka服务,本地Host绑定这些域名解析为127.0.0.1 但是部署到容易当中的时候就无法进行通信了,因此需要使用其他方法来建立通信。更改配置,使用ip并不是我的意图,虽然在实际生产环境是可以的,但是硬编码的风格显然不是最好的方案。

解决方案

经过资料的收集和整理,发现docker-compose能够解决这个问题,它能够自定义编排要发布的容易,包括容器依赖和通信并且统一部署。因此我在此使用它来解决上述的部署问题。

准备工作

首先我们需要构建Eureka服务,使用不同的端口,具体源码见我的开源项目

主要工作

我们需要在项目根目录下新建一个docker文件夹,用于存储Docker部署相关的文件,在此文件夹下新建一个docker-compose.yml文件,内容如下

1
version: "2.1"
2
services:
3
  eureka-server-01:
4
    image: eureka-server-01
5
    hostname: eureka-server-01
6
    networks:
7
      - eureka-net
8
    ports:
9
      - "1001:1001"
10
11
  eureka-server-02:
12
    image: eureka-server-02
13
    hostname: eureka-server-02
14
    networks:
15
      - eureka-net
16
    ports:
17
      - "1002:1002"
18
19
  eureka-server-03:
20
    image: eureka-server-03
21
    hostname: eureka-server-03
22
    networks:
23
      - eureka-net
24
    ports:
25
      - "1003:1003"

这里的配置主要是编排我们将要发布的三个容器基本信息,包括容器的名称,镜像名称,host名称,端口等等。这样我们统一发布这些应用,应用之间就可以相互通信了,而不是网络隔离的状态。

最后我们在idea中运行这个文件,不出意外的话,我们就可以看到三个容器正常部署并且输出打印日志。同样的,访问http://eureka-server-01:1001/eureka/ 查看是否有挂载3个节点,并且都处于正常状态。到这里高可用Eureka高可用服务注册与发现中心得Docker部署搭建就完成啦,这个也是同样可以用于Feign等Eureka客户端的部署使用过程当中的,大家可以自行尝试。我的项目当中已经为大家准备好了Demo,可以自行查看。我们下期见吧。

使用docker部署我的第一个SpringBoot应用

Hi,我们又见面啦。最近Storm在研究docker部署相关的知识点,也是踩了不少坑,于是就有了下面的这篇文章,给准备捣鼓这一块的小伙伴一点前车之鉴,话不过说我们开始动手吧。

ps:还是忍不住安利一波,因为Storm之前一直使用的主力生产工具是Eclipse,公司里面也是统一使用的这个工具,为此老大还自己研发了一些插件,这款IDE还是相当强大的。

但是从两个月前上手idea开始就一发不可收拾,再也回不去了,墙裂安利大家去折腾一下,你也会爱不释手的。不用担心说团队的原因或者快捷键等等,因为这些都已经有人踩过坑啦。解决方案都是现成的,工欲善其事必先利其器,所以蠢蠢欲动不如行动吧,哈哈哈。

写在前面的话

在这篇文章当中我不过多介绍SpringBoot项目的构建过程,因为随便搜一下就有一堆。简单的几项配置,就能跑起来,重要的还是需要检查一下你的环境。包括IDE,Maven,Java等等

安装docker

下载传送门:docker官网
根据你的开发环境,选择安装相应的版本(实测基本使用mac和win是类似的)
安装就是傻瓜式的软件安装,注意设置你的docker镜像存放位置,因为它默认是放在C:盘

设置docker

启动docker后,在任务栏处会看到一个docker的小鲸鱼图标,右键打开Settings切换到General选项

1
勾选 Expose daemon on tcp://localhost:2375 without TLS

将docker连接的端口暴露到开发环境当中,便于我们连接docker操作我们的镜像和容器

idea配置docker环境

接下来我们需要在idea中安装一个docker的插件Docker integration
打开idea,从File->Settings->Plugins->Install JetBrains plugin进入插件安装界面,在搜索框中输入docker,可以看到Docker integration,点击右边的Install按钮进行安装。

安装后重启idea,从File->Settings->Build,Execution,Deployment->Docker打开配置界面
新建一个docker连接,名字随意,将上面我们勾选的地址复制到TCP socket选项中 看到下方窗体中出现Connection Successful就说明配置成功啦

部署项目到docker

项目打包

首先我们要先将我们构建好的SpringBoot项目打包,这里使用到maven的构建工具。
在SpringBoot项目的pom文件中加入如下配置:

1
<build>
2
	<plugins>
3
		<plugin>
4
			<artifactId>maven-antrun-plugin</artifactId>
5
			<executions>
6
				<execution>
7
					<id>SpringBootApp</id>
8
					<phase>package</phase>
9
					<configuration>
10
						<tasks>
11
							<copy todir="docker" file="target/${project.artifactId}-								${project.version}.${project.packaging}" />
12
						</tasks>
13
					</configuration>
14
					<goals>
15
						<goal>run</goal>
16
					</goals>
17
				</execution>
18
			</executions>
19
		</plugin>
20
	</plugins>
21
</build>

构建镜像 部署docker

到这一步,我们就要开始进入关键点啦,前面都是准备工作。
首先,在项目的根目录(注意是根,不是src或者mian)新建一个docker文件夹用于存放docker相关的文件
新建一个Dockerfile文件 (这个是镜像构建文件,即告诉docker需要为我们的项目准备什么环境)
具体内容如下:

1
FROM java:8u111
2
VOLUME /tmp
3
ADD *.jar app.jar
4
EXPOSE 8088
5
ENTRYPOINT ["java","-jar","/app.jar"]
6
# Ubuntu 时区
7
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

如上配置,说明我们的项目运行在java8环境当中,使用的是Ubuntu系统的环境,将8088端口映射到docker环境之外,我们就可以在宿主机(开发机器)访问到这个端口,其他配置具体细节可以自行Google

配置好Dockerfile之后,我们右键它create docker/Dockerfile 在docker配置对话框中输入容器的名称
添加端口映射8088:8088
添加在构建之前运行的maven命令:

1
clean package -Dmaven.test.skip = true

构建项目时,会自动将target下打好的jar复制到项目根目录,我们发布到docker时会使用这个jar。

运行

一切准备就绪,接下来我们右键运行Dockerfile文件,在docker文件夹下就会出现打好的项目jar包,docker就开始构建镜像,配置容器,然后发布jar包到容器的tmp目录下,最后启动项目,你就可以在插件的控制台看到Spring的Banner以及相关启动日志啦。

结语

以上就是这一期docker部署我的第一个SpringBoot项目的全部内容啦,大家多动手试试吧。可能会有细节没有把握到,请多多包涵,我们下期见吧。

Hello World

Hi,你好。欢迎来到Storm的博客,这是我的第一篇文章。作为一个技术人,是有点汗颜,已经在这个圈子里混了两三年了才真正去搭建属于自己的博客。其实也不知道,自己此举能够坚持多久,但是喜欢折腾的个性还是逼自己去做了这件事。不管能做的有多好,也不再去有这个遗憾了。

期望还是要有的,就希望自己能从一个喜欢分享的人,慢慢成长为一个会分享的人吧。一切静静地开始,如果你对我来说是个陌生人,看到一些对你有用的东西那就请你祝福我吧,我们一起努力。成长为更好的自己。如果你是Storm的朋友,那就支持一下我吧,我就是一个需要肯定才会走的更远的人啊。

搭建自己的简易博客

创建一个Github仓库

1
仓库名称格式为:<你的GitHub账户名>.github.io

传送门: Github

本地环境搭建

1
#安装 git
2
#配置你的账号密码 生成ssh密钥 配置到Github
3
#安装 node.js 
4
cd 到目标文件夹 安装 hexo	
5
$ sudo npm install -g hexo
6
#初始化	$ hexo init
7
#安装依赖	$ npm install

传送门: hexo文档

本地部署

1
$ hexo g
2
$ hexo s
3
#访问 loaclhost:4000 验证部署

传送门: hexo文档

部署到Github

1
#cd 到hexo安装根目录 修改_config.yml文件
2
#这里可以按照自己的需要更改标题、副标题等基础配置
3
#在deploy参数项下增加
4
type: 'git'
5
repository: '之前创建的仓库地址'
6
branch: 'master' ##当然是默认设置成master分支啦
7
  
8
#保存之后 执行以下命令
9
$ hexo generate
10
$ hexo deploy
11
#这里会提示你输入GitHub的账户名和密码
12
#然后访问https://<你的GitHub账户名>.github.io 查看部署情况
13
#会比较慢 耐心等一下(没办法 毕竟免费的嘛)

嗯 不出意外的话,你就能看到自己亲手搭建的博客啦,虽然功能比较简单,但是刚刚开始玩的话还是非常够用了,博客重要的是分享的内容,而不是花里胡哨的框架啦。然后就开始用Markdown愉快的写文字吧。我们下期再见啦。