庖丁解牛(二):360集群控制系统概述

庖丁解牛(二):360集群控制系统概述

如何在5s之内控制5w台服务器执行命令

之前在360主导设计了360的服务器集群控制系统,当时由于保密原因,没有机会在外面讲一下,现在终于有机会,希望能给大家一点借鉴。

什么是控制系统

  • 快速,安全的进行服务器任务分配。
    • 最终达到的性能指标是5s对30000台服务器进行任务分发&执行&结果获取
  • 用要求的权限进行执行,精确控制任务
    • 严格的权限树限制
      • 插件审核机制
      • 用户只能操作自己有权限的树节点
    • agent控制信道加密(对称加密算法 with salt)
    • 能在任务执行的任何时间进行 暂停、继续、停止
      • 仅限于子任务(机器)粒度
    • HTTP回调接口
      • 如果在创建任务时提供一个HTTP URL,子任务(机器粒度)的任何状态变化都会通过这个回调进行通知
    • 对任务的输出,返回值进行收集,汇总,入库

    控制系统架构

    由于我们的控制系统属于基础服务,一般来说我们对公司的一般业务的可用性要求是要达到99.99%。作为基础架构类的服务必须比普通业务的可用性要高1~2个数量级,也就是:99.999%~99.9999%。

    所以,一个最基本的要求就是,整套系统必须没有任何单点。

    我们整套系统的架构图如下:


    我们为了达到这一点,我们做了如下设计:

    1. 我们的控制系统本身没有任何状态,任何状态都是保存在数据库中。
    2. 我们单独设计了一个hermes-sitter模块, 负责健康检查以及在健康检查失败的时候进行必要的触发错误逻辑。

    整套系统在各种情况下的故障恢复逻辑如下图所示:

    整套系统的开发是在我们详细分析并绘制了上述图表的情况下给出的, 所以整体的Coding时间也只用了一周,在加上我们用gtest做的较为详尽的单元测试 由于各种错误检查的逻辑在里面,最后的单侧行覆盖率也就在70%左右, 系统上线后3年,仅修复了一处关于暂停点的bug。

    控制系统的实现细节

    第三方库的应用

    我们在构建控制系统的时候主要应用的第三方库有:

    • libev
      • 著名网络事件库libevent的弟弟,更轻,更快
    • c-ares
      • 异步DNS解析库,对DNS over TCP的支持,提高响应速度
      • 由于只对select模型提供了支持,我们给它加了个patch来配合libev使用
    • gtest
      • google的单测框架
      • 单测是一种很好的在软件构建初期就可以发现潜在的bug的方法
    网络模型
    • 我们的网络模型是参考memcached的线程模型,对等多线程模型
      • 对client模型和server模型都给予很好的支持
      • “对等”不会由于线程分工造成性能瓶颈,减少内存拷贝

    代码地址:auxten/gko_pool · GitHub

    内存分配优化

    在项目优化的后期,我们发现内存分配是我们系统的一个性能瓶颈, 为此,我们特意构建了一个比较专用的内存池。

    主要针对我们在创建连接之前所要开辟的国定大小的内存进行加速。

    主要目的有如下两点:

    • 减少为每个连接分配初始的r/w buf的开销
    • 实现了一个简单的内存池,只能分配4K块大小的内存块,bucket大小可以配置,stupid but works

    代码地址:auxten/gkoAlloc · GitHub

    瓶颈发现与解决

    经过我们利用gpreftools的寻找性能瓶颈,我们发现DNS成为我们系统最大的瓶颈。

    我们尝试过用多线程DNS解析,但收效甚微,就像这样:


    然后,我们又很自然的想到,可以使用DNS Cache来解决。

    但是,遇到不能解析正确的域名,我们还是需要走DNS查询,还是弱爆了, 就像这样:


    我们需要一个突破性的解决方案,像这样的:


    首先,需要解释一个问题,我们的控制系统由于一些特殊的网络ACL限制原因设计成了:

    由我们的中心控制端主动发起连接去连接我们的agent

    这点,也让我们的控制系统成为了一个在普通服务端工程中比较少见的高性能"客户端"编程模型。

    一般来说,"服务端"编程模型的特征是:监听在一个端口上等待连接的到来

    • 服务端编程模型的流程伪代码是这样的:
      • 短连接:
    bind端口
    listen监听等待连接
    while True:
      accept新连接
      read请求数据
      处理业务逻辑
      write响应数据
      close连接
    
      • 长连接:
    bind端口
    listen监听等待连接
    accept新连接
    while True:
      read请求数据
      处理业务逻辑
      write响应数据
    

    而"客户端"编程模型的特征:调用connect(2)来主动发起连接:

    短连接:

    while True:
      connect发起连接
      write请求数据
      read响应数据
      close连接
    

    长连接:

    connect发起连接
    while True:
      write请求数据
      read响应数据
    

    我们的控制系统就属于"客户端高并发短连接"编程的一个很好的实例。

    客户端网络编程里大家容易忽略,也是glibc给大家无意间误导的就是:DNS查询也是一个 请求&响应的网络操作。

    通常情况下,DNS都是基于UDP,请求和响应都是各自一个UDP数据包就可以完成的。 其速度也是非常快的。但由于DNS查询是一个递归的过程,如果一个DNS域名查询的请求在第一次请求的DNS服务器中没有被找到,DNS服务器就会询问上层DNS……依次递归。

    这样就会导致当查询的域名为不存在的域名的时候就格外的费时,在异步非阻塞的main loop 中这种费时是致命性的。

    所以,我们的解决方案是:

    包括DNS在内,全异步 libev + 状态机 + c-ares

    经过我们的异步DNS改造,这个问题被完美的解决。

    相关网络框架代码参见:auxten/gko_pool · GitHub

    易用性改造

    为了安全性考虑,我们没有直接的把bash命令调用的功能直接开放给用户。

    因为,我们的控制系统是可以在5s之内给6万台主机下发命令的, 所以,如果发生误操作那影响也是相当大的。

    我们只允许用户调用审核过的插件进行任务执行。

    为了让我们的系统更容易的接入,我们为我们的系统包装了一层RESTful 的API:

    http://hermes:8360/TaskInterface/CreateTask.php?
    task_type=1234&           # 插件ID
    task_name=$task_name&     # 任务名,非必须
    work_account=$work_account& # 任务执行的用户
    concurrency=$concurrency& # 任务执行的并发度
    plugin_info=$plugin_info& # 给插件传递的参数
    time_out=$time_out&       # 任务执行超时时间
    hostlist=$hostlist&       # 任务执行的机器列表
    callback=urlencode($callback) # 任务执行的回调
    

    通过这种方式,我们就允许用户用任何的语言进行开发。


    著作权归作者所有,转载请联系作者获得授权。
    作者:auxten
    链接:zhuanlan.zhihu.com/auxt
    来源:知乎

    服务端开发群:365534424,本文仅授权51 Reboot相关账号发布。

    编辑于 2016-07-28

    文章被以下专栏收录