1. tcp-server状态上报
get-tcp-ip接口怎么知道tcp-server集群中各台服务器是否可用呢,tcp-server主动上报是一个潜在方案,如果某一个tcp-server挂了,则会终止上报,对于停止上报状态的tcp-server,get-tcp-ip接口,将不返回给client相应的tcp-server的外网IP。
该设计的存在的问题?
诚然,状态上报解决了tcp-server高可用的问题,但这个设计犯了一个“反向依赖”的耦合小错误:使得tcp-server要依赖于一个与本身业务无关的web-server。
2. tcp-server状态拉取
更优的方案是:web-server通过“拉”的方式获取各个tcp-server的状态,而不是tcp-server通过“推”的方式上报自己的状态。
这样的话,每个tcp-server都独立与解耦,只需专注于资深的tcp业务功能即可。
高可用、负载均衡、扩展性等任务由get-tcp-ip的web-server专注来执行。
3. 多说一句,将负载均衡实现在服务端,还有一个好处,可以实现异构tcp-server的负载均衡,以及过载保护:静态实施:web-server下的多个tcp-server的IP可以配置负载权重,根据tcp-server的机器配置分配负载(nginx也有类似的功能)
动态实施:web-server可以根据“拉”回来的tcp-server的状态,动态分配负载,并在tcp-server性能极具下降时实施过载保护
4. 很多时候,业务需要跨公网调用一个第三方服务提供的接口,为了避免每个调用方都依赖于第三方服务,往往会抽象一个服务:解除调用方与第三方接口的耦合
当第三方的接口变动时,只有服务需要修改,而不是所有调用方均修改
问题:内部服务可能对上游业务提供了很多服务接口,当有一个接口跨公网第三方调用超时时,可能导致所有接口都不可用,即使大部分接口不依赖于跨公网第三方调用。
为什么会出现这种情况呢?
内部服务对业务方提供的N个接口,会共用服务容器内的工作线程(假设有100个工作线程)。
假设这N个接口的某个接口跨公网依赖于第三方的接口,发生了网络抖动,或者接口超时(不妨设超时时间为5秒)。
潜台词是,这个工作线程会被占用5秒钟,然后超时返回业务调用方。
假设这个请求的吞吐量为20qps,言下之意,很短的时间内,所有的100个工作线程都会被卡在这个第三方超时等待上,而其他N-1个原本没有问题的接口,也得不到工作线程处理。
潜在优化方案:
增大工作线程数(不根本解决问题)
降低超时时间(不根本解决问题)
垂直拆分,N个接口拆分成若干个服务,使得在出问题时,被牵连的接口尽可能少(依旧不根本解决问题,难道一个服务只提供一个接口吗?)
5. 跨公网调用第三方,可能存在的问题:公网抖动,第三方服务不稳定,影响自身服务
一个接口超时,占住工作线程,影响其他接口
降低影响的优化方案:
增大工作线程数
降低超时时间
服务垂直拆分
业务需求决定技术方案,结合业务的解决方案:
业务能接受旧数据:读取本地数据,异步代理定期更新数据
有多个第三方服务提供商:多个第三方互备
向第三方同步数据:本地写成功就算成功,异步向第三方同步数据
6. 架构设计时,能够巧用dns做一些什么事情:
1). 反向代理水平扩展:在dns-server对于同一个域名可以配置多个nginx的外网ip,每次dns解析请求,轮询返回不同的ip,这样就能实现nginx的水平扩展,这个方法叫“dns轮询”。
2). web-server负载均衡:
既然“dns轮询”可以将同一个域名的流量均匀分配到不同的nginx,那么也可以利用它来做web-server的负载均衡:
(1)架构中去掉nginx层
(2)将多个web-server的内网ip直接改为外网ip
(3)在dns-server将域名对应的外网ip进行轮询解析
和nginx相比,dns来实施负载均衡有什么优缺点呢?
优点:
利用第三方dns实施,服务端架构不用动
少了一层网络请求
不足:
dns只具备解析功能,不能保证对应外网ip的可用性(即使能够做80口的探测,实时性肯定也是比nginx差很多的),而nginx做反向代理时,与web-server之间有保活探测机制,当web-server挂掉时,能够自动迁移流量
当web-server需要扩容时,通过dns扩容生效时间长,而nginx是服务端完全自己可控的部分,web-server扩容更实时更方便
因为上面两个原因,架构上很少取消反向代理层,而直接使用dns来实施负载均衡。
3).用户就近访问:
http请求的第一个步骤域名到外网ip的转换,发生在整个服务端外部,服务端不可控,那么如果要实施“根据客户端ip来分配最近的服务器机房访问”,就只能在dns-server上做了:
(1)电信用户想要访问某一个服务器资源
(2)浏览器向dns-server发起服务器域名解析请求
(3)dns-server识别出访问者是电信用户
(4)dns-server将电信机房的nginx外网ip返回给访问者
(5)访问者就近访问
根据用户ip来返回最近的服务器ip,称为“智能dns”,cdn以及多机房多活中最常用。
总结,架构设计中,dns有它独特的功能和作用:
1).dns轮询,水平扩展反向代理层
2).去掉反向代理层,利用dns实施负载均衡
3).智能dns,根据用户ip来就近访问服务器
7. 负载均衡:
1).做两台nginx组成一个集群,分别部署上keepalived,设置成相同的虚IP,保证nginx的高可用;当一台nginx挂了,keepalived能够探测到,并将流量自动迁移到另一台nginx上,整个过程对调用方透明
优点:1)解决了高可用的问题
缺点:1)资源利用率只有50%
2)nginx仍然是接入单点,如果接入吞吐量超过的nginx的性能上限怎么办,例如qps达到了50000咧
2).nginx毕竟是软件,性能比tomcat好,但总有个上限,超出了上限,还是扛不住。lvs就不一样了,它实施在操作系统层面;f5的性能又更好了,它实施在硬件层面;它们性能比nginx好很多,例如每秒可以抗10w,这样可以利用他们来扩容
99.9999%的公司到这一步基本就能解决接入层高可用、扩展性、负载均衡的问题。
这就完美了嘛?还有潜在问题么?
好吧,不管是使用lvs还是f5,这些都是scale up的方案,根本上,lvs/f5还是会有性能上限,假设每秒能处理10w的请求,一天也只能处理80亿的请求(10w秒吞吐量*8w秒),那万一系统的日PV超过80亿怎么办呢?(好吧,没几个公司要考虑这个问题)
3).如之前文章所述,水平扩展,才是解决性能问题的根本方案,能够通过加机器扩充性能的方案才具备最好的扩展性。
facebook,google,baidu的PV是不是超过80亿呢,它们的域名只对应一个ip么,终点又是起点,还是得通过DNS轮询来进行扩容:
此时:
1)通过DNS轮询来线性扩展入口lvs层的性能
2)通过keepalived来保证高可用
3)通过lvs来扩展多个nginx
4)通过nginx来做负载均衡,业务七层路由
8. 信息安全:
1).黑客定理一:网络上传递的数据是不安全的,属于黑客公共场所,能被截取
2).黑客定理二:客户端的代码是不安全的,属于黑客本地范畴,能被逆向工程,任何客户端与服务端提前约定好的算法与密钥都是不安全的
3).黑客定理三:用户客户端内存是安全的,属于黑客远端范畴,不能被破解。(使用“具备用户特性的东西(如密码散列)”作为加密密钥,一人一密,是安全的。只是,当“具备用户特性的东西”泄漏,就有潜在风险。)
4).对于不同加密方法明:
明文消息传递如同裸奔,不安全
客户端和服务端提前约定加密算法和密钥,不安全(好多公司都是这么实现的=_=)
服务端随机生成密钥,发送给客户端,不安全
一人一密,客户端使用“具备用户特性的东西”作为加密密钥,弱安全
一次一密,三次握手建立安全信道,安全
5). 特点:每次通信前,进行密钥协商,一次一密
密钥协商过程,如下图所述,需要随机生成三次密钥,两次非对称加密密钥(公钥,私钥),一次对称加密密钥,简称安全信道建立的“三次握手”,在客户端发起安全信道建立请求后:
服务端随机生成公私钥对(公钥pk1,私钥pk2),并将公钥pk1传给客户端 (注意:此时黑客能截获pk1)
客户端随机生成公私钥对(公钥pk11,私钥pk22),并将公钥pk22,通过pk1加密,传给服务端 (注意:此时黑客能截获密文,也知道是通过pk1加密的,但由于黑客不知道私钥pk2,是无法解密的)
服务端收到密文,用私钥pk2解密,得到pk11
服务端随机生成对称加密密钥key=X,用pk11加密,传给客户端(注意:同理,黑客由密文无法解密出key)
客户端收到密文,用私钥pk22解密,可到key=X
至此,安全信道建立完毕,后续通讯用key=X加密,以保证信息的安全性
9. 通过上面的分析,Worker线程在执行的过程中,有一部计算时间需要占用CPU,另一部分等待时间不需要占用CPU,通过量化分析,例如打日志进行统计,可以统计出整个Worker线程执行过程中这两部分时间的比例,例如:
执行计算,占用CPU的时间(粉色时间轴)是100ms
等待时间,不占用CPU的时间(橙色时间轴)也是100ms
得到的结果是,这个线程计算和等待的时间是1:1,即有50%的时间在计算(占用CPU),50%的时间在等待(不占用CPU):
假设此时是单核,则设置为2个工作线程就可以把CPU充分利用起来,让CPU跑到100%
假设此时是N核,则设置为2N个工作现场就可以把CPU充分利用起来,让CPU跑到N*100%
结论:
N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。
经验:
一般来说,非CPU密集型的业务(加解密、压缩解压缩、搜索排序等业务是CPU密集型的业务),瓶颈都在后端数据库访问或者RPC调用,本地CPU计算的时间很少,所以设置几十或者几百个工作线程是能够提升吞吐量的。
10. 总结
1).线程数不是越多越好
2).sleep()不占用CPU
3).单核设置多线程不但能使得代码清晰,还能提高吞吐量
4).站点和服务最常用的线程模型是“IO线程与工作现场通过任务队列解耦”,此时设置多工作线程可以提升吞吐量
5).N核服务器,通过日志分析出任务执行过程中,本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化
11. 不止sleep()函数,在进行一些阻塞调用时,例如网络编程中的:阻塞accept(),等待客户端连接; 阻塞recv(),等待下游回包; 都不占用CPU资源。
12. 业务有定时任务或者定时超时的需求,当任务量很大时,可能需要维护大量的timer,或者进行低效的扫描。
1).“轮询扫描法”
a.用一个Map<uid, last_packet_time>来记录每一个uid最近一次请求时间last_packet_time
b.当某个用户uid有请求包来到,实时更新这个Map
c.启动一个timer,当Map中不为空时,轮询扫描这个Map,看每个uid的last_packet_time是否超过30s,如果超过则进行超时处理
问题:只启动一个timer,但需要轮询,效率较低
2).“多timer触发法”
a.用一个Map<uid, last_packet_time>来记录每一个uid最近一次请求时间last_packet_time
b.当某个用户uid有请求包来到,实时更新这个Map,并同时对这个uid请求包启动一个timer,30s之后触发
c.每个uid请求包对应的timer触发后,看Map中,查看这个uid的last_packet_time是否超过30s,如果超过则进行超时处理
问题:不需要轮询,但每个请求包要启动一个timer,比较耗资源
3).环形队列法:
三个重要的数据结构:
1)30s超时,就创建一个index从0到30的环形队列(本质是个数组)
2)环上每一个slot是一个Set<uid>,任务集合
3)同时还有一个Map<uid, index>,记录uid落在环上的哪个slot里
同时:
1)启动一个timer,每隔1s,在上述环形队列中移动一格,0->1->2->3…->29->30->0…
2)有一个Current Index指针来标识刚检测过的slot
当有某用户uid有请求包到达时:
1)从Map结构中,查找出这个uid存储在哪一个slot里
2)从这个slot的Set结构中,删除这个uid
3)将uid重新加入到新的slot中,具体是哪一个slot呢 => Current Index指针所指向的上一个slot,因为这个slot,会被timer在30s之后扫描到
4)更新Map,这个uid对应slot的index值
哪些元素会被超时掉呢?
Current Index每秒种移动一个slot,这个slot对应的Set<uid>中所有uid都应该被集体超时!如果最近30s有请求包来到,一定被放到Current Index的前一个slot了,Current Index所在的slot对应Set中所有元素,都是最近30s没有请求包来到的。
所以,当没有超时时,Current Index扫到的每一个slot的Set中应该都没有元素。
优势:
(1)只需要1个timer
(2)timer每1s只需要一次触发,消耗CPU很低
(3)批量超时,Current Index扫到的slot,Set中所有元素都应该被超时掉
这个环形队列法是一个通用的方法,Set和Map中可以是任何task,本文的uid是一个最简单的举例。
HashedWheelTimer也是类似的原理,有兴趣的同学可以百度一下这个数据结构,Netty中的一个工具类
13. URI设计原则
1). URI的末尾不要添加“/”
多一个斜杠,语义完全不同,究竟是目录,还是资源,还是不确定而多做一次301跳转?
负面case:http://api.canvas.com/shapes/
正面case:http://api.canvas.com/shapes
2).使用“-”提高URI的可读性
目的是使得URI便于理解,用“-”来连接单词
正面case:http://api.example.com/blogs/my-first-post
3).禁止在URL中使用“_”
目的是提高可读性,“_”可能被文本查看器中的下划线特效遮蔽
负面case:http://api.example.com/blogs/my_first_post
别争,看到效果就明白了
4).禁止使用大写字母
RFC 3986中规定URI区分大小写,但别用大写字母来为难程序员了,既不美观,又麻烦
负面case:http://api.example.com/My-Folder/My-Doc
正面case:http://api.example.com/my-folder/my-doc
5).不要在URI中包含扩展名
应鼓励REST API客户端使用HTTP提供的格式选择机制Accept request header
正面case:http://58.com/bj/ershou/310976
一个case:http://58.com/bj/ershou/310976x.shtml
6).建议URI中的名称使用复数
额,楼主不知道为何会有这么奇怪的建议
正面case:http://api.college.com/students/3248234/courses
负面case:http://api.college.com/student/3248234/course