接手过一批 Linux 主机日志接入统一日志平台的需求之后,我把方案定成了 Rsyslog 方式。最初团队里有人建议在这批机器上装一个采集 Agent,好处是字段丰富、能断点续传,但坏处也很明显:机器多、配置杂,Agent 版本一更新就要全线重推。而每台 Linux 主机都自带 Rsyslog,改几行配置就能把系统日志和应用日志转发到中心端,几乎零侵入。这篇文章就把我这次实施的全过程写出来,包括选型逻辑、配置拆解、验证方法,以及几个很容易踩的坑。
1. 为什么统一日志平台选型时,Rsyslog 往往被最先考虑
1.1 主机日志接入的常见路线:Agent、SDK 与远程转发
先把三条路线摆在桌子上:给每台机器装采集 Agent,在应用里集成 SDK 直接上报,或者让机器通过远程 syslog 协议把日志转发到中心端。
Agent 的特点是高保真。它能直接读取日志文件,解析多行异常栈,甚至采集容器元数据,在前端做过滤再传输。代价是部署、升级、配置分发都要纳入运维体系,机器规模一大,Agent 本身就成了一个新的维护负担。SDK 上报适合应用主动推送,比如业务埋点、函数计算日志,但系统日志、内核消息、服务管理器日志它一概管不到。远程 syslog 转发则恰好覆盖这批“系统自带但没 Agent”的主机,它基于标准协议,Linux 原生支持,不需要额外安装任何东西。用这种方式接入统一日志平台,核心就是把 Rsyslog 当作一个轻量转发器:本地收集、格式化、加密(按需)、推送到平台侧接收端。
实际场景中三者不是互斥的。很多平台收敛的主机有上百台,其中一部分机器只跑 Nginx、内网 DNS、本地应用,日志量不大,装 Agent 纯属资源浪费;另一部分核心业务机器已经装了 Agent 或者用了 SDK。这时用 Rsyslog 做梯度补充,是最省事的路径。这次的实施对象就是那批“不想装 Agent”的资源受限主机。
1.2 Rsyslog 的优势和适用边界
Rsyslog 能长期活在主流里的唯一理由,其实不是功能多,而是“刚刚好”。它读取 /dev/log 或者 journald 都不需要额外权限,系统日志、认证日志、邮件日志都有现成来源;它支持 TCP、TLS、RELP 等可靠传输,避免了 UDP 丢包问题;它还有动作队列,可以在网络瞬断时把日志缓存到磁盘,恢复后再继续发送。这些特性对于一个日志接入方案来说已经足够扎实。
但它也有边界。Rsyslog 对多行日志的处理比较吃力,比如 Java 异常堆栈跨了四五行,它默认会把每一行当成独立消息处理,后续在平台侧要花很大功夫合并。它对结构化格式的支持需要自己写模板,不像专门采集器那样开箱就能吐 JSON。如果你的平台对日志字段的完整度要求极高,或者需要采集任意路径下的海量日志文件,Rsyslog 可能就不是最优解;此时更合适的还是专门的采集 Agent。所以我的选型判断标准很简单:日志量中等、机器资源紧张、希望零侵入接入的,优先 Rsyslog;日志量大且要求高保真解析的,仍然走 Agent。
1.3 整体链路:从主机日志到平台检索
这次实施的整体链路分四段。第一段在 Linux 主机内,系统日志由 imjournal 模块从 journald 读取,普通文本日志由 imfile 模块读取指定文件;第二段在客户端 rsyslog 的动作层,通过 omfwd 模块将经过模板格式化后的日志发送到接收端;第三段是平台侧的 rsyslog 接收端,它监听一个 TCP 端口,把消息按来源主机拆到不同文件或者直接转发到更下游的消费程序;第四段才是统一日志平台的写入通道,常见的是接入消息队列后由消费端写入检索引擎,最后在查询界面上按主机、时间、设施、级别、关键字完成检索。
链路听起来简单,但每一段都有细节。比如客户端模板里要带上原始主机的 IP 和 tag,否则所有日志到了中心端后都变成接收服务器的来源;服务端如果直接写入检索引擎,必须考虑日期字段时区;消息队列如果不做数据压缩,流量会跑掉好几倍。后面我会按顺序把这些点展开,直接给出可用配置。
2. 动手前先理清清单:日志源、端口、格式与流量
2.1 日志源清单:哪些该接、哪些不该接
动手配 Rsyslog 之前,我先整理了一张日志源清单。清单里的核心字段是:日志类别、原始路径、日志量级、重要程度。系统日志这一类,包括 /var/log/messages、/var/log/secure、/var/log/cron,它们本身已经通过 syslog 通道送到本机 rsyslog,因此重点是把本机收集结果转发出去,而不需要再重复读文件。
业务应用日志这一类,一般是应用自己写文件的路径,比如 /data/logs/portal/app.log。Rsyslog 要读这种文件,必须用 imfile 模块主动轮询,而且要注意文件轮转。Nginx 这类访问日志同样适合 imfile,但它有自己特有的日志格式,我建议在 Rsyslog 侧只负责整体搬运,把原始内容传过去,到平台解析阶段再按格式解析,后续扩展也方便。
哪些日志不该接?一是重复的、粒度太细的内核调试信息,比如某些设备的驱动疯狂输出内核日志,接了只会挤占带宽;二是临时文件、缓存日志,比如应用启动时会写一个带 PID 的临时文件,内容经常为空;三是已经被其他 Agent 采集过的日志,避免同一份日志在平台侧重复计费、重复索引。清单确认后,还要给每条日志定一个 tag,这个 tag 最终会变成平台侧一个检索字段,tag 命名规范最好统一为“应用名_模块名”。
2.2 端口与会话规划:不要默认 514
Rsyslog 默认端口是 514,但这个端口有两个问题:一是属于特权端口,服务端进程要用 root 权限才能监听;二是很多安全团队会对默认端口格外敏感,扫描器第一个扫的就是它。所以这次我们把客户端到接收端的端口规划成 5514,这个数字只是示意,关键是明确端口用途:
接收端监听一个 TCP 端口用于可靠传输,同时留一个 UDP 端口用于兼容那些不支持 TCP 的老设备。TLS 用同一个 TCP 端口,在接收端配置里通过证书校验区分普通连接和 TLS 连接。如果服务端有多个接收节点,还需要提前约定同一台主机的日志只会持久化到一个节点上,防止重复写入。防火墙放通规则也要写成白名单:客户端出方向只允许连接收服务器的 5514 端口的 TCP;服务端入方向只允许平台网段的流量进入该端口。端口规划看似小事,但实施后反复变更端口非常痛苦,因为客户端配置分散在几十台机器里,每改一次都要重新推送。
2.3 日志格式模板:从 RFC3164 到 RFC5424
Rsyslog 默认转发时用的传统 syslog 格式(RFC3164)长这样:<PRI>Aug 10 09:00:01 hostname app[pid]: message。它的最大问题是时间没有年份,日期只有三个字母缩写,消息体里如果包含空格和冒号,服务端解析时很容易产生歧义。这次我强烈建议用 RFC5424 格式,它的头部包含版本、结构化字段,消息体有明确的长度标记。Rsyslog 中可以通过模板定义生成 RFC5424,核心是让消息带一个合适的 PRI 值和1的版本号。
更重要的是,模板里要嵌入自定义字段。比如我想让平台侧知道日志来自哪台机器、原始客户端 IP、应用 tag,就可以在模板的消息体前插入一段 JSON 结构的元数据字段:
template(name="UnifiedLogFormat" type="string" string="<%PRI%>1 %timestamp:::date-rfc3339% %HOSTNAME% %app-name% %procid% %msgid% [log@6870 clientip=\"%fromhost-ip%\" tag=\"%syslogtag%\"] %msg%\n")很多人会忽略%fromhost-ip%,导致接收端看到的主机名不是真实客户端,而是 NAT 或跳板机。把客户端 IP 直接放进日志头里,后续做来源审计会省事很多。
2.4 日志量与队列内存的粗算方法
配置队列之前,最好先粗算日志流量。公式很简单:单台主机日均日志量除以 86400,得到平均每秒行数;再乘以平均每行字节数,就是 bps。日均 10GB 日志的主机,平均每秒约 120 条(假设一条 800 字节),但由于访问日志有明显波峰,峰值往往是均值的 5 倍左右,也就是约 600 条/秒,瞬时带宽约 5Mbps。一百台这样的主机,平台侧峰值入口流量可能在 500Mbps 甚至更高,如果中间再叠加 TLS 加解密,处理压力会进一步放大。
队列参数也据此估算。客户端 rsyslog 默认使用内存队列,队列大小设置成能缓存多少条消息,比如 10000 条队列大小,每条按 1KB 算,占用约 10MB 内存。如果队列大小设置为 100000,就是 100MB,很多小主机受不了。我给客户端定的是:内存队列 10000,队列写满后 spill 到磁盘,磁盘缓冲上限 1GB。这样既保证了突发消息不丢,又不会把内存撑爆。服务端接收节点则需要更大队列,因为它要处理多台主机的汇总流量,磁盘队列文件建议单独挂一块硬盘,避免和系统日志共用 IO。
3. 客户端配置:把日志干净地送出去
3.1 模块与配置文件结构检查
客户端配置的第一步不是写配置,而是确认 Rsyslog 版本和模块。用rsyslogd -v查看版本,低于 8.x 的版本我建议先升级,因为 8.x 对配置语法和队列的改进很大。然后检查/etc/rsyslog.conf主文件,确认加载了以下核心模块:
imuxsock:读取本机 /dev/log 上的 syslog 消息;imjournal:读取 systemd journal 的日志(如果使用 journald);imfile:让我们能配置任意文本文件作为输入;omfwd:远程转发动作模块,属于内置模块,一般无需显式 load,但需要确认可用。
配置文件结构方面,现在主流发行版的 rsyslog 都会include /etc/rsyslog.d/*.conf。我建议把所有自定义配置放到独立文件里,比如/etc/rsyslog.d/shipper.conf,不要动主文件里已经写好的规则。改完配置文件后,先执行rsyslogd -N1做语法检查,看到end of config file提示后再重启服务。注意语法检查只查配置语法,不保证业务逻辑正确,所以最终还是要用真实日志来验证。
3.2 最小可用转发配置与参数解读
下面这段配置是我们客户端实际用的核心片段,去掉敏感信息后大概这样:
module(load="imuxsock" SysSock.Use="on") module(load="imjournal" StateFile="/var/lib/rsyslog/imjournal.state") template(name="UnifiedLogFormat" type="string" string="<%PRI%>1 %timestamp:::date-rfc3339% %HOSTNAME% %app-name% %procid% %msgid% [log@6870 clientip=\"%fromhost-ip%\" tag=\"%syslogtag%\"] %msg%\n") ruleset(name="sendToPlatform") { action(type="omfwd" Target="10.0.100.10" Port="5514" Protocol="tcp" Template="UnifiedLogFormat" TCP_Framing="octet-counted" Queue.FileName="q_forward" Queue.MaxDiskSpace="1g" Queue.SaveOnShutdown="on" Queue.Type="LinkedList" Queue.Size="10000" ResendLastMSGOnReconnect="off") } syslog.* stop *.info;authpriv.none action(type="omfile" file="/var/log/messages")这段看起来简单,但有三个关键参数要特别说明:
TCP_Framing="octet-counted":每条 syslog 消息前加一个长度字段,这样接收端可以正确切分消息,避免半包和粘包问题。接收端需要配套支持,两边必须一致。Queue.Type="LinkedList":使用链表型内存队列,适合突发写入,任务执行速度相对较快。ResendLastMSGOnReconnect="off":关闭断线重连后的消息重发,否则消息量大的时候可能出现重复日志。如果要求严格不丢失,需要换用 RELP 协议或让接收端做去重。
3.3 应用日志接入:用 imfile 维护文件读取状态
系统日志能通过上面的配置直接转发,但应用日志连的是自己的文本文件,Rsyslog 默认不认。这时要用imfile模块。对每条应用日志文件添加一个 input 实例:
module(load="imfile" Mode="polling") input(type="imfile" File="/data/logs/app/portal.log" Tag="portal_biz" Severity="info" Facility="local7" Ruleset="sendToPlatform" ReadMode="tail" MaxLinesPerPass="1000" StateFile="portal_biz.state")ReadMode="tail"非常重要,它让 imfile 从文件尾部开始读取,而不是从头扫描整个历史文件。否则第一次启动时,Rsyslog 可能会把几个 GB 的历史日志全部转发出去。StateFile用来记录当前文件读取位置的偏移量,Rsyslog 会把它写到/var/lib/rsyslog/下,只要文件没被改名或清空,重启后可以继续从断点读取。MaxLinesPerPass限制每次读取的行数,防止某一瞬间文件里堆积了几万行时,Rsyslog 单次处理耗时过长,阻塞其他动作。
应用日志还有个容易忽略的细节:如果应用自己按照日期滚动文件,比如app.log.2025-01-01,你只配置了app.log,那么滚动后新文件会继续被读取,旧文件不会重复读。但如果你需要采集历史归档文件,要再配置额外的 input,并小心处理重复消息。
3.4 客户端自测重启与状态观察
配置写完后,我不会立刻重启 rsyslog 服务,而是先做三件事:
- 执行
rsyslogd -N1,确认没有语法错误。 - 用
logger -p local7.info "integration test message"发送一条测试日志,隔几秒查看队列文件是否增长。 - 检查
/var/log/messages是否正常写入,确认本机日志功能没被“转发规则”误伤。
重启服务后,立刻观察/var/log/rsyslog/status或者用journalctl -u rsyslog查看进程日志,看有没有队列初始化失败、模块加载失败的提示。如果测试日志没有出现,优先检查接收端端口是否可达:telnet 10.0.100.10 5514,能通再看 Rsyslog 规则。很多“没日志”的问题,根因是客户端配置里ruleset名称写错,或者stop语句条件过宽,把本不该转发的消息全部拦截了。
4. 服务端接入与平台写入通道设计
4.1 接收端 Rsyslog 配置
服务端接收配置相对简单,核心是加载 TCP 输入模块并监听指定端口:
module(load="imtcp" MaxSessions="5000") input(type="imtcp" port="5514" name="syslog_tcp" ruleset="receiveAll")MaxSessions要按峰值客户端数量预留,否则连接数一多,新客户端连接会被拒绝。接收端还可以再加载imudp监听一个 UDP 端口,专门接收老设备发来的不可靠传输,但 UDP 和 TCP 的规则集最好分开,避免把 UDP 收到的过期消息混入主链路。接收端的动作先落到本地文件,也就是原始日志文件,作为第一层安全网;然后再异步转发给下游处理程序。这样可以做到“即使下游挂了,原始日志也不丢”。
4.2 日志路由规则:按主机、设施、级别分发
接收端收到日志后,必须在 ruleset 里做路由拆分。我们可以按$hostname或$fromhost-ip来区分来源主机,按$syslogfacility区分系统日志与应用日志,按$syslogseverity-text区分错误级别。中心端不建议把所有来源写进同一个文件,最好一个主机一个目录、一个目录下分系统日志与应用日志两个文件。路由配置示例:
ruleset(name="receiveAll") { if ($fromhost-ip == "10.0.10.0/24") then { action(type="omfile" file="/data/loghub/net/10.0.0.0-24.log") stop } if ($syslogfacility == "local7") then { action(type="omfile" file="/data/loghub/app/app_log.log" template="RawLog") stop } action(type="omfile" file="/data/loghub/others/other.log") }实际生产环境的路由规则可能多达几十条,很容易写出互相覆盖的条件。我的经验是把“stop”语句放在每个分支尾部,让规则“命中即短路”,这样既清晰又高效。
4.3 写入通道选型:直连检索引擎还是走消息队列
接收端把日志写到文件后,还需要把它们送入统一日志平台。这里有三条路:
- 直接调用检索引擎的批量写入接口。适合日志量小、对实时性要求不高的场景。优点是链路短,缺点是检索引擎一旦不稳定,日志就会因为写入失败而堆积,Rsyslog 动作队列被阻塞。
- 先写入消息队列,再由消费程序写入检索引擎。这是大规模日志平台最常用的做法。消息队列天然具备削峰填谷的能力,接收集群和检索引擎解耦,重建索引时,消息不会丢。
- 使用 Logstash 直接消费 Rsyslog 格式数据,但 Logstash 自身也要维护,我不建议把 Rsyslog 接收、解析、写入全塞给 Logstash 一个进程,除非日志量确实很小。
我这次采用的方案是:接收端 rsyslog 写文件 -> 轻量采集器采集该文件 -> 消息队列 -> 消费程序解析 -> 检索引擎。你可能觉得绕了一圈,但好处非常实际:消息队列的消费端可以重复消费,修复解析 bug 后不用等原始日志重新上传;监控队列积压也很简单。
4.4 字段规范与索引映射准备
日志进入检索引擎之前,要提前规划好字段。最基础的字段包括:@timestamp、hostname、facility、severity、tag、msg。日期字段在检索引擎里应显式声明为date类型,否则同一字段里混入字符串后 mapping 就乱了。tag和hostname这类枚举值有限且用于聚合的字段,推荐声明为keyword类型。最怕的是直接用默认动态映射,把 hostname 也映射成 text,导致聚合不准。
消息体msg可以保存为 text 或 keyword。如果只需要关键字检索,建议限制长度或设置ignore_above,避免一条超长堆栈占用大量存储。平台侧最好配一个索引生命周期策略,比如热阶段存 7 天、温阶段存 30 天、冷阶段存 90 天,然后自动删除超过策略的索引。这个策略必须在接入前配好,否则日志一旦大量进来,索引管理会变成磁盘杀手。
5. 上线前的五步验证与踩坑实录
5.1 五步验证法:从 logger 到检索界面
我把上线前的验证分成五步,每步都不能省略:
- 客户端生成测试消息:
logger -p local7.info "integration test from host-a"。 - 客户端确认消息已发送:查看 rsyslog 队列状态文件,或抓包
tcpdump -i eth0 port 5514。 - 服务端确认消息已收到:查看接收端对应日志文件,或抓包确认端口有入流量。
- 平台侧确认字段解析正确:到查询界面按
tag:integration搜索,核对时间、主机名、消息体是否完整。 - 端到端延迟确认:从发送到可检索的时间,最好在 5 秒以内,超过 30 秒就要检查队列和消费端积压。
每一步都要留下输出记录,方便后续回滚。上线时我会按批次推送配置,先推 5 台,观察 1 小时,再推 20 台,最后全量,切忌一次性把所有机器全部重配。
5.2 坑 1:时间显示偏移八小时
第一批日志进平台后,就发现查询界面的时间比真实时间早了 8 小时。根因很快定位:Rsyslog 模板里用的%timestamp%默认是 UTC 时区,而平台查询界面默认用本地时区显示。处理办法有两个:要么在客户端模板里改用%timegenerated%并加上date-rfc3339,要么让平台消费端把时间字段按 UTC 解析后再在展示层转换。我最后选择了后者,因为日志传输链路里统一使用 UTC,服务器本地时间不同也没关系,展示层再做时区转换,不会因为某台机器时区配错而出现时间交叉。
这个坑提醒我,接日志前一定要先约定时间规范,并且用测试日志验证时间戳,而不是等到数据量大了才发现。
5.3 坑 2:系统日志与 journald 重复入库
有一台机器同时开了imjournal采集 journald 日志,又通过 imfile 读取了/var/log/messages文件,结果平台里同一份系统日志出现两次。原因是 journald 本身就包含了系统日志,/var/log/messages 里的内容也是由 rsyslog 从 imjournal 写入文件的,二者同源。
解决方式很简单:系统日志只保留一条链路。我选择用 imjournal 作为系统日志的来源,把*.* action(type="omfile")写入 messages 的部分保留,但不针对/var/log/messages再配置 imfile。如果平台侧仍然出现少量重复,可以在规则集里对“主机名+tag+消息体+时间戳”做一次聚合去重。重复日志在平台里很耗存储,必须在一开始就堵住。
5.4 坑 3:大日志文件导致 Rsyslog 卡顿
某台应用服务器的日志文件在高峰期每分钟增长上百 MB,配置 imfile 之后发现 rsyslog 进程 CPU 占用 100%,其他日志也延迟发送。排查后发现,imfile 在ReadMode设置错误时,会不断重复读取新行。即使设置正确,高峰期一次读取数万行,主循环被一个动作阻塞,也容易让系统日志排队。
处理办法是调小MaxLinesPerPass,比如改成 2000;把日志文件的 input 单独放到一个ruleset,通过队列把它和系统日志的动作分成不同的任务流。还需要给 imfile 加上PollingInterval参数,避免文件变更触发过于频繁。另外,建议把大日志文件按天轮转,减少单文件扫描开销。
5.5 坑 4:TLS 认证失败与配置语法隐蔽错误
给少量公网主机加 TLS 时,踩过的坑是证书链权限。Rsyslog 要求证书和私钥对当前用户可读,如果私钥文件权限是 600 且属主是 root,而 rsyslog 以非特权用户运行,就会报权限错误。这个坑很隐蔽,看起来像 TLS 握手失败,实际是文件无法读取。
另一个隐蔽错误是模板里的转义。引号必须写成\",否则配置解析会直接报错。还有两个少见的坑:同一条消息同时被多条规则输出到不同文件,会产生复制;规则里使用了stop但后边还有动作,会让配置行为变得难以理解。遇到这种问题,我的排查顺序是:rsyslogd -N1查语法,再看/var/log/messages里有没有 rsyslog 自身的报错,最后抓包看消息有没有发出。
6. 性能与稳定性调优:从能跑到稳跑
6.1 队列类型与大小:Direct、LinkedList、FixedArray
Rsyslog 的队列决定日志在内存和磁盘之间如何排队。常用三种:
Direct队列不缓冲,消息到达后立即执行动作,响应最快,但网络抖动时容易丢消息。LinkedList队列内存粒度动态分配,适合突发流量,请求高峰时能吸收较大抖动。FixedArray预分配固定数组,内存占用稳定,但队列写满时行为不如 LinkedList 平滑。
我通常给客户端用 LinkedList,给接收端用 FixedArray(如果日志量稳定)或 LinkedList(如果波动大)。队列大小设置要根据消息量计算,太小会频繁写磁盘,太大会消耗内存。队列写满后的行为也要配置:queue.discardmark默认是 90%,超过这个水位会丢弃新消息;queue.timeoutEnqueue控制在队列满时投递等待时间。生产环境建议开启queue.saveonshutdown,确保服务重启时队列里的日志能落盘。
6.2 worker 线程与 TLS 开销
多核机器上的 Rsyslog 默认只有一个 worker 线程,处理海量日志时会成为瓶颈。可以对队列设置workerThreads,比如 4 核机器设置 4 个线程。但并不是线程越多越好,动作如果集中写一个文件,多线程反而会争抢文件锁,性能下降。写不同文件时可以多线程,写同一个文件保持单线程。TLS 的开销主要在握手和加解密,普通日志量下不明显,但流量大时会显著增加 CPU。所以要么让收到 TLS 日志后的处理动作尽量异步化,要么在特定网段内使用明文 TCP 加内网安全策略,公网链路才启用 TLS。
6.3 日志量突增的限流与丢弃策略
日志平台最怕的不是正常流量,而是突增。某台应用异常时,每秒可能产生几千甚至上万条错误日志,如果客户端 rsyslog 不设限,消息队列和检索引擎会被瞬间打爆。应对手段是在客户端加限流。Rsyslog 的 ruleset 可以配置速率窗口和突发数量,比如每秒最多接收 1000 条日志,突增时超出的消息直接丢弃或根据级别选择性丢弃。这样做的目的是保护整体平台可用性,不能为了收集所有异常日志而拖垮整个集群。接收端对同一主机也应该加单主机限速,防止某一个客户端异常影响其他主机。
6.4 Failover 与高可用设计
接收端如果只有一台,客户端网络或服务端宕机都会导致日志丢失。Rsyslog 的omfwd支持配置多个 target,并按顺序尝试连接:
action(type="omfwd" target="10.0.100.10" port="5514" protocol="tcp") action(type="omfwd" target="10.0.200.10" port="5514" protocol="tcp" queue.filename="q_alt" queue.type="LinkedList")这会让 Rsyslog 在主 target 连接失败时自动切换备 target,切换期间日志落入磁盘队列。但要注意两个 target 都失败时,队列可能持续增长,磁盘空间会被耗完。因此还需要定期清理过期队列文件,或接入消息中间件做高可用缓冲层。我们的实施中,最终用两台接收节点做完全一样的接收配置,前面放一个四层负载均衡,客户端连负载均衡地址,两台节点同时消费,虽然重复写入风险增加了,但高可用性大幅提升,重复可以在平台侧去重。
到这里,本次 Linux 主机日志接入统一日志平台的实施过程基本写完了。回头看,最重要的其实不是某一条配置,而是把链路里各个环节的边界、格式、容量都预先对齐。按这个思路一步步落地,接入过程的返工率会低很多,平台侧的运维也会轻松不少。