丁靖:2007年开始PHP, PECL开发,Swoole开发组成员;2015年开始Golang,从事存储,图像处理,高并发服务开发;目前是贝壳找房基础服务负责人
前言
今天我分享的主题是go的工程效率实践,做一个简单的自我介绍,我叫丁靖,8年PHP、PECL开发,2015年开始接触go,现在从事存储和图象处理以及高并发服务开发,目前是贝壳找房基础服务负责人。
先做一个铺垫,分享一下我对技术团队价值的理解,还有我们有哪些效率问题,以及我们是怎么解决的。
技术价值
技术团队的价值
首先看技术的价值,商业社会,在商言商,技术团队扮演了什么角色?我总结了三点:
第一点,做业务流程的自动化,可以提升业务的效率,在很多公司IT方面都是这个作用;第二点,可以让老板的想法快速落地,有一个指标,从老板一拍脑袋到项目上线的时间,时间越短表示我们的技术价值越大;第三,技术创新,改变世界。说的范围有点大,但是我们周围都在发生,比如说我们贝壳找房的VR看房。
技术从本质上讲都是解决效率问题
我觉得技术在本质上都是要解决效率问题,上面提到的业务流程提效、想法的快速落地以及技术创新,都是为了提高大家的工作效率和生活效率,比如在找房的过程中提升看房效率等等。
有哪些效率问题
接下来我讲一下开发过程中存在的效率问题,我总结了以下几点,可能降低我们开发的效率:
首先是我们怎么去组织工程?golang很松散,很灵活,这可能会让我们不知道怎么做,或者看不懂别人的代码;第二点是代码复用,代码复用率越高,开发效率越高;第三点问题定位,这个效率越高,故障恢复的效率越高;最后是接口设计,接口定义的通用性,权限设计的抽象性也会直接影响后期维护成本。
工程效率方面:介绍模块化、生命周期管理 、依赖注入、系统分层、现在使用的目录结构和包管理;代码复用:封装了数据库、缓存等基础组建、解决配置和路由问题;问题定位:主要介绍我们怎么做日志和监控;接口设计:简单介绍我们的接口抽象原则以及权限管理的方法。
工程组织
生命周期管理
首先从工程组织开始,PHP大概也是这样处理生命周期。不同的是,PHP只需要处理接收请求之后的逻辑,而Go还要关注怎么接收请求。大致分这么几个阶段:第一,进程启动,加载配置,创建一些临时文件,记下进程PID以方便进程管理,这些做完之后才开始进程请求。请求开始,并行的多个请求都要开始,每个请求都要分配内存,申请一些资源,比如创建数据库连接,做权限验证,签名校验,之后才到我们后面真正的业务逻辑。这样可以更好地组织工程。请求结束之后把申请的内存和资源释放掉。进程结束时,删除创建的临时文件等。
模块化
我刚刚说到生命周期,现在开始介绍模块化。基于这个生命周期,我们把很多建立的业务模型模块化,并和生命周期对应上。比如说做权限验证,有一种做法是在每个请求之前都调鉴权方法做权限验证,有了生命周期,可以定义一个权限模块,并定义请求开始时回调函数来实现。这就实现了我们常说的高内聚低耦合,把权限验证的逻辑内聚到模块里,而与其他逻辑低耦合。
依赖注入
下面一个问题,对于依赖注入可能有理解偏差和模糊的地方,这里举个例子,比如要组织一个工程,可能需要拆分成很多包,不同的包之间存在互相的调用。例如我现在有两个模块,互相之间存在调用关系,很多的做法是定义一个全局的容器(container),把各个包实例化后放到这个容器里面,运行过程中就可以调用到这些模块了。依然注入能自动化完成这个工作。我们可以在程序启动时把模块一二三实例化后放到框架里面,通过反射把实例注入到需要的模块中。这里还有个细节存在问题,如果模块一先初始化,那模块一初始化时模块二和三没有分配内存,无法在模块一初始化时使用模块二和三。因此依赖注入要分析他们之间的关系,模块三应该最先初始化,然后是模块二,最后是模块一。我们实现的依赖注入需要先注册,然后对所有注册的模块进行排序,利用反射知道模块二依赖模块三,模块一也依赖模块三,于是就排成这样的序,先对模块三分配内存,再把模块三注入到模块二里面去。后面逻辑就可以直接调用了,省去了很多手工劳动,依赖注入就是解决这个问题。
系统分层
接下来是系统分层,我们最初的想法是结合模块和生命周期,就可以比较好地组织代码了,但是实际上发现还是不行,特别是不同的人可能习惯不一样,比如有些人更习惯MVC,于是我们也支持了MVC的代码组织方式。
包管理
包管理,引用vendor属性之后各种包管理都出现了,我们选择了dep,理由是支持私有git仓库、本地代码缓存、忽略_test文件等等特性。忽略_test文件这点做得比较细,因为依赖包里面的这些测试文件,对使用来说没什么意义,忽略它们可以更快地拉取。还有一点就是Dep项目一直在更新,一直在跟进修复问题。现在官方又出来vgo,但是没有一些文档,所以我们没有深入了解。
工程目录
工程目录,vendor还在实验阶段的时期,我们把项目目录作为GOPATH,go1.7之后改为了vendor 方式管理代码仓库。编译统一使用Makefile进行,一个项目中可能存在多个service,比如这里就可以使用make app-service来编译app-service,编译用到了 go build。这里面有个坑,编译时加-race参数可以做动态并发检查,但是对并发有限制,当并发达到域值就会自动退出。所以我们做了区分,DEBUG环境编译时才加这个参数。make test执行测试。这样基本上go编译的几个环节可以比较完好的映射到 make 命令上。
代码复用
基础组建
代码复用,首先挑选了一些优秀的开源项目放到框架里,比如 gorm、redigo 等等。但我们并不是按照操作资源来封装,而是面向功能封装。这样封装是为了通过配置改变底层操作的资源,通过接口抽象出各种操作而不直接调用原生的,以便扩展。我们封装了数据库、队列、缓存、会话等功能。总结一下这样做主要的目:1. 为了方便扩展 2. 接口更稳定
配置
接下来介绍配置,因为使用了go语言,可以做更深层次优化,现在我们配置的逻辑是这样的:三个数据源:环境变量、配置文件、配置服务,最终都缓存到内存中,同时监听配置服务h和配置文件的变化,发现变化就拉到内存里缓存起来。应用层只从内存获取配置,配置又分业务配置和系统配置,业务配置为什么要分开呢?因为业务配置的数据结构更复杂,有树状结构和网络结构,但系统配置相对固定,基本都是key-value的结构。
请求路由
路由:可分请求路由和命令行路由。命令行路由有什么用呢?比如服务启动时候,运维工具有更多参数,不同参数有不同作用。请求路由,我们在原生路由的基础上做了扩展,原生路由是指定一个回调函数(handler),我们可以把 handler 拆成更多小handler,可以把一个过程拆分成很多小 handler,有的 handler 只负责记录访问日志,有的handler负责加 recover 把做异常恢复,捕获异常后打一条日志,还有可以加权限验证的 handler,每个 handler 一旦失败就会走终止执行 handler 。再结合刚刚介绍的模块化,可以帮助我们更好的组织代码,构建可扩展易维护的项目。
问题定位
日志
问题定位:首先分享一下我对日志的观点,总结下来有这几个日志:访问日志、错误日志、系统错误日志、调试日志。访问日志,可以搜集很多信息,可以参考常见nginx访问日志格式,能有效的帮助我们分析系统运行状态;错误日志,正常的请求响应,没有错误日志,主要目的是快速定位异常请求的问题;系统错误日志,在系统崩溃(panic)recover时会打,方便追溯问题,这属于比较严重的错误,所以独立出来没有和错误日志放在一起。同时所有日志都可以输出到不同的存储引擎里,比如在Kafka、文件、Hdfs。
日志格式
日志格式,首先介绍一个日志组件,有些同学习惯把错误日志分开打印,这样不太容易检索,比较浪费存储空间。我们的做法是当发生错误时,把调用栈拼接起来打印一条日志。所以一个请求会产生一条访问日志,如果出错则只会产生一条错误日志,这条错误日志需要包含能帮助你快速定位错误的信息。另外在处理逻辑中可以多打一些调试日志,提高在开发阶段的调试效率,在线上把调试日志大打印开关关闭。这是我们定的日志格式,通过request_id能够很快过滤出某个请求执行过程中的所有日志。另外我们把所有日志都统一输出到一个文件里,不管错误日志还是访问日志都以相对统一的格式输出,这样能方便我们做日志切分。日志里面需包含几个关键信息:时间、错误级别、来源请求ID、请求ID、日志信息、上下文。通过请求ID可以在服务间做链路追踪。
监控
后面的是监控,前面的日志搞定了,通过切割和分析,最后输出到存储里面,当然就很容易了,方法很多,这里就不展开讲了。
接口设计
接口抽象
最后介绍一下我们的接口设计。接口抽象,将所有操作都能够抽象到一个资源上,如果能把大量操作都能抽象到增删改查上,那这个建模就比较成功了。但是不可避免在某些前端界面上不能按照这个方法抽象,比如说登录等等,所以前端的我们管不了了,希望我们的后端接口是稳定的。这就是Restful的设计思想,1.资源 2. 表现层 3. 状态转换。这里面映射到接口里面,资源对应 URI,表现层对应接口返回内容,状态转换对应我们的更新和删除、获取,操作资源使它的状态变换。
如何确保身份合法
设计接口用不用Restful有什么区别?我们不光要把接口实现,还要控制权限。Restful接口做权限控制就非常简单了,要做权限首先要获取调用接口的调用者身份,身份如何标识?我们参照AWS IAM的设计通过AK来标识。那么如何保证AK合法呢?这就涉及到签名,也是接口设计的一部分。签名通过,下一步才是鉴权。
权限配置
同样参考AWS IAM,通过Policy来设置权限。一个接口能否被访问到,是应用在该调用者身上的很多条Policy综合作用的结果,每个Policy配置包含能访问什么资源,能做什么操作,这个操作是允许还是不允许。把所有规则遍历一遍,就根据这个流程图知道这个资源到底能不能被执行。实现起来非常简单,仅需要几十行代码。剩余工作只要给每个调用者添加Policy,但是前提是接口能通过资源来标识。