[ https://issues.apache.org/jira/browse/SENTRY-1891?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]
Vadim Spector updated SENTRY-1891: ---------------------------------- Description: Sentry server can trigger full update to NameNode for no good reason, in high load cases, due to the bug in concurrency handling. Full update is always done at the initialization time and for large number of permissions and/or hdfs paths it takes lots of time and significantly increases (temporarily) heap size. It is performed during normal operations only if Sentry server decides that it has gaps in the partial updates' sequence numbers, so it restores the entire snapshot. Full update during normal operations is a highly disruptive event, leading to huge increasw in heap size, system slowdown, and even crash. Below is the explanation of the problem: ==================================================================== *SentryPlugin.java* has multiple permission update methods, such as _onDropSentryRole()_, _onDropSentryPrivilege()_, _onAlterSentryRoleDeleteGroups()_, etc. They, of course, can be called concurrently. And they all delegate work to _permsUpdater.handleUpdateNotification(update)_ call, which, in turn, serializes those requests by turning them into the Runnable tasks and submitting them into a single-threaded thread pool. Each job ends up appending an update to the updates list. So far so good, all updates are serialized. However, each of those methods first creates _PermissionsUpdate_ object with auto-incremented sequence number, and only then passes this object to _permsUpdate.handleUpdateNotification(update)_. Here is typical code snippet: {quote}PermissionsUpdate update = new PermissionsUpdate(permSeqNum.incrementAndGet(), false); // what if another permission update thread preempts this one right here and starts and finishes the whole method ??? update.addPrivilegeUpdate(authzObj).putToAddPrivileges(roleName, privilege.getAction().toUpperCase()); // ... or here ??? permsUpdater.handleUpdateNotification(update); LOGGER.debug("Authz Perm preUpdate [" + update.getSeqNum() + "]..");{quote} The problem is that sequence number assignment to _PermissionsUpdate_ object and appending this object to the updates list (by calling _permsUpdater.handleUpdateNotification(update)_ ) must be an atomic pair of operations, and it is not. In multi-threaded environment sooner or later we can end up with appending updates into the updates list with out of order sequence numbers. The problem is not that the updates may end up appended to the updates list not in the order of their arrival. We never guaranteed the right order for concurrent requests coming from different clients. The problem is that they end up appended to the list with their sequence numbers out of order, e.g. #101, #100, instead of #100, #101. Then handleUpdateNotification() method has the following lines: {quote}final boolean editNotMissed = lastSeenSeqNum.incrementAndGet() == update.getSeqNum();{quote} Suppose lastSeenSeqNum was 99, and the next appended update is #101. Then, obviously, 99+1 != 101, so editNotMissed is set to false. It triggers full update logic a few lines of code later: bq. if (imageRetreiver != null) { bq. try { bq. toUpdate = imageRetreiver.retrieveFullImage(update.getSeqNum()); bq. } catch (Exception e) { bq. LOGGER.warn("failed to retrieve full image: ", e); bq. } bq. updateable = updateable.updateFull(toUpdate); bq. } Since imageRetriever != null for permissions updater, all the hell breaks loose. It can be fixed in _SentryPlugin.java_ for all permission update methods as follows: {quote}PermissionsUpdate update; synchronized (permsUpdateLock) { // now sequence number increment and adding update to the list are performed atomically update = new PermissionsUpdate(permSeqNum.incrementAndGet(), false); update.addPrivilegeUpdate(authzObj).putToAddPrivileges(roleName, privilege.getAction().toUpperCase()); permsUpdater.handleUpdateNotification(update); } LOGGER.debug("Authz Perm preUpdate [" + update.getSeqNum() + "].."); {quote} _handleUpdateNotification()_ is always fast since it submits real work to thread pool; so, it does not block, and introducing additional synchronization should not affect concurrency. was: Sentry server can trigger full update to NameNode for no good reason, in high load cases, due to the bug in concurrency handling. Full update is always done at the initialization time and for large number of permissions and/or hdfs paths it takes lots of time and significantly increases (temporarily) heap size. It is performed during normal operations only if Sentry server decides that it has gaps in the partial updates' sequence numbers, so it restores the entire snapshot. Full update during normal operations is a highly disruptive event, leading to huge increasw in heap size, system slowdown, and even crash. Below is the explanation of the problem: ==================================================================== *SentryPlugin.java* has multiple permission update methods, such as _onDropSentryRole()_, _onDropSentryPrivilege()_, _onAlterSentryRoleDeleteGroups()_, etc. They, of course, can be called concurrently. And they all delegate work to _permsUpdater.handleUpdateNotification(update)_ call, which, in turn, serializes those requests by turning them into the Runnable tasks and submitting them into a single-threaded thread pool. Each job ends up appending an update to the updates list. So far so good, all updates are serialized. However, each of those methods first creates _PermissionsUpdate_ object with auto-incremented sequence number, and only then passes this object to _permsUpdate.handleUpdateNotification(update)_. Here is typical code snippet: {quote}PermissionsUpdate update = new PermissionsUpdate(permSeqNum.incrementAndGet(), false); // what if another permission update thread preempts this one right here and starts and finishes the whole method ??? update.addPrivilegeUpdate(authzObj).putToAddPrivileges(roleName, privilege.getAction().toUpperCase()); // ... or here ??? permsUpdater.handleUpdateNotification(update); LOGGER.debug("Authz Perm preUpdate [" + update.getSeqNum() + "]..");{quote} The problem is that sequence number assignment to _PermissionsUpdate_ object and appending this object to the updates list (by calling _permsUpdater.handleUpdateNotification(update)_ ) must be an atomic pair of operations, and it is not. In multi-threaded environment sooner or later we can end up with appending updates into the updates list with out of order sequence numbers. The problem is not that the updates may end up appended to the updates list not in the order of their arrival. We never guaranteed the right order for concurrent requests coming from different clients. The problem is that they end up appended to the list with their sequence numbers out of order, e.g. #101, #100, instead of #100, #101. Then handleUpdateNotification() method has the following lines: {quote}final boolean editNotMissed = lastSeenSeqNum.incrementAndGet() == update.getSeqNum();{quote} Suppose lastSeenSeqNum was 99, and the next appended update is #101. Then, obviously, 99+1 != 101, so editNotMissed is set to false. It triggers full update logic a few lines of code later: {quote}if (imageRetreiver != null) { try { toUpdate = imageRetreiver.retrieveFullImage(update.getSeqNum()); } catch (Exception e) { LOGGER.warn("failed to retrieve full image: ", e); } updateable = updateable.updateFull(toUpdate); } {quote} Since imageRetriever != null for permissions updater, all the hell breaks loose. It can be fixed in _SentryPlugin.java_ for all permission update methods as follows: {quote}PermissionsUpdate update; synchronized (permsUpdateLock) { // now sequence number increment and adding update to the list are performed atomically update = new PermissionsUpdate(permSeqNum.incrementAndGet(), false); update.addPrivilegeUpdate(authzObj).putToAddPrivileges(roleName, privilege.getAction().toUpperCase()); permsUpdater.handleUpdateNotification(update); } LOGGER.debug("Authz Perm preUpdate [" + update.getSeqNum() + "].."); {quote} _handleUpdateNotification()_ is always fast since it submits real work to thread pool; so, it does not block, and introducing additional synchronization should not affect concurrency. > SentryPlugin triggers full update due to concurrency bug > -------------------------------------------------------- > > Key: SENTRY-1891 > URL: https://issues.apache.org/jira/browse/SENTRY-1891 > Project: Sentry > Issue Type: Bug > Reporter: Vadim Spector > > Sentry server can trigger full update to NameNode for no good reason, in high > load cases, due to the bug in concurrency handling. Full update is always > done at the initialization time and for large number of permissions and/or > hdfs paths it takes lots of time and significantly increases (temporarily) > heap size. It is performed during normal operations only if Sentry server > decides that it has gaps in the partial updates' sequence numbers, so it > restores the entire snapshot. Full update during normal operations is a > highly disruptive event, leading to huge increasw in heap size, system > slowdown, and even crash. Below is the explanation of the problem: > ==================================================================== > *SentryPlugin.java* has multiple permission update methods, such as > _onDropSentryRole()_, _onDropSentryPrivilege()_, > _onAlterSentryRoleDeleteGroups()_, etc. They, of course, can be called > concurrently. And they all delegate work to > _permsUpdater.handleUpdateNotification(update)_ call, which, in turn, > serializes those requests by turning them into the Runnable tasks and > submitting them into a single-threaded thread pool. Each job ends up > appending an update to the updates list. So far so good, all updates are > serialized. > However, each of those methods first creates _PermissionsUpdate_ object with > auto-incremented sequence number, and only then passes this object to > _permsUpdate.handleUpdateNotification(update)_. Here is typical code snippet: > {quote}PermissionsUpdate update = new > PermissionsUpdate(permSeqNum.incrementAndGet(), false); > // what if another permission update thread preempts this one right here and > starts and finishes the whole method ??? > update.addPrivilegeUpdate(authzObj).putToAddPrivileges(roleName, > privilege.getAction().toUpperCase()); > // ... or here ??? > permsUpdater.handleUpdateNotification(update); > LOGGER.debug("Authz Perm preUpdate [" + update.getSeqNum() + "]..");{quote} > The problem is that sequence number assignment to _PermissionsUpdate_ object > and appending this object to the updates list (by calling > _permsUpdater.handleUpdateNotification(update)_ ) must be an atomic pair of > operations, and it is not. In multi-threaded environment sooner or later we > can end up with appending updates into the updates list with out of order > sequence numbers. > The problem is not that the updates may end up appended to the updates list > not in the order of their arrival. We never guaranteed the right order for > concurrent requests coming from different clients. The problem is that they > end up appended to the list with their sequence numbers out of order, e.g. > #101, #100, instead of #100, #101. Then handleUpdateNotification() method has > the following lines: > {quote}final boolean editNotMissed = lastSeenSeqNum.incrementAndGet() == > update.getSeqNum();{quote} > Suppose lastSeenSeqNum was 99, and the next appended update is #101. Then, > obviously, 99+1 != 101, so editNotMissed is set to false. It triggers full > update logic a few lines of code later: > bq. if (imageRetreiver != null) { > bq. try { > bq. toUpdate = > imageRetreiver.retrieveFullImage(update.getSeqNum()); > bq. } catch (Exception e) { > bq. LOGGER.warn("failed to retrieve full image: ", e); > bq. } > bq. updateable = updateable.updateFull(toUpdate); > bq. } > Since imageRetriever != null for permissions updater, all the hell breaks > loose. > It can be fixed in _SentryPlugin.java_ for all permission update methods as > follows: > {quote}PermissionsUpdate update; > synchronized (permsUpdateLock) { // now sequence number increment and adding > update to the list are performed atomically > update = new PermissionsUpdate(permSeqNum.incrementAndGet(), false); > update.addPrivilegeUpdate(authzObj).putToAddPrivileges(roleName, > privilege.getAction().toUpperCase()); > permsUpdater.handleUpdateNotification(update); > } > LOGGER.debug("Authz Perm preUpdate [" + update.getSeqNum() + "].."); > {quote} > _handleUpdateNotification()_ is always fast since it submits real work to > thread pool; so, it does not block, and introducing additional > synchronization should not affect concurrency. -- This message was sent by Atlassian JIRA (v6.4.14#64029)