引言
最近很长一段时间的工作重心在接手历史项目. 接手的项目, 一般来说几种情况:
- 之前项目成员拍屁股走人了, 或者项目是买回来的, 确交接, 少文档, 处于无人维护无人知晓细节的状况
- 业务需求很多, 但是由于之前粗猛糙堆功能, 代码堆砌, 变更困难, 需求越做越慢, 拖累了进度, 领导决心整治
- 业务火了, 量级上去之后遇到严重的性能问题, 或者线上问题不断, 需要来救火解决性能问题, 或者成本高昂, 需要优化
接手过程中踩了不少坑, 也算是总结了一些套路. 这里泛泛总结一下, 有些也算是技术团队日常的一些工作经验吧.
历史项目整治四板斧:
- 代码重构
- 变更管理
- 量化指标
- 流程优化
下文在这四大点上展开讨论
代码重构
代码重构无非几个目的:
- 更容易修改以满足新的需求
- 满足性能问题, 或者降低成本
因此, 首先明确代码重构的前提: 就算项目代码很糟糕, 如果可预见的一段时间内没有需求变更, 以及不会有性能问题时, 不要修改. 这个对于有技术洁癖的人来说, 非常难忍, 但是没错, 忍着. 领导不会在意你把代码重写了多么整齐优美, 只关心能不能快速搞定事情, 或者不要花那么多机器成本. 因为你的好心修改重构, 不论测试多么充分, 也不能保证不引入问题, 如果成功重构上线, 你的工作无人知晓, 而出了问题, 那么锅是一定要背的. 因此, 哪怕看上去是现有逻辑的BUG, 如果它历史足够久远, 就可能是个特性了, 可能既有业务逻辑依赖, 改动需要审慎.
代码考古
不做代码实现的重构, 一些文档梳理类的工作是可以做的. 一般来说, 很多项目初期小步快跑, 草台班子搭起来, 不注重技术规范的指定和落地, 导致项目代码质量比较糟糕, 文档缺乏或者过时. 初期接手, 一定有一个非常痛苦的阶段, 我们戏称为”代码考古”: 保护好现场, 不要乱动; 需要挖掘(修改)的地方, 做好说明记录, 做最小创伤调整. 代码通读, 多写些注释, 业务背景. 做一些比较简单安全的”等价变换”操作, 将整个项目逐步迁移到一个相对可控的状态.
- 近年来”微服务”概念妖风肆虐, 代码库按照涉及到的服务拆分, 导致屁大一个业务线, 涉及的代码仓库数不甚数. 最近接手的一个较大的项目, 涉及20+多个代码仓库, 且都有比较强的依存关系.
- 每个人负责一个或者多个项目的开发, 自己代码都改不过来, 就更没空看别的仓库是怎么做的. 另外由于权限的问题, 没有放开代码仓库权限给所有相关同学, 导致大部分开发同学只见树叶, 不见森林.
- 新业务模块的开发方式是既有代码的拷贝粘贴, 比如说今天要做海外版的系统, 就把原来国内版的项目全部复制一份, 然后就分头开发去了. 导致很多逻辑看上去时曾相识, 确有不能直接合并. 后续业务需要打通国内/国际的时候困难重重. 类似的逻辑在不同的项目里面重复造论子.
- 更严重的是, 数据/接口定义, 通过跨项目拷贝的方式来保障一致, 导致发布后线上接口一致性靠人工确认.
这些项目就好比散落的文物, 需要注意识别, 梳理发掘关系, 并加以登记确认.
通过项目合并到单一仓库的”整治”运动来让既有模块重新捡起来.
单一仓库
策略是先大家进一个家门, 认知拉齐, 信息共享, 统一版本管理.
所以一上来的体力活, 把涉及到的代码仓库, 先合并到一个单一仓库 (monorepo) 里面. 为了保障和线上的一致, 尽量不做任何修改. 可以先原始项目丢新仓库一个目录. 尽快讲线上运行服务基于该monorepo运行, 走统一版本管理. 这个阶段要忍住每个项目的奇葩姿势, 尽量不要去做代码层面的修改, 因为改的越多, “上车” (替换线上实例) 越难.
项目合并需要结合发布来做, 只有所有的线上部署居于monorepo, 以一个比较固定的模式发布后才算结束.
单以仓库开发的好处: 所有的变更串行化到一个项目来统一管理, 多个项目分别发布的情况下, 各种跨项目的隐式依赖需要靠人力; 代码共享, 比较容易实现姿势趋同, 在分散项目管理情况下, 大家不太有动力去看别人维护的项目; 便于重构, 在一个项目中做一次全局替换即可, 不需要走版本, 不用太考虑兼容性问题. 在分散项目情况下, 要做一个变更是很困难的.
固化依赖, 统一技术栈
需要将外部依赖需要固化下来, 从而确保确定性的构建. 有的不严肃的发布, 实在线上安装各种依赖, 这就导致很难重新部署, 需要将所有依赖明确固化下来. 考虑到中国特色的网络情况, 外部依赖尽量不要每次重新拉取, 直接丢到项目里面去做变更管理, 从而能够无网环境下构建运行.
这个阶段还会遇到一个问题, 就是技术工具依赖版本的统一. 有的可能在用py3, 有的上古项目还在用py2, 有的在用node8, node10, 有的在用旧的django框架, 有的在用的依赖库版本不一样, 甚至可能还会有功能冲突. 大部分情况下统一到一个较新的版本改动是较小的, 但是如果统一版本成本太高, 只能先忍着, 接受现实, 但是对于新依赖引入要有严格的准入制度.
有些同学为了技术追求, 上一些高大上的组件, 其实没有必要. 用成熟的, 行业充分验证过的东西. 玩透用深入. 或者准入门槛太低, 大家捡到一个就用, 导致类似功能的引入了不同的库依赖. 能不引入的框架, 或者服务, 尽量不要引入. 现有在用的外部库, 至少要保证有人深入研究, 追踪上游各种BUG修复和改进.
技术栈统一是必要的. 曾见过一个项目, web端开发三个同学, python/ruby/go/php语言轮番上, python里面又django/flask混着来. go写的代码, 不同功能用了不同的路由和框架. 严肃的业务项目开发纯粹变成了技术同学练手学新技术工具的游乐场, 项目后面也是做不久关停散伙.
其实大部分的业务需求, 不需要太多的外部依赖, 或者很多都是重复的轮子. 所以从维护性角度考虑, 收紧技术栈, 减少维护的敞口.
代码清理
项目合并完成线上发布替换后, 相当于踏上一条船了, 那就可以做些最简单的整治了. 做一些安全的操作, 如重复的数据定义合并, 枚举, 无用代码的删除等等, 这个过程对于代码洁癖患者来说无疑是最爽的阶段.
注意: 充分利用自动化工具, 最小化人工体力甄别的劳动, 最小改动, 发布验证. 涉及到小函数逻辑实现方式的代码修改, 先加单元测试, 再做功能替换, 保证替换正确性.
始终追求最少的内容, 无用的坚决删掉. 不要怀念, 即便真的有用, 仓库历史记录是可以找回来的. 一些我看来应该从代码中删除的东西:
- 无调用的代码逻辑, 静态语言可以通过代码分析找出来, 全删了; 动态语言在不确定的情况下, 可以通过线上profile的方式来确诊
- 注释掉的代码, 过时的注释, 甚至描述性代码逻辑的注释内容都应该删掉, 因为很难保证失效性, 反而起到误导, 给读者以困惑
- 作者信息, 通过git的变更记录来找人, 以及在代码很多人改过的情况下, 作者信息等于噪音
这种不涉及到功能改动的的重构, 要定时搞. 就和搞卫生一样, 一旦没有定期做, 再来一次大扫除就困难了. 有的同学没有回顾旧代码的意识, 日积月累, 技术债就这么欠下来了.
统一格式化, 引入静态检查工具
由于分项目开发的历史原因, 很多代码的风格不够统一. 合并后, 就要逐步推动一些简单的代码格式化工具, 并通过逐步收紧代码合并规范. 代码格式的统一是最简单的一步, 但是实践起来也很困难, 因为大家惯性的力量是巨大的. 遇到过前端同学, 你让她大括号换行写, 她能够和你罢工. 可以通过增加提交后的自动格式化工具, 减少大家适应新规则期间的效率下降问题. 基于代码语法树的等价变换的工具是最为推荐的, 也不会担心引入新的BUG, 如python的black, javascript的standard 格式化尽量不要支持各种自定义的格式化, 否则又会陷入无休止的这些表层的代码格式审美争论中去.
除了格式化, 也要逐步引入一些静态检查工具, 抹掉一些无意义的个性化的习惯. 每种语言都有现成的工具, 比如在用的Python的pytlint, go的golangci.
这些静态检查工具一般来说都趋严格. 实践中要注意方法, 逐步推进. 对于一些写法的统一格式化, 要认清现实, 寻找一个最大公约数, 一定要做渐进改良式的推进, 每次增加一个小规则. 比如说pylint的规则, 一上来套一个比较严格的规则, 那基本代码库要翻篇重写了, 可以先把现有的所有没有违反的规则先纳入检查, 控制住新的变更违反规则的敞口, 然后通过几轮专项整治运动, 逐步引入新的检查规则.
格式化/静态检查工具的目的是为了形式上的统一, 减少阅读解析代码过程中无意义的自由度, 减少代码阅读解析成本. 这个是最简单的第一步, 但真正内容上的趋同是更为重要, 也更困难的部分.
代码审查
通过完善代码审查将大家对于代码内容加深了解, 统一写法姿势.
把大家拉在一个项目里面开发, 不能自然保证代码写法的趋同, 需要建立起常态化的代码评审(Code Review)机制. 我们使用gitlab做代码仓库, 将master分支锁死, 通过MR (merge request) 进行代码审查合并.
MR应该尽早提, 这样避免最后的审查过程中需要太多返工的地方导致延期, 或者迫于上线压力, 就降低标准合并上线了. 理想情况下, 应该有技术评审环节, 将实现方案记录, 供大家看是否”货不对板”.
MR的讨论可以在线的方式进行, 代码审查避免总是团队leader负责, 这样leader容易成为单点, 要分担给相关的同学来看, 但是leader保留否决的权利.
实践中我们采用定期结对的方式: 我的代码给你审, 我的代码给你审, 大家互相”伤害”, 促成意识的统一.
团队代码审查: 比较大的调整, 可以在功能上线后拉会议的方式进行团队讨论, 写得好的地方供大家借鉴, 或者安利一些基础方法/套路; 写得不好的地方需要重构的地方作为后续的重构需求. 大家都是”要脸”的, 有这个代码要给大家看的压力情况下, 会自觉地注重代码的风格, 可理解性这些基础的东西.
这个过程做几轮之后, 可以保证大家至少在大的方向上不会有明显差异. 此外代码审查过程中要注意规则积累, 能够通过自动化/静态检查工具解决掉的, 坚决纳入自动化检查, 这样避免MR讨论总是限于细枝末节的讨论; 也要注重项目共识规则的积累, 这样后续类似问题指出时, 可以理直气壮地”引经据典”, 避免讨论相持不下的冲突.
何时做代码审查, 先做功能验收, 还是先代码评审? 我觉得还是应该先功能验收再做代码评审: 指望业务在功能验收阶段不做需求调整是不现实的, 甚至很多情况会有较大的调整; 代码评审基本上无关功能调整, 因此先功能验收固化下来需求后, 再对实现的方案进行评审, 如果有问题需要重构, 也应该是技术同学的责任来保障重构后的功能和验收的一致.
代码审查, 引入同行评审机制, 逼着大家多去看别人的代码, 提高每个人对于代码仓库的理解, 提高团队的冗余度, 也能够避免造轮子.
代码分层, 业务代码/基础代码分层抽离
业务代码中, 一定有一些是业务逻辑无关的逻辑, 将这些共性的组件逐步抽离, 才能逐步做厚业务开发的脚手架. 这个需要再mono-repo的基础上, 摸清主要流程后, 交给有一定的敏感度的同学来做.
对于项目目录, 和依赖, 一定要逐步剥离依赖关系, 从繁杂的依赖关系中梳理, 制定隔离规则/依赖的规则并严格执行. 从而避免代码项一团浆糊一样牵一发而动全身.
python的项目一般apps分业务模块, apps子目录禁止互调, 然后外层一个lib目录; golang的项目推荐的cmd+pkg目录结构: cmd目录下面下面都是main package, 从而天然隔离了, 另外一个pkg目录丢各种复用的库.
一些对于代码重构的常见手段这里不展开了, 实际操作中还是要有常识, 嗅探代码的坏味道. 不过也要避免掉入过早优化的陷阱. 过早地抽象, 结构化改造, 反而降低了需求变更的适应性. 一个新手或者频繁变化的需求开发, 将其限制在小范围内演化. 当其相对稳定之后, 我们可以解剖看看, 有没有什么值得利用, 或者需要调整地方.
旗帜鲜明地反对从0开始开发所谓的基础代码库, 公司内部的几次都非常失败, 没有人使用. 基础代码脱离了业务实际, 无源之水, 实际业务重不好用; 基础代码库和业务代码库地分离管理, 会导致变更管理非常麻烦. 久而久之, 大家倾向于自造轮子, 基础代码库无人问津.
业务代码: 随业务需求变更, 这里不用考虑太多的抽象逻辑, 过多的设计模式反而不利于业务代码的变更. 基础代码: 供业务代码调用的脚手架, 禁止写具体业务规则代码, 但可以实现核心业务运行机制代码. 这里注意区别.
基础代码一般要能够剥洋葱一样, 方便组合嵌套, 支持中间件模式利用. 基础代码间做接口依赖, 避免不必要的实现绑定.
一些应该放到基础代码的例子: 数据库相关, 缓存相关, 链接池相关, HTTP请求相关, 监控暴露, 等等.
比较反感所谓的架构设计, 好的架构一定是结合业务需求改出来的, 不断验证完善的. 但是一些基本的套路是可以总结利用的. 比如说引入数据队列, 引入CDC, 引入调用链路追踪等等. 这些落地在代码的抽象基础服务层面, 让相似的业务数据流用相同的办法解决.
数据库定义
业务很多是重数据的处理流程, 数据库表定义我建议是纳入项目版本管理. 从变更频繁度来说, 数据库定义是比较频繁的, 也是牵一发而动全身的, 响应读写代码都要跟着调整. 对于小的项目, 不太建议引入ORM, 增加一层抽象反而引入了理解和变更的复杂度.
数据定义管理后面在这里再展开讨论.
数据结构定义统一
我们统一通过proto来定义数据结构, 从而跳出具体语言实现的限制, 方便跨语言的交流, 也比较好做数据版本的兼容和演化. 服务接口通过gRPC来定义, 方便生成各语言的代码. 此外, 在go语言里面生成的interface, 强迫里做面向接口的编程, 也保证了实现的可替换性.
微服务的一些看法
跟据对于微服务团队的”两个披萨”原则定义: 一个服务的维护者不要超过12个人左右来看, 大部分业务团队还没到一个服务团队的规模.
大公司咱也没见识过, 但是在没有完善的服务注册/发现/监控限流等等基础设施的前提下, 盲目的拆分服务不靠谱.
我们实践中, 统一以gRPC定义有可能拆分成独立部署服务的组件, 但在实际部署的时候通过配置决定是lib调用还是接口调用. 接口调用我们跟据实际情况, 生成http/redis协议/gRPC的三种接入方式, 视情况选择.
举IP信息查询为例, 本来就是一个数据库/缓存查询的事情, 走接口的话还得再外部请求走一般, 引起的网络开销, 延迟, 和不可控性反而更麻烦. 因此我们后面做成了库依赖的方式, 直接启动加载IP数据库就好, 热更新通过库主动reload加载文件来解决.
此外, 我们服务, 大部分时间都只是两实例, 由于gRPC的单链接复用机制, 以及我们gRPC相关中间件的积累匮乏, 简单的基于链接的负载均衡, 容易导致负载不平衡. 因此我们线上接口调用绝大部分实现还是走redis协议的裸TCP传输, 并利用haproxy等成熟已验证的中间件来控制流量.
测试与覆盖率
前面还没有提到测试. 事实上曾经的一些项目, 有专门的测试团队介入, 但更多的是功能验收阶段, 不能避免糟糕的设计问题. 代码重构过程中, 需要对于核心业务逻辑建立起一些基本的测试, 来保障重构后/前的逻辑是一致的. 对于业务代码可以少一些随代码的单元测试, 因为维护起来成本较高, 可以通过线上灰度发布的方式来保障; 基础代码则更为重要, 要充分单元测试, 考虑到各种情况, 要TDD的态度来做. 这里要避免对于实现的测试, 写的太多导致代码重构束缚太多, 无法展开手脚. 实际上, 有些历史代码的单元测试写得太多, 然而并没有什么卵用, 索性直接一删了事. 完善的回归测试, 还是需要成熟的基础设施的基础上来进行, 否则构建模拟请求验证结果的代码较多, 改动频繁, 反而成为的拖慢开发的瓶颈.
测试覆盖率是一个比较片面的指标, 可以作为一个参考, 但不能片面追求. 既有代码就不用管了, 也没有必要对于已上线的功能补单元测试. 新的功能开发, 或者既有功能的改造尝试逐步引入TDD的方式, 逐步提高测试覆盖率.
简化分支, 避免版本管理
很多人推崇git-flow的分支管理规范. 但我对此不太感冒, 一直觉得带来麻烦较多, 好处不很显著.
我主要推行一个最简单的规则: master是线上分支, 只允许通过MR (merge request) 合并到master, 灰度/测试分支就是对应的开发分支.
允许一个develop分支, 产生很多麻烦的问题, 导致最后大家都在develop分支开发, 随意改动并引入BUG到线上.
同样地理由, 不鼓励版本化地发布管理. 版本化对于客户端或者App类发布场景也许更加适合. 对于web/后端服务, 过多地版本维护负担沉重, 应该以总是向前兼容, 小步修改地方式进行迭代. 多个功能如果走统一版本, 意味着需要以同发布, 一个延迟其他跟着延迟, 以及会有集成的问题. 或者换句话说, 一次合并到master的commit都是一个新的版本. 因此我们强调功能一定是可单独发布, 不允许强互相依赖的功能一起发布.
变更管理
前面我们也涉及到了代码的变更管理. 这里主要想说部署阶段的事情, 代码写好只是婴儿出生, 更重要的在于线上运维调整. 很多混乱的根源在于线上发布无序, 依赖不清晰.
此外, 出现故障的时候, 出了问题, 不清楚谁何时改了啥导致, 完全靠问, 遇上不自觉地同学还会刻意隐瞒操作. 这对于我们问题复盘带来很大的难度. 对外和业务交代的时候也不能把事故流程写仔细. 因此任何的变更都要能够记录下来.
变更四原则:
- 任何变更有记录
- 最细粒度变更, 鼓励多次小步变更
- 任何变更能保证回退至少一次
- 变更自动化, 努力减少变更的人工参与度
曾经我也恨冗余的工作流, 但是随着系统的越来越复杂, 人员越来越多, 工作流是必要的, 通过牺牲一定的效率来杜绝掉一些低级的错误, 保障过程的可见性.
服务登记
其实这个和”代码考古”是一个工作. 不过这里不一定是代码层面的依赖发掘. 这里服务不单止自己代码开发的程序, 还有各种线上用到的服务, 机器, 数据库, web服务器, 中间件等等.
说个不是笑话的事例: 曾今一个同学半年的工作职责就是把一套系统从托管机房迁移上云; 中间除了需要保证业务不中断做迁移带来的技术挑战外, 更大的麻烦是太多陈年未记录的服务器配置修改, 第三方服务部署等等, 需要人工一个个去踩坑填坑.
我认为, 一个部署自动化的标准就是: 假设现有环境全丢了, 我立刻重新构建一个一样的业务系统, 需要多少人工参与环节. 机器规格, 数据库规格, 配置, 等等, 这些修改很多时候都缺少记录.
服务调用或者数据依赖关系写清楚, 将线上部署的”地图”绘制出来.
完全人工的登记, 实际操作和登记流程分离, 很容易产生与实际环境的不符. 因此需要线上自动化的登记措施, 或者强制配置决定部署. 实际中我们主要通过ansible项目来进行服务发布变更. 以后有机会尝试下配置决定所有线上设施的工具.
功能发布 / 服务替换
对于一个有一定用户的成熟业务, 重构一个功能并发布, 相当于开着汽车换轮子. 难度高, 审慎.
套路: 先架新桥, 再拆旧桥, 逐步引流, 灰度发布. 将服务/数据依赖关系画出来, 正常来说是一个DAG, 依赖树, 从根节点网上游不断嫁接新桥逐个替换, 实践中也是挺有意思的过程.
变更步骤需明确, 写清楚一二三四, 相关负责人以及检查点, 每一步完成确认后进行下一步, 并保证可回退. 此类较大的线上变更再怎么强调做预案都不过分, 重点在于各端的充分沟通. 太多的线上事故由于未协调的操作导致冲突故障.
对于代码修改的要求, 始终做向前兼容的改动, 确保可以随时回滚. 一次性不可回滚的发布, 不亚于赌博.
对于业务核心模块, 非常重要的接口类服务, 加一个接口网关, 对于请求流量拷贝到新服务. 如果是接口重构类的接口, 需要比较差异, 并分析原因.
在入口跟据流量比例, 或者按照业务性质逐步引流到新的模块, 灰度验证, 密切关注指标, 从而保证即便有问题也能够控制在较小的范围.
实践中采用nginx的upstream, 或者haproxy来分配流量比例.
配置变更
很多时候一些业务逻辑开关, 为了方便调整, 通过配置文件加载的方式来管理. 由于配置里面有很多密码等涉密信息, 不方便走项目变更管理. 很多时候就直接线上修改了, 这里就产生了不可追踪的线上变更.
所以实践做法, 把服务配置文件统一走一个项目管理, 涉及密码的通过环境变量替换掉. 避免线上改配置的情况. 发布时候有两个版本记录, 一个是程序构建版本, 一个是加载的配置版本.
后期我们调研并部分项目采用了apollo做配置中心.
动态配置
很多时候大家希望动态配置开关, 就是程序不重启直接改加载配置参数. 这个特性是吸引人的, 但是对于程序开发引入很多不必要的复杂性, 比如配置内容读写的保护问题以及潜在的性能损失等等. 因此, 实践中没有太多的动态配置开关模式, 修改配置重启服务生效更为简单容易理解. 也更容易将配置变更记录下来.
一些调试需要的动态配置, 如改LOG登记等, 通过程序暴露HTTP端口来控制.
另外一些太重的程序, 比如说算法服务, 启动需要加载预处理数据太多, 又由于资源限制不能开太多实例. 单个实例较长时间的不可用容易造成其他实例负载偏高, 对于整体稳定性不利. 这种情况下, 我们也会通过数据文件热加载的方式来保证一致.
会导致业务变更的动态配置, 如数据模型更新, IP库更新等, 也要做好变更管理.
定时任务
很多项目的定时任务使用cron来管理任务, 任务的依赖关系通过时间错开假定, 缺少失败重试及告警等基础功能. 我们将crontab统一替换到调度系统, 解决了任务依赖, 并发控制, 失败重试及通知等功能. 并将定时任务的变更做到有历史记录可查.
量化指标
一个技术团队最糟糕的状态, 凡事儿拍脑袋, 先功能做完了, 不评估有什么性能问题, 能抗住多大的量级. 一般就是资源不够了, 升级机器资源, 知道有一天加机器也不能解决问题, 或者老板对着成本账单来找你的时候. 所以一定要有量化指标和成本意识.
其实对于初创业务, 业务无关的技术层面量化指标不能算是优先级很高的事情, 因为绝大部分项目在没有遇到性能问题的时候就已经凉凉了. 曾今一个内部公司团队分享性能优化的讨论会上, 有些同学幽幽的眼神吐槽到, 我们服务最大的性能问题就是没人用. 当业务步入稳健期后, 这些东西就要未雨绸缪, 来应对可能更大的增长.
这里主要探讨一些偏技术测得量化指标管理, 和性能优化有很大的关系.
消防员角色
一般被拉入一个项目, 初期的征兆就是线上访问不稳定, 机器/数据库告警连连, 业务方发现并反馈问题多多. 这个时候老板把重担交给你, 一定要有背锅意识.
初期”救火” 就不用太讲究方法论, 追求速度快, 尽可能地把系统故障带来地业务损失降到最小.
屏蔽/降级非核心业务逻辑请求, 将资源保障给最核心业务流程. 不需要实施响应地数据队列可以先dump出来供后续恢复, 缓解线上处理压力.
但是每次救火后要做原因分析, “消防检查”, 避免类似的故障再出现.
在架构短期内改不动的情况下, 争取通过增加资源来保障服务地可用稳定, 给重构争取时间.
但是消防的重点在于日常排查, 而不是着火时候的英雄主义.
服务注册, 监控指标
这里服务注册不同于服务登记, 主要是偏线上运行实例的管理, 不涉及依赖关系等文档. 前期使用zookeeper做自定义的服务注册, 后续改成了consul的方式.
监控告警, 有的项目完全没有. 有些项目做了, 但是做法各不一样: 有的是push到graphite; 有的是暴露一个接口, 另外脚本去抓取写告警规则, 有的是业务逻辑代码层面做监控告警规则. 方法各异.
在接手项目的过程中, 逐步统一到基于服务注册的拉取模式: 服务启动后 统一http的方式暴露指标接口供抓取, 用influxdb存储, 后续统一换成prometheus, grafana展示并设置告警规则.
业务层面的告警, 避免在代码层面手动做, 而是应该基于业务数据流单独进行.
使用grafana的annotation功能, 结合发布记录, 可以很直观的发现监控指标变化和发布的关联关系.
监控指标再强调也不为过, 是我们观察服务的唯一途径 (不要指望靠盯服务日志).
通过业务/非业务指标的暴露梳理, 也能够推动团队对于维护服务的量化意识.
告警处理
结合各来源的数据指标, 需要总结业务各环节的告警逻辑, 将问题尽早发现并及时处理. 避免业务出问题了技术还一脸蒙圈的窘态.
告警太多导致大家习以为常: 除了亲自像讨债的一样向每个同学跟进处理结果, 对于现阶段不予处理的告警屏蔽通知.
故障报告
故障不可避免, 出了问题, 我们还是避免简单粗暴的分人惩罚. 但是要求主要人员负责去写故障报告. 故障影响范围分析, 估算成业务流水损失. 倒逼其做好变更管理, 和监控指标.
成本意识
作为技术更要关注服务器成本, 这对于老板来说更为心痛.
我们有个业务功能, 但看业务流水是赚的, 但是算上引发的额外流量和机器成本, 反而亏得很惨. 这块儿需要技术领导有成本担当意识. 业务同学按照流水提成, 才不会管你服务器成本几多. 之后我们推动技术成本业务分摊后, 那些技术开销大, 实际上不挣钱的功能模块就砍掉了.
量化指标完善到一定程度, 可以对于某个功能进行费用统计供老板决策. 在需求评估的功能确定后, 除了给出开发预估, 还能给出每个功能模块的单位业务流量成本, 决策应该做到何种程度. 这对于尤其种数据的业务更为重要.
对于设计上, 尽量避免不必要的初始开销 (upfront cost), 尽量能够按照业务规模扩容降容. 从而在保证一定的服务可靠性的程度上尽量节约成本.
要做经济适用型架构设计.
流程优化
上面更多的是从技术的角度的一些”硬措施”, 然而很多时候, 更需要的是一些管理沟通上的”软方法”更为重要, 也有效.
一个相对比较糟糕的现状, 一定是日积月累组织结构的结果. 这里乱记一些组织管理上的策略.
技术团队建设
技术团队很多时候业务驱动, 纯粹的乙方角色, 偏被动. 技术同学发展途径两种: 业务敏感, 能和业务打成一片, 混成业务专家, 不过这种人可遇不可求; 技术上有追求, 或者说能学到新东西, 目标技术专家, 逐步提高技术自豪感, 这个是大部分做业务开发同学的诉求, 那就要通过需求分配上来避免, 比如业务需求搭配新技术调研, 挑战性的重构任务, 性能优化任务, 等等, 避免枯燥. 看菜下饭, 看人安排任务, 努力让每个同学保持再一个相对比较有劲的状态.
技术团队内部需要激活工作积极性, 对外也要重建信心: 在比较糟糕的情况下, 业务对于技术团队缺少信心, 很多时候, 即便是合作伙伴的问题, 业务也会第一时间怀疑我们自家是否除了问题. 这种憋屈, 只能忍着, 通过不断靠谱的响应, 来重建业务对于技术团队的信心. 这种情况也非常适合我们通过”耻感教育”, 来去努力提高技术同学的技术自尊心, 并提高大家对于交付的质量意识.
业务需求方沟通
很多时候并不是技术同学没有重构意识, 看不到风险/瓶颈, 而是业务驱动开发的模式导致技术同学缓不过气来, 只能退而求其次, 功能先上线再说, 已知问题以后有空再说. 不过这”以后有空”永远是个空头支票.
另外和业务方解释技术需要实践重构这个事情也很困难, 你和业务说我们需要花时间重构, 在他们的理解就是需求的挂起, 自然是一百个不乐意. 所以完全停下来需求上线做重构是不现实的, 这里要考验技术负责人的沟通水平.
我的策略是, 将大的调整时间拉长, 拆解多个小目标, 忍住耐心, 分轻重缓急. 每次迭代, 技术同学需求要做, 但是技术优化需求也一定要做. 一步一步解决技术债务.
另外一个情况就是业务需求做乏味了, 尤其是内部系统, 翻来覆去就那几样. 不能让然产生兴奋的需求. 对于接业务需求, 要避免乙方的角色, 想办法混成甲方, 对于业务的需求也逐步提高门槛, 倒逼需求质量; 或者从业务中常见场景提炼, 通过技术立项, 做一些既有业务价值 (从而保障老板点头), 技术上又有挑战的事情, 通过有意思的需求来激活大家的工作积极性.
组织结构优化
这就是更高一个层面的考量了.
“康威法则”: 设计一定体现组织结构. 一直依赖希望推动技术团队独立业务团队, 但是实际情况是每个业务组都想要自己的人, 更看重业务团队独立作战的灵活性, 所以改成技术团队虚线汇报公司技术负责人, 实线汇报业务负责人. 久而久之, 这个虚线也就不存在了.
遇到太多的情况, 同样的业务, 公司内部需要两个团队赛马, 因此都要技术团队, 大家做一样的事情, 但是大家分头去做, 以邻为壑, 更谈不上交流共享了. 等到一个团队不行了要裁撤合并的时候, 技术倒霉了, 两个类似又不同的系统要做合并.
所以现在”中台”这个词火了起来, 大家都希望公司有个牛逼的中台, 各业务支持有力, 搭建的很快, 技术又有积累, 至于实践中要具体怎么操作, 那就是后话了. 希望能够在后续工作中能找到更好的实践并记录输出.
总结
技术做的越好, 一般需要的人就越少, 久而久之就越没有存在感, 因为没有故障, 性能问题, 按业务流量需要扩容, 大家把这些当作习以为常不需特别关注的事情. 填坑不挖坑, 技术人要有这份自觉, 把这些老板/业务关注不到的地方的工作做细致, 做完善, 并努力提高自己的专业水平和自我要求.