追蹤Redis Sentinel的CPU占有率長期接近100%的問題 一

問題描述


近日,在生產(chǎn)環(huán)境上,一個使用了Jedis 操作Redis數(shù)據(jù)庫的客戶端應(yīng)用,出現(xiàn)了如下異常:

15:43:55.102 ERROR [RpcWorkThread_7] cache.MstpMysqlCacheClient getInstance 168 - Cache 異常 
redis.clients.jedis.exceptions.JedisConnectionException: All sentinels down, cannot determine where is X.X.X.X-6379 master is running...
 at redis.clients.jedis.JedisSentinelPool.initSentinels(JedisSentinelPool.java:153) ~[jedis-2.6.2.jar:na]
 at redis.clients.jedis.JedisSentinelPool.<init>(JedisSentinelPool.java:69) ~[jedis-2.6.2.jar:na]
 at redis.clients.jedis.JedisSentinelPool.<init>(JedisSentinelPool.java:52) ~[jedis-2.6.2.jar:na]

省略

從異常可以明確,這是一個Jedis連接異常。All sentinels down? 馬上去檢查了所有Sentinel服務(wù)器,發(fā)現(xiàn)所有的Sentinel服務(wù)進(jìn)程都在運(yùn)行,沒有Down掉。但是卻出現(xiàn)了下面的問題:

image.png

可以發(fā)現(xiàn)每臺Sentinel的Cpu占有率一直都是接近于100%(注意是接近,經(jīng)常在99%上下),然后使用Redis-cli連接到Sentinel上進(jìn)行狀態(tài)查看,但是輸入任何命令都會超時:

image.png

現(xiàn)象就這么簡單,敘述完了,問題很詭異,現(xiàn)在開始進(jìn)行問題的追蹤和分析。

問題追蹤


生產(chǎn)環(huán)境Redis使用的版本是2.8.21,Jedis是2.6.2。為了方便定位,我們需要瀏覽他們的源碼。

尋找問題發(fā)生所在地

首先我們要了解目前生產(chǎn)環(huán)境的Jedis連接Redis集群的機(jī)制。目前的集群架構(gòu)設(shè)計為:

image.png

如上圖所示,客戶端使用JedisClient(即JedisSentinelPool)連接到各個Sentinel,通過Sentinel操作相應(yīng)的Redis主服務(wù)器。

首先,我們定位到異常所在函數(shù):initSentinels。這段函數(shù)比較長,但很關(guān)鍵,能讓我們清楚問題到底出現(xiàn)在Jedis和服務(wù)器的哪一端。

private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {

    HostAndPort master = null;
    boolean sentinelAvailable = false;

    log.info("Trying to find master from available Sentinels...");

    for (String sentinel : sentinels) {
      final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));

      log.fine("Connecting to Sentinel " + hap);

      Jedis jedis = null;
      try {
        jedis = new Jedis(hap.getHost(), hap.getPort());

        List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);

        // connected to sentinel...
        sentinelAvailable = true;

        if (masterAddr == null || masterAddr.size() != 2) {
          log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap
              + ".");
          continue;
        }

        master = toHostAndPort(masterAddr);
        log.fine("Found Redis master at " + master);
        break;
      } catch (JedisException e) {
        // resolves #1036, it should handle JedisException there's another chance
        // of raising JedisDataException
        log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e
            + ". Trying next one.");
      } finally {
        if (jedis != null) {
          jedis.close();
        }
      }
    }

    if (master == null) {
      if (sentinelAvailable) {
        // can connect to sentinel, but master name seems to not
        // monitored
        throw new JedisException("Can connect to sentinel, but " + masterName
            + " seems to be not monitored...");
      } else {
        throw new JedisConnectionException("All sentinels down, cannot determine where is "
            + masterName + " master is running...");
      }
    }

    log.info("Redis master running at " + master + ", starting Sentinel listeners...");

    for (String sentinel : sentinels) {
      final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));
      MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
      // whether MasterListener threads are alive or not, process can be stopped
      masterListener.setDaemon(true);
      masterListeners.add(masterListener);
      masterListener.start();
    }

我們先了解initSentinels的作用:

  • 首先,根據(jù)客戶端提供的Sentinel集合(存儲了所有Sentinel服務(wù)器的地址)進(jìn)行遍歷,每次遍歷中:
    • 構(gòu)造一個Jedis實(shí)例與當(dāng)前Sentinel建立連接。
    • 通過調(diào)用sentinelGetMasterAddrByName向Sentinel取得Master的地址和端口。
  • 以上遍歷結(jié)束后,如果成功的獲取所有Master的地址和端口話,則繼續(xù)啟動多個(與Sentinel服務(wù)器個數(shù)對應(yīng))MasterListener線程。
    這些線程的作用是為了不斷的訂閱Sentinel的消息, 以監(jiān)聽Master的主從切換。

為什么要監(jiān)聽主從切換呢,這涉及到了另一個函數(shù)initPool,篇幅限制,我們只要了解它的作用:
如果發(fā)生了主從切換,則重新初始化連接池,以改變連接池里的Jedis的句柄關(guān)聯(lián)對象,以始終關(guān)聯(lián)Master而不是Slave。所以,監(jiān)聽主從切換的目的是確保連接池給用戶的Jedis連接句柄絕對指向Master,因?yàn)槲覀兊募褐挥蠱aster接受讀寫。

現(xiàn)在 ,我們開始追蹤問題。可以清楚的看到,問題出現(xiàn)在第50行“All sentinels down...”。程序運(yùn)行到這里,說明前面的sentinel集合遍歷結(jié)束了。并且,此時“master == null、sentinelAvailable==false”。而“sentinelAvailable==false”說明了程序還未執(zhí)行到“sentinelAvailable = true;”就發(fā)生了異常,繼而說明問題出現(xiàn)在了前面:

jedis = new Jedis(hap.getHost(), hap.getPort());
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);

結(jié)合之前連接Sentinel出現(xiàn)的超時現(xiàn)象,猜測或許是因?yàn)闊o法與Sentinel建立正常的連接導(dǎo)致這里拋出異常。至此,可以明確這個問題與本地客戶端沒有多大關(guān)系,所以我們可以大膽的將注意力投入到Sentinel服服務(wù)器之上。

尋找問題函數(shù)

通過第一步,我們確定了問題出現(xiàn)在Sentinel服務(wù)器之上,繼續(xù)追蹤和分析。

根據(jù)我的經(jīng)驗(yàn),一個進(jìn)程的CPU占有率一直接近或達(dá)到100%,大部分情況下都可以說明該處于一個無限或類無限循環(huán)中,并且循環(huán)間隔沒有進(jìn)行Sleep的調(diào)用,使得進(jìn)程持續(xù)占有CPU時間片。這個猜測對嗎?我也不確定。

對于主服務(wù)是單進(jìn)程的Sentinel服務(wù),如果出現(xiàn)死循環(huán),對于外界的響應(yīng)必定是超時的。所以,我們目前首先要定位這個假設(shè)的循環(huán)到底在哪里。

此時,需要祭出兩個大殺器:GDB以及Strace,這兩個工具我就不做過多介紹了。

首先,在使用GDB前,我們需要得到Sentinel進(jìn)程ID,運(yùn)行top:

image.png

然后,根據(jù)其進(jìn)程ID得到其下所有線程的運(yùn)行狀況,運(yùn)行top -H -p 5515:

image.png

可以發(fā)現(xiàn),Sentinel總共有三個線程,5515線程CPU占用率異常,我們記住這個線程ID:5515。然后,使用GDB調(diào)試正在運(yùn)行的Sentinel進(jìn)程,執(zhí)行 gdb icdn 5515:

image.png

然后,我們輸入info thread以列出當(dāng)前進(jìn)程的所有線程信息:

image.png

上面,LWP即表示線程ID,所以,我們定位到了1號,繼續(xù)執(zhí)行thread 1表示切換到該進(jìn)程:

image.png

接著輸入bt,查看當(dāng)前的函數(shù)棧幀信息:

image.png

觀察棧幀信息,這一行很引人注目:

#1  0x00000000004194d1 in anetGenericAccept (err=0x6f9438 "accept: Too many open files", s=5, sa=0x7fff4851ab00, len=0x7fff4851ab8c) at anet.c:487

也就是說anetGenericAccept函數(shù)的一個err參數(shù)的值為“accept:Too many open files”。

另外,如果你多次運(yùn)行g(shù)db icdn 5515,有時候也會得到不同與上面的如下信息(每次attach不一樣說明程序調(diào)用的不是同一個函數(shù)棧):

image.png

可以看到,redisLograw的參數(shù)也包括了“Too many open files”這個錯誤。

綜上,可以發(fā)現(xiàn)兩類棧幀都出現(xiàn)了“Too many open files”錯誤,難道這些都是因?yàn)榇蜷_的文件數(shù)太多造成的?不急,我們先記住這個錯誤。

由前面可知,當(dāng)前Sentinel進(jìn)程的函數(shù)調(diào)用鏈?zhǔn)恰癿ain->aeMain->aeProcessEvents->acceptTcpHandler->anetTcpAccept->anetGenericAcept->accept”。馬上打開Redis的源碼,最終定位到這里:

static int anetGenericAccept(char *err, int s, struct sockaddr *sa, socklen_t *len) {
    int fd;
    while(1) {
        fd = accept(s,sa,len);
        if (fd == -1) {
            if (errno == EINTR)
                continue;
            else {
                anetSetError(err, "accept: %s", strerror(errno));
                return ANET_ERR;
            }
        }
        break;
    }
    return fd;
}

看到一個代碼里的while(1)讓我很是激動,所以我猜測是不是這里出現(xiàn)了問題,但只是猜測。此時,還需要使用另外一個殺器——strace,我們利用它對Sentinel使用的系統(tǒng)調(diào)用進(jìn)行跟蹤。執(zhí)行strace -T -r -c -p 5515(需要等待一定時間后手動結(jié)束strace):

image.png

由上圖可以清楚的看到,在跟蹤的這段時間內(nèi),accept、epoll_wait、open三個系統(tǒng)調(diào)用就占用了總計98%的時間,這當(dāng)然是不正常的。另外,accetp函數(shù)在短短的時間內(nèi)調(diào)用了2434680次,失敗調(diào)用占據(jù)了100%,這說明了之前anetGenericAccept中,accept 返回值每次都是-1。

最開始猜測的是Sentinel在anetGenericAccept中陷入while(1)不能自拔,但發(fā)現(xiàn)我想錯了,因?yàn)榇藭rerrno != EINTR,而應(yīng)該是“Too many open files”(錯誤號我沒查),整個函數(shù)返回的是ANET_ERR(-1)。

為了再次證實(shí)這個結(jié)論,決定使用strace打印當(dāng)前Sentinel正在執(zhí)行的函數(shù),運(yùn)行strace -t -p 5515:

image.png

果然,可以看到基本上都是這三個函數(shù)的執(zhí)行,說明Sentinel正在被這三個函數(shù)所糾纏。但是這個如何與之前GDB調(diào)試的結(jié)果對應(yīng)呢?很清楚,這里accept即為之前anetGenericAccept的accept調(diào)用。

而open函數(shù),是否對應(yīng)了redisLograw函數(shù)?查看了redisLograw的源碼:

void redisLogRaw(int level, const char *msg) {
    const int syslogLevelMap[] = { LOG_DEBUG, LOG_INFO, LOG_NOTICE, LOG_WARNING };
    const char *c = ".-*#";
    FILE *fp;
    char buf[64];
    int rawmode = (level & REDIS_LOG_RAW);
    int log_to_stdout = server.logfile[0] == '\0';

    level &= 0xff; /* clear flags */
    if (level < server.verbosity) return;

    fp = log_to_stdout ? stdout : fopen(server.logfile,"a");
    if (!fp) return;

    if (rawmode) {
        fprintf(fp,"%s",msg);
    } else {
        int off;
        struct timeval tv;

        gettimeofday(&tv,NULL);
        off = strftime(buf,sizeof(buf),"%d %b %H:%M:%S.",localtime(&tv.tv_sec));
        snprintf(buf+off,sizeof(buf)-off,"%03d",(int)tv.tv_usec/1000);
        fprintf(fp,"[%d] %s %c %s\n",(int)getpid(),buf,c[level],msg);
    }
    fflush(fp);

    if (!log_to_stdout) fclose(fp);
    if (server.syslog_enabled) syslog(syslogLevelMap[level], "%s", msg);
}

原來,關(guān)鍵就在于fp = log_to_stdout ? stdout : fopen(server.logfile,”a”);這一句,下面請聽解釋:

  • fopen底層實(shí)際調(diào)用的是open系統(tǒng)調(diào)用:int open(const char *pathname, int flags, mode_t mode);。
  • 通過查看fopen源碼,模式a表示的即為O_WRONLY|O_CREAT|O_APPEND,默認(rèn)權(quán)限為0666。
  • 所以,最終體現(xiàn)為:open(“/usr/local/SINO/redis/log/sentinel.log”,O_WRONLY|O_CREAT|O_APPEND, 0666)

這樣,我們可以得出這個結(jié)論:accept和fopen的所在函數(shù)anetGenericAccept與redisLogRaw的調(diào)用,導(dǎo)致CPU居高不下。

繼續(xù)往下定位

前面GDB的調(diào)試的兩類函數(shù)棧幀結(jié)果,發(fā)現(xiàn)調(diào)用棧中都存在acceptTcpHandler這個函數(shù):

image.png

acceptTcpHandler的源碼非常關(guān)鍵:

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[REDIS_IP_STR_LEN];
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);
    REDIS_NOTUSED(privdata);

    while(max--) {
        // accept 客戶端連接
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                redisLog(REDIS_WARNING,
                    "Accepting client connection: %s", server.neterr);
            return;
        }
        redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
        // 為客戶端創(chuàng)建客戶端狀態(tài)(redisClient)
        acceptCommonHandler(cfd,0);

根據(jù)redislogRaw記錄的錯誤“Accepting client connection…”,可以發(fā)現(xiàn), 問題出現(xiàn)在這句:redisLog(REDIS_WARNING,”Accepting client connection: %s”, server.neterr);

原來,anetTcpAccept中的accept調(diào)用返回了ANET_ERR,并且errno 對應(yīng)錯誤內(nèi)容為“Too many open files”,所以acceptTcpHandler會馬上返回?;旧峡梢源_定,CPU占用率居高不下的原因是對acceptTcpHandler不停的進(jìn)行調(diào)用,并且調(diào)用時間很短。

看到acceptTcpHandler這個函數(shù),如果了解Redis事件處理機(jī)制的人應(yīng)該會比較熟悉。對滴!這就是Redis事件處理機(jī)制中文件事件的處理函數(shù)之一,大概提一下:
Redis的事件包括時間和文件兩種事件,其中文件事件使用IO多路復(fù)用監(jiān)聽多個套接字。

  • 當(dāng)套接字變得可讀時,如客戶端執(zhí)行write、close或connect操作時,套接字產(chǎn)生AS_READBALE事件
  • 當(dāng)套接字變得可寫時,如客戶端執(zhí)行read操作時,套接字產(chǎn)生AE_WRITABLE事件。

而這里的acceptTcpHandler,是專門針對connect操作的,是Redis的連接應(yīng)答處理函數(shù),用來對連接服務(wù)器監(jiān)聽套接字的客戶端進(jìn)行應(yīng)答。

在initServer函數(shù)中可以看到,專門為連接事件注冊了acceptTcpHandler事件處理函數(shù)(傳入函數(shù)指針)。

void initServer() {
省略
  /* Create an event handler for accepting new connections in TCP and Unix
     * domain sockets. */
    for (j = 0; j < server.ipfd_count; j++) {
        if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL) == AE_ERR)
            {
                redisPanic(
                    "Unrecoverable error creating server.ipfd file event.");
            }
    }

省略
}

誰在不停的調(diào)用acceptTcpHandler

現(xiàn)在,我們定位到了CPU占有率持續(xù)增長的原因,是由于“Too many open files”導(dǎo)致acceptTcpHandler持續(xù)調(diào)用而產(chǎn)生的。歸納為兩個問題:

  • 為什么會產(chǎn)生too many open files
  • acceptTcpHandler為什么會持續(xù)調(diào)用

對于第一個問題,我們最后再做解釋,我們首先分析第二個。

在前面的GDB調(diào)試的結(jié)果的圖中,我們都發(fā)現(xiàn)了main->aeMain->aeProcessEvents這樣一個調(diào)用鏈,查看源碼,發(fā)現(xiàn)在aeMain中,這樣調(diào)用aeProcessEvents:

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

aeMain是Redis事件處理的主循環(huán),這里有個while()循環(huán),我覺得似乎發(fā)現(xiàn)了什么。原來,這一句表示,只要eventLoop->stop == 0,循環(huán)會一直繼續(xù),aeProcessEvents會一直被調(diào)用。其中,eventLoop你可以理解為保存了所有事件的一個結(jié)構(gòu)體,里面的每個事件都會綁定一個相應(yīng)的事件處理函數(shù),如對連接事件,會綁定acceptTcpHandler這個事件,具體可以查看源碼。

進(jìn)入aeProcessEvents中,我們可以發(fā)現(xiàn)其首先調(diào)用了aeApiPoll得到就緒事件的個數(shù)numevents,然后使用一個for循環(huán)去循環(huán)處理事件,直到numevents個數(shù)為0。

aeProcessEvents中主要通過fe->rfileProc(eventLoop,fd,fe->clientData,mask);來調(diào)用acceptTcpHandler以處理事件。為什么是rfileProc,回顧之前的Redis事件定義,因?yàn)檫B接事件是讀事件。

aeProcessEvents源碼如下:

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
省略
        numevents = aeApiPoll(eventLoop, tvp);
        for (j = 0; j < numevents; j++) {
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];

            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int rfired = 0;

           /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn't
             * processed, so we check if the event is still valid. */
            if (fe->mask & mask & AE_READABLE) {
                // rfired 
                rfired = 1;
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
            }
            if (fe->mask & mask & AE_WRITABLE) {
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
            }

            processed++;
        }

省略
}

所以,結(jié)合以上代碼,我們終于可以得出結(jié)論:

  • 由于aeMain中的while循環(huán)一直進(jìn)行,導(dǎo)致aeProcessEvents會不斷執(zhí)行。
  • aeProcessEvents先調(diào)用aeApiPoll,aeApiPoll會返回就緒事件的個數(shù)numevents 。
  • aeProcessEvents在有限循環(huán)(numevents )中調(diào)用fe->rfileProc(eventLoop,fd,fe->clientData,mask);,也就是調(diào)用了acceptTcpHandler去處理事件。
  • 而acceptTcpHandler又因?yàn)槌霈F(xiàn)了“Too many open files”問題快速返回,導(dǎo)致整個aeProcessEvents快速返回。
  • 如此周而復(fù)始。

當(dāng)然,有人會問,為什么CPU占有率不一直是100%,上面這個調(diào)用鏈即給出了答案,因?yàn)檫€有一丁點(diǎn)的時間片分給了這個調(diào)用鏈里的其他調(diào)用,而不只是accept、epoll_wait、open這三個系統(tǒng)調(diào)用。

至此,我們只剩下一個問題,為什么會出現(xiàn)“Too many open files”這個問題,造成前面acceptTcpHandler無法正確處理連接事件,這也是本次問題追蹤的最終目標(biāo)。下一篇文章將針對這個問題繼續(xù)進(jìn)行探討,敬請期待。

第一次寫真正意義上的技術(shù)博文,希望能得到大家的認(rèn)可,如有錯誤,請不吝指出,非常感謝。我會繼續(xù)努力,O(∩_∩)O哈哈~。

補(bǔ)充


aeApiPoll

aeApiPoll會調(diào)用epoll_wait,這也對應(yīng)了前面strace展示的三個函數(shù)之一,但epoll_wait調(diào)用正常,如圖:

image.png

aeApiPoll會獲取可執(zhí)行事件(其實(shí)是保存在了eventLoop->fired的fired數(shù)組中),最后返回就緒事件個數(shù)。源碼如下:


static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);

    if (retval > 0) {
        int j;

        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE;

            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }

    return numevents;
}

aeEventLoop

可以保存所有的事件及其對應(yīng)的事件處理器,這是一個很重要的結(jié)構(gòu)體。
aeApiPoll將事件保存到aeEventLoop 里面,然后aeProcessEvents調(diào)用aeEventLoop 的事件處理函數(shù)去處理事件。當(dāng)然,事件處理函數(shù)一開始已經(jīng)注冊給aeEventLoop 了。

/* State of an event based program 
 */
typedef struct aeEventLoop {
    int maxfd;   /* highest file descriptor currently registered */
    int setsize; /* max number of file descriptors tracked */
    long long timeEventNextId;
    time_t lastTime;     /* Used to detect system clock skew */
    aeFileEvent *events; /* Registered events */
    aeFiredEvent *fired; /* Fired events */
    aeTimeEvent *timeEventHead;
    int stop;
    void *apidata; /* This is used for polling API specific data */
    aeBeforeSleepProc *beforesleep;
} aeEventLoop;

aeFileEvent 即表示了一個事件,其包括事件數(shù)據(jù)clientData及對應(yīng)的處理函數(shù)rfileProc或wfileProc。

/* File event structure
 */
typedef struct aeFileEvent {
    int mask; /* one of AE_(READABLE|WRITABLE) */
    aeFileProc *rfileProc;
    aeFileProc *wfileProc;
    void *clientData;
} a

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容