通过增加timeout-control以解决当tcp连接一直处于SYN_SENT状态导致java中的ldap-client的failover不工作的问题
今天遇到一个问题是当测试模拟ldap
的服务端主节点挂掉的时候并在页面点击登录, 后端一直未作出响应.
这个问题的原因是因为代码中的ldap-client
的failover
未生效, 通过排查发现ldap-client
一直在连接ldap
的主节点,
并且tcp
连接一直处于SYN_SENT
状态.
由于ldap-client
没有默认情况下没有控制超时, 导致代码一直堵塞, 从而导致failover
不工作.
下面将开始介绍具体细节以及解决方案.
排查过程
确认服务配置的ldap
地址有那些
执行docker inspect
命令, 确认服务配置的ldap
地址
sudo docker inspect fastone-api |grep 'spring.ldap.urls'
输出如下
"spring.ldap.urls=ldap://10.0.3.16:389,ldap://10.0.3.3:389",
可以看到配置了两个ldap
地址分别为
- ldap://10.0.3.16:389
- ldap://10.0.3.3:389
确认服务的进程ID
执行ps
命令, 确认服务的进程ID
ps -ef |grep FastoneApplication
命令输出如下
ubuntu 22663 14459 0 16:52 pts/5 00:00:00 grep --color=auto FastoneApplication
root 31320 31246 1 14:23 ? 00:02:39 java -cp @/app/jib-classpath-file com.fastonetech.fastone.FastoneApplication
可以看到进程ID为31320
, 接下来通过lsof
命令查看31320
进程的tcp
连接状态
确认服务进程的tcp
连接状态
通过lsof
查看tcp
连接状态
sudo lsof -p 31320 |grep '10.0.3.16'
命令输出如下
java 31320 root 506u IPv6 5728853 0t0 TCP fastone:37408->10.0.3.16:ldap (SYN_SENT)
可以看到服务进程的tcp
连接状态一直处于SYN_SENT
状态.
SYN_SENT
状态的连接, 说明客户端已经发送了SYN
包, 但是没有收到SYN/ACK
包, 此时会处于SYN_SENT
状态
Workaround
由于这个问题会blocking
测试进度所以需要先给出一个workaround
来解决这个问题.
目前的解决方案是通过iptables
限制tcp
连接, 使其直接Connection refused
. 这样可以确保测试能够继续进行
通过iptables
新增OUTPUT
规则使tcp
连接直接失败
- 新增OUTPUT规则
注意
target
应该为REJECT
而不是DROP
.
这是因为DROP
对于tcp
连接来说, 会直接丢弃数据包.
所以需要使用REJECT
来拒绝连接, 并且需要指定--reject-with tcp-reset
来拒绝连接.
这样客户端会知道连接被拒绝了而不是一直处于SYN_SENT
状态.
sudo iptables -A OUTPUT -p tcp -d 10.0.3.16 --dport 389 -j REJECT --reject-with tcp-reset
- 查看规则列表
sudo iptables -L OUTPUT -v -n
输出如下
Chain OUTPUT (policy ACCEPT 301 packets, 95101 bytes)
pkts bytes target prot opt in out source destination
0 0 REJECT tcp -- * * 0.0.0.0/0 10.0.3.16 tcp dpt:389 reject-with tcp-reset
可以看到, 新增了一条REJECT
规则
- 测试规则是否生效
telnet 10.0.3.16 389
输出如下
Trying 10.0.3.16...
telnet: Unable to connect to remote host: Connection refused
可以看到连接被拒绝了, 说明规则生效了
删除规则
当修复代码后, 需要删除规则, 以便测试继续进行
- 删除规则
sudo iptables -D OUTPUT -p tcp -d 10.0.3.16 --dport 389 -j REJECT --reject-with tcp-reset
- 查看规则列表
sudo iptables -L OUTPUT -v -n
输出如下
Chain OUTPUT (policy ACCEPT 1922 packets, 947K bytes)
pkts bytes target prot opt in out source destination
- 确认连接状态恢复如初
telnet 10.0.3.16 389
输出如下
Trying 10.0.3.16...
^C
可以看到连接状态恢复如初, 客户端一直处于SYN_SENT
状态
问题原因
根据上面的分析, 问题的原因TCP
连接状态一直处于SYN_SENT
状态导致ldap
的failover
未生效
偏离设计预期
这里跟我一开始设计有偏差, 因为我一开始的设想的预期应该是直接Connection refused
, 类似如下行为
非堵塞
telnet localhost 389
当执行上述命令时, 会直接报错
Trying 127.0.0.1...
telnet: Unable to connect to remote host: Connection refused
服务端会直接拒绝连接, 但是实际上并没有这样的行为, 服务端一直处于SYN_SENT
状态, 也就是说服务端一直在尝试连接
堵塞
类似如下行为
telnet 10.0.3.16 389
输出如下
Trying 10.0.3.16...
^C
可以看到一直在尝试连接, 但是并没有超时, 也就是说一直在尝试连接
ldap-client
的timeout-control
未生效
另外一个问题是tpc
一直处于SYN_SENT
状态理论来说应该触发timeout-control
, 但是实际上并没有触发
接下来我们需要看一下java-ldap
中是如何实现timeout-control
的
默认超时时间
在java
中ldap
的连接是由com.sun.jndi.ldap.LdapCtx.connectTimeout
控制的.
参考代码如下
public final class LdapCtx extends ComponentDirContext
implements EventDirContext, LdapContext {
private int connectTimeout = -1; // no timeout value
}
可以看到connectTimeout
默认值为-1
, 也就是说默认情况下是没有超时的. 这其实不合理, 因为网络连接是有超时的,
否则会导致代码一直堵塞
解决方案
继承LdapContextSource
并增加timeout-control
配置
默认情况下ldap
是没有timeout-control
的, 不过其提供了配置connectTimeout
的方法, 是通过environment
来配置的.
对于上层应用来说我们用的是通过spring
封装过的ldap
, 所以我们需要对于spring
进行扩展,
主要是通过继承LdapContextSource
来实现
/**
* The extension of {@link LdapContextSource} to support more environment.
* Sometimes we need to set more environment to environment.
* For example, we need to set the connectTimeout to avoid thread blocking and makesure the failover works.
* For more details about the connectTimeout, see {@link com.sun.jndi.ldap.LdapCtx.connectTimeout}
* For more details about the readTimeout, see {@link com.sun.jndi.ldap.LdapCtx.readTimeout}
* For more details about the ldap failover, see {@link com.sun.jndi.ldap.LdapCtxFactory#getUsingURLs}
*
* @author Xiangcheng.Kuo
*/
public class WithMoreEnvironmentContextSource extends LdapContextSource {
private Integer connectTimeout = -1;
private Integer readTimeout = -1;
public void setConnectTimeout(Integer connectTimeout) {
this.connectTimeout = connectTimeout;
}
public void setReadTimeout(Integer readTimeout) {
this.readTimeout = readTimeout;
}
@Override
protected DirContext getDirContextInstance(Hashtable<String, Object> environment) throws NamingException {
// The module named "com.sun.jndi.ldap.LdapCtx" is not exported.
// So we can't use the following code
// LdapCtx.CONNECT_TIMEOUT
// For more information about CONNECT_TIMEOUT
// see 1. LdapCtx.initEnv 2. Connection.createSocket
environment.put("com.sun.jndi.ldap.connect.timeout", connectTimeout.toString());
environment.put("com.sun.jndi.ldap.read.timeout", readTimeout.toString());
return super.getDirContextInstance(environment);
}
}
将WithMoreEnvironmentContextSource
添加到容器中
接下来需要将WithMoreEnvironmentContextSource
添加到容器中, 确保上层的依赖如LdapTemplate
以及自定义的Reposity
使用到我们扩展的LdapContextSource
@Configuration
public class LdapConfiguration {
@Bean
public LdapContextSource ldapContextSource(
LdapProperties properties,
Environment environment,
ObjectProvider<DirContextAuthenticationStrategy> dirContextAuthenticationStrategy
) {
// copy from org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration#ldapContextSource
WithMoreEnvironmentContextSource source = new WithMoreEnvironmentContextSource();
dirContextAuthenticationStrategy.ifUnique(source::setAuthenticationStrategy);
PropertyMapper propertyMapper = PropertyMapper.get().alwaysApplyingWhenNonNull();
propertyMapper.from(properties.getUsername()).to(source::setUserDn);
propertyMapper.from(properties.getPassword()).to(source::setPassword);
propertyMapper.from(properties.getAnonymousReadOnly()).to(source::setAnonymousReadOnly);
propertyMapper.from(properties.getBase()).to(source::setBase);
propertyMapper.from(properties.determineUrls(environment)).to(source::setUrls);
propertyMapper.from(properties.getBaseEnvironment()).to(
(baseEnvironment) -> source.setBaseEnvironmentProperties(Collections.unmodifiableMap(baseEnvironment)));
// We need to make sure the failover works fast when using multiple ldap backend
if (properties.getUrls().length > 1) {
source.setConnectTimeout(2000);
source.setReadTimeout(2000);
}
return source;
}
}
主要代码复制自org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration#ldapContextSource
,
具体细节可以参考其源码
备注
当iptables
规则较多自定义的受影响, 如下所示
sudo iptables -L OUTPUT -v -n
Chain OUTPUT (policy ACCEPT 186K packets, 95M bytes)
pkts bytes target prot opt in out source destination
0 0 DROP tcp -- * * 10.0.3.16 0.0.0.0/0 tcp dpt:3306
0 0 DROP tcp -- * * 10.0.3.16 0.0.0.0/0 tcp dpt:389
0 0 DROP tcp -- * * 10.0.3.16 0.0.0.0/0 tcp spt:389
0 0 DROP tcp -- * * 10.0.3.16 0.0.0.0/0 tcp spt:389
63 3276 DROP tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:389
0 0 REJECT tcp -- * * 0.0.0.0/0 10.0.3.16 tcp dpt:389 reject-with tcp-reset
需要清空所有OUTPUT规则
sudo iptables -F OUTPUT
重新查看OUTPUT规则
sudo iptables -L OUTPUT -v -n
Chain OUTPUT (policy ACCEPT 1883 packets, 1126K bytes)
pkts bytes target prot opt in out source destination