您的位置 首页 golang

保障 IDC 安全:分布式 HIDS 集群架构设计

背景

近年来,互联网上安全事件频发,企业信息安全越来越受到重视,而 IDC 服务器安全又是纵深防御体系中的重要一环。保障 IDC 安全,常用的是基于主机型入侵检测系统 Host-based Intrusion Detection System,即 HIDS。在 HIDS 面对几十万台甚至上百万台规模的 IDC 环境时,系统架构该如何设计呢?复杂的服务器环境,网络环境,巨大的数据量给我们带来了哪些技术挑战呢?

需求描述

对于 HIDS 产品,我们安全部门的产品经理提出了以下需求:

  1. 满足 50W-100W 服务器量级的 IDC 规模。
  2. 部署在高并发服务器生产环境,要求 Agent 低性能低损耗。
  3. 广泛的部署兼容性。
  4. 偏向应用层和用户态入侵检测(可以和内核态检测部分解耦)。
  5. 针对利用主机 Agent 排查漏洞的最急需场景提供基本的能力,可以实现海量环境下快速查找系统漏洞。
  6. Agent 跟 Server 的配置下发通道安全。
  7. 配置信息读取写入需要鉴权。
  8. 配置变更历史记录。
  9. Agent 插件具备自更新功能。

分析需求

首先,服务器业务进程优先级高,HIDS Agent 进程自己可以终止,但不能影响宿主机的主要业务,这是第一要点,那么业务需要具备 熔断 功能,并具备自我恢复能力。

其次,进程保活、维持心跳、实时获取新指令能力,百万台 Agent 的全量控制时间一定要短。举个极端的例子,当 Agent 出现紧急情况,需要全量停止时,那么全量停止的命令下发,需要在 1-2 分钟内完成,甚至 30 秒、20 秒内完成。这些将会是很大的技术挑战。

还有对配置动态更新,日志级别控制,细分精确控制到每个 Agent 上的每个 HIDS 子进程,能自由地控制每个进程的启停,每个 Agent 的参数,也能精确的感知每台 Agent 的上线、下线情况。

同时,Agent 本身是安全 Agent,安全的因素也要考虑进去,包括通信通道的安全性,配置管理的安全性等等。

最后,服务端也要有一致性保障、可用性保障,对于大量 Agent 的管理,必须能实现任务分摊,并行处理任务,且保证数据的一致性。考虑到公司规模不断地扩大,业务不断地增多,特别是 美团 和大众点评合并后,面对的各种操作系统问题,产品还要具备良好的兼容性、可维护性等。

总结下来,产品架构要符合以下特性:

  1. 集群高可用。
  2. 分布式 ,去中心化。
  3. 配置一致性,配置多版本可追溯。
  4. 分治与汇总。
  5. 兼容部署各种 Linux 服务器,只维护一个版本。
  6. 节省资源,占用较少的 CPU、内存。
  7. 精确的熔断限流。
  8. 服务器数量规模达到百万级的集群负载能力。

技术难点

在列出产品需要实现的功能点、技术点后,再来分析下遇到的技术挑战,包括不限于以下几点:

  • 资源限制,较小的 CPU、内存。
  • 五十万甚至一百万台服务器的 Agent 处理控制问题。
  • 量级大了后,集群控制带来的控制效率,响应延迟,数据一致性问题。
  • 量级大了后,数据传输对整个服务器内网带来的流量冲击问题。
  • 量级大了后,运行环境更复杂,Agent 异常表现的感知问题。
  • 量级大了后,业务日志、程序运行日志的传输、存储问题,被监控业务访问量突增带来监控数据联动突增,对内网带宽,存储集群的爆发压力问题。

我们可以看到,技术难点几乎都是 服务器到达一定量级 带来的,对于大量的服务,集群分布式是业界常见的解决方案。

架构设计与技术选型

对于管理 Agent 的服务端来说,要实现高可用、 容灾 设计,那么一定要做多机房部署,就一定会遇到数据一致性问题。那么数据的存储,就要考虑分布式存储组件。 分布式数据存储中,存在一个定理叫 CAP 定理

CAP 的解释

关于 CAP 定理 ,分为以下三点:

  • 一致性(Consistency):分布式数据库的数据保持一致。
  • 可用性(Availability):任何一个节点宕机,其他节点可以继续对外提供服务。
  • 分区容错性(网络分区)Partition Tolerance:一个数据库所在的机器坏了,如硬盘坏了,数据丢失了,可以添加一台机器,然后从其他正常的机器把备份的数据同步过来。

根据定理,分布式系统只能满足三项中的两项而不可能满足全部三项。理解 CAP 定理 的最简单方式是想象两个节点分处分区两侧。允许至少一个节点更新状态会导致数据不一致,即丧失了 Consistency。如果为了保证数据一致性,将分区一侧的节点设置为不可用,那么又丧失了 Availability。除非两个节点可以互相通信,才能既保证 Consistency 又保证 Availability,这又会导致丧失 Partition Tolerance。

参见: CAP Theorem

CAP 的选择

为了容灾上设计,集群节点的部署,会选择的异地多机房,所以 「Partition tolerance」是不可能避免的。 那么可选的是 AP 与 CP

在 HIDS 集群的场景里,各个 Agent 对集群持续可用性没有非常强的要求,在短暂时间内,是可以出现异常,出现无法通讯的情况。但最终状态必须要一致,不能存在集群下发关停指令,而出现个别 Agent 不听从集群控制的情况出现。所以,我们需要一个满足 CP 的产品。

满足 CP 的产品选择

在开源社区中,比较出名的几款满足 CP 的产品,比如 etcd、ZooKeeper、Consul 等。我们需要根据几款产品的特点,根据我们需求来选择符合我们需求的产品。

插一句,网上很多人说 Consul 是 AP 产品,这是个错误的描述。既然 Consul 支持分布式部署,那么一定会出现「网络分区」的问题, 那么一定要支持「Partition tolerance」。另外,在 consul 的官网上自己也提到了这点 Consul uses a CP architecture, favoring consistency over availability.

Consul is opinionated in its usage while Serf is a more flexible and general purpose tool. In CAP terms, Consul uses a CP architecture, favoring consistency over availability. Serf is an AP system and sacrifices consistency for availability. This means Consul cannot operate if the central servers cannot form a quorum while Serf will continue to function under almost all circumstances.
 

etcd、ZooKeeper、Consul 对比

借用 etcd 官网上 etcd 与 ZooKeeper 和 Consul 的比较图。

在我们 HIDS Agent 的需求中,除了基本的 服务发现 配置同步 配置多版本控制 变更通知 等基本需求外,我们还有基于产品安全性上的考虑,比如 传输通道加密 用户权限控制 角色管理 基于 Key 的权限设定 等,这点 etcd 比较符合我们要求。很多大型公司都在使用,比如 Kubernetes AWS OpenStack Azure Google Cloud Huawei Cloud 等,并且 etcd 的社区支持非常好。基于这几点因素,我们选择 etcd 作为 HIDS 的分布式集群管理。

选择 etcd

对于 etcd 在项目中的应用,我们分别使用不同的 API 接口实现对应的业务需求,按照业务划分如下:

  • Watch 机制来实现配置变更下发,任务下发的实时获取机制。
  • 脑裂问题在 etcd 中不存在,etcd 集群的选举,只有投票达到 N/2+1 以上,才会选做 Leader,来保证数据一致性。另外一个网络分区的 Member 节点将无主。
  • 语言亲和性,也是 Golang 开发的,Client SDK 库稳定可用。
  • Key 存储的数据结构支持范围性的 Key 操作。
  • User、Role 权限设定不同读写权限,来控制 Key 操作,避免其他客户端修改其他 Key 的信息。
  • TLS 来保证通道信息传递安全。
  • Txn 分布式事务 API 配合 Compare API 来确定主机上线的 Key 唯一性。
  • Lease 租约机制,过期 Key 释放,更好的感知主机下线信息。
  • etcd 底层 Key 的存储为 BTree 结构,查找时间复杂度为 O(㏒n),百万级甚至千万级 Key 的查找耗时区别不大。

etcd Key 的设计

前缀按角色设定:

  • Server 配置下发使用 /hids/server/config/{hostname}/master
  • Agent 注册上线使用 /hids/agent/master/{hostname}
  • Plugin 配置获取使用 /hids/agent/config/{hostname}/plugin/ID/conf_name

Server Watch /hids/server/config/{hostname}/master ,实现 Agent 主机上线的瞬间感知。Agent Watch /hids/server/config/{hostname}/ 来获取配置变更,任务下发。Agent 注册的 Key 带有 Lease Id,并启用 keepalive,下线后瞬间感知。 (异常下线,会有 1/3 的 keepalive 时间延迟)

关于 Key 的权限,根据不同前缀,设定不同 Role 权限。赋值给不同的 User,来实现对 Key 的权限控制。

etcd 集群管理

在 etcd 节点容灾考虑,考虑 DNS 故障时,节点会选择部署在多个城市,多个机房,以我们服务器机房选择来看,在大部分机房都有一个节点,综合承载需求,我们选择了 N 台服务器部署在个别重要机房,来满足负载、容灾需求。但对于 etcd 这种分布式一致性强的组件来说,每个写操作都需要 N/2-1 的节点确认变更,才会将写请求写入数据库中,再同步到各个节点,那么意味着节点越多,需要确认的网络请求越多,耗时越多,反而会影响集群节点性能。这点,我们后续将提升单个服务器性能,以及牺牲部分容灾性来提升集群处理速度。

客户端填写的 IP 列表,包含域名、IP。IP 用来规避 DNS 故障,域名用来做 Member 节点更新。最好不要使用 Discover 方案,避免对内网 DNS 服务器产生较大压力。

同时,在配置 etcd 节点的地址时,也要考虑到内网 DNS 故障的场景,地址填写会混合 IP、域名两种形式。

  1. IP 的地址,便于规避内网 DNS 故障。
  2. 域名形式,便于做个别节点更替或扩容。

我们在设计产品架构时,为了安全性,开启了 TLS 证书认证,当节点变更时,证书的生成也同样要考虑到上面两种方案的影响,证书里需要包含固定 IP,以及 DNS 域名范围的两种格式。

etcd Cluster 节点扩容

节点扩容,官方手册上也有完整的方案,etcd 的 Client 里实现了健康检测与故障迁移,能自动的迁移到节点 IP 列表中的其他可用 IP。也能定时更新 etcd Node List,对于 etcd Cluster 的集群节点变更来说,不存在问题。需要我们注意的是,TLS 证书的兼容。

分布式 HIDS 集群架构图

集群核心组件高可用,所有 Agent、Server 都依赖集群,都可以无缝扩展,且不影响整个集群的稳定性。即使 Server 全部宕机,也不影响所有 Agent 的继续工作。

在以后 Server 版本升级时,Agent 不会中断,也不会带来雪崩式的影响。etcd 集群可以做到单节点升级,一直到整个集群升级,各个组件全都解耦。

编程语言选择

考虑到公司服务器量大,业务复杂,需求环境多变,操作系统可能包括各种 Linux 以及 Windows 等。为了保证系统的兼容性,我们选择了 Golang 作为开发语言,它具备以下特点:

  1. 可以静态编译,直接通过 syscall 来运行,不依赖 libc,兼容性高,可以在所有 Linux 上执行,部署便捷。
  2. 静态编译语言,能将简单的错误在编译前就发现。
  3. 具备良好的 GC 机制,占用系统资源少,开发成本低。
  4. 容器化的很多产品都是 Golang 编写,比如 Kubernetes、 Docker 等。
  5. etcd 项目也是 Golang 编写,类库、测试用例可以直接用,SDK 支持快速。
  6. 良好的 CSP 并发模型支持,高效的协程调度机制。

产品架构大方向

HIDS 产品研发完成后,部署的服务都运行着各种业务的服务器,业务的重要性排在第一,我们产品的功能排在后面。为此,确定了几个产品的大方向:

  • 高可用,数据一致,可横向扩展。
  • 容灾性好,能应对机房级的网络故障。
  • 兼容性好,只维护一个版本的 Agent。
  • 依赖低,不依赖任何动态链接库。
  • 侵入性低,不做 Hook ,不做系统类库更改。
  • 熔断降级可靠,宁可自己挂掉,也不影响业务 。

产品实现

篇幅限制,仅讨论 框架设计 熔断限流 监控告警 自我恢复 以及产品实现上的 主进程 进程监控

框架设计

如上图,在框架的设计上,封装常用类库,抽象化定义 Interface ,剥离 etcd Client ,全局化 Logger ,抽象化 App 的启动、退出方法。使得各 模块 (以下简称 App )只需要实现自己的业务即可,可以方便快捷的进行逻辑编写,无需关心底层实现、配置来源、重试次数、熔断方案等等。

沙箱隔离

考虑到子进程不能无限的增长下去,那么必然有一个进程包含多个模块的功能,各 App 之间既能使用公用底层组件( Logger etcd Client 等),又能让彼此之间互不影响,这里进行了 沙箱化 处理,各个属性对象仅在各 App sandbox 里生效。同样能实现了 App 进程的 性能熔断 ,停止所有的业务逻辑功能,但又能具有基本的 自我恢复 功能。

IConfig

对各 App 的配置抽象化处理,实现 IConfig 的共有方法接口,用于对配置的函数调用,比如 Check 的检测方法,检测配置合法性,检测配置的最大值、最小值范围,规避使用人员配置不在合理范围内的情况,从而避免带来的风险。

框架底层用 Reflect 来处理 JSON 配置,解析读取填写的配置项,跟 Config 对象对比,填充到对应 Struct 的属性上,允许 JSON 配置里只填写变化的配置,没填写的配置项,则使用 Config 对应 Struct 的默认配置。便于灵活处理配置信息。

 复制代码

typeIConfiginterface{
Check() error// 检测配置合法性
}
funcConfigLoad(confByte []byte, config IConfig)(IConfig, error){
...
// 反射生成临时的 IConfig
varconfTmp IConfig
confTmp = reflect.New(reflect.ValueOf(config).Elem().Type()).Interface().(IConfig)
...
// 反射 confTmp 的属性
confTmpReflect := reflect.TypeOf(confTmp).Elem()
confTmpReflectV := reflect.ValueOf(confTmp).Elem()
// 反射 config IConfig
configReflect := reflect.TypeOf(config).Elem()
configReflectV := reflect.ValueOf(config).Elem()
...
fori =0; i < num; i++ {
// 遍历处理每个 Field
envStructTmp := configReflect.Field(i)
// 根据配置中的项,来覆盖默认值
ifenvStructTmp.Type == confStructTmp.Type {
configReflectV.FieldByName(envStructTmp.Name).Set(confTmpReflectV.Field(i))
 

Timer、Clock 调度

在业务数据产生时,很多地方需要记录时间,时间的获取也会产生很多系统调用。尤其是在每秒钟产生成千上万个事件,这些事件都需要调用 获取时间 接口,进行 clock_gettime 等系统调用,会大大增加系统 CPU 负载。 而很多事件产生时间的准确性要求不高,精确到秒,或者几百个毫秒即可,那么框架里实现了一个颗粒度符合需求的(比如 100ms、200ms、或者 1s 等)间隔时间更新的时钟,即满足事件对时间的需求,又减少了系统调用。

同样,在有些 Ticker 场景中, Ticker 的间隔颗粒要求不高时,也可以合并成一个 Ticker ,减少对 CPU 时钟的调用。

Catcher

在多协程场景下,会用到很多协程来处理程序,对于个别协程的 panic 错误,上层线程要有一个良好的捕获机制,能将协程错误抛出去,并能恢复运行,不要让进程崩溃退出,提高程序的稳定性。

抽象接口

框架底层抽象化封装 Sandbox 的 Init、Run、Shutdown 接口,规范各 App 的对外接口,让 App 的初始化、运行、停止等操作都标准化。App 的模块业务逻辑,不需要关注 PID 文件管理,不关注与集群通讯,不关心与父进程通讯等通用操作,只需要实现自己的业务逻辑即可。App 与框架的统一控制,采用 Context 包以及 Sync.Cond 等条件锁作为同步控制条件,来同步 App 与框架的生命周期,同步多协程之间同步,并实现 App 的安全退出,保证数据不丢失。

限流

网络 IO

  • 限制数据上报速度。
  • 队列存储数据任务列表。
  • 大于队列长度数据丢弃。
  • 丢弃数据总数计数。
  • 计数信息作为心跳状态数据上报到日志中心,用于数据对账。

磁盘 IO

程序运行日志,对日志级别划分,参考 /usr/include/sys/syslog.h

  • LOG_EMERG
  • LOG_ALERT
  • LOG_CRIT
  • LOG_ERR
  • LOG_WARNING
  • LOG_NOTICE
  • LOG_INFO
  • LOG_DEBUG

在代码编写时,根据需求选用级别。级别越低日志量越大,重要程度越低,越不需要发送至日志中心,写入本地磁盘。那么在异常情况排查时,方便参考。

日志文件大小控制,分 2 个文件,每个文件不超过固定大小,比如 20M 50M 等。并且,对两个文件进行来回写,避免日志写满磁盘的情况。

IRetry

为了加强 Agent 的鲁棒性,不能因为某些 RPC 动作失败后导致整体功能不可用,一般会有重试功能。Agent 跟 etcd Cluster 也是 TCP 长连接(HTTP2),当节点重启更换或网络卡顿等异常时,Agent 会重连,那么重连的频率控制,不能是死循环般的重试。假设服务器内网交换机因内网流量较大产生抖动,触发了 Agent 重连机制,不断的重连又加重了交换机的负担,造成雪崩效应,这种设计必须要避免。 在每次重试后,需要做一定的回退机制,常见的 指数级回退 ,比如如下设计,在规避雪崩场景下,又能保障 Agent 的鲁棒性,设定最大重试间隔,也避免了 Agent 失控的问题。

 复制代码

// 网络库重试 Interface
typeINetRetry interface {
// 开始连接函数
Connect()error
String()string
// 获取最大重试次数
GetMaxRetry() uint
...
}
// 底层实现
func (this *Context) Retry(netRetry INetRetry)error{
...
maxRetries = netRetry.GetMaxRetry()// 最大重试次数
hashMod = netRetry.GetHashMod()
for{
ifc.shutting {
returnerrors.New("c.shutting is true...")
}
ifmaxRetries >0&& retries >= maxRetries {
c.logger.Debug("Abandoning %s after %d retries.", netRetry.String(), retries)
returnerrors.New(" 超过最大重试次数 ")
}
...
ife := netRetry.Connect(); e != nil {
delay =1<< retries
ifdelay ==0{
delay =1
}
delay = delay * hashInterval
...
c.logger.Emerg("Trying %s after %d seconds , retries:%d,error:%v", netRetry.String(), delay, retries, e)
time.Sleep(time.Second *time.Duration(delay))
}
...
 

事件拆分

百万台 IDC 规模的 Agent 部署,在任务执行、集群通讯或对宿主机产生资源影响时,务必要错峰进行,根据每台主机的唯一特征取模,拆分执行,避免造成雪崩效应。

监控告警

古时候,行军打仗时,提倡「兵马未动,粮草先行」,无疑是冷兵器时代决定胜负走向的重要因素。做产品也是,尤其是大型产品,要对自己运行状况有详细的掌控,做好监控告警,才能确保产品的成功。

对于 etcd 集群的监控,组件本身提供了 Metrics 数据输出接口,官方推荐了 Prometheus 来采集数据,使用 Grafana 来做聚合计算、图标绘制,我们做了 Alert 的接口开发,对接了公司的告警系统,实现 IM、短信、电话告警。

Agent 数量感知,依赖 Watch 数字,实时准确感知。

如下图,来自产品刚开始灰度时的某一时刻截图,Active Streams(即 etcd Watch 的 Key 数量)即为对应 Agent 数量,每次灰度的产品数量。因为该操作,是 Agent 直接与集群通讯,并且每个 Agent 只 Watch 一个 Key。且集群数据具备唯一性、一致性,远比心跳日志的处理要准确的多。

etcd-Grafana-Watcher-Monitor

etcd 集群 Members 之间健康状况监控

etcd-Grafana-GC-Heap-Objects

用于监控管理 etcd 集群的状况,包括 Member 节点之间数据同步,Leader 选举次数,投票发起次数,各节点的内存申请状况,GC 情况等,对集群的健康状况做全面掌控。

程序运行状态监控告警

agent-mem-es

agent-cpu-es

全量监控 Agent 的资源占用情况,统计每天使用最大 CPU\内存的主机 Agent,确定问题的影响范围,及时做策略调整,避免影响到业务服务的运行。并在后续版本上逐步做调整优化。

百万台服务器,日志告警量非常大,这个级别的告警信息的筛选、聚合是必不可少的。减少无用告警,让研发运维人员疲于奔命,也避免无用告警导致研发人员放松了警惕,前期忽略个例告警,先解决主要矛盾。

  • 告警信息分级,告警信息细分 ID。
  • 根据告警级别过滤,根据告警 ID 聚合告警,来发现同类型错误。
  • 根据告警信息的所在机房、项目组、产品线等维度来聚合告警,来发现同类型错误。

数据采集告警

  • 单机数据数据大小、总量的历史数据对比告警。
  • 按机房、项目组、产品线等维度的大小、总量等维度的历史数据对比告警。
  • 数据采集大小、总量的对账功能,判断经过一系列处理流程的日志是否丢失的监控告警。

熔断

  • 针对单机 Agent 使用资源大小的阈值熔断,CPU 使用率,连续 N 次触发大于等于 5%,则进行保护性熔断,退出所有业务逻辑,以保护主机的业务程序优先。
  • Master 进程进入空闲状态,等待第二次时间 Ticker 到来,决定是否恢复运行。
  • 各个 App 基于业务层面的监控熔断策略。

灰度管理

在前面的 配置管理 中的 etcd Key 设计里,已经细分到每个主机(即每个 Agent)一个 Key。那么,服务端的管理,只要区分该主机所属机房、环境、群组、产品线即可,那么,我们的管理 Agent 的颗粒度可以精确到每个主机,也就是支持任意纬度的灰度发布管理与命令下发。

数据上报通道

组件名为 log_agent ,是公司内部统一日志上报组件,会部署在每一台 VM、Docker 上。主机上所有业务均可将日志发送至该组件。 log_agent 会将日志上报到 Kafka 集群中,经过处理后,落入 Hive 集群中。(细节不在本篇讨论范围)

主进程

主进程实现跟 etcd 集群通信,管理整个 Agent 的配置下发与命令下发;管理各个子模块的启动与停止;管理各个子模块的 CPU、内存占用情况,对资源超标进行进行熔断处理,让出资源,保证业务进程的运行。

插件化管理其他模块,多进程模式,便于提高产品灵活性,可更简便的更新启动子模块,不会因为个别模块插件的功能、BUG 导致整个 Agent 崩溃。

进程监控

方案选择

我们在研发这产品时,做了很多关于 linux 进程创建监控 的调研,不限于 安全产品 ,大约有下面三种技术方案:

对于公司的所有服务器来说,几十万台都是已经在运行的服务器,新上的任何产品,都尽量避免对服务器有影响,更何况是所有服务器都要部署的 Agent。 意味着我们在选择 系统侵入性 来说,优先选择 最小侵入性 的方案。

对于 Netlink 的方案原理,可以参考这张图:

系统侵入性比较

  • cn_proc Autid 在「系统侵入性」和「数据准确性」来说, cn_proc 方案更好,而且使用 CPU、内存等资源情况,更可控。
  • Hook 的方案,对系统侵入性太高了,尤其是这种最底层做 HOOK syscall 的做法,万一测试不充分,在特定环境下,有一定的概率会出现 Bug,而在百万 IDC 的规模下,这将成为大面积事件,可能会造成重大事故。

兼容性上比较

  • cn_proc 不兼容 Docker,这个可以在宿主机上部署来解决。
  • Hook 的方案,需要针对每种 Linux 的发行版做定制,维护成本较高,且不符合长远目标(收购外部公司时遇到各式各样操作系统问题)

数据准确性比较

在大量 PID 创建的场景,比如 Docker 的宿主机上,内核返回 PID 时,因为 PID 返回非常多非常快,很多进程启动后,立刻消失了,另外一个线程都还没去读取 /proc/ ,进程都丢失了,场景常出现在 Bash 执行某些命令。

最终,我们选择 Linux Kernel Netlink 接口的 cn_proc 指令 作为我们进程监控方案,借助对 Bash 命令的收集,作为该方案的补充。当然,仍然存在丢数据的情况,但我们为了系统稳定性,产品侵入性低等业务需求,牺牲了一些安全性上的保障。

对于 Docker 的场景,采用宿主机运行,捕获数据,关联到 Docker 容器,上报到日志中心的做法来实现。

遇到的问题

内核 Netlink 发送数据卡住

内核返回数据太快,用户态 ParseNetlinkMessage 解析读取太慢,导致用户态网络 Buff 占满,内核不再发送数据给用户态,进程空闲。对于这个问题,我们在用户态做了队列控制,确保解析时间的问题不会影响到内核发送数据。对于队列的长度,我们做了定值限制,生产速度大于消费速度的话,可以丢弃一些数据,来保证业务正常运行,并且来控制进程的内存增长问题。

疑似“内存泄露”问题

在一台 Docker 的宿主机上,运行了 50 个 Docker 实例,每个 Docker 都运行了复杂的业务场景,频繁的创建进程,在最初的产品实现上,启动时大约 10M 内存占用,一天后达到 200M 的情况。

经过我们 Debug 分析发现,在 ParseNetlinkMessage 处理内核发出的消息时,PID 频繁创建带来内存频繁申请,对象频繁实例化,占用大量内存。同时,在 Golang GC 时,扫描、清理动作带来大量 CPU 消耗。在代码中,发现对于 linux/connector.h 里的 struct cb_msg linux/cn_proc.h 里的 struct proc_event 结构体频繁创建,带来内存申请等问题,以及 Golang 的 GC 特性,内存申请后,不会在 GC 时立刻归还操作系统,而是在后台任务里,逐渐的归还到操作系统,见: debug.FreeOSMemory

FreeOSMemory forces a garbage collection followed by an attempt to return as much memory to the operating system as possible. (Even if this is not called, the runtime gradually returns memory to the operating system in a background task.)
 

但在这个业务场景里,大量频繁的创建 PID,频繁的申请内存,创建对象,那么申请速度远远大于释放速度,自然内存就一直堆积。

从文档中可以看出, FreeOSMemory 的方法可以将内存归还给操作系统,但我们并没有采用这种方案,因为它治标不治本,没法解决内存频繁申请频繁创建的问题,也不能降低 CPU 使用率。

为了解决这个问题,我们采用了 sync.Pool 的内置对象池方式,来复用回收对象,避免对象频繁创建,减少内存占用情况,在针对几个频繁创建的对象做对象池化后,同样的测试环境,内存稳定控制在 15M 左右。

大量对象的复用,也减少了对象的数量,同样的,在 Golang GC 运行时,也减少了对象的扫描数量、回收数量,降低了 CPU 使用率。

项目进展

在产品的研发过程中,也遇到了一些问题,比如:

  1. etcd Client Lease Keepalive 的 Bug。
  2. Agent 进程资源限制的 Cgroup 触发几次内核 Bug。
  3. Docker 宿主机上瞬时大量进程创建的性能问题。
  4. 网络监控模块在处理 Nginx 反向代理时,动辄几十万 TCP 链接的网络数据获取压力。
  5. 个别进程打开了 10W 以上的 fd。

方法一定比困难多,但方法不是拍脑袋想出来的,一定要深入探索问题的根本原因,找到系统性的修复方法,具备高可用、高性能、监控告警、熔断限流等功能后,对于出现的问题,能够提前发现,将故障影响最小化,提前做处理。在应对产品运营过程中遇到的各种问题时,逢山开路,遇水搭桥,都可以从容的应对。

经过我们一年的努力,已经部署了除了个别特殊业务线之外的其他所有服务器,数量达几十万台,产品稳定运行。在数据完整性、准确性上,还有待提高,在精细化运营上,需要多做改进。

本篇更多的是研发角度上软件架构上的设计,关于安全事件分析、数据建模、运营策略等方面的经验和技巧,未来将会由其他同学进行分享,敬请期待。

总结

我们在研发这款产品过程中,也看到了网上开源了几款同类产品,也了解了他们的设计思路,发现很多产品都是把主要方向放在了单个模块的实现上,而忽略了产品架构上的重要性。

比如,有的产品使用了 syscall hook 这种侵入性高的方案来保障数据完整性,使得对系统侵入性非常高,Hook 代码的稳定性,也严重影响了操作系统内核的稳定。同时,Hook 代码也缺少了监控熔断的措施,在几十万服务器规模的场景下部署,潜在的风险可能让安全部门无法接受,甚至是致命的。

这种设计,可能在服务器量级小时,对于出现的问题多花点时间也能逐个进行维护,但应对几十万甚至上百万台服务器时,对维护成本、稳定性、监控熔断等都是很大的技术挑战。同时,在研发上,也很难实现产品的快速迭代,而这种方式带来的影响,几乎都会导致内核宕机之类致命问题。这种事故,使用服务器的业务方很难进行接受,势必会影响产品的研发速度、推进速度;影响同事(SRE 运维等)对产品的信心,进而对后续产品的推进带来很大的阻力。

以上是笔者站在研发角度,从可用性、可靠性、可控性、监控熔断等角度做的架构设计与框架设计,分享的产品研发思路。

笔者认为大规模的服务器安全防护产品,首先需要考虑的是架构的稳定性、监控告警的实时性、熔断限流的准确性等因素,其次再考虑安全数据的完整性、检测方案的可靠性、检测模型的精确性等因素。

九层之台,起于累土。只有打好基础,才能运筹帷幄,决胜千里之外。

欢迎工作一到五年的Java工程师朋友们加入Java程序员开发: 721575865

群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka, Mysql ,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用”没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!

文章来源:智云一二三科技

文章标题:保障 IDC 安全:分布式 HIDS 集群架构设计

文章地址:https://www.zhihuclub.com/95767.shtml

关于作者: 智云科技

热门文章

网站地图