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

微服务通用代码组织实践

时间:2023-03-16 01:20:07 科技观察

我们知道,微服务架构由多个相对简单的服务组成,依靠服务之间的隔离来降低系统复杂度。理论上,在拆解一个完整的微服务时,业务代码复用的机会应该不会太多,因为服务之间的有效隔离会让每个代码只关注自己的上下文。微服务边界清晰,不仅包括职责清晰,代码层级也应该清晰划分。但是,我们还是推荐微服务组出品的两类代码共享:第一类是交互协议代码,微服务之间的交互协议标准的代码。由于每个独立的微服务都有自己的单一职责,因此服务之间的微服务交互被暴露为新功能。交互协议的标准代码是公开的,也可以看作是以微服务易于理解的方式发布的,有利于保持微服务间交互的便捷性和准确性。第二类是纯工具代码,独立于微服务组的业务特性,往往提供更底层的功能库或组件,如版本对比、时间对比工具、上传资源组件等。这类代码应该算是第三方独立仓库,类似于我们引用的Github上的开源库。第二类代码比较简单,可以作为独立于微服务事业群的仓库存在。对于第一类,微服务之间的同步信息传输往往是通过HTTP、PRC等通信协议,将传输的JSON或Protobuf等按照交互方定义的业务协议解析为业务可理解的信息。这部分交互相关的代码可以看作是普通代码。本文探讨如何将这部分代码更好的组织起来,运用到整个微服务系统中,以提高研发效率,减少相关故障。这部分的代码组织问题本质上不是系统或模块的依赖问题,而是如何在微服务架构系统中更方便地同步和使用公共代码的问题。在我们公司走上微服务实践的不归路之后,这部分交互代码组织也经历了不同的阶段。本文将以此案例进行探讨。Go是我们公司的指定语言。本文中的一些示例和功能在Golang中显示。Git是最流行的分布式版本控制系统,讨论也是基于Git。1.交互示例在讨论的开始,我们首先列出了微服务之间交互的代码和目录结构示例,以及三个简单的定义:Sourceservice:产生Model和Client的服务。Model:定义数据模型的代码,用作微服务间交互协议的代码解析。Client:微服务发起请求的代码,使用Model解析相互之间传递的数据。通常,源服务提供API时,可以作为附件提供。微服务交互的简化代码和目录如下,其中model存放数据模型代码,client存放网络请求代码:数据模型实体代码:project/base/model/user.gotypeUserstruct{UserIDint64`json:"user_id"形式:“user_id”`UserNamestring`json:“user_name”形式:“user_name”`Statusint`json:“状态”形式:“状态”`Avatarstring`json:“头像”形式:“头像”`AvatarSmallstring`json:“avatar_small"form:"avatar_small"`}交互请求代码:project/base/client/user_client.gourl:="http:///project/"userClient:=http.Client{Timeout:time.Second*2,//Maximumof2secs}req,_:=http.NewRequest(http.MethodGet,url,nil)res,_:=userClient.Do(req)body,_:=ioutil.ReadAll(res.Body)user:=model.User{}jsonErr:=json.Unmarshal(body,&user)ifjsonErr!=nil{log.Fatal(jsonErr)}fmt.Println(user.UserName)2.练气期-手动文案:微服务的核心意义不仅在于服务拆分,更在于团队组织架构的调整d通讯表格。当团队最初切换到微服务时,每个成员将数据中的模型和客户端从源服务复制到各自的相关服务。虽然代码是单独复制修改的,但是一点影响都没有,但是随着服务的增多,要找到源服务复制代码就越来越麻烦了。大家的沟通过程通常是:A:“兄弟们,我提过代码。”B:“又改了!改了什么。”A:“xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx”,B:“妈的,这么多,快把文件传给我!”G:“我也要!”在完全靠人力维护代码的过程中,很容易遗漏和修改文件,而且更新操作不流畅,长期下去容易导致相关微服务和源服务维护的基础差异较大.从源服务复制回来的代码,和自身项目库的约束没有区别,可以任意修改,本地手动修改容易造成协议不一致,随着团队成员的增加和规模的增加,这种方法已经开始被团队成员抱怨和批评。3.基础建设期——集中式仓库程序员不会满足于现状,程序员天生就是为了解决手工操作的问题。原始粗暴的方法copycode自然就很容易被淘汰了,有成员提出单独建一个仓库,集中管理每个微服务的基础代码,代码是手动copy从源服务到基础仓库。所有成员都从这个单独的基础仓库中拉取更新,这是理所当然的事情。队员们一致认为这是修仙的必然趋势,大家一拍即合。调整为统一代码仓库后,虽然还是需要手动复制代码到基础仓库。但是用户端好操作,只要没事做,拉下基础仓库拉取最新的依赖代码即可。此外,整个团队的微服务之间交互的所有客户端和模型都可以直接在基础中找到。大家对基地仓库有了统一的认识,一会载歌载舞,相安无事。整个代码仓库的组织结构如下,其中base仓库包含多个project-base目录,与项目仓库中的base目录完全一致:此时团队成员的沟通流程通常是:A,B、C、D、E、F、G:“推!推!推!推!”A:“兄弟们,我更新了代码,更新了基础!”B、C、D、E、F、G:“拉!拉!拉!拉!”95%的情况下,大家的合作和沟通都是愉快的,但也有倒霉的时候,尤其是小G被迫紧急修复一个稳定维护期半年的项目:A、B、C、D、E、F、G:继续默默递交……G:“拉!”G:“天哪,我只需要更新B的一个update,为什么要拉下几百个update,我编译不过来!”小G很负责,遇到问题还得解决哎,他终于解决了半夜两点,终于上线了,却不知道更糟糕的事情还在后头。发生了在线事故。老板:“这次线上事故影响很大,小G明天复习一下!”G:“我就是想更新B的基数,A、B、C、D、E、F都更新了这么多个月,我都不知道自己掉坑里了。”小G放声大哭。问题根源:中心化仓库Base是各个微服务的Base代码的集合。一次更新会引起所有更新,Base的某一部分不能单独更新。比如示例中聚合项目中project1-base更新时,project2-base、project3-base、project4-base也会不假思索直接被Git更新。这些代码更改通常不在预期和测试范围内。只有base中的数据结构和请求代码比较简单。天下虽久太平,骤变必致灾祸。请记住,任何更改都是不安全的,如何减少公共代码存储库更改的影响是我们要探索的。4.打结期——独立分仓小G痛定思痛,第二天肿着眼睛来到公司,请研发小伙伴进行review。审核的结果是,这种统一集中的仓库管理方式,肯定还有改进的余地,大家议论纷纷:B:“你可以tag”G:“不行,解决不了我要a的问题部分更新并拉下一堆更新。”C:》可以直接引用源项目,Govendor只把需要的文件复制到当前项目》D:》不行,直接引用微服务代码心理负担比较重,直接引用容易引起混淆在源微服务中使用其他代码。而且,当跨部门的代码权限存在差异时,这种方案就不适用了,比如保密级别高的项目。”E:“……”小G的眼泪没有白流。有方便的分仓拆分和同步方案,避免人工复制,容易被接受和推广。毕竟人都是懒惰的。目标很明确。经过一番猛查,小G惊喜地发现:虽然Git没有让G仓库直接引用B仓库某个目录的功能,但是B项目已经有一个子目录,直接关联到B-Base子目录。用于保持存储库同步的内置功能:gitsubtree。B仓库的子目录和B-Base子仓库保持同步,小G可以直接使用B-Base子仓库。这一定是因为很多同行也对Git提出了类似的要求。虽然该工具还不完善,但我们有一些限制。只提交了源码仓库,只有它最基本的功能还是够用的!小G叹了口气,早点知道就好了。下面列出了独立子仓库同步机制的详细信息。此时代码仓库的组织结构如下:A:“兄弟们,我提到了代码,如果需要更新我的Base”B、C、D、E、F:无视。G:“好的!拉A基地!”虽然这个方案有一些额外的成本,比如小仓库数量的增加,但是需要大家了解gitsubtree命令。但权衡多半是积极的,也可以直接使用脚本或者Githooks来屏蔽gitsubtree命令,让更新范围更可控,也更方便和自动化。该方案使用现有的工具系统,不会增加学习成本。同时,微服务的拥有者可以决定何时选择同步对服务的修改,有效减少未经测试的代码直接上线导致的失败。可能性。对于上面提到的纯工具代码,我们也建议避免使用多合一的工具存储库,例如公共存储库。更好的办法是将包罗万象的库按功能拆分成多个上下文独立的库,比如创建上下文存储、util、log等仓库。这样可以把不经常变化的代码和经常变化的代码分开,用户可以自己控制每次变化的范围。独立子仓库同步机制详解:格式约定:约定对于每个微服务,创建对应的子仓库,仓库名称以<-base>为后缀。比如Project1对应的子仓库是Project1-base同意在源码微服务代码组织中创建一个基础包,model和client存放在里面(命名也可以根据自己公司规范),如图:约定其他微服务需要与源微服务交互时,直接使用子仓库。由于子仓库完全是作为一个独立的仓库来依赖,所以每天更新一次普通的gitpull命令就足够了。源服务基仓拆分:源服务基仓库拆分核心命令如下:cd#没有基目录时,直接添加子仓库gitsubtreeadd-Pbasemaster#如果base已经存在,先拆分提交到子仓库,然后删除本地,关联远程子仓库。cd#没有base目录时,直接添加子仓库gitsubtreeadd-Pbasemaster#如果是base已经存在,删除先提交到子仓库,然后删除本地和关联远程子仓库。每日同步:每日提交仅限于源服务,避免过度使用高级gitsubtree命令。commit过多后定时gitsubtreesplit--rejoin解决所有commit需要重新遍历commit耗时过长的问题。建议通过脚本和Git钩子实现自动化:#submitgitsubtreepush-Pmaster#updategitsubtreepull-Pmaster5。总结和讨论公共代码组织形式的演变过程,这是复杂而宏大的研发项目中的一个小工具化和标准化过程。通常,集中仓的操作简单直观,容易被识别,大部分时间都能正常运作。在这个问题的驱动下,我们转向了一种更细化的独立子存储库方法。独立分仓方案利用现有的工具系统提供自动推送变更和选择性同步公共代码的能力,而不会增加复杂度。在实践中,有效的提高了效率,降低了未经测试的代码直接上线导致失败的概率,是本文推荐的解决方案。当然,我们看到每个解决方案在不同的公司都有不同的做法。比如有的公司手工copy代码,用的很好。实践中,大家可以根据自己团队的口味选择解决方案,也欢迎有更好体验的朋友一起交流。6.作者介绍齐征,曾在Adobe和百度担任高级工程师,现任互联网公司后端业务线负责人。他曾从事C++、Android和Golang开发工作。