LDAP连接池来自Apache Java库 – 我们需要解绑吗

huangapple go评论58阅读模式
英文:

LDAP Pooled Connection from Apache Java Library - Do We Need to unbind

问题

Background

  • 我们正在从Java(Spring Boot)应用程序连接到LDAP(OpenLDAP)服务。我们在TLS和内存使用方面遇到了问题。
  • 我们使用Apache Directory LDAP API(v2)库进行连接。
  • 我们使用了连接池连接到LDAP服务器。
  • 我们使用StartTLS来保护Java服务与LDAP服务器之间的连接。
  • 我们实际上并不在这里对LDAP服务器进行身份验证!
    • 我们的API网关处理身份验证(与同一LDAP服务进行身份验证)。
    • 我们在代码中执行两件事:
      • 在收到API请求时获取更多用户数据,
      • 从将其与另一个源同步的服务更新LDPA。

内存问题

我们的Java服务出现了内存不足错误。堆栈跟踪如下:

Exception in thread "pool-2454-thread-1" java.lang.OutOfMemoryError: Java heap space
	at java.util.HashMap.resize(HashMap.java:704)
	at java.util.HashMap.putVal(HashMap.java:629)
	...
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [?:1.8.0_261]
	at java.lang.Thread.run(Unknown Source) [?:1.8.0_261]

我的同事在JVM上调试了一个具有128M内存的简单API,LDAP连接池看起来在不做太多事情的情况下使用了大量内存:
LDAP连接池来自Apache Java库 – 我们需要解绑吗

我注意到代码在执行查询后进行了unbind操作。这看起来不对劲 - 我们不是在每个用户上进行绑定,我们有一个单一的(只读)用户,API服务连接到该用户,使它们能够读取连接用户的详细信息,还有另一个(读写)用户用于同步服务。据我所知,绑定类似于登录,从使用其他连接池的经验来看,不是每次都这样做。我想知道解绑但不关闭是否会留下僵尸连接并占用内存?

SSL问题

然而,如果我们不执行解绑操作,日志中会经常出现以下错误,而没有合理的方法找出它来自哪里。我没有找到太多相关信息:

2020-10-14 11:08:57.817 [NioProcessor-3] WARN  org.apache.directory.ldap.client.api.LdapNetworkConnection - Outbound done [MDC: {}]
javax.net.ssl.SSLException: Outbound done
	at org.apache.mina.filter.ssl.SslFilter.messageReceived(SslFilter.java:513) ~[mina-core-2.1.3.jar:?]
	...
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [?:1.8.0_261]
	at java.lang.Thread.run(Unknown Source) [?:1.8.0_261]

可能的解决方法

我在网上找到了一些示例,建议像下面这样的模式:

if (connection.isConnected() && connection.isAuthenticated()) {
	connection.bind();
	try {
		// 做一些操作
	} finally {
		connection.unBind();
	}
}

但是这感觉不对 - 或者至少是一个权宜之计。

问题

因此,问题分为两部分:

  1. 我们应该为每个查询进行绑定和解绑操作(即使我们始终作为同一用户进行身份验证),还是会失去连接池的好处?
  2. 是否有人对javax.net.ssl.SSLException: Outbound done异常有任何信息?这是否相关,如何解决?
英文:

We are connecting to an LDAP (OpenLDAP) service from a Java (Spring Boot) application. We are having issues with TLS and with memory usage.

Background

  • We are using the Apache Directory LDAP API (v2) library for the connection.
  • We are using a pooled connection to the LDAP server.
  • We are using StartTLS to secure the connections between the Java service and the LDAP server.
  • We are not actually authenticating against the LDAP server from here!
    • Our API gateway handles authentication (against the same LDAP service).
    • We are doing two thing in our code:
      • Fetching more data on the user (when receiving API requests) and
      • Updating the LDPA from a service that keeps it synchronised with another source.

Memory Issues

We are getting out-of-memory errors on the Java service. The stack trace looks like:

Exception in thread "pool-2454-thread-1" java.lang.OutOfMemoryError: Java heap space
	at java.util.HashMap.resize(HashMap.java:704)
	at java.util.HashMap.putVal(HashMap.java:629)
	at java.util.HashMap.put(HashMap.java:612)
	at sun.security.util.MemoryCache.put(Cache.java:365)
	at sun.security.ssl.SSLSessionContextImpl.put(SSLSessionContextImpl.java:181)
	at sun.security.ssl.ClientHandshaker.serverFinished(ClientHandshaker.java:1293)
	at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:379)
	at sun.security.ssl.Handshaker.processLoop(Handshaker.java:1082)
	at sun.security.ssl.Handshaker.process_record(Handshaker.java:1010)
	at sun.security.ssl.SSLEngineImpl.readRecord(SSLEngineImpl.java:1032)
	at sun.security.ssl.SSLEngineImpl.readNetRecord(SSLEngineImpl.java:913)
	at sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:783)
	at javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:626)
	at org.apache.mina.filter.ssl.SslHandler.unwrap(SslHandler.java:774)
	at org.apache.mina.filter.ssl.SslHandler.unwrapHandshake(SslHandler.java:710)
	at org.apache.mina.filter.ssl.SslHandler.handshake(SslHandler.java:596)
	at org.apache.mina.filter.ssl.SslHandler.messageReceived(SslHandler.java:355)
	at org.apache.mina.filter.ssl.SslFilter.messageReceived(SslFilter.java:517)
	at org.apache.mina.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(DefaultIoFilterChain.java:650)
	at org.apache.mina.core.filterchain.DefaultIoFilterChain.access$1300(DefaultIoFilterChain.java:49)
	at org.apache.mina.core.filterchain.DefaultIoFilterChain$EntryImpl$1.messageReceived(DefaultIoFilterChain.java:1128)
	at org.apache.mina.core.filterchain.IoFilterAdapter.messageReceived(IoFilterAdapter.java:122)
	at org.apache.mina.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(DefaultIoFilterChain.java:650)
	at org.apache.mina.core.filterchain.DefaultIoFilterChain.fireMessageReceived(DefaultIoFilterChain.java:643)
	at org.apache.mina.core.polling.AbstractPollingIoProcessor.read(AbstractPollingIoProcessor.java:539)
	at org.apache.mina.core.polling.AbstractPollingIoProcessor.access$1200(AbstractPollingIoProcessor.java:68)
	at org.apache.mina.core.polling.AbstractPollingIoProcessor$Processor.process(AbstractPollingIoProcessor.java:1222)
	at org.apache.mina.core.polling.AbstractPollingIoProcessor$Processor.process(AbstractPollingIoProcessor.java:1211)
	at org.apache.mina.core.polling.AbstractPollingIoProcessor$Processor.run(AbstractPollingIoProcessor.java:683)
	at org.apache.mina.util.NamePreservingRunnable.run(NamePreservingRunnable.java:64)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
2020-10-13 10:03:23.388677637+03:00 Starting: /etc/alternatives/jre/bin/java -Xms128M -Xmx256M -Dlogging.config=/services/registry.svc/log4j2.json -jar 

My colleague debugged a simple API with 128m memory on the JVM and the LDAP pool looked to be using a lot of memory for not doing much:
LDAP连接池来自Apache Java库 – 我们需要解绑吗

I noticed that the code was doing unbind after making the queries. This smelt wrong - we are not binding as each user, we have a single (read-only) user that the API services connect as that allows them to read details on the user connecting and another (read-write) user for the synch service. As I understand it, bind is like login and from using other connection pools that's what you don't do each time. I wondered if by unbinding but not closing we were leaving zombie connections and eating memory?

SSL Issues

However, if we don't unbind we get the following error appearing quite a lot in the logs, without any reasonable way to find where it comes from. Haven't found much on it:

2020-10-14 11:08:57.817 [NioProcessor-3] WARN  org.apache.directory.ldap.client.api.LdapNetworkConnection - Outbound done [MDC: {}]
javax.net.ssl.SSLException: Outbound done
	at org.apache.mina.filter.ssl.SslFilter.messageReceived(SslFilter.java:513) ~[mina-core-2.1.3.jar:?]
	at org.apache.mina.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(DefaultIoFilterChain.java:650) [mina-core-2.1.3.jar:?]
	at org.apache.mina.core.filterchain.DefaultIoFilterChain.access$1300(DefaultIoFilterChain.java:49) [mina-core-2.1.3.jar:?]
	at org.apache.mina.core.filterchain.DefaultIoFilterChain$EntryImpl$1.messageReceived(DefaultIoFilterChain.java:1128) [mina-core-2.1.3.jar:?]
	at org.apache.mina.core.filterchain.IoFilterAdapter.messageReceived(IoFilterAdapter.java:122) [mina-core-2.1.3.jar:?]
	at org.apache.mina.core.filterchain.DefaultIoFilterChain.callNextMessageReceived(DefaultIoFilterChain.java:650) [mina-core-2.1.3.jar:?]
	at org.apache.mina.core.filterchain.DefaultIoFilterChain.fireMessageReceived(DefaultIoFilterChain.java:643) [mina-core-2.1.3.jar:?]
	at org.apache.mina.core.polling.AbstractPollingIoProcessor.read(AbstractPollingIoProcessor.java:539) [mina-core-2.1.3.jar:?]
	at org.apache.mina.core.polling.AbstractPollingIoProcessor.access$1200(AbstractPollingIoProcessor.java:68) [mina-core-2.1.3.jar:?]
	at org.apache.mina.core.polling.AbstractPollingIoProcessor$Processor.process(AbstractPollingIoProcessor.java:1222) [mina-core-2.1.3.jar:?]
	at org.apache.mina.core.polling.AbstractPollingIoProcessor$Processor.process(AbstractPollingIoProcessor.java:1211) [mina-core-2.1.3.jar:?]
	at org.apache.mina.core.polling.AbstractPollingIoProcessor$Processor.run(AbstractPollingIoProcessor.java:683) [mina-core-2.1.3.jar:?]
	at org.apache.mina.util.NamePreservingRunnable.run(NamePreservingRunnable.java:64) [mina-core-2.1.3.jar:?]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) [?:1.8.0_261]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [?:1.8.0_261]
	at java.lang.Thread.run(Unknown Source) [?:1.8.0_261]

Possible Work-Around

I did find some examples online that suggested a pattern like:

if (connection.isConnected() && connection.isAuthenticated()) {
	connection.bind();
try {
	// do stuff
} finally {
	connection.unBind();
}

But this feels wrong - or at least a work-around

Question

So, two parts to my question:

  1. Should we be binding and unbinding each query (even though we are always authenticated as the same user), or are we losing the benefit of the pool then?
  2. Does anyone have any info on the javax.net.ssl.SSLException: Outbound done exception? is it related and how to solve it?

答案1

得分: 1

所以,关闭连接似乎是我错误的做法。我假设当从池中获取连接时,如果我“关闭”连接,它会将连接返回到池中。但事实上它关闭了连接,但仍将其保留在池中(可能是借出的状态,也可能是不可用的状态,没有深入调查)。看起来我实际上需要将其返回到池中,并为此保留对池的引用。

我有一个处理连接池并返回连接的函数(实际上是一个注入的服务)。我想要做的是:

try (final LdapConnection connection = ldapService.getConnection()) {
   // 使用连接进行操作
}

但我最终做的是定义了一个类,类似于:

/**
 * 包装来自连接池的连接,以便关闭操作将其释放回池中。
 *
 * <p>您需要对池保持引用才能释放它,因此使用了一个包装器。</p>
 */
public class PooledLdapConnection implements Closeable {

  private final LdapConnection connection;
  private final LdapConnectionPool pool;

  public PooledLdapConnection(final LdapConnection connection, final LdapConnectionPool pool) {
    this.connection = connection;
    this.pool = pool;
  }

  public LdapConnection getConnection() {
    return connection;
  }

  @Override
  public void close() throws IOException {
    if (pool != null) {
      try {
        pool.releaseConnection(connection);
      } catch (final LdapException e) {
        throw new IOException(e.getMessage(), e);
      }
    }
  }
}

然后,我的LDAP服务现在返回这个类 - 在函数中,不再只返回 pool.getConnection(),而是返回 new PooledLdapConnection(pool.getConnection(), pool)

然后我可以这样使用:

try (final PooledLdapConnection connection = ldapService.getConnection()) {
   // 使用 connection.getConnection() 进行操作
}

当它完成并“关闭”时,实际上只是返回到了池中。我本可以用我的 PooledLdapConnection 实现 LdapConnection 接口,并将除 close 之外的所有函数的实现直接委托给底层的连接对象,但这样做更简单,而且如果接口被更新,就不需要重构了。

我觉得这实际上应该是库为我所做的事情!由池返回的实现应该是与获取单个连接时返回的实现不同的对象,差异在于自动关闭的行为。但这似乎也可以工作。

我还有一个未解决的问题。在我们的开发环境中,DNS 配置错误,它指向了错误的服务器,尝试连接 LDAP 时出现问题。在这种情况下,它仍然在消耗连接,直到达到 Java 文件限制。尚未进一步调查这个问题。

英文:

So, it seems I was wrong to close the connection. I assumed that, when getting a connection from a pool if I "close" the connection it returned it to the pool. Seems it closed it but kept it in the pool (maybe left borrowed, maybe just unusable, didn't investigate that far). It seems I need instead to return it to the pool, and to keep a reference to the pool for that.

I had a function (actually an injected service) that handles the pool and returns a connection. What I was trying to do was:

try (final LdapConnection connection = ldapService.getConnection()) {
   // Do stuff with connection
}

What I ended up doing was defining a class like:

/**
 * Wraps a connection from a connection pool such that close with release it back to the pool.
 *
 * &lt;p&gt;You need a reference to the pool in order to release it, so using a wrapper&lt;/p&gt;
 */
public class PooledLdapConnection implements Closeable {

  private final LdapConnection connection;
  private final LdapConnectionPool pool;

  public PooledLdapConnection(final LdapConnection connection, final LdapConnectionPool pool) {
    this.connection = connection;
    this.pool = pool;
  }

  public LdapConnection getConnection() {
    return connection;
  }

  @Override
  public void close() throws IOException {
    if (pool != null) {
      try {
        pool.releaseConnection(connection);
      } catch (final LdapException e) {
        throw new IOException(e.getMessage(), e);
      }
    }
  }
}

Then my LDAP service now returns that - in the function, instead of just returning pool.getConnection() I return new PooledLdapConnection(pool.getConnection(), pool)

Then I can

try (final PooledLdapConnection connection = ldapService.getConnection()) {
   // Do stuff with connection.getConnection()
}

and when it completes and "closes" it actually just returns to the pool. I could have implemented the LdapConnection interface with my PooledLdapConnection and simply proxied the implementation of all the functions except close directly to my underlying connection object, but this was easier, and also won't need refactoring if the interface is ever updated.

I do feel this is what the library should have done for me! The implementation returned by the pool should be a different object than the one returned by getting a single connection, with the difference being in what the auto-close does. But this seems to work.

I have one remaining issue. We had a misconfiguration of the DNS in our development environment, so it pointed at the wrong server for trying to connect to an LDAP. In this case, it still ate connections until we hit the java file limit. Haven't investigated that further yet

huangapple
  • 本文由 发表于 2020年10月16日 23:28:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/64392111.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定