一、好用的API具备什么
1、好的命名,详细的文档
a.根据api的任务来命名,如果无法命名,说明这个接口承载的任务过多,需要分解
b.保持一致性, 如:动词+名词
例: 一个接口叫:UserList,另一个接口叫:ListUser 易混淆。
- 短变量名声明和上次使用间的距离很短时效果最好。
- 长变量名称需要证明自己的合理性; 名称越长,需要提供的价值越高
type Person struct {
Name string Age int
}
// AverageAge returns the average age of people.
func AverageAge(people []Person) int {
if len(people) == 0 {
return 0
}
var count, sum int
for _, p := range people {
sum += p.Age count += 1
}
return sum / count
}
变量p
的在第12
行被声明并且也只在接下来的一行中被引用。 p
在执行函数期间存在时间很短。如果要了解p
的作用只需阅读两行代码。
相比之下,people
在函数第7
行参数中被声明。sum
和count
也是如此,他们用了更长的名字。读者必须查看更多的行数来定位它们,因此他们名字更为独特。
2、一个易于使用,难以误用的API
举例:警惕相同类型参数的函数
#简单, 但难以正确使用的API
func CopyFile(to, from string) error
//CopyFile的两个参数类型一致,是可以交换的,但是一旦顺序弄错就会出现问题
//没有文档,你无法分辨。 如果没有查阅文档,代码审查员也无法知道你写对了顺序。
CopyFile("/tmp/backup", "presentation.md")
CopyFile("presentation.md", "/tmp/backup")
#改进如下:
type Source string
func (src Source) CopyTo(dest string) error {
return CopyFile(dest, string(src))
}
#使用方法
//通过这种方式,CopyFile总是能被正确调用
var from Source = "presentation.md"
from.CopyTo("/tmp/backup")
3、松耦合
如果修改一个接口的时候,不影响其它接口,修改一个数据结构的时候不会影响其他无关的功能,那么我们就说他的代码是松耦合的。
对于设计人员,如果变动一个模块的设计不会影响另一个模块,模块间的api接口保持相对稳定,那么我们也会说他设计的模块时松耦合的。
4、高可用
4.1、服务冗余
- 无状态化
服务分无状态部分和有状态部分, 如果服务中有状态,就应该把状态抽取出来保存在有状态的中间件中如:缓存、数据库、对象存储、大数据平台、消息队列等更加擅长处理数据的组件来处理。 这样无状态的部分可以很容易的横向扩展,在用户分发的时候,可以很容易分发到新的进程进行处理,后端有状态的中间件设计之初,就考虑了扩容的迁移,复制,同步等机制。
- 冗余
冗余在不同物理机,不同机房,甚至不同地震带上
4.2、异步化改造:
有严格先后顺序的保持顺序执行,能同步执行的服务异步化
一次购买计划课程需要经历十几个的服务调用,如果每个服务耗时100ms,加起来就会 > 1s。接口的QPS就会大大降低。
这样的顺序调用的方式也一定会造成系统处理一次前端请求所花的时间较长,给服务的会话处理线程带来长时间的资源占用,对于服务器整体的系统吞吐量带来巨大的影响。遇到高并发的时候,服务器就无法处理导致服务不可用,甚至雪崩反应。
当用户购买计划后,顺序执行的操作放入队列后完成返回。 并发起课程更新操作,完成后发起分班信息更新。
事务如何处理:
BASE :Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)
柔性事务通过日志和补偿机制实现无锁化事务
回滚的逻辑过于复杂的话,尽量通过消息队列重试正向补偿。
4.3 防雪崩
服务之间进行rpc
或者http
调用时,我们一般都会设置调用超时
,失败重试
等机制来确保服务的成功执行,看上去很美,如果不考虑服务的熔断和限流,就是雪崩的源头。
假设我们有两个访问量比较大的服务A和B,这两个服务分别依赖C和D,C和D服务都依赖E服务
A和B不断的调用C,D处理客户请求和返回需要的数据。当E服务不能供服务的时候,C和D的超时
和重试
机制会被执行
由于新的调用不断的产生,会导致C和D对E服务的调用大量的积压,产生大量的调用等待和重试调用,慢慢会耗尽C和D的资源比如内存或CPU,然后也down掉。
A和B服务会重复C和D的操作,资源耗尽,然后down掉,最终整个服务都不可访问。
解决方案:
流量问题可以扩容
服务本身问题的话需要限流或者熔断
4.3.1、限流
限制客户端的调用来达到限流的做法是很常见的,比如,我们限制每秒最大处理200个请求,超过个数量直接拒绝请求。常见的有令牌通算法
以一定的速度在桶里放令牌,当客户端请求服务的时候,要先从桶里得到令牌,才能被处理,如果桶里的令牌用完了,则拒绝访问。
4.3.2 、熔断
让业务得到一个确定的返回值——要么成功,要么就彻底失败。
在客户端控制对依赖的访问,如果调用的依赖不可用时,则不再调用,直接返回错误,或者降级处理。开源的库比如hystrix-go,主要思想是,设置一些阀值,比如最大并发数,错误率百分比,熔断尝试恢复时间等。能过这些阀值来转换熔断器的状态:
- 关闭状态,允许调用依赖
- 打开状态,不允许调用依赖,直接返回错误,或者调用fallback
- 半开状态,根据
熔断尝试恢复时间
来开启,允许调用依赖,如果调用成功则关闭
失败则继续打开
接口上实现上游调用时会设置一个超时时间,这个时间通过context 传递到下游,每个下游节点在收到请求时开始记录自己消耗的时间,如果自己耗时已经超出上游规定的超时时间就会主动停止一切 I/O 调用,快速返回错误
5、幂等
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。 在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
我们发起一笔付款请求,应该只扣用户账户一次钱,当遇到网络重发或系统bug重发,也应该只扣一次钱
给用户推送消息,同样的信息也应该只推送一次。避免消息队列重试消费。
技术方案:
查询、删除多次,在数据不变的情况下,结果是一样的。select,del 是天然的幂等操作;
对于插入更新接口来讲有多种方案
- 1、唯一索引做幂等
- 2、redis+token 利用redis单线程排队处理,
- 3、悲观锁,select for update
- 4、通过状态机,每个状态只流转一次。如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂
6、日志
日志的作用:快速的了解trace链路、上下游信息,快速定位错误。
建议:err:%v||msg: %v||req:%v||res:%v
//「手机号不存在」: 什么手机号?是手机号参数问题?还是手机号为空?还是出现了海外的手机号不识别?
appModule:user host: - httpMethod:POST forwardAppName:default date:June 17th 2019, 05:46:52.000 uri:/m/mobile/exists clientIp:120.229.163.196 appName:qpi logId:0223c53d1c9d1327ae38aa293f3c31b9 messages:手机号不存在 url:/m/mobile/exists tags:apierrlog, api, beats_input_codec_plain_applied forwardAppModule:default serverIp:172.17.16.25 path:/alidata/app/log/php/app/api/20190616.apioutput @timestamp:June 16th 2019, 21:46:58.168 hostName:***.top input.type:log code:10,002,001 @version:1 _id:ec6KYGsBbj79lVQShOhk _type:log _index:api-errlog-2019.06.16 _score: -
//【用户没有加入此计划】:哪个用户?是上层调用时忘记传?传空了?格式转换错了导致的?超过int范围导致的用户id不存在?
appModule:vk host: - httpMethod:POST forwardAppName:default date:June 17th 2019, 05:46:54.000 uri:/vk/classroom/getexpandmenu clientIp:223.104.6.48 appName: api logId:5d76945da4b60e953555eb926781a374messages:用户没有加入此计划 url:/vk/classroom/getexpandmenu data:[] tags:apierrlog, api, beats_input_codec_plain_applied forwardAppModule:default serverIp:172.17.16.21 path:/alidata/app/log/php/app/api/20190616.apioutput @timestamp:June 16th 2019, 21:46:57.581 hostName:***.top input.type:log code:1,604 @version:1 _id:0M6KYGsBbj79lVQSgeS- _type:log _index:api-errlog-2019.06.16 _score:
二、为什么选择Go和thrift
1、Go语言的一些优点
- 速度接近于C
- 文档丰富,语法简单,最小知识开销最快入门
- 并发与协程
- 函数多返回
- 完善的工具链(编码检查,格式统一,单元测试,性能压测)
- 版本兼容性好,几年前的go版本依然可以兼容
- .....等等等
2、RPC和HTTP
随着互联网业务越来越复杂,前端逻辑越来越重,我们发现业务服务开始慢慢分化:页面渲染的工作回到了前端;Model 层逐步下沉成独立服务,并且催生了 RPC 协议的流行;业务接入层只需要提供 API。于是,MVC 中的 V 和 M 逐步消失,演变成了RPC 框架
RPC:
HTTP:
3、为什么使用thrift
Thrift是Facebook于2007年开发的跨语言的rpc服框架,提供多语言的编译功能,并提供多种服务器工作模式;
用户通过Thrift的IDL(接口定义语言)来描述接口函数及数据类型(方便服务间调用、单元测试、压测、mock),然后通过Thrift的编译环境生成各种语言类型的接口文件,用户可以根据自己的需要采用不同的语言开发客户端代码和服务器端代码。
三、
设计好大型微的服务框架,可以遵循够用就好的原则,好的架构不是设计出来的,是演进过来的
“Rule of least power”(够用就好)。 --- Tim Berners-Lee 万维网之父
最好的设计不是解决所有问题,而是恰好解决当下问题。就是因为我们面对的需求实际上是多变的,所以我们要尽可能只设计最本质的东西,减少复杂性,这样做反而让框架具有更多可能性。
一般在设计架构的时候,会比较倾向于“大而全”,由于我们一般都很难预测使用者会如何使用,于是自然而然的会提供想象中“可能会被用到”的各种功能,导致设计越来越可扩展的同时也越来越复杂。