How Basic Performance Analysis Saved Us Millions-------火焰图

This is the story of how I applied basic performance analysis techniques to find a small change that resulted in a 10x improvement in CPU use for our Postgres cluster and will save Heap millions of dollars over the next year.
Indexing Data for Customer Analytics
Heap is a customer analytics tool that automatically captures every user interaction with your website or app. Once installed on a website, Heap will automatically track every pageview, click, form submission, and more. From there, the owner of the website can use Heap to perform many different kinds of aggregations over different subsets of the raw data.
In order to make it possible to get insights out of this data, Heap lets users define events in terms of the raw data. An example might be a “Login”, which could be defined as a “form submission on the /login
page”.
To make analyses fast, we use a very unusual indexing strategy which relies on Postgres’ partial indexing feature. A partial index is like a normal Postgres index, except it only contains rows that satisfy a specified predicate. You can think of it like a regular index with a WHERE
clause. For every event definition one of our customers creates, we create a partial index on that customer’s raw event data, restricted to the rows which match the definition. Whenever a new row is inserted into our events
table, Postgres will automatically test the event against the predicate of each partial index on the table and add the row to the necessary indexes.
For each event definition, the corresponding partial index makes it very fast to retrieve all matching events because the index contains exactly the events that satisfy the definition. If you want to learn more about how we use partial indexes, you should read our blog post on how we index our data which goes more in depth.
Problem: Unusually High CPU Usage
When we first rolled out this indexing strategy, our CPU use was significantly higher than it was with our previous indexing strategy. This made sense, we thought: our largest customers have thousands of these indexes and in order to support filters based on CSS selectors, lots of these partial indexes contain a regular expression filter. We thought that since regular expressions are fairly expensive to evaluate, it only made sense that testing a thousand regexes against every event as it was inserted would cause Postgres to use a ton of CPU. There was no real evidence this was the case, but it became the explanation everyone at Heap gave for why Postgres used so much CPU. We assumed it was a fundamental tradeoff of the indexing strategy.
Around October, as our data volume continued to increase, we started having issues ingesting all of the data coming in during peak hours. On some days it would take hours for a new event to show up in the Heap dashboard. This is completely unacceptable for a tool meant for real time analytics. Instead of going the typical route and throwing money at the problem, I thought I would try my hand at optimizing Heap’s ingestion throughput.
Visualizing CPU Use with Flame Graphs
Prior to this I had limited experience debugging performance issues. After googling for a bit, I came across one of Brendan Gregg’s posts on flame graphs. A flame graph is a type of visualization Brendan Gregg invented as a way to quickly identify which parts of your code are taking up CPU. The first step in creating a flame graph is to take samples of the stack of the process using the Linux perf
tool:
perf record -p $(pid of process) -F 99 -g -- sleep 60
This will sample the stack of the given process at 99 times a second for 60 seconds and write the data to a file called perf.data. From there, you can run the following commands from Brendan Gregg’s flame graph library to process the file and generate a flame graph:
perf script | ./stackcollapse-perf.pl > out.perf-folded ./flamegraph.pl out.perf-folded > flame-graph.svg
One of the first flame graphs I created was of a Postgres backend process. Due to our use of connection pooling, a single backend process will serve multiple queries. Since the vast majority of queries we run are INSERT
s, a flame graph of a Postgres backend process would give us a good idea of where the CPU was spent when inserting events into the database. After running the above commands on a pid for a Postgres process I got frompg_stat_activity
, I obtained the following flame graph:
For the uninitiated, a flame graph can be pretty difficult to understand. Brendan Gregg gives the following explanation for how to interpret one:
The x-axis shows the stack profile population, sorted alphabetically (it is not the passage of time), and the y-axis shows stack depth. Each rectangle represents a stack frame. The wider a frame is is, the more often it was present in the stacks. The top edge shows what is on-CPU, and beneath it is its ancestry. The colors are usually not significant, picked randomly to differentiate frames.
It’s pretty clear from the flame graph that ~55% of CPU time is spent inExecOpenIndices
(the large yellow bar in the center right of the image). Looking up the flame graph a tiny bit, it appears that most of the time is split between two different functions, BuildIndexInfo
and index_open
. BuildIndexInfo
calls intoRelationGetIndexPredicate
where ~20% of all CPU time is spent. It looks like the majority of that time is spent in RelationGetIndexPredicate
.
Looking into the source code for RelationGetIndexPredicate, it appears its purpose is to parse and optimize a partial index predicate. It makes sense that so much time is spent inRelationGetIndexPredicate
since parsing an arbitrary expression is much more difficult than evaluating an already parsed expression.
Now let’s look at the rest of the time spent in ExecOpenIndices
. Most of the remaining time is spent in index_open
. It looks like index_open
calls into relation_open
which then calls into RelationIdGetRelation
. From the documentation of RelationIdGetRelation in the source code, its purpose is to lookup the metadata for different relations. (In this case it is mainly being used for looking up the partial indexes.) Based on how the time is spent in RelationGetIndexPredicate
andRelationIdGetRelation
, it appears that Postgres spends a lot more time fetching and parsing the partial index predicates than it does evaluating them.
Implementing a Fix
Looking at the source code for these different functions, there is a significant amount of caching going on. In RelationGetIndexPredicate
, Postgres first checks if it has already extracted the predicate and immediately returns it.
RelationIdGetRelation
first uses RelationIdCacheLookup to check if the relation metadata has already been calculated and cached. It appears that under normal circumstances, the index metadata would be fetched and parsed once, and then read from cache the rest of the time.
Unfortunately for us, the caching doesn’t work well if you’re writing events one at a time to tens of thousands of different tables. Postgres has a pool of processes that it uses to serve queries, and each of these processes keeps its own cache. Every insert is assigned round-robin amongst these processes. When inserting events one at a time, to a sharded schema with tens of thousands of underlying tables, it is unlikely that two inserts going to the same table will be served by the same process. This means that index metadata is almost never cached in the process that’s executing the insert. So, Postgres needs to fetch and parse the index metadata for the destination table once for almost every event we insert.
This suggests a simple change we could make: instead of inserting all of the events individually, we could batch insert many events going to the same table. By using a single command to insert many events, Postgres would only need to fetch and parse the index metadata once per batch. We had thought of batching our inserts before to reduce transaction counts, but never to save CPU resources, as we assumed all the CPU was going towards evaluating index predicates.
Initial benchmarks of batched inserts showed a 10x reduction in CPU usage. Once we obtained these results, we began testing the batched inserts in production. Ultimately, we did get about a 10x improvement to ingestion throughput when using batches of an average size of ~50 events. Here is what our ingestion latency for different kafka partitions looked like right before and after we deployed batching:

After deploying batching, I took another flame graph of inserts:
This time, it appears a large portion of the time is now going to ExecQual
(red bar in the middle), which based on the source code, is the function used to evaluate partial index predicates. That means Postgres is now spending most of the CPU doing the actual work of evaluating partial index predicates.
I made this discovery six months ago. Since then, we haven’t needed to add any additional CPU to our cluster and it doesn’t look like we will need to in the next few months either! I was able to find this win using only rudimentary performance analysis techniques. It really doesn’t take much to find 10x wins.
By the way, if you are interested in doing this kind of work, we are hiring! Apply here or reach out on twitter.
How Basic Performance Analysis Saved Us Millions-------火焰图的更多相关文章
- Linux Performance Analysis and Tools(Linux性能分析和工具)
首先来看一张图: 上面这张神一样的图出自国外一个Lead Performance Engineer(Brendan Gregg)的一次分享,几乎涵盖了一个系统的方方面面,任何人,如果没有完善的计算系统 ...
- Serialization performance analysis
Serialization performance analysis http://www.skyscanner.net/blogs/serialization-performance-analysi ...
- 笔试算法题(58):二分查找树性能分析(Binary Search Tree Performance Analysis)
议题:二分查找树性能分析(Binary Search Tree Performance Analysis) 分析: 二叉搜索树(Binary Search Tree,BST)是一颗典型的二叉树,同时任 ...
- Performance Analysis of Logs (PAL) Tool
Performance Analysis of Logs (PAL) Tool 背景 在众多的独立项目中,我们如何快速了解数据库(SQL Server)服务器的性能,以及数据库的基线情况是怎样的,或者 ...
- 火焰图&perf命令
最近恶补后端技术,发现还是很多不懂,一直写业务逻辑容易迷失,也没有成长.自己做系统,也习惯用自己已知的知识来解决,以后应该多点调研,学到更多的东西应用起来. 先学一个新的性能分析命令. NAME pe ...
- perf + Flame Graph火焰图分析程序性能
1.perf命令简要介绍 性能调优时,我们通常需要分析查找到程序百分比高的热点代码片段,这便需要使用 perf record 记录单个函数级别的统计信息,并使用 perf report 来显示统计结果 ...
- 用 CPI 火焰图分析 Linux 性能问题
https://yq.aliyun.com/articles/465499 用 CPI 火焰图分析 Linux 性能问题 yangoliver 2018-02-11 16:05:53 浏览1076 ...
- linux系统分析工具续-SystemTap和火焰图(Flame Graph)
本文为网上各位大神文章的综合简单实践篇,参考文章较多,有些总结性东西,自认暂无法详细写出,建议读文中列出的参考文档,相信会受益颇多.下面开始吧(本文出自 “cclo的博客” 博客,请务必保留此出处ht ...
- Linux火焰图-ubuntu
关注火焰图非常长的时间了!~~一直未能自己做个火焰图出来.今天小试一把. ubuntu18.04 ssh登陆之后执行命令 安装软件 apt-get install -y linux-cloud-too ...
随机推荐
- javascript sleep方法
function sleep(numberMillis) { var now = new Date(); var exitTime = now.getTime() + numberMi ...
- Django实现文章按年月归档、点赞和评论、发送邮件
文章归档的实现 我们在创建文章时,会在数据库中存储文章创建的时间这样的字段,一般用的都是datetime类型,记录文章创建的年月日和时分秒,所以我们直接使用文章的创建时间分类是无法实现文章的按年月归档 ...
- C++如何取得int型的最大最小值
转:http://www.cnblogs.com/alex4814/archive/2011/09/12/2174173.html 當題目涉及到求最大最小值時,最初的比較數字就應當設置爲INT_MAX ...
- vue 子父组件之间的通信,及在调用组件的地方
这里是用了 element ui 你们也可以看一下管方的文档 http://element.eleme.io/#/zh-CN/component/installation 组件html <div ...
- Jmeter----5.1 设置中文
注意:JMeter5需要Java8 以上,本文环境是Win7 64位 设置永久默认汉化:在Jmeter的安装目录下的bin目录中找到 jmeter.properties这个文件,用文本编辑器打开.在# ...
- iOS控制器与视图加载方法
转载记录, 请看原文: 1. iOS中的各种加载方法(initWithNibName,loadNibNamed,initWithCoder,awakeFromNib等等)简单使用 http://w ...
- java EE : tomacat 基础
tomacat 目录结构 conf 配置文件 server.xml
- linq to sql: 在Entityfamework Core中使用多个DbContext
最近在学习DotNetCore并做一个自己的小项目,分为了多个数据库,AccountDbContext和BlogDbContext, 发blog的时候需要用到Account的信息,但是再Blog中只记 ...
- spring_150910_hibernate_id_auto
package com.spring.model; import javax.persistence.Entity; import javax.persistence.GeneratedValue; ...
- thinkphp5.0生命周期
本篇内容我们对ThinkPHP5.0的应用请求的生命周期做大致的介绍,以便于开发者了解整个执行流程. 1.入口文件 用户发起的请求都会经过应用的入口文件,通常是 public/index.php文件. ...