<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Vasilii Petrushin</title>
    <description>The latest articles on Forem by Vasilii Petrushin (@redlineeyes).</description>
    <link>https://forem.com/redlineeyes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2249895%2Fe5b84743-e050-4e84-be7f-63030512c101.png</url>
      <title>Forem: Vasilii Petrushin</title>
      <link>https://forem.com/redlineeyes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/redlineeyes"/>
    <language>en</language>
    <item>
      <title>Applications in prod. How to handle skyrocket growth with caching.</title>
      <dc:creator>Vasilii Petrushin</dc:creator>
      <pubDate>Fri, 28 Mar 2025 14:54:37 +0000</pubDate>
      <link>https://forem.com/redlineeyes/applications-in-prod-how-to-handle-skyrocket-growth-with-caching-6i2</link>
      <guid>https://forem.com/redlineeyes/applications-in-prod-how-to-handle-skyrocket-growth-with-caching-6i2</guid>
      <description>&lt;p&gt;We are lucky, and the company we work at has skyrocketed. In the previous steps, &lt;a href="https://dev.to/redlineeyes/postgres-in-prod-part-1-setup-and-startup-4fl1"&gt;we set up the Postgres database(s) well&lt;/a&gt;, set up monitoring and alerting, &lt;a href="https://dev.to/redlineeyes/postgres-in-prod-part-2-problem-solving-and-optimizations-heading-to-10x-growth-2fko"&gt;optimized our data and queries&lt;/a&gt;, and even switched the application to read from read replicas. And now we have so high request rate that our app and Postgres can’t handle it. There are many tricks to increase Postgres productivity. But in the end, to apply a long-term fix, we have to fight the root cause of our problems. We have only one option: to decrease the QPS (Query Per Second) on our Postgres instances and decrease the database size. Real life with a highly loaded system is complex and full of pain, and we don’t want to get lost with it. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa2nh8ehw8amxyn2m9vds.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa2nh8ehw8amxyn2m9vds.png" alt="Great success!" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
We have 6 basic strategies to fight QPS and table sizes as a long-term solution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;caching,&lt;/li&gt;
&lt;li&gt;rebuilding business processes to decrease SQL queries numbers required to process the transaction,&lt;/li&gt;
&lt;li&gt;Split application to microservice architecture and move tables used by microservice to other instances or database technology, like switching to NoSQL databases.&lt;/li&gt;
&lt;li&gt;Switching to asynchronous transaction processing,&lt;/li&gt;
&lt;li&gt;tables partitioning,&lt;/li&gt;
&lt;li&gt;sharding the database, or splitting transaction processing from one database cluster to multiple.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Following only one strategy will not guide us to success; in real life, it is always mixed.&lt;/p&gt;

&lt;p&gt;Let’s discuss how it is better to apply these strategies. It is a huge topic, so in this article, we start with cashing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching
&lt;/h2&gt;

&lt;p&gt;Caching is a brilliant strategy to decrease both QPS and API latency heavily. It is applicable when the data and content produced by the application and/or results of API calls are the same for a group of users at some period of time and, therefore, can be cached. The main things - you should have a clear caching rules (why, when, what to cache), TTL for cached content, and a fallback algorithm. There are 3 main approaches to apply caching:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;in-app caching like cachetools for Python, Caffeine Cache for Java/SpringBoot, etc.,&lt;/li&gt;
&lt;li&gt;use a key-value in-memory database like Redis or Memcached,&lt;/li&gt;
&lt;li&gt;use an external app for caching like reverse proxy - nginx, traefik, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes, terms like ‘cache tiering’ are being used, but I don’t like it; the most successful high-load applications typically use all ‘tiers’ at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benefits of in-app caching:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;cached data can be shared with and controlled by all classes and functions in the worker process,&lt;/li&gt;
&lt;li&gt;no dependency on any third-party infrastructure application like an external Nginx cache, Redis, or Memcached,&lt;/li&gt;
&lt;li&gt;The FASTEST cache solution for apps ever, provides maximum LATECY DECREASING effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Weaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the data in the cache is NOT PERSISTENT (*), any restart of the worker process will completely destroy the cache,&lt;/li&gt;
&lt;li&gt;requires additional memory for application worker process, and it is always limited,&lt;/li&gt;
&lt;li&gt;dedicated to a single worker process and cannot be shared between multiple processes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Taking into account benefits and weaknesses, in-app caching best suits any short-lived, frequently-accessible, small-sized data, like session details, tokens, feature flags, counters - for throttling, accounting, and metrics, user/sessions limits, etc. Some weaks can be mitigated; for example, on worker process start sequence, the local in-app cache can be preheated with useful data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benefits of key-value databases for caching - Redis and its clones, Memcached, etc:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;data can be distributed between application worker processes&lt;/li&gt;
&lt;li&gt;atomic operations, operations like INCR/DECR without race conditions&lt;/li&gt;
&lt;li&gt;some PERSISTENCY features&lt;/li&gt;
&lt;li&gt;sharding and high-availability features&lt;/li&gt;
&lt;li&gt;data structures like Strings, Lists, Sets, Sorted Sets, Hashes, and more&lt;/li&gt;
&lt;li&gt;housekeeping policies like LRU, LFU, and TTL-based eviction&lt;/li&gt;
&lt;li&gt;Pub/Sub &amp;amp; Streams: can be used for messaging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Weaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Even with persistency features enabled, you may LOOSE some DATA on Redis restarts,&lt;/li&gt;
&lt;li&gt;it is an in-memory databases, and the total cache size is still limited.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, key-value databases are suitable for distributed caching and messaging, real-time analytics, distributed locking, leaderboards, session storages, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  With external caching proxies,
&lt;/h2&gt;

&lt;p&gt;the responses could be sent to the client before reaching the application server. The benefits of external caching on proxies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduces large amounts of requests to the application workers with very low latencies,&lt;/li&gt;
&lt;li&gt;reduces bandwidth to your servers if some kind of CDN is being used, like CloudFlare, AWS CloudFront, Google Cloud CDN, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Weaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;supports only HTTP/HTTPS protocols&lt;/li&gt;
&lt;li&gt;less flexibility, can cache only entire response, not all data can be cached due to security reasons, etc,&lt;/li&gt;
&lt;li&gt;hard to maintain cache keys, proxy configs can be very complicated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Very important to understand the caching rules.
&lt;/h2&gt;

&lt;p&gt;Here are some basic questions to answer to build the caching rules and choose the caching strategies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What data to cache, what are the data types, what is the size, etc?&lt;/li&gt;
&lt;li&gt;What will the outcome be from caching - how much QPS are we gonna save, how faster is the API call going to be, etc?&lt;/li&gt;
&lt;li&gt;Security - What if data will be exposed to another worker process or another class/function in your app? In the case of Redis, what if data can be accessible with the terminal and redis-cli?&lt;/li&gt;
&lt;li&gt;What should cache keys look like?&lt;/li&gt;
&lt;li&gt;What group of users/API calls/business processes/whatever grouping we have will be affected by the caching algorithm?&lt;/li&gt;
&lt;li&gt;What about sharing the cache between processes?&lt;/li&gt;
&lt;li&gt;etc&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TTL
&lt;/h2&gt;

&lt;p&gt;Just believe me and never use cached data without TTL! It is the basic metric and easiest way to refresh the stale data and free the cache space if data is not needed anymore. Also, if the cache supports eviction policies like LRU or LFU, the policy should be set, too. TTL and eviction policies help to keep your cache operational and automate housekeeping processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fallback.
&lt;/h2&gt;

&lt;p&gt;Fallbacks are the most important part of building a caching system. Fallbacks should handle any exceptions with caching: refresh the cache if data is stale with TTL expired,  recalculate and update cache if no data available by the key, do the calculations if cache is not available (any Redis error, network issue between worker process and Redis, in-app cache is disabled, etc). In other words, fallbacks allow applications to work even if caches are not available at all. IMHO, a well-designed system should keep working and at least handle the median workload without any 503/502 or other errors, even if caches become unavailable by an incident or by misconfiguration. It may work with higher latencies or may go asynchronous, but it keeps working and serving clients in any case. But it could happen in a perfect world only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache preheat.
&lt;/h2&gt;

&lt;p&gt;In theory, your app should work without caches, but real life is more interesting. Usually, applications can’t work and/or can’t handle the workload without data in caches. You will definitely lose the data in in-app caches on restarts, or you could lose data in Redis or proxy caches, and it will affect the application performance. To avoid performance degradation after data loss, you need to fill caches with data. This process is called cache preheat. Preheating routines usually run on application restarts, but sometimes the preheating runs periodically like cron does to refresh the data in caches.&lt;/p&gt;

&lt;p&gt;And, of course, your Grafana or an Application Performance Monitoring (APM) tool must have enough metrics and graphs to see the outcome and efficiency of your efforts to decrease QPS and latencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Caching is a great strategy to decrease QPS and latency.&lt;/li&gt;
&lt;li&gt;We have in-app, external, and proxy caching solutions, and all of them have their own benefits and weaknesses. Should consider it when applying.&lt;/li&gt;
&lt;li&gt;Always keep clear caching rules and policies - what data and how to cache.&lt;/li&gt;
&lt;li&gt;TTL and fallbacks should always be applied, it is a hygienic factor.&lt;/li&gt;
&lt;li&gt;To prevent performance degratation if data in caches has lost consider use cache preheating.&lt;/li&gt;
&lt;li&gt;Always use the graphs, metrics, and numbers before and after applying caching to evaluate your efforts and make your clients and bosses happy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PS: I use AI a lot as a learning partner or an advisor but not in production. No AI was used to write this article.&lt;/p&gt;

&lt;p&gt;(*) For clarity, you can persist the in-app cache, for example, as Nginx does. But it is a very complicated topic with a lot of factors to be taken into account, like security, IOPSes, housekeeping, surviving on restarts, etc. By the current most popular common backend application design approach, the application worker processes are stateless, we consider the in-app cache is not persistent.&lt;/p&gt;

</description>
      <category>highload</category>
      <category>devops</category>
      <category>database</category>
    </item>
    <item>
      <title>How to Postgres indexes review</title>
      <dc:creator>Vasilii Petrushin</dc:creator>
      <pubDate>Tue, 25 Feb 2025 15:02:54 +0000</pubDate>
      <link>https://forem.com/redlineeyes/postgres-indexes-analysis-26pa</link>
      <guid>https://forem.com/redlineeyes/postgres-indexes-analysis-26pa</guid>
      <description>&lt;p&gt;Hey, in addition to &lt;a href="https://dev.to/redlineeyes/postgres-in-prod-part-2-problem-solving-and-optimizations-heading-to-10x-growth-2fko"&gt;Part 2&lt;/a&gt;, I recently started to review indexes in one of my prod databases. I would say, it turned out to be very educational.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy6smain8xer8pncmo00w.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy6smain8xer8pncmo00w.jpeg" alt="Work a bit smarter" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The database has been developed for a few years, and thousands of new features and migrations have been applied. I was surprised, at how many indexes have been created and never used, or did not used for a long time.&lt;/p&gt;

&lt;p&gt;Why index management is so important? The indexes are costly! The costs of indexing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Indexes are eating storage, you need high-speed, highly-available storage for that.&lt;/li&gt;
&lt;li&gt;Indexing takes CPU and some IOPSes on each INSERT/UPDATE/DELETE query, and gives additional latency on these queries.&lt;/li&gt;
&lt;li&gt;Indexes take time to rebuild on database restore during incident recovery.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Any index is a trade-off. On one side it accelerates queries and reduces IOPSes, on another it creates an additional workload.&lt;/p&gt;

&lt;p&gt;At the last database review, I found unused indexes for 50GB on a 600GB database. How am I found that?&lt;br&gt;
How to find unused indexes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Unused indexes
SELECT 
    relname AS table_name, 
    indexrelname AS index_name, 
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0  -- No scans
ORDER BY pg_relation_size(indexrelid) DESC;
-- Total size of unused indexes
SELECT 
    pg_size_pretty(SUM(pg_relation_size(indexrelid))) AS indexes_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unused indexes are sorted by size and it is a priority to fix first. The total size of unused indexes shows how bad the whole situation is.&lt;/p&gt;

&lt;p&gt;How to find indexes, that cover the same columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Duplicate indexes cover the same columns
SELECT 
    indrelid::regclass AS table_name,
    array_agg(indexrelid::regclass) AS duplicate_indexes
FROM pg_index
GROUP BY indrelid, indkey
HAVING COUNT(*) &amp;gt; 1;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, it is very interesting and a good reason to review the indexes.&lt;/p&gt;

&lt;p&gt;A sorted table to see the percentage of idx_scan/(seq_scan + idx_scan) when using the table. A good occasion to review the index coverage of tables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- Index usage vs seq scan
SELECT
    relname AS table_name,
    seq_scan, seq_tup_read,
    idx_scan, idx_tup_fetch,
    round(100 * idx_scan::numeric / NULLIF(seq_scan + idx_scan, 0), 2) AS index_usage_percent
FROM pg_stat_user_tables
ORDER BY index_usage_percent DESC NULLS LAST;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query example for migrations, that drops the unused index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALTER TABLE table_name DROP INDEX IF EXISTS index_name;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reasons, why this index "crisis" has happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a lot of new features were added and &lt;em&gt;removed&lt;/em&gt; in the application, and indexes have been created, but we forgot to review and remove indexes;&lt;/li&gt;
&lt;li&gt;a lot of indexes have been created because it was obvious - developers plan to filter or join by these columns, but Postgres plan in real life does not want to use it;&lt;/li&gt;
&lt;li&gt;indexes have never been reviewed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conclusions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Indexing is required to run queries, but it is costly for the CPU on INSERTs/UPDATEs/DELETEs, and takes extra storage space.&lt;/li&gt;
&lt;li&gt;Indexes review should be included in the regular database audit and maintenance procedures.&lt;/li&gt;
&lt;li&gt;Here are the extremely useful queries to get an overview of the current situation with indexes for your Postgres database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;PS: I use AI a lot as a learning partner or an advisor, but not in production. No AI was used to write this article.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>indexes</category>
      <category>database</category>
      <category>devops</category>
    </item>
    <item>
      <title>Postgres in prod part 2. Problem-solving and optimizations, heading to 10x growth.</title>
      <dc:creator>Vasilii Petrushin</dc:creator>
      <pubDate>Fri, 31 Jan 2025 12:41:45 +0000</pubDate>
      <link>https://forem.com/redlineeyes/postgres-in-prod-part-2-problem-solving-and-optimizations-heading-to-10x-growth-2fko</link>
      <guid>https://forem.com/redlineeyes/postgres-in-prod-part-2-problem-solving-and-optimizations-heading-to-10x-growth-2fko</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/redlineeyes/postgres-in-prod-part-1-setup-and-startup-4fl1"&gt;part 1.&lt;/a&gt; we discussed how to build the Postgres cluster and supplementary infra to start the project in production. I assume, we were lucky with the startup and it began to grow. In my experience, at this point the baby’s problems and stupid bugs are solved, the database size is more than 350GB of data, we have a couple of tables of more than 50-80 GB, QPS (Queries Per Second) is 3000 - 20 000 queries per second at the peak times, and so on. The users/clients count increased by 2x-10x per year or more, and growth and workloads become more or less predictable and follow our marketing activities. Now we have other, more complicated challenges.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvelix0r5d7cl4duyprte.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvelix0r5d7cl4duyprte.jpg" alt="Pain and gain!" width="651" height="383"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Solving the issues
&lt;/h2&gt;

&lt;p&gt;Now our app started to crash at peak time, and we have 5xx - HTTP timeouts and server errors, or 499 caused by client timeouts. Many possible reasons appear in different ways. We started digging logs with the why-why-why method and on the next “why” found that in most cases the root cause is increased Postgres latencies.&lt;/p&gt;

&lt;p&gt;The “why” way to the root cause could be indirect, for example, you see in the logs API throws timeouts because can’t obtain a database connection from its own pool. Why? Because all connections are busy serving other earlier requests, and all older requests are waiting in the queue. Why is that? Because average SQL query times become much longer than before. The same indirect case - HTTP-worker pool could be busy, and HTTP requests could wait in your application queue until client timeout, and you could get 499 in the log. In the easiest case, we could see the API endpoint with the long-running query, which leads to the 502 timeout or 503 server error. All of these cases could produce different visual effects on the application, logging, monitoring, and alerting behavior, but all of them are caused by increased Postgres times. The bad is, and it is proven by my life, that increasing HTTP or database connections pool sizes in the application config would not help and could made the things worse. As well as increasing CPU/memory/IOPSes for Postgres would not help too. Why?...&lt;/p&gt;

&lt;p&gt;But how to diagnose and fix issues? First of all, you need a set of monitoring tools, as described in &lt;a href="https://dev.to/redlineeyes/postgres-in-prod-part-1-setup-and-startup-4fl1"&gt;Part 1&lt;/a&gt;.&lt;br&gt;
In the easiest case, when the situation is clear you can extract a timeouting query from logs or from the monitoring system and you can go and debug it.&lt;br&gt;
In harder cases, I start the diagnosis process like this.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Dashboards - the starting point
&lt;/h2&gt;

&lt;p&gt;Check the Postgres dashboard in Grafana for CPU load and QPS - it should be at higher levels than usual. Just in case, if we have no other (network or...) issues.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Digging logs
&lt;/h2&gt;

&lt;p&gt;Ensure the slow queries in Postgres logs are enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;log_min_duration_statement: "100ms"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would recommend then starting with the &lt;a href="https://grafana.com/oss/loki/" rel="noopener noreferrer"&gt;Loki&lt;/a&gt; query to get queries running for more than 1 second to exclude more effective queries and focus on the heaviest queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{namespace="production-db",pod="postgres-0"} |~ "duration: \\d{4,}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For all long-running queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{namespace="production-db",pod="postgres-0"} |~ "duration: "
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Blocking and blocked queries
&lt;/h2&gt;

&lt;p&gt;If you get a lot of queries - usually COMMIT, UPDATE …, or SELECT … FOR UPDATE - running more than 30 seconds - it is a symptom of locking. In that case, let’s check the blocked queries in psql console.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- blocked queries
select pid, username, pg_blocking_pids(pid) as blocked_by, 
query as blocked_query
from pg_stat_activity
where cardinality(pg_blocking_pids(pid)) &amp;gt; 0;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Resolving locks
&lt;/h2&gt;

&lt;p&gt;If found locks, resolve by killing the locking queries, and check if locking and blocked queries are optimal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT pg_cancel_backend(pid) -- for graceful stop
SELECT pg_terminate_backend(pid) -- forced query termination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it with caution, in general, it is safe to kill locked queries because they are waiting for execution permission and do nothing at the moment of termination. If it's inside the transaction block - it is also safe, the transaction will be just rolled back.&lt;/p&gt;

&lt;p&gt;To analyze &lt;code&gt;SELECT … FOR UPDATE&lt;/code&gt; - run &lt;code&gt;EXPLAIN ANALYZE SELECT …&lt;/code&gt; without &lt;code&gt;FOR UPDATE&lt;/code&gt;, and ensure that the query uses indexes and filters without sequential scans. If it runs with seqscans create additional indexes.&lt;br&gt;
How to analyze the UPDATE against production data?&lt;br&gt;
It is scary to run UPDATE on a production database, but what to do if you fall into an incident and have to fix it ASAP? In some cases, we have a safe way to debug UPDATE or INSERT queries by using transactions. Be careful, if UPDATE runs on a huge table and changes a lot of records - it could run for hours and could lock the table for writing for a long time to perform UPDATE as it is, and then ROLLBACK.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- easy way if we're lucky
BEGIN;
EXPLAIN ANALYZE UPDATE …;
ROLLBACK;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we have a huge table and our UPDATE changes a lot of records, then we can analyze SELECT with the same WHERE part.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- safest way is the table is huge and UPDATE changes a lot of rows
EXPLAIN ANALYZE SELECT * FROM table WHERE … here are the conditions from the UPDATE query.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If EXPLAIN reports that the query runs with seqscans - create additional indexes.&lt;/p&gt;

&lt;p&gt;If the queries are already good, but you are still having issues - congrats, you and your developers must fix the application logic to avoid locking. There are three major ways to go - change the application logic to utilize an append-only approach, change the logic to make such update requests asynchronous, or decrease the overall QPS to allow the Postgres instance to focus on the activities, which causes the locks. The third way may be cheaper at the moment of the incident but will not solve the root cause of locking. I want to write an additional article on these approaches, and how they can help to survive with high loads. The good is that often resolving the locks manually immediately helps to bring your application up until the next deadlock, so your team has some time to develop and apply the hotfix.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Temporary files
&lt;/h2&gt;

&lt;p&gt;If there are no locks, and/or most long-running queries are below 30 seconds - with the high CPU usage - are the symptoms of nonoptimal queries, nonoptimal data structures, or high temporary files usage. We need to dig deeper. Ensure that we have temp_files logs enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;log_temp_files: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two ways to check if queries use temporary files. One way - Loki queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{app="postgres", log~="temporary file"}
{namespace="production-db", pod=~"postgres.*"} |= "temporary"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second - check temp_blks_read and temp_blks_written:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT * FROM pg_stat_statements WHERE 
temp_blks_read &amp;gt; 0 OR temp_blks_written &amp;gt; 0;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, you could check the Grafana or Postgres pod directly if it uses high I/O and has high iowait. If you use remote data storage like AWS Elastic Block Storage volume, it has limited IOPSes, and absolute numbers of IOPSes could be relatively small, but Postgres container will consume high CPU iowait!&lt;/p&gt;

&lt;p&gt;If high temporary file usage is confirmed, increase the &lt;code&gt;work_mem&lt;/code&gt; and &lt;code&gt;maintenance_work_mem&lt;/code&gt; Postgres parameters and memory requests/limits in the Kubernetes manifest. Be careful; changing these parameters requires a restart of Postgres instance. If it is unclear from logs or our &lt;code&gt;temp_blks_written&lt;/code&gt; analysis how much &lt;code&gt;work_mem&lt;/code&gt; to add, I recommend upgrading with big steps, like increasing 2x from the current.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Nonoptimal queries and data.
&lt;/h2&gt;

&lt;p&gt;The most common cause of long-running SELECT queries is sequential scan when Postgres applies filters or joins by scanning the entire table. Imagine, how long will it take if your table has 100+GB of data. The query could take minutes with significant I/O consumption. How do we detect it?&lt;/p&gt;

&lt;p&gt;The symptoms are, together or separately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;high IOPSes in Grafana or iotop output, high iowait&lt;/li&gt;
&lt;li&gt;the query appears in the Postgres log with a duration of more than 100ms (we enabled the log of long-running queries in Part 1.),&lt;/li&gt;
&lt;li&gt;in Grafana query analysis this query appears with high time consumption,&lt;/li&gt;
&lt;li&gt;502/503 errors in your app API calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Analyzing the query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM wallet_transaction WHERE currency_id = 10;
Seq Scan on wallet_transaction  (cost=0.00..25000000.00 rows=10000 width=100) (actual time=0.100..7500.500 rows=10000 loops=1)
  Filter: (currency_id = 10)
  Buffers: shared hit=1571 read=60955 dirtied=739
  I/O Timings: shared read=6878.111
  Rows Removed by Filter: 5000000
Planning Time: 0.200 ms
Execution Time: 7502.300 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a very primitive query far from real life, but showing the problem of sequential scans: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unpredictable execution cost (cost from zero to high numbers, not matching with actual running time),&lt;/li&gt;
&lt;li&gt;high buffer usage,&lt;/li&gt;
&lt;li&gt;high I/O consumption.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It means that we cannot predict the actual load, generated by this query, the cache and memory usage is high and could be ineffective, and we could abuse the cache, which is limited, for other queries. Also, we’re abusing the disk I/O, which is critical and limited for clouds and its managed database solutions. High disk I/O also leads to high CPU iowait utilization, it just steals CPU for other tasks. Happily, in the most cases the solution is simple - to create the right index for your table. With index, Postgres will perform the index scan and you can save from 100x to even 10 000x of I/O and buffers. In general, to prevent/decrease such issues set the rule for your project to perform the EXPLAIN for all SELECT queries at the code development stage. And keep this problem in mind when developing the database and table structure. In my experience, the sequential scan is acceptable and more effective for small tables up to a few thousand rows. For bigger tables indexes and index scans are necessary.&lt;/p&gt;

&lt;p&gt;In some cases, when you have complex filters, the Postgres planner performs the sequential scans even if you have proper indexes. In those cases, consider creating indexes with more complex expressions, order, and other parameters, matching the SELECT query WHERE expressions. Or create composite indexes by 2-3 columns if it is closer to WHERE expressions. Also, check the &lt;code&gt;random_page_cost&lt;/code&gt; parameter and consider lowering it. For SSD it should be between 1.1 and 1.5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventive actions
&lt;/h2&gt;

&lt;p&gt;Earlier we discussed a lot of pain, but the good is that we can avoid the most. We can identify and fix problematic queries peacefully, during working hours, with proper testing, and without issues to the clients. In my routine, I just set up in the Calendar application the recurring event on each second Monday of each month to preserve time for Postgres analysis. Once a month I look at the dashboards, dig the logs, and play with the Postgres console to find, identify, and analyze potential problems, queries, and application behavior. The result is the tickets to our developers and to my team. For complex cases I prepare the information to discuss the potential issues and prevention actions with the team and with the boss.&lt;/p&gt;

&lt;p&gt;Depending on the database change rate and release cycle of your app you can set up the Postgres analysis sessions once in 2 or 4 weeks.&lt;/p&gt;

&lt;p&gt;Very helpful to prevent the issues is to accept the EXPLAIN rule in your organization. According to this rule, when your backenders developing something, they must check the &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; for all new or modified SELECTs, SELECTs … FOR UPDATE, and UPDATE queries, and ensure that all these queries run effectively.&lt;/p&gt;

&lt;p&gt;With these simple steps in a couple of months, you will optimize your database and prevent a lot of issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  You did a lot of optimizations, but it did not help
&lt;/h2&gt;

&lt;p&gt;You did a lot of optimizations, increased IOPSes, and added CPU and memory resources, fixed non-optimal queries, but it did not help. Maybe, it is time to switch application reads to read replicas? There are a couple of problems with that, but it surely helps a lot to focus the CPU, memory, and IOPSes to process transactions on the cluster primary and move all read workload and analytics to replicas. The pain points of read replicas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read replica needs for near-real-time data synchronization with the primary&lt;/li&gt;
&lt;li&gt;each read replica requires the same resources as the primary, the cluster with primary and 2 read replicas would take 3x CPU, 3x RAM, and 3x disk space&lt;/li&gt;
&lt;li&gt;your application has to support reads from another database, and you need application reconfiguration or fixes to use read replicas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;IMHO the ideal architecture with read replicas is 1 primary and 2 replicas with small &lt;code&gt;max_standby_archive_delay&lt;/code&gt; - I use from 15 to 30 seconds for production, and 1 replica outside of cluster with huge &lt;code&gt;max_standby_archive_delay&lt;/code&gt; - up to 30-90 minutes.&lt;br&gt;
Why is that?&lt;br&gt;
For application reads and to prevent data losses we need a replica with the most actual data as possible, and a small &lt;code&gt;max_standby_archive_delay&lt;/code&gt; will help us to achieve it. It will stop any queries on read replicas to catch up with the primary if the replication lag becomes &amp;gt;= &lt;code&gt;max_standby_archive_delay&lt;/code&gt;.&lt;br&gt;
For high availability, we need two replicas behind the pooler to read from the application if we need any maintenance or reconfiguration.&lt;br&gt;
We also realize that we have analytics queries with huge datasets returning, which can prevent wal-logs from applying to the replica, and could lead to big replica lag. For those cases, we need the read replica with high &lt;code&gt;max_standby_archive_delay&lt;/code&gt;. Usually, such a workload does not require high availability, so we can have only one read replica tolerant for big replica lag to save some resources.&lt;br&gt;
With read replicas, you also have to control the replication lag carefully. The query example to get the current replica lag in seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT EXTRACT(EPOCH FROM NOW() - 
pg_last_xact_replay_timestamp()) AS replication_lag_seconds;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the link to the Postgres manifest to use with the Zalando operator - &lt;a href="https://github.com/petrushinvs/databases-in-prod/blob/main/postgres/kubernetes/pg-prod-grow10x.yaml" rel="noopener noreferrer"&gt;1 primary, 2 replicas with increased resources&lt;/a&gt;.&lt;br&gt;
Here is the link to the Postgres manifest to use with the Zalando operator - &lt;a href="https://github.com/petrushinvs/databases-in-prod/blob/main/postgres/kubernetes/pg-single-read-replica.yaml" rel="noopener noreferrer"&gt;single replica cluster for analytics and high max_standby_archive_delay&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Debugging long-running queries on read replicas.
&lt;/h2&gt;

&lt;p&gt;The queries with huge returning data rows can cause problems on Read replicas. Imagine, if your query has no or minimal filters and runs against a huge 100GB+ table. The problem is not only in the query, CPU, or I/O usage itself, but it also takes a lot of time for the application to retrieve data. On the server, it also causes buffer abuse and if it runs on read replicas it causes locks and delays in applying the archivelogs. You also will see this query in the Postgres logs as a long-running. As I said before, in case of lag &amp;gt;= &lt;code&gt;max_standby_archive_delay&lt;/code&gt; read replica will cancel all queries until catches up with the primary with the error &lt;code&gt;ERROR:  canceling statement due to conflict with recovery&lt;/code&gt;. To prevent such behavior and protect your read replicas all SELECTs returning many data must be paged and must have ordering and limits. Example of bad query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT * FROM wallet_transaction 
WHERE currency_id = 10;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, you have to check for indexes by currency_id, and create it if not. But anyway, the query will run for a long time and the amount of data is huge if the wallet_transaction table is big. On read replica, the wal-log application will be blocked while the client is consuming data from the query result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT * FROM wallet_transaction 
WHERE currency_id = 10 
ORDER BY created_at DESC
OFFSET 10100 LIMIT 100;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query is better, but anyway, it is bad. It limits the dataset, but Postgres will scan the full table and/or indexes anyway, consuming CPU/Mem/IOPSes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT * FROM wallet_transaction
WHERE currency_id = 10 
AND created_at BETWEEN NOW() - INTERVAL '1 month' AND NOW()
ORDER BY created_at DESC
OFFSET 10100 LIMIT 100;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The best query. Now the result is limited, it takes a little time to retrieve data for the client, and the index/table scans will be limited by date. Also, it is paged, so your app can get and show all data to a user.&lt;/p&gt;

&lt;p&gt;In real life, in my experience, the first query could take from 10 seconds to minutes and could lead to query cancelling errors. 2nd query could take 1 - 15 seconds, and 3rd from 5 to 500 milliseconds. So, for big tables always use ORDER/OFFSET/LIMIT to limit the results, and intervals to limit the data selection and scans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;In the second part, we discussed the second stage of growth problems of Postgres databases. The main root causes are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;high temporary files usage,&lt;/li&gt;
&lt;li&gt;locks and deadlocks in the database,&lt;/li&gt;
&lt;li&gt;non-optimal table structure and lack of indexing, which causes sequential scans,&lt;/li&gt;
&lt;li&gt;huge result sets from SELECT queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I estimate this growth stage in the following numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database size 350GB to 2TB,&lt;/li&gt;
&lt;li&gt;there are big tables from 50GB to 1TB,&lt;/li&gt;
&lt;li&gt;there are tables with 100+ million to billions of records,&lt;/li&gt;
&lt;li&gt;QPS (queries per second) starts from 3000 to 10-20 000 and more,&lt;/li&gt;
&lt;li&gt;the number of clients/orders/views and/or other business metrics are growing 2x to 10x per 6-12 months.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this stage, you are going to face new problems and challenges and I showed the debug process from query/database optimizations to making decisions and debugging read replicas.&lt;br&gt;
Also, I provided some examples of useful queries to detect problems and Postgres cluster manifests for databases. Please check my Github repo  &lt;a href="https://github.com/petrushinvs/databases-in-prod" rel="noopener noreferrer"&gt;https://github.com/petrushinvs/databases-in-prod&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Good luck.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/redlineeyes/postgres-indexes-analysis-26pa"&gt;Postgres indexes review&lt;/a&gt; has been added.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
    </item>
    <item>
      <title>Postgres in prod. Part 1 - setup and startup for growth.</title>
      <dc:creator>Vasilii Petrushin</dc:creator>
      <pubDate>Thu, 30 Jan 2025 11:26:59 +0000</pubDate>
      <link>https://forem.com/redlineeyes/postgres-in-prod-part-1-setup-and-startup-4fl1</link>
      <guid>https://forem.com/redlineeyes/postgres-in-prod-part-1-setup-and-startup-4fl1</guid>
      <description>&lt;p&gt;In this article, I wanted to share my experience gained with the pain and challenges of business growth and other database-related problems in production use. When your system starts with an empty database and the number of users and requests is low, everything is easy, and it's a good time to prepare for future high loads, to prepare for great success in business terms. My approach to systems and infrastructure engineering is to focus on being ready to support the business growth and success, not only on current operations. You start with your business from some small numbers, then grow 2x, 4x, 10x, and at some point, the business growth skyrockets, and you have to handle this or lose. It’s a challenge and no one wants to lose.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F67vflrnher0y12dyxlbz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F67vflrnher0y12dyxlbz.png" alt="Great success!" width="800" height="761"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is my experience building and maintaining databases to support business growth. There will be 3 major parts: the beginning - set up to grow, from 2x to 10x - the basic steps of optimizations, and from 10x to skyrocket - complex optimizations and architecture for a fast-growing business.&lt;/p&gt;

&lt;p&gt;Any big clouds like AWS, Google, or Azure has its own perfectly managed solutions for Postgres database with a lot of nice docs on how to implement it according to the rules of the well-architected framework, you just pay for it ;), and live with their restrictions. The key benefit of using Postgres in Kubernetes on-premise is nearly unlimited IOPSes. The current SSDs and NVMes provide 100-120 000 IOPS. But AWS RDS limits are up to 16 000 per instance. They have dedicated IO too, but it is costly, at the price starting from a monthly salary of senior DevOps ;). The second great benefit - the Postgres on dedicated hardware is 5 to 10 times cheaper than a managed solution. So, I vote for Postgres and Kubernetes to save some cash and invest it in business growth and/or in my pocket.&lt;/p&gt;

&lt;p&gt;To build good and reliable solutions is nothing to invent, we’re just going to follow the best practices. Best practices are pretty much the same for all IT industries, and suitable for any cloud platform or on-premise deployments. The examples are &lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/framework/welcome.html" rel="noopener noreferrer"&gt;AWS Well-Architected Framework&lt;/a&gt; or &lt;a href="https://cloud.google.com/architecture/framework" rel="noopener noreferrer"&gt;Google Cloud Architecture Framework&lt;/a&gt;, just read. At the start, we’re going to run the Postgres in small/medium instance size, but in a safe, reliable, and ready-to-grow configuration.&lt;/p&gt;

&lt;p&gt;There are two major ways to deploy Postgres to Kubernetes. First is using Helm charts like &lt;a href="https://github.com/bitnami/charts/tree/main/bitnami/postgresql-ha" rel="noopener noreferrer"&gt;bitnami/postgresql&lt;/a&gt; or &lt;a href="https://github.com/sergelogvinov/helm-charts/tree/main/charts/postgresql-single" rel="noopener noreferrer"&gt;Serge’s postgresql-single&lt;/a&gt;, which has benefits compared to bitnami chart. The second way to run Postgres is to use the Kubernetes operator. There are a couple of them, here we will talk about &lt;a href="https://github.com/zalando/postgres-operator" rel="noopener noreferrer"&gt;Zalando Postgres operator&lt;/a&gt;. The helm way is better when in the future you will not plan to use more than one or two Postgres clusters per Kubernetes cluster. If you realize, that you will run more, then the Operator way is for you with all its automation and management advantages.&lt;br&gt;
Here is an example of PostgreSQL database manifest for Zalando Postgres Operator &lt;a href="https://github.com/petrushinvs/databases-in-prod/blob/main/postgres/kubernetes/pg-prod.yaml" rel="noopener noreferrer"&gt;pg-prod.yaml&lt;/a&gt;.&lt;br&gt;
This example was tested for intensive transaction processing with query rates up to 3000-4000 per second and database size up to 350GB (data and indexes). It is enough to start most projects with thousands of active users like medium-volume webshops, news- and community-support websites, medium-sized gaming or gambling, etc. We assume, that we already set up the operator and the S3 bucket with encryption and access control for backups and WAL-log storage. &lt;/p&gt;
&lt;h2&gt;
  
  
  Let’s review the config.
&lt;/h2&gt;

&lt;p&gt;Please, open this &lt;a href="https://github.com/petrushinvs/databases-in-prod/blob/main/postgres/kubernetes/pg-prod.yaml" rel="noopener noreferrer"&gt;manifest&lt;/a&gt; in the new tab or window and let's have a look.&lt;/p&gt;

&lt;p&gt;We want maximum availability with minimum downtime. So, numberOfInstances: 2. It will create a classic Postgres primary-secondary cluster pair managed by Zalando Postgres operator and Patroni under the hood.&lt;/p&gt;

&lt;p&gt;The top lines in the config are about name, version, etc. Let's look at the Postgres config part. &lt;br&gt;
Firstly, I prefer autovacuum enabled for small and medium-sized deployments. Autovacuum helps to keep the tables in good condition for effective data searches and manipulations from one side, from another, it does not affect much the overall Postgres performance. For huge and high-load deployments you can deactivate autovacuum. I am going to discuss it in future articles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      autovacuum_analyze_scale_factor: "0.1"
      autovacuum_vacuum_scale_factor: "0.2"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second, and very important part is logging. With these settings we will control slow queries, lock waits, and temporary files - which heavily affect overall Postgres performance, and all issues can be detected and fixed quickly. Detailed explanations of each type of issue and how to fix it I’ll give later in the next articles. For detailed migration control, data protection, and for security reasons, we want to log all DDL (data definition language) queries too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      log_destination: "stderr"
      log_connections: "off"
      log_disconnections: "off"
      log_min_duration_statement: "100ms"
      log_statement: "ddl"
      log_lock_waits: "on"
      log_temp_files: "0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next part is connection limits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      max_connections: "150"
      superuser_reserved_connections: "5"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s simple, Postgres spend a lot of resources to control connections. For production use, we must limit the number of connections to prevent CPU/Memory consumption and preserve some connection pool for superuser for maintenance, recovery, and problem-solving.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      max_standby_archive_delay: "900s"
      max_standby_streaming_delay: "900s"
      wal_level: "logical"
      max_wal_senders: "4"
      max_replication_slots: "4"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our cluster has been built using physical replication protocol, we set the wal_level to replica to allow replication. The max_wal_senders and max_replication_slots are stand for 1 or 2 standby replicas, base-backup (if you will use it), and 1 or 2 reserved connections for future physical replication. For example, if you want to have running replicas in another region, or for data analytics. Also, we must set the limits for delays between secondary and primary. Some queries can cause significant replication delays between primary and secondary. But we want to have the most recent data in the replica. If these parameters are set, the secondary will drop and decline all queries, decreasing workload to allow itself to catch up with the primary. Also, there is a metric for Prometheus monitoring to control the replica lag - “pg_replication_lag”. Here is an example of a Grafana graph to control the replica lag, and adding the expression &amp;gt;= will give you the alert expression.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum by(kubernetes_pod_name) (pg_replication_lag{source="$cluster",kubernetes_pod_name="$instance"})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parallelism settings would work more or less well for 2 to 10 CPU cores instance, for current values refer to resources section of manifest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      max_wal_senders: "4"
      max_replication_slots: "4"
      max_worker_processes: "16"
      max_parallel_workers: "8"
      max_parallel_workers_per_gather: "2"
      max_parallel_maintenance_workers: "2"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Memory and other configuration options are like this. The work_mem and maintenance_work_mem are good for orders/financial data processing when most rows are numbers or relatively small varchars, text, or JSON. If your workload and most of your data are big texts, JSONs, or blogss, consider increasing these values, and increasing memory requests/limits in the resources section too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      max_wal_size: "4GB"
      min_wal_size: "2GB"
      wal_keep_size: "2GB"
      effective_cache_size: "4GB"
      shared_buffers: "2GB"
      work_mem: "64MB"
      maintenance_work_mem: "256MB"
      # we're on ssd
      effective_io_concurrency: "100"
      random_page_cost: "1.1"
      # enable the extentions, pg_stat_statements is a must-have for production
      shared_preload_libraries: "pg_stat_statements,pg_cron,pg_trgm,pgcrypto,pg_stat_kcache"
      track_io_timing: "on"
      pg_stat_statements.max: "1000"
      pg_stat_statements.track: "all"
      cron.database_name: "postgres"
      synchronous_commit: "local"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Very important podAnnotations to protect our Postgres pods from eviction and for monitoring&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  podAnnotations:
    cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
    prometheus.io/port: "9187"
    prometheus.io/scrape: "true"
    prometheus.io/path: "/metrics"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To protect our production to avoid connection storms and improve connection management enable the connection pooler (pbgouncer).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  enableConnectionPooler: true
  enableReplicaConnectionPooler: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For backup I chose the logical backup method supported by the operator: I wanted the operator to care for backups with its own supplied methods, otherwise, you have to setup your own backup solution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  enableLogicalBackup: true
  logicalBackupSchedule: "30 10 * * *"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now check the &lt;code&gt;additionalVolumes&lt;/code&gt; and sidecars sections. We’re going to enable as a sidecars promtail to ship the logs to our Loki service, and postgres-exporter for advanced monitoring. The configs are in configmaps supplied in the manifest. Promtail config is pretty straightforward. Let’s check the &lt;code&gt;postgres-monitoring-queries&lt;/code&gt; &lt;a href="https://github.com/petrushinvs/databases-in-prod/blob/main/postgres/kubernetes/pg-prod.yaml#L205-L275" rel="noopener noreferrer"&gt;configmap&lt;/a&gt;. We run the pg_stat_statements set of metrics for the query analysis dashboard to manage the query efficiency and to get the basic info for future query optimizations. Also pg_replication_lag metric is here, this is a very important metric for managing the data quality in replicas. The 3-rd metric pg_postmaster_start_time_seconds is good for Postgres uptime/restart management and alerting.&lt;/p&gt;

&lt;p&gt;Under the hood, the Zalando Postgres operator takes care of many things and helps us a lot with routine maintenance. In operator configuration, we set up S3 storage for backups and WAL-log archive, backup options, healthchecks, timeouts, etc. After deployment, we will have WAL-logs and backups shipped to S3, and the operator will take care of its retention. Also operator will gracefully restart Postgres instances and poolers for reconfiguration, or in case of failures.&lt;/p&gt;

&lt;p&gt;Now a few words about monitoring and alerting. With the Prometheus or Victoriametrics installed, we will have our Postgres metrics scraped. For Grafana I would recommend to setup &lt;a href="https://github.com/petrushinvs/databases-in-prod/tree/main/infra/grafana" rel="noopener noreferrer"&gt;kubernetes-pods, pg-monitoring, pgbouncer, and pg-replication-lag dashboards&lt;/a&gt;. And also, it would be great to setup pg-query-overview.json and pg-query-drilldown.json. These dashboards was originally been built by Percona for its PMM, and then quickly ported by me to pure Grafana and Victoriametrics/Prometheus, so its configuration could be tricky and take time. But you will get a cool picture of pg_stat_statements_calls, a sets of most time-consuming queries, most called queries, etc, and will get insights about queries from query analytics - timings, buffers, reads, et cetera. And you don’t need to set up any third parties and extra Postgres monitoring tools like pgbadger, SolarWinds Postgres analytics, and others. &lt;br&gt;
The alerts set we use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;alert on postgresql is running if &lt;code&gt;pg_up != 1&lt;/code&gt; or &lt;code&gt;no-data&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;alert on &lt;code&gt;pg_replication_lag &amp;gt; max_standby_archive_delay&lt;/code&gt; or any number you consider reasonable&lt;/li&gt;
&lt;li&gt;alerts like &lt;code&gt;PersistentVolume is filling up&lt;/code&gt; from default alerts set&lt;/li&gt;
&lt;li&gt;alerts like &lt;code&gt;High CPU iowait&lt;/code&gt; from node-exporter to control IO&lt;/li&gt;
&lt;li&gt;a set of basic kubernetes alerts like &lt;code&gt;PodCPULimit&lt;/code&gt;, &lt;code&gt;ContainerCrashLooping&lt;/code&gt;, &lt;code&gt;KubePodNotReady&lt;/code&gt;, etc.
In addition to altering I would recommend referring to the &lt;a href="https://github.com/samber/awesome-prometheus-alerts" rel="noopener noreferrer"&gt;Awesome Prometheus Alerts&lt;/a&gt; project to get ideas and solutions on how to monitor your infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;So, in the end, after applying the Posgresql manifest we have up and running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the highly available Postgres cluster with primary and standby nodes,&lt;/li&gt;
&lt;li&gt;the encrypted WAL-logs storage and backup storage in the S3 bucket with auth and access control,&lt;/li&gt;
&lt;li&gt;pg-bouncer for primary with SSL encryption,&lt;/li&gt;
&lt;li&gt;pg-bouncer for replica with SSL encryption,&lt;/li&gt;
&lt;li&gt;Kubernetes service for direct connection to primary with SSL (cluster-name.namespace.svc),&lt;/li&gt;
&lt;li&gt;Kubernetes service for direct connection to replica with SSL (cluster-name-repl.namespace.svc),&lt;/li&gt;
&lt;li&gt;provisioned primary database,&lt;/li&gt;
&lt;li&gt;provisioned application users with creds in secrets,&lt;/li&gt;
&lt;li&gt;all stuff runs on special nodes (node groups), defined by nodeAffinity,&lt;/li&gt;
&lt;li&gt;logging subsystem is set up and allows control of slow queries, temporary files, data definition queries, and default Postgres logs,&lt;/li&gt;
&lt;li&gt;all basic Postgres metrics are provided by posgres-exporter with enhanced query and replication lag monitoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Victoriametrics/Prometheus and Grafana, we also have the full monitoring and alerting solution to manage and maintain our databases. With your business growth, the workload will grow, and it provides you with information to support this growth, detect failures, and tune your Postgres instance, database, tables, and queries. And by the most of parameters, this setup of Postgres and monitoring tools will comply with the best practices from the point of view of ‘well-architected frameworks’, and be ready to grow up to 1-2-3 million users.&lt;br&gt;
Check the Posgres manifests for Zalando operator in my Github repo: &lt;a href="https://github.com/petrushinvs/databases-in-prod/tree/main/postgres/kubernetes" rel="noopener noreferrer"&gt;https://github.com/petrushinvs/databases-in-prod/tree/main/postgres/kubernetes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How to solve slow application responses caused by slow queries, Postgres performance degradation, replica lags, and other issues - see the next article, Part 2 - 2x to 10x growth, basic steps of query and database optimizations.&lt;/p&gt;

&lt;p&gt;Good luck.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/redlineeyes/postgres-in-prod-part-2-problem-solving-and-optimizations-heading-to-10x-growth-2fko"&gt;Part 2.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>devops</category>
      <category>database</category>
    </item>
  </channel>
</rss>
