How Basic Performance Analysis Saved Us Millions

May 19, 2017
9 min read

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 INSERTs, 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:

You can click the image to open it in a new tab, then click a rectangle to zoom in. Hovering over a rectangle will pull up some information about the rectangle.

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_openBuildIndexInfo 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 RelationIdGetRelationFrom 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:

The unit on the left is hours of latency. We were able to clear about an hour of backlog in only minutes.

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-------火焰图的更多相关文章

  1. Linux Performance Analysis and Tools(Linux性能分析和工具)

    首先来看一张图: 上面这张神一样的图出自国外一个Lead Performance Engineer(Brendan Gregg)的一次分享,几乎涵盖了一个系统的方方面面,任何人,如果没有完善的计算系统 ...

  2. Serialization performance analysis

    Serialization performance analysis http://www.skyscanner.net/blogs/serialization-performance-analysi ...

  3. 笔试算法题(58):二分查找树性能分析(Binary Search Tree Performance Analysis)

    议题:二分查找树性能分析(Binary Search Tree Performance Analysis) 分析: 二叉搜索树(Binary Search Tree,BST)是一颗典型的二叉树,同时任 ...

  4. Performance Analysis of Logs (PAL) Tool

    Performance Analysis of Logs (PAL) Tool 背景 在众多的独立项目中,我们如何快速了解数据库(SQL Server)服务器的性能,以及数据库的基线情况是怎样的,或者 ...

  5. 火焰图&perf命令

    最近恶补后端技术,发现还是很多不懂,一直写业务逻辑容易迷失,也没有成长.自己做系统,也习惯用自己已知的知识来解决,以后应该多点调研,学到更多的东西应用起来. 先学一个新的性能分析命令. NAME pe ...

  6. perf + Flame Graph火焰图分析程序性能

    1.perf命令简要介绍 性能调优时,我们通常需要分析查找到程序百分比高的热点代码片段,这便需要使用 perf record 记录单个函数级别的统计信息,并使用 perf report 来显示统计结果 ...

  7. 用 CPI 火焰图分析 Linux 性能问题

    https://yq.aliyun.com/articles/465499 用 CPI 火焰图分析 Linux 性能问题   yangoliver 2018-02-11 16:05:53 浏览1076 ...

  8. linux系统分析工具续-SystemTap和火焰图(Flame Graph)

    本文为网上各位大神文章的综合简单实践篇,参考文章较多,有些总结性东西,自认暂无法详细写出,建议读文中列出的参考文档,相信会受益颇多.下面开始吧(本文出自 “cclo的博客” 博客,请务必保留此出处ht ...

  9. Linux火焰图-ubuntu

    关注火焰图非常长的时间了!~~一直未能自己做个火焰图出来.今天小试一把. ubuntu18.04 ssh登陆之后执行命令 安装软件 apt-get install -y linux-cloud-too ...

随机推荐

  1. 批量删除.svn文件夹和.svn文件

    新建可运行文件 Windows环境 将下面的代码保存为 kill-svn.bat文件,放到要删除.svn文件的目录下,双击运行即可 @echo on @rem 删除SVN版本控制目录 @rem for ...

  2. 制作一棵ztree

    我们在做web项目时,常会用到一些树形菜单.在此,我们利用ztree实现树形菜单的效果.zTree 是一个依靠 jQuery 实现的多功能 “树插件”.优异的性能.灵活的配置.多种功能的组合是 zTr ...

  3. Linux软件安装install命令

    install  1.作用 install命令的作用是安装或升级软件或备份数据,它的使用权限是所有用户. 2.格式 (1)install [选项]... 来源 目的地 (2)install [选项]. ...

  4. Codeforces 445A Boredom(DP+单调队列优化)

    题目链接:http://codeforces.com/problemset/problem/455/A 题目大意:有n个数,每次可以选择删除一个值为x的数,然后值为x-1,x+1的数也都会被删除,你可 ...

  5. Codeforces 86D - Powerful array(莫队算法)

    题目链接:http://codeforces.com/problemset/problem/86/D 题目大意:给定一个数组,每次询问一个区间[l,r],设cnt[i]为数字i在该区间内的出现次数,求 ...

  6. NIO-2通道(Channel)

    import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import ...

  7. python 分词库jieba

    算法实现: 基于Trie树结构实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图(DAG) 采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合 对于未登录词,采用了基于汉字 ...

  8. Python全栈开发之8、装饰器详解

    一文让你彻底明白Python装饰器原理,从此面试工作再也不怕了.转载请注明出处http://www.cnblogs.com/Wxtrkbc/p/5486253.html 一.装饰器 装饰器可以使函数执 ...

  9. cent 7.0 安装mysql

    安装命令 wget http://dev.mysql.com/get/mysql-community-release-el7-5.noarch.rpm rpm -ivh mysql-community ...

  10. thinkphp5.0命名空间

    命名空间 ThinkPHP采用命名空间方式定义和自动加载类库文件,有效的解决了多模块和Composer类库之间的命名空间冲突问题,并且实现了更加高效的类库自动加载机制. 如果不清楚命名空间的基本概念, ...