记一次解决线上OOM的心路历程(配置中心)

背景:随着Best Diamond的不断推广、成熟,内部使用其来进行配置统一管理的项目越来越多,在各自的测试环境中测试达到他们的预期后,逐渐将其投入生产环境使用。

事故:有一个公司内部的核心系统生产发布时Best Diamond client日志输出连接server端失败,导致项目配置文件不能及时拉取(由于配置中心的客户端在拉取到服务端的配置后会将其存入服务器本地,当因网络或者其他原因导致没有拉取到server端的配置时会自动读取本地已有的配置,因此并没有影响该项目的正常启用,但这是侥幸的,因为万一该项目在Best Diamond的服务端修改了某一变量值,则后果不堪设想)。

事故排查:

      在日志平台查看Best Diamond服务端的日志输出,看到某一台服务端的日志中有:OutOfMemoryError: GC overhead limit exceeded。看到上面这个错时基本已经断定是内存泄漏引起。

临时处理:由于是生产环境,为不一样用户的生产环境的正常使用第一时间对server端进行重启(此处未将当时的内存先dump出来再重启,属于严重的错误操作,为后续的排查带来一些困难)。

原因分析:

            1、测试环境中为何从未出现过该问题,测试环境中client的连接数远比生产环境中的大,测试环境和生产环境有哪里不一样。

            2、生产环境为何上线一年多了从未出现此问题,却在此时出现了。

继续排查:看代码提交记录及生产发版日志,拉取线上版本代码(注:代码多人在维护),正如上面所提,并未将当时的现场内存dump到文件中(算是给以后一个警醒),此时再分析是有一定难度的,但不能放任不管或者等到下一次宕机再来排查,处理:既然怀疑是内存泄漏导致的,那毕竟是代码有处理不到的地方,于是dump出了另一台生产环境同版本的服务端内存,命令为:

jmap -dump:live,format=b,file=dump.hprof 24971(PID)

将dump出来的内存文件借助jvisualvm进行分析:

有此可以看到String、Date、ClinetInfo这类型的实例数均过百万(char[] 是因为String内部是有char[]实现,从实例数看程序单独使用char[]的情况排除),占用了虚拟机60%的内存,由此我们怀疑ClientInfo实例存在内存泄漏,于是查看代码:

package com.best.diamond.model.netty;

import java.util.Date;

public class ClientInfo {

    private String address;

    private Date connectTime;

    public ClientInfo(String address, Date connectTime) {
        this.address = address;
        this.connectTime = connectTime;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public Date getConnectTime() {
        return connectTime;
    }

    public void setConnectTime(Date connectTime) {
        this.connectTime = connectTime;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + ((address == null) ? 0 : address.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        ClientInfo other = (ClientInfo) obj;
        if (address == null) {
            if (other.address != null)
                return false;
        }
        else if (!address.equals(other.address))
            return false;
        return true;
    }

}

果不其然,ClinetInfo中有String、Date两类属性,于是便断定ClientInfo是导致内存泄漏的罪魁祸首。

继续看代码发现有很多地方都使用到了ClientInfo,进一步步排查、排除,最终锁定了自实现Netty的一个ChannelHandler,DiamondServerHandler该类主要代码如下:

@Sharable
public class DiamondServerHandler extends SimpleChannelInboundHandler<String> {

    private final static String HEARTBEAT = "heartbeat";

    private final static String DIAMOND = "bestdiamond=";

    private final static Logger logger = LoggerFactory.getLogger(DiamondServerHandler.class);

    private final static Charset CHARSET = Charset.forName("UTF-8");

    public static ConcurrentHashMap<ClientKey, List<ClientInfo>> clients = new ConcurrentHashMap<>();

    private ConcurrentHashMap<String /*client address*/, ChannelHandlerContext> channels = new ConcurrentHashMap<String, ChannelHandlerContext>();

    @Autowired
    private ProjectConfigService projectConfigService;

    @Autowired
    private ProjectModuleService projectModuleService;

            modules = new ArrayList<>(Arrays.asList(StringUtils.split(facet.getModules(), Constants.COMMA)));
        }
        if (CollectionUtils.isEmpty(modules)) {
            throw new ServiceException(StatusCode.ILLEGAL_ARGUMENT, "Module has not been maintained yet.");
        }
        ClientKey key = new ClientKey();
        key.setProjCode(facet.getProjCode());
        key.setProfile(facet.getProfile());
        key.setModules(modules);
        List<ClientInfo> addrs = clients.get(key);
        if (addrs == null) {
            addrs = new ArrayList<>();
        }
        String clientAddress = ctx.channel().remoteAddress().toString().substring(1);
        ClientInfo clientInfo = new ClientInfo(clientAddress, new Date());
        addrs.add(clientInfo);
        clients.put(key, addrs);
        channels.put(clientAddress, ctx);
        return facet;
    }

    @Override
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        String address = ctx.channel().remoteAddress().toString();
        channels.remove(address);
        String watchConfigs = removeClientConnectionInfo(ctx);
        removeConfigWatchNode(watchConfigs, address);
        for (List<ClientInfo> infoList : clients.values()) {
            for (ClientInfo client : infoList) {
                if (address.equals(client.getAddress())) {
                    infoList.remove(client);
                    break;
                }
            }
        }
        logger.info(ctx.channel().remoteAddress() + " 断开连接。");
    }

从代码我们可以看出ClinetInfo主要用来Server对于Client的部分信息的存储,客户端在连接上服务端时创建ClientInfo实例,连接断开时释放,心细的同学应该发现了,此处用于存储和删除ClientInfo的key--address变量处理是有问题的,存储时:

String clientAddress = ctx.channel().remoteAddress().toString().substring(1);

删除时:

String address = ctx.channel().remoteAddress().toString();

这样ClientInfo中对于断开后的Clinet信息是永远不会删除的,由此便找到了内存泄漏的地方。(说明:由于DiamondServerHandler使用了Netty的@Sharable注解,它将被所有channel公用,引用一直不会失效,因此它一直不会被回收)。

调整后的代码:

@Sharable
public class DiamondServerHandler extends SimpleChannelInboundHandler<String> {

    private final static String HEARTBEAT = "heartbeat";

    private final static String DIAMOND = "bestdiamond=";

    private final static Logger logger = LoggerFactory.getLogger(DiamondServerHandler.class);

    private final static Charset CHARSET = Charset.forName("UTF-8");

    private static Object locker = new Object();

    public static ConcurrentHashMap<ClientKey, List<ClientInfo>> clients = new ConcurrentHashMap<>();

    private ConcurrentHashMap<String /*client address*/, ChannelHandlerContext> channels = new ConcurrentHashMap<String, ChannelHandlerContext>();

    @Autowired
    private ProjectConfigService projectConfigService;

    @Autowired
    private ProjectModuleService projectModuleService;

            modules = new ArrayList<>(Arrays.asList(StringUtils.split(facet.getModules(), Constants.COMMA)));
        }
        if (CollectionUtils.isEmpty(modules)) {
            throw new ServiceException(StatusCode.ILLEGAL_ARGUMENT, "Module has not been maintained yet.");
        }
        ClientKey key = new ClientKey();
        key.setProjCode(facet.getProjCode());
        key.setProfile(facet.getProfile());
        key.setModules(modules);
        List<ClientInfo> addrs = clients.get(key);
        synchronized (locker) {
            if (null == addrs) {
                addrs = new ArrayList<>();
            }
        }
        String clientAddress = ctx.channel().remoteAddress().toString().substring(1);
        ClientInfo clientInfo = new ClientInfo(clientAddress, new Date());
        addrs.add(clientInfo);
        clients.put(key, addrs);
        channels.put(clientAddress, ctx);
        return facet;
    }

    @Override
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
        String address = ctx.channel().remoteAddress().toString().substring(1);
        channels.remove(address);
        removeConfigWatchNode(watchConfigs, address.substring(1));
        removeConfigWatchNode(watchConfigs, address);
        for (List<ClientInfo> infoList : clients.values()) {
            for (ClientInfo client : infoList) {
                if (address.equals(client.getAddress())) {
                    infoList.remove(client);
                    break;
                }
            }
        }
        logger.info(ctx.channel().remoteAddress() + " 断开连接。");
    }

调整后发版测试,问题解决。

其实还没有结束,即使这里会存在内存泄漏但是超过百万的实例数还是有点太多了,进过排查、确定发现是历史问题导致的,前期是使用了公司的HA做的服务端的高可用,HA代理机每分钟便会断开连接,再重新连接,咨询运维同学说这个是HA的一种机制。

此处坑:我们当初使用Netty本来就是为了服务端与客户端之间维护长连接,HA的这种机制与其相违背,后来我们架构上有所调整,不在借助前端的HA来代理客户端的连接,这样也合理一点,最终解决了问题。

总结:

        1、出现OOM时绝大部分是代码的问题,第一时间需要dump出当时虚机的内存快照,便于定位问题。

        2、借助工具去分析dump出的内存文件,可以提高排查的效率,此处jvisualvm其实只是最基础的排查工具,后面我们有使用了其他的可视化工具解决了其他宕机情况(后续在写)。

        3、定位到问题解决后,依然需要找到提交代码的同学,给予警醒。

猜你喜欢

转载自my.oschina.net/u/3345762/blog/1644973