(背景:我目前所在一家互联网公司,负责技术方面的事情。为了让大家看懂此文,有必要介绍一下我们公司的背景。我们公司的主要产品,是一个汽车电商相关产品,涉及到C端用户,B端经销商,集团,ToB销售等多个角色。订单状态众多,业务流程复杂。我们的产品规模,忙时:DAU 20K,Mysql CRUD 2000W q/d)
[TOC] <---- 为嘛简书不支持这个……
人们总是这样,到了年底的时候,才会想起时间。
回想这一年,虽忙的琐碎,却也充满收获,这真让人感到欣慰。
自打接手公司技术坑之日起,“保障服务可用,梳理研发流程,提升团队技术栈”,便是给自己制定的一个小目标。于是这一年尤其是后半年,“坚持,亢奋,努力”便是整个团队工作状态的真实写照。
就像所有技术团队一样,没有什么努力,会被白白浪费。回头看,那些经历,那些挑战,都值得被纪念。
持续集成(Topka-CI)
说起我们的产品线,客户端有“TopDeals”、“TopSales”、“AK47”,三款App。每款App分别有iOS和Android两个版本。当时客户端这边有10名同学,每月每款App大约迭代1至2个版本,这样的迭代速度不可谓慢。而就在这样的速度之下,app打包的工作还要人工完成,发布时,Android十几个渠道包一打就是大半天。即便在测试过程中,daily build也要工程师们手动完成。这极大影响了工作效率。而且手工打包,往往还会出现更多意想不到的问题——手滑弄错了配置项之类……
在这样的背景下,TopkaCI应运而生。
TopkaCI 主要完成以下实现以下功能:
- 支持Android、iOS、ionic等平台项目的自动打包
- 按照预定规则,从代码库的预定分支自动拉取代码
- 支持mvn,gradle,npm,cocoapod以及bower的依赖管理,根据依赖定义及源信息来决定是否更新依赖
- 对Android及iOS代码进行lint检查
- 编译代码
- 对编译后的App进行UI层面的自动化测试(使用两个平台的原生测试工具——UIAutomator与UIAutomation)
- 成果物管理:App编译后,生成安装二维码,实现测试人员用手机扫描二维码自动将App安装到手机的功能
- 成果物管理:iOS App自动上传到iTunesConnect并发出TestFlight邮件,通知相关人员测试
- 成果物管理:每次打包结果根据版本自动上传到TopkaRepo,更新资产库
TopkaCI在开发过程中,遇到了很多坑——几乎每一个环节都有技术问题需要解决。这些困难主要包括了以下几个方面:
- 对CI服务的扩展开发
- 对编译过程所涉及到的相关工具的钩子进行开发,让CI过程按照上述流程连通起来
- 原有项目的配置管理方式过于手工化,需要改造
虽然TopkaCI遇到了一些困难,但在TopkaCI在上线之后,大家用起来还是很爽的。持续集成极大提高了团队的研发效率,现在研发团队各项目的打包过程完全自动化,“原打包架构师就此转岗,去承担更为重要和艰巨的任务”。哈哈。
MySQL 高可用改造
话说有一天,收到这样一封报警邮件,顿时惊呆了。
其实,我们并不是第一次收到这样的邮件。而对云服务的可用性来说,9999的概念是,每1万天,这样的邮件至多只能收到一次。但是很显然,他们并没有做到。
尽管可以向服务商提工单,但我还是不相信这样的问题会得到妥善解决。我也不想陷入无休止的拉锯战中,于是我们决定自己解决。
对MySQL的高可用,我们有如下需求:
- 集群内的多个数据库实例实现热切换,当有服务失败时,自动切换到另一实例。
- 迁移过程中保证数据的一致性以及事务的可用性
- 迁移后,当发现down掉的服务恢复时,自动恢复数据,保证集群同步
- 读写分离,当集群内的可用实例数量>=2时,自动分配1读1写。
- 应用无感,无需为对应数据库的高可用需求做任何修改。
好了,既然有了需求,就来分析一下怎么实现。
高可用的核心需求,是故障迁移——即当系统出现故障时,能够自动将服务切换至可用服务。遗憾的是,MySQL本身并没有提供这样的failover方案,不过像这样的轮子肯定是有人造过的了,但是如何选择轮子,还需要费一番功夫。除了满足上述需求以外,我们还希望框架尽可能的做到:
- 方案成熟
- 开源,可扩展
MySQL的高可用,目前来看主要基于以下几种技术:
- 主从复制技术
- Galera
- NDB
- 中间件及代理
- 共享存储
- 主机高可用
最终根据自身情况,我们选择了相对成熟的MHA方案做为我们的高可用方案。MHA能实现之前提到的除了读写分离之外的大部分需求,并可以保证数据的一致性。而读写分离,我们使用keepalived来解决。目前上线几个月,效果令人满意。
微服务
微服务这个事,近年挺火,但很遗憾,我们微不起来。
这里既有历史原因,又有历史原因。说来说去,都是历史原因。但是,任何一家互联网公司,经过这么多年产品的成长与迭代,除非具备了推倒重来的勇气,否则各种“历史原因”都会存在。
但让我决定要做这样一件事的原因,是因为这样一张图。
是的,你没看错,纵轴单位是秒,这着实让人崩溃。看到这张图的第一感觉,是我们的程序有问题,于是:
- 赶紧翻代码,然而从代码上看,没什么大事
- 翻慢查询日志,涉及到这个接口的sql全都查一遍,看看慢查询是否存在
- 优化慢查询sql
- 优化相关代码的实现逻辑,看看能不能用缓存什么的
折腾了半天,No luck!接着我继续看上面这张图,这张图很有意思,7秒、14秒来回蹦,7秒发生在白天,14秒发生在晚上……等等,我似乎搞清楚怎么一回事了。
排查所有定时任务,尤其是晚上执行的。确定这些任务在执行时,整个系统哪里存在压力风险……结果我找到了我们系统中一些非常重要定时任务,这些任务在运行过程中,极大的占用了网络带宽,以及数据库资源。
行了,拆吧。
拆库,拆代码,构建服务。更为重要的是,要让这个新服务的执行过程与线上服务完全分离。也就是说,新服务完全不占用线上环境的资源,我们为它搭建独立的内网环境以及公网IP。而这个服务与线上服务的交互,则通过IPSec隧道来打通。
至此,问题解决。而且所有接口的响应速度都有了极大提高。爽!
ELK
ELK,我们很早就有了。但是ELK如果想真正用起来,其实还是要解决很多问题的。
让我下定决心把ELK用起来的关键事件,是某次参加一个技术会议,听了“小红书”运营总监的一个分享……
先来说需求。我们的ELK目前只打算记录一件事——nginx访问日志。但为了让记录的日志发挥最大的效能,我们需要将日志结构化。具体说来,有以下几个方面的需求:
- 结构化nginx日志,常规的日志字段结构化
- 结构化通用业务信息,如:访问机型、app版本、token等
- 保存非文件类型的http response_body
- nginx日志记录率达到100%,结构化过程中不能出现任何异常导致数据丢失
具体部署方式,其实也都比较简单。我们做了下面几个事情算是比较独特吧:
- 写了一个 nginx lua script 来把response body搞出来
- 为了分摊压力,我们采用了由3台机器组成的kafka集群做为消息队列,并用一台机器做为logstash客户端去专门消费kafka队列中的数据,而ElasticSearch也使用3台机器实现集群部署(后来证明此架构被过度设计了)
- 在logstash消费过程中,写了一些ruby script来结构化nginx日志以及通用业务信息
- 保证记录率,其实是之前踩到的一个坑。ElasticSearch自动映射类型有些不准,偶尔会出现冲突。出现冲突之后,再进来的数据就会exception
上线之后,ELK向运营及技术团队开放。大家在kibana中查一查日志、做一些数据可视化什么的,用起来还都比较Happy。不过有一个点,在我心里一直有个疑问。
这套系统,总共开了7台机器。呃呃呃。我们抓4台web服务器的日志要开7台机器呀,这样真的科学么?
观察了一下这七台服务器的负载,我的天啊,低得可怜。那就,合并吧。
于是,我们使用Docker,将以上服务不同的Docker镜像跑在一台机器上,并且将这些服务的数据做为不同的挂载盘挂载到host系统。运行起来一切正常!硬件利用率大增,成本大省,哈哈。
并发与弹性计算
2016,不提到并发与弹性计算都不好意思说自己设计过什么架构。
之前提到,我们在忙时,DAU 20K左右。但任何互联网服务,都有忙闲,忙时与闲时,对服务器的负载压力必然不同。如果说云端计算能力对互联网服务来说是一种资源,那无论何时都开着同样数量的服务器,保持着一成不变的系统架构的做法,就好像家里无论何时都开着自来水一样——这不是钱不钱的问题,而是傻不傻的问题。正因如此,本着smart & geek的作风,我们怎能忍受自家的自来水成天开着。
“用最适当的成本支撑目前的业务,并有能力应对用户暴涨的情形”是这套系统设计的初衷及核心思想。
我们的线上系统按照PaaS层来分,大致可以分为负载均衡、缓存、静态文件、CDN、Web服务、日志服务、流式计算、消息队列,关系型数据库,非关系型数据库等多种服务,其中大部分服务为集群服务,线上系统大约有几十台机器的规模,我们首先需要实现对Web集群服务的弹性计算。
那么,这件事情的需求其实是这样的:
- web服务器无状态化(这个在之前已经基本实现)
- 通过对web服务器四种核心能力——CPU、内存、磁盘IO、网络负载的使用率监测,决定是否需要减少机器数量或开启新的机器
- 开启新的机器需要一个初始化代码库、配置文件、服务可用性检测的过程
- 关闭机器前,开启机器后,需要自动修改负载均衡策略来引导流量
- 通过云服务商的API进行数据监测及弹性部署
- 支持根据业务需要自定义弹性策略:如我们每天可能会在同一时间向部分用户发送推送,届时部分服务的流量可能会增大。所以我们需要能自定义弹性策略,做到提前部署
这个项目现在还没有最终完成。而未来,除了Web服务,我们也会考虑实现其他服务的弹性计算方案。届时除了节约成本,对运维团队的自动化运维的能力,也将是很大的提升。
生产环境的Docker
生产环境的Docker,我目前持谨慎态度,尽管京东在这两年618大促在生产环节上大量使用Docker。
Docker目前最大的痛点,在于集群化。生产环境使用Docker,只要牵扯到集群,你就得考虑太多问题:镜像管理、容器管理、编排……虽然现在有mesos,但是个人认为现在docker生态与集群相关的轮子还不足够好,自己造轮子得不偿失,依然得谨慎。
但Docker我们确实是用了的,没用在生产环境,却用在测试、测试的日志等重要度其次且暂时没有集群需求的环境。在这些场景中,用起来还是很赞的。
Mock与全栈
互联网产品,即便是渡过了从0到1时期的产品,其迭代速度也是惊人的。传统软件工程学所谓的瀑布流的软件开发模式,早已无法适应互联产品的研发迭代速度。且不说瀑布流,哪怕是敏捷,又有多少团队能够真正做到“在对的时间兑现你的承诺”。
“设计等待产品,接口等待产品,客户端等待接口,客户端等待设计……”
这些等待,有太多我不能理解。按照我的想法,产品只要有了概要定义,所有人都可以开始干活。因为软件开发中的任何一项工作,无论是产品定义、界面设计、接口实现、客户端实现,都必然要经历一个“从构建到精化”的过程,这个过程的起点,并不必要有上一环节的非常详细的输入才可以开始。
道理是这个道理,但有些事情一旦成为了习惯,就很难解决。三十来人的研发团队,习惯已经养成,怎么掰,确实是个问题。
想让技术团队有所改变,必须要引入新的技术。于是,我决定引入mock。先把“客户端等待接口”这个事情给掰过来!
mock这个东西,对于客户端团队来说,是欢迎的。因为客户端同学可以更早拿到可以运行的API接口,可以更早的开始开发。对完成kpi更有益处。
而对于服务端团队,就得说服他们能在实现接口之前,先做接口设计,并形成mock文档。这样会增加他们的工作量。但说服他们其实也有足够的理由——有了mock之后,没人催着你“快点出接口”,接口可以和客户端同步开发,最后留几天联调即可,其实跟之前相比不用那么紧张了。
对于mock server的需求,我们有如下几点:
- 开源,免费,协议友好,支持二开
- 有UI——这一点非常重要
- 支持LDAP
最后,我们选用了阿里的RAP……RAP在阿里据说超多团队在用,但其实它的代码写的非常差,bug很多,更有诸如”太复杂的json结构保存容易出错”这样的恶性bug。但mock server,现在开源项目不太多,选RAP实属无奈。
等到了下半年,产品需求呈发散态势,多条业务线齐头并进。但各个端的研发需求量却不均衡,经常出现某个月App这边需求很少,但后台、经销商平台需求爆棚的情况。于是,我开始组织App团队学习服务端开发,利用培训、技术沙龙及code review等手段,鼓励(bi)(zhe)大家承担一些服务端、前端的开发任务。虽然起初很累,但现在大家已经基本实现自己写自己的接口、app。而为了保险起见,比较深的逻辑及模型的建立,目前还是由资深的服务端同学完成。
结合mock与全栈,对比之前,现在整个团队的研发效率上了一个大台阶。配合更为细化和有针对的敏捷流程,一个充满战斗力和承诺精神的研发团队已经基本建立。
写到这里,我不禁想到一个问题——“究竟什么才是技术领导力?”
“通过智慧的想法和技术的手段,让组织及研发团队都能坚信我们有能力兑现承诺。”在我看来,这就是技术领导力的最好体现吧。
PS:这一年参加了不少技术会议,全部都是开发向。所有跟技术领导力有关的,我一概没有参加。
不是总结的总结
回忆总是让人充满希望。上面说提到的这些项目,每一个其实都有继续做下去的意义。而2016对于我们来说,是打基础的一年,我们希望能够利用这一年,为组织在技术方面构件一套坚实、适用而又充满无限可能的技术栈,来应对可能到来的高流量及业务膨胀。现在看来,这个目标基本算是实现了。
而从个人角度说,在2016工作方面值得纪念的却远不止这些,这里仅记录了一些与业务无关的方面。所以说到收获,也不仅于此吧。
但是,遗憾也还是有的。最遗憾的就是React Native在移动端团队没有应用起来,那么未来的一年,为今年的小遗憾定个小目标:希望能够改组前端团队为“大前端团队”,React Native/JS用起来~
希望新的一年团队能更加给力,自己能成长更多。