本文主要想给大家分享一下, 宋小菜这三年来, 是如何从单点巨石系统演变成领域驱动的服务化设计的. 这个演变现在还在继续, 我们在实践过程中遇到了很多坑, 也收获了经验和思考.
一, 早期的系统
1.1 唯快不破
我是在宋小菜处于筹备阶段就加入的第一个程序员, 见证了宋小菜系统从 0 到 1 的全过程. 那时候的技术团队只有一个产品经理, 一个程序员 (就是我), 还有五个外包同学. 在宋小菜第一天工作的日子我还记得非常清楚, 是 2015 年 1 月 18 日, 我们的第一个任务我也记得非常清楚, 在 3 月份前产出一个可以交易的平台. 交易需要什么, 用户, 账户, 商品, 资金, 订单...... 这些模块组成了宋小菜最早的交易系统. 那时候为了快, 牺牲了太多的东西, 比如缺乏了整体的架构设计, 缺乏了充足的业务理解, 缺乏了功能的抽象, 缺乏了性能的分析. 这些行为在现在看起来, 非常不可思议也不可接受. 我时常在想一个问题, 如果以我们现在的经验和理解, 穿越回到三年前去开发最早期的宋小菜系统, 又会是一番怎么样的场景呢? 我会一开始就规划出用户中心, 商品中心等几大中心吗? 我会一开始就设计无状态系统和水平扩展能力吗? 我会一开始就开发网关平台收拢和规范所有 API 的调用吗? 答案一定是不会的, 而且这样做结果往往也不一定会比当初更好. 早期系统并不需要一些类似核武器一样的架构设计和中间件, 真正需要的是最快速度为公司的业务提供战斗的能力.
1.2 面临的问题
那时候主要的系统有两个, 分别为内部的 ERP 系统和供客户端调用的 API 服务系统, 这两个系统完成了我们的交易的闭环. 随后的两年时间里, 疯狂的业务代码堆积和不受控制的重复代码, 使得这个系统越来越笨重. 那时候的系统结构非常简单, 公共的模块以 jar 包的形式抽离出来, 其中 ERP 系统的依赖关系可以参见下图:
这时候的系统改造就变得非常痛苦, 每次发布都会变得异常紧张, 生怕有哪个模块的 jar 包改动了没有更新, 就导致系统无法顺利启动. 曾经有那么一段时间, 我会很惧怕系统的发布, 如果顺利发布, 我也会长舒一口气.
再发展下去, 就已经不是害怕系统发布这么简单了, 系统逐渐进入一个可怕的死循环了:
在系统的各个地方散落了一些异常刺眼的注释, 比如 "这段代码太牛逼了, 我不敢改, 所以就复制了一份","这部分的逻辑是 xxx 写的, 不要轻易改动" 等等. 以下六点是我们系统存在的主要问题:
jar 包管理混乱, 经常导致系统无法构建;
笨拙, 耦合严重, 发布影响点混乱, 底层 jar 的变更, 导致每个应用系统都需要发布;
开发过程中协作困难;
新人很难上手, 理解困难;
不容易管理, 出现很多不洁代码;
技术债务越背越多; 当公司规模还小的时候, 业务复杂度和系统复杂度都还处于一个比较好接受的阶段, 巨石系统往往是一个比较高效的架构设计. 但是随着业务的不断扩展, 复杂度和访问量进一步的增加, 如果突破了某一个临界点了, 系统架构的升级和服务的拆分, 就变成不得不做的一件事了.
二, 服务拆分上遇到的坑
2.1 缺乏规划
当时决定进行服务拆分的时候, 技术团队的规模也并不是特别庞大, 也并没有哪位同学对于服务的拆分有较多经验. 现在回想起来, 那时候的起步确实草率了点. 我们没有做太多的规划, 直接到了 "说干就干" 的环节了. 可想而知服务会出现的多么凌乱, 一会一个 "价格中心", 一会一个 "权限中心", 哪个简单就先拆哪个, 哪个新业务要上了, 先起一个服务再说. 随后就暴露出了服务器资源不够, 运维困难, 服务之间调用混乱等问题. 大家意识到这个问题的时候, 还不算病入膏肓. 我们立刻进行了宋小菜整体服务大盘的设计和划分, 将服务分为上下两层. 上层为业务层, 下层为能力层, 尽量使得服务之间的调用变成树状, 而不是环状.
2.2 服务的粒度
服务的拆分是一件非常容易的事情, 关键在于粒度的把控. 那时候对于微服务有一种近乎偏执的理解, 认为只要某个功能模块独立, 就得变成一个服务独立出来, 而且为了保证可用性还必须起两个节点. 按照这个理论我们进行了实践, 也确实出现了不少极端的操作. 逐渐我们明白这个思路是有一些问题的, 而要解决这个问题, 我们必须对于服务拆分的理念达成统一. 我们尝试使用 DDD(领域驱动设计) 来进行服务的规划, 基于 DDD 的设计方法, 从当前业务出发, 抽象业务找到宋小菜业务的核心能力, 并以此作为服务进行拆分. 但 DDD 是方法论, 并不是教条和金科玉律, 在使用的过程中也千万不要走火入魔. 使用了 DDD 进行分析之后, 我们发现了一个更加头疼的问题, 那就是以前的服务拆的太细了, 把很多不该拆分出去的服务拆成了两个甚至更多. 所以之后我们内部启动了 "方舟项目", 该项的目的主要有这些:
对于服务如何拆分, 在团队中构建共同的认知, 并推进使用 DDD;
定义微服务工程结构和规范;
将以往粒度过细的服务进行合并; 通过这次趟坑, 我们也明白服务的拆分是一件非常慎重的事情, 拆分一时爽, 要是再想合并和统一, 就不是那么容易的一件事情了.
2.3 可能出现的风险
2.3.1 经验
开发团队是否具备足够的经验, 能否驾驭微服务的技术栈, 可能是第一个需要考虑的点. 这里并不是要求团队必须具备完善的经验才能启动服务拆分, 如果团队中有这方面的专家固然是最好的. 如果没有, 那可能就需要事先进行充分的技术论证和预演, 至少不打无准备之仗. 上文也提到了, 我们的启动略微草率了一些, 团队中也并没有这方面非常专业的同学坐镇, 所以在一些分布式常见的问题上, 都踩了坑, 比如调用重试, 超时机制, 分布式事务等, 这些问题一出现, 很多没有经验的同学会非常抓狂, 甚至无从下手.
2.3.2 稳定的测试
服务的拆分, 必然会出现的问题就是加大了同学们的开发自测难度. 以前的系统无论是在本地启动, 或是发布在测试环境, 所有的调用都是确定性的. 但是一旦某一块服务拆分出来了, 可能会面临很多问题:
本地针对远程服务的调用, 可能是被禁止的, 那就需要使用 mock 的方式来解决了, 在本机单元测试的时候, 主要测试的是业务逻辑和流程, 并不能很完美的测到远程调用的返回数据.
调用的测试环境可能很不稳定, 当你想要调用的时候, 服务可能早就挂了.
多个项目组开发新功能的时候, 调用了同一个服务, 这时候有可能会因为服务器资源有限, 导致大家资源竞争. 不同公司情况不同, 为开发同学营造的测试环境也不尽相同. 但有一点是一定的, 如果测试复杂, 会给开发同学造成很大的压力. 明明一个很简单的功能, 改一改可能只需要 5 分钟, 但是想走完一次完整的测试, 却花了他 1 个小时的时间. 如果在服务化拆分后不能很好的解决, 会导致开发同学有越来越多的侥幸心理和偷懒的情况, 不进行测试就提交了代码或进行了发布.
2.3.3 日志
服务的拆分必然会导致日志散落在各地, 无论是定位问题需要查看日志, 还是一些事件需要基于日志去实现, 都会变得比以往复杂很多. 一开始的时候, 我们并没有意识到这个问题, 所以很多开发同学出现 bug 或是故障之后, 不知道如何去定位了. 因为一个方法的调用, 可能会因为服务化的调用被放大几次甚至十几次, 最后的错误到底出现在哪里, 如何去找去看, 都是一个问题.
三, 结束语
服务拆分可能出现的问题还有很多, 在这里也只是将一些最容易出现的场景分享给大家. 希望通过本文, 可以给那些准备进行服务拆分, 或在正处于服务拆分起步阶段的团队一些建议和经验.
来源: https://juejin.im/post/5b1a9d9c5188257d692df407