转载请注明出处
1 架构目标
- 将系统的可扩展性作为最高优先级考虑
- 避免系统复杂度随着系统内服务规模呈指数级增长 假设:
- 设备与控制器的连接是有规划的,控制器不需要支持统一南线地址
- 控制器中的静态数据量级在百万级
- 系统的基础设施是可靠的(基于这一假设进行软件设计)
2 架构设计
2.1 逻辑架构
总体逻辑架构在k8s docker的基础上分为“三纵”、“四横”。
三纵”分别是控制器的公共服务列(非业务服务)、业务服务列以及外部依赖服务列(三方服务)。其中公共服务和业务服务都属于控制器自身的服务,也部署在同一个k8s集群中;而三方服务可以是控制器独占的服务也可以是与其它系统共享的服务,部署形式也不局限,只需要服务间访问可达。
三方服务这里只举一些典型的例子:
- 统一网关:如Zuul,控制器系统一般使用独立的网关,多与其它系统共用同一网关;
- 服务注册中心:如Eureka,用于异构系统间的服务发现,控制器内部通信不依赖;
- 统一配置中心:如Apollo,同样由于运维需要将一些业务配置集中管理时需要用到(侵入式编程,非必要不建议使用);
- 消息总线:如Kafka,用于下层服务上报消息给上层服务,或者控制器系统内部生产消息给外部系统;
- 外存储:持久化存储(这里的外特指独立于k8s集群外的服务器),建议是独立的存储服务器。关系型数据库如MySQL、PostgreSQL,非关系性数据库MongoDB、elasticsearch,缓存数据库如redis。 “四横”是控制器业务服务的分层,分别为: 表现层:主要提供控制器的可视化操作界面; 应用层:提供不同场景的业务服务; 资源层:维护控制网络的设备信息和控制连接层Agent的信息,以及设备与Agent之间的连接关系; 南向控制连接层:负责维持与设备间的多协议连接,并为系统内部提供统一的接口。 业务服务的分层是确保系统横向可扩展性。其中首要是将业务逻辑与南向连接完全解耦(即将应用层与南向控制连接层解耦),当应用层和南向控制连接层可以独立扩展时,整个控制器系统将保持良好的可扩展性。 按照前面的假设,设备与控制器的连接是有规划的,这表示每个Agent负责连接的设备数量是有限的,一个网络域的设备可能与一个或者多个Agent建立某种协议的连接,而应用层需要控制设备时仍需知道哪个Agent是与设备建立邻居关系的,这就是业务与连接间的耦合。 业务与连接间解耦需要先对控制器语义进行抽象:控制器的本质就是以“某种形式”向设备下发“指令”进行网络控制。可以将应用层认为是“指令”的源,而南向层则是“某种形式”的代理,所以控制器的语义就变成应用层通过某个南向层的Agent下发指令到设备。 如何确定“某个”南向层的Agent?这就引入了资源层服务,资源层服务维持着设备和Agent信息以及设备和Agent的连接关系,控制器的语义进一步成为:应用层需要向某个设备下发指令,通过设备信息先向资源层获取南向Agent服务地址,然后通过对应Agent下发指令到设备上。如下图所示:
资源层的作用将应用层和南向层解耦,无论是应用层实例扩展还是南向层实例扩展都不需要对方感知。
对于南向层收到从设备报上来的数据如果要上报给应用层,则通过消息总线进行上报。
2.2 数据架构
对于数据库选型这里不做特别限制,可以根据业务需要选择关系型/非关系型数据库,对用于做历史分析的数据可以考虑采用类似elasticsearch的数据库。
根据前面的假设,控制器系统的单表静态数据为百万级,所以这里暂不考虑数据存储层面的性能、高可用性等复杂问题。
从网络设计出发,也应当避免同一网络域出现强一致性的海量数据。
2.3 开发架构
由于服务是运行在docker的容器中,所以整个系统对开发语言没有特别限制,要求所有的服务都打包成docker的镜像进行交付。
服务间的接口通过x-rpc(具体rpc框架选择只要满足语言无关性并且内部达成一致即可)定义。
开发依赖的包由统一的包管理文件进行管理(golang使用gomod进行管理,java使用pom管理,java的包通过团队maven仓进行管理)。
2.4 运行架构
保持单容器单进程的设计,利用k8s的特性,可以帮助我们监控服务进程状态,并且当服务down掉后由k8s重新拉起容器。
而服务内部应当有“防呆”设计:
避免线程长期占有“锁”;
所有有状态的等待都有超时处理;
监控线程的意外退出(如java线程)。
2.5 物理架构
控制器系统是基于k8s docker的,所以可以认为系统部署的基础设施为k8s集群。
k8s集群天然具备可伸缩性,因此控制器系统的扩缩容在这不是一个复杂问题,不再赘述。
K8s自身没有提供容器网络的实现,对于跨POD间的通信,无外乎两种方案:一种是underlay直接互通,即通信的双方有彼此的路由信息并且该路由信息在underlay的路径上存在;另一种是overlay方案,通过vxlan隧道实现互通(方案的具体通信流程可以参考k8s的网络插件介绍)。
除了k8s POD间通信外,对于Agent还需要POD内容器与网络设备间进行通信,所以对于k8s集群的服务器需要支持双网卡,一个用来进行POD间通信,另一个与设备网络打通。
考虑到系统可用性,k8s的集群节点可以分布到多个园区,在部署服务时每个服务的多个实例可以分布到多个园区。
系统的访问通道分为外部通道和内部通道,内部通道是与设备建立连接的通道,属于内网;外部通道则通过统一网关访问k8s集群服务。
3 架构原则
系统的架构原则继承云原声系统的设计理念:
面向分布式设计(Distribution):容器、微服务、API 驱动的开发;
面向配置设计(Configuration):一个镜像,多个环境配置;
面向韧性设计(Resistancy):故障容忍和自愈;
面向弹性设计(Elasticity):弹性扩展和对环境变化(负载)做出响应;
面向交付设计(Delivery):自动拉起,缩短交付时间;
面向性能设计(Performance):响应式,并发和资源高效利用;
面向自动化设计(Automation):自动化的 DevOps;
面向诊断性设计(Diagnosability):集群级别的日志、metric 和追踪;
面向安全性设计(Security):安全端点、API Gateway、端到端加密。
还有一些具体的补充:
服务无状态:即服务不对容器的磁盘数据有依赖,服务运行数据可以自恢复,通过持久化数据解决有状态的场景。这样做是确保系统的scale-out能力,同时解决服务进程失败后自恢复的问题。
系统内通信自闭环:系统内服务间通信不依赖外部服务(包括三方服务列),系统内的服务发现和寻址均依赖k8s的APIServer和DNS。这样做是提高系统的健壮性,在系统基础设施正常的情况下系统可用性得到保障。
服务间只有接口依赖,无状态依赖:不要让服务间感知对方是否“活着”。这样做是为了满足架构目标——避免系统复杂度随着系统内服务规模呈指数级增长,如果服务间有状态依赖,当服务的实例增加时,这种状态依赖也会成倍增长。
失败是必然的:服务的失败不可避免,不要多度设计抵制故障,在设计中尽可能考虑发生故障后阻止问题扩散并如何从故障中恢复。