当技术重构遇上DDD,如何实现业务、技术双赢?
全文9000字,预计阅读时间21分钟
一、困境:项目背景
爱番番沟通基于百度商桥快速完成了产品功能和技术架构的从无到有,但同时也继承了百度商桥历史功能繁杂、技术架构陈旧的缺点。为了能更好地服务于爱番番沟通将来的产品演进,提高产研能效,需要从实际问题出发,聚焦主要矛盾,对产品架构和业务架构进行重构。
为了更好的理解本文内容,以下是必要的名词解释:
名词 | 解释 |
访客 | 浏览客户网站的网民 |
C端 | 浏览客户网站网民看到的咨询图标、邀请框、沟通窗口、留言板 |
B端 | 客服接待网民咨询的工作台,即客服业务系统的客户端、手机端、网页端 |
进站 | 网民打开客户的网站进行浏览 |
离站 | 网民关闭了客户的网站 |
DDD | Domain Driven Design,领域驱动设计,一种旨在解决复杂系统的设计方法论 |
1.1 爱番番沟通是什么?
爱番番沟通是连接访客和商家的在线咨询工具。一方面访客可以随时随地咨询,缩短访客获取服务的途径,另一方面商家也可以快速响应并提供服务。同时在推广场景中,商家还可以根据访客的咨询内容反哺回上游广告通路,优化投放模型,提升营销转化效果。
1.2 为什么要重构?
百度商桥经历了几次不同的产品定位和多年版本迭代,产研团队也换了几波人。客户问题较多,架构长期缺乏系统性治理。给产研团队带来多个层面的掣肘:
团队内对产品的主要业务逻辑没有知识储备。经常需要研发去翻阅项目代码东拼西凑出现有逻辑的大致模样。 客户反馈问题数量居高不下,典型问题如:
访客进站识别不及时,客服感知不到访客已进站。访客离站识别不及时,容易误导客服向离站的访客继续发起沟通,引发沟通不畅的表象; 访客的广告相关信息(来源、搜索词、关键词)获取不及时、不完整; 访客全生命周期内的行为数据有概率性延迟和缺失; 商家欢迎语、自动回复发送顺序错乱,不触发发送等; 服务稳定性引起的登录失败,消费发送失败、移动端消息提醒不及时等; 还有一部分客户问题属于新需求范畴,比如咨询组件样式灵活定制、支持离线沟通。
二、反思:定义问题和挑战
面对当前困境,整个产研团队都意识到了需要尽快做出改变。透过现象找本质,上述现象背后的关键问题是什么呢?又会面临哪些挑战呢?
2.1 定义问题
通过进一步分析问题的根本原因,可以把问题分为以下几类:
【产品层面】产品方向及定位不明确,功能层级及分类不清晰
产品演进方向不清晰,业务领域主次不清,各模块的业务主路径不清晰。平时开发都是堆砌功能,导致不少业务场景存在使用体验的卡点; 由于历史原因系统支持的角色冗余且复杂,既有平台型角色比如支持百度顾问和商家直接沟通。又有B端其他角色比如支持销售直接查看线索; 从PC时代到移动时代,但是产品还保留着一些历史兼容的痕迹。比如常用语是按照PC和移动进行一级分类,站点样式类型只能设置一个端。
旧版客户端界面示例
【架构层面】客户端架构多年未演进,功能迭代难以为继
客户端仅支持Windows系统且架构一直未演进,技术栈基于C++,和团队主要技术栈偏离,只能艰难维护,无力承接新功能需求。迫切需要演进为能跨平台、主流的、前端技术栈; 访客侧前端还未做到前后端分离架构,体验和开发效率大打折扣。
【架构层面】服务端架构的基础沟通层待演进
沟通协议层作为沟通产品非常重要的一环,还存在架构方面的不足:
多种网络连接协议下的稳定性需提高; 和不同端的消息发送性能需提高。
【架构层面】服务端架构的业务层待演进
业务层包含 20+ 服务模块,主要的业务逻辑采用共享库的方式维护,导致模块边界不清,数据链路混乱,功能重叠耦合严重,迫切需要演进为主流微服务架构。
模块内职责不够内聚,模块间调用关系耦合高; 同样的数据存在多份存储,数据一致性存在问题; 数据流的同步异步传输链路混乱。
【架构层面】整体服务端架构自运维成本高,可维护性很低
历史遗留系统中需要运维多种自运维中间件,导致团队不能聚焦业务功能开发。既降低了研发生产力,也给系统稳定性带来巨大挑战。
自运维了反向代理 Nginx 集群、Zookeeper 集群、Storm 集群、Kafka 集群、Solr 集群、Prometheus 集群; 离部门的主服务端集群面向云原生的服务治理架构还有不小差距。
【组织层面】产研团队整体对业务的理解不够且未拉齐
业务架构和研发架构长期脱钩,导致团队对大到沟通行业小到具体某个模块的领域知识沉淀缺乏,迫切需要在产研层面拉齐现有认知; 在团队达成共识的基础上将来才能形成随产品快速演进从而快速迭代领域认知的正向循环。
2.2 认清挑战
归因清楚问题后,重构的方向逐渐清晰起来。但执行落地阶段也会面临着业务演进压力,原架构基础薄弱,资源短缺等挑战。
架构陈旧,代码里有不少隐蔽的『坑』
从以往经历看,有时候一个很小的改动,看起来很有把握的一次上线也可能造成客户问题。一方面代码中缺乏设计的地方多,另一方面整体回归测试覆盖不全。组内自嘲这种状态为『每一行代码都刚刚好』,不能多也不能少。
重构和业务演进既要又要
这个挑战是大部分团队都会遇到的,业务不可能停止演进等待技术重构。如何能在不影响已有业务且保证部分高优业务需求正常迭代的情况下进行重构是必须要回答的问题。
不能仅仅是重构,客户可感知的体验要更好
涉及客户端架构升级,必然会带来一些新的用户体验,需要管理好存量用户的预期。本次重构范围大,产品质量不下降既是要求也是挑战。
产研团队较新,对原有业务功能缺乏足够了解
业务研发团队很依赖领域专家的业务知识指导,子领域间和模块间的职责和边界划分,数据归属等理解需要建立在业务理解的基础上。这些对现有团队是个不小的挑战。
因此,抓主要矛盾,分阶段小步快跑是本次重构的基调。
三、纾困:解决问题
仅仅从技术层面做重构只能解决眼前的技术问题,随着业务快速迭代,纯技术重构的成果很容易消失殆尽。考虑到需要对业务和技术层面双管齐下做出改变,在现有复杂业务基础上仍能保持高效的产研交付效率,加上隔壁兄弟团队之前在线索管家产品已经收获了 DDD 改造的收益,因此本次技术重构决定结合 DDD 来做,从产品到技术来一次认知升级、架构升级。
3.1 定位:确定产品方向及核心痛点
产品定位及差异价值
产品定位:选择『不做什么』更加重要
聚焦在售前接待场景,帮助商家获取联系方式,不做售后服务场景; 聚焦在广告营销场景,帮助广告主接待推广流量并优化效果; 由于是 ToB SaaS 模式,所以暂时聚焦企业客户需求,不做平台型针对企业的上层需求。
产品使用角色:谁是我们的用户?
聚焦在B端客服角色。剥离其他角色相关功能,比如跟进线索的名片功能归到线索管家模块(销售角色),反哺功能归到 oCPC 反哺模块(SEM角色)。
差异化价值:客户为什么会选择我们?
全链路闭环:从推广开始到访客进站、对话、留资,直至标记会话反馈oCPC目标,全程无缝衔接; 与线索管家结合:智能识别会话和留言板中的线索信息,自动沉淀至线索管家,有效节省线索梳理工作; 智能营销:访客意图智能分析识别,千人千话引导访客开口留资; 多端共用:支持 Web、App、PC 端同时使用,随时随地实现沟通。
3.2 分析:识别核心领域和模块,拆解业务逻辑
3.2.1 事件风暴:剖析流程和对齐认知的好帮手
3.2.2 边界是合作的基础:划分领域和模块,形成统一语言
访客域和客服域属于核心域比较自然,同时作为底层的基础能力,协议连接域包括tcp、websocket、http、long polling协议,协议报文格式,连接状态维护等也应该是核心域。其次会话域也是核心域,互发消息才算进入真正沟通,会话内容里的意图表达和留资才是沟通的主要目的; 核心域的策略是围绕产品价值,重点投入资源。尽可能把非核心功能从核心域剥离,警惕容易引起团队失焦的投入。
数据分析域是必要的功能但目前还不是重点,线索域对沟通来说是后链路必经环节,但应该更多利用爱番番线索管家的能力。广告域包含访客推广信息解析,会话效果反哺,照理是核心能力。但这里划为支持域是因为关键的能力在搜索团队已提供,沟通团队做好数据接入和数据供给工作; 支撑域的策略是尽可能以较少资源建设必要能力。当然,随着业务的发展支撑域也可能在未来变成核心域。
账号权限功能是大多数系统的通用能力。访客场景属于ToC场景,会遇到黑产流量攻击,包括访客进站和访客发送消息需要引入风控反作弊能力。爱番番沟通主要借助了爱番番策略团队和厂内安全部的能力; 通用域的策略是尽可能不亲自建设系统,借助外部能力快速完成能力建设。
架构目标及设计要点
根据流量南北向把各种服务按照职责类别分为多个层次,用户界面、接入网关、业务前后台、沟通协议连接等5层由沟通团队建设维护,底下基础服务和存储层主要借助基础技术能力。分层建设能够定义服务不同等级、高效使用团队研发资源、承接不同流量类型(实际用户流量、后台用户流量、异步调用流量、定时任务流量等)、简化请求涉及的数据链路、根据层次不同建设非功能性需求(技术栈选择、熔断限流、弹性伸缩等)。 技术架构匹配业务架构。服务模块边界符合业务边界。核心服务内需设计领域模型,围绕领域层和应用层构建业务逻辑,搭建DDD四层分层架构,做到领域模型和技术细节分离,不稳定实现依赖稳定实现。 符合典型微服务架构。服务职责内聚,服务和数据一体。数据归服务私有,服务间不共享业务逻辑,服务间通过API或领域事件进行协作。 数据架构合理。尽可能采用数据最终一致性策略。每种数据非必要不多处存储,多处存储须有最终一致性方案保证。涉及nosql类存储如Redis、HBase、ES(Elastic Search)时,防止大key造成分片不均,业务数据按需进行分库分表存储。
3.4.1 落地真正的微服务架构
有些功能粒度太细,徒增维护成本,可以合并。 某些类似功能散落在多个服务,比如5个模块都有提供访客相关信息查询,可以合并。 有些服务随着老客户端的升级,功能改造后更合适合并到其他服务,原服务可以下线。 反向代理层职责划分不合理导致服务集群太多,绝大部分可以迁移至公司级的 BFE 进群,少数包含很多 lua 逻辑 Nginx 集群暂时保留,但可以合并。
访客广告信息解析服务。广告信息对于客服刻画访客画像,理解访客非常重要。但之前的解析逻辑散落在多个模块且实现不统一,解析准确率不高,没有足够的补偿策略保证必要的解析成功率。 机器人智能回复服务。这也是产品定位的一个差异化价值。为了让客服更高效接待访客,引导访客多留资,这块的产品演进越来越多,复杂度也随着加大。 线索服务。这里的线索服务是爱番番沟通和线索管家产品的边界,主要是针对会话内容或者留言内容提取联系方式,然后通过接口或事件的方式流转到线索管家,同时也要形成咨询到线索的闭环数据。
通过公共库( 即 java 的 jar 包 )共享业务逻辑。同一段业务代码被多个业务服务依赖,既降低了代码可维护性,也降低了服务的可测试性。 通过缓存( Redis )传递数据。一个 redis key 经常既有多个服务在写入,也有多个服务在读取。 通过 DB 共享数据,直接读取属于其他服务职责的数据表。
发布/订阅模式:上游服务利用消息队列把相关数据以消息为载体发,下游服务订阅该消息并做相应的持久化。整个沟通服务端在大量使用这种方法,也是服务解耦的一大利器。 CDC 模式( Change Data Capture ):简单说就是通过监听 MySQL 的binlog 感知到上游服务的数据变化(包括新增、更新、删除),解析日志并做一些处理(比如关联表查询等)后发送到消息队列,下游按需订阅处理。
3.4.2 数据链路治理
storm 拓扑设计不合理,拓扑节点职责不清; 拓扑节点中存在大量的业务逻辑,普遍利用 redis 传递数据,redis 键设计混乱,可维护性很差; storm 集群是几年前引入的,版本低,一直没升级。
去除这个集中式的计算集群,按业务场景梳理各自数据流,避免互相干扰。让对应业务服务模块承接业务逻辑,如需提高业务响应可通过缓存集群加速; 服务模块间尽可能通过异步方式( kafka 消息队列 )传递数据,目前消息队列也能达到近实时效果,同时增强消息队列的灾备功能和订阅情况监控; 访客一段时间不说话需要自动回复等延时场景通过延时任务的方案解决; redis key 重新梳理,优化大 key( 一个 key 承载的内容特别大,比如一个key 就包含全系统访客的部分信息,这样的 key 设计显然太大 ),尽量不跨服务模块直接操作 redis。
3.4.3 沟通协议优化
为什么要做协议优化?
现有协议缺乏鲁棒性,从协议层面埋藏着隐患。一个事件(如进站、建立沟通、离站)需要多个包来完成交互,如果一个访客操作频繁,访客状态也会频繁做变更,很容易出错。 富客户端模式,端上维护了过多的状态信息,过度依赖推送包的顺序,而且缺乏容错、自恢复恢复机制,容易出现访客不展示,消息不上屏等问题。
如何优化?
通知模块采用分布式锁控制并发,并为报文增加SeqId来确认早晚顺序,为客户端提供判断依据。 优化状态协议,简化掉动作通知类报文,采用以访客状态为主的报文,如下图所示,将动作报文简化掉,只保留状态报文,报文数量减少约 60%,降低客户端处理复杂度,减小出错概率。 客户端侧,由 socket 长连接改为为 http + socket 推拉结合的方式,当断网重连、或者报文丢失、错乱时,则客户端主动拉取最新状态,彻底接解决访客状态不对,消息不上屏等问题。
猜你想问:
小结
访客、客服、会话管理模块的 DDD 改造。 由贫血模型改为富血模型,通过状态机控制状态变更。 客户端请求以 http 为主,同步得到返回值,降低出错概率。socket 主要用于给端上的通知。 协议包简化, 以访客状态维度进行交互,极大减少包的数量。
3.4.4 去除自运维中间件
集群改造下线
Zookeeper 集群:改造前主要用来做业务配置中心,迁移到 k8s 更友好的ConfigMap( 由基础技术团队运维 ); Nginx 集群:改造前有好几套反向代理集群,其中既有路由转发逻辑,也有业务逻辑。业务逻辑下沉至对应的 gateway 服务,由团队维护。路由转发逻辑迁移至 bfe 集群,由基础技术团队统一运维; Storm 集群:逻辑改造,下线。细节上面已交代; Solr 集群:下线,相应查询逻辑改造迁移至 ES 集群。
集群迁移
为什么选择 Electron ?
开源的核心扩展比较容易。 界面定制性强,原则上只要是 Web 能做的它都能做。 是目前最廉价的跨平台技术方案,HTML + JS 的技术储备,而且有海量的现存 UI 库。 相对其他跨平台方案( 如 QT GTK+ 等 ),更稳定,bug 少, 只要浏览器跑起来了,问题不会太多 。 方便拓展,可以直接嵌入现有 web 页面。
Electron 系统架构
开发中遇到的问题
什么是插件化架构
插件化架构优势
爱番番插件化现状
插件化架构方案 提供两种接入方式:JS-SDK 接入、Webview 方式嵌入。 第三方插件与爱番番客户端存在两种通信机制:事件广播、实例注入。 番番客户端插件分类:左侧菜单插件、会话工具栏插件、会话侧边栏插件。 插件配置文件说明:
{
"version":"0.0.1", // 版本号
"id":"demo-name", // 绑定事件ID
"name":"组件名称", // 插件名称
"viewUrl":"", // 如果是菜单插件需要提供webview地址
"target":"toolbar", // menuList——菜单插件、toolbarList——沟通区插件、infoList——右侧工具栏插件
"dependent": {
"method": [],
"version":"1.0.6" // 依赖客户端版本
}
}
按照 DDD 原则,来定义菜单模块并抽象功能层级; 结构对比老版层级更加清晰,功能扩展性更强; 容器变更并重新定义,释放核心会话功能的区域; 三端( Mac + Win + Web )合一,共用一套产品设计,操作灵活便捷。
就像敏捷迭代的精髓不是看交付过程的单点效率,而是看发现需求到需求上线的整体效率。这也是通过 DDD 带给这次技术重构的最大价值。经过需求和业务的分析、设计、实现等环节,让产品、设计、研发整个团队的磨合和对业务的理解提升到新的高度,辅助以合理的技术架构,能整体提升需求交付效率。
直接体现是更少的人支撑更大的产品范围。以前技术研发 12 人,现在 7人; 间接体现是代码维护成本大大降低,服务模块数量和团队人数比例协调,模块职责和协作关系明确,接口设计质量高,代码规范度高,新人上手速度快。
服务模块数量和团队人数比例协调; 模块职责和协作关系明确; 业务数据流流转链路清晰; 项目代码结构规范、易懂。新人上手速度快; 接口文档在线化。
更多沟通格式:已和业务系统解耦,很容易增加沟通内容格式如视频、语音等; 更多连接形式:目前已支持包括 http、tcp、websocket、long polling 等推拉形式协议,几乎满足了绝大部分场景; ;更多业务类型接入:基础沟通能力已有开放能力,可利用 api 方式低成本接入 沟通功能的持续演进:比如更智能化、和线索管家更无缝集成、更强的风控能力,这些需求可按需建设相应业务模块,独立演进。
产研团队聚焦到创造业务价值,从能解决客户问题视角开展日常工作; 产研协作效率更顺畅,基于统一语言沟通需求和设计; 在业务迭代过程中沉淀了领域知识。
技术问题的答案往往要从业务中寻找答案,理解业务是开展技术的前提。不同业务带来不同的技术诉求,适配的技术才是最好的,也是先进的; 经过重构的架构能适配当前业务发展,研发能把绝大部分精力放在业务实现上,屏蔽了日常开发的很多噪音。
通过本次重构提高了每个成员对沟通业务的全方位熟悉。既有自身业务的全貌,也有行业友商的演进现状,对未来演进方向有了对齐; 在了解技术架构的来龙去脉和全貌基础上,让每个业务研发能聚焦建设自己负责的模块。通过 DDD 实践提升自己的应用架构水平,提供了技术进阶的新方向,发挥出模块负责人的主观能动性。
飞邪:架构师,擅长通过微服务架构和 DDD 落地复杂系统; 坚果:产品经理,擅长 ToB SaaS 及广告产品; 甯甯:一个和商桥、在线沟通有不解之缘的产品经理; 小麦:资深前端工程师,在光速演进的前端领域内苦苦挣扎的 FE; flyme: 资深研发工程师,擅长通过改进技术方案来应对复杂多变的业务场景。