“风语者客服+”是针对中小型企业推出的客服SaaS,节约了企业自建客服系统所需的巨大成本。为了给企业提供稳定可靠且优质的服务,我们在整体架构上费尽心思。虽然不尽完美,希望借此抛砖引玉,互相切磋。
前言
”Look deep into nature, and then you will understand everything better.“ -- Albert Einstein
我国传统文化上,要做成一件事,讲究三个方面:明道,优术,取势。在软件架构设计方面而言,也是类似的道理:遵循自然规律以明确大的方向,使用优秀的实操战术,再根据实际情况落地。
这是个快餐年代,几乎所有人都只做一件事 -“取势”。 几乎没有多少人会去理解一个Servlet的工作原理,去理解一次HTTP请求的完整流程,因为有超多框架帮你屏蔽了这里的细节。询问一个人会什么技术,回答也往往是我会Hibernate、Spring、Ibatis、会PullToRefresh组件、会使用SDWebimage。不过这些框架(Framework)其实并不是软件架构。软件架构是一所有生命力的房子,而这些框架只是大一点的板砖。
因为笔者水平有限,这里只提一些普遍准则,也就是”正确的废话“,以飨视听。不会深入到实操战术上,比如怎么用Spring实施MVC架构,怎么使用Maven管理依赖,Redis的常用操作,怎么搭建一个负载均衡的集群,如何使用阿里巴巴的Dubbo框架进行服务化等等。如果大家有兴趣,可以自行搜索,有很多优秀的文章可供参考。
不幸的“程序猿”和“程序媛”各有各的痛苦,幸福的程序员都是相似的。其实说幸福有点言过其实,下面就说说怎么让他们不那么痛苦。
一. 很好的模块化支持
“At the bottom of every person's dependency, there is always pain, Discovering the pain and healing it is an essential step in ending dependency.” --Chris Prentiss
他们都在一个相对稳定的软件架构里编码,自己的代码不会依赖很多模块,不会因为自己微小的改动造成全局的失败。正如"1984"中的老大哥说的,Ignorance is strength(”对外界的“无知就是一种力量). 任何一个模块都不能有太强的存在感。
曾经在一个大型互联网公司里面,任何人只要用到一个核心模块的功能,就必须依赖一个部署在某远程服务器的库,而且还有IP限制,只能把代码部署到指定网段才能运行起来。导致基本上没法在本地进行单元测试或者简单调试。这个核心库的存在感太强,就成了开发的瓶颈,严重的降低了生产力和码农的幸福程度。
在“风语者客服+”的架构中,每个码农都可以很方便的在本地把服务启动起来,一分钟up and running,随便做一些改动就可以立竿见影的看到效果。这里要归功于几个东西:
1. Git代码管理
在团队作战中,每个程序员可以取下来完整的最新代码库,也可以在本地分支上尽情挥毫泼墨,而不担心影响别人的工作。也可以把本地修改先stash起来,review一下别人的代码,再unstash恢复回来。要想提高团队效率,代码仓库管理建议尽快迁移到Git上。
2. Maven、Gradle、Cocopods等依赖管理
Maven是一个管理依赖(Dependency)的工具,现在在Java社区应该是比较普及的,无法想象现在还有团队直接拷贝jar包来管理依赖。虽然早期没有Maven的时候,都是拷贝jar包这么过来的,碰到的问题也是显而易见的,依赖的jar包作者改了某个bug,没能及时传导到调用方。多个调用方使用不一致的jar包,导致各种奇异bug。对应的在安卓社区,使用gradle的比较多,iOS的Objective-C开发中,多采用CocoaPods。
二. 高内聚,低耦合
He should focus on his knitting, not trying to do everything. Do one thing well.-- Steve Jobs
"Do one thing well"其实不算是老乔的专利,UNIX哲学和Google哲学都提倡这一点。这句话本身不完全对,比如对于一个商人,如果只会Do one thing well,那他无法在市场中存活,但是在工程师中却是万般推崇的哲学。
我们可以期望一个人具备一百种技能,然而对一个工具只期望它把一个需求解决好解决彻底,对于实现工具的一个类,一个方法,更是如此。但是,实际经验中,我们经常看到一个5000行以上代码的类,活像一个巨人版的瑞士军刀,什么都能做,但是什么都做不好。这就是”Separation of Duty"没有做好的典范。
在风语者”客服+“对外提供的SDK和API中,我们也提倡同样的思想,力争把App使用”客服+“SDK的门槛降到最低,每个API都能自言其一,而且API直接没有时序上的依赖关系。内部各个模块的开发,也秉承同样的责任分割原则。责任分割原则的落实,没有什么好的框架或者工具来支持。只能通过老鸟经常去做Code Review,找出存在的问题,提出重构方案,并督促菜鸟改进。
个人一般采用的重构思路,仅作为参考,照搬后被老板批评乃至造成工伤概不负责:
- 把一个大的工具类,根据主题不同,拆分成若干个互不干扰的高内聚工具类;举个例子,一个万能的NetworkUtils可能可以拆成HttpUtils, FTPUtils,TelnetUtils等;
- 对于一个被频繁调用的类,仔细观察调用情况,如果有一些方法的被调用频率远远低于其他方法,那么需要考虑这个方法是不是应该放在这个类中;
- 存在A,B两个类之间的相互依赖,或者更多类的混乱依赖,那么就更要抽丝剥茧,通过合理安排类的功能来去除环形依赖;
- 尝试一句话说清楚一个类的功能,不要使用“和”,“以及”,“或者”等连接词;如果出现了这些连接词,就需要引起重视;
三. 用进化拥抱变化
“It is not the strongest or the most intelligent who will survive but those who can best manage change.” ― Leon C. Megginson
前段时间,朋友圈疯传一篇文章 -——“架构腐化之谜”,大家都深表同感,纷纷表示对自己架构的未来的担忧。然而,说句不合时宜的话, 90%的担忧是杞人忧天,因为以现在产品更新换代的速度,90%的项目面市即意味着死亡,没等到架构腐朽,产品已经入土了。
剩下10%里面,也许有9%会一直坚持活下去,但是不会蓬勃发展,也就是说,只要保证不出现内存泄露之类的问题,代码就会一直在几台小服务器上运行下去,哪怕后面没有人维护也没关系。只有1%的产品,会日新月异的更新迭代,最终成长为巨无霸,或者巨无霸的生态下的一个环节。这个言论看似悲观,却是对现实最好的妥协。
谬用一下泰戈尔的名言:“不是槌的打击,而是水的载歌载舞,使鹅卵石臻于完美”, 不是闭门造车的架构,而是不断拥抱变化的需求,才使得架构臻于完美。
假如在早期就纠结于架构的完美性,而延迟产品的交付,是非常得不偿失的。只有生存下来,才有机会。再根据市场变化,不断优化架构,从而延长软件的生命周期。那么,假如撞大运,真的成了这1%,怎样做才能算是拥抱变化?
首先,请参考本文第一点和第二点。如果这两点基本功没有练好,那么谈架构的进化就和还没有通关十八罗汉的新手就想练成九阴真经是一个道理。
在设计之初,初步考虑系统的Scalability(可伸缩性)
下面在第四点会详细阐述。
内部的各个模块尽量做到可插拔
一方面是接口和实现的分离,可以随着需求的变化更换实现;另一方面,尽量把功能服务化,成为微服务,并且可以监控到服务的互相调用情况,当某个服务老化,可以逐步废弃或使用新的服务取代之。这一点上,阿里巴巴的Dubbo框架是一个不错的选择。
尽量采用优秀的框架,站在巨人的肩膀上
例如在Web层面,我们使用Twitter的Bootstrap前端框架来实现响应式Web编程,提高生产效率的同时减少了为解决各种设备适配问题的投入。当然,这就需要设计师配合,按照Bootstrap规范来设计页面,减少一些个性化设计。
最后,考虑系统的Resilience(弹性,也叫耐受性)
俗一点说,就是变成一只打不死的小强,代码中尽量提前预判可能遇到的各种情形。经常看到代码里面有一堆的if{}判断语句,我就问作者,“你考虑过else{}吗?”一般回答都是,“这绝对只有if,不会有else的”,可如果真的遇到else怎么办?千年虫问题就是这么诞生的。可能很多新同学还不知道什么是千年虫问题,简单地说,就是当年的码农,为了省一点内存空间,只用了2位数来表达年份,比如int year = 98; 表达1998年。我猜码农当时的心态也是,“就我这代码,还能活到2000年,搞笑吧?”
程序员们平时可以多扩大自己的脑洞,想想有哪些else情况自己没有处理,而且可以轻易处理的。比如服务器挂了,那么App端是不是也要跟着crash,还是给出友好一点的提示,或者更友好一点,使用本地缓存。
四. 设计可扩展,但不要过度设计
it's better to have infinite scalability and not need it, than to need infinite scalability and not have it--@littleidea 网友
无限的扩展能力是一种奢望,但是起码不能让扩展能力成为0。试想一下,你辛辛苦苦为老板开发了一个网站,过了一个月,网站超负荷了,老板说,“小A啊,之前2台服务器花了我5万块,预计流量马上要翻倍了,再给你5万块,帮我扛过去啊。”结果你发现,问题不是线性增加服务器就能解决的,原来的程序没有做分层(Web,Business Logic, Data Access等),导致加服务器也只能把所有层的代码全搬到新的服务器,虽然只是Business Logic的计算有压力,却要浪费老板很多服务器。更糟糕的是,因为程序里面用到了文件系统和操作系统命令,不好做负载均衡。
这里有一些准则供参考:
- 代码分层是必须的,层次明朗以后,当哪个层次的负载较重,想办法对该层次进行优化或者扩容即可;
- 保持核心服务是无状态的,所谓无状态就是没有和请求相关的数据依赖;
- 尽可能的选用已被验证的广泛采用的成熟基础架构;
- 充分利用Zookeeper等集群管理工具,来对服务进行管理;
风语者“客服+”中,把业务相关的代码内部组装为风语者ServiceBox,使用阿里巴巴的Dubbo服务进行注册管理。当负载增加时,可以迅速在运维层面增加服务节点,以提供更高的服务能力,从而保证客户的优质体验。
作者简介:
黄耀华
具有11年的计算机软件研发及大型互联网产品研发经验,曾在IBM,百度,人人网等业内顶级企业任职,特别对企业级软件,大规模数据处理,搜索引擎,移动App开发 等领域的原理和实现有着全面且深入的认识。(编辑/魏伟)