{"id":303307,"date":"2020-05-10T03:02:38","date_gmt":"2020-05-10T03:02:38","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=303307"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=303307","title":{"rendered":"Parallelism in PostgreSQL: treatment of trees and conscience"},"content":{"rendered":"\n<div class=\"post__text post__text-html post__text_v1\" id=\"post-content-body\" data-io-article-url=\"https:\/\/habr.com\/ru\/company\/postgrespro\/blog\/500442\/\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/b0\/s3\/rq\/b0s3rqkffufh7baruji0jc_vbgq.jpeg\"><\/p>\n<p>  Database scaling is a continually coming future. DBMS get improved and better scaled on hardware platforms, while the hardware platforms themselves increase the performance, number of cores, and memory \u2014 Achilles is trying to catch up with the turtle, but has not caught up yet. The database scaling challenge manifests itself in all its magnitude.<\/p>\n<p>  Postgres Professional had to face the scaling problem not only theoretically, but also in practice: through their customers. Even more than once. It&#8217;s one of these real-life cases that this article<br \/>  will discuss.  <\/p>\n<blockquote><p>Many thanks to Elena Indrupskaya for the translation.  <\/p><\/blockquote>\n<p><a name=\"habracut\"><\/a><br \/>  PostgreSQL scales well on NUMA systems in the case of a single motherboard with multiple processors and multiple data buses. You can read about some optimizations <a href=\"https:\/\/habr.com\/company\/postgrespro\/blog\/270827\/\">here<\/a> and <a href=\"http:\/\/akorotkov.github.io\/blog\/2016\/05\/09\/scalability-towards-millions-tps\/\">here<\/a>.<\/p>\n<p>  However, there is another group of systems: they have several motherboards that exchange data using the interconnect and run one instance of the OS; for the user this design looks like a single machine. And although formally such systems can also be referred to NUMA, in essence, they are closer to supercomputers because access to the local memory of a node drastically differs from access to the memory of a neighboring node.<\/p>\n<p>  The PostgreSQL community believes that the cause of the issues is the only instance of Postgres running on such architectures, and there is no systematic approach to resolving them yet. Designers of shared-memory oriented software initially presumed that access times to local and remote memory would be more or less comparable. When we work with many nodes, it doesn\u2019t make sense to rely on shared memory as on a fast communication channel: because of latency, it is much \u00abcheaper\u00bb to send a request to perform a certain action to the node where the data of interest is located than to send the data through the bus. So, cluster solutions are relevant for supercomputers and for multi-node systems in general.<\/p>\n<p>  This does not mean that the combination of multi-node systems and the shared-memory architecture, which is typical for Postgres, should be trashed. After all, if Postgres processes spend most of their time performing complex computations locally, this architecture will be even more efficient. In our situation, the customer had already purchased a powerful multi-node server, and we had to resolve PostgreSQL issues on it.<\/p>\n<p>  And the issues were serious: execution of simplest write queries (to change several field values in one row) took from a few minutes to an hour. These issues came out full force exactly due to a large number of cores and accordingly, to massive parallelism in query execution with a relatively slow exchange between nodes.<\/p>\n<p>  Therefore, the article will be somewhat dual-purpose:<\/p>\n<ul>\n<li>To share experiences on: what if on a multi-node system, the database seriously slows down. What to begin with, how to diagnose, where to go.<\/li>\n<li>Tell how the problems of the PostgreSQL database itself can be solved with a high level of parallelism. Among the rest, how a change to the algorithm of acquiring locks affects the efficiency of PostgreSQL.<\/li>\n<\/ul>\n<h3>The server and database<\/h3>\n<p>  The system consisted of 8 blades with 2 sockets each. More than 300 cores in total (hyper-threading not taken into account). The fast bus (the manufacturer&#8217;s proprietary technology) connected the blades. Not that a supercomputer, but the configuration was impressive for a single DBMS instance. The load was also rather big. More than 1 terabyte of data. About 3000 transactions per second. More than 1000 connections to Postgres.<\/p>\n<p>  Having started analyzing hour-long write waits, first we made sure that writing to disk could not cause the delay. As soon as unclear delays began to occur, the tests were done exclusively on <code>tmpfs<\/code>. The picture did not change. So the disk was not to blame.<\/p>\n<h3>Starting to collect diagnoses: views<\/h3>\n<p>  Since the issues were most likely due to the high concurrency of processes that try to access the same objects, the first thing to check was the locks. For this check, PostgreSQL has <code>pg.catalog.pg_locks<\/code> and <code>pg_stat_activity<\/code> views. Starting as early as with version 9.6, the second one includes the information on what the process is waiting for (<i>Amit Kapila, Ildus Kurbangaliev<\/i>) \u2014 <code>wait_event_type<\/code>. Possible values for this field are described <a href=\"https:\/\/postgrespro.ru\/docs\/postgresql\/10\/monitoring-stats\">here<\/a>.<\/p>\n<p>  But first, let&#8217;s just count:<\/p>\n<pre><code class=\"pgsql\">postgres=# SELECT COUNT(*) FROM pg_locks;<\/code><\/pre>\n<pre><code class=\"plaintext\">count \u2014---\u2014 88453 (1 row)<\/code><\/pre>\n<pre><code class=\"pgsql\">postgres=# SELECT COUNT(*) FROM pg_stat_activity;<\/code><\/pre>\n<pre><code class=\"plaintext\">count \u2014---\u2014 1826 (1 row)<\/code><\/pre>\n<pre><code class=\"pgsql\">postgres=# SELECT COUNT(*) FROM pg_stat_activity WHERE state='active';<\/code><\/pre>\n<pre><code class=\"plaintext\"> count \u2014---\u2014 1005 (1 row)<\/code><\/pre>\n<p>  And these are actual figures. It reached 200 000 locks. At the same time, the following locks were acquired for the ill-fated query:<\/p>\n<pre><code class=\"pgsql\">SELECT COUNT(mode), mode FROM pg_locks WHERE pid=580707 GROUP BY mode;<\/code><\/pre>\n<pre><code class=\"plaintext\">count | mode \u2014-----+---------------\u2014 93 | AccessShareLock 1 | ExclusiveLock<\/code><\/pre>\n<p>  When reading the buffer, the DBMS uses the <code>share<\/code> lock, and when writing \u2014 the <code>exclusive<\/code> lock. That is, write locks made less than 1% of all queries. In the <code>pg_locks<\/code> view, the kinds of locks do not always appear as described in the user <a href=\"https:\/\/postgrespro.ru\/docs\/postgrespro\/10\/explicit-locking\">documentation<\/a>.<\/p>\n<p>  Here is a table of matches:<\/p>\n<pre><code class=\"plaintext\">AccessShareLock = LockTupleKeyShare RowShareLock =LockTupleShare ExclusiveLock = LockTupleNoKeyExclusive AccessExclusiveLock = LockTupleExclusive<\/code><\/pre>\n<p>  The query \u00abSELECT mode FROM pg_locks\u00bb showed that 234 INSERTs were waiting for execution of the CREATE INDEX command (without the CONCURRENTLY keyword) and 390 INSERTs were awaiting the <code>buffer content lock<\/code>. A possible solution is to \u00abteach\u00bb INSERTs from different sessions to reduce buffer overlapping.<\/p>\n<h3>It&#8217;s time to involve perf<\/h3>\n<p>  The <b><code>perf<\/code><\/b> utility collects pretty much diagnostic information. In the <code>record<\/code>\u2026 mode, it writes system event statistics to files (by default they are in <code>.\/perf_data<\/code>), and in the <code>report<\/code> mode, it analyzes the collected data; for example, you can filter events by being related to <code>postgres<\/code> or to a given <code>pid<\/code>:<\/p>\n<pre><code class=\"plaintext\">$ perf record-u postgres or $ perf record-p 76876 and then, say $ perf report &gt; .\/my_results<\/code><\/pre>\n<p>  As a result we will see something like<\/p>\n<p>  <img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/rn\/ta\/rv\/rntarvj2jticq7glciiqockk3-y.jpeg\"><\/p>\n<p>  Usage of <code>perf<\/code> for PostgreSQL diagnostics is described, for example, <a href=\"https:\/\/blog.2ndquadrant.com\/tracing-postgresql-perf\/\">here<\/a>, as well as in <a href=\"https:\/\/wiki.postgresql.org\/wiki\/Profiling_with_perf\">pg wiki<\/a>.<\/p>\n<p>  In our situation, important information was obtained even in the simplest mode \u2014 <code>perf top<\/code>, which certainly worked a la <code>top<\/code> command of the operating system. With <code>perf top<\/code>, we&#8217;ve seen that the processor spends most of the time in kernel locks, as well as in the <code>PinBuffer()<\/code> and <code>LWLockAttemptLock()<\/code> functions.<\/p>\n<p>  <code>PinBuffer()<\/code> is a function that increments the count of references to the buffer (mapping of a data page to RAM), through which Postgres processes know what buffers can be evicted and which cannot.<\/p>\n<p>  <code>LWLockAttemptLock()<\/code> is a function to acquire <code>LWLock<\/code>. <code>LWLock<\/code> is a kind of lock with two levels \u2013 <code>shared<\/code> and <code>exclusive<\/code> \u2013 and without <code>deadlock<\/code> detection; locks are preallocated in <code>shared memory<\/code>, and waiting processes are queued.<\/p>\n<p>  These functions were already optimized considerably in PostgreSQL 9.5 and 9.6. Spinlocks inside them were replaced with a direct use of atomic operations.<\/p>\n<h3>Flame graphs<\/h3>\n<p>  No way without them: even if they were useless, they would have still been worth telling about since they are extremely elegant. Moreover, they are useful! Here goes an illustration from <code>github<\/code> \u2013 not from the case discussed (neither we nor the customer are ready to disclose details yet).<\/p>\n<p>  <img decoding=\"async\" src=\"https:\/\/habrastorage.org\/getpro\/habr\/post_images\/75b\/909\/e8d\/75b909e8de5177a48fa7d73f53eff437.svg\"><\/p>\n<p>  These impressive pictures very clearly show what the processor cycles are spent on. The same <code>perf<\/code> can collect data, but <code>flame graph<\/code> clearly visualizes the data and builds trees based on the collected call stacks. You can read about profiling with flame graphs, for example, <a href=\"https:\/\/queue.acm.org\/detail.cfm?id=2927301\">here<\/a> and download everything you need <a href=\"https:\/\/github.com\/brendangregg\/FlameGraph\">here<\/a>.<\/p>\n<p>  In our situation, we could see a huge number of <code>nestloop<\/code>s on the flame graphs. It seems like JOINs of a large number of tables in numerous parallel read queries caused a large number of <code>access share<\/code> locks.<\/p>\n<p>  Statistics collected by <code>perf<\/code> show where the CPU cycles are spent. And although we&#8217;ve seen that most of the CPU time passes in the locks, we did not see what exactly leads to such long waits of the locks because we do not understand where exactly the waits of the locks occur \u2014 CPU time is not spent in waits.<\/p>\n<p>  In order to see the waits themselves, we can query the <code>pg_stat_activity<\/code> system view.<\/p>\n<pre><code class=\"pgsql\">SELECT wait_event_type, wait_event, COUNT(*) FROM pg_stat_activity GROUP BY wait_event_type, wait_event;<\/code><\/pre>\n<p>  revealed that:<\/p>\n<pre><code class=\"plaintext\">LWLockTranche | buffer_content | UPDATE ************* LWLockTranche | buffer_content | INSERT INTO ******** LWLockTranche | buffer_content | \\r | | insert into B4_MUTEX | | values (nextval('hib | | returning ID Lock | relation | INSERT INTO B4_***** LWLockTranche | buffer_content | UPDATE ************* Lock | relation | INSERT INTO ******** LWLockTranche | buffer_mapping | INSERT INTO ******** LWLockTranche | buffer_content | \\r<\/code><\/pre>\n<p>  (here asterisks just replace the details of the query that we do not share).<\/p>\n<p>  The values are visible for <code>buffer_content<\/code> (lock on the contents of buffers) and for <code>buffer_mapping<\/code> (locks on parts of the <code>shared_buffers<\/code> hash table).<\/p>\n<h3>To GDB for help<\/h3>\n<p>  But why are there so many waits for these locks? For more detailed information about locks, we had to use the <code>GDB<\/code> debugger. With <code>GDB<\/code>, we can get a stack of calls to specific processes. Using sampling, i.e., having collected a certain number of random call stacks, you can get an idea of the stacks where the longest waits happen.<\/p>\n<p>  Let&#8217;s consider the process of collecting statistics. We will consider the \u00abmanual\u00bb collection of statistics, although in real life special scripts are used to do it automatically.<\/p>\n<p>  First, it is necessary to attach <code>gdb<\/code> to the PostgreSQL process. To do this, you need to find <code>pid<\/code> of the server process, say, from<\/p>\n<pre><code class=\"bash\">$ ps aux | grep postgres<\/code><\/pre>\n<p> Assume we found:<\/p>\n<pre><code class=\"plaintext\">postgres 2025 0.0 0.1 172428 1240 pts\/17 S jul23 0:00 \/usr\/local\/pgsql\/bin\/postgres -D \/usr\/local\/pgsql\/data<\/code><\/pre>\n<p> and now we will feed the debugger with <code>pid<\/code>:<\/p>\n<pre><code class=\"plaintext\">igor_le:~$gdb -p 2025<\/code><\/pre>\n<p>  Once inside the debugger, we type <code>bt<\/code> [that is, <code>backtrace<\/code>] or <code>where<\/code>. And we get a lot of information of this kind:<\/p>\n<pre><code class=\"plaintext\">(gdb) bt #0 0x00007fbb65d01cd0 in __write_nocancel () from \/lib64\/libc.so.6 #1 0x00000000007c92f4 in write_pipe_chunks ( data=0x110e6e8 &quot;2018-06-01 15:35:38 MSK [524647]: [392-1] db=bp,user=bp,app=[unknown],client=192.168.70.163 (http:\/\/192.168.70.163) LOG: relation 23554 new block 493: 248.389503\\n2018\u201006\u201001 15:35:38 MSK [524647]: [393-1] db=bp,user=bp,app=[&quot;..., len=409, dest=dest@entry=1) at elog.c:3123 #2 0x00000000007cc07b in send_message_to_server_log (edata=0xc6ee60 &lt;errordata&gt;) at elog.c:3024 #3 EmitErrorReport () at elog.c:1479&lt;\/errordata&gt;<\/code><\/pre>\n<p>  Having gathered statistics, which included call stacks from all Postgres processes that were collected repeatedly at different points in time, we saw that 3706 seconds (about an hour) were spent awaiting the <code>buffer partition lock<\/code> inside the <code>relation extension lock<\/code>, that is, a lock on a piece of the buffer manager&#8217;s hash table that was needed to evict the former buffer in order to later replace it with a new one, corresponding to the extended part of the table. A certain number was also noticeable for <code>buffer content lock<\/code>, which corresponded to the wait of the lock of <code>B-tree<\/code> index pages for insertion.<\/p>\n<p>  <img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/6c\/y6\/cw\/6cy6cwmfye29jw84dkzoabjlgvg.jpeg\"><\/p>\n<p>  At first, there were two explanations for such a monstrous waiting time:<\/p>\n<ul>\n<li>Someone else acquired this <code>LWLock<\/code> and got stuck. But this is unlikely. Because nothing complicated happens inside the buffer partition lock.<\/li>\n<li>We came across some sort of pathologic behavior of <code>Lwlock<\/code>. That is, although none acquired the lock for too long, the wait for it lasted unnecessarily long.<\/li>\n<\/ul>\n<h3>Diagnostic patches and treatment of trees<\/h3>\n<p>  By reducing the number of simultaneous joins, we would certainly reduce the flow of requests for locks. But it would be like a surrender. Instead, <i>Alexander Korotkov<\/i>, chief architect of Postgres Professional (he certainly helped to prepare this article), proposed a series of patches.<\/p>\n<p>  First of all, it was necessary to get a more detailed picture of the disaster. Although off-the-shelf tools are pretty universal, company&#8217;s own diagnostic patches still prove useful.<\/p>\n<p>  A patch was written to add detailed logging of the time spent in <code>relation extension<\/code> and of what happens inside the <code>RelationAddExtraBlocks()<\/code> function. This way we get to know what the time inside <code>RelationAddExtraBlocks() is spent on.<\/code><\/p>\n<p>  And another patch was written in support of the previous, which reports in <code>pg_stat_activity<\/code> on what we are doing now in <code>relation extension<\/code>. It was done like this: when <code>relation<\/code> is extended, <code>application_name<\/code> becomes <code>RelationAddExtraBlocks<\/code>. This process is now convenient to analyze in maximum details using <code>gdb bt<\/code> and <code>perf<\/code>.<\/p>\n<p>  As for treatment patches (rather than diagnostic), two of them were written. The first patch changed the behavior of <code>B\u2010tree<\/code> leaf locks: previously, in an insert query, the leaf was locked as <code>share<\/code>, and only then acquired <code>exclusive<\/code>. Now it acquires <code>exclusive<\/code> from the beginning. Now this patch <a href=\"https:\/\/git.postgresql.org\/gitweb\/?p=postgresql.git;a=commit;h=d2086b08b023c0749a53d617ff3fe0f052646254\">is already committed<\/a> for <b>PostgreSQL 12<\/b>. Thanks to the <a href=\"https:\/\/wiki.postgresql.org\/wiki\/Committers\">committer status<\/a>, which <i>Alexander Korotkov<\/i> received last year \u2014 the second PostgreSQL committer in Russia and the second in the company.<\/p>\n<p>  The value of <code>NUM_BUFFER_PARTITIONS<\/code> was also increased from 128 to 512 to reduce the load on the mapping locks: the hash table of the buffer manager was divided into smaller pieces with a view of decreasing the load on each particular piece.<\/p>\n<p>  After applying this patch, locks on the contents of the buffers were gone, but despite the increase of <code>NUM_BUFFER_PARTITIONS<\/code>, there remained <code>buffer_mapping<\/code> locks, that is, to recall, locks on pieces of the buffer manager&#8217;s hash table:<\/p>\n<pre><code class=\"plaintext\">locks_count | active_session | buffer_content | buffer_mapping ----\u2010\u2010\u2010--\u2010\u2010\u2010+\u2010------\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010+\u2010\u2010\u2010------\u2010\u2010\u2010\u2010\u2010\u2010\u2010+\u2010\u2010------\u2010\u2010\u2010   12549     |      1218      |        0       |    15<\/code><\/pre>\n<p>  And this is not much. B\u2010tree was no longer a bottleneck. The extension of <code>heap<\/code> came to the forefront.<\/p>\n<h3>Treatment of conscience<\/h3>\n<p>  Then Alexander suggested the following hypothesis and solution:<\/p>\n<p>  We are waiting too long on the <code>buffer partition lock<\/code> during the buffer eviction. It is possible that on the same <code>buffer partition lock<\/code> there lies some highly demanded page, for example, the root of some <code>B\u2010tree<\/code>. At this location, there is a persistent flow of requests for the <code>shared-lock<\/code> from read queries.<\/p>\n<p>  The wait queue in <code>LWLock<\/code> is \u00abnot fair\u00bb. Since as many <code>shared lock<\/code>s as you want can be acquired at the same time, if <code>shared lock<\/code> is already acquired then next <code>shared lock<\/code>s do not wait in line. Therefore, if the flow of shared locks is intensive enough to not leave gaps between them, the wait of the <code>exclusive lock<\/code> goes virtually to infinity.<\/p>\n<p>  To fix this, we can try to offer the patch for \u00abgentleman\u00bb behavior of locks. It stirs the conscience of <code>shared locker<\/code>-s and they honestly stand in line when <code>exclusive lock<\/code> is already standing there (interestingly, heavyweight locks \u2014 <code>hwlock<\/code> \u2014 have no trouble with the conscience: they always honestly stand in line)<\/p>\n<pre><code class=\"plaintext\">locks_count | active_session | buffer_content | buffer_mapping | reladdextra | inserts&gt;30sec \u2010\u2010\u2010\u2010\u2010\u2010-\u2010\u2010\u2010\u2010\u2010+\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010+\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010+\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010\u2010--\u2010-\u2010+\u2010\u2010\u2010\u2010\u2010\u2010-\u2010\u2010\u2010\u2010\u2010\u2010+\u2010\u2010\u2010\u2010------    173985   |      1802      |        0       |       569      |      0      |    0<\/code><\/pre>\n<p>  Everything is OK! There are no long <code>INSERT<\/code>s. However, locks on pieces of hash tables remain. Well, it can\u2019t be helped \u2014 these are the properties of the bus of our little supercomputer.<\/p>\n<p>  This patch was also <a href=\"https:\/\/www.postgresql.org\/message-id\/CAPpHfdvJhO1qutziOp%3Ddy8TO8Xb4L38BxgKG4RPa1up1Lzh_UQ%40mail.gmail.com\">offered to the community<\/a>. But regardless of the future of these patches in the community, nothing prevents them from getting into next versions of <b>Postgres Pro Enterprise<\/b>, which are designed right for customers with heavily loaded systems.<\/p>\n<h3>Lesson learned<\/h3>\n<p>  Upstanding lightweight <code>share<\/code> locks, which let <code>exclusive<\/code> locks into the queue, resolved the issue of hour-long delays on a multi-node system. The hash table of the <code>buffer manager<\/code> failed to work because of a too heavy flow of <code>share lock<\/code>-s, which left no chance for the locks needed to evict the old buffers and load the new ones. Issues with the extension of the buffer for database tables only was the result of this. Previously, it was possible to fix a weakness with access to the root of <code>B-tree<\/code>.<\/p>\n<p>  PostgreSQL was not created with an eye to NUMA-architectures and supercomputers. Accommodating Postgres to these architectures is a huge job, which would (and will possibly) require coordinated efforts of many people and even companies. But troublesome consequences of these architectural problems can be mitigated. And we have to: the load types that resulted in delays such as those described are quite typical, and we continue to receive similar distress alerts from other places. Similar troubles manifested themselves before \u2014 on systems with fewer cores, but the consequences were just not so terrible, and the symptoms were treated differently and with other patches. Now another treatment is available: although not universal, but definitely useful.<\/p>\n<p>  So, when PostgreSQL works with the memory of the whole system as local, no high-speed bus between nodes can be compared with the time of access to local memory. Uneasy problems arise because of this, often urgent, but interesting. And the experience of solving them is useful not only to the solvers, but also for the whole community.<\/p>\n<p>  <img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/od\/n1\/sf\/odn1sf_id7l60ezlyo-padxymmi.jpeg\"><\/div>\n<p> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/company\/postgrespro\/blog\/500442\/\"> https:\/\/habr.com\/ru\/company\/postgrespro\/blog\/500442\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"\n<div class=\"post__text post__text-html post__text_v1\" id=\"post-content-body\" data-io-article-url=\"https:\/\/habr.com\/ru\/company\/postgrespro\/blog\/500442\/\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/b0\/s3\/rq\/b0s3rqkffufh7baruji0jc_vbgq.jpeg\"><\/p>\n<p>  Database scaling is a continually coming future. DBMS get improved and better scaled on hardware platforms, while the hardware platforms themselves increase the performance, number of cores, and memory \u2014 Achilles is trying to catch up with the turtle, but has not caught up yet. The database scaling challenge manifests itself in all its magnitude.<\/p>\n<p>  Postgres Professional had to face the scaling problem not only theoretically, but also in practice: through their customers. Even more than once. It&#8217;s one of these real-life cases that this article<br \/>  will discuss.  <\/p>\n<blockquote><p>Many thanks to Elena Indrupskaya for the translation.  <\/p><\/blockquote>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-303307","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/303307","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=303307"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/303307\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=303307"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=303307"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=303307"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}