一.故障现象

1.influxdb内存使用极高

内存占用达到37.1GiB:
image.png

2.influxdb的goroutine很多

goroutine数目达18W之巨,并且持续增长:
image.png

二.pprof排查

1.pprof heap

内存占用高的话,一般会pprof 堆内存;

下图可以看到,主要的heap内存在解析point,写入数据;
image.png

2. pprof goroutine

17W个goroutine都在执行远程写,并且有dial创建远程连接的操作:
image.png

看一下goroutine内部都在干啥:

curl controller24399:8086/debug/pprof/goroutine?debug=1 > goroutine.txt 

其中,有17w个goroutine锁在了RLock上,初步猜测RWLock可能出现了deadlock:
image.png

三.RWLock不可重入导致死锁

1.RWLock不可冲入

RWLock中的RLock()是不可重入的,这是因为

  • 中间一旦有Lock(),就会导致后面的RLock()被锁住;

    • 这是避免Lock()锁饥饿而做出的动作;
  • 最终出现:

    • Lock()等第一个RLock()释放读锁;
    • 第二个RLock()等待Lock()释放写锁;
    • 产生死锁;

image.png

2.RWLock避免Lock()写'锁饥饿'的机制

image.png

四.故障原因

下面结合influxdb的源码,进行原因分析。

influxdb远程连接的clientPool,goroutine内部出现RLock()的可重入调用,一旦中间其它goroutine有Lock(),就会产生死锁。

image.png

// cluster/client_pool.go
func (c *clientPool) conn(nodeID uint64) (net.Conn, error) {
   c.mu.RLock()
   conn, err := c.pool[nodeID].Get()
   c.mu.RUnlock()
   return conn, err
}

conn()函数:

  • 先拿到RLock();
  • 调用c.pool[nodeId].Get()创建链接;
  • 最后释放RUnlock();
// cluster/pool.go
func (c *boundedPool) Get() (net.Conn, error) {
   conns := c.getConns()
   if conns == nil {
      return nil, pool.ErrClosed
   }

   // Try and grab a connection from the pool
   select {
   case conn := <-conns:
      if conn == nil {
         return nil, pool.ErrClosed
      }
      return c.wrapConn(conn), nil
   default:
      // Could not get connection, can we create a new one?
      if atomic.LoadInt32(&c.total) < int32(cap(conns)) {
         conn, err := c.factory()        // 创建连接的时候,调用clientPool.size()尝试获取RLock()
         if err != nil {
            return nil, err
         }
         atomic.AddInt32(&c.total, 1)
         return c.wrapConn(conn), nil
      }
   }
    …
}

factory()实际调用的是connFactory.dial()函数:

  • 函数内部又调用了clientPool.size();

    func (c *connFactory) dial() (net.Conn, error) {
     if c.clientPool.size() > maxConnections {
        return nil, errMaxConnectionsExceeded
     }
    
     ni, err := c.metaClient.DataNode(c.nodeID)
     if err != nil {
        return nil, err
     }
     ...
    }

而clientPool.size()函数内部,又尝试申请RLock();

  • 产生可重入的RLock()调用
// cluster/client_pool.go
func (c *clientPool) size() int {
   c.mu.RLock()
   var size int
   for _, p := range c.pool {
      size += p.Len()
   }
   c.mu.RUnlock()
   return size
}

而中间一旦遇到其它goroutine对clientPool内部的Lock()就会产生死锁:

// cluster/client_pool.go
func (c *clientPool) setPool(nodeID uint64, p pool.Pool) {
   c.mu.Lock()
   c.pool[nodeID] = p
   c.mu.Unlock()
}

参考:

  1. https://www.cnblogs.com/luoming1224/p/14636543.html

a朋
63 声望38 粉丝