Hero Image
Grafana上监控kubernetes中Pod已用内存不准问题分析

结合上次Golang服务内存增长的分析,近期线上多个服务出现内存持续增长的问题,就这个现象分析一下Prometheus+Grafana的监控问题 #kubernetes #k8s #内存分析 #oom #golang #grafana 问题现象 近期在Grafana上显示生产环境多个服务出现内存持续增长的问题,有Golang的服务,也有JAVA的服务。都是服务重启之后,内存来到一个最低水平, 随着服务运行时间增长,pod的内存占用也随之水涨船高。直到内存占用增长到pod限制的上限附近,内存才出现回收的迹象,并且回收幅度不是特别明显, 但同时又不会出现OOM。 Golang某个服务内存占用情况 JAVA某个服务内存占用情况 简单分析 记一次线上的内存持续增长问题 ↑这个是初次遇到这个问题时候的分析,当时以为是代码写的有漏洞,程序发生了内存泄漏。于是祭出了pprof分析了一顿,结果可想而知,当然是没看出有问题。 现在是多个服务都出现类似问题,那这个情况相对也就比较值得重视了。之前那个服务是因为日志写的比较多,造成磁盘IO比较大。同样的, 近期发现的这几个内存持续不断增长的服务也都是日志量比较大的。 进一步分析 集群日志架构 所有pod中的日志都是写入挂载到/data/log目录的物理机磁盘中,因此所有写日志的操作都会有磁盘IO。日志量越大的pod,磁盘IO相应地也越高。 集群监控 普通pod监控采用了常见的Prometheus+Grafana的方案。 数据源计算方式 监控数据是采集的kubernetes中监控程序cadvisor上报的container_memory_working_set_bytes字段( 表格参照 ) 查看cadvisor源码中setMemoryStats 可知,container_memory_working_set_bytes字段是cgroup memory.usage_in_bytes(RSS + Cache)与memory.stat total_inactive_file二者的差值 func setMemoryStats(s *cgroups.Stats, ret *info.ContainerStats) { // ... // ... inactiveFileKeyName := "total_inactive_file" if cgroups.IsCgroup2UnifiedMode() { inactiveFileKeyName = "inactive_file" } workingSet := ret.Memory.Usage if v, ok := s.MemoryStats.Stats[inactiveFileKeyName]; ok { if workingSet < v { workingSet = 0 } else { workingSet -= v } } ret.

Hero Image
Hadoop生态组件

#hadoop 最近在学习大数据相关的东西,看了HDFS,Hive,HBas,Spark相关的东西,总结一下Hadoop生态中常见的组件。 HDFS(hadoop分布式文件系统) HDFS是hadoop体系中数据存储管理的基础。他是一个高度容错的系统,能检测和应对硬件故障。 有以下几个角色: client:切分文件,访问HDFS,与那么弄得交互,获取文件位置信息,与DataNode交互,读取和写入数据。 namenode:master节点,在hadoop1.x中只有一个,管理HDFS的名称空间和数据块映射信息,配置副本策略,处理客户 端请求。 DataNode:slave节点,存储实际的数据,汇报存储信息给namenode。 secondary namenode:辅助namenode,分担其工作量:定期合并fsimage和fsedits,推送给namenode;紧急情况下和辅助恢复namenode,但其并非namenode的热备。 mapreduce(分布式计算框架) mapreduce是一种计算模型,用于处理大数据量的计算。其中map对应数据集上的独立元素进行指定的操作,生成键-值对形式中间,reduce则对中间结果中相同的键的所有值进行规约,以得到最终结果。 jobtracker:master节点,只有一个,管理所有作业,任务/作业的监控,错误处理等,将任务分解成一系列任务,并分派给tasktracker。 tacktracker:slave节点,运行 map task和reducetask;并与jobtracker交互,汇报任务状态。 map task:解析每条数据记录,传递给用户编写的map()并执行,将输出结果写入到本地磁盘(如果为map—only作业,则直接写入HDFS)。 reduce task:从map 它深刻地执行结果中,远程读取输入数据,对数据进行排序,将数据分组传递给用户编写的reduce函数执行。 hive(基于hadoop的数据仓库) 由Facebook开源,最初用于解决海量结构化的日志数据统计问题。 hive定于了一种类似sql的查询语言(hql)将sql转化为mapreduce任务在hadoop上执行。 hbase(分布式列存数据库) hbase是一个针对结构化数据的可伸缩,高可靠,高性能,分布式和面向列的动态模式数据库。和传统关系型数据库不同,hbase采用了bigtable的数据模型: 增强了稀疏排序映射表(key/value)。其中,键由行关键字,列关键字和时间戳构成,hbase提供了对大规模数据的随机,实时读写访问,同时,hbase中保 存的数据可以使用mapreduce来处理,它将数据存储和并行计算完美结合在一起。 zookeeper(分布式协作服务) 解决分布式环境下的数据管理问题:统一命名,状态同步,集群管理,配置同步等。 sqoop(数据同步工具) sqoop是sql-to-hadoop的缩写,主要用于传统数据库和hadoop之间传输数据。数据的导入和导出本质上是mapreduce程序,充分利用了MR的并行化和容错性。 pig(基于hadoop的数据流系统) 定义了一种数据流语言-pig latin,将脚本转换为mapreduce任务在hadoop上执行。通常用于离线分析。 mahout(数据挖掘算法库) mahout的主要目标是创建一些可扩展的机器学习领域经典算法的实现,旨在帮助开发人员更加方便快捷地创建只能应用程序。mahout现在已经包含了聚类,分类, 推荐引擎(协同过滤)和频繁集挖掘等广泛使用的数据挖掘方法。除了算法是,mahout还包含了数据的输入/输出工具,与其他存储系统(如数据库,mongoDB或 Cassandra)集成等数据挖掘支持架构。 flume(日志收集工具) cloudera开源的日志收集系统,具有分布式,高可靠,高容错,易于定制和扩展的特点。他将数据从产生,传输,处理并写入目标的路径的过程抽象为数据流,在 具体的数据流中,数据源支持在flume中定制数据发送方,从而支持收集各种不同协议数据。 资源管理器的简单介绍(YARN和mesos) 随着互联网的高速发展,基于数据 密集型应用 的计算框架不断出现,从支持离线处理的mapreduce,到支持在线处理的storm,从迭代式计算框架到 流式处理框 架s4,…,在大部分互联网公司中,这几种框架可能都会采用,比如对于搜索引擎公司,可能的技术方法如下:网页建索引采用mapreduce框架,自然语言处理/ 数据挖掘采用spark,对性能要求到的数据挖掘算法用mpi等。公司一般将所有的这些框架部署到一个公共的集群中,让它们共享集群的资源,并对资源进行统一使 用,这样便诞生了资源统一管理与调度平台,典型的代表是mesos和yarn。 其他的一些开源组件: cloudrea impala: 一个开源的查询引擎。与hive相同的元数据,SQL语法,ODBC驱动程序和用户接口,可以直接在HDFS上提供快速,交互式SQL查询。impala不再使用缓慢的 hive+mapreduce批处理,而是通过与商用并行关系数据库中类似的分布式查询引擎。可以直接从HDFS或者Hbase中用select,join和统计函数查询数据,从而 大大降低延迟。 spark: spark是个开源的数据 分析集群计算框架,最初由加州大学伯克利分校AMPLab,建立于HDFS之上。spark与hadoop一样,用于构建大规模,延迟低的数据分析 应用。spark采用Scala语言实现,使用Scala作为应用框架。 spark采用基于内存的分布式数据集,优化了迭代式的工作负载以及交互式查询。 与hadoop不同的是,spark与Scala紧密集成,Scala象管理本地collective对象那样管理分布式数据集。spark支持分布式数据集上的迭代式任务,实际上可 以在hadoop文件系统上与hadoop一起运行(通过YARN,MESOS等实现)。 storm storm是一个分布式的,容错的计算系统,storm属于流处理平台,多用于实时计算并更新数据库。storm也可被用于“连续计算”,对数据流做连续查询,在计算 时将结果一流的形式输出给用户。他还可被用于“分布式RPC”,以并行的方式运行昂贵的运算。 kafka kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的 网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。 这些数据通常是由于吞吐量的要求 而通过处理日志和日志聚合来解决。 对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka的目的是通 过Hadoop的并行加载机制来统一线上和离线的消息处理,也是为了通过集群来提供实时的消息

Hero Image
记一次线上的内存持续增长问题

#golang #oom #内存分析 #grafana #kubernetes #k8s 问题现象 前些天从Grafana上看到某一个pod内存涨上去就再没下来(从9/1~9/2之间的一个时间开始),并且看这个趋势涨上去就没有下来的意思。中间有几次pod重新发布 才导致内存恢复到一个比较低的水平,但内存依旧持续上涨。 初步分析 初步推测大概率与日志有关,此次发版改动了日志输出格式,以及修改了日志没有写入磁盘的问题。 先把服务稳住 由于清楚问题的大致方向,先将服务中几个打印log比较频繁的位置注释掉,在9/3~9/4之间的一个位置重新发布。从之后的趋势上可以看出,注释掉几个打印日志的 地方之后,内存增长速度明显放缓。 至此,基本可以确认内存增长与日志相关。 问题排查 猜测一 回头又捋了几遍代码,也没发现什么端倪。 于是祭出pprof抓了一下内存分析了一通,依旧无果。 可以看出,内存占用并没有多高。 猜测二 在 Go1.12 以前,Go Runtime在Linux上使用的是MADV_DONTNEED策略,可以让RSS下降的比较快,就是效率差点。 在 Go1.12 及以后,Go Runtime专门针对其进行了优化,使用了更为高效的MADV_FREE策略。但这样子所带来的副作用就是RSS不会立刻下降, 要等到系统有内存压力了才会释放占用,RSS才会下降。 查看容器的 Linux 内核版本: # 查看命令 uname -a 课件容器版本为3.10.0,但MADV_FREE的策略改变,需要Linux内核在4.5及以上(详细可见go/issues/23687 ), 因此可以排除。 猜想三 通过top命令可以通过可以查看容器中程序的内存占用VSZ为711,无法查看RSS,关于RSS和VSZ的区别,可以参考RSS和VSZ 容器内存判定是通过container_memory_working_set_bytes,而container_memory_working_set_bytes是由cadvisor提供的。 原因 从cadvisor/issues/638 可得知container_memory_working_set_bytes指标的组 成实际上是RSS + Cache。而Cache高的情况,常见于进程有大量文件IO,占用Cache可能就会比较高,猜测也与Go版本、Linux 内核版本的Cache释放、回收方式有较大关系。 只要是涉及有大量文件IO的服务,基本上是这个问题的老常客了,写这类服务基本写一个中一个,因为这是一个混合问题,像其它单纯操作为主的业务服务就很 “正常”,不会出现内存居高不下。 没多久看到烤鱼佬的一篇文章,与这个情况类似,他的解决办法也就是写了个脚本,“手动"HPA(其实也就是自动重启)。 总结 虽然这问题时间跨度比较长,整体来讲都是阶段性排查,本质上可以说是对Kubernetes的不熟悉有关。但因为内存居高不下的可能性有很多种,要一个个排查。

Hero Image
Golang性能分析工具-pprof

#golang #pprof #内存分析 pprof is a tool for visualization and analysis of profiling data. pprof reads a collection of profiling samples in profile.proto format and generates reports to visualize and help analyze the data. It can generate both text and graphical reports (through the use of the dot visualization package). PProf是用于可视化和分析性能分析数据的工具,PProf以profile.proto读取分析样本的集合,并生成报告以可视化并帮助分析数据(支持文本和图形报告)。 简介 采集方式 runtime/pprof:采集程序(非Server)的指定区块的运行数据进行分析。 net/http/pprof:基于HTTPServer运行,并且可以采集运行时数据进行分析。 gotest:通过运行测试用例,并指定所需标识来进行采集。 功能 CPUProfiling:CPU分析,按照一定的频率采集所监听的应用程序CPU(含寄存器)的使用情况,可确定应用程序在主动消耗CPU周期时花费时间的位置。 MemoryProfiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏。 BlockProfiling:阻塞分析,记录Goroutine阻塞等待同步(包括定时器通道)的位置,默认不开启,需要调用runtime.SetBlockProfileRate进行设置。 MutexProfiling:互斥锁分析,报告互斥锁的竞争情况,默认不开启,需要调用runtime.SetMutexProfileFraction进行设置。 GoroutineProfiling:Goroutine分析,可以对当前应用程序正在运行的Goroutine进行堆栈跟踪和分析。这项功能在实际排查中会经常用到, 因为很多问题出现时的表象就是Goroutine暴增,而这时候我们要做的事情之一就是查看应用程序中的Goroutine正在做什么事情,因为什么阻塞了, 然后再进行下一步。 简单的例子 注意要在import中引入 _ "net/http/pprof" package main import ( "log" "net/http" _ "net/http/pprof" "time" ) func main() { go func() { for { log.

Hero Image
Golang反射

#golang #reflect 反射简介 Golang提供了一种机制,在编译时不知道类型的情况下,可更新变量、运行时查看值、调用方法以及直接对他们的布局进行操作的机制,称为反射。 reflect 包中的官方注释:Package reflect implements run-time reflection, allowing a program to manipulate objects with arbitrary types. reflect 实现了运行时的反射能力,能够让程序操作不同类型的对象。反射包中有两对非常重要的函数和类型, 两个函数分别是: reflect.TypeOf 能获取类型信息 reflect.ValueOf 能获取数据的运行时表示 三大法则 运行时反射是程序在运行期间检查其自身结构的一种方式。反射带来的灵活性是一把双刃剑,反射作为一种元编程方式可以减少重复代码, 但是过量的使用反射会使我们的程序逻辑变得难以理解并且运行缓慢。我们在这一节中会介绍Go语言反射的三大法则,其中包括: 从interface{}变量可以反射出反射对象; 从反射对象可以获取interface{}变量; 要修改反射对象,其值必须可设置; 第一法则 反射的第一法则是我们能将Go语言的interface{}变量转换成反射对象。很多读者可能会对这以法则产生困惑—为什么是从interface{}变量到反射对象? 当我们执行reflect.ValueOf(1)时,虽然看起来是获取了基本类型int对应的反射类型,但是由于 reflect.TypeOf 、 reflect.ValueOf 两个方法的入参都是interface{}类型,所以在方法执行的过程中发生了类型转换。 因为Go语言的函数调用都是值传递的,所以变量会在函数调用时进行类型转换。基本类型int会转换成interface{}类型, 这也就是为什么第一条法则是从接口到反射对象。 上面提到的reflect.TypeOf 和reflect.ValueOf 函数就能完成这里的转换,如果我们认为Go语言的类型和反射类型处于两个不同的世界,那么这两个函数就是连接这两个世界的桥梁。 我们可以通过以下例子简单介绍它们的作用, reflect.TypeOf 获取了变量author的类型, reflect.ValueOf 获取了变量的值ormissia。如果我们知道了一个变量的类型和值,那么就意味着我们知道了这个变量的全部信息。 package main import ( "fmt" "reflect" ) func main() { author := "ormissia" fmt.Println("TypeOf author:", reflect.TypeOf(author)) fmt.Println("ValueOf author:", reflect.ValueOf(author)) } $ go run main.

Hero Image
Go 惯用模式:函数选项模式

#golang 作为 Golang 开发者,遇到的许多问题之一就是尝试将函数的参数设置成可选项。这是一个十分常见的场景,您可以使用一些已经设置默认配置和开箱即用的对象,同时您也可以使用一些更为详细的配置。 问题出发点 对于许多编程语言来说,这很容易。在 C 语言家族中,您可以提供具有同一个函数但是不同参数的多个版本;在 PHP 之类的语言中,您可以为参数提供默认值,并在调用该方法时将其忽略。但是在 Golang 中,上述的做法都不可以使用。那么您如何创建具有一些其他配置的函数,用户可以根据他的需求(但是仅在需要时)指定一些额外的配置。 有很多的方法可以做到这一点,但是大多数方法都不是尽如人意,要么需要在服务端的代码中进行大量额外的检查和验证,要么通过传入他们不关心的其他参数来为客户端进行额外的工作。 下面我将会介绍一些不同的选项,然后为其说明为什么每个选项都不理想,接着我们会逐步构建自己的方式来作为最终的干净解决方案:函数选项模式。 让我们来看一个例子。比方说,这里有一个叫做StuffClient的服务,它能够胜任一些工作,同时还具有两个配置选项(超时和重试)。 type StuffClient interface { DoStuff() error } type stuffClient struct { conn Connection timeout int retries int } 这是个私有的结构体,因此我们应该为它提供某种构造函数: func NewStuffClient(conn Connection, timeout, retries int) StuffClient { return &stuffClient{ conn: conn, timeout: timeout, retries: retries, } } 嗯,但是现在我们每次调用NewStuffClient函数时都要提供timeout和retries。因为在大多数情况下,我们只想使用默认值,我们无法使用不同参数数量带定义多个版本的NewStuffClient,否则我们会得到一个类似NewStuffClient redeclared in this block编译错误。 一个可选方案是创建另一个具有不同名称的构造函数,例如: func NewStuffClient(conn Connection) StuffClient { return &stuffClient{ conn: conn, timeout: DEFAULT_TIMEOUT, retries: DEFAULT_RETRIES, } } func NewStuffClientWithOptions(conn Connection, timeout, retries int) StuffClient { return &stuffClient{ conn: conn, timeout: timeout, retries: retries, } } 但是这么做的话有点蹩脚。我们可以做得更好,如果我们传入了一个配置对象呢:

Hero Image
我的博客后端Docker镜像打包自动部署流程

博客后端使用Golang重构之后使用GitHub-DockerHub-Jenkins自动打包部署流程 虽然说Golang打包生成的是二进制可执行文件,不需要像JAVA一样部署环境变量,但依然也是需要打包的流程。由于考虑到在不(hen)久(yuan)的将来可能做成简单的微服务程序,又要使用Docker部署,所以在这就直接使用Docker镜像的方式来部署运行。 本地代码→GitHub 这一步是通过git commit-git push或是直接使用IDE将代码托管到GitHub上。 在这一步的同时需要编写Dockerfile文件,用来指定Docker镜像打包时的各种参数 # Go程序编译之后会得到一个可执行的二进制文件,其实在最终的镜像中是不需要go编译器的,也就是说我们只需要一个运行最终二进制文件的容器即可。 # 作为别名为"builder"的编译镜像,下面会用到 FROM golang AS builder # 为镜像设置必要的环境变量 ENV GO111MODULE=on \ CGO_ENABLED=0 \ GOOS=linux \ GOARCH=amd64 \ GOPROXY=https://goproxy.cn # 设置工作目录:/build WORKDIR /build # 复制项目中的 go.mod 和 go.sum文件并下载依赖信息 COPY go.mod . COPY go.sum . RUN go mod download # 将代码复制到容器中 COPY 2021-03-04T18:02:29 . # 将代码编译成二进制可执行文件app RUN go build -o go-blog-app . ################### # 接下来创建一个小镜像 ################### FROM scratch # 设置程序运行时必要的环境变量,包括监听端口、数据库配置等等 ENV SERVER_PORT=8085 \ DATASOURCE_DRIVERNAME=mysql \ DATASOURCE_HOST=192.