容器设了 2GB 内存上限服务却反复被 OOMKilled、可 JVM 堆明明没满:JVM 不感知容器限制按宿主机算堆撑爆容器的避坑复盘

这是我们把一个老 Java 服务上容器时踩的第一个也是最懵的一个坑。我们把这个服务打成镜像部署到 K8s 给它的容器设了一个内存上限 2GB,我们估摸着这个服务平时也就用几百兆内存 2GB 绰绰有余了。可服务一启动跑不了多久就被 K8s 给 OOMKilled 因内存超限被杀了,杀了之后自动重启重启完跑一会又被杀如此反复陷入了 CrashLoopBackOff 崩溃重启循环根本起不来。我特别纳闷这服务平时撑死用几百兆我给了 2GB 怎么会内存超限,我登进去看 JVM 的堆内存堆明明没满啊离 2GB 还远着呢可容器就是被 OOMKilled 了。排查到最后真相是一个容器化时代特有的极其经典的认知陷阱——JVM 不感知容器的内存限制。原来老版本的 JVM 在计算自己默认能用多大堆内存时看的是宿主机的总内存而不是容器被限制的那点内存,我们那台宿主机有 64GB 内存 JVM 一看哦有 64GB 就按一个比例通常是四分之一给自己分配了默认的最大堆它以为自己能用 16GB 的堆,可容器的内存上限明明只有 2GB,于是 JVM 毫无顾忌地把堆往上涨一旦涨过 2GB 这条容器红线 cgroup 就把容器 OOMKilled,而此时 JVM 自己还一脸无辜以为离它认为的 16GB 上限还远得很。这篇文章从这次容器明明限了 2GB JVM 却以为有 16GB 的事故出发,讲透容器化资源避坑:理解容器只是受限的视图而进程可能看不见限制、让 JVM 感知容器或显式设堆并给堆外留余量、基于真实用量设 limit 并监控、CPU 也有一模一样的坑、分清 OutOfMemoryError 和 OOMKilled,以及一个根本认知——换了环境就要重新审视所有想当然的假设。

这是我们把一个老 Java 服务"上容器"时,踩的第一个、也是最懵的一个坑。我们把这个服务打成镜像、部署到 K8s,给它的容器设了一个内存上限(memory limit)2GB——我们估摸着,这个服务平时也就用几百兆内存,2GB 绰绰有余了。可服务一启动、跑不了多久,就被 K8s 给 OOMKilled(因内存超限被杀)了;杀了之后自动重启,重启完跑一会儿又被杀,如此反复,陷入了 CrashLoopBackOff(崩溃重启循环),根本起不来。我特别纳闷:这服务平时撑死用几百兆,我给了 2GB,怎么会内存超限?我登进去看 JVM 的堆内存,堆明明没满啊,离 2GB 还远着呢,可容器就是被 OOMKilled 了。

排查到最后,真相是一个容器化时代特有的、极其经典的认知陷阱——JVM 不"感知"容器的内存限制。原来,老版本的 JVM(或者没配相关参数的 JVM),在计算自己"默认能用多大堆内存"时,看的是宿主机的总内存,而不是容器被限制的那点内存。我们那台宿主机有 64GB 内存,JVM 一看"哦,有 64GB",就按一个比例(通常是 1/4)给自己分配了默认的最大堆——它以为自己能用 16GB 的堆!可容器的内存上限,明明只有 2GB。于是,当 JVM 兴致勃勃地、毫无顾忌地把堆往上涨(因为它以为自己有 16GB 的空间),一旦涨过了 2GB 这条容器红线,K8s(底层的 cgroup)就会毫不留情地把这个"超额使用内存"的容器 OOMKilled——而此时 JVM 自己还一脸无辜,它以为才用了 2GB、离它认为的 16GB 上限还远得很,根本没意识到自己已经撞穿了那道它"看不见"的 2GB 容器内存墙。这篇文章,就从这次"容器明明限了 2GB、JVM 却以为有 16GB"的事故讲起,把容器化部署里这个"进程与容器内存限制认知错位"的经典坑,讲清楚。

故障现场:一个"以为自己很有钱"的 JVM

先把这个"认知错位"的过程理一理:

事故的根源: JVM 看的是"宿主机内存", 而不是"容器内存限制"

  宿主机: 总内存 64GB
  容器(K8s): memory limit = 2GB (我们给这个容器的内存上限)

  老版本JVM 计算"默认最大堆(-Xmx)"的逻辑:
    看到宿主机有 64GB → 默认最大堆 = 64GB / 4 = 16GB
    (JVM 不知道自己被关在一个只有2GB的容器里, 它看到的是整台宿主机!)

  于是悲剧发生:
    JVM 以为: "我有16GB堆可以用, 随便涨"
    容器实际: "你只有2GB, 超了我就杀你"
    → JVM 把堆涨过 2GB 时 → cgroup 检测到容器内存超限 → OOMKilled
    → 而 JVM 自己: "我才用2GB, 离16GB还远, 我没问题啊?" (一脸无辜)
    → 杀了重启, 又涨, 又被杀 → CrashLoopBackOff

看明白这个"以为自己很有钱"的 JVM 是怎么把自己作死的了吗?核心矛盾,是"JVM 对自己可用内存的认知"和"容器对它的实际限制"之间,出现了严重的错位。 JVM 是个"老实人",它启动时会问一句"这台机器有多少内存啊?",好据此规划自己默认的堆大小(它默认最多用机器内存的 1/4 当堆)。可在容器里,它问到的答案,是宿主机的内存(64GB)——因为老版本的 JVM,根本不知道"容器"和"cgroup 内存限制"这回事,它眼里只有那台它运行其上的物理机。于是它以为自己富得流油(16GB 堆随便用),便毫无节制地往上涨堆;殊不知,它其实被关在一个只有 2GB 的小囚笼(容器)里,一旦它用的内存撞穿了这 2GB 的笼壁,看守(cgroup)就把它"咔嚓"了。

而最让人迷惑的,正是那个"堆没满却被杀"的现象:从 JVM 自己的视角看,它的堆离它以为的 16GB 上限还远,一切正常,所以它绝不会主动触发自己的 OOM(OutOfMemoryError);可从容器的视角看,它早已超过了 2GB 的限制,被外部的 cgroup 强制杀死(OOMKilled)。这是两种不同的"OOM":一种是 JVM 内部"堆满了"主动抛的 OutOfMemoryError;另一种是容器"内存超限了"被外部 cgroup 强杀的 OOMKilled。我们这次,是后者——而排查时容易被前者的思路带偏(去看 JVM 堆满没满),从而想不通"堆没满怎么会 OOM"。分清这两种 OOM,是看懂这个坑的关键。

第一件事:理解容器只是"受限的视图",而进程可能看不见这个限制

要避开这个坑,核心是理解容器化的一个本质:容器(本质上是靠 Linux 的 cgroup + namespace 实现的)给进程提供的,是一个"受限的、隔离的运行环境"——它限制了进程能用多少 CPU、多少内存。但是,运行在容器里的进程,并不一定"知道"自己被限制了:很多程序(尤其是老的),在查询"系统有多少 CPU、多少内存"时,拿到的还是宿主机的真实数据,而不是容器给它的那个限额。

容器化的认知陷阱: "限制"是一回事, "进程感不感知到限制"是另一回事

  cgroup 做的: 限制这个容器最多用 2GB 内存、最多用 1 个 CPU 核
              (这个限制是真实生效的: 超了就杀、CPU 超了就限流)

  但进程查询时:
    老程序问"机器多少内存?" → 可能拿到宿主机的 64GB (没感知到 2GB 限制!)
    老程序问"几个CPU核?"   → 可能拿到宿主机的 32 核 (没感知到 1 核限制!)

  → 于是进程会基于"错误的、宿主机的"资源数据, 做出错误的决策:
    JVM 按 64GB 算堆 → 撑爆 2GB 容器
    线程池按 32 核算线程数 → 开了远超实际可用CPU的线程, 上下文切换爆炸
    (不只JVM, 任何"根据机器资源自动调参"的程序, 在容器里都可能踩这个坑)

关键认知是:容器对资源的"限制"是真实生效的(cgroup 说限 2GB 就限 2GB,超了真杀),但进程能不能"感知"到这个限制,是另一回事——很多老程序感知不到,它们查询资源时拿到的还是宿主机的数据。于是就出现了"限制是 2GB、进程却以为有 64GB"的认知错位,进而做出撑爆容器的错误决策。这个坑不只属于 JVM——任何"会根据机器的 CPU/内存资源,来自动调整自己行为(分配内存、设置线程数、开缓冲区……)"的程序,在容器里都可能踩中:它们基于宿主机的"虚高"资源数据自动调参,结果配出来的参数(堆大小、线程数)远超容器的实际限额,轻则资源浪费、上下文切换爆炸,重则像我这次一样直接被 OOMKilled。所以,把一个程序"装进容器"时,一定要多想一层:这个程序,会不会根据'机器资源'来自动决定自己的行为?如果会,它感知到的是容器的限额、还是宿主机的真实资源?——这个'认知错位'的隐患,是容器化时一个必须警惕的、共性的坑。

第二件事:正解之一——让 JVM "感知"容器,或显式设堆

针对 JVM 这个坑,有两类解法:一是让 JVM 能"感知"到容器的限制(新版 JVM 已经支持),二是干脆显式地告诉 JVM "你只有这么多内存"(手动设堆大小)。

# 正解1a: 用新版 JDK + 开启容器感知(JDK 10+ 默认就开了)
#   JDK 8u191+ / JDK 10+: 支持 -XX:+UseContainerSupport (JDK10+默认开启)
#   开启后, JVM 会读取 cgroup 的内存限制, 按"容器的2GB"而非"宿主机64GB"算堆
-XX:+UseContainerSupport
# 并用"百分比"来设堆, 让它按容器内存的比例自适应:
-XX:MaxRAMPercentage=75.0    # 最大堆 = 容器内存 * 75% (2GB * 75% = 1.5GB)
#   (留 25% 给堆外内存、线程栈、元空间等 —— 重要! 别把100%都给堆)

# 正解1b: 最稳妥 —— 直接显式设死堆大小, 匹配容器限制
-Xmx1536m -Xms1536m          # 明确告诉JVM最大堆1.5GB, 别自己瞎猜
#   (同样要给容器的2GB留出余量给堆外, 别 -Xmx2g 把整个容器内存都给堆!)

这两种解法各有适用:正解1a(容器感知)是治本的现代方案——新版本的 JDK(8u191+、10+)引入了 UseContainerSupport,能让 JVM 正确地读取 cgroup 的限制、按"容器的内存"而非"宿主机的内存"来计算默认堆;配合 MaxRAMPercentage(让堆按容器内存的百分比自适应),既正确又灵活。正解1b(显式设堆)是最稳妥、最不容易出错的方案——直接用 -Xmx 把最大堆写死成一个明确的值,不给 JVM "自己瞎猜"的机会。但无论用哪种,都有一个极其重要、却极易被忽略的细节:堆(Heap)只是 JVM 内存的一部分,不能把容器的内存全分给堆! JVM 除了堆,还要用内存于:堆外内存(直接内存)、线程栈(每个线程一份)、元空间(Metaspace,存类信息)、JIT 编译的代码缓存等等。所以,容器限 2GB,你的最大堆绝不能设成 2GB(那堆外的部分一用,总内存就超了 2GB,照样 OOMKilled);要留出 20%~30% 的余量给这些"堆之外"的内存。这是 JVM 容器化里另一个高频的"二次踩坑点"。我把"JVM 容器内存"这个分配画成图:

这张图想强调的是那个最易被忽略的点:容器内存 = 堆 + 堆外,你设的最大堆,必须给"堆外内存"留足余量,绝不能等于容器的内存上限。很多人改了堆感知、却把 -Xmx 设成了容器内存的 100%,结果堆外内存一涨,总量超了容器限制,又被 OOMKilled——掉进了同一个坑的"第二层"。记住:给堆留 70%~80%,剩下的留给堆外。

第三件事:正解之二——监控容器内存,设合理的 limit

配好了 JVM,还有一个运维层面的功课:合理地设置容器的内存 limit,并监控容器的真实内存使用,而不是凭感觉拍一个数。因为"容器该给多少内存"和"JVM 该设多大堆",是要相互匹配、并基于真实用量来定的。

# 正解2: 基于真实用量, 匹配地设置 容器limit 和 JVM堆

  1. 先压测/观察服务的真实内存需求:
     - JVM 堆实际用多少? (看 GC 日志、堆监控)
     - 堆外内存用多少? (直接内存、线程数 × 栈大小、元空间)
     - 加起来, 才是这个服务真实需要的总内存

  2. 容器 limit = 服务真实需要 + 一定缓冲 (别卡得太死, 留点波动空间)
     JVM 最大堆 = 容器 limit × 70%~80% (留余量给堆外)

  3. 监控告警:
     - 监控"容器实际内存使用 / 容器limit"的比例 (接近100%就危险)
     - 监控 OOMKilled 事件 (K8s 里 Pod 的重启原因、events)
     - 监控 JVM 堆使用、GC 频率/耗时

  关键: 容器内存、JVM堆 这俩参数, 不是拍脑袋定的, 而是基于
        "服务真实需要多少内存"这个事实, 相互匹配地定出来的。

这一步的核心,是把"容器内存 limit"和"JVM 堆大小"这两个参数的设定,从"凭感觉拍脑袋"变成"基于真实用量的、相互匹配的科学决策"。容器 limit 设小了,服务真实需求超过它就 OOMKilled;设大了,又浪费资源(尤其在 K8s 这种按 limit 调度的环境,设太大会降低节点的部署密度)。JVM 堆和容器 limit 还得相互匹配(堆 ≈ limit 的 70%~80%)。这一切的基础,是你得先搞清楚"这个服务真实到底需要多少内存"——这需要通过压测、观察 GC 日志和内存监控来得出,而不是像我们最初那样"估摸着 2GB 够了"。而监控,是这一切的"眼睛":你必须监控容器的实际内存使用率、监控 OOMKilled 事件、监控 JVM 的堆和 GC,这样才能及时发现"内存吃紧"的苗头、才能基于真实数据去调优 limit 和堆,而不是等服务 CrashLoopBackOff 了才手忙脚乱。把"基于真实用量、相互匹配地设参数、并持续监控"这套做扎实,容器内存这块就稳了。

第四件事:不只内存——CPU 也有一模一样的坑

这个"进程感知不到容器限制"的坑,不只发生在内存上,CPU 上有一个一模一样的坑,而且更隐蔽(它不会把你 OOMKilled,而是悄悄地拖慢你)。很多程序会根据"CPU 核数"来自动设置线程池大小、并行度等——而它们查到的核数,可能是宿主机的,而不是容器限的。

# CPU 的同款坑: 进程按"宿主机核数"开线程, 远超容器实际能用的CPU

  宿主机: 32 核
  容器: CPU limit = 1 核 (cgroup 限制这个容器最多用1核的算力)

  老程序问"几个CPU核?" → Runtime.availableProcessors() 返回 32!
  → 程序据此自动开了 32 个工作线程、或设了 32 的并行度
  → 可容器实际只有 1 核的算力!
  → 32个线程挤在 1 核上疯狂争抢、上下文切换 → 不仅没并行加速, 反而更慢!

  受影响的(任何"按核数自动调参"的):
  - JVM 的 GC 线程数、ForkJoinPool/parallelStream 的并行度
  - 各种框架的默认线程池大小(按核数算)
  - Go 的 GOMAXPROCS(老版本也按宿主机核数, 需 automaxprocs 等修正)
  - Node、Nginx 的 worker 数 ...

# 解法: 同样是"让进程感知容器限制"(新版JVM的UseContainerSupport也管CPU),
#       或显式指定线程数/并行度/GOMAXPROCS, 别让它按宿主机核数自动算

CPU 这个坑和内存那个是"孪生兄弟",根源完全一样(进程按宿主机资源、而非容器限额来自动调参),但它更隐蔽:内存超限会"啪"地把你 OOMKilled(动静大、好发现);而 CPU 这个坑,不会杀你,只会让你"开了一堆线程挤在一个核上互相争抢、上下文切换爆炸",表现为"性能莫名其妙地差"——这种"慢"比"崩"更难定位。所以,任何会"根据 CPU 核数自动调整行为"的程序(JVM 的 GC 线程和并行流、各种框架按核数设的线程池、Go 的 GOMAXPROCS、Nginx/Node 的 worker 数……),在容器里都要警惕这个坑——让它感知容器限制,或显式指定参数,别让它按宿主机核数瞎算。记住:容器化时,"内存"和"CPU"这两个最基础的资源,进程对它们的认知都可能和容器的实际限制错位——这是一个共性的、必须成对警惕的坑。把这两种 OOM 的区别整理成一张表:

OutOfMemoryError OOMKilled
谁触发 JVM 自己(堆满了) 容器/cgroup(超内存限制)
原因 JVM 堆用满, 分配不出对象 容器总内存超过 limit
表现 应用抛异常, 有堆栈日志 进程被外部 kill, 容器重启
堆满了吗 满了 不一定(堆没满也可能被杀)
怎么查 看应用日志/堆 dump 看容器/Pod 的退出码137、events

第五件事:容器化是"装进盒子",别忘了告诉程序盒子有多大

这次事故,本质上揭示了容器化的一个核心要义。我把容器化时几个容易"认知错位"的资源点,整理成一张表,提醒自己每次容器化时都过一遍:

资源/认知 容器化的坑 怎么办
内存(JVM堆) 按宿主机内存算堆 → OOMKilled UseContainerSupport / 显式 -Xmx(留堆外余量)
CPU(线程/并行) 按宿主机核数开线程 → 争抢变慢 感知容器 / 显式设线程数、GOMAXPROCS
磁盘 容器内写文件占的是宿主机/卷 注意挂载、容量、日志写到卷
网络/主机名 容器内拿到的是容器的, 非宿主机 注意服务发现、回调地址配置
时区/locale 容器默认 UTC, 与预期不符 显式配置时区

这张表的核心,可以归纳成一句话:容器化,本质上是把你的程序"装进了一个有大小限制的盒子"里;而你必须确保,这个程序"知道"自己被装进了多大的盒子——否则它就会按"盒子外面那个大世界"的尺寸来行事,然后撞穿盒子。我那次的 JVM,就是不知道自己被装进了 2GB 的盒子,还按外面 64GB 的世界来规划自己,结果撞穿了盒壁。所以,容器化一个程序时,最关键的功课之一,就是"明确地告诉程序它的盒子有多大"——给 JVM 配好堆和容器感知、给线程池配好和容器 CPU 匹配的大小、给一切"按资源自动调参"的地方,都换上"按容器限额、而非宿主机"的认知。容器给了我们"资源隔离、限制"的好处(一个容器再怎么吃,也不会吃垮整台机器、影响别的容器),可享受这个好处的前提,是让容器里的程序,真正地适应、感知到它所在的那个"受限的盒子"——否则,隔离的边界,就会变成把程序撞死的墙。

一张"服务容器化前必查"的决策图

把这次踩坑沉淀成一张图。每当你要把一个服务容器化、设资源 limit 时,照着它过一遍:

这张图的核心,是那个关键判断——"这服务会不会按机器资源自动调参?"会的话(JVM、各种线程池、GOMAXPROCS),就必须让它感知容器限制、并留好余量。配合"基于真实用量定 limit"和"监控 OOMKilled",容器化的内存/CPU 坑就能被挡在上线前。

我立下的几条容器化规矩

这次"容器 OOMKilled"的事故后,团队的容器化规范里加了这么几条:

  1. JVM 必配容器感知或显式堆:容器里的 Java 用新版 JDK 开 UseContainerSupport + MaxRAMPercentage,或显式 -Xmx,绝不让它按宿主机算堆。
  2. 堆要给堆外留余量:最大堆设为容器内存的 70%~80%,绝不设成 100%(留给直接内存、线程栈、元空间)。
  3. CPU 也要感知容器:按核数自动调参的(线程池、并行度、GOMAXPROCS),让它感知容器 CPU 限制或显式指定。
  4. 基于真实用量定 limit:容器内存/CPU limit 通过压测和监控的真实用量来定,别拍脑袋。
  5. 监控 OOMKilled 与资源水位:监控容器内存使用率、OOMKilled 事件、JVM 堆和 GC,内存吃紧能提前预警。
  6. 分清两种 OOM:遇到内存问题,先分清是 JVM 的 OutOfMemoryError(堆满)还是容器 OOMKilled(超 limit),对症排查。
  7. 容器化前评估资源认知:任何服务容器化前,评估它会不会按机器资源自动调参、感知的是容器还是宿主机。

这几条里,第七条是总纲。我这次踩坑最大的教训,是认识到:"容器化"绝不只是"把程序打个镜像跑起来"这么简单——它意味着把程序放进了一个"资源受限、且与宿主机隔离"的新环境,而程序原本对'运行环境'的种种假设(比如'我能用整台机器的内存和 CPU'),在这个新环境里可能全都不成立了。我们最初就是天真地以为"打个镜像、设个 limit,跑起来不就行了",完全没意识到 JVM 对"可用内存"的那个假设,在容器里已经错位了。所以,容器化一个程序,真正的功课不在"怎么打镜像",而在"审视并修正这个程序对运行环境的所有假设"——它假设有多少内存?多少 CPU?这些假设在容器的限额下还成立吗?把这些假设一个个地检查、修正到位,才算真正把一个程序"正确地"容器化了。

写在最后:换了环境,就要重新审视所有"想当然"的假设

这次容器化踩的坑,给我一个超越技术细节的深刻启示:当我们把一个东西,从它原本熟悉的环境,搬到一个新环境里时,那些在旧环境里"理所当然、从不需要明说"的假设,在新环境里可能统统失效——而最危险的,正是那些我们习以为常、以至于根本意识不到它是个"假设"的东西。 JVM "我能用整台机器的内存"这个假设,在物理机时代天经地义、从来不是问题,以至于没人会把它当成一个"假设";可一搬进容器,这个隐形的假设就崩了,而恰恰因为它太隐形、太理所当然,我们排查时才绕了一大圈、迟迟想不到它头上。

想通这一点,我对"迁移""换环境"这类事,生出了一份新的审慎:每当要把一个系统、一个程序,迁移到一个新的环境(从物理机到容器、从单机到集群、从一个云到另一个云、从开发到生产),都不能想当然地以为"它在那边怎么跑,在这边也一样"——而要主动地、系统地去审视:它对运行环境,做了哪些隐含的假设?这些假设,在新环境里还成立吗?从物理机到容器,要审视"对资源的假设";从单机到集群,要审视"对'只有我一个实例'的假设"(就像我之前那篇定时任务的复盘);从开发到生产,要审视"对数据量、并发量、网络环境的假设"……迁移的风险,往往不在那些"显眼的、你会主动去改的"地方,而在那些"隐形的、你从没意识到自己依赖着的"假设里——而把这些隐形假设一个个地揪出来、重新审视、修正到位,正是一次成功迁移最核心、也最容易被忽视的功课。

所以,如果你也要把什么东西搬到一个新环境里去,我想把这次踩坑最想说的话送给你:别被"它在旧环境跑得好好的"麻痹了,主动地去把那些"理所当然"的假设翻出来,在新环境的光照下,重新检视一遍。问问自己:这个程序,默认了它的运行环境是什么样的?新环境,还满足这些默认吗?越是那些你"从来没想过、觉得天经地义"的假设,越要警惕——因为正是它们,最可能在你毫无防备时,因为环境的改变而悄悄失效,然后以一个让你百思不得其解的诡异故障(比如'堆没满却被 OOMKilled')来给你上一课。那个在 2GB 容器里以为自己有 64GB 的 JVM,最终教给我的,正是这份"换了环境,就要重新审视所有想当然"的清醒——它让我明白,真正考验一个工程师的,往往不是在熟悉环境里把事做好,而是在切换到新环境时,能不能敏锐地察觉到那些"水土不服"的隐形假设,并把它们一一校正。愿你我在每一次迁移、每一次环境切换中,都能保有这份审视假设的清醒,让那些隐形的坑,在我们的主动检视下,无所遁形。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

下游换了 IP 发布完成后我们死活连不上、下游明明健康重启自己就好:网络第一步 DNS 缓存导致连旧 IP 刻舟求剑的避坑复盘

2026-6-1 17:30:29

技术教程

AI 功能上线一个月财务找上门说账单是预估的好几倍、而我们对自己每天到底花了多少钱完全无感:大模型 API token 成本失控的避坑复盘

2026-6-1 17:43:06

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索