当前位置: 首页 > 科技观察

微信研发系统下分布式配置系统设计总结

时间:2023-03-16 21:59:49 科技观察

作者:ypaapyyang,腾讯WXG后台开发工程师,个人公众号:码农班代表。本文旨在分析分布式配置系统的必要性、可行性和关键约束,并基于这一系列分析介绍微信研发体系下的实践尝试。前言对于很多业务开发的同学来说,业务素材的处理并不是一件容易的事情,通常需要定制化的数据清洗、格式转换、工具开发。笔者曾有过这样一段不愉快的回忆。一次导入近十种配置数据,耗时两天。如果说这段经历有价值的话,它让我想到了分布式配置系统,并在工作中实践出来,避免再次陷入这样糟糕的过程。本文旨在分析分布式配置系统的必要性、可行性和关键约束,并基于这一系列分析介绍微信研发体系下的实践尝试。配置的定义我们知道,软件建模的本质是现实世界(人、物、物和规则)的映射,映射的输出包括编程系统和配置。配置为我们提供了在运行时动态修改程序行为的能力,也就是常说的“系统运行时飞行姿态的动态调整”。总是需要为系统的一些功能特性预留一些控制线,以便我们以后需要的时候可以人为地摆弄这些线来控制系统的行为特性。”因此,本文所指的配置特指内部操作员(广义的系统操作员,包括产品、运营、研发等)产生的数据作用于编程系统(包括实时系统、批运行程序、数据任务等)作为输入参数。归纳起来,配置通常包括以下三种:环境配置,定义应用运行的环境相关参数,如IP、Port等;b.应用配置,定义与应用本身或信息安全控制等相关的参数,如初始内存分配大小、数据库连接池大小、日志级别、账户密码等;(密码、证书等东西一定不要放在配置系统里,要使用统一的加解密服务)c.业务配置定义了应用程序执行的业务行为数据,比如最常用的功能开关、参与活动的商户列表等。系统约束数据模型配置最基本的数据单元是key=value(配置物品)。比如功能开关,通常是最简单的类型,使用布尔值来影响程序执行环节(不考虑灰度)。但是,只有键值类型是不够的。比如DB的连接配置,包含ip、port、username、password等字段,在ini文件的实现中由不同的配置项组成,逻辑上属于同一个Configuration对象,所以基于在面向对象的设计思想上,key=object是一种更通用的配置模型,物理实现上可以是json也可以是xml也可以是protobuf消息。对象类型的数据可以是平面的,也可以是多层的(嵌套的)。在实际业务应用中,平面数据有其特殊性,即通常表项较多,最典型的数据是白名单,可能有数万个表项。线下,内部操作员使用excel来管理这类数据。如果只是粗略的封装成一个对象,那么数据太大可能会导致系统效率下降(要么是配置写入效率下降,要么是配置读取效率下降),所以我们会用普通对象数组来表示,即key=表类型的数据。访问模型不同于产品用户生成的数据。配置系统的数据流是单向的,离线系统结合实时系统,读写分离(异步写入,实时读取)。最后,我们要构建的分布式配置系统,它的系统设计也必须基于这种访问模型。系统约束显然,内部运营商是生产者,所有配置都必须是文本类型(Readable),数据量小(相对于用户和系统等生产数据),需要的存储空间和更新频率较低。.可以理解,在整个配置系统架构中,输入端就像一个键盘,相对于CPU来说是一个超慢速的设备,它们对易用性、易操作性、安全性都有更高的要求。系统。再想想用户画像系统,它部分满足配置系统的访问模型,即数据流向是单向的,离线系统负责写入画像数据,实时系统读取数据。但首先,它的数据生产者通常是离线任务,而不是运营商;第三,涉及的数据量巨大,通常需要定制存储引擎。与此相比,配置系统无足轻重。相比之下,配置系统的消费者具有高频读访问,对系统吞吐量、延迟、网络流量、可用性、一致性、请求单调性等要求更高。下面,我们将一一进行深入思考。配置系统的设计应充分考虑上述数据模型、访问模型和系统约束。(奇怪的是,笔者在查阅相关配置系统的实现时,很少看到一致性和请求单调性的讨论。这也是促使作者写这篇文章的原因)安全约束正因为配置可以很容易地调整系统运行时行为,所以配置的安全性非常重要。实现安全的必要条件是让正确的人在正确的时间以正确的方式发布正确的配置。因此,配置系统不仅要支持灰度发布的基础能力,还要在权限管理、权限粒度管理、配置变更审核、审计、历史版本等方面加强建设。系统演进单机配置文件在单机系统时代,我们基本都是使用配置文件来存放配置数据(如ini文件、xml文件等)。配置文件易于理解、易于实现、高可用,因此进入了分布式集群时代,至今仍被广泛使用。但是配置文件也有很多缺点,包括:易用性差,主要体现在表达的数据类型单一。比如ini只能管理配置项,即key=value类型的数据;而如果使用xml文件来管理key=table类型的数据,那么文件内容的初始化效率低,容易出错,不易维护;可操作性较差,配置文件基本上只能由开发修改发布,产品和运营的常规业务素材变更都必须参与开发和执行。业务流程效率受到严重影响;正确性和安全性难以保证,由于配置文件实现容易,很多团队忽视了操作系统的建设,研发人员随意、恶意修改配置文件的情况无法杜绝。细粒度的权限管理、操作审核、审计无从谈起;发布效率低,配置文件部署在单机上。在大型集群的情况下,配置文件的任何改动都需要经历一个漫长的灰度发布过程。对于整个网络,如果静态加载配置文件,则需要重启binary,需要研发和运维人员耗费大量精力;文件的一致性很难保证。在发布配置变更的过程中,如果集群崩溃,会导致不同机器的配置存在差异,无法自动修正,依赖人员或运维系统的支持,从而导致undefinedbehavior的业务。如果通过构建操作系统可以提高易用性、可操作性、正确性和安全性,那么发布效率低、难以保证文件一致性是单机配置文件的致命弱点。配置文件系统是被动的、离散的,无法接受外界的变化,没有主动的能力。集中式配置文件中心于是,集中式配置文件系统应运而生,专门解决了上述问题。开发者将配置文件存放在独立的第三方服务中(一般由ZooKeeper管理,部分团队自己实现微服务管理),然后由agent周期性的拉取配置到本地缓存(pull),或者通过事件订阅通知能力(推送)。集中式配置文件系统专门解决了发布变更效率和配置文件一致性保证的问题。但在笔者已知的应用案例中,仍然存在以下问题亟待解决:一致性粒度较粗,集中式配置文件只能保证分布式集群的最终一致性(时间视情况而定)pull和push的频率和速率),但是,不能保证在任何时候,对于任何配置,所有进程、线程和协程都看到相同的数据,这通常会导致意外的业务失败;无法保证请求的单调性。在业务请求中,我们希望用户看到的配置内容是静态的。如果中间有变化,可能会导致业务失败,严重的会造成用户数据状态混乱;而基于集中式配置文件系统的配置通常是动态加载的,配置的变化可能随时响应实时系统,导致一个业务请求依次看到不同的数据状态;安全性仍然没有得到充分保障,虽然集中配置文件的修改可以控制权限,但是在消费者机器上,开发者仍然可以手动修改本地配置文件缓存来影响程序的运行行为;不支持灰度能力,配置文件变化分布满。如果要支持灰度发布能力,需要业务介入,自己去实现;配置文件系统,无论是单机配置文件还是集中式配置文件,存在的问题归根结底是由配置文件的载体和集中式配置文件系统的流水线定位决定的,导致精细化管理成本高:视觉可读性对生产者很重要,但对消费者无关。因此,整个链接由配置文件承载,可能导致加载效率低下(如处理千万级别的黑白名单,或业务方请求链接实时动态加载);配置文件很难安全方便的管理元信息,为了实现一致性、单调性和安全性,配置需要对一些元数据信息进行管理(下面详述),但是配置文件系统不具备这种能力,除非业务side用高成本自己实现;配置文件数量与配置数量密切相关,随着时间的发展,配置文件数量不断膨胀,带来新的运行问题;集中式配置文件系统通常只定位自己,据笔者所知,它并不理解或维护配置文件的内容。代理功能单一,业务消费者不直接与系统交互,只能看到配置文件。松耦合虽然可以提高可用性,但也让业务方仍然投入大量的开发成本来处理配置文件。配置文件只是配置的物理载体。上述缺点并非无法克服。只是在基于配置文件的配置体系下,实现上述能力的成本较高,需要更多的使用约束和外设支持。数据库配置存储对于结构复杂、类型多的配置,业务研发同学通常不会直接使用配置文件来承载,而是使用数据库(关系型或非关系型)的表来存储配置,然后编写工具来承载。导入数据。该存储解决方案克服了配置文件的一些问题,并支持更细粒度的配置管理。但是也有明显的缺点,就是定制化程度高,不可复用,开发重复性高。因此,我们需要对此进行改进,将配置存储、读取、写入和管理过程的共性化、通用化、平台化提炼出来。解决思路物理模型由于配置文件难以微调管理,并且具有容易入侵的物理实体(本地文件),我们需要一种新的数据结构来承载配置。前面我们讨论过,配置的数据模型有两种,分别是key=object和key=table。对于用户来说,配置必须是可见的、可读的和可管理的。为了实现这个目标,我们只需要在内部操作人员和配置系统核心之间构建一个设计良好的操作系统。后端呢?对于消费者来说,最重要的是传输和计算的效率。同时,为了与微服务框架接轨,protobuf消息无疑是最好的形式。但是,protobuf无法解释自己。没有消息定义,我们既不能将文本配置转换为pb二进制流,也不能反序列化。所以业务消息的定义必须参考操作系统,但是protobuf对可视化编辑不是很友好。因此,一个可行的思路是基于JSON数据定义、可视化、传输和存储配置。数据类型转换只有到达业务端才进行。安全管理构建配置运营体系,成为运营商管理配置的唯一入口,轻松获得高额回报。我们可以根据操作系统进行各种配置安全加固。例如,配置变更必须有相应的权限,审核通过后才能应用到系统中。所有的操作都要具备可审计的能力,可以快速查看配置的历史版本。同时,灰度、回滚等能力也需要基于操作系统进行操作。上面配置系统SDK中提到,集中式配置文件系统的流水线位置,agent只负责周期性拉取配置,然后缓存到本地文件系统。业务系统与配置系统松耦合。我们认为配置文件的开发成本仍然很高。对于业务端来说,最好的开发形式应该是:intGetConfig(conststd::string&key,::google::protobuf::Message&msg);而不是然后去了解文件的内容和形式。那么我们就需要为业务端提供一套配置系统SDK,屏蔽配置系统的细节、数据结构等信息,让业务只能看到配置好的ProtobufMessage对象。在SDK的基础上,消费者只需稍微介入(业务插件,见下文),我们就可以完成协议转换、配置缓存、进程、线程、协程快速最终一致性、单调请求、灰度发布能力。配置系统SDK是精细化管理的基础。我们可以通过维护配置本身的内容以外的配置元数据信息来实现上述能力。异步异步是配置SDK的关键。很多本地缓存的更新都是通过实时链接请求周期性处理的,实现起来容易,但是效率上存在问题,特别是考虑到我们还需要配置业务逻辑进行配置。因此,最好的解决方案应该是通过异步过程来加载、初始化和配置的其他逻辑处理。异步带来的问题是异步进程与实时请求之间的并发问题,即在异步进程的配置变更过程中,如何处理实时链接的读请求。这是一个工程问题,我们将在另一篇文章中讨论。一个可行的思路是多版本和引用计数技术。异步业务插件提供的另一个好处是业务可以在配置生效时进行一些初始化动作,比如检查配置的正确性,构建适合业务的数据结构等。比如业务白名单就是pb中的一个数组。如果业务进行命中搜索,成本相对较高。业务最期待的方式肯定是用map来存储。因此配置SDK异步为业务插件能力提供了基础。推送和拉取我们更喜欢将SDK配置为主动拉取配置更新。推和拉的辩证法在于效率和可用性。推送更高效,没有无用的网络消耗。但是推送引入了新的系统依赖(即事件中心)。除非必要,否则不要添加实体。基于这样的想法,我们倾向于通过SDK周期性的主动拉取。至于效率,可以通过各种工程手段优化到可以接受的程度。当然,这也取决于系统的规模。如果我们要讨论的是公司机器的配置系统而不是一些中央层面,那么我们也会认真考虑push或者push-pull组合模式。快速的最终一致性无论是单机配置文件系统还是集中式配置文件系统,都存在严重的不一致性。对于一次配置变更,基本上需要很长时间才能达到最终一致性(即所有并发看到的是同一个数据状态)。一个可行的想法是有多个版本并定期生效。只有在SDK拉取最新数据后的某个时间点,配置才会对外可见。至于如何保证所有的SDK都拉取了数据,这就涉及到易用性的问题,我们将在另一篇文章中讨论。请求单调定时生效不能解决请求单调的问题。请求单调性是指实时服务在处理请求时,读取的配置内容在请求的调用栈过程中必须是静态不变的,即使中间要生效的数据变成了有效数据。一种思路是我们可以通过线程私有变量(协程私有变量)来缓存配置版本。在配置SDK多版本能力的基础上,也很容易实现灰度发布能力。灰度发布的能力无非就是选择有效配置版本的能力。如果本次请求的本地机器、角色、业务key(如用户、商户、订单)命中灰度范围,则使用新版本;否则,将使用原始版本。效率提升效率提升包括减少网络传输的数据量和减轻配置存储服务的压力。这些都是具体的工程方法,我们在本理论章节不做讨论。可用性改进分布式系统的可用性改进是一个常见的话题。为了重点介绍配置系统的独特能力,本文不做具体讨论。(但是尽量减少系统中的单点是一个重要的原则,在上一节“PushandPull”中也有涉及。同时,为了业务的可用性,第三方配置系统的可操作性能力、故障主动发现能力、故障通知能力、复现能力、定位能力也很重要,这也是重新发明轮子的一个重要原因,很多团队软件可能做的很好,但是服务能力(主要指运营能力)有点不尽如人意。)【本文为专栏作者《腾讯技术工程》原创稿件,转载请联系原作者(微信ID:Tencent_TEG)】点此查看该作者更多好文