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

ENGINEERING

How Basic Performance Analysis Saved Us Millions

 Michael Malis

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.

postgresql

时间: 2024-12-30 00:00:38

How Basic Performance Analysis Saved Us Millions-------火焰图的相关文章

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

首先来看一张图: 上面这张神一样的图出自国外一个Lead Performance Engineer(Brendan Gregg)的一次分享,几乎涵盖了一个系统的方方面面,任何人,如果没有完善的计算系统知识,网络知识和操作系统的知识,这张图中列出的工具,是不可能全部掌握的. 出于本人对linux系统的极大兴趣,以及对底层知识的强烈渴望,并作为检验自己基础知识的一个指标,我决定将这里的所有工具学习一遍(时间不限),这篇文章将作为我学习这些工具的学习笔记.尽量组织得方便自己日后查阅,并希望对别人也有一

The Performance Analysis of MENCODER with PARROT

1 ENVIRONMENTS               Table 1 ENV A CPU Intel(R) Xeon(R) CPU E7- 4870 @ 2.40GHz 10-core Memory 100G Linux Version 3.11.0 Ubuntu Version 12.04 2 RESULTS *We could find that MENCODER with PARROT is faster than that without PARROT over four threa

Performance analysis of our own full blown HTTP

In previous post Let's do our own full blown HTTP server with Netty 4 you and I were excited by creation of our own web server. So far so good. But how good? Given ordinary notebook cat /proc/cpuinfo | grep model\ name model name      : Intel(R) Core

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

1.perf命令简要介绍 性能调优时,我们通常需要分析查找到程序百分比高的热点代码片段,这便需要使用 perf record 记录单个函数级别的统计信息,并使用 perf report 来显示统计结果: usage: perf [--version] [--help] [OPTIONS] COMMAND [ARGS] The most commonly used perf commands are: annotate Read perf.data (created by perf record)

Linux火焰图

关注火焰图非常长的时间了!~~一直未能自己做个火焰图出来.今天小试一把. ubuntu18.04 ssh登陆之后执行命令 安装软件 apt-get install -y linux-cloud-tools-generic linux-tools-generic 访问https://github.com/brendangregg/FlameGraph download zip然后解压完传到ubuntu18.04的/root/ 然后找个程序执行(我拿iperf用来做性能测试的对象,你可以用dd或者p

火焰图分析openresty性能瓶颈

注:本文操作基于CentOS 系统 准备工作 用wget从https://sourceware.org/systemtap/ftp/releases/下载最新版的systemtap.tar.gz压缩包,然后解压../configure; make; make install 安装到目标主机:执行命令 stap -ve 'probe begin { log("hello systemtap!") exit() }' 如果提示pass 5: run completed ... 就表示安装成

转发 Java火焰图在Netflix的实践

为了分析不同软件或软件的不同版本使用CPU的情况,相关设计人员通常需要进行函数的堆栈性能分析.相比于定期采样获得数据的方式,利用定时中断来收集程序运行时的PC寄存器值.函数地址以及整个堆栈轨迹更加高效.目前,OProfile.gprof和SystemTap等工具都是采用该方法,给出详细的CPU使用情况报告.然而,这些工具在处理复杂的统计数据时,给出的报告往往过于繁杂.不够直观.不能直接反应分析员所需要的数据.为此,Brendan Gregg开发了专门把采样到的堆栈轨迹(Stack Trace)转

使用perf + FlameGraph生成进程火焰图

FlameGraph代码:https://github.com/cobblau/FlameGraph 使用方法 1,perf record --call-graph dwarf -p 12345 2,perf script | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > process.svg 使用其他工具如何生成火焰图的方法在FlameGraph的Readme只有详细的介绍

perf + 火焰图分析程序性能

1.perf命令简要介绍 性能调优时,我们通常需要分析查找到程序百分比高的热点代码片段,这便需要使用 perf record 记录单个函数级别的统计信息,并使用 perf report 来显示统计结果: perf record perf report 举例: sudo perf record -e cpu-clock -g -p 2548 -g 选项是告诉perf record额外记录函数的调用关系 -e cpu-clock 指perf record监控的指标为cpu周期 -p 指定需要reco