近年來,互聯網上安全事件頻發,企業信息安全愈來愈受到重視,而IDC服務器安全又是縱深防護體系中的重要一環。保障IDC安全,經常使用的是基於主機型入侵檢測系統Host-based Intrusion Detection System,即HIDS。在HIDS面對幾十萬臺甚至上百萬臺規模的IDC環境時,系統架構該如何設計呢?複雜的服務器環境,網絡環境,巨大的數據量給咱們帶來了哪些技術挑戰呢?html
對於HIDS產品,咱們安所有門的產品經理提出瞭如下需求:linux
首先,服務器業務進程優先級高,HIDS Agent進程本身能夠終止,但不能影響宿主機的主要業務,這是第一要點,那麼業務須要具有熔斷功能,並具有自我恢復能力。golang
其次,進程保活、維持心跳、實時獲取新指令能力,百萬臺Agent的全量控制時間必定要短。舉個極端的例子,當Agent出現緊急狀況,須要全量中止時,那麼全量中止的命令下發,須要在1-2分鐘內完成,甚至30秒、20秒內完成。這些將會是很大的技術挑戰。面試
還有對配置動態更新,日誌級別控制,細分精確控制到每一個Agent上的每一個HIDS子進程,能自由地控制每一個進程的啓停,每一個Agent的參數,也能精確的感知每臺Agent的上線、下線狀況。數據庫
同時,Agent自己是安全Agent,安全的因素也要考慮進去,包括通訊通道的安全性,配置管理的安全性等等。編程
最後,服務端也要有一致性保障、可用性保障,對於大量Agent的管理,必須能實現任務分攤,並行處理任務,且保證數據的一致性。考慮到公司規模不斷地擴大,業務不斷地增多,特別是美團和大衆點評合併後,面對的各類操做系統問題,產品還要具有良好的兼容性、可維護性等。安全
總結下來,產品架構要符合如下特性:bash
在列出產品須要實現的功能點、技術點後,再來分析下遇到的技術挑戰,包括不限於如下幾點:服務器
咱們能夠看到,技術難點幾乎都是服務器到達必定量級帶來的,對於大量的服務,集羣分佈式是業界常見的解決方案。網絡
對於管理Agent的服務端來講,要實現高可用、容災設計,那麼必定要作多機房部署,就必定會遇到數據一致性問題。那麼數據的存儲,就要考慮分佈式存儲組件。 分佈式數據存儲中,存在一個定理叫CAP定理
:
關於CAP定理
,分爲如下三點:
根據定理,分佈式系統只能知足三項中的兩項而不可能知足所有三項。理解CAP定理
的最簡單方式是想象兩個節點分處分區兩側。容許至少一個節點更新狀態會致使數據不一致,即喪失了Consistency。若是爲了保證數據一致性,將分區一側的節點設置爲不可用,那麼又喪失了Availability。除非兩個節點能夠互相通訊,才能既保證Consistency又保證Availability,這又會致使喪失Partition Tolerance。
參見:CAP Theorem。
爲了容災上設計,集羣節點的部署,會選擇的異地多機房,因此 「Partition tolerance」是不可能避免的。那麼可選的是 AP
與 CP
。
在HIDS集羣的場景裏,各個Agent對集羣持續可用性沒有很是強的要求,在短暫時間內,是能夠出現異常,出現沒法通信的狀況。但最終狀態必需要一致,不能存在集羣下發關停指令,而出現個別Agent不遵從集羣控制的狀況出現。因此,咱們須要一個知足 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在項目中的應用,咱們分別使用不一樣的API接口實現對應的業務需求,按照業務劃分以下:
N/2+1
以上,纔會選作Leader,來保證數據一致性。另一個網絡分區的Member節點將無主。前綴按角色設定:
/hids/server/config/{hostname}/master
。/hids/agent/master/{hostname}
。/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節點容災考慮,考慮DNS故障時,節點會選擇部署在多個城市,多個機房,以咱們服務器機房選擇來看,在大部分機房都有一個節點,綜合承載需求,咱們選擇了N臺服務器部署在個別重要機房,來知足負載、容災需求。但對於etcd這種分佈式一致性強的組件來講,每一個寫操做都須要N/2-1
的節點確認變動,纔會將寫請求寫入數據庫中,再同步到各個節點,那麼意味着節點越多,須要確認的網絡請求越多,耗時越多,反而會影響集羣節點性能。這點,咱們後續將提高單個服務器性能,以及犧牲部分容災性來提高集羣處理速度。
客戶端填寫的IP列表,包含域名、IP。IP用來規避DNS故障,域名用來作Member節點更新。最好不要使用Discover方案,避免對內網DNS服務器產生較大壓力。
同時,在配置etcd節點的地址時,也要考慮到內網DNS故障的場景,地址填寫會混合IP、域名兩種形式。
咱們在設計產品架構時,爲了安全性,開啓了TLS證書認證,當節點變動時,證書的生成也一樣要考慮到上面兩種方案的影響,證書裏須要包含固定IP,以及DNS域名範圍的兩種格式。
etcd Cluster節點擴容
節點擴容,官方手冊上也有完整的方案,etcd的Client裏實現了健康檢測與故障遷移,能自動的遷移到節點IP列表中的其餘可用IP。也能定時更新etcd Node List,對於etcd Cluster的集羣節點變動來講,不存在問題。須要咱們注意的是,TLS證書的兼容。
集羣核心組件高可用,全部Agent、Server都依賴集羣,均可以無縫擴展,且不影響整個集羣的穩定性。即便Server所有宕機,也不影響全部Agent的繼續工做。
在之後Server版本升級時,Agent不會中斷,也不會帶來雪崩式的影響。etcd集羣能夠作到單節點升級,一直到整個集羣升級,各個組件全都解耦。
考慮到公司服務器量大,業務複雜,需求環境多變,操做系統可能包括各類Linux以及Windows等。爲了保證系統的兼容性,咱們選擇了Golang做爲開發語言,它具有如下特色:
HIDS產品研發完成後,部署的服務都運行着各類業務的服務器,業務的重要性排在第一,咱們產品的功能排在後面。爲此,肯定了幾個產品的大方向:
篇幅限制,僅討論框架設計
、熔斷限流
、監控告警
、自我恢復
以及產品實現上的主進程
與進程監控
。
如上圖,在框架的設計上,封裝經常使用類庫,抽象化定義Interface
,剝離etcd Client
,全局化Logger
,抽象化App的啓動、退出方法。使得各模塊
(如下簡稱App
)只須要實現本身的業務便可,能夠方便快捷的進行邏輯編寫,無需關心底層實現、配置來源、重試次數、熔斷方案等等。
沙箱隔離
考慮到子進程不能無限的增加下去,那麼必然有一個進程包含多個模塊的功能,各App
之間既能使用公用底層組件(Logger
、etcd Client
等),又能讓彼此之間互不影響,這裏進行了沙箱化
處理,各個屬性對象僅在各App
的sandbox
裏生效。一樣能實現了App
進程的性能熔斷
,中止全部的業務邏輯功能,但又能具備基本的自我恢復
功能。
IConfig
對各App的配置抽象化處理,實現IConfig的共有方法接口,用於對配置的函數調用,好比Check
的檢測方法,檢測配置合法性,檢測配置的最大值、最小值範圍,規避使用人員配置不在合理範圍內的狀況,從而避免帶來的風險。
框架底層用Reflect
來處理JSON配置,解析讀取填寫的配置項,跟Config對象對比,填充到對應Struct
的屬性上,容許JSON配置裏只填寫變化的配置,沒填寫的配置項,則使用Config
對應Struct
的默認配置。便於靈活處理配置信息。
type IConfig interface {
Check() error //檢測配置合法性
}
func ConfigLoad(confByte []byte, config IConfig) (IConfig, error) {
...
//反射生成臨時的IConfig
var confTmp 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()
...
for i = 0; i < num; i++ {
//遍歷處理每一個Field
envStructTmp := configReflect.Field(i)
//根據配置中的項,來覆蓋默認值
if envStructTmp.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
:
在代碼編寫時,根據需求選用級別。級別越低日誌量越大,重要程度越低,越不須要發送至日誌中心,寫入本地磁盤。那麼在異常狀況排查時,方便參考。
日誌文件大小控制,分2個文件,每一個文件不超過固定大小,好比20M
、50M
等。而且,對兩個文件進行來回寫,避免日誌寫滿磁盤的狀況。
IRetry
爲了增強Agent的魯棒性,不能由於某些RPC動做失敗後致使總體功能不可用,通常會有重試功能。Agent跟etcd Cluster也是TCP長鏈接(HTTP2),當節點重啓更換或網絡卡頓等異常時,Agent會重連,那麼重連的頻率控制,不能是死循環般的重試。假設服務器內網交換機因內網流量較大產生抖動,觸發了Agent重連機制,不斷的重連又加劇了交換機的負擔,形成雪崩效應,這種設計必需要避免。 在每次重試後,須要作必定的回退機制,常見的指數級回退
,好比以下設計,在規避雪崩場景下,又能保障Agent的魯棒性,設定最大重試間隔,也避免了Agent失控的問題。
//網絡庫重試Interface
type INetRetry interface {
//開始鏈接函數
Connect() error
String() string
//獲取最大重試次數
GetMaxRetry() uint
...
}
// 底層實現
func (this *Context) Retry(netRetry INetRetry) error {
...
maxRetries = netRetry.GetMaxRetry() //最大重試次數
hashMod = netRetry.GetHashMod()
for {
if c.shutting {
return errors.New("c.shutting is true...")
}
if maxRetries > 0 && retries >= maxRetries {
c.logger.Debug("Abandoning %s after %d retries.", netRetry.String(), retries)
return errors.New("超過最大重試次數")
}
...
if e := netRetry.Connect(); e != nil {
delay = 1 << retries
if delay == 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集羣Members之間健康情況監控
用於監控管理etcd集羣的情況,包括Member
節點之間數據同步,Leader選舉次數,投票發起次數,各節點的內存申請情況,GC狀況等,對集羣的健康情況作全面掌控。
程序運行狀態監控告警
全量監控Agent的資源佔用狀況,統計天天使用最大CPU\內存的主機Agent,肯定問題的影響範圍,及時作策略調整,避免影響到業務服務的運行。並在後續版本上逐步作調整優化。
百萬臺服務器,日誌告警量很是大,這個級別的告警信息的篩選、聚合是必不可少的。減小無用告警,讓研發運維人員疲於奔命,也避免無用告警致使研發人員放鬆了警戒,前期忽略個例告警,先解決主要矛盾。
數據採集告警
Ticker
到來,決定是否恢復運行。在前面的配置管理
中的etcd Key
設計裏,已經細分到每一個主機(即每一個Agent)一個Key。那麼,服務端的管理,只要區分該主機所屬機房、環境、羣組、產品線便可,那麼,咱們的管理Agent的顆粒度能夠精確到每一個主機,也就是支持任意緯度的灰度發佈管理與命令下發。
組件名爲 log_agent
,是公司內部統一日誌上報組件,會部署在每一臺VM、Docker上。主機上全部業務都可將日誌發送至該組件。 log_agent
會將日誌上報到Kafka集羣中,通過處理後,落入Hive集羣中。(細節不在本篇討論範圍)
主進程實現跟etcd集羣通訊,管理整個Agent的配置下發與命令下發;管理各個子模塊的啓動與中止;管理各個子模塊的CPU、內存佔用狀況,對資源超標進行進行熔斷處理,讓出資源,保證業務進程的運行。
插件化管理其餘模塊,多進程模式,便於提升產品靈活性,可更簡便的更新啓動子模塊,不會由於個別模塊插件的功能、BUG致使整個Agent崩潰。
方案選擇
咱們在研發這產品時,作了不少關於linux進程建立監控
的調研,不限於安全產品
,大約有下面三種技術方案:
方案 | Docker兼容性 | 開發難度 | 數據準確性 | 系統侵入性 |
---|---|---|---|---|
cn_proc | 不支持Docker | 通常 | 存在內核拿到的PID,在/proc/ 下丟失的狀況 |
無 |
Audit | 不支持Docker | 通常 | 同cn_proc | 弱,但依賴Auditd |
Hook | 定製 | 高 | 精確 | 強 |
對於公司的全部服務器來講,幾十萬臺都是已經在運行的服務器,新上的任何產品,都儘可能避免對服務器有影響,更況且是全部服務器都要部署的Agent。 意味着咱們在選擇系統侵入性
來講,優先選擇最小侵入性
的方案。
對於Netlink
的方案原理,能夠參考這張圖(來自:kernel-proc-connector-and-containers)
系統侵入性比較
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使用率。
在產品的研發過程當中,也遇到了一些問題,好比:
方法必定比困難多,但方法不是拍腦殼想出來的,必定要深刻探索問題的根本緣由,找到系統性的修復方法,具有高可用、高性能、監控告警、熔斷限流等功能後,對於出現的問題,可以提早發現,將故障影響最小化,提早作處理。在應對產品運營過程當中遇到的各類問題時,逢山開路,遇水搭橋,均可以從容的應對。
通過咱們一年的努力,已經部署了除了個別特殊業務線以外的其餘全部服務器,數量達幾十萬臺,產品穩定運行。在數據完整性、準確性上,還有待提升,在精細化運營上,須要多作改進。
本篇更多的是研發角度上軟件架構上的設計,關於安全事件分析、數據建模、運營策略等方面的經驗和技巧,將來將會由其餘同窗進行分享,敬請期待。
咱們在研發這款產品過程當中,也看到了網上開源了幾款同類產品,也瞭解了他們的設計思路,發現不少產品都是把主要方向放在了單個模塊的實現上,而忽略了產品架構上的重要性。
好比,有的產品使用了syscall hook
這種侵入性高的方案來保障數據完整性,使得對系統侵入性很是高,Hook代碼的穩定性,也嚴重影響了操做系統內核的穩定。同時,Hook代碼也缺乏了監控熔斷的措施,在幾十萬服務器規模的場景下部署,潛在的風險可能讓安所有門沒法接受,甚至是致命的。
這種設計,可能在服務器量級小時,對於出現的問題多花點時間也能逐個進行維護,但應對幾十萬甚至上百萬臺服務器時,對維護成本、穩定性、監控熔斷等都是很大的技術挑戰。同時,在研發上,也很難實現產品的快速迭代,而這種方式帶來的影響,幾乎都會致使內核宕機之類致命問題。這種事故,使用服務器的業務方很難進行接受,勢必會影響產品的研發速度、推動速度;影響同事(SRE運維等)對產品的信心,進而對後續產品的推動帶來很大的阻力。
以上是筆者站在研發角度,從可用性、可靠性、可控性、監控熔斷等角度作的架構設計與框架設計,分享的產品研發思路。
筆者認爲大規模的服務器安全防禦產品,首先須要考慮的是架構的穩定性、監控告警的實時性、熔斷限流的準確性等因素,其次再考慮安全數據的完整性、檢測方案的可靠性、檢測模型的精確性等因素。
九層之臺,起於累土。只有打好基礎,才能指揮若定,決勝千里以外。
如發現文章有錯誤、對內容有疑問,給我留言哦~