Skip to content

Commit 9a1725d

Browse files
authored
Merge pull request #208722 from jonels-msft/hsc-perf-guide-1
Performance tuning guide
2 parents 1b1f25b + 3a79f48 commit 9a1725d

File tree

2 files changed

+373
-0
lines changed

2 files changed

+373
-0
lines changed

articles/postgresql/TOC.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,8 @@
745745
items:
746746
- name: Monitoring
747747
href: hyperscale/concepts-monitoring.md
748+
- name: Performance tuning
749+
href: hyperscale/concepts-performance-tuning.md
748750
- name: Audit logs
749751
href: hyperscale/concepts-audit.md
750752
- name: Columnar storage
Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
---
2+
title: Performance tuning - Hyperscale (Citus) - Azure Database for PostgreSQL
3+
description: Improving query performance in the distributed database
4+
ms.author: jonels
5+
author: jonels-msft
6+
ms.service: postgresql
7+
ms.subservice: hyperscale-citus
8+
ms.topic: conceptual
9+
ms.date: 08/30/2022
10+
---
11+
12+
# Hyperscale (Citus) performance tuning
13+
14+
[!INCLUDE[applies-to-postgresql-hyperscale](../includes/applies-to-postgresql-hyperscale.md)]
15+
16+
Running a distributed database at its full potential offers high performance.
17+
However, reaching that performance can take some adjustments in application
18+
code and data modeling. This article covers some of the most common--and
19+
effective--techniques to improve performance.
20+
21+
## Client-side connection pooling
22+
23+
A connection pool holds open database connections for reuse. An application
24+
requests a connection from the pool when needed, and the pool returns one that
25+
is already established if possible, or establishes a new one. When done, the
26+
application releases the connection back to the pool rather than closing it.
27+
28+
Adding a client-side connection pool is an easy way to boost application
29+
performance with minimal code changes. In our measurements, running single-row
30+
insert statements goes about **24x faster** on a Hyperscale (Citus) server
31+
group with pooling enabled.
32+
33+
For language-specific examples of adding pooling in application code, see the
34+
[app stacks guide](quickstart-app-stacks-overview.md).
35+
36+
> [!NOTE]
37+
>
38+
> Hyperscale (Citus) also provides [server-side connection
39+
> pooling](concepts-connection-pool.md) using pgbouncer, but it mainly serves
40+
> to increase the client connection limit. An individual application's
41+
> performance benefits more from client- rather than server-side pooling.
42+
> (Although both forms of pooling can be used at once without harm.)
43+
44+
## Scoping distributed queries
45+
46+
### Updates
47+
48+
When updating a distributed table, try to filter queries on the distribution
49+
column--at least when it makes sense, when the new filters don't change the
50+
meaning of the query.
51+
52+
In some workloads, it's easy. Transactional/operational workloads like
53+
multi-tenant SaaS apps or the Internet of Things distribute tables by tenant or
54+
device. Queries are scoped to a tenant- or device-ID.
55+
56+
For instance, in our [multi-tenant
57+
tutorial](tutorial-design-database-multi-tenant.md#use-psql-utility-to-create-a-schema)
58+
we have an `ads` table distributed by `company_id`. The naive way to update an
59+
ad is to single it out like this:
60+
61+
```sql
62+
-- slow
63+
64+
UPDATE ads
65+
SET impressions_count = impressions_count+1
66+
WHERE id = 42; -- missing filter on distribution column
67+
```
68+
69+
Although the query uniquely identifies a row and updates it, Hyperscale (Citus)
70+
doesn't know, at planning time, which shard the query will update. Citus takes a
71+
ShareUpdateExclusiveLock on all shards to be safe, which blocks other queries
72+
trying to update the table.
73+
74+
Even though the `id` was sufficient to identify a row, we can include an
75+
extra filter to make the query faster:
76+
77+
```sql
78+
-- fast
79+
80+
UPDATE ads
81+
SET impressions_count = impressions_count+1
82+
WHERE id = 42
83+
AND company_id = 1; -- the distribution column
84+
```
85+
86+
The Hyperscale (Citus) query planner sees a direct filter on the distribution
87+
column and knows exactly which single shard to lock. In our tests, adding
88+
filters for the distribution column increased parallel update performance by
89+
**100x**.
90+
91+
### Joins and CTEs
92+
93+
We've seen how UPDATE statements should scope by the distribution column to
94+
avoid unnecessary shard locks. Other queries benefit from scoping too, usually
95+
to avoid the network overhead of unnecessarily shuffling data between worker
96+
nodes.
97+
98+
99+
100+
```sql
101+
-- logically correct, but slow
102+
103+
WITH single_ad AS (
104+
SELECT *
105+
FROM ads
106+
WHERE id=1
107+
)
108+
SELECT *
109+
FROM single_ad s
110+
JOIN campaigns c ON (s.campaign_id=c.id);
111+
```
112+
113+
We can speed up the query up by filtering on the distribution column,
114+
`company_id`, in the CTE and main SELECT statement.
115+
116+
```sql
117+
-- faster, joining on distribution column
118+
119+
WITH single_ad AS (
120+
SELECT *
121+
FROM ads
122+
WHERE id=1 and company_id=1
123+
)
124+
SELECT *
125+
FROM single_ad s
126+
JOIN campaigns c ON (s.campaign_id=c.id)
127+
WHERE s.company_id=1 AND c.company_id = 1;
128+
```
129+
130+
In general, when joining distributed tables, try to include the distribution
131+
column in the join conditions. However, when joining between a distributed and
132+
reference table it's not required, because reference table contents are
133+
replicated across all worker nodes.
134+
135+
If it seems inconvenient to add the extra filters to all your queries, keep in
136+
mind there are helper libraries for several popular application frameworks that
137+
make it easier. Here are instructions:
138+
139+
* [Ruby on Rails](https://docs.citusdata.com/en/stable/develop/migration_mt_ror.html),
140+
* [Django](https://docs.citusdata.com/en/stable/develop/migration_mt_django.html),
141+
* [ASP.NET](https://docs.citusdata.com/en/stable/develop/migration_mt_asp.html),
142+
* [Java Hibernate](https://www.citusdata.com/blog/2018/02/13/using-hibernate-and-spring-to-build-multitenant-java-apps/).
143+
144+
## Efficient database logging
145+
146+
Logging all SQL statements all the time adds overhead. In our measurements,
147+
using more a judicious log level improved the transactions per second by
148+
**10x** vs full logging.
149+
150+
For efficient everyday operation, you can disable logging except for errors and
151+
abnormally long-running queries:
152+
153+
| setting | value | reason |
154+
|---------|-------|--------|
155+
| log_statement_stats | OFF | Avoid profiling overhead |
156+
| log_duration | OFF | Don't need to know the duration of normal queries |
157+
| log_statement | NONE | Don't log queries without a more specific reason |
158+
| log_min_duration_statement | A value longer than what you think normal queries should take | Shows the abnormally long queries |
159+
160+
> [!NOTE]
161+
>
162+
> The log-related settings in our managed service take the above
163+
> recommendations into account. You can leave them as they are. However, we've
164+
> sometimes seen customers change the settings to make logging aggressive,
165+
> which has led to performance issues.
166+
167+
## Lock contention
168+
169+
The database uses locks to keep data consistent under concurrent access.
170+
However, some query patterns require an excessive amount of locking, and faster
171+
alternatives exist.
172+
173+
### System health and locks
174+
175+
Before diving into common locking inefficiencies, let's see how to view locks
176+
and activity throughout the database cluster. The
177+
[citus_stat_activity](reference-metadata.md#distributed-query-activity) view
178+
gives a detailed view.
179+
180+
The view shows, among other things, how queries are blocked by "wait events,"
181+
including locks. Grouping by
182+
[wait_event_type](https://www.postgresql.org/docs/14/monitoring-stats.html#WAIT-EVENT-TABLE)
183+
paints a picture of system health:
184+
185+
```sql
186+
-- general system health
187+
188+
SELECT wait_event_type, count(*)
189+
FROM citus_stat_activity
190+
WHERE state != 'idle'
191+
GROUP BY 1
192+
ORDER BY 2 DESC;
193+
```
194+
195+
A NULL `wait_event_type` means the query isn't waiting on anything.
196+
197+
If you do see locks in the stat activity output, you can view the specific
198+
blocked queries using `citus_lock_waits`:
199+
200+
```sql
201+
SELECT * FROM citus_lock_waits;
202+
```
203+
204+
For example, if one query is blocked on another trying to update the same row,
205+
you'll see the blocked and blocking statements appear:
206+
207+
```
208+
-[ RECORD 1 ]-------------------------+--------------------------------------
209+
waiting_gpid | 10000011981
210+
blocking_gpid | 10000011979
211+
blocked_statement | UPDATE numbers SET j = 3 WHERE i = 1;
212+
current_statement_in_blocking_process | UPDATE numbers SET j = 2 WHERE i = 1;
213+
waiting_nodeid | 1
214+
blocking_nodeid | 1
215+
```
216+
217+
To see not only the locks happening at the moment, but historical patterns, you
218+
can capture locks in the PostgreSQL logs. To learn more, see the
219+
[log_lock_waits](https://www.postgresql.org/docs/current/runtime-config-logging.html#GUC-LOG-LOCK-WAITS)
220+
server setting in the PostgreSQL documentation. Another great resource is
221+
[seven tips for dealing with
222+
locks](https://www.citusdata.com/blog/2018/02/22/seven-tips-for-dealing-with-postgres-locks/)
223+
on the Citus Data Blog.
224+
225+
### Common problems and solutions
226+
227+
#### DDL commands
228+
229+
DDL Commands like `truncate`, `drop`, and `create index` all take write locks,
230+
and block writes on the entire table. Minimizing such operations reduces
231+
locking issues.
232+
233+
Tips:
234+
235+
* Try to consolidate DDL into maintenance windows, or use them less often.
236+
237+
* PostgreSQL supports [building indices
238+
concurrently](https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY),
239+
to avoid taking a write lock on the table.
240+
241+
* Consider setting
242+
[lock_timeout](https://www.postgresql.org/docs/14/runtime-config-client.html#GUC-LOCK-TIMEOUT)
243+
in a SQL session prior to running a heavy DDL command. With `lock_timeout`,
244+
PostgreSQL will abort the DDL command if the command waits too long for a write
245+
lock. A DDL command waiting for a lock can cause later queries to queue behind
246+
itself.
247+
248+
#### Idle in transaction connections
249+
250+
Idle (uncommitted) transactions sometimes block other queries unnecessarily.
251+
For example:
252+
253+
```sql
254+
BEGIN;
255+
256+
UPDATE ... ;
257+
258+
-- Suppose the client waits now and doesn't COMMIT right away.
259+
--
260+
-- Other queries that want to update the same rows will be blocked.
261+
262+
COMMIT; -- finally!
263+
```
264+
265+
To manually clean up any long-idle queries on the coordinator node, you can run
266+
a command like this:
267+
268+
```sql
269+
SELECT pg_terminate_backend(pid)
270+
FROM pg_stat_activity
271+
WHERE datname = 'citus'
272+
AND pid <> pg_backend_pid()
273+
AND state in ('idle in transaction')
274+
AND state_change < current_timestamp - INTERVAL '15' MINUTE;
275+
```
276+
277+
PostgreSQL also offers an
278+
[idle_in_transaction_session_timeout](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-IDLE-IN-TRANSACTION-SESSION-TIMEOUT)
279+
setting to automate idle session termination.
280+
281+
#### Deadlocks
282+
283+
Citus detects distributed deadlocks and cancels their queries, but the
284+
situation is less performant than avoiding deadlocks in the first place. A
285+
common source of deadlocks comes from updating the same set of rows in a
286+
different order from multiple transactions at once.
287+
288+
For instance, running these transactions in parallel:
289+
290+
Session A:
291+
292+
```sql
293+
BEGIN;
294+
UPDATE ads SET updated_at = now() WHERE id = 1 AND company_id = 1;
295+
UPDATE ads SET updated_at = now() WHERE id = 2 AND company_id = 1;
296+
```
297+
298+
Session B:
299+
300+
```sql
301+
BEGIN;
302+
UPDATE ads SET updated_at = now() WHERE id = 2 AND company_id = 1;
303+
UPDATE ads SET updated_at = now() WHERE id = 1 AND company_id = 1;
304+
305+
-- ERROR: canceling the transaction since it was involved in a distributed deadlock
306+
```
307+
308+
Session A updated ID 1 then 2, whereas the session B updated 2 then 1. Write
309+
SQL code for transactions carefully to update rows in the same order. (The
310+
update order is sometimes called a "locking hierarchy.")
311+
312+
In our measurement, bulk updating a set of rows with many transactions went
313+
**3x faster** when avoiding deadlock.
314+
315+
## I/O during ingestion
316+
317+
I/O bottlenecking is typically less of a problem for Hyperscale (Citus) than
318+
for single-node PostgreSQL because of sharding. The shards are individually
319+
smaller tables, with better index and cache hit rates, yielding better
320+
performance.
321+
322+
However, even with Hyperscale (Citus), as tables and indices grow larger, disk
323+
I/O can become a problem for data ingestion. Things to look out for are an
324+
increasing number of 'IO' `wait_event_type` entries appearing in
325+
`citus_stat_activity`:
326+
327+
```sql
328+
SELECT wait_event_type, wait_event count(*)
329+
FROM citus_stat_activity
330+
WHERE state='active'
331+
GROUP BY 1,2;
332+
```
333+
334+
Run the above query repeatedly to capture wait event related information. Note
335+
how the counts of different wait event types change.
336+
337+
Also look at [metrics in the Azure portal](concepts-monitoring.md),
338+
particularly the IOPS metric maxing out.
339+
340+
Tips:
341+
342+
- If your data is naturally ordered, such as in a time series, use PostgreSQL
343+
table partitioning. See [this
344+
guide](https://docs.citusdata.com/en/stable/use_cases/timeseries.html) to learn
345+
how to partition distributed tables in Hyperscale (Citus).
346+
347+
- Remove unused indices. Index maintenance causes I/O amplification during
348+
ingestion. To find which indices are unused, use [this
349+
query](howto-useful-diagnostic-queries.md#identifying-unused-indices).
350+
351+
- If possible, avoid indexing randomized data. For instance, some UUID
352+
generation algorithms follow no order. Indexing such a value causes a lot
353+
overhead. Try a bigint sequence instead, or monotonically increasing UUIDs.
354+
355+
## Summary of results
356+
357+
In benchmarks of simple ingestion with INSERTs, UPDATEs, transaction blocks, we
358+
observed the following query speedups for the techniques in this article.
359+
360+
| Technique | Query speedup |
361+
|-----------|---------------|
362+
| Scoping queries | 100x |
363+
| Connection pooling | 24x |
364+
| Efficient logging | 10x |
365+
| Avoiding deadlock | 3x |
366+
367+
## Next steps
368+
369+
* [Advanced query performance tuning](https://docs.citusdata.com/en/stable/performance/performance_tuning.html)
370+
* [Useful diagnostic queries](howto-useful-diagnostic-queries.md)
371+
* Build fast [app stacks](quickstart-app-stacks-overview.md)

0 commit comments

Comments
 (0)